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 知识结构总览

下面这张图,把本章的核心脉络串起来了。你照着这个结构去理解,思路会清晰很多:

CAN驱动开发知识结构 CAN驱动开发 Linux SocketCAN socket() → bind() write() / read() can_frame 结构体 STM32 bxCAN 时钟 + GPIO + CAN 过滤器配置 中断收发回调 核心:初始化配置决定成败,收发只是API调用

你看,左边是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位