Author Topic: User interface event handling  (Read 4208 times)

0 Members and 1 Guest are viewing this topic.

Offline Dan MoosTopic starter

  • Frequent Contributor
  • **
  • Posts: 359
  • Country: us
User interface event handling
« on: February 23, 2017, 04:11:41 am »
Ok, I have a project that uses a rotary encoder in the UI. The interface's response to encoder inputs is varying depending on the situation.

Also, I want my encoder logic to be in a dedicated library so I can use it in other projects.

Both these requirements mean that the actual result of the encoder's movement should be separate from the encoder logic functions.

Finally, the encoder logic is interrupt based. I'm pretty certain my current project would be fine if I polled, but again, I want an encoder library that is universal, so I want it fast enough for more MCU intensive projects.

My question is this. If I have my encoder ISRs return the result (Clockwise turn/ Counterclockwise), I would still have to pol to get that result, right? So maybe for instance, the ISR stores its return value in some global. I still need to periodically check that global, which amounts to polling again. Is this a valid method? My gut says the main reason for using interrupts is to not miss encoder events, but as long as that's working, I lose nothing by polling for the result in the global, right?

My other, less tasteful idea was to use some trick using a function pointer that can be assigned as needed to the current appropriate function needed for encoder results. I am not super in love with this notion though, because it seems like making what should be simple complicated.

How is this typically done?
 

Offline newbrain

  • Super Contributor
  • ***
  • Posts: 1918
  • Country: se
Re: User interface event handling
« Reply #1 on: February 23, 2017, 08:47:18 am »
My usual solution is to have a queue collecting all the input events.

For encoders and pushbuttons, I use polling every n<5 milliseconds (more than enough for a slow human) either in the main loop or triggered by interrupt.

UART input is interrupt driven, the received characters go into the same queue.

The size of the queue can be adapted to the actual needs, as can the behaviour in overflow (drop oldest vs. newest event).

The only interfaces to the rest of the code are initialization/start/stop functions, an event read function, a queue flush function, and the polling routine to be periodically called.

If a display is used, its updates are (almost) asynchronous, and usually triggered 20-50 times per second.

Using a single global to store the latest UI event is almost guaranteed to lose inputs if they come in bursts wrt the polling, e.g. a fast spin on the encoder.

Function pointers can be useful, especially if the UI has different states, where the same input events have different meanings.
Much better than having a tangle of switch/cases and nested if.

Nandemo wa shiranai wa yo, shitteru koto dake.
 

Offline bktemp

  • Super Contributor
  • ***
  • Posts: 1616
  • Country: de
Re: User interface event handling
« Reply #2 on: February 23, 2017, 08:57:17 am »
Instead of using each event, you could increment the number of counts. That number gets polled by the UI.
If the knob is turned slowly, the UI will be fast enough and use each step. If the knob is turned faster, it may read 10s or 100s of counts and processes them at once.
 

Online nctnico

  • Super Contributor
  • ***
  • Posts: 30371
  • Country: nl
    • NCT Developments
Re: User interface event handling
« Reply #3 on: February 23, 2017, 08:59:07 am »
Instead of using each event, you could increment the number of counts. That number gets polled by the UI.
If the knob is turned slowly, the UI will be fast enough and use each step. If the knob is turned faster, it may read 10s or 100s of counts and processes them at once.
That is how I would do it too. Don't forget to add a reset function so you can reset the count when entering a menu.
There are small lies, big lies and then there is what is on the screen of your oscilloscope.
 

Offline MarkF

  • Super Contributor
  • ***
  • Posts: 3302
  • Country: us
Re: User interface event handling
« Reply #4 on: February 23, 2017, 08:59:46 am »
Here is a code snippet I did for a PIC18F2550. The ISR is called at the rate you wish to read the encoder. The global encoder variable is incremented for clockwise turns and decremented for counterclockwise turns in the ISR. The main loop processes the user inputs and updates a display. After the global encoder variable is read, it is set to zero to restart the count.

Code: [Select]
int8_t en0;
uint8_t currA0,lastA0;

void main(void)
{
   int clicks;

   // Setup rotary encoder
   en0=0;
   lastA0=PORTCbits.RC1;

   // Main loop
   while (TRUE) {

      // Process encoder data
      clicks = en0;
      en0 = 0;
      // Encoder data
      //    0 == No change
      //    >0 == Number of clockwise clicks
      //    <0 == Number of counter-clockwise clicks
      if (clicks != 0) {
         // Do something with encoder data

      }

      // Update display
      drawDisplay(0);
   }
}

