Having many rotary encoders. - Page 1
Forum section: electronics / general

author: paulca
author_rank: Super Contributor
timestamp: January 26, 2023, 09:28:14 am
content: I wanted to start making a TFT+Rotary encoder interface for a project, but when I came to work out what I needed the rotary encoders turned out to be a bit more involved than I thought.I would like 6 to start with, might need more, 10 maybe.STM32's have a rather handy encoder mode for it's timers. However, from a quick search the largest amount of these available, from the F103 up to the H750 is .... 4.I went through a few google searches and seen a few solutions, which seemed either "janky" like HC74 multiplexers and hot loop polling them in the main loop, to using additional micros.I searched and found a few breakout style AdaFruit et.al boards which presented an I2C bus interface allowing the encoders to be chained into long strings of I2C devices on the same bus. They even do their own counting internally, raise hardware interrupts on change etc. etc. They are also damned expensive, come from the US and all it really is, is a £3 micro-controller under the encoder. While convenient in purchasable form, it's a bit inefficient to use a single micro per encoder when an F103 can handle at least 4 (more if you do the others in software with EXTIrq's).A rotary encoder multiplexer sounds like a fun project.I thought I'd ask if anyone has any magical ways of reading 6 or more encoders without having to use 1 or 2 small MCUs to do so? In fairness, 6 encoders is 18 pins straight up. Add your uplink periph, like I2S or SPI and the usual other pin items and a 48pin MCU is way over 50% pin allocated. A F013 running 4 rotary encoders and raising interrupts, transferring data over I2C or SPI probably isn't going to have much left in terms of timing and performance either.I have an F401 which was dedicated to the UI, now it seems as though that expands to the F401 for the TFT and main UI code + 2xF103's to run the rotary encoders! Good new is, they are cheap, might do the job without an external clock, available in 10s of thousands for £3 each.

author: Ian.M
author_rank: Super Contributor
timestamp: January 26, 2023, 10:23:06 am
content: It depends on how fast you need to read them. Save the MCU's built in QEI/encoder modules for anything fast e.g. high RPM shaft encoders.Assuming you need to read six clickable knob encoders, if you use an ADC input + resistor ladder to read the click switch (which means you cant read clicks further down the ladder if a higher one is held in), you can save five pins, dropping the requirements to 12 digital and one analog port pins. If you don't want to have to poll them when idle, those will need to be IOC capable + the analog pin will need to be able to generate a comparator interrupt.Another approach to minimising the pin count for mechanical (switch to ground) knob encoders is to wire them in a matrix with a schottky diode in series with each encoder's A, B and switch pins. You can then select an encoder by driving its common pin low, deselecting the others by driving their commons high. That gets you down to nine I/O pins, but you cant use IOC wakeup, unless the encoder knobs have detents with both A and B open at the detent positions.

author: dave j
author_rank: Regular Contributor
timestamp: January 26, 2023, 10:24:50 am
content: RP2040s have no problem supporting eight rotary encodersusing PIO.

author: Berni
author_rank: Super Contributor
timestamp: January 26, 2023, 11:24:26 am
content: You can just use a GPIO pin change interrupt to fire off a small state machine for each of the encoders.You also get notified of what pin number saw the change so if you distribute the encoders over the right pins you can have the flags tell you only what encoder needs updating and only execute its corresponding state machine to save CPU cycles.The code only runs when a encoder makes a step and it is so short that it doesn't really eat many cycles to service. So given enough pins you could easily have one MCU service 50 encoders.

author: paulca
author_rank: Super Contributor
timestamp: January 26, 2023, 11:44:31 am
content: 6 encoders. 4 of them on the Timer Encoders. The other 2 on ExtIrq GPIOs. Still requires 18 pins, but an F103 should cover that fine and so the SPI updates to the processor with the config.I think the point about speed is valid, these hardware "encoder" modes might be more useful if you are using, say, high percision, detent-less encoders that encode hundreds of steps per rotation and you can't be expected to handly each step with an interrupt routine. For a few volume knobs and some parameter select + value knobs.... overkill.However, if I have 4 of them why not use them. Actually it works out okay, as 4 of them are volume controls and I already worked out how to limit them to 0-100 in the ISR.The other two are "Parameter select" and "Parameter value", which have much more free-form ranges and steps so are probably better done in a custom Irq handler anyway. Obviously it depends on what paramter you have selected as to how the encoder behaves.... I might step through 0-100 volume in 1s, but I'm not stepping through a "Filter cut off" frequency entry in 10s or 100s even, it will need to log something. The gain is a linear float (unless I get more annoyed with that than I already am), so it needs to step in log-something steps from 0.001 to 1.0.Putting the TFT and all 6 encoders onto the single F401 might be doable, I mean it's still only 27 pins or something, but I don't think creating a little encoder bank PCB with a F103 is "excessive modularisation". I'm sure I will have other projects that require a set of encoders. I'm also striving to find ways to modularise and limit the overall size of the prototype on breadboads. Wrapping something like this onto a PCB with some JST-XH connectors, rather than a bit hairy jumpered breadboard birds nest.

author: paulca
author_rank: Super Contributor
timestamp: January 26, 2023, 11:49:11 am
content: BTW.... I am also aware it would be possible to do the whole UI with a single encoder.It's just that is one of my pet peeves about electronics in general. "You could have done it so much justice, you could have created a truely excellent product, but you had to go and bean count things didn't you? 4 Rotary encoders worked out to be more expensive over the full product production cycle than the time it would take the out-sourced programming contractor to code it for a single encoder.", therefore the product has a single encoder whether that was the best solution or not.I set the bar quickly by saying, "I want an independant, always available volume control on each input and each output. No Select from menu, change value."The other two could, again be done with 1. Rotate to highlight parameter, press to select, rotate to change, press to commit. I still think it would be much easier, faster to use with 2. I have 2 hands for a start!Also, "Why not just 100K POTs and ADC channel for volume knobs?". It's a good question. My answer is mostly subjective. I hate pots slightly more than I hate rotary encoders, for the same reasons. They end up scratchy, fissly, jump values, or my favourite.... the volume control stops at 99% and never goes to 100% because the ADC VREF is VCC. Also. non-motorised POTs can't be automated and remain in sync. So if you automate the value and a bird takes off from the window sill causing a twitch on the ADC and it overwrites your value back to the physical position of the POT... rar..rar...rar. etc.

author: wek
author_rank: Frequent Contributor
timestamp: January 26, 2023, 11:50:59 am
content: Quote from: paulca on January 26, 2023, 09:28:14 amOn STM32H750, you can use TIM1/TIM8, TIM2/TIM3/TIM4/TIM5, and also LPTIM1 and LPTIM2. That's 8.JW

author: paulca
author_rank: Super Contributor
timestamp: January 26, 2023, 12:02:03 pm
content: Quote from: wek on January 26, 2023, 11:50:59 amInteresting. To be honest I was basing my "4" from the high level features list.QuoteI wouldn't be surprised if that isn't 100% correctI could use the 750 for the whole UI, but even then it feels like overkill. I happen to have an H7B0 as well, even it would be overkill. Timing is too tight on the processing MCU to task it with UI work and it's complicated enough.What is bound to happen here is... I'll push forward with an F401, get it nearly working and something will crop up which means I need a bigger micro anyway and end up re-doing it for the H7 anyway. Still the journey will be fun.The UI needing the H7B0 of H750 will come about if/when I implement the touchscreen and present the EQ settings as a rendered graphical curve and allow the EQ grab handles to be clicked and dragged around. Recalculating the curves and rendering them realtime with enough low latency to give enough feedback for dragging will require a descent refresh rate. I'm not doing that this time round... mostly because I don't have the graphical programming or maths skills to do that and won't really know where to begin. So a weedier micro will do.

author: peter-h
author_rank: Super Contributor
timestamp: January 26, 2023, 02:29:04 pm
content: This is not of direct use to you but in the 1980s I implemented a rotary encoder interface on a Z80, via the Z80-PIO chip, and a few gates. The PIO has two 16 bit counters and this meant the CPU didn't have to sample it very often. It was an elegant solution which went into lots of products and never gave any trouble. I recognise thishttps://www.fpga4fun.com/QuadratureDecoder.htmlas something very similar.

author: Peabody
author_rank: Super Contributor
timestamp: January 26, 2023, 03:44:42 pm
content: Can you say that at most two encoders would be turning at the same time? If so, then the interrupt burden wouldn't be all that great, but the limitation would be identifying which encoder generated the interrupt without having to read all the GPIOs. For the Atmega328P for example, I found that I could have four encoders (but not their push buttons) with very little IRQ overhead. That chip only has two "hardware" interrupt pins. And it has one pin change interrupt flag per port, so you don't know which pin changed.In the end, I worked out a system in which the interrupt is enabled on only one of each encoder's pins at a time (which also greatly reduced interrupts due to bouncing). To deal properly with direction changes, the 16-byte lookup table was extended to 32 bytes. Anyway, I could have four encoders on that processor - one on the the hardware interrupt pins, and one on each of ports B, C, and D using pin change.But if you had a processor that had a more capable pin change flag register that would identify which pin actually generated the interrupt, then it seems the only limitation on the number of encoders would be the pins available. I think even the lowly MSP430 chips would do that. They don't have pin change interrupts, but do have both rising and falling edges, by pin, and do identify the interrupting pin in the flag register.Anyway, my point is that if your processor lets you identify which encoder generated the interrupt without having to read GPIO pins, and if you only have to service at most two encoders at the same time, then the only real limit is the number of pins available.

author: mino-fm
author_rank: Regular Contributor
timestamp: January 26, 2023, 05:15:06 pm
content: You need one >= ATtiny202 per encoder and can readout via IIC:http://mino-elektronik.de/mt12_iic/mt12_iic.htm#qcnt_tiny202Look above, there is a circuit using an ATtiny25.

author: paulca
author_rank: Super Contributor
timestamp: January 26, 2023, 06:05:11 pm
content: Quote from: peter-h on January 26, 2023, 02:29:04 pmIt has to be similar to what STM32 are doing with the Encoder mode.

