A lightweight C library for decoding Morse code keyed on a microcontroller button input. A companion to misclick — same timer-agnostic, callback-based architecture, same event-feed API.
- Straight-key decoding - Feed raw key up/down edges, get decoded characters
- Hardware debouncing - Filters out electrical noise from key contacts
- Full code table - Letters, digits, and standard punctuation (53 characters)
- Element events - Optional per-dot/dash/gap callbacks (e.g. for a sidetone or echo display)
- Encoder included - Character to
".-"string lookup for playback - Configurable timing - Keying speed set by a single unit time
- Callback-based - Non-blocking event-driven architecture
- Memory efficient - Low memory footprint
- Timer agnostic - Works with any timer implementation
Standard Morse timing, relative to the unit (dot) time:
| Element | Nominal | Classified as |
|---|---|---|
| Dot | key down 1 unit | down < 2 units |
| Dash | key down 3 units | down ≥ 2 units |
| Intra-character gap | key up 1 unit | up < 2 units |
| Letter gap | key up 3 units | up ≥ 2 units → character emitted |
| Word gap | key up 7 units | up ≥ 5 units → ' ' emitted |
The thresholds sit between the nominal values so hand keying has slack both ways. The default unit time of 100 ms corresponds to ≈12 WPM; lower it as your fist improves.
Decoded characters arrive on the character callback as a plain text stream:
uppercase letters, digits, punctuation, ' ' for word gaps, and 0 for an
element sequence that matches no known code.
Add this to your CMakeLists.txt to automatically download and build the library:
include(FetchContent)
FetchContent_Declare(
morsel
GIT_REPOSITORY https://github.com/cyborgize/morsel.git
GIT_TAG main # or a specific version tag like v1.0.0
)
FetchContent_MakeAvailable(morsel)
# Link to your target
target_link_libraries(your_target PRIVATE morsel)This example shows integration with Zephyr RTOS, but the same patterns apply to other platforms (it is intentionally identical to the misclick integration — the two libraries can share their GPIO plumbing).
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include "morsel/morsel.h"
// Zephyr timers for the morsel library
static struct k_timer morsel_state_timer;
static struct k_timer morsel_gap_timer;
// Timer callback handlers
static void morsel_state_timer_handler(struct k_timer *timer) {
morsel_handle_state_timeout(k_uptime_get() * 1000); // Convert ms to us
}
static void morsel_gap_timer_handler(struct k_timer *timer) {
morsel_handle_gap_timeout(k_uptime_get() * 1000); // Convert ms to us
}
// Timer interface functions for morsel library
static void morsel_stop_timer(void *handle) {
struct k_timer *timer = (struct k_timer *)handle;
k_timer_stop(timer);
}
static void morsel_start_timer(void *handle, int64_t timeout_us) {
struct k_timer *timer = (struct k_timer *)handle;
k_timer_start(timer, K_USEC(timeout_us), K_NO_WAIT);
}// Decoded text stream: letters/digits/punctuation, ' ' on word gaps,
// 0 for an unrecognized element sequence
static void key_char_callback(void *callback_arg, int key_id,
char c, int64_t timestamp) {
if (c == 0) {
printk("?"); // bad sequence
} else {
printk("%c", c);
}
}
// Optional: individual element events, e.g. for a sidetone or echo display
static void key_element_callback(void *callback_arg, int key_id,
enum morsel_element_t element, int64_t timestamp) {
switch (element) {
case MORSEL_KEY_DOWN: /* sidetone on */ break;
case MORSEL_KEY_UP: /* sidetone off */ break;
case MORSEL_DOT: printk("."); break;
case MORSEL_DASH: printk("-"); break;
case MORSEL_LETTER_END: printk(" "); break;
case MORSEL_WORD_END: printk(" / "); break;
}
}static struct morsel_t *morse_key = NULL;
static void init_morse_key(void) {
// Initialize timers
k_timer_init(&morsel_state_timer, morsel_state_timer_handler, NULL);
k_timer_init(&morsel_gap_timer, morsel_gap_timer_handler, NULL);
// Configure the morsel library
int64_t unit_us = DEFAULT_MORSEL_UNIT_TIME_US; // 100 ms/unit ≈ 12 WPM
struct morsel_config_t config = {
.state_timer_handle = &morsel_state_timer,
.gap_timer_handle = &morsel_gap_timer,
.stop_timer = morsel_stop_timer,
.start_timer = morsel_start_timer,
.debounce_time_us = DEFAULT_MORSEL_DEBOUNCE_TIME_US,
.dash_time_us = MORSEL_DASH_TIME_US(unit_us),
.letter_gap_time_us = MORSEL_LETTER_GAP_TIME_US(unit_us),
.word_gap_time_us = MORSEL_WORD_GAP_TIME_US(unit_us),
};
morsel_init(&config);
// Add the key
struct morsel_params_t key_params = {
.key_id = 0,
.callback_arg = NULL,
.char_callback = key_char_callback,
.element_callback = key_element_callback,
};
morse_key = morsel_add(&key_params);
}// GPIO interrupt callback (called from ISR context)
static void key_gpio_callback(const struct device *dev,
struct gpio_callback *cb, uint32_t pins) {
if (!morse_key) {
return;
}
// Read current key state (inverted since the key is active low);
// sample semantics match misclick: 0 = down, non-zero = up
int key_state = !gpio_pin_get_dt(&key_gpio);
// Send key event to morsel library
// Convert milliseconds to microseconds for timestamp
morsel_handle_input_event(morse_key, key_state, k_uptime_get() * 1000);
}The code table is also exposed directly (no morsel_init required):
char seq[8];
int n = morsel_encode('R', seq, sizeof(seq)); // seq = ".-.", n = 3
char c = morsel_decode("-.-"); // c = 'K', 0 if unrecognizedPlayback timing is the caller's job: 1 unit on per dot, 3 on per dash, 1 off between elements, 3 off between letters, 7 off between words.
#define DEFAULT_MORSEL_DEBOUNCE_TIME_US 5000 // 5ms debounce
#define DEFAULT_MORSEL_UNIT_TIME_US 100000 // 100ms unit ≈ 12 WPM
#define MORSEL_DASH_TIME_US(unit_us) (2 * (unit_us))
#define MORSEL_LETTER_GAP_TIME_US(unit_us) (2 * (unit_us))
#define MORSEL_WORD_GAP_TIME_US(unit_us) (5 * (unit_us))Per-key debounce override via morsel_params_t.debounce_time_us: 0 inherits
the global value, a positive value overrides it, a negative value disables
software debounce (for inputs already debounced in hardware).
The library requires two timers with microsecond precision:
- State timer: Used for input debouncing
- Gap timer: Used for letter/word gap detection
Requirements:
- Create two separate timer instances (for state and gap timers)
- Configure timers for one-shot mode (fire once, then stop)
- Implement microsecond-precision timing
- Call the library timeout handlers from your timer interrupt callbacks
Host-side tests (simulated timers, hand-keyed input) run with CTest:
cmake -B build && cmake --build build && ctest --test-dir build- Per key: ~60 bytes
- Global state: ~80 bytes + a 106-byte code table in ROM
- No dynamic allocation after initialization
This library is not thread-safe. If using in a multi-threaded environment, provide your own synchronization.
Licensed under the Apache License, Version 2.0. See LICENSE for the full license text.