EMS 知识库
首页 / knowledge/代码解析/midware/ethernt/lan

dhcp.c

源文件路径:midware/ethernt/lan/dhcp.c 所属模块:midware/ethernt/lan(W5500 以太网协议栈应用层 — DHCP/DNS/Socket) 代码行数:1017

1. 文件职责

本文件是 WIZnet 官方 DHCP 客户端协议库(W5500 版)的实现,在 EMS 固件中承担"上电时为网口申请 IP 地址"的职责:完成 DISCOVER / OFFER / REQUEST / ACK / NACK / DECLINE 六种报文的组帧、收发、解析与超时重发,并把协商结果落到 W5500 的 SIPR(本机 IP)/ SUBR(掩码)/ GAR(网关)寄存器。

它处在网络接入层的最上游:只有 DHCP 得到地址后,task_ethernet 才会进入 DNS 解析与 TCP 客户端建链环节。文件内部是标准的六态状态机(INIT/DISCOVER/REQUEST/LEASED/REREQUEST/RELEASE/STOP),由 DHCP_run() 单函数驱动,被 task_ethernet 以 1 秒周期调用(ethernet.c:1022 → dhcp_function() → dhcp_pro_fun() → DHCP_run())。

计时由外部秒计数 dhcp_tick_1s 提供,DHCP_time_handler() 由更高优先级的 task_system_aux 每秒调用一次(ethernet.c:1086)。地址分配/变更/冲突三件事通过函数指针回调(默认实现写 W5500 寄存器,工程里被 my_ip_assign() 替换)通知上层。

代码基本保持 WIZnet 原版,工程仅做了两处环境适配:主机名后拼 MAC 低 3 字节保证多机区分,以及 check_DHCP_leasedIP() 中依赖外部秒计数做忙等。

2. 关键数据结构与宏

名称类型说明
STATE_DHCP_INIT / _DISCOVER / _REQUEST / _LEASED / _REREQUEST / _RELEASE / _STOP宏状态机取值 0~6(第 62-68 行);_RELEASE(5) 注释为 No use,实际未使用
DHCP_FLAGSBROADCAST / DHCP_FLAGSUNICAST宏报文 flags 字段广播位 0x8000 / 单播 0x0000(第 70-71 行)
DHCP_BOOTREQUEST / DHCP_BOOTREPLY宏BOOTP 的 op 字段:1=请求,2=应答(第 74-75 行)
DHCP_DISCOVER … DHCP_INFORM宏option 53 报文类型:1 DISCOVER、2 OFFER、3 REQUEST、4 DECLINE、5 ACK、6 NAK、7 RELEASE(未用)、8 INFORM(未用)(第 78-85 行)
DHCP_HTYPE10MB / DHCP_HTYPE100MB宏硬件类型 1/2(第 87-88 行),本文件固定填 1
DHCP_HLENETHERNET / DHCP_HOPS / DHCP_SECS宏硬件地址长度 6 / 跳数 0 / 秒数 0(第 90-92 行)
INFINITE_LEASETIME宏无限租期 0xffffffff,也是 dhcp_lease_time 的初值(第 94 行)
OPT_SIZE / RIP_MSG_SIZE宏option 区 312 字节 / 总报文 236+312 = 548 字节(第 96-97 行)
option 枚举枚举匿名枚举(第 102-167 行),列出 RFC1533 全部 option 号;本文件实际用到 padOption(0)、subnetMask(1)、routersOnSubnet(3)、dns(6)、hostName(12)、dhcpRequestedIPaddr(50)、dhcpIPaddrLeaseTime(51)、dhcpMessageType(53)、dhcpServerIdentifier(54)、dhcpParamRequest(55)、dhcpT1value(58)、dhcpT2value(59)、dhcpClientIdentifier(61)、endOption(255)
RIP_MSG结构体DHCP/BOOTP 报文(第 172-188 行):op,htype,hlen,hops(4B) + xid(4B) + secs,flags(4B) + ciaddr,yiaddr,siaddr,giaddr(16B) + chaddr[16] + sname[64] + file[128] + OPT[312],共 548 字节,天然对齐无填充

全局变量(第 190-215 行):

