MCU程序调试方法有很多,比如软/硬件仿真、添加数据打印等。
像Keil MDK就支持不少单片机的软件仿真,在没有拿到单片机的情况下,就可以先仿真调试部分功能,查看代码逻辑是否正确。硬件仿真则需要借助仿真器,如调试Cortex内核MCU常用的J-Link/ST-Link等。通过watch窗口可以查看变量的值:
在代码中添加数据的打印,则需要借助MCU的串口功能,将运行时的关键数据通过串口打印至PC,便于观察。这是我调试时非常喜欢使用的一个功能,因为需要打印哪些数据完全自主可控,而且可以做到基本不影响程序正常运行。
这里顺便把如何使用printf的方法讲一下,比较简单,会的同学可以直接略过:
/@@* 头文件不能少 */
#include <stdio.h>
/@@* 平台的选择 */
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /@@* __GNUC__ */
PUTCHAR_PROTOTYPE
{
/@@* 这里只需要实现一个字符ch的发送即可,以下以ST为例 */
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);
return ch;
}
有了串口数据打印,寻找BUG方便了不少;但是随着使用场景的增多:比如我需要在某个时刻打印某些数据、需要控制程序进入某个分支、调试算法时需要经常修改某些变量的值。此时光有打印就不行了,我需要一个可以实时和MCU进行交互的系统,那就是shell。
这里介绍一个体积极小的嵌入式shell,功能如下:
定义shell全局实体:
SHELL_TypeDef shell;
在main中初始化,这里需要提供write函数,即字符发送函数:
/@@* 初始化shell */
shell.read = NULL; //采用中断方式,所以不需要提供read方法
shell.write = user_shellWrite;
shellInit(&shell);
/@@* shell write定义 */
void user_shellWrite(const char ch)
{
/@@* 实现一个字符ch的发送功能,使用阻塞方式发送 */
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);
}
在串口接收中断中,调用shellHandler处理函数:
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart1);
//将收到的字符实时送入shell处理
shellHandler(&shell, uart1_it_buf);
HAL_UART_Receive_IT(&huart1, (uint8_t *)&uart1_it_buf, 1);
}
将SHELL_DISPLAY_RETURN关闭,不然会打印shell函数返回值;SHELL_USING_CMD_EXPORT则根据个人喜好来,我这里将其关闭,所以以下将使用命令表来添加命令:
/@@**
* 是否显示命令调用函数返回值
* 使能此宏,则每次调用shell命令之后会以整形和十六进制的方式打印函数的返回值
*/
#define SHELL_DISPLAY_RETURN 0
/@@**
* @brief 是否使用命令导出方式
* 使能此宏后,可以使用`SHELL_EXPORT_CMD()`或者`SHELL_EXPORT_CMD_EX()`
* 定义shell命令,关闭此宏的情况下,需要使用命令表的方式
*/
#define SHELL_USING_CMD_EXPORT 0
因为定义了SHELL_USING_CMD_EXPORT为0,所以我们使用命令表来添加命令。在shell.c中,这里我们可以看到默认实现了两个命令help和cls:
const SHELL_CommandTypeDef shellDefaultCommandList[] =
{
SHELL_CMD_ITEM_EX(help, shellHelp, command help, help [command] --show help info of command),
SHELL_CMD_ITEM(cls, shellClear, clear command line),
/@@* 在这里按照格式添加自己的命令,如显示版本 */
SHELL_CMD_ITEM(version, shell_showVersion, show current version),
};
shell_showVersion的实现,可以在其他C文件中实现
/@@**
* @brief shell显示当前软件版本
*
*/
void shell_showVersion(void)
{
SHELL_TypeDef *shell = shellGetCurrent();
if (!shell)
{
return;
}
shellDisplay(shell, "\r\n V1.0.0\r\n");
shellDisplay(shell, "\r\n Build: "__DATE__" "__TIME__"\r\n");
return;
}
实际效果如下:
按TAB可以显示所有命令,在输入命令时按TAB还可以自动补全。
这里只是完成了最基础的移植工作,还有一些高级的功能就等着大家自行摸索啦。
有了shell调试起来肯定如虎添翼,呼呼哈哈!
还有一点,给客户演示demo的时候,逼格也高了很多,哈哈哈!
谢谢,分享。