STM32串口通信之轮询标志位、中断方式以及队列实现

STM32串口通信之队列实现

1、学习资源

学习资源来自于B站优质单片机up铁头山羊,有需要全程学习的同学可以点击如下链接:
铁头山羊教程
铁头山羊

2、为什么要用队列实现串口的消息收发?

为了实现串口消息收发这一目的,我们有许多方式,包括轮训标志位中断收发队列收发,其中。队列收发可看做中断收发的一种升级形式,其能更好的收发消息,避免消息的遗失。下面我们来看一下具体为什么。

2.1 轮训标志位

2.1.1 数据的发送

总代码如下

#include "stm32f10x.h"
#include "stm32f10x_pal.h"
#include <string.h>

int main(void)
{
	GPIO_InitTypeDef GPIOInitStruct;
	
	PAL_Init();
	
	// 初始化Tx PB6 AF_PP 10MHz
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 开启GPIOB的时钟
	GPIOInitStruct.GPIO_Pin = GPIO_Pin_6;
	GPIOInitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIOInitStruct.GPIO_Speed = GPIO_Speed_10MHz;
	GPIO_Init(GPIOB, &GPIOInitStruct);
	
	// 初始化Rx PB7 IPU
	GPIOInitStruct.GPIO_Pin = GPIO_Pin_7;
	GPIOInitStruct.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_Init(GPIOB, &GPIOInitStruct);
	
	// 重映射USART1的Tx和Rx引脚
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //开启AFIO的时钟
	GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE); 
	
	// 使能USART1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
	
	// 设置USART1的参数
	USART_InitTypeDef USARTInitStruct;
	USARTInitStruct.USART_BaudRate = 9600;
	USARTInitStruct.USART_WordLength = USART_WordLength_8b;
	USARTInitStruct.USART_StopBits = USART_StopBits_1;
	USARTInitStruct.USART_Parity = USART_Parity_No;
	USARTInitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	USARTInitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
	
	USART_Init(USART1, &USARTInitStruct);
	
	// 闭合USART1的总开关
	USART_Cmd(USART1, ENABLE);
//	
 发送单个数据	
//	// 1. 等待TDR寄存器清空
//	while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET){}
//		
//	// 2. 写入要发送的数据
//	USART_SendData(USART1, 0x5a);
//		
//	// 3. 等待数据发送完成
//	while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET){}

//
发送一个数组
//  uint8_t a[] = {0,1,2,3,4,5};
//	uint32_t i;
//---------------------------------------------------------------
//	方法一:
//	for(i=0;i<sizeof(a) / sizeof(uint8_t);i++)
//	{
//		// 1. 等待TXE置位
//		while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
//		// 2. 数据写入TDR
//		USART_SendData(USART1, a[i]);
//	}
//	
//	// 3. 等待数据发送完成
//	while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
//------------------------------------------------------------------
//方法二:
//  while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
//	USART_SendData(USART1, 'a');
//	while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);

发送一个字符串:
//  const char *str = "Hello world";
//	uint32_t i;
//	for(i=0; i<strlen(str);i++)
//	{
//		// 1. 等待TXE置位
//		while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
//		// 2. TDR
//		USART_SendData(USART1, str[i]);
//	}
//	// 3. TC置位
//	while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);


  

轮询标志位最大的特点就是在USART/UART初始化时没有对NVIC进行配置,同时在消息收发时采用如下代码对标志位进行检验:
单个数据的发送

	while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET){}
		
	// 2. 写入要发送的数据
	USART_SendData(USART1, 0x5a);
		
	// 3. 等待数据发送完成
	while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET){}

数组的发送


  uint8_t a[] = {0,1,2,3,4,5};
	uint32_t i;
	
	for(i=0;i<sizeof(a) / sizeof(uint8_t);i++)
	{
		// 1. 等待TXE置位
		while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
		// 2. 数据写入TDR
		USART_SendData(USART1, a[i]);
	}
	
	// 3. 等待数据发送完成
	while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
  while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
	USART_SendData(USART1, 'a');
	while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
	