名称类型说明
DHCP_SOCKETuint8_tDHCP 使用的 W5500 socket 号,本工程为 SOCK_DHCP=1
DHCP_SIP[4]uint8_t[]DHCP 服务器 IP,dhcpServerIdentifier 解析结果
OLD_allocated_ip[4]uint8_t[]续租前的旧 IP,用于判断续租后地址是否变化
DHCP_allocated_ip/gw/sn/dns[4]uint8_t[]服务器下发的 IP / 网关 / 掩码 / DNS
dhcp_stateint8_t当前状态机状态,初值 STATE_DHCP_INIT
dhcp_retry_countint8_t当前状态已超时重发次数
dhcp_lease_timeuint32_t租期(秒),初值 INFINITE_LEASETIME
dhcp_tick_1svolatile uint32_t秒计数,由 DHCP_time_handler() 递增;volatile 是 v1.1.1 的修正
dhcp_tick_nextuint32_t下一次超时阈值,初值 DHCP_WAIT_TIME(10)
DHCP_XIDuint32_t事务 ID,初值 0x12345678,每次续租 ++
pDHCPMSGRIP_MSG*指向外部传入的收发共用缓冲区
HOST_NAME[]uint8_t[]由宏 DCHP_HOST_NAME 初始化,即 "WIZnet\0"
DHCP_CHADDR[6]uint8_t[]客户端 MAC,DHCP_init() 时从 W5500 SHAR 读出
dhcp_ip_assign / dhcp_ip_update / dhcp_ip_conflict函数指针三个事件回调,默认指向 default_ip_*

3. 函数清单

函数名作用域说明(一句话)
default_ip_assign全局(第 252 行)默认"首次分到 IP"回调:把 IP/掩码/网关写进 W5500 的 SIPR/SUBR/GAR
default_ip_update全局(第 260 行)默认"IP 变更"回调:先软复位 W5500,再重新下发地址与 MAC
default_ip_conflict全局(第 270 行)默认"IP 冲突"回调:软复位 W5500 并重写 MAC
reg_dhcp_cbfunc全局(第 279 行)注册三个回调;非空参数才覆盖默认实现
makeDHCPMSG全局(第 290 行)填充所有请求报文共用的 BOOTP 头 + MAGIC_COOKIE,清空 chaddr 尾部/sname/file
send_DHCP_DISCOVER全局(第 350 行)组 DISCOVER(含 client-id、hostname、param-request 列表)并广播到 255.255.255.255:67
send_DHCP_REQUEST全局(第 413 行)组 REQUEST;续租态用单播 + ciaddr,初始态用广播 + option 50/54
send_DHCP_DECLINE全局(第 509 行)组 DECLINE(声明分配的 IP 不可用)并广播
parseDHCPMSG全局(第 569 行)收包并遍历 option 区,提取消息类型/掩码/网关/DNS/租期/服务器 IP,返回消息类型
DHCP_run全局(第 691 行)DHCP 状态机主函数,每次调用收一次包并按状态推进,返回 DHCP_* 事件码
DHCP_stop全局(第 826 行)关闭 DHCP socket 并把状态置为 STATE_DHCP_STOP
check_DHCP_timeout全局(第 832 行)超时判定:未超重试上限则重发,超了则降级/回退状态并返回 DHCP_FAILED
check_DHCP_leasedIP全局(第 900 行)用 UDP 触发 ARP 的方式检测分配到的 IP 是否与局域网内其他设备冲突
DHCP_init全局(第 938 行)初始化:读 MAC(全 0 时填默认 MAC)、绑定 socket 与缓冲区、清空本机 IP/网关、复位计时与状态
reset_DHCP_timeout全局(第 969 行)秒计数清零、阈值复位为 DHCP_WAIT_TIME、重试计数清零
DHCP_time_handler全局(第 976 行)秒计数 dhcp_tick_1s++
getIPfromDHCP全局(第 981 行)拷贝 DHCP_allocated_ip 到调用者缓冲区
getGWfromDHCP全局(第 989 行)拷贝 DHCP_allocated_gw 到调用者缓冲区
getSNfromDHCP全局(第 997 行)拷贝 DHCP_allocated_sn 到调用者缓冲区
getDNSfromDHCP全局(第 1005 行)拷贝 DHCP_allocated_dns 到调用者缓冲区
getDHCPLeasetime全局(第 1013 行)返回 dhcp_lease_time

第 218-249 行是本文件内部函数的前置声明区(default_ip_*、send_DHCP_*、check_DHCP_leasedIP、check_DHCP_timeout、reset_DHCP_timeout、parseDHCPCMSG)。文件内没有 static 函数,也没有 static 全局变量。

