嵌入式Morse码LED闪烁库:非阻塞状态机实现
1. MorseGenerator2 库概述
MorseGenerator2 是一个轻量级、可移植的嵌入式 Morse 码生成库,专为资源受限的微控制器设计。其核心目标并非通用通信协议栈,而是以最小化代码体积与运行时开销为前提,实现LED 指示灯的精确摩尔斯电码闪烁控制。该库不依赖操作系统(bare-metal ready),亦不强制要求 HAL 或 CMSIS 封装,可直接操作 GPIO 寄存器或通过抽象层接入,适用于 STM32F0/F1/F4、nRF52、ESP32、RP2040 等主流 MCU 平台。
与早期 MorseGenerator 相比,MorseGenerator2 的关键演进在于:
- 状态机驱动替代轮询延时:摒弃
HAL_Delay()或usleep()类阻塞调用,采用非阻塞有限状态机(FSM)管理字符编码、点划时序、字符/单词间隔; - 时间参数可配置化:单位时间(
dot_duration_ms)作为唯一基准,所有时序(dash = 3×dot,intra-character = 3×dot,inter-word = 7×dot)均由此推导,避免硬编码; - 零内存动态分配:全部运行时数据驻留于栈或静态变量中,无
malloc/free调用,符合 IEC 61508 / ISO 26262 功能安全基础要求; - 中断安全设计:内部状态更新与 LED 控制分离,支持在 SysTick 中断中驱动状态机,在主循环中注入新字符串,或完全由主循环驱动(无中断依赖)。
该库的工程定位非常明确:它不是 Morse 解码器,不处理输入信号;它不是 UART/I2C 驱动,不负责串行收发;它是一个纯粹的“LED 闪烁时序发生器”。典型应用场景包括:
- 工业设备状态指示(如故障码以 Morse 形式闪灯);
- 教学实验板 Morse 编码演示;
- 低功耗传感器节点通过 LED 发送简短告警(如 “SOS”、“LOW_BAT”);
- 与按钮/旋钮组合构成无屏人机交互(用户按键输入字母,LED 即刻反馈 Morse 编码)。
2. 核心设计原理与状态机详解
2.1 Morse 码时序规范映射
MorseGenerator2 严格遵循国际电信联盟(ITU)《Morse Code Timing》标准,将时序抽象为四个层级:
| 时序类型 | 定义说明 | 相对关系(以dot_duration_ms为单位) |
|---|---|---|
| Dot(·) | 最短信号单元,即 LED 亮起时间 | 1 |
| Dash(–) | 长信号单元,LED 亮起时间 | 3 |
| Intra-character | 同一字符内点/划之间的静默时间 | 1(即一个 dot 时长的熄灭) |
| Inter-character | 字符之间的静默时间 | 3(即三个 dot 时长的熄灭) |
| Inter-word | 单词之间的静默时间 | 7(即七个 dot 时长的熄灭) |
工程考量:选择
dot_duration_ms作为唯一可调参数,而非分别配置所有时序,极大简化了 API 接口与用户配置。例如,设定dot_duration_ms = 100,则 dash 为 300ms,字符间隔为 300ms,单词间隔为 700ms —— 所有值自动同步,避免人为配置冲突。
2.2 非阻塞状态机(FSM)架构
MorseGenerator2 的心脏是三级嵌套状态机,完全消除任何阻塞延时,确保 MCU 在生成 Morse 过程中仍可响应外部事件(如按键、ADC 采样、定时器中断):
typedef enum { MORSE_IDLE, // 空闲:等待新字符串输入 MORSE_PROCESSING, // 处理中:正在解析当前字符 MORSE_TRANSMITTING, // 发送中:正在输出点/划/间隔 } morse_state_t; typedef enum { CHAR_IDLE, // 字符空闲:准备取下一个字符 CHAR_ENCODING, // 字符编码:查表获取点划序列 CHAR_EMITTING, // 字符发送:逐个输出点/划及字符内间隔 } char_state_t; typedef enum { SYMBOL_IDLE, // 符号空闲:准备输出下一个符号(· 或 –) SYMBOL_ON, // 符号点亮:LED 开启,计时开始 SYMBOL_OFF, // 符号熄灭:LED 关闭,计时开始 } symbol_state_t;整个 FSM 的驱动逻辑如下(伪代码):
void morse_update(void) { static uint32_t last_tick = 0; uint32_t now = HAL_GetTick(); // 或自定义毫秒计数器(如 SysTick->VAL 反算) uint32_t elapsed = now - last_tick; switch (morse_state) { case MORSE_IDLE: if (pending_string != NULL) { current_char_ptr = pending_string; morse_state = MORSE_PROCESSING; char_state = CHAR_IDLE; } break; case MORSE_PROCESSING: if (char_state == CHAR_IDLE) { // 取下一个字符,查表得符号序列(如 'A' → {DOT, DASH}) current_symbol_seq = morse_lookup(*current_char_ptr); if (*current_char_ptr == '\0') { morse_state = MORSE_IDLE; return; } current_char_ptr++; symbol_idx = 0; char_state = CHAR_EMITTING; } break; case MORSE_TRANSMITTING: // 根据 symbol_state 和 elapsed 切换 LED 状态与状态 switch (symbol_state) { case SYMBOL_IDLE: if (symbol_idx < seq_length) { current_symbol = current_symbol_seq[symbol_idx]; symbol_state = SYMBOL_ON; led_on(); start_timer(current_symbol == DOT ? dot_ms : dash_ms); } else { // 当前字符发送完毕,进入字符间隔 symbol_state = SYMBOL_OFF; led_off(); start_timer(inter_char_ms); } break; case SYMBOL_ON: if (elapsed >= timer_target) { led_off(); symbol_state = SYMBOL_OFF; start_timer(intra_char_ms); // 点划间间隔 } break; case SYMBOL_OFF: if (elapsed >= timer_target) { if (symbol_idx < seq_length - 1) { symbol_idx++; symbol_state = SYMBOL_IDLE; // 准备下一点/划 } else { // 字符结束,重置并进入下一字符逻辑 char_state = CHAR_IDLE; morse_state = MORSE_PROCESSING; } } break; } break; } last_tick = now; }关键优势:
morse_update()可被置于主循环while(1)中高频调用(如每 1ms 一次),或由 SysTick 中断以固定周期触发。无论哪种方式,CPU 始终保持响应能力,且 LED 时序精度仅取决于系统滴答源(如 HSI/LSI 或外部晶振)的稳定性,而非软件延时抖动。
3. API 接口详解与参数说明
MorseGenerator2 提供极简但完备的 C API,全部函数声明位于morse_generator.h,实现位于morse_generator.c。无头文件依赖,仅需用户实现一个底层 LED 控制钩子。
3.1 初始化与配置接口
| 函数签名 | 功能说明 | 参数说明 | 返回值 |
|---|---|---|---|
void morse_init(uint16_t dot_duration_ms) | 初始化库,设置基准点时长 | dot_duration_ms: 单位点持续时间(毫秒),推荐范围 50–500ms(过小易误判,过大影响响应) | void |
void morse_set_led_control(morse_led_control_t led_func) | 注册 LED 控制回调函数 | led_func: 函数指针,原型为typedef void (*morse_led_control_t)(bool on),on=true表示点亮 LED,on=false表示熄灭 | void |
注意:
morse_set_led_control()必须在morse_init()之后、morse_start()之前调用。若未注册,库将静默失败(LED 不动作),便于调试阶段快速定位硬件连接问题。
3.2 运行时控制接口
| 函数签名 | 功能说明 | 参数说明 | 返回值 |
|---|---|---|---|
bool morse_start(const char* str) | 启动 Morse 发送 | str: 以\0结尾的 ASCII 字符串,仅支持 A-Z、0-9、.,?,',!,/,(),&,:,;=,+,-,_,",$,@ 及空格。非法字符将被跳过(静默忽略) | true:成功入队;false:队列满或str==NULL |
void morse_stop(void) | 立即终止当前发送,清空待发队列 | 无 | void |
bool morse_is_busy(void) | 查询当前是否处于发送状态 | 无 | true:正在发送;false:空闲或已停止 |
3.3 用户必须实现的底层钩子
库不包含任何硬件操作,用户必须提供morse_led_control_t回调的具体实现。以下是 STM32 HAL 与寄存器直驱两种典型实现:
HAL 方式(推荐用于快速验证):
#include "stm32f4xx_hal.h" extern GPIO_TypeDef* LED_GPIO_Port; extern uint16_t LED_Pin; void led_control_hal(bool on) { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, on ? GPIO_PIN_SET : GPIO_PIN_RESET); } // 在 main() 中注册: morse_init(150); morse_set_led_control(led_control_hal);寄存器直驱方式(极致精简,适用于裸机):
// 假设 LED 接在 GPIOA Pin 5,且时钟已使能 #define LED_PORT GPIOA #define LED_PIN GPIO_PIN_5 void led_control_reg(bool on) { if (on) { LED_PORT->BSRR = LED_PIN; // 置位寄存器写 1 点亮 } else { LED_PORT->BSRR = LED_PIN << 16; // 复位寄存器写 1 熄灭 } }重要约束:该回调函数必须是可重入的(reentrant),即允许多次快速调用。因此禁止在其中使用
HAL_Delay()、printf()或任何可能阻塞的函数。其执行时间应控制在微秒级。
4. 字符编码表与扩展机制
4.1 内置 ASCII 到 Morse 映射表
MorseGenerator2 内置一个紧凑的 128 字节查找表(static const uint8_t morse_table[128]),覆盖标准 Morse 字符集。每个字符映射为一个 8-bit 值,高 4-bit 存储符号数量(最多 15 个点/划),低 4-bit 为索引偏移,指向符号序列数组morse_symbols[]。
例如,字符'A'(ASCII 65):
morse_table[65] = 0x12→ 高 4-bit0x1= 1 个符号,低 4-bit0x2= 从morse_symbols[2]开始读取;morse_symbols[2] = SYMBOL_DOT,morse_symbols[3] = SYMBOL_DASH(实际存储为紧凑 bitfield,此处为示意)。
完整支持字符列表(共 40 个):
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 7 8 9 . , ? ' ! / ( ) & : ; = + - _ " $ @ (space)4.2 自定义字符扩展方法
当需支持非标字符(如中文拼音首字母、厂商特定符号)时,可通过修改源码实现扩展:
- 扩展
morse_table数组:在morse_generator.c中找到该数组,将新字符 ASCII 码位置填入对应值; - 追加符号序列:在
morse_symbols数组末尾添加新字符的点划序列(SYMBOL_DOT/SYMBOL_DASH枚举值); - 更新长度常量:修改
#define MAX_MORSE_SYMBOLS与#define MORSE_TABLE_SIZE 128(若需超 ASCII 范围,需重构为哈希或二分查找)。
工程建议:对于仅需少量扩展的场景,更推荐在应用层预处理 —— 将自定义字符串(如
"ERR")在调用morse_start()前,手动替换为标准字符组合(如"E R R"),利用库原生空格分隔逻辑自动插入单词间隔。
5. 典型集成示例:STM32 + FreeRTOS
以下为在 STM32F407 + FreeRTOS 环境下的完整集成方案,展示如何将 MorseGenerator2 与实时操作系统协同工作,实现“按键触发 Morse 发送”的典型人机交互。
5.1 硬件连接与初始化
- LED:GPIOA Pin 5(推挽输出,上拉)
- 按键:GPIOC Pin 13(外部中断,下降沿触发)
// main.c 片段 void SystemClock_Config(void) { // 168MHz HCLK, SysTick 1ms } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); // 初始化 LED 和按键 GPIO morse_init(120); // dot = 120ms → dash=360ms, inter-word=840ms morse_set_led_control(led_control); // 创建 Morse 任务 osThreadDef(morseTask, morse_task, osPriorityNormal, 0, 128); osThreadCreate(osThread(morseTask), NULL); // 启动调度器 osKernelStart(); while(1); }5.2 Morse 任务实现(FreeRTOS)
// morse_task.c #include "morse_generator.h" #include "cmsis_os.h" static const char* pending_msg = NULL; static osMutexId msg_mutex; void morse_task(void const * argument) { osMutexDef(msg_mutex); msg_mutex = osMutexCreate(osMutex(msg_mutex)); for(;;) { // 检查是否有新消息 if (osMutexWait(msg_mutex, 0) == osOK) { if (pending_msg != NULL) { morse_start(pending_msg); pending_msg = NULL; } osMutexRelease(msg_mutex); } // 非阻塞更新状态机 morse_update(); // 1ms 周期,保证时序精度 osDelay(1); } } // 按键中断服务程序(HAL_GPIO_EXTI_Callback) void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == GPIO_PIN_13) { if (osMutexWait(msg_mutex, 0) == osOK) { pending_msg = "SOS"; osMutexRelease(msg_mutex); } } }5.3 低功耗优化技巧
在电池供电场景下,可结合 STOP 模式进一步降低功耗:
// 在 morse_idle 状态下进入低功耗 if (morse_is_busy() == false && pending_msg == NULL) { __WFI(); // 等待中断(按键、RTC 等) }此时需确保 SysTick 或其他唤醒源已配置,且morse_update()在唤醒后立即被调用,以维持时序连续性。
6. 调试与故障排查指南
6.1 常见问题与解决方案
| 现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| LED 完全不闪烁 | 未调用morse_set_led_control();LED 引脚配置错误;morse_init()未调用 | 1. 在led_control回调中添加__BKPT(0)断点2. 用万用表测 LED 引脚电压 | 确保回调注册顺序正确;检查 GPIO 模式(推挽输出)、速度、上下拉 |
| 闪烁节奏混乱(过快/过慢) | HAL_GetTick()返回值异常;dot_duration_ms设置超出合理范围;SysTick 配置错误 | 1. 打印HAL_GetTick()增量是否稳定2. 检查 morse_init()参数值 | 校准 SysTick;将dot_duration_ms设为 100–300 区间;确认HAL_InitTick()已调用 |
| 字符发送不全(如只发 "S" 不发 "O") | 输入字符串未以\0结尾;morse_start()被重复调用覆盖前序;RAM 溢出破坏静态变量 | 1. 在morse_start()入口打印strlen(str)2. 检查调用频次 | 确保字符串字面量或缓冲区以\0结尾;使用morse_is_busy()防重入 |
| 发送过程中系统卡死 | led_control回调中存在阻塞操作(如HAL_Delay);栈溢出(尤其在中断中调用) | 1. 检查回调函数内所有 API 调用 2. 增大任务栈大小(FreeRTOS)或主栈(裸机) | 移除所有阻塞调用;将耗时操作移至任务中;启用栈溢出检测 |
6.2 硬件级时序验证方法
使用示波器探头直接测量 LED 驱动引脚,捕获以下关键波形:
- Dot 波形:高电平宽度应为
dot_duration_ms ± 5%; - Dash 波形:高电平宽度应为
3 × dot_duration_ms ± 5%; - Inter-character 间隔:两个字符首个点之间的低电平宽度应为
3 × dot_duration_ms ± 5%; - Inter-word 间隔:两个单词首个点之间的低电平宽度应为
7 × dot_duration_ms ± 5%。
若实测偏差超过容差,需检查:
- MCU 主频配置是否与
HAL_GetTick()基准一致; - 是否存在高优先级中断频繁抢占,导致
morse_update()调用间隔抖动; led_control()执行时间是否过长(应 < 1μs)。
7. 性能与资源占用分析
在 STM32F407VG(168MHz)平台实测数据:
| 指标 | 数值 | 说明 |
|---|---|---|
| 代码体积(.text) | 1.2 KB | 启用-Os编译,含全部字符表与 FSM |
| RAM 占用(.data + .bss) | 48 bytes | 全局状态变量(无堆内存) |
单次morse_update()执行时间 | 0.8–1.2 μs | 无 LED 操作时;含 LED 操作约 2.5 μs |
| 最高支持 dot_rate | 1000 wpm(words per minute) | 对应dot_duration_ms = 12,需 1MHz+ SysTick 精度 |
对比传统方案:基于
HAL_Delay()的实现,代码体积约 800 bytes,但无法并发处理其他任务,且dot_duration_ms < 10时因HAL_Delay()最小分辨率为 1ms 而失效。MorseGenerator2 以增加 400 bytes 代码为代价,换取了完整的实时性与可配置性。
8. 实际项目经验总结
在某工业振动传感器节点项目中,我们采用 MorseGenerator2 替代原有基于HAL_Delay()的 LED 报警方案,获得显著收益:
- 故障定位效率提升:现场工程师无需连接调试器,仅凭 LED 闪烁模式(如
... --- ...= SOS,.-.. --- .--= LOW)即可判断电池电量、通信中断、传感器失效等 7 类状态,平均排故时间缩短 65%; - 功耗降低:原方案在报警时 CPU 持续运行
while(1)等待延时,现改为morse_update()+__WFI(),待机电流从 12mA 降至 18μA; - 固件维护简化:所有 Morse 逻辑集中于单一库,新增报警类型只需修改字符串常量,无需触碰时序代码。
一个值得强调的工程细节:我们曾尝试将morse_update()放入 SysTick 中断(每 1ms),初期出现 LED 闪烁轻微抖动。经示波器抓取发现,是因 SysTick 中断优先级高于部分外设中断(如 UART),导致 UART 接收偶尔丢帧。最终解决方案是将morse_update()移回主循环,改用 FreeRTOSosTimer以 1ms 周期触发,既保证时序精度,又规避中断嵌套风险。
MorseGenerator2 的价值,不在于它实现了多么复杂的算法,而在于它用最克制的代码,解决了嵌入式开发中最朴素的需求——让 LED 以人类可读的方式,可靠地说话。
