在f1c100s芯片上移植spi网卡enc28j60的linux驱动

发布时间 2023-08-21 11:53:53作者: 可爱无辜猫猫头

前言

我个人与全志的芯片颇有故事。在我还是一个不懂事的高中生时,我看到荔枝派的官方文档,顿时被这小小的板子给吸引住。点开文档的初见:

荔枝派Nano(下面简称Nano)是一款精致迷你的 Arm9 核心板/开发板,可用于初学者学习linux或者商用于产品开发。 Nano 在与SD卡相当的尺寸上(25.4 * 33mm)提供了丰富的外设 (LCD,UART,SPI,I2C,PWM,SDIO,KEYADC...)和较为强劲的性能(24M~408MHz, 32MB DDR)。
Nano 延续并发展了Zero精巧的PCB设计,使得开发和使用非常方便:

2.54mm排针直插面包板
直插40P RGB LCD
使用OTG口进行供电和数据传输(虚拟串口,更新固件等)
可配合使用使用堆叠式的WiFi 模块联网
?可直接贴片

当时高一的我连什么是单片机和微处理器都分不清楚,只觉得好神奇,居然有这样一种单片机,性能居然这么强悍,芯片封装那么小,还能跑linux?于是速速下单买了一块荔枝派回来。拿到手也不咋会用,照着教程不是报错就是缺库,再加上学业繁重,就放在那吃灰了。

后来高二暑假,再次捡起这块板子,把uboot编译了,又把磕磕碰碰地把linux的设备树照抄过来,磕磕碰碰地开了两个窗口照抄内核配置。当时就是喜欢配内核,配来配去,虽然这个过程非常操蛋,但是当终于解决了好多文档里根本不提的问题(比如说regulator必须要在内核配置中打开等)后,又觉得人生十分圆满,我终于会搞嵌入式linux了。21年的高二暑假把内核和rootfs配好后,手上刚好又有个为了给单片机提供以太网用的enc28j60模块。刚好点开linux的menuconfig,也发现内核支持这个模块,从此就和enc28j60结下了梁子。

我用的内核版本是5.4

第一次移植过程

在我脑海中,我依稀记得当初有多稚嫩。我在whycan上翻来翻去,总算知道了给linux内核加驱动要改设备树。然后我去翻了半天linux的documentation,却还是没搞懂设备树到底要怎么写。这不纯纯坑人+浪费时间吗?当时高二期末考试前没事干,就用电脑一直翻torvalds/linux仓库的源码,把enc28j60的源码看了,看不懂,又去看设备树的源代码,那个更加是天书,不知道在说个啥。后来放假了,就动手开始移植,我首先打开了linux/Documentation/devicetree/bindings/net/microchip,enc28j60.txt,这个是驱动开发者为我们留下的文档,让我们看看他说了个啥

* Microchip ENC28J60

This is a standalone 10 MBit ethernet controller with SPI interface.

For each device connected to a SPI bus, define a child node within
the SPI master node.

Required properties:
- compatible: Should be "microchip,enc28j60"
- reg: Specify the SPI chip select the ENC28J60 is wired to
- interrupts: Specify the interrupt index within the interrupt controller (referred
              to above in interrupt-parent) and interrupt type. The ENC28J60 natively
              generates falling edge interrupts, however, additional board logic
              might invert the signal.
- pinctrl-names: List of assigned state names, see pinctrl binding documentation.
- pinctrl-0: List of phandles to configure the GPIO pin used as interrupt line,
             see also generic and your platform specific pinctrl binding
             documentation.

Optional properties:
- spi-max-frequency: Maximum frequency of the SPI bus when accessing the ENC28J60.
  According to the ENC28J80 datasheet, the chip allows a maximum of 20 MHz, however,
  board designs may need to limit this value.

The MAC address will be determined using the optional properties
defined in ethernet.txt.

Example (for NXP i.MX28 with pin control stuff for GPIO irq):

        ssp2: ssp@80014000 {
                compatible = "fsl,imx28-spi";
                pinctrl-names = "default";
                pinctrl-0 = <&spi2_pins_b &spi2_sck_cfg>;

                enc28j60: ethernet@0 {
                        compatible = "microchip,enc28j60";
                        pinctrl-names = "default";
                        pinctrl-0 = <&enc28j60_pins>;
                        reg = <0>;
                        interrupt-parent = <&gpio3>;
                        interrupts = <3 IRQ_TYPE_EDGE_FALLING>;
                        spi-max-frequency = <12000000>;
                };
        };

        pinctrl@80018000 {
                enc28j60_pins: enc28j60_pins@0 {
                        reg = <0>;
                        fsl,pinmux-ids = <
                                MX28_PAD_AUART0_RTS__GPIO_3_3    /* Interrupt */
                        >;
                        fsl,drive-strength = <MXS_DRIVE_4mA>;
                        fsl,voltage = <MXS_VOLTAGE_HIGH>;
                        fsl,pull-up = <MXS_PULL_DISABLE>;
                };
        };

