1. 1. 系列导读
  2. 2. 1. libtc6 版本与配置
    1. 2.1. 1.1 版本
    2. 2.2. 1.2 编译时配置(tc6-conf.h)
      1. 2.2.1. 派生宏(tc6-queue.h)
    3. 2.3. 1.3 平台断言
  3. 3. 2. libtc6 公开类型
    1. 3.1. 2.1 不透明结构体 TC6_t
    2. 3.2. 2.2 错误枚举 TC6_Error_t
    3. 3.3. 2.3 寄存器操作描述
    4. 3.4. 2.4 发送段描述
    5. 3.5. 2.5 回调函数类型
  4. 4. 3. libtc6 必实现 API(integrator API)
    1. 4.1. 3.1 实例生命周期
    2. 4.2. 3.2 主循环心跳
    3. 4.3. 3.3 数据通路开关
    4. 4.4. 3.4 发送原始以太网帧
    5. 4.5. 3.5 寄存器访问
    6. 4.6. 3.6 状态查询
    7. 4.7. 3.7 SPI 完成通知(异步支持)
    8. 4.8. 3.8 扩展状态控制
    9. 4.9. 3.9 错误码到恢复动作的映射
  5. 5. 4. libtc6 必实现回调(integrator 必须提供)
    1. 5.1. 4.1 TC6_CB_OnSpiTransaction
    2. 5.2. 4.2 TC6_CB_OnRxEthernetSlice
    3. 5.3. 4.3 TC6_CB_OnRxEthernetPacket
    4. 5.4. 4.4 TC6_CB_OnNeedService
    5. 5.5. 4.5 TC6_CB_OnError
    6. 5.6. 4.6 TC6_CB_OnExtendedStatus
  6. 6. 5. 队列实现:tc6-queue.h
    1. 6.1. 5.1 三个队列
    2. 6.2. 5.2 通用 API(每队列)
    3. 6.3. 5.3 7 阶段 regop_queue 的状态机
    4. 6.4. 5.4 内存布局示例(regop_queue 槽位)
  7. 7. 6. tc6-regs 寄存器层
    1. 7.1. 6.1 API 清单
    2. 7.2. 6.2 必实现回调
    3. 7.3. 6.3 事件枚举 TC6Regs_Event_t
    4. 7.4. 6.4 LAN865x 寄存器初始化表
    5. 7.5. 6.5 芯片 ID 校验
    6. 7.6. 6.6 MAC 地址写入顺序
  8. 8. 7. lwIP 集成层
    1. 8.1. 7.1 关键文件
    2. 8.2. 7.2 netif 三个回调
      1. 8.2.1. lwIpInit(struct netif *netif) —— 初始化回调
      2. 8.2.2. lwIpOut(struct netif *netif, struct pbuf *p) —— 发送回调
      3. 8.2.3. etharp_input —— RX 入口
    3. 8.3. 7.3 命名空间冲突的处理
    4. 8.4. 7.4 sys_now() 的实现
    5. 8.5. 7.5 lwIP 内存配置(lwipopts.h 关键项)
  9. 9. 8. 移植到非 Arduino 平台
    1. 9.1. 8.1 必实现的回调
    2. 9.2. 8.2 必实现的 lwIP 桥接
    3. 9.3. 8.3 必须周期性调用
    4. 9.4. 8.4 替换 tc6-regs.cpp
  10. 10. 9. 错误恢复流程图
  11. 11. 10. 调试日志与可观测性
    1. 11.1. 10.1 启用 libtc6 错误打印
    2. 11.2. 10.2 用 Wireshark 验证数据通路
    3. 11.3. 10.3 添加 RX/TX 计数器
  12. 12. 11. 小结

10BASE-T1S Arduino 代码接口深度解析(三):libtc6 协议核心与 lwIP 集成

系列导读

本系列最后一篇深入到最底层——协议核心 libtc6 的 C API 和 lwIP 集成层:

  1. 应用层 UDP Socket API
  2. PHY Interface 与 HAL 层
  3. libtc6 协议核心与 lwIP 集成(本篇)

目标:让读者在不读 libtc6 源码的情况下,能正确实现新的回调、能替换 tc6-regs.cpp 的寄存器表、能复用到非 Arduino 的 lwIP 平台。

1. libtc6 版本与配置

1.1 版本

1
2
3
4
5
// src/microchip/lib/libtc6/inc/tc6.h:55-58
#define TC6_LIB_VER_MAJOR (3U)
#define TC6_LIB_VER_MINOR (1U)
#define TC6_LIB_VER_BUGFIX (3U)
#define TC6_LIB_VER_STRING "V3.1.3"

