STM32标准库——(12)USART串口协议

目录

1.全双工、半双工及单工通讯

2.同步与异步通讯

3.通信接口

4.串口通信 

5.硬件电路 

6.电平标准

7.串口参数

8.串口时序

9.USART简介

10.USART框图

11.USART基本结构

12.数据帧

12.1 字长设置

12.2 停止位配置

13.起始位检测

14.数据采样

15.波特率发生器

16.数据模式

17.USART库函数

18.串口发送数据

18.1 接线图

18.2 相关代码

18.2.1 Serial.c

18.2.2 Serial.h

 18.2.3 main.c

19.串口发送+接收数据

19.1 接线图

19.2 相关代码

19.2.1 Serial.c

19.2.2 Serial.h

19.2.3 main.c


1.全双工、半双工及单工通讯

2.同步与异步通讯

 在同步通讯中,收发设备双方会使用一根信号线表示时钟信号,在时钟信号的驱动下双方进行协调, 同步数据,见图 同步通讯。 通讯中通常双方会统一规定在时钟信号的上升沿或下降沿对数据线进行采样。

在异步通讯中不使用时钟信号进行数据同步,它们直接在数据信号中穿插一些同步用的信号位,或者把主体数据进行打包, 以数据帧的格式传输数据,见图 某种异步通讯 ,某些通讯中还需要双方约定数据的传输速率,以便更好地同步。

3.通信接口

  • 通信的目的:将一个设备的数据传送到另一个设备,扩展硬件系统
  • 通信协议:制定通信的规则,通信双方按照协议规则进行数据收发

4.串口通信 

  • 串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备的互相通信
  • 单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信,极大地扩展了单片机的应用范围,增强了单片机系统的硬件实力

5.硬件电路 

  • 简单双向串口通信有两根通信线(发送端TX和接收端RX)
  • TX与RX要交叉连接
  • 当只需单向的数据传输时,可以只接一根通信线
  • 当电平标准不一致时,需要加电平转换芯片

6.电平标准

电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:

  • TTL电平:+3.3V或+5V表示1,0V表示0
  • RS232电平:-3~-15V表示1,+3~+15V表示0
  • RS485电平:两线压差+2~+6V表示1,-2~-6V表示0(差分信号)

7.串口参数

  1.  每隔1秒发送一位,那么接收方也必须每隔1秒接收一位。如果接收方过早接收,则可能会重复接收某些位;如果接收方过晚接收,则可能会错过某些位。因此,发送方和接收方必须约定好传输速率,这个速率参数,就是波特率。
  2. 起始位:它是标志一个数据帧的开始,固定为低电平。首先,串口的空闲状态是高电平,也就是没有数据传输的时候,然后需要传输的时候,必须要先发送一个起始位,这个起始位必须是低电平,来打破空闲状态的高电平,产生一个下降沿。这个下降沿,就告诉接收设备,这一帧数据要开始了。如果没有起始位,那当我发送8个1的时候,是不是数据线就一直都是高电平,没有任何波动。

  3. 数据位:这里数据位表示数据帧的有效载荷,1为高电平,0为低电平,低位先行。比如我要发送一个字节,是0x0F,那就首先把0F转换为二进制,就是0000 1111,然后低位先行,所以数据要从低位开始发送,也就是1111 0000,像这样,依次放在发送引脚上。所以说如果你想发0x0F这一个字节数据,那就按照波特率要求,定时翻转引脚电平,产生一个这样的波形就行了。

  4. 数据校验:如果数据出错了,可以选择丢弃或者要求重传,校验可以选择3种方式,无校验、奇校验和偶校验。无校验,就是不需要校验位,波形就是左边这个,起始位、数据位、停止位,总共3个部分。奇校验要求有效数据和校验位中“1”的个数为奇数,比如一个8位长的有效数据为:01101001,此时总共有4个“1”, 为达到奇校验效果,校验位为“1”,最后传输的数据将是8位的有效数据加上1位的校验位总共9位。偶校验与奇校验要求刚好相反,要求帧数据和校验位中“1”的个数为偶数, 比如数据帧:11001010,此时数据帧“1”的个数为4个,所以偶校验位为“0”。0校验是不管有效数据中的内容是什么,校验位总为“0”,1校验是校验位总为“1”。

