STM32学习笔记07-USART串口通信

目录

通信接口

串口通信

硬件电路

串口参数及时序

USART简介

USART框图

USART基本结构

数据帧

起始位侦测

数据采样

波特率发生器

串口通信应用

串口发送+接收

串口发送数据包

串口发送HEX数据包

串口发送文本数据包


通信接口

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

上表只是列举了最典型的参数,仅仅是最常用、最简单的配置。电平特性上面三个都是单端信号,也就是引脚高低电平都是对GND的电压差,所以单端通信的双方必须要共地,就是把GND接在一起,不接GND是无法通信的。CAN和USB是差分信号,靠两个差分引脚的电压差来传输信号,在通信时可以不需要GND,不过USB协议也有一些地方需要单端信号,所以USB还是需要共地的。差分信号可以极大的提高抗干扰特性,所以差分信号的速度和距离都非常高。 

串口通信

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

 在单片机领域串口是一种最简单的通信接口,它的协议相比较I2C、SPI已经是非常简单的了。一般单片机都会有串口的硬件外设,使用也非常方便。

硬件电路

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

TX和RX是单端信号,它们的高低电平是相对于GND的,所以串口通信TX、RX、GND是必须接的,上面的VCC如果两个设备都有独立供电那VCC可以不接。比如设备1是STM32单片机,设备2是蓝牙串口模块,STM32有独立供电,蓝牙串口没有独立供电,那就需要把蓝牙串口的VCC和STM32的VCC接在一起,STM32通过这根线向蓝牙串口模块供电。 

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

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

串口参数及时序

串口中,每一个字节都装载在一个数据值里,每个数据帧都由起始位、数据位和停止位组成。左边的图数据位8位,代表一个字节的8位。右边这个数据帧位的最后加了一个奇偶校验位,这样数据位总共就是9位,其中有效载荷是前8位代表一个字节。 接下来看串口的参数:

  • 波特率:串口通信的速率。规定串口通信的速率。
  • 起始位:标志一个数据帧的开始,固定为低电平
  • 数据位:数据帧的有效载荷,1为高电平,0为低电平,低位先行
  • 校验位:用于数据验证,根据数据位计算得来
  • 停止位:用于数据帧间隔,固定为高电平

前文提到串口一般是异步通信,需要双方约定一个通信速率,比如我每隔1秒发送1位,那你就得每隔1秒接收一位,如果接收快了就会重复接收某些位,接收慢了会漏掉某些位。

串口的空闲状态是高电平,也就是没有数据传输的时候引脚必须要置高电平作为空闲状态,然后需要传输的时候必须先发送一个起始位,这个起始位必须是低电平,来打破空闲状态的高电平产生一个下降沿,这个下降沿就告诉接收设备,这一帧数据要开始了。如果没有起始位,那当我发送8个1时数据线一直都是高电平没有任何波动,这样接收方不知道我发送了数据。同理在一个字节数据发送完成后,必须要有一个停止位,停止位的作用是用于数据帧间隔,固定为高电平,同时这个停止位也为下一个起始位做准备,如果没有停止位并且数据最后一位为0时,当下次发送新的一帧就无法产生下降沿了。

数据部分,低位先行,比如我要发送一个字节0x0F,也就是二进制0000 1111,低位先行所以数据从低位开始发送,也就是1111 0000 像这样依次放在发送引脚上:

串口采用奇偶校验法,可以判断数据是不是出错了,出错了可以选择丢弃或者要求重传。校验可以选择三种方式:无校验、奇校验、偶校验。无校验就是不需要校验位,奇校验就是包括校验位在内的数据会出现奇数个1,偶校验 就是包括校验位在内的数据会出现偶数个1。检错率不是很高,比如两位同时出错校验不出来。更高的检错率可以了解一下CRC校验。

下面看几个实测的串口时序图:

USART简介

了解完串口协议,接下来看STM32的USART外设。

  • USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步收发器
  • USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里
  • 自带波特率发生器,最高达4.5Mbits/
  • 可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2
  • 可选校验位(无校验/奇校验/偶校验)
  • 支持同步模式、硬件流控制、DMA、智能卡、IrDALIN
  • STM32F103C8T6 USART资源: USART1 USART2 USART3

 波特率发生器就是用来配置波特率的,它其实就是一个分频器,比如我们APB2总线给个72MHz的频率,然后波特率发生器进行一个分频,得到我们想要的波特率时钟,最后在这个时钟下进行收发就是我们指定的通信波特率。

