10BASE-T1S Arduino 开源库架构分析(二):OA TC6 协议栈深度分析

系列导读

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

为什么需要 OA TC6 SPI 协议

MCU 通过 SPI 与 LAN8651 MAC-PHY 通信,但不能直接把以太网帧丢到 SPI 总线上。原因:

  1. 全双工同步 — SPI 是同步全双工的,TX 和 RX 同时发生,需要协调机制
  2. 帧边界标识 — SPI 是字节流,需要标记以太网帧的起止位置
  3. 流控 — MAC-PHY 内部缓冲区有限,MCU 不能无限制地灌数据
  4. 带外信息 — 需要传递状态(错误、中断、同步状态)而不干扰数据流

Open Alliance TC6 协议解决了这些问题:将以太网帧拆分为固定大小的 chunk,每个 chunk 附加 header(TX)或 footer(RX),在一次 SPI 全双工传输中同时完成数据发送和接收。

SPI 物理层配置

1
2
// src/microchip/TC6_Io.cpp
static SPISettings const LAN865x_SPI_SETTING{24 * 1000 * 1000UL, MSBFIRST, SPI_MODE0};
参数 说明
时钟频率 24 MHz LAN8651 最大支持 25 MHz
位序 MSB First 网络字节序
SPI 模式 Mode 0 (CPOL=0, CPHA=0) 空闲低电平,上升沿采样
CS Active Low 软件控制,每次 transaction 拉低
IRQ Active Low, Falling edge MAC-PHY 请求 MCU 服务
RESET Active Low 硬件复位,100ms 低 + 100ms 恢复

SPI 传输的核心实现极其简洁:

1
2
3
4
5
6
7
8
9
10
11
// src/microchip/TC6_Io.cpp:88-99
bool TC6_Io::spiTransaction(uint8_t const *pTx, uint8_t *pRx, uint16_t const len)
{
digitalWrite(_cs_pin, LOW);
_spi.beginTransaction(LAN865x_SPI_SETTING);
memcpy(pRx, pTx, len); // Arduino SPI.transfer() 是 in-place 的
_spi.transfer(pRx, len); // pRx 同时用作发送和接收缓冲区
_spi.endTransaction();
digitalWrite(_cs_pin, HIGH);
return true;
}

注意这里的 trade-off:memcpy + in-place transfer 比分离的 TX/RX buffer 多一次拷贝,但简化了 Arduino SPI API 的调用。对于 24MHz SPI,64 字节传输约 21μs,memcpy 开销(< 1μs)可忽略。

数据传输帧结构

每次 SPI transaction 传输一个 chunk:4 字节 Header + 64 字节 Payload(TX 方向),同时接收 64 字节 Payload + 4 字节 Footer(RX 方向)。

TX Data Header(32 bits,MCU → MAC-PHY)

1
2
3
4
Byte 0: [DNC(1) | SEQ(1) | NORX(1) | VS(2) | DV(1)  | SV(1) | SWO[3](1)]
Byte 1: [SWO[2:0](3) | -------- | ----- | ------ | ----- | --------]
Byte 2: [EV(1) | EBO[5:0](6) | -----(1) ]
Byte 3: [TSC(2) | -----(5) | P(1) ]
1
2
3
4
5
6
7
8
9
10
// src/microchip/lib/libtc6/src/tc6.cpp:70-80
#define HDR_DNC FLD(0u, 7u, 1u) // Data, Not Control (1=数据帧)
#define HDR_SEQ FLD(0u, 6u, 1u) // 序列号(交替 0/1,用于检测丢失)
#define HDR_DV FLD(1u, 5u, 1u) // Data Valid(payload 包含有效数据)
#define HDR_SV FLD(1u, 4u, 1u) // Start Valid(本 chunk 包含帧起始)
#define HDR_SWO FLD(1u, 0, 4u) // Start Word Offset(帧起始在 payload 中的字偏移)
#define HDR_EV FLD(2u, 6u, 1u) // End Valid(本 chunk 包含帧结束)
#define HDR_EBO FLD(2u, 0u, 6u) // End Byte Offset(帧结束的字节偏移)
#define HDR_TSC FLD(3u, 6u, 2u) // Timestamp Capture(0-3 时间戳通道)
#define HDR_P FLD(3u, 0u, 1u) // Parity(奇校验)