字符串的发送

  const char *str = "Hello world";
	uint32_t i;
	for(i=0; i<strlen(str);i++)
	{
		// 1. 等待TXE置位
		while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
		// 2. TDR
		USART_SendData(USART1, str[i]);
	}
	// 3. TC置位
	while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);

相信看完这些代码,你一定会感到疑惑,这些USART_FLAG_TXE、USART_FLAG_TC究竟是什么?现在让我们来看这张图:
寄存器图
我们跟着上面这张图,当一个数据被发送时,其实就是一个数据被写入TDR(Transmit Data Register)这一寄存器中,之后,会导致状态寄存器中的TXE(Tranimit Pin Empty)被从ReSet情况跳转到Set,当我们的数据全部发送完后TC(Transimit Compete)寄存器便会从ReSet变为Set。

所以,当我们想要发送数据时,其实与之相关的寄存器只有TXE、TC以及TDR寄存器。
我们在发送数据前,先要查看TXE是否是SET,即TX引脚是否已经空了,否则就一直在一个while循环中等待其变为空,
再将数据写入TDR寄存器中,
之后一直等待TC置1,否则一直在一个while循环中等待。

这个过程也就是我们代码中所写的东西,如果由不理解的同学可以回到原视频看铁头山羊的视频进行学习。

现在,我们总结一下发送的步骤:
1.等待TXE置位,确保没有数据在发送;(while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);)
2. 将数据写入TDR寄存器中;(USART_SendData(USART1, a[i]);)
3. 等待TC置位;(while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);)

2.2 数据的接收

说完了数据发送,接下来来讲讲数据的接受:

#include "stm32f10x.h"
#include "stm32f10x_pal.h"
#include <string.h>

int main(void)
{
	GPIO_InitTypeDef GPIOInitStruct;
	
	PAL_Init();
	
	// 初始化Tx PB6 AF_PP 10MHz
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 开启GPIOB的时钟
	GPIOInitStruct.GPIO_Pin = GPIO_Pin_6;
	GPIOInitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIOInitStruct.GPIO_Speed = GPIO_Speed_10MHz;
	GPIO_Init(GPIOB, &GPIOInitStruct);
	
	// 初始化Rx PB7 IPU
	GPIOInitStruct.GPIO_Pin = GPIO_Pin_7;
	GPIOInitStruct.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_Init(GPIOB, &GPIOInitStruct);
	
	// 重映射USART1的Tx和Rx引脚
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //开启AFIO的时钟
	GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE); 
	
	// 使能USART1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
	
	// 设置USART1的参数
	USART_InitTypeDef USARTInitStruct;
	USARTInitStruct.USART_BaudRate = 9600;
	USARTInitStruct.USART_WordLength = USART_WordLength_8b;
	USARTInitStruct.USART_StopBits = USART_StopBits_1;
	USARTInitStruct.USART_Parity = USART_Parity_No;
	USARTInitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	USARTInitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
	
	USART_Init(USART1, &USARTInitStruct);
	
	// 闭合USART1的总开关
	USART_Cmd(USART1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
	GPIOInitStruct.GPIO_Pin = GPIO_Pin_13;
	GPIOInitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
	GPIOInitStruct.GPIO_Speed = GPIO_Speed_2MHz;
	GPIO_Init(GPIOC, &GPIOInitStruct);
	
	GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET); // 熄灭LED

  uint8_t c;
	
	while(1)
	{
		// 1. RXNE
		while(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
		c = USART_ReceiveData(USART1);
		
		if(c == '0')
		{
			 // 熄灭
			GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);
		}
		else if(c=='1')
		{
			 //点亮
			GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET);
		}
		else
		{
		}
	}
}

我们回到这个图中来看看数据的接受是怎么样一个情况:
寄存器
相比较数据的发送,数据的接受只需要用到RXNE这么一个标志位,我们所有的数据接受都将围绕着这么一个标志位进行展开:
首先,从RXNE寄存器的英文名字RX Not Empty可以看出该寄存器类似TXE寄存器,主要用来判断RX引脚是否是空的,当其为1(SET)时,也就是说RX非空,有东西传入,当其为0(RESET)时,也就是RX非空是假的,那么换言之就是RX是空的。
基于此,那么逻辑就很明确了:
我们首先等待RXNE寄存器由0(RESET)变为1(SET),然后将数据读入RDR寄存器中(不知道为什么这张图中标位了TDR),即可完成数据的接受。