我们发现配置好enc28j60模块,首先我们需要一个能用的spi设备节点。然后把我们的以太网设备挂在这个节点下,并且为它指定外部中断引脚。
于是我便照猫画虎,靠着代码能力把这一部分设备树的代码移植了过去。我知道这个设备树代码的意图是啥,但有的谜语是真的想不通:

  • 什么是ssp?我猜应该是spi吧
  • 为什么&ssp2enc28j60节点下都要塞一个pinctrl-names and pinctrl-0,这些是干嘛的
  • 为什么enc28j60pinctrlssp2的不一样?
  • interrupt-parentinterrupts是什么鬼?在全志平台上这俩参数应该咋写?
  • enc28j60_pins的意图是来一个GPIO的外部中断的引脚配置,这个在全志平台又得咋写?

当时怎么也无法思索出结果,随便瞎折腾了好几个星期,最后还是放弃了,又因为学业繁重,备战高考,一年多也没碰过这个荔枝派。高三后再试了一次,还是以失败告终。高一暑假买了荔枝派回来吃灰,高二暑假开始尝试却啥也不懂,高三暑假再次放弃,这一切成了我的一个心结,直到来到华子的一周年纪念日,我才琢磨出了这一切。

第二次移植过程

这几天突然发现可以在linux代码仓库里边用它的搜索功能全局搜索想要的代码,于是我就开始了递归学习。打开suniv-f1c100s.dtsi,对这里边的compatible等字符串一顿乱搜,终于发现了sunxi做bsp驱动移植的秘密,但是这些太复杂了,说也说不完,让我们直接开始移植。前置条件是首先应该先准备好一个已经调通的uboot、内核与根文件系统。

第零步:接线

  • VCC ---> 3V3
  • CS ---> PE7 (SPI1_CS)
  • SI ---> PE8 (SPI1_MOSI)
  • SCK ---> PE9 (SPI1_CLK)
  • SO ---> PE10 (SPI1_MISO)
  • INT ---> PE11
  • GND ---> GND

需要注意的是,全志f1c系列芯片只有GPIOD GPIOE GPIOF有外部中断,所以在选取引脚时要特别注意,而且要关注这个引脚是否被开发板引出了。

第一步:配置Menuconfig

终端输入make menuconfig,使能以下config:

  • Device Drivers ---> Network device support ---> Ethernet driver support ---> Microchip devices ---> ENC28J60 support
  • Device Drivers ---> SPI support

ENC28J60可以编译为模块,也可以直接编译进内核。保存.config

第二步:修改设备树

接着让我们修改设备树。根据驱动开发者留给我们的文档,我们初步的设备树需要的改动如下:

suniv-f1c100s.dtsi:在pinctrl下加入spi和enc28j60的中断引脚定义

pio: pinctrl@1c20800 {
        compatible = "allwinner,suniv-f1c100s-pinctrl";
        reg = <0x01c20800 0x400>;
        interrupts = <38>, <39>, <40>;
        clocks = <&ccu CLK_BUS_PIO>, <&osc24M>, <&osc32k>;
        clock-names = "apb", "hosc", "losc";
        gpio-controller;
        interrupt-controller;
        #interrupt-cells = <3>;
        #gpio-cells = <3>;

        uart0_pe_pins: uart0-pe-pins {
                pins = "PE0", "PE1";
                function = "uart0";
        };

        uart1_pa_pins: uart1-pa-pins {
                pins = "PA2", "PA3";
                function = "uart1";
        };
        
        mmc0_pins: mmc0-pins {
                pins = "PF0", "PF1", "PF2", "PF3", "PF4", "PF5";
                function = "mmc0";
        };

        spi0_pins: spi0-pins{
                        pins = "PC0", "PC1", "PC2", "PC3";
                        function = "spi0";
        };
        
        spi1_pins: spi1-pins{
                pins="PE9","PE7","PE10","PE8";
                function = "spi1";
        };

        enc28j60_pins: enc28j60_pins{
                pins="PE11";
                function = "irq";
        };

};

改变引脚基本就是对着手册和文档照猫画虎的事情,这个引脚的function应该如何选取呢?让我们打开内核代码中的pinctrl-suniv-f1c100s.c

