Adding Persistent Settings to Drop CTRL

5 October, 2020

I recently bought a Drop CTRL keyboard and I’m quite pleased, but after a few days I noticed that every time I unplugged it, it would reset itself to the default LED animation. I did some digging into the firmware, a variant of the popular QMK, and discovered that the keyboard’s persistent storage driver is just stubbed out as an array in memory! By default, when the keyboard loses power, it forgets all the settings configured while it was on.

This article will go through the technical details of what needs to happen to get this keyboard to remember its settings correctly. It turns out that the keyboard fully supports saving its settings through a power loss, and I have ben using it with these changes for the past week.

Just tell me the steps!

Before we dive in, in case you just want the steps to make it work, here is how you enable persistent storage on your Drop ALT or CTRL keyboard:

Clone and build the modified mdloader

1
2
3
git clone https://github.com/ottobonn/mdloader
cd mdloader
make

Enable Smart EEPROM using mdloader

1
2
cd build
./mdloader --first --smarteep

While the mdloader command is waiting for a device, enter flashing mode on your keyboard with Fn + B or the tiny button on the back. After the new firmware uploads, unplug and plug in the keyboard to restart it (it will look dead, but it’s just not restarting).

Clone and build the modified default keymap

1
2
3
git clone https://github.com/ottobonn/qmk_firmware
cd qmk_firmware
make massdrop:ctrl/default_md

If you have an ALT keyboard, you can adapt this keymap file for it.

Load the new firmware on to the keyboard

1
./mdloader --first --restart --download path/to/qmk_firmware/.build/massdrop_ctrl_default_md.hex

Your keyboard should restart and should remember its settings from now on!


How it all works

With the how-to out of the way, let’s go over how the changes work to support persistent settings on the keyboard.

Drop CTRL architecture

Let’s start with a bit of information on how the Drop CTRL and ALT are constructed. These keyboards use an ARM processor from Micron (developed by Atmel before Micron bought them) called the ATSAMD51. This processor is popular in the newer Arduino-compatible boards like Adafruit’s Feather M4. It’s super overpowered (in a good way!) for a keyboard and more than capable of its task of updating LEDs and scanning for pressed keys.

The actual chip inside my Drop CTRL keyboard.

The keyboards additionally have a USB 2.0 hub to allow one of the two onboard USB-C connectors to act as an additional port for the host computer. The keyboard processor connects to this hub as one of its downstream clients.

The keyboards are cool in other ways; check out my full review for all the other details.

Persistent storage in microcontrollers

Microcontrollers use random-access memory to keep track of their state while they have power, just like a full-size computer does. When they lose power, they lose the contents of RAM. However, unlike a typical computer, most microcontrollers don’t have much persistent storage, like a hard drive or flash chip. Those things have to be added to the system as additional chips.

If you’ve programmed Arduino boards, you may already know that common microcontrollers offer a tiny bit of persistent storage in the form of EEPROM, which is like RAM that doesn’t lose its contents on power loss. Most QMK-compatible keyboards seem to be based on the Arduino-staple AVR microcontrollers, and use the AVR EEPROM to store user settings in case the keyboard loses power.

However, the ATSAMD51 is a much more powerful microcontroller and its designers opted to include on-board flash for nonvolatile memory instead of the more traditional EEPROM. Flash is another form of persistent memory, but it has some quirks; the main one that separates it from EEPROM is that it can only be erased in large blocks at a time, while single bytes of EEPROM are erasable.

To overcome the limitations of flash for more traditional EEPROM applications, the SAMD51 includes “SmartEEPROM,” a programming API that uses flash to emulate EEPROM.

See page 594 of the datasheet for the full introduction to SmartEEPROM. Here’s the overview from that page:

The introduction to SmartEEPROM from the ATSAMD51 data sheet.

Unfortunately, the Drop CTRL and ALT come from the factory with SmartEEPROM disabled. Fortunately, it’s easy to enable!

Enabling SmartEEPROM on the microcontroller

