[分享] MCU调试大法:使用串口实现简单shell功能

Jacktang   2019-11-6 14:06 楼主

MCU程序调试方法有很多,比如软/硬件仿真、添加数据打印等。
像Keil MDK就支持不少单片机的软件仿真,在没有拿到单片机的情况下,就可以先仿真调试部分功能,查看代码逻辑是否正确。硬件仿真则需要借助仿真器,如调试Cortex内核MCU常用的J-Link/ST-Link等。通过watch窗口可以查看变量的值:

在代码中添加数据的打印,则需要借助MCU的串口功能,将运行时的关键数据通过串口打印至PC,便于观察。这是我调试时非常喜欢使用的一个功能,因为需要打印哪些数据完全自主可控,而且可以做到基本不影响程序正常运行。
这里顺便把如何使用printf的方法讲一下,比较简单,会的同学可以直接略过:


 
  1. /@@* 头文件不能少 */
  2. #include <stdio.h>
  3.  
  4. /@@* 平台的选择 */
  5. #ifdef __GNUC__
  6. #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
  7. #else
  8. #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
  9. #endif /@@* __GNUC__ */
  10.  
  11. PUTCHAR_PROTOTYPE
  12. {
  13. /@@* 这里只需要实现一个字符ch的发送即可,以下以ST为例 */
  14. HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);
  15. return ch;
  16. }

可交互的调试方法—shell

有了串口数据打印,寻找BUG方便了不少;但是随着使用场景的增多:比如我需要在某个时刻打印某些数据、需要控制程序进入某个分支、调试算法时需要经常修改某些变量的值。此时光有打印就不行了,我需要一个可以实时和MCU进行交互的系统,那就是shell。
这里介绍一个体积极小的嵌入式shell,功能如下:

  • 命令自动补全,使用tab键补全命令
  • 命令长帮助,使用help [command]显示命令长帮助
  • 长帮助补全,输入命令后双击tab键补全命令长帮助指令
  • 快捷键,支持使用Ctrl + A~Z组合按键直接调用函数
  • shell变量,支持在shell中查看和修改变量值,支持变量作为命令参数

开始移植

1. 下载源码并添加至工程中:

 

360截图20191106140620843.jpg

360截图20191106140437649.jpg
算上h文件,也就5个。

2. 初始化shell

定义shell全局实体:


 
  1. SHELL_TypeDef shell;

在main中初始化,这里需要提供write函数,即字符发送函数:


 
  1. /@@* 初始化shell */
  2. shell.read = NULL; //采用中断方式,所以不需要提供read方法
  3. shell.write = user_shellWrite;
  4. shellInit(&shell);
  5.  
  6. /@@* shell write定义 */
  7. void user_shellWrite(const char ch)
  8. {
  9. /@@* 实现一个字符ch的发送功能,使用阻塞方式发送 */
  10. HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);
  11. }

3. shell调用

在串口接收中断中,调用shellHandler处理函数:


 
  1. void USART1_IRQHandler(void)
  2. {
  3. HAL_UART_IRQHandler(&huart1);
  4. //将收到的字符实时送入shell处理
  5. shellHandler(&shell, uart1_it_buf);
  6. HAL_UART_Receive_IT(&huart1, (uint8_t *)&uart1_it_buf, 1);
  7. }

4. 配置

将SHELL_DISPLAY_RETURN关闭,不然会打印shell函数返回值;SHELL_USING_CMD_EXPORT则根据个人喜好来,我这里将其关闭,所以以下将使用命令表来添加命令:


 
  1. /@@**
  2. *  是否显示命令调用函数返回值
  3. * 使能此宏,则每次调用shell命令之后会以整形和十六进制的方式打印函数的返回值
  4. */
  5. #define SHELL_DISPLAY_RETURN 0
  6.  
  7. /@@**
  8. * @brief 是否使用命令导出方式
  9. * 使能此宏后,可以使用`SHELL_EXPORT_CMD()`或者`SHELL_EXPORT_CMD_EX()`
  10. * 定义shell命令,关闭此宏的情况下,需要使用命令表的方式
  11. */
  12. #define SHELL_USING_CMD_EXPORT 0

5. 添加命令

因为定义了SHELL_USING_CMD_EXPORT为0,所以我们使用命令表来添加命令。在shell.c中,这里我们可以看到默认实现了两个命令help和cls:


 
  1. const SHELL_CommandTypeDef shellDefaultCommandList[] =
  2. {
  3. SHELL_CMD_ITEM_EX(help, shellHelp, command help, help [command] --show help info of command),
  4. SHELL_CMD_ITEM(cls, shellClear, clear command line),
  5.  
  6. /@@* 在这里按照格式添加自己的命令,如显示版本 */
  7. SHELL_CMD_ITEM(version, shell_showVersion, show current version),
  8. };

shell_showVersion的实现,可以在其他C文件中实现


 
  1. /@@**
  2. * @brief shell显示当前软件版本
  3. *
  4. */
  5. void shell_showVersion(void)
  6. {
  7. SHELL_TypeDef *shell = shellGetCurrent();
  8. if (!shell)
  9. {
  10. return;
  11. }
  12. shellDisplay(shell, "\r\n V1.0.0\r\n");
  13. shellDisplay(shell, "\r\n Build: "__DATE__" "__TIME__"\r\n");
  14.  
  15. return;
  16. }

6. run

实际效果如下:

按TAB可以显示所有命令,在输入命令时按TAB还可以自动补全。

其他

这里只是完成了最基础的移植工作,还有一些高级的功能就等着大家自行摸索啦。
有了shell调试起来肯定如虎添翼,呼呼哈哈!
还有一点,给客户演示demo的时候,逼格也高了很多,哈哈哈!

回复评论 (2)

不错的方法

 

点赞  2019-11-6 16:58

谢谢,分享。

点赞  2019-11-7 10:53
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复