首页 > 其他分享 >stm32学习笔记---USART串口外设(代码部分)串口发送/串口发送+接收

stm32学习笔记---USART串口外设(代码部分)串口发送/串口发送+接收

时间:2024-07-03 16:00:50浏览次数:3  
标签:--- 函数 USART 发送 串口 GPIO Serial

目录

第一个代码:串口发送

初始化串口的步骤

USART的库函数

三个初始化函数

USART_ClockInit和USART_ClockStructInit

USART_Cmd和USART_ITConfig

USART_DMACmd

USART_SendData和USART_ReceiveData

四个标志位相关的函数

代码实现

Serial.c

第一步,开启时钟

第二步,GPIO初始化

第三步,配置USART

第四步,开关控制

发送数据的函数

数据模式

发送数组的函数

发送字符串的函数

发送数字的函数

Serial.h

main.c

printf函数的移植方法

第一种方法

第二种方法

第三种方法

显示汉字的操作方法

第一种方法

第二种方法

第二个代码:串口的发送+接收


声明:本专栏是本人跟着B站江科大的视频的学习过程中记录下来的笔记,我之所以记录下来是为了方便自己日后复习。如果你也是跟着江科大的视频学习的,可以配套本专栏食用,如有问题可以QQ交流群:963138186

接下来我们来学习一下串口的代码部分

第一个代码:串口发送

接线图:

意:USD转串口的模块上面的跳线帽要插在VCC3V3这两个引脚上选择通信的TTL电平3.3V然后通信引脚TXD和RXD要接在STM32的PA9和PA10口,为什么是这两个口?查引脚定义表可得。

这里看到USART1的TX是PA9,RX是PA10,我们计划用USART1进行通信,所以就选这两个脚。还要注意一个问题,TXRX一定要交叉连接比如这里PA9STM32TX,那么它要接的就是串口模块的RX接收然后串口模块的TX发送要接在STM32PA10,也就是RX接收外。然后两个设备之间要把负极接在一起,进行共地。

一般多个系统之间互联都要进行这样电平才能有高低的参考。就像两个人比身高一样,他俩必须要站在同一地平面上才能比较。如果一个人站在地球,一个人站在月球,那怎么知道谁高谁低?这就是共地的问题。

最后这个串口模块和STLINK或者DAP都要插在电脑上,这样STM32和串口模块都有独立供电。所以这里通信的电源正极就不需要接了,直接三根线就行。

我们第一个代码只有STM32发送的部分,所以通讯线只有这个发送的有用,另一根线第一个代码没有用的,暂时可以不接。

在我们下一个串口发送加接收的代码,两根通信线就都需要接了。所以我们把这两根通信线一起都接上,这样两个代码的经线图是一模一样的。

连好线后,回到电脑端,此电脑-右键-属性-打开设备管理器,确保串口的驱动没问题,在这个端口目录下,可以看到有这个CH340的驱动,如果出现了COM号,并且前面图标没有感叹号,那就证明串口驱动没问题,否则的话需要安装一下串口模块的驱动(如需要安装串口驱动可以看江科大的第二节视频)。

复制OLED显示屏那一节的工程并改名

初始化串口的步骤

初始化串口的步骤看这个基本结构图。

第一步开启时钟把需要用的USART和GPIO的时钟打开。

第二步,GPIO初始化把TX配置成复用输出,RX配置成输入。

第三步配置USART,直接使用一个结构体,就可以把这里所有的参数都配置好了。

第四步开关控制,如果只需要发送的功能,就直接开启USART初始化就结束了。如果需要接收的功能,可能还需要配置中断。那就在开启USART之前,再加上ITConfig和NVIC的代码就行了。

初始化完成之后,如果要发送数据,调用一个发送函数就行了。如果要接收数据,就调用接收的函数。如果要获取发送和接收的状态,就调用获取标志位的函数,这就是USART外设的使用思路。

USART的库函数

打开usart.h,拖到最后

三个初始化函数

这三个初始化函数大家应该都很熟悉了,不用说了

USART_ClockInit和USART_ClockStructInit

这两个函数是用来配置同步时钟输出的。包括时钟是不是要输出,时钟的极性、相位等参数,因为参数也比较多,所以也是用结构体这种方式来配置的,需要时钟输出的话可以了解一下这两个函数。

USART_Cmd和USART_ITConfig

这两个函数应该也很熟悉了。

USART_DMACmd

这个可以开启USART到DMA的触发通道,需要用DMA的话可以了解一下。

USART_SendData和USART_ReceiveData

这两个函数在我们发送和接收的时候会用到。SendData就是写DR寄存器,ReceiveData就是读DR寄存器。DR寄存器内部有四个寄存器,控制发送与接收,执行细节。我们上一小节已经分析过了,这里程序上就非常简单,写DR就是发送,读DR就是接收。至于怎么产生波形,怎么判断输入,软件一概不管。