1.2 编译时配置(tc6-conf.h

src/microchip/lib/libtc6/cfg-example/tc6-conf.h

默认值 含义 调小影响
TC6_MAX_INSTANCES 1 多 PHY 实例数 仅影响 RAM(每实例队列)
TC6_HEADER_SIZE 4 OA TC6 帧头/尾大小(规范固定) 不可改
TC6_CHUNK_SIZE 64 每个 SPI chunk 的 payload 字节数 32 也合法,但要 TC6_TX_ETH_MAX_SEGMENTS 翻倍
TC6_CHUNK_BUF_SIZE TC6_CHUNK_SIZE + TC6_HEADER_SIZE = 68 单 chunk 的总 SPI 传输长度 派生值
REG_OP_ARRAY_SIZE 4(必须是 2^n) 寄存器操作队列容量 改小可省 RAM,但频繁读写会丢请求
TC6_CHUNKS_XACT 31 单次 SPI 突发最大 chunk 数 改小降低单次中断延迟
TC6_CONCAT_THRESHOLD 1024 超过此大小的帧会跨多个 SPI 突发 一般不改
SPI_FULL_BUFFERS 1 全 MOSI/MISO 缓冲对的数量 影响 SPI 流水深度
TC6_TX_ETH_QSIZE 4 待发以太网帧队列 改小增加丢帧概率
TC6_TX_ETH_MAX_SEGMENTS 8 单帧最多 pbuf 段数 与 lwIP pbuf 配置相关
TC6_MAX_CNTRL_VARS 1 同时挂起的寄存器访问数 改大会增加队列大小

派生宏(tc6-queue.h

1
2
#define TC6_CNTRL_BUF_SIZE  ((2u + (TC6_MAX_CNTRL_VARS * 2u)) * 4u)  // = 12
#define TC6_SPI_BUF_SIZE (TC6_CHUNKS_XACT * TC6_CHUNK_BUF_SIZE) // = 2108

RAM 占用估算(默认配置)

  • 寄存器操作队列:REG_OP_ARRAY_SIZE * sizeof(register_operation) ≈ 4 × ~24B = 96B
  • SPI 缓冲:SPI_FULL_BUFFERS * sizeof(qspibuf) ≈ 1 × (2108+2108+4) ≈ 4.2KB
  • TX 帧队列:TC6_TX_ETH_QSIZE * sizeof(qtxeth) ≈ 4 × ~80B = 320B
  • 每实例约 4.6KB RAM

1.3 平台断言

1
2
3
4
5
6
7
#ifndef TC6_ASSERT
#ifdef DEBUG
#define TC6_ASSERT(condition) __conditional_software_breakpoint(condition)
#else
#define TC6_ASSERT(condition)
#endif
#endif

DEBUG 模式下挂断点,Release 模式下空——可以替换成 assert() 或自定义打印。

2. libtc6 公开类型

2.1 不透明结构体 TC6_t

1
2
3
// src/microchip/lib/libtc6/inc/tc6.h:60-61
struct TC6_t;
typedef struct TC6_t TC6_t;

所有 API 都通过 TC6_t* 操作,调用方不应解引用。这是 libtc6 内部的 struct TC6_ttc6.cpp),包含:

  • eth_q — 待发以太网帧队列
  • qSpi — SPI 全双工缓冲队列
  • regop_q — 寄存器操作 7 阶段队列
  • currentOp — 当前 SPI 操作类型
  • seq_num — chunk 序列号(0/1 交替)
  • txc / rca — TX credit / RX chunk available
  • enableData / synced / eth_started — 状态标志

2.2 错误枚举 TC6_Error_t

1
2
3
4
5
6
7
8
9
10
11
12
13
// tc6.h:63-75
typedef enum {
TC6Error_Succeeded, // 成功
TC6Error_NoHardware, // MISO 数据全 1,表明无硬件
TC6Error_UnexpectedSv, // 异常 Start Valid 标志
TC6Error_UnexpectedDvEv, // 异常 Data/End Valid 标志
TC6Error_BadChecksum, // Footer 校验错
TC6Error_UnexpectedCtrl, // 收到非预期的控制包
TC6Error_BadTxData, // Header Bad 标志
TC6Error_SyncLost, // Sync 标志丢失
TC6Error_SpiError, // SPI 传输失败
TC6Error_ControlTxFail, // 控制传输失败
} TC6_Error_t;

2.3 寄存器操作描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// tc6.h:107-122
typedef enum {
MemOp_Write = 0,
MemOp_ReadModifyWrite = 1,
MemOp_Read = 2
} MemoryOp_t;

typedef struct {
uint32_t address;
uint32_t value;
uint32_t mask; // 仅 ReadModifyWrite 用
MemoryOp_t op;
bool secure; // true = 启用 secure mode(normal + 反转)
} MemoryMap_t;

2.4 发送段描述

1
2
3
4
5
// tc6.h:77-81
typedef struct {
const uint8_t *pEth; // 段数据指针(无需拷贝,libtc6 用到 SpiBufferDone 才释放)
uint16_t segLen; // 段字节数
} TC6_RawTxSegment;

2.5 回调函数类型

1
2
3
4
5
// tc6.h:92, 105
typedef void (*TC6_RawTxCallback_t)(TC6_t *pInst, const uint8_t *pTx, uint16_t len,
void *pTag, void *pGlobalTag);
typedef void (*TC6_RegCallback_t)(TC6_t *pInst, bool success, uint32_t addr,
uint32_t value, void *pTag, void *pGlobalTag);

两个回调的语义相似:

  • pInst — TC6 实例指针(多实例时区分用)
  • pTag — 调用 API 时传入的 tag(per-request)
  • pGlobalTagTC6_Init 时传入的全局 tag(per-instance)

3. libtc6 必实现 API(integrator API)

3.1 实例生命周期

API 说明
TC6_t *TC6_Init(void *pGlobalTag) 创建 TC6 实例,返回不透明指针;失败返回 NULL
void TC6_Destroy(TC6_t *pInst) 销毁实例,释放内部所有队列
void TC6_Reset(TC6_t *pInst) 仅重置状态机/队列,不动 PHY 寄存器

3.2 主循环心跳

1
bool TC6_Service(TC6_t *pInst, bool interruptLevel);
参数 含义
pInst TC6 实例
interruptLevel IRQ 引脚电平
false = IRQ active(IRQ 触发)
true = IRQ inactive
返回 含义
true 所有 pending 工作完成(可释放 IRQ)
false 仍有待处理,但当前无法继续(需等 SPI 完成或下次 IRQ)

调用时机(两者之一即可):

  1. TC6_CB_OnNeedService() 被回调时(任务上下文)
  2. 物理 IRQ 触发时(中断上下文外,必须 defer 到任务)

当前实现TC6_Arduino_10BASE_T1S::service()):

  • 中断路径(_int_in != _int_out):TC6_Service(tc6, false)
  • 非中断路径(tc6NeedService 标志):TC6_Service(tc6, true)