8.串口时序

9.USART简介

其中这里USART1是APB2总线上的设备,剩下的都是APB1总线中的设备,开启时钟的时候注意一下。 

10.USART框图

  1. TX: 发送数据输出引脚
  2. RX: 接收数据输入引脚
  3. SCLK: 发送器时钟输出引脚。这个引脚仅适用于同步模式。
  4. SW_RX: 数据接收引脚,只用于单线和智能卡模式,属于内部引脚,没有具体外部引脚。
  5. nRTS: 请求以发送(Request To Send),n表示低电平有效。如果使能RTS流控制,当USART接收器准备好接收新数据时就会将nRTS变成低电平; 当接收寄存器已满时,nRTS将被设置为高电平。该引脚只适用于硬件流控制。
  6. nCTS: 清除以发送(Clear To Send),n表示低电平有效。如果使能CTS流控制,发送器在发送下一帧数据之前会检测nCTS引脚, 如果为低电平,表示可以发送数据,如果为高电平则在发送完当前数据帧之后停止发送。该引脚只适用于硬件流控制。
  7. 一个是发送数据寄存器TDR(Transmit DR),另一个是接收数据计算器RDR(Receive DR),这两个寄存器占用同一个地址,就跟51单片机串口的SBUF寄存器一样,在程序上只表现为一个寄存器,就是数据寄存器DR(Data Register),但实际硬件中是分成了两个寄存器,一个用于发送TDR,一个用于接收RDR,TDR是只写的,RDR是只读的,当你进行写操作时,数据就写到了TDR,当你进行读操作时,数据就是从RDR读出来的。

  8. 流控有两个引脚,一个是nRTS,一个是nCTS。nRTS(Request To Send)是请求发送,是输出脚,也就是告诉别人,我当前能不能接收;nCTS (Clear To Send)是清除发送,是输入脚,也就是用于接收别人nRTS的信号的。

    首先,我们需要找到一个支持流控的串口,并将它的TX连接到我们的RX。同时,我们的RTS需要输出一个接收反馈信号,并将其连接到对方的CTS。当我们可以接收数据时,RTS会置为低电平,请求对方发送。对方的CTS接收到信号后,就可以继续发送数据。如果处理不过来,比如接收数据寄存器未及时读取,导致新数据无法接收,此时RTS会置为高电平,对方的CTS接收到信号后,就会暂停发送,直到接收数据寄存器被读走,RTS重新置为低电平,数据才会继续发送。

    反过来当我们的TX向对方发送数据时,对方的RTS会连接到我们的CTS,用于判断对方是否可以接收数据。TX和CTS是一对对应的信号,RX和RTS也是一对对应的信号。此外,CTS和RTS之间也需要交叉连接,这就是流控的工作模式。然而,我们一般不使用流控,因此只需要了解一下即可。

  9. 中断申请位,就是状态寄存器这里的各种标志位,状态寄存器这里,有两个标志位比较重要,一个是TXE发送寄存器空,另一个是RXNE接收寄存器非空,这两个是判断发送状态和接收状态的必要标志位。(位于图中接收器控制下方)

  10. 波特率发生器其实就是分频器,APB时钟进行分频,得到发送和接收移位的时钟。看一下,这里时钟输入是fPCLKx(x=1或2),(USART1挂载在APB2,所以就是PCLK2的时钟,一般是72M;其他的USART都挂载在APB1,所以是PCLK1的时钟,一般是36M)之后这个时钟进行一个分频,除一个USARTDIV的分频系数,并且分为了整数部分和小数部分,因为有些波特率,用72M除一个整数的话,可能除不尽,会有误差。所以这里分频系数是支持小数点后4位的,分频就更加精准,之后分频完之后,还要再除个16,得到发送器时钟和接收器时钟,通向控制部分。然后右边这里,如果TE (TX Enable)为1,就是发送器使能了,发送部分的波特率就有效;如果RE(RX Enable)为1,就是接收器使能了,接收部分的波特率就有效。(位于图中虚线框部分)

