Skip to content

Commit 9603bb1

Browse files
author
Hendry Kaak
committed
Handle menu encoder with the Attiny841
The menu encoder is currently handled by the mainboard in place of locally with the Attiny841 MCU, which is not exactly ideal. The code here should be the first part of the code migration to the Attiny841, the second part is to be done on the mainboard which should process the new `GET_STATUS` command.
1 parent dd50fd5 commit 9603bb1

4 files changed

Lines changed: 254 additions & 5 deletions

File tree

ButtonEncoder.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#include "ButtonEncoder.h"
2+
#include "Arduino.h"
3+
#include "Hardware.h"
4+
5+
template <>
6+
volatile int8_t ButtonEncoder<ENC_SW, ENC_A, ENC_B>::encoder_detents = 0;
7+
template <>
8+
volatile uint8_t ButtonEncoder<ENC_SW, ENC_A, ENC_B>::button_presses = 0;
9+
10+
ISR(PCINT0_vect) {
11+
ButtonEncoder<ENC_SW, ENC_A, ENC_B>::button_isr();
12+
}
13+
14+
ISR(PCINT1_vect) {
15+
ButtonEncoder<ENC_SW, ENC_A, ENC_B>::encoder_isr();
16+
}

ButtonEncoder.h

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
#pragma once
2+
3+
#include "util/atomic.h"
4+
#include "Arduino.h"
5+
#include "Hardware.h"
6+
7+
/**
8+
* This class handles events from a rotary encoder with a combined
9+
* button, assuming external pullups are present on all pins.
10+
*
11+
* The encoder used has both outputs high when idle (at a detent), and
12+
* switches through a full cycle (both outputs go low and high again)
13+
* for each detent moved.
14+
*
15+
* When moving clock-wise, output A goes low before output B, and when
16+
* moving counter-clockwise, output B goes low before output A (and they
17+
* go high again in the same order).
18+
*
19+
* The moment you feel the knob "click" into the next detent matches the
20+
* moment where both outputs are high again. This is the moment where
21+
* events should be triggered.
22+
*
23+
* Clockwise: Counterclockwise:
24+
* __. ._____ ____. ._____
25+
* A |___| A |___|
26+
* ____. .___ __. .___
27+
* B |___| B |___|
28+
* ^ ^
29+
* Event here Event here
30+
*
31+
* To accurately catch these events, this class registers interrupt
32+
* handlers on both pins. It seems sufficient to only trigger on rising
33+
* edges to catch the above events, but then spurious events will be
34+
* triggered when the encoder is turned slightly past a detent and back
35+
* again (the forward movement will be missed, while the movement back
36+
* will trigger a spurious event).
37+
*
38+
* Hence, this class monitors both edges on both pins, and keeps track
39+
* of the exact value of the encoder (where each edge adds or subtracts
40+
* one to the value kept). Whenever the value is divisible by 4, the
41+
* encoder is at a detent.
42+
*
43+
* For tracking the encoder value, a lookup table approach is used. This
44+
* is based on the approach described (among other places) here:
45+
* https://www.circuitsathome.com/mcu/reading-rotary-encoder-on-arduino
46+
*
47+
* This uses a lookup table that maps a combination of the previous pin
48+
* states and the current pin states to the change in value that this
49+
* should result in. However, the lookup table used here is slightly
50+
* modified from the one commonly used.
51+
*
52+
* The original table contains 1 to indicate clockwise motion, -1 to
53+
* indicate counter-clockwise motion, 0 to indicate no motion (pin
54+
* values unchanged ) and also 0 to indicate an invalid transition (e.g.
55+
* both pins changed, which should never happen in quadrature encoding).
56+
*
57+
* The table used here, uses 2 and -2 for the invalid transitions
58+
* instead of 0. Even though these should never occur, it is not
59+
* unthinkable that bouncing or noise will very rarely cause them to
60+
* happen anyway. With the original table, these transitions did not
61+
* modify the value, but did modify the "previous pin state" that was
62+
* stored. Effectively, this could cause the tracked value to get an
63+
* off-by-two error relative to the actual position. Effectively, these
64+
* invalid transitions mean the encoder position has changed by 2
65+
* instead of 1, though there is no way to tell which way the encoder
66+
* has turned.
67+
*
68+
* For this reason, the lookup table contains 2 (and -2, to allow any
69+
* errors to possibly even out) for these invalid transitions. This will
70+
* keep the value accurate in some cases, but since we're just making a
71+
* guess as to the direction, it will by off-by-four in other cases.
72+
* Even though not perfectly accurate, now at least the assumption that
73+
* the tracked value is a multiple of four will remain valid, regardless
74+
* of what values are read.
75+
*
76+
* This approach means that the reading is pretty resilient against
77+
* invalid readings. When there is an invalid reading on a single pin,
78+
* the value might be temporary off-by-one, but this will be corrected
79+
* when the reading is correct again. Since normally, only one pin
80+
* should change at a time, this means that any bouncing of the
81+
* encoder's contacts should be handled without any problems by this
82+
* code.
83+
*
84+
* When both pins return an invalid reading, the absolute position might
85+
* get off-by-four errors, but the position within a single cycle should
86+
* remain correct, meaning detents are still detected correctly.
87+
*/
88+
89+
void assert_interrupt_pin();
90+
void clear_interrupt_pin();
91+
92+
template <uint8_t button_pin, uint8_t pina, uint8_t pinb>
93+
class ButtonEncoder {
94+
public:
95+
void setup(void) {
96+
// These have external pullups
97+
pinMode(button_pin, INPUT_PULLUP /* TEMPORARY with PULLUP! */);
98+
pinMode(pina, INPUT);
99+
pinMode(pinb, INPUT);
100+
101+
// Set Pin Change Interrupts Masks that are used for the encoder
102+
PCMSK0 = _BV(PCINT5);
103+
PCMSK1 = _BV(PCINT10);
104+
105+
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
106+
// Be sure that every interrupt has been set, including all the pin change interrupts
107+
GIMSK |= (_BV(PCIE1) | _BV(PCIE0));
108+
109+
attachInterrupt(digitalPinToInterrupt(pinb), encoder_isr, CHANGE);
110+
111+
// Reset any queued events. attachInterrupt does not clear any
112+
// previously pending interrupts (e.g. due to startup noise, or
113+
// due to different interrupt settings), so these might trigger
114+
// right away. Clear away the result of that. See also
115+
// https://github.com/arduino/Arduino/issues/510
116+
GIFR |= (_BV(INTF0) | _BV(PCIF1) | _BV(PCIF0));
117+
}
118+
}
119+
120+
int8_t process_encoder(void) {
121+
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
122+
int8_t ret = encoder_detents;
123+
encoder_detents = 0;
124+
return ret;
125+
}
126+
return 0; // Not reached, but keeps compiler happy
127+
}
128+
129+
uint8_t process_button() {
130+
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
131+
uint8_t ret = button_presses;
132+
button_presses = 0;
133+
return ret;
134+
}
135+
return 0; // Not reached, but keeps compiler happy
136+
}
137+
138+
// Interrupt routine functions
139+
static void button_isr(void) __attribute__((__always_inline__));
140+
static void encoder_isr(void) __attribute__((__always_inline__));
141+
142+
static volatile uint8_t button_presses;
143+
static volatile int8_t encoder_detents;
144+
};
145+
146+
template <uint8_t button_pin, uint8_t pina, uint8_t pinb>
147+
inline void ButtonEncoder<button_pin, pina, pinb>::button_isr(void) {
148+
static bool previous_state = true;
149+
150+
// No need to debounce, since hardware filtering is used
151+
bool button_state = digitalRead(button_pin);
152+
153+
// Falling edge detection
154+
if(previous_state && !button_state) {
155+
button_presses++;
156+
assert_interrupt_pin();
157+
}
158+
previous_state = button_state;
159+
}
160+
161+
template <uint8_t button_pin, uint8_t pina, uint8_t pinb>
162+
inline void ButtonEncoder<button_pin, pina, pinb>::encoder_isr(void) {
163+
// Initialize to 3, assuming the encoder starts at a detent. This
164+
// matches the initial value of `value`, so even if it is
165+
// incorrect, it should resynchronise on the first interrupt.
166+
static uint8_t prev_reading = 3;
167+
static int8_t encoder_value = 0;
168+
169+
// This lookup table maps clockwise rotation to positive changes.
170+
static const int8_t table[] PROGMEM = { 0, -1, 1, 2, 1, 0, 2, -1, -1, -2, 0, 1, -2, 1, -1, 0 };
171+
uint8_t reading = digitalRead(pina) << 1 | digitalRead(pinb);
172+
uint8_t key = prev_reading << 2 | reading;
173+
encoder_value += pgm_read_byte(&table[key]);
174+
prev_reading = reading;
175+
176+
// Generate an event whenever the encoder has moved a full
177+
// detent (value changed by 4).
178+
if (encoder_value >= 4) {
179+
++encoder_detents;
180+
encoder_value -= 4;
181+
assert_interrupt_pin();
182+
} else if (encoder_value <= -4) {
183+
--encoder_detents;
184+
encoder_value += 4;
185+
assert_interrupt_pin();
186+
}
187+
}