void interrupt isr(void)
{
   if (PIE1bits.CCP1IE && PIR1bits.CCP1IF) {
      PIR1bits.CCP1IF = 0;    // clear CCP1 Interrupt Flag

      // Read rotary encoder
      currA0=PORTCbits.RC1;
      if (lastA0==0 && currA0==1) {
         if (PORTCbits.RC2==1) en0--; else en0++;
      }
      lastA0=currA0;
   }
}
 

Offline bktemp

  • Super Contributor
  • ***
  • Posts: 1616
  • Country: de
Re: User interface event handling
« Reply #5 on: February 23, 2017, 09:10:02 am »
Here is a code snippet I did for a PIC18F2550.
That is is not the best example:
First you need to make en0 volatile, otherwise a good compiler will replace en0 with a constant value of 0, because it does not get changed inside the main loop to any other value (in your case it only works, because you are calling the drawDisplay() function).
Then you should disable interrupts while reading the encoder value and resetting the count in the main loop, otherwise you may lose counts if there is an interrupt changing the count between those two instructions.
 

Offline obiwanjacobi

  • Super Contributor
  • ***
  • Posts: 1030
  • Country: nl
  • What's this yippee-yayoh pin you talk about!?
    • Marctronix Blog
Re: User interface event handling
« Reply #6 on: February 23, 2017, 09:11:55 am »
Both these requirements mean that the actual result of the encoder's movement should be separate from the encoder logic functions.
Yes. I have used a logical navigation enum that the UI framework uses. As long as something generates these logical 'events' the UI framework will do its thing. This means you can also have push buttons generate these navigation values.

Finally, the encoder logic is interrupt based. I'm pretty certain my current project would be fine if I polled, but again, I want an encoder library that is universal, so I want it fast enough for more MCU intensive projects.
I would separate the logic for reading an encoder from how these results/states are used/read by the application. Have two initialization methods: one for using interrupts and one for using a callback fn-ptr.

What type of interrupt to use (timer or pin-change) is up to you. You could support a specific init function for each. I would not specify any code for the interrupt-handlers themselves. Just support an function that is optimized for being called from an ISR that allows the code to retrieve the button state.

My question is this. If I have my encoder ISRs return the result (Clockwise turn/ Counterclockwise), I would still have to pol to get that result, right? So maybe for instance, the ISR stores its return value in some global. I still need to periodically check that global, which amounts to polling again. Is this a valid method? My gut says the main reason for using interrupts is to not miss encoder events, but as long as that's working, I lose nothing by polling for the result in the global, right?
If you follow the mechanism I described, this becomes an application concern and it can choose whichever method it prefers - and also decide how much work is to be done inside the ISR.

Hope it helps.
Wrong code should not compile!
 
The following users thanked this post: newbrain

Offline newbrain

  • Super Contributor
  • ***
  • Posts: 1918
  • Country: se
Re: User interface event handling
« Reply #7 on: February 23, 2017, 09:54:58 am »
I have used a logical navigation enum that the UI framework uses. As long as something generates these logical 'events' the UI framework will do its thing. This means you can also have push buttons generate these navigation values.
[...]
If you follow the mechanism I described, this becomes an application concern and it can choose whichever method it prefers - and also decide how much work is to be done inside the ISR.
I'm with obiwanjacobi here (and I'm also using an enum).
It's the application responsibility to decide if a counter is the right abstraction, and it's easy to do if one has the input events.
In my projects, I never needed it.
Increment and decrement events might come from other input interfaces (in my case: UART, touch screen and physical buttons).

Of course, if all you need is one encoder and the counter is enough the solution can be simpler, but I read the OP's question as aiming to something more general.
Nandemo wa shiranai wa yo, shitteru koto dake.
 

Offline JPortici

  • Super Contributor
  • ***
  • Posts: 3921
  • Country: it
Re: User interface event handling
« Reply #8 on: February 23, 2017, 10:12:51 am »
+1 for separating things.

there is the menu routine which will look for new encoder events and do things. <- called when needed

