10BASE-T1S Arduino 开源库架构分析(三):移植实战指南

系列导读

  1. 架构概览与开源评估 — 分层设计、编译平台、许可证分析
  2. 协议栈深度分析 — OA TC6 SPI 协议帧结构、数据流、PLCA
  3. 移植实战指南(本篇)— 抽象边界、可复用模块、移植 checklist

移植目标

将 Arduino 10BASE-T1S 库的测试验证能力移植到竞品 T1S 芯片环境,实现:

  • 在 Arduino 平台上驱动竞品 MAC-PHY 芯片
  • 复用 lwIP 协议栈和 UDP 测试框架
  • 用相同的 UDP echo 测试方法验证竞品芯片功能

架构抽象边界

移植的核心问题是:哪些模块与 Microchip LAN8651 芯片绑定,哪些是通用的?

graph TB
    subgraph "可直接复用 (Chip-Independent)"
        A1["Arduino_10BASE_T1S_UDP"]
        A2["lwIP TCP/IP Stack"]
        A3["T1SPlcaSettings"]
        A4["T1SMacSettings"]
        A5["MacAddress"]
    end

    subgraph "条件复用 (取决于竞品是否兼容 OA TC6)"
        B1["libtc6 协议核心 (tc6.cpp)"]
        B2["Credit 管理 / Chunk 分帧"]
        B3["Header/Footer 解析"]
    end

    subgraph "必须替换 (Chip-Specific)"
        C1["TC6_Io (SPI 配置)"]
        C2["tc6-regs.cpp (寄存器初始化)"]
        C3["芯片 ID 校验逻辑"]
        C4["PHY 校准参数"]
        C5["PLCA 寄存器地址"]
    end

    A1 --> A2
    A2 --> B1
    B1 --> C1
    B1 --> C2

场景一:竞品兼容 OA TC6 SPI 协议

如果竞品芯片也实现了 Open Alliance TC6 SPI 接口(DNC/SEQ/DV/SV/EV 帧结构一致),移植工作量最小:

需要修改的文件

文件 修改内容 工作量
src/microchip/TC6_Io.cpp SPI 时钟/模式(如果不同)、引脚定义
src/microchip/lib/libtc6/src/tc6-regs.cpp 整个寄存器初始化表、芯片 ID 校验、PLCA 寄存器地址
src/Arduino_10BASE_T1S.h 板卡引脚宏定义

TC6_Io 适配

SPI HAL 层极其简洁,只有 4 个需要确认的参数:

1
2
3
4
5
6
// 需要根据竞品 datasheet 确认这些值
static SPISettings const COMPETITOR_SPI_SETTING{
?? * 1000 * 1000UL, // 时钟频率(查竞品 SPI 最大时钟)
MSBFIRST, // 大概率相同
SPI_MODE0 // 确认 CPOL/CPHA
};

复位时序也需要确认:

1
2
3
4
5
6
7
8
9
10
11
12
bool TC6_Io::begin()
{
digitalWrite(_cs_pin, HIGH);
pinMode(_cs_pin, OUTPUT);
pinMode(_reset_pin, OUTPUT);
digitalWrite(_reset_pin, LOW);
delay(??); // 竞品复位保持时间
digitalWrite(_reset_pin, HIGH);
delay(??); // 竞品复位恢复时间
_spi.begin();
return true;
}

寄存器初始化表替换

这是工作量最大的部分。需要根据竞品 datasheet 重写 TC6_MEMMAP[] 数组:

1
2
3
4
5
6
7
8
9
10
11
// 原始 LAN8651 初始化表(32 条)
static const MemoryMap_t TC6_MEMMAP[] = {
{ .address=0x00000004, .value=0x00000026, .op=MemOp_Write, .secure=false },
// ... Microchip 特定的寄存器配置
};

// 替换为竞品芯片的初始化表
static const MemoryMap_t TC6_MEMMAP[] = {
{ .address=??, .value=??, .op=MemOp_Write, .secure=?? },
// ... 根据竞品 datasheet 填写
};

需要从竞品 datasheet 中提取的信息:

  1. CONFIG0 等 SPI 配置寄存器 — chunk size、保护模式设置
  2. MAC 寄存器 — MAC 地址写入地址、网络控制寄存器
  3. PHY 寄存器 — 校准参数、电气特性配置
  4. PLCA 寄存器 — Node ID、Node Count、Burst 参数的地址和位域
  5. 中断掩码 — 状态上报配置

芯片 ID 校验修改

1
2
3
4
5
6
7
8
9
// 原始校验逻辑(tc6-regs.cpp:346-356)
// 读 REG 0x00000001, 检查 OUI=0x1F0, Model=0x1B
// 读 REG 0x000A0094, 获取 chip revision

// 需要替换为竞品的 ID 寄存器地址和期望值
while (!TC6_ReadRegister(pReg->pTC6, COMPETITOR_ID_REG, false, OnReadId1, NULL)) {
TC6_Service(pReg->pTC6, true);
}
// 校验逻辑中比较竞品的 OUI 和 Model 值