关键字段解读:

字段 功能 值域
DNC 区分数据帧和控制帧 1=数据, 0=控制
SEQ 帧序列号,检测传输丢失 0/1 交替
DV 本 chunk 是否携带有效以太网数据 0=空 chunk(仅为接收服务)
SV+SWO 以太网帧起始位置 SWO 为 4-bit 字偏移 (0-15 words)
EV+EBO 以太网帧结束位置 EBO 为 6-bit 字节偏移 (0-63)

帧拼接机制:一个以太网帧可能跨越多个 chunk。SV 标记起始 chunk,EV 标记结束 chunk,中间的 chunk 只设 DV=1。短帧可以在同一个 chunk 中同时设置 SV 和 EV。

RX Data Footer(32 bits,MAC-PHY → MCU)

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/microchip/lib/libtc6/src/tc6.cpp:102-116
#define FTR_EXST FLD(0u, 7u, 1u) // Extended Status 可用(需要读状态寄存器)
#define FTR_HDRB FLD(0u, 6u, 1u) // TX Header Bad(MCU 发的 header 校验错)
#define FTR_SYNC FLD(0u, 5u, 1u) // 配置同步完成
#define FTR_RCA FLD(0u, 0u, 5u) // Receive Chunks Available(可接收的 chunk 数)
#define FTR_DV FLD(1u, 5u, 1u) // 接收 payload 有效
#define FTR_SV FLD(1u, 4u, 1u) // 接收帧起始
#define FTR_SWO FLD(1u, 0u, 4u) // 起始字偏移
#define FTR_FD FLD(2u, 7u, 1u) // Frame Drop(MAC-PHY 丢帧)
#define FTR_EV FLD(2u, 6u, 1u) // 接收帧结束
#define FTR_EBO FLD(2u, 0u, 6u) // 结束字节偏移
#define FTR_RTSA FLD(3u, 7u, 1u) // RX Timestamp Added
#define FTR_TXC FLD(3u, 1u, 5u) // TX Credits(MCU 还能发多少 chunk)

Footer 中最关键的两个流控字段:

  • TXC (TX Credits) — MAC-PHY 告知 MCU 还有多少 TX buffer 空间,MCU 每发一个有效 chunk 消耗一个 credit
  • RCA (RX Chunks Available) — MAC-PHY 告知有多少 chunk 等待 MCU 读取,MCU 需要发送空 chunk(DV=0)来接收它们

控制传输帧结构

控制帧用于寄存器读写,与数据帧通过 DNC=0 区分:

1
2
3
4
5
6
7
8
9
// src/microchip/lib/libtc6/src/tc6.cpp:86-94
#define HDR_C_DNC FLD(0u, 7u, 1u) // 0 = Control frame
#define HDR_C_WNR FLD(0u, 5u, 1u) // Write Not Read
#define HDR_C_AID FLD(0u, 4u, 1u) // Address Increment Disable
#define HDR_C_MMS FLD(0u, 0u, 4u) // Memory Map Selector (0-15)
#define HDR_C_ADDR_HI FLD(1u, 0u, 8u) // 寄存器地址高 8 位
#define HDR_C_ADDR_LO FLD(2u, 0u, 8u) // 寄存器地址低 8 位
#define HDR_C_LEN FLD(3u, 1u, 7u) // 批量操作长度
#define HDR_C_P FLD(3u, 0u, 1u) // Parity

寄存器地址空间通过 MMS(Memory Map Selector)分区:

MMS 地址空间 用途
0 0x0000xxxx SPI 配置、中断状态
1 0x0001xxxx MAC 寄存器(NETWORK_CONTROL, MAC 地址等)
4 0x0004xxxx PHY 寄存器(PLCA, 碰撞检测, 校准参数)
10 0x000Axxxx 间接访问寄存器(芯片修订, 校准数据)

