#Using PCA95xx via I2C

1 messages · Page 1 of 1 (latest)

plain isle
#

Hi folks, I've seen some people here use an MCP230xx as an IO expander but I haven't seen anyone use a PCA95xx. I see there's a driver in Zephyr for the PCA series of expanders.

I'm running into an error that suggests the column GPIO might be not defined properly, but not sure how to troubleshoot this. Previous forum posts suggest enabling enabling I2C in kconfig.defconfig (done) and setting i2c status to "okay"(also done). Not sure where to go from here after having adding these options.

''' modules/drivers/kscan/libzmk__drivers__kscan.a(kscan_gpio_matrix.c.obj):(.data.kscan_matrix_cols_1+0xc): undefined reference to `__device_dts_ord_39'''

neat brook
#

In a nutshell: That means your referencing a node that doesn't end up with a device driver loaded that's linked to it.

#

So, usually that means Kconfig issue causing the driver not to get enabled.

#

If you're building locally, and changed a Kconfig.defconfig file, and have an existing build dir, you may need to re-build from scratch to get the changed default picked up.

#

Can you post the final zephyr/.configfile from your build?

#

Are you sure you used the right compatible value?

plain isle
neat brook
#

Ok, yup.

plain isle
#

Looks like in the driver itself that might be nxp_pca95xx.. hmm

neat brook
#

That's always converted like that.

#

comma is correct.

tardy badger
#

I'd even suggest copying the mcp230xx example and putting that in your board just to see if it builds

neat brook
#

Does your .config end up w/ CONFIG_GPIO_PCA95XX=y in it?

tardy badger
#

for instance I see CONFIG_GPIO_MCP23017=y in the mcp23017 example

plain isle
#

lets see if I can post that

neat brook
#

You can see that the Zephyr driver will be auto-enabled if I2C enabled and the DT node is there and enabled:

menuconfig GPIO_PCA95XX
    bool "PCA95XX I2C-based GPIO chip"
    default y
    depends on DT_HAS_NXP_PCA95XX_ENABLED
    depends on I2C
    help
      Enable driver for PCA95XX I2C-based GPIO chip.
#

(This is from Zephyr 3.2, but I'm 90% certain also true on Zephyr 3.0)

#

Oh! No, it's not in Zephyr 3.0.

#

@plain isle You'll need to explicitly enable it as @tardy badger noted.

plain isle
#

Hmm. Where should I be explicitly enabling this? And yes I have been building locally FYI. Not sure where the final .config file for the build ends up.

#

oh in the defconfig file

#

?

neat brook
#

Yes, exactly.

#

In particular, if you really need it for your shield to work, in that file you could force the shield config to select the other things you need.

#

e.g.

config SHIELD_FOO
    select I2C
    select GPIO_PCA95XX
#

That would force those on if you have the shield enabled, versus just defaulting them (which someone could muck with accidentally later in menuconfig and turn off.)

plain isle
neat brook
#

(I didn't when I first created ZMK and was creating our first shields, so there's lots of "less than ideal" Kconfig still lurking)

plain isle
#

Seems with CONFIG_GPIO_PCA95XX=y in the kconfig.defconfig I'm still getting the same issue. Where can I find that final .config that might show whether it actually changed anything? Maybe I'm just throwing the wrong command in here somehow

neat brook
#

Are you specifying a build output directory w/ -d ?

#

It'll be base build dir (build/by default, otherwise value of -d foo) + zephyr/.config

#

Or you can build with west build -t menuconfig and inspect in there.

plain isle
#

this menu is a lot phew

neat brook
#

Hit / to search

#

Until you learn the hierarchy

plain isle
#

well, if I had to enable it in this menu, I assume whatever I put in my kconfig.defconfig file didn't take for some reason

tardy badger
#

I find it easiest to search elsewhere and put CONFIG_... in the .conf file

neat brook
#

Right, so either:
Config updates weren't right, or
Didn't blow away build dir and start from scratch, so default changes weren't picked up

plain isle
#

so I have been building pristine. Once I went into menuconfig, enabled the driver, and stopped building pristine - it built. yay, progress!

#

but not sure why in my own config file it just.. isn't taking it

neat brook
#

Pristinemay not pick up def config file changes.

#

Which is why I always suggest scorched earth rm -rf build/

plain isle
#

nice. Well it seems to build just fine with messing about. now need to figure out exactly why.. will have to try that rm -rf build in the AM and see if the thing actually works

rough maple
#

I also ran into the error you're facing (.data.kscan_matrix_cols_1+0xc): undefined reference to __device_dts_ord_xx'''` when I was changing the board name. I was able to get past the build error after fixing the following defines when the shield name changed.

config I2C
    default y
    
config GPIO_PCA95XX
    default y
plain isle
#

everybody wants to use the pca95xx for some reason it seems!

neat brook
#

The mcp23x1(7/8) was unobtainium for a long time.

plain isle
#

I'm still having some issues with mine so trying to figure out if it's this stuff, I2C issues, or something else

neat brook
#

I used mcp23s08 for Zaphod Lite, because the 8-bit ones were easier to find.

plain isle
#

thinking about just wiping out the build folder as I had issues trying to build another board, strangely enough

plain isle
# rough maple weteor has a board that uses i2c with pca9555 that might be useful reference to ...

Also I noticed that he's using all column gpio on the expander.. which I know is a limitation for stuff like shift register use as they can only be configured as outputs. I don't think that limitation exists for the PCA series as far as I know we should be able to configure both inputs and outputs. I just haven't really seen any examples of using this particular device in ZMK aside from this one just now

neat brook
#

So, you can use for both inputs and outputs but unless you hook up the INT line(s), you won't be able to idle when no keys are pressed, and you'll need to enable the Kconfig setting to force constant polling.

plain isle
#

that's what I figured, thanks.

#

You shouldn't need to set polling if you have the INT line configured right?

rough maple
plain isle
#

I was under the impression the driver itself for the pca95xx handles it?

neat brook
#

Yeah, the zephyr driver for pca95xx has built in support.

#

So it'd "just work" (in theory) with our existing matrix driver.

plain isle
#

hmm

rough maple
#

pca95xx will toggle the int line if input port have state change, but I am not sure how zmk will be able to use that

#

also not all pca95xx ics have the int line

neat brook
#

You set up the int line in the DTS when creating the expander node.

neat brook
#

And that will allow the driver to support using gpio_configure_interrupt(blah) on the device.

rough maple
neat brook
#

Yeah, that'd be it.

#

Again, that's the theory, no one has yet breadboarded or otherwise validated it.

#

Just for full transparency.

rough maple
#

Thanks. I have hardware coming in the mail to test

rough maple
#

using pca9535pw, at ~$0.80 with interrupt

plain isle
#

but alas no dice yet

rough maple
plain isle
hoary bay
#

Did either of you ever get this expander working with ZMK?

plain isle
rough maple
# hoary bay Did either of you ever get this expander working with ZMK?

I can get it to work as output on both ports, but no luck using one as output and the other as input.

My test board was 5 col x 4 row (col to row). ZMK would detect keypresses but also get false positives, e.g. pressing [0,0] will result in keyevents [0,0], [0,1] ... [0, 4], while pressing [0,1] will result in keyevents [0,1], [0,2] ... [0, 4]. The false positives that I got wasn't always the same, sometimes I get more and sometimes less, but I am 90% sure that it was only the downstream columns.This was in polling mode. I had also experimented with debounce timing but was really only able to make the behavior worse. This was few weeks ago so I don't remember the exact details.

I have a new breakout board that I can use for testing the interrupt line, but I don't have much motivation to keep messing with this right now😆

hoary bay
#

Thanks for the updates

neat brook
rough maple
rough maple
#

I wasn't aware of it earlier, but I can give it another go

rough maple
#

test board is 5col x 4 rows. with a single switch installed in [0,0], key press will trigger an indeterminate amount of presses on the same row. Example:

[00:00:41.246,612] <dbg> zmk: kscan_matrix_read: Sending event at 0,0 state on
[00:00:41.246,673] <dbg> zmk: kscan_matrix_read: Sending event at 0,1 state on
[00:00:41.246,734] <dbg> zmk: kscan_matrix_read: Sending event at 0,2 state on
[00:00:41.246,795] <dbg> zmk: kscan_matrix_read: Sending event at 0,3 state on
[00:00:41.246,826] <dbg> zmk: kscan_matrix_read: Sending event at 0,4 state on
[00:00:41.246,978] <dbg> zmk: zmk_kscan_process_msgq: Row: 0, col: 0, position: 0, pressed: true
[00:00:41.247,070] <dbg> zmk: zmk_keymap_apply_position_state: layer: 0 position: 0, binding name: KEY_PRESS
[00:00:41.247,100] <dbg> zmk: on_keymap_binding_pressed: position 0 keycode 0x7001E
[00:00:41.247,131] <dbg> zmk: hid_listener_keycode_pressed: usage_page 0x07 keycode 0x1E implicit_mods 0x00 explicit_mods 0x00
[00:00:41.247,161] <dbg> zmk: zmk_hid_implicit_modifiers_press: Modifiers set to 0x00
[00:00:41.247,161] <dbg> zmk: zmk_endpoints_send_report: usage page 0x07
[00:00:41.247,283] <dbg> zmk: zmk_kscan_process_msgq: Row: 0, col: 1, position: 1, pressed: true
[00:00:41.247,375] <dbg> zmk: zmk_keymap_apply_position_state: layer: 0 position: 1, binding name: KEY_PRESS
[00:00:41.247,406] <dbg> zmk: on_keymap_binding_pressed: position 1 keycode 0x7001F
[00:00:41.247,436] <dbg> zmk: hid_listener_keycode_pressed: usage_page 0x07 keycode 0x1F implicit_mods 0x00 explicit_mods 0x00
[00:00:41.247,467] <dbg> zmk: zmk_hid_implicit_modifiers_press: Modifiers set to 0x00
[00:00:41.247,467] <dbg> zmk: zmk_endpoints_send_report: usage page 0x07
[00:00:41.248,107] <dbg> zmk: zmk_kscan_process_msgq: Row: 0, col: 2, position: 2, pressed: true
[00:00:41.248,168] <dbg> zmk: zmk_keymap_apply_position_state: layer: 0 
neat brook
rough maple
neat brook
#

Yeah. So the pins would be floating

#

Some support only pulling in one direction or another

#

If not supported, you'd need to add external pulls.

rough maple
#

Checking the datasheet again - the 9555 have a non-configurable internal pull up in the functional block diagram. I'm using 9535 and that requires external pull up. Let me try to breadboard some in.

neat brook
#

Yeah, I'd definitely see if that helps

#

Could be parasitic high values for the other inputs.

rough maple
#

Good new, I got it to work. The interrupt line worked as expected too.


&xiao_i2c {
    status = "okay";
    clock-frequency = <I2C_BITRATE_FAST>;
    expander_io: pca95xx@20 {
        compatible = "nxp,pca95xx";
        status = "okay";
        gpio-controller;
        reg = <0x20>;
        label = "PCA9555";
        interrupt-gpios = <&xiao_d 0 (GPIO_ACTIVE_LOW|GPIO_PULL_UP)>;
        #gpio-cells = <2>;
        ngpios = <16>;
    };
};

@hoary bay

neat brook
#

Nice.

#

Was the missing pulls the key?

rough maple
#

I guess the recommendation is to use pca9555 if you need to use input ports, pca9535 for outputs only

rough maple
neat brook
#

Because the 9555 has pull capabilities?

rough maple
#

it worked without joel's PR btw

neat brook
#

Yeah, should work without it, but be more efficient with it.

rough maple
rough maple
#

the 9555 pullup is not configurable,

neat brook
#

Wait, is the pull up... Always on?

#

On the full port(s)?

rough maple
neat brook
#

If so, better to use 9535 with pulls on only your input pins.

#

Yeah. Like that diagram

rough maple
#

It's great to know that you only need 3 gpio pins for a 8x8 matrix with functional interrupt

rough maple
neat brook
#

Oh, sorry, forgot it was i2c, was thinking spi

reef cobalt
#

Hi, I’m trying to implement this myself, what did you connect the device address pins to?

reef cobalt
rough maple
rough maple
reef cobalt
neat brook
#

It's the address you'd use in the devicetree.

#

It's how the MCI talks to the right device on the i2c bus.

reef cobalt
#

where would you put it?

#

oh is it the reg

#

ah ok

#

found it

neat brook
#

It's both the reg property and the value after the @ in the node specifier

reef cobalt
#

Thanks

#

To confirm, the code for the xiao runs the same for the promicro, just rename it?

neat brook
#

If you're using the Xiao standard i2c pins, you'd simply nest under the xiao_i2c node to put it on that bus

#

That's an abstraction so any XIAO compatible board can be used.

reef cobalt
#

wait, so if I’m using a nice nano, I just have to connect the pins like connecting to the xiao?

#

aren’t there standard sda and scl pins for the nano?

neat brook
#

Oh, if using the nano, nest under pro_micro_i2c

#

That's the equivalent abstraction for the pro micro footprint

reef cobalt
#

ah ok, makes sense

#

and use the same sda and scl as promicro

reef cobalt
#

Thank you!

neat brook
#

So the same shield could be used with a kb2040 for wired ZMK too

reef cobalt
#

Cool

rough maple
reef cobalt
#

good catch, thanks for telling me

#

where do I specify the interrupt pin? I swear I've seen it somewhere

#

oh, this right?

interrupt-gpios = <&xiao_d 0 (GPIO_ACTIVE_LOW|GPIO_PULL_UP)>;
#

So in total, I need to pull up SCL, SDA, INT, input and output pins with a 10k resistor and the 3.7v from the n!n vcc?

#

*3.3v i meant

#

and ground the address pins so it applies to reg = <0x20>; and expander_io: pca95xx@20

rough maple
#

the interrupt config looks ok

reef cobalt
reef cobalt
#

also would it be fine if I just used the bat+ raw, or is it recommended to use the vcc from the mcu (for the pullup and Vdd)

neat brook
reef cobalt
#

if I’m using col2row, wouldn’t using pull-up resistors suppress (or for lack of a better term, smother) the input to the pca9535?

#

because normally, the switch is open, and because of the pull-up resistor the input to the pca9535 is high

#

when scanning that column though (I assume zmk sets the column high because of col2row), when the switch is closed, the row would be high, but the input to the pca9535 would still be high so no input would be detected

#

so shouldn’t a pull down be used?

#

maybe im very misinformed

#

my impression of a keyboard matrix was that you send high down a column, and then you read from each of the rows, determining which key was pressed by seeing which row is high

#

but my thoughts are that because of the pull up resistor, the rows would be high anyway, and you wouldn’t be able to tell which key is pressed because they are all high

#

forgive me I am not very good at electronics

#

TLDR: my impression was that the mcu reads which switch was pressed based on which row (when using col2row) is set high, but because of the pull up resistor, all rows are set high so it is impossible to read the rows properly

reef cobalt
#

going off what you explained to me before, zmk uses an active high matrix, unless the pca9535 reverses all the outputs?

reef cobalt
reef cobalt
#

Ohh I have found the answer I think

#

do I have to set it active low

#

e.g col-gpios = <&pro_micro X GPIO_ACTIVE_LOW>

reef cobalt
#

oh

#

In the diagram, it shows pins P13-P17 as “inputs”, but for our purpose we would use the pins pulled high as outputs to go into the matrix right?

#

because normally it is high, but you can also specify it to be low as well

#

then the other ones you read as “inputs” to the pca9535 (the rows if you’re using col2row)

reef cobalt
#

wait then why do we need a pull-up in the first place if we are outputting

rough maple
#

Can you share your schematic? It's hard to give you specific answers without it. @reef cobalt

rough maple
#

I went through the matrix code in zmk, the "diode-direction" col2row only sets columns to be GPIO output and rows to be GPIO input (row2col the other way), and has nothing to do with the actual polarity of the diode. GPIO_ACTIVE_HIGH/LOW allows you to flip the logic level, and GPIO_PULL_UP/DOWN let's you bias the inputs.

If you go through zmk config examples, you will find a lot of the boards that has diode anode=column/cathode=row uses col2row, uses GPIO_ACTIVE_HIGH for both columns and rows, plus GPIO_PULL_DOWN on the rows.

Comparing with QMK, in the avr days you don't always get to choose to pull up/down the GPIO, so the default is pull up only. It assumes that a low voltage level is logic high (so GPIO_ACTIVE_LOW in zmk). To make the implicit active low, input with pull up work, diode direction col2row means col=input/row=output in QMK and row2col means row=input/col=output.

#

With PCA9535, there is no internal pull up/down, so you can do "active high" or "active low". With PCA9555, with the internal pull up, you can only use "active low".

#

A fun example is the one for ferris 0.2. Electronically, the anodes are connected to cols, cathodes are connected to rows, and that is the same on both splits. On the right split, MCP23018 is used with internal pull up, so the zmk diode-direction needs to be set to row2col with "active_low". On the left split, the MCU pins are used, so the bias can be high or low, and col2row is used with "active_high".

    kscan_left: kscan_left {
        compatible = "zmk,kscan-gpio-matrix";
        label = "KSCAN_LEFT";

        diode-direction = "col2row";

        col-gpios
        = <&gpiob 8 (GPIO_ACTIVE_HIGH)>
...
        , <&gpioa 14 (GPIO_ACTIVE_HIGH)>
        ;
        row-gpios
        = <&gpiob 7  (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)>
...
        , <&gpioa 2 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)>
        ;
    };

    kscan_right: kscan_right {
        compatible = "zmk,kscan-gpio-matrix";
        label = "KSCAN_RIGHT";

        diode-direction = "row2col";

        col-gpios
        = <&right_io 0 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
...
        , <&right_io 4 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
        ;
        row-gpios
        = <&right_io 8  (GPIO_ACTIVE_LOW)>
..
        , <&right_io 11 (GPIO_ACTIVE_LOW)>
        ;
    };
reef cobalt
#

i don’t have a schematic because i used ergogen, but it’s basically this without the pull up at the top

rough maple
#

do you already have the pcb made or is it a handwired thing?

reef cobalt
#

Thanks for clearing that up, pcb is not printed yet but i think the first option would be easier (mainly because I’m lazy and don’t want to route row2col). So basically use pull down on the rows?

rough maple
reef cobalt
#

yeah sure, is there any advantage to using option 2?

rough maple
#

just want to confirm you actually have the pull down in the right place

reef cobalt
rough maple
reef cobalt
#

yeah

#

oh well it only takes like 10 mins

#

ive already started :/

#

i've given up, heres te wiring. its really messy, you probably cant see anything

#

heres the ersistors

#

i just need to change the diode direction?

#

basically change the directrion on the silkscreen (and when im wiring it)

#

or do i need to change a lot more wiring

#

it should be just a matter of switching the diode direction right?

rough maple
reef cobalt
#

this is the left side of my keyboard, which doesn’t have a nano. The right does, but the signal is transferred through Ethernet cable to the left.

#

I’ve just designed it to be reversible.

rough maple
#

my 2 cents is that this would be really difficult to check for mistakes without a schematic. You can make a fully fleshed out schematic and just use the ergogen output as a reference for switch placement

reef cobalt
#

I mean there’s a net list, that’s better than nothing…

reef cobalt
rough maple
# reef cobalt so to change from col2row to row2col all that is needed is to flip the direction...

let me clarify, imo the zmk config property diode-direction is a misnomer because all it affects is whether row-gpios and col-gpios are configured as input/output on the mcu.

Your pcb above and the schematic both showed that the anode side (+) of the diodes are connected to columns, and cathodes (-) connected to rows. Current can only flow from column pins to row pins because of the diodes. Therefore, to send a signal, the column pins must be at a higher than the row pins.

The easy solution now, because you have connected pull-up resistors to the columns, is to use the columns as inputs and rows as output. The pull-up resistor set the columns to a default high voltage when the switch is not conducting. By providing a low resistance path to ground on the other side of the switch, the MCU can then tell that a switch is conducting when the column voltage is at 0.

To configure this in zmk, rows are the output, cols are input (because we need to read the voltage on them to know the state of a switch), this is done by setting diode-direction = 'row2col'. In addition, the col-gpios will need GPIO_ACTIVE_LOW | GPIO_PULL_UP for the MCU pins and GPIO_ACTIVE_LOW for the expander pins (otherwise, it will complain about not having the ability to set pull direction because the expander does not have that abiliity). The row-gpios will all use GPIO_ACTIVE_LOW, so that when zmk scans a row, it will set a low voltage on that row.

Without changing the diode polarity, you can also use a high voltage for signal, then row pins will need to be the input. To do that zmk will need to be configured as col2row, the row and column flags flipped, and pull resistors moved to pull the row pins to ground.

reef cobalt
#

Thank you for writing such a detailed clarification, I probably need to reread this a few more times but with GPIO_ACTIVE_HIGH, zmk would set a low voltage on that row to scan it? That seems a bit counter intuitive to me

rough maple
reef cobalt
#

haha

#

Also

#

Why are diodes even needed in the active low version of the matrix?

reef cobalt
#

I can’t wrap my head around using ground instead of vcc…

#

So I would need to flip the polarity of the diodes when using the row2col scheme?

#

I assumed that implicitly because of the “without changing the diode polarity, you can also use high voltage signal”

rough maple
reef cobalt
#

Yeah I’m just naming your 1st solution as row2col scheme

rough maple
#
  • anode: col, cathode: row, logic high = low voltage, zmk diode-direction = row2col, input uses pull up
  • anode: col, cathode: row, logic high = high voltage, zmk diode-direction = col2row, input uses pull down
  • anode: row, cathode: col, logic high = low voltage zmk diode-direction = col2row, input uses pull up
  • anode: row, cathode: col, logic high = high voltage, zmk diode-direction = row2col, input uses pull down

note: in qmk, logic high = low voltage and input pulled up is default, and the diode direction is the opposite of the configuration in zmk.

  • anode: col, cathode: row, qmk diode-direction = col2row
  • anode: row, cathode: col, qmk diode-direction = row2col
reef cobalt
#

I’m just gonna say, thank you sooo much for your time and patience.

reef cobalt
#

I FINALLY UNDERSTAND YOU

#

after the entire morning

#

thank you very much