4、CAN驱动开发:Linux SocketCAN、STM32 bxCAN库函数初始化与收发
驱动开发,说白了就是让CPU能跟CAN控制器“说话”。
我这些年折腾下来,发现大部分坑都出在初始化配置上。配置对了,收发就是调几个函数的事。配置错了,示波器一挂,总线上一片寂静。
这一章,咱们把Linux和STM32两条路都走一遍。你跟着我的习惯来,能少走不少弯路。
4.1 Linux SocketCAN:把CAN当网络设备玩
Linux下操作CAN,我个人习惯用SocketCAN。它把CAN总线抽象成了一个网络接口,你操作CAN就像操作TCP/IP一样——socket、bind、read、write,一套组合拳打完。
核心思路: SocketCAN 将 CAN 设备映射为网络接口(如 can0),通过标准 socket API 进行通信。
4.1.1 初始化流程
先上代码,再解释。这是我项目里常用的初始化模板:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <linux/can.h>
#include <linux/can/raw.h>
int can_init(const char *ifname) {
int s;
struct sockaddr_can addr;
struct ifreq ifr;
// 1. 创建 socket
s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if (s < 0) {
perror("socket");
return -1;
}
// 2. 指定设备名称
strcpy(ifr.ifr_name, ifname);
ioctl(s, SIOCGIFINDEX, &ifr);
// 3. 绑定
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind");
close(s);
return -1;
}
return s;
}
这里有个细节——绑定前一定要先设置好CAN接口的比特率。我见过有人代码写对了,但忘了配比特率,结果怎么都收不到数据。
命令行配置参考:
# 设置比特率 500k
sudo ip link set can0 type can bitrate 500000
# 启动设备
sudo ip link set can0 up
4.1.2 数据收发
收发就简单了。发送用 write(),接收用 read()。数据格式是 struct can_frame。
// 发送一帧
struct can_frame frame;
frame.can_id = 0x123;
frame.can_dlc = 8;
memcpy(frame.data, "HelloCAN", 8);
int nbytes = write(s, &frame, sizeof(frame));
if (nbytes != sizeof(frame)) {
printf("发送失败\n");
}
// 接收一帧
struct can_frame rx_frame;
nbytes = read(s, &rx_frame, sizeof(rx_frame));
if (nbytes > 0) {
printf("收到 ID=0x%x, 数据=", rx_frame.can_id);
for (int i = 0; i < rx_frame.can_dlc; i++) {
printf("%02x ", rx_frame.data[i]);
}
printf("\n");
}
嗯,这里要注意:can_id 的高位有个 CAN_EFF_FLAG 标志位。如果你用扩展帧,得手动置位。我曾经在这个坑里趴了一下午——发出去的ID总是对不上。
避坑指南: 标准帧 ID 范围 0x000~0x7FF,扩展帧 ID 范围 0x00000000~0x1FFFFFFF。发送扩展帧时,必须将 can_id 与 CAN_EFF_FLAG 按位或。
frame.can_id = 0x12345678 | CAN_EFF_FLAG;
4.2 STM32 bxCAN:寄存器级别的掌控感
STM32 的 bxCAN 跟 SocketCAN 完全是两码事。你得直接跟寄存器打交道,或者用 STM32 HAL/LL 库。我个人偏爱 LL 库——轻量、透明,出了问题好定位。
4.2.1 初始化流程
初始化分三步:时钟、GPIO、CAN 控制器。我习惯按这个顺序来:
void MX_CAN1_Init(void) {
// 1. 使能时钟
__HAL_RCC_CAN1_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
// 2. GPIO 配置:CAN1_RX=PB8, CAN1_TX=PB9
GPIO_InitTypeDef gpio = {0};
gpio.Pin = GPIO_PIN_8 | GPIO_PIN_9;
gpio.Mode = GPIO_MODE_AF_PP;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
gpio.Alternate = GPIO_AF9_CAN1;
HAL_GPIO_Init(GPIOB, &gpio);
// 3. CAN 控制器配置
hcan1.Instance = CAN1;
hcan1.Init.Prescaler = 6; // 分频系数
hcan1.Init.Mode = CAN_MODE_NORMAL; // 正常模式
hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ;
hcan1.Init.TimeSeg1 = CAN_BS1_8TQ;
hcan1.Init.TimeSeg2 = CAN_BS2_7TQ;
hcan1.Init.TimeTriggeredMode = DISABLE;
hcan1.Init.AutoBusOff = DISABLE;
hcan1.Init.AutoWakeUp = DISABLE;
hcan1.Init.AutoRetransmission = ENABLE;
hcan1.Init.ReceiveFifoLocked = DISABLE;
hcan1.Init.TransmitFifoPriority = DISABLE;
if (HAL_CAN_Init(&hcan1) != HAL_OK) {
Error_Handler();
}
}
比特率计算小公式:
CAN 时钟 = 主时钟 / Prescaler
比特率 = CAN 时钟 / (1 + BS1 + BS2 + SJW)
比如 72MHz 主频,Prescaler=6,BS1=8,BS2=7,SJW=1:
CAN 时钟 = 72/6 = 12MHz
比特率 = 12M / (1+8+7+1) = 12M/17 ≈ 705k
想要精确 500k?调 Prescaler 和 BS 段就行。
4.2.2 过滤器配置
bxCAN 的过滤器是个好东西。你可以只收你关心的ID,其他的统统丢掉。我一般在项目里这么配:
void CAN_Filter_Config(void) {
CAN_FilterTypeDef filter = {0};
filter.FilterBank = 0;
filter.FilterMode = CAN_FILTERMODE_IDMASK;
filter.FilterScale = CAN_FILTERSCALE_32BIT;
filter.FilterIdHigh = 0x123 << 5; // 标准帧 ID 左移 5 位
filter.FilterIdLow = 0;
filter.FilterMaskIdHigh = 0x7FF << 5; // 掩码:只匹配 ID
filter.FilterMaskIdLow = 0;
filter.FilterFIFOAssignment = CAN_RX_FIFO0;
filter.FilterActivation = ENABLE;
HAL_CAN_ConfigFilter(&hcan1, &filter);
}
为什么 ID 要左移 5 位?因为 bxCAN 的寄存器里,标准帧ID占高11位,后面是RTR、IDE这些标志位。我第一次用的时候没左移,结果过滤器形同虚设——什么ID都进来了。
4.2.3 收发函数
发送用 HAL_CAN_AddTxMessage(),接收用中断或轮询。我推荐用中断,省CPU:
// 发送
CAN_TxHeaderTypeDef tx_header;
uint8_t tx_data[8] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
uint32_t tx_mailbox;
tx_header.StdId = 0x123;
tx_header.ExtId = 0;
tx_header.IDE = CAN_ID_STD;
tx_header.RTR = CAN_RTR_DATA;
tx_header.DLC = 8;
tx_header.TransmitGlobalTime = DISABLE;
HAL_CAN_AddTxMessage(&hcan1, &tx_header, tx_data, &tx_mailbox);
// 接收中断回调
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
CAN_RxHeaderTypeDef rx_header;
uint8_t rx_data[8];
if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rx_header, rx_data) == HAL_OK) {
// 处理收到的数据
printf("收到 ID=0x%x, DLC=%d\n", rx_header.StdId, rx_header.DLC);
}
}
注意: 使用中断接收前,必须调用 HAL_CAN_Start() 和 HAL_CAN_ActivateNotification() 开启中断。否则回调函数永远不会被调用。
HAL_CAN_Start(&hcan1);
HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);
4.3 两种方案的对比
我把两个平台的差异整理成了表格,方便你选型时参考:
| 对比项 | Linux SocketCAN | STM32 bxCAN |
|---|---|---|
| 抽象层次 | 网络接口(socket) | 寄存器 / HAL库 |
| 开发难度 | 低(API标准) | 中(需理解寄存器) |
| 实时性 | 受Linux调度影响 | 高(硬件中断) |
| 过滤器配置 | 需setsockopt设置 | 硬件过滤器,灵活 |
| 典型场景 | 工控机、车载电脑 | 嵌入式节点、ECU |
4.4 知识结构总览
下面这张图,把本章的核心脉络串起来了。你照着这个结构去理解,思路会清晰很多:
你看,左边是Linux路线,右边是STM32路线。两条路最终都指向同一个目标——让数据在CAN总线上跑起来。区别在于,Linux帮你封装好了大部分细节,而STM32需要你亲手拧好每一颗螺丝。
我个人建议:如果你刚接触CAN,先从Linux SocketCAN入手,把收发流程跑通,再回头啃STM32的寄存器。这样理解会更深。
本章要点回顾:
- Linux SocketCAN:socket → bind → write/read,操作简单,适合快速验证
- STM32 bxCAN:时钟 → GPIO → CAN初始化 → 过滤器 → 中断收发,配置细致
- 比特率计算:Prescaler、BS1、BS2、SJW 四个参数决定
- 过滤器:用ID掩码模式,只收关心的帧,减轻CPU负担
- 避坑:扩展帧ID要置位CAN_EFF_FLAG;STM32过滤器ID要左移5位