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