4. 核心逻辑解析

4.1 状态机与迁移条件(DHCP_run(),第 691-824 行)

每调用一次,函数做三件事:① 若状态为 STATE_DHCP_STOP 直接返回 DHCP_STOPPED;② 若 socket 不是 SOCK_UDP 则重新 socket(DHCP_SOCKET, Sn_MR_UDP, DHCP_CLIENT_PORT(68), 0);③ 先无条件调用 parseDHCPMSG() 收一包得到 type,再按 dhcp_state 分派。

当前状态输入事件动作次态
STATE_DHCP_INIT(0)—清零 DHCP_allocated_ip,send_DHCP_DISCOVER()STATE_DHCP_DISCOVER
STATE_DHCP_DISCOVER(1)type == DHCP_OFFER(2)从 pDHCPMSG->yiaddr 取分配 IP,send_DHCP_REQUEST()STATE_DHCP_REQUEST
其它/超时check_DHCP_timeout()(重发或判失败)不变或回 INIT
STATE_DHCP_REQUEST(2)type == DHCP_ACK(5)check_DHCP_leasedIP() 通过 → dhcp_ip_assign() + 复位计时STATE_DHCP_LEASED,返回 DHCP_RUNNING
type == DHCP_ACK 但 ARP 冲突复位计时、dhcp_ip_conflict()STATE_DHCP_INIT
type == DHCP_NAK(6)复位计时STATE_DHCP_DISCOVER
STATE_DHCP_LEASED(3)每次进入即 ret = DHCP_IP_LEASED当 dhcp_lease_time != 0xFFFFFFFF 且 dhcp_lease_time/2 < dhcp_tick_1s(即租期过半):保存 OLD_allocated_ip、DHCP_XID++、send_DHCP_REQUEST()、复位计时STATE_DHCP_REREQUEST
STATE_DHCP_REREQUEST(4)type == DHCP_ACKdhcp_retry_count=0;若新旧 IP 不同 → ret = DHCP_IP_CHANGED 并 dhcp_ip_update();复位计时STATE_DHCP_LEASED
type == DHCP_NAK复位计时STATE_DHCP_DISCOVER
STATE_DHCP_STOP(6)—立即返回 DHCP_STOPPED不变(需重新 DHCP_init())

注意 STATE_DHCP_RELEASE(5) 从未被赋值到 dhcp_state,switch 中也没有对应分支,落到 default: 空操作。

4.2 超时与重试(check_DHCP_timeout(),第 832-898 行)

判据为 dhcp_tick_next < dhcp_tick_1s(严格小于)。未超时返回 DHCP_RUNNING;超时后分两档:

所以一次失败周期约为 2×10 s,DHCP_FAILED 只有"DISCOVER 阶段两次重发都拿不到 OFFER"才产生。

4.3 IP 冲突检测(check_DHCP_leasedIP(),第 900-936 行)

利用 W5500 的 ARP 硬件行为做冲突探测,技巧是"发一个 UDP 包到刚分配到的 IP:5000":

  1. 保存并临时把 W5500 的 RCR(重试次数寄存器,控制 ARP 超时)改为 0x03;
  2. sendto(DHCP_SOCKET, "CHECK_IP_CONFLICT", 17, DHCP_allocated_ip, 5000),此时协议栈会先发 ARP 请求;
  3. 恢复 RCR;
  4. 若返回 SOCKERR_TIMEOUT → 认为"ARP 无人应答,IP 唯一",返回 1(成功);否则 → 发 send_DHCP_DECLINE() 声明该地址不可用,并做最长 2 秒的忙等 while((dhcp_tick_1s - ret) < 2); 等待 DECLINE 发出,返回 0。

第 4 步的忙等依赖 dhcp_tick_1s 被别的任务递增。在本工程中成立:DHCP_run() 运行在 task_ethernet(app_cfg.h:68,优先级 17),而 DHCP_time_handler() 由 task_system_aux(app_cfg.h:59,优先级 8,数值更小=优先级更高)每秒调用,因此忙等可以被抢占推进而不会死锁——但代价是 task_ethernet 最多被占满 2 秒。

4.4 报文帧格式与组帧(第 290-566 行)

所有请求报文都基于 makeDHCPMSG() 生成的 BOOTP 头,再用同一个 k 下标填充 option 区(k = 4 起,因为前 4 字节是魔数):

