系列导读
本系列最后一篇深入到最底层——协议核心 libtc6 的 C API 和 lwIP 集成层:
- 应用层 UDP Socket API
- PHY Interface 与 HAL 层
- libtc6 协议核心与 lwIP 集成(本篇)
目标:让读者在不读 libtc6 源码的情况下,能正确实现新的回调、能替换 tc6-regs.cpp 的寄存器表、能复用到非 Arduino 的 lwIP 平台。
1. libtc6 版本与配置
1.1 版本
1 | // src/microchip/lib/libtc6/inc/tc6.h:55-58 |
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 |
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 |
DEBUG 模式下挂断点,Release 模式下空——可以替换成 assert() 或自定义打印。
2. libtc6 公开类型
2.1 不透明结构体 TC6_t
1 | // src/microchip/lib/libtc6/inc/tc6.h:60-61 |
所有 API 都通过 TC6_t* 操作,调用方不应解引用。这是 libtc6 内部的 struct TC6_t(tc6.cpp),包含:
eth_q— 待发以太网帧队列qSpi— SPI 全双工缓冲队列regop_q— 寄存器操作 7 阶段队列currentOp— 当前 SPI 操作类型seq_num— chunk 序列号(0/1 交替)txc/rca— TX credit / RX chunk availableenableData/synced/eth_started— 状态标志
2.2 错误枚举 TC6_Error_t
1 | // tc6.h:63-75 |
2.3 寄存器操作描述
1 | // tc6.h:107-122 |
2.4 发送段描述
1 | // tc6.h:77-81 |
2.5 回调函数类型
1 | // tc6.h:92, 105 |
两个回调的语义相似:
pInst— TC6 实例指针(多实例时区分用)pTag— 调用 API 时传入的 tag(per-request)pGlobalTag—TC6_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) |
调用时机(两者之一即可):
TC6_CB_OnNeedService()被回调时(任务上下文)- 物理 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 | bool TC6_SendRawEthernetPacket(TC6_t *pInst, const uint8_t *pTx, uint16_t len, |
| 参数 | 含义 |
|---|---|
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 | uint8_t TC6_GetRawSegments(TC6_t *pInst, TC6_RawTxSegment **pSegments); |
TC6_GetRawSegments返回可用 segment 数组指针(通过出参)pSegments中每个段描述一个 pbuf 段- callback 的
pTx指向第一个段(不是整个帧)——这是 Arduino 集成代码中需要pbuf_free(p)的依据
3.5 寄存器访问
1 | bool TC6_ReadRegister(TC6_t *pInst, uint32_t addr, bool secure, |
| 参数 | 说明 |
|---|---|
addr |
寄存器地址(32 位,含 MMS 字段) |
secure |
true 启用保护模式(payload 同时含 normal + 反转数据,MAC-PHY 校验) |
*Callback |
完成回调(可 NULL) |
pTag |
透传标签 |
TC6_MultipleRegisterAccess 返回成功入队的条目数(可能少于 mapLength,队列满时)。
3.6 状态查询
1 | void TC6_GetState(TC6_t *pInst, uint8_t *pTxCredit, uint8_t *pRxCredit, bool *pSynced); |
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 | bool TC6_CB_OnSpiTransaction(TC6_t *pInst, uint8_t *pTx, uint8_t *pRx, uint16_t len, void *g) { |
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 | extern bool TC6_CB_OnSpiTransaction(TC6_t *pInst, uint8_t *pTx, uint8_t *pRx, |
契约:
| 项 | 说明 |
|---|---|
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 | bool TC6_CB_OnSpiTransaction(TC6_t *pInst, uint8_t *pTx, uint8_t *pRx, uint16_t len, void *pGlobalTag) |
4.2 TC6_CB_OnRxEthernetSlice
1 | extern void TC6_CB_OnRxEthernetSlice(TC6_t *pInst, const uint8_t *pRx, |
契约:
| 项 | 说明 |
|---|---|
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 | extern void TC6_CB_OnRxEthernetPacket(TC6_t *pInst, bool success, uint16_t len, |
契约:
| 项 | 说明 |
|---|---|
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 | void TC6_CB_OnNeedService(TC6_t *pInst, void *pGlobalTag) { |
只是一个 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 | init_<name>_queue(q, buffer, size) |
环形索引:
1 | return &q->buffer_[(q->stageN_<role>_ & (q->size_ - 1u))]; // size_ 必须是 2 的幂 |
判空/判满:
1 | ready: ((stage - ref) < size) |
5.3 7 阶段 regop_queue 的状态机
1 | enqueue (1) → send (2) → int (3) → modify (4) → send (5) → int (6) → event (7) |
ReadModifyWriteRegister 的 modifyValue/modifyMask 在阶段 4 被应用,然后阶段 5 写入新值。
5.4 内存布局示例(regop_queue 槽位)
1 | struct register_operation { |
每个槽约 ~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 | uint32_t TC6Regs_CB_GetTicksMs(void); // 返回当前毫秒 tick |
实现(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_ErrorRX_Non_Recoverable_ErrorTX_Non_Recoverable_Error
6.4 LAN865x 寄存器初始化表
src/microchip/lib/libtc6/src/tc6-regs.cpp:297-331
1 | static const MemoryMap_t TC6_MEMMAP[] = { |
关键地址汇总(移植到竞品时需要替换):
| 地址 | 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 | // tc6-regs.cpp:451-467 |
移植时:替换为竞品的 OUI/Model 期望值。
6.6 MAC 地址写入顺序
tc6-regs.cpp:371-389
1 | regVal = ((mac[3] << 24) | (mac[2] << 16) | (mac[1] << 8) | mac[0]); |
字节序反转: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.cpp—sys_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) —— 发送回调
调用路径:
udp_sendto→ lwIP 内部 →etharp_output→netif->linkoutput=lwIpOutTC6_GetRawSegments(tc6, &txSeg)申请 segment 数组- 把 pbuf 链表每个段填入 segment 数组(
p->len,p->next) TC6_SendRawEthernetSegments(tc6, txSeg, seg, p->tot_len, 0, callback, p)提交- callback 释放 pbuf
关键引用计数:第 305 行 pbuf_ref(p)——为什么?
在
TC6_SendRawEthernetSegments路径中,MAC-PHY 异步发送完成前,pbuf 必须保持有效。但 lwIP 内部会在udp_sendto返回后立刻pbuf_free(p)。pbuf_ref增加引用计数,使得后续pbuf_free只减少引用,最后由 libtc6 TX 完成的 callbackpbuf_free真正释放。
返回码:
ERR_OK— 发送已入队(实际发送异步完成)ERR_WOULDBLOCK— TX 队列满,无可用 segmentERR_IF— TC6 拒绝(极少见)
etharp_input —— RX 入口
由 lwIP 内部通过 netif->input(pbuf, netif) 调用,即 TC6_Arduino_10BASE_T1S::lwIpOut 的反向路径。
EtherType 过滤(:358-373):
1 | bool FilterRxEthernetPacket(uint16_t ethType) { |
其他 EtherType(如 0x88A8 服务 VLAN tag、0x88CC LLDP、IPv6)的帧被静默丢弃。
7.3 命名空间冲突的处理
src/lib/liblwip/cfg/lwipopts.h:590-742 通过 #define xxx t1s_xxx 重命名所有 lwIP 符号:
1 |
|
为什么需要重命名?
- lwIP 内部函数会与 Arduino 核心库同名(例如
sys_now) - 库的多个 cpp 文件都会 include lwIP 头,重命名防止链接时符号冲突
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 | // src/lib/lwip_sys_now.cpp:32 |
注意:因为重命名,链接器看到的实际符号是 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 1500:netif.mtu = 1536(在 TC6_Arduino_10BASE_T1S.cpp:288),但 lwIP ETHERNET_MTU = 1500。lwIP 在发送路径会用 min(netif.mtu, ETHERNET_MTU),所以实际 MTU 是 1500。1536 是给 RX pbuf 分配的容量(容纳 VLAN tag 等扩展)。
8. 移植到非 Arduino 平台
把 libtc6 移植到非 Arduino 平台(例如 RTOS + 通用 lwIP)需要提供:
8.1 必实现的回调
1 | bool TC6_CB_OnSpiTransaction(...); // 平台 SPI |
8.2 必实现的 lwIP 桥接
1 | u32_t sys_now(void); // → 平台 tick |
8.3 必须周期性调用
1 | TC6_Service(tc6, /*IRQ 电平*/); |
调用频率:建议 1ms 或更快。10BASE-T1S 64-byte chunk 在 24MHz SPI 下约 21μs 传输,每秒可处理 ~47000 chunks。
8.4 替换 tc6-regs.cpp
tc6-regs.cpp 是 Microchip 专有 的——所有寄存器地址、值、magic numbers 都是 LAN8651 特有。移植到竞品:
- 保留
tc6-regs.h的 API(TC6Regs_Init/Reinit/SetPlca/...) - 重写
tc6-regs.cpp:- 替换
TC6_MEMMAP[]为竞品的初始化序列 - 替换
OnReadId1/2的 OUI/Model 校验 - 替换
HandlePlca中的寄存器地址(如果竞品 PLCA 寄存器布局不同) - 替换
InitChip(PHY 校准间接寄存器读取公式)
- 替换
- 如果竞品使用不同地址空间分布,调整
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 | // #define PRINT(...) // 默认空 |
重新编译,串口会看到所有 TC6Error_* 和 TC6Regs_Event_*。
10.2 用 Wireshark 验证数据通路
LAN8651 收到帧 → RX pbuf → FilterRxEthernetPacket → netif->input → udp_recv → Arduino_10BASE_T1S_UDP::_rx_pkt_list。lwIP 本身不输出任何日志(LWIP_STATS = 0)。
要在 lwIP 层加观测,可以临时打开:
1 |
但这会显著增加 RAM,需要重新评估 UNO R4 Minima 的 32KB 限制。
10.3 添加 RX/TX 计数器
最简单的方法是在 TC6_CB_OnRxEthernetPacket 和 lwIpOut 里加 static 计数器,周期性打印:
1 | // 临时调试代码 |
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