11.USART基本结构

12.数据帧

12.1 字长设置

这里有4种选择,9位字长,有校验或无校验;8位字长,有校验或无校验。但我们最好选择9位字长 有校验,或8位字长 无校验,这两种,这样每一帧的有效载荷都是1字节。 

12.2 停止位配置

13.起始位检测

当输入电路侦测到数据帧的起始位后,将以波特率的频率连续采样一帧数据。同时,从起始位开始,采样位置要对齐到位的正中间。只要第一位对齐了,后面就都是对齐的。

为了实现这些功能,输入电路对采样时钟进行了细分,以波特率的16倍频率进行采样。在一位的时间里,可以进行16次采样。比如最开始时,空闲状态为高电平,采样一直是1。在某个位置突然采到0,说明两次采样之间出现了下降沿,如果没有噪声,那之后就应该是起始位了。在起始位,会进行连续16次采样,没有噪声的话,这16次采样肯定都是0。但是实际电路还是会存在一些噪声,所以这里即使出现下降沿了,后续也要再采样几次以防万一。

根据手册描述,接收电路在下降沿之后的第3次、5次、7次进行一批采样,在第8次、9次、10次再进行一批采样。这两批采样都要求每3位里面至少应有2个0。如果没有噪声,那肯定全是0,满足情况;如果有一些轻微的噪声导致3位里面只有两个0另一个是1,那也算是检测到了起始位(但是在状态寄存器里会置一个NE(Noise Error),提醒你数据收到了但是有噪声,你悠着点用);如果3位里面只有1个0,那就不算检测到了起始位可能前面那个下降沿是噪声导致的,这时电路就忽略前面的数据重新开始捕捉下降沿。

14.数据采样

15.波特率发生器

 波特率发生器就是分频器,发射器和接收器的波特率,由波特率寄存器BRR里面的DIV确定,上面这个图就是BRR寄存器,里面就是分频系数div ,div分为整数部分和小数部分,可以实现更细腻的分频。
那波特率和分频系数的关系可以由计算公式进行计算,为什么这里多个16,看上面这个图就明白了吧 因为它内部还有一个16倍波特率的采样时钟,所以这里输入时钟除以div要等于16倍的波特率,最终计算波特率自然要多出一个16了。

16.数据模式

17.USART库函数

18.串口发送数据

18.1 接线图

18.2 相关代码

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

/**
  * 函    数:串口初始化
  * 参    数:无
  * 返 回 值:无
  */
void Serial_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//开启USART1的时钟 USART1是APB2总线上的设备 剩下的都是APB1总线中的设备
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开启GPIOA的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//TX引脚是USART外设控制的输出脚 所以要用复用推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	/*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使能*/
	USART_Cmd(USART1, ENABLE);//使能USART1,串口开始运行
}

/**
  * 函    数:串口发送一个字节
  * 参    数:Byte 要发送的一个字节
  * 返 回 值:无
  */
void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1,Byte);
	/*  USARTx->DR = (Data & (uint16_t)0x01FF);//数据最终通向TDR(发送数据寄存器) TDR再传递给移位寄存器 最终一位一位把数据移出Tx引脚 完成数据发送*/
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);	//等待发送完成
	/* 当TDR寄存器中的数据被硬件转移到移位寄存器的时候,该位被硬件置位。如果USART_CR1寄存器中的TXEIE为1,则产生中断。对USART_DR的写操作,将该位清零。*/
	/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}

/**
  * 函    数:串口发送一个数组
  * 参    数: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累乘到结果 Result*X=X 累乘Y次即X的Y次方 用次来得到后面数字的位数
	}
	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:i从0开始 这样子数字遍历出来得到的是最低位(各位)在最前面 需取反*/
		/*%10:得到数字的位数*/
		/*+‘0’:比如函数给234 不加'0'按原规则遍历完显示的是020304 所以每次遍历需要移位 将前面的0移去*/
	}
}

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