偏移  内容                                          本文件的取值
0     op                                            1 (DHCP_BOOTREQUEST)
1     htype                                         1 (DHCP_HTYPE10MB)
2     hlen                                          6
3     hops                                          0
4-7   xid                                           DHCP_XID,大端手工拆字节
8-9   secs                                          0
10-11 flags                                         0x8000 广播 / 0x0000 单播(大端手工拆字节)
12-15 ciaddr                                        0;续租时填 DHCP_allocated_ip
16-19 yiaddr                                        0(收到 OFFER 后由 parseDHCPMSG 读出)
20-27 siaddr / giaddr                               全 0
28-43 chaddr[16]                                    MAC(6B) + 10 个 0
44-107 sname[64] / file[128]                        全 0
236-239 MAGIC_COOKIE                                0x63825363
240-547 OPT[312]                                    见下

三种报文的 option 序列(k 从 4 开始写,即实际偏移 240):

主机名 option 的构造是"先写长度占位再回填":先 OPT[k++]=hostName; OPT[k++]=0;,抄完 HOST_NAME("WIZnet",循环以 \0 结束,i=6)后再补 3 个 MAC 字节,最后 OPT[k-(i+3+1)] = i+3 把长度改写为"主机名长度 + 3"(第 384、484 行)。宏 DCHP_HOST_NAME 写作 "WIZnet\0",正是为了让 for(i=0; HOST_NAME[i]!=0; i++) 在显式 \0 处停下。

4.5 应答解析(parseDHCPMSG(),第 569-689 行)

  1. getSn_RX_RSR(DHCP_SOCKET) 无数据直接返回 0(不阻塞);
  2. recvfrom() 收包,得到对端 IP/端口与长度;
  3. 端口必须是 DHCP_SERVER_PORT(67);
  4. 逐字节比对 chaddr[0..5] 与 DHCP_CHADDR,不一致返回 0(过滤其它客户端的广播应答,也是本实现唯一的报文归属校验,未校验 xid 一致性);
  5. option 遍历:p = &pDHCPMSG->op + 240(240 = 236 头部 + 4 魔数),e = p + (len - 240),while(p < e) 按 option 号分派:
    • endOption(255):p = e 退出循环;
    • padOption(0):p++;
    • dhcpMessageType(53):跳过 2 字节取类型,存入 type;
    • subnetMask(1):取 4 字节到 DHCP_allocated_sn(未读长度字段,隐式假设长度 4);
    • routersOnSubnet(3) / dns(6):读长度后取前 4 字节,再 p += opt_len - 4 跳过剩余;
    • dhcpIPaddrLeaseTime(51):4 字节大端拼成 dhcp_lease_time;
    • dhcpServerIdentifier(54):4 字节到 DHCP_SIP(opt_len 读出但未用于跳过剩余字节);
    • default:p++ 跳 option 号,读长度,p += opt_len。
  6. 返回 type。

4.6 初始化与复位(第 938-979 行)

DHCP_init(s, buf):getSHAR(DHCP_CHADDR) 读 W5500 的 MAC;如果 6 字节全 0,则填入临时 MAC 00:08:DC:00:00:00 并回写 W5500("必须先设 SHAR 再调用本函数"的官方注释,本工程在 ethernet.c:289 先 network_init() 保证了这点);随后保存 socket 号、把 buf 强转为 RIP_MSG*、DHCP_XID = 0x12345678、把 W5500 的 SIPR(写两次,疑似笔误,setSUBR 被写成了 setSIPR,见风险点)/ GAR 清零、reset_DHCP_timeout()、状态置 STATE_DHCP_INIT。

reset_DHCP_timeout():dhcp_tick_1s = 0、dhcp_tick_next = DHCP_WAIT_TIME(10)、dhcp_retry_count = 0。

4.7 与上层(ethernet.c)的时序配合

5. 对外接口

本文件导出的函数(声明见 dhcp.h):DHCP_init、DHCP_run、DHCP_stop、DHCP_time_handler、reg_dhcp_cbfunc、getIPfromDHCP、getGWfromDHCP、getSNfromDHCP、getDNSfromDHCP、getDHCPLeasetime。

