SKILL.md 元数据
---
name: serial-dma-frame-parser
description: >-
[Scope: global] UART/RS-485/DMX512/Modbus RTU 串口 IDLE+DMA 环形缓冲区接收、表驱动/状态机拆包、短帧/断帧/粘包防御与半双工 DE/RE 方向切换时序。
当用户需要编写或调试串口自定义协议、Modbus RTU、DMX512 灯光协议、解决 RS-485 尾字节截断、DMA 缓冲回绕丢包或粘包错位问题时使用此技能。
---
1. 适用范围
- 技术方向:
protocols-debug - 使用范围 (Scope):
global(全平台 MCU 通用,涵盖 UART / RS-232 / RS-485 / Modbus RTU / DMX512) - 设计目标:ISR 只搬运数据,主循环解析协议,缓冲区静态分配。拆包状态机需要处理粘包、断帧和载荷中的伪帧头,半双工发送需要等待尾字节发完。
2. 接收、拆包与半双工发送
2.1 UART IDLE + DMA Circular 双游标搬运模型
- DMA RX 配置为循环模式(Circular Mode)写入静态数组
s_dmaRxBuf[N]。 - 在 UART 空闲中断(IDLE ISR)、DMA 半传输中断(HT ISR) 和 DMA 传输完成中断(TC ISR) 中调用同一个搬运函数:
- 计算
writePos = N - DMA_GetCurrDataCounter(); - 将
[lastPos, writePos)新增字节(含回绕段)压入软件无锁环形队列RingBuffer,仅更新lastPos = writePos。
- 计算
- 禁止在每次 IDLE 中断里暴停 DMA 重装
CNDTR:停 DMA 重装期间到达的高速串口字节会直接丢失或触发ORE(Overrun Error)。
2.2 状态机拆包与“伪帧头回退扫描”机制
- 主循环从
RingBuffer逐字节喂入拆包状态机(SYNC_HEAD -> LEN -> PAYLOAD -> CRC -> PUBLISH)。 - 关键防死锁设计:
- 帧内字节超时(Inter-Byte Timeout):若状态机停留在非
SYNC_HEAD状态且超过T_timeout(如20 ms)未收齐剩余字节,自动复位状态机并丢弃残帧,防止断帧导致下一帧合法报文被吞。 - CRC 失败滑动重同步:若帧头命中但最终 CRC 校验失败(说明帧头字节恰好是上一帧残片里的载荷数据),不要整包丢弃全部已收字节,应从帧头下一字节重新扫描
SYNC_HEAD。
- 帧内字节超时(Inter-Byte Timeout):若状态机停留在非
2.3 DMX512 协议接收与短帧发布条件
- Break + MAB 帧同步:利用 UART 帧错误标志(
FEFraming Error + 接收数据为0x00)识别>= 88 μs的BREAK低电平脉冲,随后第 1 个正常字节为Start Code(标准调光帧为0x00),后续为Slot 1..512(250 kbps, 8N2)。 - 短帧发布条件:DMX 控台允许发送少于 512 通道的短帧(Short Frame)。不得要求必须收满 513 字节才发布数据;当检测到下一个
BREAK或帧空闲超时,只要已收到合法Start Code == 0x00且已接收通道数 ≥ 本灯具所需的目标起始通道范围,即刻将双缓冲中的完整快照原子发布给调光层;若在中途发生ORE/NE错误或不足本设备通道数,则丢弃本帧保持上一帧输出,绝不把半帧残缺数据直接写向 PWM!
2.4 半双工 RS-485 DE/RE 方向切换与 Modbus 3.5T 边界
- 发送完成后切回接收(等待
TC而非TXE):TXE=1(或 DMA TX 完成)仅代表最后一个字节刚送入移位寄存器,此时停止位尚未从引脚发出!- 必须在
USART_FLAG_TC(Transmission Complete)置位后,才可将 RS-485DE/RE引脚拉低切回接收态,否则最后一个字节(通常是 CRC 高字节)会被物理截断。
- 硬件异常自愈(
ORE / FE / NE):- 在 UART ISR 中必须同时检查并清除
ORE(过载)、FE(帧错误)、NE(噪声错误)标志(在 STM32/N32G45x 上需先读STS/SR再读DAT/DR清除),否则ORE置位后会永久锁死后续接收中断。
- 在 UART ISR 中必须同时检查并清除
可直接复用的无锁
RingBuffer+ 带超时状态机拆包 C99 源码模板见 examples/serial_ring_frame_parser.c。
3. 关键安全与并发约束检查清单
- 单生产者/单消费者无锁队列:
RingBuffer的head(ISR 写)与tail(主循环读)各自只由单一上下文修改,无需关中断即可保证线程安全。 - 长度字段防缓冲区溢出:状态机解析出
LEN字段后,必须立即校验LEN >= MIN_LEN && LEN <= MAX_PAYLOAD_SIZE;越界立即拒绝并退回SYNC_HEAD,严禁按未校验的LEN继续向payload[]写入。 - 上电
DE/RE默认接收态:RS-485 控制脚初始化时先写低电平(接收态)再配输出模式,防止多机总线上电瞬间同时抢占驱动器拉死总线。
4. 验证方法
- 粘包与断帧压力测试:
- 上位机一次发送 3 帧首尾相连的合法报文(粘包),验证解析计数
+3; - 先发送半截伪造帧头,间隔
50 ms(超过帧内超时)后再发 1 帧合法报文,验证合法报文不被吞没。
- 上位机一次发送 3 帧首尾相连的合法报文(粘包),验证解析计数
- 示波器/逻辑分析仪 RS-485 波形核查:
- 测量
TX最后一个停止位结束边沿到DE变低的延迟:确认DE在停止位完整发出后5~50 μs内拉低,既无尾字节截断,也不影响从机最快响应。
- 测量
代码范例:examples/serial_ring_frame_parser.c
/**
* @file serial_ring_frame_parser.c
* @brief Lock-free SPSC RingBuffer + Timeout-protected Frame Parser (C99).
*
* Frame Format Example:
* [0xAA] [0x55] [CMD:1B] [LEN:1B] [PAYLOAD:0..64B] [CRC16_LE:2B]
*/
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#define SERIAL_RING_CAPACITY (256U) /* Must be power of 2 */
#define SERIAL_RING_MASK (SERIAL_RING_CAPACITY - 1U)
#define FRAME_MAX_PAYLOAD (64U)
#define FRAME_INTERBYTE_TIMEOUT (20U) /* ms */
typedef struct
{
uint8_t buf[SERIAL_RING_CAPACITY];
volatile uint16_t head; /* Written only by producer (ISR) */
volatile uint16_t tail; /* Written only by consumer (Main loop) */
volatile uint32_t dropCount;
} SerialRingBuffer;
typedef enum
{
PARSE_SYNC_AA = 0,
PARSE_SYNC_55,
PARSE_CMD,
PARSE_LEN,
PARSE_PAYLOAD,
PARSE_CRC_LO,
PARSE_CRC_HI
} ParseState;
typedef struct
{
ParseState state;
uint8_t cmd;
uint8_t len;
uint8_t payIdx;
uint8_t payload[FRAME_MAX_PAYLOAD];
uint16_t rxCrc;
uint32_t lastByteTick;
uint32_t validFrameCount;
uint32_t crcErrorCount;
uint32_t timeoutErrorCount;
} FrameParserCtx;
/* ---- Lock-Free SPSC Ring Buffer ---- */
void SerialRing_Init(SerialRingBuffer *rb)
{
memset(rb, 0, sizeof(*rb));
}
/* Call from UART IDLE / DMA HT / DMA TC ISR */
void SerialRing_PushFromIsr(SerialRingBuffer *rb, const uint8_t *data, uint16_t len)
{
uint16_t h = rb->head;
uint16_t t = rb->tail;
for (uint16_t i = 0U; i < len; i++)
{
uint16_t next = (uint16_t)((h + 1U) & SERIAL_RING_MASK);
if (next == t)
{
rb->dropCount++;
break;
}
rb->buf[h] = data[i];
h = next;
}
rb->head = h;
}
/* Call from Main Loop */
bool SerialRing_Pop(SerialRingBuffer *rb, uint8_t *outByte)
{
uint16_t t = rb->tail;
if (t == rb->head)
{
return false;
}
*outByte = rb->buf[t];
rb->tail = (uint16_t)((t + 1U) & SERIAL_RING_MASK);
return true;
}
/* ---- CRC-16/MODBUS ---- */
static uint16_t CalcCrc16Modbus(const uint8_t *data, uint16_t len, uint16_t initCrc)
{
uint16_t crc = initCrc;
for (uint16_t i = 0U; i < len; i++)
{
crc ^= (uint16_t)data[i];
for (uint8_t b = 0U; b < 8U; b++)
{
if ((crc & 0x0001U) != 0U)
{
crc = (uint16_t)((crc >> 1U) ^ 0xA001U);
}
else
{
crc >>= 1U;
}
}
}
return crc;
}
/* ---- Non-Blocking Frame Parser with Inter-Byte Timeout ---- */
void FrameParser_Init(FrameParserCtx *ctx)
{
memset(ctx, 0, sizeof(*ctx));
ctx->state = PARSE_SYNC_AA;
}
void FrameParser_Process(FrameParserCtx *ctx,
SerialRingBuffer *rb,
uint32_t currentTickMs,
void (*onFrame)(uint8_t cmd, const uint8_t *payload, uint8_t len))
{
uint8_t byte = 0U;
/* Check inter-byte timeout when mid-frame */
if ((ctx->state != PARSE_SYNC_AA) &&
((uint32_t)(currentTickMs - ctx->lastByteTick) >= FRAME_INTERBYTE_TIMEOUT))
{
ctx->state = PARSE_SYNC_AA;
ctx->timeoutErrorCount++;
}
while (SerialRing_Pop(rb, &byte))
{
ctx->lastByteTick = currentTickMs;
switch (ctx->state)
{
case PARSE_SYNC_AA:
if (byte == 0xAAU) { ctx->state = PARSE_SYNC_55; }
break;
case PARSE_SYNC_55:
if (byte == 0x55U) { ctx->state = PARSE_CMD; }
else if (byte == 0xAAU) { ctx->state = PARSE_SYNC_55; } /* Consecutive 0xAA */
else { ctx->state = PARSE_SYNC_AA; }
break;
case PARSE_CMD:
ctx->cmd = byte;
ctx->state = PARSE_LEN;
break;
case PARSE_LEN:
if (byte > FRAME_MAX_PAYLOAD)
{
ctx->state = PARSE_SYNC_AA;
ctx->crcErrorCount++;
}
else
{
ctx->len = byte;
ctx->payIdx = 0U;
ctx->state = (byte == 0U) ? PARSE_CRC_LO : PARSE_PAYLOAD;
}
break;
case PARSE_PAYLOAD:
ctx->payload[ctx->payIdx++] = byte;
if (ctx->payIdx >= ctx->len)
{
ctx->state = PARSE_CRC_LO;
}
break;
case PARSE_CRC_LO:
ctx->rxCrc = (uint16_t)byte;
ctx->state = PARSE_CRC_HI;
break;
case PARSE_CRC_HI:
{
ctx->rxCrc |= (uint16_t)((uint16_t)byte << 8U);
uint8_t hdr[4] = { 0xAAU, 0x55U, ctx->cmd, ctx->len };
uint16_t calc = CalcCrc16Modbus(hdr, 4U, 0xFFFFU);
calc = CalcCrc16Modbus(ctx->payload, ctx->len, calc);
if (calc == ctx->rxCrc)
{
ctx->validFrameCount++;
if (onFrame != NULL)
{
onFrame(ctx->cmd, ctx->payload, ctx->len);
}
}
else
{
ctx->crcErrorCount++;
}
ctx->state = PARSE_SYNC_AA;
break;
}
default:
ctx->state = PARSE_SYNC_AA;
break;
}
}
}