4、UART驱动封装:波特率配置、数据格式设置、中断接收与轮询发送、FIFO管理
UART,说白了就是串口。做嵌入式的人,没人离得开它。
调试要它,通信要它,跟蓝牙模块、GPS模块打交道还得靠它。我做了十几年汽车电子,UART驱动写过不下二十个版本。每次移植平台,最头疼的就是这部分——看似简单,坑却不少。
今天咱们就把UART驱动封装这件事聊透。从波特率配置到FIFO管理,一个环节都不放过。
4.1 波特率配置——别小看这个数字
波特率,就是每秒传多少位。听起来简单,对吧?
但我在项目中遇到过一件事:两个ECU用UART通信,波特率都设成了115200,结果数据全是乱码。查了两天,最后发现一个芯片的主频是16MHz,另一个是16.384MHz。分频算下来,实际波特率差了将近3%。
嗯,这就是问题所在。
核心要点:波特率配置的本质,是用系统时钟分频得到目标波特率。分频器精度不够,就会产生误差。
一般UART模块的波特率计算公式是这样的:
// 假设时钟源频率为 f_clk,目标波特率为 baud
// 分频系数 divisor = f_clk / (16 * baud)
uint32_t UART_CalcDivisor(uint32_t f_clk, uint32_t baud)
{
uint32_t divisor = f_clk / (16 * baud);
// 有些芯片需要 -1,看具体手册
return divisor;
}
你想想看,如果f_clk是16MHz,目标115200,算出来divisor是8.68。取整后实际波特率就变成了16M/(16*8)=125000,误差8.5%。这通信能稳定才怪。
我的建议:配置波特率时,先算误差。误差超过2%就要换一个波特率,或者调整时钟源。我一般会准备一个表格,把常用波特率对应的分频值和误差都列出来。
| 目标波特率 | 16MHz分频值 | 实际波特率 | 误差 |
|---|---|---|---|
| 9600 | 104 | 9615 | 0.16% |
| 19200 | 52 | 19230 | 0.16% |
| 115200 | 8 | 125000 | 8.5% |
| 115200 | 9 | 111111 | 3.55% |
看到了吧?16MHz下跑115200,怎么调误差都大。这时候要么换一个晶振,要么用更高精度的分频模式。
4.2 数据格式设置——8N1是标配,但不是万能
数据位、校验位、停止位,这三个参数组成了UART的数据格式。最常见的组合是8N1:8个数据位,无校验,1个停止位。
但汽车电子里,有时候会用7E1或者8O1。为什么?
我曾经在一个CAN网关项目里,需要跟一个老旧的仪表盘通信。那家伙只认7位数据加偶校验。我一开始没注意,直接用了8N1,结果仪表盘死活不响应。查了三天手册才发现问题。
注意:数据格式必须通信双方完全一致。少一位、多一位都不行。特别是校验位,很多新手容易忽略。
封装驱动时,我习惯用一个结构体来管理这些参数:
typedef struct {
uint32_t baudRate; // 波特率
uint8_t dataBits; // 数据位:5,6,7,8
uint8_t stopBits; // 停止位:1,2
uint8_t parity; // 校验:0=无, 1=奇校验, 2=偶校验
} UART_ConfigType;
初始化函数就接收这个结构体,然后根据参数配置寄存器。这样移植的时候,只需要改结构体里的值,不用动驱动代码。
4.3 中断接收与轮询发送——各司其职
接收用中断,发送用轮询。这是我个人比较推荐的做法。
为什么?
接收是异步的。你不知道数据什么时候来。用中断,来了就处理,不会丢数据。发送是同步的。你知道要发什么,轮询等发送完成就行,简单可靠。
中断接收的设计要点:
- 中断服务程序要短,只做数据搬运
- 把数据放到环形缓冲区里,交给主循环处理
- 注意临界区保护,防止中断和主循环冲突
来看一个典型的中断接收实现:
// 环形缓冲区
static uint8_t rx_buffer[RX_BUFFER_SIZE];
static volatile uint16_t rx_head = 0;
static volatile uint16_t rx_tail = 0;
// UART接收中断服务函数
void UART_RX_IRQHandler(void)
{
uint8_t data;
// 读取接收数据寄存器
data = UART->DR;
// 计算下一个写入位置
uint16_t next = (rx_head + 1) % RX_BUFFER_SIZE;
// 如果缓冲区没满,就写入
if (next != rx_tail) {
rx_buffer[rx_head] = data;
rx_head = next;
}
// 缓冲区满了?那就丢数据。实际项目中要加错误计数
}
轮询发送就简单多了:
void UART_SendByte(uint8_t data)
{
// 等待发送缓冲区空
while (!(UART->SR & UART_SR_TXE));
// 写入数据
UART->DR = data;
// 等待发送完成(有些芯片需要这步)
while (!(UART->SR & UART_SR_TC));
}
你可能会问:为什么发送不用中断?
其实也可以用。但轮询发送的代码更简单,而且发送数据时CPU本来就要等,不如直接轮询。中断发送适合大数据量、高吞吐的场景。普通应用,轮询就够了。
4.4 FIFO管理——别让数据丢了
FIFO,先进先出。说白了就是个环形缓冲区。
UART驱动里,FIFO管理是最容易出问题的地方。我见过太多因为缓冲区溢出导致的数据丢失案例。
避坑指南:我曾经在一个项目里,接收缓冲区只设了64字节。结果对方一次发了200字节的数据包,直接溢出。从那以后,我定了个规矩:缓冲区大小至少是最大数据包的2倍。
FIFO的核心操作就三个:写入、读取、判空判满。
typedef struct {
uint8_t* buffer; // 缓冲区指针
uint16_t size; // 缓冲区大小
volatile uint16_t head; // 写入位置
volatile uint16_t tail; // 读取位置
} FIFO_Buffer_t;
// 初始化FIFO
void FIFO_Init(FIFO_Buffer_t* fifo, uint8_t* buf, uint16_t size)
{
fifo->buffer = buf;
fifo->size = size;
fifo->head = 0;
fifo->tail = 0;
}
// 写入一个字节
bool FIFO_Write(FIFO_Buffer_t* fifo, uint8_t data)
{
uint16_t next = (fifo->head + 1) % fifo->size;
// 缓冲区满了?
if (next == fifo->tail) {
return false; // 溢出
}
fifo->buffer[fifo->head] = data;
fifo->head = next;
return true;
}
// 读取一个字节
bool FIFO_Read(FIFO_Buffer_t* fifo, uint8_t* data)
{
// 缓冲区空了?
if (fifo->head == fifo->tail) {
return false;
}
*data = fifo->buffer[fifo->tail];
fifo->tail = (fifo->tail + 1) % fifo->size;
return true;
}
特别注意:中断和主循环都会访问head和tail。中断里只修改head,主循环里只修改tail。这样能避免大部分竞态问题。如果芯片支持,最好用原子操作或者关中断保护。
FIFO的大小怎么定?我一般遵循这几个原则:
- 接收FIFO:至少能存下最大数据包的2倍
- 发送FIFO:如果用了中断发送,大小跟接收类似
- 轮询发送不需要FIFO,直接写寄存器就行
嗯,说到这,UART驱动的核心内容基本都覆盖了。波特率别算错,数据格式要对齐,中断接收加FIFO,轮询发送保底。这套组合拳打下来,大部分场景都能应付。
下一章咱们聊聊SPI驱动的封装。那个比UART稍微复杂一点,但套路是一样的。