3、 中断收发数据

我们在学习一个新的概念之前,始终要持有这样一种态度,那么便是我们为什么要学这个东西,这个东西相比较我们之前所拥有的东西来说有什么优点。
那么我们现在来思考一下轮询标志位有何缺点:

//	// 1. 等待TDR寄存器清空
	while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET){}
		
//	// 2. 写入要发送的数据
	USART_SendData(USART1, 0x5a);
		
//	// 3. 等待数据发送完成
	while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET){}

在这里,我们仅仅以单个数据的发送来进行思考:如果我们一直不发送数据,那么就会一直陷入第一个while循环之中无法再做其他事情,对于只需要发送数据或者接受数据来说,这当然是没有问题的,但是当我们需要在发送数据之外再干一些其他事时,好像就显得有些力不从心了。
因此,我们希望当有东西需要发送或者接收时,我们再调用相关的程序,而不需要时,我们则进行其他任务。有没有这样两全齐美的好事呢?恰巧使用中断的方式来接受数据就能达到这一点:
中断接受
本章我们与前一章节的顺序不同,会先讲中断接受再讲队列实现再讲中断发送,这样的顺序是为了和铁头山羊的视频顺序一致所考虑的,方便各位同学学习。

3.1 串口中断的标志位

寄存器
又回到这个清切的图中,我们在上衣章节中其实仅仅用到了紫色部分的状态寄存器(SR,Situation Register)的部分,而没有使用到蓝色部分的中断使能寄存器(IER,Interruption Enable Register)。其实,SR寄存器都能够产生中断源,但是这个中断源是否能够产生中断信号就得看中断使能寄存器是否愿意。例如,当SR中的TXE置1时,如果IER的TXE IE也为1,那么就能产生一个中断,如果TXEIE是0 ,那么就无法产生中断,他们二者的数值会进行一个“与”那么一个操作。

3.2 串口中断的接受

总代码:

#include "stm32f10x.h"
#include "stm32f10x_pal.h"

static void USART_Recv_Init(void);

int main(void)
{
	GPIO_InitTypeDef GPIOInitStruct;
	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	PAL_Init();
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
	GPIOInitStruct.GPIO_Pin = GPIO_Pin_13;
	GPIOInitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
	GPIOInitStruct.GPIO_Speed = GPIO_Speed_2MHz;
	GPIO_Init(GPIOC, &GPIOInitStruct);
	
	USART_Recv_Init();
	
	while(1){}
}

static void USART_Recv_Init(void)
{
	// 1. 初始化IO引脚
	// PB6 Tx PB7 Rx
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	GPIO_InitTypeDef GPIOInitStruct;
	GPIOInitStruct.GPIO_Pin = GPIO_Pin_6;
	GPIOInitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIOInitStruct.GPIO_Speed = GPIO_Speed_10MHz;
	GPIO_Init(GPIOB, &GPIOInitStruct);
	
	GPIOInitStruct.GPIO_Pin = GPIO_Pin_7;
	GPIOInitStruct.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_Init(GPIOB, &GPIOInitStruct);
	
	// 复用功能重映射
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
	GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE);
	
	// 2. 使能USART1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
	
	// 3. 配置USART的参数
	// 9600 8 No 1 Tx|Rx
	USART_InitTypeDef USARTInitStruct;
	
	USARTInitStruct.USART_BaudRate = 9600;
	USARTInitStruct.USART_WordLength = USART_WordLength_8b;
	USARTInitStruct.USART_Parity = USART_Parity_No;
	USARTInitStruct.USART_StopBits = USART_StopBits_1;
	USARTInitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
	USARTInitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	USART_Init(USART1, &USARTInitStruct);
	
	// 4. 配置中断源
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
	
	NVIC_InitTypeDef NVICInitStruct;
	NVICInitStruct.NVIC_IRQChannel = USART1_IRQn;
	NVICInitStruct.NVIC_IRQChannelPreemptionPriority = 0;
	NVICInitStruct.NVIC_IRQChannelSubPriority = 0;
	NVICInitStruct.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVICInitStruct);
	
	// 5. 闭合USART1的总开关
	USART_Cmd(USART1, ENABLE);
}

