init: Air780EPM 官方 LuatOS 项目代码基线
- 来源: 合宙 LuatOS 官方仓库 air780epm 模块完整代码 - 路径: luatos/air780epm/module/Air780EPM/demo 含官方 demo(含 mqtt/mqtts/socket/uart 等) - 后续: 基于 demo 开发 UART<->MQTT 数据上报功能
This commit is contained in:
@@ -0,0 +1,399 @@
|
||||
# ARP 1000ms 定时器低功耗化改造 / netdrv.arpSleep API
|
||||
|
||||
> 适用:基于 `net_lwip2` adapter + `netdrv` 框架的 BSP(Air8000A/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 task,task 收到后立即 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 需要 ARP(admin_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/)
|
||||
@@ -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-T(RFC 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/21(ECP-256/384/521,RFC 4753),
|
||||
ESP AES-GCM-128/256(RFC 4106 AEAD,SA 内无 INTEG transform);
|
||||
- MOBIKE(RFC 4555,opt-in,默认关闭):本地地址变化时发送
|
||||
INFORMATIONAL(UPDATE_SA_ADDRESSES + NAT-D),处理对端发起的地址更新,
|
||||
以及 established 状态下的对端地址学习;不做 ADDITIONAL_IP4_ADDRESS
|
||||
通告与 COOKIE2;
|
||||
- CP(Configuration Payload)下发虚拟 IPv4 + DNS;
|
||||
- ESP 隧道模式(RFC 4303)AES-CBC-128/256 + HMAC-SHA1-96 /
|
||||
HMAC-SHA2-256-128 或 AES-GCM-128/256,32 包反重放窗口,
|
||||
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` | MD4(Apache-2.0, 从 mbedTLS 2.x vendor, 自包含) |
|
||||
| `include/ipsec/ipsec_vendor_chap_ms.h` + `src/ipsec_vendor_chap_ms.c` | MS-CHAPv2(BSD, 移植自 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_INIT,NAT 探测后切换到本地/远端 4500(RFC 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 transform,transform id 20 + KEY_LENGTH 属性)。
|
||||
- CHILD_SA 重协商默认带 PFS:请求载荷顺序 `SA | Ni | KE | TSi | TSr`,
|
||||
使用 IKE_SA_INIT 协商出的 DH 组生成新的临时密钥对;响应解析新 Nr 与
|
||||
KE,KEYMAT = 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 AEAD(RFC 4106):8 字节显式 IV,GCM IV = salt(4) | IV(8),
|
||||
AAD = SPI | SEQ,ICV 16 字节,padding 按 4 字节对齐;KEYMAT 每方向为
|
||||
`enc_key(16/32) + salt(4)`(RFC 7296 §3.3.2),无 INTEG 密钥。
|
||||
- AUTH:EAP 模式按 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 7296(IKEv2)、RFC 3948(NAT-T)、RFC 4303(ESP)、
|
||||
RFC 2759/3079(MS-CHAPv2/MSK)
|
||||
- `docs/l2tp-design.md`(线程模型/胶水层/构建接线参考)
|
||||
- `components/network/l2tp/src/l2tp_client.c` /
|
||||
`components/network/openvpn/src/ovpn_client.c`(传输与 netif 参考)
|
||||
@@ -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
|
||||
@@ -0,0 +1,322 @@
|
||||
# netdrv L2TPv2 客户端驱动设计文档
|
||||
|
||||
> 适用范围:在 LuatOS `netdrv` 框架下新增 L2TPv2(RFC 2661)客户端驱动(LAC),
|
||||
> 当前首期落地 PC 模拟器(`bsp/pc`),硬件 BSP 按需开启 `LUAT_USE_NETDRV_L2TP`。
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
LuatOS 需要在内网/办公网场景下提供「二层隧道 + PPP 拨号」的能力,典型形态是
|
||||
接入企业 LNS(L2TP Network Server)获得内网 IP 并访问内网资源。
|
||||
|
||||
需求边界(与 lwip 自带 `pppol2tp.c` 的能力边界一致):
|
||||
|
||||
- ✅ L2TPv2 客户端(LAC),PPP over L2TP(UDP 隧道)
|
||||
- ✅ 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 AVP(MD5(消息类型|secret|challenge))。
|
||||
- 认证选项由**对端**在 LCP ConfReq 中提出:lwip 客户端的 LCP ConfReq 本身
|
||||
不携带 auth 选项(`lcp_addci` 使用 gotoptions,客户端侧该位不置位),
|
||||
因此 LNS 必须主动要求认证(真实 LNS 行为一致)。
|
||||
|
||||
### 3.6 断线检测与自动重连
|
||||
|
||||
- **PPP 层**:LCP keepalive(3s 间隔、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->24(PPP/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 对该网卡返回 -1(IP 由 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 必须用**客户端分配**的 ID(lwip 按 `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、MPPE(CCP)、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 2661(Layer 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 Agent)Thin 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 rfa(Radio 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 API(mobile.rfTestMode/Input/Param/Imei/ImeiSet)。
|
||||
- bsp/pc/port/luat_mobile_pc.c:PC 仿真后端完整实现,单元测试 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.c:AT+ECRFNST 调用 RfAtNstCmdPreHandle()。
|
||||
- PLAT/middleware/developed/at/atcust/src/atec_product.c:AT+ECNPICFG、AT+ECGMDATA、AT+ECRFSTAT 等。
|
||||
- PLAT/middleware/developed/at/atcust/src/atec_plat_dev.c:AT+ECPMUCFG。
|
||||
- PLAT/middleware/developed/open_am/driver/src/am_service.c:soc_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 API:param、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 1:luatos-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 2:LuatOS 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 3:LuatOS 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 4:Golden / 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 1:C 后端补齐 | PR 到 luatos-soc-2024 | ec718pm / ec718hm 编译通过;mobile.rfTestParam/Imei 真机可用 |
|
||||
| W1-W2 | Phase 2:rfa.lua 扩展 | PR 到 LuatOS | PC 端 13+ 新增 case 全过 |
|
||||
| W2 | Phase 3:C 绑定修正 | 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 而非直接写 /rfTestFile;PC 端用内存缓冲 |
|
||||
| 产线工具对 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 行为)
|
||||
Reference in New Issue
Block a user