项目简介
GD32VF103V-EVAL板采用基于RISC-V内核的GD32VF103VBT6 MCU,GD32VF103系列是全球首个基于RISC-V内核的32位通用MCU产品,主频高达108MHz,Flash高达128K,SRAM高达32K,片内集成种类丰富、数量众多的外设,提供工业级的操作温度(-40℃~85℃),面向“主流型开发需求”,具备“完整的兼容性”。GD32V系(RISC-V内核)和经典的GD32系列(ARM内核)之间建立了快速通道,并搭建ARM和RISC-V的桥梁,可以实现跨内核的产品选型和设计切换,在节省开发成本的同时,给用户带来相同的使用体验。
在计算机科学中,Shell俗称壳(用来区别于核),是指为使用者提供操作界面的软件(命令解析器)。它类似于DOS下的command和后来的cmd.exe。它接收用户命令,然后调用相应的应用程序。如果在MCU开发调试过程中有一个Shell命令解析器,通过串口调试软件输入一些命令然后运行相对应的调试函数,将会为开发调试提供极大的便利和帮助。
目前有很多开源的应用在MCU上的Shell代码,本文主要介绍了基于GD32VF103V-EVAL硬件平台上如何移植Letter-shell,以及实现基于Letter-shell来辅助开发调试。
开箱自拍
项目详情
1.新建工程
1.1.GD32 RISC-V的开发环境使用的是芯来科技的NucleiStudio IDE,下载链接:https://www.nucleisys.com/download.php,目前只有Windows 64位这一个版本。NucleiStudio IDE是一个免安装的软件,下载后直接解压到某个目录下即可。需要注意的是,NucleiStudio IDE是基于Java环境运行的,所以需要安装JDK运行环境,并正确配置好系统环境变量。
1.2.双击NucleiStudio_IDE_201909NucleiStudio目录下的eclipse.exe运行NucleiStudio IDE,在首次运行的时候需要选择一个默认的工作路径(为了方便操作,我给eclipse.exe创建了一个快捷方式,重命名为NucleiStudio IDE,并将快捷方式的图片修改为NucleiStudio.ico,最后将快捷方式放在桌面上,方便后面的使用^^),软件默认打开的界面如下图所示:
1.3.接下来我们可以参考《GD32V103 MCU工具链和应用开发》一步步的来创建自己的第一个工程(后面所有的工程也都是基于这个工程来做开发的)。点击菜单栏File->New->C/C++ Project
1.4.在弹出的New C/C++ Project对话框中选择C Managed Build选项,然后点击Next
1.5.在弹出的C Project对话框中,输入Proejct Name,选择项目文件存放的路径Location,(如果不选择,则存放在默认的工作路径下面),选择Project Type为Executable项下的GigaDevice RISC-V C Project,Toolchains为RISC-V Cross GCC,点击Next
1.6.在弹出的Project settings对话框中,选择MCU型号:GD32V103,点击Next
1.7.在弹出的Project settings对话框中,使用默认选项,点击Next
1.8.在弹出的Select Configurations对话框中,点击Select all,点击Next
1.9.在弹出的GNU RISC-V Cross Toolchain对话框中,使用默认选项,Toochain name:GNU MCU RISC-V GCC(riscv-none-embed-gcc),Toolchain path:${eclipse_home}toolchainRISC-V Embedded GCCbin,点击Finish
1.10.至此一个基于Running_led的一个最小工程就创建完毕了,在点击Finish后需要稍等片刻,等待工程创建完成;创建后我工程目录结构如下图所示:
1.11.我们现在点击NucleiStudio IDE左上角像锤子的图标,对新建立的工程进行编译,并输出编译结果,新建的工程是0 errors,0 warnings
1.12.接下来我们就通过点击NucleiStudio IDE左上角的第二个绿色图标,将程序通过板载的GD-LINK下载到单板运行啦(刚点击下载,弹出的是红色的字体,吓了一跳,以为是出错了呢,到最后单板正确运行(4个LED灯依次间隔1秒钟点亮),才松了口气;想当然的以为红色就是报错了^^)
第一个工程创建下来感觉很舒服,向导一步接一步的,容易上手;生成的工作目录划分清楚,代码清晰。
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
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)rn");
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 "rnMBB & 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] = "rnrn"
"***********************************************************rn"
"* (C) COPYRIGHT 2019 *rn"
"* MBB & GD32 RISC-V v"SHELL_VERSION" *rn"
"* Build: "__DATE__" "__TIME__" *rn"
"***********************************************************rn",
[TEXT_PWD_HINT] = "rnPlease input password:",
[TEXT_PWD_RIGHT] = "rnpassword confirm success.rn",
[TEXT_PWD_ERROR] = "rnpassword confirm failed.rn",
[TEXT_FUN_LIST] = "rnCOMMAND LIST:rnrn",
[TEXT_VAR_LIST] = "rnVARIABLE LIST:rnrn",
[TEXT_CMD_NONE] = "Command not foundrn",
[TEXT_CMD_TOO_LONG] = "rnWarnig: Command is too longrn",
[TEXT_READ_NOT_DEF] = "error: shell.read must be definedrn",
};
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 */
项目实测效果
至此一个控制LED的shell应用就完成了,我们可以在串口调试助手的界面输入"led 1 1"来点亮LED1,又或者输入“led 1 0”来关闭LED1。