3.3 数据通路开关

1
void TC6_EnableData(TC6_t *pInst, bool enable);
  • enable=true:允许数据帧收发(控制帧仍可用)
  • enable=false:仅允许控制帧(用于初始化阶段)

当前用法:库内部不在 begin() 后显式调用,但 tc6-regs.cpp 完成全部初始化后会通过 NETWORK_CONTROL = 0x0C 隐式启用数据通路。

3.4 发送原始以太网帧

1
2
3
bool TC6_SendRawEthernetPacket(TC6_t *pInst, const uint8_t *pTx, uint16_t len,
uint8_t tsc,
TC6_RawTxCallback_t txCallback, void *pTag);
参数 含义
pTx 整帧数据指针,必须保持有效直到 txCallback 被调用
len 帧字节数(14 字节以太网头 + 上层负载)
tsc Timestamp Capture 通道选择:
0 = 不捕获
1/2/3 = 捕获到对应 TTSCAx 寄存器
txCallback 发送完成回调(可 NULL)
pTag 透传给回调的标签

当前用法TC6_Arduino_10BASE_T1S.cpp 不使用这个 API,而是用 TC6_SendRawEthernetSegments(因为 lwIP 的 pbuf 是链表)。

1
2
3
4
uint8_t TC6_GetRawSegments(TC6_t *pInst, TC6_RawTxSegment **pSegments);
bool TC6_SendRawEthernetSegments(TC6_t *pInst, const TC6_RawTxSegment *pSegments,
uint8_t segmentCount, uint16_t totalLen, uint8_t tsc,
TC6_RawTxCallback_t txCallback, void *pTag);
  • TC6_GetRawSegments 返回可用 segment 数组指针(通过出参)
  • pSegments 中每个段描述一个 pbuf 段
  • callback 的 pTx 指向第一个段(不是整个帧)——这是 Arduino 集成代码中需要 pbuf_free(p) 的依据

3.5 寄存器访问

1
2
3
4
5
6
7
8
9
10
bool TC6_ReadRegister(TC6_t *pInst, uint32_t addr, bool secure,
TC6_RegCallback_t rxCallback, void *pTag);
bool TC6_WriteRegister(TC6_t *pInst, uint32_t addr, uint32_t value, bool secure,
TC6_RegCallback_t txCallback, void *pTag);
bool TC6_ReadModifyWriteRegister(TC6_t *pInst, uint32_t addr, uint32_t value,
uint32_t mask, bool secure,
TC6_RegCallback_t modifyCallback, void *pTag);
uint16_t TC6_MultipleRegisterAccess(TC6_t *pInst, const MemoryMap_t *pMap,
uint16_t mapLength,
TC6_RegCallback_t multipleCallback, void *pTag);
参数 说明
addr 寄存器地址(32 位,含 MMS 字段)
secure true 启用保护模式(payload 同时含 normal + 反转数据,MAC-PHY 校验)
*Callback 完成回调(可 NULL)
pTag 透传标签

TC6_MultipleRegisterAccess 返回成功入队的条目数(可能少于 mapLength,队列满时)。

3.6 状态查询

1
2
void TC6_GetState(TC6_t *pInst, uint8_t *pTxCredit, uint8_t *pRxCredit, bool *pSynced);
uint8_t TC6_GetInstance(TC6_t *pInst);
  • TC6_GetState 把当前 TX credit / RX chunk available / sync 状态写入输出参数
  • TC6_GetInstance 返回实例编号(0..N-1)

3.7 SPI 完成通知(异步支持)

1
void TC6_SpiBufferDone(TC6_t *pInst, bool success);

TC6_CB_OnSpiTransaction 内部完成 SPI 后必须调用此函数通知 libtc6。

当前实现TC6_Arduino_10BASE_T1S.cpp:386-395 同步 SPI 后立即调用。

如果要把 HAL 改成 DMA + 异步:

1
2
3
4
5
6
7
8
9
10
bool TC6_CB_OnSpiTransaction(TC6_t *pInst, uint8_t *pTx, uint8_t *pRx, uint16_t len, void *g) {
// 排队到 DMA,enqueue 立即返回 true
dma_enqueue(pInst, pTx, pRx, len);
return true;
}

// DMA 完成 ISR
void dma_done_isr(TC6_t *pInst, bool success) {
TC6_SpiBufferDone(pInst, success);
}

