A while back I was trying to figure out how to make a Daisy Seed work with a MCP23017 chip with a single KY040 push-encoder.
After putting the project down for many months, I came back to it and was finally able to stand up a fairly solid toy that’s able to read a quadrature encoder, increment/decrement a value from its direction, read the encoder pushbutton switch, and light up a blue LED connected to the MCP’s GPIOB_7 pin when the button is pressed. It will support multiple encoders/buttons, but I ran out of room on the breadboard. To help anyone else who may be baffled by the MCP23017 datasheet and the dearth of C++ examples for this particular combo, I’m posting my code, as well as my hack/port of Ben Buxton’s Rotary library for Arduino.
Rotary - QuadState.h
#define quadstate_h
#include "daisy.h"
#include "daisy_seed.h"
#include "daisysp.h"
// Enable this to emit codes twice per step.
#define HALF_STEP
// Values returned by 'process'
// No complete step yet.
#define DIR_NONE 0x0
// Clockwise step.
#define DIR_CW 0x10
// Anti-clockwise step.
#define DIR_CCW 0x20
class QuadState
{
public:
QuadState();
uint8_t process(uint8_t, uint8_t);
private:
uint8_t state;
};
#endif
QuadState.cpp:
/* Quadrature state handler for Daisy Seed, ported from Ben Buxton's
* Rotary Arduino library.
*
* Licenced under the GNU GPL Version 3.
* Original header info:
*
* A typical mechanical rotary encoder emits a two bit gray code
* on 3 output pins. Every step in the output (often accompanied
* by a physical 'click') generates a specific sequence of output
* codes on the pins.
*
* There are 3 pins used for the rotary encoding - one common and
* two 'bit' pins.
*
* The following is the typical sequence of code on the output when
* moving from one step to the next:
*
* Position Bit1 Bit2
* ----------------------
* Step1 0 0
* 1/4 1 0
* 1/2 1 1
* 3/4 0 1
* Step2 0 0
*
* From this table, we can see that when moving from one 'click' to
* the next, there are 4 changes in the output code.
*
* - From an initial 0 - 0, Bit1 goes high, Bit0 stays low.
* - Then both bits are high, halfway through the step.
* - Then Bit1 goes low, but Bit2 stays high.
* - Finally at the end of the step, both bits return to 0.
*
* Detecting the direction is easy - the table simply goes in the other
* direction (read up instead of down).
*
* To decode this, we use a simple state machine. Every time the output
* code changes, it follows state, until finally a full steps worth of
* code is received (in the correct order). At the final 0-0, it returns
* a value indicating a step in one direction or the other.
*
* It's also possible to use 'half-step' mode. This just emits an event
* at both the 0-0 and 1-1 positions. This might be useful for some
* encoders where you want to detect all positions.
*
* If an invalid state happens (for example we go from '0-1' straight
* to '1-0'), the state machine resets to the start until 0-0 and the
* next valid codes occur.
*
* The biggest advantage of using a state machine over other algorithms
* is that this has inherent debounce built in. Other algorithms emit spurious
* output with switch bounce, but this one will simply flip between
* sub-states until the bounce settles, then continue along the state
* machine.
* A side effect of debounce is that fast rotations can cause steps to
* be skipped. By not requiring debounce, fast rotations can be accurately
* measured.
* Another advantage is the ability to properly handle bad state, such
* as due to EMI, etc.
* It is also a lot simpler than others - a static state table and less
* than 10 lines of logic.
*/
#include "QuadState.h"
/*
* The below state table has, for each state (row), the new state
* to set based on the next encoder output. From left to right in,
* the table, the encoder outputs are 00, 01, 10, 11, and the value
* in that position is the new state to set.
*/
#define R_START 0x0
#ifdef HALF_STEP
// Use the half-step state table (emits a code at 00 and 11)
#define R_CCW_BEGIN 0x1
#define R_CW_BEGIN 0x2
#define R_START_M 0x3
#define R_CW_BEGIN_M 0x4
#define R_CCW_BEGIN_M 0x5
const uint8_t ttable[6][4] = {
// R_START (00)
{R_START_M, R_CW_BEGIN, R_CCW_BEGIN, R_START},
// R_CCW_BEGIN
{R_START_M | DIR_CCW, R_START, R_CCW_BEGIN, R_START},
// R_CW_BEGIN
{R_START_M | DIR_CW, R_CW_BEGIN, R_START, R_START},
// R_START_M (11)
{R_START_M, R_CCW_BEGIN_M, R_CW_BEGIN_M, R_START},
// R_CW_BEGIN_M
{R_START_M, R_START_M, R_CW_BEGIN_M, R_START | DIR_CW},
// R_CCW_BEGIN_M
{R_START_M, R_CCW_BEGIN_M, R_START_M, R_START | DIR_CCW},
};
#else
// Use the full-step state table (emits a code at 00 only)
#define R_CW_FINAL 0x1
#define R_CW_BEGIN 0x2
#define R_CW_NEXT 0x3
#define R_CCW_BEGIN 0x4
#define R_CCW_FINAL 0x5
#define R_CCW_NEXT 0x6
using namespace daisy;
const uint8_t ttable[7][4] = {
// R_START
{R_START, R_CW_BEGIN, R_CCW_BEGIN, R_START},
// R_CW_FINAL
{R_CW_NEXT, R_START, R_CW_FINAL, R_START | DIR_CW},
// R_CW_BEGIN
{R_CW_NEXT, R_CW_BEGIN, R_START, R_START},
// R_CW_NEXT
{R_CW_NEXT, R_CW_BEGIN, R_CW_FINAL, R_START},
// R_CCW_BEGIN
{R_CCW_NEXT, R_START, R_CCW_BEGIN, R_START},
// R_CCW_FINAL
{R_CCW_NEXT, R_CCW_FINAL, R_START, R_START | DIR_CCW},
// R_CCW_NEXT
{R_CCW_NEXT, R_CCW_FINAL, R_CCW_BEGIN, R_START},
};
#endif
QuadState::QuadState() {
state = R_START;
}
uint8_t QuadState::process(uint8_t _pin1, uint8_t _pin2) {
uint8_t pinstate = (_pin2) << 1 | _pin1;
// Determine new state from the pins and state table.
state = ttable[state & 0xf][pinstate];
// Return emit bits, ie the generated event.
return state & 0x30;
};
Daisy Seed FW - Mcp1Enc1Btn.cpp
/*
Simple MCP23017 encoder/button read test using KY040 clones
with onboard pullups. QuadState is a port of Ben Buxton's
quadrature library for Arduino.
*/
#include "daisy.h"
#include "daisy_seed.h"
#include "daisysp.h"
#include "QuadState.cpp"
using namespace daisy;
using namespace daisysp;
using namespace daisy::seed;
DaisySeed hw;
Mcp23017 mcp;
// Local namespace instead of static or global variables.
namespace lvars {
uint8_t port_val0;
uint8_t port_val1;
int count [4] = {0};
uint8_t btn_state [4] = {0};
uint8_t enc_state[4] = {0};
// uint8_t writeA = 0x0;
// uint8_t writeB = 0x0;
bool led_state; // onboard
bool blue_led_state;
const int num_encs = 4;
uint8_t led_pin = 7;
// uint8_t rot_led_pin = 6;
QuadState r1 [4];
// Cribbed from another forum post. This works, at least to the point where
// I see what looks like I2C waveforms for SCL and SDA on my cheap oscilloscope.
I2CHandle::Config i2c_config
= {I2CHandle::Config::Peripheral::I2C_1,
{{DSY_GPIOB, 8}, {DSY_GPIOB, 9}},
I2CHandle::Config::Speed::I2C_100KHZ, //3.3v does mushy square waves above this.
I2CHandle::Config::Mode::I2C_MASTER};
// The address param is set to 0x20, with A0, A1, and A2 pins
// on the MCP pulled low on the breadboard. The MCP23017 class
// defaults to 0x27 on a bare Init().
Mcp23017::Config config {i2c_config, 0b100000}; //was constexpr
}
using namespace lvars;
void AudioCallback(AudioHandle::InputBuffer in, AudioHandle::OutputBuffer out, size_t size)
{
for (size_t i = 0; i < size; i++)
{
out[0][i] = in[0][i];
out[1][i] = in[1][i];
}
}
void on_count_up(uint8_t control_num)
{
//perform the increment actions
count[control_num]++;
}
void on_count_down(uint8_t control_num)
{
// perform the decrement actions
count[control_num]--;
}
//Timer callback - Can I do control reads here?
void EncoderTimerCallback(void* data)
{
// Toggle LED based on encoder state.
// Read GPA_[n] and GPA_[n+1] to get encoder direction. We read the whole port at once, then
// work with individual bit values. Makes more sense with many encoders/buttons.
// Read the button port here as well. Only one encoder and one button for now.
uint8_t cur_port_val0 = mcp.ReadPort(MCPPort::A); // encoders
uint8_t cur_port_val1 = mcp.ReadPort(MCPPort::B); // buttons.
// Rotary encoders. Look for change in port state to trigger process.
if ( cur_port_val0 != port_val0 ) {
for (uint8_t i = 0; i < 4; i++ ) {
uint8_t p0 = (port_val0 >> (i*2)) & uint8_t(1);
uint8_t p1 = (port_val0 >> ((i*2) + 1)) & uint8_t(1);
uint8_t enc_current = r1[i].process (p0, p1);
if (enc_current != 0) {
if (((enc_current >> 4 ) & 1) == 1)
on_count_up(i);
else
on_count_down(i);
led_state = !led_state;
Logger<LOGGER_INTERNAL>::PrintLine("State: 0x%x, p0: 0x%x, p1: 0x%x, Count[%i]: %i", enc_state[i], p0, p1, i, count[i]);
hw.SetLed(led_state);
}
}
port_val0 = cur_port_val0;
}
// Buttons
if (cur_port_val1 != port_val1) {
for (uint8_t i = 0; i < 1; i++) {
btn_state[i] = (cur_port_val1 >> (i+1) ) & uint8_t(1);
blue_led_state = !btn_state[i]; // Push is off, release is on.
mcp.WritePin(led_pin, (uint8_t)blue_led_state);
}
Logger<LOGGER_INTERNAL>::PrintLine("Buttons-- state1: 0x%x, state2: 0x%x, state3: 0x%x", btn_state[0],btn_state[1],btn_state[2]);
port_val1 = cur_port_val1;
}
}
int main(void)
{
hw.Init();
System::Delay(100);
GPIO McpReset;
GPIO Mcp3_3v;
McpReset.Init(D13, GPIO::Mode::INPUT);
Mcp3_3v.Init(D14, GPIO::Mode::OUTPUT);
System::Delay(100);
led_state = false;
hw.SetLed(led_state);
// Create encoder states
for (auto i = 0x0; i < 0x4; i++) r1[i] = QuadState();
// Reset MCP. Hold high from Daisy GPIO
Mcp3_3v.Write(0);
System::Delay(30); // Hold low for MCP datasheet 25ms minimum and then some.
Mcp3_3v.Write(1); // Stays high until reset. Could hardwire? Nah.
hw.StartLog(true);
System::Delay(50);
mcp.Init(config);
// Setting ports, etc must be done AFTER init.
System::Delay(50);
//0x00 is all input mode, 0xff is all output. Dir/pullup//invert, GPA7 is output only
mcp.PortMode(MCPPort::A,0x07, 0x00, 0x07); // Invert doesn't do anything.
// mcp.PinMode(led_pin , MCPMode::OUTPUT, true); //This doesn't do anything either.
mcp.WritePin(led_pin, (uint8_t)false);
port_val0 = mcp.ReadPort(MCPPort::A);
port_val1 = mcp.ReadPort(MCPPort::B);
// Encoder and button read timer.
TimerHandle tim_enc;
auto tim_base_freq = System::GetPClk2Freq();
Logger<LOGGER_INTERNAL>::PrintLine("tim_enc time base freq: %i, ",tim_base_freq);
System::Delay(10);
TimerHandle::Config enc_tim_cfg;
enc_tim_cfg.periph = TimerHandle::Config::Peripheral::TIM_5;
enc_tim_cfg.enable_irq = true;
auto enc_target_freq = 10000; // Fast enough, but not too fast
enc_tim_cfg.period = (tim_base_freq / 100000000) * enc_target_freq;
System::Delay(10);
// Initialze timer and input callback.
tim_enc.Init(enc_tim_cfg);
tim_enc.SetCallback(EncoderTimerCallback);
// Start the timer.
tim_enc.Start();
for(;;)
{
// Loop forever.
}
};
Hopefully this helps someone. Cheers!