嵌入式面试题:CAN 与 I2C 核心对比(含优缺点,实操视角)
核心结论:CAN 偏工业 / 车载 “远距离、抗干扰、多电机” 场景,I2C 偏板级 “近距离、多传感器、低成本” 场景,差异全在适配性,优缺点互为补充。
| 对比维度 | CAN(控制器局域网) | I2C(内部集成电路) |
|---|---|---|
| 核心定位 | 工业 / 车载级多设备总线,主打可靠与实时性 | 板级近距离总线,主打简洁与低成本 |
| 身份标识 | 帧内 ID 字段(动态贴标签,收发均需手动设 ID) | 设备自带从机地址(固定地址 + 引脚可调,主机喊地址匹配) |
| 总线结构 | 2 根线(CAN_H/CAN_L,差分信号)+ 两端 120Ω 终端电阻 | 2 根线(SDA 数据线 / SCL 时钟线),无需终端电阻 |
| 通信距离 & 速率 | 距离远(最长 10km@5kbps),速率中等(最高 1Mbps) | 距离近(最长 10m@100kbps),速率中等(最高 400kbps 高速模式) |
| 抗干扰能力 | 强(差分信号,适配电机、车载等强电磁干扰环境) | 弱(单端信号,易受板内布线干扰,仅适合板级短距离) |
| 多设备支持 | 支持 110+ 节点,无地址冲突(ID 可灵活分配) | 支持 10+ 节点(受地址位限制,多设备需调地址防冲突) |
| 实时性 & 容错 | 高(优先级传输 + 自动错误重发,紧急数据先传) | 低(主机主导时钟,无优先级,冲突后需重新通信) |
| 代码复杂度 | 较高(需配置波特率、滤波、ID,处理帧结构) | 极低(仅需指定从机地址,API 简洁,无需复杂协议处理) |
| 硬件成本 | 中高(需 CAN 收发器如 TJA1050,终端电阻) | 极低(无需额外芯片,MCU 自带 I2C 外设直接接线) |
| 典型场景 | 电机控制、车载电子、工业机械臂、远距离传感器网络 | 板载传感器(温湿度、陀螺仪)、OLED 屏、EEPROM、芯片间通信 |
一、CAN 优缺点
优点:
- 抗干扰极强:差分信号设计,能在电机、汽车等强电磁环境下稳定传输,几乎不会丢包;
- 远距离 + 多节点:支持千米级传输,可挂百个设备,适合多电机、分布式控制;
- 实时容错:支持数据优先级(比如电机过载报警比转速指令先传),自动检测错误并重发,避免失控;
- 无地址冲突:ID 可灵活分配,新增设备只需设新 ID,不用改现有设备配置。
缺点:
- 硬件复杂:需额外加 CAN 收发器和终端电阻,布线要注意差分线匹配;
- 代码繁琐:要配置帧头、滤波、ID,协议层处理比 I2C 多;
- 成本较高:收发器 + 电阻增加硬件成本,不适合低成本小项目。
二、I2C 优缺点
优点:
- 极简易用:仅 2 根线,无需额外硬件,MCU 直接接线,代码只需调用地址 + 收发 API;
- 低成本:无额外芯片成本,布线简单,适合板内多传感器密集布局;
- 地址灵活:部分设备可通过 ADDR 引脚调地址,解决多设备冲突问题;
- 适配小数据:单次传 1-8 字节,完美匹配传感器(温湿度、光照)等小数据场景。
缺点:
- 抗干扰差:单端信号,板内布线过长或靠近电机、电源会丢包;
- 距离受限:仅适合板级(比如一块 PCB 上的多个芯片),超过 10cm 信号就不稳定;
- 实时性弱:主机控制时钟,所有设备按时钟同步,无紧急数据优先级;
- 节点有限:7 位地址仅支持 127 个设备,实际中多设备易冲突,且无错误重发机制。
简单总结:
- 若做「车载 / 工业多电机、远距离控制」:选 CAN,牺牲一点复杂度换可靠性;
- 若做「电路板上多传感器(如温湿度、OLED)」:选 I2C,用极简方案省成本。
CAN 总线必须用 120 欧终端电阻,核心是匹配 CAN 双绞线特征阻抗并适配协议特性,既符合国际标准,又能解决信号传输中的多个关键问题,具体原因如下:
- 匹配阻抗,消除信号反射:CAN 总线常用的双绞线特征阻抗恰好是 120 欧。高速传输时信号到总线两端会因阻抗突变反射,反射波与原信号叠加会产生振铃、过冲,导致信号失真。120 欧终端电阻能吸收这部分反射能量,让信号波形规整,这也是其最核心作用,且 ISO11898 国际标准也明确规定该阻值。
- 加速总线状态切换:CAN 总线有显性和隐性两种状态,显性状态下总线寄生电容会充电。若没有终端电阻,电容只能通过收发器内部高阻电路放电,放电慢会导致总线无法快速切换到隐性状态,影响通信速率。120 欧电阻能提供低阻放电通道,让电容快速释放能量,保障高低速通信时状态切换的及时性。
- 提升隐性状态抗干扰能力:隐性状态下 CAN 收发器处于高阻态,极小的干扰能量就可能让总线误触发为显性状态。120 欧终端电阻作为差分负载,可给干扰信号提供泄放路径,吸收高频噪声能量,避免干扰伪造显性信号,大幅降低外界干扰对总线通信的影响。
分别提供 CAN 通信(STM32 控制电机) 和 I2C 通信(STM32 读取温湿度传感器) 的极简示例代码,基于 STM32 HAL 库,可直接参考移植。
一、CAN 通信示例(控制带 CAN 接口的电机驱动器)
功能:STM32 通过 CAN 总线给电机发送转速指令,并接收电机反馈的实际转速
#include "stm32f1xx_hal.h"// CAN句柄及帧结构
CAN_HandleTypeDef hcan;
CAN_TxHeaderTypeDef tx_header; // 发送帧头(含ID)
CAN_RxHeaderTypeDef rx_header; // 接收帧头(含ID)
uint8_t tx_buf[8]; // 发送数据缓存
uint8_t rx_buf[8]; // 接收数据缓存
uint32_t tx_mailbox; // 发送邮箱// 1. CAN初始化(波特率500Kbps,匹配工业常用标准)
void CAN_Init(void) {hcan.Instance = CAN1;hcan.Init.Prescaler = 6; // 波特率=36MHz/(6*(1+11+4))=500Kbpshcan.Init.Mode = CAN_MODE_NORMAL; // 正常收发模式hcan.Init.SyncJumpWidth = CAN_SJW_1TQ;hcan.Init.TimeSeg1 = CAN_BS1_11TQ;hcan.Init.TimeSeg2 = CAN_BS2_4TQ;hcan.Init.AutoBusOff = ENABLE; // 自动恢复总线HAL_CAN_Init(&hcan);// 配置滤波器(只接收电机反馈ID=0x201的数据)CAN_FilterTypeDef filter;filter.FilterBank = 0;filter.FilterMode = CAN_FILTERMODE_IDMASK;filter.FilterScale = CAN_FILTERSCALE_32BIT;filter.FilterIdHigh = 0x201 << 5; // 目标ID=0x201(左移5位适配32位格式)filter.FilterMaskIdHigh = 0x7FF << 5; // 掩码:只匹配ID=0x201的帧filter.FilterFIFOAssignment = CAN_RX_FIFO0;filter.FilterActivation = ENABLE;HAL_CAN_ConfigFilter(&hcan, &filter);HAL_CAN_Start(&hcan); // 启动CANHAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING); // 使能接收中断
}// 2. 发送电机转速指令(目标ID=0x101,数据格式:前2字节为转速值)
void CAN_SendSpeedCmd(uint16_t speed) {tx_header.StdId = 0x101; // 电机驱动器接收ID(收件人身份)tx_header.RTR = CAN_RTR_DATA; // 数据帧tx_header.IDE = CAN_ID_STD; // 标准11位IDtx_header.DLC = 2; // 数据长度2字节tx_buf[0] = (speed >> 8) & 0xFF; // 转速高8位tx_buf[1] = speed & 0xFF; // 转速低8位HAL_CAN_AddTxMessage(&hcan, &tx_header, tx_buf, &tx_mailbox); // 发送
}// 3. 接收中断:解析电机反馈的实际转速(ID=0x201)
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rx_header, rx_buf);if (rx_header.StdId == 0x201) { // 确认是电机反馈ID(发件人身份)uint16_t actual_speed = (rx_buf[0] << 8) | rx_buf[1]; // 解析转速// 可通过串口打印调试:printf("实际转速:%d RPM\r\n", actual_speed);}
}// 主函数调用
int main(void) {HAL_Init();SystemClock_Config(); // 系统时钟初始化(需自行实现)CAN_Init(); // 初始化CANwhile (1) {CAN_SendSpeedCmd(1500); // 发送转速指令(1500 RPM)HAL_Delay(1000); // 每秒发送一次}
}
二、I2C 通信示例(读取 AHT20 温湿度传感器)
功能:STM32 通过 I2C 读取 AHT20 的温度和湿度数据(传感器从机地址固定为 0x38)
#include "stm32f1xx_hal.h"// I2C句柄
I2C_HandleTypeDef hi2c1;// 1. I2C初始化(速率100Kbps,标准模式)
void I2C_Init(void) {hi2c1.Instance = I2C1;hi2c1.Init.ClockSpeed = 100000; // 100Kbpshi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;hi2c1.Init.OwnAddress1 = 0; // 主机模式,无需自身地址hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; // 7位地址hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;HAL_I2C_Init(&hi2c1);
}// 2. 读取AHT20温湿度数据(从机地址0x38)
void AHT20_ReadData(float *temp, float *humi) {uint8_t cmd[3] = {0xAC, 0x33, 0x00}; // 读取数据指令uint8_t data[6]; // 接收传感器返回的6字节数据// 步骤1:发送读取指令(地址0x38,写操作)HAL_I2C_Master_Transmit(&hi2c1, 0x38 << 1, cmd, 3, 100);HAL_Delay(80); // 等待传感器采样完成// 步骤2:接收返回数据(地址0x38,读操作:0x38<<1 | 0x01)HAL_I2C_Master_Receive(&hi2c1, (0x38 << 1) | 0x01, data, 6, 100);// 步骤3:解析数据(按AHT20手册公式计算)uint32_t humi_raw = ((uint32_t)data[1] << 12) | ((uint32_t)data[2] << 4) | (data[3] >> 4);uint32_t temp_raw = ((uint32_t)(data[3] & 0x0F) << 16) | ((uint32_t)data[4] << 8) | data[5];*humi = (humi_raw * 100.0f) / (1 << 20); // 湿度:0~100%*temp = (temp_raw * 200.0f) / (1 << 20) - 50; // 温度:-50~150℃
}// 主函数调用
int main(void) {HAL_Init();SystemClock_Config(); // 系统时钟初始化(需自行实现)I2C_Init(); // 初始化I2Cfloat temperature, humidity;while (1) {AHT20_ReadData(&temperature, &humidity);// 可通过串口打印:printf("温度:%.1f℃ 湿度:%.1f%%\r\n", temperature, humidity);HAL_Delay(2000); // 每2秒读一次}
}
核心差异对比(代码层面)
| 特性 | CAN 代码特点 | I2C 代码特点 |
|---|---|---|
| 身份配置 | 需手动设置StdId(收发均需指定 ID) | 只需指定从机地址(如0x38 << 1) |
| 硬件依赖 | 需初始化 CAN 收发器(TJA1050)和终端电阻 | 直接用 MCU 的 I2C 引脚,无需额外硬件 |
| 数据处理 | 需解析帧头 ID,支持多设备区分 | 地址匹配后直接收发,无需处理帧结构 |
| 典型 API | HAL_CAN_AddTxMessage/ 中断回调 | HAL_I2C_Master_Transmit/Receive |
