4. UART驱动封装(上):波特率配置、数据帧格式、环形缓冲区设计与实现
UART,说白了就是串口。做单片机开发的,没人离得开它。调试打印、跟传感器通信、和上位机握手……哪样都少不了它。
但很多新手写UART驱动,就是调个库函数完事。一旦波特率对不上、数据丢帧、缓冲区溢出,立马抓瞎。我个人习惯是,把UART驱动做成一个独立的硬件抽象层,上层应用只管读写,底层细节全部封装好。
今天咱们就先把UART驱动的三个核心模块拆开讲:波特率怎么配才准、数据帧格式怎么设、环形缓冲区怎么设计才不掉数据。
4.1 波特率配置:不是随便填个数就行
波特率就是每秒传输的比特数。比如9600波特率,就是每秒传9600个bit。但问题在于——单片机的主频不是为波特率量身定做的,你得通过分频算出一个接近的值。
我遇到过最坑的一次,是某款国产芯片的主频是16MHz,我想配115200波特率。按公式算出来分频系数是个小数,取整后实际波特率变成了115384,误差0.16%。看起来不大对吧?但连续传1000个字节,最后一位的累积误差直接导致帧同步失败。
波特率计算公式(以常见的USART为例):
// 波特率计算公式
// 过采样模式:OVER8 = 0 时(16倍过采样)
// BaudRate = Fck / (8 * (2 - OVER8) * USARTDIV)
//
// 简化版(OVER8=0):
// USARTDIV = Fck / (16 * BaudRate)
// 示例:Fck = 72MHz,目标波特率 115200
uint32_t usartdiv = 72000000 / (16 * 115200); // = 39.0625
// 取整后 USARTDIV = 39,实际波特率 = 72000000 / (16 * 39) = 115384
// 误差 = (115384 - 115200) / 115200 = 0.16% ✅ 可用
| 目标波特率 | 主频(MHz) | 理论分频值 | 实际分频值 | 实际波特率 | 误差(%) | 结论 |
|---|---|---|---|---|---|---|
| 9600 | 8 | 52.083 | 52 | 9615 | 0.16 | ✅ 可用 |
| 115200 | 72 | 39.062 | 39 | 115384 | 0.16 | ✅ 可用 |
| 115200 | 16 | 8.680 | 9 | 111111 | -3.55 | ❌ 不可用 |
| 250000 | 72 | 18.000 | 18 | 250000 | 0.00 | ✅ 完美 |
4.2 数据帧格式:别让双方"对不上暗号"
UART的数据帧,就像两个人约定好怎么说话。你得告诉对方:多少位数据、什么校验、几个停止位。
标准UART帧结构长这样:
起始位(1bit) + 数据位(5~9bit) + 校验位(0/1bit) + 停止位(0.5/1/1.5/2bit)
最常见配置:1 + 8 + 0 + 1 = 10bit 一帧
我见过最离谱的bug,是两边的工程师一个配了7位数据+偶校验,另一个配了8位数据+无校验。两边都能发能收,但数据全是乱码。排查了三天才发现是帧格式没对齐。
配置帧格式时,记住三个要点:
- 数据位:绝大多数场景用8位。7位用于老式ASCII传输,9位用于多机通信。
- 校验位:无校验最常用。偶校验用于对数据完整性有要求的场景。奇校验用得少。
- 停止位:1位停止位是标配。2位停止位用于低速或噪声环境,给接收端多点时间处理。
4.3 环形缓冲区:UART驱动的"心脏"
没有环形缓冲区的UART驱动,就像没有蓄水池的自来水厂——来一股水你就得接一股,慢了就洒一地。
环形缓冲区(Ring Buffer)说白了就是一个固定大小的数组,加上两个指针:写指针(生产者)和读指针(消费者)。写指针追着读指针跑,永远不会溢出——除非你写得太快,把读指针给"套圈"了。
我早期做的一个项目,用环形缓冲区处理GPS数据。每秒10条NMEA语句,每条最长82字节。如果不用环形缓冲区,中断里处理不过来,直接丢帧。用了环形缓冲区后,中断只管往缓冲区塞数据,主循环慢慢解析,再也没丢过数据。
4.3.1 环形缓冲区数据结构
/* 环形缓冲区结构体 */
typedef struct {
uint8_t *buffer; // 缓冲区起始地址
uint32_t size; // 缓冲区大小(必须是2的幂)
uint32_t head; // 写指针(生产者)
uint32_t tail; // 读指针(消费者)
} ring_buffer_t;
为什么size必须是2的幂?因为可以用位运算代替取模,速度更快。你想想看,中断服务程序里每来一个字节都要做一次取模运算,累积起来开销不小。
/* 用位运算实现指针递增(size必须是2的幂) */
#define RING_BUFFER_MASK(size) (size - 1)
static inline void ring_buffer_advance_head(ring_buffer_t *rb) {
rb->head = (rb->head + 1) & RING_BUFFER_MASK(rb->size);
}
static inline void ring_buffer_advance_tail(ring_buffer_t *rb) {
rb->tail = (rb->tail + 1) & RING_BUFFER_MASK(rb->size);
}
4.3.2 核心操作:写入与读取
/* 向环形缓冲区写入一个字节 */
int ring_buffer_put(ring_buffer_t *rb, uint8_t data) {
uint32_t next_head = (rb->head + 1) & RING_BUFFER_MASK(rb->size);
// 检查是否满(写指针追上读指针)
if (next_head == rb->tail) {
return -1; // 缓冲区已满
}
rb->buffer[rb->head] = data;
rb->head = next_head;
return 0;
}
/* 从环形缓冲区读取一个字节 */
int ring_buffer_get(ring_buffer_t *rb, uint8_t *data) {
// 检查是否空(读写指针重合)
if (rb->head == rb->tail) {
return -1; // 缓冲区为空
}
*data = rb->buffer[rb->tail];
rb->tail = (rb->tail + 1) & RING_BUFFER_MASK(rb->size);
return 0;
}
4.3.3 在UART中断中的典型用法
/* 全局环形缓冲区实例 */
static ring_buffer_t uart_rx_buffer;
static uint8_t uart_rx_data[256]; // 256字节缓冲区
/* UART接收中断服务程序 */
void USART1_IRQHandler(void) {
if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
uint8_t data = USART_ReceiveData(USART1);
// 中断中只做一件事:把数据塞进环形缓冲区
if (ring_buffer_put(&uart_rx_buffer, data) != 0) {
// 缓冲区满了,可以置一个溢出标志
// 或者丢弃旧数据(取决于你的策略)
}
}
}
/* 主循环中处理接收数据 */
void main_loop(void) {
uint8_t byte;
while (1) {
// 从环形缓冲区取数据,非阻塞
if (ring_buffer_get(&uart_rx_buffer, &byte) == 0) {
// 处理接收到的字节
process_received_byte(byte);
}
// 做其他事情...
}
}
4.4 本章知识结构
下面这张图把UART驱动封装的三个核心模块串起来了。你可以看到,波特率配置和数据帧格式是"初始化阶段"的事,而环形缓冲区是"运行时"的核心。
4.5 写在最后
UART驱动封装的第一步,就是把这三个基础模块做扎实。波特率配准了,帧格式对上了,环形缓冲区设计好了,剩下的就是把这些模块组合成一个完整的驱动层。
嗯,这里要注意一点:环形缓冲区的大小不是越大越好。我见过有人给串口缓冲区分配4KB,结果RAM不够用。一般256字节对于115200波特率已经绰绰有余,除非你的主循环处理速度极慢。