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. 索引加数据口
上一篇算出的两个地址在驱动里就是结构体的两个成员:
// 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 校验
初始化的第一件事是确认芯片真的在:
// 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 校验通过之后,按固定顺序配置:
| 顺序 | 动作 | 目的 |
|---|---|---|
| 1 | DM9000_Set_PHYMode(AUTO) | 使能 PHY 自协商 |
| 2 | NCR = 0x00、TCR = 0x00 | 清控制寄存器 |
| 3 | BPTR、FCTR、FCR、SMCR 赋值 | 后端压力、流控与特殊模式 |
| 4 | NSR = NSR_WAKEST | NSR_TX2END | NSR_TX1END | 清状态位(写 1 清) |
| 5 | ISR = 0x0F | 清掉上电残留的中断标志 |
| 6 | TCR2 = 0x80 | LED 模式 |
| 7 | DM9000_Set_MACAddress() / DM9000_Set_Multicast() | 写入 PAR / MAR |
| 8 | RCR = RCR_DIS_LONG | RCR_DIS_CRC | RCR_RXEN | 打开接收,长包与 CRC 错的包直接丢 |
| 9 | IMR = IMR_PAR → 读链路状态 → IMR = dm9000cfg.imr_all | 先屏蔽中断查链路,最后才开中断 |
第 9 步的顺序值得留意:"查链路状态"期间先不开中断,避免刚上电、链路还没协商好时被杂散中断打扰。
3. 初始化成功的判据
串口上出现这一行才算网卡就绪:
DM9000: ID = 0x90000A46读到 0x00000000 或 0xFFFFFFFF 多半是总线问题(FSMC 没初始化、片选或数据宽度不对),而不是芯片坏了——排查思路见 LV001 第五节。
四、 发送路径
1. 写入发送 SRAM
发送要解决的核心问题是:pbuf 可能是链式的,而 TX FIFO 只接受连续的 16 位字流。
驱动的做法是把整条 pbuf 链逐字节"拼"成 16 位字再写进去:
// 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 位:
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)不弹包,只是把下一个包的首字节预取出来。驱动必须先做一次假读、再读数据口拿这个字节:
| 首字节 | 含义 | 处理 |
|---|---|---|
| 0 | RX FIFO 里没有包 | 返回 NULL |
| 1 | 有包,指针正常 | 继续弹包 |
| 其它 | 指针错乱 | 打印 rx error, stop device,关接收(RCR = 0x00),返回 NULL |
第三个分支是这套机制的自检:一旦 FIFO 指针与芯片预期不一致,继续读下去只会读到垃圾,所以宁可先停下接收。能恢复的场合(比如长度越界)驱动会写 NCR 的 NCR_RST 做一次软复位后继续。
2. 弹出数据包与状态检查
确认有包之后,写 MRCMD(0xF2),随后连续读数据口,前 4 个字节是包头:
读 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 清除之后才会回到高电平。触发方式又是下降沿,所以回调里不能只置标志就返回:
// 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 也退化成空操作。
六、 临界区与诊断计数
1. 临界区保护哪些段落
| 段落 | 为什么要保护 |
|---|---|
DM9000_ReadReg / DM9000_WriteReg | "写索引"与"读写数据"两步之间不能被插入别的访问 |
| 发送整包写入 | 整段写入依赖索引停在 MWCMD |
| 接收弹包 | 同上,且要与中断回调互斥 |
DM9000_TakeRxFlag() | 读标志与清标志要原子,否则可能漏掉一次置位 |
实现方式统一是"保存 PRIMASK → 关中断 → 处理 → 恢复 PRIMASK",而不是简单开关中断——这样嵌套调用时不会破坏外层的中断状态。
2. 诊断计数怎么用
驱动里留了一组默认关闭的计数器(dm9000.c 顶部的 DM9000_NETDBG_EN):
| 计数 | 含义 |
|---|---|
s_dbg_exti | EXTI 触发次数 |
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. 参考资料
- 驱动源码:
dm9000.c、dm9000.h - EXTI 中断服务函数:
stm32f1xx_it.c - 中断引脚配置:
gpio.c - 手册:
STM32F1 LWIP开发手册(DM9000版)_V1.1.pdf第一章 1.3 节
本文档由 markdowncli 技能辅助生成