STM32 USB Macro Keyboard

Background

This project started from a simple question: does a programmable keyboard really need a configuration application?

Most programmable macro keyboards depend on dedicated software running on Windows, macOS or Linux. For a device that ultimately just stores a few keyboard shortcuts, that felt unnecessarily complicated.

My approach was instead to make the keyboard itself temporarily behave as a USB flash drive.

During normal operation, the device appears only as a standard USB HID keyboard. No drivers, background services or configuration software are required.

When a key is held while connecting the keyboard, it boots into configuration mode and instead appears as a USB Mass Storage device. The configuration can then be changed by editing a simple text file.

Normal startup
     │
     └── USB HID Keyboard
              │
              └── Ready to use

Hold CONFIG key during startup
     │
     └── USB Mass Storage
              │
              ├── CONFIG.TXT
              ├── STATUS.TXT
              └── README.HTM

After saving the configuration, the keyboard is disconnected and reconnected normally. It then returns to being an ordinary USB keyboard.

Hardware

The controller is built around an STM32C071G8U6, a Cortex-M0+ microcontroller running at up to 48 MHz.

The final version uses the 64 KB Flash variant. I originally designed around the 128 KB STM32C071GBU6, but the 64 KB device was more readily available for manufacturing.

That required checking whether the smaller device was actually practical rather than simply selecting it based on availability.

An optimized Release build of the complete firmware is approximately 45 KB, leaving enough room in the 64 KB device for the application and a dedicated configuration area.

The MCU also provides 24 KB RAM, USB Full Speed device support and, importantly for this project, crystal-less USB operation. This keeps the component count and PCB area down.

Why STM32C071?

Several characteristics made the C071 particularly suitable:

  • Native USB 2.0 Full Speed device peripheral
  • Internal 48 MHz USB clocking
  • No external crystal required
  • Small 4 × 4 mm UFQFPN28 package
  • Enough GPIO for the keyboard interface
  • ADC inputs for hardware configuration
  • Timers for RGB LED PWM
  • Low cost

The result is a surprisingly small controller around a fairly capable USB device.

USB architecture

I deliberately chose not to implement the device as a permanent HID + Mass Storage composite USB device.

Instead, the USB personality is selected during boot.

Normal startup gives:

USB
 └── HID Keyboard

Configuration startup gives:

USB
 └── Mass Storage

This means that during everyday use the host sees nothing unusual. It is simply a USB keyboard.

It also prevents the configuration disk from permanently appearing on the user’s computer.

The configuration mode is selected before USB enumeration, so the host sees a clean USB device corresponding to the selected mode rather than interfaces appearing and disappearing while connected.

A virtual disk without a filesystem

One of the more interesting parts of the project is that the configuration disk isn’t actually a disk.

There is no SD card, external Flash or conventional FAT filesystem.

Instead, the firmware implements a small GhostFAT-style synthetic filesystem. When the computer requests sectors through USB Mass Storage, the STM32 generates the appropriate FAT structures and file contents dynamically.

The computer sees:

MACROPAD
│
├── CONFIG.TXT
├── STATUS.TXT
└── README.HTM

but internally there is no equivalent filesystem stored in memory.

README.HTM, for example, is compiled directly into MCU Flash. USB sector reads are mapped into the corresponding part of the HTML data.

This avoids pulling in a full filesystem implementation and avoids wasting RAM on files that are essentially static.

Configuration

The idea was to make the configuration format understandable without requiring documentation for normal use.

For example:

KEY1=CTRL+C
KEY2=CTRL+V
KEY3=MEDIA:PLAY_PAUSE
KEY4=TEXT:"Hello world!"

Whitespace around separators is optional, and commands are case-insensitive:

KEY1 = SHIFT + A
key2 = media : volume_up

Text actions can also contain escape sequences:

KEY1=TEXT:"Hello\nWorld"
KEY2=TEXT:"Name:\tJonas"
KEY3=TEXT:"C:\\Users\\Jonas"

The firmware parses this into a compact internal representation rather than storing and interpreting the text file during normal keyboard operation.

If the new configuration is invalid, it is rejected and the previous working configuration remains active.

STATUS.TXT provides information about whether the configuration was accepted or why parsing failed.

Keyboard architecture

The firmware was designed to scale beyond this particular four-button PCB.

The underlying scanner supports a matrix of up to:

7 rows × 7 columns = 49 keys

The number of rows and columns can be selected in hardware using two ADC inputs.

Each ADC input uses a 10 kΩ pull-up and one of seven resistor values to encode a number from 1 to 7:

1.0 kΩ  = 1
2.2 kΩ  = 2
3.9 kΩ  = 3
6.8 kΩ  = 4
12 kΩ   = 5
27 kΩ   = 6
68 kΩ   = 7

This means the same firmware can determine the physical keyboard dimensions at startup without requiring separate firmware builds for every PCB variant.

For this four-key version:

ROWS = 1
COLS = 4

so the hardware configuration uses:

ROW_CFG = 1.0 kΩ
COL_CFG = 6.8 kΩ

Why the four-key version doesn’t need matrix diodes

A conventional multi-row keyboard matrix normally needs one diode per switch if arbitrary simultaneous key presses must be detected without ghosting.

This first PCB only has one row.