四个标志位相关的函数

代码实现

Serial.c

第一步开启时钟

第二步,GPIO初始化

选择引脚模式,TX引脚是USART外设控制的输出脚。所以要选复用推挽输出,RX引脚是USART外设数据输入脚,所以要选择输入模式,输入模式并不分什么普通输入复用输入,一根线只能有一个输出,但可以有多个输入,所以对于输入脚,外设和GPIO都可以同时用。一般RX配置是浮空输入或者上拉输入,因为串口波形空闲状态是高电平,所以不使用下拉输入。

引脚模式如果不清楚的话,还是看一下手册GPIO那一节有个推荐的配置表,可以参考一下。

我们第一个程序只需要数据发送,所以只初始化TX就行。

第三步配置USART

定义结构体,列出结构体成员,选择每个成员的取值,调用USART_Init函数初始化USART。

第一个成员是波特率,写9600,写完之后,这个USART_Init函数内部会自动算好9600对应的分频系数。然后写到BRR寄存器,计算步骤上节讲过。

技巧:查找每个成员的取值时,可以把成员变量名复制放到等号右边,按CTRL+ALT+空格联想,就能看到取值的列表,如果没有那可能这个成员的取值是要自己写的,可以右键跳转到定义看看。

第二个成员是硬件流控制,它的取值列表

None不使用流控,只用CTS,只用RTS,或者CTS、RTS都使用

我们不使用流控,所以选择None。

第三个成员是串口模式,取值可以选择TX发送模式和RX接收模式。如果你既需要发送,又需要接收,那就用或符号把TX和RX或起来。我们这个程序只需要发送功能,所以就选择TX这一个参数就行。

第四个成员是校验位,取值是

检验位可以选择NO无校验,Odd奇校验,Even偶校验,我们不需要校验,所以选择NO。

第五个成员是停止位,取值可以是

我们选择1位停止位.

第六个成员是字长,可以选择八位或九位,我们不需要校验,所以字长就选择八位。

到这里,我们结构体参数的初始化就完成了。

第四步开关控制

发送数据的函数

接下来我们来写一个发送数据的函数Serial_SendByte,调用这个函数,就可以从TX引脚发送一个字节数据。

在这里面我们需要调用串口的USART_SendData函数,参数第一个给USART1,第二个给Byte。

这个函数的内部是这样的,把我们传的Byte给它的Data

Data与上01FF就是把无关的高位清零。然后直接赋值给DR寄存器,因为这是写入DR,所以数据最终通向TDR发送数据寄存器,TDR再传递给发送移位寄存器,最后一位一位地把数据移出到TX引脚,完成数据的发送。

调用函数,Byte变量就写入到TDR了。

写完之后,我们还需要等待一下,等TDR的数据转移到移位寄存器了,我们才能放心。要不然如果数据还在TDR进行等待,我们再写入数据,就会产生数据覆盖。所以在发送之后,我们还需要等待一下标志位,在这里调用USART_GetFlagStatus函数,这个函数的第二个参数可以选择

我们需要使用这个TXE发送数据寄存器空标志位复制。然后我们要等待TXE置1。

然后是标志位是否需要手动清除的问题,这个可以看一下手册

所以说这里标志位置1之后,不需要手动清零。当我们下一次再写入Data时,这个标志位会自动清零。

那这样我们这个Serial_SendByte函数就完成了,

如果需要测试这个发送部分的功能,则主函数里可以写这样写

调用这个函数之后,TX引脚就会产生一个0x41对应的波形。这个波形可以发送给其他支持串口的模块,也可以通过USB转串口模块发送到电脑端。

我们本节主要是和电脑通信,所以是在电脑端接收数据。

测试的时候别忘记将以上函数放在头文件中声明一下。

那我们编译下载后,我们按一下复位键。这时可以看到串口模块的接收指灯闪了一下,说明有波形发过来了。

那在电脑端我们需要打开串口助手软件,来查看接收到的数据。

串口助手软件领取链接

链接:https://pan.baidu.com/s/1ASVoP_LLIPUQ91fJ4dz6vw

提取码:hm48

注意:串口助手上的串口号要和设备管理器上显示的串口号一致。并且串口助手上的参数也必须和我们代码中配置的一致。

串口助手上的参数配置好后,选择打开串口,然后串口助手就准备就绪,此时在USB转串口模块上按一下复位键,就能在串口助手上的接收区看到数据了。

这就是发送一个字节数据的现象。

然后大家注意到下面这里有一个接收模式。目前选择的是HEX模式,也就是以原始数据的形式显示。发送41显示就是41本身。

