ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

嵌入式Linux网卡驱动实战:从零编写DM9000驱动全流程解析

嵌入式Linux网卡驱动实战:从零编写DM9000驱动全流程解析 搞嵌入式Linux的这两年我是真被“网卡驱动”折磨过几回。前阵子拿到一块Cortex-A7核心板U-Boot里网络正常系统起来之后死活没有eth0最后查来查去问题就出在板载的DM9000有线网卡设备树节点没配对。类似的坑我见过不少干脆把DM9000有线网卡驱动编写这件事从头到尾捋一遍。DM9000这颗芯片在嵌入式板卡上的出场率相当高S3C2440、S3C6410、i.MX系列、STM32MP1这类板子几乎都能见到它。对驱动开发入门者来说它集齐了平台驱动注册、内存映射IO、中断处理、net_device操作函数集、skb收发、PHY管理这些内核网络驱动核心要素啃透这一颗再看其他网卡驱动会顺畅很多。这篇文章不讲空话直接把我实际调试和编写驱动的完整流程、踩过的坑、排查思路全部摊开。内容面向正在学Linux驱动开发、或者拿到一块带DM9000板子不知道怎么下手的嵌入式工程师就算你之前没写过网卡驱动按这个思路走也能把驱动跑起来。1. DM9000这颗芯片为什么值得单独写一篇驱动教程1.1 芯片基本面10/100M自适应、内置PHY、16位总线DM9000系列是Davicom联杰国际出的10/100M自适应以太网控制芯片常见型号有DM9000A、DM9000C、DM9000E等。它最大的特点就是内部集成了PHY外部不需要再接单独的物理层收发芯片一颗芯片直接接RJ45变压器网络变压器就能出网口。整体BOM成本低、外围电路简单所以很多低成本嵌入式方案都在用。从CPU接口角度看DM9000挂的既不是SPI也不是I2C而是一条类SRAM的总线。它对外暴露两个主要的16位寄存器窗口一个是地址端口一个是数据端口。CPU把寄存器偏移地址写到地址端口再通过数据端口读写寄存器内容。系统上电后芯片支持8位和16位两种总线模式由硬件引脚配置决定目前绝大多数板卡都用16位模式。所以编写驱动时IO访问最好按16位来操作否则读出来的CHIP ID不对后面步骤全白搭。1.2 学这一颗驱动能顺手摸清网络驱动的整个骨架很多初学者一上来就想去写千兆/万兆网卡驱动那些驱动的复杂程度相当劝退。但DM9000不是这样它的寄存器数量有限收发路径清晰内核里还有现成的dm9000.c可以参考非常适合作为“第一个网络驱动”来学。把DM9000驱动写完你基本能掌握这些东西platform_driver的注册与内核设备模型的匹配机制platform_get_resource与ioremap如何拿到板级资源net_device结构体与net_device_ops操作函数集的填充alloc_etherdev分配网络接口与netdev_priv私有数据的用法中断处理函数里如何区分收发中断、上报skb给协议栈收包路径上的MTU、环形缓冲区、NAPI优化等概念后面无论去移植anybus、ax88796、lan8720还是其他厂商的网卡驱动核心思路都是同一套。只是寄存器不同、PHY管理方式略有差异你在DM9000上积累的框架感可以迁移过去。2. 驱动框架先行平台驱动注册与两个IO端口的访问模型2.1 platform_driver的入口怎么搭DM9000在大多数嵌入式Linux板卡上被描述为一个平台设备所以驱动入口用platform_driver最合理。你不用自己在init函数里手工ioremap和request_irq初始化逻辑全部放到probe回调里内核设备模型会自动帮你管理匹配、探测和释放的时机。驱动入口的骨架如下#include linux/module.h #include linux/kernel.h #include linux/netdevice.h #include linux/etherdevice.h #include linux/platform_device.h #include linux/interrupt.h #include linux/io.h #include linux/of.h static int dm9000_probe(struct platform_device *pdev) { return 0; } static int dm9000_remove(struct platform_device *pdev) { return 0; } static const struct of_device_id dm9000_of_match[] { { .compatible davicom,dm9000 }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, dm9000_of_match); static struct platform_driver dm9000_driver { .probe dm9000_probe, .remove dm9000_remove, .driver { .name dm9000, .of_match_table dm9000_of_match, }, }; module_platform_driver(dm9000_driver); MODULE_LICENSE(GPL);关键点在于compatible davicom,dm9000必须和设备树里的节点字符串完全一致。我调试时遇到过设备树里写的是davicom,dm9000a带a驱动却匹配davicom,dm9000导致probe函数根本没被调用。如果insmod后/sys/bus/platform/devices下没有生成对应设备先查字符匹配。2.2 probe里拿资源和中断号DM9000在硬件上占用两个IO地址段一个给地址端口一个给数据端口。设备树里对应的reg通常有两个单元interrupts配置则指向外部中断控制器。ethernet0 { compatible davicom,dm9000; reg 0x0 0x0 0x2, 0x0 0x400 0x2; interrupt-parent gpio1; interrupts 29 IRQ_TYPE_LEVEL_LOW; local-mac-address [00 11 22 33 44 55]; };probe函数里拿资源的方式如下static int dm9000_probe(struct platform_device *pdev) { struct resource *res; void __iomem *addr_port; void __iomem *data_port; int irq; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENXIO; addr_port devm_ioremap(pdev-dev, res-start, resource_size(res)); res platform_get_resource(pdev, IORESOURCE_MEM, 1); if (!res) return -ENXIO; data_port devm_ioremap(pdev-dev, res-start, resource_size(res)); irq platform_get_irq(pdev, 0); if (irq 0) return irq; dev_info(pdev-dev, dm9000 resource: addr%px, data%px, irq%d\n, addr_port, data_port, irq); return 0; }这里有个新手容易忽略的问题platform_get_irq的返回值如果小于0它不是“没有irq”而是错误码。很多老代码只判断irq 0在现代内核中0也是合法中断号必须改成判断irq 0否则中断申请那一步可能悄悄失败。2.3 地址端口数据端口DM9000的寄存器访问方式DM9000所有内部寄存器都通过地址端口数据端口两个IO地址来访问。地址端口写入要访问的寄存器偏移数据端口读写数据。以ioremap后的地址为例static inline u16 dm9000_ior(struct dm9000_priv *priv, int reg) { writeb(reg, priv-addr_port); return readw(priv-data_port); } static inline void dm9000_iow(struct dm9000_priv *priv, int reg, u16 value) { writeb(reg, priv-addr_port); writew(value, priv-data_port); }注意addr_port是8位写data_port按16位读写。DM9000的数据端口寄存器在16位模式下一次readw会读取两个字节。按这个模型寄存器0x28到0x2B的VID/PID读取方法就很好理解u32 id_val; id_val dm9000_ior(priv, 0x28); /* VIDL */ id_val | (u32)dm9000_ior(priv, 0x29) 8; /* VIDH */ id_val | (u32)dm9000_ior(priv, 0x2A) 16; /* PIDL */ id_val | (u32)dm9000_ior(priv, 0x2B) 24; /* PIDH */有人说读出来的ID高低字节是反的先别急着交换字节序考虑一下你的CPU端序和数据总线宽度。在16位总线上读一个32位寄存器值不同的ioremap映射方式可能导致高低16位整体互换这是调试时最容易懵的地方之一后面会展开说。3. net_device初始化顺序与MAC地址的命门3.1 分配net_device、填充net_device_opsDM9000驱动最终要呈现给内核的是一个标准的net_device接口。分配网络设备用alloc_etherdev它会自动为以太网设备分配默认的hard_header_len等参数同时为私有数据预留空间。私有数据结构通常放端口基地址、中断号、锁、统计信息等。net_device_ops是驱动和协议栈交互的“函数表”对DM9000来说至少要填充这几个回调ndo_open使能硬件、注册中断、启动队列ndo_stop关中断、停队列、释放中断ndo_start_xmit实际发送一个skbndo_set_mac_address运行时修改MAC地址可选但建议实现ndo_validate_addr可选用内核默认即可分配和初始化的代码大致是struct net_device *ndev; ndev alloc_etherdev(sizeof(struct dm9000_priv)); if (!ndev) return -ENOMEM; ndev-netdev_ops dm9000_netdev_ops; ndev-flags | IFF_MULTICAST; /* 设备私有数据 */ priv netdev_priv(ndev); priv-ndev ndev; priv-addr_port addr_port; priv-data_port data_port; priv-irq irq; platform_set_drvdata(pdev, ndev);顺序上有一个容易忽略的点先把私有数据填好再去做硬件reset和寄存器访问。因为在Linux驱动模型里中断有可能在request_irq之后立刻触发如果中断处理函数里会访问priv那priv必须在申请中断前已经初始化完毕否则读到野指针直接Oops。3.2 读芯片ID确认硬件可达拿到IO地址后第一步就是读芯片ID确认DM9000确实在总线上。这一步相当于硬件“握手”。如果读出来的ID跟预期相差很远后面一切寄存器配置都没有意义。芯片ID放在寄存器0x28-0x2B标准ID应为0x90000A46这里的高16位是PID、低16位是VID。实际内核源码的判断逻辑是id_val 0; for (i 0; i 4; i) id_val | (u32)dm9000_ior(priv, 0x28 i) (i * 8); if (id_val ! 0x90000A46) { dev_err(pdev-dev, wrong ID: 0x%08x\n, id_val); return -ENODEV; }注意有些DM9000A修订版本读出来可能不是严格的0x90000A46比如读ID低8位有差异但高16位一定是0x9000。如果高16位对得上、低16位对不上先不要立刻判定硬件坏排查一下是不是EEPROM里内容影响了VID一般来说VID是固定的0x0A46出现0x0A47之类就不正常。3.3 MAC地址检查没有EEPROM的板子最容易被坑DM9000可以在外部挂一颗93C46 EEPROM来保存MAC地址硬件复位后芯片自动把EEPROM里的值加载到MAC寄存器寄存器0x10-0x15。但不少低成本的板子根本没焊EEPROM此时芯片MAC寄存器全是0。Linux网络栈不允许一个网卡的MAC地址是全零不带MAC的网卡看起来能up但ARP回应、DHCP都会异常。更麻烦的是如果板卡在U-Boot阶段没设置MAC内核驱动就会拿到00:00:00:00:00:00。所以probe阶段必须做一次MAC地址的兜底处理/* 从硬件寄存器读MAC */ for (i 0; i 6; i) ndev-dev_addr[i] (u8)dm9000_ior(priv, 0x10 i); if (!is_valid_ether_addr(ndev-dev_addr)) { dev_warn(pdev-dev, MAC address invalid, using eth_random_addr\n); eth_random_addr(ndev-dev_addr); /* 写回硬件 */ for (i 0; i 6; i) dm9000_iow(priv, 0x10 i, ndev-dev_addr[i]); }这里我在多个项目里踩过坑如果板卡设计上EEPROM位号是空的你必须在驱动里随机生成MAC否则业务侧会反馈“设备偶尔上不了网”。早期U-Boot也可能设置过MAC但如果内核驱动在probe阶段把它覆盖成全零同样出问题。稳妥的做法是把MAC检查放在读取硬件寄存器之后确认无效时才写随机地址。3.4 开硬件收发前先做软件复位和PHY准备DM9000除了数据收发寄存器外还有几个控制寄存器需要配置到位。我的习惯是先把软件复位做了再等待芯片稳定然后设置接收控制寄存器和发送控制寄存器。软件复位的实现方式是设置通用控制寄存器GPR的bit0为1再清除。有些平台代码里会直接操作NCR寄存器但GPR是数据手册明确标出来用于软件复位的寄存器/* 软件复位 */ dm9000_iow(priv, 0x1e, 0x01); /* GPR bit01 */ msleep(20); dm9000_iow(priv, 0x1e, 0x00); msleep(20);复位之后设置接收和发送控制寄存器/* RCR: 丢弃长包 丢弃CRC错包 使能接收 */ dm9000_iow(priv, 0x05, 0x10 | 0x08 | 0x01); /* TCR: 使能发送 */ dm9000_iow(priv, 0x02, 0x01);RCR的bit0是RXEN接收使能bit3是丢弃CRC错误包bit4是丢弃长包TCR的bit0是TXEN发送使能。这些寄存器相对简单但必须配置在ndo_open里而不是probe里否则会出现网口up之前芯片就开始收包中断风暴先于网络栈准备好系统直接被拖死。4. 数据收发主路径从FIFO到内核协议栈4.1 发送路径ndo_start_xmit到MWCMD发送路径是理解“写驱动到底在写什么”的第一站。协议栈把要发出的数据包封装成skb调用ndo_start_xmit。驱动的任务就是把skb里的数据搬进DM9000的发送FIFO然后设置发送长度寄存器最后触发发送。DM9000的发送FIFO通过内存写命令寄存器MWCMD0xF8访问发送长度寄存器是TXPLL/TXPLH。实际发送函数骨架static netdev_tx_t dm9000_start_xmit(struct sk_buff *skb, struct net_device *ndev) { struct dm9000_priv *priv netdev_priv(ndev); u32 len skb-len; u8 *data skb-data; if (len DM9000_MAX_PACKET_SIZE) { dev_kfree_skb(skb); return NETDEV_TX_OK; } /* 写发送长度 */ dm9000_iow(priv, 0xFC, len 0xff); dm9000_iow(priv, 0xFD, (len 8) 0xff); /* 数据写入FIFO */ dm9000_iow(priv, 0xF8, 0x00); /* 第一次触发 */ for (i 0; i (len 1) / 2; i) writew(((u16 *)data)[i], priv-data_port); /* 触发发送 */ dm9000_iow(priv, 0x02, 0x01); dev_kfree_skb(skb); return NETDEV_TX_OK; }等等一般先使能TCR设置长度再填数据最后再触发一次。具体芯片手册会有microcode时序要求。我这里写的是一个典型流程在实际项目里我建议先参考内核现有dm9000.c把发送状态机和MWCMD的触发顺序抄对再根据板级情况调整。最重要的点是调用ndo_start_xmit时如果返回忙协议栈会重传不要在中断上下文里反复尝试发送否则死锁风险很大。发送完成之后驱动还需要在发送完成中断里更新统计信息netif_trans_update(ndev)这样协议栈知道发包没有卡死。4.2 接收路径中断与MRCMD接收路径是DM9000驱动另一个核心也是中断处理函数的主要工作。芯片收到网络包后会触发接收中断ISR的bit0驱动在中断服务函数里读出包状态和长度然后从FIFO读取数据构造skb最后调用netif_rx把skb交给协议栈。接收中断处理的大致思路static irqreturn_t dm9000_interrupt(int irq, void *dev_id) { struct net_device *ndev dev_id; struct dm9000_priv *priv netdev_priv(ndev); u8 isr; isr (u8)dm9000_ior(priv, 0xFE); /* ISR */ if (isr 0x01) { /* 接收完成 */ dm9000_rx(ndev); } if (isr 0x02) { /* 发送完成 */ clear_bit(...); /* 清发送状态 */ netif_wake_queue(ndev); } /* 写1清中断写回ISR */ dm9000_iow(priv, 0xFE, isr); return IRQ_HANDLED; }接收函数dm9000_rx的核心逻辑是先读MRCMDX判断FIFO里是否有包再读MRCMD获取状态和长度然后逐个16位字读取数据。这里有一个关键点接收数据长度寄存器读回来要拼成完整长度新手经常只读低8位导致大包截断。代码骨架static void dm9000_rx(struct net_device *ndev) { struct dm9000_priv *priv netdev_priv(ndev); struct sk_buff *skb; u8 rxb, status; u16 len; while (1) { rxb (u8)dm9000_ior(priv, 0xF0); /* MRCMDX */ if (!(rxb 0x01)) break; /* FIFO空退出 */ status (u8)dm9000_ior(priv, 0xF2); /* MRCMD低字节 */ len (u16)dm9000_ior(priv, 0xF2); /* MRCMD */ len (len 0xff) | (dm9000_ior(priv, 0xF2) 8); if (status 0x01) { /* 出错 */ continue; } skb netdev_alloc_skb(ndev, len 2); if (!skb) { ndev-stats.rx_dropped; continue; } skb_reserve(skb, 2); /* 从FIFO读数据 */ for (i 0; i (len 1) / 2; i) ((u16 *)skb_put(skb, 2))[0] readw(priv-data_port); skb-protocol eth_type_trans(skb, ndev); netif_rx(skb); ndev-stats.rx_packets; ndev-stats.rx_bytes len; } }这段代码看起来简单但实际要处理好几个边界条件skb_put的字节数不足时会产生越界MRCMD读FIFO时机不对会导致数据错位status里某些错误位需要跳过而不是直接丢弃整个包。这些都是在调试中一点点发现的建议先用一台PC和板子互ping再逐步加大流量验证。4.3 关于NAPI先跑通再优化的节奏DM9000属于低速率网卡中断模式在小流量下完全够用。但如果你在压力测试中发现CPU占用率异常高或者中断太频繁导致系统卡顿这时可以引入NAPI。NAPI的核心思想是高负载时把收包从“每个包都触发中断”切换为“关闭接收中断轮询收包”减少中断上下文切换开销。在内核网卡驱动里实现NAPI要改动几个地方分配net_device后调用netif_napi_add注册poll函数在中断处理函数里如果判断有接收事件就调用napi_schedule并在poll函数里循环调用收包逻辑。相较普通中断收包把netif_rx改成napi_gro_receive还会带来LRO/GRO合并的好处。但我的建议是第一版驱动先用中断模式跑通功能正常后再加NAPI优化。过早引入NAPI会让收发路径变复杂一旦出错很难判断是基础配置问题还是NAPI调度问题。我自己就在项目里因为NAPI的poll预算没处理好出现高负载时一个队列的包永远收不完最后只能改回中断模式。性能优化的前提是功能正确。5. 实测中绕不开的坑ID读不到、link起不来、中断异常5.1 芯片ID读错地址与字节序的二选一ID读不对最常见原因是设备树里两个reg的地址给反了。DM9000的地址端口和数据端口是由CMD#引脚或者说地址线A0区分的两个IO段可能是相邻的也可能相差很大。设备树里reg[0]给了地址端口、reg[1]给了数据端口如果反了你写寄存器地址时实际写到数据端口读写全乱。排查方法很简单在probe函数里读出ID后立刻打印如果ID是0x00000000或0xffffffff先交换设备树reg的两个段试试如果ID的字节序是倒的比如读出0x460a0090那多半是16位总线高低字节交换问题可以对ioremap后的地址加一个readw字节交换封装或者用ioremap_nocache再配合le16_to_cpu转换。另外一个隐蔽原因是GPIO复位脚没释放。有些板卡DM9000的RESET脚接在GPIO上GPIO默认为低导致芯片一直被复位此时读ID全是0xff。排查时用示波器或者先读一个可以自检的寄存器看看如果连GPR寄存器都写不进去八成复位信号就没拉起来。5.2 ping不通的排查顺序驱动加载成功、ifconfig看到eth0、链路也显示UP RINING但ping不通这种情况我在不同板卡上遇到过不下五次。每次排查顺序都很固定先确认MAC地址不是全零特别是没有EEPROM的板子。再确认ARPping一下自己的IP看有没有ARP应答没有则检查收发路径。然后抓包看发送方向在另一台PC上开Wireshark抓ping包如果PC能收到请求但板子不回包问题在DM9000接收路径或协议栈回包路径如果PC收不到任何包问题在发送路径。接着检查MTUDM9000建议保持默认1500不要盲目调大。有一次我的驱动问题出在RCR配置上我把PROMISC位设置了结果接收路径收到了大量非本机MAC的广播包和混杂包协议栈把它们都过滤了正常单播反而被吞掉。后来去掉PROMISC位一切正常。遇到ping不通先检查寄存器配置别一头扎进中断处理函数里改代码。5.3 中断服务函数里别乱来网卡驱动中断服务函数受限于上下文不能调用可能睡眠的函数。很多驱动新手习惯在中断里加printk调试这在测试的时候会拖慢中断处理速度严重时直接触发中断嵌套或丢失后到的网络包。我自己调试时一般是先关掉CONFIG_DEBUG_ATOMIC_SLEEP检查确认没有调度器报错之后再逐步把printk删掉。还有一个细节是中断触发类型。DM9000的中断输出是低电平有效设备树里一定要配置成IRQ_TYPE_LEVEL_LOW如果用IRQ_TYPE_EDGE_FALLING在某些芯片上会丢边沿导致中断半天不触发。如果用了共享中断GPIO控制器可能不支持电平触发就要在驱动里额外做中断状态确认否则一个中断源没处理完其他设备的中断全被连带阻塞。6. 调完不是结束性能验证与稳定性测试6.1 基础连通性验证清单驱动能跑起来只是第一步我一般按这个清单做基础验证ifconfig确认MAC、IP、RX/TX计数ping网关100个包确认丢包率为0ssh登录板子确认TCP交互正常通过scp传一个超过100MB的文件确认大流量下断流情况ethtool查link速度和双工确认自协商结果是100M/Full如果哪一步出问题先回看对应路径ping不通查收发寄存器scp中断查中断频率和skb分配双工不对查PHY配置。6.2 吞吐量怎么看DM9000是10/100M网卡理论吞吐上限大概在90Mbps到95Mbps左右实际受中断开销和协议栈处理影响。测试时建议用iperf分别测TCP和UDP# 服务端PC iperf -s # 客户端板子 iperf -c 192.168.1.100 -t 30如果在板子上跑iperf单核CPU占用率往往比较高这是正常现象因为DM9000的收发路径以中断方式触发固定带宽下中断频率很高。如果实测吞吐只有二三十Mbps重点检查以下几项skb分配是否每次都失败看rx_dropped计数中断处理函数里是否做了过多不必要的寄存器读取发送路径是否有无谓的数据拷贝比如多了一次memcpy是否有其他设备与DM9000共享中断导致延迟抖动过大我接触过的几个项目里DM9000吞吐偏低的原因基本都是中断处理里多读了一两次状态寄存器每包多花1到2微秒累积起来就损失二三十Mbps带宽。优化时用perf看一下中断处理函数的热点通常会有收获。6.3 压力测试与长期运行嵌入式设备一般要求7x24小时不掉线。我在交付前通常会做一轮长时间压力测试板子与PC之间连续双向打流1小时观察有无断流每5分钟ping一次记录最大延迟和丢包反复拔插网线确认link up/down中断处理正常用watch -n 1 cat /proc/interrupts观察dm9000中断计数是否稳定增长这种测试最容易暴露一类问题长期运行后skb泄漏。如果cat /proc/slabinfo里skbuff_head_cache持续增长不回落说明驱动在某个分支上没释放skb。还有一个常见泄漏点是中断里分配skb失败后没有释放FIFO里的数据导致芯片FIFO卡在满状态驱动从此收不了包。压力测试阶段遇到这种问题不用慌在接收函数里加一个计数器看rx_dropped是否在持续增加很快就能定位到是上层来不及处理还是驱动自身丢包。写到这里我把DM9000有线网卡驱动编写的完整思路基本盘了一遍。对我个人而言这颗芯片驱动最大的价值不在于代码量多少而在于它把一个网络驱动该有的骨架全展示了一遍平台总线的接入、IO访问模型、net_device初始化、MAC管理、中断处理、skb收发、PHY配置、NAPI取舍、性能验证。把这些点串起来再去看任何一款网卡驱动的代码都不会觉得是无字天书。最后分享一个小技巧调试网卡驱动时先不要打开NAPI不要开硬件时间戳也不要试图做任何零拷贝优化。先用最简单的中断模式把链路跑通再逐步加功能。很多人花大量时间优化一个本身就低速率的老芯片最后发现瓶颈在协议栈而不是驱动得不偿失。驱动开发讲究的是稳中求进这条规则适用到任何网络设备驱动上。
返回列表