author: langwadt
author_rank: Super Contributor
timestamp: January 26, 2023, 06:35:19 pm
content: I'm quite sure an F4xx will do six, from stm32f11ex.hCode:/****************** TIM Instances : supporting encoder interface **************/#define IS_TIM_ENCODER_INTERFACE_INSTANCE(INSTANCE) (((INSTANCE) == TIM1) || \((INSTANCE) == TIM2) || \((INSTANCE) == TIM3) || \((INSTANCE) == TIM4) || \((INSTANCE) == TIM5) || \((INSTANCE) == TIM9))

author: SiliconWizard
author_rank: Super Contributor
timestamp: January 26, 2023, 06:44:07 pm
content: Sorry if I missed a post and that is redundant.But obviously if you have many encoders to handle, just use GPIOs configured as inputs, one time and sample the inputs, do not trigger interrupts on them. Just use a single timer and sample everything in it.

author: paulca
author_rank: Super Contributor
timestamp: January 26, 2023, 07:52:55 pm
content: Quote from: langwadt on January 26, 2023, 06:35:19 pmYes I also found the F401 will do 5. Possibly the F4* can do 6, but pin count etc. they just say "4" across the board? Probably a case of "I should FTFD".

author: Dave
author_rank: Super Contributor
timestamp: January 26, 2023, 09:10:57 pm
content: Quote from: SiliconWizard on January 26, 2023, 06:44:07 pmThis is the way to go. Triggering interrupts on pin changes is silly. The contacts tend to bounce more as the encoders get old, so it eventually might start to affect the performance of your application. Not good.In my experience, sampling encoder pins with 1kHz was fast enough that I was unable to make the count skip when twiddling the encoder (12 pulses per rotation) as fast as I could with my fingers.

author: peter-h
author_rank: Super Contributor
timestamp: January 26, 2023, 09:42:48 pm
content: QuoteYes, probably. I remember wrong though. It was a Z80-CTC and the two 16 bit counters were fed with upcount and downcount, and some software subtracted them from each other, using modular arithmetic.If you were interrupting from each edge you would do the same thing.The bit I never understood was how the boundary conditions were handled e.g. an edge arriving at exactly the "wrong" time. But it did. Somebody worked it out

author: nctnico
author_rank: Super Contributor
timestamp: January 26, 2023, 10:03:23 pm
content: Quote from: Berni on January 26, 2023, 11:24:26 amInterrupts on GPIO pins is asking for trouble. Don't do that. Especially encoders can get flaky and then the whole thing comes crashing down. But still you can use GPIOs. But sample these under a timer interrupt while using a switch matrix. That is how it is done in test equipment like oscilloscopes with rather slow microcontrollers... Easy peasy for a modern day microcontroller.

author: PCB.Wiz
author_rank: Super Contributor
timestamp: January 26, 2023, 10:04:33 pm
content: Quote from: paulca on January 26, 2023, 09:28:14 amThat depends on the speed.You cannot hardware MUX fast quad pins, as the edges contain the information.There are hardware Quad counter chips, but they cost more than the small MCU you want to avoidhttps://lsicsi.com/shop-products/counters/You would use those if your encoder was motor position driven, needing high capture speed.Quote from: paulca on January 26, 2023, 11:49:11 amAh, OK, for a UI operator control, you do not need MHz speeds,so you have many solutions.If you expect to use many channels, I'd skip the many-hardware-timer config step, as that's a lot of testing (or maybe config one timer, for easy testing the Sw counters.. )As the operator has only two hands, total edge rate will be quite low.Your choices are a pins-change interrupt, or a timer interrupt that polls pins.In both cases, you read the whole group of Quad Ctrs and act on the one that changed, in a state machine. Interrupt code path is always fast.It is a good idea to use a series RC filter on the encoder pins, that works with the Schmitt pin (most MCUs have) to limit the highest bounce frequencies.Notice the decision to use a remote small MCU or poll-read via a i2c/SPI GPIO, is often made by the wiring.Placing the pin-read device on the front panel, can slash the wires needed.There are shiploads of 8 and 16 bit GPIO parts, you can even buy > 16 GPIO, but they are into the rare and expensive space.Addit: If you are not making hundreds, there are at-the-encoder i2c solutions, that place a MCU at each encoder ( little MCU these days are cheaper than cables !)https://www.duppa.net/shop/those have 32-bit i2c counters, mounted on the back of the encoder, and one supports a nifty RGB illuminated encoder.

author: SiliconWizard
author_rank: Super Contributor
timestamp: January 26, 2023, 10:22:39 pm
content: Quote from: Dave on January 26, 2023, 09:10:57 pmYeah. Interrupts on A/B signals change for rotary encoders may be used in very specific cases, for instance if you want your device to be in a deep sleep mode most of the time, and wake up on some action on encoders. (And sure it requires proper debouncing.) Otherwise, simultaneous sampling of all signals in a single timer is the best approach. The sampling period doesn't need to be very short. 1 ms is plenty. You can probably go with even slower sampling.

author: dobsonr741
author_rank: Frequent Contributor
timestamp: January 27, 2023, 05:35:45 am
content: A long shift register chain read by DMA over SPI. Example at midibox:http://www.ucapps.de/mbhp_din.html+ the firmware for ST32Not polling, not IRQ, DMA instead.

