Skip to content

LV005-DM9000驱动实现 ​

前置阅读:上一篇 LV001-以太网与DM9000硬件接口 讲了引脚、片选与地址线怎么接;本篇讲拿到这块硬件之后,驱动里每一段代码在干什么。

一、 驱动概览与对外接口 ​

1. 文件与职责 ​

驱动只有两个文件,职责分得很清楚:

文件内容
Drivers/BSP/Ethernet/dm9000.h寄存器索引与位定义、DM9000_TypeDef 结构体、配置结构体 dm9000_config、对外函数声明
Drivers/BSP/Ethernet/dm9000.c复位、寄存器读写、PHY 访问、初始化、收发、中断回调、诊断计数

它不依赖 lwIP 的核心头文件以外的东西:除了发送函数接收 struct pbuf *,其余全部是裸寄存器操作。这样分层的好处是"协议栈换版本、驱动不用动"。

2. 对外函数一览 ​

函数作用
DM9000_Init()初始化芯片,返回 0 成功、1 读 ID 失败
DM9000_Reset()硬复位(GPIO 拉低 RST)+ 复位寄存器,并等待 NCR_RST 自动清零
DM9000_ReadReg() / DM9000_WriteReg()读写芯片寄存器(索引 + 数据口两次访问)
DM9000_PHY_ReadReg() / DM9000_PHY_WriteReg()通过 EPAR / EPCR 间接读写内部 PHY 寄存器
DM9000_Get_DeviceID()拼出 32 位芯片 ID
DM9000_Set_PHYMode()设置 PHY 工作模式(10M/100M、半双工/全双工/自协商)
DM9000_Get_SpeedAndDuplex()读回当前链路速度与双工状态
DM9000_Set_MACAddress() / DM9000_Set_Multicast()写 MAC 到 PAR、写组播哈希到 MAR
DM9000_SendPacket()发送一个 pbuf 链
DM9000_Receive_Packet()从 RX SRAM 取一个包,返回 pbuf,无包或丢包时返回 NULL
DM9000_TakeRxFlag()读取并清除"有待收包"的软件标志(临界区内完成)
DM9000_DbgPoll()诊断计数周期打印(默认关闭)
HAL_GPIO_EXTI_Callback()EXTI 回调,清中断源并置接收标志

二、 寄存器访问模型 ​

1. 索引加数据口 ​

上一篇算出的两个地址在驱动里就是结构体的两个成员:

c
// Drivers/BSP/Ethernet/dm9000.c
uint16_t DM9000_ReadReg(uint16_t reg)
{
    uint32_t pm = DM9000_BusEnter();
    DM9000->REG = reg;      /* 第一步: 写索引 */
    uint16_t value = DM9000->DATA;   /* 第二步: 读数据 */
    DM9000_BusExit(pm);
    return value;
}

2. 为什么这两步必须原子完成 ​

"写索引 + 读写数据"是一个整体,中间绝不能被别的访问插进来。 原因是寄存器窗口只有一个:一旦被插入一次别的寄存器访问,索引就被改掉了,接下来的数据读到的就是另一个寄存器的值——而且是静默错误,读出来是个合法但不对的数。

这个风险在本工程里是真实存在的:收发数据包时会长时间连续读写数据口(流式读 FIFO),而 EXTI 中断回调里又要读 ISR、写 ISR。两个上下文都在用同一个寄存器窗口,必然打架。

所以驱动里有一对互斥函数 DM9000_BusEnter() / DM9000_BusExit():用 __get_PRIMASK() 保存、__disable_irq() 关中断、退出时原样恢复,可嵌套。中断模式下它们真的关中断,轮询模式下则编译成空操作。

三、 初始化流程 ​

1. 复位与 ID 校验 ​

初始化的第一件事是确认芯片真的在:

c
// Drivers/BSP/Ethernet/dm9000.c
uid = *(volatile uint32_t *)0x1FFFF7E8U;         /* 芯片 96 位 UID 的前 32 位 */
...                                              /* 组 MAC: 02:00:00 + UID 低三字节 */
DM9000_Reset();                                  /* 硬复位 + 软复位 */
delay_ms(100);
temp = DM9000_Get_DeviceID();
printf("DM9000: ID = 0x%08X\r\n", temp);
if (temp != DM9000_ID) { return 1; }             /* 期望 0x90000A46 */

顺带完成的两件事:

(1)MAC 地址在这里生成:高三字节固定为 02:00:00(本地管理地址块),低三字节取自芯片 UID,所以每一块板子都不重复。理由见 LV030-ARP与MAC地址 第一节。

(2)中断掩码在这里定:imr_all = IMR_PAR | IMR_PRI(指针自动回卷 + 接收中断);轮询模式下只留 IMR_PAR。

复位是硬件与软件各来一次:先把 RST 引脚拉低再拉高(硬复位,DM9000_Reset() 内部用 GPIO 完成),再写 NCR 的 NCR_RST 位做软复位,并轮询等待该位自动清零。