void USART1_IRQHandler(void)
{	
	uint8_t c;
	if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
  {
		// 清除中断,读取数据
		c = USART_ReceiveData(USART1);
		
		if(c=='0')
    {
			// 熄灯
			GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);
		}
		else if(c=='1')
		{
			//亮灯
			GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET);
		}
	}
}

我们对比前一章的代码,不难发现相比较而言,多出来的东西仅仅只是在初始化时配置了中断源以及NVIC,并且多了中断函数

初始化时配置中断源以及NVIC

	// 4. 配置中断源
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
	
	NVIC_InitTypeDef NVICInitStruct;
	NVICInitStruct.NVIC_IRQChannel = USART1_IRQn;
	NVICInitStruct.NVIC_IRQChannelPreemptionPriority = 0;
	NVICInitStruct.NVIC_IRQChannelSubPriority = 0;
	NVICInitStruct.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVICInitStruct);

其中,USART_ITConfig是用来选择哪些中断能够产生中断信号,其原理其实是对着之前说的IER寄存器进行赋值操作。由于这里我们仅仅只是需要接受消息,所以我们便选择RXNE这一个寄存器的中断标志位。

中断函数

void USART1_IRQHandler(void)
{	
	uint8_t c;
	if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
  {
		// 清除中断,读取数据
		c = USART_ReceiveData(USART1);
		
		if(c=='0')
    {
			// 熄灯
			GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);
		}
		else if(c=='1')
		{
			//亮灯
			GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET);
		}
	}
}

中断函数中,我们需要先对中断源是哪个进行判断,这里需要说一句,便是这个中断源的清除并不需要像其他外设那样需要我们用ClearFlag之类的api自己手动清除,只需要我们将数据读出即可清除标志位。
由于课程安排,到了这里中断就已经结束了,没有将中断实现数据接受,接下来来到队列实现数据收发

4. 队列实现数据收发

现在,我们来讲讲为什么我们希望使用队列来实现数据的收发。

我们首先来看之前写的中断数据接受的代码:

void USART1_IRQHandler(void)
{	
	uint8_t c; //0.1us
	if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //0.1us
  {
		// 清除中断,读取数据
		c = USART_ReceiveData(USART1); //0.1us
		
		if(c=='0') // 0.1us
    {
			// 熄灯
			GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET); //0.1us
		}
		else if(c=='1') // 0.1 us 
		{
			//亮灯
			GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET); // 0.1us
		}
	}
}

不难看出,这段代码想要实现的功能即为当我们接受到一个数时,我们将他存下来并进行判断,然后如果是0就灭灯,如果是1就开灯。
这样将数据就在中断函数中对数据进行处理的方式便是就地处理,现在让我们又来思考这种方式有什么缺点。

其实,就点灯这一项目来说,就地处理的方式是没有问题的,因为该段代码较为简单,所需要的时间仅仅只是如上代码右侧所需总和0.6us罢了。但是当我们遇到如下的代码情况却遇到了问题。

就地处理在这里插入图片描述

如图所示,我们的消息接受间隔其实是 1 / b a u d 1/baud 1/baud,其近似于0.1ms,当我们想要执行这段代码时,却会由于SendString(a)这一段语句导致会超时。为什么呢?因为当我们发送一个很长的数组时
(橙色区域),可能在发到一半时便又有了新的消息被接受到,从而导致TDR寄存还没发送完数据之后就被打断,重新发送新的数据。

那么,有什么办法解决这一问题呢?

4.1 延迟处理

