EMS 知识库
首页 / knowledge/代码解析/common/usmart

usmart.c

源文件路径: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 位图描述:

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命令行为代码位置
0help打印 USMART V3.1 帮助文本与 6 个命令说明usmart.c:156-173
1list遍历 usmart_dev.funs[0..fnum-1] 打印已注册函数名usmart.c:175-180
2id遍历函数表,用 usmart_get_fname 取出裸函数名并打印其函数指针地址usmart.c:182-191
3hex取一个参数:解析成功则打印 HEX:0X%X 做进制转换;若参数是纯非数字(usmart_str2num 返回 4)则把 sptype 设为 SP_TYPE_HEX(仅切换显示进制)usmart.c:193-219
4dec同 hex,打印 DEC:%lu 或切换 sptype 为 SP_TYPE_DECusmart.c:221-246
5runtime参数直接赋给 usmart_dev.runtimeflag,非 0 打印 Run Time Calculation ONusmart.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 注释):

4.5 关键全局变量的读写与触发条件

全局变量写点读点触发条件
usmart_dev.idusmart.c:298(usmart_cmd_rec 匹配成功时)usmart.c:324(usmart_exe)串口字符串首个词与 usmart_nametab[] 某项函数名相等
usmart_dev.pnumusmart.c:307(实参个数)usmart.c:350 的 switchusmart_get_fparam 解析完 (...) 内容
usmart_dev.parmtype / plentbl / parmusmart_str.c:434-445usmart.c:331-339逐个参数解析时按类型写入
usmart_dev.sptypeusmart.c:112(init 置 1)、usmart.c:210(hex)、usmart.c:238(dec)usmart.c:340、usmart.c:394用户输入 hex / dec 命令
usmart_dev.runtimeflagusmart.c:256usmart.c:400用户输入 runtime 1 / runtime 0
usmart_dev.runtimeusmart.c:64、usmart.c:81usmart.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:69midware/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_periodhardware/timer/timer3.h(实现在 hardware/timer/timer3.c)
timer_interrupt_flag_get / timer_interrupt_flag_clearGD32 标准外设库 gd32f4xx_timer.h
usart0_is_recv_async_data / usart0_get_data_len / usart0_read_data_asynchardware/usart0/usart0.h
OSIntEnter / OSIntExitucos_ii.h(uCOS-II)
printf / memsetstdio.h / string.h
rcu_clock_freq_get / RESET / SystemCoreClockGD32 标准外设库、chip.h
usmart_get_cmdname / usmart_strcmp / usmart_get_fname / usmart_get_aparm / usmart_str2num / usmart_get_parmpos / usmart_get_fparamcommon/usmart/usmart_str.c
usmart_dev / usmart_nametab[]midware/debug/shell_usmart.c

6. 依赖关系

7. 备注与风险点

  1. 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 字节即会破坏任务栈。
  2. usmart_get_runtime 的返回值被丢弃:usmart.c:391 调用后未接收结果,注释写「获取函数执行时间」,但真正打印的是下一行读的 usmart_dev.runtime(由该函数副作用更新)。写法易误读。
  3. 计时窗口未关闭:usmart_reset_runtime() 把 ARR 改成 0xFFFF 后,在本文件内没有任何地方把 ARR 恢复成 999,只靠中断里的 timer3_set_period(100)(usmart.c:45)在 TIMER3 溢出时才恢复。若函数执行超过约 6.55s(ARR=0xFFFF / 10kHz)才会触发恢复,这段时间内 TIMER3 的 100ms 节拍实际被破坏。
  4. timer3_set_period(100) 与初始化值不一致:usmart_init 用的是 timer3_init(999, ...)(usmart.c:111),中断里却恢复为 100(usmart.c:45),注释写「恢复原来的设置」与实际值矛盾。
  5. 中断与任务重复扫描且无互斥:usmart_scan() 同时挂在 TIMER3 中断(usmart.c:43)和 task_usmart 任务(shell_usmart.c)上,二者都读写 usmart_dev、都调用 printf,无信号量或临界区保护,存在数据竞态与串口输出交叉。
  6. usmart_test 里被注释掉的延迟:usmart.c:97 的 // delay_ms(1000); 是调试残留,导致函数打印的 diff 永远接近 0,失去自测意义。
  7. 裸机向量名在 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)。
  8. 两个函数未加 static 也无头文件声明:usmart_sys_cmd_exe(usmart.c:137)与全局数组 sys_cmd_tab[](usmart.c:121)都没有对外契约,却导出为全局符号,存在命名冲突与误用风险。
  9. 强转函数指针调用依赖 ABI 巧合:usmart_exe 把所有函数指针统一转成 u32(*)()(usmart.c:353-389)。对返回 void、参数为 u8/u16/指针的已注册函数,只在 ARM AAPCS 下「参数放寄存器、返回值放 r0」的约定下才勉强工作,spnum > rpnum 只检查个数不检查类型(usmart.c:296),类型不匹配时会静默传错值。
  10. sizeof(sys_cmd_tab)/4 的硬编码元素大小(usmart.c:149):用 /4 而不是 /sizeof(u8*),在指针非 4 字节的平台(如 64 位主机侧编译)会算出错误项数。当前 GD32F407 为 4 字节指针,暂无问题。
  11. usmart_scan 丢弃长度 1 的报文(usmart.c:422-425):len < 2 直接 return,且没有把已到达的数据读走,下次调用会读到陈旧内容。
  12. usmart_dev.scan() 在中断中做了重活(usmart.c:43):中断服务程序里执行字符串解析、函数调用与 printf,严重违反 ISR 应尽量短的原则,会拉高系统中断延迟。
  13. 当前构建下整文件被裁掉:DEBUG_SUMART 为 0(midware/debug/shell_usmart.h:13),上述所有问题在发布固件中不生效,但一旦把该宏改为 1 调试,第 1 条越界会立刻暴露。