author: Berni
author_rank: Super Contributor
timestamp: January 27, 2023, 06:38:21 am
content: Quote from: nctnico on January 26, 2023, 10:03:23 pmHave been using the interrupt based encoder method a few times and it worked fine.I always put capacitors on the encoder pins to clean up the contact bounce. But even if they do bounce, correctly written code will not care, it just results in extra CPU load of entering an interrupt multiple times. The interrupt will just have its count value twitch back and forth as if the user was twitching the encoder back and forth (Since that's what the pins look like is happening). Overall the CPU usage is very low because humans can't spin encoders very fast.The kind of encoder code that breaks with bouncing this is the kind where you interrupt on say A signal and then check the state of B to see if you will increment/decrement. Any contact bounce will completely screw that up and give ghost counts. The method you should be using instead is a state machine with 4 states that transitions between states upon seeing the appropriate edge on the A and B pins.The more tricky part is getting velocity control to work nicely. That needs the involvement of a timer to periodically check the rotation speed and amplify it up when needed. This is where a lot of products fall on its face. A lot don't have velocity control at all while others implement it in a bad way, like where there is a thin line between no velocity but going a bit faster shoots you off at the speed of light, or even velocity control that seams to sometimes go slow sometimes fast randomly.

author: paulca
author_rank: Super Contributor
timestamp: January 27, 2023, 09:08:45 am
content: Quote from: PCB.Wiz on January 26, 2023, 10:04:33 pmI seen these. They aren't cheap especially if you wanted 6 of them! When I seen them I thought, well, if I was to buy those, why not make something instead.I had a play last night. An STM32F103, 4 timer based encoders, SPI uplink to the main UI. Interrupts of the switches. On change handler to filter the output. Debug UART. It was rather straight forward to get one encoder working. 4 should mostly be copy and paste. The F103 has very little left, it maybe has about 7 usable pins left. Each encoder has a small struct which is sent over SPI "onchange".I actually went off on a tangent trying to implement the full button state machine events like, Down, Up, Click, Hold, Release, Double Click. The first 3 are easy enough, the later 3 require state machine and timers. In the end however I came face to face with the fact that to implement all those "events" properly, I would need to think it through carefully and create some form of event system, winging it and hacking it would just lead to tears. Up, Down and Click will be fine for nowGood points on the bouncing interrupts. I will get the same for the switches, but in testing it doesn't seem that bad, they are quite heavily gated microswitches so you have to push them quiet hard and then they "SNAP" closed with a tactile click. I know they will still bounce, but the onchange handler will stop some of the outputs.On polling a higher number of encoders. It sounds interesting. I have done the state machine variant before, we had a discussion in here about it. I favoured the approach of ignoring everything except what are valid transitions between previous and current states. It still suffers from bounce, as we discussed, but, it seems solid enough with a new encoder. It might not work as well with a dirty old encoder.Assuming I carry on with the F103 approach, the plan is to make it a PCB with the rotary encoders being mounted mechanically to the PCB via their ring nuts and the pins wired to the board with short wires. This combines the front panel creation, as I can make the PCB front panel sized, use black solder mask, have the 4 encoders + their MCU on the panel and a large hole and mounting screws for the TFT screen. I don't need to fix the size of the device, if I need it bigger than I make the front panel, I can just seat the front panel into a larger 3D printed lid.EDIT: What about the other 2 encoders?Well, as I said, 4 of them are volume controls, the other 2 are more complicated and will change many different parameters. So it will be far easier to deal with it local to the UI MCU. The timer encoders have state (the count). The 4 volume control board SPI is bi-directional and the UI can send an interrupt to the encoders and send new state for them all, such as at power on to select a default profile or initial volume state. Trying to use this to set the encoders range, step size etc. Would be a complete pain. So they are probably better off local.

author: Nominal Animal
author_rank: Super Contributor
timestamp: January 27, 2023, 10:49:12 am
content: Because I too like rotary encoders, especially the Bourns PEC11R –– I even have a M7x0.75 tap so I can screw the encoder directly to a tapped hole in my cast aluminium project enclosures ––, I've looked at making tiny boards with footprint for the encoder and a TinyAVR 0-series (ATtiny202 or ATtiny402, 0.51€ at Mouser in singles, well stocked; uses UPDI for programming). As they have I²C/TWI capability (incl. slave) and pin change interrupts on all I/O pins, they're particularly well suited for this. One could also use them as a SPI slave (chained slave?), but the two need to use different pins, and therefore a different board.As far as I can tell, the price per such I²C would be around 3€ apiece in lots of ten (including the encoders, which are the majority of the cost – the MCU, board, and passives should be only about 1€ apiece), although one would need to solder the encoder, SOIC-8 MCU, and the couple of passives oneself.I particularly like the idea of using these in a SPI chain, as SPI slaves, with buffer mode enabled (CTRLB.BUFWR set to 1). Each encoder would yield a 24-bit record, where the first byte is a 6-bit decisecond counter describing how long the button has been pressed, clamped to 63, starting at 1, with two MSB always 0b10. The next two bytes are the encoder state as a 16-bit unsigned integer. The slave chain works like a shift register, and the MCU can find out the number of encoders connected at run time from the first byte in each 24-bit record. Each encoder state is latched on a suitable transition of the /SS, noting that there must be a couple of milliseconds between that and the first clock pulse, so that all encoders have time to latch their state before the SPI clock starts cycling.

author: voltsandjolts
author_rank: Unknown
timestamp: January 27, 2023, 10:53:14 am
content: Quote from: paulca on January 27, 2023, 09:08:45 amThey're not expensive either and they supportopen sourcetoo.

author: paulca
author_rank: Super Contributor
timestamp: January 27, 2023, 12:06:01 pm
content: Quote from: Nominal Animal on January 27, 2023, 10:49:12 amI have looked into "exotic" SPI bus architectures a few times for various things. The main draw back of custom bus signalling as you suggest, is the timing requirements. I needed microsecond timing, but once you try and add "grace time" for slaves to respond to the SS line and tight timing goes out the window. Once tight timing is gone, the corner cases all come out of the shadows until you decide that you can afford to use more than one SPI bus and KISS principle.SPI bus chaining, I have seen it in some advanced SPI use cases with the MOSI leaves the master to the first slave, which at the same time, in it's slot sends the last buffer out it's while receiving the current on the MISO. Each in turn along the line do this. I can see this means you only need 1 SS line. It also adds latency.In terms of HID speeds none of that really matters, if each SPI transaction takes 1ms and the chain is 6 long it's still an order of magnitude below perceivability.Tangent question, but what is the next logical step up from SPI? I know there are parallel GPIO connection approaching, but is/are there many established/standard protocols for linking multiple devices more tightly that SPI?I'm thinking at one extreme you could implement a whole sub bus with address and data. However that might be OTT and pointless.What might be a better approach is using something Quad SPI and the endpoints support a section of "shared memory" which is mapped and broadcast via the QuadSPI bus to all members. The result "in code" is a section of memory that if you write to it, automagically in a whirlwind of concurrency traps appears on all other devices.What other options lie between SPI and PCI/ISA or custom full bus?

author: NorthGuy
author_rank: Super Contributor
timestamp: January 27, 2023, 02:22:21 pm
content: Quote from: nctnico on January 26, 2023, 10:03:23 pmYes, that's the best way for user-operated knobs. They are very slow. You may need to debounce them though. Create a timer interrupt. Inside the ISR, read the whole GPIO port (or two) and use bitwise operations to process them all at once. You can easily do a dozen, even with a slow MCU.

author: NorthGuy
author_rank: Super Contributor
timestamp: January 27, 2023, 02:30:55 pm
content: Quote from: paulca on January 27, 2023, 12:06:01 pmCAN. It can handle hundreds of devices, long wires, provides "free" arbitrage, priorities, error handling. In modern cars everything is CAN.

author: Nominal Animal
author_rank: Super Contributor
timestamp: January 27, 2023, 02:51:50 pm
content: Quote from: paulca on January 27, 2023, 12:06:01 pmNothing exotic here! Well, actually, I was thinking of having the encoders latch their state at the rising edge of /SS.In practice, you can either pulse /SS, in which case you're treating it as a latch strobe (in addition to slave select active low), or treat it as a normal slave select active low signal, in which case the slaves latch their state at the end of each read sequence. In neither case is there any kind of tight signaling: just a minimum /SS high time. With 16 or 20 MHz ATtiny202/204/402/404, I'd say 10µs minimum /SS high would be plenty (and lead to rock solid performance, since it corresponds to 160 or 200 cycles, several times the cycles the actual latch operation needs, even if accounting for concurrect encoder interrupts occurring – although I'd set it to higher priority than encoder interrupts anyway).Sure, it would be nice if you had a separate latch strobe, but then you need more than 8 pins (VCC, GND, 3 for the encoders with a button, CLK, MOSI/DI, MISO/DO). ATtiny204 has 14 pins, is in stock, and at Mouser costs about 0.67€ apiece, so you could have a separate latch strobe and simultaneous support for I²C and chained SPI with those.(I do have an idea how the minimum could be eliminated by "pre-buffering" the initial byte to the SPI outgoing data register whenever it changes, but I'd need to verify its timing in practice before I can claim it is possible to do.)Quote from: paulca on January 27, 2023, 12:06:01 pmThat's why you want the latch strobe. That sets the exact moment when the encoder is read. True, there is latency, but let's consider for a moment. Even if you used a 400 kHz clock (similar to fast I²C), you can read 16 encoders once every millisecond (16×24×1000 = 384,000, leaving 40µs per millisecond for the latch strobe). Conversely, the latency is then less than a millisecond. USB HID devices like mice and keyboards also have at least a millisecond of hardware latency (due to USB HID protocol), so I'd say it is perfectly acceptable.Even with naïve coding, I believe up to 1 MHz SPI clock should be easy to support, maybe higher. (I do believe the maximum SPI slave clock these ATtinys can support is half the main clock, so 8/10 MHz, but it needs to be limited it low enough so the encoder interrupts occurring during SPI transfers cause no glitches or errors to occur.)So, no, latencies are not a problem. The 10µs minimum latch strobe duration can be annoying, considering it is 10Xclock cycles atXMHz, but anything up to a millisecond should be perfectly acceptable, as it just adds to the latency (which is a fraction of a millisecond to a millisecond). Furthermore, repeated latching (without reading the data in between) is perfectly harmless.If one considers the protocol, then one could just as easily support push buttons and analog potentiometers in the same bus, by e.g. using the two or three most significant bits of the 24-bit report as the report type identifier, with 000 or 111 indicating no device (achieved by a terminating resistor to ground or VCC in the MOSI/DI pin at the last device in the chain). For any kind of button-like device, I do like the idea of it reporting the duration it has been kept pressed (which is not automatically cleared when no longer pressed, only after the report has been successfully sent and it is no longer pressed).Another option would be to have each encoder be an I²C master, with the MCU being the I²C slave, so that whenever the encoder state changes, it sends an I²C event to the MCU (slave!); inverted to the normal situation, essentially. One would need to assign each encoder a fixed identifier (included in the I²C data packet), basically their own 7-bit ID, repeated in the event report. Optimally, it would also encode the type of the encoder (maybe two bits?).However, I haven't implemented such a many-master single-MCU-slave I²C bus myself, so I don't know if there are gotchas I haven't thought of that make it impractical. In particular, I don't know if the MCU-slave should respond to all messages on the I²C bus, with the address field indicating thesender, except when it itself is the actual master (sending control commands to a particular encoder), or if there are issues with that.I certainly don't like the idea of having to poll each device via I²C. Itfeels inefficient, somehow, to me –– a purely emotive argument, though.

author: paulca
author_rank: Super Contributor
timestamp: January 27, 2023, 04:17:45 pm
content: Mostly academic at this stage. I have enough this time for the timers for the volume controls, for the Parameter select and value knobs they will be on a different micro and have the option of poll, interrupt or timer.On the interrupts and bounces. It's amazing how something really janky like making your debug logging blocking fixes bouncing pretty sharpish. Of course its because you spend several ms waiting on the UART unaware of the bounces.The trouble I have with that approach is of course later, in a week or two I might strip back a lot of debug calls and ... it all stops working due to bounce city.So I'm going to take another tangent and go and write myself a Logging.h file that takes a UART (or a CDC VCOM endpoint) and gets on with it, so I don't have to run into async logging issues in ISRs every again.

author: cv007
author_rank: Frequent Contributor
timestamp: January 27, 2023, 04:22:24 pm
content: QuoteI use interrupt on A or B (any edge), then read/save the other pin state in the isr, switch interrupts to the other pin, and inc/dec the encoder count if needed. The interrupt can be caused by a bouncy pin but that pin's interrupt is turned off in the isr so no repeat isr for the same pin. Reading of the other pin will/should be a stable state, and when you get to a place where both pins last state were 0 you have a step and the direction is determined by the pin that caused the interrupt. Doesn't matter if direction changed anywhere in the process, and debouncing is not needed since the bouncy pin only indicates a pin change and its irq will then be disabled.A good test for me is to use the encoder knob index to see if I can get the same absolute position after turning the knob many times fast in either/both directions, then return the index to its original position where I should get the same absolute value. Most of the time I do get back to the same value, but there can be times where a step is missed.Works pretty well for me, and can be used on most mcu's where you can differentiate what pin caused the interrupt.https://github.com/cv007/NUCLEO32_G031K8_B/blob/main/Encoder.hppI indicate in the comments that the stm32 needs to use unique pin numbers to make this work, but that may not be the case as one could use the irq enabled state of the pins to differentiate (but if more than one encoder then that will fail to work if using 3 pin numbers that are the same). The g031 does have a lptim with encoder which would make a good test by using that at the same time and compare values. It would also be better to move the pin irq to a high priority so if another irq is using up mcu time there is less chance of a missed step.Complete example for a tiny416-https://godbolt.org/z/YKc99oj91If I had a group of encoders, I would be inclined to move that over to some cheap any bit mcu with enough pins to handle the job and use i2c as the interface. This assumes one can do this quickly/easily without a lot of extra time involved, and end up with an always works result that you mostly do not have to touch again.

author: Peabody
author_rank: Super Contributor
timestamp: January 27, 2023, 05:37:34 pm
content: cv007 +1. And here's my example of switching the interrupt to the other pin using a processor that doesn't even identify which pin interrupted, only the port. With only one encoder per port you know which pin triggered the interrupt because you've only enabled interrupts on one pin. And you know the true non-bouncy state necessary to trigger the interrupt, so you don't even have to read the pin's current value, which read would be subject to bouncing errors. You read the other pin, which transitioned a while back, and should be stable now.https://github.com/gbhug5a/Rotary-Encoder-Servicing-Routines/tree/master/ArduinoI'm still having trouble with the idea of using separate processors and inventing a new addressable communications protocol when a main processor with enough pins should be able to easily handle at most two encoders at a time, particularly if bouncing issues are greatly reduced through switching the interrupt enables. And maybe hardware debouncing to make it perfect, but that's not really necessary.

author: cv007
author_rank: Frequent Contributor
timestamp: January 27, 2023, 10:07:35 pm
content: QuoteI had a mega328 example in compiler explorer that does same/similar, but am unable to test so was just exploring what was possible without specific pin flags for that popular mcu-https://godbolt.org/z/fjTz5KncTQuoteWhen you have 2 and later want to add another, and another, and now have to look for the same mcu series in a higher pin count, it may have been better to separate the procedure out to another mcu. Just depends. The communications protocol would be pretty simple and not much inventing would be needed.

author: Peabody
author_rank: Super Contributor
timestamp: January 27, 2023, 11:50:34 pm
content: Maybe so. But in the original post paulca said he might need as many as 10 encoders. Counting the pushbuttons, that's 30 pins. It seems you could plan for that to begin with. In any case, pin availability is the only issue I can see with doing it all in the main processor. The processing overhead should not be significant because you're only servicing two encoders maximum at a time. And contrary to other comments, I think interrupts are exactly what should be used - the more encoders you have, the more interrupts make sense.

author: nctnico
author_rank: Super Contributor
timestamp: January 27, 2023, 11:55:52 pm
content: For such a simple task, it is completely unnecessary to use two (or more) microcontrollers. Don't make a system distributed unless you really have to. Think about what your system has to do when something fails or communication becomes garbled.Maybe it is better to really dive into what is necessary to read an encoder. A typical encoder has like 24 to 36 pulses per rotation. How fast can you rotate it and make a precise adjustment? Maybe half a rotation per second. That is 18 pulses per second per contact. Each encoder has 2 contacts. So if you have 10 encoders in a 5 x 4 matrix (to handle 20 contacts) and want some oversampling - say 4 times- then you'd need to scan each row 18 * 4 * 4 = 288 times per second. IOW: a 500 Hz timer interrupt to scan the entire matrix is more than sufficient and on a modern microcontroller this is a tiny task.And again: never ever use interrupts for reading stuff like contacts. Recipy for a dissaster when (not if) the contact goes flaky and you can't predict when something is happening so you get an unpredictable CPU load. A timer interrupt scanning a matrix OTOH is a well defined and constant load on a system so it is easy to test & verify the system meets timing criteria.

author: pqass
author_rank: Super Contributor
timestamp: January 28, 2023, 03:52:53 am
content: Building on nctnico's switch matrix scanning solution...you can adapt my single switch debounce implementation herehttps://www.eevblog.com/forum/microcontrollers/switch-software-debounce/msg3684220/#msg3684220(specifically, see the "debounce.ino.txt" attachment).To summarize, setup a 200Hz timer interrupt and execute a DebouncePin() call for every switch.In the case of a matrix, first activate a "row" where the common of 4 encoders (8 switches) are tied together, then read the 8-bit MCU port that the non-common encoder pins are attached to. Do this for 3 rows of commons (up to 12 encoders possible) whose non-common encoder pins share the same 8-bit MCU port pins (as other row's encoders). That's just 3 "row" writes (activations) and 3 port reads per interrupt call. And just 11 MCU pins.The DebouncePin() function basically checks if the last 8 calls (40ms) for the given switch has settled and only changes the debounced state variable once that happens. My AVR version of the function is compiled to 20 instructions giving about 1.25us execution @16MHz system clock. That's 25us for 20 switches (function executions) and then 20 calls to the function, 3 writes (row activations), and 3 port reads, which should be minimal. Additionally, for encoders, you don't really care of the state of each switch but rather the quantity being incremented/decremented. So additional logic will need to be added for that in the interrupt.I've adapted my single switch debounce implementation to track encoder changes. I HAVE NOT TESTED THIS YET. But you should get the gist. Of course you can refactor the pair of vars per switch into a better datastructure and implement the 3 row activations, 3 port reads, and 20 calls logic in the timer interrupt.Code:// last parameter is changed only after 40ms passes (8 checks * 5ms timer interval) after the last time the button changed state.uint8_t DebouncePin(uint8_t current_pin_state, volatile uint8_t *candidate_state, volatile uint8_t *debounced_state) {uint8_t hasChanged = 0;*candidate_state = (*candidate_state) << 1 | (current_pin_state & 0x01);if ((*candidate_state) == 0xff) {*debounced_state = HIGH;hasChanged = 1;} else if ((*candidate_state) == 0x00) {*debounced_state = LOW;hasChanged = 1;}return hasChanged;}static volatile uint8_t enc_sw0_tmp = 0; // temp key state area; only used by DebouncePin().static volatile uint8_t enc_sw0_state = HIGH; // used elsewhere to query debounced state of button.// initialize to normally inactive state; ie. an unpressed button with pullup to Vcc.static volatile uint8_t enc_sw1_tmp = 0; // temp key state area; only used by DebouncePin().static volatile uint8_t enc_sw1_state = HIGH; // used elsewhere to query debounced state of button.// initialize to normally inactive state; ie. an unpressed button with pullup to Vcc.static volatile uint16_t enc_quantity = 0; // var that is incremented or decremented by the encoder.ISR(TIMER1_COMPA_vect) { // called 200 times per second; every 5ms; see timer_setup()if (DebouncePin((PINA>>(PA0)), &enc_sw0_tmp, &enc_sw0_state)|| DebouncePin((PINA>>(PA1)), &enc_sw1_tmp, &enc_sw1_state)) {if (enc_sw0_state && !enc_sw1_state) enc_quantity++;if (!enc_sw0_state && enc_sw1_state) enc_quantity--;}// add similar to previous and static vars (see above) for every encoder to debounce.}// use enc_quantity in main code for current value of encoder.On second thought,using a 24 pulse per rotation encoder at 1 rotation per second means that there is a  switch change every 21ms. A switch needs to finish bouncing and stick to one state for 8 timer interrupts. Assuming we want to test the state at the 1/3rd way point (6.6ms; can't do half-way as the other channel changes then) and assuming half of that is bounce-time, then 8 timer interrupts need to execute in 3.3ms. Therefore, the timer interrupt frequency needs to be increased to 2.4Khz (every 416us). Assuming 40us to execute a single timer interrupt (checking 20 switches and updating encoder quantity vars), then scanning would consume 10% of available MCU time.

