STM32-江科大

新建工程

引入启动文件

Start中是启动文件,是STM32中最基本的文件,不需要修改,添加即可。
启动文件包含很多类型,要根据芯片型号来进行选择:
请添加图片描述

如果是选择超值系列,那就使用带 VL 的启动文件,若是普通版就选择不带VL的
然后再根据Flash的大小,选择LD(lower density),MD,HD或者XL

stm32f10x.h 是STM32的外设寄存器描述文件,是用来描述STM32有哪些寄存器和它对应地址的
system文件 是用来配置时钟的

由于STM32是由内核和内核外围的设备组成的,并且内核的寄存器描述(CoreSupport)和外围的寄存器描述文件(DeviceSupport)不是在一起的,因此还需添加“内核寄存器”的描述文件。

两个CM3 是内核的寄存器描述

引入库函数文件

当project中没有引入Library时,即无库函数,使用编程是通过直接操作寄存器来进行的。因此需要引入STM32的库函数。

1.引入头文件和源文件

STM32F10x_StdPeriph_Driver 为STM32标准外设驱动文件夹,其中
inc 是库函数的头文件
src 是库函数的源文件,在其中:misc 是内核的库函数,其他的是内核外的外设库函数

将这两个文件夹下的头文件和源文件全部复制到,项目文件夹下的Library中。
keil5 中将文件加入工程。

2.引入配置文件

STM32F10x_StdPeriph_Template 是一个示例项目文件夹,其中
stm32f10x_conf.h (configuration) 文件是用来配置库函数头文件的包含关系的,还有用于参数检查的函数定义,是所有库函数都需要的。
两个以 it 结尾的文件 是用来存放中断函数的。

将这3个文件复制到项目的User中。
keil5 中将文件加入工程。

4.Keil软件配置
打开主函数所引入的第一个头文件 stm32f10x.h, 在偏向最下方的部分找到

#ifdef USE_STDPERIPH_DRIVER
  #include "stm32f10x_conf.h"
#endif

这是一个条件编译,表示:如果定义了 USE_STDPERIPH_DRIVER 语句(使用标准外设驱动),其中的引入头函数语句才有效。

因此,复制语句USE_STDPERIPH_DRIVER到 keil5 软件的魔法棒 → C/C++ → Define:。

再把下方的头文件路径中加入 Library和user。

工程新建完成

经过以上两步操作,引入了启动文件和库函数文件,分别放在 Start 和 Library 之中,配置完成后,这两个文件夹中的文件都是不需要修改的(人家给的权限也是只读)。
需要修改的只有 User 文件夹下的代码。

GPIOC 是指单片机(如 STM32、Arduino 等)中的一个 GPIO 端口,其中 GPIO 是 General Purpose Input Output(通用输入输出)的缩写,而 C 表示这个端口属于 C 组。
在单片机中,GPIO 可以被用来控制数字信号的输入和输出。例如,可以将 GPIOC 配置为输出模式,在程序中控制它的高低电平,从而控制外部设备的状态或执行某些操作。另外,GPIOC 也可以被配置为输入模式,从而读取外部设备发送的数字信号。

GPIO

简介

  • GPIO (General Purpose Input Output) 通用输入输出口
  • 可配置8种输入输出模式
  • 引脚电平:0V~3.3V,部分引脚可容忍5V(具体有哪些可以参考 STM32 的引脚定义,带FT的就是可容忍5V的)
  • 输出模式下可控制端口输出高低电平,用以驱动LED、控制蜂鸣器、模拟通信协议输出时序等
  • 输入模式下可读取端口的高低电平或电压,用以读取按键输入、外接模块电平信号输入(压敏)、ADC电压采集、模拟通信协议接收数据(MQTT)等

基本结构

  • 在STM32种,所有的GPIO都是挂载到 APB2 外设总线上的,其中GPIO外设的名称是按照GPIOA,GPIOB,GPIOC这样来命名的,每个GPIO外设总共有16个引脚,编号是0到15。
  • 寄存器:STM32 是32位的单片机,故寄存器都是32位的,寄存器的每一位对应一个引脚。但端口只有16位,因此寄存器只有低16位有对应端口,高16位未使用到。
  • 驱动器:用来增加信号的驱动能力,寄存器只负责存储数据,若要进行点灯之类的操作,需要驱动器来增大驱动能力。
    请添加图片描述