/**
  * 函    数:自己封装的prinf函数
  * 参    数:format 格式化字符串
  * 参    数:... 可变的参数列表
  * 返 回 值:无
  */
void Serial_Printf(char *format, ...)
{
	char String[100];				//定义字符数组
	va_list arg;					//定义可变参数列表数据类型的变量arg va_list是一个类型名 arg是变量名
	va_start(arg, format);			//从format开始,接收参数列表到arg变量
	vsprintf(String, format, arg);	//使用vsprintf打印格式化字符串和参数列表到字符数组中 因为sprintf只能接收直接写的参数
	va_end(arg);					//结束变量arg
	Serial_SendString(String);		//串口发送字符数组(字符串)
}
18.2.2 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
 18.2.3 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");
	/*Serial_Printf("你好,世界"); 这里也可以输入汉字 只不过需要电机上面设置的魔术棒中的C/C++部分 倒数第二行空白位置输入--no-multibyte-chars即可*/
	
	while (1)
	{
		
	}
}

现象:根据主函数中 串口助手接收数据分别是ABCDE 111 222 333 444

(接收模式是文本模式 发送模式是HEX模式)

19.串口发送+接收数据

19.1 接线图

19.2 相关代码

19.2.1 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的时钟 USART1是APB2总线上的设备 剩下的都是APB1总线中的设备
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开启GPIOA的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//TX引脚是USART外设控制的输出脚 所以要用复用推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//一般RX配置是浮空输入或上拉输入
	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;//波特率
	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_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);
	/*  USARTx->DR = (Data & (uint16_t)0x01FF);//数据最终通向TDR(发送数据寄存器) TDR再传递给移位寄存器 最终一位一位把数据移出Tx引脚 完成数据发送*/
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);	//等待发送完成
	/* 当TDR寄存器中的数据被硬件转移到移位寄存器的时候,该位被硬件置位。如果USART_CR1寄存器中的TXEIE为1,则产生中断。对USART_DR的写操作,将该位清零。*/
	/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}

/**
  * 函    数:串口发送一个数组
  * 参    数: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累乘到结果 Result*X=X 累乘Y次即X的Y次方 用次来得到后面数字的位数
	}
	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:i从0开始 这样子数字遍历出来得到的是最低位(各位)在最前面 需取反*/
		/*%10:得到数字的位数*/
		/*+‘0’:比如函数给234 不加'0'按原规则遍历完显示的是020304 所以每次遍历需要移位 将前面的0移去*/
	}
}

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

/**
  * 函    数:自己封装的prinf函数
  * 参    数:format 格式化字符串
  * 参    数:... 可变的参数列表
  * 返 回 值:无
  */
void Serial_Printf(char *format, ...)
{
	char String[100];				//定义字符数组
	va_list arg;					//定义可变参数列表数据类型的变量arg va_list是一个类型名 arg是变量名
	va_start(arg, format);			//从format开始,接收参数列表到arg变量
	vsprintf(String, format, arg);	//使用vsprintf打印格式化字符串和参数列表到字符数组中 因为sprintf只能接收直接写的参数
	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标志位
																//读取数据寄存器会自动清除此标志位
																//如果已经读取了数据寄存器,也可以不执行此代码
	}
}

最后一部分自动清零解释: 

19.2.2 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
19.2.3 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);	//显示串口接收的数据
		}
		//不经过函数封装如下
		/*
		if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE) == SET)//与上面函数功能一样 在串口助手发送断写入二位数会直接显示在OLED上 同时会自动清零
		{
			RxData = USART_ReceieveData(USART1);
			OLED_ShowHexNum(1,1,RxData,2);
		}
		*/
	}
}

现象:在串口助手发送位置输入两位数(0~F) 在串口助手和OLED显示屏会分别呈现出该2位数

(接收模式和发送模式都是HEX模式)

  • 26
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值