SUNXI_PIN(SUNXI_PINCTRL_PIN(E, 7),
        SUNXI_FUNCTION(0x0, "gpio_in"),
        SUNXI_FUNCTION(0x1, "gpio_out"),
        SUNXI_FUNCTION(0x2, "csi"),  /* D4 */
        SUNXI_FUNCTION(0x3, "uart2"),  /* TX */
        SUNXI_FUNCTION(0x4, "spi1"),  /* CS */
        SUNXI_FUNCTION_IRQ_BANK(0x6, 1, 7)),
SUNXI_PIN(SUNXI_PINCTRL_PIN(E, 8),
        SUNXI_FUNCTION(0x0, "gpio_in"),
        SUNXI_FUNCTION(0x1, "gpio_out"),
        SUNXI_FUNCTION(0x2, "csi"),  /* D5 */
        SUNXI_FUNCTION(0x3, "uart2"),  /* RX */
        SUNXI_FUNCTION(0x4, "spi1"),  /* MOSI */
        SUNXI_FUNCTION_IRQ_BANK(0x6, 1, 8)),
SUNXI_PIN(SUNXI_PINCTRL_PIN(E, 9),
        SUNXI_FUNCTION(0x0, "gpio_in"),
        SUNXI_FUNCTION(0x1, "gpio_out"),
        SUNXI_FUNCTION(0x2, "csi"),  /* D6 */
        SUNXI_FUNCTION(0x3, "uart2"),  /* RTS */
        SUNXI_FUNCTION(0x4, "spi1"),  /* CLK */
        SUNXI_FUNCTION_IRQ_BANK(0x6, 1, 9)),
SUNXI_PIN(SUNXI_PINCTRL_PIN(E, 10),
        SUNXI_FUNCTION(0x0, "gpio_in"),
        SUNXI_FUNCTION(0x1, "gpio_out"),
        SUNXI_FUNCTION(0x2, "csi"),  /* D7 */
        SUNXI_FUNCTION(0x3, "uart2"),  /* CTS */
        SUNXI_FUNCTION(0x4, "spi1"),  /* MISO */
        SUNXI_FUNCTION_IRQ_BANK(0x6, 1, 10)),
SUNXI_PIN(SUNXI_PINCTRL_PIN(E, 11),
        SUNXI_FUNCTION(0x0, "gpio_in"),
        SUNXI_FUNCTION(0x1, "gpio_out"),
        SUNXI_FUNCTION(0x2, "clk0"),  /* OUT */
        SUNXI_FUNCTION(0x3, "i2c0"),  /* SCK */
        SUNXI_FUNCTION(0x4, "ir"),  /* RX */
        SUNXI_FUNCTION_IRQ_BANK(0x6, 1, 11)),
SUNXI_PIN(SUNXI_PINCTRL_PIN(E, 12),
        SUNXI_FUNCTION(0x0, "gpio_in"),
        SUNXI_FUNCTION(0x1, "gpio_out"),
        SUNXI_FUNCTION(0x2, "i2s"),  /* MCLK */
        SUNXI_FUNCTION(0x3, "i2c0"),  /* SDA */
        SUNXI_FUNCTION(0x4, "pwm0"),  /* PWM0 */
        SUNXI_FUNCTION_IRQ_BANK(0x6, 1, 12)),

所以,我们现在知道了function这个字符串的值域。那么对于外部中断引脚,应该怎么填function呢?像spi的功能,上面的代码告诉我们,直接填spi1,那么SUNXI_FUNCTION_IRQ_BANK又是个啥?继续看它的头文件pinctrl-sunxi.h

#define SUNXI_FUNCTION_IRQ(_val, _irq)    \
{       \
        .name = "irq",     \
        .muxval = _val,     \
        .irqnum = _irq,     \
}

#define SUNXI_FUNCTION_IRQ_BANK(_val, _bank, _irq)  \
{       \
        .name = "irq",     \
        .muxval = _val,     \
        .irqbank = _bank,    \
        .irqnum = _irq,     \
}

所以我猜对于中断引脚,我们应该把function设置为irq,这样他就会自动帮我们初始化这个引脚的复用功能(全志可能没这个说法)。

接下来让我们修改suniv-f1c100s-licheepi-nano.dts

&spi1{
        status = "okay";
        pinctrl-names = "default";
        pinctrl-0 = <&spi1_pins>;
        enc28j60: ethernet@0 {
                compatible = "microchip,enc28j60";
                pinctrl-names = "default";
                pinctrl-0 = <&enc28j60_pins>;
                reg = <0>;
                interrupt-parent = <&pio>;
                interrupts = <4 11 IRQ_TYPE_EDGE_FALLING>;
                spi-max-frequency = <12000000>;
        };
};

