MCP23017 Encoder/Button test in C++

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!

3 Likes