PLCA 寄存器地址映射

如果竞品芯片支持 PLCA 但寄存器地址不同,需要修改 HandlePlca()

1
2
3
4
5
6
7
8
9
10
11
// LAN8651 PLCA 寄存器地址
#define PLCA_CONTROL_0 0x0004CA01 // PLCA 使能
#define PLCA_CONTROL_1 0x0004CA02 // Node ID + Count
#define PLCA_BURST_MODE 0x0004CA05 // Burst 配置
#define COL_DET_CTRL0 0x00040087 // 碰撞检测模式

// 替换为竞品地址
#define PLCA_CONTROL_0 0x????????
#define PLCA_CONTROL_1 0x????????
#define PLCA_BURST_MODE 0x????????
#define COL_DET_CTRL0 0x????????

场景二:竞品使用私有 SPI 协议

如果竞品芯片不兼容 OA TC6(使用自定义寄存器协议或不同的帧封装),需要替换整个协议层:

需要替换的模块

graph TB
    subgraph "保留"
        A["Arduino_10BASE_T1S_UDP + lwIP"]
    end

    subgraph "重新实现的接口层"
        B["新的 PHY Driver Class"]
    end

    subgraph "全部替换"
        C["竞品 SPI 协议实现"]
        D["竞品寄存器驱动"]
        E["竞品 chunk/frame 处理"]
    end

    A --> B
    B --> C
    B --> D
    C --> E

需要实现的接口

Arduino_10BASE_T1S_PHY_Interface 定义了上层需要的抽象接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/Arduino_10BASE_T1S_PHY_Interface.h
class Arduino_10BASE_T1S_PHY_Interface {
public:
virtual bool begin(IPAddress ip, IPAddress mask, IPAddress gw,
MacAddress mac,
T1SPlcaSettings plca,
T1SMacSettings mac_settings) = 0;
virtual void service() = 0;
virtual bool getPlcaStatus(PlcaStatusCallback callback) = 0;
virtual void enablePlca() = 0;
virtual bool sendWouldBlock() = 0;
virtual void digitalWrite(DIO dio, bool value) = 0;
};

新的驱动类需要实现这些接口,并完成以下内部工作:

  1. 初始化 SPI 和竞品芯片
  2. 集成 lwIP — 注册 netif,提供 lwIpOut() 回调
  3. 实现 service() — 驱动 SPI 收发,将接收帧送入 lwIP
  4. PLCA 管理 — 如果竞品支持 PLCA

lwIP 集成的关键回调

无论竞品用什么 SPI 协议,与 lwIP 的集成模式不变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 发送回调:lwIP 要发帧时调用
static err_t lwIpOut(struct netif *netif, struct pbuf *p)
{
// 将 pbuf 数据交给竞品驱动发送
// pbuf 可能是链表,需要遍历所有 segment
uint8_t *frame_buf = ...;
uint16_t frame_len = p->tot_len;
for (struct pbuf *q = p; q != NULL; q = q->next) {
memcpy(frame_buf + offset, q->payload, q->len);
offset += q->len;
}
competitor_send_frame(frame_buf, frame_len);
return ERR_OK;
}

// 接收路径:竞品驱动收到完整帧后调用
void on_frame_received(uint8_t *frame, uint16_t len)
{
struct pbuf *p = pbuf_alloc(PBUF_RAW, len, PBUF_POOL);
if (p != NULL) {
memcpy(p->payload, frame, len);
netif->input(p, netif); // 送入 lwIP(即 ethernet_input)
}
}

回调接口清单

libtc6 通过回调函数与外部交互。如果复用 libtc6,需要实现这些回调:

回调函数 用途 实现要点
TC6_CB_OnSpiTransaction() 执行 SPI 全双工传输 调用 TC6_Io::spiTransaction()
TC6_CB_OnRxEthernetSlice() 收到帧片段 拼接到 lwIP pbuf
TC6_CB_OnRxEthernetPacket() 收到完整帧 调用 netif->input()
TC6_CB_OnNeedService() TC6 内部需要服务 设置 tc6NeedService 标志
TC6_CB_OnError() 错误上报 日志输出或重新初始化
TC6_CB_OnExtendedStatus() 扩展状态(中断事件) 处理 TX/RX timestamp、错误标志

如果不复用 libtc6(场景二),这些回调不需要实现,但等效功能需要在新驱动中完成。

移植步骤 Checklist

Phase 1: 环境准备

  • 获取竞品 T1S 芯片 datasheet
  • 确认竞品是否兼容 OA TC6 SPI 协议
  • 准备硬件:竞品评估板 + Arduino 开发板 + SPI 连接
  • Fork Arduino_10BASE_T1S 仓库,创建移植分支

Phase 2: SPI 通信验证

  • 确认 SPI 物理连接(MOSI/MISO/CLK/CS/IRQ/RESET 引脚)
  • 修改 TC6_Io.cpp 中的 SPI 时钟/模式/时序参数
  • 修改 Arduino_10BASE_T1S.h 中的引脚定义
  • 验证基本 SPI 通信:读取芯片 ID 寄存器

