你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

STM32CubeMX Nucleo F767ZI 教程(3) 串口调试工具 Letter Shell

[复制链接]
STMCU小助手 发布时间:2022-8-23 16:13
前言) 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 20210220214542675.gif * 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 }
20210124211939416.png
% 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 20210124222836342.png
; 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 2021012422385383.png 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
  1. /* USER CODE BEGIN Header_StartDefaultTask */  g$ `3 x8 x, _% c. P- b
  2. /**
    8 N7 I0 ^5 z, v7 r6 B
  3.   * @brief  Function implementing the defaultTask thread.
    . [% U( R$ W5 _& M( J! W7 S7 ]5 @5 `. C
  4.   * @param  argument: Not used% O" U. k, b" V0 u  l
  5.   * @retval None
    ) y% _& p( Y- {
  6.   */
    ) F0 N" B8 S+ Y* a. _; [) W
  7. /* USER CODE END Header_StartDefaultTask */
    7 q3 f. r+ D( D) N+ ?
  8. void StartDefaultTask(void *argument)
    7 w3 K$ h5 y2 M# G, E
  9. {0 a2 Z0 v. c/ t1 Z  W6 @3 r
  10.   /* USER CODE BEGIN StartDefaultTask */
    ( T) J& r0 ]1 O* G
  11.   /* Infinite loop */% K% B2 @) n. Q" l+ i
  12.   for(;;)3 D% w" e7 e: V7 }" t- ~
  13.   {
    + W. d; P! P6 L6 z$ h6 Z4 _; }2 t
  14.     HAL_GPIO_TogglePin(LD3_GPIO_Port,LD3_Pin);& v6 l4 w( A) n" ~! r2 d( `6 H
  15.     osDelay(100);4 ^) `1 v- R1 I$ G
  16.   }
    ' S9 T: |8 b$ d! {$ j
  17.   /* USER CODE END StartDefaultTask */8 X* N/ e; L* ^/ M
  18. }
    4 n; u' D* X+ d4 [0 G: M
复制代码
- p4 `) R/ K: z% m
注意,配置的Keil工程,默认是没有设置下载完自动复位,我们需要按一下Nucleo 板子上面黑色的复位按钮,另外默认的配置下,有一个以太网的初始化 “MX_ETH_Init()”,需要接入网线到路由器,不然初始化不通过,会跳转到Error_Handler();
  1. if (HAL_ETH_Init(&heth) != HAL_OK)7 W4 b7 t- U7 |5 m
  2. {9 j( F# \# w9 J
  3.   Error_Handler();0 y" h! q9 D' S, a3 }! j# K* ~! o
  4. }
复制代码
# 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
20210124230430874.png
$ 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 20210124231354443.png
* 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 20210124231834258.png 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
  1. ..\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
  1. ..\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
  1. #include "shell.h"' Y2 L' a6 C. L3 L
  2. #include "usart.h"
复制代码

6 ], s2 R2 e6 k下面两个函数是shell读写的实现,里面使用的是固件库的函数。6 C% q; ~5 m6 d! t
  e% m! M+ m( c  r* U# a
  1. /**3 j$ j& h! w3 l) D- [
  2. * @brief 用户shell写
    : T5 @# C& E3 ]6 _( M
  3. * 2 E. y' X4 H9 k2 }- T6 ?' F+ _- @
  4. * @param data 数据
    $ d5 q. l# v& W' ?' F. g
  5. */) a( Z' Q# R. b* E, U0 Y
  6. void userShellWrite(char data)" n( |* y, o$ M4 Q2 G8 `+ |
  7. {% B# T" Z& \5 `  D  P
  8.     serialTransmit(&debugSerial, (uint8_t *)&data, 1, 0xFF);+ X4 q* c' Z2 O) F" ^$ ^
  9. }! j. m/ ?0 ~5 h7 B8 Q. n
  10. ; \5 H, o' v+ `; M0 }. Z1 q8 {: h3 X
  11. /**
    1 v2 Z1 f" P! T+ z1 @
  12. * @brief 用户shell读
    ) [8 |. |2 y) k% C/ T
  13. *
    1 L2 N* [$ ?+ i% F9 o! A* a* b
  14. * @param data 数据4 `" M! [9 V! p9 N
  15. * @return char 状态
    & a8 ~3 ]( ]8 g5 E% n) m: k
  16. */7 N% B5 |* `4 T/ S  u* a) C
  17. signed char userShellRead(char *data): _# P" W  e# A% N; N
  18. {
    - M$ Y1 u2 b' O/ D0 ]0 `
  19.     if (serialReceive(&debugSerial, (uint8_t *)data, 1, 0) == 1)
    4 [5 E8 }! e; y6 o$ ^) C0 q
  20.     {0 _9 m" ]. g  N2 E& }5 j5 F
  21.         return 0;
    ( _3 {/ P0 S2 Z, D. s7 g$ B% s
  22.     }
    ( B: z. [, I" x8 W8 D' A5 ^
  23.     else
    ! z; _+ q3 i+ r3 g
  24.     {2 b! Y* w5 p3 S6 e  m
  25.         return -1;
    8 G# C5 B, V2 y& _! ~; F( B, k
  26.     }
    . B( H% g% N$ ^
  27. / E  c( f4 {/ j! A1 G+ |
  28. }
复制代码
2 j! k& j5 i( \' W$ Q- p, w7 f) `
既然使用了STM32CubeMX来生成工程,这里我们改成HAL库的形式。
: t! ?& @0 u( Z# A0 ~. ?/ L. h; v. z* H+ t9 `% `% ~
  1. /**& i" s/ }' L: [% _+ Q+ F
  2. * @brief 用户shell写
    # w7 e4 i. r7 p' O9 D
  3. *
    / v5 ]' s6 x2 P2 l) }, r2 N
  4. * @param data 数据: `/ p5 n) z; V9 \
  5. */
    9 v7 `) ~0 }" J) j9 y1 _" M, Y. z
  6. void userShellWrite(char data)
    % l; q* d& a: y( ~
  7. {
    ) I/ B( V. u1 _0 X5 b% J
  8.   HAL_UART_Transmit(&huart3,(uint8_t *)&data, 1,1000);. r8 B# p1 h- z. e' h$ {' _/ U
  9. }+ j0 O* N  ?5 K- h" n- v, |$ c
  10. ' X+ @6 t6 o) X! p8 N. ^- _5 ~( U
  11. ; f7 A$ t( c8 H- A4 l; ^! g  b; |
  12. /**/ K0 a, F- W5 ~) j! s* {
  13. * @brief 用户shell读
    6 m  b+ G! g! M9 ?/ s+ l
  14. *
    , U% ~( d) |( F. G& A1 L9 J
  15. * @param data 数据
    0 E8 x0 ~% B/ |( y
  16. * @return char 状态
    ( s0 U* @- H& f0 J
  17. */
    % h/ _3 k& F; F. e- Y
  18. signed char userShellRead(char *data)
    ) A+ w. i8 J/ W3 S8 a
  19. {
    1 g: p6 q: U; d
  20.   if(HAL_UART_Receive(&huart3,(uint8_t *)data, 1, 0) == HAL_OK)
    ' x% x% y# e9 F8 A" y- ?6 u
  21.   {& z7 }% Z9 H: \" K
  22.       return 0;( ~- A1 C9 G: a- m
  23.   }: P& B$ V9 {# w6 Z- W# O! b
  24.   else
      V! h# N3 w) j& }, P3 o
  25.   {
    9 O# K3 H/ A4 a$ `
  26.       return -1;
    6 l; \+ z+ o# o9 Q, j
  27.   }
    : R# K  g6 I; E3 q
  28. }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. Shell shell;
复制代码
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
  1. /**( B/ P# P; S# |; i- Z  B
  2. * @brief shell读取数据函数原型& b; D9 H$ ^$ n% G, D
  3. *
    + }2 s, x, @/ P6 h, T
  4. * @param char shell读取的字符6 o. z% l9 a, Y* @
  5. *
    , `% s/ F9 u( C+ b7 A
  6. * @return char 0 读取数据成功1 Q  G& G1 J/ d0 |: Q6 ^: D
  7. * @return char -1 读取数据失败
    5 J% u1 H# \4 {  h# O
  8. */
    ! i6 f6 I: w5 d  @, k
  9. typedef signed char (*shellRead)(char *);2 o7 f! X7 Q6 ?

  10. 3 h2 X! h8 v6 r) i' n$ K$ l+ s6 P+ w" d* p
  11. /**# G7 A& f1 R- `, q" j, j
  12. * @brief shell写数据函数原型3 q. _4 i* C5 d, \7 j
  13. *  t+ e, V; s5 h; R/ ~, d
  14. * @param const char 需写的字符" @2 T& o6 H  \1 }% J. ~! A+ I
  15. */2 c) C2 K5 i  T5 }& D: u* L& I* n
  16. 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 [
  1. char shellBuffer[512];
复制代码

$ 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
  1. /**
    # h8 l; z6 o6 F2 x1 S( r
  2. * @brief 用户shell初始化
    ! ~2 I' }9 L2 P3 b9 R5 n
  3. *
    ( U: R4 D7 _, U/ ]7 _
  4. */
    4 x1 J, M8 {  x8 T+ P
  5. void userShellInit(void)
    7 q) }# E& U: c. n. U
  6. {
    6 ?3 }0 n3 C! V) {. v) y1 w9 g
  7.     shell.write = userShellWrite;7 v! _0 v" s' k: Z' z2 f
  8.     shell.read = userShellRead;9 H& X' V' L* t1 e  u- ^
  9.     shellInit(&shell, shellBuffer, 512);- p9 ]8 w% Y) r! [# W$ Q4 r4 p
  10. }