GPIO位结构

数据输入(从右向左看):

  • 保护二极管:保护内部电路
    当输入电压高于3.3V时,上方保护二极管导通,输入电压产生的电流会流入 V D D V_{DD} VDD,而不会流入内部电路,可避免过高的电压对内部电路产生伤害。
    当输入电压比0V还要低(相比于 V S S V_{SS} VSS,故可以有负电压),下方二极管就会导通,电流会从 V S S V_{SS} VSS直接流出,也不会从内部汲取电流,保护了内部电路。
  • 上拉电阻和下拉电阻(配置参数):给输入提供一个默认的输入电平(输入引脚不接高低电平)
    上导通,下断开——上拉输入模式,此时引脚默认高电平(1)
    下导通,上断开——下拉输入模式,此时引脚默认低电平(0)
    两个都断开——浮空输入模式,此时引脚的输入电平极易受外界干扰而改变(?)
  • 施密特触发器:对输入电压进行整形
    给一个上限阈值和下限阈值,当输入电压高于上限阈值时,输出就是高电平。当然输入电压低于下限阈值时,输出就是低电平。这样能够使得有噪声的模拟信号整形成稳定的信号,可以有效避免因信号波动造成的输出抖动现象。

由此,信号便进入了输入数据寄存器,再用程序读取寄存器中的数据,便可以获得端口的输入电平了。
也可以输入到片上外设,分别有模拟输入和复用功能输入。

请添加图片描述

数据输出(从左往右看):
数据输出的来源有两种:输出数据寄存器和片上外设。

  • 位设置/清除寄存器:对单独某一位进行置1或置0
    由于输出数据寄存器只能整体进行操作,故若想对某一位进行单独处理的话,就只能读出来,改变它,再写入的方式进行,而使用“位设置/清除寄存器”可以更高效地实现该目的。
  • 输出控制(配置参数):选择数据来源是寄存器还是片上外设。
  • MOS电子开关(配置参数):
    • 推挽输出模式:P-MOS和N-MOS均有效
      当寄存器的数据为1时,上管导通,下管断开,输出连接 V D D V_{DD} VDD,输出高电平。
      当寄存器的数据为0时,上管断开,下管导通,输出连接 V S S V_{SS} VSS,输出低电平。
      这种模式下,高低电平均有较强的驱动能力,故该模式也称 强推输出模式。
    • 开漏输出模式:N-MOS有效,P-MOS无效(上管断开)
      当寄存器的数据为1时,下管断开,输出相当于断开,高阻模式。Unknown
      当寄存器的数据为0时,下管导通,输出连接 V S S V_{SS} VSS,输出低电平。
      这种模式下,只有低电平均有驱动能力,故该模式可以作为通信协议的驱动方式,或用于输出5V的电平信号。
    • 关闭模式:P-MOS和N-MOS均无效(引脚配置为输入模式时)

GPIO的8种工作模式

请添加图片描述

GPIO常用库函数

初始化函数