最常用的配置是:波特率9600或115200,数据位8位,停止位1位,无校验。

同步模式就是多了个时钟CLK的输出,硬件流控制是:比如A设备有个TX向B设备的RX发送数据,A设备一直在发,发送太快了,B处理不过来,如果没有硬件流控制B就只能抛弃新数据或者覆盖原数据了,如果有硬件流控制那硬件电路上会多一根线,如果B没准备好接收就置高电平,如果准备好了就置低电平,A接收到B反馈的准备信号就只会在B准备好的时候才发数据,STM32有不过我们一般不用。

DMA是这个串口支持DMA进行数据转运,如果有大量数据进行收发,可以使用DMA转运数据减轻CPU负担。最后智能卡、IrDA、LIN了解即可。

USART1是APB2总线上的设备,USART2、USART3是APB1总线上的。

USART框图

大体上就是数据寄存器和移位寄存器,发送移位寄存器往TX引脚移位,接收移位寄存器从RX引脚移位,当然发送还要加上帧头帧尾,接收还需要剔除帧头帧尾这些操作内部有电路自动执行,我们知道有硬件帮我们做了这些工作就行了。 

现在我们看看串口的引脚:

USART基本结构

波特率发生器用于产生约定的通信速率,时钟来源是PCLK2或者1,经过波特率发生器分频之后产生的时钟通向发送控制器和接收控制器,用来控制发送移位和接收移位,之后由发送寄存器和发送移位寄存器这两个寄存器配合,将数据一位一位地移出去,通过GPIO口的复用输出,输出到TX引脚,产生串口协议规定的波形。当数据由TDR转移到移位寄存器时,会置一个TXE的标志位,判断这个标志位就可以知道是不是可以写下一个数据了。

接收部分也类似,RX引脚的波形通过GPIO输入,在接收控制器控制下。一位一位地移入接收数据寄存器,移完一帧后,数据就会统一转移到接收移位寄存器,在转移的同时置一个RXNE标志位,我们检查这个标志位就可以知道是不是可以接收数据了。 同时这个标志位可以申请中断,这样就可以在收到数据时直接进入中断函数然后快速读取和保存数据。

右边部分有四个寄存器,但是软件层面只有一个DR寄存器可以供我们读写。

数据帧

起始位侦测

根据前文的介绍,串口的输出TX应该是比输入RX简单很多,输出就定时翻转TX引脚的高低电平就行了,但是输入就复杂一些,你不仅要保证输入的采样频率和波特率一致,还要保证每次输入采样的位置,要正好处于每一位的中间,这样高低电平读进来才是最可靠的,如果采样点过于靠前或者靠后,那有可能高低电平还在翻转,电平还不稳定或者稍有偏差数据就采样错了。另外输入最好对噪声有判断能力,如果是噪声最好置标志位提醒我,这些就是输入数据面临的问题。那我们来看一下STM32是如何来设计输入电路的。

首先是USART起始位侦测,当输入电路侦测到一个数据帧的起始位后,就会以波特率的频率连续采样一帧数据,同时从起始位开始采样位置就要对齐到位的正中间,只要第一位对齐了后面肯定是对齐的,那为了实现这些功能,首先输入的这部分电路对采样时钟进行了细分,它会以波特率的16倍频率进行采样,也就是在1位的时间里,可以进行16次采样。

如果通过了这个起始位侦测,那接收状态就由空闲转变为接收起始位,同时第8、9、10次采样的位置就正好是起始位的正中间,之后接收数据位时就都在第8、9、10次进行采样,这样就能保证采样位置在位的正中间了。 

数据采样

紧接着我们就可以看数据采样的流程了。

在一个数据位,有16个采样时钟,由于起始位侦测已经对齐了采样时钟,所以直接在8、9、10次采样数据位,为了保证数据的可靠性,这里是连续采样三次,没有噪声的情况下肯定是全0或者全1,全为1认为收到了1,全为0认为收到了0。如果有噪声,导致3次采样不是全为0或者全为1,那就按照2:1的规则来,2次为1就认为收到1,两次0就认为收到0。这种情况下噪声标志位NE也会置1,告诉你我收到了数据但是有噪声。