如果我们想显示一下字符串怎么办?那就可以选择文本模式,这样就是以字符的形式

这里补充说明一下数据模式的解释

数据模式

HEX模式/十六进制模式/二进制模式(这些描述都是一个意思):以原始数据的形式显示

文本模式/字符模式(这些描述都是一个意思):以原始数据编码后的形式显示(对应ASCII码表上的符号)

下面这个表展示的就是ASCII码字符集,比如0x41这个数据对应的就是大写字母A

字符集的第一个字符,原始数据是0x00,对应字符是空字符,也就是保留位,不映射任何字符。一般这个0经常作为字符串的结束标志位,字符串遇到数据0x00之后,就代表字符串结束了。

随着计算机的发展,全球互相通信。为了防止不同国家编码的不兼容现象,我们可以把所有国家的字符全部收录到一个统一的字符集,这就是Unicode字符集。Unicode最常用的传输形式是UTF,有关字符编码的内容,大家可以自己再去网上搜一搜。如果编码不匹配,就会出现非常烦人的乱码,这个得注意一下。

下面这个图描述的是字符和数据在发送和接收的转换关系

比如最上面发送0x41数据,发送的线路传输的就是0x41。接收方如果以原始数据形式显示,就是0x41。如果以字符显示,就是走下面这一路,通过字符集译码,找到字符,然后显示字符’A’。

在发送方也可以直接发送字符,比如发送字符A,这时它就会先从字符集找到A的数据进行编码,发现A对应的数据是0x41。最终在线路中传输的必须是十六进制数0x41。然后接收方可以选择查看原始数据0x41,也可进行译码得到字符A。这就是字符和数据在发送接收过程中经历的变化。

接下来我们再分装一些函数模块,这些函数大家之后用串口肯定会经常用的,光有一个发送字节函数满足不了需求。

接下来写的函数其实都是对SendByte的封装。

发送数组的函数

首先是发送一个数组,我们定义一个发送数组的函数Serial_SendArray

然后在主函数中这样调用

发送结果

发送字符串的函数

接下来写一个发送字符串的函数Serial_SendString,然后由于字符串自带一个结束标志位,所以就不需要再传递长度参数了。

在里面执行逻辑和申德尔瑞是非常类似的先定义变量由in特八杠t哎。在负i等于零,这里循环条件就可以用结束标志位来判断了string i。

在主函数里这样调用

现象

如果想要换行可以在后面加上\r\n来执行换行命令

这样就能在每次打印之后就会执行一次换行命令了

发送数字的函数

接下来我们继续分装发送数字的函数Serial_SendNumber。在函数里面,我们需要把number的个位、十位、百位等等以十进制拆分开,然后转换成字符数字对应的数据,依次发送出去。

怎么以十进制拆开?

比如有个数字是12345,

取万位,就是12345/1000%10=1,

取千位,就是12345/1000%10=2,

取百位,就是12345/100%10=3,

取十位,就是12345/10%10=4,

取个位,就是12345/1%10=5。

总结下来,取某一位,就是数字除以十的x次方,再对此取余。除以十的x次方,就是把这一位的右边去掉,对十取余就是把左边去掉。这就是拆分数字的思路。

所以我们先需要写一个次方函数,

现在就可以写发送数字的函数了,这个函数默认是发送十进制数字,然后接收是接收到字符形式的数字

在主函数里这样调用

结果

Serial.c

#include "stm32f10x.h"                  // Device header
#include <stdio.h>
#include <stdarg.h>

/**
  * 函    数:串口初始化
  * 参    数:无
  * 返 回 值:无
  */
void Serial_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);	//开启USART1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA9引脚初始化为复用推挽输出
	
	/*USART初始化*/
	USART_InitTypeDef USART_InitStructure;					//定义结构体变量
	USART_InitStructure.USART_BaudRate = 9600;				//波特率
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;	//硬件流控制,不需要
	USART_InitStructure.USART_Mode = USART_Mode_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);				//将结构体变量交给USART_Init,配置USART1
	
	/*USART使能*/
	USART_Cmd(USART1, ENABLE);								//使能USART1,串口开始运行
}

/**
  * 函    数:串口发送一个字节
  * 参    数:Byte 要发送的一个字节
  * 返 回 值:无
  */
void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1, Byte);		//将字节数据写入数据寄存器,写入后USART自动生成时序波形
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);	//等待发送完成
	/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}

/**
  * 函    数:串口发送一个数组
  * 参    数:Array 要发送数组的首地址
  * 参    数:Length 要发送数组的长度
  * 返 回 值:无
  */
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
	uint16_t i;
	for (i = 0; i < Length; i ++)		//遍历数组
	{
		Serial_SendByte(Array[i]);		//依次调用Serial_SendByte发送每个字节数据
	}
}

