Control-Surface icon indicating copy to clipboard operation
Control-Surface copied to clipboard

PCF8574 Digital I/O Port Expander (with toggle Buttons and KY-040 Digital Encoder)

Open nullscm opened this issue 5 years ago • 4 comments
trafficstars

Hi,

I have 6 toggle buttons and 6 KY-040 digital Encoder on the ports of 3 PCF8574 Port Expander. They are driven via i2C and have one Interrupt pin and 8 digital I/O Pins. I had already success via Wire to get a byte with the actual button states with the interrupt pin. (see example)

Is it possible to use this port expander with your library or maybe if not to use the byte that i get via wire to map it via your library to the toggle switches/encoders?

Thanks for your work!!

(example)

#define pcf1 0x20
#define pcf2 0x24
byte pcf1byte;
pinMode(INT_PIN, INPUT_PULLUP);

Wire.begin(); 
Wire.beginTransmission(pcf1); /
Wire.write(0xFF);     
Wire.endTransmission();
Wire.beginTransmission(pcf2); 
Wire.write(0xFF);  
Wire.endTransmission(); 
[...]
if (digitalRead(INT_PIN) == LOW) {
Wire.requestFrom(0x20, 1);    // request 1 bytes from PCF8574 at address 0x20  [output e.g: 11111010]
pcf1byte = Wire.read(); 
[...]

`

nullscm avatar Nov 14 '20 00:11 nullscm

If you want to use it with Control Surface's NoteButton classes etc., you'll have to implement the ExtendedIOElement interface for the PCF8574. You have to inherit from the ExtendedIOElement class and override and implement all pure virtual member functions, see https://github.com/tttapa/Control-Surface/blob/master/src/AH/Hardware/ExtendedInputOutput/ExtendedIOElement.hpp. Just provide implementations for the pinMode, digitalRead, digitalWrite methods, as well as initialization in the begin method. You can have a look at the multiplexer and shift register implementations in the same folder for some inspiration. I'm not sure what your experience level is, so if you need more help, just let me know.

This approach will not work for rotary encoders. Polling them through a port expander will probably be too slow, and the PJRC Encoder library which is used internally by Control Surface doesn't support the ExtendedIOElement interface.
Control Surface does support rotary encoders through MCP23017 expanders, see MCP23017Encoders.hpp, but you cannot mix buttons and encoders on the same expander.

You could of course also just read the port expanders yourself, in your main loop. You can then call Control_Surface.sendNoteOn(...) directly instead of using the NoteButton class, for example.

tttapa avatar Nov 14 '20 14:11 tttapa

Thanks for your time! Maybe you could give me more hints in the right direction how to implement he read and write functions the right way with your library that i mentionted above: Wire.requestFrom(0x20, 1);
pcf1byte = Wire.read();

Do i have to modify the library and build it from source to implement my own IOElement or can i do the override in my code?

nullscm avatar Nov 14 '20 15:11 nullscm

using namespace MIDI_Notes;
void setup() {
	pinMode(4, INPUT_PULLUP);
	Wire.begin();        
	Wire.beginTransmission(0x20); // transmit to PCF8574
	Wire.write(0xFF);      
	Wire.endTransmission(); // stop transmitting
	Wire.beginTransmission(0x24); // transmit to PCF8574
	Wire.write(0xFF);      
	Wire.endTransmission(); 
	Wire.beginTransmission(0x26); 
	Wire.write(0xFF);       
	Wire.endTransmission(); 
	Control_Surface.begin();
}
void loop() {
	if (digitalRead(4) == LOW) {
		char bytes[3];
		Wire.requestFrom(0x20, 1);    // request 1 bytes from PCF8574 at address 0x20
		bytes[0] = Wire.read();   // receive byte
		Wire.requestFrom(0x24, 1);
		bytes[1] = Wire.read();
		Wire.requestFrom(0x26, 1);
		bytes[2] = Wire.read();
		char bits[24];
		int i;
		for (i=0; i<sizeof(bytes)*8; i++) {
			bits[i] = ((1 << (i % 8)) & (bytes[i/8])) >> (i % 8);
		}
		compareBuffer(bits);
	}
	Control_Surface.loop();
}

void compareBuffer (char bits[24]){
	for (int i = 0; i<24; ++i) {
		if (bits[i] != bitbuffer[i]) {
			if (bits[i] == LOW) Control_Surface.sendCC({i, CHANNEL_1}, LOW);
			if (bits[i] == HIGH) Control_Surface.sendCC({i, CHANNEL_1}, HIGH);
			bitbuffer[i] = bits[i];
		}
	}}

This works for now but i think i have to debounce sth.. to integrate it further as an ExtendedIO i don't know how to pass the bits in my array to other elements as pins that they could read button states from.

Sorry since I am not a native English speaker, it is not easy to communicate this correctly.

nullscm avatar Nov 16 '20 12:11 nullscm

For the ExtendedIO integration, you could do something like this:

Header

class PCF8574 : public StaticSizeExtendedIOElement<8> {
  public:
    PCF8574(uint8_t address = 0x20, pin_t interruptPin = NO_PIN)
        : address(address), interruptPin(interruptPin) {}

    void pinModeBuffered(pin_t pin, PinMode_t mode) override;
    void digitalWriteBuffered(pin_t pin, PinStatus_t status) override;
    int digitalReadBuffered(pin_t pin) override;
    analog_t analogReadBuffered(pin_t pin) override;
    void analogWriteBuffered(pin_t, analog_t) override;

    void begin() override;

    void updateBufferedOutputs() override;
    void updateBufferedInputs() override;
    void updateBufferedPinModes();

  private:
    uint8_t address;
    pin_t interruptPin;

    bool pinModesDirty = true;
    BitArray<8> bufferedPinModes;
    bool pullupsDirty = true;
    BitArray<8> bufferedPullups;
    bool outputsDirty = true;
    BitArray<8> bufferedOutputs;
    uint8_t bufferedInputs = 0;
};

Implementation

oid PCF8574::pinModeBuffered(pin_t pin, PinMode_t mode) {
    if (mode == INPUT) {
        pinModesDirty |= bufferedPinModes.get(pin) == 1;
        pullupsDirty |= bufferedPullups.get(pin) == 1;
        bufferedPinModes.clear(pin);
        bufferedPullups.clear(pin);
    } else if (mode == OUTPUT) {
        pinModesDirty |= bufferedPinModes.get(pin) == 0;
        bufferedPinModes.set(pin);
    } else if (mode == INPUT_PULLUP) {
        pinModesDirty |= bufferedPinModes.get(pin) == 1;
        pullupsDirty |= bufferedPullups.get(pin) == 0;
        bufferedPinModes.clear(pin);
        bufferedPullups.set(pin);
    }
}

void PCF8574::digitalWriteBuffered(pin_t pin, PinStatus_t status) {
    bool boolstate = status == HIGH;
    outputsDirty |= bufferedOutputs.get(pin) != boolstate;
    bufferedOutputs.set(pin, boolstate);
}

int PCF8574::digitalReadBuffered(pin_t pin) {
    return bitRead(bufferedInputs, pin) ? HIGH : LOW;
}

analog_t PCF8574::analogReadBuffered(pin_t pin) {
    return bitRead(bufferedInputs, pin) ? 1023 : 0;
}

void PCF8574::analogWriteBuffered(pin_t pin, analog_t value) {
    digitalWriteBuffered(pin, value >= 0x80 ? HIGH : LOW);
}

void PCF8574::begin() {
    if (interruptPin != NO_PIN)
        ExtIO::pinMode(interruptPin, INPUT_PULLUP);
    // I²C setup if necessary
}

void PCF8574::updateBufferedOutputs() {
    updateBufferedPinModes();
    if (!outputsDirty)
        return;
    uint8_t outputData = bufferedOutputs.getByte(0);
    // Write this byte over I²C
    outputsDirty = false;
}

void PCF8574::updateBufferedInputs() {
    // Only update if a pin change interrupt happened
    if (interruptPin != NO_PIN && ExtIO::digitalRead(interruptPin) == HIGH)
        return;
    bufferedInputs = ... // read from I²C
}

void PCF8574::updateBufferedPinModes() {
    if (pinModesDirty) {
        uint8_t outputData = bufferedPinModes.getByte(0);
        // Write this byte over I²C
        pinModesDirty = false;
    }
    if (pullupsDirty) {
        uint8_t outputData = bufferedPullups.getByte(0);
        // Write this byte over I²C
        pullupsDirty = false;
    }
}

After typing it out, I realized that the PCF8574 doesn't have any pull-up resistors, so you can ignore most code related to it.

tttapa avatar Nov 19 '20 14:11 tttapa