author: pcprogrammer
author_rank: Super Contributor
timestamp: January 28, 2023, 06:35:15 am
content: Quote from: nctnico on January 27, 2023, 11:55:52 pmI totally agree with this. Don't over complicate things. The premise here is mainly volume control. How fast and often is one going to adjust these. Even on old 8 bit micros keyboard scanning was done by reading a matrix, often in the main loop, and how often was a key stroke missed?I have written code for interfacing lots of sensors and switches to midi on 8051 microcontrollers without problems. These are much slower then any modern ARM bases micro.A simple user interface even with a display can easily run on a modern micro like a STM32F4. Yes pins might be a limit, but with a matrix it does not need that much pins to connect multiple rotary encoders, including the switches they might have. And there are variants of most STM32F micros with a lot of pins. Need pins take a STM32F407 like this one:https://nl.aliexpress.com/item/1005004950482623.html

author: nctnico
author_rank: Super Contributor
timestamp: January 28, 2023, 08:43:15 am
content: Quote from: pqass on January 28, 2023, 03:52:53 amWhy would you want so many sample points? You can do with 3 or even 2 sampling points to check whether a switch is stable. I typically use 3; a switch is done bouncing after a few ms anyway. When using a scanning matrix, adding a bit of filtering in the hardware is cheap. And make sure to enable as much hysteresis as you can get from the microcontroller.

author: 8goran8
author_rank: Contributor
timestamp: January 28, 2023, 09:03:58 am
content: One of the solutions, for this task, can be a Microchip 8-bit or 16-bit microcontroller that has a Configurable Logic Cell in it, e.g. PIC18F25/45/55Q43 family with 8 CLC.In my opinion, that would be a very good combination of software and hardware solutions to this problem.AN2133 and AN2805 documents from Microchip can be a good starting points for using CLC.

author: paulca
author_rank: Super Contributor
timestamp: January 28, 2023, 10:46:23 am
content: If ALL I was doing was running 4, 6, 10 rotary encoders obviously a single micro. However I'm not only running the encoders, they are meerly the tactile interface. The bulk of the code will be in the actual graphical user interface on a TFT, with touchscreen. The twist there is the TFT will happily take 40Mbit/s +, but the touch screen absolutely wont. Easiest way, path of least resistance is 2 SPI periphs. So the pins go quickly.I have an F405 in 144 pin format and 160Mhz. pretty sure I can get 10 encoders, 3 or 4 SPI, a couple of I2C and several TFTs and gadgets and still have pins spare. I'm not denying that is a possibility.There are other non-functional aspects though.1. A contained 4 encoder PCB component would be quite reusable.2. A separate PCB for the front panel is hardly a unique technique.3. Saves on wiring or the need for ribbon connectors to the front panelWe have discussed my engineering technique in the past and I won't flog a dead horse, except to say you won't get the big iron techniques out of me, I'm too long in the tooth. So "Divide and conquer", "Isolation of responsibility", "Facade/Encapsulate complexity". That doesn't mean farming everything out to a dev board with an MCU and trying to implement the Spring embedded framework! It just means if there are clear divisions or layers in functionality, I personally and professionally like to draw lines of isolation and contract of interface etc. Even if that wastes a little outright performance or efficiency (where they are not important!).If I follow the 4xEncoder approach with an F103, take it to PCB I will have a small pile of boards which I can lift one of should I need any encoders in a project or on a breadboard. I like that idea!

author: RoGeorge
author_rank: Super Contributor
timestamp: January 28, 2023, 11:00:31 am
content: Buy a lot of the cheapest computer mice, in bulk, a lot of USB hubs and a raspberry PI or alike cheapest Linux SBC.Plug all together and use the wheels from the mice as rotary encoders. Bonus, a lot of switch buttons and swipe sensors from the optical sensor inside each mouse. And it's a scalable solution.