波特率发生器

  • 发送器和接收器的波特率由波特率寄存器BRR里的DIV确定
  • 计算公式:波特率 = fPCLK2或 /  (16 * DIV)

现在应该明白为什么波特率计算为什么多个16,看 数据采样的图就明白了,因为他内部还有一个16倍波特率的采样时钟,所以这里输入时钟/DIV=16倍波特率,最后计算波特率自然要多除个16了。

举个例子,我要配置USART1(时钟为72MHz)为9600波特率,那BRR寄存器配置如下:

  • 9600 = 72M/(16*DIV)

解得DIV=72M/9600/16,最终等于468.75,二进制也就是:11101 0111.11

所以最终写到这个寄存器的就是整数部分(前面多出来补0)000111010100,小数部分1100(后面多出来补0)。不过我们用库函数配置就非常方便,需要多少波特率直接写就行了库函数会自动帮我们算。

串口通信应用

串口发送+接收

我们计划用USART1进行通信。引脚选PA9和PA10。PA9是STM32的TX,发送,接到串口模块的RXD接收。串口模块的TXD,发送,接在STM32的PA10也就是RX,接收。两个设备把GND接在一起共地。

 

 给串口建立模块叫Serial,在Serial.c的Serial_Init()写串口初始化,初始化步骤根据USART基本结构图:

初始化完成后,如果要发送数据,调用一个发送函数就行了,如果要接收数据就调用接收的函数,如果要获取发送和接收的状态,就调用获取标志位的函数。

首先看一下USART的库函数:

void USART_DeInit(USART_TypeDef* USARTx);//恢复缺省配置
void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);//初始化
void USART_StructInit(USART_InitTypeDef* USART_InitStruct);//结构体初始化
void USART_Cmd(USART_TypeDef* USARTx, FunctionalState NewState);//使能USART
void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState);//启用或禁用指定的USART中断
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);//发送数据,写DR寄存器
uint16_t USART_ReceiveData(USART_TypeDef* USARTx);//接收数据,读DR寄存器
FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG);//检查是否设置了指定的USART标志。
void USART_ClearFlag(USART_TypeDef* USARTx, uint16_t USART_FLAG);//清除USARTx的挂起标志。
ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT);//检查指定的USART中断是否发生。
void USART_ClearITPendingBit(USART_TypeDef* USARTx, uint16_t USART_IT);//清除USARTx的中断挂起位。

第一步RCC开启USART和GPIO时钟第二步GPIO初始化,把TX配置成复用输出,RX配置成输入。前两步已经很熟悉了:

/* 第一步RCC开启USART和GPIO时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
/* 第二步GPIO初始化,把TX配置成复用输出,RX配置成输入 */
GPIO_InitTypeDef GPIO_InitStructure;
/* 初始化TX PA9 */
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);
/* 初始化RX PA10 */
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_Init(),第一个参数是USART1,第二个参数是结构体,有六个成员:

  • USART_BaudRate,波特率
  • USART_HardwareFlowControl,硬件流控制
  • USART_Mode,串口模式
  • USART_Parity,校验位
  • USART_StopBits,停止位
  • USART_WordLength,字长

 我们选择9600波特率,不需要硬件流控制,需要同时开启发送和接收,无校验,1位停止位,8位字长。

/* 第三步配置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);//开启RXNE标志位到NVIC的输出
/* 配置NVIC */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//分组
//初始化NVIC的USART1通道
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。

/* 第五步,开启USART*/
USART_Cmd(USART1,ENABLE);

接下来我们封装串口发送函数:

首先是TX引脚发送一个字节数据的函数:

void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1,Byte);
    // 等待TDR的数据转移到移位寄存器(TXE标志位置1),要不然数据还在TDR进行等待,我们再写入就会数据覆盖
	while (USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
    //不需要手动清零,我们再次发送时这个标志位自动清零。
}

 接下来对SendByte进行封装:

/* 发送数组 */
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
	uint16_t i;
	for (i = 0;i < Length;i++)
	{
		Serial_SendByte(Array[i]);
	}
}

/* 发送字符串 */
void Serial_SendString(char *String)
{
	uint8_t i;
	for (i = 0;String[i] != '\0';i++)
	{
		Serial_SendByte(String[i]);
	}
}