/**
  * 函    数:串口发送一个字符串
  * 参    数:String 要发送字符串的首地址
  * 返 回 值:无
  */
void Serial_SendString(char *String)
{
	uint8_t i;
	for (i = 0; String[i] != '\0'; i ++)//遍历字符数组(字符串),遇到字符串结束标志位后停止
	{
		Serial_SendByte(String[i]);		//依次调用Serial_SendByte发送每个字节数据
	}
}

/**
  * 函    数:次方函数(内部使用)
  * 返 回 值:返回值等于X的Y次方
  */
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
	uint32_t Result = 1;	//设置结果初值为1
	while (Y --)			//执行Y次
	{
		Result *= X;		//将X累乘到结果
	}
	return Result;
}

/**
  * 函    数:串口发送数字
  * 参    数:Number 要发送的数字,范围:0~4294967295
  * 参    数:Length 要发送数字的长度,范围:0~10
  * 返 回 值:无
  */
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');	//依次调用Serial_SendByte发送每位数字
	}
}

/**
  * 函    数:使用printf需要重定向的底层函数
  * 参    数:保持原始格式即可,无需变动
  * 返 回 值:保持原始格式即可,无需变动
  */
int fputc(int ch, FILE *f)
{
	Serial_SendByte(ch);			//将printf的底层重定向到自己的发送字节函数
	return ch;
}

/**
  * 函    数:自己封装的prinf函数
  * 参    数:format 格式化字符串
  * 参    数:... 可变的参数列表
  * 返 回 值:无
  */
void Serial_Printf(char *format, ...)
{
	char String[100];				//定义字符数组
	va_list arg;					//定义可变参数列表数据类型的变量arg
	va_start(arg, format);			//从format开始,接收参数列表到arg变量
	vsprintf(String, format, arg);	//使用vsprintf打印格式化字符串和参数列表到字符数组中
	va_end(arg);					//结束变量arg
	Serial_SendString(String);		//串口发送字符数组(字符串)
}

Serial.h

#ifndef __SERIAL_H
#define __SERIAL_H

#include <stdio.h>

void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array, uint16_t Length);
void Serial_SendString(char *String);
void Serial_SendNumber(uint32_t Number, uint8_t Length);
void Serial_Printf(char *format, ...);

#endif

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"

int main(void)
{
	/*模块初始化*/
	OLED_Init();						//OLED初始化
	
	Serial_Init();						//串口初始化
	
	/*串口基本函数*/
	Serial_SendByte(0x41);				//串口发送一个字节数据0x41
	
	uint8_t MyArray[] = {0x42, 0x43, 0x44, 0x45};	//定义数组
	Serial_SendArray(MyArray, 4);		//串口发送一个数组
	
	Serial_SendString("\r\nNum1=");		//串口发送字符串
	
	Serial_SendNumber(111, 3);			//串口发送数字
	
	/*下述3种方法可实现printf的效果*/
	
	/*方法1:直接重定向printf,但printf函数只有一个,此方法不能在多处使用*/
	printf("\r\nNum2=%d", 222);			//串口发送printf打印的格式化字符串
										//需要重定向fputc函数,并在工程选项里勾选Use MicroLIB
	
	/*方法2:使用sprintf打印到字符数组,再用串口发送字符数组,此方法打印到字符数组,之后想怎么处理都可以,可在多处使用*/
	char String[100];					//定义字符数组
	sprintf(String, "\r\nNum3=%d", 333);//使用sprintf,把格式化字符串打印到字符数组
	Serial_SendString(String);			//串口发送字符数组(字符串)
	
	/*方法3:将sprintf函数封装起来,实现专用的printf,此方法就是把方法2封装起来,更加简洁实用,可在多处使用*/
	Serial_Printf("\r\nNum4=%d", 444);	//串口打印字符串,使用自己封装的函数实现printf的效果
	Serial_Printf("\r\n");
	
	while (1)
	{
		
	}
}

printf函数的移植方法

最后介绍一下printf函数的移植方法。

第一种方法

使用printf之前,我们需要打开工程选项,把这个勾选上

MicroLIB是Keil为嵌入式平台优化的一个精简库,我们等会要用的printf函数,就可以用这个MicroLIB,所以先勾上这个。

然后我们还需要对printf进行重定向,将printf函数打印的东西输出到串口。

因为printf函数默认是输出到屏幕,我们单片机没有屏幕,所以要进行重定向,步骤就是在串口模块里最开始加上#include <stdio.h>,

之后在最底下重写fputc函数

那重定向fputc跟printf有什么关系?