void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
初始化时,第二个参数是一个结构体:
GPIO_InitTypeDef GPIO_InitStruct;
其中需要设置3个属性值,分别是 GPIO_Mode(工作模式)、GPIO_Pin(引脚选择)、GPIO_Speed(输出速度)

  1. GPIO_Mode的参数有以下8个,分别对应上一小节说的GPIO的8种工作模式
{
    
     GPIO_Mode_AIN = 0x0,  //模拟输入
  GPIO_Mode_IN_FLOATING = 0x04,  //浮空输入
  GPIO_Mode_IPD = 0x28, //下拉输入
  GPIO_Mode_IPU = 0x48, //上拉输入
  GPIO_Mode_Out_OD = 0x14, //开漏输出
  GPIO_Mode_Out_PP = 0x10, //推挽输出
  GPIO_Mode_AF_OD = 0x1C, //复用开漏
  GPIO_Mode_AF_PP = 0x18 //复用推挽
}GPIOMode_TypeDef;
  1. GPIO_Pin是使用宏定义来进行命名的,直接复制变量名作为属性值。当使用多个引脚时,可使用或运算|(由下面的定义可以看出,不同的引脚分别在不同的二进制位处为1,或运算,可将其进行选择)
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_13
#define GPIO_Pin_0                 ((uint16_t)0x0001)  /*!< Pin 0 selected */
#define GPIO_Pin_1                 ((uint16_t)0x0002)  /*!< Pin 1 selected */
#define GPIO_Pin_2                 ((uint16_t)0x0004)  /*!< Pin 2 selected */
#define GPIO_Pin_3                 ((uint16_t)0x0008)  /*!< Pin 3 selected */
#define GPIO_Pin_4                 ((uint16_t)0x0010)  /*!< Pin 4 selected */
#define GPIO_Pin_5                 ((uint16_t)0x0020)  /*!< Pin 5 selected */
#define GPIO_Pin_6                 ((uint16_t)0x0040)  /*!< Pin 6 selected */
#define GPIO_Pin_7                 ((uint16_t)0x0080)  /*!< Pin 7 selected */
#define GPIO_Pin_8                 ((uint16_t)0x0100)  /*!< Pin 8 selected */
#define GPIO_Pin_9                 ((uint16_t)0x0200)  /*!< Pin 9 selected */
#define GPIO_Pin_10                ((uint16_t)0x0400)  /*!< Pin 10 selected */
#define GPIO_Pin_11                ((uint16_t)0x0800)  /*!< Pin 11 selected */
#define GPIO_Pin_12                ((uint16_t)0x1000)  /*!< Pin 12 selected */
#define GPIO_Pin_13                ((uint16_t)0x2000)  /*!< Pin 13 selected */
#define GPIO_Pin_14                ((uint16_t)0x4000)  /*!< Pin 14 selected */
#define GPIO_Pin_15                ((uint16_t)0x8000)  /*!< Pin 15 selected */
#define GPIO_Pin_All               ((uint16_t)0xFFFF)  /*!< All pins selected */
  1. GPIO_Speed一般情况下选择50MHz即可
{
    
     
  GPIO_Speed_10MHz = 1,
  GPIO_Speed_2MHz, 
  GPIO_Speed_50MHz
}GPIOSpeed_TypeDef;

综上,下面是一个初始化的例子:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE); // 初始化GPIOC的外设时钟,若使用的是PB或PA就改成GPIOB,GPIA

GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; //工作模式设置为推挽输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13; //初始化引脚PA13,一般在STM32板子上是自带的LED
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; //输出速度选择为50MHz
GPIO_Init(GPIOC, &GPIO_InitStruct);

8个读写函数

uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); //可把指定的端口设置为高电平
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//可把指定的端口设置为低电平
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal); // 可同时对16个端口i进行写入操作

gpio_writebit() 是用于设置一个 GPIO 引脚的值,只能将其设置为 0 或 1。
gpio_setbits() 和 gpio_resetbits() 则可以同时设置多个引脚的状态。 gpio_setbits() 可以将指定的引脚设置为 1 ,而 gpio_resetbits() 可以将它们设置为 0 。因此,这两个函数比 gpio_writebit() 更适合同时控制多个 GPIO 引脚(按位或 )的情况。

串口通信

通信接口

  • 通信的目的:将一个设备的数据传送到另一个设备,用于STM32与其他模块间的通信
  • 通信协议:制定通信的规则,通信双方按照协议规则进行数据收发。
    请添加图片描述
  • USART串口:TX(Transmit Exchange)是数据发送脚;RX(Receive Exchange)是数据接收脚。
  • I2C:SCL(Serial Clock)是时钟;SDA(Serial Data)是数据
  • SPI:SCLK(Serial Clock)是时钟;OSI(Master output Slave Input)是主机输出数据脚;MISO(Master Input Slave output)是主机输入数据脚;CS(Chip Select)是片选,用于指定通信的对象
  • CAN:CAN_H,CAN_L这两个是差分数据脚,用两个引脚表示一个差分数据
  • USB:DP(Data Positive D+),DM(Data Minus D-)也是一对差分数据脚

USART串口协议

具体细节不记录了,看视频。
总结:TX引脚输出定时翻转的高低电平,RX引脚定时读取引脚的高低电平。每个字节的数据加上起始位、停止位、可选的校验位打包为数据帧,一次输出在TX引脚,另一端RX引脚依次接收,这样就完成了字节数据的传递。