3.8 扩展状态控制

1
void TC6_UnlockExtendedStatus(TC6_t *pInst);
  • Footer.EXST=1 时 libtc6 调用 TC6_CB_OnExtendedStatus
  • 用户应在读 STATUS0/STATUS1 寄存器后主动调用此函数解锁下一个扩展状态事件
  • 防止高流量时产生大量回调

3.9 错误码到恢复动作的映射

错误 库默认恢复 实际代码位置
TC6Error_NoHardware TC6Regs_Reinit TC6_Arduino_10BASE_T1S.cpp:505
TC6Error_BadChecksum TC6Regs_Reinit :515
TC6Error_UnexpectedCtrl TC6Regs_Reinit :519
TC6Error_BadTxData TC6Regs_Reinit :523
TC6Error_SyncLost TC6Regs_Reinit :527
TC6Error_SpiError TC6Regs_Reinit :531
其他 仅日志

4. libtc6 必实现回调(integrator 必须提供)

4.1 TC6_CB_OnSpiTransaction

1
2
extern bool TC6_CB_OnSpiTransaction(TC6_t *pInst, uint8_t *pTx, uint8_t *pRx,
uint16_t len, void *pGlobalTag);

契约

说明
pTx libtc6 提供的 MOSI 数据,指针在 TC6_SpiBufferDone 前必须有效
pRx libtc6 提供的 MISO 缓冲,同样必须保持有效
len 字节数(必须全部传输)
pGlobalTag TC6_Init 时传入的 tag
返回 true = SPI 数据已成功 enqueue/transfered;false = 错误
警告 实现中不要调用 TC6_Service()——会导致重入

实现TC6_Arduino_10BASE_T1S.cpp:386-395):

1
2
3
4
5
6
7
8
bool TC6_CB_OnSpiTransaction(TC6_t *pInst, uint8_t *pTx, uint8_t *pRx, uint16_t len, void *pGlobalTag)
{
TC6LwIP_t *lw = TC6::GetContextTC6(pInst);
if (lw == nullptr) return false;
bool const success = lw->io->spiTransaction(pTx, pRx, len);
TC6_SpiBufferDone(pInst, success);
return success;
}

4.2 TC6_CB_OnRxEthernetSlice

1
2
extern void TC6_CB_OnRxEthernetSlice(TC6_t *pInst, const uint8_t *pRx,
uint16_t offset, uint16_t len, void *pGlobalTag);

契约

说明
pRx 当前 chunk 中的以太网数据(不含 header/footer)
offset 在最终帧中的字节偏移(首段为 0)
len 当前段字节数
上下文 任务上下文(非 ISR)
多段 一个以太网帧会被拆成多个 chunk → 多次调用 OnRxEthernetSlice,最后一次伴随 OnRxEthernetPacket

实现要点

  • TC6_Arduino_10BASE_T1S.cpp:397-430 把所有 chunk 累积到一个 1536 字节 pbuf
  • 首次调用时分配 pbuf_alloc(PBUF_RAW, MTU, PBUF_RAM)
  • 校验 pbuf_alloc 返回单段pbuf->next == NULL),失败标记 rxInvalid
  • 长度超过 MTU 也标记 rxInvalid

4.3 TC6_CB_OnRxEthernetPacket

1
2
extern void TC6_CB_OnRxEthernetPacket(TC6_t *pInst, bool success, uint16_t len,
uint64_t *rxTimestamp, void *pGlobalTag);

契约

说明
success 整个帧是否成功接收
len 整个帧的字节数(与之前所有 slice 的 len 之和一致)
rxTimestamp 接收时间戳指针(如果存在),回调返回后失效

实现TC6_Arduino_10BASE_T1S.cpp:432-491):

  • 校验长度 ≥ 42 (MIN_HEADER_LEN = 42)——最小 ARP 包大小
  • 解析 EtherType(eth_hdr->type),调用 FilterRxEthernetPacket 决定是否送入 lwIP
  • 0x0800 (IPv4) 和 0x0806 (ARP) 走 netif->input(pbuf, netif)
  • 失败路径只释放本地 pbuf,返回给 lwIP

4.4 TC6_CB_OnNeedService

1
extern void TC6_CB_OnNeedService(TC6_t *pInst, void *pGlobalTag);

契约

说明
触发条件 libtc6 内部有 pending 工作(典型:TX 队列待发)
实现要求 必须非阻塞,仅置标志/事件,不要直接调用 TC6_Service
警告 可能从 ISR 上下文调用

实现TC6_Arduino_10BASE_T1S.cpp:375-379):

1
2
3
4
void TC6_CB_OnNeedService(TC6_t *pInst, void *pGlobalTag) {
TC6LwIP_t *lw = TC6::GetContextTC6(pInst);
lw->tc.tc6NeedService = true;
}

只是一个 bool 写入,线程安全(在单核 Cortex-M 上原子)。

4.5 TC6_CB_OnError

1
extern void TC6_CB_OnError(TC6_t *pInst, TC6_Error_t err, void *pGlobalTag);

实现 见 3.9 表格中的恢复动作。

4.6 TC6_CB_OnExtendedStatus

1
extern void TC6_CB_OnExtendedStatus(TC6_t *pInst, void *pGlobalTag);

契约

