刚入行嵌入式开发时,我最常犯的错误就是盲目选择总线协议。记得有次做温湿度传感器项目,图省事直接用了SPI,结果布线时发现要穿过两个金属隔板,信号干扰严重到数据根本读不出来。后来换成I2C加屏蔽线才解决问题。这个教训让我明白:总线协议没有绝对的好坏,只有适合与否。
四大协议(I2C/SPI/Modbus 485/CAN)就像不同型号的货车:I2C是灵活的小皮卡,SPI是高速的集装箱车,Modbus 485像重型卡车,而CAN则是特种运输车。选错协议就像用皮卡运集装箱——要么根本装不下,要么半路散架。实际选型时要看三个硬指标:
有个很实用的决策技巧:先画系统拓扑图。去年做智能农业大棚项目时,我把所有传感器位置标在图纸上,发现节点呈星型分布且距离超过3米,立刻排除了I2C和SPI,最终选择Modbus 485加中继器的方案,省去了后期改方案的麻烦。
I2C最让我惊艳的是用两根线(SDA+SCL)就能组建多设备网络。但新手常会踩三个坑:上拉电阻取值、地址冲突、时钟拉伸。曾有个项目因为10K上拉电阻导致400Kbps速率下波形畸变,后来改用2.2K电阻才稳定。
速度选择有门道:
分享一个真实案例:用STM32驱动4个MPU6050陀螺仪。关键点在于:
c复制// 实测可用的MPU6050初始化代码
void MPU_Init(I2C_HandleTypeDef *hi2c, uint8_t addr) {
uint8_t check, data;
HAL_I2C_Mem_Read(hi2c, addr<<1, WHO_AM_I, 1, &check, 1, 100);
if(check != 0x68) Error_Handler();
data = 0x00; // 唤醒设备
HAL_I2C_Mem_Write(hi2c, addr<<1, PWR_MGMT_1, 1, &data, 1, 100);
data = 0x07; // 陀螺仪±1000dps量程
HAL_I2C_Mem_Write(hi2c, addr<<1, GYRO_CONFIG, 1, &data, 1, 100);
}
遇到总线锁死时,有个应急办法:连续发送9个时钟脉冲,大多数从机会自动复位。我在调试AT24C02 EEPROM时这招救过好几次场。
SPI号称全双工,但很多新手会被这个名词误导。实际项目中,约80%的SPI设备工作在"伪全双工"模式——主机发命令时从机返回的是无效数据。比如W25Q128 Flash芯片,读取数据时要先发0x03指令和地址,此时从机返回的字节其实可以忽略。
四种模式要记牢:
| 模式 | CPOL | CPHA | 适用场景 |
|---|---|---|---|
| 0 | 0 | 0 | 多数传感器 |
| 1 | 0 | 1 | SD卡初始化 |
| 2 | 1 | 0 | 某些ADC芯片 |
| 3 | 1 | 1 | 多数Flash芯片 |
最近用ESP32驱动1.54寸TFT屏时,就遇到了模式配置问题。屏幕规格书要求模式3,但我的初始化代码一直白屏。后来用逻辑分析仪抓波形发现:
c复制// 正确的SPI初始化(HAL库)
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH; // CPOL=1
hspi1.Init.CLKPhase = SPI_PHASE_2EDGE; // CPHA=1
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; // 10MHz
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
HAL_SPI_Init(&hspi1);
高速SPI(>20MHz)布线要注意:
去年给某化工厂改造DCS系统时,一条485总线挂了28个压力变送器,最远的距离控制室800米。调试第一天就遇到数据乱码,排查发现三个典型问题:
Modbus RTU报文要像背公式一样熟记:
[地址][03][起始地址高][低][数量高][低][CRC低][CRC高][地址][06][寄存器地址高][低][值高][低][CRC低][CRC高]分享一个实用的CRC16计算代码(比查表法省内存):
c复制uint16_t ModBus_CRC16(uint8_t *buf, uint16_t len) {
uint16_t crc = 0xFFFF;
while(len--) {
crc ^= *buf++;
for(uint8_t i=0; i<8; i++)
crc = (crc&1) ? (crc>>1)^0xA001 : (crc>>1);
}
return (crc<<8)|(crc>>8); // 高低字节交换
}
组网拓扑建议:
第一次拆解汽车ECU时,发现CAN总线的抗干扰能力令人惊叹——点火线圈的强电磁干扰下,1Mbps的通信居然零误码。后来在工业现场用CAN替代RS485,设备异常复位率直接降了90%。
CAN协议的精髓在帧格式:
用STM32CubeMX配置CAN时,这几个参数最易出错:
c复制hcan.Instance = CAN1;
hcan.Init.Prescaler = 6; // 时钟分频
hcan.Init.Mode = CAN_MODE_NORMAL;
hcan.Init.SyncJumpWidth = CAN_SJW_1TQ;
hcan.Init.TimeSeg1 = CAN_BS1_13TQ; // 相位段1
hcan.Init.TimeSeg2 = CAN_BS2_2TQ; // 相位段2
hcan.Init.TimeTriggeredMode = DISABLE;
hcan.Init.AutoBusOff = DISABLE;
hcan.Init.AutoWakeUp = DISABLE;
hcan.Init.AutoRetransmission = ENABLE; // 必须开启!
hcan.Init.ReceiveFifoLocked = DISABLE;
hcan.Init.TransmitFifoPriority = DISABLE;
布线规范决定成败:
调试时若遇到"总线关闭"错误,先检查CANH-CANL电压:静态时应为2.5V左右,显性位时CANH=3.5V/CANL=1.5V,隐性位时都回2.5V。去年遇到个诡异故障,最后发现是某节点TVS二极管击穿导致总线电压被拉低。