复制代码
( 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
  1. /* USER CODE BEGIN Includes */8 ^0 O8 k( ^! b, X6 {2 z
  2. #include "shell_port.h"+ P- n5 X9 F& ]$ o' @
  3. /* 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
  1. /* USER CODE BEGIN 2 */- y/ z6 y- W$ H" T6 ?* |3 D
  2.   userShellInit();    //letter shell 的初始化" T0 q( Y. f7 {: @
  3.   /* 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
  1. ..\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
20210124235734855.png
: s* S% ?; O9 a' [9 [3 m& x- h$ _
由于我们还没编写串口接收方面的函数,按下字符是不会进行回传的,这里我们先按一下复位按钮,接上网线等到初始化完成后,就可以看到 letter shell 初始化完成后的串口信息。
' J. a5 c5 h% h" B& T. F/ ^2 O2 R7 }
20210125000101173.png
' 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 Z9[D2ZUY1[R~NU1CT(4F8BN.png , _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; ^
  1. /**
    ) D- ?, [5 V1 `8 ~7 X# i8 \
  2. * @brief 是否使用默认shell任务while循环,使能宏`SHELL_USING_TASK`后此宏有意义
    6 S- N% D+ R0 I" L" A
  3. *        使能此宏,则`shellTask()`函数会一直循环读取输入,一般使用操作系统建立shell
    2 K; D! ~; ~' n1 E, }2 L5 p
  4. *        任务时使能此宏,关闭此宏的情况下,一般适用于无操作系统,在主循环中调用`shellTask()`  n7 J3 C/ D% m5 q3 s5 w
  5. */
    / c9 [, ]3 Q. [0 I2 x# C, w$ c
  6. #define     SHELL_TASK_WHILE            1
    ( G0 K3 \- N5 W& T& E, V

  7. # K- M0 z( r, B2 `4 z
  8. /**7 \7 W* |8 N: }
  9. * @brief 是否使用命令导出方式2 I( p. D; h6 k" H3 U% y
  10. *        使能此宏后,可以使用`SHELL_EXPORT_CMD()`等导出命令# L- C% m% m* U5 o
  11. *        定义shell命令,关闭此宏的情况下,需要使用命令表的方式
      H% e+ ]+ C9 m- f
  12. */; e% c% z+ Y& r7 Z# P1 f8 U
  13. #define     SHELL_USING_CMD_EXPORT      1
    + Y( k+ L: U+ Z* `7 D* w9 \
  14. 2 |8 p( o0 _" |' @* q2 L- P
  15. /**
    " X9 l4 V" u9 d7 J, p: z
  16. * @brief 是否使用shell伴生对象
    ; ^  h7 P; [5 ]1 L! z/ D
  17. *        一些扩展的组件(文件系统支持,日志工具等)需要使用伴生对象. u. N7 p; D! @8 n. o; H( r) b
  18. */
    - W6 Z/ D: o2 B5 G
  19. #define     SHELL_USING_COMPANION       09 B+ C: [1 R& L7 _  y3 ^. Z# T

  20. # F7 p$ j8 K; J# v6 g$ k
  21. /**
    3 W5 _+ r0 J! K4 ]' u
  22. * @brief 支持shell尾行模式
    4 Y- g7 }0 w3 ?# \
  23. */- S  w3 z* h& Z+ m$ b  T
  24. #define     SHELL_SUPPORT_END_LINE      0
    4 O( Q0 _" o( }( m( a
  25. 7 E8 ~7 j8 @' ?9 f2 B7 D7 V- n8 W5 I
  26. /**, ^# c2 d5 N( j
  27. * @brief 是否在输出命令列表中列出用户
    " E, G1 ?9 G# V1 ?
  28. *// H5 Z. x. T  @% i" Z
  29. #define     SHELL_HELP_LIST_USER        0: p& b* ^, L( I' l6 q9 Y
  30. : d. \, j1 _! t/ x; h  A# S4 }
  31. /**/ T3 j$ r! w' d! m
  32. * @brief 是否在输出命令列表中列出变量
    ( v" F$ f$ l. z. i6 ?& ]/ T
  33. */+ ^/ c$ O2 D6 I  g2 L1 `* S
  34. #define     SHELL_HELP_LIST_VAR         0
    & X# _# e+ {/ `) i# H
  35. 7 p4 i9 d/ W8 i
  36. /**
    : F2 A/ t  p8 b+ N) o
  37. * @brief 是否在输出命令列表中列出按键( [4 T9 F" D+ \5 ]
  38. */* U, R( A$ L, e1 i3 s# X
  39. #define     SHELL_HELP_LIST_KEY         0
      A4 w+ P( P) l: E! D/ _. }* V

  40. 9 |/ {' ?4 A! A2 q! o( ?
  41. /**2 u  s$ E" r' V' A1 l! o
  42. * @brief 是否在输出命令列表中展示命令权限& H' u, A2 u( g7 T
  43. */4 M, T: U& |& `4 f) y2 O6 [
  44. #define     SHELL_HELP_SHOW_PERMISSION  15 F1 U3 `6 I8 Z) `; e8 ^

  45. 7 \0 K1 ~# y1 R
  46. /**
    ) o: `$ C" b+ ^( k) u! k
  47. * @brief 使用LF作为命令行回车触发7 w  T% }) G5 k+ I  |7 [
  48. *        可以和SHELL_ENTER_CR同时开启" A0 I' V$ h% k9 e' z
  49. */& x0 S( J4 @1 m- g
  50. #define     SHELL_ENTER_LF              0. ~1 k# ]$ j2 Q' q: ?; C

  51. # G; S9 ^" a, x
  52. /**
    , f/ I/ k2 |7 O
  53. * @brief 使用CR作为命令行回车触发1 @$ J8 v. @9 m& W: s3 u" }* `
  54. *        可以和SHELL_ENTER_LF同时开启
    . g; I' N, w( u0 u$ j4 e7 l2 Q
  55. */% F7 d+ ]* i/ e6 _1 b" b
  56. #define     SHELL_ENTER_CR              0. ?' D' E/ B9 o6 f+ j, a( n

  57. 3 J! v1 o% S) C# o% h  q
  58. /**
    ( ?# D3 G' w! u- J
  59. * @brief 使用CRLF作为命令行回车触发$ o$ l1 n. @$ j. i
  60. *        不可以和SHELL_ENTER_LF或SHELL_ENTER_CR同时开启
    5 G, d  W" m8 o' D( K
  61. */
    $ t" u5 u7 q* M4 E: d% S, C
  62. #define     SHELL_ENTER_CRLF            1; I3 k, o- J- p: Y
  63. 4 ?' I# _0 B" \; B0 ^
  64. /**
    # Y: t/ \% B& d+ V- ~, P
  65. * @brief 使用执行未导出函数的功能
    , b) V2 b* ^* o" U0 i  T# g
  66. *        启用后,可以通过`exec [addr] [args]`直接执行对应地址的函数
    4 r, S( M6 l( m) K- @$ f* B
  67. * @attention 如果地址错误,可能会直接引起程序崩溃" G6 u3 a% l# x' j5 a
  68. */8 }$ A" R7 Z$ v* T0 U* I! S6 q
  69. #define     SHELL_EXEC_UNDEF_FUNC       0
    ( Y) B( d3 O; m8 p1 q  N- e& f
  70. % Y; V7 H. e( a0 D
  71. /**9 U  l/ U  W# h1 o5 A
  72. * @brief shell命令参数最大数量, U- h2 b, r5 o6 h, n* b, o  L
  73. *        包含命令名在内,超过8个参数并且使用了参数自动转换的情况下,需要修改源码
    / ]! H9 D& ~" M$ W
  74. */
    , V/ A4 y  t. S( ]6 P3 L
  75. #define     SHELL_PARAMETER_MAX_NUMBER  8
    ; O* S1 L- k; d3 b: u( `

  76. ( I( Q3 c1 M; h5 {8 }" l) P
  77. /**
    ) `2 Y- ^( k" v, C: {
  78. * @brief 历史命令记录数量& C' x0 c* u. f5 R( b
  79. */
    # c0 G1 {, _& R3 m: f
  80. #define     SHELL_HISTORY_MAX_NUMBER    5
    1 j3 I2 U) F: h8 x7 c

  81. 4 X$ S3 m8 G8 I1 x
  82. /**
    1 M. l! u: W! P! s
  83. * @brief 双击间隔(ms)' p, s0 \: o5 B! X, F
  84. *        使能宏`SHELL_LONG_HELP`后此宏生效,定义双击tab补全help的时间间隔7 i. M9 V0 w. K$ ~4 ~8 K
  85. */
    2 L( C, o4 f; ^" J2 v
  86. #define     SHELL_DOUBLE_CLICK_TIME     200
    , b( E, {2 j- d* {1 Y" n: ~
  87. + |) ]3 e* x1 e5 L0 W
  88. /**
    9 W" q  S# E2 V% P
  89. * @brief 管理的最大shell数量3 e" z% I+ g/ j- g( u2 T
  90. *// \9 g0 w6 p/ O6 ]7 ^
  91. #define     SHELL_MAX_NUMBER            5! X3 j4 g- S! o: H3 N! j' r4 q; O

  92. ) f, ^8 ~0 T5 Q( W0 R3 s% p
  93. /**
    4 [# f, K& J, l) V. j5 {
  94. * @brief shell格式化输出的缓冲大小& f2 _  T  k) k% q4 B" S
  95. *        为0时不使用shell格式化输出/ k8 C. Z% ?% f  l1 r5 s' O3 }/ f
  96. */
    2 f6 q4 _* J2 `! u" V5 U
  97. #define     SHELL_PRINT_BUFFER          128
    + G& l& j" S- ~+ u% h8 {2 D
  98. ! a; d9 k, Q! ^$ _$ m" o5 H* @2 t2 `
  99. /**
    & S2 f0 @3 {  T; |* ]8 e
  100. * @brief 获取系统时间(ms)- D6 c! y) N" z* y% U2 P, h% A
  101. *        定义此宏为获取系统Tick,如`HAL_GetTick()`) w& V( r! ?9 ]+ q; u8 J
  102. * @note 此宏不定义时无法使用双击tab补全命令help,无法使用shell超时锁定
    $ J) L$ D& P% `: i+ ]. W
  103. */
    1 R( i9 p( J7 F4 a3 s
  104. #define     SHELL_GET_TICK()            HAL_GetTick()
    - N; d8 p: |; r& C9 H5 \
  105. - H& G4 R6 Z# J! t7 P& x) R4 L* l0 ~
  106. /**
    3 ~6 r( c- x/ ^# ]4 @+ ^9 ]" x
  107. * @brief shell内存分配
    % Y6 L% T2 c" M) b) {" D
  108. *        shell本身不需要此接口,若使用shell伴生对象,需要进行定义
    , o; T* y+ V& F6 S
  109. */
    ! }2 h9 i5 M- R) W
  110. #define     SHELL_MALLOC(size)          0
    9 a1 N$ T7 T# [+ O& F
  111. 6 n/ Q& c0 [: c* _1 M2 M: o2 Z
  112. /**
    2 }$ @/ V- Q+ z) [
  113. * @brief shell内存释放  _% b. M. v( j/ I8 c
  114. *        shell本身不需要此接口,若使用shell伴生对象,需要进行定义
    7 b2 E5 R0 R  @8 |
  115. */
    % p) s2 l0 v9 x" [
  116. #define     SHELL_FREE(obj)             0+ r( M1 a8 m: X# a# r9 G& w/ s# Z

  117. 8 M$ E/ U6 F* Z8 d
  118. /**
    , I+ i* w$ i) a! @7 W0 \* k! K+ `
  119. * @brief 是否显示shell信息
    7 R2 |+ I' c1 U# w# j: o
  120. */8 z: n! l: D  V
  121. #define     SHELL_SHOW_INFO             1
    8 u% w& M' q  m# a8 I% y6 C" I

  122. ! G) ?+ G; b" t, I0 D8 ^% t
  123. /**  Q0 L: Q. `2 l; Q& g/ j
  124. * @brief 是否在登录后清除命令行
    . w, q" d0 S( r7 r/ `$ m& S
  125. */
    $ p: R& w9 n! \# f
  126. #define     SHELL_CLS_WHEN_LOGIN        1
    7 @" ^# V" F# r* p
  127. # F3 v) h# E0 j3 {- P
  128. /**$ c. S/ N& b! E' h- |$ i) i( s7 e
  129. * @brief shell默认用户
    ) V" Y- e/ X' J8 L( @: j$ W8 x
  130. */
    % x0 Q: u; i% g6 E+ f2 k% Z6 ]
  131. #define     SHELL_DEFAULT_USER          "letter"5 u4 u! \' G1 d' }; z; h1 k8 G

  132. 4 Q4 g+ {5 m% ?
  133. /**$ V% h" l  u' X5 C$ u
  134. * @brief shell默认用户密码
    & o2 q# a( [, {. @7 U
  135. *        若默认用户不需要密码,设为""
      m2 s4 c) ]4 b6 I9 |
  136. */% h1 G1 f' P  V8 J+ y
  137. #define     SHELL_DEFAULT_USER_PASSWORD ""6 N- Q5 s( }1 w
  138. * R7 j% J; b% d! A- M$ u
  139. /**. p7 A6 N7 v2 a9 e  U5 M3 X$ X
  140. * @brief shell自动锁定超时( P1 ]& B2 e; s( F
  141. *        shell当前用户密码有效的时候生效,超时后会自动重新锁定shell6 {) w3 v% z* {( C% z; e  D+ Y
  142. *        设置为0时关闭自动锁定功能,时间单位为`SHELL_GET_TICK()`单位
    " O- y: w% S$ I0 B( w7 k, Z
  143. * @note 使用超时锁定必须保证`SHELL_GET_TICK()`有效
    * N9 Y& m8 C2 j/ N
  144. */
    3 g9 i" {- E$ a: _' ?4 B  W
  145. #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( `
  1. /**3 J# W. v; A$ k* k- Q: a
  2. * @brief shell 任务
    7 v9 D( E$ h4 I' ]2 y, d3 a: Y% K! f
  3. * 0 ?; o8 d  ^  N
  4. * @param param 参数(shell对象)
    2 p/ q. T2 s4 o4 L
  5. *
    * j+ u/ K" ~3 N
  6. */
      r) O; T* |  A! @; M
  7. void shellTask(void *param)
    1 e: f+ {0 O1 D  w6 f  C
  8. {1 A2 T6 R& }' t0 \+ r9 U1 g
  9.     Shell *shell = (Shell *)param;
    * k2 u9 S1 c% _7 b1 K: Z
  10.     char data;/ W: c% ]0 V( V- H4 r. o
  11. #if SHELL_TASK_WHILE == 1
    0 c  v4 w! e! g' i
  12.     while(1)( P# P) e# o. l* {
  13.     {
    0 R% O8 c) p& J, s7 ~
  14. #endif" N, ^" f) |4 k4 O, c' Y3 q2 a! ?
  15.         if (shell->read && shell->read(&data) == 0)
    $ u$ K" e3 y; n$ t
  16.         {
    5 P  r7 Y2 \1 _4 w
  17.             shellHandler(shell, data);
    3 @+ l( W. e! n. q6 ?
  18.         }
    ; ^* N& A; ]7 k% g! ^
  19. #if SHELL_TASK_WHILE == 1
    6 y& u% m: ~3 [: Q! u7 E
  20.     }* ?7 Z' |9 E3 `: r# s; {1 v
  21. #endif
    " y) H2 n4 {- P, o  t
  22. }
    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
  1. #define     SHELL_ENTER_LF              0, G- m) u$ I- f
  2. #define     SHELL_ENTER_CR              1
    " e/ ~, [1 P4 j# d) V) e- l4 }
  3. #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
  1. #define     SHELL_TASK_WHILE            01 h* o( e- z: t! N, D; s- O. \
  2. #define     SHELL_USING_CMD_EXPORT      1
    9 J8 \6 O2 R$ v# z6 N( a
  3. #define     SHELL_USING_COMPANION       0+ I$ F9 ~) X+ D8 Z4 b& i
  4. #define     SHELL_USING_COMPANION       0
    3 W! v  y5 [( N
  5. #define     SHELL_SUPPORT_END_LINE      0; y# b' ?6 M' n7 _7 N4 I% c
  6. #define     SHELL_HELP_LIST_USER        07 {: L. i, [& q
  7. #define     SHELL_HELP_LIST_VAR         0
    9 ]0 Y8 i3 `/ H
  8. #define     SHELL_HELP_LIST_KEY         0
    # |5 k9 z$ y+ G3 j! H. l
  9. #define     SHELL_HELP_SHOW_PERMISSION  1$ o' e) d8 v( I  B
  10. #define     SHELL_ENTER_LF              0
    $ K1 N. x" y( j, B
  11. #define     SHELL_ENTER_CR              1* y* F: j- L$ ]6 g0 H* ]) L$ @
  12. #define     SHELL_ENTER_CRLF            0
    ( k( x* p3 C- ]$ \7 T" }, T
  13. #define     SHELL_EXEC_UNDEF_FUNC       01 M& p4 [2 p# G3 o6 K; S" u
  14. #define     SHELL_PARAMETER_MAX_NUMBER  8# @6 Q/ s  `0 ^: H( i
  15. #define     SHELL_HISTORY_MAX_NUMBER    5
    - O5 n+ I5 O2 c$ u' f5 H1 P
  16. #define     SHELL_DOUBLE_CLICK_TIME     200
    5 [  W3 V1 J+ n: p# W+ [3 D
  17. #define     SHELL_MAX_NUMBER            5
    " m' l, b7 z4 A  p
  18. #define     SHELL_PRINT_BUFFER          128
    ) E$ e  ?4 ^+ y$ `' _- Y
  19. #define     SHELL_GET_TICK()            HAL_GetTick()( b0 l5 \, U; i! X* S  j- `# A1 Y
  20. #define     SHELL_MALLOC(size)          01 B  M. w9 I% S+ P+ V
  21. #define     SHELL_FREE(obj)             0% E8 j! p4 t0 ?" i& Z
  22. #define     SHELL_SHOW_INFO             1
    9 V* O( _- G- A
  23. #define     SHELL_CLS_WHEN_LOGIN        1
    9 g3 n0 n* {) j" o. G/ g
  24. #define     SHELL_DEFAULT_USER          "letter"
    2 F$ y; {2 R2 c- b8 V8 Q
  25. #define     SHELL_DEFAULT_USER_PASSWORD ""
    3 t" O: t; S0 z% Z1 p* ]
  26. #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- @
  1. #define HAL_USART3_RXBUFFERSIZE         1) d5 s- V) C1 m6 a; @% \$ c) J
  2. 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
  1. 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; @
  1. void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)( n+ J) n0 l7 w7 b1 t& n9 c. ?6 S
  2. {) M6 E3 {1 X# n
  3.   uint8_t udata;  p7 h; ?: l5 {1 @; y. }9 ?
  4.   if(huart->Instance == USART3)      //串口3的接收部分
    : W0 ~8 O2 A2 @; g& J( _' r" N
  5.   {
    ' t5 l( ~1 N$ T- u
  6.     HAL_UART_Receive_IT(&huart3,(uint8_t *)HAL_USART3_RxBuffer, HAL_USART3_RXBUFFERSIZE);     //接收串口的缓存
    - T' ]" \. q; R3 f7 u
  7.     udata = HAL_USART3_RxBuffer[0];
    2 ^+ f1 m1 {3 @& t+ }9 v0 G
  8.     shellHandler(&shell, udata);
    0 O3 s, K; n* f( A0 g9 l1 N- f! ?
  9.   }
    9 b3 \+ x# N* D* W6 ~' H
  10. }
复制代码

: Q% S" s0 b% |5 w" }编译后下载程序,复位,这时候输入cmd 命令,按下回车,就会列出可以支持的命令了。后续如果自定义了导出函数,也会在这里显示。到这里,就说明了串口的收发是正常的,shell 也移植成功了。
  Y7 g0 u- [+ D- x: A4 Z% c/ L  O6 V& [* H
2021012523522544.png : 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
  1. void LD1_ON(void)
    $ P& N0 v1 T+ y) B% t
  2. {
    8 Z$ S* S; v6 ~
  3.   HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_SET);: k  E8 D% c6 e, `% G3 z) p) V
  4. }% u  J- X- D' F9 `

  5. % F) S3 G. ?- f* r
  6. 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, {

  7. 6 ^& ^& b' z" N: x. h6 b
  8. void LD1_OFF(void)3 D) k- N3 m4 [* F" E
  9. {
    2 J' }8 V! A7 q- Z
  10.   HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_RESET);& Y, n# x# }  k7 L
  11. }
    2 ?; z! y0 Q* N
  12.   @; J' @1 `+ D' j1 Y4 y
  13. 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
  1. /**
    8 g2 |5 p4 _4 A' l$ J# T
  2.      * @brief shell 命令定义
    8 @& R; R: Q' N; ?& m: y
  3.      *   H7 K: _% K% |( ]
  4.      * @param _attr 命令属性2 m  f  F& @- H7 f$ C/ }# U! K
  5.      * @param _name 命令名% N6 I( {. m# \; k* ~- }9 o$ |
  6.      * @param _func 命令函数# F3 z5 u4 H: k0 @5 p8 U
  7.      * @param _desc 命令描述
    ( s1 g7 Y% Z# T6 {% r* {6 b
  8.      */
    ' ]* M7 @  N' K' s$ |6 _
  9.     #define SHELL_EXPORT_CMD(_attr, _name, _func, _desc) \6 V1 M* z$ ~( ^8 W
  10.             const char shellCmd##_name[] = #_name; \
    . }$ m/ F  r$ |4 P7 z6 R
  11.             const char shellDesc##_name[] = #_desc; \
      k- Z  E# g: |. G/ ^
  12.             const ShellCommand \
    3 ]' g0 g; q, H9 p7 H" q0 I
  13.             shellCommand##_name SECTION("shellCommand") =  \( Y5 K% \/ N5 s7 E0 {
  14.             { \5 P6 \$ S$ B! n1 |. ^3 O; G
  15.                 .attr.value = _attr, \: O% y8 J, x2 y" [% H
  16.                 .data.cmd.name = shellCmd##_name, \5 J$ z3 U* S" u6 d, l$ y. R* Y
  17.                 .data.cmd.function = (int (*)())_func, \
    % {& Z0 ~! ?6 I8 k8 g5 d; Q1 e1 \1 J
  18.                 .data.cmd.desc = shellDesc##_name \+ x2 }6 k9 S. I$ z- c: b
  19.             }
复制代码

/ 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
  1. /**4 t9 G2 I  T' {# ]$ ]
  2. * @brief shell 命令权限; l" ]5 I% B6 |) b8 x# w: Z0 C8 ^
  3. *
    ) H! S' |) r2 c/ K" n9 ^" R
  4. * @param permission 权限级别! v/ j; I  A, @7 {5 q
  5. */
    ) o/ ~) S, n; Z8 N
  6. #define     SHELL_CMD_PERMISSION(permission) \
    % j$ C# H# a  V- u
  7.             (permission & 0x000000FF)' y( k  I: D/ M  g; P
  8. 0 a3 e. @. v7 T3 ?  U4 r
  9. /**2 K7 e8 C5 P- N; O3 k8 [
  10. * @brief shell 命令类型
    8 a5 ]; A8 o7 p1 ~
  11. * ) S5 q1 i" f5 H
  12. * @param type 类型9 G# L$ V4 @" H: a2 S
  13. */
    9 X# [- t+ e1 q, |- p  {1 O
  14. #define     SHELL_CMD_TYPE(type) \
    2 E3 K- y- X6 i$ ~! j6 E
  15.             ((type & 0x0000000F) << 8)
    0 H% f- F  u8 a2 w
  16. ( e; x3 ?& U/ ~0 Y  k. e: C$ `" l
  17. /**
    0 G1 p" n7 X) P
  18. * @brief 使能命令在未校验密码的情况下使用
    7 n5 J, b0 j& h8 H
  19. */
    & V) R6 b7 B. z, F7 Q7 Z
  20. #define     SHELL_CMD_ENABLE_UNCHECKED \
    3 h' K8 s' @3 `1 r- G
  21.             (1 << 12)
    6 k5 I: {. B! Z

  22. - h5 z7 M1 W6 ?+ w  K7 H* R
  23. /**
    2 i: `' j+ H! v6 V4 x! F( v! L
  24. * @brief 禁用返回值打印
    ' P2 C" U" I; M5 C6 _$ `
  25. */
    / D. K& X( q& `; A7 c* H
  26. #define     SHELL_CMD_DISABLE_RETURN \# K/ _& @) j1 O* N
  27.             (1 << 13)
    ) _; S( V* l8 e9 t1 T
  28. : l" k, a/ L3 o! m7 w  O; F* N
  29. /**5 |& n' W8 r5 E1 ?  d% ]* [
  30. * @brief 只读属性(仅对变量生效)
    0 Y3 \9 I! s* n9 |) X
  31. */
    % Z1 c( e% c. x- \5 Z0 R6 U
  32. #define     SHELL_CMD_READ_ONLY \! l9 M) r% S$ u1 T7 K
  33.             (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
  1. 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
  1. led1_on
    * H& k( t2 D/ u; {; P1 d$ r
  2. 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 20210126222149487.png 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
  1. int func(int argc, char *agrv[])& M- T  p8 d8 v( V- J: L
  2. {" K  ]) G. s, j$ c/ _/ ]( T7 p
  3.     printf("%dparameter(s)\r\n", argc);' w/ o! V9 V) ^% |8 Y2 o
  4.     for (char i = 1; i < argc; i++)
      T  i6 @+ j% V8 m/ ^2 V8 {2 p
  5.     {
    & L0 ^+ A7 D% A% X; k' [# m/ L3 u
  6.         printf("%s\r\n", argv);
    2 E0 G5 A! p! {% A
  7.     }
    6 C- A9 u2 J% C, P; ~6 V; G
  8. }
    7 m2 u& c3 s; o3 i
  9. 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
  1. letter:/$ func "hello world"2 m% v" W! X& b0 V' J4 A& l
  2. 2 parameter(s)
    4 v+ t5 b# X7 e' `
  3. 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
  1. void LD1_ON_OFF_ms(uint16_t on_ms,uint16_t off_ms)' L$ b! E- l% d. Y4 F" a
  2. {7 T8 @$ Q& N/ k( F' {% L% V% A
  3.   HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_SET);    //点亮LD15 o# j4 v) T* y/ B1 @" @
  4.   HAL_Delay(on_ms);                                         //延时
    7 K8 K9 Y( Q; {6 e
  5.   HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_RESET);  //熄灭LD1
    ! h  e( Y! p4 T. x1 k
  6.   HAL_Delay(off_ms);                                        //延时8 v" c! S1 Z' l5 n
  7.   HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_SET);    //点亮LD1& r, g5 J6 d3 O7 ]) W
  8. }7 d5 I( \- n1 r& U9 c5 Z
  9. 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
  1. 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
  1. /**! ~4 T: i& M5 t; f8 A* v( m% o
  2. * @brief 重命名/移动文件或子目录! q- F, w+ x; F" ]- M
  3. */& L7 F: T+ |) Q! P) o" h
  4. void shell_mv(char *old_path,char *new_path); n: [" W# l* D& ^0 p' G
  5. {; p8 }$ M! h2 u( o$ _/ q/ |; M
  6.   FRESULT res;$ T! w" i$ i( n8 N: S- M
  7. , ?3 m6 R& ~# u* J  E6 ^' J2 m
  8.   shell_active = shellGetCurrent();   //Get the currently active shelll9 H: V. P' ?2 N
  9.   res = f_rename(old_path, new_path);
    3 u& B* H; r/ ~
  10.   if(res != FR_OK)( K4 Q, L- O2 l) A3 r
  11.   {
    - D% w/ s' o! W" h! L- S  s7 G
  12.     shellWriteString(shell_active,"Operation failed! Error Code:");+ I! M9 m! [/ _7 S& |! S
  13.     shellPrint(shell_active,"%d\r\n",res);" P5 I# k" I) W6 a6 E3 E; x/ ]
  14.   }
    ; p! k& M1 Q8 y3 k' ~; Y8 ^# N; k; f
  15. }' \4 H! W0 K5 b/ g2 K% x! G! t
  16. 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
  1. 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
  1. /**
    8 i  q. l. N. g" q
  2. * @brief 解析参数( o+ x4 G' M: \
  3. * ) a7 W% X& f! N" ?7 `& u0 u
  4. * @param shell shell对象
    ( ]$ s& H- m" w3 Z7 B1 n
  5. * @param string 参数
    - w, N2 T2 A& S3 O) D7 C* [* H; Q8 x
  6. * @return unsigned int 解析结果% l- v$ V: O( p/ A' T8 [
  7. */+ D4 R0 [; G9 M; n% a7 `) t
  8. unsigned int shellExtParsePara(Shell *shell, char *string)
    0 e/ `" E! o% p7 l! S+ U0 r, D, e) v
  9. {
    ; p# q0 E; y8 x; w- P
  10.         uint16_t i = 1;! Z; x/ \& V8 v+ G
  11.     if (*string == '\'' && *(string + 1)); c+ D- @- ]5 V6 c& [
  12.     {
    7 u7 F% t, b# q; I
  13.         return (unsigned int)shellExtParseChar(string);# W8 H9 [2 J! \8 n+ ?
  14.     }- K& S# Z0 X1 t, R0 l& _
  15.     else if (*string == '-' || (*string >= '0' && *string <= '9'))4 _- M2 u' i/ ?1 r
  16.     {
    $ @  S6 g" w7 R& I2 r
  17.                 while(1)
    6 u1 J. F# d0 A
  18.                 {
    9 f, Q/ e* d9 S& K$ h
  19.                         //寻找下一个表示该参数已经结束的字符
    / g# ]: I0 k4 H' H: f6 T' {& S) [
  20.                         if((*(string + i)  == ' ')                 //下一个为空格表示该参数已经结束
    7 _3 P! @- z, ~
  21.                                 #if SHELL_ENTER_LF == 1                        // LF 表示命令结束$ O8 N. N# K& X1 V
  22.                                 || (*(string + i)  == 0x0A))        
    / e3 i+ G4 K! q. A. Z' c. X
  23.                                 #endif
      _- [+ {- J& _" z
  24.                                 #if SHELL_ENTER_CR == 1                        // CR 表示命令结束% \- Z2 s# j6 c9 @  ~
  25.                                 || (*(string + i)  == 0x0D))/ A0 x) S9 L( {  r8 r) \/ \
  26.                                 #endif6 W4 P2 [3 ]1 C
  27.                                 #if SHELL_ENTER_CRLF == 1                // CR LF 表示命令结束1 h& A, n# z4 L
  28.                                 || ((*(string + i)  == 0x0A) && (*(string + i + 1)  == 0x0D))+ m0 V' [# l* N5 u4 `2 |
  29.                                 #endif
    4 Y& a4 S& ^2 w1 ^: |: E# N7 ~) l$ \
  30.                         break;
    / e# i' N8 k! r; d
  31. 4 l4 \: F/ t" f7 u5 U
  32.                         //出现不为小数点或者非数字的字符,表明该参数为字符串4 ~1 \, q. j. }2 ]
  33.                         if((*(string + i) != '.') || (*(string + i) < '0') || (*(string + i) > '9'))
    / _4 w5 D0 l3 @" Z' ~
  34.                                 return (unsigned int)shellExtParseString(string);
    8 C: L7 \6 q* B. B5 t* M( U* r+ e

  35. ! }. T- \5 C& c) d* F
  36.                         i++;
    $ s4 Q/ E% _6 l3 E$ _% ?
  37.                 }
    * @, P5 ?0 x7 U* F
  38.         return (unsigned int)shellExtParseNumber(string);! w1 d% G5 D3 g' K0 y2 Y2 ]
  39.     }
    $ h& t6 ^$ k' z& L0 q; s) }+ N4 y/ [
  40.     else if (*string == '- O4 m8 _; \8 u* a& J6 S0 G
  41. [b][size=3]六、其他常用的 Letter Shell 函数[/size][/b]
    % u; o! X) X& ?2 c4 W
  42. [b]6.1 shellWriteString[/b]
    * K6 e& @( B6 f/ z8 t: d& T  ^1 J
  43. 函数原型如下,这个功能就是 printf 。这个应该很好理解,就不细说了。% ]$ W6 ?) f5 w

  44. ( S7 W, s4 Y! D9 j$ w$ `
  45. [code]/**, A# y  k+ l% l
  46. * @brief shell 写字符串* q1 M7 {/ u5 |9 P
  47. * - |$ p. O# r: o. o+ X. Z3 s
  48. * @param shell shell对象; ?* P+ c" e; _  v
  49. * @param string 字符串数据
    : \$ J# f& h/ u; q
  50. * & y$ R+ g% b3 R8 J7 U' h9 m7 U+ a
  51. * @return unsigned short 写入字符的数量* R/ L* z; K) n5 {' t9 c
  52. */% T8 Y7 }4 G; I8 c4 @, x+ u
  53. 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
  1. /**4 k4 I% {% k& s5 f) h# }% t
  2. * @brief 获取当前活动shell4 O& K$ b" T. W) ?0 R* F
  3. *
    . x& _  Y4 d2 p- B. x
  4. * @return Shell* 当前活动shell对象
    + C) z  e6 I2 ]3 F* E6 J+ `
  5. */
    ' J7 m6 ?; p; l5 V/ }. ?4 L
  6. 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
  1. 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
  1. /**
    7 X, z0 M) U( W; d, ]1 m6 p/ B
  2. * @brief 重命名/移动文件或子目录% l' x% E- s- C- X9 Z
  3. */# o% m( ?( ^" I
  4. void shell_mv(char *old_path,char *new_path)" i4 Z' u) _$ G4 T1 [+ s- s1 W6 R
  5. {* y, X! h. L- _+ E
  6.   FRESULT res;8 b3 D5 k" K8 @1 \4 L/ W$ y
  7. 8 p' A% [. \3 A8 @
  8.   shell_active = shellGetCurrent();   //Get the currently active shelll
    3 n% O- O3 A/ Q! n
  9.   res = f_rename(old_path, new_path);
    & p( |0 Y3 [9 i! O4 j+ \; s$ A, W
  10.   if(res != FR_OK)$ L  }" r/ R$ T
  11.   {
      T8 N* ^7 H( a5 G
  12.     shellWriteString(shell_active,"Operation failed! Error Code:");+ x5 c8 U/ k$ T" I; \
  13.     shellPrint(shell_active,"%d\r\n",res);0 ?/ }+ Y) @6 o1 J6 A2 \
  14.   }
    , D7 ~# }) Y1 |% j8 t' S" z9 Q  s/ e
  15. }
    , b7 ^2 V8 ]" S' \2 T* A! H) F; x' d
  16. 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
  1. #if SHELL_PRINT_BUFFER > 0
    1 p. x: }1 E  g% \8 H9 d5 j+ B
  2. /*** e* u1 J9 h( ~5 {5 z+ F
  3. * @brief shell格式化输出
    8 h& i/ M: V; ?. F# d
  4. *
    ( W9 T* Q+ p( e4 D, y
  5. * @param shell shell对象6 B6 S" h( r7 {% X8 s0 I) |- Q
  6. * @param fmt 格式化字符串
    # r8 H" f* ~2 f, ^& F, B7 E
  7. * @param ... 参数
    & c' C% K' G3 @4 r/ w) u
  8. */! c1 n8 [! W2 p# G* i
  9. void shellPrint(Shell *shell, char *fmt, ...)( X- ?, n- ]3 \( |; V: T+ I
  10. {
    - ?' U- C/ ?4 T: ?
  11.     char buffer[SHELL_PRINT_BUFFER];
    - i' h- a! T. w' ~- w# \
  12.     va_list vargs;5 l1 R/ |6 D+ j

  13. 4 `" V& E' @* i5 L! R0 w
  14.     SHELL_ASSERT(shell, return);
    7 k. {% W0 _2 r, }# z* L: \

  15. . I3 a5 V' {% [# g1 s/ c& u9 I! b
  16.     va_start(vargs, fmt);
    ; Z! C2 u$ `- ]5 @" n- r5 k
  17.     vsnprintf(buffer, SHELL_PRINT_BUFFER - 1, fmt, vargs);9 H5 v# R3 L4 k: t
  18.     va_end(vargs);9 z! {, o% u. w% i) V4 S* b
  19. # t+ p3 W; L; z: L$ d& J! k3 j$ h
  20.     shellWriteString(shell, buffer);
    + i3 w" b1 `& G+ X, V! U) v) @( E
  21. }
    7 F1 M" e! o! u3 F% H) t: ~- P+ u
  22. #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
  1. /**
    ! |' M' x& B+ p& @
  2. * @brief shell 写字符串- r' M8 t9 n7 u4 m. T' K4 n
  3. *
    - d: j" d9 K: d% r, S
  4. * @param shell shell对象% a4 S& U1 q; x$ |% E1 i# r; Y
  5. * @param string 字符串数据
    + A/ u$ R) E3 ~0 Q' t0 D8 a3 p
  6. *
    2 M! v3 k( Y  o2 p
  7. * @return unsigned short 写入字符的数量
    9 g8 o* E2 Z" N
  8. */( O! H/ ~9 X. Z" i+ [
  9. 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
  1. /**
    6 P/ |1 r# [, k/ G1 M0 B9 c
  2. * @brief 获取当前活动shell$ [- i8 z7 F) E& b
  3. *
    6 e& ], V9 [5 Q- H( o; D9 M% I0 Z+ T
  4. * @return Shell* 当前活动shell对象* ]6 @/ h+ U1 X: F+ Y: q' U
  5. */
    # n3 s& J8 J, K& W$ T' Z0 w! q
  6. 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
  1. Shell *shell_active;&nbsp; &nbsp;&nbsp; &nbsp;//当前活动的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
  1. /**% U# B. a% q  l& |2 \0 Z4 T
  2. * @brief 重命名/移动文件或子目录# b. a0 D3 O6 l! O
  3. */  W; v9 _" t+ k( p# ^4 q, ?8 o
  4. void shell_mv(char *old_path,char *new_path)4 m) U/ i7 z% t
  5. {
    : K! w7 k" |# J5 i
  6. &nbsp;&nbsp;FRESULT res;
    7 L/ K0 i4 q) W% Y" a& l( \5 @+ {
  7. ; M2 ]$ {0 p' J! i0 _
  8. &nbsp;&nbsp;shell_active = shellGetCurrent();&nbsp; &nbsp;//Get the currently active shelll* e% H" }3 h: P5 B& c+ _
  9. &nbsp;&nbsp;res = f_rename(old_path, new_path);
    5 p2 `5 l" S+ `% N7 u* b5 u, u
  10. &nbsp;&nbsp;if(res != FR_OK); g. Z& }( h; O6 f& M; Z
  11. &nbsp;&nbsp;{3 Q- M* |, `" P, d, s1 l
  12. &nbsp; &nbsp; shellWriteString(shell_active,"Operation failed! Error Code:");: n( C& G% p6 `$ z9 J0 p
  13. &nbsp; &nbsp; shellPrint(shell_active,"%d\r\n",res);
    - y0 r% T; Y& y5 T% G5 r* [/ d
  14. &nbsp;&nbsp;}
    2 @! s1 z. T0 z+ l' v8 E5 L9 o
  15. }
    & ]8 G$ q" }0 c1 G+ ]5 c. M3 p
  16. 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  ?
  1. #if SHELL_PRINT_BUFFER &gt; 0
    / O! C; z) w# r3 A
  2. /**
    0 ~2 j0 R: ~. M( o3 t1 n/ O. ~3 o$ U
  3. * @brief shell格式化输出( D# e: ]$ E6 A4 Z1 `- u
  4. * * @. ]4 q, U0 M8 _$ T
  5. * @param shell shell对象% u% q: o* U, z
  6. * @param fmt 格式化字符串
    / y- f4 Y) P) v" l% v
  7. * @param ... 参数% R8 k& d2 s- \3 u7 P5 p) a
  8. */# Q) {3 c9 O$ W7 M
  9. void shellPrint(Shell *shell, char *fmt, ...)
    / y2 A) y4 l( I0 c* F7 J5 K+ P  J4 o
  10. {: Z% w, Z: L1 m* G1 ~
  11. &nbsp; &nbsp; char buffer[SHELL_PRINT_BUFFER];
    ; W, ~' y' g; r' T$ g
  12. &nbsp; &nbsp; va_list vargs;
    % H  l2 `# S  o

  13. % i: {9 L% E  l) S& K5 B
  14. &nbsp; &nbsp; SHELL_ASSERT(shell, return);# S' V# z7 q* C* K5 V. q, k
  15. 4 Y0 ~3 c2 [" d  P, U5 N
  16. &nbsp; &nbsp; va_start(vargs, fmt);
    ) Z" b0 E1 I* ~3 v1 P+ W
  17. &nbsp; &nbsp; vsnprintf(buffer, SHELL_PRINT_BUFFER - 1, fmt, vargs);* E& c8 L# f  F3 [  I- c, `
  18. &nbsp; &nbsp; va_end(vargs);9 D( L$ c+ g) t& e

  19. 3 K- b8 p; p/ k  t
  20. &nbsp; &nbsp; shellWriteString(shell, buffer);
    " H& n4 I0 j: L+ x% Q
  21. }  ~7 A5 m9 O( h- ~$ G
  22. #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
R`)~GN97DP3}]5R[IQUN}XE.png
收藏 评论0 发布时间:2022-8-23 16:13

举报

0个回答

所属标签

相似分享

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版