【前言】
最近入手纳芯微NS800开发板一块。这一篇详解串口不定长接收。
【硬件简介】
芯片:NovoSense NS800RT7P65(Cortex-M7 双核)
开发环境:Keil MDK5
功能:利用 UART IDLE LINE 中断实现任意长度数据的接收,接收完成后原样回传
波特率:230400 8N1
【整体架构】

【环形缓冲区】
#define UART_RXBUF_SIZE 256U /* 循环接收缓冲区大小 */ static uint8_t rxBuf[UART_RXBUF_SIZE]; static volatile uint16_t rxHead = 0U; /* 写指针:RX ISR 更新 */ static volatile uint16_t rxTail = 0U; /* 读指针:主循环更新 */
rxHead == rxTail 表示缓冲区空
写入时 rxHead = (rxHead + 1) % SIZE
读出时 rxTail = (rxTail + 1) % SIZE
缓冲区满时(新字节到达但队列已满)直接丢弃该字节
【中断配置详解】
1、 NVIC 层:向量号分配
NS800RT7P65 的 UART2 有 三个独立中断源,分配在两个向量号上:

IDLE 中断和 RX 共用向量 200,这意味着在同一个 ISR 里需要同时处理两种中断。
/* 注册两个向量 */ Interrupt_register(UART2_TX_IRQn, &UART2_TX_handler); // 向量 199 Interrupt_register(UART2_RX_IRQn, &UART2_RX_handler); // 向量 200(RX + IDLE 共用) Interrupt_enable(UART2_TX_IRQn); Interrupt_enable(UART2_RX_IRQn);
2、 UART 外设层:中断使能
初始化时只打开 RX 中断,IDLE 中断由 RX ISR 在收到第1字节后动态使能:
/* 初始状态:只开 RX 中断 */ UART_enableInterrupt(uart, UART_INT_RX_DATA_REG_FULL); // 每字节触发 /* IDLE 中断暂不使能,等收到第1字节再开 */
为什么要收到第1字节才开 IDLE?
如果初始化时就打开 IDLE 中断,UART 未开始接收时线路默认处于空闲状态,会立即触发一次 IDLE 中断,导致误判。
3、 接收完成检测:空闲中断原理
/* 设置空闲字符计数 = 1,即检测到 1 个字符时间的空闲后产生 IDLE */ UART_setRxIdleCharacter(uart, UART_IDLE_CHARACTER_CNT1);
IDLE_LINE 检测机制:当 RX 线从高电平(数据位的最后一位)变为低电平并持续 1 个字符时间(包括起始位 + 数据位 + 停止位)时,硬件自动置位 IDLE 标志。
以 230400 波特率、8N1 为例:
• 每字符时间 = 10 bit ÷ 230400 ≈ 43.4μs
• 数据帧之间的间隔超过 43.4μs 即触发 IDLE
4、 RX 中断 handler(向量 200)
向量 200 同时承载 RX_DATA_REG_FULL 和 IDLE_LINE,在同一个 handler 里通过 UART_getIntMode() + UART_getStatusFlag() 区分:
/*******************************************************************************
* UART RX 中断处理函数
*
* UART2_RX_IRQn 同时承载 RX_DATA_REG_FULL 和 IDLE_LINE 两种中断源。
* 在这里分别检查两种中断的 enable 状态和 flag 状态,模仿原驱动的双重检查风格。
*
* 接收流程:
* 1. 收到第1字节 -> 打开 IDLE 中断
* 2. 线路空闲 1 字符时间 -> IDLE 触发 -> rxDone=1 -> 关闭 IDLE
* 3. 主循环取出数据 -> 启动回传
* 4. 回传完成后重新打开 RX+IDLE 中断
******************************************************************************/
void UART2_RX_handler(void)
{
/* ---- IDLE LINE 检测(与 RX 共用同一向量) ---- */
if (UART_getIntMode(uart, UART_INT_IDLE_LINE))
{
if (UART_getStatusFlag(uart, UART_IDLE_LINE_DETECT))
{
/* 读空 FIFO(可能还有残留字节)*/
while (UART_getRxFifoStatus(uart) > UART_FIFO_RX0)
{
uint16_t next = (rxHead + 1U) % UART_RXBUF_SIZE;
if (next != rxTail)
{
rxBuf[rxHead] = UART_readChar(uart);
rxHead = next;
}
else
{
(void)UART_readChar(uart);
}
}
rxDone = 1U;
rxActive = 0U;
UART_disableInterrupt(uart, UART_INT_IDLE_LINE);
}
}
/* ---- RX DATA REG FULL(每字节到达时触发) ---- */
if (UART_getIntMode(uart, UART_INT_RX_DATA_REG_FULL))
{
if (UART_getStatusFlag(uart, UART_RX_DATA_REG_FULL))
{
while (UART_getRxFifoStatus(uart) > UART_FIFO_RX0)
{
uint16_t next = (rxHead + 1U) % UART_RXBUF_SIZE;
if (next != rxTail)
{
rxBuf[rxHead] = UART_readChar(uart);
rxHead = next;
}
else
{
(void)UART_readChar(uart);
}
}
if (!rxActive)
{
rxActive = 1U;
UART_enableInterrupt(uart, UART_INT_IDLE_LINE);
}
}
}
}双重检查的原因:UART_getIntMode() 检查中断是否在 NVIC 层面被使能,UART_getStatusFlag() 检查硬件状态标志位。两层检查确保只有真正使能的中断源才会被处理,防止误触发。
5 、TX 中断 handler(向量 199)
/*******************************************************************************
* UART TX 中断处理函数
******************************************************************************/
void UART2_TX_handler(void)
{
/* TX DATA REG EMPTY:TX FIFO 至少空 1 字节,填充它 */
if (UART_getIntMode(uart, UART_INT_TX_DATA_REG_EMPTY))
{
if (UART_getStatusFlag(uart, UART_TX_DATA_REG_EMPTY))
{
while (UART_isSpaceAvailable(uart) && txIdx < txCount)
{
UART_writeChar(uart, txBuf[txIdx++]);
}
/* 全部发送完毕?切换到 TX_COMPLETE 等待发送器排空 */
if (txIdx >= txCount)
{
UART_disableInterrupt(uart, UART_INT_TX_DATA_REG_EMPTY);
UART_enableInterrupt(uart, UART_INT_TX_COMPLETE);
}
}
}
/* TX COMPLETE:TX FIFO + 移位器均空,发送彻底完成 */
if (UART_getIntMode(uart, UART_INT_TX_COMPLETE))
{
if (UART_getStatusFlag(uart, UART_TX_COMPLETE))
{
UART_disableInterrupt(uart, UART_INT_TX_COMPLETE);
/* 释放总线:重新打开 RX 和 IDLE 中断,准备下一次接收
* 注意:rxTail 和 rxHead 必须同时清零,防止残留数据与新数据混叠 */
txActive = 0U;
rxActive = 0U;
rxTail = 0U;
rxHead = 0U;
UART_enableInterrupt(uart, UART_INT_RX_DATA_REG_FULL);
UART_enableInterrupt(uart, UART_INT_IDLE_LINE);
}
}
}【状态机总结】