既然我们在中断函数中进行数据处理的方式可能会导致有些数据在发送时可能会被打断从而导致遗失,那么我们不妨换一种想法,我们将这些数据存储起来,哪怕被打断了,RDR寄存器已经换了新的值,也不会导致我们的数据会被遗失。
那么,这个存储数据的结构需要做到先存起来的数据先被发送,后存储的数据后被发送,满足这点性质的数据结构其实就是队列的FIFO(First In First Out)。

4.1.1 队列的实现

有关于队列的实现在这里不会多讲,而会直接贴出代码,如果有需要可以自己去查相关资料即可。
Queue.c

void Queue_Init(Queue_HandleTypeDef *hQueue)
{
	hQueue->Tail = 0;
}

void Queue_Enqueue(Queue_HandleTypeDef *hQueue, uint8_t Element)
{
	hQueue->Data[hQueue->Tail ++] = Element;
}

ErrorStatus Queue_Dequeue(Queue_HandleTypeDef *hQueue, uint8_t *pElement)
{
	uint32_t i;
	if(hQueue->Tail == 0) return ERROR; // 队列空,操作无效
	*pElement = hQueue->Data[0];
	for(i=0;i<hQueue->Tail-1;i++)
	{
		hQueue->Data[i] = hQueue->Data[i+1];
	}
	hQueue->Tail--;
	return SUCCESS;
}


Queue.h

#ifndef _QUEUE_H_
#define _QUEUE_H_

#include "stm32f10x.h"

typedef struct 
{
	uint8_t Data[100];
	uint16_t Tail; // 队尾
} Queue_HandleTypeDef;

void Queue_Init(Queue_HandleTypeDef *hQueue);
void Queue_Enqueue(Queue_HandleTypeDef *hQueue, uint8_t Element);
ErrorStatus Queue_Dequeue(Queue_HandleTypeDef *hQueue, uint8_t *pElement);

#endif // 防止头文件被重复引用

4.2 队列实现数据发送与接受

总代码

#include "stm32f10x.h"
#include "stm32f10x_pal.h"
#include "queue.h"
#include <string.h>

static Queue_HandleTypeDef hQueue;
static Queue_HandleTypeDef hTxQueue;

static void USART_Echo_Init(void);
static void USART_Echo_Proc(void);
static void USART1_SendString(const char *Str);

int main(void)
{
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	PAL_Init();
	
	USART_Echo_Init();
	
	while(1)
	{
		USART_Echo_Proc();
	}
}

static void USART_Echo_Init(void)
{
	GPIO_InitTypeDef GPIOInitStruct;
	
	Queue_Init(&hQueue);
	Queue_Init(&hTxQueue);
	
	// 1. 初始化IO引脚
	// PB6 PB7
	// Tx
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	GPIOInitStruct.GPIO_Pin = GPIO_Pin_6;
	GPIOInitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIOInitStruct.GPIO_Speed = GPIO_Speed_10MHz;
	GPIO_Init(GPIOB, &GPIOInitStruct);
	// Rx
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	GPIOInitStruct.GPIO_Pin = GPIO_Pin_7;
	GPIOInitStruct.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_Init(GPIOB, &GPIOInitStruct);
	// AFIO
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
	GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE);
	
	// 2. 使能USART1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
	
	// 3. 配置USART1的参数
	USART_InitTypeDef USARTInitStruct;
	USARTInitStruct.USART_BaudRate = 9600;
	USARTInitStruct.USART_WordLength = USART_WordLength_8b;
	USARTInitStruct.USART_Parity = USART_Parity_No;
	USARTInitStruct.USART_StopBits = USART_StopBits_1;
	USARTInitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
	USARTInitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	USART_Init(USART1, &USARTInitStruct);
	
	// 4. 配置中断
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
	
	NVIC_InitTypeDef NVICInitStruct;
	NVICInitStruct.NVIC_IRQChannel = USART1_IRQn;
	NVICInitStruct.NVIC_IRQChannelPreemptionPriority = 0;
	NVICInitStruct.NVIC_IRQChannelSubPriority = 0;
	NVICInitStruct.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVICInitStruct);
	
	// 5. 闭合USART1的总开关
	USART_Cmd(USART1, ENABLE);
}