Secure Mode:控制帧可选启用保护模式,payload 区域同时包含正常数据和反转数据,MAC-PHY 做一致性校验。用于关键寄存器写入(MAC 地址、PLCA 配置等)。

端到端数据流

TX 路径

sequenceDiagram
    participant App as "Arduino App"
    participant UDP as "Arduino_10BASE_T1S_UDP"
    participant lwIP as "lwIP Stack"
    participant TC6 as "libtc6"
    participant IO as "TC6_Io"
    participant HW as "LAN8651"

    App->>UDP: beginPacket() + write() + endPacket()
    UDP->>lwIP: udp_sendto(pbuf)
    lwIP->>lwIP: ARP resolve + IP header
    lwIP->>TC6: lwIpOut(netif, pbuf)
    TC6->>TC6: enqueue to eth_q
    Note over TC6: service() 被调用时
    TC6->>TC6: process_tx(): 从 eth_q 取帧
    TC6->>TC6: mk_data_tx(): 拆分为 64B chunks
    TC6->>TC6: 生成 TX Header (DNC=1, DV=1, SV/EV)
    TC6->>IO: TC6_CB_OnSpiTransaction(pTx, pRx, len)
    IO->>HW: SPI 全双工传输 (68 bytes)
    HW->>HW: MAC 处理 + PHY 发送

RX 路径

sequenceDiagram
    participant HW as "LAN8651"
    participant IO as "TC6_Io"
    participant TC6 as "libtc6"
    participant lwIP as "lwIP Stack"
    participant UDP as "Arduino_10BASE_T1S_UDP"
    participant App as "Arduino App"

    HW->>HW: PHY 接收 + MAC 校验
    HW-->>IO: IRQ 拉低
    IO->>IO: onInterrupt() (ISR, 计数器++)
    Note over IO: service() 检测到中断
    IO->>TC6: TC6_Service()
    TC6->>IO: TC6_CB_OnSpiTransaction(empty_tx, pRx)
    IO->>HW: SPI 传输(发空 chunk 换接收数据)
    TC6->>TC6: process_rx(): 解析 Footer
    TC6->>TC6: 根据 SV/EV/SWO/EBO 重组帧
    TC6->>lwIP: TC6_CB_OnRxEthernetPacket()
    lwIP->>lwIP: ethernet_input() -> ARP/IP
    lwIP->>UDP: udp_recv callback
    UDP->>UDP: 存入 _rx_pkt_list 队列
    App->>UDP: parsePacket() + read()

Service Loop 核心逻辑

service() 是整个协议栈的驱动心跳,必须高频调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/microchip/TC6_Arduino_10BASE_T1S.cpp:205-219
void TC6_Arduino_10BASE_T1S::service()
{
sys_check_timeouts(); // lwIP 定时器(ARP 老化、TCP 重传等)

if (_tc6_io.isInterruptActive())
{
if (TC6_Service(_lw.tc.tc6, false)) // false = 非强制服务
_tc6_io.releaseInterrupt();
}
else if (_lw.tc.tc6NeedService)
{
_lw.tc.tc6NeedService = false;
TC6_Service(_lw.tc.tc6, true); // true = 强制服务(有待发数据)
}
}

TC6_Service() 内部状态机:

  1. Control Phase — 处理挂起的寄存器读写请求
  2. Data Phase — 处理 TX 队列和 RX 接收
  3. Credit 检查 — 只有 TXC > 0 时才发送有效 chunk

Credit 流控机制

Credit 机制防止 MCU 溢出 MAC-PHY 的内部缓冲区:

1
2
3
// src/microchip/lib/libtc6/src/tc6.cpp (TC6_t 结构体)
uint8_t txc; // TX credits,初始值由 MAC-PHY 在首次 Footer 中报告
uint8_t rca; // RX chunks available
graph TD
    A["MCU 发送有效 chunk"] -->|"txc--"| B{"txc > 0?"}
    B -->|"Yes"| C["继续发送"]
    B -->|"No"| D["等待,发空 chunk"]
    D --> E["读取 Footer 中 TXC 字段"]
    E -->|"更新 txc"| A

    F["Footer.RCA > 0"] --> G["MCU 发空 chunk (DV=0)"]
    G --> H["接收 RX payload"]
    H --> I["RCA-- 直到 0"]

空 chunk 的作用:当 MCU 没有数据要发但有数据要收时,发送 DV=0 的空 chunk,SPI 全双工特性保证同时能收到 MAC-PHY 的数据 chunk。这是 OA TC6 协议的关键设计——用”发送换接收”。

PLCA 多点总线管理

PLCA vs CSMA/CD

特性 PLCA CSMA/CD
总线仲裁 确定性轮询 随机退避
最大延迟 可预测(与节点数线性相关) 不确定(碰撞退避指数增长)
需要协调器 是(Node 0)
适合场景 实时控制 低负载通用

配置寄存器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/microchip/lib/libtc6/src/tc6-regs.cpp:414-437
static void HandlePlca(TC6Reg_t *pReg)
{
// 碰撞检测模式
uint32_t regVal = pReg->enablePlca ? 0x0083u : 0x8083u;
TC6_WriteRegister(pReg->pTC6, 0x00040087u /* COL_DET_CTRL0 */, regVal, ...);

if (pReg->enablePlca) {
// Node ID + Node Count
regVal = ((uint32_t)pReg->nodeCount << 8) | pReg->nodeId;
TC6_WriteRegister(pReg->pTC6, 0x0004CA02 /* PLCA_CONTROL_1 */, regVal, ...);

// Burst Count + Burst Timer
regVal = ((uint32_t)pReg->burstCount << 8) | pReg->burstTimer;
TC6_WriteRegister(pReg->pTC6, 0x0004CA05 /* PLCA_BURST_MODE */, regVal, ...);

// Enable PLCA
TC6_WriteRegister(pReg->pTC6, 0x0004CA01 /* PLCA_CONTROL_0 */, (1u << 15), ...);
}
}

PLCA 寄存器映射

寄存器 地址 字段 说明
PLCA_CONTROL_0 0x0004CA01 bit[15] PLCA 使能
PLCA_CONTROL_1 0x0004CA02 [15:8] Node Count, [7:0] Node ID 节点配置
PLCA_STATUS 0x0004CA03 bit[15] PLCA 状态(活跃/回退到 CSMA/CD)
PLCA_BURST_MODE 0x0004CA05 [15:8] Burst Count, [7:0] Burst Timer 突发模式
COL_DET_CTRL0 0x00040087 模式选择 0x0083=PLCA, 0x8083=CSMA/CD

运行时监控:应用代码每秒读取 PLCA_STATUS 寄存器,如果 bit[15]=0 表示 PLCA 失效(协调器离线),自动调用 enablePlca() 重新尝试。

LAN865x 初始化序列

初始化是一个严格有序的多阶段过程:

graph TD
    A["硬件复位 (RESET pin LOW 100ms)"] --> B["软复位 (REG 0x03 = 0x01, unprotected)"]
    B --> C["软复位 (REG 0x03 = 0x01, protected)"]
    C --> D["读取 Chip ID (REG 0x01)"]
    D --> E{"OUI=0x1F0, Model=0x1B?"}
    E -->|"No"| F["初始化失败"]
    E -->|"Yes"| G["读取 Chip Revision (REG 0x000A0094)"]
    G --> H["写入 32 条初始化寄存器 (TC6_MEMMAP)"]
    H --> I["修订相关配置 (Rev1: 0x5F21, Rev2: 0x3F31)"]
    I --> J["写入 MAC 地址 (SPEC_ADD1/2)"]
    J --> K["设置混杂模式 (NETWORK_CONFIG)"]
    K --> L["PHY 校准 (InitChip - 读间接寄存器)"]
    L --> M["PLCA 配置 (HandlePlca)"]
    M --> N["Cut-Through/Store-Forward 模式 (CONFIG0)"]
    N --> O["使能 MAC (NETWORK_CONTROL = 0x0C)"]