这是因为这个fputc是printf函数的底层,printf函数在打印的时候,就是不断调用fputc函数一个个打印的,我们把fputc函数重定向到了串口,那printf自然就输出到串口了。

这样printf就移植好了,我们到主函数试一下,这里直接写printf

结果

接下来再介绍两种printf函数的移植方法。刚才这一种方法,printf只能有一个,你重定向到串口1了,那串口2再用就没有了。如果多个串口都想用printf怎么办?

第二种方法

这时就可以用sprintf,sprintf可以把格式化字符输出到一个字符串里。所以这里可以先定义一个字符串,长度给100,然后sprintf第一个参数是打印输出的位置,我们指定打印到string,之后就跟printf一样了。

目前这个格式化的字符串在string里,最后需要再来一个Serial_SendString把字符串通过串口发送出去,这样就完成了。

这里就是在内存里创建一块char类型的string数组,然后赋值,再通过之前定义好的Serial_SendString函数通过USART1发送String。

因为sprintf可以指定打印位置,不涉及重定向的东西,所以每个串口都可以使用sprintf进行格式化打印。

现象是一样的

最后再介绍一种方法。

第三种方法

sprintf每次都得先定义字符串,再打印到字符串,再发送字符串,太麻烦了。我们要是能分装一下这个过程就再好不过了。

所以第三种方法就是分装sprintf。由于printf这类函数比较特殊,它支持可变的参数,像我们之前写的函数,参数的个数都是固定的,可变现参数,这个执行起来比较复杂。

首先在串口模块里先添加头文件#include <stdarg.h>

然后在最后对sprintf函数进行分装

这里面出现了很多没见过的函数,如果没学过这种用法,可能比较难理解,这个也没关系,知道这样移植就行了。如果学过了,那其实是基本操作。也可以学学这种可变参数的用法,自己写其他函数的时候,也可以用。

在主函数里调用

这样也可以实现printf的功能

以上就是printf函数的移植方法,最常见的是第一种。如果你有多个printf的需求,可以了解一下后两种方法。

显示汉字的操作方法

最后再讲一个显示汉字的操作方法

第一种方法

目前我们这个汉字编码格式选的是UTF8,所以最终发送到串口,汉字会以UTF8的方式编码,最终串口助手也得选择UTF8才能解码正确。

先说一下UTF8不乱码的方案,比如这里写个字符串,你好世界。

不过这样直接写汉字,编译器有时候会报错,这里需要打开工程选项,这里写上--no-multibyte-chars,需要给编译器输入一个这样的参数。

ok这样编译下载看看在串口助手这里目前是乱码

编码方式要选择UTF-8这样就不会乱码了

这是UTF8的解决方案,但是UTF8可能有些软件兼容性不好,所以第二种方式就是切换为GB2312编码,这是汉字的编码方式。

第二种方法

点击OK之后目前这个文件编码格式其实还是UTF-8,我们需要把发送的汉字删掉,再把文件关掉,再打开,等字体变为这种宋体了,编码格式才算改过来。

然后再写你好世界,这样编码就是GB2312了

在串口助手里选择GBK编码,一般windows软件默认就是GBK的编码。GBK和GB2312一样都是中文的编码,基本都是兼容的。

这样也是没问题的。

有关串口发送的代码到这里就结束了。

接下来我们来学习一下串口的接收。

第二个代码:串口的发送+接收

接线图:

接线图和上一个是一样的。


复制上一个代码并改名

然后初始化这里加上接收的部分。

首先是GPIO口,我们要使用RX的引脚。在引脚定义表里,我们知道USART1的RX,复用在了PA10引脚

所以这里需要再初始化一下PA10。

引脚模式可以选择浮空输入或上拉输入,我们就选择上拉输入模式。

下面这些参数大部分不用改,只需要改一下模式,这个参数后面加上一个或串口模式Rx。

这样就是同时开启发送和接收的部分。

如果你只需要接收,那就把前面这个TX去掉就行了。

对串口接收来说,可以使用查询和中断两种方法。如果使用查询,那初始化就结束了。如果使用中断,那还需要在这里开启中断配置NVIC。像我们最开始演示的那个现象,使用查询的方法就可以完成。

这里先演示一下查询,再演示一下中断。

查询的流程是在主函数里不断判断RXNE标志位,如果置1了,就说明收到数据了,那再调用receive Data读取DR寄存器,这样就行了。

直接在主函数演示,就不再分装了。

目前接收到的一个字节,数据就已经在RxData里了。

接下来我们可以进行显示。然后还有一个清除标志位的问题

当RDR移位寄存器中的数据被转移到USART_DR寄存器中,该位被硬件置位。如果USART_CR1寄存器中的RXNEIE为1则产生中断。对USART_DR的读操作可以将该位清零。