说明
触发 Footer 中 EXST=1
实现要求 读 STATUS0/STATUS1 寄存器,调用 TC6_UnlockExtendedStatus 解锁下一个事件
重要 解锁过早会导致高流量下大量回调(每秒数千次)

当前实现tc6-regs.cpp 实现了这个回调(在 tc6-regs.h 声明 void TC6_CB_OnExtendedStatus(...)),不需要 Arduino 集成层重复实现。

5. 队列实现:tc6-queue.h

src/microchip/lib/libtc6/src/tc6-queue.h 是一个自动生成的多阶段无锁环形队列。

5.1 三个队列

队列 结构 阶段数 大小
qtxeth_queue 待发以太网帧 2 (enqueue → convert) TC6_TX_ETH_QSIZE = 4
qspibuf_queue SPI 全双工缓冲 3 (transfer → int → process) SPI_FULL_BUFFERS = 1
regop_queue 寄存器操作 7 (enqueue → send → int → modify → send → int → event) REG_OP_ARRAY_SIZE = 4

5.2 通用 API(每队列)

1
2
3
4
5
6
init_<name>_queue(q, buffer, size)
<name>_stage<N>_<role>_ready(q) // 是否有空间
<name>_stage<N>_<role>_ptr(q) // 取槽位指针
<name>_stage<N>_<role>_done(q) // 完成本阶段
<name>_stage<N>_<role>_undo(q) // 撤销本阶段
<name>_stage<N>_<role>_cap(q) // 当前可用容量

环形索引

1
return &q->buffer_[(q->stageN_<role>_ & (q->size_ - 1u))];  // size_ 必须是 2 的幂

判空/判满

1
ready:   ((stage - ref) < size)

5.3 7 阶段 regop_queue 的状态机

1
2
3
4
5
6
7
8
enqueue (1) → send (2) → int (3) → modify (4) → send (5) → int (6) → event (7)
│ │
│ TC6_ReadRegister 入队 │ 用户回调被调用
│ │
└─ SPI 写入完成 ────────────────────────────────────────────┘
↑ ↑
Read-Modify-Write:
阶段 2-3 是读阶段,4-7 是改+回写阶段

ReadModifyWriteRegistermodifyValue/modifyMask 在阶段 4 被应用,然后阶段 5 写入新值。

5.4 内存布局示例(regop_queue 槽位)

1
2
3
4
5
6
7
8
9
10
struct register_operation {
uint8_t tx_buf[TC6_CNTRL_BUF_SIZE]; // = 12
uint8_t rx_buf[TC6_CNTRL_BUF_SIZE]; // = 12
TC6_RegCallback_t callback;
void *tag;
enum register_op_type op;
uint32_t modifyValue, modifyMask, regAddr;
uint16_t length;
bool secure;
};

每个槽约 ~40-50 字节,4 个槽 = ~200 字节 RAM。

6. tc6-regs 寄存器层

6.1 API 清单

src/microchip/lib/libtc6/inc/tc6-regs.h

API 说明
bool TC6Regs_Init(TC6_t*, void *pTag, const uint8_t mac[6], bool enablePlca, uint8_t nodeId, uint8_t nodeCount, uint8_t burstCount, uint8_t burstTimer, bool promiscuous, bool txCutThrough, bool rxCutThrough) 完整初始化(阻塞,内部用 while + TC6_Service 轮询)
bool TC6Regs_GetInitDone(TC6_t*) 查询初始化是否完成
void TC6Regs_Reinit(TC6_t*) 重大错误后重新初始化
bool TC6Regs_SetPlca(TC6_t*, bool enable, uint8_t nodeId, uint8_t nodeCount) 运行时修改 PLCA
uint8_t TC6Regs_GetChipRevision(TC6_t*) 读芯片 revision
void TC6Regs_EnableDio_A0/A1(TC6_t*) 配置 LAN8651 GPIO 为输出
void TC6Regs_ToggleDio_A0/A1(TC6_t*) 翻转 GPIO
void TC6Regs_CheckTimers(void) 内部超时检查(必须定期调用
void TC6_CB_OnExtendedStatus(TC6_t*, void*) 实现 libtc6 回调

6.2 必实现回调

1
2
uint32_t TC6Regs_CB_GetTicksMs(void);  // 返回当前毫秒 tick
void TC6Regs_CB_OnEvent(TC6_t*, TC6Regs_Event_t event, void *pTag);

实现TC6_Arduino_10BASE_T1S.cpp:381):

1
uint32_t TC6Regs_CB_GetTicksMs(void) { return millis(); }

6.3 事件枚举 TC6Regs_Event_t

35 种事件(tc6-regs.h:47-85),分两类:

颜色 事件类型
绿色 信息性(Reset_Complete、PHY_Interrupt、TX_Timestamp_Available_*)
红色 错误(FSM_State_Error、SRAM_ECC_Error、Undervoltage、Chip_Error 等)
黄色 中间状态(MCLK_GEN_Status、SPI_Err_Int 等)

触发自动 Reinit 的事件TC6_Arduino_10BASE_T1S.cpp:540-666):

  • Loss_of_Framing_Error
  • RX_Non_Recoverable_Error
  • TX_Non_Recoverable_Error

6.4 LAN865x 寄存器初始化表

src/microchip/lib/libtc6/src/tc6-regs.cpp:297-331

