【开坑国产单片机GD32系列,带你零死角玩转GD32】


GD32F103C8T6下的Letter Shell移植(基于KEIL)

(1)背景介绍

我去除了大部分Bug,但我保留了一部分,我觉得保留的这一部分Bug,才会让你知道,程序是靠Bug运行的;

      串口,应该是各位彦祖初学单片机时,最早接触到的一批功能吧?还记得大学刚开始学那会,用的还是郭天祥的51开发板,就连配套资料都是郭天祥老师的,刚学完点灯,我就跳到串口开发章节,之所以这么着急,是因为某位不知名的学长告诉我,用串口可以实现下面这样的效果:
在这里插入图片描述      我当时竟然信了,兴高采烈地去学串口,结果,没想到的是,就在我捣鼓出下面这个玩意之后,我就为我的“年少有为”而感到怀疑:
在这里插入图片描述
      说好的科技感满满呢?就这?先不说这个看起来长得有些狗急跳墙的上位机软件,单就这个乱码,就已经让我幼小的心灵受到了一丝丝震撼,后来我才知道,学长给我描述那幅精致的蓝图,其实是用QT做的。。。。
在这里插入图片描述
      但后续通过对串口的学习,也渐渐意识到了,串口,真的是一个调试利器,各位彦祖试想一下,如果不使用串口,面对一个没有显示屏,没有指示灯,更没有其他能通过人的感官来传递信息的功能,你要如何获取系统此刻的运行状态和运行数据?靠第六感吗?
      再假设,如果有按键,指示灯,蜂鸣器,屏幕这一类的状态显示功能,但是如果设备的工作地点很特殊,比如在某座高山上工作的无人值守气象站,又该如何获取它的运行状态和数据呢?
在这里插入图片描述
      这个时候想必各位彦祖已经知道咋办了,朴实无华的串口,就可以成功应用在这些场景中,没有指示灯?没有屏幕?不要紧!用上串口,电脑的显示屏可比那小小的3.2寸彩屏香多了!手头没有电脑?串口屏了解一下,带串口屏的无线终端,都可以做成手机样式的了,设备工作地点太刁钻?用上串口,连接4G通信模块,就算你在非洲看大猩猩呲牙,你也可以收到远在亚洲大山深处,犄角旮旯里的气象站发给你的今日有雪的提示信息,正是因为串口在硬件上的易扩展性,以及在软件上的兼容性,使得串口在8位,16位,32位MCU上广泛使用,大量支持串口通讯的设备也应用在了诸如消费电子,工业控制等场景中。
在这里插入图片描述

      那照这么说,这个串口简直就是无所不能喽?那为什么日常用起来,却显得有那么返璞归真呢?要么只是打印一下调试数据,要么就是串口输入一些约定好的指令,运行一些简单功能,而且还时不时地提高你的血压。
在这里插入图片描述

(2)问题分析

      在日常调试过程中,串口通常用于和MCU进行少量数据的交互,以及指令和反馈的传输,下面根据不同的场景,我们来依次分析常规串口调试程序的特点。

  • 输出提示信息
          假设要求单片机在接收到串口发来的字符 T 时,返回一个提示信息 The Receive data is T! ,这个功能,相信只要是会串口的彦祖,都能很轻松实现它,接下来我们在GD32F103C8T6上实现这个功能。

    第一步:初始化USART0的时钟和GPIO
          这里采用的是GD32F103C8T6的USART0,对应的就是PA9和PA10两个引脚:
    在这里插入图片描述
          配置代码如下:

/* enable USART clock */
    rcu_periph_clock_enable(RCU_USART0);
    
    #if defined USART0_REMAP															//1
        /* enable GPIO clock */
        rcu_periph_clock_enable(RCU_GPIOB);
        rcu_periph_clock_enable(RCU_AF);
        /* USART0 remapping */
        gpio_pin_remap_config(GPIO_USART0_REMAP, ENABLE);
        /* connect port to USARTx_Tx */
        gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_6);
        /* connect port to USARTx_Rx */
        gpio_init(GPIOB, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_7);
    #else
        /* enable GPIO clock */
        rcu_periph_clock_enable(RCU_GPIOA);												//2
        
        /* connect port to USARTx_Tx */
        gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9);
        /* connect port to USARTx_Rx */
        gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_10);

      这里可能就有彦祖发现了,这段代码好像初始化了4个GPIO,常用的串口不是只有TXD和RXD么?这里其实涉及到的概念,叫做 “重映射” ,即USART0的TXD和RXD在特殊情况下,可以从PA9和PA10切换到PB6和PB7,只需要如 “代码1” 一样,对USART0_REMAP进行宏定义 ,就可以实现,这里不需要,因为在原理图上USART0的正常引脚没有其他特殊用途,所以可以使用代码2对USART0所使用的GPIO进行初始化(这里的#if-else是预编译指令),该代码的流程就是:开启USART0时钟–>开启GPIO时钟–>初始化PA9为推挽模式(复用),PA10为浮空输入模式,这里需要注意的是,RXD引脚如果闲置不用的话,建议关闭,因为浮空输入模式很容易会被噪声干扰,导致帧的错误判断,从而致使MCU死机。
在这里插入图片描述

第二步:配置USART0诸如波特率,收发中断等参数
      这里主要就是配置串口通信的波特率,数据帧长度,是否需要校验位,以及是否开启串口收发使能和收发中断,代码如下:

   /* USART configure */
  usart_deinit(USART0);
  usart_baudrate_set(USART0, 57600U);											//设置波特率
  usart_word_length_set(USART0, USART_WL_8BIT);
  usart_stop_bit_set(USART0, USART_STB_1BIT);
  usart_parity_config(USART0, USART_PM_NONE);
  usart_hardware_flow_rts_config(USART0, USART_RTS_DISABLE);
  usart_hardware_flow_cts_config(USART0, USART_CTS_DISABLE);
  	
  usart_receive_config(USART0, USART_RECEIVE_ENABLE);						//接收功能开启
  usart_transmit_config(USART0, USART_TRANSMIT_ENABLE);						//发送功能开启

  usart_interrupt_disable(USART0,USART_INT_TBE);							//发送完成中断关闭
  usart_interrupt_enable(USART0,USART_INT_RBNE);							//接受完成中断开启

  nvic_irq_enable(USART0_IRQn, 0, 0);	//配置USART0的中断优先级为第0分组,第0抢占级		\
  										这里可能不是很好理解,我会在后续章节详细讨论一下中断优先级
  	
  usart_enable(USART0);					//开启串口

      其实诸如串口波特率,是否开启中断这些参数,都可以作为函数的入口参数,方便修改。
第三步:重定向printf函数
      这一步其实既熟悉又陌生,步骤彦祖们应该很熟悉了,次序如下:
在这里插入图片描述
在这里插入图片描述
      这里需要注意的是,fputc函数,它是stdio.h的库函数,使用时,最好包含<stdio.h>头文件,而printf函数的重映射的原理和细节,后续会和中断优先级一起,单独拿一篇文章出来讨论,很快的!
在这里插入图片描述

第四步:编写串口中断函数以及串口数据帧解析程序

      这一步,各位彦祖应该时很熟悉了,我就直接贴代码了

void USART0_IRQHandler(void)
{
  if(usart_interrupt_flag_get(USART0,USART_INT_FLAG_RBNE) == SET)	//接受缓冲区非空标志位为1,表示有数据被接收
  {
  	usart_interrupt_disable(USART0, USART_INT_RBNE);				//关闭接收中断允许,防止被第二次中断打断	
  	
  	revbuf= usart_data_receive(USART0);
  	
  	usart_interrupt_enable(USART0, USART_INT_RBNE);					//恢复
  }
}
//该段代码置于主函数循环或者OS任务中
if( revbuf == 'T' )
{
  printf("The Receive data is T!\r\n");
  revbuf = 0//防止重复进入该if循环体
}

      运行一下,结果如下:
在这里插入图片描述

      这个时候就有彦祖会问了,就这?
在这里插入图片描述
      当然不是啦!如果这个时候,要再升级一下,要求串口输入函数的名称,MCU就会执行对应的函数,请问该怎么办?比如说串口输入函数名称为:“Print_Adc_Val”,要求MCU去执行程序中的Print_Adc_Val()函数,这个要怎么实现呢?如果输入的函数有入口参数,这时在上位机输入入口参数的数值,要求这个函数代入入口参数后运行起来,又该如何实现?再比如…
在这里插入图片描述
      其实我相信各位彦祖要写的话,肯定也是能写出来的,但是,如果有现成的,岂不是更好?所以我们今天的主题,在讲了半天废话之后,它终于来力!
在这里插入图片描述

(3)Letter Shell移植

(3.1)Letter Shell介绍

      对于Linux,想必各位彦祖都有所了解,命令行模式是它区别于Windows的一大特点,由于诸如安全、复杂、繁琐等原因,用户是不可以直接接触Linux内核的,但是又不能让用户手搓机器码吧?所以需要另外再开发一个程序,用户直接使用这个程序,这个程序的功能,就是接收用户的操作,比如:点击图标,输入某些指令之类的,然后这个程序经过约定好的解析格式,将数据再传递给Linux内核,通过这么一个流程,用户就能间接地操作使用Linux内核了,既简化了操作,又保证了Linux的安全,这个中间层的程序,就是Shell,它充当的,就是一个桥梁作用,通过Shell,用户就可以查询系统信息,运行状态,或者调用某个系统API。
      而Letter Shell秉承的,就是Linux Shell的思路,只不过在功能上,没有Linux Shell这么丰富,但是基本的功能,包括账户设置,密码设置,命令行调用,并运行程序中的函数等功能,只需要使用一个串口和PC上位机,就能在MCU上体验一把Linux Shell。
在这里插入图片描述

(3.2)Letter Shell文件结构

      Letter Shell是一项嵌入式开源项目,官方代码存储在github上,下载地址为:

Letter Shell源码: https://github.com/NevermindZZT/letter-shell

      不过,由于GitHub网址有时会打不开,所以如果有彦祖打不开GitHub网址时,我这还有百度网盘的地址,同步官方最新版本:

Letter Shell源码: https://pan.baidu.com/s/1YE2yiQfMadJQSQjLPpGKHw?pwd=o1xb

      下载好之后,主要包含以下几个文件:
在这里插入图片描述
      其中我们真正需要的,是src文件夹,demo文件夹里面存储的三个移植例程,有需要的彦祖可以瞅瞅,tools文件夹内,是python工具,用于查询Letter Shell支持的指令列表,extensions文件夹就是一些像文件系统之类的扩展功能,我们主要是看src文件夹的内容。
      src文件夹内包含的文件如下:
在这里插入图片描述
      红框内的shell.cshell.h是Letter Shell的核心代码,包含了帧解析后的数据处理,以及帧打包时的数据格式编码,黄框中的shell_cfg.h是一系列诸如用户名,用户名密码,用户函数注册等宏定义代码,黑框中的shell_cmd_list.c其实就是Letter Shell支持的命令行清单的定义,而shell_companion.c文件目前用不上它,但它是后期拓展需要用到的重要文件,所以也将它加入进来,最后绿框中的shell_ext.cshell_ext.h文件主要的功能是进行数据类型的格式转换,然后这里还需要我们自己新建两个接口文件shell_port.cshell_port.h,我们需要在这两个文件中加入串口读写程序,以实现Letter Shell最基础的串口数据读写功能,这也是Letter Shell可移植性强特点的体现。

(3.3)Letter Shell移植步骤

      这里以GD32F103C8T6模板工程(KEIL)为基础移植Letter Shell。

  • 第一步:在文件夹中新建Letter Shell文件夹,然后将Letter Shell的src文件夹中的所有 .c.h文件移动到Letter Shell文件夹中,如下图所示:
    在这里插入图片描述

  • 第二步:在KEIL的工程文件夹中新建Letter_Shell文件夹(起一个自己喜欢的名字也行),然后把文件夹中的程序文件加入到Letter_Shell中,记得在Option of target中把文件路径也添加进去。
    在这里插入图片描述

  • 第三步:这一步最容易被忽略,就是在Option of target的Linker选项下,在Misc Controls一栏中输入 –keep shellCommand* ,因为KEIL的编译器版本不同,某些版本下,KEIL自带的编译器在链接过程中,会将Letter Shell的部分代码进行优化,导致Letter Shell工作异常,所以添加 –keep shellCommand* ,以防被编译器优化。
    在这里插入图片描述

  • 第四步:最关键的地方来了,这一步需要编写shell_port.cshell_port.h这两个移植接口文件,不过好在需要编写的代码量比较少,在此之前,彦祖们需要将串口调试完成,将串口的发送中断使能关闭,接收中断使能打开,方便后续使用。
          那具体需要编写哪些函数呢?这里可以打开shell.h文件,在第367行~368行,这两个串口读写函数,就是Letter Shell所需要的串口读写函数,但是我们这里只需要实现串口写函数,而串口读函数会在串口接收中断函数实现。
    在这里插入图片描述
          这里的shell写函数其实是结构体类型shell内部的一个函数指针,要求返回值是signed short型,还有两个 char * 型和 unsngned short 入口参数,第一个入口参数是一个指向待发送数据的字符型指针,第二个入口参数是待发送字符串的字节数,也就是长度,所以我们编写的串口写函数也要按照这个格式来,然后才能将串口写函数正确赋值给这个函数指针。
          接下来就是具体代码,如下:

/*
 *shell_port.c
*/
#include "shell.h"
#include "User_Usart.h"
#include "shell_port.h"
#include "stdio.h"
 
Shell shell;
char shellBuffer[512];				//设置一个串口接收缓冲区
 
/**
 * @brief :用户shell写函数
 * 
 * @param in: data,指向待发送字符串的字符型指针
 * @param in: len,待发送的字符串长度
 * @note:Letter Shell的3.0版本新加了入口参数len,2.0版本是没有len参数的
 */
short userShellWrite(char *data,unsigned short len)
{
	unsigned short temp = len;

	while (temp--)
	{
		usart_data_transmit(USART0, *data++);

		/* 等待串口发送寄存器数据发送完成,即串口发送缓冲区为空标志 置位 */
		while(usart_flag_get(USART0, USART_FLAG_TBE) == RESET);
	}
    return len;
}
 
/**
 * @brief:用户shell初始化
 */
void userShellInit(void)			
{
    shell.write = userShellWrite;				//注册shell写函数,其实就是将函数代码体的首地址赋值给函数指针,注册	\
    											  听起来还是别扭
    shellInit(&shell, shellBuffer, 512);
}

      userShellInit函数需要在主函数初始化过程中调用,写函数实现了,剩下的就是在串口中断函数中实现shell读函数,上代码!

void USART0_IRQHandler(void)
{
	unsigned char Res;

	if(usart_interrupt_flag_get(USART0,USART_INT_FLAG_RBNE) == SET)  
	{
		Res =usart_data_receive(USART0);	
		
		shellHandler(&shell, Res);
	}
}

      我们只需要在串口中断函数中添加shellHandler函数,就可以直接实现shell读函数,而不用在shell_port.c中单独实现串口读函数,更为灵活。

  • 最后一步:把userShellInit函数添加到主函数中
    在这里插入图片描述
          到这里其实移植就已经结束了,讲解两小时,移植5分钟,GD32F103C8T6模板工程,以及移植好的Letter Shell工程,我放在了下面的链接中,有需要的彦祖可以下载下来操作一波。
    在这里插入图片描述

GD32F103C8T6模板工程(KEIL5): https://pan.baidu.com/s/1y-Lk0Kb4U5ij_N-FvdaGCw?pwd=2dxr

基于GD32F103C8T6的Letter Shell移植(KEIL5): https://pan.baidu.com/s/12ZgeDtpI-MLQSYS-Bf88VA?pwd=llod

(4)Letter Shell实际演示

      这里需要注意的是,使用的PC上位机最好是MobaXterm,或者是SecureCRT,因为Letter Shell对键盘键值的设置,是按照这两个上位机软件来做的,如果使用诸如Sscom之类的上位机,就会出现以下这种情况:
在这里插入图片描述
但正常情况下,输入help,应该出现的是下面这个(MobaXterm):
在这里插入图片描述
      篮框内是输入,而黄框内则是MCU返回的信息,所以建议各位彦祖使用以上这两个软件,软件的下载链接我也放在下面了。

MobaXterm(免费版本就够用了): https://mobaxterm.mobatek.net/download.html

SecureCRT: https://pan.baidu.com/s/1dgIWv0DVetnXWlQnAK6rpQ?pwd=su7i

      个人偏向于使用MobaXterm,因为它的操作相较SecureCRT更为简单,界面也更友好(美丽),所以接下来就以MobaXterm为上位机,展示一下Letter Shell的功能。

(4.1)使用MobaXterm连接开发板

      首先打开MobaXterm,按照下图顺序依次点击对应区域和数值设置
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

      在第三步中,在Serial port中选择你所使用的串口号,在Speed中选择你所使用的波特率,然后点击下面的OK ,就可以进入以下界面了:
在这里插入图片描述
      如果出现连接失败,或者串口号不识别的情况,建议检查一下硬件是否正常,接线的线序是否正常,波特率是否设置正确,或者是否有其他软件在占用串口等。

(4.2)输入第一个指令:help

      MobaXterm在Letter Shell使用中,支持Tab键自动补全命令,类似的功能在许多命令行场景都有应用,我们这里输入help时,可以输入h后,单击Tab键,软件就会自动补全命令。
      输入help指令后,回车,如果出现以下界面,说明Letter Shell移植成功了,如果没有出现的话,建议筛查以下几点:

  • 串口连接参数是否正确,如果选择了错误的串口号,或者波特率,那出错就理所应当了。
  • 串口配置代码是否正确,比如串口发送,接收功能是否使能,串口接收中断函数是否正确。
  • 串口写函数,以及在中断中是否正确调用shellHandler函数,这个是实现串口读关键。
    在这里插入图片描述
          黄框中,就是MCU返回的信息了。

(4.3)Letter Shell调用指定函数运行

      这个是我认为最有特色的一个功能了,以前想要调用一个指定函数并运行,基本上,最方便的,就是在Debug中,使用断点运行功能,而现在,通过串口,移植了Letter shell之后,在代码中只添加一行宏定义,就能在串口中调用此函数开始执行,比如在调试EEPROM,SPI Flash,屏幕,传感器之类的设备时,一行串口指令,就能指定某个函数运行,多是一件美事啊!
在这里插入图片描述

      实现这个功能只需三步:

  • 第一步:编写需要调用的函数,这里稍微美中不足的地方在于,对于函数格式有一定的要求,主要是以下两种格式,

      形如main函数定义的func(int argc, char *agrv[])

      形如普通C函数的定义func(int i, char *str, …)

      这里就会有彦祖问了,怎么第二种函数形式就只有两个入口参数,万一我需要传入的入口参数比较多呢?
在这里插入图片描述
      不要慌!Letter Shell支持最多8-1=7个入口参数,目前支持的参数类型为整型、字符型、字符串型(暂时不支持浮点型哦),在shell_cfg.h的第90行,有相关宏定义,如下:
在这里插入图片描述
      这里采用普通C函数定义,先写一个简单的测试函数,把这段代码添加进去,就OK了,先看效果,黄色的是输入信息,输入信息包括命令名称,入口参数,之间要用空格隔开哦,而蓝色的是MCU返回信息了。

int print_data(int i, char ch, char *str)
{
	printf("input int: %d, char: %c, string: %s\r\n", i, ch, str);
	return 0;
}

SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC), print, print_data, data_printf);

