项目简介

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来辅助开发调试。

开箱自拍


http://www.gd32mcu.com/data/news_img/20210128/16118014902634.jpg

http://www.gd32mcu.com/data/news_img/20210128/16118014926225.jpg


项目详情

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,最后将快捷方式放在桌面上,方便后面的使用^^),软件默认打开的界面如下图所示:

http://www.gd32mcu.com/data/news_img/20210128/16118015303657.jpg

1.3.接下来我们可以参考《GD32V103 MCU工具链和应用开发》一步步的来创建自己的第一个工程(后面所有的工程也都是基于这个工程来做开发的)。点击菜单栏File->New->C/C++ Project

http://www.gd32mcu.com/data/news_img/20210128/16118015479509.jpg

1.4.在弹出的New C/C++ Project对话框中选择C Managed Build选项,然后点击Next

http://www.gd32mcu.com/data/news_img/20210128/16118015605048.jpg

1.5.在弹出的C Project对话框中,输入Proejct Name,选择项目文件存放的路径Location,(如果不选择,则存放在默认的工作路径下面),选择Project Type为Executable项下的GigaDevice RISC-V C Project,Toolchains为RISC-V Cross GCC,点击Next

http://www.gd32mcu.com/data/news_img/20210128/16118015731212.jpg

1.6.在弹出的Project settings对话框中,选择MCU型号:GD32V103,点击Next

http://www.gd32mcu.com/data/news_img/20210128/16118015856396.jpg

1.7.在弹出的Project settings对话框中,使用默认选项,点击Next

http://www.gd32mcu.com/data/news_img/20210128/16118015978113.jpg

1.8.在弹出的Select Configurations对话框中,点击Select all,点击Next

http://www.gd32mcu.com/data/news_img/20210128/16118016116241.jpg

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

http://www.gd32mcu.com/data/news_img/20210128/16118016371089.jpg

1.10.至此一个基于Running_led的一个最小工程就创建完毕了,在点击Finish后需要稍等片刻,等待工程创建完成;创建后我工程目录结构如下图所示:

http://www.gd32mcu.com/data/news_img/20210128/16118016523522.jpg

1.11.我们现在点击NucleiStudio IDE左上角像锤子的图标,对新建立的工程进行编译,并输出编译结果,新建的工程是0 errors,0 warnings

http://www.gd32mcu.com/data/news_img/20210128/16118016641761.jpg

1.12.接下来我们就通过点击NucleiStudio IDE左上角的第二个绿色图标,将程序通过板载的GD-LINK下载到单板运行啦(刚点击下载,弹出的是红色的字体,吓了一跳,以为是出错了呢,到最后单板正确运行(4个LED灯依次间隔1秒钟点亮),才松了口气;想当然的以为红色就是报错了^^)

http://www.gd32mcu.com/data/news_img/20210128/16118016879313.jpg

第一个工程创建下来感觉很舒服,向导一步接一步的,容易上手;生成的工作目录划分清楚,代码清晰。

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。

http://www.gd32mcu.com/data/news_img/20210128/16118017439284.jpg