初始化寄存器表(部分关键项)

1
2
3
4
5
6
7
8
9
// src/microchip/lib/libtc6/src/tc6-regs.cpp:297-331
static const MemoryMap_t TC6_MEMMAP[] = {
{ .address=0x00000004, .value=0x00000026, .op=MemOp_Write, .secure=false }, // CONFIG0: SPI 配置
{ .address=0x00010000, .value=0x00000000, .op=MemOp_Write, .secure=true }, // NETWORK_CONTROL: MAC 禁用
{ .address=0x00040091, .value=0x00009660, .op=MemOp_Write, .secure=true }, // PHY 配置
// ... 28 条 PHY 校准/配置寄存器 ...
{ .address=0x0000000C, .value=0x00000100, .op=MemOp_Write, .secure=true }, // IMASK0: 中断掩码
{ .address=0x00040081, .value=0x000000E0, .op=MemOp_Write, .secure=true }, // DEEP_SLEEP_CTRL_1
};

Trade-off 分析:初始化使用了大量 “magic number”(如 0x9660, 0x1CF8),这些是 Microchip 的芯片特征化参数,没有公开文档说明含义。移植到竞品芯片时,这整个表需要用竞品 datasheet 的寄存器定义重写。

lwIP 集成架构

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/microchip/TC6_Arduino_10BASE_T1S.cpp:121-194 (begin 方法)
bool TC6_Arduino_10BASE_T1S::begin(...)
{
lwip_init(); // 初始化 lwIP 内核(仅一次)
_lw.tc.tc6 = TC6_Init(&_lw); // 创建 TC6 实例
TC6Regs_Init(_lw.tc.tc6, ...); // 芯片寄存器初始化
while (!TC6Regs_GetInitDone(...)) // 等待初始化完成
TC6_Service(_lw.tc.tc6, true);

netif_add(&_lw.ip.netint, &ip, &mask, &gw,
&_lw, lwIpInit, ethernet_input); // 注册网络接口
netif_set_link_up(&_lw.ip.netint); // 链路上线
}

lwIP 网络接口的两个关键回调:

  • lwIpInit() — 设置 MTU=1536, flags=BROADCAST|ETHARP|ETHERNET, 链接 output 函数
  • lwIpOut() — lwIP 要发帧时调用,将 pbuf 转交 TC6 发送队列

TC6 内部状态机

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/microchip/lib/libtc6/src/tc6.cpp (TC6_t 结构体关键字段)
struct TC6_t {
struct qtxeth_queue eth_q; // 待发以太网帧队列
struct qspibuf_queue qSpi; // SPI 缓冲区队列
struct regop_queue regop_q; // 寄存器操作队列
volatile SpiOp_t currentOp; // 当前 SPI 操作类型
uint8_t seq_num; // 序列号 (0/1 交替)
uint8_t txc; // TX credits
uint8_t rca; // RX chunks available
bool enableData; // 数据通道使能
bool synced; // 与 MAC-PHY 同步
bool eth_started; // 当前帧是否已开始
};

状态转换:INVALID → REG (控制) → DATA (数据) → INVALID (空闲),每次 TC6_Service() 调用驱动一次状态转换。

小结

OA TC6 SPI 协议的核心设计决策:

  1. 固定 68 字节 chunk — 简化 DMA 和缓冲区管理,代价是短帧有 padding 浪费
  2. Footer 内嵌流控 — 零额外开销的信用机制,不需要专门的握手周期
  3. 空 chunk 换接收 — SPI 全双工特性的巧妙利用,简化中断处理
  4. Secure Mode 可选 — 关键寄存器写入双重校验,普通数据不额外开销

对于移植:如果竞品芯片兼容 OA TC6 协议,上述帧结构和流控逻辑可完全复用;如果使用私有 SPI 协议,则需要在 libtc6 这一层重新实现等效功能。