Phase 3: 寄存器初始化

  • 根据 datasheet 编写新的 TC6_MEMMAP[] 初始化表
  • 实现芯片 ID 校验逻辑
  • 配置 MAC 地址寄存器
  • 配置 PHY 基础参数
  • 验证:寄存器回读确认写入正确

Phase 4: PLCA 配置

  • 确认竞品 PLCA 寄存器地址和位域
  • 修改 HandlePlca() 函数
  • 测试 Node 0(协调器)模式
  • 测试 Node 1+ 模式
  • 验证:PLCA status 寄存器读取正确

Phase 5: 数据通路

  • 如果兼容 OA TC6:验证 chunk 发送/接收
  • 如果私有协议:实现新的帧收发驱动
  • 集成 lwIP netif
  • 验证:ping 测试(ICMP echo)

Phase 6: UDP 功能验证

  • 运行 UDP_Server example(竞品芯片侧)
  • 从 EVB-LAN8670-USB 或另一个节点发 UDP 包
  • 验证双向通信
  • 验证 PLCA 多节点场景(2+ 节点)

Phase 7: 性能与稳定性

  • 长时间运行测试(24h+)
  • 吞吐量测量
  • 异常恢复:拔线重插、节点加入/退出
  • 温度/电压边界条件(如果需要)

验证方案

测试拓扑

graph LR
    A["Arduino + 竞品 T1S\n(Node 1, UDP Client)"] -->|"单对双绞线"| B["Multi-drop Bus"]
    C["Arduino + LAN8651\n(Node 0, UDP Server)"] -->|"单对双绞线"| B
    D["EVB-LAN8670-USB\n(Linux, tcpdump)"] -->|"单对双绞线"| B

验证项

测试 方法 通过标准
SPI 通信 读芯片 ID 寄存器 返回正确的 OUI/Model
链路建立 PLCA status 读取 bit[15] = 1 (PLCA active)
IP 连通性 ping from Linux host ICMP reply 正常
UDP 收发 UDP echo test 双向数据正确、无丢包
多节点 3 节点同时通信 所有节点均能收发
稳定性 持续发包 1000+ 次 无累积错误、无 PLCA 回退

tcpdump 抓包验证

1
2
3
4
# Linux EVB 侧
sudo ip addr add dev eth1 192.168.42.100/24
sudo ifconfig eth1 up
tcpdump -i eth1 -v udp port 8888

期望输出:

1
2
3
4
192.168.42.101.8889 > 192.168.42.100.8888: UDP, length 48
"Single-Pair Ethernet / 10BASE-T1S: packet cnt = 0"
192.168.42.100.8888 > 192.168.42.101.8889: UDP, length 48
(echo reply)

常见移植陷阱

  1. SPI 时序差异 — 竞品可能要求 CS 到第一个 CLK 有最小间隔,Arduino SPI 库不保证这一点。可能需要插入 delayMicroseconds()
  2. 中断极性 — LAN8651 IRQ 是 active-low falling-edge,竞品可能是 level-triggered 或 active-high
  3. Chunk size 不同 — OA TC6 标准 chunk 是 64 字节,但有些芯片支持 32 字节 chunk,需要修改 TC6_CHUNK_SIZE
  4. MAC 地址字节序 — LAN8651 按特定顺序写入 SPEC_ADD1/ADD2,竞品可能不同
  5. 初始化时序 — 有些芯片需要等待 PLL lock 或者 PHY ready 信号,不能立即开始寄存器写入
  6. lwIP 重入service() 调用频率影响网络定时器精度,太慢会导致 ARP timeout

代码结构建议

建议的移植后目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
src/
├── Arduino_10BASE_T1S.h # 修改引脚定义
├── Arduino_10BASE_T1S_PHY_Interface.h # 不变
├── Arduino_10BASE_T1S_UDP.h/cpp # 不变
├── MacAddress.h/cpp # 不变
├── T1SPlcaSettings.h/cpp # 不变
├── T1SMacSettings.h/cpp # 不变
├── competitor/ # 新建:竞品驱动
│ ├── Competitor_10BASE_T1S.h/cpp # 实现 PHY_Interface
│ ├── Competitor_Io.h/cpp # SPI HAL
│ └── lib/ # 竞品 SDK 或协议库
├── lib/liblwip/ # 不变
└── microchip/ # 保留作为参考(或删除)

小结

移植策略取决于一个关键判断:竞品是否兼容 OA TC6 SPI 协议

  • 兼容:改寄存器表和引脚配置,工作量约 1-2 周
  • 不兼容:需要实现新的帧收发驱动并对接 lwIP,工作量约 3-4 周

无论哪种场景,lwIP 协议栈、UDP 封装层、测试框架(UDP echo + tcpdump)都可以完整复用,这是这个开源库最大的移植价值。