10BASE-T1S Arduino 代码接口深度解析(一):应用层 UDP Socket API

系列导读

架构概览协议栈深度分析 已经从分层和数据流的角度描述了整库。本系列三篇从代码接口视角,把每一层的函数、类、回调、配置项逐一拆解:

  1. 应用层 UDP Socket API(本篇)— Arduino_10BASE_T1S_UDP 完整 API、UdpRxPacket 内部类、生命周期/资源模型
  2. PHY Interface 与 HAL 层 — 抽象类 Arduino_10BASE_T1S_PHY_Interface、具体类 TC6_Arduino_10BASE_T1S、SPI HAL TC6_Io
  3. 协议核心 libtc6 与 lwIP 集成 — libtc6 的 C API 清单、回调契约、lwIP netif 集成、队列实现

目标读者:在移植、定制、调试本库或竞品实现时需要逐函数查阅接口契约的工程师。

1. 入口宏:Arduino_10BASE_T1S_PHY_TC6

1
2
3
#define Arduino_10BASE_T1S_PHY_TC6(__SPI, __CS_PIN, __RESET_PIN, __IRQ_PIN) \
TC6::TC6_Io t1s_io(__SPI, __CS_PIN, __RESET_PIN, __IRQ_PIN); \
TC6::TC6_Arduino_10BASE_T1S t1s_phy(t1s_io);

文件位置: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 调用约束

  1. 必须在文件作用域调用,不能放进函数体内——展开后的两个对象是 static 链接的全局对象
  2. 板卡引脚默认宏 (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"
  3. 同一个 sketch 中只能调用一次(重复展开会得到同名的全局对象 t1s_io / t1s_phy,编译报重复定义)

1.4 完整初始化调用序列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <Arduino_10BASE_T1S.h>
#include <SPI.h>

// 1. 宏展开,生成全局对象
#if defined(ARDUINO_GIGA) || defined(ARDUINO_PORTENTA_C33)
Arduino_10BASE_T1S_PHY_TC6(SPI1, CS_PIN, RESET_PIN, IRQ_PIN);
#else
Arduino_10BASE_T1S_PHY_TC6(SPI, CS_PIN, RESET_PIN, IRQ_PIN);
#endif

Arduino_10BASE_T1S_UDP udp_client;

void setup() {
Serial.begin(115200);

// 2. 中断脚配置:INPUT_PULLUP + FALLING
pinMode(IRQ_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(IRQ_PIN),
[]() { t1s_io.onInterrupt(); },
FALLING);

// 3. SPI + 硬件复位
t1s_io.begin(); // 100ms LOW + 100ms HIGH 复位

// 4. PHY 初始化:lwip_init + TC6_Init + 32 条寄存器初始化 + netif_add
MacAddress const mac = MacAddress::create_from_uid();
t1s_phy.begin(ip_addr, network_mask, gateway,
mac, t1s_plca_settings, t1s_default_mac_settings);

// 5. UDP socket 绑定本地端口
udp_client.begin(UDP_CLIENT_PORT);
}

void loop() {
// 6. 协议栈心跳,必须高频调用
t1s_phy.service();
// ... 应用 UDP 收发逻辑
}

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
2
Arduino_10BASE_T1S_UDP();           // src/Arduino_10BASE_T1S_UDP.cpp:27
virtual ~Arduino_10BASE_T1S_UDP(); // src/Arduino_10BASE_T1S_UDP.cpp:36
方法 行为
默认构造 初始化所有成员为 0/NULL/空。不会自动调用 begin(),也不会调用 lwip_init()
析构 调用 stop() 释放 udp_pcb 资源

重要约束begin() 之前 _udp_pcb == nullptr,所有发送方法都会返回失败码(0 或 -1)。

2.3 生命周期:begin / stop

1
2
virtual uint8_t begin(uint16_t port) override;   // src/Arduino_10BASE_T1S_UDP.cpp:45
virtual void stop() override; // src/Arduino_10BASE_T1S_UDP.cpp:62

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 != nullptrudp_disconnect() + udp_remove() + _udp_pcb = nullptr
析构器自动调用 析构函数会调用 stop(),所以动态分配的对象泄漏 PCB 的风险低

注意:未实现的 beginPacket(const char* host, ...) 重载

beginPacket(const char *host, uint16_t port) 仍然存在但永远返回 0src/Arduino_10BASE_T1S_UDP.cpp:86-90),注释 /* TODO */ 表明 DNS 解析未集成。当前不能用域名发包,必须用 IPAddress 重载。

2.4 发送:beginPacket / write / endPacket

1
2
3
4
virtual int beginPacket(IPAddress ip, uint16_t port) override;  // :72
virtual int endPacket() override; // :92
virtual size_t write(uint8_t data) override; // :125
virtual size_t write(const uint8_t * buffer, size_t size) override; // :131