Microcontrollers are designed to support configuration options that the software running on them can’t change, for example to protect the firmware from deletion. They store this less-voltatile configuration in “fuse bits,” so named because they emulate actual hardware wires being connected or cut during manufacture. For the most part, fuse bits retain their values when the microcontroller is reprogrammed. However, unlike hardware fuses, they are editable after manufacturing.

To program the fuse bits, we need to modify the application that loads new firmware onto the keyboard. For Drop keyboards, the program is “mdloader.” I didn’t write the changes we’re about to see, so I have to thank and give credit to Alexandre d’Alton for implementing it.

Here’s the code that runs on the host computer to enable the SmartEEPROM fuse bits on the keyboard microcontroller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
int write_user_row(uint32_t *data)
{
uint16_t status = read_half_word(DSU_STATUSB);
write_half_word(DSU_STATUSB, status);

/* clear nvm interrupt status bits */
uint32_t cfg = read_word(NVMCTRL_CTRLA);
cfg &= ~(0xf0);
write_word(NVMCTRL_CTRLA, cfg);

/* set user row address */
write_word(NVMCTRL_ADDR, NVMCTRL_USER);

/* erase page */
write_half_word(NVMCTRL_CTRLB, (NVMCTRL_CTRLB_CMDEX_KEY|NVMCTRL_CTRLB_CMD_EP));

slp(100);

/* erase write buffer */
write_half_word(NVMCTRL_CTRLB, (NVMCTRL_CTRLB_CMDEX_KEY|NVMCTRL_CTRLB_CMD_PBC));

slp(100);

/* write in the write buffer */
for(int i = 0; i < 4; i++) {
write_word(NVMCTRL_USER + i * 4, data[i]);
}

/* set user row address */
write_word(NVMCTRL_ADDR, NVMCTRL_USER);

/* program quad word (128bits) */
write_half_word(NVMCTRL_CTRLB, (NVMCTRL_CTRLB_CMDEX_KEY|NVMCTRL_CTRLB_CMD_WQW));

slp(100);

return 0;
}

Now that we have SmartEEPROM enabled, we can implement an EEPROM driver for it!

Adding support for SmartEEPROM to QMK

QMK supports several different microcontrollers for keyboards, and each supported controller needs its own definitions for things like pin functions, timing, and EEPROM.

Let’s start by looking at the existing EEPROM code for the Drop keyboards:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "eeprom.h"

#define EEPROM_SIZE 32

static uint8_t buffer[EEPROM_SIZE];

uint8_t eeprom_read_byte(const uint8_t *addr) {
uintptr_t offset = (uintptr_t)addr;
return buffer[offset];
}

void eeprom_write_byte(uint8_t *addr, uint8_t value) {
uintptr_t offset = (uintptr_t)addr;
buffer[offset] = value;
}

This code uses an in-memory array to pretend to implement EEPROM support, so it’s not suprising that the settings get lost on power down!

Thankfully, the ATSAMD51 datasheet includes an example snippet for writing to SmartEEPROM:

1
2
3
4
5
6
7
8
// Declare a pointer to the SmartEEPROM start address
volatile uint8_t *SmartEEPROM8 = (uint8_t *) SEEPROM_ADDR;

// Wait for the NVM to be ready
while (NVMCTRL->SEESTAT.bit.BUSY);

// now write to SEEPROM_ADDR like a normal array, e.g.:
SEEPROM_ADDR[0] = 10;

Let’s build on that to implement our actual EEPROM driver:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
__attribute__((aligned(4))) static uint8_t buffer[EEPROM_SIZE];
volatile uint8_t *smart_eeprom = (uint8_t *) SEEPROM_ADDR;

bool smart_eeprom_enabled(void) {
return NVMCTRL->SEESTAT.bit.PSZ > 0 && NVMCTRL->SEESTAT.bit.SBLK > 0;
}

bool wait_for_eeprom_ready(void) {
int timeout = 10000;
while (NVMCTRL->SEESTAT.bit.BUSY && timeout > 0) {
timeout -= 1;
};
return !NVMCTRL->SEESTAT.bit.BUSY;
}