USART串口外设

  • USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接成一个字节数据,存放在数据寄存器里
  • STM32F103C8T6 的USART资源:USART1(APB2总线上),USART2(APB1总线上),USART3(APB1总线上)

串口代码

驱动层

在此引入库函数小节,我们知道Library文件夹存放了STM32的库函数,而库函数就是在寄存器操作层面向上抽象了一层,提供了丰富的硬件进行操作的函数接口。
但在主函数中,我们很多情况下只想要简单明确的函数来进行一个想要的操作。比如在使用串口,我在写主函数时,只想简单地,我给出数据,它负责传输。不想去管具体我还要初始化什么时钟,使用哪个端口,配置什么乱七八糟的设置。因此,我们就需要在库函数和主函数之间再架起一个桥梁,使用其将这些使用库函数操作硬件的细节封装起来,向上对主函数提供简单易用的新函数接口。
因此,我们在库函数层面上再向上抽象一层,就是所谓的驱动。在此,我们新建一个文件夹Hardware,用来存放这些硬件驱动(eg:按键驱动,LED驱动),以及即将编写的串口驱动。

代码编写

在Hardware文件夹中新建Serial.cSerial.h文件,Hardware文件夹是用来存放硬件驱动(eg:按键驱动,LED驱动),与Library文件夹就是使用库函数来实现我们自己所需的逻辑,
首先查看串口的库函数中带有哪些函数,打开Library/stm32f10x_usart.c,发现有许许多多的函数,但我们在写驱动时,需要用到的只有其中常用的就够了,比如:

下面是具体的代码:
下面这些函数就是我们将库函数封装起来,向上抽象给主函数的方便易用的新函数接口,是可以在主函数中直接调用的。

USART串口初始化

串口的初始化封装为函数Serial_Init,有如下步骤

  1. 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); //开启USART1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //开启GPIOA的时钟
  1. 初始化GPIO引脚,可以对照小节GPIO初始化函数,此处是将PA9(STM32引脚定义中,PA9不仅可以作为GPIO口,也可以复用作为 USART1_TX 即串口输出 来使用)配置成复用推挽输出,将PA10PA10复用为 USART1_RX 即串口接收输入 来使用配置成上拉输入。
    (这里的输入和输出,都是相对于这个接口所在硬件来说的,比如这里就是STM32,USART1_TX作为串口输出,指的就是数据从STM32经由PA9作为串口TX进行向外输出;USART1_RX作为串口接收输入,指的就是数据从外部经由PA10作为串口RX进行向内输入到STM32)
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
  1. 初始化USART
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600; //波特率:9600
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //不使用流控
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //串口模式:TX发送、接收双模式
USART_InitStructure.USART_Parity = USART_Parity_No; //校验位:无校验
USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位:1位
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长:8位

USART_Init(USART1, &USART_InitStructure);
  1. 开启RXNE标志位到NVIC的输出
    使用中断来实现串口的接收功能,需要将RXNE标志位连接到NVIC中断控制器。具体:RXNE标志位一旦置1,就会向NVIC申请中断,之后可以在中断函数中接收数据。中断函数的名字可以在启动文件startup_stm32f10x_md.s中找到——USART1_IRQHandler
    请添加图片描述
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 
  1. 初始化NVIC,//TODO 这里对于NVIC还不了解
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //分组???

NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //中断通道:
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //开启
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //优先级:
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;

NVIC_Init(&NVIC_InitStructure);
  1. 串口使能
USART_Cmd(USART1, ENABLE);
串口发送
  • 发送字节Serial_SendByte
    调用该函数便可以从TX引脚发送一个字节的数据
void Serial_SendByte(uint8_t Byte)
{
    
    
	USART_SendData(USART1, Byte); \\直接调用库函数SendData来将Byte写入TDR
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); \\等待标志位,避免数据覆盖
}

  • 发送数组Serial_SendArray
\*
 * Array : 数组指针
 * Length :数组长度
*\
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
    
    
	uint16_t i;
	for (i = 0; i < Length; i ++)
	{
    
    
		Serial_SendByte(Array[i]); //依次取出数组Array的每一项,利用Serial_SendByte进行发送
	}
}
  • 发送字符串函数Serial_SendString