Hardware.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ static const uint8_t H_Led = PIN_A7;
2222
static const uint8_t H_Sens = PIN_A1;
2323
static const uint8_t H_Sens_ADC_Channel = 1;
2424
static_assert(analogInputToDigitalPin(H_Sens_ADC_Channel) == H_Sens, "Hopper sensor ADC channel mismatch");
25-
static const uint8_t H_Out = PIN_A0;
25+
static const uint8_t STATUS_PIN = PIN_A0;
2626

2727
static const uint8_t EN_Boost = PIN_A3;
2828
static const uint8_t EN_3V3 = PIN_A2;

Main.cpp

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,30 @@
2020
#include "TwoWire.h"
2121
#include "BaseProtocol.h"
2222
#include <util/atomic.h>
23+
#include "ButtonEncoder.h"
2324

2425
//#define ENABLE_SERIAL
2526

27+
static bool hopper_empty = false;
2628
const uint16_t hopper_threshold = 20;
2729
uint16_t measurement[2];
30+
ButtonEncoder<ENC_SW, ENC_A, ENC_B> encoder;
2831

2932
struct Commands {
3033
enum {
3134
GET_LAST_MEASUREMENT = 0x80,
35+
GET_LAST_STATUS = 0x81,
3236
};
3337
};
3438