uint8_t eeprom_read_byte(const uint8_t *addr) {
uintptr_t offset = (uintptr_t)addr;
if (offset >= EEPROM_SIZE) {
return 0;
}

if (!smart_eeprom_enabled()) {
return buffer[offset];
}

wait_for_eeprom_ready();
return smart_eeprom[offset];
}

void eeprom_write_byte(uint8_t *addr, uint8_t value) {
uintptr_t offset = (uintptr_t)addr;
if (offset >= EEPROM_SIZE) {
return;
}

if (!smart_eeprom_enabled()) {
buffer[offset] = value;
return;
}

if (wait_for_eeprom_ready()) {
smart_eeprom[offset] = value;
}
}

This code preserves the existing in-memory behavior for ATSAM chips without SmartEEPROM enabled, and supports presistent storage in chips with SmartEEPROM enabled. See the full change here.

Supporting EEPROM in the keymap

Using QMK, each supported keyboard can have more than one key layout. Each layout for a keyboard gets its own keymap.c file to define it. The keymap additionally defines user settings, including which values the user wants to store in EEPROM.

To support persistent storage for settings, then, we need to modify the default CTRL keymap that Drop provides. Each time the user changes a persisted setting, like LED brightness, we will save it to EEPROM immediately in case the keyboard loses power. On startup, we will read the settings from EEPROM and restore them to their in-memory variables.

Using the QMK EEPROM library has a catch: there are two provided settings areas, one for keyboard settings and one for user settings, and each gets four bytes of EEPROM. We will pack our settings into the four bytes of keyboard storage, leaving the user storage available for individual user tweaks.

We have a lot to pack into four bytes, but some of the values are small, like booleans. We can use a C union type to overlay named fields onto a 4-byte integer:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef union {
uint32_t raw;
struct {
uint8_t led_animation_id: 3,
led_lighting_mode: 2,
led_animation_breathing: 1,
led_enabled: 1,
led_animation_direction: 1;
uint8_t gcr_desired;
uint8_t led_animation_speed;
uint8_t _unused;
};
} kb_config_t;

Next, we need to make this data structure the source of truth for the keyboard configuration so we can persist it to EEPROM and load it on startup. I did that by writing little helper functions for each keyboard setting key press handler. Here’s an example to advance to the next LED animation:

1
2
3
4
void led_pattern_next(void) {
kb_config.led_animation_id = (kb_config.led_animation_id + 1) % led_setups_count;
sync_settings(); // implemented below
}

We’ve implemented functions to store values to the new structure, so the final touch is to save and load the structure from EEPROM.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void load_saved_settings(void) {
kb_config.raw = eeconfig_read_kb();

led_animation_id = kb_config.led_animation_id;
gcr_desired = kb_config.gcr_desired;
led_lighting_mode = kb_config.led_lighting_mode;

bool prev_led_animation_breathing = led_animation_breathing;
led_animation_breathing = kb_config.led_animation_breathing;
if (led_animation_breathing && !prev_led_animation_breathing) {
gcr_breathe = gcr_desired;
led_animation_breathe_cur = BREATHE_MIN_STEP;
breathe_dir = 1;
}

led_animation_direction = kb_config.led_animation_direction;
led_animation_speed = kb_config.led_animation_speed;

bool led_enabled = kb_config.led_enabled;
I2C3733_Control_Set(led_enabled);
}

void save_settings(void) {
// Save the keyboard config to EEPROM
eeconfig_update_kb(kb_config.raw);
}

void sync_settings(void) {
save_settings();
load_saved_settings();
}

You can find the full new keymap here.

Conclusion

I had already decided to keep my Drop CTRL before knowing whether EEPROM storage would work, so I am really glad it is possible to support after all! I am not sure why Drop hadn’t already implemented this feature, because a lot of users have been asking about it. It makes the keyboard way better to use, particularly on a laptop where the power is intermittent.

Thanks for reading! Hopefully this post helps if you’re a Drop CTRL or ALT owner who needs persistent settings storage.