author: pqass
author_rank: Super Contributor
timestamp: January 28, 2023, 06:36:47 pm
content: Quote from: nctnico on January 28, 2023, 08:43:15 amYou're probably right. But the algorithm came fromhttp://www.ganssle.com/debouncing-pt2.htm(bottom of the page) and was targeting shitty momentary switches with 10's of ms bounces. Encoders need to be better than that otherwise they're useless. One could modify my implementation to mask-off the top 4 bits before the comparison (eg: "if (((*candidate_state)|0xf0) == 0xff)...") and lower the timer interrupt frequency by half (to 1KHz).My implementation assumes no hardware filtering since I assumed up to 12 encoders (3 rows of 4) can be multiplexed on one 8-bit port + 3 row activation pins. I think caps on the 8-bit port will just muddle things.So, for giggles I decided to get the code working on an Arduino. See attached enc_debounce.ino.txt.Also, you can see the raw+debounced waveforms. See raw.png where GREEN/BLUE is the raw encoder switches that are processed into YELLOW/RED debounced state. Notice it takes about 100us to 300us to settle.And, you can see in cw-ccw-intr-zoom.png debounced switches (YELLOW/RED) on the left with zoomed in in-interrupt indicator (BLUE) and enc_quantity var change indicator (GREEN). It takes 8us to service 2 switches (one encoder). And the GREEN blip shows when the encoder is rotated CW or CCW. Keep in mind that the debounced state (YELLOW/RED) is changed in the main line so it lags when it actually changed in the interrupt.

author: RoGeorge
author_rank: Super Contributor
timestamp: January 28, 2023, 06:50:34 pm
content: The settle time is long, but the glitches are high frequency. Hardware RC debouncing should help a lot.

author: NorthGuy
author_rank: Super Contributor
timestamp: January 28, 2023, 07:13:20 pm
content: For encoders, you often can skip debouncing. When the contacts bounce, you would perceive this as the encoder rotating back and forth very quickly. Therefore, if you're only interested in the position, then you would get the correct position after the contacts stop bouncing anyway.In contrast, for mom buttons, bouncing would be perceived as multiple presses, so debouncing is a must.

author: SiliconWizard
author_rank: Super Contributor
timestamp: January 28, 2023, 07:20:11 pm
content: Quote from: NorthGuy on January 28, 2023, 07:13:20 pmThat completely depends on the encoder's construction.Unfortunately, many cheaper encoders do have significant bouncing that can be read as more pulses in one direction that was intended by the user, if you don't debounce. Very common, and particularly problematic if you use encoders with detent, for which the user expects only one "increment" for one detent.

author: langwadt
author_rank: Super Contributor
timestamp: January 28, 2023, 07:27:00 pm
content: Quote from: SiliconWizard on January 28, 2023, 07:20:11 pmfor that to happen both lines have to bounce at the same time, or the decoding done wrong

author: cv007
author_rank: Frequent Contributor
timestamp: January 28, 2023, 08:32:07 pm
content: QuoteIf you use the method I gave examples for, the 'bouncy' pin is used only to indicate a state change- doesn't matter what transition took place on that pin (H-L/L-H) because you are not relying on its current state for anything else. Once you get to the isr the 'bouncy' pin irq is disabled and you read the stable pins state (saved as 'last state') and enable its pin irq. It then requires a transition on the now stable pin to get to the next isr, and does not matter if direction change takes place at that point because we are now only looking for the change on the now stable pin and ignoring the pin which may change again due to bounce or direction change. When you get to either pin's isr, you process as stated and if the last state of both pins was 0 you have a valid step (I describe them both as last state, but the stable pin state was just read in the isr).You end up with 4 irq's per step, and only 4 (excluding direction change). If you can turn the encoder at 20 steps/sec that will take 80 irq's/sec, and if not turning no irq's take place so no mcu time is used. The better measurement would be how fast can you ever get from one step to the next, and can you process 4 pin irq's in that time. Using the previous value of 20 steps/sec and assuming one single step in that process could be twice as fast (true or not), that would require being able to process 4 isr's in 12.5ms for an extreme single case.The problem I see when trying to read the 'bouncy' pin state is the only reliable info you can get is that it changed, and its current state has little meaning because it can be either state due to bounce. You can work your way around it via a time component, but I think its easier to simply ignore the state of the pin that changed so do not have to deal with bounce at all and any time component is no longer needed. The isr's will be short, so a higher priority irq (if available) is desired to lessen any missed steps, but probably makes little difference in most cases.

author: NorthGuy
author_rank: Super Contributor
timestamp: January 29, 2023, 12:17:08 am
content: Quote from: SiliconWizard on January 28, 2023, 07:20:11 pmIf B is stable then rising edges on A are one direction and negative edges on A are the other direction. More moves in one direction would mean more rising than falling edges (or the opposite), but this is impossible.If B can start bouncing in the middle of the stable state (e.g. some dirt on contacts) then of course all bets are off and you probably may trow such encoder away.