1
2
3
4
5
6
7
8
static const MemoryMap_t TC6_MEMMAP[] = {
{ .address=0x00000004, .value=0x00000026, .op=MemOp_Write, .secure=false }, // CONFIG0
{ .address=0x00010000, .value=0x00000000, .op=MemOp_Write, .secure=true }, // NETWORK_CONTROL
{ .address=0x00040091, .value=0x00009660, .op=MemOp_Write, .secure=true }, // PHY
// ... 30+ 条 PHY 校准/配置寄存器
{ .address=0x0000000C, .value=0x00000100, .op=MemOp_Write, .secure=true }, // IMASK0
{ .address=0x00040081, .value=0x000000E0, .op=MemOp_Write, .secure=true }, // DEEP_SLEEP_CTRL_1
};

关键地址汇总(移植到竞品时需要替换):

地址 MMS 寄存器名 用途
0x00000004 0 CONFIG0 SPI 块大小、cut-through
0x0000000C 0 IMASK0 中断屏蔽
0x00010000 1 NETWORK_CONTROL MAC 使能
0x00010001 1 NETWORK_CONFIG 混杂模式
0x00010022/24/25 1 SPEC_ADD1_BOTTOM / SPEC_ADD2_BOTTOM/TOP MAC 地址
0x00040081 4 DEEP_SLEEP_CTRL_1 低功耗
0x00040087 4 COL_DET_CTRL0 碰撞检测模式(PLCA / CSMA-CD)
0x00040091 4 PHY 配置 PHY 基础
0x000400B0..BB 4 PHY 校准区 模拟参数(Microchip magic numbers)
0x000400D0 4 revision-specific Rev1/2 不同
0x0004CA01 4 PLCA_CONTROL_0 PLCA 使能
0x0004CA02 4 PLCA_CONTROL_1 Node ID/Count
0x0004CA03 4 PLCA_STATUS 状态读取
0x0004CA05 4 PLCA_BURST_MODE Burst Count/Timer
0x000A0094 10 间接:Chip Revision 修订版本

6.5 芯片 ID 校验

1
2
3
4
5
6
7
8
9
10
// tc6-regs.cpp:451-467
void OnReadId1(TC6_t *pInst, bool success, uint32_t addr, uint32_t value, void *pTag, void *pGlobalTag) {
uint32_t oui = value >> 10;
uint32_t model = (value >> 4) & 0x3FFu;
if ((0x1F0u != oui) || (0x1Bu != model)) {
// 不是 LAN8651
TC6Regs_CB_OnEvent(pInst, TC6Regs_Event_Unsupported_Hardware, pReg->pTag);
pReg->initialized = false;
}
}

移植时:替换为竞品的 OUI/Model 期望值。

6.6 MAC 地址写入顺序

tc6-regs.cpp:371-389

1
2
3
4
5
6
regVal = ((mac[3] << 24) | (mac[2] << 16) | (mac[1] << 8) | mac[0]);
TC6_WriteRegister(... 0x00010024 /* SPEC_ADD2_BOTTOM */, regVal, true);
regVal = ((mac[5] << 8) | mac[4]);
TC6_WriteRegister(... 0x00010025 /* SPEC_ADD2_TOP */, regVal, true);
regVal = ((mac[5] << 24) | (mac[4] << 16) | (mac[3] << 8) | mac[2]);
TC6_WriteRegister(... 0x00010022 /* SPEC_ADD1_BOTTOM */, regVal, true);

字节序反转:MAC 地址 [AA:BB:CC:DD:EE:FF] 写入顺序为:

  • SPEC_ADD2_BOTTOM = DDCCBBAA(小端)
  • SPEC_ADD2_TOP = FFEE
  • SPEC_ADD1_BOTTOM = FFEEDDCC(用于生成 back-off time)

这是 LAN8651 datasheet 规定的写入方式,竞品芯片不一定遵循

7. lwIP 集成层

7.1 关键文件

  • src/microchip/TC6_Arduino_10BASE_T1S.cpp — 集成代码(~700 行)
  • src/lib/liblwip/cfg/lwipopts.h — 编译配置
  • src/lib/lwip_sys_now.cppsys_now() 桥接到 millis()

7.2 netif 三个回调

TC6_Arduino_10BASE_T1S.cpp:282-346

lwIpInit(struct netif *netif) —— 初始化回调