39+
void assert_interrupt_pin() {
40+
digitalWrite(STATUS_PIN, HIGH);
41+
}
42+
43+
void clear_interrupt_pin() {
44+
digitalWrite(STATUS_PIN, LOW);
45+
}
46+
3547
cmd_result processCommand(uint8_t cmd, uint8_t * /*datain*/, uint8_t len, uint8_t *dataout, uint8_t maxLen) {
3648
switch (cmd) {
3749
case Commands::GET_LAST_MEASUREMENT: {
@@ -43,6 +55,28 @@ cmd_result processCommand(uint8_t cmd, uint8_t * /*datain*/, uint8_t len, uint8_
4355
dataout[3] = measurement[1];
4456
return cmd_result(Status::COMMAND_OK, 4);
4557
}
58+
case Commands::GET_LAST_STATUS: {
59+
if (len != 0 || maxLen < 2)
60+
return cmd_result(Status::INVALID_ARGUMENTS);
61+
62+
// Note that we run inside an I2c interrupt, so there is no race condition here
63+
clear_interrupt_pin();
64+
65+
// Process the encoder result and keep the hopper sensor bit cleared, just to be sure.
66+
uint8_t button_presses = encoder.process_button();
67+
int8_t encoder_detents = encoder.process_encoder();
68+
69+
// Truncate buttonpresses to keep bit 8 free for hopper sensor
70+
dataout[0] = min(0x7F, button_presses);
71+
72+
// Check if there is anything before the hopper sensor
73+
if (hopper_empty)
74+
dataout[0] |= 0x80;
75+
76+
dataout[1] = encoder_detents;
77+
78+
return cmd_result(Status::COMMAND_OK, 2);
79+
}
4680
default:
4781
return cmd_result(Status::COMMAND_NOT_SUPPORTED);
4882
}
@@ -96,10 +130,16 @@ void measure_hopper() {
96130

97131
// Lower reading means more light
98132
if (on < off && (off - on) > hopper_threshold)
99-
digitalWrite(H_Out, HOPPER_EMPTY);
133+
hopper_empty = HOPPER_FULL;
100134
else
101-
digitalWrite(H_Out, HOPPER_FULL);
102-
#endif
135+
hopper_empty = HOPPER_EMPTY;
136+
137+
static bool previous_hopper_empty = false;
138+
if(hopper_empty != previous_hopper_empty) {
139+
previous_hopper_empty = hopper_empty;
140+
assert_interrupt_pin();
141+
}
142+
#endif /*ENABLE_SERIAL*/
103143
}
104144

105145

@@ -110,13 +150,19 @@ void setup() {
110150
#endif
111151

112152
pinMode(H_Led, OUTPUT);
113-
pinMode(H_Out, OUTPUT);
153+
154+
pinMode(STATUS_PIN, OUTPUT);
155+
114156
#ifndef ENABLE_SERIAL // Serial reuses the H_sens pin
115157
pinMode(H_Sens, INPUT);
116158
#endif
117159

160+
pinMode(H_Sens_ADC_Channel, INPUT);
161+
118162
TwoWireInit(/* useInterrupts */ true, I2C_ADDRESS);
119163

164+
encoder.setup();
165+
120166
start_display();
121167
}
122168

0 commit comments

Comments
 (0)