With no second row, the usual matrix ghosting paths don’t exist, so the anti-ghosting diodes aren’t necessary for this version.

Future versions using multiple rows can add a diode to each switch.

The firmware and electrical architecture were deliberately designed with that expansion in mind rather than locking the project to four buttons.

Switches

The keyboard uses Kailh Choc V2 low-profile mechanical switches.

I chose a relatively compact 17.5 mm center-to-center spacing rather than conventional 19.05 mm keyboard spacing.

This requires suitably narrow keycaps, approximately 16.5 mm wide, but allows the complete four-key controller to remain significantly smaller.

The switches themselves comfortably fit at this pitch.

Status indication

A low-current RGB LED provides feedback without requiring a display.

The three channels are driven by STM32 timer PWM at approximately 1 kHz.

The basic states are:

Normal operation       LED off

Configuration mode     Blue

Configuration accepted Green flash

Configuration error    Red

Using hardware PWM means LED effects don’t require software-generated timing loops and consume very little CPU time.

USB PCB routing

The PCB is a two-layer design.

For USB Full Speed, I normally want D+ and D− routed together over a continuous ground reference.

This PCB presented an interesting compromise: a 5 mm mechanical hole associated with the switch geometry sits directly in the preferred USB routing path.

Instead of extending the board or substantially rearranging the layout, I routed D+ and D− separately around the hole while keeping their lengths approximately equal.

This is not the routing I would choose as the ideal USB differential-pair layout.

However, this is USB Full Speed at 12 Mbit/s, the separated section is short, and the design has a good ground reference. I decided that for this revision the more useful engineering answer was to manufacture it and verify the result on real hardware rather than continue optimizing the PCB around a theoretical ideal.

If it causes signal-integrity or enumeration problems, keeping the pair together around the obstruction will be an obvious revision for the next PCB.

Power supply

The keyboard is powered from USB 5 V and uses a 3.3 V linear regulator for the STM32 and associated electronics.

The current consumption is low enough that a switching regulator would provide little practical benefit while adding cost, PCB area and switching noise.

A simple LDO is therefore the better fit.

Firmware architecture

The firmware is deliberately bare-metal rather than RTOS based.

There is simply not enough concurrency in this application to justify an RTOS. USB processing, keyboard scanning, configuration handling, RGB status and text/macro execution can all be handled using small non-blocking state machines.

The main firmware components are roughly:

                    ┌──────────────┐
                    │    Startup   │
                    └──────┬───────┘
                           │
                  CONFIG key pressed?
                     /            \
                   No              Yes
                   │                │
            ┌──────▼─────┐   ┌────▼───────┐
            │ USB HID    │   │ USB MSC    │
            │ Keyboard   │   │ GhostFAT   │
            └──────┬─────┘   └────┬───────┘
                   │                │
            Key scanning       CONFIG.TXT
            HID actions        parser
            TEXT actions           │
                   │           Validation
                   │                │
                   │            Flash storage
                   │
            ┌──────▼─────┐
            │ USB reports│
            └────────────┘

Dynamic memory allocation is avoided. Buffers and configuration structures have explicit upper bounds, which makes memory consumption deterministic and reduces failure modes on a small MCU.

Configuration safety

A configuration file coming from a computer should be treated as untrusted input, even for something as small as a macro keyboard.

The parser therefore checks things such as:

  • maximum file and line lengths
  • valid key numbers
  • duplicate key definitions
  • valid HID usages and modifiers
  • maximum simultaneous HID keys
  • valid media commands
  • TEXT escape sequences
  • text storage boundaries
  • physical keyboard size

A new configuration is parsed into a separate candidate structure.

Only after the entire configuration has been parsed and validated is it allowed to replace the active configuration.

The parsed configuration is then stored in a dedicated Flash area with integrity information so the text file doesn’t need to be parsed every time the keyboard starts.

Standard USB HID

Once configured, there is nothing proprietary about using the keyboard.

It sends standard USB HID keyboard and consumer-control reports. This should make normal keyboard functionality usable across systems that support USB keyboards, including:

Windows
Linux
macOS
Android
iOS / iPadOS with suitable USB connectivity

No driver or AIVA-specific application needs to remain installed.

There is one important limitation for text macros: USB HID describes physical keyboard usages, not characters. Characters such as @, ", \, å, ä and ö therefore depend on the keyboard layout selected by the host.

The initial firmware uses a US keyboard mapping for text generation. Supporting selectable layouts such as Swedish is a logical future extension.

What’s next?

The four-key board is intentionally the first physical implementation rather than the final limit of the architecture.

The firmware has been structured to support larger matrices, and the configuration language can be expanded beyond individual shortcuts and text strings.

One planned extension is a more expressive macro format capable of combining text, key combinations, media controls and delays, for example:

KEY1=MACRO:{CTRL+L}"https://aiva-robotics.com"{ENTER}

The interesting part of the project isn’t really four mechanical switches connected to an STM32.

It’s the idea that a programmable USB peripheral can remain completely self-contained: plug it into almost any computer and it behaves like an ordinary keyboard; hold a button during startup and the same device becomes its own configuration interface.

No configuration application, no driver and no cloud service required.

Macro Keyboard User Guide – AIVA Robotics