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.
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:
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
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;