1.概述
在计算机科学中,Shell俗称壳(用来区别于核),是指为使用者提供操作界面的软件(命令解析器)。它类似于DOS下的command和后来的cmd.exe。它接收用户命令,然后调用相应的应用程序。如果在MCU开发调试过程中有一个Shell命令解析器,通过串口调试软件输入一些命令然后运行相对应的调试函数,将会为开发调试提供极大的便利和帮助。
目前有很多开源的应用在MCU上的Shell代码,本文主要介绍了基于GD32VF103V-EVAL硬件平台上如何移植Letter-shell,以及实现基于Letter-shell来辅助开发调试。Letter-shell可以到下载中心下载:https://mbb.eet-china.com/download/36059.html,当然也可以到GitHub上下载:https://github.com/NevermindZZT/letter-shell(速度有时候相当慢...)
2.移植前的准备
2.1.基于MCU的Shell是通过串口的数据交互来实现的,我们基于上次新建的工程,使用USART0来作为Shell的通讯串口,所以我们需要对串口的初始化做一些修改:初始化串口、配置串口中断、使能串口接收中断,代码如下:
- /* gd32vf103v_eval.c */
- /*!
- \brief configure COM port
- \param[in] com: COM on the board
- \arg EVAL_COM0: COM0 on the board
- \arg EVAL_COM1: COM1 on the board
- \param[out] none
- \retval none
- */
- void gd_eval_com_init(uint32_t com)
- {
- uint32_t com_id = 0U;
- if(EVAL_COM0 == com){
- com_id = 0U;
- }else if(EVAL_COM1 == com){
- com_id = 1U;
- }
- /* USART interrupt configuration */
- eclic_global_interrupt_enable();
- eclic_priority_group_set(ECLIC_PRIGROUP_LEVEL3_PRIO1);
- eclic_irq_enable(USART0_IRQn, 1, 0);
- /* enable GPIO clock */
- rcu_periph_clock_enable(COM_GPIO_CLK[com_id]);
- /* enable USART clock */
- rcu_periph_clock_enable(COM_CLK[com_id]);
- /* connect port to USARTx_Tx */
- gpio_init(COM_GPIO_PORT[com_id], GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, COM_TX_PIN[com_id]);
- /* connect port to USARTx_Rx */
- gpio_init(COM_GPIO_PORT[com_id], GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, COM_RX_PIN[com_id]);
- /* USART configure */
- usart_deinit(com);
- usart_baudrate_set(com, 115200U);
- usart_word_length_set(com, USART_WL_8BIT);
- usart_stop_bit_set(com, USART_STB_1BIT);
- usart_parity_config(com, USART_PM_NONE);
- usart_hardware_flow_rts_config(com, USART_RTS_DISABLE);
- usart_hardware_flow_cts_config(com, USART_CTS_DISABLE);
- usart_receive_config(com, USART_RECEIVE_ENABLE);
- usart_transmit_config(com, USART_TRANSMIT_ENABLE);
- usart_enable(com);
- /* enable USART RBNE interrupt */
- usart_interrupt_enable(USART0, USART_INT_RBNE);
- }
- /*!
- \brief com send data
- \param[in] com: COM on the board
- \arg EVAL_COM0: COM0 on the board
- \arg EVAL_COM1: COM1 on the board
- \arg data
- \param[out] none
- \retval none
- */
- void gd_eval_com_send(uint32_t com, uint8_t data)
- {
- switch(com)
- {
- case EVAL_COM0:
- usart_data_transmit(USART0, data);
- while(usart_flag_get(USART0, USART_FLAG_TBE) == RESET){
- }
- break;
- case EVAL_COM1:
- usart_data_transmit(USART1, data);
- while(usart_flag_get(USART1, USART_FLAG_TBE) == RESET){
- }
- break;
- default:
- break;
- }
- }
2.2.在工程中我们添加了queue消息队列来处理从USART0中断接收到的数据,关于消息队列的实现可以参考queue.c和queue.h两个文件,USART0中断处理代码如下:
- /* gd32vf103v_it.c */
- #include "gd32vf103_it.h"
- #include "gd32vf103v_eval.h"
- #include "queue.h"
- /*!
- \brief this function handles USART RBNE interrupt request and TBE interrupt request
- \param[in] none
- \param[out] none
- \retval none
- */
- void USART0_IRQHandler(void)
- {
- if(RESET != usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE)){
- /* receive data */
- QUEUE_WRITE(QUEUE_DBG_RX_IDX, usart_data_receive(USART0));
- }
- }
2.3.在main.c中调用串口初始化函数及消息初始化函数,在while(1)中去调用消息处理函数,代码如下所示:
- /* main.c */
- #include "gd32vf103.h"
- #include "gd32vf103v_eval.h"
- #include "message.h"
- #include "systick.h"
- #include <stdio.h>
- void xInitSystem(void)
- {
- gd_eval_led_init(LED1); gd_eval_led_off(LED1);
- gd_eval_led_init(LED2); gd_eval_led_off(LED2);
- gd_eval_led_init(LED3); gd_eval_led_off(LED3);
- gd_eval_led_init(LED4); gd_eval_led_off(LED4);
- gd_eval_com_init(EVAL_COM0);
- printf("GD32VF103V-EVAL(RISC-V)\r\n");
- MSG_Init();
- }
- /*!
- \brief main function
- \param[in] none
- \param[out] none
- \retval none
- */
- int main(void)
- {
- xInitSystem();
- while(1){
- MSG_Handler();
- }
- }
2.4.在message.c中实现串口输出函数、消息初始化函数和消息处理函数,最终达到的功能是:程序运行后,MCU的USART0打印输出通过串口工具输入给MCU的数据,至此移植前的准备就完成了。代码如下所示:
- #include "message.h"
- #include "queue.h"
- /**
- * @brief
- * @param None
- * @retval None
- */
- void SerialPutByte(const char ch)
- {
- gd_eval_com_send(EVAL_COM0, ch);
- }
- /**
- * @brief
- * @param None
- * @retval None
- */
- void MSG_Init(void)
- {
- QUEUE_INIT(QUEUE_DBG_RX_IDX);
- }
- /**
- * @brief
- * @param None
- * @retval None
- */
- void MSG_Handler(void)
- {
- if(QUEUE_EMPTY(QUEUE_DBG_RX_IDX) == 0)
- {
- SerialPutByte(QUEUE_READ(QUEUE_DBG_RX_IDX));
- }
- }
3.移植Letter-shell
3.1.Letter-shell是一个体积极小的嵌入式shell,功能如下:
- 命令自动补全,使用tab键补全命令
- 命令长帮助,使用help [command]显示命令长帮助
- 长帮助补全,输入命令后双击tab键补全命令长帮助指令
- 快捷键,支持使用Ctrl + A~Z组合按键直接调用函数
- shell变量,支持在shell中查看和修改变量值,支持变量作为命令参数
- 登录密码,支持在shell中使用登录密码,支持超时自动锁定
3.2.在message.c中定义shell对象
- SHELL_TypeDef shell;
3.3.调用shellInit进行初始化
- void MSG_Init(void)
- {
- QUEUE_INIT(QUEUE_DBG_RX_IDX);
- shell.write = SerialPutByte;
- shellInit(&shell);
- }
3.4.我们使用的是裸机开发环境,所以在接收到数据时,调用shellHandler就可以了
- void MSG_Handler(void)
- {
- if(QUEUE_EMPTY(QUEUE_DBG_RX_IDX) == 0)
- {
- shellHandler(&shell, QUEUE_READ(QUEUE_DBG_RX_IDX));
- }
- }
3.5.接下来在shell_cfg.h中需要对shell做一些配置
- #ifndef __SHELL_CFG_H__
- #define __SHELL_CFG_H__
- #define SHELL_USING_TASK 0
- #define SHELL_TASK_WHILE 1
- #define SHELL_USING_CMD_EXPORT 0
- #define SHELL_USING_VAR 0
- #define SHELL_DISPLAY_RETURN 0
- #define SHELL_AUTO_PRASE 1
- #define SHELL_LONG_HELP 1
- #define SHELL_COMMAND_MAX_LENGTH 50
- #define SHELL_PARAMETER_MAX_NUMBER 8
- #define SHELL_HISTORY_MAX_NUMBER 5
- #define SHELL_DOUBLE_CLICK_TIME 200
- #define SHELL_MAX_NUMBER 5
- #define SHELL_PRINT_BUFFER 128
- #define SHELL_GET_TICK() 0
- #define SHELL_DEFAULT_COMMAND "\r\nMBB & GD32 RISC-V >>"
- #define SHELL_USING_AUTH 0
- #define SHELL_USER_PASSWORD "letter"
- #define SHELL_LOCK_TIMEOUT 5 * 60 * 1000
- #endif
3.6.修改shell.c中的TEXT_INFO信息如下
- static const char *shellText[] =
- {
- [TEXT_INFO] = "\r\n\r\n"
- "***********************************************************\r\n"
- "* (C) COPYRIGHT 2019 *\r\n"
- "* MBB & GD32 RISC-V v"SHELL_VERSION" *\r\n"
- "* Build: "__DATE__" "__TIME__" *\r\n"
- "***********************************************************\r\n",
- [TEXT_PWD_HINT] = "\r\nPlease input password:",
- [TEXT_PWD_RIGHT] = "\r\npassword confirm success.\r\n",
- [TEXT_PWD_ERROR] = "\r\npassword confirm failed.\r\n",
- [TEXT_FUN_LIST] = "\r\nCOMMAND LIST:\r\n\r\n",
- [TEXT_VAR_LIST] = "\r\nVARIABLE LIST:\r\n\r\n",
- [TEXT_CMD_NONE] = "Command not found\r\n",
- [TEXT_CMD_TOO_LONG] = "\r\nWarnig: Command is too long\r\n",
- [TEXT_READ_NOT_DEF] = "error: shell.read must be defined\r\n",
- };
3.7.在shell.h中将SHELL_TypeDef结构体当中的status修改为不使用位域的定义方式,在KEIL中支持位域操作,但在NucleiStudio IDE中不支持,也正是这个问题,挖了半天坑才搞出来^^,定义如下:
- typedef struct
- {
- char *command;
- char buffer[SHELL_COMMAND_MAX_LENGTH];
- unsigned short length;
- unsigned short cursor;
- char *param[SHELL_PARAMETER_MAX_NUMBER];
- char history[SHELL_HISTORY_MAX_NUMBER][SHELL_COMMAND_MAX_LENGTH];
- unsigned short historyCount;
- short historyFlag;
- short historyOffset;
- SHELL_CommandTypeDef *commandBase;
- unsigned short commandNumber;
- #if SHELL_USING_VAR == 1
- SHELL_VaribaleTypeDef *variableBase;
- unsigned short variableNumber;
- #endif
- int keyFuncBase;
- unsigned short keyFuncNumber;
- struct
- {
- char inputMode;
- char tabFlag;
- char authFlag;
- } status;
- unsigned char isActive;
- shellRead read;
- shellWrite write;
- #if SHELL_LONG_HELP == 1 || (SHELL_USING_AUTH && SHELL_LOCK_TIMEOUT > 0)
- int activeTime;
- #endif
- }SHELL_TypeDef;
3.8.至此Letter-shell就移植完成了,程序编译烧录后,就可以看到最小配置的shell输出了,可以通过输入help查看,当然直接按下TAB按键也可以看到支持的shell命令列表。
4.Letter-shell应用
4.1.在message.c中通过参数的传递实现对所有LED控制的函数
- void LED_TurnOnOFF(int idx, int en)
- {
- switch(idx)
- {
- case 1:
- if(en) gd_eval_led_on(LED1);
- else gd_eval_led_off(LED1);
- break;
- case 2:
- if(en) gd_eval_led_on(LED2);
- else gd_eval_led_off(LED2);
- break;
- case 3:
- if(en) gd_eval_led_on(LED3);
- else gd_eval_led_off(LED3);
- break;
- case 4:
- if(en) gd_eval_led_on(LED4);
- else gd_eval_led_off(LED4);
- break;
- default:
- break;
- }
- }
4.2.在shell.c中将LED_TurnOnOFF添加到shellDefaultCommandList命令列表当中去
- #if SHELL_USING_CMD_EXPORT != 1
- extern void LED_TurnOnOFF(int idx, int en);
- const SHELL_CommandTypeDef shellDefaultCommandList[] =
- {
- SHELL_CMD_ITEM_EX(help, shellHelp, command help, help [command] --show help info of command),
- #if SHELL_USING_VAR == 1
- SHELL_CMD_ITEM(vars, shellListVariables, show vars),
- SHELL_CMD_ITEM_EX(setVar, shellSetVariable, set var, setVar $[var] [value]),
- #endif /** SHELL_USING_VAR == 1 */
- SHELL_CMD_ITEM(cls, shellClear, clear command line),
- SHELL_CMD_ITEM(led, LED_TurnOnOFF, turn LEDn on or off),
- };
- #if SHELL_USING_VAR == 1
- const SHELL_VaribaleTypeDef shellDefaultVariableList[] =
- {
- };
- #endif /** SHELL_USING_VAR == 1 */
- #endif /** SHELL_USING_CMD_EXPORT != 1 */
4.3.至此一个控制LED的shell应用就完成了,我们可以在串口调试助手的界面输入"led 1 1"来点亮LED1,又或者输入“led 1 0”来关闭LED1
5.工程代码