【主循环与中断的协作】
/**
* @brief 主函数:初始化后等待接收,接收完毕后回传数据
*/
int main(void)
{
Device_init();
Device_unlockPeriphReg();
Board_init();
uart_init(uart);
printf("UART IDLE-line variable-length receive demo!\r\n");
printf("Baud:230400 8N1. Send data, I'll echo it back.\r\n");
while (1)
{
/* 等待一帧接收完毕(IDLE 检测到 rxDone=1)*/
if (rxDone)
{
uint16_t len = 0U;
rxDone = 0U;
/* 从循环缓冲区取出数据到发送缓冲区 */
while (rxHead != rxTail)
{
txBuf[len++] = rxBuf[rxTail];
rxTail = (rxTail + 1U) % UART_RXBUF_SIZE;
}
if (len > 0U)
{
/* 打印接收到的数据(ASCII,逐字符打印避免 \0 覆盖问题)*/
printf("[RX %u]: ", len);
for (uint16_t i = 0U; i < len; i++)
{
printf("%c", txBuf[i]);
}
printf("\r\n");
/* 启动回传 */
txIdx = 0U;
txCount = len;
txActive = 1U;
/* 发送期间禁止接收(防止 TX 回传数据触发新的 IDLE)*/
UART_disableInterrupt(uart, UART_INT_RX_DATA_REG_FULL);
UART_disableInterrupt(uart, UART_INT_IDLE_LINE);
UART_enableInterrupt(uart, UART_INT_TX_DATA_REG_EMPTY);
}
}
}
}【FIFO 配置】

【硬件连接】

【验证】
使用串口助手连接到开发板,打开串口助手,发送任意长度字符串均可接收无误:

【总结】
在串口使用中,不定长接收是非常常见,因此搞通不定长接收的意义非常有必要。
我要赚赏金