/* x的y次方 */
uint32_t Serial_Pow(uint32_t x, uint32_t y)
{
	uint32_t Result = 1;
	while (y--)
	{
		Result *= x;
	}
	return Result;
}

/* 发送数字,电脑显示字符串形式的数字 */
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
	uint8_t i;
	for (i = 0;i < Length;i++)
	{
		/* 
			12345 / 10^4 % 10 = 1
			12345 / 10^3 % 10 = 2
			12345 / 10^2 % 10 = 3...
		*/
		Serial_SendByte(Number / Serial_Pow(10,Length - i - 1) % 10 + '0');
	}
}

下面介绍一下C语言printf函数的移植方法:

首先打开工程选项,把Use MicroLIB勾选上。

接下来对printf函数进行重定向,将printf函数打印的东西输出到串口。printf函数默认输出到屏幕,我们的单片机没有屏幕。 步骤就是在串口模块最开始加上:

#include <stdio.h>

在末尾重写fputc函数,fputc是printf函数的底层,printf函数是不断调用fputc函数一个个打印的:

int fputc(int ch,FILE *f)
{
	Serial_SendByte(ch);
	return ch;
}

然后就可以在主函数使用printf了。这种方法printf只能有一个,你重定向到串口1,那串口2再用就没有了。如果多个串口都想用printf,就可以用sprintf,它可以把格式化字符输出到一个字符串里,第一个参数是打印输出的位置,后面的参数和printf函数一样,最后通过一个Serial_SendString把字符串String通过串口发送出去:

char String[100];
printf(String, "\r\nNum3=%d",333);
Serial_SendString(String);

最后在介绍一种方法,因为sprintf每次都得先定义字符串,再打印到字符串,再发送字符串,太麻烦了,我们要是能封装这个过程就再好不过了:

在串口模块先添加头文件:

#include <stdarg.h>

 在最后进行封装:

void Serial_Printf(char *format, ...)
{
	char String[100];
	va_list arg;
	va_start(arg, format);
	vsprintf(String, format, arg);
	va_end(arg);
	Serial_SendString(String);
}

在主函数就可以这样调用:

Serial_Printf("\r\nNum4=%d",444);
Serial_Printf("\r\n");

 以上就是printf函数封装的介绍,最常用还是第一种方法。

接下来开始写串口接收部分。

对与串口接收可以使用查询和中断两种方法,先看一下查询:

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

while(1)
{
		/* 查询方法 */
		if (USART_GetFlagStatus(USART1,USART_FLAG_RXNE) == SET)
		{
			RxData = USART_ReceiveData(USART1);
			OLED_ShowHexNum(1, 1, RxData, 2);
            //对USART_DR的读操作可以将该位清零,不需要手动清除
		}
}

 程序比较简单可以考虑查询方法,我们重点掌握中断方法:

我们在第四步配置了中断,接下来我们写中断函数,名字我们在启动文件找

void USART1_IRQHandler(void)
{
	if (USART_GetITStatus(USART1,USART_IT_RXNE) == SET)
	{
        //可以在这里直接读取DR,执行一些操作,用于这个是在模块里,不太适合加入过多其他代码
        //选择封装
		Serial_RxData = USART_ReceiveData(USART1); //先读取到模块的变量里
		Serial_RxFlag = 1; //读完之后置个自己的标志位
		USART_ClearITPendingBit(USART1,USART_IT_RXNE);
	}
}

 然后对变量封装Get函数,其实就是在中断里对数据进行了一次转存,最终还是要扫描查询RxFlag来接收数据,对于单字节接收转存意义不大,这里这样主要是演示中断接收的操作方法,也为下一个多总结接收做铺垫。整体如下:

uint8_t Serial_GetRxFlag(void)
{
	if (Serial_RxFlag == 1)//自己的标志位读后自动清除的功能
	{
		Serial_RxFlag = 0;
		return 1;
	}
	return 0;
}

uint8_t Serial_GetRxData(void)
{
	return Serial_RxData;
}