2. 参数与接收控制 ​

ID 校验通过之后,按固定顺序配置:

顺序动作目的
1DM9000_Set_PHYMode(AUTO)使能 PHY 自协商
2NCR = 0x00、TCR = 0x00清控制寄存器
3BPTR、FCTR、FCR、SMCR 赋值后端压力、流控与特殊模式
4NSR = NSR_WAKEST | NSR_TX2END | NSR_TX1END清状态位(写 1 清)
5ISR = 0x0F清掉上电残留的中断标志
6TCR2 = 0x80LED 模式
7DM9000_Set_MACAddress() / DM9000_Set_Multicast()写入 PAR / MAR
8RCR = RCR_DIS_LONG | RCR_DIS_CRC | RCR_RXEN打开接收,长包与 CRC 错的包直接丢
9IMR = IMR_PAR → 读链路状态 → IMR = dm9000cfg.imr_all先屏蔽中断查链路,最后才开中断

第 9 步的顺序值得留意:"查链路状态"期间先不开中断,避免刚上电、链路还没协商好时被杂散中断打扰。

3. 初始化成功的判据 ​

串口上出现这一行才算网卡就绪:

text
DM9000: ID = 0x90000A46

读到 0x00000000 或 0xFFFFFFFF 多半是总线问题(FSMC 没初始化、片选或数据宽度不对),而不是芯片坏了——排查思路见 LV001 第五节。

四、 发送路径 ​

1. 写入发送 SRAM ​

发送要解决的核心问题是:pbuf 可能是链式的,而 TX FIFO 只接受连续的 16 位字流。

驱动的做法是把整条 pbuf 链逐字节"拼"成 16 位字再写进去:

c
// Drivers/BSP/Ethernet/dm9000.c
DM9000_WriteReg(DM9000_IMR, IMR_PAR);       /* 发送期间关网卡中断 */
pm = DM9000_BusEnter();                     /* 整包写入期间索引必须保持 MWCMD */
DM9000->REG = DM9000_MWCMD;                 /* 之后对 DATA 口的写即进入 TX SRAM */
...                                         /* 逐字节拼 16 位写入, 奇数长度补最后一个字节 */
DM9000_BusExit(pm);

两个细节:

(1)临界区包住整段写入。因为整段写入依赖"索引一直停在 MWCMD",中途一旦被中断抢走并改写索引,后面写进去的数据就跑到别的寄存器去了。这是这套访问模型下最容易踩的坑。

(2)偶数对齐处理。DM9000 的内部总线是 16 位的,长度是奇数时最后要补一个字节,否则芯片会认为这一包没收完。

2. 触发与等待发送完成 ​

写完之后依次是:写长度低字节 TXPLL、高字节 TXPLH、置 TCR 的 0x01 触发发送,然后带超时地等 ISR 的 ISR_PTS 位:

c
timeout = 0x00FFFFFFU;
while (((DM9000_ReadReg(DM9000_ISR) & ISR_PTS) == 0U) && (--timeout != 0U)) { }
DM9000_WriteReg(DM9000_ISR, ISR_PTS);       /* 写 1 清发送完成标志 */
DM9000_WriteReg(DM9000_IMR, dm9000cfg.imr_all);  /* 恢复中断 */

注意这里是轮询 ISR 而不是 NSR,且必须带超时:网线拔掉、PHY 挂了、对端一直不回,都可能让发送完成位永远不来;没有超时保护就是死等,整块板子卡死在这里。

五、 接收路径与中断 ​

1. 预读握手与首字节校验 ​

DM9000 的接收 FIFO 采用"预读"机制:读 MRCMDX(0xF0)不弹包,只是把下一个包的首字节预取出来。驱动必须先做一次假读、再读数据口拿这个字节:

首字节含义处理
0RX FIFO 里没有包返回 NULL
1有包,指针正常继续弹包
其它指针错乱打印 rx error, stop device,关接收(RCR = 0x00),返回 NULL

第三个分支是这套机制的自检:一旦 FIFO 指针与芯片预期不一致,继续读下去只会读到垃圾,所以宁可先停下接收。能恢复的场合(比如长度越界)驱动会写 NCR 的 NCR_RST 做一次软复位后继续。

2. 弹出数据包与状态检查 ​

确认有包之后,写 MRCMD(0xF2),随后连续读数据口,前 4 个字节是包头:

text
读 MRCMD 后面的前 4 字节: [0x01][状态][长度低][长度高]

驱动据此申请 pbuf_alloc(PBUF_RAW, rx_length, PBUF_POOL),把剩余字节按 16 位读进 pbuf。包头与长度的合法性检查是:

检查判定条件失败处理
状态位(rx_status & 0xBF00) != 0打印 bad status / fifo error / crc error,丢包
长度下限rx_length < 0x40(小于 64 字节)丢弃(以太网最小帧长)
长度上限rx_length > DM9000_PKT_MAX(1536)丢弃并软复位恢复指针

丢包时必须把 FIFO 里这一包的数据读完再丢,否则剩下的字节会污染下一个包——这也是"pbuf 申请失败仍要继续读完"的原因。

3. 中断回调必须写 1 清标志 ​

DM9000 的 INT 引脚是电平保持型:有包时拉低,只有把 ISR 里对应的位写 1 清除之后才会回到高电平。触发方式又是下降沿,所以回调里不能只置标志就返回:

c
// Drivers/BSP/Ethernet/dm9000.c
do
{
    isr = DM9000_ReadReg(DM9000_ISR);
    if ((isr & ISR_PRS) != 0U) { s_rx_flag = 1; }
    DM9000_WriteReg(DM9000_ISR, isr);   /* 关键: 写 1 清中断源 */
} while (HAL_GPIO_ReadPin(DM9000_INT_GPIO_Port, DM9000_INT_Pin) == GPIO_PIN_RESET);

漏掉这一步的典型症状是:第一次收包正常,之后所有包全丢——因为下降沿只出现过一次,之后 INT 一直保持在低电平。

回调里只做三件事:读 ISR、置软件标志、清中断源。真正的取包留到主循环,因为中断里做 FIFO 流式读会长时间占用总线,还会与协议栈的其他访问互相打断。

4. 收包模式与中断线 ​

项取值
模式宏DM9000_RX_INT_MODE(dm9000.h,当前为 1 = 中断模式)
中断线EXTI9_5_IRQn(PG6)
触发边沿下降沿,内部上拉
中断优先级3, 3
主循环取包入口DM9000_TakeRxFlag() → ethernetif_input()

轮询模式下 DM9000_RX_INT_MODE = 0,驱动不打开接收中断,主循环无条件查 FIFO 状态,BusEnter / BusExit 也退化成空操作。

DM9000 驱动的初始化、发送与接收路径

六、 临界区与诊断计数 ​

1. 临界区保护哪些段落 ​

段落为什么要保护
DM9000_ReadReg / DM9000_WriteReg"写索引"与"读写数据"两步之间不能被插入别的访问
发送整包写入整段写入依赖索引停在 MWCMD
接收弹包同上,且要与中断回调互斥
DM9000_TakeRxFlag()读标志与清标志要原子,否则可能漏掉一次置位

实现方式统一是"保存 PRIMASK → 关中断 → 处理 → 恢复 PRIMASK",而不是简单开关中断——这样嵌套调用时不会破坏外层的中断状态。

2. 诊断计数怎么用 ​

驱动里留了一组默认关闭的计数器(dm9000.c 顶部的 DM9000_NETDBG_EN):

计数含义
s_dbg_extiEXTI 触发次数
s_dbg_rx_ok成功取到的包数
s_dbg_rx_drop因状态错误或内存不足丢弃的包数
s_dbg_tx发送次数

打开后 DM9000_DbgPoll() 会每 2 秒打一行 exti=… rx_ok=… rx_drop=… tx=… nsr=0x…。排错时的读法是:

  • exti 远大于 rx_ok:中断来了但主循环没及时取包,说明主循环被什么堵住了;
  • rx_drop 持续增长:多半是 pbuf_alloc 失败(PBUF_POOL_SIZE = 8 用光),要去查上层有没有卡住收发;
  • nsr 的低位反映链路状态,可以用来确认 PHY 有没有协商成功。

调通之后这个宏默认关掉,避免刷屏占用串口带宽——这也是为什么看到 DM9000_NETDBG_EN 0 时不要以为诊断能力被删掉了。

七、 小结与参考资料 ​

1. 本篇小结 ​

  • 驱动的核心是"索引 + 数据口"两次访问模型,两步之间必须原子,靠 DM9000_BusEnter/BusExit(PRIMASK 保存恢复)保证,轮询模式下退化为空操作。
  • DM9000_Init() 的顺序是:生成 MAC → 硬复位 + 软复位 → 读 ID 校验(0x90000A46)→ 配 PHY 与各类控制寄存器 → 写 PAR/MAR → 开接收 → 最后才打开中断。
  • 发送靠把 pbuf 链逐字节拼成 16 位字写入 TX SRAM,然后写长度、置 TCR 触发,带超时等 ISR_PTS。
  • 接收先对 MRCMDX 做预读握手,首字节非 0/1 说明指针错乱要停接收;弹包后校验状态位与长度,失败也必须把 FIFO 读完。
  • 中断是电平保持型,回调必须写 1 清 ISR,否则只有第一次能收到包。
  • 诊断计数用来区分"中断没来""主循环没取""内存不够丢包"三类问题。

2. 参考资料 ​


本文档由 markdowncli 技能辅助生成