4、CAN设备树配置:st,can节点属性、波特率设置、过滤器配置、中断映射

好,咱们接着往下聊。上一章我们把CAN硬件抽象层捋了一遍,这一章要动真格的了——设备树配置。

说实话,我刚开始接触Zephyr的设备树时,也觉得这东西有点绕。但用久了你会发现,它其实就是一块「硬件描述面板」。你告诉系统:我这颗芯片的CAN外设挂在哪条总线上,中断号是多少,波特率想设多少。剩下的,Zephyr的驱动框架帮你搞定。

今天咱们就以ST的芯片为例,把st,can节点从头到尾拆一遍。嗯,这里要注意,不同厂商的CAN控制器,设备树绑定是不一样的。ST的用的是st,stm32-can,NXP的可能就是nxp,flexcan。但配置思路是相通的。

4.1 st,can节点属性详解

先看一个典型的设备树节点长什么样。我在STM32H743的项目里写过这样的配置:

&can1 {
    status = "okay";
    compatible = "st,stm32-can";
    reg = <0x40006400 0x400>;
    interrupts = <19 0>;
    interrupt-names = "can1";
    clocks = <&rcc STM32_CLOCK_BUS_APB1 0x20000000>;
    bus-speed = <500000>;
    sjw = <1>;
    sample-point = <875>;
    st,can-primary = <1>;
    st,can-mode = <0>;
};

这里每个属性都有它的讲究。我一个个说。

compatible:这是驱动匹配的关键字。Zephyr的驱动框架通过它找到对应的驱动文件。你写st,stm32-can,系统就知道去调用drivers/can/can_stm32.c里的初始化函数。我见过有人把这里拼写错了,结果驱动死活加载不上,查了半天才发现少了个字母。

reg:定义CAN控制器的基地址和地址范围。比如0x40006400就是CAN1外设的起始地址,0x400是寄存器映射的大小。这个值你得查芯片参考手册,不能瞎填。

clocks:时钟配置。CAN外设需要时钟才能工作,这里引用RCC(Reset and Clock Control)节点,告诉系统打开APB1总线上的对应时钟位。我踩过坑——时钟没配好,CAN模块初始化时直接挂掉,连错误日志都来不及打印。

核心要点:设备树里的每个属性,最终都会映射到驱动代码里的某个结构体成员。你改设备树,就是在改驱动的行为。

4.2 波特率设置——别小看这个数字

波特率设置,说白了就是让CAN控制器知道:一个位的时间有多长。

CAN总线的位时间由三部分组成:同步段(Sync_Seg)、传播段(Prop_Seg)和相位缓冲段(Phase_Seg1 + Phase_Seg2)。Zephyr里,你不需要手动算这些段,只需要告诉它目标波特率和采样点位置。

看这个配置:

bus-speed = <500000>;   /* 500 kbps */
sjw = <1>;              /* 同步跳转宽度,单位是时间量子 */
sample-point = <875>;   /* 采样点位置,87.5% */

bus-speed:目标波特率,单位是bps。常用的有125k、250k、500k、1M。我个人的习惯是,能用500k就别用125k,速度越快,实时性越好。但前提是你的总线长度和节点数量能撑得住。

sample-point:采样点位置。这个值很关键。它决定了你在位时间的哪个时刻去采样总线电平。标准CAN推荐87.5%,CAN FD推荐75%左右。为什么?因为采样点越靠后,你对总线传播延迟的容忍度就越高。我在一个长距离(约50米)的CAN项目中,把采样点从87.5%改到了80%,误码率直接降了一个数量级。

sjw:同步跳转宽度。它决定了控制器能容忍多大的时钟偏差。一般设1或2就够了,设太大反而可能引入噪声。

小技巧:如果你不确定采样点怎么设,可以用Zephyr自带的can_calc_timing()函数来算。它会根据你的时钟频率和目标波特率,自动算出最优的时序参数。

4.3 过滤器配置——别让CPU被无关报文淹死

CAN总线是广播式的,所有节点都能收到所有报文。但你的CPU不是垃圾桶,不能什么都往里装。这时候就需要过滤器了。

ST的CAN控制器支持两种过滤模式:标识符列表模式标识符掩码模式

  • 列表模式:只接收ID完全匹配的报文。适合节点只关心少数几个ID的场景。
  • 掩码模式:接收ID中某些位匹配的报文。适合需要接收一组连续ID的场景。

在Zephyr的设备树里,过滤器配置长这样:

can-filters {
    compatible = "st,can-filters";
    filter0: filter@0 {
        st,can-filter-id = <0x123>;
        st,can-filter-mask = <0x7FF>;
        st,can-filter-mode = <0>;  /* 0=掩码模式, 1=列表模式 */
        st,can-filter-fifo = <0>;  /* 0=FIFO0, 1=FIFO1 */
    };
    filter1: filter@1 {
        st,can-filter-id = <0x456>;
        st,can-filter-mask = <0x7FF>;
        st,can-filter-mode = <1>;
        st,can-filter-fifo = <1>;
    };
};

这里要注意几个点:

st,can-filter-mask:掩码为1的位表示「必须匹配」,为0的位表示「忽略」。比如ID=0x123,掩码=0x7FF,那就只接收ID=0x123的报文。如果掩码=0x700,那就接收ID高3位为0x1的所有报文(0x100~0x1FF)。

st,can-filter-fifo:指定匹配的报文放到哪个FIFO。ST的CAN控制器有两个FIFO,你可以把高优先级报文放到FIFO0,低优先级的放到FIFO1。这样中断处理时可以先处理FIFO0。

我曾经在一个项目中,因为过滤器配置错了,导致ECU收到了大量无关报文,CPU负载直接飙到80%。后来把过滤器一收紧,负载降到了15%。你想想看,这差距有多大。

注意:过滤器数量是有限的。STM32的bxCAN最多有14个过滤器组。如果你需要过滤的ID很多,就得合理规划,或者考虑用CAN FD的更多过滤器资源。

4.4 中断映射——让CPU知道什么时候该干活

CAN控制器收到报文后,怎么通知CPU?靠中断。

设备树里的中断配置:

interrupts = <19 0>;
interrupt-names = "can1";

interrupts:第一个数字是中断号,第二个是中断触发方式。19是CAN1在STM32H7上的中断号,0表示高电平触发。这个中断号你得查芯片的向量表,不同系列不一样。

interrupt-names:给中断起个名字,方便驱动里引用。驱动初始化时会调用irq_connect_dynamic(),根据这个名字找到对应的中断号。

Zephyr里,中断处理函数是在驱动层注册的。你不需要自己写ISR,只需要在设备树里配好中断号,驱动框架会自动帮你把中断处理函数挂上去。

但有一点要注意:CAN中断有多个来源——接收中断、发送完成中断、错误中断、总线关闭中断等等。驱动默认会开启接收中断和错误中断。如果你需要发送完成中断,得在应用层额外配置。

我建议你至少把错误中断打开。为什么?因为CAN总线一旦出现错误,比如总线关闭或被动错误,你需要第一时间知道。否则整个通信链路断了,你还在那傻等数据,那就尴尬了。

4.5 完整配置示例

最后,给一个完整的设备树配置示例。这是我之前在STM32G474项目里用过的,跑的是500k波特率,过滤了三个关键ID:

&can1 {
    status = "okay";
    compatible = "st,stm32-can";
    reg = <0x40006400 0x400>;
    interrupts = <19 0>;
    interrupt-names = "can1";
    clocks = <&rcc STM32_CLOCK_BUS_APB1 0x20000000>;
    bus-speed = <500000>;
    sjw = <1>;
    sample-point = <875>;
    st,can-primary = <1>;
    st,can-mode = <0>;

    can-filters {
        compatible = "st,can-filters";
        filter0: filter@0 {
            st,can-filter-id = <0x100>;
            st,can-filter-mask = <0x7FF>;
            st,can-filter-mode = <0>;
            st,can-filter-fifo = <0>;
        };
        filter1: filter@1 {
            st,can-filter-id = <0x200>;
            st,can-filter-mask = <0x7FF>;
            st,can-filter-mode = <0>;
            st,can-filter-fifo = <0>;
        };
        filter2: filter@2 {
            st,can-filter-id = <0x300>;
            st,can-filter-mask = <0x7FF>;
            st,can-filter-mode = <0>;
            st,can-filter-fifo = <1>;
        };
    };
};

这个配置里,ID 0x100和0x200走FIFO0,ID 0x300走FIFO1。为什么这么分?因为0x100和0x200是控制指令,需要优先处理;0x300是状态反馈,可以稍后处理。

好了,设备树配置这块就聊到这。下一章我们讲CAN驱动的初始化流程,看看设备树里的这些属性,是怎么一步步变成寄存器里的值的。