usmart.c
usmart_sys_cmd_exe,usmart.c:137)4.4 定时/调度与 uCOS-II 的配合4.5 关键全局变量的读写与触发条件4.6 错误的产生与回显5. 对外接口6. 依赖关系7. 备注与风险点源文件路径:
common/usmart/usmart.c所属模块:common / usmart(串口调试交互组件) 代码行数:486
1. 文件职责
本文件是 ALIENTEK USMART V3.1 串口调试组件的核心实现(注释标明来自 ALIENTEK,usmart.c:158),被 Alpha EMS Team 移植到 GD32F407 + uCOS-II 平台上。它让工程师通过 UART0 串口助手,直接以「函数名(参数,...)」的文本形式调用固件里的任意已注册 C 函数,并回显返回值与执行耗时,从而不必重新编译烧录即可现场调试。它在数据流的旁路调试通道上:不属于 EMS 正常业务链路,由 midware/debug/shell_usmart.c:task_usmart() 这个独立 uCOS-II 任务驱动,并另有 TIMER3 溢出中断调用 usmart_dev.scan()。整个文件(usmart.c:27 起至 usmart.c:486)被 #if DEBUG_SUMART 包住,而 DEBUG_SUMART 在 midware/debug/shell_usmart.h:13 定义为 0,因此在当前发布固件中本文件编译后为空,仅保留源码。它注册的函数表 usmart_nametab[] 与设备实例 usmart_dev 定义在 midware/debug/shell_usmart.c:14 / shell_usmart.c:32,不在本文件内。
2. 关键数据结构与宏
| 名称 | 类型(结构体/枚举/宏/联合体) | 说明 |
|---|---|---|
struct _m_usmart_nametab | 结构体(定义于 usmart.h:39) | USMART 函数注册表表项:void* func 函数指针 + const u8* name 声明字符串(如 "void usmart_test(void)") |
struct _m_usmart_dev | 结构体(定义于 usmart.h:45) | USMART 设备实例类型,聚合函数表指针、4 个操作函数指针(init/cmd_rec/exe/scan)、fnum 函数数量、pnum 参数数量、id 命中函数 ID、sptype 进制显示、parmtype 参数类型位图、plentbl[MAX_PARM] 参数长度表、parm[PARM_LEN] 参数缓冲、runtimeflag / runtime 计时 |
usmart_dev | 全局变量(定义在 midware/debug/shell_usmart.c:32,usmart.h:66 声明) | 本文件所有函数操作的单例 |
usmart_nametab[] | 全局数组(定义在 midware/debug/shell_usmart.c:14,usmart.h:65 声明) | 已注册可调用函数表 |
sys_cmd_tab[] | 全局数组(定义于 usmart.c:121) | 6 个内置系统命令字符串表:help / list / id / hex / dec / runtime |
MAX_FNAME_LEN | 宏(usmart.h:17) | 30,函数名最大长度 |
MAX_PARM | 宏(usmart.h:18) | 10,单函数最多参数个数 |
PARM_LEN | 宏(usmart.h:19) | 200,所有参数合并存储的字节上限 |
USMART_ENTIMX_SCAN | 宏(usmart.h:22) | 1,声明「用定时器中断扫描」。本文件内未被引用,实际扫描入口被硬编码在 TIMER3_IRQHandler |
USMART_USE_WRFUNS | 宏(usmart.h:26) | 0,读写任意地址功能开关,控制 usmart.c:461 的 read_addr / write_addr 是否编译 |
USMART_OK | 宏(usmart.h:29) | 0,无错误 |
USMART_FUNCERR | 宏(usmart.h:30) | 1,函数错误 |
USMART_PARMERR | 宏(usmart.h:31) | 2,参数错误 |
USMART_PARMOVER | 宏(usmart.h:32) | 3,参数溢出 |
USMART_NOFUNCFIND | 宏(usmart.h:33) | 4,未找到匹配函数 |
SP_TYPE_DEC | 宏(usmart.h:35) | 0,参数十进制显示 |
SP_TYPE_HEX | 宏(usmart.h:36) | 1,参数十六进制显示 |
DEBUG_SUMART | 宏(midware/debug/shell_usmart.h:13) | 0,本文件整体编译开关 |
局部结构:u8 data[50](usmart.c:415)、u32 temp[MAX_PARM](usmart.c:321)、u8 tstr[PARM_LEN+1](usmart_str.c:413)。
3. 函数清单
| 函数名 | 作用域(static/全局) | 说明(一句话) |
|---|---|---|
TIMER3_IRQHandler | 全局(裸机中断向量名,usmart.c:35) | TIMER3 溢出中断服务程序:进入临界区、调用 usmart_dev.scan()、复位 CNT 与 ARR 为 100、清中断标志 |
usmart_get_runtime | 全局(usmart.h:75 声明) | 读取函数执行耗时:把溢出次数按 0xFFFF 折算后加上当前 CNT,返回以 0.1ms 为单位的累计值 |
usmart_reset_runtime | 全局(usmart.h:76 声明) | 计时归零:清 TIMER3 溢出标志、把 ARR 设为 0xFFFF、CNT 清 0、usmart_dev.runtime=0 |
usmart_test | 全局(usmart.h:77 声明) | 调试自测函数,打印 TIMER3 前后的 CNT 值与 ARR 值,用于确认计时器是否在跑 |
usmart_init | 全局(usmart.h:69 声明) | 初始化:timer3_init(999, 时钟/10000-1) 配成 10kHz / 100ms 中断,并把 sptype 预设为 1(十六进制显示) |
usmart_sys_cmd_exe | 全局(usmart.h 未声明,仅本文件内使用) | 解析并执行 6 个内置系统命令(help/list/id/hex/dec/runtime),以及 hex/dec 的进制转换功能 |
usmart_cmd_rec | 全局(usmart.h:70 声明) | 识别串口收到的字符串:先解析函数名与外部参数个数,在 usmart_dev.funs[] 中匹配,命中后解析具体参数,写入 usmart_dev.id / pnum |
usmart_exe | 全局(usmart.h:71 声明) | 执行已识别的函数:打印函数名与参数,按 pnum 走 0~10 个参数的分支做强制类型转换调用,回显返回值与耗时 |
usmart_scan | 全局(usmart.h:72 声明) | 主扫描入口:检查 UART0 是否有异步接收数据,取长度并读空 FIFO,先尝试函数调用再退化为系统命令,最后按错误码打印中文提示 |
read_addr | 全局(usmart.h:73 声明,受 USMART_USE_WRFUNS 保护,usmart.c:469) | 按绝对地址读 32 位值(默认不编译) |
write_addr | 全局(usmart.h:74 声明,受 USMART_USE_WRFUNS 保护,usmart.c:480) | 按绝对地址写 32 位值(默认不编译) |
本文件无 static 函数;sys_cmd_tab[](usmart.c:121)是唯一在本文件定义的全局数据。
4. 核心逻辑解析
4.1 命令解析状态机
USMART 的核心是一条「文本 → 函数指针调用」的流水线,没有显式状态变量,而是由三个函数串联成隐式状态机,状态保存在 usmart_dev.id / usmart_dev.pnum / usmart_dev.parm* 上:
UART0 收到字节
│
▼
usmart_scan() ──1── usart0_is_recv_async_data()? 否 → 返回
──2── 是 → len = usart0_get_data_len()
│ len < 2 → 返回(丢弃 1 字节命令)
│ for i in 0..len-1: data[i] = usart0_read_data_async()
▼
usmart_cmd_rec(data)
│ usmart_get_fname(data, rfname, &rpnum, &rval) ← usmart_str.c 解析「接收到的函数名+形参个数」
│ 在 usmart_dev.funs[] 中逐个 usmart_get_fname + usmart_strcmp 比对
│ 命中:spnum > rpnum → USMART_PARMERR(本地函数形参比用户输入多)
│ 否则记 usmart_dev.id = i 并跳出
│ 未命中:i == usmart_dev.fnum → USMART_NOFUNCFIND
│ 然后 usmart_get_fparam(data, &i) 解析实参 → usmart_dev.pnum = i
▼
sta == 0 ?
├── 是 → usmart_exe() (用户函数调用路径)
└── 否 → usmart_sys_cmd_exe(data)(内置命令路径)
若返回 != USMART_FUNCERR 则覆盖 sta
最后按 sta 打印「函数错误!/参数错误!/参数太多!/未找到匹配的函数!」
状态迁移条件可以总结为:「能匹配到注册表中的函数名」→ 走 usmart_exe;否则尝试内置命令;两者都不成立则输出错误文本。注意 usmart_cmd_rec 返回非 0 时才去试系统命令,所以解析失败(如缺右括号)不会退化到命令解析。
4.2 参数编码与调用约定(关键算法)
参数在内存中被「打包」进 usmart_dev.parm[PARM_LEN],用法由 usmart_dev.plentbl[MAX_PARM] 与 usmart_dev.parmtype 位图描述:
plentbl[i](usmart.h:58):第 i 个参数占用的字节数。数值参数固定 4(usmart_str.c:436),字符串参数为strlen+1(usmart_str.c:445)。parmtype(usmart.h:57):bit i = 1 表示第 i 个参数是字符串,= 0 表示数字(usmart_str.c:435清位、usmart_str.c:444置位)。usmart_get_parmpos(num)(usmart_str.c:392):把plentbl[0..num-1]累加,得到第 num 个参数在parm[]中的起始偏移。由于参数变长,取值必须靠这个前缀和,不能按下标乘 4 计算。
usmart_exe()(usmart.c:317)的调用分派是一段 0~10 参数的手工 switch(usmart.c:350-390):
switch(usmart_dev.pnum) {
case 0: res = (*(u32(*)())func)(); break; // usmart.c:352
case 1: res = (*(u32(*)())func)(temp[0]); break; // usmart.c:355
...
case 10: res = (*(u32(*)())func)(temp[0]..temp[9]); break; // usmart.c:386
}
temp[] 的填充规则(usmart.c:329-347):若参数是字符串,temp[i] 存该字符串在 parm[] 中的地址;若是数字,temp[i] 存该地址里读出的 u32 值。这就是注释里说的「参数转换,使之支持了字符串」(usmart.c:321)——把字符串按值传递、数字按地址解引用,统一塞进 u32 数组后按 u32 原型强转调用。函数指针一律被转成 u32(*)(),因此返回值也一律按 u32 取。
rval(返回值存在与否)来自函数名字符串的返回类型解析:usmart_get_fname 在 usmart_str.c:231 判断函数名前 5 个非空字符是否为 "void",是则 *rval=0,否则 1。usmart_exe() 在 usmart.c:392 依据它决定是打印 =0X%X; 还是只打印 ;。
4.3 6 个内置系统命令(usmart_sys_cmd_exe,usmart.c:137)
先 usmart_get_cmdname 取出命令词,再在 sys_cmd_tab[](usmart.c:121)里线性查找,得到 0~5 的索引直接作为 switch 分支:
| index | 命令 | 行为 | 代码位置 |
|---|---|---|---|
| 0 | help | 打印 USMART V3.1 帮助文本与 6 个命令说明 | usmart.c:156-173 |
| 1 | list | 遍历 usmart_dev.funs[0..fnum-1] 打印已注册函数名 | usmart.c:175-180 |
| 2 | id | 遍历函数表,用 usmart_get_fname 取出裸函数名并打印其函数指针地址 | usmart.c:182-191 |
| 3 | hex | 取一个参数:解析成功则打印 HEX:0X%X 做进制转换;若参数是纯非数字(usmart_str2num 返回 4)则把 sptype 设为 SP_TYPE_HEX(仅切换显示进制) | usmart.c:193-219 |
| 4 | dec | 同 hex,打印 DEC:%lu 或切换 sptype 为 SP_TYPE_DEC | usmart.c:221-246 |
| 5 | runtime | 参数直接赋给 usmart_dev.runtimeflag,非 0 打印 Run Time Calculation ON | usmart.c:248-267 |
default 分支返回 USMART_FUNCERR(usmart.c:269-270),由 usmart_scan 打印「函数错误!」。
4.4 定时/调度与 uCOS-II 的配合
本文件涉及两条并发的驱动路径,这是理解它行为的关键:
路径 A:专用任务轮询。
midware/debug/shell_usmart.c 的 task_usmart()(for(;;) { usmart_scan(); OSTimeDlyHMSM(0,0,0,100); })每 100ms 调用一次 usmart_scan(),是正常设计的驱动方式。该任务由 user/main.c:321 起的 #if DEBUG_SUMART 块中的 OSTaskCreateExt(task_usmart, ...) 创建,堆栈为 TASK_USMART_STK(user/main.c:50)。
路径 B:TIMER3 溢出中断。
usmart_init()(usmart.c:109)用 timer3_init(999, rcu_clock_freq_get(CK_APB1)*4/10000 - 1) 把 TIMER3 配成「计数频率 10kHz、每 100ms 溢出一次」。中断里(usmart.c:41-47):
if (timer_interrupt_flag_get(TIMER3, TIMER_INT_FLAG_UP) != RESET) {
usmart_dev.scan(); // 执行 usmart 扫描
timer3_set_counter(0); // 清空 CNT
timer3_set_period(100); // 恢复原来的设置
timer_interrupt_flag_clear(...); // 清中断标志
}
进入/退出中断用 OSIntEnter() / OSIntExit() 包裹(usmart.c:37-39、usmart.c:49-51),交给 uCOS-II 做中断嵌套计数与任务切换。路径 A 与路径 B 同时存在意味着:一旦使能 DEBUG_SUMART,usmart_scan() 会既被任务每 100ms 调一次、又被 TIMER3 中断每 100ms 调一次,两者对同一个 usmart_dev 无任何互斥保护。
耗时统计(runtime)机制。 以 TIMER3 的 CNT 为时基,约定「计数频率 10kHz ⇒ 1 个 CNT = 0.1ms」(usmart.c:111 注释):
usmart_exe()在执行函数前调usmart_reset_runtime()(usmart.c:349):清溢出标志 + 把 ARR 从 999 改成0xFFFF(usmart.c:79),即让计数器只在大约 6.55s 后才溢出,从而单次函数耗时不被打断;同时 CNT 清 0、runtime清 0。- 执行完调用
usmart_get_runtime()(usmart.c:391,返回值被丢弃),该函数把「是否已溢出」折算为+0xFFFF后累加当前 CNT(usmart.c:62-67)。 - 打印时用
usmart_dev.runtime/10与%10拼成%d.%1dms(usmart.c:402)。 - 只有
usmart_dev.runtimeflag为真才打印(usmart.c:400),由runtime 1命令开启。 - 中断路径中的
timer3_set_period(100)(usmart.c:45)负责把 ARR 从0xFFFF恢复到 100(注释写 100,与usmart_init里的 999 不一致)。
4.5 关键全局变量的读写与触发条件
| 全局变量 | 写点 | 读点 | 触发条件 |
|---|---|---|---|
usmart_dev.id | usmart.c:298(usmart_cmd_rec 匹配成功时) | usmart.c:324(usmart_exe) | 串口字符串首个词与 usmart_nametab[] 某项函数名相等 |
usmart_dev.pnum | usmart.c:307(实参个数) | usmart.c:350 的 switch | usmart_get_fparam 解析完 (...) 内容 |
usmart_dev.parmtype / plentbl / parm | usmart_str.c:434-445 | usmart.c:331-339 | 逐个参数解析时按类型写入 |
usmart_dev.sptype | usmart.c:112(init 置 1)、usmart.c:210(hex)、usmart.c:238(dec) | usmart.c:340、usmart.c:394 | 用户输入 hex / dec 命令 |
usmart_dev.runtimeflag | usmart.c:256 | usmart.c:400 | 用户输入 runtime 1 / runtime 0 |
usmart_dev.runtime | usmart.c:64、usmart.c:81 | usmart.c:402 | 每次 usmart_exe 前后 |
usmart_dev.funs / fnum | 由 midware/debug/shell_usmart.c:14/39 静态初始化 | usmart.c:178,187,289,325 | 只读 |
4.6 错误的产生与回显
错误码在两条链路上汇总:usmart_cmd_rec 与 usmart_get_fparam 各自返回 USMART_* 码,usmart_sys_cmd_exe 也返回同族错误码(usmart.c:147,205,216,233,243,262,265,270)。usmart_scan 在 usmart.c:439-455 把它们翻译成中文提示:
| 错误码 | 提示文本 |
|---|---|
USMART_FUNCERR | 函数错误! |
USMART_PARMERR | 参数错误! |
USMART_PARMOVER | 参数太多! |
USMART_NOFUNCFIND | 未找到匹配的函数! |
注意 usmart_scan:436 处 if(len != USMART_FUNCERR) sta = len; —— 变量 len 在此被复用为系统命令的返回值,若系统命令返回 USMART_FUNCERR,则保留先前的 sta(usmart_cmd_rec 的错误码),因此用户看到的是「函数/参数/查找失败」提示而不是「函数错误」。
5. 对外接口
导出(供外部调用)
| 符号 | 声明位置 | 使用者 |
|---|---|---|
usmart_init(u8 sysclk) | usmart.h:69 | midware/debug/shell_usmart.c:task_usmart() |
usmart_cmd_rec(u8*str) | usmart.h:70 | 通过 usmart_dev.cmd_rec 函数指针间接调用(usmart.c:431) |
usmart_exe(void) | usmart.h:71 | 通过 usmart_dev.exe 函数指针间接调用(usmart.c:432) |
usmart_scan(void) | usmart.h:72 | 直接调用:usmart.c:43(中断内)、shell_usmart.c:task_usmart();函数指针 usmart_dev.scan |
read_addr(u32 addr) | usmart.h:73 | 经 usmart_nametab[] 注册(shell_usmart.c:16,仅 USMART_USE_WRFUNS==1 时) |
write_addr(u32 addr,u32 val) | usmart.h:74 | 同上(shell_usmart.c:17) |
usmart_get_runtime(void) | usmart.h:75 | 本文件内部(usmart.c:391) |
usmart_reset_runtime(void) | usmart.h:76 | 本文件内部(usmart.c:349) |
usmart_test(void) | usmart.h:77 | 经 usmart_nametab[] 注册(shell_usmart.c:19) |
sys_cmd_tab[] | 无声明(仅 usmart.c:121 定义) | 仅本文件内使用 |
usmart_sys_cmd_exe(u8 *str) | usmart.h 未声明(仅 usmart.c:137 定义) | 仅本文件内使用(usmart.c:435);未加 static,会导出为全局符号 |
TIMER3_IRQHandler(void) | user/startup_gd32f407.s:263(EXPORT + WEAK),startup_gd32f407.s:116 向量表第 46 项 | 硬件向量表 |
本文件不定义自己的全局变量;它操作的全部状态都来自 midware/debug/shell_usmart.c 中定义的 usmart_dev。
被本文件调用(外部依赖)
| 符号 | 来源 |
|---|---|
timer3_init / timer3_set_counter / timer3_set_period / timer3_get_counter / timer3_get_period | hardware/timer/timer3.h(实现在 hardware/timer/timer3.c) |
timer_interrupt_flag_get / timer_interrupt_flag_clear | GD32 标准外设库 gd32f4xx_timer.h |
usart0_is_recv_async_data / usart0_get_data_len / usart0_read_data_async | hardware/usart0/usart0.h |
OSIntEnter / OSIntExit | ucos_ii.h(uCOS-II) |
printf / memset | stdio.h / string.h |
rcu_clock_freq_get / RESET / SystemCoreClock | GD32 标准外设库、chip.h |
usmart_get_cmdname / usmart_strcmp / usmart_get_fname / usmart_get_aparm / usmart_str2num / usmart_get_parmpos / usmart_get_fparam | common/usmart/usmart_str.c |
usmart_dev / usmart_nametab[] | midware/debug/shell_usmart.c |
6. 依赖关系
- 依赖:
usmart.h(结构体、宏、原型)、stdio.h、usart0.h、string.h、timer3.h、ucos_ii.h、shell_usmart.h(提供DEBUG_SUMART),并通过它们间接依赖 GD32 标准外设库与common/usmart/usmart_str.c。 - 被依赖:
midware/debug/shell_usmart.c(定义usmart_dev/usmart_nametab[],实现task_usmart()调用usmart_init与usmart_scan)、user/main.c:50与user/main.c:321(TASK_USMART_STK与task_usmart的创建)、user/startup_gd32f407.s(弱定义的TIMER3_IRQHandler被本文件覆盖)。当前DEBUG_SUMART == 0,实际无任何模块会链接到本文件的符号。
7. 备注与风险点
usmart_scan的栈缓冲越界(最严重):u8 data[50](usmart.c:415)而len = usart0_get_data_len()(usmart.c:420)最大可达 UART0 收包缓冲的UART0_BUFFER_LEN = 256(hardware/usart0/usart0.h:7)。for(size_t i = 0; i < len; i++) data[i] = usart0_read_data_async();(usmart.c:427-430)在没有长度钳位的情况下最多写 256 字节到 50 字节的栈数组,属确定性栈溢出。上位机若一次发下超过 50 字节即会破坏任务栈。usmart_get_runtime的返回值被丢弃:usmart.c:391调用后未接收结果,注释写「获取函数执行时间」,但真正打印的是下一行读的usmart_dev.runtime(由该函数副作用更新)。写法易误读。- 计时窗口未关闭:
usmart_reset_runtime()把 ARR 改成0xFFFF后,在本文件内没有任何地方把 ARR 恢复成 999,只靠中断里的timer3_set_period(100)(usmart.c:45)在 TIMER3 溢出时才恢复。若函数执行超过约 6.55s(ARR=0xFFFF / 10kHz)才会触发恢复,这段时间内 TIMER3 的 100ms 节拍实际被破坏。 timer3_set_period(100)与初始化值不一致:usmart_init用的是timer3_init(999, ...)(usmart.c:111),中断里却恢复为100(usmart.c:45),注释写「恢复原来的设置」与实际值矛盾。- 中断与任务重复扫描且无互斥:
usmart_scan()同时挂在 TIMER3 中断(usmart.c:43)和task_usmart任务(shell_usmart.c)上,二者都读写usmart_dev、都调用printf,无信号量或临界区保护,存在数据竞态与串口输出交叉。 usmart_test里被注释掉的延迟:usmart.c:97的// delay_ms(1000);是调试残留,导致函数打印的diff永远接近 0,失去自测意义。- 裸机向量名在 RTOS 文件中实现:
TIMER3_IRQHandler直接写在common/usmart/usmart.c(usmart.c:35),与hardware/timer/timer3.c的职责边界混乱;hardware/timer/timer3.c中并无同名实现,全靠启动文件的WEAK定义兜底(user/startup_gd32f407.s:263、:346)。 - 两个函数未加
static也无头文件声明:usmart_sys_cmd_exe(usmart.c:137)与全局数组sys_cmd_tab[](usmart.c:121)都没有对外契约,却导出为全局符号,存在命名冲突与误用风险。 - 强转函数指针调用依赖 ABI 巧合:
usmart_exe把所有函数指针统一转成u32(*)()(usmart.c:353-389)。对返回void、参数为u8/u16/指针的已注册函数,只在 ARM AAPCS 下「参数放寄存器、返回值放 r0」的约定下才勉强工作,spnum > rpnum只检查个数不检查类型(usmart.c:296),类型不匹配时会静默传错值。 sizeof(sys_cmd_tab)/4的硬编码元素大小(usmart.c:149):用/4而不是/sizeof(u8*),在指针非 4 字节的平台(如 64 位主机侧编译)会算出错误项数。当前 GD32F407 为 4 字节指针,暂无问题。usmart_scan丢弃长度 1 的报文(usmart.c:422-425):len < 2直接 return,且没有把已到达的数据读走,下次调用会读到陈旧内容。usmart_dev.scan()在中断中做了重活(usmart.c:43):中断服务程序里执行字符串解析、函数调用与printf,严重违反 ISR 应尽量短的原则,会拉高系统中断延迟。- 当前构建下整文件被裁掉:
DEBUG_SUMART为 0(midware/debug/shell_usmart.h:13),上述所有问题在发布固件中不生效,但一旦把该宏改为 1 调试,第 1 条越界会立刻暴露。