9bbe117f97
用户选定最彻底方案(B2):DMA + 定时器 TRGO 硬件触发 + 消费上一拍 已完成的半区。对应 P1-5 拆出的"单拍硬上界",现已办结。 硬件事实更正(查 CubeMX 器件库 + 本仓 HAL 头证实):F4 上 ADC1 的 外部触发清单里没有 TIM6/TIM7 TRGO(只接 DAC)。故 1 kHz 节拍源由 TIM6 改为 TIM3(PSC 83 / ARR 999,TRGO=UPDATE),同一个更新事件 同时 ① ISR 脉冲 TPAFE CONVST ② 经 TRGO 触发 ADC1 一轮 9 通道 ③ 置位 ADC_WAKE_FLAG 唤醒 adcTask。TIM6 从 .ioc 整体移除。 代码: - Src/tim.c/h:新增 MX_TIM3_Init(含 MasterSlaveMode),删 htim6 - Src/adc.c:ExternalTrigConv=T3_TRGO + EDGE_RISING + DMAContinuousRequests=ENABLE;MspInit 挂 hdma_adc1(DMA2_Stream4 循环模式)。DDS 位关掉 = 一轮只发一次请求,静默丢 8 路 - Src/dma.c:DMA2_Stream4 NVIC 优先级 6(CubeMX 换流会把它重置为 0, 优先级 0 会抢占 FreeRTOS 临界区,违反"外设中断 ≥5") - Src/main.c:周期回调加 TIM3 分支;stm32f4xx_it.c 向量同步 - App/algo/adc_input.c:stm32_scan() → stm32_take(),逐通道 PollForConversion 全删;乒乓半区由完成计数奇偶推出(只读一个 32 位量 → 与 ISR 无竞态);STM32 侧数据固定滞后 1 拍 - App/task/adc_task.c:心跳日志拆出四类计数 观测性:adc1_seq(完成计数)/ adc1_gap(seq 前进 >1 = 丢帧)/ adc1_err(连续 ≥2 拍无新帧 = 停摆)/ adc1_hw(HAL 错误回调)。 单拍无新帧是消费者相位伪影(DMA 完成落在唤醒后 ≈67 µs),不计故障。 验证: - 主机单测 79 → 101 全绿;反向验证四连均咬(半区奇偶取反 11 条、 停摆拍置 valid 4 条、STALL_RUN_MAX 改 1 为 6 条、gap 阈值 1 条) - @build 0 错 0 警,Code 256872 → 257660 - @test PASS:adcp 122 µs → 8 µs、adc1_err=0、adc1_hw=0、 adc1_gap=27/20002 拍、adc1_seq 与 seq 恒定偏移 2 - stack_report 7/7 PASS,adcTask 832 B / 余 59%(不变) - cubemx-survival 新增 5 项审计(TIM3 分支 / T3_TRGO / DDS / DMA 挂载 / 流优先级),28 通过 0 失败 文档:ADC 采样节拍设计说明新增 §11;看门狗停摆分析 §4.4 与 §8 收口; 算法规划 L1-1/L1-3、通道映射、测试说明、各 README 同步。
291 lines
11 KiB
Python
291 lines
11 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
FreeRTOS 任务表一致性工具(D-2 / D-7)。
|
||
|
||
背景
|
||
----
|
||
`README.md` 与 `AGENTS.md` 各有一张 FreeRTOS 任务表(任务名 / 栈大小 / 优先级)。
|
||
历史上这两张表全靠手写,`Src/freertos.c` 改栈后表格忘记同步——2026-09 就出现过
|
||
两张表都比源码落后一大截(写 2048/4096/4096/4096/16384/2048,实际 1024...512)。
|
||
本脚本以 `Src/freertos.c` 为**唯一事实源**,生成 / 校验这两张表。
|
||
|
||
事实源
|
||
------
|
||
解析 `Src/freertos.c` 里形如:
|
||
|
||
const osThreadAttr_t netTask_attributes = {
|
||
.name = "netTask",
|
||
.stack_size = 1024 * 4, /* 注释 */
|
||
.priority = (osPriority_t) osPriorityNormal,
|
||
};
|
||
|
||
的块,抽出 name / stack_words / priority。`stack_size` 一律按 `N * 4`
|
||
(CMSIS-RTOS2 的 stack_size 单位是字节,工程约定写成 `words * 4`)换算为 words。
|
||
|
||
用法
|
||
----
|
||
python scripts/gen_task_table.py # 检查模式(默认):不一致即退出码 1
|
||
python scripts/gen_task_table.py --check # 同上
|
||
python scripts/gen_task_table.py --write # 就地重写 README/AGENTS 中的任务表
|
||
python scripts/gen_task_table.py --diff # 只打印期望表格,不写文件
|
||
|
||
pre-commit 用法:`python scripts/gen_task_table.py --check`,
|
||
任一被跟踪文档里的任务表与 `Src/freertos.c` 不一致即拦截。
|
||
|
||
退出码:0 通过;1 不一致 / 解析失败。
|
||
"""
|
||
import argparse
|
||
import pathlib
|
||
import re
|
||
import sys
|
||
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||
FREERTOS_C = REPO_ROOT / "Src" / "freertos.c"
|
||
|
||
# 需要保持同步的文档及其任务表锚点
|
||
# anchor_begin / anchor_end 之间即为「受管表格区」,脚本只替换这一段,
|
||
# 表格外的说明文字(含各文档自己的职责描述/口径注记)原样保留。
|
||
TARGETS = [
|
||
{
|
||
"path": REPO_ROOT / "README.md",
|
||
"begin": "<!-- TASK_TABLE:BEGIN generated by scripts/gen_task_table.py -->",
|
||
"end": "<!-- TASK_TABLE:END -->",
|
||
"header": "| 任务 | 栈大小 | 优先级 | 职责 |\n"
|
||
"|------|--------|--------|------|",
|
||
},
|
||
{
|
||
"path": REPO_ROOT / "AGENTS.md",
|
||
"begin": "<!-- TASK_TABLE:BEGIN generated by scripts/gen_task_table.py -->",
|
||
"end": "<!-- TASK_TABLE:END -->",
|
||
"header": "| Task | 栈 (words) | 优先级 |\n|---|---|---|",
|
||
},
|
||
]
|
||
|
||
# 文档中的展示顺序(与 README/AGENTS 既有一致;不在表内的任务按源码顺序追加)。
|
||
# 不用源码顺序:freertos.c 里 timeSyncTask 定义在最前,但文档按「主任务→辅助任务」排列。
|
||
DISPLAY_ORDER = ["defaultTask", "netTask", "ftpTask", "testTask", "uiTask",
|
||
"timeSyncTask", "adcTask"]
|
||
|
||
# 各任务在文档里的「职责」描述(README 需要,AGENTS 不需要)。
|
||
# 只影响展示文案,不参与一致性判定;未知任务回落为空。
|
||
ROLE = {
|
||
"defaultTask": "FTL + FatFS 挂载、配置引擎属主、syslog 唯一写者",
|
||
"netTask": "网络初始化 + CH395F 轮询",
|
||
"ftpTask": "FTP 服务(等待网络+存储就绪)",
|
||
"testTask": "硬件测试入口",
|
||
"uiTask": "LVGL UI 界面",
|
||
"timeSyncTask": "墙钟时间同步",
|
||
"adcTask": "TPAFE5160 采样(TIM3 1kHz 事件驱动 + 原子快照 + STM32 ADC DMA)",
|
||
}
|
||
|
||
|
||
def sort_tasks(tasks):
|
||
"""按 DISPLAY_ORDER 排序;未列出的任务保持源码相对顺序排在最后。"""
|
||
idx = {n: i for i, n in enumerate(DISPLAY_ORDER)}
|
||
return sorted(tasks, key=lambda t: (idx.get(t[0], len(idx)),))
|
||
|
||
# 任务块:const osThreadAttr_t <id>_attributes = { ... };
|
||
BLOCK_RE = re.compile(
|
||
r"const\s+osThreadAttr_t\s+(?P<id>\w+)_attributes\s*=\s*\{(?P<body>.*?)\};",
|
||
re.S,
|
||
)
|
||
STACK_RE = re.compile(r"\.stack_size\s*=\s*(?P<expr>[^,]+),")
|
||
PRIO_RE = re.compile(r"\.priority\s*=\s*\(osPriority_t\)\s*(?P<prio>\w+)")
|
||
|
||
# 优先级枚举 → 文档里使用的短名(去掉 osPriority 前缀)
|
||
PRIO_SHORT = lambda p: p[len("osPriority"):] if p.startswith("osPriority") else p
|
||
|
||
|
||
def parse_tasks(text: str):
|
||
"""从 freertos.c 文本解析任务列表,按出现顺序返回。
|
||
|
||
stack_size 允许以下写法(工程约定):
|
||
1024 * 4 -> 1024 words
|
||
(1024 * 4) -> 1024 words
|
||
4096 -> 1024 words(按 bytes/4 折算,并给出提示)
|
||
返回 [(name, words, prio, raw_expr), ...]
|
||
"""
|
||
tasks = []
|
||
for m in BLOCK_RE.finditer(text):
|
||
body = m.group("body")
|
||
sm = STACK_RE.search(body)
|
||
pm = PRIO_RE.search(body)
|
||
if not (sm and pm):
|
||
continue
|
||
expr = sm.group("expr").strip()
|
||
name_lit = re.search(r'\.name\s*=\s*"([^"]+)"', body)
|
||
name = name_lit.group(1) if name_lit else m.group("id")
|
||
|
||
# 去掉注释后求值:`1024 * 4` -> 4096 (bytes)
|
||
clean = re.sub(r"/\*.*?\*/", "", expr, flags=re.S).strip()
|
||
clean = clean.strip("()").strip()
|
||
try:
|
||
num = eval(clean, {"__builtins__": {}}, {}) # noqa: S307 (受控字面量)
|
||
except Exception:
|
||
print(f"[WARN] 无法解析 {name} 的 stack_size 表达式:{expr!r}",
|
||
file=sys.stderr)
|
||
continue
|
||
|
||
# 工程约定 = words*4,故 words = 字节/4
|
||
words = num // 4 if num % 4 == 0 else num
|
||
tasks.append((name, words, PRIO_SHORT(pm.group("prio")), expr))
|
||
return tasks
|
||
|
||
|
||
def render_table(tasks, header: str, with_role: bool) -> str:
|
||
lines = [header]
|
||
for name, words, prio, _expr in tasks:
|
||
if with_role:
|
||
lines.append(f"| {name} | {words} words | {prio} | {ROLE.get(name, '')} |")
|
||
else:
|
||
lines.append(f"| {name} | {words} | {prio} |")
|
||
return "\n".join(lines)
|
||
|
||
|
||
def replace_managed(text: str, target: dict, table: str):
|
||
"""替换 <!-- TASK_TABLE:BEGIN --> ... <!-- TASK_TABLE:END --> 之间的内容。
|
||
|
||
返回 (新文本, 是否变化)。若锚点不存在则原样返回(由调用方决定是否补锚点)。
|
||
"""
|
||
b, e = target["begin"], target["end"]
|
||
if b not in text or e not in text:
|
||
return text, False
|
||
start = text.index(b) + len(b)
|
||
end = text.index(e)
|
||
old = text[start:end]
|
||
new = "\n" + table + "\n"
|
||
if old == new:
|
||
return text, False
|
||
return text[:start] + new + text[end:], True
|
||
|
||
|
||
def extract_managed(text: str, target: dict):
|
||
b, e = target["begin"], target["end"]
|
||
if b not in text or e not in text:
|
||
return None
|
||
start = text.index(b) + len(b)
|
||
end = text.index(e)
|
||
return text[start:end].strip()
|
||
|
||
|
||
def read_doc(path: pathlib.Path):
|
||
"""读文档,返回 (文本, 原换行符, 是否有 BOM)。
|
||
|
||
本仓库 core.autocrlf=true 且无 .gitattributes,工作区换行混杂
|
||
(README=LF、AGENTS=CRLF);带 BOM 文件(App/cfg/cfg_file.c 等)另有铁律。
|
||
--write 必须**按原样回写**,否则整文件行尾/BOM 漂移 → 巨型无意义 diff。
|
||
"""
|
||
raw = path.read_bytes()
|
||
bom = raw.startswith(b"\xef\xbb\xbf")
|
||
body = raw[3:] if bom else raw
|
||
crlf = body.count(b"\r\n")
|
||
lf_only = body.count(b"\n") - crlf
|
||
newline = "\r\n" if crlf > lf_only else "\n"
|
||
text = body.decode("utf-8", errors="replace")
|
||
# 统一成 \n 供内部处理
|
||
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
||
return text, newline, bom
|
||
|
||
|
||
def write_doc(path: pathlib.Path, text: str, newline: str, bom: bool):
|
||
"""按原换行符 / BOM 状态回写(text 内部为 \n)。"""
|
||
out = text if newline == "\n" else text.replace("\n", newline)
|
||
data = out.encode("utf-8")
|
||
if bom:
|
||
data = b"\xef\xbb\xbf" + data
|
||
path.write_bytes(data)
|
||
|
||
|
||
def main(argv):
|
||
ap = argparse.ArgumentParser(description="FreeRTOS 任务表生成/校验")
|
||
g = ap.add_mutually_exclusive_group()
|
||
g.add_argument("--check", action="store_true", help="检查模式(默认)")
|
||
g.add_argument("--write", action="store_true", help="就地重写受管表格区")
|
||
g.add_argument("--diff", action="store_true", help="只打印期望表格")
|
||
args = ap.parse_args(argv[1:])
|
||
|
||
if not FREERTOS_C.exists():
|
||
print(f"[FAIL] 找不到事实源:{FREERTOS_C}", file=sys.stderr)
|
||
return 1
|
||
c_text, _nl, _bom = read_doc(FREERTOS_C)
|
||
tasks = parse_tasks(c_text)
|
||
if not tasks:
|
||
print("[FAIL] 未能从 Src/freertos.c 解析出任何任务,请检查正则与源码格式。",
|
||
file=sys.stderr)
|
||
return 1
|
||
tasks = sort_tasks(tasks)
|
||
|
||
if args.diff:
|
||
for t in TARGETS:
|
||
with_role = t["path"].name == "README.md"
|
||
print(f"=== {t['path'].name} ===")
|
||
print(render_table(tasks, t["header"], with_role))
|
||
print()
|
||
return 0
|
||
|
||
changed, missing, problems = [], [], []
|
||
for t in TARGETS:
|
||
if not t["path"].exists():
|
||
problems.append(f"{t['path'].name} 不存在")
|
||
continue
|
||
text, newline, bom = read_doc(t["path"])
|
||
with_role = t["path"].name == "README.md"
|
||
table = render_table(tasks, t["header"], with_role)
|
||
|
||
if args.write:
|
||
new_text, did = replace_managed(text, t, table)
|
||
if did:
|
||
write_doc(t["path"], new_text, newline, bom)
|
||
changed.append(t["path"].name)
|
||
else:
|
||
got = extract_managed(text, t)
|
||
if got is None:
|
||
missing.append(t["path"].name)
|
||
else:
|
||
# 提取区已归一为 \n;与期望表格比较时同样归一
|
||
if got.replace("\r\n", "\n") != table.strip():
|
||
changed.append(t["path"].name)
|
||
|
||
if args.write:
|
||
if missing:
|
||
print("[WARN] 以下文档缺少受管锚点,未写入:"
|
||
+ ", ".join(missing))
|
||
print(f" 请在任务表上下各加一行锚点:\n"
|
||
f" {TARGETS[0]['begin']}\n {TARGETS[0]['end']}")
|
||
if changed:
|
||
print("已更新任务表:" + ", ".join(changed))
|
||
if not changed and not missing:
|
||
print("任务表已是最新,无需改动。")
|
||
return 0
|
||
|
||
if problems:
|
||
for p in problems:
|
||
print(f"[FAIL] {p}", file=sys.stderr)
|
||
return 1
|
||
|
||
if missing or changed:
|
||
if missing:
|
||
print(f"[FAIL] 以下文档缺少受管锚点 "
|
||
f"({TARGETS[0]['begin']} / {TARGETS[0]['end']}):")
|
||
for n in missing:
|
||
print(f" - {n}")
|
||
if changed:
|
||
print(f"[FAIL] 以下文档的任务表与 Src/freertos.c 不一致:")
|
||
for n in changed:
|
||
print(f" - {n}")
|
||
print("\n事实源(Src/freertos.c):")
|
||
for name, words, prio, _ in tasks:
|
||
print(f" {name}: {words} words / {prio}")
|
||
print("\n期望表格(README 口径):")
|
||
print(render_table(tasks, TARGETS[0]["header"], True))
|
||
print("\n修复:运行 `python scripts/gen_task_table.py --write`")
|
||
return 1
|
||
|
||
print(f"任务表一致({len(tasks)} 个任务,"
|
||
f"{len(TARGETS)} 个文档)。")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main(sys.argv))
|