4. CAN总线驱动移植:MCP2515/FlexCAN驱动源码分析、内核配置、驱动编译与加载

这一章,我们直接切入工控网关最核心的环节——CAN总线驱动移植。说白了,就是让Linux内核认识并控制你的CAN控制器硬件。我见过不少项目,硬件设计没问题,却在驱动移植这一步卡了半个月。其实摸清套路后,这事儿没那么玄乎。

咱们主要讲两种主流方案:MCP2515(SPI接口,外挂芯片)和FlexCAN(集成在i.MX、S32K等SoC内部)。前者适合低成本、灵活扩展;后者性能高、延迟低,适合多路CAN场景。

核心结论: 无论哪种方案,驱动移植的本质就是“内核配置 + 设备树描述 + 驱动编译 + 模块加载”四步走。本章会把这四步拆开揉碎。
CAN驱动移植四步流程 驱动源码分析 MCP2515 / FlexCAN 内核配置 make menuconfig 驱动编译 内核模块 / 内置 加载验证 insmod / modprobe 关键说明: • MCP2515 驱动位于 drivers/net/can/spi/mcp251x.c • FlexCAN 驱动位于 drivers/net/can/flexcan.c • 设备树节点描述硬件连接(中断、SPI片选、时钟频率等) • 编译为模块(.ko)便于调试,稳定后编入内核

4.1 MCP2515驱动源码分析

MCP2515是Microchip的经典SPI转CAN芯片。Linux内核里它的驱动是mcp251x.c,我最早看这个文件时,第一反应是“怎么才两千多行?”——比起动辄上万行的网卡驱动,这算很精简了。

驱动核心结构其实就三块:

  • SPI通信层:通过spi_transfer结构体收发数据,读写寄存器
  • CAN核心层:注册net_device,实现open/close/start_xmit/do_set_mode等回调
  • 中断处理:处理接收、发送完成、错误状态等中断事件

我个人习惯先看mcp251x_can_probe()函数,它把整个驱动的初始化流程串起来了:

static int mcp251x_can_probe(struct spi_device *spi)
{
    struct net_device *net;
    struct mcp251x_priv *priv;
    int ret;

    // 1. 分配net_device结构体
    net = alloc_candev(sizeof(*priv), TX_ECHO_SKB_MAX);
    if (!net)
        return -ENOMEM;

    priv = netdev_priv(net);
    priv->spi = spi;
    priv->net = net;

    // 2. 设置SPI参数:最大时钟、模式
    spi->mode = SPI_MODE_0;
    spi->max_speed_hz = 10 * 1000 * 1000;  // 10MHz

    // 3. 获取中断号、复位芯片
    priv->irq = spi->irq;
    mcp251x_reset(priv);

    // 4. 注册net_device
    ret = register_candev(net);
    if (ret)
        goto out_free;

    return 0;
}
我的经验: 调试MCP2515时,80%的问题出在SPI时钟极性/相位不匹配。MCP2515要求SPI_MODE_0,但有些SoC的SPI控制器默认是MODE_3。记得在设备树里显式指定。

4.2 FlexCAN驱动源码分析

FlexCAN是NXP(原Freescale)芯片内部集成的CAN控制器。驱动文件flexcan.c比MCP2515复杂不少,因为它要处理更多硬件特性:邮箱机制、总线时钟、唤醒、FD模式等。

FlexCAN驱动的核心差异点:

特性 MCP2515 FlexCAN
接口类型 SPI(外挂) 内存映射(集成)
收发缓冲区 硬件FIFO(3 TX / 2 RX) 邮箱阵列(最多64个)
时钟源 外部晶振(通常8/16MHz) SoC内部总线时钟 / 外设时钟
中断处理 单中断线 多中断(ORed / 独立)
驱动复杂度 ~2500行 ~5000行

FlexCAN驱动里有个关键函数flexcan_chip_start(),它负责初始化硬件:

static int flexcan_chip_start(struct net_device *dev)
{
    struct flexcan_priv *priv = netdev_priv(dev);
    struct flexcan_regs __iomem *regs = priv->regs;

    // 1. 软复位CAN控制器
    flexcan_enter_freeze_mode(priv);
    flexcan_exit_freeze_mode(priv);

    // 2. 配置位时序:波特率 = 时钟 / (presdiv * (seg1 + seg2 + 3))
    flexcan_set_bitrate(priv);

    // 3. 配置邮箱:通常使用8个邮箱
    flexcan_setup_mailbox(priv);

    // 4. 使能中断:接收、发送完成、错误
    writel(FLEXCAN_CTRL_ERR_MSK | FLEXCAN_CTRL_BOFF_MSK,
           &regs->ctrl);

    // 5. 退出冻结模式,开始工作
    flexcan_enter_freeze_mode(priv);
    flexcan_exit_freeze_mode(priv);

    return 0;
}
曾经踩过的坑: FlexCAN的时钟源选择。i.MX6ULL上FlexCAN的时钟可以来自IPG_CLK或OSC_CLK,默认是IPG_CLK(66MHz)。但如果你设备树里没配clocks属性,驱动会拿不到时钟频率,导致波特率计算全错。我调试了整整两天才发现是设备树少写了一行。

4.3 内核配置

驱动源码看完了,接下来要让内核把它编译进去。配置内核用make menuconfig,路径如下:

Device Drivers  --->
  [*] Network device support  --->
    <*>   CAN bus subsystem support  --->
      CAN Device Drivers  --->
        <M>   Microchip MCP251x/MCP25625
        <M>   Freescale FlexCAN

这里<M>表示编译为模块(.ko),<*>表示编入内核。我建议前期用模块方式,方便调试时单独替换。

配置完成后,检查.config文件里是否出现:

CONFIG_CAN=y
CONFIG_CAN_DEV=y
CONFIG_CAN_MCP251X=m
CONFIG_CAN_FLEXCAN=m
小技巧: 如果你用的是Yocto或Buildroot,可以在defconfig里直接写这几行,省得每次menuconfig手动勾选。

4.4 设备树配置

驱动和内核都准备好了,但Linux还不知道你的CAN芯片接在哪个SPI总线上、中断脚是哪个。这就需要设备树(Device Tree)来描述硬件。

MCP2515设备树节点示例:

&ecspi2 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_ecspi2>;

    mcp2515: can@0 {
        compatible = "microchip,mcp2515";
        reg = <0>;                  // SPI片选0
        clocks = <&clk16m>;         // 16MHz晶振
        interrupt-parent = <&gpio1>;
        interrupts = <19 IRQ_TYPE_EDGE_FALLING>;
        spi-max-frequency = <10000000>;
        status = "okay";
    };
};

FlexCAN设备树节点示例:

&can1 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_flexcan1>;

    fsl,clk-source = <0>;          // 0=IPG_CLK, 1=OSC_CLK
    xceiver-supply = <&reg_can_3v3>; // CAN收发器电源
};
注意: MCP2515的interrupts属性里,我习惯用IRQ_TYPE_EDGE_FALLING。因为MCP2515的中断输出是低电平有效,但有些SoC的GPIO中断控制器对电平触发支持不好,边沿触发更稳定。

4.5 驱动编译与加载

配置和设备树都搞定后,编译驱动:

# 编译内核模块
make modules

# 如果只编译CAN相关模块,可以指定路径
make M=drivers/net/can

# 编译设备树
make dtbs

编译完成后,把.ko文件和.dtb文件拷贝到目标板。加载驱动:

# 加载MCP2515驱动
insmod mcp251x.ko

# 或者用modprobe(会自动处理依赖)
modprobe mcp251x

# 查看是否加载成功
lsmod | grep can

# 查看CAN接口是否出现
ip link show can0

如果一切顺利,你会看到can0接口。设置波特率并启动:

# 设置波特率125kbps
ip link set can0 type can bitrate 125000

# 启动CAN接口
ip link set can0 up

# 查看接口状态
ip -details link show can0
验证方法: 用candump can0监听总线,再用cansend can0 123#11223344发送一帧数据。如果能收到自己发的帧,说明驱动移植成功。

4.6 常见问题与避坑指南

驱动移植过程中,我遇到过不少问题,挑几个典型的说说:

  • SPI通信失败:检查SPI片选、时钟极性、最大频率。MCP2515最高支持10MHz,但有些板子走线长了,降到5MHz更稳。
  • 中断不触发:用cat /proc/interrupts看中断号有没有注册。我曾经因为设备树里interrupts写成了<19 0>(没指定触发类型),导致中断完全不工作。
  • 波特率不准:FlexCAN的时钟源一定要确认。用clk_get_rate打印一下实际时钟频率,跟数据手册对一下。
  • 模块加载报错:insmod提示Unknown symbol,通常是内核版本不匹配。检查uname -r和编译内核的版本是否一致。
我的调试习惯: 在驱动里加dev_dbg()或netdev_dbg()打印关键寄存器值。比如MCP2515的CANCTRL寄存器、FlexCAN的CTRL1寄存器,看一眼就知道配置有没有生效。

嗯,到这里,CAN总线驱动移植的核心内容就讲完了。从源码分析到内核配置,从设备树到编译加载,每一步都有坑,但每一步也都有迹可循。你只要按这个流程走一遍,基本不会出大问题。