User interface event handling - Page 1
Forum section: electronics / general

author: Dan Moos
author_rank: Frequent Contributor
timestamp: February 23, 2017, 04:11:41 am
content: 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?

author: newbrain
author_rank: Super Contributor
timestamp: February 23, 2017, 08:47:18 am
content: 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.

author: bktemp
author_rank: Super Contributor
timestamp: February 23, 2017, 08:57:17 am
content: 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.

author: nctnico
author_rank: Super Contributor
timestamp: February 23, 2017, 08:59:07 am
content: Quote from: bktemp on February 23, 2017, 08:57:17 amThat 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.

author: MarkF
author_rank: Super Contributor
timestamp: February 23, 2017, 08:59:46 am
content: 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:int8_t en0;uint8_t currA0,lastA0;void main(void){int clicks;// Setup rotary encoderen0=0;lastA0=PORTCbits.RC1;// Main loopwhile (TRUE) {// Process encoder dataclicks = en0;en0 = 0;// Encoder data// 0 == No change// >0 == Number of clockwise clicks// <0 == Number of counter-clockwise clicksif (clicks != 0) {// Do something with encoder data}// Update displaydrawDisplay(0);}}void interrupt isr(void){if (PIE1bits.CCP1IE && PIR1bits.CCP1IF) {PIR1bits.CCP1IF = 0; // clear CCP1 Interrupt Flag// Read rotary encodercurrA0=PORTCbits.RC1;if (lastA0==0 && currA0==1) {if (PORTCbits.RC2==1) en0--; else en0++;}lastA0=currA0;}}

author: bktemp
author_rank: Super Contributor
timestamp: February 23, 2017, 09:10:02 am
content: Quote from: MarkF on February 23, 2017, 08:59:46 amThat 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.

author: obiwanjacobi
author_rank: Super Contributor
timestamp: February 23, 2017, 09:11:55 am
content: Quote from: Dan Moos on February 23, 2017, 04:11:41 amYes.I have used a logical navigation enumthat 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.Quote from: Dan Moos on February 23, 2017, 04:11:41 amI 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.Quote from: Dan Moos on February 23, 2017, 04:11:41 amIf 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.

author: newbrain
author_rank: Super Contributor
timestamp: February 23, 2017, 09:54:58 am
content: Quote from: obiwanjacobi on February 23, 2017, 09:11:55 amI'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.

author: JPortici
author_rank: Super Contributor
timestamp: February 23, 2017, 10:12:51 am
content: +1 for separating things.there is the menu routine which will look for new encoder events and do things. <- called when neededthere 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

author: obiwanjacobi
author_rank: Super Contributor
timestamp: February 23, 2017, 10:29:52 am
content: Quote from: JPortici on February 23, 2017, 10:12:51 amPulling 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...

author: JPortici
author_rank: Super Contributor
timestamp: February 23, 2017, 10:39:07 am
content: actually, that's what i meant. I see that it wasn't clearlet's say we have a structure withnewevent <- could be a varaible on its own but when i can i try to keep it inside the structuredirectionspeedpushbuttonnewevent 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

author: John Coloccia
author_rank: Super Contributor
timestamp: February 27, 2017, 02:25:52 pm
content: 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.

author: Jeroen3
author_rank: Super Contributor
timestamp: February 27, 2017, 03:14:58 pm
content: 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.QuoteThe 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.