Ganymede: an ARM based split, ergonomic keyboard

Awesome work! I’m glad that this is opened source and I always wanted to dive in deeper to the advanced PCB side of things but I don’t even know where to begin… I would appreciate if you could a guide on how you debugged/developed your per key RGB via IS31FL3733 in QMK or any other feature in general because I want to learn as well!

I hope you find this useful. I made this 100% open source because of the great example of the ErgoDox EZ (supportive folks, :heart:),

I can document the steps I took to get things working with QMK. EEPROM Integration with QMK is still fresh in memory, so I can write that down and then circle back to per key RGB.

Note though that none of the code is up-streamed, so what I’m doing will most likely not reflect what needs to be done to get your keeb merged into the QMK repo.

Eventually I want to replace the QMK firmware with one written in Rust or Go, so I have no plans of keeping QMK compatibility for ever :slight_smile:


I’m following your project for quite some time now and wow…
The work, decication and results are simply astonishing!
You have in your hands a pretty nice and compact ortho keyboard :slight_smile:

A good bunch of keyboard PCBs are still using ATMEGAs (so is the one I made), how is the difficulty of using an ARM chip compared to an atmega32u4 for example?


Thank you so much, having a step-by-step guide is a tremendous help for newcomers to learn or even implement something new. I’m totally fine with the code not being up-streamed or being QMK compatible forever and I respect that decision, it’s your project so pursue your interest!

This is rather long, but it’s a first shot at how I went about adding EEPROM support:

On the weekend I wrote some code to integrate a M24M01 EEPROM with the Ganymede. The M24M01 is an I2C enabled EEPROM which supports fast mode plus (up to 1Mhz I2C), which is important for Ganymede because that’s the speed at which the two halves communicate.

The schematic is fairly straight forward, which makes for a great example because it’s easy to get it right the first time:

For more complex components I’d start by designing a dedicated prototype PCB and probing with a Raspberry PI but this is simple enough to directly start onboard with QMK.

I start with two empty files, m24m01.h and m24m01.c, and I all I want to see for now is if the device is ready.


#define M24M01_WC_PAD GPIOC
#define M24M01_WC_PIN 13

// per datasheet, if E1 and E2 are pulled low
#define EEPROM_ADDRESS         0x50
#define EEPROM_LONG_TIMEOUT    1000

uint8_t init_m24m01(void);


