第4章 通信接口性能优化:UART、SPI、I2C实战调优
通信接口是MCU Bootloader的命脉。你想想看,固件升级要是卡在传输上,后面所有优化都白搭。这一章我把自己在几个量产项目中踩过的坑、调过的参数,全部分享出来。
4.1 UART:波特率与FIFO的博弈
UART是Bootloader最常用的接口。简单、可靠,但想跑得快,门道不少。
4.1.1 波特率不是越高越好
我个人习惯,先看晶振精度。MCU内部RC振荡器,常温下还行,温度一变化,误差就飘了。
| 晶振类型 | 典型精度 | 建议最高波特率 | 备注 |
|---|---|---|---|
| 外部晶振(8MHz~16MHz) | ±20ppm ~ ±50ppm | 921600 | 稳定,可跑高速 |
| 内部RC振荡器 | ±1% ~ ±3% | 115200 | 超过此值,高温下可能丢包 |
| PLL倍频输出 | 取决于参考源 | 230400 | 需实测验证 |
我在一个车载项目里遇到过,用内部RC跑460800波特率,常温测试全过。结果夏天路试,车机晒到70°C,Bootloader升级成功率直接掉到60%。查了两天,最后发现是波特率误差累积导致帧同步失败。降到230400后,问题消失。
4.1.2 FIFO配置的玄机
很多MCU的UART外设都有硬件FIFO。这个FIFO用好了,CPU占用率能降一大截。
// 以STM32为例,配置UART FIFO中断阈值
// 场景:Bootloader接收固件数据包(每包256字节)
void UART_FIFO_Config(void)
{
// 使能FIFO
USART1->CR1 |= USART_CR1_FIFOEN;
// 配置接收FIFO阈值为半满(16字节)
// 这样每收到16字节才触发一次中断
USART1->CR3 = (USART1->CR3 & ~USART_CR3_RXFTCFG) |
(0x02 << USART_CR3_RXFTCFG_Pos);
// 配置发送FIFO阈值为空(触发发送中断)
USART1->CR3 = (USART1->CR3 & ~USART_CR3_TXFTCFG) |
(0x00 << USART_CR3_TXFTCFG_Pos);
}
这里有个细节:接收阈值设得太低(比如1字节),中断太频繁,CPU被拖死。设得太高(比如满),响应延迟大,可能丢数据。我一般取中间值,16字节或32字节,具体看数据包大小。
4.2 SPI:时钟极性与DMA传输
SPI在Bootloader里常用于连接外部Flash。速率可以跑到几十MHz,但配置错了,数据全是乱的。
4.2.1 时钟极性(CPOL)和相位(CPHA)
说白了,就是主设备和从设备在哪个时钟沿采样数据。配错了,读出来的数据就是错位的。
| 模式 | CPOL | CPHA | 采样沿 | 常用场景 |
|---|---|---|---|---|
| 模式0 | 0 | 0 | 上升沿采样 | 大多数SPI Flash |
| 模式1 | 0 | 1 | 下降沿采样 | 部分传感器 |
| 模式2 | 1 | 0 | 下降沿采样 | 少数外设 |
| 模式3 | 1 | 1 | 上升沿采样 | 兼容性模式 |
我记得有一次,客户说SPI Flash读写偶尔出错。我拿示波器一看,时钟线上有毛刺。原来是CPOL配置导致时钟空闲电平变化时,产生了额外的跳变。改成模式0后,问题解决。
4.2.2 DMA传输:让CPU歇着
Bootloader擦写Flash时,CPU本来就很忙。如果SPI传输还用轮询或中断,CPU就彻底被占死了。
// SPI + DMA 传输固件数据到外部Flash
// 使用双缓冲DMA,传输和擦写并行
void SPI_Flash_Write_DMA(uint8_t *data, uint32_t len)
{
// 配置DMA通道1:SPI发送
DMA1_Channel1->CMAR = (uint32_t)data;
DMA1_Channel1->CNDTR = len;
DMA1_Channel1->CCR |= DMA_CCR_EN;
// 配置DMA通道2:SPI接收(虚拟接收,忽略数据)
DMA1_Channel2->CMAR = (uint32_t)dummy_rx_buf;
DMA1_Channel2->CNDTR = len;
DMA1_Channel2->CCR |= DMA_CCR_EN;
// 等待DMA传输完成
while(!(DMA1->ISR & DMA_ISR_TCIF1));
// 清理标志位
DMA1->IFCR |= DMA_IFCR_CTCIF1;
}
这里有个技巧:SPI是全双工,发送时必须同时接收。我习惯用一个固定的dummy buffer来收垃圾数据,省得每次分配内存。
4.3 I2C:速率与中断的平衡
I2C在Bootloader里用得少一些,但有些板子用I2C连接EEPROM存配置参数。速率上不去,Bootloader启动就慢。
4.3.1 标准模式 vs 快速模式
I2C速率有100kHz、400kHz,甚至1MHz。但速率越高,对上下拉电阻的要求越苛刻。
| 模式 | 速率 | 建议上拉电阻 | 总线电容 | 最大从设备数 |
|---|---|---|---|---|
| 标准模式 | 100kHz | 4.7kΩ | <400pF | 8~16 |
| 快速模式 | 400kHz | 2.2kΩ | <200pF | 4~8 |
| 快速+模式 | 1MHz | 1kΩ | <100pF | 2~4 |
我曾经在一个项目里,为了省成本,用了长排线连接I2C设备。400kHz下,波形已经严重畸变。最后不得不降到100kHz,并加上软件滤波。
4.3.2 中断优化:减少不必要的开销
I2C的中断服务函数,如果写得不好,很容易成为性能瓶颈。
// 优化前的I2C中断处理(每次中断都做状态机跳转)
void I2C1_EV_IRQHandler(void)
{
uint32_t status = I2C1->SR1;
if(status & I2C_SR1_SB) { /* 处理起始位 */ }
if(status & I2C_SR1_ADDR) { /* 处理地址 */ }
if(status & I2C_SR1_TXE) { /* 发送数据 */ }
if(status & I2C_SR1_RXNE) { /* 接收数据 */ }
if(status & I2C_SR1_STOPF) { /* 处理停止位 */ }
}
// 优化后的I2C中断处理(使用状态机预判)
void I2C1_EV_IRQHandler(void)
{
static uint8_t state = 0;
switch(state)
{
case 0: // 起始位已发送,准备发送地址
I2C1->DR = slave_addr << 1;
state = 1;
break;
case 1: // 地址已发送,准备发送数据
I2C1->DR = *tx_buf++;
tx_count--;
if(tx_count == 0) state = 2;
break;
case 2: // 数据发送完毕,发送停止位
I2C1->CR1 |= I2C_CR1_STOP;
state = 0;
break;
}
}
优化前,每次中断都要检查5个标志位。优化后,用状态机直接跳转到下一步。实测中断处理时间从12μs降到4μs。在400kHz速率下,这8μs的差距,意味着能多传3个字节。
4.4 本章知识体系
下面这张图,把三种接口的优化要点串起来了。你可以把它当作调试时的检查清单。
4.5 实战总结
三种接口,三种性格。UART要防误差,SPI要管时序,I2C要控负载。但核心思路是一样的:先保证通信可靠,再谈性能。
- UART:波特率留余量,FIFO阈值取中间值,中断服务函数要短。
- SPI:时钟模式配对外设手册,高速传输必须上DMA,PCB走线要短。
- I2C:上拉电阻按速率选,中断用状态机优化,总线电容要控制。