系列导读
架构概览 与 协议栈深度分析 已经从分层和数据流的角度描述了整库。本系列三篇从代码接口视角,把每一层的函数、类、回调、配置项逐一拆解:
- 应用层 UDP Socket API(本篇)—
Arduino_10BASE_T1S_UDP完整 API、UdpRxPacket内部类、生命周期/资源模型 - PHY Interface 与 HAL 层 — 抽象类
Arduino_10BASE_T1S_PHY_Interface、具体类TC6_Arduino_10BASE_T1S、SPI HALTC6_Io - 协议核心 libtc6 与 lwIP 集成 — libtc6 的 C API 清单、回调契约、lwIP netif 集成、队列实现
目标读者:在移植、定制、调试本库或竞品实现时需要逐函数查阅接口契约的工程师。
1. 入口宏:Arduino_10BASE_T1S_PHY_TC6
1 |
文件位置:src/Arduino_10BASE_T1S.h:61-63
1.1 语义
这是一个对象生成宏,不是函数。展开后在调用点声明两个全局对象:
TC6::TC6_Io t1s_io— SPI / CS / RESET / IRQ 的硬件抽象封装TC6::TC6_Arduino_10BASE_T1S t1s_phy— 持有t1s_io引用、对上提供Arduino_10BASE_T1S_PHY_Interface接口的 PHY 类
1.2 参数约定
| 参数 | 类型 | 取值 | 说明 |
|---|---|---|---|
__SPI |
HardwareSPI & |
SPI / SPI1 |
Arduino SPI 设备引用,UNO R4 / SAMD 默认 SPI;Portenta C33 / GIGA 使用 SPI1 |
__CS_PIN |
int |
引脚号 | 片选,低电平有效,必须能作为 OUTPUT |
__RESET_PIN |
int |
引脚号 | 硬件复位线,低电平有效 |
__IRQ_PIN |
int |
引脚号 | MAC-PHY 中断请求,低电平下降沿触发,必须能 INPUT_PULLUP |
1.3 调用约束
- 必须在文件作用域调用,不能放进函数体内——展开后的两个对象是
static链接的全局对象 - 板卡引脚默认宏 (
src/Arduino_10BASE_T1S.h:34-55):- UNO form factor(含 SAMD、UNO R4、GIGA、AVR UNO WiFi Rev2):
CS=9, RESET=6, IRQ=2 - Portenta H7(含 M7/M4 核,MID carrier):通过
pinDefinitions.h把 PH_6/PH_15/PC_7 转换为引脚号 - Portenta C33:
CS=25, RESET=6, IRQ=2 - 未识别板卡:三个常量都为
-1,并产生# warning "No pins defined for your board"
- UNO form factor(含 SAMD、UNO R4、GIGA、AVR UNO WiFi Rev2):
- 同一个 sketch 中只能调用一次(重复展开会得到同名的全局对象
t1s_io/t1s_phy,编译报重复定义)
1.4 完整初始化调用序列
1 |
|
2. UDP Socket 类:Arduino_10BASE_T1S_UDP
文件位置:src/Arduino_10BASE_T1S_UDP.h / src/Arduino_10BASE_T1S_UDP.cpp
2.1 类继承
1 | class Arduino_10BASE_T1S_UDP : public UDP // Arduino 核心库 api/Udp.h |
继承自 Arduino 标准 UDP 基类(#include <Udp.h>),因此可直接用于 WiFiUDP 风格的现有 Arduino 例程(例如 EthernetUDP 移植过来的代码),只要替换类型名即可。
2.2 构造 / 析构
1 | Arduino_10BASE_T1S_UDP(); // src/Arduino_10BASE_T1S_UDP.cpp:27 |
| 方法 | 行为 |
|---|---|
| 默认构造 | 初始化所有成员为 0/NULL/空。不会自动调用 begin(),也不会调用 lwip_init() |
| 析构 | 调用 stop() 释放 udp_pcb 资源 |
重要约束:begin() 之前 _udp_pcb == nullptr,所有发送方法都会返回失败码(0 或 -1)。
2.3 生命周期:begin / stop
1 | virtual uint8_t begin(uint16_t port) override; // src/Arduino_10BASE_T1S_UDP.cpp:45 |
begin(uint16_t port)
| 项 | 说明 |
|---|---|
| 入参 | port — 本地监听端口 |
| 返回 | 1 成功 / 0 失败(udp_bind 出错时) |
| 实现要点 | 1) 若 _udp_pcb == nullptr 则调用 udp_new() 分配 PCB;2) udp_bind(_udp_pcb, IP_ADDR_ANY, port);3) udp_recv(_udp_pcb, lwIp_udp_raw_recv, this) 注册接收回调(this 作为 arg) |
| 失败场景 | udp_bind 返回非 ERR_OK(lwIP 内部错误、内存不足) |
| 不调副作用 | 不调用 netif_set_default() / netif_set_link_up()——这些由 TC6_Arduino_10BASE_T1S::begin() 完成 |
| 可重入性 | 同一个对象可重复 begin(),但需要在中间 stop(),否则旧的 PCB 不会被释放 |
stop()
| 项 | 说明 |
|---|---|
| 返回 | void |
| 实现要点 | 若 _udp_pcb != nullptr:udp_disconnect() + udp_remove() + _udp_pcb = nullptr |
| 析构器自动调用 | 析构函数会调用 stop(),所以动态分配的对象泄漏 PCB 的风险低 |
注意:未实现的 beginPacket(const char* host, ...) 重载
beginPacket(const char *host, uint16_t port) 仍然存在但永远返回 0(src/Arduino_10BASE_T1S_UDP.cpp:86-90),注释 /* TODO */ 表明 DNS 解析未集成。当前不能用域名发包,必须用 IPAddress 重载。
2.4 发送:beginPacket / write / endPacket
1 | virtual int beginPacket(IPAddress ip, uint16_t port) override; // :72 |
发送状态机
1 | [未初始化] |
各方法细节
beginPacket(IPAddress ip, uint16_t port)
- 仅记录目标 IP/port 到
_send_to_ip/_send_to_port _tx_data.clear():清空内部 std::vector(src/Arduino_10BASE_T1S_UDP.cpp:81),不会丢弃正在传输的包——endPacket()之前不可能开始第二个包- 返回
1/0(仅检查_udp_pcb != nullptr)
write(uint8_t data) / write(const uint8_t* buf, size_t size)
- 写入内部
_tx_data(std::vector<uint8_t>),无网络 IO - 单字节版固定返回 1;批量版返回
size(不做边界检查——endPacket时如果分配 pbuf 失败才报 -1)
endPacket()
- 分配
pbuf(PBUF_TRANSPORT, PBUF_RAM) pbuf_take()拷贝_tx_data到 pbuf payload_tx_data.clear()之后才调用udp_sendto(),所以失败重发时需要重新beginPacket- 返回值约定混乱(实际行为,非 API 契约):
1— 成功0—_udp_pcb == nullptr(初始化失败)-1—pbuf_take或udp_sendto失败
- 注意
pbuf_free(p)总是会被调用
典型陷阱
1 | udp_client.beginPacket(server_ip, 8888); |
2.5 接收:parsePacket / available / read / peek / flush
1 | virtual int parsePacket() override; // :137 |
内部队列模型
1 | lwIP UDP RX 回调 (lwIp_udp_raw_recv) |
parsePacket()
- 从
_rx_pkt_list头部弹出一个包,存到_rx_pkt - 返回包的总长度
totalSize();若列表空则_rx_pkt.reset()并返回 0 - 注意:连续调用
parsePacket()多次而中间不read()/flush()会丢弃中间的包(每次都把队首移到_rx_pkt)
available() / read() / peek()
- 都代理到
_rx_pkt内部的std::deque<uint8_t>操作 - 若
_rx_pkt == nullptr(没parsePacket过、或刚flush()过),返回 -1 / 0
flush()
- 释放
_rx_pkt(_rx_pkt.reset()),等同于”放弃当前包剩余数据” - 不是网络层 flush(UDP 没有 buffered TX flush 语义)
_rx_pkt_list中的排队包不受影响
接收缓冲上限
1 | int _rx_pkt_list_size = 10; // src/Arduino_10BASE_T1S_UDP.h:239 |
onUdpRawRecv 收到新包时如果 _rx_pkt_list.size() > _rx_pkt_list_size(注意是 > 不是 >=),就丢弃最老的包。可通过 bufferSize(int size) 调整(src/Arduino_10BASE_T1S_UDP.h:227-229)。
反直觉的边界行为:bufferSize(N) 实际允许 N 个包,第 N+1 个到达时才丢第 1 个。
2.6 内部类 UdpRxPacket
src/Arduino_10BASE_T1S_UDP.h:241-300
1 | class UdpRxPacket { |
| 字段/方法 | 说明 |
|---|---|
| 构造 | 用 std::deque(p_data, p_data + data_len) 把 lwIP pbuf payload 拷贝到自己的 deque |
remoteIP() / remotePort() |
用 const 修饰,但 remoteIP() 返回值 IPAddress 不是引用,可以安全缓存 |
totalSize() |
返回构造时的原始长度,不会因 read 减少 |
available() |
返回 deque 中剩余字节数 |
read() / read(buf, len) / peek() |
deque 的对应操作,行为同 Arduino 标准 UDP |
关键实现选择:_rx_data_len 与 deque.size() 可能不一致(前者是接收时的原始长度,后者是未读长度)。totalSize() 返回原始长度。
生命周期:SharedPtr 通过 std::shared_ptr 管理。_rx_pkt_list 持有一份引用(队列中排队),_rx_pkt 持有一份引用(当前包)。两者都释放后,对象析构。
2.7 内部回调 onUdpRawRecv
1 | void onUdpRawRecv(struct udp_pcb *pcb, struct pbuf *p, |
调用来源:lwIP 的 udp_recv() 回调 lwIp_udp_raw_recv(src/Arduino_10BASE_T1S_UDP.cpp:252-256)转发 arg 中的 this 指针到此方法。
处理流程:
- 从
ip_addr_t提取 IPv4 四元组(ip4_addr1..4),构造IPAddress std::make_shared<UdpRxPacket>(remote_ip, remote_port, p->payload, p->len)— 只取 pbuf 第一段(p->len,不是p->tot_len),这意味着如果 UDP 数据跨越多个 pbuf 段,只有第一段被拷贝- 队列溢出保护(
bufferSize控制) pbuf_free(p)— 释放 lwIP pbuf(已经从第一段 payload 拷出到 deque)
已知限制
- pbuf 分段丢失:UDP packet 跨多个 pbuf 时,只有第一段进入应用。如果实际抓包看到
p->tot_len > p->len,那部分数据被静默丢弃。 - 没有 TOS/TTL 设置:走 lwIP 默认值。
- 没有广播过滤:
FilterRxEthernetPacket在 PHY 层只放过 IPv4 (0x0800) 和 ARP (0x0806),不区分单播/广播。
2.8 缓冲区 / 内存占用
Arduino_10BASE_T1S_UDP 实例本身不大:
1 | struct udp_pcb * _udp_pcb; // lwIP 控制块,编译时决定大小 |
_tx_data 在每次 endPacket() 后被 clear(),但容量不释放(std::vector::clear() 不缩容)。多次 beginPacket + write 后保留最大发送长度对应的容量。
UdpRxPacket 内部 std::deque<uint8_t> 用小块分配,与 1536 字节 MTU 对应,单包占用 ~1.5KB + deque 节点开销。10 个排队包最大占用 ~15KB RAM。
2.9 完整的 UDP API 速查表
| 方法 | 返回 | 失败时 | 备注 |
|---|---|---|---|
Arduino_10BASE_T1S_UDP() |
— | — | 必须先 begin() |
~Arduino_10BASE_T1S_UDP() |
— | — | 自动 stop() |
begin(port) |
uint8_t |
0 |
唯一入口 |
stop() |
void |
— | 析构器已调 |
beginPacket(IPAddress, port) |
int |
0 |
仅 _udp_pcb == nullptr 时失败 |
beginPacket(const char*, port) |
int |
0 永远 |
DNS 未实现 |
write(uint8_t) |
size_t |
1(无错) |
|
write(buf, size) |
size_t |
size(无错) |
|
endPacket() |
int |
0 或 -1 |
见 2.4 陷阱 |
parsePacket() |
int |
0(无包) |
返回包总长度 |
available() |
int |
0 |
当前包剩余字节 |
read() |
int |
-1 |
无当前包时 |
read(buf, len) |
int |
-1 |
|
peek() |
int |
-1 |
|
flush() |
void |
— | 只释放当前包,不刷队列 |
remoteIP() |
IPAddress |
IPAddress() 默认值 |
|
remotePort() |
uint16_t |
0 |
|
onUdpRawRecv(...) |
void |
— | 私有回调,用户不调 |
bufferSize(int) |
void |
— | 设队列上限,默认 10 |
3. 与 Arduino 标准 UDP 的差异
| 特性 | Arduino UDP(Ethernet/WiFi 库基类) |
Arduino_10BASE_T1S_UDP |
|---|---|---|
| 绑定方式 | begin(port) 即可 |
需 t1s_phy.begin(...) 已初始化 lwIP |
| 远程端口 / IP 上下文 | 每个 UDP 实例有”远程”概念(connect() 后) |
每个包独立记录,remoteIP()/remotePort() 仅对当前 _rx_pkt 有效 |
| 多个远端 | 单连接模型 | remoteIP() 在 parsePacket() 之后才确定,可服务多个远端 |
| 异步发送 | 部分实现支持 | endPacket() 同步等 lwIP 入队(不是等发送完成) |
| 广播 | 基类支持 | 支持(lwIP 默认开 broadcast) |
| 多实例 | 一般支持 | 单 PHY 下只能有一个 udp_pcb 绑定同一 port,绑不同 port 可多实例 |
4. 错误排查速查表
| 现象 | 可能原因 | 验证方法 |
|---|---|---|
begin() 返回 0 |
lwIP 未初始化(t1s_phy.begin() 失败/未调) |
检查 Serial.println(ip_addr) 之前的输出 |
endPacket() 持续返回 -1 |
ARP 未解析(目标 IP 不在同一子网) | ping 目标 IP,看 ARP 表 |
parsePacket() 永远返回 0 |
接收缓冲区满(bufferSize(10) 但 10 个包都没读) |
临时调大 bufferSize;检查 flush() 逻辑 |
read() 返回 -1 |
已 flush() 或未 parsePacket() |
增加调试打印 |
| 数据只读到前 N 字节 | pbuf 分段未拷贝(lwIP 大包跨 segment) | 抓包确认 MTU/包长 |
5. 小结
Arduino_10BASE_T1S_UDP 看似是简单的 Arduino UDP 封装,但有几个值得移植/调试时关注的实现细节:
- 失败返回值不一致:
endPacket()既返回0也返回-1,调用方必须同时处理 flush()不刷队列:很多人误以为是 Arduino 通用Stream::flush()语义- pbuf 跨段静默丢失:
onUdpRawRecv只取p->len,对tot_len > len的包截断 - DNS 缺失:域名版本
beginPacket永远失败 - 缓冲区按容量不释放:
std::vector _tx_data保留峰值容量
下一篇进入 PHY Interface 与 HAL 层:Arduino_10BASE_T1S_PHY_Interface 抽象、TC6_Arduino_10BASE_T1S 实现、TC6_Io SPI HAL 的逐函数拆解。