void USART1_IRQHandler(void)
{
	uint8_t c;
	if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
	{
		c = USART_ReceiveData(USART1);
		
		Queue_Enqueue(&hQueue, c);
	}
	else if(USART_GetITStatus(USART1, USART_IT_TXE) == SET)
	{
		if(Queue_Dequeue(&hTxQueue, &c) == SUCCESS)
		{
			USART_SendData(USART1, c);
		}
		else
		{
			USART_ITConfig(USART1, USART_IT_TXE, DISABLE);
		}
	}
}

static uint8_t a[100];
static uint16_t cursor = 0; //游标

static void USART_Echo_Proc(void)
{
	uint8_t c;
	
	USART_ITConfig(USART1, USART_IT_RXNE, DISABLE);
	ErrorStatus error = Queue_Dequeue(&hQueue, &c);
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
	
	if(error == SUCCESS)
	{
		a[cursor++] = c;
		
		if(cursor>2 && a[cursor-2] == '\r'&& a[cursor - 1] == '\n') // 收到新行
		{
			// 发送出去
			a[cursor] = 0;
			USART1_SendString((const char *)a);
			cursor=0;
		}
	}
}

static void USART1_SendString(const char *Str)
{
	uint32_t i;
	USART_ITConfig(USART1, USART_IT_TXE, DISABLE);
	for(i=0;i<strlen(Str);i++)
	{
		Queue_Enqueue(&hTxQueue, Str[i]);
	}
	USART_ITConfig(USART1, USART_IT_TXE, ENABLE);
}



最为重要的部分为中断函数与SendString函数函数,不难发现其实就是入队与出队即可完成。下面我们来详解整个流程。

int main(void)
{
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	PAL_Init();
	
	USART_Echo_Init();
	
	while(1)
	{
		USART_Echo_Proc();
	}
}

不难发现,在主函数中,其实已经被包装的很包装了,我们实现了各种初始化(中断、PAL库、USART,有关PAL库的内容如果以后有机会可以写一个),并在while循环中进行了Echo的进程函数。

void USART1_IRQHandler(void)
{
	uint8_t c;
	if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
	{
		c = USART_ReceiveData(USART1);
		
		Queue_Enqueue(&hQueue, c);
	}
	else if(USART_GetITStatus(USART1, USART_IT_TXE) == SET)
	{
		if(Queue_Dequeue(&hTxQueue, &c) == SUCCESS)
		{
			USART_SendData(USART1, c);
		}
		else
		{
			USART_ITConfig(USART1, USART_IT_TXE, DISABLE);
		}
	}
}

而中断函数,则是对中断源进行处理:
如果是RXNE中断源置1,我们就将c这个数字读进RDR寄存器,又将这个数读进我们的hQueue这个队列中,以等待后续的处理;
如果是由于TXE置1引起的中断,那么我们就判断hTxQueue这个队列中的元素是否能出队,如果能出队就将出队的元素写入TDR中,如果数据发送完了,此时就无法出队,就清楚中断源。

但是我们知道,RXNE可以由外界的数据决定是否置1,但是TXE怎么置1呢?答案是我们手动对其进行置1。我们转到USART_SendString这个函数中来看一下:

static void USART1_SendString(const char *Str)
{
	uint32_t i;
	USART_ITConfig(USART1, USART_IT_TXE, DISABLE);
	for(i=0;i<strlen(Str);i++)
	{
		Queue_Enqueue(&hTxQueue, Str[i]);
	}
	USART_ITConfig(USART1, USART_IT_TXE, ENABLE);
}

不难发现,这段代码实现的是首先将TXE置为0,这是中断源的初始化,然后,我们将Str这个数组中的元素写入hTxQueue之中,待Str写完之后,我们就开启TXE这一中断源,引起中断函数的开始。

写到这里已经是整篇文章的尾声了,串口这一章节的学习实属不易,市面上存在的教学资源也良莠不齐,我认为这种队列实现延迟处理的方式是最为稳妥的,如果有更好的方法,欢迎各位的讨论!

  • 24
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值