这里DR可以自动清零标志位,所以这里读完DR就不需要我们再清除标志位了。

下载看一下现象

我们需要在串口助手发送区里写入数据,发送模式选择hex模式。然后在这里写,比如41。

在hex模式下,这里只能写十六进制数,非法字符都将会被忽略。

点发送,在OLED上可以看到就收到了数据41。

这就是查询方法的串口接收程序现象。

如果程序比较简单,查询方法是可以考虑的。

那接下来我们再演示一下中断方法的程序。

如何使用中断?

首先在GPIO初始化后面这里要加上开启中断的代码。

开启RXNE标志位到NVIC的输出,

之后,就是配置NVIC。

先分组

在初始化NVIC

到这里RXNE标志位一旦置1了,就会向NVIC申请中断之后,我们可以在中断函数里接收数据。

中断函数的名字要启动文件找一下:

找到名字后写中断函数

进入if之后,那if的最后要不要清除标志位?

如果你读取了DR,就可以自动清除。如果没读取DR,就需要手动清除,我们这里直接给清一下,这个也不影响。

之后在这里面,可以直接读取DR执行一些操作。当然由于这个代码是在模块里不太适合加入过多其他的代码。素以就先在最上面定义两个变量

然后在这个中断函数里面,我们先读取到模块的变量里,读完之后,置一个自己的标志位。

我们也实现一个读后自动清除的功能

下面再写一个获取串口接收的数据

到这里,中断接收和变量的封装就完成了。其实这里就是在中断里把数据进行了一次转存。最终还是要扫描查询这个RxFlag来接收数据的。

对于这种单字节接收来说,可能转存一下意义不大。这里这样写,主要是给大家演示一下中断接收的操作方法。另外也是为我们下节多字节数据包接收做一个铺垫。

Serial.c

#include "stm32f10x.h"                  // Device header
#include <stdio.h>
#include <stdarg.h>

uint8_t Serial_RxData;		//定义串口接收的数据变量
uint8_t Serial_RxFlag;		//定义串口接收的标志位变量

/**
  * 函    数:串口初始化
  * 参    数:无
  * 返 回 值:无
  */
void Serial_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);	//开启USART1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA9引脚初始化为复用推挽输出
	
	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);					//将PA10引脚初始化为上拉输入
	
	/*USART初始化*/
	USART_InitTypeDef USART_InitStructure;					//定义结构体变量
	USART_InitStructure.USART_BaudRate = 9600;				//波特率
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;	//硬件流控制,不需要
	USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;	//模式,发送模式和接收模式均选择
	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);				//将结构体变量交给USART_Init,配置USART1
	
	/*中断输出配置*/
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);			//开启串口接收数据的中断
	
	/*NVIC中断分组*/
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);			//配置NVIC为分组2
	
	/*NVIC配置*/
	NVIC_InitTypeDef NVIC_InitStructure;					//定义结构体变量
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;		//选择配置NVIC的USART1线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//指定NVIC线路使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;		//指定NVIC线路的抢占优先级为1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;		//指定NVIC线路的响应优先级为1
	NVIC_Init(&NVIC_InitStructure);							//将结构体变量交给NVIC_Init,配置NVIC外设
	
	/*USART使能*/
	USART_Cmd(USART1, ENABLE);								//使能USART1,串口开始运行
}

/**
  * 函    数:串口发送一个字节
  * 参    数:Byte 要发送的一个字节
  * 返 回 值:无
  */
void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1, Byte);		//将字节数据写入数据寄存器,写入后USART自动生成时序波形
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);	//等待发送完成
	/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}

/**
  * 函    数:串口发送一个数组
  * 参    数:Array 要发送数组的首地址
  * 参    数:Length 要发送数组的长度
  * 返 回 值:无
  */
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
	uint16_t i;
	for (i = 0; i < Length; i ++)		//遍历数组
	{
		Serial_SendByte(Array[i]);		//依次调用Serial_SendByte发送每个字节数据
	}
}

/**
  * 函    数:串口发送一个字符串
  * 参    数:String 要发送字符串的首地址
  * 返 回 值:无
  */
void Serial_SendString(char *String)
{
	uint8_t i;
	for (i = 0; String[i] != '\0'; i ++)//遍历字符数组(字符串),遇到字符串结束标志位后停止,这里也可以直接写String[i] !=0,空字符
	{
		Serial_SendByte(String[i]);		//依次调用Serial_SendByte发送每个字节数据
	}
}

/**
  * 函    数:次方函数(内部使用)
  * 返 回 值:返回值等于X的Y次方
  */
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
	uint32_t Result = 1;	//设置结果初值为1
	while (Y --)			//执行Y次
	{
		Result *= X;		//将X累乘到结果
	}
	return Result;
}