在这里插入图片描述

      可能就有彦祖会问了,上面那个函数我看得明白,下面那一大串是什么玩意?其实可以把它理解为一个注册作用的语句,它实际上是一个宏定义,在shell.h的第132行,有下面这段代码:
在这里插入图片描述
      这个就是刚刚所使用的的宏定义,包含四个参数,第一个参数是命令属性,我们需要修改的,是下面画黄线的代码
在这里插入图片描述      如果使用的是普通C函数类型,就用SHELL_TYPE_CMD_FUNC,如果用的是main函数类型,就用SHELL_TYPE_CMD_MAIN
      第二个参数是命令名称,这个就是我们会在Letter Shell的命令提示界面(输入help或者双击Tab键就可以调出来)看到的函数指令名称,当我们需要调用这个函数时,需要在Letter Shell输入这个命令名称,然后再输入入口参数。

      第三个参数是命令函数,这个就相当于告诉宏定义我们需要注册的是哪个函数,命令函数是不会显示在Letter Shell界面上的,而命令名称会。
      第四个参数是命令描述,类似于函数注释的作用,告诉用户这个函数的功能,这里其实可以展开写,我嫌麻烦,就直接写一个data_printf,也就是数据打印功能。

(5)小结

      其实Letter Shell还有很多其他诸如变量名查询,修改,适用于RTOS的锁等功能,篇幅有限,彦祖们阔以自行去测试一波。
      下一系列,就是包括I2C,SPI,USART,USB,I2S等嵌入式通信协议了,会采用协议文本+数据手册+代码调试+逻辑分析仪实测的方式,和彦祖们一起讨论这些既熟悉,又陌生的常用技术。
在这里插入图片描述

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