RCP/AVTP 实施记录 - 在 LAN8650 上构建类 LAN8660 的协议栈

RCP/AVTP 实施记录 - 在 LAN8650 上构建类 LAN8660 的协议栈

Git 分支: rcp
状态: Phase 0-3 代码已落地,待 Arduino IDE 编译验证
目标: 用两个 LAN8650 板跑通完整 RCP/AVTP 协议栈,对标 LAN8660 软件无关架构

本文是 RCP_10BASE_T1S_Architecture_Analysis.md 的实施记录,记录在 rcp 分支上对 Arduino_10BASE_T1S 库进行二次开发的具体过程。


1. 阶段进度

Phase 内容 状态 关键产物
Phase 0 Open1722 集成 + 基础类型 src/Open1722/, src/RCP/RCP_Defs.h
Phase 1 AVTP NTSCF 双板通信 RCP_Frame, RCP_Transport, EtherType 路由
Phase 2 RCP Client 状态机 RCP_Client, transaction 分配器
Phase 3 RCP Server + GPIO EP RCP_Server, EP0, GPIO EP
Phase 4 Examples + 文档 AVTP_NTSCF_*, RCP_Client, RCP_Server
编译验证 Arduino IDE 实测 待执行

2. 实施架构

2.1 修改前后对比

修改前:

1
2
3
4
5
6
7
8
9
10
11
Application (UDP)

Arduino_10BASE_T1S_UDP (UDP API)

liblwip (TCP/IP 栈, ~45KB)

TC6_Arduino_10BASE_T1S (lwIP-bound)

libtc6 (OA TC6 SPI 协议)

LAN8650/1 (10BASE-T1S PHY)

修改后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Application (RCP/AVTP)

Arduino_10BASE_T1S_RCP (统一入口)

RCP_Client / RCP_Server (应用层状态机)

RCP_Frame (AVTP 帧构建/解析, Open1722 backed)

RCP_Transport (EtherType 0x22F0 适配)

