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 targettarget_link_libraries(your_targetPRIVATEmorsel)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 librarystaticstructk_timermorsel_state_timer;
staticstructk_timermorsel_gap_timer;
// Timer callback handlersstaticvoidmorsel_state_timer_handler(structk_timer*timer) {
morsel_handle_state_timeout(k_uptime_get() *1000); // Convert ms to us
}
staticvoidmorsel_gap_timer_handler(structk_timer*timer) {
morsel_handle_gap_timeout(k_uptime_get() *1000); // Convert ms to us
}
// Timer interface functions for morsel librarystaticvoidmorsel_stop_timer(void*handle) {
structk_timer*timer= (structk_timer*)handle;
k_timer_stop(timer);
}
staticvoidmorsel_start_timer(void*handle, int64_ttimeout_us) {
structk_timer*timer= (structk_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 sequencestaticvoidkey_char_callback(void*callback_arg, intkey_id,
charc, int64_ttimestamp) {
if (c==0) {
printk("?"); // bad sequence
} else {
printk("%c", c);
}
}
// Optional: individual element events, e.g. for a sidetone or echo displaystaticvoidkey_element_callback(void*callback_arg, intkey_id,
enummorsel_element_telement, int64_ttimestamp) {
switch (element) {
caseMORSEL_KEY_DOWN: /* sidetone on */break;
caseMORSEL_KEY_UP: /* sidetone off */break;
caseMORSEL_DOT: printk("."); break;
caseMORSEL_DASH: printk("-"); break;
caseMORSEL_LETTER_END: printk(" "); break;
caseMORSEL_WORD_END: printk(" / "); break;
}
}staticstructmorsel_t*morse_key=NULL;
staticvoidinit_morse_key(void) {
// Initialize timersk_timer_init(&morsel_state_timer, morsel_state_timer_handler, NULL);
k_timer_init(&morsel_gap_timer, morsel_gap_timer_handler, NULL);
// Configure the morsel libraryint64_tunit_us=DEFAULT_MORSEL_UNIT_TIME_US; // 100 ms/unit ≈ 12 WPMstructmorsel_config_tconfig= {
.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 keystructmorsel_params_tkey_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)staticvoidkey_gpio_callback(conststructdevice*dev,
structgpio_callback*cb, uint32_tpins) {
if (!morse_key) {
return;
}
// Read current key state (inverted since the key is active low);// sample semantics match misclick: 0 = down, non-zero = upintkey_state= !gpio_pin_get_dt(&key_gpio);
// Send key event to morsel library// Convert milliseconds to microseconds for timestampmorsel_handle_input_event(morse_key, key_state, k_uptime_get() *1000);
}The code table is also exposed directly (no morsel_init required):
charseq[8];
intn=morsel_encode('R', seq, sizeof(seq)); // seq = ".-.", n = 3charc=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.
#defineDEFAULT_MORSEL_DEBOUNCE_TIME_US 5000 // 5ms debounce
#defineDEFAULT_MORSEL_UNIT_TIME_US 100000 // 100ms unit ≈ 12 WPM
#defineMORSEL_DASH_TIME_US(unit_us) (2 * (unit_us))
#defineMORSEL_LETTER_GAP_TIME_US(unit_us) (2 * (unit_us))
#defineMORSEL_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.