void Serial_SendString(char *String)
{
    
    
	uint8_t i;
	for (i = 0; String[i] != '\0'; i ++) //字符串会以'\0'结束
	{
    
    
		Serial_SendByte(String[i]);
	}
}
  • 发送数字函数Serial_SendNumber
    //TODO 解释原因
    由于发送字符串的时候,本质上还是发送一个数字,最终在电脑显示字符串形式的数字
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
    
    
	uint8_t i;
	for (i = 0; i < Length; i ++)
	{
    
    
		Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');
	}
}

  • printf重定向
    一般情况下,printf函数是将数据发送到屏幕,但对于单片机来说没有屏幕,那就只能将数据的流向经由串口导入到电脑,显示在串口助手中,这样间接的方式来实现打印。
    因此,需要重写printf的底层函数fputc(为什么直接在c文件中定义了,就改变了库中的printf函数?此处也没有使用类似于java的重写override操作)
int fputc(int ch, FILE *f)
{
    
    
	Serial_SendByte(ch);
	return ch;
}
串口接收

对于串口接收来说,可以使用查询中断两种方法。

  • 查询:在主函数中不断判断RXNE标志位,如果置1,说明收到数据了。再调用ReceiveData,读取DR寄存器。
while(1){
    
    
  if(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == SET){
    
     //判断RXNE标志位
    RXData = USART_ReceiveData(USART1); //接收一个字节,放在变量RXData中
    OLED_ShowHexNum(1, 1, RXData, 2); //若硬件连有OLED显示屏,可以显示在其上
  }
}
  • 中断:需要在初始化中,加入开启中断的代码。另外编写中断函数USART1_IRQHandler来接收数据。
void USART1_IRQHandler(void)
{
    
    
	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //判断RXNE标志位
	{
    
    
		Serial_RxData = USART_ReceiveData(USART1); //Serial_RxData 为定义在该文件中的全局变量,用来暂存数据
		Serial_RxFlag = 1;  //Serial_RxFlag 为定义在该文件中的全局变量,用来暂存标志位
		USART_ClearITPendingBit(USART1, USART_IT_RXNE); //以防没读取DR时,不能自动清除标志位,此处咱们手动清除标志位
	}
}

为了向上封装完全,此处还提供两个函数,分别供主函数来判断是否接收完毕,以及获取暂存在驱动文件中的Serial_RxData数据

uint8_t Serial_GetRxFlag(void)
{
    
    
	if (Serial_RxFlag == 1)
	{
    
    
		Serial_RxFlag = 0;
		return 1;
	}
	return 0;
}

uint8_t Serial_GetRxData(void)
{
    
    
	return Serial_RxData;
}
串口发送和接收主函数
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"

uint8_t RxData;

int main(void)
{
    
    
	OLED_Init();
	OLED_ShowString(1, 1, "RxData:");
	
	Serial_Init();

	//串口发送
	Serial_SendByte(0x41);

	uint8_t MyArray[] = {
    
    0x42, 0x43, 0x44, 0x45};
	Serial_SendArray(MyArray, 4);

	Serial_SendString("\r\nNum1=");
	
	Serial_SendNumber(111, 3);
	
	printf("\r\nNum2=%d", 222);
	
	char String[100];
	sprintf(String, "\r\nNum3=%d", 333);
	Serial_SendString(String);
	
	Serial_Printf("\r\nNum4=%d", 444);
	Serial_Printf("\r\n");

	while (1)
	{
    
     
    //串口接收
		if (Serial_GetRxFlag() == 1) //判断标志位
		{
    
    
			RxData = Serial_GetRxData(); //接收一个字节的数据
			Serial_SendByte(RxData); //回传显示
			OLED_ShowHexNum(1, 8, RxData, 2);
		}
	}
}
串口数据包收发
HEX数据包

通过在包头和包尾指定专用的标志数据,来划分不同的包。
问题:当载荷数据与与包头包尾重复,该怎么办?

  1. 限制载荷数据的范围,使得包头包尾不在数据范围中
  2. 使用固定长度的数据包,这样便可以使用包头包尾来进行数据对齐
    数据对齐:在接收载荷数据位置的数据时,并不会判断是包头包尾。但在接收包头包尾位置的数据时,会判断它是否确实是包头、包尾。
  3. 增加包头包尾的数量,使其呈现出载荷数据出现不了的情况

优点:传输最直接,解析数据简单,适合模块发送原始的数据。比如使用串口通信的陀螺仪,温湿度传感器
缺点:灵活性不足,载荷容易和包头包尾重复