void USART1_IRQHandler(void)
{
	if (USART_GetITStatus(USART1,USART_IT_RXNE) == SET)
	{
		Serial_RxData = USART_ReceiveData(USART1);
		Serial_RxFlag = 1;
		USART_ClearITPendingBit(USART1,USART_IT_RXNE);
	}
}

 完整代码:

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开启USART和GPIO时钟 */
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	/* 第二步GPIO初始化,把TX配置成复用输出,RX配置成输入 */
	GPIO_InitTypeDef GPIO_InitStructure;
	/* 初始化TX PA9 */
	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);
	/* 初始化RX PA10 */
	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;//波特率
	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);//开启RXNE标志位到NVIC的输出
	/* 配置NVIC */
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//分组
	//初始化NVIC的USART1通道
	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*/
	USART_Cmd(USART1,ENABLE);
}

void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1,Byte);
	while (USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
}

/* 发送数组 */
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
	uint16_t i;
	for (i = 0;i < Length;i++)
	{
		Serial_SendByte(Array[i]);
	}
}

/* 发送字符串 */
void Serial_SendString(char *String)
{
	uint8_t i;
	for (i = 0;String[i] != '\0';i++)
	{
		Serial_SendByte(String[i]);
	}
}

/* x的y次方 */
uint32_t Serial_Pow(uint32_t x, uint32_t y)
{
	uint32_t Result = 1;
	while (y--)
	{
		Result *= x;
	}
	return Result;
}

/* 发送数字,电脑显示字符串形式的数字 */
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
	uint8_t i;
	for (i = 0;i < Length;i++)
	{
		/* 
			12345 / 10^4 % 10 = 1
			12345 / 10^3 % 10 = 2
			12345 / 10^2 % 10 = 3...
		*/
		Serial_SendByte(Number / Serial_Pow(10,Length - i - 1) % 10 + '0');
	}
}

int fputc(int ch,FILE *f)
{
	Serial_SendByte(ch);
	return ch;
}

void Serial_Printf(char *format, ...)
{
	char String[100];
	va_list arg;
	va_start(arg, format);
	vsprintf(String, format, arg);
	va_end(arg);
	Serial_SendString(String);
}

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

uint8_t Serial_GetRxData(void)
{
	return Serial_RxData;
}

void USART1_IRQHandler(void)
{
	if (USART_GetITStatus(USART1,USART_IT_RXNE) == SET)
	{
		Serial_RxData = USART_ReceiveData(USART1);
		Serial_RxFlag = 1;
		USART_ClearITPendingBit(USART1,USART_IT_RXNE);
	}
}

main.c:

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

uint8_t RxData;

int main(void)
{
	OLED_Init();
	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);//printf封装1

	char String[100];
	sprintf(String, "\r\nNum3=%d",333);//printf封装2
	Serial_SendString(String);
	
	Serial_Printf("\r\nNum4=%d",444);//printf封装3
	Serial_Printf("\r\n");
	
	OLED_ShowString(1, 1, "RxData:");
	while(1)
	{
		/* 查询方法 */
//		if (USART_GetFlagStatus(USART1,USART_FLAG_RXNE) == SET)
//		{
//			RxData = USART_ReceiveData(USART1);
//			OLED_ShowHexNum(1, 1, RxData, 2);
//		}
		if(Serial_GetRxFlag() == 1)//接收
		{
			RxData = Serial_GetRxData();
			Serial_SendByte(RxData);//数据回传,接收到的数据回传到电脑
			OLED_ShowHexNum(1, 8, RxData, 2);
		}
		
	}
}


串口发送数据包

上一个应用只支持1个字节的接收,现在很多模块都需要回传大量数据,这时候就需要数据包的形式进行传输,接收部分也需要按数据包格式来接收,这样才能接收多字节数据包。数据包的作用是把一个个数据打包起来,方便我们进行多字节数据通信。

串口发送HEX数据包

 

使用状态机的方法接收一个数据包。 在上一个代码的基础上,为了收发数据包先定义两个缓冲区数组存储发送或接收的载荷数据,包头包尾就不存了:

uint8_t Serial_TxPacket[4];
uint8_t Serial_RxPacket[4];

头文件里声明为外部可调用: 

extern uint8_t Serial_TxPacket[];
extern uint8_t Serial_RxPacket[];

初始化代码和发送的模块不需要更改。先写一个SendPacket函数,我们想要调用一下这个函数,TxPactet数组里的四个数据就会自动加上包头包尾发送出去:

void Serial_SendPacket(void)
{
	Serial_SendByte(0xFF);
	Serial_SendArray(Serial_TxPacket, 4);
	Serial_SendByte(0xFE);
}

 接收中断函数里,我们需要用状态机了执行接收逻辑,接收数据包然后把载荷数据存在RxPactet数组里。

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.c:

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

uint8_t Serial_TxPacket[4];
uint8_t Serial_RxPacket[4];
uint8_t Serial_RxFlag;

void Serial_Init(void)
{
	/* 第一步RCC开启USART和GPIO时钟 */
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	/* 第二步GPIO初始化,把TX配置成复用输出,RX配置成输入 */
	GPIO_InitTypeDef GPIO_InitStructure;
	/* 初始化TX PA9 */
	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);
	/* 初始化RX PA10 */
	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;//波特率
	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);//开启RXNE标志位到NVIC的输出
	/* 配置NVIC */
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//分组
	//初始化NVIC的USART1通道
	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 如果需要接收可能还需要配置中断*/
	USART_Cmd(USART1,ENABLE);
}

void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1,Byte);
	while (USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
}

void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
	uint16_t i;
	for (i = 0;i < Length;i++)
	{
		Serial_SendByte(Array[i]);
	}
}

void Serial_SendString(char *String)
{
	uint8_t i;
	for (i = 0;String[i] != '\0';i++)
	{
		Serial_SendByte(String[i]);
	}
}

uint32_t Serial_Pow(uint32_t x, uint32_t y)
{
	uint32_t Result = 1;
	while (y--)
	{
		Result *= x;
	}
	return Result;
}

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');
	}
}

int fputc(int ch,FILE *f)
{
	Serial_SendByte(ch);
	return ch;
}

void Serial_Printf(char *format, ...)
{
	char String[100];
	va_list arg;
	va_start(arg, format);
	vsprintf(String, format, arg);
	va_end(arg);
	Serial_SendString(String);
}

void Serial_SendPacket(void)
{
	Serial_SendByte(0xFF);
	Serial_SendArray(Serial_TxPacket, 4);
	Serial_SendByte(0xFE);
}

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



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);//清除标志位
	}
}


main.c:

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

uint8_t RxData;
uint8_t KeyNum;

int main(void)
{
	OLED_Init();
	Key_Init();
	Serial_Init();
	
	OLED_ShowString(1, 1, "TxPacket");
	OLED_ShowString(3, 1, "RxPacket");
	
	Serial_TxPacket[0] = 0x01;
	Serial_TxPacket[1] = 0x02;
	Serial_TxPacket[2] = 0x03;
	Serial_TxPacket[3] = 0x04;
	
	while(1)
	{
		KeyNum = Key_GetNum();
		if(KeyNum == 1)
		{
			Serial_TxPacket[0] ++;
			Serial_TxPacket[1] ++;
			Serial_TxPacket[2] ++;
			Serial_TxPacket[3] ++;
			
			Serial_SendPacket();//发送数据包
			
			OLED_ShowHexNum(2, 1, Serial_TxPacket[0], 2);
			OLED_ShowHexNum(2, 4, Serial_TxPacket[1], 2);
			OLED_ShowHexNum(2, 7, Serial_TxPacket[2], 2);
			OLED_ShowHexNum(2, 10, Serial_TxPacket[3], 2);
		}
		if(Serial_GetRxFlag() == 1)//收到数据包
		{
			OLED_ShowHexNum(4, 1, Serial_RxPacket[0], 2);
			OLED_ShowHexNum(4, 4, Serial_RxPacket[1], 2);
			OLED_ShowHexNum(4, 7, Serial_RxPacket[2], 2);
			OLED_ShowHexNum(4, 10, Serial_RxPacket[3], 2);
		}
		
	}
}


串口发送文本数据包