there is the encoder routine which will look at the values from the hardware and decide wether there is a new event <- called at a fixed time interval (for example write in a structure the new state, direction, speed/number of pulses/state of pushbutton)

and then there is the hardware layer, which will be changed based on processor/pinout. <- called on an hardware pin change (increment/decrement counter, whatever) or at a fixed timebase (sampling the pins)

or any other flavour of it. in the end you just have to change the hardware part and as newbrain said the event could come from other things, not only an encoder
 
The following users thanked this post: newbrain

Offline obiwanjacobi

  • Super Contributor
  • ***
  • Posts: 1030
  • Country: nl
  • What's this yippee-yayoh pin you talk about!?
    • Marctronix Blog
Re: User interface event handling
« Reply #9 on: February 23, 2017, 10:29:52 am »
there is the menu routine which will look for new encoder events and do things. <- called when needed

Pulling requires more logic/information/dependencies than pushing (don't ask, tell).

I usually prefer to pass data in the main loop. So the main loop would retrieve the new button state and push it into the UI framework -translated to a logical navigation command or something similar...
Wrong code should not compile!
 

Offline JPortici

  • Super Contributor
  • ***
  • Posts: 3921
  • Country: it
Re: User interface event handling
« Reply #10 on: February 23, 2017, 10:39:07 am »
actually, that's what i meant. I see that it wasn't clear :)
let's say we have a structure with

newevent <- could be a varaible on its own but when i can i try to keep it inside the structure
direction
speed
pushbutton

newevent gets set if (during the "encoder routine") hardware changes are detected, either rotation or pushbutton.
the main checks new event, in case it pulls the rest of the data and push it into a structure of the same type created by the menu handling routine.
then when the menu routine is called it checks the state of HIS newevent, in case uses the data and then clears the structure.

not sure if there are better ways but i think it is reasonably secure/robust
 

Offline John Coloccia

  • Super Contributor
  • ***
  • Posts: 1622
  • Country: us
Re: User interface event handling
« Reply #11 on: February 27, 2017, 02:25:52 pm »
One thing to keep in kind, especially with things like rotary encoders which are kind of "continuous" in nature, at least to the user. If you naively queue the "events" from the ISR, and then process them all at once when you get to it, sometimes it will feel very funny to the user, especially if they're turning things quickly. Sometimes you want to decouple the logic a bit and turn it into something like, "He's turning anti-clockwise at approximately this rate, so scroll the selection at this approximately matching rate"...or "he's STILL turning, so let's keep scrolling"..or "he's stopped! So stop!" 

That's much different than processing events in strict order. For example, the user may have turned the knob super fast and queued up 100s of pulses, and you start handling them and then the user stops...because he's where he wants to be. You don't want to just keep stubbornly moving around anyhow. You want to be intelligent enough to know that the user intends to stop NOW. He may even change directions. Hey, no problem....stop and change directions, because he's operating closed loop based on what he's seeing, not counting pulses and trying to imagine your internal processing.

It's just the difference between doing precisely what the user told you to do as opposed to doing what he actually wants you to do and giving a pleasant UI experience.
 

Offline Jeroen3

  • Super Contributor
  • ***
  • Posts: 4628
  • Country: nl
  • Embedded Engineer
    • jeroen3.nl
Re: User interface event handling
« Reply #12 on: February 27, 2017, 03:14:58 pm »
You'd want to give the UI thread a relative movement since last reset based on knob velocity.
Slow: 1 tick.
Fast: more ticks.
Short fast burst: many ticks. (like a real jerk on the knob)
Of which these modes are configurable per viewed item.
Changing a number: 1, 5 and 50.
In a menu: 1, 5 and top/bottom.

Never queue multiple UI events. If for some reason the UI thread is slowed down, all the queued events will process quickly when the UI thread restores and it might jump all over the place.
The user loses control, and it shows poor software.
Like on the DS1054, where the knob is super senstive and the UI often lags. It's even worse on the Tektronix TBS 1152.

Quote
It's just the difference between doing precisely what the user told you to do as opposed to doing what he actually wants you to do and giving a pleasant UI experience.
The same applies to "hold to increase" buttons. When you're running trough the numbers and the user releases the button. You'd want to go a few digits back, and possible round it as well. Humans are slow.
« Last Edit: February 27, 2017, 03:16:50 pm by Jeroen3 »
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf

 

- - -
-->