author: NorthGuy
author_rank: Super Contributor
timestamp: January 29, 2023, 12:36:41 am
content: Quote from: cv007 on January 28, 2023, 08:32:07 pmBut you lose a bit of precision. When pin B is stable, pin A may be low or high (depending whether it is left of the contact's edge or right of it). These are different positions but you don't see the difference, So, your resolution is half of the resolution you could've got with conventional method.With conventional method and debouncing, you know the exact position once the debouncing is done, but it takes a delay and your position will not get updated until debouching is done.Or, if you use conventional method and ignore bouncing completely, there will be a bouncing chaotic period where it is exactly on the contact's edge. During such period, you may think that the encoder is right of the contact's edge while it is still on the left, or vise versa. But this uncertainty will disappear as soon as you move away from the contact's edge.

author: cv007
author_rank: Frequent Contributor
timestamp: January 29, 2023, 05:44:41 pm
content: QuoteI guess I am using a 24 position encoder with detent, so precision is already about as high as usable for human interaction. I probably misspoke about the information to be had from the bouncy pin- with what I am doing, the bouncy pin still cannot be read reliably, but one can deduce its state by knowing its last state (which was a stable read). So the bouncy pin state can be deduced by simply inverting its stored last state value in its isr (we are in its isr because the pin changed).Here is a modification to a previous example to use all 4 states with the same alternating pin irq method-https://godbolt.org/z/d3rhaE8rjthe switch/case could probably be simplified although any compiler produced code to do this would probably end up taking mostly the same time.I tested this example as I did the previous example, and it works but a 24 position encoder w/detent is not a great fit for doing this since there is already a detent and those detent stops are a little touchy, plus the extra resolution is unusable. It still holds absolute position pretty well. My original example/method actually ends up with counting a step at the 'third' irq, so the touchy detent stops do not end up doing anything (to the count).edit- a little less verbose isr-https://godbolt.org/z/sWKj8vzvW

author: ejeffrey
author_rank: Super Contributor
timestamp: January 29, 2023, 10:01:51 pm
content: Quote from: NorthGuy on January 27, 2023, 02:22:21 pmTell that to the nearly every designer of rotary encoder UIs that poll too slowly. There is very little that bothers me more than using a $200,000 VNA or a $50,000 car and spinning the menu knob only to have the cursor move backwards. That isn't really acceptable on $5 IoT crap, but it's pervasive on devices in every class.That's not to say that you can't do it with polling, only that polling too slow is pervasive, and most advice on the topic does not address this.

author: Peabody
author_rank: Super Contributor
timestamp: January 29, 2023, 11:51:10 pm
content: Quote from: cv007 on January 29, 2023, 05:44:41 pmIndeed. Just for future reference, below is an Arduino sketch that keeps track of the pins' states, so it doesn't need to trust a read of the interrupting pin when it might be bouncing. And once a pin interrupts, further interrupts on that pin are disabled, but enabled on the other pin, which should be stable, so bouncing should not trigger interrupts. The code uses the state machine, or lookup table, method, where an index is created made up of the current and previous pin states and the interrupting pin, and the index selects one of 32 possible table values to add to the count. Then at a detent, if the count is +4 or -4, a tick is generated. When I run this code, it seems very smooth, properly detects changes of direction, and most of the time there are only 4 interrupts between detents. The code is for the Atmega328P, which could have three such encoders (one each on ports B, C and D), plus another on the hardware interrupt pins D2 and D3.I just wanted to make the point that using interrupts for rotary encoders is definitely not a recipe for disaster, and they can produce by far the lowest servicing overhead in case the processor has other things to do. But you do need to prevent bounces from triggering interrupts if you can.Code:const int aPIN = 4; // encoder pinsconst int bPIN = 5;/* This section defines the port being used for the encoder pins, along with the PCI registersand interrupt vector. The values shown here are for Port D (D0 - D7), but Port B (D8 - D13)or Port C(A0 - A5) could also be used.*/#define pinZero 0 // data pin name of PD0 (0). PB0 (8) PC0 (A0) for ports B and C#define PORT PIND // port input register (port D includes D4 and D5)#define portVECT PCINT2_vect // The ISR vector for port D pin change interrupt#define portINT PCIE2 // used to enable pin change interrupts on port D as a whole#define ENABLE PCMSK2 // individual pin interrupt enabled = 1#define FLAG PCIF2 // only one flag for whole portconst byte encoderType = 0; // encoder with equal # of detents & pulses per rev//const byte encoderType = 1; // encoder with pulses = detents/2. pick one, commment out the otherconst int THRESH =(4-(2*encoderType)); // transitions needed to recognize a tick - type 0 = 4, type 1 = 2const byte CWPIN = bit(aPIN - pinZero); // bit value for switch that leads on CW rotationconst byte CCWPIN = bit(bPIN - pinZero);// bit value for switch that leads on CCW rotationconst byte PINS = CWPIN + CCWPIN; // sum of bit values of the encoder pinsconst byte ZEERO = 0x80; // "byte" data type doesn't do negative, so make 128 = zerobyte EDGE; // the edge direction of the next pin change interruptbyte CURRENT; // the current state of the switchesbyte TOTAL = ZEERO; // accumulated transitions since last tick (0x80 = none)byte INDEX = 0; // Index into lookup state tableint Setting = 0; // current accumulated value set by rotary encodervolatile byte tickArray; // circular buffer of ticksvolatile byte beginTICK = 0; // pointers to beginning and ending of circular buffervolatile byte endTICK = 0;// The table is now 32 bytes long so as to include the identity of the pin currently interrupting.// The +2 and -2 entries are for a change of direction.int ENCTABLE[] = {0,1,0,-2,-1,0,2,0,0,2,0,-1,-2,0,1,0,0,0,-1,2,0,0,-2,1,1,-2,0,0,2,-1,0,0};void setup() {Serial.begin(115200);pinMode(aPIN, INPUT); // set up encoder pins as INPUT. Assumes external 10K pullupspinMode(bPIN, INPUT);EDGE = PINS; // identifies next pin-change interrupt as falling or rising// assume current state is low, so any change will be risingif(PORT & PINS) { // but if actual current state is already high,EDGE = 0; // make EDGE lowINDEX = 3; // and make "current" bits of INDEX match the current high state}ENABLE |= CWPIN; // enable only CWPIN interrupt in mask registerPCIFR |= bit(FLAG); // clear interrupt flag if anyPCICR |= bit(portINT); // enable interrupts on Port D}void loop() {if(beginTICK != endTICK) { // if anything in circular bufferbeginTICK++;if(tickArray == 1) Setting ++;else Setting--;Serial.println(Setting);}}ISR (portVECT) { // pin change interrupt service routine. interrupts// automatically disabled during executionCURRENT = PORT & PINS; // read the entire port, mask out all but our pins/* Time passes between the interrupt and the reading of the port. So if there is bouncing,the read value of the interrupting pin may be wrong. But we know it must be the EDGE state.So in CURRENT, we clear the bit that just caused the interrupt, and replace it with the EDGEvalue. The non-interrupting pin is assumed to be stable, with a valid read. */CURRENT &= ~ENABLE; // clear the bit that just caused the interruptCURRENT |= (EDGE & ENABLE); // OR the EDGE value back inINDEX = INDEX << 2; // Shift previous state left 2 bits (0 in)if(CURRENT & CWPIN) bitSet(INDEX,0); // If CW is high, set INDEX bit 0if(CURRENT & CCWPIN) bitSet(INDEX,1); // If CCW is high, set INDEX bit 1INDEX &= 15; // Mask out all but prev and current. bit 4 now zeroif(ENABLE & CCWPIN) bitSet(INDEX,4); // if CCWPIN is the current enabled interrupt, set bit 4// INDEX is now a five-bit index into the 32-byte ENCTABLE state table.TOTAL += ENCTABLE; // Accumulate transitionsif((CURRENT == PINS) || ((CURRENT == 0) && encoderType)) { // A valid tick can occur only at a detent// If we have a valid number of TOTAL transitions at a detent, add the tick direction// to the circular buffer. The MAIN loop will update the Setting value and print it.if(TOTAL == (ZEERO + THRESH)) {endTICK++;tickArray = 1;}else if(TOTAL == (ZEERO - THRESH)) {endTICK++;tickArray = 0xFF;}TOTAL = ZEERO; // Always reset TOTAL to 0x80 at detent}if(CURRENT == EDGE) EDGE ^= PINS; // if both pins have reached EDGE state, switch EDGE to oppositeENABLE ^= PINS; // always switch interrupt to other pinPCIFR |= bit(FLAG); // clear flag if any by writing a 1} // end of ISR - interrupts automatically re-enabled

author: PCB.Wiz
author_rank: Super Contributor
timestamp: January 30, 2023, 02:53:10 am
content: Quote from: paulca on January 27, 2023, 12:06:01 pmIt's a shame MCUs mostly ignore the simplicity of SPI chaining.Quote from: paulca on January 27, 2023, 12:06:01 pmFor this application you do not need faster than SPI, and the option best supported is i2c, which easily runs to 400kHz/1MHzSince the RGB encoder was mentioned, it would be nice to support Encoder read and LED out.A small MCU is one solution, if you need a handful the examples linked above save time.- but there are shiploads of i2c GPIO parts, that are very low cost, and LED capability has been added to many. If you are happy with 5 CMOS pins driving 3 leds the cheapest digital IO ones will do.Parts like TLC59108 add more LED-Drive smarts with linear current control and a error/open readback that can be used for buttons.It has 14-devices Address map, which is better than the 8-devices vanilla parts, but not as good as some newer multi-state pins that support 64 or 126 devices.Adafruit has a AW9523 LED/GPIO module, lcsc has the parts for 31c/100That has only 4-device Address map, but has 16 io.There is also a related AW21009 25c/100 with 8io, also 4-device and open/short led detect.Or, because 8 channels may not mean highest volume, (lowest price points) we can relax channels/LEDs and search moreThe lumisil IS31FL3205, lcsc has for 60c/100 and that has 12 channels, open/short detect and 4-deviceThe IS31FL3193 is 45c/100, in QFN32 and many more channels than needed for UI drive/sense. (12x9 matrix)

author: Berni
author_rank: Super Contributor
timestamp: January 30, 2023, 07:08:11 am
content: Quote from: ejeffrey on January 29, 2023, 10:01:51 pmIndeed, few things ruin the user experience of an expensive product as much as slow/glitchy controls.I been mostly using GPIO interrupts to update a encoder state machine and it always worked great and does not need any special hardware, so you can have as many rotary encoders as you have pins to connect them to. Not really worth bothering reading them in a matrix when MCUs in modern SMD packages commonly come with 48 to 144 pins. I like to give the implementation a thorough testing. Marking 0 on the knob with a dot then going apeshit on the encoder, randomly turning it and flicking it as fast as i can..etc then turning it back to the marked 0 and checking the count is 0. Both with or without de-bouncing caps.Some even take it to extremes. Like for example the Agilent MSO9000 series of scopes actually have a little Xilinx FPGA inside the front panel that is handling encoders,button matrix LEDs..etc This is overkill and the more mainstream MSOX3000 series probably have have a more cost optimal solution (Tho i haven't checked) that still had to perform as well as the older scopes. But they do have a VERY good implementation of encoders on there scopes. They are responsive, never miss steps and have really good velocity control. The velocity control is so good that they just let you adjust parameters with 4 digits without giving you an option to choose what digit you change(like most do). You can just flick the knob fast enough to change the 4th digit, or just turn it fast to change 2nd digit or slowly to do 1st digit (Tho this works well on a scope, you likely wouldn't want that on a lab PSU)

author: NorthGuy
author_rank: Super Contributor
timestamp: January 30, 2023, 02:55:09 pm
content: Quote from: ejeffrey on January 29, 2023, 10:01:51 pmThis is not because rotary encoders in the knobs are overly fast. Those people have RTOS, gazillion of interrupts which they often disable with and without need, use high levels of abstraction, "don't reinvent the wheel", "fast way to market", and all that. They have no idea what their timing is. That's why you see skipped points.36 points per revolution rotated by hand? If you think this is fast, I don't know what is slow. Even PIC16 can handle several of these and never skip a point, and this will consume only a fraction of the CPU time.Compare this to a 1000-point encoder on a shaft of a motor rotating at 1500 rpm. See the difference?

author: PCB.Wiz
author_rank: Super Contributor
timestamp: January 30, 2023, 08:24:31 pm
content: Quote from: Peabody on January 29, 2023, 11:51:10 pmThe problem with that assumed quadrature flow, is it will not ignore 'wiper noise' bounce properly.Also, systems that assume a state engine will be always ok (ie bounce cancels), also presumes the state engine nodes not miss any edges.The best design approach seems to be a combination of HW and SW, where the hardware filters the pins so there is a firm lower limit to the edge to edge time (RC filter and schmitt does that ) and the SW is guaranteed to always follow up to that speed.

author: langwadt
author_rank: Super Contributor
timestamp: January 30, 2023, 08:28:57 pm
content: Quote from: NorthGuy on January 30, 2023, 02:55:09 pmor they googled "how to use rotary encoder" and picked the first hit, use one channel as clock the other as direction

author: cv007
author_rank: Frequent Contributor
timestamp: January 30, 2023, 08:29:11 pm
content: QuoteThat is what I do when testing any encoder code/method as you at least will know if you have any missed steps. Some proposed encoder methods are quite bad when you do this (a lot of missed steps), but I suspect the programmer was happy with it only because they were happy with the human feedback loop (turn, see response, turn some more, see response, etc.) and never realize how many steps they are missing. If I can do this and get back to the original position with the same count then I am happy (I'll also settle for +/-1 when doing it fast and furious).For the stm32, I added the lptim1 hardware encoder along with my software encoder with the single encoder driving both- they both match up in absolute position as done above. I initially had driven the lptim encoder with the hsi16 and that did not work at all as there were way too many steps (I had clock filter set to max), but when I switched to the LSI clock then it worked ok. I guess there are too many samples taken when using the high speed clock and my encoder has no hardware filtering.

author: SiliconWizard
author_rank: Super Contributor
timestamp: January 30, 2023, 08:40:44 pm
content: You really start realizing you're doing it wrong when using encoders with detent. Those without detent are a lot more forgiving as users don't have the same expectations with these.

author: nctnico
author_rank: Super Contributor
timestamp: January 30, 2023, 09:23:06 pm
content: Quote from: SiliconWizard on January 30, 2023, 08:40:44 pmUsing an encoder with a detent isn't bad perse but expecting not to miss a step is. IMHO any decent encoder implementation has proportional speed. So the faster you turn it, the bigger the steps it takes. This means that the user never has to turn it really fast to begin with and thus the software doesn't need to support crazy high speeds.

author: NorthGuy
author_rank: Super Contributor
timestamp: January 30, 2023, 09:36:49 pm
content: For testing the software, you can generate the pulse sequence automatically and feed it to your MCU without using actual encoders, then send the discovered positions back through UART. This way you can run the test which includes lots of test situations in a matter of seconds.

author: 8goran8
author_rank: Contributor
timestamp: January 30, 2023, 09:40:48 pm
content: Quote from: PCB.Wiz on January 30, 2023, 08:24:31 pmI agree. For this reason, I suggested the use of Microchip microcontrollers with integrated Configurable Logic Cells. See how it looks on an example:https://github.com/microchip-pic-avr-examples/pic18f16q40-quadrature-decoder/blob/master/README.md

author: ejeffrey
author_rank: Super Contributor
timestamp: January 30, 2023, 10:07:20 pm
content: Quote from: NorthGuy on January 30, 2023, 02:55:09 pmYet in practice, nobody ships products where the shaft encoder misses steps, but everybody ships rotary knob encoders that miss steps. All the stuff about interrupts, abstraction layers, RTOS, etc may be some of the mechanisms, but the underlying cause is that people that the mechanical encoder is slow and they don't bother to check *how* slow. Or they lazily use a single polling interval for the entire UI set by the time needed to debounce push buttons, but an encoder may need 10x or more the sample rate as push buttons. The shaft encoder is recognized as "fast" so people do it correctly.

author: cv007
author_rank: Frequent Contributor
timestamp: January 30, 2023, 10:10:31 pm
content: QuoteYou only need to give up 2 CLC's and 3 timers-QuoteAny timers left to anything else?QuoteMaybe talk to the oscope users that have an option selector encoder that has no detents, so when they push the encoder switch to select they end up moving the encoder to another selection in the process.I see nothing wrong with detents as they can be made to work well, so not hard to meet the expectations of one click == one step.

author: ejeffrey
author_rank: Super Contributor
timestamp: January 30, 2023, 10:15:59 pm
content: Quote from: nctnico on January 30, 2023, 09:23:06 pmThis is exactly what I am ranting about. Tweaking acceleration parameters on an encoder that can't be relied on to count properly is dumb.If I can turn the encoder knob by hand fast enough that it looses pulses, your product is defective. End of story. Only once you have that dead reliable, go nuts with the acceleration controls.

author: 8goran8
author_rank: Contributor
timestamp: January 30, 2023, 10:40:25 pm
content: Quote from: cv007 on January 30, 2023, 10:10:31 pmIt's just an example of using CLC and the same task can be done with fewer resources. Yes, there is still a lot of hardware available on that series of microcontrollers, and the choice should be tailored to the specific application. The point is that the CLC hardware is available on chip and will relieve the software side of the project. If It Don't Fit, Use a Bigger Hammer

author: nctnico
author_rank: Super Contributor
timestamp: January 30, 2023, 11:08:55 pm
content: Quote from: ejeffrey on January 30, 2023, 10:15:59 pmNo, then you are using it wrong. I have actually implemented rotary encoders in several products this way and they work great. There is no need to look for strawman arguments here. If you hit your hand with a hammer, it will hurt. It doesn't mean the hammer is defective; just don't hit your hand with a hammer and all is well.

author: ejeffrey
author_rank: Super Contributor
timestamp: January 30, 2023, 11:30:03 pm
content: OK, but if I swing the hammer down at a nail, and the hammer software decides to interpret that as "go the other way bash the user in the face', the hammer is wrong.If I turn a knob right and the cursor moves left, the device is wrong not me. I honestly can't believe anyone would even have this argument, but it does help explain why there are so many broken devices out there.

author: nctnico
author_rank: Super Contributor
timestamp: January 30, 2023, 11:50:53 pm
content: With proportional accelleration, your point is just moot. You'd be at an insanely wrong value anyway when you spin the knob way too fast. A better analogy is driving a car into a corner too fast. You'll crash the car and it is 100% user error. It is a situation that will not happen when users operate a device the way it is intended. If not, it is immediately clear to the user they are operating the device wrong. So any behaviour outside normal operational limits doesn't need to be covered or dealt with. In the end the device will be cheaper to make which is more of interest to a potential customer compared to having an overengineered front panel that contains more parts that can fail. Besides that, -as I have shown before- it doesn't take much CPU power to sample an encoder at a decent speed that makes missing steps in normal operation (IOW: not turning the encoder with a Dremel at full speed) a non-issue. But yes, some people do have problems getting parallel processes to work well.

author: PCB.Wiz
author_rank: Super Contributor
timestamp: January 30, 2023, 11:58:59 pm
content: Quote from: ejeffrey on January 30, 2023, 10:07:20 pmEven then I've seen a "second pass" needed - the designer ran the tool bed velocity numbers fine, but forgot about real world site load and shock and whiplash effects. (operators can be rough)So it worked fine on the bench, but lost counts in the field, so he came to me.We used a PLD to solve it, tho I think the newest LSI Quad counters have upped their MHz to be ok now too.

author: artag
author_rank: Super Contributor
timestamp: January 31, 2023, 12:07:32 am
content: Quote from: paulca on January 26, 2023, 11:49:11 amMy 3D printer does this. It's horrible. Pressing the control often generates an edge, resulting in the wrong thing being selected. Maybe it's just poor implementation or low quality encoder. The selection items are quite coarse, not a precise value so a deadzone at the start of a move, or a capture of the recent value just before click would improve it.My HP scope does it, but sparingly - only 2 of 14 encoders have pressbuttons and those are used with coarse selections, not vernier adjustments. It manages not to annoy me most of the time. I guess they did a bit more UX testing than the 3D printer people.Suggestions ?

author: nctnico
author_rank: Super Contributor
timestamp: January 31, 2023, 12:12:22 am
content: A seperate select button is an option but if you have proportional accelleration (here I go again, I know), you can use an encoder with a lower resolution. This makes it far less likely to turn the encoder enough to change a parameter while you press it.

author: SiliconWizard
author_rank: Super Contributor
timestamp: January 31, 2023, 12:18:19 am
content: Different encoders, different use cases, different approaches.Again if you use an encoder with detent with a purpose, which is usually to make one detent = 1 "step" from a UI POV, acceleration, or uneven steps, is an UI failure.Acceleration schemes can be useful in other cases and should not be used with encoders with detent, at least IMHO, for a lack of UI consistency.Obviously UI is kind of a lost art anyway, so everything goes.

author: nctnico
author_rank: Super Contributor
timestamp: January 31, 2023, 12:30:48 am
content: IMHO detents don't matter where it comes to accelleration or not. When turned slowly, you can expect 1 increment per click. When turned faster, anything goes. Who can keep track of the number of clicks when rotating a knob quickly anyway? The only differentiator for using encoders with detents is when these are combined with a select function. With a detent it is unlikely the encoder changes the selection when pressed. OTOH the software can be programmed in a way that the encoder needs to rotate two positions before the selection is changed (basically halving the resolution) to make it less likely to 'slip'.

author: RoGeorge
author_rank: Super Contributor
timestamp: January 31, 2023, 12:34:26 am
content: The contacts can be very noisy. It might be impossible to preserve the absolute position alignment without continuously estimating the expected duration of the quadrature pulses.

author: langwadt
author_rank: Super Contributor
timestamp: January 31, 2023, 12:35:00 am
content: Quote from: nctnico on January 31, 2023, 12:12:22 amI guess you could get clever and keep a history of positions and when pressed rollback movement that happened right before a press

author: SiliconWizard
author_rank: Super Contributor
timestamp: January 31, 2023, 12:39:46 am
content: Very noisy: that's why I usually implement an analog filter, just like what they recommend in 99.9% of encoders' datasheets. The rest if of course a good decoding - there are several ways of decoding and some are definitely worse than others.On a more general level, the key here is to test, test, test and make sure your implementation will be robust. There are many ways to skin the cat here, as long as it gets the job done.As a few have mentioned in this thread, as trivial as some make it sound, faulty implementations abound everywhere, so please test before releasing a product - and don't take "looks ok on my desk" as acceptable. Just my 2 cents.

author: Peabody
author_rank: Super Contributor
timestamp: January 31, 2023, 12:46:23 am
content: Quote from: PCB.Wiz on January 30, 2023, 08:24:31 pmI agree that when the supposedly stable switch is closed, but actually has two conductive surfaces dragging against each other, there can be noise. But I've tested my code on some KY-040 encoder modules, which are just cheap stuff from the Far East, and it works quite well with them. They aren't perfect, but errors are pretty rare. And if those modules work ok, then a good encoder ought to be pretty foolproof. If an encoder is noisy enough, it isn't going to work with any software or hardware because at some point you just can't distinguish noise from legitimate transitions. But in my experience a reasonably well-behaved encoder gives satisfactory performance when using a good servicing routine, whether with interrupts or polling.QuoteIf you have the budget and board space for the hardware part, that's fine. But I think for most uses software alone works well enough. Maybe it's just that my expectations for a mechanical encoder are just not as high as others'. If you spin the knob as fast as you can, how do you know whether it missed some detents or not, and why would you care? If you insist that you should always get the same value when you return the knob to the same place, no matter how you abuse it, I think that's asking too much of a mechanical rotary encoder. It should just give good results if you turn it at a reasonable speed, with infrequent misses.

author: NorthGuy
author_rank: Super Contributor
timestamp: January 31, 2023, 02:12:24 am
content: Quote from: RoGeorge on January 31, 2023, 12:34:26 amLooking at the pictures, all the noise is in the low state. I guess there's a weak pull-down keeping it low. If so, a stronger pull-down would eliminate the noise.

author: RoGeorge
author_rank: Super Contributor
timestamp: January 31, 2023, 03:10:48 am
content: Low for that encoder was when a contact is closed. When the contact is open, there is a 10k pull up resistor. The noise during a low level means the contact is not firmly closed to GND.That is visible when the encoder is turning very, very fast. That model has 20 clicks per full turn and did them in about 25ms (it was turning at 2400 RPM).

author: pqass
author_rank: Super Contributor
timestamp: January 31, 2023, 03:36:23 am
content: If anyone is interested...here is my final version of my rotary encoder debouncer code. It's fairly simple and works well. It can sense turning speed such that faster turns change by 5 steps instead of 1. It only needs a 1KHz timer interrupt (no pin change interrupts) to support as many encoders as you'll want; just add an encoders[] array element and call UpdateEncoder() with your encoder switch-MCU pin mapping in the timer interrupt.Athough I had only one physical encoder in this test, I used its data 10 times as if 10 encoders existed (with the same switch changes occurring). It consumes worst case 1.75% of available MCU time per encoder or 17.5% for 10 encoders (ATMega328@16MHz). I'm sure it'll go unnoticed on >100MHz ARM MCU.See the waveforms below for the overall and zoomed in views. YELLOW/RED are the debounced quadrature output which shows that it can keep up with 2.7 turns/second (15ms pulse period on a 24 pulse/rev encoder). Zoomed-in it shows the BLUE in-interrupt time with GREEN blips indicating when the an encoder's current value integer has changed (in responce to cw/ccw rotation).Code:/** Encoder Debounce Test** Debounce testing of 10 rotary encoders (with momentary push button).* Each encoder maintains a .value integer which is inc/decremented (turned cw/ccw).* We sense turning speed such that faster turns change .value by 5 instead of 1.* Using a 1KHz timer interrupt and no pin change interrupts, polling and debouncing all 10 encoder pins* has a worst case load of 17.5% of MCU time on a 16MHz ATMega328 (1.75% per encoder).* Idle load (no switch changes) is 14.1%.* The encoder (Bourns PEC11-4015K-S0024) used in this test was mechanical with 24 pulses per rotation,* momentary push switch, and no detents.*** Author: PQASS* Date: Jan 30, 2023** Hardware:* ATmega328; Arduino UNO with 16MHz xtal* output pins: PB5, PB4, PB3, PB2; these are optional for debugging (viewing via o-scope). they represent:* one debounced switch state, the other debounced switch state,* in-interrupt indicator, when encoder value is changed indicator, respectively.* input pins: PD7, PD6, PD5 with internal pullup to Vcc;* encoder switches connecting to ground when rotated or momentary is pressed.* we only have one physical encoder in this test but we use the same data* 10 times as if 10 encoders existed (with the same switch changes occurring).* multiplexing/routing 10 real encoders is left as an exercise for the diligent student.** IDE Tools menu settings:* Board: "Arduino/Genuino UNO 16Mhz"* Port: "/dev/ttyACM0"* Programmer: via optiboot bootloader* Fuses: default for Arduino UNO*/#define DEBUG 1void timer_setup() {cli();TCCR1A = 0;TCCR1B = 0;// 16000000 / <prescaler> = x then set OCR1A = x / <desired Hz>OCR1A = 14; // 15624=1s 78=4.99ms 7=448us 14=896us with 1024 prescalerTCNT1H = 0; // must clear count otherwise we'll wait till it overflows before the first interrupt will occur!TCNT1L = 0;TCCR1B |= _BV(WGM12); // turn on CTC mode// Set CS10 and CS12 bits for 1024 prescalerTCCR1B |= _BV(CS10); // set CS10TCCR1B &= ~_BV(CS11); // clear CS11TCCR1B |= _BV(CS12); // set CS12TIMSK1 |= _BV(OCIE1A); // enable timer compare interruptsei();}// see bottom of this page: http://www.ganssle.com/debouncing-pt2.htm// state is changed only after 3.6ms passes (4 checks * 900us timer interval) after the last time the switch changed state.bool DebouncePin(uint8_t current_pin_state, volatile uint8_t *tmpp, volatile uint8_t *statep) {uint8_t hasChanged = false;*tmpp = *tmpp << 1 | (current_pin_state & 0x01);if ((*tmpp|0xf0) == 0xff) {if (*statep != HIGH) {*statep = HIGH;hasChanged = true;}} else if ((*tmpp&0x0f) == 0x00) {if (*statep != LOW) {*statep = LOW;hasChanged = true;}}return hasChanged;}// tickle pins to signal ep->value has changed, "in-interrupt" indicator.#ifdef DEBUG#define CHANGE_IND PORTB |= _BV(PB2); PORTB &= ~_BV(PB2)#define ENTER_INTR_IND PORTB |= _BV(PB3)#define LEAVE_INTR_IND PORTB &= ~_BV(PB3)#else /* DEBUG */#define CHANGE_IND#define ENTER_INTR_IND#define LEAVE_INTR_IND#endif /* DEBUG */struct debounce_t {uint8_t tmp; // holds last n states of bouncing switch (most current in lsb)uint8_t state; // debounced state of switch};struct encoder_t {struct debounce_t sw; // encoder pinA sw, pinB sw, and momentary push buttonuint16_t value; // cw debounced state increments, ccw debounced state decrementsunsigned long chgms; // last time (in millis()) that .value was changed};static volatile struct encoder_t encoders[] = {// add one line per [3 switch] encoder; .state initialized to inactive state (pulledup to Vcc).{ .sw = { { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH } }, .value = 0, .chgms = 0 },{ .sw = { { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH } }, .value = 0, .chgms = 0 },{ .sw = { { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH } }, .value = 0, .chgms = 0 },{ .sw = { { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH } }, .value = 0, .chgms = 0 },{ .sw = { { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH } }, .value = 0, .chgms = 0 },{ .sw = { { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH } }, .value = 0, .chgms = 0 },{ .sw = { { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH } }, .value = 0, .chgms = 0 },{ .sw = { { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH } }, .value = 0, .chgms = 0 },{ .sw = { { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH } }, .value = 0, .chgms = 0 },{ .sw = { { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH } }, .value = 0, .chgms = 0 }};void UpdateEncoder (uint8_t sw0, uint8_t sw1, uint8_t sw2, unsigned long currms, volatile struct encoder_t *ep) {volatile uint8_t *tp0 = &(ep->sw.tmp);volatile uint8_t *sp0 = &(ep->sw.state);volatile uint8_t *tp1 = &(ep->sw.tmp);volatile uint8_t *sp1 = &(ep->sw.state);volatile uint8_t *tp2 = &(ep->sw.tmp);volatile uint8_t *sp2 = &(ep->sw.state);if (DebouncePin(sw0, tp0, sp0)) {if (*sp0 == HIGH && *sp1 == LOW ) {// fast turns change value by 5 instead of 1.if ((currms - ep->chgms) < 30) ep->value+=5; else ep->value++;ep->chgms = currms;CHANGE_IND;}}if (DebouncePin(sw1, tp1, sp1)) {if (*sp0 == LOW && *sp1 == HIGH) {// fast turns change value by 5 instead of 1.if ((currms - ep->chgms) < 30) ep->value-=5; else ep->value--;ep->chgms = currms;CHANGE_IND;}}DebouncePin(sw2, tp2, sp2); // momentary}ISR(TIMER1_COMPA_vect) { // called 1100 times per second; every 900us; see timer_setup()ENTER_INTR_IND;unsigned long currms = millis();uint8_t data = PIND;// 'scope readings; worst case duration of interrupt (every 1040Hz)// processing one switch change for each of the 10 encoders being updated// 113us when no switch was changed (w/o speed sensing). 113us*1040Hz*100 = 11.7% of MCU time used// 131us when a switch per encoder was changed (w/o speed sensing). 131us*1040Hz*100 = 13.6% of MCU time used// 12.8us processing time per encoder + 3us interrupt call overhead.// 136us when no switch was changed (with speed sensing). 136us*1040Hz*100 = 14.1% of MCU time used// 169us when a switch per encoder was changed (with speed sensing). 169us*1040Hz*100 = 17.5% of MCU time used// 16.4us processing time per encoder + 5us interrupt call overhead.// normally, you'd get the data from different pins// or multiplex 5 "rows" of 2 [3 switch] encoders on the same input port (or any other mapping).// but we only have one physical encoder in this test so we use the same data// 10 times as if 10 encoders existed (with the same switch changes occurring).UpdateEncoder(data>>PD7, data>>PD6, data>>PD5, currms, &encoders);UpdateEncoder(data>>PD7, data>>PD6, data>>PD5, currms, &encoders);UpdateEncoder(data>>PD7, data>>PD6, data>>PD5, currms, &encoders);UpdateEncoder(data>>PD7, data>>PD6, data>>PD5, currms, &encoders);UpdateEncoder(data>>PD7, data>>PD6, data>>PD5, currms, &encoders);UpdateEncoder(data>>PD7, data>>PD6, data>>PD5, currms, &encoders);UpdateEncoder(data>>PD7, data>>PD6, data>>PD5, currms, &encoders);UpdateEncoder(data>>PD7, data>>PD6, data>>PD5, currms, &encoders);UpdateEncoder(data>>PD7, data>>PD6, data>>PD5, currms, &encoders);UpdateEncoder(data>>PD7, data>>PD6, data>>PD5, currms, &encoders);LEAVE_INTR_IND;}void setup() {DDRD &= ~(_BV(PD7)|_BV(PD6)|_BV(PD5)); // enable input on encoder pins.PORTD |= _BV(PD7)|_BV(PD6)|_BV(PD5); // enable internal pull-up on input pins.#ifdef DEBUGDDRB |= _BV(PB5); // enable output on LED pin (Arduino pin 13).PORTB &= ~_BV(PB5); // turn off LED. LED shows debounced rotary encoder movement.DDRB |= _BV(PB4); // enable output on another pin for the other encoder switch.PORTB &= ~_BV(PB4); // turn off pin. pin shows debounced rotary encoder movement.DDRB |= _BV(PB3); // enable output.PORTB &= ~_BV(PB3); // default is cleared. shows when control is in the timer interrupt routine.DDRB |= _BV(PB2); // enable output.PORTB &= ~_BV(PB2); // default is cleared. shows when control has changed .value.#endif /* DEBUG */Serial.begin(38400);timer_setup(); // should be last to setup since it starts the timer interrupt.}unsigned long prevMillis = 0;unsigned long currMillis = 0;void loop() {volatile struct encoder_t *ep = &encoders;#ifdef DEBUG// check switch state var anywhere in your code.// it reflects the debounced state of one encoder switch at any moment.// but you don't really need this as your code would just use .value.// it's really only useful to check momentary switches' state.if (ep->sw.state == HIGH) {PORTB |= _BV(PB5); // turn on an LED as a response to the debounced switch.} else if (ep->sw.state == LOW) {PORTB &= ~_BV(PB5); // turn off an LED as a response to the debounced switch.}if (ep->sw.state == HIGH) {PORTB |= _BV(PB4); // turn on an LED as a response to the debounced switch.} else if (ep->sw.state == LOW) {PORTB &= ~_BV(PB4); // turn off an LED as a response to the debounced switch.}#endif /* DEBUG */currMillis = millis();if (currMillis > (prevMillis + 250)) { // print quantity value and momentary sw status every quarter-secondprevMillis = currMillis;Serial.print(ep->value);if (ep->sw.state == HIGH) {Serial.println(" r");} else if (ep->sw.state == LOW) {Serial.println(" p");}}}