3. 以太网基础
好,咱们进入第三章。CAN总线,说实话,在车载网络里它就像老黄牛——干活最多,要求最稳。我做了这么多年车载项目,从最早的动力CAN到现在的CAN FD,踩过的坑能装一箩筐。今天咱们就把CAN节点的硬件选型、设计要点,还有STM32和Linux下的编程实战,一次性说透。
3.1 CAN控制器与收发器选型:别让芯片坑了你
选型这事儿,我个人的习惯是:先定控制器,再配收发器。为什么?因为控制器决定了你的协议栈能力,收发器决定了你的物理层抗不抗造。
3.1.1 CAN控制器选型要点
现在主流方案就两条路:
- MCU内置CAN控制器:比如STM32F4/F7/H7系列、NXP S32K系列。优点是省成本、省PCB面积。我建议能用内置就别外挂。
- 独立CAN控制器:比如MCP2515、SJA1000。一般用于MCU不带CAN外设的老平台,或者需要多路CAN扩展的场景。
选型时重点看这几个参数:
| 参数 | 说明 | 我的建议 |
|---|---|---|
| CAN协议版本 | 支持CAN 2.0B还是CAN FD | 新项目直接上CAN FD,向下兼容 |
| FIFO深度 | 报文缓冲能力 | 至少32个报文,否则高负载丢帧 |
| 过滤模式 | 硬件ID过滤 | 必须有,否则CPU被中断淹死 |
| 时钟精度 | 影响位时序 | 误差必须小于1.5% |
我曾经踩过的坑: 有次选了一款便宜MCU,内置CAN控制器FIFO只有8个报文。结果在总线负载40%时,连续丢帧。查了三天,最后发现是FIFO溢出。从那以后,FIFO深度我至少要求32个。
3.1.2 CAN收发器选型要点
收发器是MCU和物理总线的桥梁。说白了,它负责把CAN控制器的差分信号转成总线电平,同时提供总线保护。
主流收发器芯片:
- TJA1040/TJA1050:NXP经典款,5V供电,速率1Mbps。我最早的项目就用它,皮实耐造。
- TJA1043:带休眠/唤醒功能,适合低功耗节点。
- SN65HVD230:TI的3.3V供电版本,适合低压系统。
- TCAN4550:带CAN FD的收发器,支持数据段8Mbps。
选型时重点看:
- 供电电压:3.3V还是5V,要和MCU电平匹配。
- 速率:普通CAN最高1Mbps,CAN FD最高8Mbps。
- 总线保护:±15kV ESD、±60V短路保护,这是车载的基本要求。
- 待机功耗:休眠节点要求μA级。
小技巧: 收发器的STB(待机)引脚,我习惯用MCU的GPIO控制。这样可以在软件里灵活切换节点的工作/休眠状态,而不是靠硬件跳线。
3.2 CAN节点硬件设计要点:细节决定成败
硬件设计这块,我见过太多人栽在看似不起眼的地方。你想想看,CAN总线在车里要扛-40℃到125℃,还要抗电磁干扰,设计上必须下功夫。
3.2.1 典型CAN节点电路结构
一个标准的CAN节点,硬件上就这几部分:
- MCU:内置CAN控制器,负责协议处理。
- CAN收发器:负责电平转换和总线驱动。
- 终端电阻:120Ω,吸收信号反射。
- 共模扼流圈:抑制共模干扰。
- ESD/TVS保护管:防静电和浪涌。
- 隔离器件:可选,用于高可靠性场景。
下面这张图是我常用的节点结构:
3.2.2 PCB布局要点
嗯,这里要注意。CAN总线的PCB布局,直接影响通信可靠性。我总结了几条铁律:
- 收发器尽量靠近MCU:CAN控制器的TX/RX走线越短越好,控制在2cm以内。
- 差分走线等长等距:CAN_H和CAN_L走线长度差不超过5mm,间距保持10mil。
- 终端电阻靠近连接器:120Ω电阻要放在总线入口处,不要放在收发器旁边。
- 电源去耦:收发器供电引脚放一个100nF陶瓷电容,紧挨引脚。
- 地平面完整:不要在CAN走线下方分割地平面。
核心原则: CAN总线是差分信号,抗干扰能力很强。但前提是你的PCB布局不能破坏差分结构。我见过有人把CAN_H和CAN_L走线绕了半块板子,结果通信距离从100米缩到10米。
3.3 CAN报文收发编程实战
好了,硬件搞定,咱们来写代码。这部分我分两个平台讲:STM32(裸机/HAL库)和Linux(SocketCAN)。这两个平台基本覆盖了车载开发的主流场景。
3.3.1 STM32 CAN编程实战
STM32的CAN外设,说白了就是bxCAN(Basic Extended CAN)。它支持标准帧(11位ID)和扩展帧(29位ID),FIFO深度3个邮箱。嗯,3个邮箱确实少了点,但配合中断和DMA,一般够用。
初始化流程:
- 使能CAN时钟和GPIO时钟
- 配置CAN引脚为复用功能(RX/TX)
- 配置CAN工作模式(正常/环回/静默)
- 配置位时序(波特率)
- 配置过滤器(硬件ID过滤)
- 使能CAN中断(可选)
- 启动CAN
下面是一个典型的初始化代码:
// STM32 CAN初始化示例(HAL库)
void CAN_Init(void)
{
CAN_HandleTypeDef hcan;
CAN_FilterTypeDef sFilterConfig;
// 1. 使能时钟
__HAL_RCC_CAN1_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
// 2. 配置GPIO:PB8=CAN1_RX, PB9=CAN1_TX
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF9_CAN1;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// 3. 配置CAN
hcan.Instance = CAN1;
hcan.Init.TimeTriggeredMode = DISABLE;
hcan.Init.AutoBusOff = ENABLE; // 自动恢复总线
hcan.Init.AutoWakeUp = ENABLE;
hcan.Init.AutoRetransmission = ENABLE; // 自动重发
hcan.Init.ReceiveFifoLocked = DISABLE;
hcan.Init.TransmitFifoPriority = DISABLE;
hcan.Init.Mode = CAN_MODE_NORMAL;
hcan.Init.SyncJumpWidth = CAN_SJW_1TQ;
hcan.Init.TimeSeg1 = CAN_BS1_13TQ;
hcan.Init.TimeSeg2 = CAN_BS2_2TQ;
hcan.Init.Prescaler = 6; // 42MHz / (1+13+2) / 6 = 500kbps
HAL_CAN_Init(&hcan);
// 4. 配置过滤器:接收所有ID(实际项目要按需配置)
sFilterConfig.FilterBank = 0;
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;
sFilterConfig.FilterIdHigh = 0x0000;
sFilterConfig.FilterIdLow = 0x0000;
sFilterConfig.FilterMaskIdHigh = 0x0000;
sFilterConfig.FilterMaskIdLow = 0x0000;
sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;
sFilterConfig.FilterActivation = ENABLE;
HAL_CAN_ConfigFilter(&hcan, &sFilterConfig);
// 5. 启动CAN
HAL_CAN_Start(&hcan);
HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING);
}
关于波特率计算: 我习惯用500kbps,这是车载CAN最常用的速率。公式是:CAN时钟 / (1 + BS1 + BS2) / Prescaler。STM32F4的CAN时钟是42MHz,所以上面配置得到500kbps。采样点位置在(1+13)/(1+13+2)=87.5%,这个值对大多数总线都合适。
报文发送:
// 发送CAN报文
HAL_StatusTypeDef CAN_SendMessage(uint32_t id, uint8_t *data, uint8_t len)
{
CAN_TxHeaderTypeDef txHeader;
uint32_t txMailbox;
txHeader.StdId = id; // 标准ID
txHeader.ExtId = 0;
txHeader.IDE = CAN_ID_STD; // 标准帧
txHeader.RTR = CAN_RTR_DATA; // 数据帧
txHeader.DLC = len; // 数据长度
txHeader.TransmitGlobalTime = DISABLE;
// 发送,超时100ms
return HAL_CAN_AddTxMessage(&hcan, &txHeader, data, &txMailbox, 100);
}
报文接收(中断方式):
// CAN接收中断回调
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
CAN_RxHeaderTypeDef rxHeader;
uint8_t rxData[8];
if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData) == HAL_OK)
{
// 处理接收到的报文
// rxHeader.StdId 是ID
// rxData[0..7] 是数据
// rxHeader.DLC 是数据长度
ProcessCANMessage(rxHeader.StdId, rxData, rxHeader.DLC);
}
}
3.3.2 Linux SocketCAN编程实战
Linux下的CAN编程,说白了就是把CAN设备当成网络接口来操作。SocketCAN是内核原生支持的,用起来非常顺手。我在做车载网关项目时,大部分时间都在和SocketCAN打交道。
环境准备:
首先确认内核支持SocketCAN:
# 加载CAN驱动
sudo modprobe can
sudo modprobe can_raw
sudo modprobe mttcan # 如果是STM32MP1等平台
# 查看CAN设备
ip link show
# 应该能看到 can0 之类的设备
配置CAN接口:
# 设置波特率500kbps并启动
sudo ip link set can0 type can bitrate 500000
sudo ip link set can0 up
# 查看状态
ip -details link show can0
# 输出示例:
# 3: can0: <NOARP,UP,LOWER_UP> mtu 16 qdisc pfifo_fast ...
# can state ERROR-ACTIVE restart-ms 100
# bitrate 500000 sample-point 0.875
发送报文:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <net/if.h>
#include <linux/can.h>
#include <linux/can/raw.h>
int main()
{
int s;
struct sockaddr_can addr;
struct ifreq ifr;
struct can_frame frame;
// 1. 创建socket
s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if (s < 0) {
perror("socket");
return -1;
}
// 2. 绑定到can0
strcpy(ifr.ifr_name, "can0");
ioctl(s, SIOCGIFINDEX, &ifr);
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
bind(s, (struct sockaddr *)&addr, sizeof(addr));
// 3. 构造报文
frame.can_id = 0x123; // 标准ID
frame.can_dlc = 4; // 数据长度
frame.data[0] = 0x01;
frame.data[1] = 0x02;
frame.data[2] = 0x03;
frame.data[3] = 0x04;
// 4. 发送
int nbytes = write(s, &frame, sizeof(frame));
if (nbytes != sizeof(frame)) {
perror("write");
}
close(s);
return 0;
}
接收报文:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <net/if.h>
#include <linux/can.h>
#include <linux/can/raw.h>
int main()
{
int s;
struct sockaddr_can addr;
struct ifreq ifr;
struct can_frame frame;
// 1. 创建socket
s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if (s < 0) {
perror("socket");
return -1;
}
// 2. 绑定到can0
strcpy(ifr.ifr_name, "can0");
ioctl(s, SIOCGIFINDEX, &ifr);
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
bind(s, (struct sockaddr *)&addr, sizeof(addr));
// 3. 设置过滤器:只接收ID为0x123的报文
struct can_filter rfilter;
rfilter.can_id = 0x123;
rfilter.can_mask = CAN_SFF_MASK; // 标准帧掩码
setsockopt(s, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));
// 4. 循环接收
while (1) {
int nbytes = read(s, &frame, sizeof(frame));
if (nbytes > 0) {
printf("收到报文: ID=0x%03X, DLC=%d, 数据=",
frame.can_id, frame.can_dlc);
for (int i = 0; i < frame.can_dlc; i++) {
printf("%02X ", frame.data[i]);
}
printf("\n");
}
}
close(s);
return 0;
}
我曾经踩过的坑: 有次在Linux上做CAN网关,发现接收报文偶尔会丢。查了半天,发现是SocketCAN的接收缓冲区默认只有8个帧。在高负载下(比如总线负载80%),缓冲区溢出导致丢帧。解决方案是用setsockopt设置更大的接收缓冲区:
int rcvbuf_size = 65536; // 64KB
setsockopt(s, SOL_SOCKET, SO_RCVBUF, &rcvbuf_size, sizeof(rcvbuf_size));
3.4 实战经验总结
最后,我把这些年做CAN项目的经验浓缩成一张表,方便你对照检查:
| 环节 | 常见问题 | 我的解决方案 |
|---|---|---|
| 硬件选型 | FIFO深度不够导致丢帧 | 选FIFO≥32的控制器 |
| PCB布局 | 差分走线不等长 | 长度差控制在5mm内 |
| 终端电阻 | 每个节点都加120Ω | 只在总线两端加 |
| 波特率配置 | 采样点位置不对 | 采样点设在87.5%左右 |
| 软件接收 | 中断处理太慢 | 用DMA或双缓冲 |
| SocketCAN | 接收缓冲区溢出 | 设置SO_RCVBUF为64KB |
嗯,CAN总线看起来简单,但真正要做好,每个细节都得抠。我见过太多项目,硬件设计没问题,软件逻辑也没问题,但就是通信不稳定。最后查出来,往往是某个小细节没注意——比如终端电阻放错了位置,或者收发器的STB引脚没拉高。
记住一句话:CAN总线的可靠性,不是靠运气,而是靠每一个设计决策的正确性。从选型到布局,从初始化到收发逻辑,每一步都要经得起推敲。