新建工程
引入启动文件
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均无效(引脚配置为输入模式时)
- 推挽输出模式: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
(输出速度)
-
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;
-
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 */
-
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.c
和Serial.h
文件,Hardware文件夹是用来存放硬件驱动(eg:按键驱动,LED驱动),与Library文件夹就是使用库函数来实现我们自己所需的逻辑,
首先查看串口的库函数中带有哪些函数,打开Library/stm32f10x_usart.c
,发现有许许多多的函数,但我们在写驱动时,需要用到的只有其中常用的就够了,比如:
下面是具体的代码:
下面这些函数就是我们将库函数封装起来,向上抽象给主函数的方便易用的新函数接口,是可以在主函数中直接调用的。
USART串口初始化
串口的初始化封装为函数Serial_Init
,有如下步骤
- 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); //开启USART1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //开启GPIOA的时钟
- 初始化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);
- 初始化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);
- 开启RXNE标志位到NVIC的输出
使用中断来实现串口的接收功能,需要将RXNE标志位连接到NVIC中断控制器。具体:RXNE标志位一旦置1,就会向NVIC申请中断,之后可以在中断函数中接收数据。中断函数的名字可以在启动文件startup_stm32f10x_md.s
中找到——USART1_IRQHandler
。
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //
- 初始化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);
- 串口使能
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数据包
通过在包头和包尾指定专用的标志数据,来划分不同的包。
问题:当载荷数据与与包头包尾重复,该怎么办?
- 限制载荷数据的范围,使得包头包尾不在数据范围中
- 使用固定长度的数据包,这样便可以使用包头包尾来进行数据对齐
数据对齐:在接收载荷数据位置的数据时,并不会判断是包头包尾。但在接收包头包尾位置的数据时,会判断它是否确实是包头、包尾。- 增加包头包尾的数量,使其呈现出载荷数据出现不了的情况
优点:传输最直接,解析数据简单,适合模块发送原始的数据。比如使用串口通信的陀螺仪,温湿度传感器
缺点:灵活性不足,载荷容易和包头包尾重复
文本数据包
在文本数据包中,每个字节经过了一层编码和译码
优点:数据直观易理解,适合人机交互的场合输入指令。比如蓝牙模块使用的AT指令,CNC和3D打印机常用的G代码。
缺点:解析效率低下
代码
- 发送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);
}
- 接收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);
}
}
-
发送文本数据包
可以使用Serial_SendString
或者重定向printf
文章来源:https://www.toymoban.com/news/detail-437327.html -
接收文本数据包
此处,为了防止主函数和硬件处理速度不匹配,导致出现数据包错位,所以要在Serial.h
将extern uint8_t Serial_RxFlag;
也声明出去,使得主函数和Serial.c
都可以读写该变量,以便于主函数处理完成这个数据包之后,Serial.c
才进行下一个数据包的接收。
这样在主函数中,if (Serial_RxFlag == 1)
表示接收到数据包,主函数开始处理;在处理完成之后,Serial_RxFlag = 0;
告诉Serial.c
的接收部分,主函数已处理完毕,可以进行下一个数据包的接收了。文章来源地址https://www.toymoban.com/news/detail-437327.html
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);
}
}
到了这里,关于STM32-江科大的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!