第4章 通信接口性能优化:UART、SPI、I2C实战调优

通信接口是MCU Bootloader的命脉。你想想看,固件升级要是卡在传输上,后面所有优化都白搭。这一章我把自己在几个量产项目中踩过的坑、调过的参数,全部分享出来。

核心观点: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后,问题消失。

避坑指南:量产Bootloader,波特率建议留50%的余量。标称115200,实际跑57600更稳妥。升级速度慢一点,总比升级失败变砖强。

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字节,具体看数据包大小。

经验值:Bootloader场景下,接收FIFO阈值设为数据包长度的1/8到1/4,效果最好。比如256字节的包,阈值设32或64。

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后,问题解决。

注意:SPI速率超过20MHz时,PCB走线长度要控制在5cm以内。否则信号反射会导致数据错误。我见过有人用杜邦线跑40MHz,结果10次里有3次校验失败。

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来收垃圾数据,省得每次分配内存。

性能对比:同样传输1MB数据到SPI Flash,轮询方式耗时约800ms,CPU占用率100%。DMA方式耗时约820ms,但CPU占用率只有15%。多出来的20ms是DMA启动开销,但CPU可以腾出手来做Flash擦除和校验。

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个字节。

关键指标:I2C中断服务函数执行时间,不能超过一个SCL时钟周期的一半。400kHz下,一个周期2.5μs,ISR必须在1.25μs内完成。否则会拖慢整个总线。

4.4 本章知识体系

下面这张图,把三种接口的优化要点串起来了。你可以把它当作调试时的检查清单。

通信接口性能优化知识体系 通信接口性能优化知识体系 UART SPI I2C 波特率精度 FIFO阈值 CPOL/CPHA DMA双缓冲 上拉电阻 中断状态机 优化原则:稳定性 > 速率 > CPU占用率 每个接口先保证不丢数据,再追求速度,最后优化资源消耗 不丢数据 追求速度 优化资源

4.5 实战总结

三种接口,三种性格。UART要防误差,SPI要管时序,I2C要控负载。但核心思路是一样的:先保证通信可靠,再谈性能。

  • UART:波特率留余量,FIFO阈值取中间值,中断服务函数要短。
  • SPI:时钟模式配对外设手册,高速传输必须上DMA,PCB走线要短。
  • I2C:上拉电阻按速率选,中断用状态机优化,总线电容要控制。
我的习惯:每次调完通信接口,我都会用逻辑分析仪抓一段波形,保存下来。下次改版或者换MCU时,直接对比波形,一眼就能看出问题。这个习惯帮我省了不少排查时间。