init: Air780EPM 官方 LuatOS 项目代码基线

- 来源: 合宙 LuatOS 官方仓库 air780epm 模块完整代码
- 路径: luatos/air780epm/module/Air780EPM/demo 含官方 demo(含 mqtt/mqtts/socket/uart 等)
- 后续: 基于 demo 开发 UART<->MQTT 数据上报功能
This commit is contained in:
wangfq
2026-08-31 08:53:49 +08:00
commit 173dd6874f
813 changed files with 125484 additions and 0 deletions
@@ -0,0 +1,399 @@
# ARP 1000ms 定时器低功耗化改造 / netdrv.arpSleep API
> 适用:基于 `net_lwip2` adapter + `netdrv` 框架的 BSPAir8000A/U/N/AB/W/D/DB/T 等),目标是消除 `pm.power(pm.WORK_MODE, 1)` 进入 Slp1 后由 ARP 1000ms 定时器(`etharp_tmr`)带来的周期性唤醒功耗。
---
## 1. 背景与问题
### 1.1 现象
Air8000A 系列模组进入低功耗模式后,预期功耗 ~40-70uA,但实测有持续 1Hz 的电流脉冲。串口/EPAT 日志可观察到:
```
03:0048:07:06464 D/net arp_pm: TICK etharp_tmr
03:0148:07:05760 D/net arp_pm: TICK etharp_tmr
03:0248:07:05440 D/net arp_pm: TICK etharp_tmr
... 每秒一次, 无限持续 ...
```
### 1.2 根因
`net_lwip2.c` 为每个 netif 维护了一个 1000ms 周期的 ARP timer`prvlwip.arp_timer`),作用是周期性扫描 ARP 表 + 重传 ARP 请求。当编译选项是 `LUAT_NETDRV_ARP_TIMER_ALWAYS_ON` 时它永久 1Hz 跑;当编译为 on-demand 时它本应在「不需要 ARP」时停止,但:
- WIFI/CH390/AP 任一 netif 处于 `link_up``gw_mac` 尚未解析时,定时器都会启动;
- 进入低功耗前没有任何代码主动告诉 net_lwip2「我要睡了,把定时器停掉」。
### 1.3 多次尝试的失败路径
| 方案 | 结果 |
|---|---|
| 在 `luat_lib_pm.c``l_pm_power_ctrl` 加 hook`id==LUAT_PM_POWER_WORK_MODE && onoff!=0` 时调用 `net_lwip2_arp_timer_sleep_prepare()` | 失败。Air8000 的 `pm.power(WORK_MODE,1)` 编译宏走 `LUAT_USE_DRV_PM` 分支,最初 hook 放在 `#else` 内,从未生效。 |
| 把 hook 移到 `#ifdef LUAT_USE_DRV_PM` **外** | 失败。Air8000 实际把 `pm.power(WORK_MODE,1)` 重定向到 `pm.request(Slp1)` 路径(日志只看到 `I/pm request mode=Slp1`,看不到我们注册的 `arp_pm: pm.power(...)` 行),完全绕过 `l_pm_power_ctrl`。 |
| 同时在 `l_pm_request` 加 hook | 部分固件构建仍未触发。该路径在 SoC 内部由 drv_pm 框架管理,钩子位置和编译条件因平台而异,不可靠。 |
| 用 `netdrv.ctrl(LWIP_ETH, CTRL_UPDOWN, 0)``LINK_DOWN` 事件触发 `apply_stop` | 在 ETH 场景下有效,但 STA/AP 场景下 WIFI 芯片会被直接掉电,netif 不一定经过标准 LINK_DOWN 事件。 |
### 1.4 最终选择
**抛弃所有 pm 钩子,改用一个显式的 Lua API`netdrv.arpSleep()` / `netdrv.arpResume()`。** 由低功耗脚本在 `pm.power(WORK_MODE,1)` 之前主动调用,直接把 `EV_LWIP_ARP_TIMER_SLEEP` 事件 post 给 lwip task。完全与 pm 框架解耦。
---
## 2. 系统架构
```
┌──────────────────────────────────────────────────────────┐
│ Lua: drv_lowpower.lua │
│ 1. dhcpsrv.stop(_G.eth_dhcpsrv) 关闭 dhcpsrv 协程 │
│ 2. netdrv.ctrl(LWIP_ETH, CTRL_UPDOWN, 0) 关 CH390 │
│ 3. netdrv.arpSleep() 关闭 ARP timer │
│ 4. pm.power(pm.WORK_MODE, 1) 进入 Slp1 │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ C: l_netdrv_arp_sleep (luat_lib_netdrv.c) │
│ net_lwip2_arp_timer_sleep_prepare() │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ C: net_lwip2.c │
│ platform_send_event(EV_LWIP_ARP_TIMER_SLEEP) │
│ └─> lwip task 收到事件 │
│ └─> apply_stop() │
│ └─> luat_stop_rtos_timer(arp_timer) │
│ └─> arp_timer_in_sleep = 1 │
└──────────────────────────────────────────────────────────┘
```
---
## 3. 改动清单
### 3.1 C 层
| 文件 | 改动 | 必须重编 |
|---|---|---|
| `components/network/adapter_lwip2/net_lwip2.h` | 声明 `net_lwip2_arp_timer_sleep_prepare/wakeup_resume` 等公共 API | ✅ |
| `components/network/adapter_lwip2/net_lwip2.c` | 实现 sleep/resume API + EV_LWIP_ARP_TIMER_SLEEP/RESUME 事件处理;修复 `LINK_STATE` 钩子用 `netif_is_link_up()`(不是 `netif_is_up()`);`any_adapter_need_arp` 同步加上 link_up 检查 | ✅ |
| `components/network/netdrv/src/luat_netdrv_lwip_etharp.c` | `gw_mac_valid` 通知(STABLE 时置 1, free 时置 0 | ✅ |
| `components/network/netdrv/src/luat_netdrv_ch390h_task.c` | 所有 CH390 处于 `CH390H_STATUS_STOPPED` 时 task 进入 `LUAT_WAIT_FOREVER` 等待,不再 1Hz 心跳;新增 `luat_ch390h_task_wakeup()` | ✅ |
| `components/network/netdrv/src/luat_netdrv_ch390h.c` | `CTRL_UPDOWN=1` 时调用 `luat_ch390h_task_wakeup()` 把 task 从 FOREVER 等待唤醒 | ✅ |
| `components/network/netdrv/binding/luat_lib_netdrv.c` | 新增 Lua API `netdrv.arpSleep()` / `netdrv.arpResume()` | ✅ |
### 3.2 Lua 层
| 文件 | 改动 | 必须重编 |
|---|---|---|
| `script/libs/dhcpsrv.lua` | 新增 `dhcpsrv.stop(srv)` API`dhcp_task``srv.stop` 退出条件 | ❌ 刷脚本即可 |
| `module/Air8000/demo/network_routing/wifi_out_ethernet_in_wifi_in/netif_app.lua` | `dhcpsrv.create` 返回值存到 `_G.eth_dhcpsrv` / `_G.ap_dhcpsrv`,供 drv_lowpower 引用 | ❌ |
| `module/Air8000/demo/network_routing/wifi_out_ethernet_in_wifi_in/drv_lowpower.lua` | 休眠前: stop dhcpsrv + CTRL_UPDOWN=0 + `netdrv.arpSleep()`UART1 唤醒回调: `netdrv.arpResume()` + CTRL_UPDOWN=1 + 重新 create dhcpsrv | ❌ |
---
## 4. 新增 Lua API
### 4.1 `netdrv.arpSleep()`
```lua
-- 进入 pm.power(pm.WORK_MODE, 1) 之前调用
netdrv.arpSleep()
pm.power(pm.WORK_MODE, 1)
```
- **作用**:直接 post `EV_LWIP_ARP_TIMER_SLEEP` 事件给 lwip tasktask 收到后立即 stop ARP 1000ms 定时器,并把当前 running 状态保存到 `arp_timer_sleep_saved` 字段以便恢复时判断。
- **返回**boolean。`true` 表示事件已投递(编译启用了 `LUAT_USE_NETDRV_LWIP_ARP` 且未开 `LUAT_NETDRV_ARP_TIMER_ALWAYS_ON`);`false` 表示当前固件 ARP timer 是 always-on 模式或未启用,无效。
- **线程安全**:可在任意 task / 协程中调用。
### 4.2 `netdrv.arpResume()`
```lua
-- 唤醒回调里调用
netdrv.arpResume()
```
- **作用**post `EV_LWIP_ARP_TIMER_RESUME` 事件给 lwip task。task 收到后清 `arp_timer_in_sleep` 标志位,然后检查 `arp_timer_sleep_saved``any_adapter_need_arp()`,若仍有 adapter 需要 ARPadmin_up + link_up + gw_mac 未解析),重新启动定时器;否则保持停止。
- **返回**boolean,同上。
---
## 5. 关键 C 实现要点
### 5.1 `LINK_STATE` 事件钩子修复
```c
// 文件: components/network/adapter_lwip2/net_lwip2.c
case EV_LWIP_NETIF_LINK_STATE:
{
uint8_t idx = event.Param3;
#ifndef LUAT_NETDRV_ARP_TIMER_ALWAYS_ON
// 注意: 这里是 link state 事件, 必须用 netif_is_link_up() 判断 link 层状态,
// 不能用 netif_is_up() (它是 admin state). CH390 CTRL_UPDOWN=0 只调
// netif_set_link_down, 不动 admin state, 误用 netif_is_up 会导致 link down
// 被错判为 LINK_UP 而重新启动 ARP 定时器.
if (prvlwip.lwip_netif[idx] != NULL
&& netif_is_up(prvlwip.lwip_netif[idx])
&& netif_is_link_up(prvlwip.lwip_netif[idx])) {
// LINK_UP 分支
prvlwip.gw_mac_valid[idx] = 0;
net_lwip2_arp_timer_apply_start();
} else {
// LINK_DOWN 分支
prvlwip.gw_mac_valid[idx] = 0;
if (!net_lwip2_any_adapter_need_arp()) {
net_lwip2_arp_timer_apply_stop();
}
}
#endif
net_lwip2_check_network_ready(idx);
}
break;
```
### 5.2 `any_adapter_need_arp` 充要条件
```c
static int net_lwip2_any_adapter_need_arp(void)
{
uint8_t i;
for (i = 0; i < NW_ADAPTER_INDEX_LWIP_NETIF_QTY; i++) {
if (prvlwip.lwip_netif[i] != NULL
&& netif_is_up(prvlwip.lwip_netif[i])
&& netif_is_link_up(prvlwip.lwip_netif[i]) // 必须 link_up
&& prvlwip.gw_mac_valid[i] == 0) {
return 1;
}
}
return 0;
}
```
### 5.3 CH390 task FOREVER 等待
```c
// 文件: components/network/netdrv/src/luat_netdrv_ch390h_task.c
static void ch390_task_main(void* args) {
// ...
while (1) {
// ...
int any_active = 0;
for (size_t i = 0; i < MAX_CH390H_NUM; i++) {
if (ch390h_drvs[i] != NULL
&& ch390h_drvs[i]->status != CH390H_STATUS_STOPPED) {
any_active = 1;
break;
}
}
if (!any_active) {
// 所有 CH390 都 STOPPED, 不再 1Hz 心跳, 直到收到消息(IRQ 或 CTRL_UPDOWN=1)
ret = task_wait_msg(LUAT_WAIT_FOREVER);
}
else if (s_ch390h_mode == 0) {
ret = task_wait_msg(5); // PULL: 5ms
}
else {
ret = task_wait_msg(1000); // IRQ: 1Hz 心跳
}
}
}
void luat_ch390h_task_wakeup(void) {
if (qt == NULL) return;
uint32_t len = 0;
luat_rtos_queue_get_cnt(qt, &len);
if (len > 4) return;
pkg_evt_t evt = { .id = 2 };
luat_rtos_queue_send(qt, &evt, sizeof(pkg_evt_t), 0);
}
```
### 5.4 `netdrv.arpSleep` Lua binding
```c
// 文件: components/network/netdrv/binding/luat_lib_netdrv.c
static int l_netdrv_arp_sleep(lua_State *L) {
#if defined(LUAT_USE_NETDRV_LWIP_ARP) && !defined(LUAT_NETDRV_ARP_TIMER_ALWAYS_ON)
net_lwip2_arp_timer_sleep_prepare();
lua_pushboolean(L, 1);
#else
lua_pushboolean(L, 0);
#endif
return 1;
}
// 注册:
{ "arpSleep", ROREG_FUNC(l_netdrv_arp_sleep)},
{ "arpResume", ROREG_FUNC(l_netdrv_arp_resume)},
```
---
## 6. 业务侧使用模板
### 6.1 应用脚本(暴露 dhcpsrv 句柄)
```lua
-- netif_app.lua
local function wifi_eth_setup()
-- ...
-- 用全局变量暴露给低功耗模块, 进入休眠前会调用 dhcpsrv.stop(eth_dhcpsrv)
_G.eth_dhcpsrv = dhcpsrv.create({ adapter = socket.LWIP_ETH })
end
local function wifi_sta_ap_setup()
-- ...
_G.ap_dhcpsrv = dhcpsrv.create(dhcpsrv_opts)
end
```
### 6.2 低功耗脚本(关闭顺序)
```lua
-- drv_lowpower.lua 在 pm.power(pm.WORK_MODE, 1) 之前
-- 1) 关 dhcpsrv 协程 (消除 1Hz sys.waitUntil 唤醒)
if dhcpsrv and dhcpsrv.stop and _G.eth_dhcpsrv then
dhcpsrv.stop(_G.eth_dhcpsrv)
_G.eth_dhcpsrv = nil
end
if dhcpsrv and dhcpsrv.stop and _G.ap_dhcpsrv then
dhcpsrv.stop(_G.ap_dhcpsrv)
_G.ap_dhcpsrv = nil
end
-- 2) 关 CH390 PHY + 让 ch390_task 进入 FOREVER 等待
if netdrv and netdrv.CTRL_UPDOWN and socket.adapter and socket.adapter(socket.LWIP_ETH) then
pcall(netdrv.ctrl, socket.LWIP_ETH, netdrv.CTRL_UPDOWN, 0)
end
-- 3) 关 ARP 1000ms 定时器
if netdrv and netdrv.arpSleep then
netdrv.arpSleep()
end
-- 4) 进入低功耗
pm.power(pm.WORK_MODE, 1)
```
### 6.3 唤醒回调(恢复顺序)
```lua
local function uart1_wakeup_read(_, len)
if len == -1 then
pm.power(pm.WORK_MODE, 0)
-- 1) 恢复 ARP 定时器 (内部判断是否真的需要启动)
if netdrv and netdrv.arpResume then
netdrv.arpResume()
end
-- 2) 重启 CH390 PHY + task
if netdrv and netdrv.CTRL_UPDOWN then
pcall(netdrv.ctrl, socket.LWIP_ETH, netdrv.CTRL_UPDOWN, 1)
end
-- 3) 重建 dhcpsrv (stop 是一次性的)
if dhcpsrv and dhcpsrv.create and not _G.eth_dhcpsrv then
_G.eth_dhcpsrv = dhcpsrv.create({adapter = socket.LWIP_ETH})
end
uart.write(1, "lowpower wakeup\r\n")
end
-- ...
end
```
---
## 7. 验证标准
### 7.1 重编 BSP(不能只刷脚本)
```powershell
# 在 bsp/Air8000 目录下
xmake -y
# 烧入新的 .soc
```
判断标准:开机日志里必须看到这一行说明 C 改动已编入:
```
I/net arp_pm: INIT adapter=2 on-demand mode, timer created (stopped)
```
### 7.2 进入休眠期望日志
```
I/user.lowpower_task enter
I/user.drv_lowpower stop ap dhcpsrv
I/user.dhcpsrv dhcp_task exit, adapter 3
I/user.drv_lowpower ch390 skip CTRL_UPDOWN=0 (LWIP_ETH not present)
I/user.drv_lowpower netdrv.arpSleep() -> stop ARP 1000ms timer
I/net arp_pm: API sleep_prepare -> post EV_SLEEP
I/net arp_pm: EVENT SLEEP saved=1 running=1
I/net arp_pm: apply_stop running=1 -> stop
I/pm request mode=Slp1, prev=Slp1
(之后串口不再出现 D/net arp_pm: TICK etharp_tmr
```
### 7.3 唤醒期望日志
```
I/pm wakeup
I/net arp_pm: API wakeup_resume -> post EV_RESUME
I/net arp_pm: EVENT RESUME saved=1 in_sleep=1
I/net arp_pm: apply_start running=0 in_sleep=0 -> start 1000ms
```
---
## 8. 排障 checklist
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 看不到 `netdrv.arpSleep()` 日志 | Lua 脚本未更新 | 重新 download script |
| 看到 Lua 日志但没 `API sleep_prepare` | C 改动没编入 | 必须重编 BSP,不能只刷脚本 |
| 看到 `apply_stop` 但 TICK 仍在 | 多个 lwip task 实例 / timer 二次注册 | 抓 `INIT` 日志确认初始化次数 |
| `not such netdrv 4` 警告 | CH390 没初始化 | 检查 `wifi_eth_setup()` 是否被调用 |
| `apply_start skip running=1 in_sleep=0` 之后又出现 TICK | LINK_UP/DOWN 误判 | 确认 net_lwip2.c 用的是 `netif_is_link_up()` 不是 `netif_is_up()` |
| 唤醒后 `arpResume` 后没自动启动 timer | `any_adapter_need_arp` 返回 false | 正常,等业务流量触发 SET_IP/LINK_UP 时会自启 |
| WIFI STA 仍 1Hz 唤醒 | LWIP_DHCP `dhcp_fine_tmr` 500ms / `dhcp_coarse_tmr` 60s | 改用静态 IP 或在 lwipopts.h 关 LWIP_DHCP |
| 仍有 10Hz 唤醒 | `LWIP_IGMP=1` (`igmp_tmr` 100ms) 或 `LWIP_IPV6_MLD=1` | 在 BSP lwipopts.h 设为 0 |
---
## 9. 其他功耗杀手提醒
`netdrv.arpSleep()` 只解决 ARP 1000ms 定时器。要达到最低功耗还需注意:
1. **LWIP_DHCP**500ms / 60s 周期,必杀。静态 IP 或编译关闭。
2. **LWIP_IGMP / LWIP_IPV6_MLD**100ms 周期,致命。`lwipopts.h` 设为 0。
3. **CH390 PULL 模式**:5ms 轮询。必须接 INT 脚走 IRQ 模式。
4. **AGPIO 高电平 + 外部下拉电阻**:每 100K 下拉 ~33uA。
5. **GNSS / GSensor 电源开关 (GPIO24)**:拉低省 88uA。
6. **WIFI 芯片 (GPIO23)**`pm.power(pm.WIFI, 0)``gpio.setup(23, nil, gpio.PULLDOWN)` 省 42uA。
7. **dhcpsrv.lua 协程**1Hz `sys.waitUntil`,用 `dhcpsrv.stop(srv)` 关掉。
8. **CH390 task 1Hz 心跳**:用 `netdrv.ctrl(LWIP_ETH, CTRL_UPDOWN, 0)` 让 task 进 FOREVER 等待。
---
## 10. 历史决策记录
| 日期 | 决策 | 原因 |
|---|---|---|
| 2026-06-23 | 在 `luat_lib_pm.c` 加 sleep hook | 最初方案,认为 `pm.power(WORK_MODE,1)` 会经过 `l_pm_power_ctrl` |
| 2026-06-23 | hook 移到 `#ifdef LUAT_USE_DRV_PM` 外 | 发现 Air8000 走 DRV_PM 分支,hook 被跳过 |
| 2026-06-24 | 修复 `EV_LWIP_NETIF_LINK_STATE``netif_is_link_up()` | 发现 `CTRL_UPDOWN=0` 时 link_down 被错判为 LINK_UP |
| 2026-06-24 | 抛弃 pm hook,改用 `netdrv.arpSleep()` Lua API | 发现 Air8000 `pm.power(WORK_MODE,1)` 实际走 `pm.request(Slp1)` 路径,绕过所有 pm 钩子 |
---
## 11. 相关文件索引
- 内核 ARP 定时器实现: [components/network/adapter_lwip2/net_lwip2.c](../components/network/adapter_lwip2/net_lwip2.c)
- ARP 定时器公共 API: [components/network/adapter_lwip2/net_lwip2.h](../components/network/adapter_lwip2/net_lwip2.h)
- gw_mac 通知: [components/network/netdrv/src/luat_netdrv_lwip_etharp.c](../components/network/netdrv/src/luat_netdrv_lwip_etharp.c)
- CH390 task: [components/network/netdrv/src/luat_netdrv_ch390h_task.c](../components/network/netdrv/src/luat_netdrv_ch390h_task.c)
- CH390 控制: [components/network/netdrv/src/luat_netdrv_ch390h.c](../components/network/netdrv/src/luat_netdrv_ch390h.c)
- netdrv Lua binding: [components/network/netdrv/binding/luat_lib_netdrv.c](../components/network/netdrv/binding/luat_lib_netdrv.c)
- dhcpsrv: [script/libs/dhcpsrv.lua](../script/libs/dhcpsrv.lua)
- 示例 demo: [module/Air8000/demo/network_routing/wifi_out_ethernet_in_wifi_in/](../module/Air8000/demo/network_routing/wifi_out_ethernet_in_wifi_in/)
+224
View File
@@ -0,0 +1,224 @@
# netdrv IKEv2/IPsec 客户端设计文档
> 适用范围:LuatOS `netdrv` 框架下的 IKEv2/IPsec 隧道模式客户端
> ESP tunnel mode)。首期已在 PC 模拟器上对接真实 strongSwan 网关
> `ipsec.air32.cn`strongSwan 5.9.13, Ubuntu 24.04 云主机)联调通过。
---
## 1. 目标与范围
在 netdrv 框架内新增 `netdrv.IPSEC` 虚拟网卡,实现:
- IKEv2 发起端(RFC 7296),服务器证书链校验 + 强制 EAP-MSCHAPv2
- NAT-TRFC 3948):UDP 500 启动,探测到 NAT 后切换 4500;
- DPD 保活、CHILD_SA 到期重协商(CREATE_CHILD_SA,默认带 PFS
KE + 新 Ni/Nr 按 RFC 7296 §2.17 派生 KEYMAT;网关不支持 PFS 时自动
无 KE 重试一次并保持旧 SA 在线)、IKE SA 到期全量重建;
- 套件扩展:IKE DH 组 19/20/21ECP-256/384/521RFC 4753),
ESP AES-GCM-128/256RFC 4106 AEADSA 内无 INTEG transform);
- MOBIKERFC 4555opt-in,默认关闭):本地地址变化时发送
INFORMATIONAL(UPDATE_SA_ADDRESSES + NAT-D),处理对端发起的地址更新,
以及 established 状态下的对端地址学习;不做 ADDITIONAL_IP4_ADDRESS
通告与 COOKIE2
- CPConfiguration Payload)下发虚拟 IPv4 + DNS
- ESP 隧道模式(RFC 4303AES-CBC-128/256 + HMAC-SHA1-96 /
HMAC-SHA2-256-128 或 AES-GCM-128/25632 包反重放窗口,
ESP-in-UDP(4500)。
明确不做:IKEv1、L2TP/IPsec、ESP 传输模式、客户端证书认证、
IPv6 隧道、MOBIKE 的 ADDITIONAL_IP4_ADDRESS 通告与 COOKIE2。
MOBIKE 默认关闭(`ipsec_mobike_enable` 未配置时行为与旧版一致,
地址变化仍走断线重连)。
## 2. 文件布局
代码按用户要求拆分到独立子模块 `components/network/ipsec/`(内部头在
`include/ipsec/`,源文件在 `src/`):
| 文件 | 说明 |
|---|---|
| `include/ipsec/ipsec_crypto.h` + `src/ipsec_crypto.c` | PRF+/HMAC、DH modp2048、IKE/CHILD 密钥派生、AUTH 计算/验签、X.509 链+SAN 校验(信任锚由 `ipsec_ca_cert_pem` 提供;不提供时默认 fail-closed,仅当显式开启 `ipsec_insecure_cert_ok` 才接受证书且仍校验 SAN |
| `include/ipsec/ipsec_esp.h` + `src/ipsec_esp.c` | ESP 隧道封装/解封(AES-CBC + HMAC 或 AES-GCM AEAD)、SPI 方向、32 包反重放 |
| `include/ipsec/ipsec_ike.h` + `src/ipsec_ike.c` | IKEv2 状态机:SA_INIT/AUTH/EAP/CREATE_CHILD_SA/INFORMATIONAL、payload 编解码、SK 加密、NAT-T 切换、DPD、重协商、虚拟 netif 与 adapter 收发 |
| `include/ipsec/ipsec_vendor_md4.h` + `src/ipsec_vendor_md4.c` | MD4Apache-2.0, 从 mbedTLS 2.x vendor, 自包含) |
| `include/ipsec/ipsec_vendor_chap_ms.h` + `src/ipsec_vendor_chap_ms.c` | MS-CHAPv2BSD, 移植自 lwIP2.2 `chap_ms.c`),含 strongSwan 线格式与 RFC 3079 MSK 派生、RFC 2759 测试向量自检 |
| `src/luat_netdrv_ipsec.c` | netdrv 胶水层:setup/ctrl(UPDOWN)/dhcp(-1)/debug + 链路状态回调 |
| `components/network/netdrv/include/luat_netdrv_ipsec.h` | 胶水层头文件(保持在 netdrv/include |
接入点:
- `luat_netdrv_drv.h``LUAT_NETDRV_IMPL_IPSEC 7`
- `luat_netdrv.h``luat_netdrv_ipsec_conf_t` + `conf->ipsec_conf`
- `luat_netdrv.c` / `luat_lib_netdrv.c`setup 分发与 `ipsec_*` 参数解析
- `bsp/pc/xmake.lua`ipsec 子模块头文件搜索路径(`components/network/ipsec/include`
- `bsp/pc/include/luat_conf_bsp.h``LUAT_USE_NETDRV_IPSEC 1`
- `bsp/pc/include/lwipopts.h``MEMP_NUM_SYS_TIMEOUT 30`
- `bsp/pc/port/luat_crypto_mini.c`:PC 模拟器 TRNG 每次调用重新播种
## 3. 架构与数据流
```
Lua (netdrv.setup / socket.*)
|
v
luat_netdrv_ipsec.c (setup/ctrl/dhcp/debug, IP_READY/IP_LOSE 事件)
|
v
ipsec_ike.c (IKEv2 状态机, tcpip 线程)
| netif (虚拟网卡, 全流量默认路由)
| | netif output -> ESP 封装 -> adapter UDP TX(4500)
| | adapter RX(4500) -> IKE(4字节零前缀) 或 ESP -> 内层 IP 注入
|
+-- network_ctrl_t (luat_network_adapter, 单 socket: 500 -> 4500)
```
- IKE 与 ESP 共用同一个 adapter UDP socket:先在 500 上完成
IKE_SA_INITNAT 探测后切换到本地/远端 4500RFC 3948 非 ESP
marker 区分 IKE 与 ESP)。这是标准 NAT-T 形态,避免了双 socket
同端口绑定的问题。
- 所有状态机工作经 `tcpip_callback_with_block` 汇聚到 tcpip 线程;
adapter 回调只拷贝数据并投递,与 L2TP/OpenVPN 客户端一致。
- CP 下发虚拟 IP 后 `netif_set_addr` + `netif_set_default`DNS 写入
`network_set_dns_server`
## 4. 协议要点与互操作细节
- 套件:IKE `{aes256-sha256, aes128-sha1} × {modp2048, ecp256, ecp384,
ecp521}!`(8 个提案,DH14 优先;响应者选中其它组时按 RFC 7296 §2.7
的 INVALID_KE 通知重试,IKE_SA_INIT 默认先发 DH14 的 KE);
ESP `aes256-sha256, aes128-sha1, aes256gcm16, aes128gcm16!`
AEAD 提案无 INTEG transformtransform id 20 + KEY_LENGTH 属性)。
- CHILD_SA 重协商默认带 PFS:请求载荷顺序 `SA | Ni | KE | TSi | TSr`
使用 IKE_SA_INIT 协商出的 DH 组生成新的临时密钥对;响应解析新 Nr 与
KEKEYMAT = prf+(SK_d, g^ir | Ni | Nr)。响应缺少 KE 或带
NO_PROPOSAL_CHOSEN/TS_UNACCEPTABLE 时,自动无 KE 重试一次并保持旧 SA
在线(与旧版行为一致)。
- MOBIKE 更新载荷顺序:`N(UPDATE_SA_ADDRESSES) | N(NAT_DETECTION_SOURCE_IP)
| N(NAT_DETECTION_DESTINATION_IP)`RFC 4555 §2.2),NAT-D 复用
IKE_SA_INIT 的 SHA-1 哈希方式;对端发起的更新用当前本地 IP 校验
目标 NAT-D,不匹配则忽略;established 状态下的地址学习只发生在
密码学验证通过之后:ESP 报文在 ICV/解密成功后、对端
UPDATE_SA_ADDRESSES 在 SK 解密 + NAT-D 校验通过后、已认证的
INFORMATIONAL 响应在 ICV 校验通过后(RFC 4555 §3
未认证报文一律不得改写对端地址)。
- IKE 请求单在途槽位:`pending_msgid/last_tx` 全 client 一份,
DPD/MOBIKE 更新/CREATE_CHILD_SA 任一在途时其余请求让路(下一秒
tick 重试);INFORMATIONAL 响应(DPD/MOBIKE ack)经 SK 解密 +
ICV 校验后才采信;msgid 全链路 32 位(RFC 7296 单调不回绕)。
- IKE SK 载荷:AES-CBC + HMAC**ICV 只覆盖 IKE 报文本身**strongSwan
收到 4500 报文会先剥离 4 字节非 ESP marker 再校验)。
- ESP AEADRFC 4106):8 字节显式 IVGCM IV = salt(4) | IV(8)
AAD = SPI | SEQICV 16 字节,padding 按 4 字节对齐;KEYMAT 每方向为
`enc_key(16/32) + salt(4)`RFC 7296 §3.3.2),无 INTEG 密钥。
- AUTHEAP 模式按 RFC 7296 §2.16 用 MSK 作为共享密钥;
`"Key Pad for IKEv2"` 为 17 字节(不含 NUL)。
- MSK:按 strongSwan 的 RFC 3079 实现——
`master=SHA1(HH|NT-Response|Magic1)`
`recv/send=SHA1(master[0:16]|0x00*40|Magic2/3|0xF2*40)`
`MSK=recv[0:16]|send[0:16]|0x00*32`。
- MS-CHAPv2 线格式采用 strongSwan/Windows 客户端格式:
`opcode|id|ms_length|value_size|...`(区别于 RFC 2759 排版)。
- ESP SPI 方向:发起端**发送**用响应者分配的 SPI(SAr2),**接收**
用自己提议的 SPI(SAi2)。
- 证书链:配置了 `ipsec_ca_cert_pem` 时按该信任锚做链校验并校验
SAN `ipsec.air32.cn`;未配置时默认 **fail-closed**(固件不再内置任何
信任锚),仅当显式设置 `ipsec_insecure_cert_ok=true` 才接受服务器证书,
且仍强制 SAN 匹配。mbedTLS 校验需传 `mbedtls_x509_crt_profile_default`
PEM 缓冲需 NUL 结尾(mbedTLS 3.x 的 PEM 识别条件)。
## 5. 配置参考(Lua
```lua
netdrv.setup(socket.LWIP_USER1, netdrv.IPSEC, {
ipsec_remote_ip = "154.8.159.79", -- 网关 IP(仅字面量)
ipsec_remote_port = 500, -- IKE 端口, 默认 500
ipsec_username = "vpnuser",
ipsec_password = "xxxx",
ipsec_san = "ipsec.air32.cn", -- 服务器 SAN 校验
-- ipsec_ca_cert_pem = "-----BEGIN CERTIFICATE-----...", -- 建议配置信任锚;
-- -- 未配置且未开启 insecure 时无法连接 (fail-closed)
-- ipsec_insecure_cert_ok = false, -- 默认 false; true 表示无 CA 时仅校验 SAN 即接受
ipsec_mtu = 1400,
ipsec_retry_enable = true,
ipsec_retry_base_ms = 1000,
ipsec_retry_max_ms = 60000,
-- ipsec_mobike_enable = true, -- MOBIKE 双向地址更新 (默认关闭)
})
```
`ipsec_mobike_enable`:布尔,默认 `false`。开启后客户端在 1s tick 轮询
本地地址,变化时发送 UPDATE_SA_ADDRESSES;底层 socket 关闭但传输仍在线
时重开同端口 socket 并走更新流程而非全量重建;同时支持对端发起的地址
更新与 established 状态下的地址学习。仅 utest 构建提供测试钩子
`netdrv.ipsec_sim_addr_change(id)` 模拟一次本地地址变更。
## 6. 联调记录(2026-08-12, PC 模拟器 ↔ ipsec.air32.cn
联调期间发现并修复的问题:
| # | 现象 | 根因 | 修复 |
|---|---|---|---|
| 1 | 网关对 IKE_SA_INIT 无响应 | Ni/NAT-D 载荷的 Next Payload 误用 notify 值(16388)而非载荷类型(41) | 改为 `IPSEC_PAYLOAD_NOTIFY` |
| 2 | IKE_AUTH 无响应, 网关日志 `message ID 16777216, expected 1` | msgid 以单字节写入 (`01 00 00 00`) 而非 4 字节大端 | `ike_put32(msg+20, msgid)` |
| 3 | 网关日志 `MAC verification failed` | mbedTLS 3 `aes_crypt_cbc` 会把最后一组密文写回 IV 缓冲, 覆盖了包内 IV | 用独立 IV 缓冲 |
| 4 | 网关日志 `MAC verification failed`(修复 3 后仍失败) | 4500 上 ICV 把 4 字节零前缀算进去了, 而 strongSwan 收到会先剥离 | ICV 只覆盖 IKE 报文 |
| 5 | `trust anchor parse failed -0x2180` | mbedTLS 3.x 仅当缓冲区 NUL 结尾才识别 PEM | 内嵌/用户 PEM 均保留 NUL 结尾 |
| 6 | `cert chain verify failed flags=0xFFFFFFFF` | `mbedtls_x509_crt_verify_with_profile` 的 profile 传 NULL 返回 BAD_INPUT_DATA | 传 `mbedtls_x509_crt_profile_default` |
| 7 | 服务器只发叶子证书, 链校验失败 | strongSwan `leftsendcert=always` 只发叶子;Let's Encrypt 2026 新链 | 内置 YE2/ISRG Root YE/ISRG Root X2 中间 CA |
| 8 | `server AUTH signature verification failed` | 裸 r\|s 转 DER 时长度公式少算 2 字节 | 修正 `total=6+r+s+pad` |
| 9 | 服务器 `INVALID_SYNTAX` 拒绝 EAP Identity 响应 | EAP 载荷构建后未 `p += plen`, 内层为空 | 修复三处 EAP 载荷 |
| 10 | `unhandled MS-CHAPv2 request opcode 1` | 用 RFC 2759 排版解析 strongSwan 的 Challenge | 按 strongSwan 线格式解析 |
| 11 | 最终 AUTH 被拒 | 内层 `"Key Pad for IKEv2"` 用 `sizeof`(含 NUL 18 字节) | 用 17 字节 |
| 12 | 最终 AUTH 被拒(修复 11 后仍失败) | MSK 的 master 用 `sizeof(inner)=79`(实际 67);且 0x36/0x5C 填充与 strongSwan 的 0x00/0xF2 不符;32 字节 key 从 20 字节 digest 越界拷贝 | 按 strongSwan 实现重写 MSK 派生 |
| 13 | ESP 无回包, 网关 `XfrmInNoStates` | ESP SPI 方向反了(发送用了自己提议的 SPI) | 发送用 SAr2 的 SPI |
| 14 | 隧道 IP 与策略不匹配 | CP 的 IPv4 字节序(`ip4_addr_set_u32` 需先 `lwip_htonl` | 恢复 `lwip_htonl(ike_get32())` |
| 15 | 证书校验 `NOT_TRUSTED`flags=0x8 | 网关改用私建 CA`CN=IKEv2 VPN CA`,自签 10 年),strongSwan 只发叶子,内置 ISRG Root X1 / LE 中间链不再适用 | 测试脚本改从 `scripts/ikev2-ca.crt` 读取 CA 作为 `ipsec_ca_cert_pem` 传入;同时按需求移除固件内置信任锚,未配 CA 的默认行为在后续提交中收紧为 fail-closed(需显式 `ipsec_insecure_cert_ok=true` 才接受证书,且仍校验 SAN) |
服务器侧配合项(已处理,用户授权调试):
- 云主机 `dirtyfrag.conf` 禁用 `esp4` 内核模块(`install esp4 /bin/false`),
导致 XFRM SAD 安装失败(`netlink error: Requested type not found (93)`):
已注释该规则并加载 `esp4`/`xfrm_user`/`xfrm4_tunnel`
- `ipsec.secrets` 私钥声明为 `: RSA` 但实际是 ECDSA P-384
已改为 `: ECDSA`
- 联调后 `charondebug` 已恢复 `ike 2``esp=aes256-sha256, aes128-sha1!`
已恢复原样。
## 7. 测试结果(PC 模拟器, testcase/unit/net/netdrv_ipsec_basic
- `connect`IKE_SA_INIT → 证书链+SAN 校验 → 服务器 AUTH 验签 →
EAP-MSCHAPv2(含服务器 Authenticator Response 校验)→ 双端 AUTH →
CP 下发虚拟 IP/DNS → 隧道上线 → 隧道内 TCP 到网关 SSH 收到 banner
**3 passed / 0 failed**
- `badpass`:错误密码 → EAP 失败 → 不 ready**通过**
- `sanit`:错误 SAN → 证书校验拒绝 → 不 ready,**通过**。
> 前置:`testcase/unit/net/netdrv_ipsec_basic/scripts/ikev2-ca.crt`
> (网关私建 CA)必须存在,测试会把它作为 `ipsec_ca_cert_pem` 传入;
> 测试账号凭据通过环境变量 `LUAT_IPSEC_USERNAME` / `LUAT_IPSEC_PASSWORD`
> 注入(不随源码分发),未设置时测试跳过。
> 固件默认 fail-closed:未传 `ipsec_ca_cert_pem` 且未显式开启
> `ipsec_insecure_cert_ok` 时拒绝连接(固件不再内置任何信任锚)。
## 8. 遗留与后续
- 集成联调(需网关侧重配,未完成项见交付说明):
- PFS:网关 `pfs=yes` + 缩短 lifetime,验证重协商日志、DPD、隧道不断流;
- ECP/GCM:网关 `ike=aes256-sha256-ecp256!` / `esp=aes256gcm16!` 建连;
- MOBIKE`netdrv.ipsec_sim_addr_change` 触发 UPDATE_SA_ADDRESSES
网关 `mobike=no` 时回退重连。
- 硬件 BSP:需确认目标板 lwip 导出符号(`pbuf/netif/sys/ip4_input`
与 adapter 行为,并按验收清单在目标板重跑;
- 服务器 FORWARD 链的 IPsec 池 ACCEPT 规则与 `rightsourceip`
段需按实际网络调整(当前云主机 iptables 规则与池配置不完全一致,
隧道到网关本机可用,跨子网转发需核对);
- `netdrv.debug` 会输出 IKE/ESP 帧日志(含 SK 明文转储仅调试用)。
## 9. 相关文档
- RFC 7296IKEv2)、RFC 3948NAT-T)、RFC 4303ESP)、
RFC 2759/3079MS-CHAPv2/MSK
- `docs/l2tp-design.md`(线程模型/胶水层/构建接线参考)
- `components/network/l2tp/src/l2tp_client.c` /
`components/network/openvpn/src/ovpn_client.c`(传输与 netif 参考)
+290
View File
@@ -0,0 +1,290 @@
# VFS 统一测试 — 已知问题 / Bug 列表
本文件由 `testcase/utest/fs/vfs_uniform/` 框架自动/手动追加, 汇总 vfs 统一接口在
6 个文件系统 (ram/posix/lfs2/fatfs/tfs/pgfs) 上的行为差异与已发现 bug.
## 格式
```markdown
### <FS>::<test_name> [severity]
- **Expected**: 一句话预期行为
- **Actual**: 一句话实际行为
- **Source**: 源文件:行号 (定位)
- **Repro**:
1. 步骤 1
2. 步骤 2
- **Tags**: fs=<fs>, mode=<mode>, feature=<area>
```
## 索引
- [ram](#ram)
- [posix](#posix)
- [lfs2](#lfs2)
- [fatfs](#fatfs)
- [tfs](#tfs)
- [pgfs](#pgfs)
---
## ram
(empty)
## posix
### posix::test_dir_nested_mkdir_auto_parent [med]
- **Expected**: `io.mkdir("a/b/c")` 在父目录 b 不存在时应自动创建 (与 ram 行为一致)
- **Actual**: `io.mkdir` 失败, 不创建父目录
- **Source**: `luat/vfs/luat_fs_posix.c:200-208` (直接调原生 `mkdir`, 不会递归创建父目录)
- **Repro**:
1. `io.mkdir("vfs_dir_a/b/c")` (b 不存在)
- **Tags**: fs=posix, mode=mkdir, feature=dir
### posix::test_edge_deep_nesting [low]
- **Expected**: `io.mkdir` 应能创建 7 级嵌套目录 (a/b/c/d/e/f/g)
- **Actual**: `io.mkdir` 失败, 因为中间父目录 (a, a/b, …) 不存在
- **Source**: `luat/vfs/luat_fs_posix.c:200-208` (同上, 单一 `mkdir` 调用)
- **Repro**:
1. `io.mkdir("vfs_edge_deep/a/b/c/d/e/f/g")`
- **Tags**: fs=posix, mode=mkdir, feature=dir
### posix::test_meta_rename_overwrite [low]
- **Expected**: `os.rename` 覆盖已存在目标文件应成功
- **Actual**: `os.rename` 失败, 源文件保持不变
- **Source**: `luat/vfs/luat_fs_posix.c:105-112` (直接调原生 `rename`; Windows 不允许 overwrite existing target)
- **Repro**:
1. `mkdir d; io.open("d/from.txt","wb"); io.open("d/to.txt","wb")`
2. `os.rename("d/from.txt", "d/to.txt")`
- **Tags**: fs=posix, mode=rename, feature=meta, platform=windows
## lfs2
> lfs2 跑通 26/30 (跳过 1 个 C13 嵌套 mkdir). 真实 bug 4 个 (含 2 个 refcount 缺失).
### lfs2::test_edge_long_filename [low]
- **Expected**: 60 字符文件名应可创建 (与 ram 一致)
- **Actual**: 60 字符文件名 open 失败
- **Source**: `luat/vfs/luat_fs_lfs2.c` (lfs2 默认 name_max=63, 但 PC 配置/驱动对长名更敏感)
- **Repro**:
1. `io.open("/lfs2/vfs_edge_<60 chars>.txt", "wb")`
- **Tags**: fs=lfs2, mode=create, feature=long-name
### lfs2::test_edge_deep_nesting [low]
- **Expected**: mkdir a/b/c/d/e/f/g 7 级嵌套应成功
- **Actual**: 失败
- **Source**: `luat/vfs/luat_fs_lfs2.c:206-223` (mkdir 不自动创建父目录)
- **Repro**:
1. `io.mkdir("/lfs2/vfs_edge_deep/a/b/c/d/e/f/g")`
- **Tags**: fs=lfs2, mode=mkdir, feature=deep-nesting
### lfs2::test_refcount_remove_open_fails [med]
- **Expected**: 打开中的文件不应被 os.remove 删除
- **Actual**: os.remove 成功了
- **Source**: `luat/vfs/luat_fs_lfs2.c:134` (直接调 `lfs_remove`, 不检查引用计数)
- **Repro**:
1. 创文件 p
2. `f = io.open(p, "r")`
3. `os.remove(p)` -- 期望失败
4. `f:close()`
- **Tags**: fs=lfs2, mode=read+remove, feature=refcount
### lfs2::test_refcount_rename_open_fails [med]
- **Expected**: 打开中的源文件不应被 os.rename 重命名
- **Actual**: os.rename 成功了
- **Source**: `luat/vfs/luat_fs_lfs2.c:134` (rename 路径同样不检查引用)
- **Repro**:
1. 创文件 src
2. `f = io.open(src, "r")`
3. `os.rename(src, dst)` -- 期望失败
4. `f:close()`
- **Tags**: fs=lfs2, mode=read+rename, feature=refcount
## fatfs
### fatfs::setup_module_not_found [high]
- **Expected**: 当 `fatfs` 模块在 PC BSP 中不可用时, `mount_fatfs.setup()` 应返回 false 让 `main.lua` 自跳过整个测试套
- **Actual**: `mount_fatfs.lua:9``require("fatfs")` 在 PC BSP 上直接抛出 "module 'fatfs' not found", VM 在 `main.lua` 检测 `fs_ok` 之前就崩溃退出, 测试套根本没有 self-skip 机会
- **Source**: `testcase/utest/fs/vfs_uniform/scripts/mount_fatfs.lua:9`
- **Repro**:
1. 在 PC 模拟器上跑 `vfs_uniform_fatfs`
2. `mount_fatfs.setup()``require("fatfs")` 失败, 抛错
3. 错误沿着 `require``vfs_uniform_mount.lua -> mount_fatfs.lua` 一路冒泡, VM 退出
4. `main.lua``if not fs_ok then ... os.exit(0) end` 永远不会执行
- **Tags**: fs=fatfs, mode=pc, feature=framework-bootstrap
### fatfs::pc_build_fatfs_module_unavailable [high]
- **Expected**: PC BSP 应当能 `require("fatfs")` 加载内置 fatfs 模块 (因为 `LUAT_USE_FATFS` 已在 `bsp/pc/include/luat_conf_bsp.h:145` 定义, 且 `luaopen_fatfs``bsp/pc/port/luat_base_mini.c:160` 注册)
- **Actual**: 当前 PC 二进制 `bsp/pc/build/out/luatos-lua.exe``require("fatfs")` 返回 "module not found", 同样 `require("uart")`/`require("json")` 等内置模块也都失败; 只有 `io`/`os`/`string`/`log`/`rtos` 这类 globals 仍可用. 整个 fatfs 测试套无法在 PC 上执行
- **Source**: `bsp/pc/port/luat_base_mini.c:159-161` (注册) vs PC BSP 实际加载行为
- **Repro**:
1.`print(type(require("fatfs")))` 的最小 main.lua
2.`bsp/pc/build/out/luatos-lua.exe ... /tmp/test/`
3. 输出 `module 'fatfs' not found` 然后 `Lua VM exit!! reboot in 1000ms`
- **Tags**: fs=fatfs, mode=pc, feature=module-registration
## tfs
### tfs::tfs_mount_format_name_marker [high]
- **Expected**: `lf.mount(flash, "/tfs0", 0, 256*1024, {fs="tfs"})` 在 PC 模拟器空白 flash 上应返回 true, mount 后 VFS 即可在 /tfs0 上读写文件
- **Actual**: 第一次 `lf.mount` 返回 false, 日志显示 `tfs: format ret=0 read_errors=0` (format 成功) 紧接着 `tfs: open name marker failed``tfs: format mount failed`, 之后才出现 `tfs: anchor written chunk=0 seq=4096`; 二次 mount 找到 anchor 但仍 `tfs: open name marker failed` 触发 `tfs: probe failed, reformatting`, 再次 `tfs: open name marker failed``tfs: format or mount failed`
- **Source**: `components/little_flash/luat_little_flash_tfs.c:478` (`lf_tfs_write_name_marker``tfs_open` 失败) 与 `:1226` (`tfs: format mount failed`)
- **Repro**:
1. 在 PC 上跑 `D:/github/LuatOS/bsp/pc/build/out/luatos-lua.exe` 并加载 vfs_uniform_tfs/scripts
2. `mount_tfs.lua``spi.deviceSetup(1,255,...)` + `lf.init(spidev)` + `lf.mount(flash,"/tfs0",0,256*1024,{fs="tfs"})`
3. 观察: format ret=0 → open name marker failed → format mount failed, 紧接着 anchor 写入但 mount 仍失败
- **Tags**: fs=tfs, mode=mount, feature=format-name-marker
### tfs::all_tests_skipped_mount_unavailable [high]
- **Expected**: 30 个共享用例应在 /tfs0 上运行, 大部分通过
- **Actual**: 30 个用例全部 0/0 跳过, 因为 tfs mount 失败直接 `os.exit(0)`, 没有任何用例被执行
- **Source**: 测试 `main.lua``mount.setup()` 返回 false 时退出; 根因是 tfs mount bug
- **Repro**:
1. 跑 vfs_uniform_tfs (与上面 tfs_mount_format_name_marker 同一触发条件)
2. 日志最后两行: `tfs mount 失败` + `tfs FS 不可用, 退出`
- **Tags**: fs=tfs, mode=run, feature=suite-execution
## pgfs
> pgfs mounted successfully on PC simulator (`/pgfs0`, 256KB partition, virtual NAND
> W25N01GVZEIG). However the FTL has very few free blocks at mount time (`next=1, total=2,
> bad=0`), and the GC allocator reports `alloc_segment: no free blocks` whenever a write
> triggers a new segment allocation. This causes most write/read roundtrip assertions to
> fail with 0-byte content. Each test below is a separate symptom of the same root cause
> (or, in two cases, an independent semantic bug).
### pgfs::test_refcount_remove_open_fails [med]
- **Expected**: 打开中的文件不应被 os.remove 删除 (refcount 应阻止 remove)
- **Actual**: os.remove 成功了 (pgfs 直接删除了正在被读取的文件)
- **Source**: `components/pgfs/luat_pgfs_posix.c` (remove 路径未检查 refcount, 沿用 lfs2 的旧语义)
- **Repro**:
1. `io.open("/pgfs0/vfs_refcount_rm.txt", "wb")` 然后 close
2. `f = io.open("/pgfs0/vfs_refcount_rm.txt", "r")`
3. `os.remove("/pgfs0/vfs_refcount_rm.txt")` -- 期望失败, 实际成功
4. `f:close()`
- **Tags**: fs=pgfs, mode=read+remove, feature=refcount
### pgfs::test_meta_rename_overwrite [low]
- **Expected**: rename 覆盖已存在的目标文件应成功 (POSIX 行为)
- **Actual**: rename 失败, 目标文件保持旧内容
- **Source**: `components/pgfs/luat_pgfs_posix.c` rename 实现 (未先 unlink 目标)
- **Repro**:
1. `io.mkdir("/pgfs0/d")`
2.`/pgfs0/d/from.txt` (内容 "new") 和 `/pgfs0/d/to.txt` (内容 "old")
3. `os.rename("/pgfs0/d/from.txt", "/pgfs0/d/to.txt")` -- 期望成功, 实际失败
- **Tags**: fs=pgfs, mode=rename, feature=metadata
### pgfs::test_basic_write_read_roundtrip [high]
- **Expected**: 写入 "hello" 后读取应得到 "hello"
- **Actual**: 读出为空字符串 (write 静默失败, 文件大小为 0)
- **Source**: `components/pgfs/pgfs_alloc_gc.c:78` `alloc_segment: no free blocks`
+ `components/pgfs/luat_pgfs_posix.c` (write 路径未检测 segment 分配失败)
- **Repro**:
1. mount pgfs (256KB)
2. `f = io.open("/pgfs0/x.txt", "wb"); f:write("hello"); f:close()`
3. `f = io.open("/pgfs0/x.txt", "rb"); f:read("*a")` -- 期望 "hello", 实际 ""
- **Tags**: fs=pgfs, mode=wb/rb, feature=write-data-loss
### pgfs::test_basic_write_read_large [high]
- **Expected**: 写入 64KB 数据后读回应得到 65536 字节
- **Actual**: 读出 0 字节 (write 静默失败, 因 segment 分配失败)
- **Source**: 同 test_basic_write_read_roundtrip (FTL 没有空闲块)
- **Repro**:
1. `f = io.open("/pgfs0/big.bin", "wb"); f:write(<64KB>); f:close()`
2. `io.open("/pgfs0/big.bin", "rb"):read("*a")` -- 期望 65536 字节, 实际 0
- **Tags**: fs=pgfs, mode=wb/rb, feature=large-write-data-loss
### pgfs::test_edge_block_size_boundary [high]
- **Expected**: 4096 字节精确写入读回后, 大小和内容完全一致
- **Actual**: 读出 0 字节 (page-aligned 4KB 写入也静默失败)
- **Source**: 同 test_basic_write_read_roundtrip (FTL 没有空闲块)
- **Repro**:
1. `f = io.open("/pgfs0/block.bin", "wb"); f:write(<4096 bytes>); f:close()`
2. `f = io.open("/pgfs0/block.bin", "rb"); f:read("*a")` -- 期望 4096, 实际 0
- **Tags**: fs=pgfs, mode=wb/rb, feature=block-boundary
### pgfs::test_basic_seek_tell [high]
- **Expected**: 写入 100 字节, seek('set', 50) 后 tell() 返回 50, 读出第 51 字节 = 50
- **Actual**: seek() 返回 0, read 返回空 (文件为 0 字节, write 静默失败)
- **Source**: 同 test_basic_write_read_roundtrip (根因 = FTL 段分配失败)
- **Repro**:
1. `f = io.open("/pgfs0/x.bin", "wb"); f:write(<100 bytes>); f:close()`
2. `f = io.open("/pgfs0/x.bin", "rb"); f:seek("set", 50); print(f:seek())` -- 期望 50, 实际 0
- **Tags**: fs=pgfs, mode=rb, feature=seek-data-loss
### pgfs::test_basic_seek_end [high]
- **Expected**: 写入 100 字节后 seek('end') 给出位置 100
- **Actual**: seek() 返回 0 (文件为空, write 失败)
- **Source**: 同 test_basic_write_read_roundtrip (根因 = FTL 段分配失败)
- **Repro**:
1. `f = io.open("/pgfs0/x.bin", "wb"); f:write("x"*100); f:close()`
2. `f = io.open("/pgfs0/x.bin", "rb"); f:seek("end"); print(f:seek())` -- 期望 100, 实际 0
- **Tags**: fs=pgfs, mode=rb, feature=seek-end
### pgfs::test_basic_seek_cur [high]
- **Expected**: seek('end') 后 seek('cur', -10) 给出位置 90
- **Actual**: seek() 返回 0 (文件为空, write 失败)
- **Source**: 同 test_basic_write_read_roundtrip (根因 = FTL 段分配失败)
- **Repro**:
1. `f = io.open("/pgfs0/x.bin", "wb"); f:write("x"*100); f:close()`
2. `f = io.open("/pgfs0/x.bin", "rb"); f:seek("end"); f:seek("cur", -10); print(f:seek())`
-- 期望 90, 实际 0
- **Tags**: fs=pgfs, mode=rb, feature=seek-cur
### pgfs::test_meta_fsize [high]
- **Expected**: 写入 1234 字节, fs.fsize 应返回 1234
- **Actual**: fs.fsize 返回 0 (write 静默失败)
- **Source**: 同 test_basic_write_read_roundtrip (根因 = FTL 段分配失败)
- **Repro**:
1. `f = io.open("/pgfs0/x.bin", "wb"); f:write("Z"*1234); f:close()`
2. `fs.fsize("/pgfs0/x.bin")` -- 期望 1234, 实际 0
- **Tags**: fs=pgfs, mode=wb, feature=fsize
### pgfs::test_meta_rename_file [high]
- **Expected**: rename a->b 应成功, 之后 b 存在且内容是 "data-a"
- **Actual**: rename 返回 err=2 (源文件不存在 / 写入失败, 链式失败)
- **Source**: 同 test_basic_write_read_roundtrip (写入失败导致源文件未创建, rename 报 ENOENT)
- **Repro**:
1. `f = io.open("/pgfs0/a.txt", "wb"); f:write("data-a"); f:close()`
2. `os.rename("/pgfs0/a.txt", "/pgfs0/b.txt")` -- 期望 true, 实际 err=2
- **Tags**: fs=pgfs, mode=rename, feature=metadata-chain-failure
### pgfs::test_basic_append_mode [high]
- **Expected**: "foo" + append "bar" 后读出 "foobar"
- **Actual**: 读出空字符串 (write 静默失败)
- **Source**: 同 test_basic_write_read_roundtrip (根因 = FTL 段分配失败)
- **Repro**:
1. `f = io.open("/pgfs0/x.txt", "wb"); f:write("foo"); f:close()`
2. `f = io.open("/pgfs0/x.txt", "ab"); f:write("bar"); f:close()`
3. `io.open("/pgfs0/x.txt", "rb"):read("*a")` -- 期望 "foobar", 实际 ""
- **Tags**: fs=pgfs, mode=ab, feature=append-data-loss
### pgfs::test_posix_mode_w_plus [high]
- **Expected**: "w+" 打开后 write("xyz") 再 seek(0) read 应得到 "xyz"
- **Actual**: read 返回空字符串 (write 静默失败)
- **Source**: 同 test_basic_write_read_roundtrip (根因 = FTL 段分配失败)
- **Repro**:
1. `f = io.open("/pgfs0/x.txt", "w+"); f:write("xyz"); f:seek("set", 0); print(f:read("*a"))`
-- 期望 "xyz", 实际 ""
- **Tags**: fs=pgfs, mode=w+, feature=mode-flags
### pgfs::test_posix_mode_r_plus [high]
- **Expected**: "r+" 打开已有文件后 write + seek + read 应得到原长度 (首字节被覆盖)
- **Actual**: read 返回 0 字节字符串 (写时静默失败, 文件被截断/清空)
- **Source**: 同 test_basic_write_read_roundtrip (根因 = FTL 段分配失败) +
`components/pgfs/luat_pgfs_posix.c` (r+ 模式 open 后的截断/清空行为)
- **Repro**:
1. `f = io.open("/pgfs0/x.txt", "wb"); f:write("initial"); f:close()`
2. `f = io.open("/pgfs0/x.txt", "r+"); f:write("X"); f:seek("set", 0); print(f:read("*a"))`
-- 期望 "Xniti..."(7 字节), 实际 ""
- **Tags**: fs=pgfs, mode=r+, feature=mode-flags
### pgfs::test_posix_binary_text_same [high]
- **Expected**: 写入 0x0A 0x0D 0x00 0xFF 后 read(4) 读出 4 字节
- **Actual**: read(4) 返回 nil (文件为空, write 静默失败)
- **Source**: 同 test_basic_write_read_roundtrip (根因 = FTL 段分配失败)
- **Repro**:
1. `f = io.open("/pgfs0/x.bin", "wb"); f:write("\x0A\x0D\x00\xFF"); f:close()`
2. `f = io.open("/pgfs0/x.bin", "rb"); f:read(4)` -- 期望 4 字节, 实际 nil
- **Tags**: fs=pgfs, mode=rb, feature=binary-mode
+322
View File
@@ -0,0 +1,322 @@
# netdrv L2TPv2 客户端驱动设计文档
> 适用范围:在 LuatOS `netdrv` 框架下新增 L2TPv2RFC 2661)客户端驱动(LAC),
> 当前首期落地 PC 模拟器(`bsp/pc`),硬件 BSP 按需开启 `LUAT_USE_NETDRV_L2TP`。
---
## 1. 背景与目标
LuatOS 需要在内网/办公网场景下提供「二层隧道 + PPP 拨号」的能力,典型形态是
接入企业 LNSL2TP Network Server)获得内网 IP 并访问内网资源。
需求边界(与 lwip 自带 `pppol2tp.c` 的能力边界一致):
- ✅ L2TPv2 客户端(LAC),PPP over L2TPUDP 隧道)
- ✅ PPP 认证:PAP + CHAP(MD5);可选 L2TP 隧道共享密钥认证(`l2tp_secret`
- ❌ LNS(服务端)、L2TPv3、隧道切换、多隧道/多会话
- ❌ MSCHAPv2 / MPPE / EAP / CCP
核心约束:**外层 UDP 传输一律走 LuatOS network adapter**`network_ctrl_t` UDP
模式,与 OpenVPN 客户端一致),不直接创建 lwip `udp_pcb`。这样 VPN 虚拟网卡与
底层物理网卡(4G/WiFi/ETH/CH390H)完全解耦,底层可以是任意 netdrv 适配器。
---
## 2. 总体设计
```
Lua 脚本 (netdrv.setup / netdrv.ready / socket.*)
|
+------------------v------------------+
| luat_netdrv_l2tp.c (胶水层) |
| setup/ctrl(UPDOWN)/dhcp/debug |
| PPP 状态回调 -> net_lwip2 链路状态 + |
| IP_READY / IP_LOSE 事件 |
+------------------+------------------+
|
+------------------v------------------+
| components/network/l2tp/src (核心) |
| - l2tp_client.c: 生命周期/定时器/传输 |
| - l2tp_ctrl.c: 控制面 (SCCRQ..StopCCN,|
| ns/nr 窗口, 定时重传) |
| - l2tp_ppp.c: PPP 数据面封装 + 状态回调|
+-------+---------------+------------+
| |
network_ctrl_t (UDP) vendored lwIP PPP 栈
luat_network_adapter l2tp/src/ppp/*.c
| |
底层物理网卡(4G/WiFi/ETH) ppp_pcb / netif
```
### 2.1 分层
1. **胶水层**`luat_netdrv_l2tp.c`):把 L2TP 客户端包装成标准 `luat_netdrv_t`
驱动,负责配置拷贝、`LUAT_NETDRV_CTRL_UPDOWN` 控制、链路状态上报。
2. **客户端核心**`components/network/l2tp/src/`,按协议层拆为三个文件):
- `l2tp_client.c`:生命周期/状态机/定时器/网络适配器传输
`l2tp_udp_send``l2tp_timeout`、重连退避、`l2tp_client_*` API);
- `l2tp_ctrl.c`L2TPv2 控制面从 lwip22 `netif/ppp/pppol2tp.c` 移植,
仅替换 UDP 收发为 `network_tx/network_rx`,去掉 `udp_pcb` 与传输
netif 参数(线格式/AVP 构造解析、ns/nr 窗口、定时重传);
- `l2tp_ppp.c`PPP 会话运行在 vendor 的 lwIP PPP 栈上,
`struct link_callbacks``write/netif_output` 走 L2TP 数据封装,
并处理 `ppp_status_cb` 链路状态回调。
- 跨文件原型在 `components/network/l2tp/include/l2tp/``l2tp_ctrl.h` /
`l2tp_ppp.h`),公共客户端 API 在 `l2tp/l2tp_client.h`
3. **PPP 栈**`components/network/l2tp/src/ppp/`):lwIP 2.2.1
`ppp.c/lcp.c/ipcp.c/auth.c/fsm.c/upap.c/chap-new.c/chap-md5.c/magic.c/utils.c`
的 vendor 副本(保留 BSD 许可头,注明来源),由构建系统按需编译。
### 2.2 线程模型
- `create/connect/close``ppp_input` 的投递统一 marshal 到 lwip core 线程
`tcpip_callback_with_block`,模式同 WG 驱动的 `exec_netif_add`);
- network adapter 回调(运行在 adapter 任务/Lua 主线程)内**只做数据拷贝**,
通过 `tcpip_callback` 投递给 tcpip 线程,PPP 状态机绝不在 adapter 线程执行;
- `transport_err` 标志由 adapter 回调置位,由 500ms 周期定时器(tcpip 线程)
消费,避免跨线程直接操作 PPP 状态机。
### 2.3 就绪信号
PPP 的 `sifup()``np_up()` 把 phase 推进到 `PPP_PHASE_RUNNING` **之前**就会
回调 `link_status_cb(PPPERR_NONE)`,因此驱动的 ready 判定使用 `ppp->if4_up`
`sifup` 内部先置位再回调),而不是 phase==RUNNING,否则会出现一次误报
`IP_LOSE` 且不再上报 `IP_READY` 的竞态。
---
## 3. 方案选择与取舍
### 3.1 传输层:network adapter UDP 而非 lwip `udp_pcb`
| 方案 | 结论 |
|---|---|
| 直接创建 lwip `udp_pcb` 走底层 netif | 与 4G/WiFi/ETH 等 LuatOS 自有适配器耦合,需要额外的 netif 路由/ARP 处理,硬件差异大 |
| `network_ctrl_t` UDP(本方案) | 与 OpenVPN 客户端一致,底层网卡即插即用;天然支持「LNS 从随机源端口应答」(posix 适配器不 connect UDP socket |
代价:多一次 adapter 层数据拷贝,VPN 隧道吞吐受 adapter 缓冲限制
`network_tx/rx` 单包缓冲,L2TP 数据面当前使用 1600B 栈缓冲)。
### 3.2 PPP 栈:vendor lwip22 源码 vs 自己实现
lwip22 自带完整的 `ppp.c` 系列(LCP/IPCP/PAP/CHAP/FSM/魔法数),直接 vendor
`components/network/l2tp/src/ppp/` 并配套两个移植补丁:
1. **`ppp_pcb` 改用 `mem_malloc/mem_free`**:本仓库 lwip22 的 `memp_std.h`
裁剪掉了 PPP/PPPOL2TP 内存池,vendor 副本内自行 `LWIP_MEMPOOL_DECLARE`
可以编译,但为减少静态池开销并贴合本仓库约定,改为堆分配。
2. **MD5 走外部 mbedTLS**`LWIP_USE_EXTERNAL_MBEDTLS=1``lwip_md5_*`
映射到 LuatOS 已有 mbedtls`MBEDTLS_MD5_C` 在 PC 与硬件配置均已启用),
不再编译 lwip 内嵌 PolarSSL 副本。
### 3.3 编译选项覆盖:`luat_ppp_opts_override.h`
`bsp/pc/include/lwipopts.h` 写死 `PPP_SUPPORT 0`,且会**无条件重定义**,因此
per-file 命令行宏会被覆盖;同时 `ppp_opts.h``#if PPP_SUPPORT` 块在 override
生效之前求值(其中定义的 FSM/UPAP/CHAP/LCP 超时、`MAXNAMELEN/MAXSECRETLEN`
`PPP_MRU``PPP_IPV4_SUPPORT` 等全部缺失)。
解决办法:xmake 仅对 vendor PPP 文件与 L2TP 客户端文件附加
`LUAT_L2TP_PPP_BUILD=1`,并在 `ppp_opts.h` 之后强制 include
`luat_ppp_opts_override.h`,统一恢复特性集:
```
PPP_SUPPORT=1, PPPOL2TP/PPPOS/PPPOE=0, LWIP_PPP_API=0,
PAP_SUPPORT=1, CHAP_SUPPORT=1, MSCHAP/EAP/CCP/MPPE/VJ/LQR=0,
PPP_IPV4_SUPPORT=(LWIP_IPV4), PPP_IPV6_SUPPORT=0, PPP_SERVER=0,
LWIP_USE_EXTERNAL_MBEDTLS=1, MEMP_NUM_PPP_PCB=1
```
配套提供 `components/network/l2tp/include/ppp_settings.h``lwip22/include/arch/cc.h`
无条件定义 `PPP_INCLUDE_SETTINGS_HEADER`,但仓库从未提供该 port 头文件
(PPP 此前从未编译),本文件为空实现(选项全部走 override 头)。
> 注意:这些宏只作用于 vendor PPP 文件与 L2TP 客户端文件,**不是全局宏**。
> 全局开启 `PPP_SUPPORT` 会触发 `lwip22/core/init.c` 的
> `#error "PPP_SUPPORT needs at least one of PPPOS_SUPPORT..."` 编译检查。
### 3.4 L2TP 控制面:自实现(`PPPOL2TP_SUPPORT=0`
lwip 的 `pppol2tp.c``udp_pcb` 强耦合,且其头文件要求
`PPPOL2TP_SUPPORT=1`。本驱动把控制面逻辑平移到
`l2tp_ctrl.c`(常量、AVP 构造/解析、ns/nr 窗口、定时重传均照搬),
传输改为 adapter 调用,因此 vendor PPP 文件以 `PPPOL2TP_SUPPORT=0` 编译,
避免 `pppol2tp.c` 参与编译。
### 3.5 认证与隧道密钥
- `ppp_set_auth(ppp, PPPAUTHTYPE_ANY, user, passwd)`PAP/CHAP-MD5 均可;
- `l2tp_secret` 非空时按 RFC 2661 在 SCCRQ/SCCRP/SCCCN 中携带
Challenge / Challenge-Response AVPMD5(消息类型|secret|challenge))。
- 认证选项由**对端**在 LCP ConfReq 中提出:lwip 客户端的 LCP ConfReq 本身
不携带 auth 选项(`lcp_addci` 使用 gotoptions,客户端侧该位不置位),
因此 LNS 必须主动要求认证(真实 LNS 行为一致)。
### 3.6 断线检测与自动重连
- **PPP 层**LCP keepalive3s 间隔、3 次失败)检测静默断线(对端不响应);
对端 StopCCN 在 DATA 阶段不主动断开(沿用 pppol2tp 语义),由 keepalive 兜底。
- **传输层**`EV_NW_RESULT_CLOSE`/`Param1!=0``transport_err`500ms 周期
定时器消费并触发重连。
- 重连策略:指数退避(base 1s,max 60s,可配置),传输离线时按 base 轮询;
用户主动 `CTRL_UPDOWN=0` 不重连。
- 在线判定使用 setup 时捕获的 `transport_index`,而非运行时默认 adapter
netdrv 虚拟网卡注册后 `network_register_get_default()` 会变成 USERx 自身,
若用运行时默认值会导致「传输离线」误判。
### 3.7 数据面 MTU 与缓冲
PPP MRU 默认 1450`L2TP_DEFAULT_MTU`,可用 `l2tp_mtu` 配置);L2TP 数据头 6B
flags + tunnel id + session id)。发送侧把 pbuf 拼成扁平缓冲后走
`network_tx`(1600B 栈缓冲),接收侧同样扁平缓冲 -> pbuf -> `ppp_input`
---
## 4. 文件与构建接线
### 4.1 新增/修改文件
| 文件 | 说明 |
|---|---|
| `components/network/l2tp/include/l2tp/l2tp_client.h` | L2TP 客户端对外结构/API、协议常量 |
| `components/network/l2tp/include/l2tp/l2tp_ctrl.h` | 控制面跨文件原型(内部) |
| `components/network/l2tp/include/l2tp/l2tp_ppp.h` | PPP 胶水跨文件原型(内部) |
| `components/network/l2tp/src/l2tp_client.c` | 客户端核心(生命周期/定时器/传输/重连) |
| `components/network/l2tp/src/l2tp_ctrl.c` | 控制面(线格式/AVP/ns-nr 窗口/定时重传) |
| `components/network/l2tp/src/l2tp_ppp.c` | PPP 胶水(link_callbacks + 状态回调) |
| `components/network/netdrv/include/luat_netdrv_l2tp.h` | netdrv 胶水层头文件 |
| `components/network/l2tp/src/luat_netdrv_l2tp.c` | netdrv 胶水层(setup/ctrl/dhcp/debug/状态回调) |
| `components/network/l2tp/src/ppp/*.c` | vendored lwip 2.2.1 PPP 源码(10 个文件) |
| `components/network/l2tp/src/ppp/luat_ppp_opts_override.h` | PPP 编译选项覆盖 |
| `components/network/l2tp/include/ppp_settings.h` | lwip `PPP_INCLUDE_SETTINGS_HEADER` port 钩子 |
| `components/network/netdrv/include/luat_netdrv.h` | `luat_netdrv_l2tp_conf_t` + `conf.l2tp_conf` |
| `components/network/netdrv/include/luat_netdrv_drv.h` | `LUAT_NETDRV_IMPL_L2TP 6` + setup 声明 |
| `components/network/netdrv/src/luat_netdrv.c` | setup 分发新增 L2TP 分支 |
| `components/network/netdrv/binding/luat_lib_netdrv.c` | `l2tp_*` 配置解析 + `netdrv.L2TP` 常量 |
| `bsp/pc/include/luat_conf_bsp.h` | `LUAT_USE_NETDRV_L2TP 1` |
| `bsp/pc/include/lwipopts.h` | `MEMP_NUM_SYS_TIMEOUT` 17->24PPP/L2TP 定时器预算) |
| `bsp/pc/xmake.lua` | lwip22 PPP 源码剔除 + vendor PPP 编译 |
| `testcase/unit/net/netdrv_l2tp_basic/` | 测试套件 + Python mock LNS |
### 4.2 xmake 接线要点(`bsp/pc/xmake.lua`
```lua
-- 剔除 lwip22 自带 PPP(避免与 vendor 副本重复编译)
remove_files(lwip_path .. "netif/ppp/**.c")
-- netdrv 核心(VPN 子模块已拆出为独立目录)
add_files(luatos .. "components/network/netdrv/**.c")
-- L2TP 子模块:头文件路径 + vendor PPP + 客户端按文件附加编译宏
add_includedirs(luatos .. "components/network/l2tp/include")
add_includedirs(luatos .. "components/network/l2tp/src/ppp")
add_files(luatos .. "components/network/l2tp/src/ppp/*.c",
{defines = {"LUAT_L2TP_PPP_BUILD=1"}})
add_files(luatos .. "components/network/l2tp/src/l2tp_client.c",
luatos .. "components/network/l2tp/src/l2tp_ctrl.c",
luatos .. "components/network/l2tp/src/l2tp_ppp.c",
{defines = {"LUAT_L2TP_PPP_BUILD=1"}})
add_files(luatos .. "components/network/l2tp/src/luat_netdrv_l2tp.c")
```
### 4.3 Lua 用法
```lua
netdrv.setup(socket.LWIP_USER1, netdrv.L2TP, {
l2tp_remote_ip = "10.0.0.1", -- 必填,LNS IP(仅 IP 字面量,同 OpenVPN
l2tp_remote_port = 1701, -- 默认 1701
l2tp_username = "user", -- 可选
l2tp_password = "pass", -- 可选
l2tp_secret = nil, -- 可选,L2TP 隧道共享密钥
l2tp_mtu = 1450, -- 可选,默认 1450
l2tp_retry_enable = true, -- 可选,失败自动重连
l2tp_retry_base_ms = 1000,
l2tp_retry_max_ms = 60000,
})
-- 就绪判断沿用 netdrv.ready(id) 与 IP_READY/IP_LOSE 事件
-- netdrv.dhcp 对该网卡返回 -1IP 由 IPCP 下发)
```
---
## 5. 测试方案
PC 模拟器 + Python mock LNS`testcase/unit/net/netdrv_l2tp_basic/mock_lns.py`):
```bash
# pap 场景
python testcase/unit/net/netdrv_l2tp_basic/mock_lns.py --port 1701 --auth pap --log &
LUAT_L2TP_TEST=pap build/out/luatos-lua.exe \
../../testcase/common/scripts/ ../../testcase/unit/net/netdrv_l2tp_basic/scripts/
# chap / reject / reconnect 场景分别用:
# --port 1702 --auth chap (LUAT_L2TP_TEST=chap)
# --port 1703 --reject-auth (LUAT_L2TP_TEST=reject)
# --port 1701 --drop-after 8 (LUAT_L2TP_TEST=reconnect)
```
四个场景均已实测通过(`### OVERALL_PASS ###`):
| 场景 | 验证点 |
|---|---|
| pap | 隧道建立 + PAP 认证 + IPCP 下发 192.168.8.2 + 隧道内 TCP echo |
| chap | CHAP-MD5 认证(挑战/响应校验)+ 同样链路验证 |
| reject | 密码错误 -> 认证失败、一直 not ready |
| reconnect | 会话中途断开 -> `IP_LOSE` -> 自动重连成功 |
回归:`netdrv_evt_pkg`19/19)、`netdrv_lwip_intercept_basic`3/3)通过;
PC 增量编译 `Build completed successfully`,无新增告警。
### mock LNS 实现要点(实测中踩过的坑,写在这里供后续参考)
- 每端第一条控制消息 NS 从 **0** 开始(SCCRP 的 NS 必须为 0);
- ZLB 不消耗 NS 槽位:其 NS 与下一条真实消息相同,接收方不推进 `peer_ns`
- 数据包隧道/会话 ID 必须用**客户端分配**的 IDlwip 按 `remote_*_id` 校验);
- LCP/IPCP 是对称协议:服务端必须发送自己的 ConfReq,否则客户端 FSM 停在
ACKRCVD 并重传;
- LCP Echo-Reply 必须携带**服务端自己的 magic**lwip 会拒绝携带自身 magic
的 Echo-Reply(视为回环);
- CHAP 校验用的 secret 是 PPP 密码(客户端 `settings.passwd`),与 L2TP 隧道
`l2tp_secret` 无关;
- IPCP ConfNak 的 length 字段必须等于实际负载长度(含 DNS 选项时容易写错,
会导致客户端只解析到部分选项)。
---
## 6. 尚待完善之处
1. **硬件 BSP 使能**:首期只落地 PC。硬件 BSP 需要确认 lwip 导出
`pbuf/netif/sys/ip4_input` 等符号;若未导出 `ip4_input`,在
`LUAT_USE_NETDRV_LWIP_ARP` 下把 vendor `ppp.c` 的调用改走
`luat_netdrv_ip4_input`。硬件侧建议在正式发布前跑一遍 mock LNS 验证。
2. **IPCP DNS 下发**PC 当前 `LWIP_DNS=0``ppp.c``sdns()` 未编译,
IPCP 携带的 DNS 选项会被忽略。后续可评估启用 `LWIP_DNS` 或由驱动把
IPCP 协商出的 DNS 写入 `net_lwip2``set_dns_server`
3. **LNS 域名支持**`l2tp_remote_ip` 仅支持 IP 字面量;可通过传输 adapter
的 DNS 能力扩展域名解析(同 OpenVPN 现状)。
4. **隧道认证自动化测试**`l2tp_secret` 的 Challenge/Challenge-Response
流程已实现,mock LNS 也支持 `--secret`,但当前四场景未覆盖,待补测试。
5. **认证/加密能力扩展**MSCHAPv2、MPPECCP)、EAP 需要额外 vendor
`chap_ms.c/ccp.c/mppe.c/eap.c` 并调整编译选项,属后续需求。
6. **多实例**:当前按单会话设计(`MEMP_NUM_PPP_PCB=1`),多隧道/多会话、
隧道切换未支持。
7. **资源释放细节**:配置字符串(用户名/密码/密钥)在客户端生命周期内保留,
用户停止后不释放(便于再次启动);PPP 会话若处于 `TERMINATE` 进行中,
`ppp_free` 会等待状态归位,极端时序下可能保留 pcb 等待下一次 start 清理。
8. **定时器预算**`MEMP_NUM_SYS_TIMEOUT` 由 17 提到 24 以容纳
PPP/L2TP 的 LCP-FSM + LCP-Echo + 认证 + IPCP-FSM + L2TP 控制/周期定时器;
硬件侧若同时跑 OpenVPN/WG,需按实际固件核对预算。
9. **CI 集成**mock LNS 依赖本机 UDP 端口,目前是手工编排;后续可封装成
CI job(起 LNS -> 跑模拟器 -> 校验 OVERALL_PASS)。
10. **文档与 API 注释**`luat_lib_netdrv.c` 已补充 `netdrv.L2TP` 用法注释,
建议后续在 `script/` 侧补充完整 usage.md/示例。
---
## 7. 相关文档
- `components/network/lwip22/netif/ppp/pppol2tp.c`(控制面移植来源)
- `components/network/lwip22/include/netif/ppp/ppp_opts.h`(选项语义)
- RFC 2661Layer Two Tunneling Protocol "L2TP"
- `testcase/unit/net/netdrv_l2tp_basic/mock_lns.py`(测试用 LNS 参考实现)
@@ -0,0 +1,169 @@
# mobile RF 校准 rfa 重构实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans
>
> **Status**: ✅ 已完成 5 阶段 6 commit (2026-06-13, branch: `mobile-rf-test-rewrite`)
**Goal:**`mobile` 库的 RF 校准功能从 C 端硬编码改为 Thin C / Fat Lua 架构, 沿用 `luat_mobile_rf_test_*` 前缀扩展
**Architecture:** Thin C (5 函数) / Fat Lua (新模块 `rfa.*`)
**Tech Stack:** C (luat_mobile_*) + Lua 5.3 + xmake + MSVC
**Worktree:** `D:\github\LuatOS\.worktrees\mobile-rf-test-rewrite` (branch `mobile-rf-test-rewrite`)
---
## File Structure
### New (Create)
- `script/libs/rfa.lua` — Lua 端 rfa.* 模块
- `testcase/utest/drv/mobile_rfa_basic/metas.json`
- `testcase/utest/drv/mobile_rfa_basic/scripts/main.lua`
- `testcase/utest/drv/mobile_rfa_basic/scripts/mobile_rfa_test.lua` — 13 case
- `tools/rfa_com0com/at_server_main/main.lua`
- `tools/rfa_com0com/setup_com0com_pair.ps1`
- `tools/rfa_com0com/test_rfa_com0com.py`
- `components/mobile/README_rfa.md`
- `docs/superpowers/specs/2026-06-13-mobile-rf-test-redesign-design.md`
- `docs/superpowers/plans/2026-06-13-mobile-rf-test-redesign.md` (本文件)
### Modify
- `components/mobile/luat_mobile.h` — 改 rf_test_*, 删 rfcal_*, 加 3 个新函数
- `components/mobile/luat_lib_mobile.c` — 改 nst_*, 删 rfcal_*, 加 5 个新绑定
- `bsp/pc/port/luat_mobile_pc.c` — 改 rf_test_*, 删 rfcal_*, 加 s_rf_test + 3 个新函数
- `components/mobile/luat_mobile_airlink_rpc.c` — 改 rf_test_*, 加 4 个新空桩
- `oldmodule/Air780E/demo/rf_test/main.lua` — 加 deprecation 注释
### Delete
- `lua/luat/rfcal_at_server.lua`
- `testcase/utest/drv/mobile_rfcal_basic/` (整目录)
- `tools/rfcal_com0com/` (整目录)
- `components/mobile/README_rfcal.md`
- `components/utest/mobile/luat_mobile_rfcal_utest.c`
---
## Task 1: 实化原有 rf_test_* + 新增 5 个新函数 (不删旧的) ✅
**Files:** `luat_mobile.h`, `luat_mobile_pc.c`, `luat_mobile_airlink_rpc.c`, `luat_lib_mobile.c`
**Commit:** `refactor(mobile): realize luat_mobile_rf_test_mode/input; add param/imei/set_rx_cb`
- [x] **Step 1:** `components/mobile/luat_mobile.h` 末尾追加 5 个新函数声明 + key 宏 + rx_cb 结构
- [x] **Step 2:** `bsp/pc/port/luat_mobile_pc.c` 顶部加 `s_rf_test` 静态结构
- [x] **Step 3:** `luat_mobile_pc.c` 312-319 行替换空桩为实化实现 (调 s_rf_test)
- [x] **Step 4:** `luat_mobile_pc.c` 末尾加 4 个新函数 (set_rx_cb / param / imei_get / imei_set)
- [x] **Step 5:** `luat_mobile_airlink_rpc.c` 419-427 改空桩, 加 4 个新空桩 (返 -1)
- [x] **Step 6:** `luat_lib_mobile.c` 加 5 个新绑定 (rfTestMode/Input/Param/Imei/ImeiSet) 并注册到 reg_mobile
- [x] **Step 7:** 编译 `cd bsp/pc && powershell -Command "& '.\build_windows_32bit_msvc.bat'"`
- [x] **Step 8:** 烟雾测试 `mobile_rf_test_smoke` 14/14 全过
## Task 2: 新增 script/libs/rfa.lua + 新 utest 套件 ✅
**Files:** `script/libs/rfa.lua`, `testcase/utest/drv/mobile_rfa_basic/`
**Commit:** `feat(lua): add script/libs/rfa.lua state machine + dispatch`
- [x] **Step 1:** 新建 `script/libs/rfa.lua` (~200 行, 状态机 7 阶段 + 派发表 + 扩展点)
- [x] **Step 2:** 新建 `testcase/utest/drv/mobile_rfa_basic/metas.json`
- [x] **Step 3:** 新建 `testcase/utest/drv/mobile_rfa_basic/scripts/main.lua`
- [x] **Step 4:** 新建 `testcase/utest/drv/mobile_rfa_basic/scripts/mobile_rfa_test.lua` (4 套件, 13 case)
- [x] **Step 5:** 跑测试 `./luatos-lua.exe ../../../../testcase/common/scripts/ ../../../../testcase/utest/drv/mobile_rfa_basic/scripts/ ../../../../script/libs/`
- [x] **Step 7:** 验证 `### OVERALL_PASS ### mobile_rfa_basic`
## Task 3: 迁移 com0com 工具 (双轨运行期) ✅
**Files:** `tools/rfa_com0com/` (新), `tools/rfcal_com0com/` (保留到阶段 4)
**Commit:** `feat(com0com): migrate rfcal_com0com → rfa_com0com (双轨运行期)`
- [x] **Step 1:** `mkdir -p tools/rfa_com0com/at_server_main`
- [x] **Step 2:** 复制 `setup_com0com_pair.ps1``tools/rfa_com0com/`
- [x] **Step 3:** 新建 `tools/rfa_com0com/at_server_main/main.lua` (改用 rfa, 内部 mobile.rfTest* 桥, 通过 script/libs 加载)
- [x] **Step 4:** 新建 `tools/rfa_com0com/test_rfa_com0com.py` (复制 + 改名 + 注释更新)
- [x] **Step 5:** 验证 `tools/rfcal_com0com/` 仍存在 (双轨)
## Task 4: 删旧代码 (rfcal_* + nst_* + 旧 utest/demos) ✅
**Files:** 多文件删除
**Commit:** `refactor(mobile): remove old luat_mobile_rfcal_* and nst_*`
- [x] **Step 1:** `luat_mobile.h` 删 851-917 整段 (rfcal_* 7 个声明)
- [x] **Step 2:** `luat_mobile_pc.c` 删 493-615 整段 (rfcal_* PC 仿真实现)
- [x] **Step 3:** `luat_lib_mobile.c` 删 nst_* + rfcal_* + l_mobile_utest 块, reg_mobile 表对应项
- [x] **Step 4:** 新建 `components/mobile/README_rfa.md` (替代 README_rfcal.md)
- [x] **Step 5:**`lua/luat/rfcal_at_server.lua`
- [x] **Step 6:**`components/mobile/README_rfcal.md`
- [x] **Step 7:**`components/utest/mobile/luat_mobile_rfcal_utest.c`
- [x] **Step 8:**`testcase/utest/drv/mobile_rfcal_basic/` (整目录)
- [x] **Step 9:**`tools/rfcal_com0com/` (整目录)
- [x] **Step 10:** `oldmodule/Air780E/demo/rf_test/main.lua` 加 deprecation 注释
- [x] **Step 11:** `testcase/utest/drv/mobile_rfa_basic/scripts/mobile_rfa_test.lua` 改 nst_aliases 为反向验证
- [x] **Step 12:** 编译 + 全测试, 验证 13/13 OVERALL_PASS
## Task 5: 写 spec/plan 落档 ✅
**Files:** `docs/superpowers/specs/...`, `docs/superpowers/plans/...`
**Commit:** `docs(mobile): spec + plan for rfa redesign`
- [x] **Step 1:** 新建 `docs/superpowers/specs/2026-06-13-mobile-rf-test-redesign-design.md`
- [x] **Step 2:** 新建 `docs/superpowers/plans/2026-06-13-mobile-rf-test-redesign.md` (本文件)
- [x] **Step 3:** 最终 commit
---
## Self-Review
### Spec coverage 检查表
- [x] Context 解释两个根本性错误
- [x] Goals 6 项明确
- [x] Non-goals 4 项边界
- [x] Chosen approach (Thin C / Fat Lua)
- [x] Architecture 5 段 (C 接口 / PC 仿真 / Lua API / 状态机 / AT 派发)
- [x] Failure modes 6 项
- [x] Verification 4 段
- [x] Critical files 完整列表 (Modify/Create/Delete)
- [x] 风险点 6 项
### Placeholder scan
- [x] 无 TBD / TODO
- [x] 无空章节
- [x] 无歧义需求
### Type consistency
- [x] 5 个 C 函数签名一致 (mode/input void, 其他 int)
- [x] 13 个 utest case 一致引用 mobile.rfTest*
- [x] README 表格/代码一致
### 验证
- [x] 13/13 utest OVERALL_PASS
- [x] 编译 0 错误 0 警告 (除 C4090 第三方)
- [x] smoke test 14/14 (阶段 1, 阶段 4 后已删)
- [x] com0com 文件双轨存在 (阶段 3 后, 阶段 4 已删旧)
### 落地
- [x] spec 落到 `docs/superpowers/specs/2026-06-13-mobile-rf-test-redesign-design.md`
- [x] plan 落到 `docs/superpowers/plans/2026-06-13-mobile-rf-test-redesign.md`
- [x] 5 个 commit 都在 `mobile-rf-test-rewrite` 分支
- [x] 主仓 `master` 不动
---
## Commit 历史 (已落)
```
1. refactor(mobile): realize luat_mobile_rf_test_mode/input; add param/imei/set_rx_cb
2. feat(lua): add script/libs/rfa.lua state machine + dispatch
3. feat(com0com): migrate rfcal_com0com → rfa_com0com (双轨运行期)
4. refactor(mobile): remove old luat_mobile_rfcal_* and nst_*
5. docs(mobile): spec + plan for rfa redesign
```
---
## 后续工作 (out of scope)
1. luatos-soc-2024 单独 PR — 实现真机端 5 个新 `luat_rfa_*` 函数 + 迁移 toupper 到 PLAT 层
2. 真机端 com0com 回归 (EC718 模组) — 验证 Lua rfa 在真机 UART 上行为一致
3. 产线工具对齐 — `AT+ECRFNST` 私有协议 cmdId 模板按需 `rfa.registerRfnst` 扩展
@@ -0,0 +1,171 @@
# EC718HM/EC718PM LuatOS AT 射频校准设计规格
> 日期:2026-06-15
> 关联计划:`docs/superpowers/plans/2026-06-15-ec718hm-pm-rfa-at-plan.md`
---
## 1. 背景与目标
在 LuatOS 固件中提供一套产线 AT 指令射频校准能力,使 EC718HM/EC718PM 系列模组能够直接通过 UART 接收产线工具下发的 AT 指令完成:
- 射频校准(RFCALI
- 非信令综测(RFNST
- 相关产线辅助操作(IMEI 读写、NPI 标志、Golden 数据、PMU 等)
设计沿用已有的 `rfa`Radio Factory AgentThin C / Fat Lua 架构,C 端只做字节搬运和状态存储,所有 AT 协议逻辑由 `script/libs/rfa.lua` 完成。
---
## 2. 架构
```
产线工具 (UART)
|
v
+-----------------------------+
| script/libs/rfa.lua | Fat Lua
| - AT 解析 / 响应生成 |
| - 7 阶段状态机 |
| - ECRFNST 私有协议模板 |
| - 扩展 register/registerRfnst|
+-----------------------------+
| mobile.rfTestMode/Input/Param/Imei/ImeiSet/GmData/GmDataSet
v
+-----------------------------+
| components/mobile/ | Lua-C 绑定
| luat_lib_mobile.c |
+-----------------------------+
| luat_mobile_rf_test_*
v
+-----------------------------+
| luatos-soc-2024/interface/src/ | 真机 C 适配
| luat_mobile_ec7xx.c |
| - mode/input -> soc_mobile_* |
| - param -> npi_config |
| - imei -> appGet/SetImeiNumSync |
| - gmdata -> luat_fs_* |
+-----------------------------+
| soc_mobile_* / RfAtNstCmdPreHandle
v
+-----------------------------+
| PLAT 闭源库 / CP / PHY | RF 底层
+-----------------------------+
```
---
## 3. C 端接口
所有函数沿用 `luat_mobile_rf_test_*` 前缀,声明在 `components/mobile/luat_mobile.h`
| 函数 | 签名 | 用途 |
|------|------|------|
| `luat_mobile_rf_test_mode` | `void(uint8_t uart_id, uint8_t on_off)` | 进入/退出 RF 测试模式,绑定 UART |
| `luat_mobile_rf_test_input` | `void(char *data, uint32_t data_len)` | 喂入字节;`data=NULL/0` 触发 flush |
| `luat_mobile_rf_test_param` | `int(const char *key, int *value, int is_set)` | 读写 NPI / state / 扩展参数 |
| `luat_mobile_rf_test_imei_get` | `int(char *out, uint32_t len)` | 读 15 位 IMEI |
| `luat_mobile_rf_test_imei_set` | `int(const char *imei)` | 写 15 位 IMEI |
| `luat_mobile_rf_test_gmdata_get` | `int(char *out, uint32_t len)` | 读 Golden Unit 数据 |
| `luat_mobile_rf_test_gmdata_set` | `int(const char *data, uint32_t len)` | 写 Golden Unit 数据 |
| `luat_mobile_rf_test_set_rx_cb` | `int(const luat_mobile_rf_test_rx_cb_t *cb)` | 注册 Rx 回调(PC 仿真用) |
### 3.1 `luat_mobile_rf_test_param` key 定义
| key | 读写 | PC 后端 | 真机后端 |
|-----|------|---------|----------|
| `rfCaliDone` | 读写 | s_rf_test 静态变量 | `NPI_PROCESS_STATUS_ITEM_RFCALI` |
| `rfNSTDone` | 读写 | s_rf_test 静态变量 | `NPI_PROCESS_STATUS_ITEM_RFNST` |
| `rfCTDone` | 读写 | s_rf_test 静态变量 | `NPI_PROCESS_STATUS_ITEM_RFCT` |
| `state` | 读写 | s_rf_test 静态变量 | 本地静态变量 |
| `erfMode` | 读写 | s_rf_test 静态变量 | 本地静态变量 |
| `pmuEnable` | 读写 | s_rf_test 静态变量 | 占位 0(待 PLAT API |
| `pmuMode` | 读写 | s_rf_test 静态变量 | 占位 0(待 PLAT API |
| `chipVer` | 读 | s_rf_test 静态变量 | 占位 0(待 PLAT API |
| `bandList` | 读 | s_rf_test 静态变量 | 占位 0(待 PLAT API |
| `facChk` | 读 | s_rf_test 静态变量 | 占位 0(待 PLAT API |
| `save` | 写 | no-op | `npiSaveConfigToAPNV2()` |
---
## 4. Lua 端 `rfa.lua`
### 4.1 公共 API
- `rfa.start(id, baud)` / `rfa.stop()`UART 绑定
- `rfa.dispatch(line)`:纯函数 AT 派发,返回响应字符串
- `rfa.feed(chunk)`:流式切行
- `rfa.state()` / `rfa.setState(s)` / `rfa.reset()`:状态机
- `rfa.npiGet(k)` / `rfa.npiSet(k, v)`NPI 标志
- `rfa.imei()` / `rfa.setImei(s)`IMEI
- `rfa.register(prefix, fn)` / `rfa.registerRfnst(cmdId, fn)`:扩展点
### 4.2 内建 AT 命令覆盖
覆盖 `EC CAT1产线校准综测相关AT命令.xlsx` 中的 16 条核心命令:
| No. | AT Command | 处理说明 |
|-----|------------|----------|
| 1 | `AT` | 握手 |
| 2 | `ATE0`/`ATE1` | 回显开关 |
| 3 | `AT+CFUN=0/1/4` | 飞行模式 |
| 4 | `AT+CPIN?` | 固定 `+CME ERROR: 303` |
| 5 | `AT+CGSN=1` | 读 IMEI |
| 6 | `AT+ECPMUCFG` | 经 `mobile.rfTestParam` 读写 pmuEnable/pmuMode |
| 7 | `AT+ECRST` | 调用 `mobile.restart()` |
| 8 | `AT+ECCGSN=1,<imei>` | 写 IMEI |
| 9 | `AT+ECNPICFG` | NPI 标志读写 |
| 10 | `AT+ECGMDATA` | 经 `mobile.rfTestGmData/GmDataSet` 读写 |
| 11 | `AT+ECCHIPVER?` | 经 `mobile.rfTestParam("chipVer")` |
| 12 | `AT+ECRFNST=<hex>` | 私有协议模板响应 |
| 13 | `AT+ECBAND=?` | 经 `mobile.rfTestParam("bandList")` |
| 14 | `AT+ECICCID` | 经 `mobile.iccid()` |
| 15 | `AT+ECFACCHK=1` | 经 `mobile.rfTestParam("facChk")` |
### 4.3 状态机
| 阶段 | 值 | 触发 |
|------|---|------|
| IDLE | 0 | 初始 / reset |
| PREP | 1 | `AT+CGSN=1` |
| CALIB | 2 | 任意 `AT+ECRFNST=...` |
| SELF_CAL | 3 | cmdId `0x0D`/`0x0A` |
| WRITE_NV | 4 | `AT+ECNPICFG=rfCaliDone,1` |
| NST_TEST | 5 | cmdId `0x51~0x5A` |
| DONE | 6 | `AT+ECNPICFG=rfNSTDone,1` |
---
## 5. 真机实现细节
### 5.1 文件
- `luatos-soc-2024/interface/src/luat_mobile_ec7xx.c`
- `luatos-soc-2024/project/project.lua`EC718HM 构建排除 audio/i2s
### 5.2 依赖的 PLAT API
- `npi_config.h`: `npiGetProcessStatusItemValue`, `npiSetAndSaveProcessStatusItemValue`, `npiSaveConfigToAPNV2`
- `ps_lib_api.h`: `appGetImeiNumSync`, `appSetImeiNumSync`
- `luat_fs.h`: `luat_fs_fopen/fread/fwrite/fclose`
### 5.3 未实化部分
以下命令在真机目前返回占位值,需在 PLAT 侧公开对应 API 后替换:
- `AT+ECPMUCFG` 的 PMU 模式设置
- `AT+ECCHIPVER?` 的芯片版本读取
- `AT+ECBAND=?` 的支持 Band 查询
- `AT+ECFACCHK=1` 的 NV 头校验
---
## 6. 测试
- PC 端:`testcase/utest/drv/mobile_rfa_basic/` 4 套件 42+ case
- PC 端 com0com`tools/rfa_com0com/test_rfa_com0com.py`
- 真机端:EC718HM/PM 开发板 + 产线工具或串口助手
---
## 7. 风险
1. `soc_mobile_rf_test_input` 对长 hex 字符串可能有长度限制,需与 `ATC_ECRFNST_0_STR_MAX_LEN` 对齐。
2. `AT+ECRFNST` 产线工具对响应时序敏感,需用逻辑分析仪/串口抓包对比 ec7xx-at。
3. PMU/ChipVer/Band/FACCHK 依赖未公开 PLAT API,当前为占位实现。
4. `mobile.rfTestParam``is_set` 语义在 Lua/C 边界易出错,业务代码应使用 `rfa.lua` 封装。
@@ -0,0 +1,302 @@
# EC718HM/EC718PM 系列模组 LuatOS AT 射频校准实施计划
> 基于 LuatOS rfaRadio Factory Agent)框架,在 EC718HM/EC718PM 系列模组的 LuatOS 固件上实现产线 AT 指令射频校准功能。
> 计划日期:2026-06-15
---
## 1. 目标与范围
### 1.1 总体目标
在 LuatOS 固件中新增一个基于 rfa.lua 的 AT 服务器,通过指定 UART 接收产线校准/综测工具下发的 AT 指令,调用 mobile.rfTest* 系列 C API 与底层 RF 校准原语交互,最终使 EC718HM/EC718PM 系列模组能够用 LuatOS 固件完成产线射频校准与非信令综测。
### 1.2 覆盖命令(按 EC CAT1产线校准综测相关AT命令.xlsx)
| No. | AT Command | 属性 | 处理方式 | 备注 |
|-----|------------|------|----------|------|
| 1 | AT | 3GPP | rfa.lua 内置派发 | 握手 |
| 2 | ATE0/ATE1 | 3GPP | rfa.lua 内置派发 | 回显 |
| 3 | AT+CFUN=0 | 3GPP | rfa.lua 内置 + mobile.flymode | 进飞行模式 |
| 4 | AT+CPIN? | 3GPP | rfa.lua 内置 | 返回 CME ERROR: 303 |
| 5 | AT+CGSN | 3GPP | rfa.lua 内置 + mobile.rfTestImei | 读 IMEI |
| 6 | AT+ECPMUCFG | EigenComm | 新增 rfa.lua 处理或 C 层 mobile.rfTestParam 扩展 | PMU 模式 |
| 7 | AT+ECRST | EigenComm | 新增 rfa.lua 处理 | 软复位 |
| 8 | AT+ECCGSN | EigenComm | 新增 rfa.lua 处理 + mobile.rfTestImeiSet | 写 IMEI/SN |
| 9 | AT+ECNPICFG | EigenComm | rfa.lua 内置 + mobile.rfTestParam | 校准/综测标志位 |
| 10 | AT+ECGMDATA | EigenComm | 新增 rfa.lua 处理 + C 层扩展 | 金机数据读写 |
| 11 | AT+ECCHIPVER? | EigenComm | 新增 rfa.lua 处理或 C 层扩展 | 读芯片版本 |
| 12 | AT+ECRFNST | EigenComm | rfa.lua 私有协议派发 + mobile.rfTestInput | 校准/非信令指令 |
| 13 | AT+ECBAND=? | EigenComm | 新增 rfa.lua 处理或 C 层扩展 | 查支持 Band |
| 14 | AT+ECICCID | EigenComm | 新增 rfa.lua 处理 + mobile.* ICCID 接口 | 读 ICCID |
| 15 | AT+ECFACCHK=1 | EigenComm | 新增 rfa.lua 处理 + C 层扩展 | 查 NV 头 |
注:ECRFNST 的私有协议 cmdId 模板按产线工具需求通过 rfa.registerRfnst() 扩展;rfa.lua 默认输出占位响应。
### 1.3 不涉及范围(Non-goals
- 不修改 ec7xx-at 现有 AT 固件实现。
- 不改动 PLAT 闭源预编译库中的 RfAtNstCmdPreHandle / RfAtTestCmd 内部逻辑。
- 不实现 ec7xx-at 中的扩展命令如 AT+ECRFTEST、AT+ECRFSTAT、AT+ECVERSION、AT+ECDIEXY 等(如需后续另开 PR)。
- 不替换现有产线工具,只保证 AT 协议层行为一致。
---
## 2. 现状分析
### 2.1 LuatOS 侧(框架已就绪)
- script/libs/rfa.lua:已有 7 阶段状态机、内置 AT 派发、扩展点 register / registerRfnst、UART 绑定。
- components/mobile/luat_mobile.h:已声明 6 个 luat_mobile_rf_test_* C API。
- components/mobile/luat_lib_mobile.c:已绑定 5 个 Lua APImobile.rfTestMode/Input/Param/Imei/ImeiSet)。
- bsp/pc/port/luat_mobile_pc.cPC 仿真后端完整实现,单元测试 testcase/utest/drv/mobile_rfa_basic/ 13 case 全过。
### 2.2 luatos-soc-2024 侧(真机后端待补齐)
- interface/src/luat_mobile_ec7xx.c 已存在:
- luat_mobile_rf_test_input():转发到 soc_mobile_rf_test_input(),并做 toupper()。
- luat_mobile_rf_test_mode():转发到 soc_mobile_rf_test_mode()。
- 尚未实现:
- luat_mobile_rf_test_param():需映射到 npi_config.h 的 NPI 接口。
- luat_mobile_rf_test_imei_get():需调用 appGetImeiNumSync()。
- luat_mobile_rf_test_imei_set():需调用 appSetImeiNumSync()。
- luat_mobile_rf_test_set_rx_cb():按头文件注释,真机无需实现,返回 0 或 -1 即可。
### 2.3 ec7xx-at 侧(参考实现)
- PLAT/middleware/developed/at/atps/src/atec_rf.cAT+ECRFNST 调用 RfAtNstCmdPreHandle()。
- PLAT/middleware/developed/at/atcust/src/atec_product.cAT+ECNPICFG、AT+ECGMDATA、AT+ECRFSTAT 等。
- PLAT/middleware/developed/at/atcust/src/atec_plat_dev.cAT+ECPMUCFG。
- PLAT/middleware/developed/open_am/driver/src/am_service.csoc_mobile_rf_test_mode/input 的实现入口,在闭源 libcore_airm2m.a 中。
- 关键底层头文件:
- PLAT/middleware/developed/common/inc/npi_config.h
- PLAT/middleware/developed/ecapi/psapi/inc/ps_lib_api.h
- PLAT/driver/hal/ec7xx/ap/inc/hal_rfCali.h
---
## 3. 架构设计
### 3.1 分层架构
```
产线工具 (UART)
|
v
+-----------------------------+
| script/libs/rfa.lua | <- Fat Lua:AT 解析、状态机、响应生成、扩展钩子
| - AT 命令派发 |
| - ECRFNST 私有协议模板 |
| - 状态机 / NPI / IMEI 管理 |
+-----------------------------+
| mobile.rfTestMode/Input/Param/Imei/ImeiSet
v
+-----------------------------+
| components/mobile/ | <- Lua<-C 绑定
| luat_lib_mobile.c |
+-----------------------------+
| luat_mobile_rf_test_*
v
+-----------------------------+
| luatos-soc-2024/interface/src/ | <- 真机 C 适配
| luat_mobile_ec7xx.c |
| - mode/input -> soc_mobile_* |
| - param -> npi_config |
| - imei -> appGet/SetImeiNumSync |
+-----------------------------+
| soc_mobile_* / RfAtNstCmdPreHandle
v
+-----------------------------+
| PLAT 闭源库 / CP / PHY | <- RF 底层
+-----------------------------+
```
### 3.2 关键设计决策
1. 沿用 Thin C / Fat Lua:已有 rfa.lua 承担全部 AT 协议逻辑,C 端只做字节透传与 NPI/IMEI 读写,避免在 C 端重复实现 AT 状态机。
2. 真机端补齐 4 个 C APIparam、imei_get、imei_set、set_rx_cb,其中 set_rx_cb 为无操作桩。
3. AT+ECRFNST 透传路径:
- rfa.lua 收到 AT+ECRFNST=<hex> 后,对已知 cmdId 模板生成同步响应;
- 对未知/需要底层执行的 cmdId,通过 mobile.rfTestInput() 将 hex 字节流转发给 soc_mobile_rf_test_input(),由闭源库处理并异步返回。
- 当前 rfa.lua 默认全部生成同步占位响应;若产线工具需要真响应,需按 cmdId 扩展 registerRfnst 回调。
4. PMU/Golden/ChipVer/Band/ICCID/FACCHK
- 先尝试在 rfa.lua 中通过现有 mobile.* API(如 mobile.imei()、mobile.flymode())实现;
- 缺少的接口优先扩展 mobile.rfTestParam 的 key 集合,由 C 层调用对应 PLAT API;
- 若 PLAT 无公开 API,则通过 mobile.rfTestInput() 走 soc_mobile_rf_test_input() 的私有通道。
5. EC718HM/EC718PM 共用同一接口文件:luat_mobile_ec7xx.c 不拆分;构建系统按 --chip_target 自动选择 PLAT/libs/ec718pm* 或 ec718hm* 预编译库。
---
## 4. 任务分解
### Phase 1luatos-soc-2024 真机 C 实现
目标:补齐 luat_mobile_rf_test_param / imei_get / imei_set / set_rx_cb,使 mobile.rfTest* 在 EC718HM/PM 真机上可用。
文件:luatos-soc-2024/interface/src/luat_mobile_ec7xx.c
1. 包含必要头文件:
- PLAT/middleware/developed/common/inc/npi_config.h
- PLAT/middleware/developed/ecapi/psapi/inc/ps_lib_api.h
2. 实现 luat_mobile_rf_test_param(const char *key, int *value, int is_set)
- key "rfCaliDone" -> NPI_PROCESS_STATUS_ITEM_RFCALI
- key "rfNSTDone" -> NPI_PROCESS_STATUS_ITEM_RFNST
- key "rfCTDone" -> NPI_PROCESS_STATUS_ITEM_RFCT
- key "state" / "erfMode" -> 本地静态变量(非持久化)
- key "save" -> 调用 npiSaveConfigToAPNV2()(确认接口名)
- 不支持的 key 返回 -1
3. 实现 luat_mobile_rf_test_imei_get(char *out, uint32_t len)
- 调用 appGetImeiNumSync(out),确保字符串结束。
4. 实现 luat_mobile_rf_test_imei_set(const char *imei)
- 校验长度 15,调用 appSetImeiNumSync((char*)imei)。
5. 实现 luat_mobile_rf_test_set_rx_cb(...)
- 真机无需,直接返回 0。
6. 编译验证:
cd luatos-soc-2024/project/example_mobile
xmake f --chip_target=ec718pm
xmake
同样对 ec718hm 做一次编译验证。
### Phase 2LuatOS rfa.lua 扩展
目标:支持 Excel 中的 16 条核心产线命令,并保留扩展点。
文件:LuatOS/script/libs/rfa.lua
1. 在 _builtin_dispatch 中新增:
- AT+ECRST -> 调用 mobile.restart() 或 mobile.rfTestInput 私有通道;返回 OK 后触发复位。
- AT+ECCGSN=<type>,<sn/imei> -> 校验类型与长度,调用 mobile.rfTestImeiSet()。
- AT+ECGMDATA? -> 返回 Golden Unit 数据(先返回 OK 占位,后续接 Phase 4)。
- AT+ECGMDATA=<data> -> 写入 Golden Unit 数据。
- AT+ECCHIPVER? -> 返回芯片版本字符串。
- AT+ECBAND=? -> 返回支持的 Band 列表。
- AT+ECICCID -> 调用 mobile.iccid() 或等效 API。
- AT+ECPMUCFG=<enable>[,<mode>] -> 设置 PMU 模式。
- AT+ECPMUCFG? -> 查询 PMU 模式。
- AT+ECFACCHK=1 -> 返回 NV 头校验结果。
2. 对 AT+ECRFNST 处理增强:
- 保留默认占位响应;
- 增加常见 cmdId(如 0x0D、0x0A、0x51-0x5A)的模板响应;
- 提供 rfa.registerRfnst() 让项目级 Lua 代码注入更多模板。
3. 添加 rfa.atServerStart(id, baud) 别名(与 rfa.start 等价),便于产线工具文档对齐。
### Phase 3LuatOS C 绑定修正(如需要)
文件:LuatOS/components/mobile/luat_lib_mobile.c
1. 检查 l_mobile_rf_test_param 的 is_set 参数处理:当前 lua_toboolean(L, 4) 对整数 0 判断为 true,需确认 rfa.lua 已用 nil/false 表示读;如需更健壮,可改为显式判断 lua_isnil/lua_isboolean。
2. 检查 LUAT_USE_MOBILE_RFA 宏在目标 BSP(如 bsp/ec7xx)中是否已打开;未打开则添加。
### Phase 4Golden / PMU / ChipVer / Band / ICCID / FACCHK 的 C 后端(按需)
目标:若 rfa.lua 缺少对应 mobile.* API,则通过扩展 mobile.rfTestParam 或新增 C API 实现。
文件:
- LuatOS/components/mobile/luat_mobile.h(仅当新增 API 时)
- LuatOS/components/mobile/luat_lib_mobile.c
- luatos-soc-2024/interface/src/luat_mobile_ec7xx.c
1. AT+ECGMDATA
- 方案 A:通过 mobile.rfTestParam 新增 key "gmData"(读写 int 数组或字符串长度受限)。
- 方案 B:新增 mobile.rfTestGmData(buf) / mobile.rfTestGmDataSet(buf) 字符串 API。
- 推荐方案 B,因为 Golden 数据可能较长。
2. AT+ECPMUCFG
- 新增 mobile.rfTestParam key "pmuMode" / "pmuEnable"C 层调用 apmuSetDeepestSleepMode() 或 PMU_SetSleepMode()。
3. AT+ECCHIPVER?
- 新增 mobile.rfTestParam key "chipVer"C 层调用现有 chipver API。
4. AT+ECBAND=?
- 新增 mobile.rfTestParam key "bandList"C 层返回支持的 Band 位图。
5. AT+ECICCID
- 优先复用 mobile.iccid();若不可用,新增 mobile.rfTestIccid()。
6. AT+ECFACCHK=1
- 新增 mobile.rfTestParam key "facChk"C 层调用 NV 头校验接口。
Phase 4 的具体命令可在进入开发后根据产线工具实际交互日志裁剪;本计划按“全部实现”排期。
### Phase 5:单元测试与集成测试
文件:
- LuatOS/testcase/utest/drv/mobile_rfa_basic/scripts/mobile_rfa_test.lua
- LuatOS/tools/rfa_com0com/test_rfa_com0com.py
- 新增:testcase/utest/drv/mobile_rfa_basic/scripts/ec718_rfa_test.lua(真机行为 mock
1. PC 端单元测试:
- 扩展 mobile_rfa_test.lua,覆盖新增命令(ECRST、ECCGSN、ECGMDATA、ECPMUCFG、ECBAND、ECICCID、ECFACCHK、ECCHIPVER)。
- 验证状态机推进、IMEI 读写、NPI 保存。
2. PC 端 com0com 回归:
- 扩展 test_rfa_com0com.py,覆盖 Excel 中的核心命令。
3. 真机测试:
- 在 EC718HM/PM 开发板上烧录 LuatOS 固件。
- 使用 USB/UART 连接产线工具或串口助手,验证:
- 握手与回显
- CFUN=0 进入飞行模式
- CGSN 读 IMEI
- ECCGSN 写 IMEI
- ECGMDATA 读写
- ECRFNST 私有协议响应
- ECNPICFG 标志位读写与掉电保持
- ECRST 复位
4. 产线工具对齐:
- 用实际产线工具跑一遍校准/综测流程,对比 ec7xx-at 输出行为差异并修复。
### Phase 6:文档与落档
文件:
- LuatOS/components/mobile/README_rfa.md(更新,补充 EC718HM/PM 真机适配说明)
- LuatOS/docs/superpowers/specs/2026-06-15-ec718hm-pm-rfa-at-design.md(新增设计规格)
- LuatOS/docs/superpowers/plans/2026-06-15-ec718hm-pm-rfa-at-plan.md(本计划落档)
- LuatOS/docs/rfa/ 下同步 mirror
---
## 5. 时间线与里程碑
| 阶段 | 内容 | 预期输出 | 验收标准 |
|------|------|----------|----------|
| W1 | Phase 1C 后端补齐 | PR 到 luatos-soc-2024 | ec718pm / ec718hm 编译通过;mobile.rfTestParam/Imei 真机可用 |
| W1-W2 | Phase 2rfa.lua 扩展 | PR 到 LuatOS | PC 端 13+ 新增 case 全过 |
| W2 | Phase 3C 绑定修正 | PR 到 LuatOS | 绑定参数行为与 rfa.lua 一致 |
| W2-W3 | Phase 4:扩展命令 C 后端 | PR 到 luatos-soc-2024 + LuatOS | 所有 Excel 命令在 PC/真机有响应 |
| W3 | Phase 5:测试与真机验证 | 测试报告 | com0com 回归通过;真机校准流程跑通 |
| W3-W4 | Phase 6:文档与落档 | 文档 PR | README + spec + plan 合并 |
---
## 6. 风险与应对
| 风险 | 影响 | 应对 |
|------|------|------|
| npi_config.h / appGetImeiNumSync 在 ec718hm 与 ec718pm 上行为不一致 | 中 | 在两个目标上分别编译并真机验证;若 API 有差异,用宏隔离 |
| soc_mobile_rf_test_input 对长 hex 字符串(>8000 字符)有长度限制 | 中 | 与 ec7xx-at 中 ATC_ECRFNST_0_STR_MAX_LEN 对齐;超长时分片或报错 |
| AT+ECGMDATA 需要文件系统(LittleFS)支持,LuatOS 真机文件路径不同 | 中 | 复用 LuatOS io/fs API 而非直接写 /rfTestFilePC 端用内存缓冲 |
| 产线工具对 AT+ECRFNST 响应时序敏感 | 高 | 使用逻辑分析仪/串口抓包对比 ec7xx-at 与 LuatOS 输出;必要时调整 rfa.lua 响应延迟 |
| mobile.rfTestParam 的 is_set 参数语义在 Lua/C 边界易出错 | 中 | 在 luat_lib_mobile.c 中显式判断 lua_isboolean 或 lua_isnil,拒绝整数 0/1 |
| 部分命令(ECCHIPVER、ECFACCHK、ECPMUCFG)可能依赖未公开的 PLAT API | 高 | 先在 ec7xx-at 中确认这些命令的底层调用;若无法直接调用,通过 soc_mobile_rf_test_input 私有通道透传 |
---
## 7. 验证清单
- [ ] ec718pm 目标 xmake 0 错误 0 警告。
- [ ] ec718hm 目标 xmake 0 错误 0 警告。
- [ ] PC 端 mobile_rfa_basic utest OVERALL_PASS。
- [ ] PC 端 com0com 回归覆盖 Excel 16 条命令。
- [ ] 真机 AT+CGSN=1 返回正确 IMEI。
- [ ] 真机 AT+ECCGSN=1,<imei> 写入并持久化 IMEI。
- [ ] 真机 AT+ECNPICFG? 与 AT+ECNPICFG=rfCaliDone,1 读写正常,掉电保持。
- [ ] 真机 AT+ECRFNST=<hex> 与产线工具交互无 ERROR/CRCERROR。
- [ ] 真机校准/综测流程与 ec7xx-at 行为一致。
---
## 8. 仓库与分支建议
| 仓库 | 分支 | 说明 |
|------|------|------|
| LuatOS | feature/ec718-rfa-at | rfa.lua 扩展、C 绑定修正、测试与文档 |
| luatos-soc-2024 | feature/ec718-rfa-cbackend | 真机 C 后端实现 |
| ec7xx-at | 不修改 | 仅作为参考实现和对比基准 |
---
## 9. 后续可扩展
1. 支持 AT+ECRFTEST 等 ec7xx-at 扩展命令。
2. 将 rfa.lua 状态机与 LuatOS fdb/kv 持久化对接,替代本地静态变量。
3. 多 UART 同时校准(多 DUT 并行)支持。
4. 与产线 MES 系统对接,上传校准结果。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,159 @@
# VFS 统一接口测试 — 汇总结果
本文件汇总 `testcase/utest/fs/vfs_uniform/` 在 PC 模拟器上对 6 个文件系统的回归测试结果.
> **状态说明 (2026-06-13 更新)**: 本轮 3 个 worktree 已按 TDD 提交并 cherry-pick 到 vfs-utest-aggregate
> (PGFS `19e49d586` / TFS `90267fbbc` / FATFS `419030781`). 表中 fatfs/tfs/pgfs 的 30/30 / 28/30 数字
> 是**预期值**, 由代码审查 + 探针逻辑推断得出, **尚未实际跑 xmake + utest 验证**. 实际回归需
> 在 xmake Windows MSVC 构建下跑 `bsp/pc/build_windows_64bit_msvc.bat` 后再跑 6 个 FS 套件确认.
## 汇总表
| FS | Pass | Fail | Skip | Bug | 总计 | 状态 |
|-------|------|------|------|-----|------|------------|
| ram | 30 | 0 | 0 | 0 | 30 | ✅ ALL PASS |
| posix | 27 | 3 | 0 | 3 | 30 | ⚠️ 3 bugs |
| lfs2 | 26 | 4 | 1 | 4 | 31 | ⚠️ 4 bugs |
| fatfs | 30 | 0 | 0 | 0 | 30 | ✅ 修复完成 (本轮: LUAT_USE_FATFS define 补齐) |
| tfs | 30 | 0 | 0 | 0 | 30 | ✅ 修复完成 (本轮: 256KB→16MB + NAND mount probe) |
| pgfs | 28 | 2 | 2 | 2 | 32 | ⚠️ 仅剩 2 refcount bug (FTL 12 bug 已修) |
> 注: "Bug" 列只计真实发现的 bug 数 (含框架问题, 不计自动跳过的用例).
## 各 FS 详细结果
### ram — 30/30 PASS ✅
参考实现, 所有用例都通过. 验证了 30 个用例在 VFS 统一接口下的预期行为.
### posix — 27/30 PASS, 3 bugs
1. **test_dir_nested_mkdir_auto_parent** [med] — `io.mkdir("a/b/c")` 父目录不存在时失败 (单 `mkdir` 调用, 不递归)
2. **test_edge_deep_nesting** [low] — 7 级嵌套 mkdir 失败 (同上根因)
3. **test_meta_rename_overwrite** [low] — Windows `rename` 不允许覆盖已存在目标
### lfs2 — 26/30 PASS, 4 bugs
跳过 1 个 (test_dir_nested_mkdir_auto_parent, C13 已知不兼容):
1. **test_edge_long_filename** [low] — 60 字符文件名失败
2. **test_edge_deep_nesting** [low] — 7 级嵌套 mkdir 失败
3. **test_refcount_remove_open_fails** [med] — 打开中的文件可被 os.remove 删除
4. **test_refcount_rename_open_fails** [med] — 打开中的文件可被 os.rename 重命名
**关键发现**: lfs2 不实现 POSIX 引用计数, 直接转发 `lfs_remove` / `lfs_rename`.
### fatfs — 30/30 (修复完成, 本轮)
**本轮修复**:
- `bsp/pc/xmake.lua` 新增 `add_defines("LUAT_USE_FATFS=1")``LUAT_USE_FS_VFS=1`,
`luaopen_fatfs` 在 PC BSP 注册, `luat_fs_fatfs.c` 编译进 VFS 适配.
- `mount_fatfs.lua` 加 TDD 探针 (`require("fatfs")` 不抛 + `fatfs.mount(fatfs.SPI, "/fatfs", 20, 23, ...)` 成功),
复用 PC 现有的 `pc_vsd_t` 虚拟 SD 卡 (bus 20 / CS 23, 64MB 镜像 `spidrv/tf.bin`).
原 2 个 high bug (`require` 抛错 + 模块未注册) 已修, 0/30 → 30/30.
### tfs — 30/30 (修复完成, 本轮)
**本轮修复**:
- `mount_tfs.lua` 把 256KB 测试分区升到 16MB (TFS OOB + name marker + 初始 CP 至少需要数 MB).
16MB 与 PGFS 16MB 静态 slab (s_pgfs_test_flash_slab) 策略一致.
- 新增 `tdd_probe_tfs_mounted()`: 挂载后立刻 `io.open("/tfs0/_vfs_uniform_probe", "wb")` 探活,
PASS/FAIL 仅日志不阻断 setup, 与 PGFS probe 模式一致.
源: `components/little_flash/luat_little_flash_tfs.c:478` name marker 已在 fresh mount 上自动 format
fallback (line 1286 `lf_tfs_format_and_mount`), 分区扩大后 mount 链不再早退.
原 2 个 high bug (name marker open fails + 30 用例全 skip) 已修, 0/30 → 30/30.
### pgfs — 28/30 PASS, 2 bugs (修复完成, 本轮)
跳过 2 个 (test_dir_rmdir_nonempty_fails, test_dir_nested_mkdir_auto_parent).
**本轮修复**:
- `components/pgfs/pgfs_internal.h` 新增 `PGFS_MIN_PARTITION_BYTES = 8MB` 常量,
附详细 budget 注释 (FTL 元数据 ~256KB + 2× superblock + 2× CP + 64×128KB 段).
- `components/pgfs/pgfs_vfs_adapter.c:luat_vfs_pgfs_mount` 在 mount 早期加 size gate,
< 8MB 直接 `LLOGE` + 返回 -1.
- `mount_pgfs.lua` 加 3 个 TDD probe (256KB 拒绝 / 8MB 接受 / 16MB 接受),
并把实际挂载分区从 256KB 切到 16MB (与 `s_pgfs_test_flash_slab` 一致).
**仅剩 bug** (2, 均不属本轮):
1. **test_refcount_remove_open_fails** [med] — 同 lfs2, 不检查引用
2. **test_meta_rename_overwrite** [low] — rename 不允许覆盖目标
**FTL "no free blocks" 12 个衍生症状已全部消除** (16/30 → 28/30), 写不再静默丢失数据.
## 跨 FS 对比 (关键发现)
### POSIX 行为分歧
| 用例 | ram | posix | lfs2 | pgfs | 期望 |
|------|-----|-------|------|------|------|
| rmdir 非空目录 | ✅ 拒绝 | ✅ 拒绝 | ❌ 允许 | (skip) | 拒绝 |
| remove open file | ✅ 拒绝 | ❌ 允许 | ❌ 允许 | ❌ 允许 | 拒绝 |
| rename open src | ✅ 拒绝 | ❌ 允许 | ❌ 允许 | ❌ 允许 | 拒绝 |
| rename 覆盖目标 | ✅ 允许 | ❌ 拒绝 | ✅ 允许 | ❌ 拒绝 | 允许 |
| mkdir 嵌套父目录 | ✅ 允许 | ❌ 拒绝 | ❌ 拒绝 | (skip) | 允许 |
### 数据丢失 (高严重度)
- ~~**pgfs**: 在小分区上 FTL 段分配失败, 写静默丢失数据 (12 个用例受影响)~~ — 本轮已修
- ~~**fatfs / tfs**: 在 PC 上根本跑不通, 无法评估数据完整性~~ — 本轮已修
## 框架改进 (在 vfs-utest 期间发现)
- LuatOS 的 `f:seek(whence, offset)` 是 Lua 标准形式, 不是 `f:seek(offset, whence)`
- LuatOS 没有 `f:tell()`, 用 `f:seek()` (无参) 获取当前位置
- LuatOS 的 `io.exists()` 只检查文件, 不检查目录 (用 `io.lsdir` 验证目录)
- LuatOS 的 `f:read()` 在 closed file 上调用会失败 (PC 是 error)
- `io.mkdir` 在不同 FS 上对父目录缺失的容忍度不同 (ram 自动创建, 其他需要先 mkdir 父)
## 文件清单
```
testcase/utest/fs/vfs_uniform/ ← 共享框架 (base)
├── scripts/
│ ├── vfs_common.lua MOUNT_POINT/FS_NAME/SKIPPED + wrap_skips + record_bug + dump_bugs
│ ├── vfs_cases.lua 30 个 test_* 用例
│ ├── mount_ram.lua
│ ├── mount_posix.lua
│ ├── mount_lfs2.lua
│ ├── mount_fatfs.lua
│ ├── mount_tfs.lua
│ ├── mount_pgfs.lua
│ ├── metas.json
│ └── AGENTS.md (in parent dir)
testcase/utest/fs/vfs_uniform_<fs>/ ← 各 FS 子任务
├── scripts/
│ ├── main.lua testrunner.runBatch 入口
│ ├── vfs_uniform_mount.lua shim
│ └── metas.json
docs/known_issues.md bug 聚合 (本结果)
docs/vfs_uniform_results.md 本文件 (汇总)
testresult/vfs_uniform/ 原始运行日志
```
## 复现方式
```bash
cd D:/github/LuatOS
export LUAT_USE_UTEST=y
bsp/pc/build_windows_64bit_msvc.bat
mkdir -p testresult/vfs_uniform/posix_tmp
bsp/pc/build/out/luatos-lua.exe \
testcase/common/scripts/ \
testcase/utest/fs/vfs_uniform/scripts/ \
testcase/utest/fs/vfs_uniform_<fs>/scripts/ \
2>&1 | tee testresult/vfs_uniform/<fs>_run.log
```
## 下一步
1. ~~修复 pgfs 的 FTL "no free blocks" 问题 (优先级 high)~~ — 本轮已修 (commit `19e49d586`)
2. ~~修复 tfs mount 的 name marker 问题 (luat_little_flash_tfs.c:478)~~ — 本轮已修 (commit `90267fbbc`)
3. ~~修复 fatfs 在 PC BSP 上的模块加载 (luat_base_mini.c 实际未注册)~~ — 本轮已修
- 主分支 commit `419030781` (require 风格探针) → 修订版在
`.worktrees/fix-fatfs-pc-mount` 上的 `9c300b19e` (LuatOS 约定:基础 C
模块走全局, 不 require; 参考 `spitf_test.lua:13`)
- 修订版需要把 `419030781` 替换/移除, 历史清理待协调
4. 修复 lfs2 的 refcount 缺失 (在 luat_fs_lfs2.c:134 加引用检查) — MED, 排期中
5. 修复 pgfs 的 refcount 缺失 (pgfs 同 lfs2) — MED, 排期中
6. 评估 posix 在 Linux/macOS 上的语义 (Windows rename 不能覆盖是 OS 行为)