前言) Y$ A: f8 ]9 q$ X- j0 {
一般使用串口来打印调试信息之类的,正点原子的USMART也不错,这边引入了一个类似于Linux 命令行的调试功能,Letter Shell,功能很强大,github的链接在下面。
1 e7 U$ c$ J+ v我们要移植一下这个功能,方便后面的调试。这个功能使用到了串口,接收到串口数据后回传到上位机,直到接收到命令行回车触发,也就是0x0A(LF 换行)以及0x0D(CR 回车),不同的软件,结束符可能也有点区别。确认收到完整的命令,就可以解析命令,根据不同函数的地址就可以进行调用了,这样子调试程序就很方便了。
6 s: }8 n) s+ x8 y效果图如下:
% s2 S* z- G8 \* ~$ ?( ?1 B
0 U7 M2 I _3 L- r4 \8 @+ m4 m
* R/ P+ h2 Z& n' L- L
# U; I! k4 N) n5 O* M2 D, ]* t' Z
一、FreeRTOS配置* W& z4 B+ G3 S( h/ r5 @/ `5 n
1.FreeRTOS
6 \$ C/ U# v9 m1 S8 {) a5 GLetter Shell 仅需要用到串口,然后使用shell 的回调命令,就可以使用了,但是后续我们需要使用到FreeRTOS,这就涉及到中断优先级的问题了,STM32CubeMX有16个优先级,0为最高优先级,15为最低优先级,FreeRTOS可以管理管理包括优先级5-16的中断,我们这里先配置一下FreeRTOS,暂时不需要其他功能,默认配置有一个defaultTask的线程,我们用这个来控制LED以指示系统的正常运行,所以这个按照默认配置就可以了。
& H9 ~; X. Y/ t$ p; L9 G* _) Z: w6 }
% D0 F$ s3 b& n9 [! j
* E" I- D* K( w- f1 x! S0 `( ?FreeRTOS需要使用时钟基,默认是SysTick,滴答定时器,默认配置是1ms中断一次,然后计时器+1,但是SysTick并不是很准确,我们可以在SYS 选项卡中改一下Timebase Source,这里我们改为定时器3当时钟基。
{" U. ^. F7 W8 w) H
; E' c. K3 }$ U" w2 n$ T* m) Q' a7 n+ a
然后配置一下串口,因为Nucleo板载了ST-LINK V2.1,有一个虚拟串口连接到USART3,所以使能USART3,然后使能中断就可以了。点击GENERATE CODE生成代码。: N9 m. O! B3 d' y
2 d2 r4 C9 s6 G4 V3 R
0 B' Q& c _& U. D4 b
3 b% Q4 B+ y& }5 C3 L3 Y先打开 “freertos.c”,里面有一个默认的线程,StartDefaultTask,我们在这里加一个LED闪烁的功能,先测试一下生成的工程没问题。
# V% |) B& I. g/ b D9 t/ T2 N$ O$ k/ V
- /* USER CODE BEGIN Header_StartDefaultTask */ g$ `3 x8 x, _% c. P- b
- /**
8 N7 I0 ^5 z, v7 r6 B - * @brief Function implementing the defaultTask thread.
. [% U( R$ W5 _& M( J! W7 S7 ]5 @5 `. C - * @param argument: Not used% O" U. k, b" V0 u l
- * @retval None
) y% _& p( Y- { - */
) F0 N" B8 S+ Y* a. _; [) W - /* USER CODE END Header_StartDefaultTask */
7 q3 f. r+ D( D) N+ ? - void StartDefaultTask(void *argument)
7 w3 K$ h5 y2 M# G, E - {0 a2 Z0 v. c/ t1 Z W6 @3 r
- /* USER CODE BEGIN StartDefaultTask */
( T) J& r0 ]1 O* G - /* Infinite loop */% K% B2 @) n. Q" l+ i
- for(;;)3 D% w" e7 e: V7 }" t- ~
- {
+ W. d; P! P6 L6 z$ h6 Z4 _; }2 t - HAL_GPIO_TogglePin(LD3_GPIO_Port,LD3_Pin);& v6 l4 w( A) n" ~! r2 d( `6 H
- osDelay(100);4 ^) `1 v- R1 I$ G
- }
' S9 T: |8 b$ d! {$ j - /* USER CODE END StartDefaultTask */8 X* N/ e; L* ^/ M
- }
4 n; u' D* X+ d4 [0 G: M
复制代码 - p4 `) R/ K: z% m
注意,配置的Keil工程,默认是没有设置下载完自动复位,我们需要按一下Nucleo 板子上面黑色的复位按钮,另外默认的配置下,有一个以太网的初始化 “MX_ETH_Init()”,需要接入网线到路由器,不然初始化不通过,会跳转到Error_Handler();- if (HAL_ETH_Init(&heth) != HAL_OK)7 W4 b7 t- U7 |5 m
- {9 j( F# \# w9 J
- Error_Handler();0 y" h! q9 D' S, a3 }! j# K* ~! o
- }
复制代码 # V) |$ k, ~7 _) ?, {
二、移植Letter Shell
3 }1 i \& C5 }! W: y2 G下载github上面的源代码,解压出来。
; [. A& d* D7 h5 q# @6 U% [( d; N. z! {+ u$ \/ t6 F& Y
$ o2 N3 s: S5 u( b2 C2 t& M' f3 {& h/ v! [$ O9 `; W
demo 有一些平台上面的例程- g2 c/ s6 L D
doc 是功能演示
) H! v! O! q* f- f) k. mextensions 有一些扩展功能6 Q" B. F V. l! ]
src 是源代码
4 K! y% l* v. {tools 是一个用于遍历工程中命令导出的工具,位于tools/shellTools.py,需要python3环境运行,可以列出工程中,所有使用SHELL_EXPORT_XXX导出的命令名,以及位置,结合VS Code可以直接进行跳转
. U0 x# ^, M' U" ~
+ I% G; O: i+ V7 h我们在STM32Cube 的工程项目下新建一个文件夹 Shell,然后把 src文件夹下的源代码都放进去,遵守开源公约,LICENSE也要记得放进去。放进去之后,demo\stm32-freertos 目录下还有三个文件,我们也拷贝过去覆盖即可。
0 X" v: t6 a) N0 k
; R: \* k, s! _) D
* d9 t2 F6 E# ^! D
: a1 D2 |, T! x* Z" P" W然后在keil工程下添加这些文件。, z' k, ` f( f0 v1 t6 V7 B& L
4 z; N, T6 X# d/ M6 f7 B+ t- Z
6 i6 \) m: G: u5 i' _
6 S9 m+ Q9 g3 d' c编译一下,此时会报错* U. H( B2 i. |+ ~0 D, i
' F4 t+ `% _3 o1 ~) J5 n* S. c( T
- ..\Shell\shell_cfg.h(15): error: #5: cannot open source input file "stm32f4xx_hal.h": No such file or directory
复制代码 " V0 c: ?: ~8 F( y+ A
原因是letter shell的demo使用的是F4的板子, “shell_cfg.h”,里面包含了f4的头文件,#include “stm32f4xx_hal.h” 。我们这是 F7的板子,并且是STM32CubeMX生成的工程文件,所以我们只要替换成 #include “main.h”,这样改了一次以后,以后换芯片系列也能很方便的移植了。再次编译,仍然会报错。
+ q, h( s3 S) [# C# u. w; p2 d, E0 Z: W& r, z5 F
- ..\Shell\shell_port.c(13): error: #5: cannot open source input file "serial.h": No such file or directory
复制代码
( g8 n7 o' v6 h8 |$ J( B这是因为 “shell_port.c” 中有 serial.h,看名字,这是与串口相关的文件,我们把这种不需要的头文件删掉,只留下两个头文件就可以了。; F+ z6 `1 n2 F( `& V5 p
5 M& I- H" i; p4 }8 h. K2 M
- #include "shell.h"' Y2 L' a6 C. L3 L
- #include "usart.h"
复制代码
6 ], s2 R2 e6 k下面两个函数是shell读写的实现,里面使用的是固件库的函数。6 C% q; ~5 m6 d! t
e% m! M+ m( c r* U# a
- /**3 j$ j& h! w3 l) D- [
- * @brief 用户shell写
: T5 @# C& E3 ]6 _( M - * 2 E. y' X4 H9 k2 }- T6 ?' F+ _- @
- * @param data 数据
$ d5 q. l# v& W' ?' F. g - */) a( Z' Q# R. b* E, U0 Y
- void userShellWrite(char data)" n( |* y, o$ M4 Q2 G8 `+ |
- {% B# T" Z& \5 ` D P
- serialTransmit(&debugSerial, (uint8_t *)&data, 1, 0xFF);+ X4 q* c' Z2 O) F" ^$ ^
- }! j. m/ ?0 ~5 h7 B8 Q. n
- ; \5 H, o' v+ `; M0 }. Z1 q8 {: h3 X
- /**
1 v2 Z1 f" P! T+ z1 @ - * @brief 用户shell读
) [8 |. |2 y) k% C/ T - *
1 L2 N* [$ ?+ i% F9 o! A* a* b - * @param data 数据4 `" M! [9 V! p9 N
- * @return char 状态
& a8 ~3 ]( ]8 g5 E% n) m: k - */7 N% B5 |* `4 T/ S u* a) C
- signed char userShellRead(char *data): _# P" W e# A% N; N
- {
- M$ Y1 u2 b' O/ D0 ]0 ` - if (serialReceive(&debugSerial, (uint8_t *)data, 1, 0) == 1)
4 [5 E8 }! e; y6 o$ ^) C0 q - {0 _9 m" ]. g N2 E& }5 j5 F
- return 0;
( _3 {/ P0 S2 Z, D. s7 g$ B% s - }
( B: z. [, I" x8 W8 D' A5 ^ - else
! z; _+ q3 i+ r3 g - {2 b! Y* w5 p3 S6 e m
- return -1;
8 G# C5 B, V2 y& _! ~; F( B, k - }
. B( H% g% N$ ^ - / E c( f4 {/ j! A1 G+ |
- }
复制代码 2 j! k& j5 i( \' W$ Q- p, w7 f) `
既然使用了STM32CubeMX来生成工程,这里我们改成HAL库的形式。
: t! ?& @0 u( Z# A0 ~. ?/ L. h; v. z* H+ t9 `% `% ~
- /**& i" s/ }' L: [% _+ Q+ F
- * @brief 用户shell写
# w7 e4 i. r7 p' O9 D - *
/ v5 ]' s6 x2 P2 l) }, r2 N - * @param data 数据: `/ p5 n) z; V9 \
- */
9 v7 `) ~0 }" J) j9 y1 _" M, Y. z - void userShellWrite(char data)
% l; q* d& a: y( ~ - {
) I/ B( V. u1 _0 X5 b% J - HAL_UART_Transmit(&huart3,(uint8_t *)&data, 1,1000);. r8 B# p1 h- z. e' h$ {' _/ U
- }+ j0 O* N ?5 K- h" n- v, |$ c
- ' X+ @6 t6 o) X! p8 N. ^- _5 ~( U
- ; f7 A$ t( c8 H- A4 l; ^! g b; |
- /**/ K0 a, F- W5 ~) j! s* {
- * @brief 用户shell读
6 m b+ G! g! M9 ?/ s+ l - *
, U% ~( d) |( F. G& A1 L9 J - * @param data 数据
0 E8 x0 ~% B/ |( y - * @return char 状态
( s0 U* @- H& f0 J - */
% h/ _3 k& F; F. e- Y - signed char userShellRead(char *data)
) A+ w. i8 J/ W3 S8 a - {
1 g: p6 q: U; d - if(HAL_UART_Receive(&huart3,(uint8_t *)data, 1, 0) == HAL_OK)
' x% x% y# e9 F8 A" y- ?6 u - {& z7 }% Z9 H: \" K
- return 0;( ~- A1 C9 G: a- m
- }: P& B$ V9 {# w6 Z- W# O! b
- else
V! h# N3 w) j& }, P3 o - {
9 O# K3 H/ A4 a$ ` - return -1;
6 l; \+ z+ o# o9 Q, j - }
: R# K g6 I; E3 q - }4 O6 L q) s! Z3 [: j/ S% J
复制代码
7 L- d, e' i* {5 F再次编译就不会出错了。
- k" d$ R. k3 {0 b9 i4 \2 K+ a- B3 C1 T. N
三、Letter Shell的使用1 N2 ~! X( f; j/ a$ i0 ]" j" K
github 上有具体的使用说明。我这里是下载的最新版本,3.0.6,这里也是根据这个版本进行一些解释性的说明。0 ~: O; `5 x& f' R) ]$ ]
7 x* [3 X$ M3 w) d3 u# I9 V
3.1 定义shell对象- l" h f! {0 t* B! ]
在 “shell_port.c” 中定义了 shell 的对象。这个Shell结构体定义了历史的输入,读写对象的函数指针等等。
; i5 ^/ f; ^ o1 Y# H' y& z. V: N7 v# M7 y- s9 `* Q0 j
1 N3 s4 N# T* s8 L: m+ u6 g6 A) n7 r
3.2 定义shell读写函数
& a* k( b2 n$ r P4 d% ~定义shell读,写函数,函数原型如下:
$ ~. I$ B/ c# g/ P6 A
' Z' l/ @9 n7 j6 y- /**( B/ P# P; S# |; i- Z B
- * @brief shell读取数据函数原型& b; D9 H$ ^$ n% G, D
- *
+ }2 s, x, @/ P6 h, T - * @param char shell读取的字符6 o. z% l9 a, Y* @
- *
, `% s/ F9 u( C+ b7 A - * @return char 0 读取数据成功1 Q G& G1 J/ d0 |: Q6 ^: D
- * @return char -1 读取数据失败
5 J% u1 H# \4 { h# O - */
! i6 f6 I: w5 d @, k - typedef signed char (*shellRead)(char *);2 o7 f! X7 Q6 ?
3 h2 X! h8 v6 r) i' n$ K$ l+ s6 P+ w" d* p- /**# G7 A& f1 R- `, q" j, j
- * @brief shell写数据函数原型3 q. _4 i* C5 d, \7 j
- * t+ e, V; s5 h; R/ ~, d
- * @param const char 需写的字符" @2 T& o6 H \1 }% J. ~! A+ I
- */2 c) C2 K5 i T5 }& D: u* L& I* n
- typedef void (*shellWrite)(const char);/ `, M7 h! C; z4 ?1 h
复制代码
1 C% Y ?. g6 h0 h& c+ S8 a我们在上面已经进行了修改。- I6 `6 t+ s! A5 s( P$ A
Y9 o# m( x+ ~7 I$ C, G) L) Q
3.3 申请缓存区
2 e2 ~$ d! C' N6 o4 I) n1 l# x申请一片缓冲区1 Y. Z3 j2 V- ]) K$ q1 g
" t* |6 X# a5 _8 [
$ l6 l0 J( P& d6 \8 s5 T5 L% e3.4 调用shellInit进行初始化
* X: t: Z* L% L# A& x0 a& p }调用shellInit进行初始化,函数实现如下:
! i" b# p& p" S; |/ W1 g; D1 I6 a8 k+ p
- /**
# h8 l; z6 o6 F2 x1 S( r - * @brief 用户shell初始化
! ~2 I' }9 L2 P3 b9 R5 n - *
( U: R4 D7 _, U/ ]7 _ - */
4 x1 J, M8 { x8 T+ P - void userShellInit(void)
7 q) }# E& U: c. n. U - {
6 ?3 }0 n3 C! V) {. v) y1 w9 g - shell.write = userShellWrite;7 v! _0 v" s' k: Z' z2 f
- shell.read = userShellRead;9 H& X' V' L* t1 e u- ^
- shellInit(&shell, shellBuffer, 512);- p9 ]8 w% Y) r! [# W$ Q4 r4 p
- }
复制代码 ( X& @, [6 |0 o v
3.5 初步完成shell的初始化+ I6 g8 d& ]( ]4 M# Z
以上步骤,除了要修改shell的读写函数,其他的在shell_port.c中都已经实现了,所以我们在main.c 添加头文件
% v3 I' S* W! ^' M; C O
; V" E3 |. v) p* _0 G3 g. F- /* USER CODE BEGIN Includes */8 ^0 O8 k( ^! b, X6 {2 z
- #include "shell_port.h"+ P- n5 X9 F& ]$ o' @
- /* USER CODE END Includes */
复制代码 1 W+ s! c* h* |( F+ G
然后在 main 函数中初始化shell,基本上就移植完毕了。/ [# P: ]# P6 `, r: X% R3 ?/ L
) C8 h' i& P" j- N6 Z. i- /* USER CODE BEGIN 2 */- y/ z6 y- W$ H" T6 ?* |3 D
- userShellInit(); //letter shell 的初始化" T0 q( Y. f7 {: @
- /* USER CODE END 2 */
复制代码 5 q5 w$ ^, A1 O* `8 z' S
此时编译一下,还是报错,老样子,是因为没有 “serial.h” 文件,我们双击报错点,跳到异常点,把这个注释掉即可。然后再次编译,就没有报错了。
. @2 e, r. _! O8 P5 W
3 x' n! q. P* Z# k8 A: }" u4 C, L- ..\Shell\shell_port.h(15): error: #5: cannot open source input file "serial.h": No such file or directory
复制代码 " p' k! {* ]0 b% Q+ O( j
我们把程序下载到Nucleo。
) k; C/ b; b$ {! z. U% p# k然后打开SecureCRT 或者 MobaXterm 或者 “putty”,建议是使用MobaXterm,因为这个功能强大,且有免费试用版。% _ i8 ~4 g2 t! l$ T
9 K+ N0 t. Y# y. d/ X, j! @
我们需要建立一个串口连接, SecureCRT 点击快速连接, MobaXterm 点击 Session ,然后找到串口的选项,串口的端口就是 STLINK-V2.1的虚拟串口, 波特率为115200,其他的默认不变,点击 OK
/ L, \* O7 \$ J. ^) g' H9 b6 d& T* @. u# q. Y
: s* S% ?; O9 a' [9 [3 m& x- h$ _
由于我们还没编写串口接收方面的函数,按下字符是不会进行回传的,这里我们先按一下复位按钮,接上网线等到初始化完成后,就可以看到 letter shell 初始化完成后的串口信息。
' J. a5 c5 h% h" B& T. F/ ^2 O2 R7 }
' p8 M6 Z- ?1 p1 O" w7 E# z/ Z! D1 o* H' U; h; B; L: ~3 z: o
四、Letter Shell的进一步了解
+ B, q) }8 d, n" K( ?第三节已经完成了Letter Shell的初始化,在这一步,已经能够shell初始化已经打印的功能,但是要想使用好letter shell,我们还需要对letter shell有进一步的了解。* \2 C% @; z0 B9 C2 l
* v: d7 v8 u8 v( s S" ~
4.1 宏定义
! Z" N3 U+ F [- w# r# p在文件 “shell_cfg.h” 中定义了各种各样的宏,用户可以根据不同的需求来配置宏。' @# k, J. J. {/ {) z( }
下面的表格列出了各个宏的意义。1 [$ p; @+ c: v- P
9 ?! s8 ?6 C* G& |; z
, _8 b7 O+ U* R
4 D$ I8 o- E7 m% A, c- ?
在文件 “shell_cfg.h” 也有具体的说明。
9 s0 {2 M! N& D7 P+ D* t: w- m# w9 b3 f" s5 q; ^
- /**
) D- ?, [5 V1 `8 ~7 X# i8 \ - * @brief 是否使用默认shell任务while循环,使能宏`SHELL_USING_TASK`后此宏有意义
6 S- N% D+ R0 I" L" A - * 使能此宏,则`shellTask()`函数会一直循环读取输入,一般使用操作系统建立shell
2 K; D! ~; ~' n1 E, }2 L5 p - * 任务时使能此宏,关闭此宏的情况下,一般适用于无操作系统,在主循环中调用`shellTask()` n7 J3 C/ D% m5 q3 s5 w
- */
/ c9 [, ]3 Q. [0 I2 x# C, w$ c - #define SHELL_TASK_WHILE 1
( G0 K3 \- N5 W& T& E, V
# K- M0 z( r, B2 `4 z- /**7 \7 W* |8 N: }
- * @brief 是否使用命令导出方式2 I( p. D; h6 k" H3 U% y
- * 使能此宏后,可以使用`SHELL_EXPORT_CMD()`等导出命令# L- C% m% m* U5 o
- * 定义shell命令,关闭此宏的情况下,需要使用命令表的方式
H% e+ ]+ C9 m- f - */; e% c% z+ Y& r7 Z# P1 f8 U
- #define SHELL_USING_CMD_EXPORT 1
+ Y( k+ L: U+ Z* `7 D* w9 \ - 2 |8 p( o0 _" |' @* q2 L- P
- /**
" X9 l4 V" u9 d7 J, p: z - * @brief 是否使用shell伴生对象
; ^ h7 P; [5 ]1 L! z/ D - * 一些扩展的组件(文件系统支持,日志工具等)需要使用伴生对象. u. N7 p; D! @8 n. o; H( r) b
- */
- W6 Z/ D: o2 B5 G - #define SHELL_USING_COMPANION 09 B+ C: [1 R& L7 _ y3 ^. Z# T
# F7 p$ j8 K; J# v6 g$ k- /**
3 W5 _+ r0 J! K4 ]' u - * @brief 支持shell尾行模式
4 Y- g7 }0 w3 ?# \ - */- S w3 z* h& Z+ m$ b T
- #define SHELL_SUPPORT_END_LINE 0
4 O( Q0 _" o( }( m( a - 7 E8 ~7 j8 @' ?9 f2 B7 D7 V- n8 W5 I
- /**, ^# c2 d5 N( j
- * @brief 是否在输出命令列表中列出用户
" E, G1 ?9 G# V1 ? - *// H5 Z. x. T @% i" Z
- #define SHELL_HELP_LIST_USER 0: p& b* ^, L( I' l6 q9 Y
- : d. \, j1 _! t/ x; h A# S4 }
- /**/ T3 j$ r! w' d! m
- * @brief 是否在输出命令列表中列出变量
( v" F$ f$ l. z. i6 ?& ]/ T - */+ ^/ c$ O2 D6 I g2 L1 `* S
- #define SHELL_HELP_LIST_VAR 0
& X# _# e+ {/ `) i# H - 7 p4 i9 d/ W8 i
- /**
: F2 A/ t p8 b+ N) o - * @brief 是否在输出命令列表中列出按键( [4 T9 F" D+ \5 ]
- */* U, R( A$ L, e1 i3 s# X
- #define SHELL_HELP_LIST_KEY 0
A4 w+ P( P) l: E! D/ _. }* V
9 |/ {' ?4 A! A2 q! o( ?- /**2 u s$ E" r' V' A1 l! o
- * @brief 是否在输出命令列表中展示命令权限& H' u, A2 u( g7 T
- */4 M, T: U& |& `4 f) y2 O6 [
- #define SHELL_HELP_SHOW_PERMISSION 15 F1 U3 `6 I8 Z) `; e8 ^
7 \0 K1 ~# y1 R- /**
) o: `$ C" b+ ^( k) u! k - * @brief 使用LF作为命令行回车触发7 w T% }) G5 k+ I |7 [
- * 可以和SHELL_ENTER_CR同时开启" A0 I' V$ h% k9 e' z
- */& x0 S( J4 @1 m- g
- #define SHELL_ENTER_LF 0. ~1 k# ]$ j2 Q' q: ?; C
# G; S9 ^" a, x- /**
, f/ I/ k2 |7 O - * @brief 使用CR作为命令行回车触发1 @$ J8 v. @9 m& W: s3 u" }* `
- * 可以和SHELL_ENTER_LF同时开启
. g; I' N, w( u0 u$ j4 e7 l2 Q - */% F7 d+ ]* i/ e6 _1 b" b
- #define SHELL_ENTER_CR 0. ?' D' E/ B9 o6 f+ j, a( n
3 J! v1 o% S) C# o% h q- /**
( ?# D3 G' w! u- J - * @brief 使用CRLF作为命令行回车触发$ o$ l1 n. @$ j. i
- * 不可以和SHELL_ENTER_LF或SHELL_ENTER_CR同时开启
5 G, d W" m8 o' D( K - */
$ t" u5 u7 q* M4 E: d% S, C - #define SHELL_ENTER_CRLF 1; I3 k, o- J- p: Y
- 4 ?' I# _0 B" \; B0 ^
- /**
# Y: t/ \% B& d+ V- ~, P - * @brief 使用执行未导出函数的功能
, b) V2 b* ^* o" U0 i T# g - * 启用后,可以通过`exec [addr] [args]`直接执行对应地址的函数
4 r, S( M6 l( m) K- @$ f* B - * @attention 如果地址错误,可能会直接引起程序崩溃" G6 u3 a% l# x' j5 a
- */8 }$ A" R7 Z$ v* T0 U* I! S6 q
- #define SHELL_EXEC_UNDEF_FUNC 0
( Y) B( d3 O; m8 p1 q N- e& f - % Y; V7 H. e( a0 D
- /**9 U l/ U W# h1 o5 A
- * @brief shell命令参数最大数量, U- h2 b, r5 o6 h, n* b, o L
- * 包含命令名在内,超过8个参数并且使用了参数自动转换的情况下,需要修改源码
/ ]! H9 D& ~" M$ W - */
, V/ A4 y t. S( ]6 P3 L - #define SHELL_PARAMETER_MAX_NUMBER 8
; O* S1 L- k; d3 b: u( `
( I( Q3 c1 M; h5 {8 }" l) P- /**
) `2 Y- ^( k" v, C: { - * @brief 历史命令记录数量& C' x0 c* u. f5 R( b
- */
# c0 G1 {, _& R3 m: f - #define SHELL_HISTORY_MAX_NUMBER 5
1 j3 I2 U) F: h8 x7 c
4 X$ S3 m8 G8 I1 x- /**
1 M. l! u: W! P! s - * @brief 双击间隔(ms)' p, s0 \: o5 B! X, F
- * 使能宏`SHELL_LONG_HELP`后此宏生效,定义双击tab补全help的时间间隔7 i. M9 V0 w. K$ ~4 ~8 K
- */
2 L( C, o4 f; ^" J2 v - #define SHELL_DOUBLE_CLICK_TIME 200
, b( E, {2 j- d* {1 Y" n: ~ - + |) ]3 e* x1 e5 L0 W
- /**
9 W" q S# E2 V% P - * @brief 管理的最大shell数量3 e" z% I+ g/ j- g( u2 T
- *// \9 g0 w6 p/ O6 ]7 ^
- #define SHELL_MAX_NUMBER 5! X3 j4 g- S! o: H3 N! j' r4 q; O
) f, ^8 ~0 T5 Q( W0 R3 s% p- /**
4 [# f, K& J, l) V. j5 { - * @brief shell格式化输出的缓冲大小& f2 _ T k) k% q4 B" S
- * 为0时不使用shell格式化输出/ k8 C. Z% ?% f l1 r5 s' O3 }/ f
- */
2 f6 q4 _* J2 `! u" V5 U - #define SHELL_PRINT_BUFFER 128
+ G& l& j" S- ~+ u% h8 {2 D - ! a; d9 k, Q! ^$ _$ m" o5 H* @2 t2 `
- /**
& S2 f0 @3 { T; |* ]8 e - * @brief 获取系统时间(ms)- D6 c! y) N" z* y% U2 P, h% A
- * 定义此宏为获取系统Tick,如`HAL_GetTick()`) w& V( r! ?9 ]+ q; u8 J
- * @note 此宏不定义时无法使用双击tab补全命令help,无法使用shell超时锁定
$ J) L$ D& P% `: i+ ]. W - */
1 R( i9 p( J7 F4 a3 s - #define SHELL_GET_TICK() HAL_GetTick()
- N; d8 p: |; r& C9 H5 \ - - H& G4 R6 Z# J! t7 P& x) R4 L* l0 ~
- /**
3 ~6 r( c- x/ ^# ]4 @+ ^9 ]" x - * @brief shell内存分配
% Y6 L% T2 c" M) b) {" D - * shell本身不需要此接口,若使用shell伴生对象,需要进行定义
, o; T* y+ V& F6 S - */
! }2 h9 i5 M- R) W - #define SHELL_MALLOC(size) 0
9 a1 N$ T7 T# [+ O& F - 6 n/ Q& c0 [: c* _1 M2 M: o2 Z
- /**
2 }$ @/ V- Q+ z) [ - * @brief shell内存释放 _% b. M. v( j/ I8 c
- * shell本身不需要此接口,若使用shell伴生对象,需要进行定义
7 b2 E5 R0 R @8 | - */
% p) s2 l0 v9 x" [ - #define SHELL_FREE(obj) 0+ r( M1 a8 m: X# a# r9 G& w/ s# Z
8 M$ E/ U6 F* Z8 d- /**
, I+ i* w$ i) a! @7 W0 \* k! K+ ` - * @brief 是否显示shell信息
7 R2 |+ I' c1 U# w# j: o - */8 z: n! l: D V
- #define SHELL_SHOW_INFO 1
8 u% w& M' q m# a8 I% y6 C" I
! G) ?+ G; b" t, I0 D8 ^% t- /** Q0 L: Q. `2 l; Q& g/ j
- * @brief 是否在登录后清除命令行
. w, q" d0 S( r7 r/ `$ m& S - */
$ p: R& w9 n! \# f - #define SHELL_CLS_WHEN_LOGIN 1
7 @" ^# V" F# r* p - # F3 v) h# E0 j3 {- P
- /**$ c. S/ N& b! E' h- |$ i) i( s7 e
- * @brief shell默认用户
) V" Y- e/ X' J8 L( @: j$ W8 x - */
% x0 Q: u; i% g6 E+ f2 k% Z6 ] - #define SHELL_DEFAULT_USER "letter"5 u4 u! \' G1 d' }; z; h1 k8 G
4 Q4 g+ {5 m% ?- /**$ V% h" l u' X5 C$ u
- * @brief shell默认用户密码
& o2 q# a( [, {. @7 U - * 若默认用户不需要密码,设为""
m2 s4 c) ]4 b6 I9 | - */% h1 G1 f' P V8 J+ y
- #define SHELL_DEFAULT_USER_PASSWORD ""6 N- Q5 s( }1 w
- * R7 j% J; b% d! A- M$ u
- /**. p7 A6 N7 v2 a9 e U5 M3 X$ X
- * @brief shell自动锁定超时( P1 ]& B2 e; s( F
- * shell当前用户密码有效的时候生效,超时后会自动重新锁定shell6 {) w3 v% z* {( C% z; e D+ Y
- * 设置为0时关闭自动锁定功能,时间单位为`SHELL_GET_TICK()`单位
" O- y: w% S$ I0 B( w7 k, Z - * @note 使用超时锁定必须保证`SHELL_GET_TICK()`有效
* N9 Y& m8 C2 j/ N - */
3 g9 i" {- E$ a: _' ?4 B W - #define SHELL_LOCK_TIMEOUT 0 * 60 * 10009 W- ?( c& q& o- [* Z$ J* j
复制代码
: W$ `' S) \3 o1 M$ \; i2 L* @: T下面介绍一下比较重要的几个宏:
* r8 K+ i% Z$ k0 f' Y/ k3 g6 O
6 j3 ]' `0 ^" H. v& B# H3 e# J4.2 SHELL_TASK_WHILE
1 [: N9 p- X, E2 L2 g; h/ G这个宏仅在这里面生效,这是在使用操作系统的时候使用到的,它会一直轮询地读取串口数据,然后调用 shellHandler 解析串口数据。这仅仅是个范例,因为这里没有进行任务切换,所以会一直堵塞在这里,而我们如果要使用FreeRTOS创建一个线程来处理这个任务,需要使用osDelay来进行切换。因为这里一次只读取一个字节,所以如果任务切换间隔过大,可能会丢失某些数据,另外,因为接收串口的时候要进行回显,所以任务间隔过大,也会影响操作体验,所以我们在串口接收中断调用 shellHandler 进行处理。我们不需要在线程中一直读取,所以这个宏 SHELL_TASK_WHILE 设为0,但是设为1也不影响就是了。
1 d6 v* l1 p" W9 Q9 M' n& S7 K. m6 c8 R. P6 \: K' `4 q( `
- /**3 J# W. v; A$ k* k- Q: a
- * @brief shell 任务
7 v9 D( E$ h4 I' ]2 y, d3 a: Y% K! f - * 0 ?; o8 d ^ N
- * @param param 参数(shell对象)
2 p/ q. T2 s4 o4 L - *
* j+ u/ K" ~3 N - */
r) O; T* | A! @; M - void shellTask(void *param)
1 e: f+ {0 O1 D w6 f C - {1 A2 T6 R& }' t0 \+ r9 U1 g
- Shell *shell = (Shell *)param;
* k2 u9 S1 c% _7 b1 K: Z - char data;/ W: c% ]0 V( V- H4 r. o
- #if SHELL_TASK_WHILE == 1
0 c v4 w! e! g' i - while(1)( P# P) e# o. l* {
- {
0 R% O8 c) p& J, s7 ~ - #endif" N, ^" f) |4 k4 O, c' Y3 q2 a! ?
- if (shell->read && shell->read(&data) == 0)
$ u$ K" e3 y; n$ t - {
5 P r7 Y2 \1 _4 w - shellHandler(shell, data);
3 @+ l( W. e! n. q6 ? - }
; ^* N& A; ]7 k% g! ^ - #if SHELL_TASK_WHILE == 1
6 y& u% m: ~3 [: Q! u7 E - }* ?7 Z' |9 E3 `: r# s; {1 v
- #endif
" y) H2 n4 {- P, o t - }
6 p2 d: ^4 v/ ]5 h: T9 w: K! g
复制代码
: o" Z/ N( ]7 z- e( f4.3 SHELL_USING_CMD_EXPORT
4 ^9 M$ `* s5 `& P5 G命令导出功能,通过这个宏,我们就可以在命令行中输入指定函数的名称以及传递函数,就可以直接调用该函数,用于调试,十分方便,所以这个宏不变,默认为1.
, a3 G/ T. s* q9 l+ c
# y- t% ~% c/ X+ F+ [# P2 O$ A& B+ D4.4 SHELL_USING_COMPANION
0 d n. P: i S. J作者定义的伴生对象,用于一些扩展的组件(文件系统支持,日志工具等),但是目前这个功能还不是很完善,所以这个宏默认设为0,不启用。6 |% G2 w) l Y2 X8 f, V8 ?0 L
1 V% @: E$ Q2 f8 N0 l' L4.5 SHELL_ENTER_CRLF
' r* ~2 R/ U" h9 k, S
6 J8 {/ ?1 O. K: k. H B+ ~+ X这个与 SHELL_ENTER_LF 以及 SHELL_ENTER_CR 互斥,作为命令行回车触发,使用的SecureCRT 以及 MobaXterm 是使用 CR 作为命令行回车触犯,所以这三个的配置为6 c0 A& {( ~2 k% w# W! q) _
6 C- a$ |: o2 Y- w/ a& A0 ]$ Q
- #define SHELL_ENTER_LF 0, G- m) u$ I- f
- #define SHELL_ENTER_CR 1
" e/ ~, [1 P4 j# d) V) e- l4 } - #define SHELL_ENTER_CRLF 0
复制代码
+ {1 p" c2 n( w5 F" O4.6 SHELL_PRINT_BUFFER
1 X% Y1 u1 x/ l" ]$ G; z" s$ f0 N这个宏定义了格式化输出的大小,在 shellPrint 中使用到,使用方法和 printf 类似,只是注意不要超过缓存大小。; K p0 t9 Y" v# I* \- v
这个宏也是按照默认值就可以了,也可以根据自己的需求进行修改。/ i' P- F. y% U# M3 A6 [9 g% X- S; j
5 E9 B; D0 r! d5 Z! a( |* B
4.7 其他的宏" E7 X1 k7 ?- i- C0 N
其他的宏理解起来很容易,这里也就不进行细说了。
; u7 c& g. ~4 b# f$ _" Z最终宏的定义如下:
& R- D+ Y' ]- b+ }9 b3 T5 g8 G# Z# ~3 |' z9 {& g
- #define SHELL_TASK_WHILE 01 h* o( e- z: t! N, D; s- O. \
- #define SHELL_USING_CMD_EXPORT 1
9 J8 \6 O2 R$ v# z6 N( a - #define SHELL_USING_COMPANION 0+ I$ F9 ~) X+ D8 Z4 b& i
- #define SHELL_USING_COMPANION 0
3 W! v y5 [( N - #define SHELL_SUPPORT_END_LINE 0; y# b' ?6 M' n7 _7 N4 I% c
- #define SHELL_HELP_LIST_USER 07 {: L. i, [& q
- #define SHELL_HELP_LIST_VAR 0
9 ]0 Y8 i3 `/ H - #define SHELL_HELP_LIST_KEY 0
# |5 k9 z$ y+ G3 j! H. l - #define SHELL_HELP_SHOW_PERMISSION 1$ o' e) d8 v( I B
- #define SHELL_ENTER_LF 0
$ K1 N. x" y( j, B - #define SHELL_ENTER_CR 1* y* F: j- L$ ]6 g0 H* ]) L$ @
- #define SHELL_ENTER_CRLF 0
( k( x* p3 C- ]$ \7 T" }, T - #define SHELL_EXEC_UNDEF_FUNC 01 M& p4 [2 p# G3 o6 K; S" u
- #define SHELL_PARAMETER_MAX_NUMBER 8# @6 Q/ s `0 ^: H( i
- #define SHELL_HISTORY_MAX_NUMBER 5
- O5 n+ I5 O2 c$ u' f5 H1 P - #define SHELL_DOUBLE_CLICK_TIME 200
5 [ W3 V1 J+ n: p# W+ [3 D - #define SHELL_MAX_NUMBER 5
" m' l, b7 z4 A p - #define SHELL_PRINT_BUFFER 128
) E$ e ?4 ^+ y$ `' _- Y - #define SHELL_GET_TICK() HAL_GetTick()( b0 l5 \, U; i! X* S j- `# A1 Y
- #define SHELL_MALLOC(size) 01 B M. w9 I% S+ P+ V
- #define SHELL_FREE(obj) 0% E8 j! p4 t0 ?" i& Z
- #define SHELL_SHOW_INFO 1
9 V* O( _- G- A - #define SHELL_CLS_WHEN_LOGIN 1
9 g3 n0 n* {) j" o. G/ g - #define SHELL_DEFAULT_USER "letter"
2 F$ y; {2 R2 c- b8 V8 Q - #define SHELL_DEFAULT_USER_PASSWORD ""
3 t" O: t; S0 z% Z1 p* ] - #define SHELL_LOCK_TIMEOUT 0 * 60 * 1000
复制代码 1 _0 f$ t; `; G( \: f6 }9 E5 B
4.8 shell的接收处理
0 ?7 m8 f9 v0 z/ u/ }- \! d要正确的将串口接收到的数据当参数传递给 shellHandler,先要确保打开串口的中断,HAL_UART_Receive_IT ,使用这个串口中断,需要定义接收到的buffer数组,以及接收buffer的大小。& c: a( g3 m j
$ `1 V3 M c! `+ E- @- #define HAL_USART3_RXBUFFERSIZE 1) d5 s- V) C1 m6 a; @% \$ c) J
- uint8_t HAL_USART3_RxBuffer[HAL_USART3_RXBUFFERSIZE]; //HAL库使用的串口接收缓冲
复制代码 ; X. _; ~$ r' }
在main函数中打开串口中断
% ^8 Z& ~4 j- Z% ?0 s
3 U$ j$ o+ z+ B i7 Q- HAL_UART_Receive_IT(&huart3, (uint8_t *)HAL_USART3_RxBuffer, HAL_USART3_RXBUFFERSIZE);//使能串口中断:标志位UART_IT_RXNE,并且设置接收缓冲以及接收缓冲接收最大数据量
复制代码 4 q7 _/ w& f' j' H
打开串口中断后,在串口中断中就能获取到串口数据了,在HAL库中,串口中断最终会回调 HAL_UART_RxCpltCallback,所以我们就在这个接收串口数据,然后调用 shellHandler 进行解析。
$ Q" C6 E4 Y! i; J1 R: f
) L% X4 ^: g9 u/ u: A9 j& v; @- void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)( n+ J) n0 l7 w7 b1 t& n9 c. ?6 S
- {) M6 E3 {1 X# n
- uint8_t udata; p7 h; ?: l5 {1 @; y. }9 ?
- if(huart->Instance == USART3) //串口3的接收部分
: W0 ~8 O2 A2 @; g& J( _' r" N - {
' t5 l( ~1 N$ T- u - HAL_UART_Receive_IT(&huart3,(uint8_t *)HAL_USART3_RxBuffer, HAL_USART3_RXBUFFERSIZE); //接收串口的缓存
- T' ]" \. q; R3 f7 u - udata = HAL_USART3_RxBuffer[0];
2 ^+ f1 m1 {3 @& t+ }9 v0 G - shellHandler(&shell, udata);
0 O3 s, K; n* f( A0 g9 l1 N- f! ? - }
9 b3 \+ x# N* D* W6 ~' H - }
复制代码
: Q% S" s0 b% |5 w" }编译后下载程序,复位,这时候输入cmd 命令,按下回车,就会列出可以支持的命令了。后续如果自定义了导出函数,也会在这里显示。到这里,就说明了串口的收发是正常的,shell 也移植成功了。
Y7 g0 u- [+ D- x: A4 Z% c/ L O6 V& [* H
: S( h2 O$ s+ X. p% K6 s
Z/ x8 [$ f ?4 T$ M五、使用Letter Shell 进行调试程序
+ _0 G3 @3 v# D' h$ I如同上面的 cmds 命令,我们用户也可以自定义命令,在编写完一个函数之后,我们可以通过SHELL_EXPORT_CMD 导出该函数,这样我们就能通过命令行来调用这个命令执行,以达到调试的目的。
7 |0 `, V. ^( v5 ?letter shell 3.0同时支持两种形式的函数定义方式,形如main函数定义的func(int argc, char *agrv[])以及形如普通C函数的定义func(int i, char *str, …),两种函数定义方式适用于不同的场景。
) s4 S7 S0 X8 ?4 w6 N2 T. v
4 ~+ ], B6 k7 J5 B( z3 O! O; P下面使用LED来举例。
, G u s4 I, A% ~+ S' K
p% N+ i1 u$ c( B2 r G4 c5.1 普通c函数的命令导出* ~- k) M( I4 d, l% W
- void LD1_ON(void)
$ P& N0 v1 T+ y) B% t - {
8 Z$ S* S; v6 ~ - HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_SET);: k E8 D% c6 e, `% G3 z) p) V
- }% u J- X- D' F9 `
% F) S3 G. ?- f* r- SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC)|SHELL_CMD_DISABLE_RETURN, led1_on, LD1_ON, LD1 ON);0 F) p0 T0 j. L, Q. c, {
6 ^& ^& b' z" N: x. h6 b- void LD1_OFF(void)3 D) k- N3 m4 [* F" E
- {
2 J' }8 V! A7 q- Z - HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_RESET);& Y, n# x# } k7 L
- }
2 ?; z! y0 Q* N - @; J' @1 `+ D' j1 Y4 y
- SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC)|SHELL_CMD_DISABLE_RETURN, led1_off, LD1_OFF, LD1 OFF);9 O2 o% l2 g8 t7 |* a5 z2 O. a
复制代码 & l4 \3 G9 c6 s x( C5 U/ k
上面写了两个函数,顾名思义,LD1_ON是点亮LD1,LD1_OFF是熄灭LD1,SHELL_EXPORT_CMD 就是导出命令,各个参数分别是 命令属性,命令名,命令函数,命令描述。
2 C" v `+ R* n( s: z& |/ b& P" ^2 S( I. T' O
- /**
8 g2 |5 p4 _4 A' l$ J# T - * @brief shell 命令定义
8 @& R; R: Q' N; ?& m: y - * H7 K: _% K% |( ]
- * @param _attr 命令属性2 m f F& @- H7 f$ C/ }# U! K
- * @param _name 命令名% N6 I( {. m# \; k* ~- }9 o$ |
- * @param _func 命令函数# F3 z5 u4 H: k0 @5 p8 U
- * @param _desc 命令描述
( s1 g7 Y% Z# T6 {% r* {6 b - */
' ]* M7 @ N' K' s$ |6 _ - #define SHELL_EXPORT_CMD(_attr, _name, _func, _desc) \6 V1 M* z$ ~( ^8 W
- const char shellCmd##_name[] = #_name; \
. }$ m/ F r$ |4 P7 z6 R - const char shellDesc##_name[] = #_desc; \
k- Z E# g: |. G/ ^ - const ShellCommand \
3 ]' g0 g; q, H9 p7 H" q0 I - shellCommand##_name SECTION("shellCommand") = \( Y5 K% \/ N5 s7 E0 {
- { \5 P6 \$ S$ B! n1 |. ^3 O; G
- .attr.value = _attr, \: O% y8 J, x2 y" [% H
- .data.cmd.name = shellCmd##_name, \5 J$ z3 U* S" u6 d, l$ y. R* Y
- .data.cmd.function = (int (*)())_func, \
% {& Z0 ~! ?6 I8 k8 g5 d; Q1 e1 \1 J - .data.cmd.desc = shellDesc##_name \+ x2 }6 k9 S. I$ z- c: b
- }
复制代码
/ u% D2 w: W! Y/ H5.1.1 命令属性, m9 m# ~( l W
命令属性 这部分在 shell.h 中进行定义,几个属性可以通过 " | " 来进行连接。属性有以下几种:
7 m2 J! A L' j' I
# u/ i* R: N: G+ t- /**4 t9 G2 I T' {# ]$ ]
- * @brief shell 命令权限; l" ]5 I% B6 |) b8 x# w: Z0 C8 ^
- *
) H! S' |) r2 c/ K" n9 ^" R - * @param permission 权限级别! v/ j; I A, @7 {5 q
- */
) o/ ~) S, n; Z8 N - #define SHELL_CMD_PERMISSION(permission) \
% j$ C# H# a V- u - (permission & 0x000000FF)' y( k I: D/ M g; P
- 0 a3 e. @. v7 T3 ? U4 r
- /**2 K7 e8 C5 P- N; O3 k8 [
- * @brief shell 命令类型
8 a5 ]; A8 o7 p1 ~ - * ) S5 q1 i" f5 H
- * @param type 类型9 G# L$ V4 @" H: a2 S
- */
9 X# [- t+ e1 q, |- p {1 O - #define SHELL_CMD_TYPE(type) \
2 E3 K- y- X6 i$ ~! j6 E - ((type & 0x0000000F) << 8)
0 H% f- F u8 a2 w - ( e; x3 ?& U/ ~0 Y k. e: C$ `" l
- /**
0 G1 p" n7 X) P - * @brief 使能命令在未校验密码的情况下使用
7 n5 J, b0 j& h8 H - */
& V) R6 b7 B. z, F7 Q7 Z - #define SHELL_CMD_ENABLE_UNCHECKED \
3 h' K8 s' @3 `1 r- G - (1 << 12)
6 k5 I: {. B! Z
- h5 z7 M1 W6 ?+ w K7 H* R- /**
2 i: `' j+ H! v6 V4 x! F( v! L - * @brief 禁用返回值打印
' P2 C" U" I; M5 C6 _$ ` - */
/ D. K& X( q& `; A7 c* H - #define SHELL_CMD_DISABLE_RETURN \# K/ _& @) j1 O* N
- (1 << 13)
) _; S( V* l8 e9 t1 T - : l" k, a/ L3 o! m7 w O; F* N
- /**5 |& n' W8 r5 E1 ? d% ]* [
- * @brief 只读属性(仅对变量生效)
0 Y3 \9 I! s* n9 |) X - */
% Z1 c( e% c. x- \5 Z0 R6 U - #define SHELL_CMD_READ_ONLY \! l9 M) r% S$ u1 T7 K
- (1 << 14)- w- z: l6 i: a- S3 P6 s# a
复制代码
; ]1 @9 X: ?8 g; t在上面的例子中,使用到的命令属性是" w L. l$ e5 m6 z
; g4 E: X1 ]" S9 w! y$ E6 j
- SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC)|SHELL_CMD_DISABLE_RETURN
复制代码 8 u; u% B/ R- H6 H7 V) _( z
这个意思是这个命令任何用户均可使用、命令的类型是普通的C函数、执行完函数后,不在命令行上显示返回的结果% H/ x; @* X( d! |8 T
. R! V+ N- P3 A+ j" {5.1.2 命令名
; ?6 o5 [$ y3 Q命令名 在命令行中,输入命令名就能执行对应的C函数,可以通过命令行的 cmds 命令进行查看
- r& {/ n% a! ?+ U在上面的例子中,实现了两个命令名,分别是 W$ g# L) X7 C+ b; p1 w
2 ?# i4 D4 r) t- t4 f4 x- led1_on
* H& k( t2 D/ u; {; P1 d$ r - led1_off
复制代码 ' J0 J# ? I4 m" ]9 p- b# m: C9 q! e
5.1.3 命令函数. j+ [- u' ?- b
命令函数 也就是我们在keil工程中,所编写的函数的函数名。0 w# w6 q& r e& H y7 M/ b$ `$ b# l
比如在命令行中,我们输入 led1_on ,那么实际调用的函数是 LD1_ON,输入 led1_off,那么实际调用的函数是 LD1_OFF! }; Y, o+ p% ^0 E% E# _
; ~0 b+ i$ r, F0 X7 q, }7 d
5.1.4 命令描述4 |5 g7 P: P" z8 q
命令描述 命令描述出现在 cmds 命令中,它主要是简明扼要的说明这个函数的功能。3 }" _, G- _, P9 J
3 q7 P0 L3 q' ?5 i( o
5.1.5 实际效果' z, l$ F) ]& B6 h9 q, V
如图所示,在输入cmds命令之后,会打印出我们可执行的命令,这里是多出了两个命令,led1_on 以及 led1_off,我们输入对应的命令,就可以控制LED1的亮灭了。' ] c2 m8 H* A
3 q2 r5 Q: [6 P9 n* S
4 @9 C, Y: ^8 _) i
) ^) M. y2 i( g2 ?+ A# ]2 u
5.2 main函数的命令导出
# k8 _4 V5 }+ c; E4 k3 C3 U( K一般我们写的程序,用普通的c函数的命令导出即可,main函数也可以,使用上也比较类似。使用此方式,一个函数定义的例子如下:, s0 {1 ^) N" b: _
% n1 f- J5 U6 u( z, t- int func(int argc, char *agrv[])& M- T p8 d8 v( V- J: L
- {" K ]) G. s, j$ c/ _/ ]( T7 p
- printf("%dparameter(s)\r\n", argc);' w/ o! V9 V) ^% |8 Y2 o
- for (char i = 1; i < argc; i++)
T i6 @+ j% V8 m/ ^2 V8 {2 p - {
& L0 ^+ A7 D% A% X; k' [# m/ L3 u - printf("%s\r\n", argv);
2 E0 G5 A! p! {% A - }
6 C- A9 u2 J% C, P; ~6 V; G - }
7 m2 u& c3 s; o3 i - SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_MAIN), func, func, test);
复制代码
, o( O2 |9 C" k终端调用
# Q5 N2 c" m. d) ?$ Y. g
# u3 S& B. _. H$ `; z$ y- letter:/$ func "hello world"2 m% v" W! X& b0 V' J4 A& l
- 2 parameter(s)
4 v+ t5 b# X7 e' ` - hello world
复制代码
7 C4 R5 |$ v! N/ u2 f7 I5.3 带参的命令导出' s8 a z' S7 ^/ [: a, s5 W
我们编写一个带参的函数,命令行是以空格划分传递的参数。" Z. C/ ^1 j; E7 @& K
; }- Y C6 [+ k" n0 X' G- void LD1_ON_OFF_ms(uint16_t on_ms,uint16_t off_ms)' L$ b! E- l% d. Y4 F" a
- {7 T8 @$ Q& N/ k( F' {% L% V% A
- HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_SET); //点亮LD15 o# j4 v) T* y/ B1 @" @
- HAL_Delay(on_ms); //延时
7 K8 K9 Y( Q; {6 e - HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_RESET); //熄灭LD1
! h e( Y! p4 T. x1 k - HAL_Delay(off_ms); //延时8 v" c! S1 Z' l5 n
- HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_SET); //点亮LD1& r, g5 J6 d3 O7 ]) W
- }7 d5 I( \- n1 r& U9 c5 Z
- SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC)|SHELL_CMD_DISABLE_RETURN, led1_on_off_ms, LD1_ON_OFF_ms, LD1 on ms);1 @8 D7 P$ J- E
复制代码
7 X/ ?0 u* L: V1 e这个例子是控制LD1亮灭的时间,LD1首先点亮 on_ms 个ms,再熄灭 "off_ms"个ms,再重新点亮。
: [# I B+ O }* Y' i输入以下命令,就能看到 LD1 先亮 1000ms,然后熄灭 100ms,最后又点亮的状态。+ B4 E8 z+ Y! s1 G) @, c
! m3 `2 y/ O. ^ a- letter:/$ led1_on_off_ms 1000 100
复制代码 这是输入一个数字的例子,也可以传递字符串,比如说我们要使用文件管理系统 FatFs 来管理文件,很多地方就要使用到字符串了,letter shell 支持伴生对象,里面可以扩展文件管理系统的功能。
) P; e- l# q% T7 w) [$ w. P0 w1 u8 E6 a
fs_support作为letter shell的插件,用于实现letter shell对常见文件系统操作的支持,比如说cd,ls等命令,fs_support依赖于letter shell的伴生对象功能,并且使用到了内存分配和内存释放,所以请确认已经配置好了letter shell' A: l$ u/ h6 P
: h4 r* W( E1 a
fs_support并非一个完全实现的letter shell插件,由于文件系统的接口和操作系统以及具体使用的文件系统相关,所以fs_support仅仅通过接入几个基本的接口以实现cd,ls命令,具体使用时,可能需要根据使用的文件系统接口修改fs_support,letter shell的demo/x86-gcc下有针对linux平台的移植,可以及进行参考. p8 K; g0 n2 k+ ]
8 n* m( q# Q* x$ u- A3 s
因为这个功能作者还未完善,所以这部分的命令需要我们自己来实现,下面是我在 FatFs中对 mv 命令的实现,功能是在将 old_path 的文件移动到 new_path,或者是将 old_path的文件移动到 new_path,并且重命名该文件。
/ B3 M; s$ W5 u8 o( W5 k
3 B! X# d; \/ C% ~ D! M$ d- /**! ~4 T: i& M5 t; f8 A* v( m% o
- * @brief 重命名/移动文件或子目录! q- F, w+ x; F" ]- M
- */& L7 F: T+ |) Q! P) o" h
- void shell_mv(char *old_path,char *new_path); n: [" W# l* D& ^0 p' G
- {; p8 }$ M! h2 u( o$ _/ q/ |; M
- FRESULT res;$ T! w" i$ i( n8 N: S- M
- , ?3 m6 R& ~# u* J E6 ^' J2 m
- shell_active = shellGetCurrent(); //Get the currently active shelll9 H: V. P' ?2 N
- res = f_rename(old_path, new_path);
3 u& B* H; r/ ~ - if(res != FR_OK)( K4 Q, L- O2 l) A3 r
- {
- D% w/ s' o! W" h! L- S s7 G - shellWriteString(shell_active,"Operation failed! Error Code:");+ I! M9 m! [/ _7 S& |! S
- shellPrint(shell_active,"%d\r\n",res);" P5 I# k" I) W6 a6 E3 E; x/ ]
- }
; p! k& M1 Q8 y3 k' ~; Y8 ^# N; k; f - }' \4 H! W0 K5 b/ g2 K% x! G! t
- SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC)|SHELL_CMD_DISABLE_RETURN, mv, shell_mv, rename or moves a file or directory);
复制代码
% ~) `. u. ], q5 x5 q; k输入以下命令,就能将 dir1目录下的 file1.txt 移动到 dir2 目录下,并且这个文件名将修改为 file2.txt。* }* j# E7 _/ C- P! h
1 I- h/ _. N5 B# e
- mv dir1/file1.txt dir2/file2.txt
复制代码 6 w" t: O7 G9 ?# W
不过要注意一点,FatFs 的磁盘是以数字打头的,不像window,各个硬盘是以字符打头的,比如 "C:\file.txt"表示 C盘目录下有个 file.txt,在FatFs中,对应的目录是 “0:\file.txt” ,假如我们以 “0:\file.txt” 当成参数传递进去,那么接收是会存在问题的,这是因为第一个输入的是 0,在解析参数的时候,它会把这一整串字符串当成数字来解析。
: C1 k5 ?3 ]& ^& }3 n, U/ @ x我们先找到解析参数的函数,可以看到 if (*string == ‘-’ || (*string >= ‘0’ && *string <= ‘9’)) 那么就是要解析数字,我们在这个判断中,再次判断,从字符开始到后面,如果出现了任何一个不属于数字的字符,我们就判定这个是字符串。而如果在空格或者回车命令前,全数字的字符,那么我们就判断这个是数字,通过这个方式,就能规避 FatFs 的硬盘号会导致参数解析异常的问题了。
$ ~* u1 P0 N0 p% A5 q
' q) m* s. k W4 Y- /**
8 i q. l. N. g" q - * @brief 解析参数( o+ x4 G' M: \
- * ) a7 W% X& f! N" ?7 `& u0 u
- * @param shell shell对象
( ]$ s& H- m" w3 Z7 B1 n - * @param string 参数
- w, N2 T2 A& S3 O) D7 C* [* H; Q8 x - * @return unsigned int 解析结果% l- v$ V: O( p/ A' T8 [
- */+ D4 R0 [; G9 M; n% a7 `) t
- unsigned int shellExtParsePara(Shell *shell, char *string)
0 e/ `" E! o% p7 l! S+ U0 r, D, e) v - {
; p# q0 E; y8 x; w- P - uint16_t i = 1;! Z; x/ \& V8 v+ G
- if (*string == '\'' && *(string + 1)); c+ D- @- ]5 V6 c& [
- {
7 u7 F% t, b# q; I - return (unsigned int)shellExtParseChar(string);# W8 H9 [2 J! \8 n+ ?
- }- K& S# Z0 X1 t, R0 l& _
- else if (*string == '-' || (*string >= '0' && *string <= '9'))4 _- M2 u' i/ ?1 r
- {
$ @ S6 g" w7 R& I2 r - while(1)
6 u1 J. F# d0 A - {
9 f, Q/ e* d9 S& K$ h - //寻找下一个表示该参数已经结束的字符
/ g# ]: I0 k4 H' H: f6 T' {& S) [ - if((*(string + i) == ' ') //下一个为空格表示该参数已经结束
7 _3 P! @- z, ~ - #if SHELL_ENTER_LF == 1 // LF 表示命令结束$ O8 N. N# K& X1 V
- || (*(string + i) == 0x0A))
/ e3 i+ G4 K! q. A. Z' c. X - #endif
_- [+ {- J& _" z - #if SHELL_ENTER_CR == 1 // CR 表示命令结束% \- Z2 s# j6 c9 @ ~
- || (*(string + i) == 0x0D))/ A0 x) S9 L( { r8 r) \/ \
- #endif6 W4 P2 [3 ]1 C
- #if SHELL_ENTER_CRLF == 1 // CR LF 表示命令结束1 h& A, n# z4 L
- || ((*(string + i) == 0x0A) && (*(string + i + 1) == 0x0D))+ m0 V' [# l* N5 u4 `2 |
- #endif
4 Y& a4 S& ^2 w1 ^: |: E# N7 ~) l$ \ - break;
/ e# i' N8 k! r; d - 4 l4 \: F/ t" f7 u5 U
- //出现不为小数点或者非数字的字符,表明该参数为字符串4 ~1 \, q. j. }2 ]
- if((*(string + i) != '.') || (*(string + i) < '0') || (*(string + i) > '9'))
/ _4 w5 D0 l3 @" Z' ~ - return (unsigned int)shellExtParseString(string);
8 C: L7 \6 q* B. B5 t* M( U* r+ e
! }. T- \5 C& c) d* F- i++;
$ s4 Q/ E% _6 l3 E$ _% ? - }
* @, P5 ?0 x7 U* F - return (unsigned int)shellExtParseNumber(string);! w1 d% G5 D3 g' K0 y2 Y2 ]
- }
$ h& t6 ^$ k' z& L0 q; s) }+ N4 y/ [ - else if (*string == '- O4 m8 _; \8 u* a& J6 S0 G
- [b][size=3]六、其他常用的 Letter Shell 函数[/size][/b]
% u; o! X) X& ?2 c4 W - [b]6.1 shellWriteString[/b]
* K6 e& @( B6 f/ z8 t: d& T ^1 J - 函数原型如下,这个功能就是 printf 。这个应该很好理解,就不细说了。% ]$ W6 ?) f5 w
( S7 W, s4 Y! D9 j$ w$ `- [code]/**, A# y k+ l% l
- * @brief shell 写字符串* q1 M7 {/ u5 |9 P
- * - |$ p. O# r: o. o+ X. Z3 s
- * @param shell shell对象; ?* P+ c" e; _ v
- * @param string 字符串数据
: \$ J# f& h/ u; q - * & y$ R+ g% b3 R8 J7 U' h9 m7 U+ a
- * @return unsigned short 写入字符的数量* R/ L* z; K) n5 {' t9 c
- */% T8 Y7 }4 G; I8 c4 @, x+ u
- unsigned short shellWriteString(Shell *shell, const char *string)
复制代码
& o+ [3 [ d7 n7 W' t. Z8 T. P6.2 shellGetCurrent X' K9 y5 c% q( K+ E9 [, z1 K+ S% m
letter shell采取一个静态数组对定义的多个shell进行管理,shell数量可以修改宏SHELL_MAX_NUMBER定义(为了不使用动态内存分配,此处通过数据进行管理),从而,在shell执行的函数中,可以调用shellGetCurrent()获得当前活动的shell对象,从而可以实现某一个函数在不同的shell对象中发生不同的行为,也可以通过这种方式获得shell对象后,调用shellWriteString(shell, string)进行shell的输出。
- |4 h2 t: W' D( L5 j0 S! k- K8 K& w. K* [- @2 z
- /**4 k4 I% {% k& s5 f) h# }% t
- * @brief 获取当前活动shell4 O& K$ b" T. W) ?0 R* F
- *
. x& _ Y4 d2 p- B. x - * @return Shell* 当前活动shell对象
+ C) z e6 I2 ]3 F* E6 J+ ` - */
' J7 m6 ?; p; l5 V/ }. ?4 L - Shell* shellGetCurrent(void)& F2 Y0 D' M0 A2 t6 A
复制代码 3 n9 w5 j) c7 |/ o1 T8 w
可以定义一个全局变量* G2 Q$ d% E/ J0 o+ s
8 }6 a9 C: ^* \; j. U
- Shell *shell_active; //当前活动的shell
复制代码 . @: H$ E6 {8 x' k) |- L
如果我们使用多个串口创建shell 对象,如果通过不同的串口访问文件,那么就不容易知道应该在哪个界面进行输出,正如这个 mv 的命令,首先获取当前活动的shell,再通过 shellWriteString(shell_active,“Operation failed! Error Code:”); 进行输出,这样就可以避免冲突了。
' O% Q1 P% `$ b3 e( ^, B. [0 [7 a
7 U0 _3 S# b/ Y* F0 S. l' ]+ j- /**
7 X, z0 M) U( W; d, ]1 m6 p/ B - * @brief 重命名/移动文件或子目录% l' x% E- s- C- X9 Z
- */# o% m( ?( ^" I
- void shell_mv(char *old_path,char *new_path)" i4 Z' u) _$ G4 T1 [+ s- s1 W6 R
- {* y, X! h. L- _+ E
- FRESULT res;8 b3 D5 k" K8 @1 \4 L/ W$ y
- 8 p' A% [. \3 A8 @
- shell_active = shellGetCurrent(); //Get the currently active shelll
3 n% O- O3 A/ Q! n - res = f_rename(old_path, new_path);
& p( |0 Y3 [9 i! O4 j+ \; s$ A, W - if(res != FR_OK)$ L }" r/ R$ T
- {
T8 N* ^7 H( a5 G - shellWriteString(shell_active,"Operation failed! Error Code:");+ x5 c8 U/ k$ T" I; \
- shellPrint(shell_active,"%d\r\n",res);0 ?/ }+ Y) @6 o1 J6 A2 \
- }
, D7 ~# }) Y1 |% j8 t' S" z9 Q s/ e - }
, b7 ^2 V8 ]" S' \2 T* A! H) F; x' d - SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC)|SHELL_CMD_DISABLE_RETURN, mv, shell_mv, rename or moves a file or directory);
复制代码
' G$ h ^% W( ~+ |: C) B" W& R& H) c6.3 shellPrint0 }% @9 Y/ n0 K& j7 a0 m
这个功能就是 vsnprintf 的简单封装了,用于向字符串中打印数据、数据格式用户自定义,但是要注意,打印的大小不要超过 SHELL_PRINT_BUFFER。) P+ p1 n2 I& m( t' C
! g1 x A4 O0 S/ T- #if SHELL_PRINT_BUFFER > 0
1 p. x: }1 E g% \8 H9 d5 j+ B - /*** e* u1 J9 h( ~5 {5 z+ F
- * @brief shell格式化输出
8 h& i/ M: V; ?. F# d - *
( W9 T* Q+ p( e4 D, y - * @param shell shell对象6 B6 S" h( r7 {% X8 s0 I) |- Q
- * @param fmt 格式化字符串
# r8 H" f* ~2 f, ^& F, B7 E - * @param ... 参数
& c' C% K' G3 @4 r/ w) u - */! c1 n8 [! W2 p# G* i
- void shellPrint(Shell *shell, char *fmt, ...)( X- ?, n- ]3 \( |; V: T+ I
- {
- ?' U- C/ ?4 T: ? - char buffer[SHELL_PRINT_BUFFER];
- i' h- a! T. w' ~- w# \ - va_list vargs;5 l1 R/ |6 D+ j
4 `" V& E' @* i5 L! R0 w- SHELL_ASSERT(shell, return);
7 k. {% W0 _2 r, }# z* L: \
. I3 a5 V' {% [# g1 s/ c& u9 I! b- va_start(vargs, fmt);
; Z! C2 u$ `- ]5 @" n- r5 k - vsnprintf(buffer, SHELL_PRINT_BUFFER - 1, fmt, vargs);9 H5 v# R3 L4 k: t
- va_end(vargs);9 z! {, o% u. w% i) V4 S* b
- # t+ p3 W; L; z: L$ d& J! k3 j$ h
- shellWriteString(shell, buffer);
+ i3 w" b1 `& G+ X, V! U) v) @( E - }
7 F1 M" e! o! u3 F% H) t: ~- P+ u - #endif
复制代码
% l. s* [' Q" ~' ~6 J& s到这里,介绍就完了,而这也足够应付绝大部分的调试内容了。
' \" U/ C5 N4 o' o& g- y1 r
$ {1 D9 \8 Y1 h: |+ k2 O1 [3 k9 l5 v6 B2 e: h# n% b( w9 P
: R& q6 s( ]% C1 o" C- q && *(string + 1))) f9 ~( X2 W: Z$ `& Q1 u# A# k
{2 o$ v* O& c: G( ~7 z
return shellExtParseVar(shell, string);$ J! X0 d/ e. r; R
} ^# N, a0 G2 O* m" P$ f2 |
else if (*string)
* E. l5 c, \+ Y' @ {# Q6 b) |: }/ K' [" ]+ a4 z
return (unsigned int)shellExtParseString(string);
, e* R0 [: F$ m }
* j' p% K, D* h _& H return 0;
! Q$ u4 P- o& Y% N5 K: L; E6 o}[/code]" P; x; ^$ p3 }) S, W6 k
六、其他常用的 Letter Shell 函数( H/ }2 {) U5 |8 b# W
6.1 shellWriteString
. {8 [+ y; }2 l6 b) A/ H& }4 h7 R3 s函数原型如下,这个功能就是 printf 。这个应该很好理解,就不细说了。) w& N, @- L# u1 u& U$ B( c
" R1 t( {) R% i4 q; o, @ n' q- /**
! |' M' x& B+ p& @ - * @brief shell 写字符串- r' M8 t9 n7 u4 m. T' K4 n
- *
- d: j" d9 K: d% r, S - * @param shell shell对象% a4 S& U1 q; x$ |% E1 i# r; Y
- * @param string 字符串数据
+ A/ u$ R) E3 ~0 Q' t0 D8 a3 p - *
2 M! v3 k( Y o2 p - * @return unsigned short 写入字符的数量
9 g8 o* E2 Z" N - */( O! H/ ~9 X. Z" i+ [
- unsigned short shellWriteString(Shell *shell, const char *string)
复制代码 ( W6 ]. M2 d: x( X5 I
6.2 shellGetCurrent
1 J% F' Q- T1 n* R" eletter shell采取一个静态数组对定义的多个shell进行管理,shell数量可以修改宏SHELL_MAX_NUMBER定义(为了不使用动态内存分配,此处通过数据进行管理),从而,在shell执行的函数中,可以调用shellGetCurrent()获得当前活动的shell对象,从而可以实现某一个函数在不同的shell对象中发生不同的行为,也可以通过这种方式获得shell对象后,调用shellWriteString(shell, string)进行shell的输出。
, \* X, d- n/ l4 Q" V! u+ m" i: _. t- {! c8 ?( R. f* E6 j
- /**
6 P/ |1 r# [, k/ G1 M0 B9 c - * @brief 获取当前活动shell$ [- i8 z7 F) E& b
- *
6 e& ], V9 [5 Q- H( o; D9 M% I0 Z+ T - * @return Shell* 当前活动shell对象* ]6 @/ h+ U1 X: F+ Y: q' U
- */
# n3 s& J8 J, K& W$ T' Z0 w! q - Shell* shellGetCurrent(void)1 B5 A6 o% M$ d4 s% h
复制代码
& N' |+ |8 B) _$ Z0 L& _& P可以定义一个全局变量
# r& r% Z' |& L& d9 B" M# n9 w( K s- z6 E! E; z
- Shell *shell_active; //当前活动的shell
复制代码 0 P! }& Z2 i5 p
如果我们使用多个串口创建shell 对象,如果通过不同的串口访问文件,那么就不容易知道应该在哪个界面进行输出,正如这个 mv 的命令,首先获取当前活动的shell,再通过 shellWriteString(shell_active,“Operation failed! Error Code:”); 进行输出,这样就可以避免冲突了。
$ H! j3 F3 _; ~9 E$ R2 Q( t- |+ H3 ~7 T. J# N9 e
- /**% U# B. a% q l& |2 \0 Z4 T
- * @brief 重命名/移动文件或子目录# b. a0 D3 O6 l! O
- */ W; v9 _" t+ k( p# ^4 q, ?8 o
- void shell_mv(char *old_path,char *new_path)4 m) U/ i7 z% t
- {
: K! w7 k" |# J5 i - FRESULT res;
7 L/ K0 i4 q) W% Y" a& l( \5 @+ { - ; M2 ]$ {0 p' J! i0 _
- shell_active = shellGetCurrent(); //Get the currently active shelll* e% H" }3 h: P5 B& c+ _
- res = f_rename(old_path, new_path);
5 p2 `5 l" S+ `% N7 u* b5 u, u - if(res != FR_OK); g. Z& }( h; O6 f& M; Z
- {3 Q- M* |, `" P, d, s1 l
- shellWriteString(shell_active,"Operation failed! Error Code:");: n( C& G% p6 `$ z9 J0 p
- shellPrint(shell_active,"%d\r\n",res);
- y0 r% T; Y& y5 T% G5 r* [/ d - }
2 @! s1 z. T0 z+ l' v8 E5 L9 o - }
& ]8 G$ q" }0 c1 G+ ]5 c. M3 p - SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC)|SHELL_CMD_DISABLE_RETURN, mv, shell_mv, rename or moves a file or directory);
复制代码
3 I. i7 @0 B7 [) ?! K6 n8 S6.3 shellPrint+ T! S/ n7 r4 q( l, t
这个功能就是 vsnprintf 的简单封装了,用于向字符串中打印数据、数据格式用户自定义,但是要注意,打印的大小不要超过 SHELL_PRINT_BUFFER。2 q( w) I b& |/ C6 ^+ {* E5 M
4 W, T1 f3 ^) b0 w2 |% E ?- #if SHELL_PRINT_BUFFER > 0
/ O! C; z) w# r3 A - /**
0 ~2 j0 R: ~. M( o3 t1 n/ O. ~3 o$ U - * @brief shell格式化输出( D# e: ]$ E6 A4 Z1 `- u
- * * @. ]4 q, U0 M8 _$ T
- * @param shell shell对象% u% q: o* U, z
- * @param fmt 格式化字符串
/ y- f4 Y) P) v" l% v - * @param ... 参数% R8 k& d2 s- \3 u7 P5 p) a
- */# Q) {3 c9 O$ W7 M
- void shellPrint(Shell *shell, char *fmt, ...)
/ y2 A) y4 l( I0 c* F7 J5 K+ P J4 o - {: Z% w, Z: L1 m* G1 ~
- char buffer[SHELL_PRINT_BUFFER];
; W, ~' y' g; r' T$ g - va_list vargs;
% H l2 `# S o
% i: {9 L% E l) S& K5 B- SHELL_ASSERT(shell, return);# S' V# z7 q* C* K5 V. q, k
- 4 Y0 ~3 c2 [" d P, U5 N
- va_start(vargs, fmt);
) Z" b0 E1 I* ~3 v1 P+ W - vsnprintf(buffer, SHELL_PRINT_BUFFER - 1, fmt, vargs);* E& c8 L# f F3 [ I- c, `
- va_end(vargs);9 D( L$ c+ g) t& e
3 K- b8 p; p/ k t- shellWriteString(shell, buffer);
" H& n4 I0 j: L+ x% Q - } ~7 A5 m9 O( h- ~$ G
- #endif
复制代码 6 R- y/ n$ m# Y% E9 C3 y3 [+ W) H
到这里,介绍就完了,而这也足够应付绝大部分的调试内容了。1 q4 i+ _/ A2 l) b- T: U8 e
) B- D# \( L3 {# a* j7 R9 H7 L$ U
————————————————
1 A+ u& B h3 |转载:fafuwxm8 o2 R9 V4 v, H" _; W
" n' Q& W" k9 n8 n
|