系列导读
本篇接续应用层 UDP Socket API,向下钻到 PHY 抽象层与硬件抽象层:
- 应用层 UDP Socket API —
Arduino_10BASE_T1S_UDP、内部队列、错误码 - 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_Interface
文件位置:src/Arduino_10BASE_T1S_PHY_Interface.h
1 | class Arduino_10BASE_T1S_PHY_Interface { |
1.1 设计动机
该抽象类是库作者预留的”可替换 PHY 后端”接口。设计上让任何实现了该接口的类都能被 Arduino_10BASE_T1S_UDP 无缝使用:
1 | Arduino_10BASE_T1S_UDP ──不直接持有 PHY 对象 |
注意:当前代码(v0.1.1)Arduino_10BASE_T1S_UDP 并未通过 Arduino_10BASE_T1S_PHY_Interface* 与 PHY 解耦——它通过全局变量 t1s_phy(宏展开的对象名)和 _lw.tc.tc6(TC6 实例指针)直接通信。该抽象类更多是面向未来扩展或用户自行实现 PHY 时的契约。
1.2 纯虚方法契约
begin(...)
| 参数 | 含义 | 约束 |
|---|---|---|
ip_addr |
本机 IPv4 地址 | 必须与 mac_addr 在同一 L2 子网,否则 ARP 无法解析 |
network_mask |
子网掩码 | 标准 IPv4 格式 |
gateway |
默认网关 | 同子网或可达路由 |
mac_addr |
6 字节 MAC 地址 | 必须在硬件上唯一(推荐 MacAddress::create_from_uid()) |
t1s_plca_settings |
PLCA 节点配置 | T1SPlcaSettings 类型,详见辅助类文档 |
t1s_mac_settings |
MAC 行为配置 | T1SMacSettings 类型 |
| 返回 | 含义 |
|---|---|
true |
PHY 已上电、寄存器已配置、netif_add 已完成 |
false |
任一步骤失败(lwIP init、SPI、TC6 init、寄存器写入等) |
service()
| 项 | 说明 |
|---|---|
| 返回 | void |
| 必须调用频率 | 越高越好(注释:Must be called cyclic. The faster the better.,见 examples/UDP_Client/UDP_Client.ino:107) |
| 内部职责 | 1) sys_check_timeouts() — lwIP 定时器;2) 根据 IRQ 状态决定是否 TC6_Service();3) TC6Regs_CheckTimers() — 寄存器超时监控 |
关键观察:service() 不是异步通知机制——它是轮询。即使没有 IRQ,TC6 内部待发的 TX 数据(来自 lwIP 上层)也会被 service 驱动出去。
1.3 移植到竞品芯片时的最小实现
1 | class MyCompetitorPHY : public Arduino_10BASE_T1S_PHY_Interface { |
2. 具体实现:TC6::TC6_Arduino_10BASE_T1S
文件位置:src/microchip/TC6_Arduino_10BASE_T1S.h / .cpp
2.1 内部数据结构
1 | // src/microchip/TC6_Arduino_10BASE_T1S.h:33-55 |
三层结构:
TC6Lib_t— TC6 协议相关状态(实例指针、当前 pbuf、回调)LwIp_t— lwIP 网络接口 + 字符串形式 IP(lwip_init 配置用)TC6LwIP_t— 聚合根,把 TC6、lwIP、HAL 三个世界连起来
2.2 构造 / 析构
1 | TC6_Arduino_10BASE_T1S(TC6_Io & tc6_io); // :106 |
构造仅做一件事:_lw.io = &tc6_io(保存 HAL 引用)。不会调用 begin(),这是用户责任。
2.3 begin() 详解
src/microchip/TC6_Arduino_10BASE_T1S.cpp:121-195
sequenceDiagram
autonumber
participant App
participant PHY as TC6_Arduino_10BASE_T1S
participant lwIP
participant TC6 as libtc6
participant Regs as libtc6-regs
App->>PHY: begin(ip, mask, gw, mac, plca, ms)
PHY->>lwIP: lwip_init() [首次]
PHY->>TC6: TC6_Init(&_lw) [注册 instance]
PHY->>TC6LwIP_instance_list: 添加节点
PHY->>Regs: TC6Regs_Init(... enablePlca, nodeId, count, burstCount, burstTimer, promisc, txCt, rxCt)
loop 等待 init done
PHY->>TC6: TC6_Service(tc6, true)
end
PHY->>lwIP: ipaddr_aton() 转换 IP/Mask/GW 字符串
PHY->>lwIP: netif_add(&netint, ..., lwIpInit, ethernet_input)
PHY->>lwIP: netif_set_link_up()
PHY->>PHY: _t1s_plca_settings = ...
PHY-->>App: true
注意点:
- lwip_init 静态保护(
:129-134):static bool is_lwip_init = false;防止重复初始化。这是库的”安全护栏”,但意味着一个进程只能有一个 PHY 实例。 - TC6 instance 列表(
:145-152):维护全局链表tc6_lwip_instance_list_head,让回调(无this上下文)能反向找到TC6LwIP_t。当前默认TC6_MAX_INSTANCES=1,但代码留了多实例扩展点。 - TC6Regs_Init 失败处理(
:155-166):返回 false 时不清理 TC6_Init,会留下泄漏的tc6_t。已知问题(v0.1.1)。 while(!TC6Regs_GetInitDone)同步阻塞(:169-170):期间通过TC6_Service(pInst, true)轮询驱动。true表示”force service”——不依赖 IRQ 状态。这是初始化阶段必需的,因为寄存器写入的响应需要 SPI 完成。- IP 字符串转换(
:173-183):用ip_addr.toString()转字符串再ipaddr_aton()回ip4_addr_t。绕了一道,原因是netif_add()的参数类型是ip_addr_t*(联合体,IPv4/IPv6 共用),而IPAddress是 IPv4-only 4 字节。轻微性能浪费(每次初始化多两次字符串转换)。
2.4 service() 详解
src/microchip/TC6_Arduino_10BASE_T1S.cpp:205-222
1 | void TC6_Arduino_10BASE_T1S::service() |
条件分支(2)的语义:
_tc6_io.isInterruptActive()通过_int_in != _int_out判断是否有未消费的中断(基于 ISR 计数)TC6_Service(..., false)的第二个参数是”IRQ 引脚电平”——false表示 IRQ 仍 active(让 libtc6 知道可以期望 RX 数据)- 如果 libtc6 在一次 service 中完成了所有 pending 工作,返回
true——此时 IRQ pin 应已回到 HIGH,调用releaseInterrupt()把_int_out推平
条件分支(3):TC6_CB_OnNeedService() 回调会被 libtc6 用来通知”我有事要做但不需要等 IRQ”(典型场景:TX 队列有待发数据)。置位 tc6NeedService,让主循环 service() 知道要调用一次 TC6_Service。
TC6Regs_CheckTimers():处理内部寄存器超时重试,例如异步写入未确认的回读检查。
2.5 PLCA 控制
getPlcaStatus(callback)
1 | // :224 |
- 发起一次异步寄存器读:
TC6_ReadRegister(tc6, 0x0004CA03, true, OnPlcaStatus, &_lw) - 寄存器地址
0x0004CA03是 LAN8651 的PLCA_status_register secure=true启用 secure mode(normal + 反转数据双重校验)—— PLCA 状态读取涉及链路健康,开销值得- 回调
OnPlcaStatus(:82-97)检查 bit[15]:status = (0u != ((1u << 15) & value)) - 返回
false仅当 TC6_ReadRegister 内部 enqueue 失败(队列满)
典型用法(来自 UDP_Server):
1 | static unsigned long prev = 0; |
enablePlca()
1 | // :230 |
- 写入
PLCA_CONTROL_0重新使能 PLCA - 复用
begin()时缓存的_t1s_plca_settings(注意:这是构造时的副本,运行中修改t1s_plca_settings对象不会影响) - Burst 计数和定时器不被重新应用(仅恢复 enable + ID/Count)
sendWouldBlock()
1 | // :235 |
- 查询 TC6 TX segment 队列是否有空闲 slot
- 不被 Arduino UDP 类使用——UDP 在
endPacket()阻塞式入队,超出 segment 容量时udp_sendto返回错 - 该方法是为将来实现非阻塞发送 API 准备的钩子
2.6 GPIO 输出:digitalWrite(DIO, value)
1 | // :197 |
DIO::A0/DIO::A1对应 LAN8651 的两个 GPIO 引脚- 首次调用时通过
TC6Regs_EnableDio_A0/A1把寄存器 PADCTRL 配置为输出 - 后续通过
TC6Regs_ToggleDio_A0/A1翻转(库实现是 toggle,所以保存本地静态状态来决定要不要翻转) - 注意:这是芯片 GPIO,不是 Arduino 引脚——是 LAN8651 自身的两个多功能 pin
示例用途:examples/tools/Control-DIOx/Control-DIOx.ino 通过这两个 GPIO 控制 PoDL(Power over Data Line)sink 的电源开关。
2.7 私有函数 digitalWrite_A0/A1
src/microchip/TC6_Arduino_10BASE_T1S.cpp:244-276
实现细节:
static bool is_dio_a0_enabled—— 避免重复调用TC6Regs_EnableDio_A0(带 cached state)static bool dio_a0_val—— 避免重复 toggle(toggle 实现)
注意:因为 static bool,多实例 PHY 会共享这些状态——这是库的隐式假设。
3. SPI 硬件抽象层:TC6::TC6_Io
文件位置:src/microchip/TC6_Io.h / .cpp
3.1 头文件
1 | // src/microchip/TC6_Io.h:30-62 |
3.2 构造与 SPI 设置常量
1 | // src/microchip/TC6_Io.cpp:28 |
| 项 | 值 | 依据 |
|---|---|---|
| 时钟 | 24 MHz | LAN8651 datasheet max 25 MHz,留 4% 余量 |
| 位序 | MSB First | OA TC6 规范 |
| 模式 | Mode 0 (CPOL=0, CPHA=0) | LAN8651 SPI 规格 |
3.3 begin() — 硬件复位 + SPI 初始化
1 | // src/microchip/TC6_Io.cpp:55 |
时序来源:LAN8651 datasheet 规定的复位保持 / 恢复时间最少各几 ms,库用 100ms 是非常保守的值(很多 reset 文档建议 ≥10ms 即可)。
约束:
- 必须在
t1s_phy.begin()之前调用,因为后者会立即通过 SPI 读芯片 ID - 多次调用
begin()会反复复位 LAN8651——不破坏,但会让 link 短暂中断 - 返回值固定
true,不检查 SPI 是否成功(_spi.begin()在 Arduino SPI 库中通常也只是pinMode(MOSI/MISO/SCK, ...))
3.4 中断计数三态机
1 | // src/microchip/TC6_Io.cpp:71-86 |
三计数器设计意图:
1 | ISR service() service() done |
| 状态 | _int_in |
_int_out |
_int_reported |
含义 |
|---|---|---|---|---|
| 初始 | 0 | 0 | 0 | 无中断 |
| ISR 触发 1 次 | 1 | 0 | 0 | 有未消费中断 |
| service 进入 | 1 | 0 | 1 | 识别到 1 次未消费 |
| service 完成(IRQ 已 HIGH) | 1 | 1 | 1 | 推平 |
| ISR 再触发 | 2 | 1 | 1 | 又有 1 次未消费 |
8-bit 计数器溢出:理论可记录 255 次未消费中断,但 isInterruptActive 的 (_int_reported != _int_out) 在两个计数器相等时返回 false——如果 ISR 比 service 快 256 次,_int_in 溢出回到 _int_out,会误判为无中断。这在 24MHz SPI + 64-byte chunk(每帧约 21μs)的速率下不太可能(要在 service 一次完成前产生 256 次 IRQ),但极端情况需注意。
volatile 的必要性:_int_in 在 ISR 中改写,主循环读,没有 volatile 编译器可能优化掉读。
releaseInterrupt 的 IRQ 电平检查:只有当 IRQ_PIN == HIGH(实际引脚已回到 inactive)才推平 _int_out。这是边沿触发的正确语义——避免在 libtc6 完成 service 后、IRQ 引脚尚未回到 HIGH 之前错误地”消费”中断。
3.5 spiTransaction()
1 | // src/microchip/TC6_Io.cpp:88 |
性能取舍(:93-94):
- Arduino SPI 库的
transfer(buf, len)是 in-place 的(一边发送 buf 内容,一边把收到的字节覆盖回 buf) - 如果 TX 和 RX 是不同的缓冲,需要先
memcpy(pRx, pTx, len)把 TX 拷贝到 RX 缓冲 - 64-byte chunk:
memcpy用时约 0.5-1μs,相比 SPI 传输本身(24MHz × 64 字节 ≈ 21μs)占比 < 5% - 优点:避免分配单独的 RX 缓冲,节省 RAM(每次可省 64 字节)
返回值的固化:return true——不检查 SPI 是否真的成功(Arduino SPI 库对失败的反馈很弱,主要是靠后续 TC6_Service 解析出的 footer 错误位来发现 SPI 问题)。
3.6 替换 SPI HAL 的最小工作量
1 | class MyCompetitorIo { |
只要这 5 个方法的语义保持一致,TC6_Arduino_10BASE_T1S 可以传入 MyCompetitorIo&(前提:也改 TC6_Arduino_10BASE_T1S 的类型签名,或把它改成 template)。当前是直接 TC6_Io &,强类型耦合。
4. 多实例支持的真实限制
虽然 libtc6 支持 TC6_MAX_INSTANCES 个实例(当前配置 1),但 Arduino 封装层有几个隐式约束:
| 约束 | 位置 | 影响 |
|---|---|---|
static bool is_lwip_init |
TC6_Arduino_10BASE_T1S.cpp:129 |
全局 lwip_init 保护,禁止两个 PHY 实例 |
static bool is_dio_a0_enabled |
TC6_Arduino_10BASE_T1S.cpp:246 |
DIO 状态共享,多实例会冲突 |
static TC6ListNode * tc6_lwip_instance_list_head |
TC6_Arduino_10BASE_T1S.cpp:46 |
全局链表,理论支持多实例 |
Arduino_10BASE_T1S_PHY_TC6 宏产生固定名 t1s_io / t1s_phy |
Arduino_10BASE_T1S.h:61-63 |
同一个 sketch 只能展开一次 |
结论:当前库是单 PHY 实例设计,多实例需要绕过宏、用 new 动态分配对象。
5. 错误恢复路径
graph TD
A["TC6_Service 返回错误"] --> B{"TC6Error 类型"}
B -->|NoHardware| C["TC6Regs_Reinit"]
B -->|BadChecksum| C
B -->|UnexpectedCtrl| C
B -->|BadTxData| C
B -->|SyncLost| C
B -->|SpiError| C
B -->|UnexpectedSv| D["日志+继续(数据错)"]
B -->|UnexpectedDvEv| D
B -->|ControlTxFail| D
B -->|Succeeded| E["忽略"]
C --> F["重新初始化全部寄存器"]
F --> G["PLCA 恢复"]
F --> H["MAC 配置恢复"]
TC6Regs_Reinit() 在多个错误场景被调用(src/microchip/TC6_Arduino_10BASE_T1S.cpp:495-544)。它会重新下发 TC6_MEMMAP[]、重新配置 PLCA。注意:Reinit 不会重置 lwIP 状态,IP/ARP 表保持——所以重连后网络可达性立即恢复。
6. 调试技巧
6.1 通过 Serial 看 TC6 事件
库中 TC6_CB_OnError 和 TC6Regs_CB_OnEvent 当前默认不打日志(#define PRINT(...) 空定义,:493)。要在调试中看:
1 | // 在 sketch 顶部 |
更彻底的方法:直接修改 src/microchip/TC6_Arduino_10BASE_T1S.cpp 的 #define PRINT(...) 改为 Serial.printf(...),然后用 arduino-cli compile 重新构建。
6.2 监控 IRQ 触发频率
1 | // 在 loop() 中加 |
当前 TC6_Io 没有暴露 _int_in——调试时要临时改成 public。
7. 小结
Arduino_10BASE_T1S_PHY_Interface 是抽象契约,目前只有一个实现 TC6_Arduino_10BASE_T1S。它依赖:
TC6_Io:5 个方法的 SPI HAL,包括一个三计数器中断状态机- libtc6 的
TC6_*C API(详见下一篇) - lwIP 的 netif 机制(也详见下一篇)
service() 是整个栈的心跳,调用频率直接决定:
- lwIP ARP 表老化是否及时
- TX credit 用尽后恢复的延迟
- PLCA 状态监控的粒度
下一篇进入最底层:libtc6 的 C API 完整清单 + lwIP netif 集成的回调契约。