/**
  * 函    数:串口发送数字
  * 参    数:Number 要发送的数字,范围:0~4294967295
  * 参    数:Length 要发送数字的长度,范围:0~10
  * 返 回 值:无
  */
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');	//依次调用Serial_SendByte发送每位数字
	}
}

//Length - i - 1为什么要减1?
//假设length为2,第一次循环,2-0-1=1,10^1,就是发送十位
//第二次循环,2-1-1=0,10^0,就是发送各位

//为什么要加'0'?
//因为我们最终是要以字符的形式显示数字,所以要加上偏移,'0'的ASCII值是0x30
//因为底层只认识Hex,这个偏移实际是把一个输入的十进制数用ASCII的方式0-9一个一个拼出来的,
//比如输入357,代码逻辑实际上是3拼5拼5拼7,3
//就是0x03,然后加上0x30就是0x33,查ASCII码表可知0x33对应字符'3',其他同理


/**
  * 函    数:使用printf需要重定向的底层函数
  * 参    数:保持原始格式即可,无需变动
  * 返 回 值:保持原始格式即可,无需变动
  */
int fputc(int ch, FILE *f)
{
	Serial_SendByte(ch);			//将printf的底层重定向到自己的发送字节函数
	return ch;
}

/**
  * 函    数:自己封装的prinf函数
  * 参    数:format 格式化字符串,用来接收格式化字符串
  * 参    数:... 可变的参数列表,用来接收可变的参数列表
  * 返 回 值:无
  */
void Serial_Printf(char *format, ...)
{
	char String[100];				//定义字符数组
	va_list arg;					//定义可变参数列表数据类型的变量arg ,va_list是类型名
	va_start(arg, format);			//从format开始,接收参数列表放到arg变量
	vsprintf(String, format, arg);	//使用vsprintf打印格式化字符串和参数列表到字符数组中
	//打印位置是String,格式化字符串是format,参数表是arg
	va_end(arg);					//结束变量arg
	Serial_SendString(String);		//串口发送字符数组(字符串)
}

/**
  * 函    数:获取串口接收标志位
  * 参    数:无
  * 返 回 值:串口接收标志位,范围:0~1,接收到数据后,标志位置1,读取后标志位自动清零
  */
uint8_t Serial_GetRxFlag(void)
{
	if (Serial_RxFlag == 1)			//如果标志位为1
	{
		Serial_RxFlag = 0;
		return 1;					//则返回1,并自动清零标志位
	}
	return 0;						//如果标志位为0,则返回0
}

/**
  * 函    数:获取串口接收的数据
  * 参    数:无
  * 返 回 值:接收的数据,范围:0~255
  */
uint8_t Serial_GetRxData(void)
{
	return Serial_RxData;			//返回接收的数据变量
}

/**
  * 函    数:USART1中断函数
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
  *           函数名为预留的指定名称,可以从启动文件复制
  *           请确保函数名正确,不能有任何差异,否则中断函数将不能进入
  */
void USART1_IRQHandler(void)
{
	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)		//判断是否是USART1的接收事件触发的中断
	{
		Serial_RxData = USART_ReceiveData(USART1);				//读取数据寄存器,存放在接收的数据变量
		Serial_RxFlag = 1;										//置接收标志位变量为1
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);			//清除USART1的RXNE标志位
																//读取数据寄存器会自动清除此标志位
																//如果已经读取了数据寄存器,也可以不执行此代码
	}
}

Serial.h

#ifndef __SERIAL_H
#define __SERIAL_H

#include <stdio.h>

void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array, uint16_t Length);
void Serial_SendString(char *String);
void Serial_SendNumber(uint32_t Number, uint8_t Length);
void Serial_Printf(char *format, ...);

uint8_t Serial_GetRxFlag(void);
uint8_t Serial_GetRxData(void);

#endif

Main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"

uint8_t RxData;			//定义用于接收串口数据的变量

int main(void)
{
	/*模块初始化*/
	OLED_Init();		//OLED初始化
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "RxData:");
	
	/*串口初始化*/
	Serial_Init();		//串口初始化
	
	while (1)
	{
		if (Serial_GetRxFlag() == 1)			//检查串口接收数据的标志位
		{
			RxData = Serial_GetRxData();		//获取串口接收的数据
			Serial_SendByte(RxData);			//串口将收到的数据回传回去,用于测试
			OLED_ShowHexNum(1, 8, RxData, 2);	//显示串口接收的数据
		}
	}
}

运行结果:

本节课的内容结束了,目前这里只支持一个字节的节奏,这功能比较简单。

现在很多模块都需要回传大量数据,这时就需要用数据包的形式进行传输,接收部分也需要按照数据包的格式来接收,这样才能接收多字节数据包。数据包的发送和接收也是比较常见和重要的内容。