配置
netif->output etharp_output(lwIP 标准 ARP 处理)
netif->linkoutput lwIpOut(实际发送回调)
netif->flags `NETIF_FLAG_BROADCAST
netif->mtu 1536(与 UdpRxPacket 容量匹配)
netif->hwaddr_len ETHARP_HWADDR_LEN = 6
netif->name "tc"(前 2 字符)
副作用 netif_set_up() + netif_set_default()

Context 解析:通过 GetContextNetif(netif) 遍历全局链表找到 TC6LwIP_t*遍历开销 O(n)——单实例不影响,多实例需要哈希表。

lwIpOut(struct netif *netif, struct pbuf *p) —— 发送回调

调用路径:

  1. udp_sendto → lwIP 内部 → etharp_outputnetif->linkoutput = lwIpOut
  2. TC6_GetRawSegments(tc6, &txSeg) 申请 segment 数组
  3. 把 pbuf 链表每个段填入 segment 数组(p->len, p->next
  4. TC6_SendRawEthernetSegments(tc6, txSeg, seg, p->tot_len, 0, callback, p) 提交
  5. callback 释放 pbuf

关键引用计数:第 305 行 pbuf_ref(p)——为什么?

TC6_SendRawEthernetSegments 路径中,MAC-PHY 异步发送完成前,pbuf 必须保持有效。但 lwIP 内部会在 udp_sendto 返回后立刻 pbuf_free(p)pbuf_ref 增加引用计数,使得后续 pbuf_free 只减少引用,最后由 libtc6 TX 完成的 callback pbuf_free 真正释放。

返回码

  • ERR_OK — 发送已入队(实际发送异步完成)
  • ERR_WOULDBLOCK — TX 队列满,无可用 segment
  • ERR_IF — TC6 拒绝(极少见)

etharp_input —— RX 入口

由 lwIP 内部通过 netif->input(pbuf, netif) 调用,即 TC6_Arduino_10BASE_T1S::lwIpOut 的反向路径。

EtherType 过滤:358-373):

1
2
3
4
5
6
7
8
bool FilterRxEthernetPacket(uint16_t ethType) {
switch (ethType) {
case 0x0800: // IPv4 → 走 lwIP
case 0x0806: // ARP → 走 lwIP
return true;
}
return false;
}

其他 EtherType(如 0x88A8 服务 VLAN tag、0x88CC LLDP、IPv6)的帧被静默丢弃

7.3 命名空间冲突的处理

src/lib/liblwip/cfg/lwipopts.h:590-742 通过 #define xxx t1s_xxx 重命名所有 lwIP 符号:

1
2
3
4
#define sys_now t1s_sys_now
#define etharp_output t1s_etharp_output
#define udp_sendto t1s_udp_sendto
// ... 100+ 项

为什么需要重命名?

  1. lwIP 内部函数会与 Arduino 核心库同名(例如 sys_now
  2. 库的多个 cpp 文件都会 include lwIP 头,重命名防止链接时符号冲突
  3. src/lib/lwip_sys_now.cpp:32 实现的是 extern "C" u32_t sys_now(void) { return millis(); },但因为 #define sys_now t1s_sys_now,实际编译出的是 t1s_sys_now——这就是为什么重命名列表里有 sys_now

7.4 sys_now() 的实现

1
2
3
4
// src/lib/lwip_sys_now.cpp:32
extern "C" u32_t sys_now(void) {
return millis();
}

注意:因为重命名,链接器看到的实际符号是 t1s_sys_now
lwIP 用它实现所有超时(ARP 老化、TCP 重传等),所以 service() 必须周期性调用 sys_check_timeouts()(实际由 t1s_Arduino_10BASE_T1S::service() 调用)。

7.5 lwIP 内存配置(lwipopts.h 关键项)

含义
NO_SYS 1 裸机模式,无 RTOS
SYS_LIGHTWEIGHT_PROT 0 无并发保护(依赖 IRQ 禁用临界区)
MEM_LIBC_MALLOC 1 使用 libc malloc/free(不维护 lwIP 私有 heap)
MEM_SIZE 0 私有 heap 大小(用 libc malloc 时无效)
LWIP_TCP 0 禁用 TCP(库当前只支持 UDP)
LWIP_UDP 1 启用 UDP
LWIP_DHCP 0 禁用 DHCP(静态 IP)
LWIP_AUTOIP 0 禁用 AutoIP
LWIP_IGMP 0 禁用多播
LWIP_DNS 0 禁用 DNS(解释 beginPacket 域名版返回 0)
LWIP_ICMP 1 启用 ICMP(让 ping 工作)
LWIP_RAW 1 启用 RAW PCB(可能未使用)
MEMP_NUM_PBUF 20 pbuf 池
MEMP_NUM_UDP_PCB 4 UDP 控制块数(最多 4 个并发 UDP socket)
MEMP_NUM_TCP_PCB 0 TCP 控制块数
MEMP_NUM_ARP_QUEUE 6 ARP 请求队列
PBUF_POOL_SIZE 1 PBUF_POOL 类型池大小
PBUF_LINK_HLEN 14 链路头长度(标准以太网)
CHECKSUM_CHECK_IP/UDP/TCP 0 禁用校验和验证(PHY 已做)
ETHERNET_MTU 1500 标准 MTU

实际 MTU 1536 vs 1500netif.mtu = 1536(在 TC6_Arduino_10BASE_T1S.cpp:288),但 lwIP ETHERNET_MTU = 1500。lwIP 在发送路径会用 min(netif.mtu, ETHERNET_MTU),所以实际 MTU 是 15001536 是给 RX pbuf 分配的容量(容纳 VLAN tag 等扩展)。

8. 移植到非 Arduino 平台

把 libtc6 移植到非 Arduino 平台(例如 RTOS + 通用 lwIP)需要提供:

8.1 必实现的回调

1
2
3
4
5
6
7
8
bool TC6_CB_OnSpiTransaction(...);  // 平台 SPI
void TC6_CB_OnRxEthernetSlice(...);
void TC6_CB_OnRxEthernetPacket(...);
void TC6_CB_OnNeedService(...);
void TC6_CB_OnError(...);
void TC6_CB_OnExtendedStatus(...); // 或用 tc6-regs 实现
uint32_t TC6Regs_CB_GetTicksMs(void);
void TC6Regs_CB_OnEvent(...);

8.2 必实现的 lwIP 桥接

1
2
3
u32_t sys_now(void);                    // → 平台 tick
struct netif *netif_default; // 已注册的网络接口
// netif_add(...) 已在 TC6_Arduino_10BASE_T1S::begin() 中完成

8.3 必须周期性调用

1
2
3
TC6_Service(tc6, /*IRQ 电平*/);
TC6Regs_CheckTimers();
sys_check_timeouts(); // lwIP

调用频率:建议 1ms 或更快。10BASE-T1S 64-byte chunk 在 24MHz SPI 下约 21μs 传输,每秒可处理 ~47000 chunks。

8.4 替换 tc6-regs.cpp

tc6-regs.cppMicrochip 专有 的——所有寄存器地址、值、magic numbers 都是 LAN8651 特有。移植到竞品:

  1. 保留 tc6-regs.h 的 API(TC6Regs_Init/Reinit/SetPlca/...
  2. 重写 tc6-regs.cpp
    • 替换 TC6_MEMMAP[] 为竞品的初始化序列
    • 替换 OnReadId1/2 的 OUI/Model 校验
    • 替换 HandlePlca 中的寄存器地址(如果竞品 PLCA 寄存器布局不同)
    • 替换 InitChip(PHY 校准间接寄存器读取公式)
  3. 如果竞品使用不同地址空间分布,调整 MMS 字段

9. 错误恢复流程图

graph TD
    Start([PHY service loop])
    Start --> SysTimeout[sys_check_timeouts]
    SysTimeout --> IrqActive{IRQ active?}
    IrqActive -- yes --> TC6SrvFalse["TC6_Service(tc6, false)"]
    IrqActive -- no --> NeedService{tc6NeedService?}
    NeedService -- yes --> TC6SrvTrue["TC6_Service(tc6, true)"]
    NeedService -- no --> CheckTimer[TC6Regs_CheckTimers]
    TC6SrvFalse --> Done{return true?}
    Done -- yes --> ReleaseIRQ[TC6_Io::releaseInterrupt]
    Done -- no --> CheckTimer
    TC6SrvTrue --> CheckTimer
    ReleaseIRQ --> CheckTimer
    CheckTimer --> Start
    
    subgraph AsyncError[Async error path]
        ErrCB[TC6_CB_OnError]
        ErrCB --> ErrType{Error type}
        ErrType -- severe --> Reinit["TC6Regs_Reinit()<br/>(re-download all regs)"]
        ErrType -- data error --> LogOnly[log only]
    end

10. 调试日志与可观测性

10.1 启用 libtc6 错误打印

修改 src/microchip/TC6_Arduino_10BASE_T1S.cpp:493

1
2
3
4
5
6
// #define PRINT(...)   // 默认空
#define PRINT(...) Serial.printf(__VA_ARGS__)
#define ESC_GREEN ""
#define ESC_RED ""
#define ESC_YELLOW ""
#define ESC_RESETCOLOR ""

重新编译,串口会看到所有 TC6Error_*TC6Regs_Event_*

10.2 用 Wireshark 验证数据通路

LAN8651 收到帧 → RX pbuf → FilterRxEthernetPacketnetif->inputudp_recvArduino_10BASE_T1S_UDP::_rx_pkt_listlwIP 本身不输出任何日志LWIP_STATS = 0)。

要在 lwIP 层加观测,可以临时打开:

1
2
#define LWIP_STATS 1
#define UDP_STATS 1

但这会显著增加 RAM,需要重新评估 UNO R4 Minima 的 32KB 限制。

10.3 添加 RX/TX 计数器

最简单的方法是在 TC6_CB_OnRxEthernetPacketlwIpOut 里加 static 计数器,周期性打印:

1
2
3
4
5
6
7
8
9
10
11
// 临时调试代码
static uint32_t rx_count = 0, tx_count = 0;
void TC6_CB_OnRxEthernetPacket(TC6_t *pInst, bool success, uint16_t len, ...) {
if (success) rx_count++;
// ... 原有实现
}

static err_t lwIpOut(struct netif *netif, struct pbuf *p) {
// ... 原有实现
tx_count++;
}

11. 小结

libtc6 作为一个 OA TC6 协议参考实现,展示了协议层 + 协议无关适配的清晰分层:

模块 替换边界
应用层 Arduino_10BASE_T1S_UDP 不依赖硬件,可保留
网络层 lwIP 不依赖硬件,可保留
协议适配层 TC6_Arduino_10BASE_T1S 网桥作用,替换 HAL/regs
协议核心 libtc6 (tc6.cpp) OA TC6 规范相关,只在协议兼容时复用
寄存器层 tc6-regs.cpp 完全 chip-specific,必须重写
HAL TC6_Io 平台 SPI 相关,可适配

整个库的复用价值集中在应用层 + lwIP;移植工作量集中在 tc6-regs.cpp 重写和 HAL 适配。

至此本系列三篇结束。读者现在应能:

  • 在不读源码的前提下调用 Arduino UDP API 并排查错误
  • 替换 HAL 实现而无需修改 libtc6
  • 实现 TC6_Arduino_10BASE_T1S 的等价类对接新芯片
  • 完整替换 tc6-regs.cpp 移植到竞品 MAC-PHY