Neokey technical details

I’ve recently got neokey 1x4 and got it nicely working with Raspberry Pi: Adafruit provides CircuitPython and Arduino (cpp) libraries - but it felt too complicated (the libraries and porting them to normal Python with smbus on Raspberry Pi). So, I just went down to trace what is needed from i2c traffic and converted it to use smbus2 (needed for sending two byte ‘internal address’ for read and write). Reading their code it all boiled down to 6 initialisation calls (4 for defining GPIOs, interrupts, pullups and something else + 2 for writing to LEDs). And for operation - it needed two writes for setting up leds and one read for reading gpio states. That part was easy and relatively logical/simple.

Problem started with PiPico. First part I did just as ‘discover’ phase until I moved it to PiPico. Same calls and yet I can’t make it work!

I can have LEDs set and they work well, or I can get GPIO’s states. But not both! Main problem is that if I invoke ‘NEOPIXEL_SHOW’ (a ‘command’ to move some kind of buffer to output for LEDs) reading GPIOs immediately stop and all I get back are 1s. I have tried different timings, ensured that ordering fo i2c operations is same across two platforms (I spent more time sorting out debug info than anything else) and still nothing.

Oh, I forgot to explain that neokey has a µController that does drive LEDs and read keys. It has its own set of i2c ‘commands’ and answers…

The question is: does anyone have similar experience or better - any idea where to go to try to find out who’s the author of the µController code who might be able to shed more light to this behaviour (and more importantly maybe suggest some ways around the problem I have).

Hm, I don’t understand your problem. Adafruit has a very good learning guide for this breakout and in the guide you will find detailed setup-instructions and examples for the Pi as well as for MCUs that support CircuitPython. Making it work is just a matter of copy&paste and in case of the pico to define the correct i2c (the pico does not have board.I2C(), but there is an additional guide for the pico explaining all of this). It took me only a few minutes to get the examples working on both platforms just by following the guide.

If you want the source-code of the MCU on the breakout, I suggest that you ask in the Adafruit forums. My experience is that they are very helpful and friendly.

Hi, thank you for the quick response. I’m not sure that this warrants simple ‘copy/paste’ approach, but oh, well…

Fair enough - I’ll try my luck on their forums. For some reason I failed to see that obvious solution.

OK I’ve cracked it. There’s definitively a gap in my knowledge or some really subtle timing that made C code work in some circumstances… Here’s what I have done for reference if anyone else decides not to use Python but C on Pi Pico and not to use slightly overcomplicated code but go straight to the simplest way of using neokey 1x4.

For Python on Raspberry Pi I’ve got:

    ...
    def write(self, buf: List[int]) -> None:
        i2c_bus.i2c_rdwr(i2c_msg.write(self.i2c_address, buf))

    def read(self, reg_base: int, reg: int, number_of_bytes_to_read: int) -> List[int]:
        buf = [reg_base, reg]
        read_data = i2c_msg.read(self.i2c_address, number_of_bytes_to_read)
        i2c_bus.i2c_rdwr(i2c_msg.write(self.i2c_address, buf))
        i2c_bus.i2c_rdwr(read_data)
        return list(read_data)

    def init(self) -> None:
        self.write([self._NEOPIXEL_BASE, self._NEOPIXEL_PIN, 3])
        self.write([self._NEOPIXEL_BASE, self._NEOPIXEL_BUF_LENGTH, 0, 12])
        pins = self._BUTTON_MASK  #    _BUTTON_MASK = (1 << 4) | (1 << 5) | (1 << 6) | (1 << 7)
        cmd = [(pins & 0xff000000) >> 24, (pins & 0xff0000) >> 16, (pins & 0xff00) >> 8, pins]

        self.write([self._GPIO_BASE, self._GPIO_DIRCLR_BULK] + cmd)
        self.write([self._GPIO_BASE, self._GPIO_PULLENSET] + cmd)
        self.write([self._GPIO_BASE, self._GPIO_BULK_SET] + cmd)
        self.write([self._GPIO_BASE, self._GPIO_INTENSET] + cmd)

    def update_leds(self) -> None:
        self.write(
            [self._NEOPIXEL_BASE, self._NEOPIXEL_BUF, 0, 0,
             self.leds[0][0], self.leds[0][1], self.leds[0][2],
             self.leds[1][0], self.leds[1][1], self.leds[1][2],
             self.leds[2][0], self.leds[2][1], self.leds[2][2],
             self.leds[3][0], self.leds[3][1], self.leds[3][2]])
        self.write([self._NEOPIXEL_BASE, self._NEOPIXEL_SHOW])

    def read_keys(self, num: int) -> List[int]:
        return self.read(self._GPIO_BASE, self._GPIO_BULK, 4)
...
    while True:
        ...
        neokey.update_leds()
        ...
        time.sleep(0.01)
        keys = neokey.read_keys(4)
        ...

Attention is here on smbus2’s i2c_rdwr() - which I though is one of i2c’s start/write/continue/read/stop combinations. And all works well - LEDs are updated, keys are read.

For pi pico’s code I’ve got the same init structure and then:

void write_leds() {
        buf[0] = NEOPIXEL_BASE;
        buf[1] = NEOPIXEL_BUF;
        buf[2] = 0;
        buf[3] = 0;
        for (int i = 0; i < 12; i++) { buf[i + 4] = leds[i]; }
        int ret = i2c_write_blocking(i2c_default, NEOKEY_I2C_ADDRESS, buf, 16, false);
        ...
}

void show_leds() {
        ...
        buf[0] = NEOPIXEL_BASE;
        buf[1] = NEOPIXEL_SHOW;
        int ret = i2c_write_blocking(i2c_default, NEOKEY_I2C_ADDRESS, buf, 2, false);
        ...
}

void read_keys_raw() {
        buf[0] = GPIO_BASE;
        buf[1] = GPIO_BULK;
        int ret = i2c_write_blocking(i2c_default, NEOKEY_I2C_ADDRESS, buf, 2, true);
        ...
        sleep_ms(2);
        uint8_t rec[4];
        ret = i2c_read_blocking(i2c_default, NEOKEY_I2C_ADDRESS, rec, 4, false);
}

Which didn’t work. Or worse - calling read_keys_raw() worked well as long as I never called show_leds().
But when I changed:

        int ret = i2c_write_blocking(i2c_default, NEOKEY_I2C_ADDRESS, buf, 2, true);

to

        int ret = i2c_write_blocking(i2c_default, NEOKEY_I2C_ADDRESS, buf, 2, false);

I completely failed to realise that (by mistake?) I did:

        i2c_bus.i2c_rdwr(i2c_msg.write(self.i2c_address, buf))
        i2c_bus.i2c_rdwr(read_data)

In Raspberry Pi’s Python code instead of:

        i2c_bus.i2c_rdwr(i2c_msg.write(self.i2c_address, buf), read_data)

which I really meant to do and hence change behaviour of i2c from start/write/continue/read/stop to start/write/stop - startread/stop. The moment I did the same in C code all settled down to the right place.