有关串口这部分进阶的内容,下节继续讲解。

QQ交流群:963138186

本篇就到这里,下篇继续!欢迎点击下方订阅本专栏↓↓↓

标签:---,函数,USART,发送,串口,GPIO,Serial
From: https://blog.csdn.net/xiaobaivera/article/details/140136027

相关文章

  • stm32学习笔记---USART串口数据包(理论部分)
    目录Hex数据包第一种是固定包长,含包头包尾第二种是可变包长,含包头包尾收发过程中的问题第一个问题就是包头包尾和数据载荷重复的问题第一种方法,限制载荷数据的范围第二种方法,如果无法避免载荷数据和包头包尾重复,就尽量使用固定长度的数据包第三种方法,就是增加包头包尾......
  • 三菱 PLC 星-三角启动
    三相异步电动机的Y-△(星-三角)启动控制程序。▲星-三角电机端子的接法器件选型三相异步电机、接触器、继电器、FX2N-32MR等。接线图设计I/O分配▲I/O分配表程序编写Y0:=(Y0ANDX1)ORX0;(*X1为急停开关,常闭*)IFY0THENOUT_T(TRUE,TC0,50);Y1:=......
  • q-analog 和 q-binomial
    模拟赛四道题三道是计数,不得不来看一看这个。当一个表达式\(f(q)\)满足\(\lim_{q\to1}f(q)=c\)时,称它是\(c\)的\(q-\)analog。例如\([n]_q=\frac{1-q^n}{1-q}=(1+q+q^2+\cdots+q^{n-1})\)是\(n\)的\(q-\)analog,因为它满足上述定义。一个自然数\(n\)的\(q-\)fact......
  • vue elementUI el-tree 下拉树功能(包括搜索/默认高亮/展开下拉框默认定位于选中项的位
    <template><div><el-form:model="formData"ref="refFormData"label-width="180px"><el-form-itemlabel="景点"prop="location_id"><el-selectv-model="formData.location_name&qu......
  • iMessage蓝号检测,苹果iMessages短信,iMessages群发,iMessages推信,完美实现总结 - 电
    一、PC电脑版苹果系统(MacOS)上实现imessages群发总结为以下几种方式:/*MacOS苹果系统,正常情况下,只能安装到苹果公司自己出品的Mac电脑,俗称白苹果,不能安装到各种组装机或者其他品牌的品牌机上,黑苹果的的原理,就是通过一些“破解补丁”工具欺骗macOS系统,让苹果系统认为你的电......
  • 聚簇索引(MySQL-InnoDB引擎下)
    聚簇索引(MySQL-InnoDB引擎下)聚簇索引并不是一种单独的索引类型,而是一种存储方式。顾名思义,聚簇,使得数据行和相邻的键值紧促的存储在一起。(物理上的)聚簇索引的数据分布Mysql内置的存储引擎并不支持选择用于聚簇的索引,主键索引默认就是聚簇索引。聚簇索引的优点:1.可以将相互关......
  • 最新AI大模型系统源码,ChatGPT商业运营版系统源(详细图文搭建部署教程)+AI绘画系统,DALL-E
    一、前言人工智能语言模型和AI绘画在多个领域都有广泛的应用.....SparkAi创作系统是一款基于ChatGPT和Midjourney开发的智能问答和绘画系统,提供一站式AIB/C端解决方案,涵盖AI大模型提问、AI绘画、文档分析、图像识别和理解、TTS&语音识别、AI换脸等多项功能。支持GPTs应......
  • 算法基础入门 - 2.栈、队列、链表
    文章目录算法基础入门第二章栈、队列、链表2.1队列2.2栈2.3纸牌游戏2.4链表如何建立链表?1.我们需要一个头指针(head)指向链表的初始。链表还没建立时头指针head为空2.建立第一个结点3.设置刚创建的这个结点的数据域(左半)和指针域(右半)4.设置头指......
  • python-进阶2
    三大特征1.继承1.1单继承1.2多继承1.3方法调用顺序1.4调用父类方法1.5super1.6多层继承2封装3多态3.1入门3.2条件3.3优势4抽象5类属性与方法5.1类属性5.2类方法5.3静态方法1.继承面向对象中的继承:指的是多个类之间的所属关系,即子类默认......
  • 自动化测试-实施方案
    1.项目讨论1.1项目中符合自动化测试的部分有哪些?(目标和范围scope,准入准出标准)稳定的需求点、变动较少的页面每日构建后的测试验证dailybuild比较频繁的回归测试需要在多平台上运行的相同测试案例、组合遍历型的测试、大量的重复任务1.2自动化用例在整个项目的测......