本文件导出的全局变量(均未加 static,符号全局可见):DHCP_SOCKET、DHCP_SIP、OLD_allocated_ip、DHCP_allocated_ip、DHCP_allocated_gw、DHCP_allocated_sn、DHCP_allocated_dns、dhcp_state、dhcp_retry_count、dhcp_lease_time、dhcp_tick_1s、dhcp_tick_next、DHCP_XID、pDHCPMSG、HOST_NAME、DHCP_CHADDR,以及三个回调指针 dhcp_ip_assign / dhcp_ip_update / dhcp_ip_conflict。

另外 makeDHCPMSG、send_DHCP_DISCOVER、send_DHCP_REQUEST、send_DHCP_DECLINE、parseDHCPMSG、check_DHCP_timeout、check_DHCP_leasedIP、reset_DHCP_timeout 也是非 static 的全局符号(头文件未声明),理论上可被其它文件链接,但工程内没有其它调用者。

本文件调用的外部函数/变量:

6. 依赖关系

7. 备注与风险点

  1. 原型与定义名字不一致:第 249 行前置声明的是 int8_t parseDHCPCMSG(void);,而第 569 行定义、第 703 行调用的是 parseDHCPMSG()(少了一个 C)。前者是一个既无定义、也无调用者的悬空声明(dhcp.h 中并未声明任何一个名字),属官方遗留笔误;若将来有人按这行声明去调用 parseDHCPCMSG,会直接链接失败。
  2. DHCP_init() 中 setSIPR 重复、setSUBR 缺失(第 959-961 行):setSIPR(zeroip); setSIPR(zeroip); setGAR(zeroip); 第二句很可能是 setSUBR(zeroip) 的笔误,导致子网掩码未被清零。
  3. subnetMask 分支不读长度字段(第 620-630 行):按固定 4 字节推进,若服务器下发长度不为 4 的掩码 option,后续 option 解析会整体错位。
  4. 只校验 chaddr,不校验 xid(第 592-595 行):理论上可被同网段伪造的广播应答欺骗,导致误接受地址。
  5. check_DHCP_leasedIP() 的 2 秒忙等(第 932 行 while((dhcp_tick_1s - ret) < 2);):占用 task_ethernet(优先级 17)CPU 且不主动让出,靠 task_system_aux(优先级 8)抢占递增秒计数才得以退出;若将来把 DHCP_time_handler() 挪进同一任务,这里会直接死锁。
  6. 冲突检测语义被 socket.c 削弱:本函数把 sendto() 返回 SOCKERR_TIMEOUT 解释为"ARP 无应答=IP 可用",但 socket.c 的 sendto() 在既无 Sn_IR_SENDOK 也无 Sn_IR_TIMEOUT 时会立即返回 SOCKERR_TIMEOUT(socket.c:446-449),因此该判定会偏向"判定为无冲突",冲突检测并不可靠。
  7. 超时判据用严格小于(第 838 行 if (dhcp_tick_next < dhcp_tick_1s)):复位时 dhcp_tick_next = 10、dhcp_tick_1s = 0,实际要等 dhcp_tick_1s 涨到 11 才触发,真实等待为 11 秒而非 10 秒。
  8. getSn_SR() 的隐形重开 socket(第 699-700 行):每次 DHCP_run() 都检查 socket 是否为 UDP 模式,不是就整掉重建;若上层的其它流程误用了 socket 1,会导致 DHCP 与业务互抢 socket。
  9. 全局符号污染:除文档化的 API 外,dhcp_state、pDHCPMSG、HOST_NAME、DHCP_CHADDR 等内部状态都是全局符号,与工程中其它模块存在命名冲突可能,也让调试期难以判断谁改了状态。
  10. dhcp.c 与 ethernet.h 的缓冲区尺寸隐式耦合:RIP_MSG_SIZE(548) 必须等于 MAX_DHCP_BUF_SIZE(ethernet.h:103,548),两边任一改动都会造成越界读写,建议加编译期断言。
  11. 未使用的常量与状态:DHCP_SECS、DHCP_HTYPE100MB、DHCP_RELEASE、DHCP_INFORM、INFINITE_LEASETIME 之外的部分 option 枚举、STATE_DHCP_RELEASE 均为死代码或残留定义。
  12. DHCP_XID 只在续租时递增:STATE_DHCP_INIT 不会重新生成 xid(只有 DHCP_init() 时置为固定值 0x12345678),同一设备重启后 xid 恒为同值,RFC 上不推荐,实际影响有限。