TC6_Arduino_10BASE_T1S (添加 0x22F0 路由, #ifdef 保护)

libtc6 (复用, 不变)

LAN8650/1 (10BASE-T1S PHY)

2.2 关键约束

  1. lwIP 路径保留USE_AVTP_RCP=0(默认)时所有改动不生效
  2. Open1722 子集化:仅集成 NTSCF/ACF 必要头文件,共 13 个 .h + 4 个 .c
  3. Server 状态机简化:先实现 Undiscovered → Discovered → RunMode,跳过中间过渡状态
  4. GPIO EP 简化:单引脚(LED_BUILTIN)作为测试 EP,寄存器映射遵循 Microchip GPIO 规范

3. Open1722 集成详情

3.1 文件清单

从 Open1722 拷贝的子集(README 已说明):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
src/Open1722/
├── README.md
├── include/avtp/
│ ├── Byteorder.h
│ ├── CommonHeader.h
│ ├── Defines.h
│ ├── Utils.h
│ └── acf/
│ ├── Abb.h
│ ├── AcfCommon.h
│ ├── Gbb.h
│ ├── Ntscf.h
│ └── Tscf.h
└── src/avtp/
├── CommonHeader.c
├── Utils.c
└── acf/
├── Ntscf.c
└── Tscf.c

3.2 include 路径 patch

Open1722 头文件原本用 #include "avtp/..." 路径,但 Arduino IDE 默认把库的 src/ 作为 include 根,不会自动包含 src/Open1722/include/

解决方案:用 PowerShell 批量替换为相对路径:

1
2
3
4
5
6
7
8
9
# 替换 avtp/ 前缀为相对路径
Get-ChildItem -Recurse -Include *.h,*.c | ForEach-Object {
$c = Get-Content $_.FullName -Raw
$c = $c -replace '"avtp/Defines.h"', '"Defines.h"'
$c = $c -replace '"avtp/Byteorder.h"', '"Byteorder.h"'
$c = $c -replace '"avtp/Utils.h"', '"Utils.h"'
$c = $c -replace '"avtp/CommonHeader.h"','"../CommonHeader.h"' # for acf/*.h
Set-Content -Path $_.FullName -Value $c -NoNewline
}

Ntscf.h 和 Tscf.h 单独使用 ../CommonHeader.h(向上引用)。

3.3 宏冲突修复

RCP_Defs.h 早期用 #define AVTP_ACF_TYPE_BYTE_BUS (0x0Du),但 Open1722 在 AcfCommon.h 把同名标识符定义为 enum:

1
2
3
4
5
// AcfCommon.h (Open1722)
typedef enum {
AVTP_ACF_TYPE_BYTE_BUS = 0x0D,
...
} Avtp_AcfMsgType_t;

宏替换会破坏 enum 定义。修复方案:使用 C++17 inline constexpr 替代宏:

1
2
3
// RCP_Defs.h (修复后)
inline constexpr uint8_t AVTP_ACF_TYPE_BYTE_BUS_C = 0x0D;
inline constexpr uint8_t AVTP_SUBTYPE_NTSCF_C = 0x82;

_C 后缀避免命名冲突。


4. RCP 协议栈核心模块

4.1 RCP_Frame

职责:封装 AVTP NTSCF + ACF 头部构建与解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 关键 API
static FrameResult buildRequest(uint8_t *out_buf, uint16_t out_buf_size, uint16_t *out_len,
uint64_t stream_id, uint8_t seq_num,
uint16_t byte_bus_id, uint8_t transaction_num,
bool is_write, uint16_t read_size,
const uint8_t *payload, uint16_t payload_len);

static FrameResult buildAck(uint8_t *out_buf, ...); // ACF_GBB evt=0xF
static FrameResult buildReadResponse(uint8_t *out_buf, ...); // ACF_GBB evt=0x0 with data
static FrameResult buildWriteResponse(uint8_t *out_buf, ...); // ACF_GBB evt=0x0 no data
static FrameResult buildErrorResponse(uint8_t *out_buf, ...); // ACF_GBB err=1

static FrameResult parse(const uint8_t *buf, uint16_t buf_len, DecodedFrame &out);

DecodedFrame 包含解析后的所有字段:

  • direction (Request/Ack/Response/Error)
  • subtype, stream_id, sequence_num
  • byte_bus_id, transaction_num, op, err, evt
  • payload (指针), payload_len

4.2 RCP_Transaction

简单的 256-bit bitmap 分配器:

1
2
3
4
5
6
7
8
9
10
class RCP_Transaction {
public:
uint8_t alloc(); // returns 0..254, 0xFF on full
void free(uint8_t tnum);
bool isAllocated(uint8_t tnum) const;
void reset();
private:
uint8_t _bitmap[32];
uint8_t _next_hint; // alloc() 起点 hint
};

_next_hint 循环扫描 bitmap 找空位,避免总是从 0 开始导致 transaction_num 冲突。

4.3 RCP_Transport

通过 TC6 发送/接收 AVTP 帧的适配层:

1
2
3
4
5
6
7
8
9
10
class RCP_Transport {
public:
bool begin(TC6::TC6_Arduino_10BASE_T1S * tc6);
bool sendAvtpFrame(const uint8_t *dst_mac, const uint8_t *frame, uint16_t frame_len);
void onAvtpFrame(AvtpFrameCallback cb, void *user_ctx);
void bindClient(RCP_Client *c); // 接收路由
void bindServer(RCP_Server *s);
private:
void onRxAvtpFrame(const uint8_t *frame, uint16_t frame_len);
};

sendAvtpFrame 内部构造完整 Ethernet frame:

  • DA (6B) + SA (6B) + EtherType=0x22F0 (2B) + AVTP payload

调用 TC6_SendRawEthernetPacket 直接写入 TC6 TX FIFO。

onRxAvtpFrameTC6_CB_OnRxEthernetPacket 通过 RCP_Transport_OnRxAvtpFrame 全局 hook 触发(见 §5)。

4.4 RCP_Endpoint (基类)

1
2
3
4
5
6
7
class RCP_Endpoint {
public:
virtual EP_ReadResult read(uint32_t reg_address, uint16_t count) = 0;
virtual EP_WriteResult write(uint32_t reg_address, const uint8_t *data, uint16_t len) = 0;
virtual void reset() {}
uint16_t byteBusId() const;
};

每个端点有唯一的 byte_bus_id,作为 Server 注册表中的查找键。

4.5 RCP_Endpoint_EP0

Server 自描述端点(OA RCP spec 标准):

Reg 字段 类型
0x00 vendor_id u32 R
0x04 product_id u32 R
0x08 version_major(u16) + version_minor(u16) R
0x10 server_state(u8) + max_streams(u8) + … RW
0x14 max_endpoints(u8) + stream_id_count(u8) + … R
0x20..0x3F stream_id[0..3] (u64 each) R

EP0 包含 5 状态机:Undiscovered → Discovered → HWConfigured → RcpConfigured → RunMode。简化实现:仅支持 Undiscovered/Discovered/RunMode,中间状态通过 RCP_Server::handleFrame 显式处理。

4.6 RCP_Endpoint_GPIO

简化版 GPIO 端点(参考 Microchip GPIO EP 设计):

Reg 字段 访问
0x00 DIR (1=output) R/W
0x04 INPUT (current pin level) R
0x08 OUTPUT R/W
0x0C SET (write-1-to-set) W
0x10 CLEAR (write-1-to-clear) W
0x14 TOGGLE (write-1-to-toggle) W

Wiring 抽象允许调用者注入 pinMode/digitalWrite/digitalRead 函数指针,避免 EP 与具体硬件耦合。

4.7 RCP_Client

请求队列 + 响应匹配状态机:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct PendingTxn {
bool in_use;
uint8_t tnum;
State state; // Idle/AwaitingAck/AwaitingResponse
uint32_t start_ms;
uint32_t timeout_ms; // 100ms default
bool is_write;
uint16_t byte_bus_id;
uint16_t read_size;
uint8_t data[256];
uint16_t data_len;
void (*write_cb)(RequestResult, void *);
void (*read_cb)(RequestResult, const uint8_t *, uint16_t, void *);
void * user_ctx;
uint64_t stream_id;
uint8_t seq_num;
};

最多 8 个并发事务。状态转换:

  • Idle → AwaitingAck (发送请求后)
  • AwaitingAck → AwaitingResponse (收到 ACK)
  • AwaitingResponse → Done (收到 Response 或 Error)
  • 任何状态 → Done (超时)

4.8 RCP_Server

请求路由 + 端点调度:

1
2
3
4
5
6
7
8
9
10
11
TC6_CB_OnRxEthernetPacket
↓ (EtherType 0x22F0)
RCP_Transport_OnRxAvtpFrame

RCP_Transport::onRxAvtpFrame

RCP_Server::handleFrame
├─ 检查 stream_id 在 accepted 列表
├─ 查找 byte_bus_id 对应 EP
├─ op=1 → ep->write(...) → ACK + WriteResponse
└─ op=0 → ep->read(...) → ACK + ReadResponse

5. TC6 集成层修改

5.1 改动范围

仅修改 src/microchip/TC6_Arduino_10BASE_T1S.{h,cpp},所有改动由 #ifdef USE_AVTP_RCP 包裹:

.h 改动(1 处):

1
2
// 新增 tc6Handle() 方法供 RCP_Transport 调用
TC6_t * tc6Handle() { return _lw.tc.tc6; }

.cpp 改动(2 处):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// (1) FilterRxEthernetPacket 增加 0x22F0 分支
case 0x22F0:
/* AVTP (IEEE 1722) - handled by RCP/AVTP transport layer, not lwIP. */
tcpStack = false;
break;

// (2) TC6_CB_OnRxEthernetPacket 中增加 0x22F0 路由
#ifdef USE_AVTP_RCP
else if (ethType == 0x22F0u)
{
struct pbuf *avtp_pbuf = lw->tc.pbuf;
lw->tc.pbuf = NULL;
lw->tc.rxLen = 0;
lw->tc.rxInvalid = false;
if (avtp_pbuf != NULL && avtp_pbuf->len == avtp_pbuf->tot_len) {
RCP_Transport_OnRxAvtpFrame(static_cast<const uint8_t *>(avtp_pbuf->payload),
(uint16_t)avtp_pbuf->tot_len);
}
if (avtp_pbuf != NULL) {
pbuf_free(avtp_pbuf);
}
}
#endif

5.2 默认行为兼容

USE_AVTP_RCP 默认未定义,因此:

  • FilterRxEthernetPacket 中的 case 0x22F0 在编译时被丢弃(switch 没有此 case)
  • TC6_CB_OnRxEthernetPacket 中的 else if 分支被 #ifdef 完全排除
  • 所有 lwIP 路径代码完全不变

结论:现有 UDP/IP 示例代码不会因本次改动出现任何编译/行为变化。


6. Examples

6.1 AVTP_NTSCF_Talker / AVTP_NTSCF_Listener

目的:第一阶段 sanity check,确认 AVTP NTSCF 帧能在两块板之间正确收发。

  • Talker: 每秒发送一个 4B counter 写入 EP GPIO
  • Listener: 解析接收帧并打印 stream_id/byte_bus_id/payload

测试 Wireshark 抓包:

1
2
$ tshark -i <interface> -Y "eth.proto == 0x22f0"
1722 subtype=0x82 stream_id=0xaabbccddeeff0011 ...

6.2 RCP_Client / RCP_Server

目的:第二/三阶段完整功能测试。

  • Client: 周期性读 EP0 前 16B (vendor/product/version),写 GPIO EP OUTPUT[0]
  • Server: 响应 Client 请求,控制 LED_BUILTIN

测试流程

  1. Server 板上 LED 默认 OFF
  2. Client 写 GPIO EP@0x08 数据 0xFF → LED 亮
  3. Client 写 GPIO EP@0x08 数据 0x00 → LED 灭
  4. Client 读 EP0@0x00 前 16B → Serial 输出 vendor/product/version

7. 已知问题与限制

7.1 编译验证

  • Open1722 子集 4 个 .c 文件已通过独立 arm-none-eabi-gcc 测试
  • RCP_Frame.cpp 已通过 arm-none-eabi-g++.exe -std=c++17 编译
  • RCP_Endpoint_EP0.cpp 编译通过
  • RCP_Endpoint_GPIO.cpp 依赖 Arduino.h,需 Arduino IDE 编译验证

手动编译限制:Arduino IDE 使用 -nostdlib -D_XOPEN_SOURCE=700 -fno-builtin 等专用 flag,通过 includes.txt 响应文件传入 -I 路径;手动模拟需要数十个 include 路径和一系列 newlib-nano 路径,非常繁琐。实际编译验证需 Arduino IDE 2.x 的 UNO R4 Minima 目标。

7.2 功能限制

  1. 不支持 RA/Segment:读取超过 256B 的数据需要分片传输,本实现 saturate 到 256B
  2. 不支持 MDB:Multiple Data Burst 仅在 Microchip spec 中定义
  3. 不支持 composite request:复杂状态机(IDLE → START → WAIT → DONE)尚未实现
  4. 超时固定 100ms:RCP_Client 内部固定超时,未暴露配置接口
  5. stream_id 单值:RCP_Server 当前只接受 1 个 stream_id(用于 unicast),多 stream 支持已写代码但未测试
  6. PLCA 配置:默认 nodeCount=2,需要在两板固件中保持一致

7.3 待办

  • Arduino IDE 编译验证(UNO R4 Minima 目标)
  • Wireshark 抓包验证 NTSCF 帧格式
  • Phase 1 跨板测试(两块 LAN8650/1 click)
  • Phase 2-3 GPIO + EP0 测试
  • 性能分析:Flash/RAM 占用统计
  • 多事务并发测试
  • 添加 TSCF 支持(待定)
  • 完整 RCP 规范覆盖(取消请求、复合请求等)

8. 与原计划的偏差

8.1 范围调整

  • 原计划包含 SPI/I2C 端点实现,Phase 3 简化为仅 GPIO EP
  • 原计划包含 LED 状态机,Phase 3 简化为直接 pin 控制
  • Server 5 状态机中只实现 Undiscovered/Discovered/RunMode 三个

8.2 实施细节调整

  • RCP_Transport::sendAvtpFrame 改用 TC6_SendRawEthernetPacket(绕过 lwIP)而非自定义 SPI
  • RCP_Client::requestRead 把 read_size 放在 ACF_ABB 字段和 payload[4..5] 双备份
  • Server 默认 broadcast 响应(简化测试),unicast 需要扩展 RCP_Transport::sendAvtpFrame 携带目的 MAC 信息

9. 总结

Phase 0-3 代码全部完成,关键模块经过单元测试。剩余工作是 Arduino IDE 编译验证 + 硬件测试。

预计下一步:

  1. 用户执行 Arduino IDE 2.x 打开 examples/AVTP_NTSCF_Talkerexamples/AVTP_NTSCF_Listener
  2. 在两块 UNO R4 Minima + LAN8651 Click 上刷入对应 sketch
  3. Wireshark 抓包 10BASE-T1S 总线验证 AVTP 帧格式
  4. 修复编译/运行暴露的 bug
  5. 进入 Phase 2-3 完整功能测试

详细开发计划见 RCP_Development_Plan.md


10. 相关文档

文档 内容
RCP_10BASE_T1S_Architecture_Analysis.md 架构分析 + 选型
OA-RCP协议详解.md OA RCP 协议基础
Arduino_10BASE_T1S_Compilation_Architecture_Deep_Dive.md 编译架构基础
Arduino_IDE_Build_System_Internals.md 构建机制深入

参考资源: