简介:SR8201F 是面向物联网场景的以太网芯片,这份代码包正是其 STM32 平台通用驱动,面向嵌入式软硬件开发者,解决 SR8201F 在 STM32 工程中初始化、MAC/DMA 收发与协议适配等底层移植难题,适合正在调试以太网外设或需要快速集成网络通信能力的工程师。包体很小,共 2 个文件——1 个 h 头文件和 1 个 c 源文件,压缩后仅 32KB;头文件用于接口声明与寄存器定义,源文件实现 PHY 配置、MAC 收发状态机启动以及 DMA 传输控制,代码精简,可直接嵌入现有嵌入式工程。从预览片段可以看到 ETH_Start 的完整启动流程:使能 MAC 收发、刷新发送 FIFO、开启 DMA 收发,对理解 STM32 以太网底层机制、排查初始化顺序和 DMA 启动问题很有帮助。整体轻量、结构清楚,便于对照 SR8201F 数据手册逐段学习,也能作为其他 STM32 平台网络驱动的移植参考。目前已有 740 人学习下载,适合作为 SR8201F 驱动移植、代码裁剪和功能验收时的直接参考。
1. SR8201F是什么:STM32要连网口时,这颗PHY和这份驱动能帮你省下两天排查时间
一块带网口的板子从贴片厂回来,SWD能连、LED能闪,唯独RJ45那两盏灯不亮。这时候十有八九要查的不是STM32的MAC,而是外挂的那颗PHY。SR8201F就是这样一颗物联网设备里常见的10/100M以太网PHY芯片:STM32内置了MAC控制器,但物理层收发、速率协商、链路检测这些活全压在PHY身上。所谓“通用驱动代码”,就是把PHY的复位、寄存器读写、协商状态查询封装成几个C接口,让你不用从头啃数据手册。这套方案适合谁?用STM32F4/H7这类内置MAC芯片做网关、采集器,手头拿到一份驱动却不知道怎么接到自己板子上的人。下面我把移植路径和会踩的坑一次讲清。
2. 先看懂通用驱动代码的结构:从PHY寄存器到STM32以太网MAC
拿到一份通用驱动,第一件事不是复制粘贴,而是搞清它内部的层次。SR8201F驱动本质上只做两件事:通过MDIO读写PHY寄存器,把协商结果交给MAC和协议栈。你以为自己在调“以太网”,其实大部分时间都在调几个16位寄存器。
2.1 RMII接口与MDIO管理口:驱动主要面对的两块硬件
PHY与STM32之间有两类信号:数据通道和管理通道。RMII模式下数据通道只用7根线:TX_EN、TXD0、TXD1、RXD0、RXD1、CRS_DV、REF_CLK,把标准MII的16根信号线压掉一半,这是STM32这类内置MAC芯片最常用的接法。管理通道则是MDC和MDIO两根线,跑的是慢速串行协议,所有PHY寄存器读写都走这两根线。
真正写代码的工作集中在MDIO上。RMII数据线的引脚由硬件原理图决定,软件改不了,而PHY的复位、自动协商、链路状态查询,全部要靠MDIO访问寄存器实现。驱动包的寄存器定义一般长这样:
/* phy_sr8201f_regs.h */ #define PHY_REG_BCR 0x00u /* 基本控制寄存器 */ #define PHY_REG_BSR 0x01u /* 基本状态寄存器 */ #define PHY_REG_ID1 0x02u /* PHY ID 高 16 位 */ #define PHY_REG_ID2 0x03u /* PHY ID 低 16 位 */ #define PHY_REG_ANAR 0x04u /* 自动协商广告寄存器 */ #define PHY_REG_ANLPAR 0x05u /* 对端能力寄存器 */ #define PHY_REG_ANER 0x06u /* 自动协商扩展寄存器 */ #define PHY_BCR_RESET 0x8000u /* 软件复位 */ #define PHY_BCR_AN_EN 0x1000u /* 自动协商使能 */ #define PHY_BCR_SPEED_100 0x2000u /* 速度:100M */ #define PHY_BCR_DUPLEX_FULL 0x0100u /* 双工:全双工 */ #define PHY_BSR_LINK 0x0004u /* 链路建立 */ #define PHY_BSR_AN_DONE 0x0020u /* 自动协商完成 */BCR、BSR、ID1、ID2、ANAR、ANLPAR是IEEE 802.3里所有PHY通用的标准寄存器,SR8201F也遵循这套布局。0x00是控制寄存器,写复位、写自动协商使能都在这里;0x01是状态寄存器,读链路状态、读协商完成标志在这里;0x02和0x03是出厂ID,用来确认总线上挂的确实是这颗片。ANAR和ANLPAR是协商时交换能力用的,对端能力都写在ANLPAR里。
底层MDIO读写,在STM32的HAL库里有现成接口,驱动包一般再包一层,方便统一返回值:
/* phy_hal_stm32.c */ static ETH_HandleTypeDef *g_heth; /* CubeMX生成的ETH句柄 */ int phy_mdio_read(uint16_t phy_addr, uint16_t reg, uint16_t *val) { if (HAL_ETH_ReadPHYRegister(g_heth, phy_addr, reg, val) != HAL_OK) { return -1; /* 读失败统一返回-1,上层按“未知状态”处理 */ } return 0; } int phy_mdio_write(uint16_t phy_addr, uint16_t reg, uint16_t val) { if (HAL_ETH_WritePHYRegister(g_heth, phy_addr, reg, val) != HAL_OK) { return -1; } return 0; }这段封装的参数很直观:phy_addr是PHY在MDIO总线上的地址,reg是寄存器编号,val是读回或要写入的16位数据。HAL库负责生成MDC时序,但前提是ETH外设已经初始化、MDC分频配好。很多人在这儿翻车:MDC频率超过2.5MHz后,读回来的ID一天一个样,这是后面第4章的重点。
2.2 通用驱动内部的三层职责:初始化、状态轮询、协议栈回调
一份能称得上“通用”的SR8201F驱动,内部通常分三层。硬件适配层负责刚才的MDIO读写和延时;芯片功能层负责复位、自动协商、link查询;协议栈接入层负责把芯片状态翻译成LwIP能理解的回调。芯片功能层是“通用”二字最值钱的部位,也是最需要按板子改的部位。
芯片功能层最常见的接口是初始化和链路状态查询:
/* phy_sr8201f.c */ static uint16_t g_phy_addr = 0x01u; /* 默认PHY地址 */ int sr8201f_init(uint16_t phy_addr) { uint16_t val = 0; g_phy_addr = phy_addr; /* 第一步:读ID,确认MDIO时序和地址没问题 */ if (phy_mdio_read(g_phy_addr, PHY_REG_ID1, &val) != 0) { return -1; } if (val == 0x0000u || val == 0xFFFFu) { return -2; /* 读到全0或全1,多半是时序或地址错 */ } /* 第二步:软件复位PHY */ phy_mdio_write(g_phy_addr, PHY_REG_BCR, PHY_BCR_RESET); /* 第三步:等PHY从复位中恢复,轮询ID直到读到有效值 */ for (int i = 0; i < 50; i++) { delay_ms(2); if (phy_mdio_read(g_phy_addr, PHY_REG_ID1, &val) == 0) { if (val != 0x0000u) { break; } } } /* 第四步:使能自动协商,允许100M/10M全双工、半双工都参与 */ val = PHY_BCR_AN_EN | PHY_BCR_SPEED_100 | PHY_BCR_DUPLEX_FULL; phy_mdio_write(g_phy_addr, PHY_REG_BCR, val); return 0; }初始化逻辑分四段,每段都很直白。读ID放在第一位的意义是尽早暴露硬件问题:地址不对、MDIO没接、PHY没复位,都会在读ID这步露出马脚。软件复位之后必须等,PHY不像MCU那么快,寄存器在复位期间读回的全是异常值。自动协商那行写的0x1000和0x2000,实际上是覆盖了整个能力范围,10M半双工也默认参与。
链路查询更简单,但有个细节:
int sr8201f_link_status(void) { uint16_t bsr = 0; if (phy_mdio_read(g_phy_addr, PHY_REG_BSR, &bsr) != 0) { return 0; /* 读失败按link down处理,交给上层重试 */ } return ((bsr & PHY_BSR_LINK) != 0u) ? 1 : 0; }这个函数只读BSR的bit2。很多驱动还会额外读ANER的bit0确认识别到对端,但通用驱动一般不做这么细。注意读失败时返回0是“宁可不通也不误报通”的思路,因为误报link up会让协议栈立刻发ARP,然后全部超时,比不通更难受。
2.3 为什么“通用”代码也要自己改:ID判断与地址扫描
通用驱动最常见的两个误解:一是“PHY地址默认0x01不用改”,二是“ID不匹配这颗片就用不了”。SR8201F这类兼容型PHY,ID1寄存器可能和经典8201F系列完全一致,也可能因为封装批次不同读到不同OUI段。把ID当唯一识别依据,往往把自己卡死。
我建议把ID检查降级为“健康检查”,不做“身份验证”:
int sr8201f_check_id(void) { uint16_t id1 = 0, id2 = 0; if (phy_mdio_read(g_phy_addr, PHY_REG_ID1, &id1) != 0) { return -1; } phy_mdio_read(g_phy_addr, PHY_REG_ID2, &id2); /* ID只用于确认MDIO总线通信正常,不用于拒绝加载驱动 */ if (id1 == 0x0000u || id1 == 0xFFFFu) { return -1; } return 0; }调试阶段如果拿不准板子上的PHY地址,可以做一个临时扫描,把MDIO总线0到31号地址全读一遍:
int sr8201f_scan_addr(void) { uint16_t id1 = 0; for (uint8_t addr = 0; addr < 32; addr++) { if (phy_mdio_read(addr, PHY_REG_ID1, &id1) != 0) { continue; } if (id1 != 0x0000u && id1 != 0xFFFFu) { return addr; /* 这个地址上有PHY应答 */ } } return -1; /* 一个都没有 */ }地址扫描是调试利器,但别直接留在量产固件里。MDIO总线上可能挂多颗PHY,扫到的第一个未必是SR8201F,而且扫描本身会打扰别的PHY。正确定位地址的方式是看原理图上PHYAD引脚的上下拉电阻,扫描只作为确认手段。
3. 在STM32上完成驱动移植:从CubeMX到打通第一个ping包
原理看完了,接下来是移植实操。以最常见的STM32F4/H7系列内置MAC为例,整个流程可以分成三步:硬件确认、CubeMX配置、接入LwIP。每一层都有对应代码可抄。
3.1 硬件前置检查:RMII引脚、50MHz时钟、复位不要漏
先花10分钟对照原理图过一遍表格,别一上来就开IDE。RMII模式下任何一根数据线接错,表现都是“PHY寄存器能读,但网口死活不通”。
| 信号 | 方向(MAC视角) | 检查重点 |
|---|---|---|
| REF_CLK | 输入 | 必须50MHz,来源要与PHY一致 |
| MDC | 输出 | 频率不高于2.5MHz |
| MDIO | 双向 | 确认有上拉电阻 |
| TX_EN/TXD0/TXD1 | 输出 | 对应PHY的RXD侧 |
| RXD0/RXD1/CRS_DV | 输入 | 对应PHY的TXD侧 |
| PHY_RESET | 输出 | 确认MCU或上电复位电路有效 |
REF_CLK是RMII的命脉,它必须由PHY、外部有源晶振或STM32的MCO提供,且两端共享同一个相位基准。最常见方案是PHY外接25MHz无源晶振,芯片内部倍频到50MHz后输出REF_CLK给STM32,这个方案最省事,但晶体负载电容没配好会导致频率偏差,百兆协商不稳定。
PHY复位也要确认:如果复位脚只是RC电路,没有MCU控制,就确保PHY在STM32复位后几百毫秒内完成上电稳定,这个时间窗口内的MDIO读写一律会失败。建议复位脚接到MCU GPIO,这样驱动在初始化前能主动拉低再拉高,拿到确定状态。
3.2 CubeMX里配好ETH外设:RMII模式、PHY地址、DMA描述符
通过CubeMX生成工程时,这几个参数直接影响驱动工作:
| 参数项 | 建议值 | 说明 |
|---|---|---|
| Media and Interface | RMII | 与硬件一致 |
| PHY Address | 按原理图填0x01或实际值 | 必须和驱动里g_phy_addr一致 |
| MAC Address | 48位唯一值 | 不能全0或全FF |
| MDC Clock Range | 分频后低于2.5MHz | 选留裕量的一档 |
| DMA TX/RX描述符 | 4到8个 | 内存够就8个,抗突发 |
| PHY Model | 选Generic或对应项 | 实际驱动靠PHY寄存器操作 |
这里有个容易忽略的坑:CubeMX里的MAC地址默认是随机生成的,如果不改,每块开发板用同一个MAC。两台设备同时接路由器,轻则地址冲突,重则整个局域网广播风暴。把MAC放到底层硬件参数里,而不是写在应用层,驱动初始化时一次性填入。
生成的HAL代码里,与PHY相关的只有这一处:
/* main.c 或 eth.c 中 */ ETH_HandleTypeDef heth; heth.Init.MACAddr[0] = 0x02; heth.Init.MACAddr[1] = 0x00; heth.Init.MACAddr[2] = 0x00; heth.Init.MACAddr[3] = 0x12; heth.Init.MACAddr[4] = 0x34; heth.Init.MACAddr[5] = 0x56; /* 这块值按自己板子唯一分配 */ heth.Init.PhyAddress = 0x01; /* 这里要和驱动里的地址一致 */ heth.Init.MediaInterface = HAL_ETH_RMII_MODE; heth.Init.AdvancedSpeed = HAL_ETH_100M_FULLDUPLEX;PhyAddress这一行是硬件适配层和芯片功能层的接口,两边不一致的表现很典型:驱动反复初始化失败,但用调试器手动读PHY寄存器又能读到值。先对这一个数,能排除一大批“玄学问题”。
3.3 接入LwIP:low_level_init只做初始化,link状态交给轮询
LwIP的ethernetif.c里,low_level_init是PHY驱动接入点。常见做法是在里面调用SR8201F初始化,再启动MAC:
/* ethernetif.c */ static err_t low_level_init(struct netif *netif) { uint8_t *mac = netif->hwaddr; /* 把MAC地址交给netif */ for (int i = 0; i < 6; i++) { mac[i] = heth.Init.MACAddr[i]; } netif->hwaddr_len = 6; netif->mtu = 1500; /* 初始化PHY,地址必须和CubeMX一致 */ if (sr8201f_init(heth.Init.PhyAddress) != 0) { return ERR_IF; /* PHY没起来,协议栈不给启动 */ } /* 启动MAC收发 */ if (HAL_ETH_Start(&heth) != HAL_OK) { return ERR_IF; } return ERR_OK; }一个容易忽略的细节:这里不要一开始就把NETIF_FLAG_LINK_UP置上。LwIP默认认为link是up的,如果PHY协商还没完成,协议栈就开始发ARP,第一个包必丢。更稳的做法是交给一个任务周期查PHY状态,再通知LwIP:
/* 独立线程:每500ms查一次链路,只在变化时通知协议栈 */ static uint32_t g_last_link_check = 0; void ethernet_link_check_task(void *arg) { for (;;) { if (HAL_GetTick() - g_last_link_check >= 500) { g_last_link_check = HAL_GetTick(); if (sr8201f_link_status() == 1) { if (!netif_is_link_up(&g_netif)) { netif_set_link_up(&g_netif); } } else { if (netif_is_link_up(&g_netif)) { netif_set_link_down(&g_netif); } } } osDelay(100); } }500ms这个周期,既能保证拔网线后在1秒内感知断链,又不会因为PHY握手过程中的瞬时抖动来回翻转。有些驱动把周期压到50ms,结果交换机端口协商慢一点,驱动就进入“link up、link down”的死循环。记住一个原则:链路检测是慢逻辑,不做实时判断。
4. 驱动的关键参数这样调:PHY地址、MDIO时序与自动协商
驱动能编译过、能跑起来,不等于网口能通。三类参数最让人头疼:PHY地址、MDC频率、自动协商。这三样正好也是“通用驱动”里最需要按板子改的地方。
4.1 PHY地址与MDC时钟:两个上电后最先检查的参数
PHY地址由芯片的PHYAD引脚上下拉决定,绝大多数RMII接口的PHY默认是0x01,但这不是理所当然的。有的板子为了兼容多颗PHY,会把PHYAD拉成0x00或0x04。驱动里写死0x01,硬件上实际是0x04,结果就是MDIO读写永远失败。
确认地址的可靠方法就是读ID。读ID之前,还要确认MDC频率。IEEE 802.3对MDC的要求是不得超过2.5MHz,但STM32的MDC是由系统时钟分频得到的,很多通用驱动模板里写的分频系数是针对某一颗MCU的,换到跑在180MHz主频的芯片上,分频不够,MDC直接飙到十几MHz,读ID必然是错乱值。
MDC频率计算很简单:
/* 假设MDIO时钟源来自HCLK */ uint32_t mdc_freq = hclk_freq / mdc_divider; /* 校验:mdc_freq 必须小于 2.5MHz,建议留20%裕量 */ if (mdc_freq > 2000000u) { /* 分频系数太小,换大一档 */ }如果驱动包没有给你分频配置入口,直接在CubeMX的ETH参数里调。调试时怎么判断MDC频率不对?读ID1:如果每次读到的值都在跳变,或者固定读到0xFFFF,优先怀疑频率而不是芯片坏。
4.2 自动协商的寄存器位与超时处理:读对方能力再定速率
自动协商不是“开个位就等着”,它需要读对端做能力交集。驱动里至少要有读ANLPAR并解析的函数,否则你永远只知道“link up”,但不知道协商成了10M还是100M:
int sr8201f_resolve_negotiation(uint16_t *speed, uint16_t *duplex) { uint16_t anar = 0, anlpar = 0, bsr = 0; phy_mdio_read(g_phy_addr, PHY_REG_ANAR, &anar); phy_mdio_read(g_phy_addr, PHY_REG_ANLPAR, &anlpar); phy_mdio_read(g_phy_addr, PHY_REG_BSR, &bsr); /* 自动协商还没完成,让上层再等等 */ if ((bsr & PHY_BSR_AN_DONE) == 0u) { return -1; } /* 本端和对端能力取交集:两边都支持哪位,结果就是哪位 */ uint16_t common = anar & anlpar; *duplex = (common & 0x20u) ? 1 : 0; /* bit5: 100M全双工 */ *speed = (common & 0x20u) ? 100 : 10; if ((common & 0x20u) == 0u && (common & 0x40u) != 0u) { *duplex = 0; /* 只支持100M半双工 */ *speed = 100; } return 0; }这段代码的思路是:ANAR是本端能做什么,ANLPAR是对端能做什么,按位与之后就是双方共同能力。读取协商结果有一个超时原则:协商通常需要1到3秒,不能在PHY复位后立刻读,否则AN_DONE位一直为0。通用驱动里建议把协商超时定在3到5秒,超时后再报“协商失败”,让应用层决定是重试还是强制模式。
4.3 当自动协商靠不住:强制速率与REF_CLK的玄学
有一种场景必须要用强制模式:对端设备是老旧工业交换机,关了自动协商,固定100M全双工。此时让SR8201F继续跑AN,对端不回应,链路永远起不来。处理办法是把PHY强制到100M全双工,同时把MAC侧速率也同步过去:
void sr8201f_force_100m_full(void) { uint16_t bcr; /* 关掉自动协商,直接写速率和双工 */ bcr = PHY_BCR_SPEED_100 | PHY_BCR_DUPLEX_FULL; phy_mdio_write(g_phy_addr, PHY_REG_BCR, bcr); /* MAC侧必须匹配:PHY是100M全双工,MAC不能还在10M半双工 */ HAL_ETH_SetSpeed(&heth, ETH_SPEED_100M); HAL_ETH_SetDuplexMode(&heth, ETH_MODE_FULLDUPLEX); }强制模式的坑在MAC和PHY必须同步,很多人只改了PHY寄存器,忘了MAC侧,结果协商寄存器显示100M全双工,实际跑起来吞吐量惨不忍睹。强制模式适合工程调试和特定工业场景,量产产品我一般还是建议开自动协商。
REF_CLK是另一个“玄学”参数。RMII要求50MHz基准时钟,但实际量到的可能是49.9MHz或者占空比不对,此时10M能通、100M必丢包,或者link状态来回翻转。排查方法很土但有效:示波器探头点到REF_CLK引脚,量频率、幅度、占空比。频率偏差超过200ppm就应该查晶体负载电容,占空比离谱就换时钟来源方案。
5. SR8201F驱动移植排查与避坑:MDIO无响应、协商异常、link反复
移植驱动最花时间的不是写代码,是排查“看起来都对了但就是不通”。下面五条坑,每一条都有完整的现象、原因、解决步骤,遇到直接对号入座。
5.1 现象:MDIO读PHY寄存器,读回全是0xFFFF
原因有三类:PHY地址不对、MDC频率太高、PHY根本没有被复位释放。这三类问题的共同表现就是读ID读到全1。地址和频率问题在前面讲过,这里强调复位:如果PHY_RESET脚被RC电路控制,上电后MCU开始执行时PHY可能还在复位状态,要等几百毫秒再访问。解决方法是把复位脚接到GPIO,在初始化时主动拉低50ms再拉高,然后延时等待,再去读ID。
读时序问题的另一个特征:连续读两次,结果不一样。只要出现“读ID结果在跳变”,不当成芯片问题,先降MDC分频。我一般把分频系数往上调两档,即使低于标准不少,也只是慢一点,不会出错。
5.2 现象:PHY能识别,link状态也能读到,但第一个ping包必丢
根源几乎都是“协议栈启动得比链路快”。LwIP默认认为link up,你的驱动如果low_level_init里一上来就把NETIF_FLAG_LINK_UP置上,PHY还在协商,第一个ping包已经在空中飘了。解决方法是把link up的置位放到协商完成之后,也就是前面写的link_check_task模式。还有一个隐藏原因:HAL_ETH_Start调用前DMA描述符没有清零。HAL库在反复Start/Stop后,TX描述符的 ownership位可能残留,表现就是“重启后才出现第一包必丢”,重按复位键又好了。解决方法是初始化DMA描述符后做一次完整复位动作。
5.3 现象:百兆协商成十兆,或者协商结果随机
REF_CLK频率不达标是首要嫌疑。用示波器量50MHz是否精确,50MHz是从25MHz倍频出来的还是独立晶振,倍频方案的晶体负载电容偏大一点,频率偏低,PHY就会把100M capability在AN阶段悄悄丢掉。另一个原因是ANAR寄存器被应用层写过,把100M的advertise bit清零了。解决方法是读一次ANAR,确认bit7和bit5都在,不在就重新写回。还有一种情况是RJ45的差分对有一根断线,RMII下表现为10M能link、100M协商失败,因为100M对线对质量更敏感。
5.4 现象:长时间运行后link up/down频繁翻转
这种“运行两小时后开始抽风”的问题最折磨人。先确认不是MDIO读错:PHY状态寄存器在临界状态下可能偶发读错一位,驱动把这一位当成link down上报给LwIP,LwIP立刻拔出网线,等下一秒又插上。解决方法是link状态连续读两次一致才切换,相当于给驱动加一个去抖:
int sr8201f_link_status_debounce(void) { uint16_t bsr1 = 0, bsr2 = 0; uint8_t cnt = 0; while (cnt < 2) { phy_mdio_read(g_phy_addr, PHY_REG_BSR, &bsr1); delay_ms(10); phy_mdio_read(g_phy_addr, PHY_REG_BSR, &bsr2); if (((bsr1 & PHY_BSR_LINK) == 0u) != ((bsr2 & PHY_BSR_LINK) == 0u)) { return -1; /* 两次不一致,认为是抖动 */ } cnt++; } return ((bsr2 & PHY_BSR_LINK) != 0u) ? 1 : 0; }如果去抖后还是翻转,就要怀疑硬件:PHY供电纹波大、REF_CLK被干扰、RMII线走线太长离电感太近,都会造成这种随机性。这已经不是驱动能修的范围,但寄存器快照能帮你定位,继续往下看第6章。
5.5 现象:多块板子同时接入,有的能ping通有的不能
这不是驱动代码的bug,是MAC地址重复。开发板出厂没有烧录唯一MAC,要么全0,要么全FF,要么CubeMX随机生成的地址一模一样。交换机看到两个端口同一个MAC,MAC地址表反复横跳,结果就是时通时不通。解决方法是量产烧录时给每块板子分配唯一MAC,可以基于STM32的UID计算低3字节,高3字节用厂商标识:
void generate_mac_from_uid(uint8_t mac[6]) { uint32_t uid0 = HAL_GetUIDw0(); uint32_t uid1 = HAL_GetUIDw1(); mac[0] = 0x02; /* 本地管理地址,bit1为1表示非厂商唯一 */ mac[1] = 0x00; mac[2] = 0x00; mac[3] = (uid0 >> 24) & 0xFF; mac[4] = (uid0 >> 16) & 0xFF; mac[5] = (uid1 >> 8) & 0xFF; }02开头的MAC是本地管理地址,不会和真实网卡冲突,适合没有申请OUI的物联网设备。这段代码放在驱动初始化最前面,覆盖掉CubeMX里的默认MAC,一劳永逸。
6. 让这套驱动长期稳定:三个值得养成的调试习惯
驱动跑通只是开始,产品要在现场稳定运行几个月才算数。基于SR8201F这套通用驱动的调试经历,我养成了三个习惯,每个都帮我在关键时刻少熬一个通宵。
6.1 把寄存器快照做成驱动标准动作
每次link状态发生变化时,把BCR、BSR、ID1、ID2、ANAR、ANLPAR读出来存到结构体里,同时记录系统时间。现场出问题时,一个日志就能分辨是PHY没协商、协商成半双工、还是MDIO总线闪断。
6.2 用事件回调替代高频轮询
通用驱动默认是轮询,但轮询周期和LwIP的tcpip_thread交互太频繁会影响吞吐。我的做法是只在link状态变化时通知协议栈,正常情况下查询周期放到1秒,省出来的CPU时间让协议栈更稳。
6.3 每次改驱动前先记录吞吐基线
| 测试项 | 条件 | 参考记录 |
|---|---|---|
| UDP 1472B发送 | 连续打流100秒 | 速率、丢包率 |
| TCP大包下载 | 内存池不调整 | 吞吐曲线 |
| 长时间ping丢包率 | 24小时,间隔100ms | 丢包个数 |
我见过不少被REF_CLK折腾到换板的案例,现在每换一次时钟源配置,都会先跑24小时ping和打流。这套习惯听起来慢,其实比现场排查快得多。希望帮到你。
本文还有配套的精品资源,点击获取