这里基本就是对着全志设备树,照着它文档抄了。现在我们也可以解答一下我高中时代的疑问。这个interrupt-parent是啥呢?interrupt-parent属性用于指定中断路由到的控制器,并包含引用中断控制器节点的单个 phandle。 该属性是继承的,因此可以在中断客户端节点或其任何父节点中指定。 “中断”属性中列出的中断始终引用节点的中断父级。翻译成人话就是它用的中断要对应在哪个中断控制器,至少在本芯片内是一种些许无用的抽象,因为f1c100s就一个简单的中断控制器INTC,但是对于更复杂的片上中断控制器则是特别好的设计。并且这样做也方便了我们配置外部引脚的中断。

dtsi中的#interrupt-cells = <3>告诉我们,interrupts这个属性应该有3个元素。通过翻其它全志的设备树,我们也知道了应该要如何配置interrupts这个属性。那就是先放引脚Port的编号,那就是A放0,B放1,C放2,D放3,E放4。。。第二个参数是引脚编号,我们用的PE11,直接填。第三个参数,直接抄模块的文档。

现在已经大功告成,让我们拷贝好设备树和内核镜像,准备启动!

第三步:启动内核

启动内核后,我们dmesg,发现并没有任何关于enc28j60的影子,甚至连关于spi的影子也没有,这是怎么回事呢?再折腾了好几天后,我发现这是因为spi根本就没有被加载。我们芯片设备树对spi的配置中是这样配置compatible属性的:"allwinner,suniv-spi", "allwinner,sun8i-h3-spi",事实上,linux内核中根本没有代码去适配allwinner,suniv-spi,所以内核会去找allwinner,sun8i-h3-spi。而在drivers/spi/spi-sun6i.c中:

static const struct of_device_id sun6i_spi_match[] = {
        { .compatible = "allwinner,sun6i-a31-spi", .data = &sun6i_a31_spi_cfg },
        { .compatible = "allwinner,sun8i-h3-spi",  .data = &sun8i_h3_spi_cfg },
        {
                .compatible = "allwinner,sun50i-r329-spi",
                .data = &sun50i_r329_spi_cfg

所以我们来到内核配置Device Drivers ---> SPI support,发现allwinner的spi就俩,一个Allwinner A31 SPI controller,一个Allwinner A31 SPI controller。查看Kconfig后发现,后者对应的symbol是SPI_SUN6I。故我们应该将后者设置成Y。而我之前是将前者设置成Y,后者设置成了N。

再次启动内核,一切都顺利加载

...

[    1.098960] enc28j60 spi0.0: Ethernet driver 1.02 probed
[    1.104485] enc28j60 spi0.0: Ethernet driver 1.02 loaded

....

[    4.343318] enc28j60 spi0.0 eth0: link down
Starting ntpd: [    4.354284] enc28j60 spi0.0 eth0: normal mode
[    4.359272] enc28j60 spi0.0 eth0: multicast mode
OK
[    5.292753] enc28j60 spi0.0 eth0: link up - Half duplex

...

移植之后

我的rootfs没有dhcp的软件,没法自动拿ip地址。所以我们需要配置一下buildroot。
进入目录,make menuconfig,进入
Target packages ---> Networking applications,勾上以下内容:

  • dhcp
    • dhcp relay
    • dhcp client
  • dhcpd
  • dhcpdump
  • dnsmasq
    • tftp support
    • dhcp support
  • ethtool
    • enable pretty printing
  • hostapd
  • httping
  • iptables
  • iw
  • lrzsz
  • ntp
    • ntpd
    • ntptime
  • openssh
    • client
    • server
    • key utilities
  • tcpdump
  • wget

再次重新上电,进入系统,输入ifconfig,非常漂亮地拿到了ip地址,不过由于校园网需要认证,目前没法访问内网和外网。

# ifconfig
eth0      Link encap:Ethernet  HWaddr 96:54:CC:C7:65:B4
          inet addr:59.66.*.*  Bcast:59.66.*.*  Mask:255.255.255.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:953 errors:0 dropped:0 overruns:0 frame:0
          TX packets:44 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:70235 (68.5 KiB)  TX bytes:3760 (3.6 KiB)
          Interrupt:67

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:128 errors:0 dropped:0 overruns:0 frame:0
          TX packets:128 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:9472 (9.2 KiB)  TX bytes:9472 (9.2 KiB)

困扰了两年的心结,从高二困扰到大一,终于解决了!