文本数据包

在文本数据包中,每个字节经过了一层编码和译码
优点:数据直观易理解,适合人机交互的场合输入指令。比如蓝牙模块使用的AT指令,CNC和3D打印机常用的G代码。
缺点:解析效率低下

代码
  1. 发送HEX数据包Serial_SendPacket
    调用该函数,TxPacket的4个数据,就会自动加上包头包尾发送出去。(定义在Serial.c中的全局变量Serial_TxPacket,如果要在主函数中使用的化。可以使用get、set函数来传递指针,或者直接在Serial.h文件中使用 extern 声明出去,因为嵌入式编程很多情况下没那么高的设计要求,因此很多使用到全局变量和这种不利于软件工程设计理念的方式)
void Serial_SendPacket(void)
{
    
    
	Serial_SendByte(0xFF);
	Serial_SendArray(Serial_TxPacket, 4);
	Serial_SendByte(0xFE);
}
  1. 接收HEX数据包USART1_IRQHandler
    在接收中断函数中,使用状态机来执行接收逻辑。接收数据包将其存放在Serial_RxPacket接收数组中。
    (static 静态变量类似全局变量,只会在函数第一次进入时进行初始化,函数退出后,数据仍然有效,但与全局变量不同的是,静态变量只能在本函数使用。)
uint8_t Serial_RxPacket[4]; // 接收缓冲区
uint8_t Serial_RxFlag; //接收标志位

void USART1_IRQHandler(void)
{
    
    
	static uint8_t RxState = 0; //使用静态变量作为状态标志
	static uint8_t pRxPacket = 0; //指示接收到哪一个了
	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
	{
    
    
		uint8_t RxData = USART_ReceiveData(USART1);
		
		if (RxState == 0)
		{
    
    
			if (RxData == 0xFF)
			{
    
    
				RxState = 1;
				pRxPacket = 0;
			}
		}
		else if (RxState == 1)
		{
    
    
			Serial_RxPacket[pRxPacket] = RxData;
			pRxPacket ++;
			if (pRxPacket >= 4)
			{
    
    
				RxState = 2;
			}
		}
		else if (RxState == 2)
		{
    
    
			if (RxData == 0xFE)
			{
    
    
				RxState = 0;
				Serial_RxFlag = 1; //接收完成
			}
		}
		
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);
	}
}
  1. 发送文本数据包
    可以使用Serial_SendString或者重定向printf

  2. 接收文本数据包
    此处,为了防止主函数和硬件处理速度不匹配,导致出现数据包错位,所以要在Serial.hextern uint8_t Serial_RxFlag;也声明出去,使得主函数和Serial.c都可以读写该变量,以便于主函数处理完成这个数据包之后,Serial.c才进行下一个数据包的接收。
    这样在主函数中,if (Serial_RxFlag == 1)表示接收到数据包,主函数开始处理;在处理完成之后,Serial_RxFlag = 0;告诉Serial.c的接收部分,主函数已处理完毕,可以进行下一个数据包的接收了。

char Serial_RxPacket[100]; //接收缓冲区

void USART1_IRQHandler(void)
{
    
    
	static uint8_t RxState = 0;
	static uint8_t pRxPacket = 0;
	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
	{
    
    
		uint8_t RxData = USART_ReceiveData(USART1);
		
		if (RxState == 0)
		{
    
    
			if (RxData == '@' && Serial_RxFlag == 0) // 遇到包头且主函数处理完毕上一个数据包
			{
    
    
				RxState = 1;
				pRxPacket = 0;
			}
		}
		else if (RxState == 1)
		{
    
    
			if (RxData == '\r') //接收时遇到第一个包尾'\r'
			{
    
    
				RxState = 2;
			}
			else
			{
    
    
				Serial_RxPacket[pRxPacket] = RxData;
				pRxPacket ++;
			}
		}
		else if (RxState == 2)
		{
    
    
			if (RxData == '\n')
			{
    
    
				RxState = 0;
				Serial_RxPacket[pRxPacket] = '\0'; // 给字符串加入结束字符'\0'
				Serial_RxFlag = 1;
			}
		}
		
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);
	}
}

猜你喜欢

转载自blog.csdn.net/qq_41168765/article/details/130568947