Ganymede: an ARM based split, ergonomic keyboard

Hi. Great design. Will you be making this project open source?

1 Like

Welcome @JASM! It is open source already. See https://github.com/nicolai86/ganymede

The repository contains everything - schematics, gerbers, stls for the case, and the QMK code to make it work.

3 Likes

What software did you make that with because KiCad won’t open it.

the schematic and board were designed using Eagle (you’d need the Standard subscription to edit the files due to the board size) and the case is designed using Fusion 360.

Still working on the TFT display integration. However, I really wanted a choc hot-swap version, so I made a new revision with only minor changes; the TFT will have to wait until I finish the X-Switch version I guess.


Changes to rev 3:

  • dedicated I2C eeprom chip for VIA/ dynamic keymap support
  • choc hot swap PCB
  • 6P JST SH between halves to have pass SYNC and INTB from the ISSI to the master side
  • 6.5mm total case height! With switches and keycaps installed it’s ~13mm top to bottom (yep, slim!)

As for the case design I learned from my mistakes - bigger gaps around everything, the PCB slides right into the case, it fits perfectly.
The case was created using SLS as 3D printing method with Xometries Media Tumble & Dyed Black finish. And oh my god, it feels amazing, and looks great in my opinion. I’m really glad I chose this printing method and finish.

I haven’t soldered anything yet - I’ll post better pictures from a macro camera once I’m done soldering & installed the tenting kit. Also, I can post comparisons between the choc and mx case then, in preparations for the grand finale: a fully enclosed metal case for my X-Switch edition of the keyboard.

5 Likes

50 % build progress is done - left side works fine. Again, some pictures, then again some learnings.

assembled left side



compared mx and choc version

with front-and backlight on

bottom

animated
ezgif-4-fe37e5fd93f5

Now off to the learnings:

  • JST SH as outside interconnect is a probably a poor choice. even with superglue the connector breaks off easily; it needs support from the top to be used as an outside connector.
  • I’ve cut an EVA foam sheet to be placed between the case and the PCB, and it dampens the typing sound. I like it a lot this way
  • I’ve used lock washers to tighten the EZ tents, and it works really well

I’ll post more pictures when I’m done with the 2nd side.
Oh, and did I mention, 6.5 mm total keyboard case high is super thin and amazing - and 13mm total height is, well also nice to type on.

3 Likes

so, both sides are finished and fitted into their respective case. here is how the finished build looks:

It’s my very first choc experience. I bought linear choc brown switches; usually I use tactile switches, so it’s quite a change, but it feels good.

In any case, now off to add support for ad-hoc keymaps using the eeprom which has been added in this revision :slight_smile:

4 Likes

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:

3 Likes

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?

2 Likes

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:
05

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.

m24m01.h

#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);

i2c2.c

...
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);
}
...

m24m01.c

#include "m24m01.h"

uint8_t init_m24m01(void) {
    palSetPadMode(M24M01_WC_PAD, M24M01_WC_PIN, PAL_MODE_OUTPUT_PUSHPULL);
    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 rules.mk to include the source file:

ganymede/rev3/rules.mk

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:

ganymede/rev3/keymaps/default/keymap.c

#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);
                numberOfDevices++;
            } 
        }

        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);
    i2c2_stop();

    palSetPad(M24M01_WC_PAD, M24M01_WC_PIN);

    // this is a hack - waiting until the device is ready should be done instead
    wait_ms(6);
    return status;
}

which matches the datasheet:

58

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;

4 Likes

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.

2 Likes

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.

3 Likes

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