ARTICLE DETAIL

资讯详情

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

W5500 DHCP实战:从最小状态机到Zigbee网关稳定续租

W5500 DHCP实战:从最小状态机到Zigbee网关稳定续租 简介W5500_DHCP.rar是一份面向嵌入式与物联网开发者的W5500网络接口在Zigbee设备上实现DHCP自动获取IP的工程资料包。围绕W5500全硬件TCP/IP协议栈芯片、CC2530等MCU的SPI通信以及DHCP客户端机制适用于为Zigbee节点接入以太网并动态分配地址的智能家居、工业控制场景。压缩包共349个文件约3.93MB以C源码、头文件、汇编文件、列表文件及工程配置为主43个.c和46个.h实现W5500驱动、DHCP客户端与STM32/CC2530外设初始化32个.s及大量lst、r51、s51为编译中间文件便于核对底层生成流程。已有269人学习下载。资源提供了完整DHCP移植示例可帮助理解ARP与UDP交互、IP地址保存机制参考工程配置后可在IAR或Keil中快速复现缩短网络模块调试周期对需快速接入以太网的Zigbee项目具备直接参考价值。1. 先想清楚W5500 的 DHCP 是 MCU 的事不是芯片的事W5500 把 TCP/IP 协议栈固化进硬件但 DHCP 属于应用层不在 W5500 硅片里。拿到 W5500_DHCP 这类工程包第一步不是找“DHCP 开关”而是确认 MCU 上有一个 DHCP 客户端状态机在跑。WIZnet 的 ioLibrary 提供了现成实现通过 W5500 的 UDP socket 收发 BOOTP 报文完成 Discover、Offer、Request、Ack 四轮交互再负责租约续期。对做 Zigbee 网关的人来说这直接解决规模化入网问题——设备一多、IP 规划不固定自动分配比逐台登记静态 IP 可靠得多。下面按启动流程、网关集成、稳定续租、量产降级四条线讲透对正在移植 W5500 驱动或想把 DHCP 跑稳的人都有用。2. W5500 DHCP 启动流程先跑通最小 DHCP 状态机2.1 最小可运行代码DHCP_init 与 DHCP_run 必须配对使用在 W5500 上启用 DHCP不是写一次寄存器就完事而是要让 MCU 不断调用DHCP_run()让状态机在每个超时周期里推进。网上流传的 W5500_DHCP.rar 工程包基本就是 WIZnet ioLibrary 里的 dhcp.c、dhcp.h 连同 socket 库打在一起移植时先核对库版本再抄比从别人工程里随机摘代码稳得多。官方库把 DHCP 客户端实现成一组状态初始化后进入 SELECTING发送 Discover 广播收到 Offer 后转 REQUESTING收到 Ack 才进入 BOUND。BOUND 之后库内部开始租约计时租约过半发起续租这些都不需要应用层干预。/* w5500_dhcp_min.c —— 最小可运行的 DHCP 客户端启动序列 */ #include wizchip_conf.h #include dhcp.h static uint8_t mac[6] {0x00, 0x08, 0xDC, 0x00, 0x11, 0x22}; static uint8_t ip[4] {0, 0, 0, 0}; static uint8_t gw[4] {0, 0, 0, 0}; static uint8_t sn[4] {0, 0, 0, 0}; static uint8_t dhcp_buf[512]; /* 库内部收发 DHCP 报文用的缓冲 */ void dhcp_start(void) { uint8_t ret; setSHAR(mac); /* 先写 MACDHCP 报文 chaddr 从这里取 */ DHCP_init(0, dhcp_buf, ip, gw, sn); /* 把 DHCP 客户端挂到 socket 0 */ do { ret DHCP_run(); /* 每次调用推进一个状态 */ } while (ret DHCP_RUNNING); /* 0xFF 表示流程仍在进行 */ if (ret DHCP_IP_ASSIGNED) { /* 1 表示拿到地址 */ getIPfromDHCP(ip); setSIPR(ip); /* 主动回写兼容不同版本的库 */ } }dhcp_buf是收发 BOOTP 报文的内存缓冲不同版本 ioLibrary 可能改成内部自动分配但显式传入 512 字节全局数组是最省心的做法。DHCP_init的第一个参数是 socket 号官方例程默认 0如果业务代码已经用socket()把 0 号 socket 占住DHCP 会初始化失败这是移植时第一件要确认的事。提示DHCP_RUNNING、DHCP_IP_ASSIGNED这类宏在不同版本里值可能不同常见定义是 0xFF、1。移植时以手头头文件为准不要跨工程影印代码。轮询周期同样是变量。DHCP_run()内部有超时判断如果应用层每隔几百毫秒才调用一次重传窗口会被拉长到不可用如果放在中断里调用又容易与其他 SPI 操作冲突。常见做法是把整个 DHCP 启动序列放进一个低优先级任务每 100ms 或更短调用一次直到返回分配成功或失败。若发现DHCP_run()一直停在 0xFF通常是 SPI 读不到网卡状态或缓冲区被复写而非服务器问题。2.2 与 DHCP 绑定最紧的 4 组寄存器GAR、SUBR、SIPR、SHAR排查 W5500 DHCP 问题时经常有人对着芯片手册找“DHCP 寄存器”其实 W5500 本身没有单独的 DHCP 控制寄存器。DHCP 客户端跑在 MCU 上它操作的是 W5500 的通用网络寄存器。调试时盯住四组就够。寄存器W5500 内偏移谁在写DHCP 成功后的含义SHAR0x0009-0x000EMCU 上电时必须写Discover 报文的 chaddr服务器据此分配地址GAR0x000F-0x0012DHCP 客户端成功时写默认网关地址SUBR0x0013-0x0016DHCP 客户端成功时写子网掩码SIPR0x0017-0x001AMCU 主动读并回写本机 IPv4 地址“谁在写”这一列不会百分之百可靠。不同版本的 ioLibrary 在 DHCP 成功后写 GAR、SUBR 的时机有差异有的要等下一次DHCP_run()才真正生效。稳妥做法是进入 BOUND 后把库读出来的 IP、网关、掩码显式写回对应寄存器。直接读寄存器打印结果也比相信日志里某一次“疑似成功”更接近真相。/* dump_regs.c —— DHCP 完成后核对寄存器值 */ void dump_dhcp_result(void) { uint8_t ip[4], gw[4], mask[4]; int i; for (i 0; i 4; i) { ip[i] WIZCHIP_READ(0x0017 i); gw[i] WIZCHIP_READ(0x000F i); mask[i] WIZCHIP_READ(0x0013 i); } printf(IP%d.%d.%d.%d GW%d.%d.%d.%d MASK%d.%d.%d.%d\n, ip[0], ip[1], ip[2], ip[3], gw[0], gw[1], gw[2], gw[3], mask[0], mask[1], mask[2], mask[3]); }WIZCHIP_READ是 ioLibrary 统一封装的寄存器读取接口传入绝对偏移量即可。注意这四个偏移是 Common Register 的绝对地址不要再乘总线宽度或加外部偏移否则读出来的字节序是乱的。打印结果里如果 IP 正常但 GW 全是 0优先怀疑库版本没把网关写入寄存器而不是 DHCP 服务器没给它网关。2.3 租约续期是第二轮状态机50% 续租失败再重走 Discover拿到 IP 只是第一步量产出问题的往往是七天后租约到期。DHCP 约定 T1 约等于租约的二分之一T2 约等于八分之七。W5500 端最常见的实现是租约过半时用单播 REQUEST 向原服务器续租失败则再尝试最终失败才退回 SELECTING 重新广播 Discover。socket 0 的归属问题在这里浮出水面。业务代码如果提前开了 TCP 连接续租时发现 socket 被占用只能强制关闭用户连接这会引发应用层断连。建议在协议栈设计上把 DHCP 固定使用的 socket 与应用套接字隔离要么把 DHCP 放在 0 号 socket 且永不用于业务要么在业务使用前等 DHCP 完成并调用DHCP_stop释放。对 Zigbee 网关来说Zigbee 数据链路往往不需要以太网 TCP 常连接用 UDP 上报数据对 DHCP 续租的干扰最小。3. Zigbee 网关集成W5500 与 zigbee 协议栈共享一套 MCU 资源3.1 两条数据通路决定启动时序一块典型的 W5500 Zigbee 网关Zigbee 协调器负责组网收包MCU 解析后把数据通过 W5500 送进以太网下行控制指令则反过来走。DHCP 发生在以太网链路启动初期这个阶段 Zigbee 还没开始大数据转发是最干净的窗口。我一般这样安排时序MCU 上电先初始化 SPI、复位 W5500、启动 DHCP等 DHCP 返回 BOUND 后再允许 Zigbee 协调器开始长时间数据转发。Zigbee 模块如果提前入网数据量一大主控会被 Zigbee 中断拖住DHCP 的 Discover 重传窗口很容易错过。拿到一块现成的网关板不管协调器用 CC2530 还是新大陆这类 Zigbee 模块先看原理图上三颗关键芯片如何连线W5500 的电源滤波、Zigbee 模块的复位脚、两者对 MCU 的中断脚。排查 DHCP 不稳定时用示波器同时看 W5500 的 nSS 和 Zigbee 的 RX/TX 是否互相串扰比反复改代码更有效。3.2 总线方案选型共享 SPI 还是独立 SPI 外设如果板上 W5500 和 Zigbee 模块挂在同一条 SPI 总线上片选和互斥是绕不开的问题。W5500 的 SPI 事务由地址段、控制段、数据段组成整个事务必须连续完成Zigbee 模块的访问穿插进来会直接读碎寄存器。两种接法的取舍比较清楚总线方案优点代价W5500 与 Zigbee 共用一条 SPI省引脚PCB 走线简单同一时刻只能一方传输需要互斥SPI 频率被较慢设备限制W5500 独立使用一组 SPIDHCP 续租与 Zigbee 收发互不阻塞多占一组 SPI 外设和片选引脚共用 SPI 时要保证锁的粒度是“一次完整的寄存器操作”或“一次完整的缓冲区传输”不能每个字节单独锁。/* spi_mux_demo.c —— SPI 总线互斥访问示意 */ static SemaphoreHandle_t spi_mux; int spi_xfer_w5500(const uint8_t *tx, uint8_t *rx, uint16_t len) { int rc; xSemaphoreTake(spi_mux, portMAX_DELAY); HAL_W5500_CS_LOW(); /* 只拉 W5500 的片选 */ rc HAL_SPI_TransmitReceive(hspi, tx, rx, len, 1000); HAL_W5500_CS_HIGH(); xSemaphoreGive(spi_mux); return rc; }xSemaphoreTake在传输开始前取锁xSemaphoreGive在片选拉高后释放。这样 Zigbee 任务即使想插进来也必须在整个 W5500 事务结束后才能碰 SPI。常见错误是在 HAL 库的每个字节中断里去抢锁结果锁本身变成调度热点反而拖慢 SPI 传输。3.3 用 FreeRTOS 把 DHCP 轮询变成独立任务Zigbee 协议栈通常自带任务与回调。如果 DHCP 启动写在主循环的while里整个系统会卡死在等 IP 的状态Zigbee 协议栈跟着停摆。常见做法是把 DHCP 拆成低优先级任务像这样void dhcp_task(void *arg) { uint8_t ret; for (;;) { if (g_dhcp_enable) { ret DHCP_run(); if (ret DHCP_IP_ASSIGNED) { g_dhcp_enable 0; /* 通知其他任务网络可用 */ } else if (ret DHCP_FAILED) { g_dhcp_enable 0; } } vTaskDelay(pdMS_TO_TICKS(100)); } }vTaskDelay(pdMS_TO_TICKS(100))给 DHCP 库一个稳定的调度节拍同时也给 Zigbee 任务留出运行时间。把周期压到 10ms 以下不会让 DHCP 更快拉到 500ms续租超时判断会明显变慢。100ms 是折中值量产环境 CPU 紧张时可以改成 250ms。任务优先级上DHCP 轮询普通优先级即可但 SPI 互斥信号量要在 Zigbee 数据任务和 DHCP 任务之间共享。现场调试时如果发现 Zigbee 设备一多 DHCP 就失败先看是不是 SPI 互斥锁等待超时导致 DHCP 轮询被饿死而不是怀疑 DHCP 服务器。提示用 Zigbee 仿真器调试时驱动装不上、上位机连不上协调器表象像是“整板上不了网”实际只是 Zigbee 通道没起来。先拔掉仿真器只保留 W5500 的 DHCP 启动流程确认以太网链路独立再回头处理 Zigbee 侧能省下大量交叉排查时间。4. 把 W5500 DHCP 从“能分配 IP”调到“稳定续租”4.1 用 tcpdump 确认四步握手是否真的到了交换机拿到“有 DHCP 请求但服务器没反应”的报障第一步永远不是改代码而是抓包。如果接入交换机有镜像口在镜像端口跑 tcpdump 是最直接的验证方式。# 在镜像口上抓 BOOTP 流量同时看 67/68 两个端口 tcpdump -i enp3s0 -vv port 67 or port 68正常的抓包结果应该出现四组连续报文Discover、Offer、Request、Ack且它们的xid相同。如果只有 Discover 没有 Offer问题在 DHCP 服务器或交换机如果四个包都有但 W5500 日志显示没拿到 IP问题多半在 W5500 的中断处理或 SPI 读包路径。抓包同时把 W5500 的 PHY 链路状态打印出来能进一步缩小范围。现象优先检查项快速判定方法只发 Discover收不到 Offer交换机 dhcp snooping、地址池配置换普通交换机直连能通就是网络侧问题四步包都出现板子仍不进入 BOUNDsocket 0 被占用、SPI 读包异常打印 socket 状态寄存器与中断标志上电能拿 IP续租阶段掉线MAC 冲突、租约被服务器回收同时抓两台设备 ARP 与 DHCP 日志跨网段分配不正常DHCP 代理/中继的 giaddr 处理对比 Offer 的 yiaddr 与 relay 所在网段抓包时最好在网关重启瞬间开始记录因为 DHCP Discover 只在启动窗口发出错过一次要等重传现场排查效率会低很多。4.2 网络侧先查 dhcp snooping 和地址池选择现场如果插在网管交换机上dhcp snooping是最常见的隐形杀手。交换机信任口只放行来自上行口的 Offer/Ack接到非信任口的 W5500 发出的 Discover 能出去Offer 却在交换机侧被丢弃。华三、华为设备上确认地址池用哪个策略时注意dhcp select global和dhcp select interface的差别全局池没有匹配到网段时客户端只能拿到 0.0.0.0。锐捷、中兴这类交换机的命令字不同但排查顺序一致先看池子是否存在、再看是否命中全局池、最后看信任口方向。运营商送的光猫路由器自带 DHCP 且没有完全关闭入口W5500 客户端和它共存没有冲突真正的问题通常是地址池太小设备一多租约全被占用。这种场景下 W5500 端不用改代码把光猫的地址池范围扩大即可。4.3 本地侧三个动作清 socket、核 MAC、匹配代理如果抓包四步握手完全正常W5500 仍然续租失败回本地检查下面三件事。第一确认 DHCP 使用的 socket 没有被业务代码占用。很多移植工程把socket()放在DHCP_init之前0 号 socket 连续申请两次DHCP 静默失败现象就是“第一次上电偶尔能拿到 IP重启后拿不到”。第二确认 MAC 地址唯一。量产板把固定 MAC 烧死在固件里两台设备同时抢同一个地址DHCP 服务器不会报错但网络一会儿通一会儿断。把唯一序列号写进外部 Flash上电回填到 SHAR是量产最可靠的做法。第三如果局域网里存在 DHCP 代理或中继抓包时注意观察giaddr字段。中继会把报文的 giaddr 改成自己的接口地址W5500 端客户端的标准行为是照常处理 Offer。如果代理实现有缺陷会出现 Offer 的yiaddr和giaddr处于不同网段客户端的“拿到 IP”判断可能被误导。PC 上常见的“续订接口以太网时出错无法联系 DHCP 服务器请求超时”报错排查顺序也可以直接搬到 W5500 上先换直连、再查交换机、最后查本机 MAC。5. 量产可用的一个小技巧租约入 Flash三次失败回退静态5.1 把上一次租约写进外部 FlashDHCP 的痛点在于重启后服务器暂时不可达时设备会长时间处于拿不到 IP 的状态。与其干等不如把上一个租约保存下来上电先用旧配置把网络撑起来再在后台补一次 DHCP 交互。typedef struct { uint32_t magic; /* 用 0x5745_3031 这类固定值标识结构有效 */ uint8_t ip[4]; uint8_t gw[4]; uint8_t mask[4]; uint32_t lease; /* 租约秒数续租时用于判断是否过期 */ } lease_record_t; /* DHCP 成功后立即写入覆写前先擦除对应扇区 */ void lease_save(lease_record_t *rec) { flash_erase(LEASE_SECTOR, 0); flash_write(LEASE_ADDR, (uint8_t *)rec, sizeof(lease_record_t)); }写入时机放在DHCP_IP_ASSIGNED分支里不要在每次续租成功后反复擦写 Flash。闪存擦写寿命有限租约过期前的续租成功不需要任何持久化只有“重新拿到一个新 IP”才值得写入。flash_erase、flash_write是具体平台的 Flash 接口需要按芯片手册实现。5.2 连续三次 DHCP 失败回退静态地址回退静态地址的判定不能太激进第一次失败就切静态后续 DHCP 恢复时不会自动切回来太保守则现场断网时间太长。我一般用连续失败计数三次超时才切换。static uint8_t dhcp_fail_cnt; void on_dhcp_result(uint8_t ret) { if (ret DHCP_IP_ASSIGNED) { dhcp_fail_cnt 0; lease_record_t lease; lease.magic LEASE_MAGIC; getIPfromDHCP(lease.ip); /* gw/mask 用同样方式读出后填入 lease */ lease_save(lease); return; } if (dhcp_fail_cnt 3) { setSIPR(static_ip); /* 静态 IP 必须来自保留地址段 */ setGAR(static_gw); setSUBR(static_mask); g_dhcp_enable 0; /* 停止轮询避免反复抢占 SPI */ } }static_ip、static_gw、static_mask是工程里预留的静态地址数组。这里有个易错点静态 IP 不能从 DHCP 动态池里随手挑一个否则 DHCP 服务器可能把它再分给别的设备形成冲突。应该让设备管理员给这类网关预留独立保留地址或者在 DHCP 服务器上按 MAC 做静态绑定然后把绑定的结果写进租约记录两边一致才不会踩坑。租约入 Flash 时把 DHCP 返回的 DNSoption 6也一并存下来。W5500 跑域名解析时优先使用这个 DNS不要写死运营商或公网 DNS否则现场 DNS 变更后网关全部跟着失效。这个细节和租约保存是一套逻辑凡是 DHCP 学到的动态配置都应当在断电后复用而不是靠人工到现场逐台改写。本文还有配套的精品资源点击获取
返回列表