串口发送指令控制LED,并且会接收到反馈回的LED状态。

 同样定义接收缓冲区:

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 == '@')//等待包头
			{
				RxState = 1;
				pRxPacket = 0;
			}
		}
		else if(RxState == 1)//载荷数据长度不确定,接收之前判断是不是包尾
		{
			if(RxData == '\r')//是第一部分包尾(\r\n是包尾),状态转移
			{
				RxState = 2;
			}
			else//不是包尾,接收数据
			{
				Serial_RxPacket[pRxPacket] = RxData;
				pRxPacket ++;
			}
		}
		else if(RxState == 2)//等待第二个包尾
		{
			if(RxData == '\n')
			{
				RxState = 0;
				Serial_RxPacket[pRxPacket] = '\0';//加字符串结束标志位
				Serial_RxFlag = 1;
			}
		}
		USART_ClearITPendingBit(USART1,USART_IT_RXNE);
	}
}

 完整代码:

Serial.c:

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


char  Serial_RxPacket[100];
uint8_t Serial_RxFlag;

void Serial_Init(void)
{
	/* 第一步RCC开启USART和GPIO时钟 */
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	/* 第二步GPIO初始化,把TX配置成复用输出,RX配置成输入 */
	GPIO_InitTypeDef GPIO_InitStructure;
	/* 初始化TX PA9 */
	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);
	/* 初始化RX PA10 */
	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;//波特率
	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);//开启RXNE标志位到NVIC的输出
	/* 配置NVIC */
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//分组
	//初始化NVIC的USART1通道
	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 如果需要接收可能还需要配置中断*/
	USART_Cmd(USART1,ENABLE);
}

void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1,Byte);
	while (USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
}

void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
	uint16_t i;
	for (i = 0;i < Length;i++)
	{
		Serial_SendByte(Array[i]);
	}
}

void Serial_SendString(char *String)
{
	uint8_t i;
	for (i = 0;String[i] != '\0';i++)
	{
		Serial_SendByte(String[i]);
	}
}

uint32_t Serial_Pow(uint32_t x, uint32_t y)
{
	uint32_t Result = 1;
	while (y--)
	{
		Result *= x;
	}
	return Result;
}

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');
	}
}

int fputc(int ch,FILE *f)
{
	Serial_SendByte(ch);
	return ch;
}

void Serial_Printf(char *format, ...)
{
	char String[100];
	va_list arg;
	va_start(arg, format);
	vsprintf(String, format, arg);
	va_end(arg);
	Serial_SendString(String);
}


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



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 == '@')//等待包头
			{
				RxState = 1;
				pRxPacket = 0;
			}
		}
		else if(RxState == 1)//载荷数据长度不确定,接收之前判断是不是包尾
		{
			if(RxData == '\r')//是第一部分包尾(\r\n是包尾),状态转移
			{
				RxState = 2;
			}
			else//不是包尾,接收数据
			{
				Serial_RxPacket[pRxPacket] = RxData;
				pRxPacket ++;
			}
		}
		else if(RxState == 2)//等待第二个包尾
		{
			if(RxData == '\n')
			{
				RxState = 0;
				Serial_RxPacket[pRxPacket] = '\0';//加字符串结束标志位
				Serial_RxFlag = 1;
			}
		}
		USART_ClearITPendingBit(USART1,USART_IT_RXNE);
	}
}


main.c:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
#include "LED.h"
#include <string.h>

uint8_t RxData;


int main(void)
{
	OLED_Init();
	LED_Init();
	Serial_Init();
	
	OLED_ShowString(1, 1, "TxPacket");
	OLED_ShowString(3, 1, "RxPacket");

	
	while(1)
	{
		
		if(Serial_GetRxFlag() == 1)
		{
			OLED_ShowString(4, 1, "                ");
			OLED_ShowString(4, 1, Serial_RxPacket);
			
			if(strcmp(Serial_RxPacket, "LED_ON") == 0)
			{
				LED1_ON();
				Serial_SendString("LED_ON_OK\r\n");
				OLED_ShowString(2, 1, "                ");//擦除第四行再显示
				OLED_ShowString(2, 1, "LED_ON_OK");
				
			}
			else if(strcmp(Serial_RxPacket, "LED_OFF") == 0)
			{
				LED1_OFF();
				Serial_SendString("LED_OFF_Ok\r\n");
				OLED_ShowString(2, 1, "                ");
				OLED_ShowString(2, 1, "LED_OFF_OK");
			}
			else
			{
				Serial_SendString("ERROR_COMMAND\r\n");
				OLED_ShowString(2, 1, "                ");
				OLED_ShowString(2, 1, "ERROR_COMMAND");
			}
		}
	}
}


  • 20
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值