发送状态机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[未初始化]
│ begin(port)

[READY] ←──┐
│ │
│ beginPacket(ip, port) ◀── 每次只能有一个 packet 在构建
▼ │
[BUILDING]──┘
│ write(...) 任意次

│ endPacket()

│ udp_sendto() ─── 失败返回 -1

[READY]

各方法细节

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_datastd::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(初始化失败)
    • -1pbuf_takeudp_sendto 失败
  • 注意 pbuf_free(p) 总是会被调用

典型陷阱

1
2
3
4
5
6
7
8
9
10
udp_client.beginPacket(server_ip, 8888);
udp_client.write("hello", 5);
int rc = udp_client.endPacket();
if (rc != 1) {
// rc 可能是 0 也可能是 -1,必须同时处理
// 失败后 _tx_data 已经被清空,不能重试,需重新 beginPacket + write
udp_client.beginPacket(server_ip, 8888); // 必须重新开始
udp_client.write("hello", 5);
udp_client.endPacket();
}

2.5 接收:parsePacket / available / read / peek / flush

1
2
3
4
5
6
7
8
9
virtual int parsePacket() override;   // :137
virtual int available() override; // :158
virtual int read() override; // :166
virtual int read(unsigned char* buffer, size_t len) override; // :174
virtual int read(char* buffer, size_t len) override; // :182
virtual int peek() override; // :190
virtual void flush() override; // :198
virtual IPAddress remoteIP() override;// :205
virtual uint16_t remotePort() override;// :213

内部队列模型

1
2
3
4
5
6
7
8
9
10
lwIP UDP RX 回调 (lwIp_udp_raw_recv)
│ _rx_pkt_list.push_back(...)

std::list<UdpRxPacket::SharedPtr> _rx_pkt_list // 默认上限 10 个包
│ parsePacket() → pop_front()

UdpRxPacket::SharedPtr _rx_pkt // 当前正在消费的包
│ read()/peek()/flush()

std::deque<uint8_t> _rx_data (内部)

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
2
3
4
5
6
7
8
class UdpRxPacket {
IPAddress const _remote_ip;
uint16_t const _remote_port;
size_t const _rx_data_len;
std::deque<uint8_t> _rx_data;
typedef std::shared_ptr<UdpRxPacket> SharedPtr;
...
};
字段/方法 说明
构造 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_lendeque.size() 可能不一致(前者是接收时的原始长度,后者是未读长度)。totalSize() 返回原始长度。

生命周期SharedPtr 通过 std::shared_ptr 管理。_rx_pkt_list 持有一份引用(队列中排队),_rx_pkt 持有一份引用(当前包)。两者都释放后,对象析构。

2.7 内部回调 onUdpRawRecv

1
2
void onUdpRawRecv(struct udp_pcb *pcb, struct pbuf *p,
const ip_addr_t *addr, uint16_t port); // :221

调用来源:lwIP 的 udp_recv() 回调 lwIp_udp_raw_recvsrc/Arduino_10BASE_T1S_UDP.cpp:252-256)转发 arg 中的 this 指针到此方法。

处理流程

  1. ip_addr_t 提取 IPv4 四元组(ip4_addr1..4),构造 IPAddress
  2. std::make_shared<UdpRxPacket>(remote_ip, remote_port, p->payload, p->len)只取 pbuf 第一段p->len,不是 p->tot_len),这意味着如果 UDP 数据跨越多个 pbuf 段,只有第一段被拷贝
  3. 队列溢出保护(bufferSize 控制)
  4. 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
2
3
4
5
6
7
struct udp_pcb *   _udp_pcb;      // lwIP 控制块,编译时决定大小
IPAddress _send_to_ip; // 4 字节
uint16_t _send_to_port; // 2 字节
std::vector<uint8_t> _tx_data; // 当前 packet 的发送缓冲,动态增长
int _rx_pkt_list_size = 10; // 4 字节
std::list<UdpRxPacket::SharedPtr> _rx_pkt_list; // 10 个 node + heap
UdpRxPacket::SharedPtr _rx_pkt; // 当前包

_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 封装,但有几个值得移植/调试时关注的实现细节:

  1. 失败返回值不一致endPacket() 既返回 0 也返回 -1,调用方必须同时处理
  2. flush() 不刷队列:很多人误以为是 Arduino 通用 Stream::flush() 语义
  3. pbuf 跨段静默丢失onUdpRawRecv 只取 p->len,对 tot_len > len 的包截断
  4. DNS 缺失:域名版本 beginPacket 永远失败
  5. 缓冲区按容量不释放std::vector _tx_data 保留峰值容量

下一篇进入 PHY Interface 与 HAL 层:Arduino_10BASE_T1S_PHY_Interface 抽象、TC6_Arduino_10BASE_T1S 实现、TC6_Io SPI HAL 的逐函数拆解。