上个星期为了连接 MCP2515,狠狠学习了一遍基于 Linux 的嵌入式开发。

特点

和裸机开发相比,基于 Linux 的嵌入式主要特点是高度抽象。在裸机开发时,外设的操作通常直接通过操作内存映射的寄存器进行。而加入了 Linux 后,为了模块化和避免代码冗余,这些操作都经过大量的封装。例如,使用外设的代码通常位于用户空间,而直接控制外设的代码在驱动中,这些驱动代码中需要的配置参数则存在设备树文件中。然而关于嵌入式设备的文档又通常停留在硬件接口和寄存器配置层次,不易直接使用和调试。厘清这些封装层次间的关系是基于 Linux 的嵌入式开发的难点。

同时,既然是嵌入式开发,那嵌入式需要解决的硬件问题不会缺席。不过,这样的抽象也有好处,即加入了 Linux 后,基于系统支持的大部分程序都可以直接使用,只需要进行交叉编译即可。

流程

基于 Linux 做嵌入式开发流程和普通的 Linux 用户程序开发类似:

  1. 为了方便修改和部署,首先需要一个能够编译出操作系统镜像的开发环境。这一步需要设计 Linux、 UBoot、Rootfs 以及海量的第三方程序,因此通常使用 buildroot 工具管理。一般厂商提供一个 SDK 负责这一步,如 duo-buildroot-sdkluckfox-pico
  2. 根据所使用的外设,寻找并编译对应的驱动。常用外设的驱动都已经在 Linux 内核中,一般只需要修改配置文件启用即可。
  3. 最关键的一步,综合多方信息,编写设备树,向驱动传递所需要的配置信息。
  4. 如果一切顺利,连接对应外设,此时上电开机后 Linux 已经将外设抽象为一个可以在用户空间访问的设备,可以用ls /dev查看,否则在dmesg里可以看到对应的日志信息。
  5. 在用户空间编写使用外设的程序。

实际上,开发时需要反复调试直到能够跑通上面的流程,尤其是设备树需要结合多方的信息判断合适写法。

举例

以 Milk-V Duo 连接 MCP2515 为例,编译 SDK 的部分在之前的博客已经介绍。编译内核时,启用 MCP251x 的驱动。

这里需要关注的是设备树配置。Milk-V Duo 使用的设备树在 cv1800b_milkv_duo_sd.dts,要启用 MCP2515 需要修改 SPI 外设的设置:

// cv1800b_milkv_duo_sd.dts
&spi2 {
    status = "okay";
    spidev@0 {
        status = "disabled";
    };
    mcp2515_1: can@0 {
        status = "okay";
        compatible = "microchip,mcp2515";
        reg = <0x00>;
        clock-frequency = <8000000>;
        interrupt-parent = <&porta>;
        interrupts = <14 IRQ_TYPE_EDGE_FALLING>;
        spi-max-frequency = <2000000>;
        spi-cpol;
        spi-cpha;
    };
};

这里首先用status="disabled";将原来的 SPI 外设关闭,然后添加自己的 MCP2515 外设。其中只有中断部分需要针对 Milk-V Duo 修改,将interrupt-parent改为一个包含interrupt-controller属性的节点,interrupts设置为引脚 14 的下降沿。如果不删除原来的spidev,直接在下面配置的话,会只提供一个 SPI 通信接口 /dev/spidev0.0,而不是 SPI 接口下的外设。

不过,Milk-V Duo 给的设备树直到版本 Duo-V1.1.0 实际上并没有实现 GPIO 中断,需要在porta下添加一行#interrupt-cells = <2>;

// cv180x_base_riscv.dtsi
gpio0: gpio@03020000 {
    porta: gpio-controller@0 {
        interrupt-controller;
        interrupts = <60 IRQ_TYPE_LEVEL_HIGH>;
        interrupt-parent = <&plic0>;
        #interrupt-cells = <2>; // !!!
    };
};

类似地,如果是 Luckfox Pico,则设备树中spidev@0部分修改为:

// rv1103g-luckfox-pico-mini-b.dts
spidev@0 {
    status = "okay";
    compatible = "microchip,mcp2515";
    reg = <0>;
    clock-frequency = <8000000>;
    interrupt-parent = <&gpio1>;
    interrupts = <RK_PC4 IRQ_TYPE_EDGE_FALLING>;
    spi-max-frequency = <2000000>;
    spi-cpol;
    spi-cpha;
};

编译出镜像即可用在命令行用ip指令查看和配置 CAN 相关参数。比较神奇的是由于收发器 TJA1050 需要 5V 电压,这里将 MCP2515 一并接到 5V 电源上,但信号仍是 3.3V,低于 0.7VCC。即便如此,在 11 模式下 SPI 通信正常,因此设置了 spi-cpol 和 spi-cpha