i2c_status_t i2c2_isDeviceReady(uint8_t address, uint16_t timeout) {
    const uint8_t* data = {0x00};
    return i2c2_transmit(address<<1, &data[0], 0, timeout);


#include "m24m01.h"

uint8_t init_m24m01(void) {
    palSetPad(M24M01_WC_PAD, M24M01_WC_PIN);

    return i2c2_isDeviceReady(EEPROM_ADDRESS, EEPROM_LONG_TIMEOUT);

Now that I want to use this in my keyboard I change the to include the source file:


SRC += ../m24m01.c

this should already compile, but it’s not used anywhere.

For debugging I like using CONSOLE_ENABLE = yes, because then I can use the QMK Toolbox application to peek at debug output:

Now I add a custom key code to my keymap, which triggers the new code:


#include "../../../m24m01.h"
#define KC_EEPROM_RDY KC_F20
bool process_record_user(uint16_t keycode, keyrecord_t *record)
  switch (keycode) {
    case KC_EEPROM_RDY: {
      if (record->event.pressed) return false;
      uint8_t result = init_m24m01();
      if (result == 0) {
          xprintf("M24M01 ready\n");
      } else {
          xprintf("M24M01 not ready: %d\n", result);
      return false;

I flash my prototype, hit the right key on the right layer - voila:

Now you might not be so lucky that you know the I2C address; in that case, I have custom code to scan for I2C devices, bound to another key:

    case KC_I2C_SCAN: { 
        if (record->event.pressed) return false;
        uint8_t error, address;
        uint8_t numberOfDevices;

        numberOfDevices = 0;
        for(address = 1; address < 255; address++ )
            error = i2c2_isDeviceReady(address, EEPROM_LONG_TIMEOUT);

            if (error == 0) {
                xprintf("I2C device found at address %x\n", address);

        xprintf("done: found %d\n", numberOfDevices);
    return false;

At this point I know that I have established communication with the device, and I basically need to flesh out the API I want to use to talk to it.
I basically repeat the above steps for the functions I need:

  • add a new keyboard code on a dummy layer
  • bind a new function to that keycode
  • flash, try, debug
  • repeat

In the case of the M24M01 I added 5 keys, all following more or less above steps:

  • dump an entire page (to see if my page_read function works
  • batch write to a page (so I can batch writes and don’t wear out the EEPROM too fast
  • single byte write at random location
  • single byte read at random location
  • is the device ready? (see above)

Now, every function of course needs to be implemented as per the datasheet.
E.g. m24m01_byte_write

uint8_t m24m01_byte_write(uint8_t address, uint16_t eepromAddr, uint8_t data) {
    const uint8_t location_and_byte[3] = {(eepromAddr >> 8) & 0x00FF, (eepromAddr & 0x00FF), data};
    palClearPad(M24M01_WC_PAD, M24M01_WC_PIN);

    uint8_t status = i2c2_transmit(address<<1, &location_and_byte[0], 3, EEPROM_LONG_TIMEOUT);

    palSetPad(M24M01_WC_PAD, M24M01_WC_PIN);

    // this is a hack - waiting until the device is ready should be done instead
    return status;

which matches the datasheet:


Now that I know that the functions work I delete the keycodes, and use the library where I actually want to use it: custom keyboard keymaps stored in EEPROM, configured using raw hid.

But that’s a separate post :slight_smile:

Note above code is not pretty, it’s just a quick and dirty way to get an integration off the ground. I glossed over many parts, but this is the rough direction I usually go;


the biggest challenge for me was that everything is 3.3v now. I did some tinkering with Atmegas and Attinys before, and I was only used to having 5v everywhere. As for the software side, that’s all abstracted nicely behind HAL, so it’s really comfortable to use STM32s, even directly without QMK. I especially like generating dummy projects with STCubeMX to play around with settings; personally I find the developer experience way better. But it took some time to get used to it for sure.


Thank you very much for your answer.
I also played a bit with STCubeMX, really nice piece of software :wink:
STM32 software environment as a whole looks to be extremely well done.

On the project I’m working on I sticked to atmega MCU, but plan to move on ARM MCUs later on :wink:
The advantages that I see is I2C speed (1 MHz on ARM, 400KHz on Atmega), and also the fact that DMA is used on QMK for sending I2C data on ARM while CPU is used for Atmega.
Looks to be much better to handle lots data though I2C, like RGB leds on your project.

What I don’t know yet is if ARM I2C DMA process is asynchronous or not.
Would be nice if it was, because sending RGB leds data takes quite some time though I2C even at 1 MHz, and in case this is synchronous the firmware can’t handle other important things like matrix scanning for example.

Great job on write up! I’ll need some time to digest it but I’m looking forward to implementing that EEPROM chip for my next project but on the STM32F072 instead.


Thanks! I’ll be sure to push the entire code to GitHub once my basic dynamic key map works, so folks can take a look at the entire code.

1 Like

Can you add alps support?

I don’t know much about alps but assuming they have MX spacing (19.05 x 19.05) and all I need to change is the PCB footprint I certainly could change rev 2 to be alps & non-swap.

I’ve been wondering about ergos recently. Numbers are on a separate layer, correct? For people who type on traditional layouts, do you often struggle with ergos? I know sometimes my typing style “reaches over” (ie my left hand hits a key over towards the middle of my board that your right hand should traditionally hit) and that’s made me a bit skeptical towards buying one.

I had the same issue with overreach when I started getting into split with my ErgoDox EZ. It took a couple of months to fix that, but it happens automatically when your daily driver is a split keyboard.

I put the number keys on the top row on a separate layer. Personally I find it more convenient than a fourth row on top as my hands have less vertical movement. However it also takes a little time to get used to first activating a different layer.

The transition to a split layout was more complicated for me as preventing overreach originally dropped my typing speed considerably. I’m glad I didn’t go back though;

1 Like

I build another choc ganymede with a slightly improved case design - also, this time, all in white. look at the nice shine through on the keycaps:


What type of external touchpad are you using there? Is this an Apple device or something different?

It’s an Apple Trackpad 2

1 Like

Thanks for the information.

Reason I asked: I’m currently looking for a external, wired-only touchpad which works with Linux and is multitouch-capable for at least two fingers. (And those by Apple seem to be all wireless ones.)

I see. The magic 2 is wireless, but it seems to work with the charging cable as a wired trackpad too. It’s the best I ever had.

Hi @nicolai86,
I’ve been following your progress on the keyboard for a while now as the layout is one of the very few that tick all boxes for me. Keep on the good work!

Recently, I’ve been in a science museum and I found a keyboard which you might find interesting.

M-Type Japanese ergonomic keyboard, 1990
Designed and manufactured by NEC.

Of course, number of rows&columns is different but the 4-key-thumb-cluster is very similar.
It’s a shame such designs didn’t get more traction.


Welcome @mmk and thank you!

I really like that keyboard. I’ve been thinking about rotating columns for a while now, though not quite as much as in your picture.

I’m currently working on a new revision for the Ganymede, which will rotate some columns slightly. Let’s see when I’ll have something to show :slight_smile: