教你手写串口收发数据(看完这篇你就会手动写啦,保姆级讲解)---- 2020.3.28

关于串口通信原理方面的文章:

嵌入式stm32 复习(工作用)— USART(串口)通信原理知识 2020.3.23
添加链接描述

先上完整串口收发部分代码!!!

#include "usart1.h"
#include "stdio.h"
#include "string.h"

#define CR1_OVER8_Set             ((u16)0x8000)  

u16 USART_GetBound(USART_TypeDef* USARTx, int bound) {
	uint32_t integerdivider = 0x00, apbclock = 0x00;
	uint32_t tmpreg = 0x00;
	uint32_t fractionaldivider = 0x00;

	apbclock = 72000000; //如是USART1是72M,否则是36M

	/* Determine the integer part */
	if ((USARTx->CR1 & CR1_OVER8_Set) != 0) {
		/* Integer part computing in case Oversampling mode is 8 Samples */
		integerdivider = ((25 * apbclock) / (2 * bound));
	} else /* if ((USARTx->CR1 & CR1_OVER8_Set) == 0) */
	{
		/* Integer part computing in case Oversampling mode is 16 Samples */
		integerdivider = ((25 * apbclock) / (4 * bound));
	}
	tmpreg = (integerdivider / 100) << 4;

	/* Determine the fractional part */
	fractionaldivider = integerdivider - (100 * (tmpreg >> 4));

	/* Implement the fractional part in the register */
	if ((USARTx->CR1 & CR1_OVER8_Set) != 0) {
		tmpreg |= ((((fractionaldivider * 8) + 50) / 100)) & ((uint8_t) 0x07);
	} else /* if ((USARTx->CR1 & CR1_OVER8_Set) == 0) */
	{
		tmpreg |= ((((fractionaldivider * 16) + 50) / 100)) & ((uint8_t) 0x0F);
	}
	return tmpreg;

}

/**
 * @brief  USART1 初始化
 * @param  None
 * @retval None
 */
void USART1_Init(int bound) {

	//1.使能时钟
	RCC->APB2ENR |= 1 << 2;	    //GPIOA 时钟使能
	RCC->APB2ENR |= 1 << 14;	//USART1 时钟使能

	//2.初始化GPIO
	GPIOA->CRH &= ~(0x0F << 4);
	GPIOA->CRH |= 0x0B << 4;	//设置GPIOA.9 -> 50MHz,复用推免

	GPIOA->CRH &= ~(0x0F << 8);
	GPIOA->CRH |= 0x04 << 8;	//设置GPIOA.10 -> 浮空输入模式

	//3.初始化USART1
	//3.1 设置波特率
	USART1->BRR = USART_GetBound(USART1, bound);// 0x1D4 << 4 | 0x4B;
	//设置波特率9600
	//3.2 设置校验位
	USART1->CR1 &= ~(1 << 10);			//不使用校验位
	//3.3 数据位
	USART1->CR1 &= ~(1 << 12);			//8位长度
	//3.4 停止位
	USART1->CR2 &= ~(0x03 << 12);		//1个停止位
	//4.使能
	USART1->CR1 |= 1 << 3;			    //使能TX
	USART1->CR1 |= 1 << 2;			    //使能RX
	USART1->CR1 |= 1 << 13;			    //使能USART1

	//配置数据接收时候,需要用到中断
	//5.配置NVIC
	//5.1 先分组
	NVIC_SetPriorityGrouping(5);
	NVIC_EnableIRQ(USART1_IRQn);		//使能USART1的全局中断
	NVIC_SetPriority(USART1_IRQn, 10);	//设置USART1的中断优先级是10

	//6 使能接受数据中断寄存器
	USART1->CR1 |= 1 << 5;
	USART1->CR1 |= 1 << 4;			    //开启IDLE中断


}

u8 USART1_REC[USART1_REC_MAX];
//USART1_STA[15] 标识是否接收完成状态位,1:标识接收完成;0:未接收完成;
//USART1_STA[14] 标识是否为第一个数据,1:非第一个数据;0:表示第一个数据;
//USART1_STA[13:0] 标识有效的数据接收长度
u16 USART1_STA = 0;

/**
 * @brief  USART1 IRQ 中断处理,注意,每次接收新数据时,会清空旧数据
 * @param  None
 * @retval None
 */
void USART1_IRQHandler(void) {

	u8 buf = 0;

	if (USART1->SR & 0x20) {	//当接收中断产生

		if (!(USART1_STA & 0x4000)) {	//第一个数据
			USART1_STA = 0x4000;	    //表示下次接收,就不再是第一个数据,
			                            //清除掉了数据接收长度
			memset(USART1_REC, 0, USART1_REC_MAX);
		}

		buf = USART1->DR;
		USART1_REC[USART1_STA & 0x3FFF] = buf;
		USART1_STA++;

		if ((USART1_STA & 0x3FFF) >= USART1_REC_MAX) {	//达到将要满载状态
			USART1_STA = 0;	//覆盖原本数据
		}
	}

	if (USART1->SR & 0x10) {	//总线空闲
		USART1_STA |= 0x8000;	//标识接收成功
		USART1_STA &= ~0x4000;	//清除14位
		USART1_REC[USART1_STA & 0x3FFF] = '\0';	//保证有效字符串
		buf = USART1->DR;	//清空状态
	}

}

/**
 * @brief  USART1 发送len长度的char数据
 * @param  data:char *,字符串;len:u8,长度;
 * @retval None
 */
void USART1_SendData(u8 *data, u8 len) {
	u8 i = 0;

	for (i = 0; i < len; i++) {
		if (*(data + i) == '\0')	//空白符号无需发送
			return;
		//判断是否允许发送数据
		while ((USART1->SR & 0x40) == 0)
			;
		USART1->DR = *(data + i);	//等效于下面的函数
//		USART_SendData(USART1, *(data + i));
	}
}


/**
 * @brief  实现printf重定向
 * @param  int ch, FILE* f
 * @retval None
 */
int fputc(int ch, FILE* f) {
	while ((USART1->SR & 0x40) == 0)
		;
	USART1->DR = (u8) ch;	//等效于下面的函数
	return ch;
}

好!按照老样子,接下来开始详细讲解每行代码的用处,以及为什么这样写!

获取波特率部分

u16 USART_GetBound(USART_TypeDef* USARTx, int bound) {
	uint32_t integerdivider = 0x00, apbclock = 0x00;
	uint32_t tmpreg = 0x00;
	uint32_t fractionaldivider = 0x00;

	apbclock = 72000000; //如是USART1是72M,否则是36M

	/* Determine the integer part */
	if ((USARTx->CR1 & CR1_OVER8_Set) != 0) {
		/* Integer part computing in case Oversampling mode is 8 Samples */
		integerdivider = ((25 * apbclock) / (2 * bound));
	} else /* if ((USARTx->CR1 & CR1_OVER8_Set) == 0) */
	{
		/* Integer part computing in case Oversampling mode is 16 Samples */
		integerdivider = ((25 * apbclock) / (4 * bound));
	}
	tmpreg = (integerdivider / 100) << 4;

	/* Determine the fractional part */
	fractionaldivider = integerdivider - (100 * (tmpreg >> 4));

	/* Implement the fractional part in the register */
	if ((USARTx->CR1 & CR1_OVER8_Set) != 0) {
		tmpreg |= ((((fractionaldivider * 8) + 50) / 100)) & ((uint8_t) 0x07);
	} else /* if ((USARTx->CR1 & CR1_OVER8_Set) == 0) */
	{
		tmpreg |= ((((fractionaldivider * 16) + 50) / 100)) & ((uint8_t) 0x0F);
	}
	return tmpreg;

}

//这里我们为了方便改变输入发热波特率值,我们可以知道波特率大小可以为4800,9600,115200,所以我们这里需要一个获取波特率值得函数,只不过我们用的是寄存器来写,所以我们必须找到USART中获取波特率的代码部分。
//由32芯片手册可以得知,波特率寄存器为USART_BRR。
//
在这里插入图片描述//
在这里插入图片描述//

在这里插入图片描述串口初始化部分

//1.使能时钟
RCC->APB2ENR |= 1 << 2;	    //GPIOA 时钟使能
RCC->APB2ENR |= 1 << 14;	//USART1 时钟使能

//
在这里插入图片描述
//由上图我们可以得知,串口1用的端口是GPIOA,所以我们这里需要开启时钟。
在这里插入图片描述//这里我们就得知USART1和GPIOA是在APB2总线下的。
//
在这里插入图片描述//在这里插入图片描述

//2.初始化GPIO
GPIOA->CRH &= ~(0x0F << 4);
GPIOA->CRH |= 0x0B << 4;	//设置GPIOA.9 -> 50MHz,复用推免

//首先先将这四位清零然后再赋值。
//因为PA9为TX(发送数据)位,所以为输出模式,这里我们将该端口设置为复用推挽输出。
//
在这里插入图片描述//所以这四位为1011。
//在这里插入图片描述//
在这里插入图片描述
//由计算器得知转换为16进制为0x0B。又因为PA9端口位于第四位,所以这里左移四位,之后如果是同样操作的话不再赘述~

GPIOA->CRH &= ~(0x0F << 8);
GPIOA->CRH |= 0x04 << 8;	//设置GPIOA.10 -> 浮空输入模式

//同理,PA10为RX(接收数据位),那么该端口为输入模式,这里我们设置为浮空输入模式。
//
在这里插入图片描述//
在这里插入图片描述
//所以这四位为0100,用计算器得知为0x04且左移8位。

//
在这里插入图片描述//这里借用下正点原子的串口调试助手,通过上面这张图我们也已经得知波特率、停止位、数据位、奇偶校验这四个需要我们进行配置的参数。所以我们在接下来的配置串口过程中只需要配置这几个参数即可。
//
在这里插入图片描述

//3.初始化USART1
//3.1 设置波特率
USART1->BRR = USART_GetBound(USART1, bound);// 0x1D4 << 4 | 0x4B;

//由前面我们已经成功创建一个关于求解波特率的函数,这里直接调用即可。

//3.2 设置校验位
USART1->CR1 &= ~(1 << 10);			//不使用校验位

/这里我们不需要校验位。
//
在这里插入图片描述

//3.3 数据位
USART1->CR1 &= ~(1 << 12);			//8位长度

//一般我们传输数据的长度就为8位。
//
在这里插入图片描述

//3.4 停止位
USART1->CR2 &= ~(0x03 << 12);		//1个停止位

//这里我们设置为1个停止位。
//
在这里插入图片描述

//4.使能
USART1->CR1 |= 1 << 3;			    //使能TX
USART1->CR1 |= 1 << 2;			    //使能RX
USART1->CR1 |= 1 << 13;			    //使能USART1

//
在这里插入图片描述//
在这里插入图片描述
//最终使能RX, TX,USART1。

//配置数据接收时候,需要用到中断
//5.配置NVIC
//5.1 先分组
NVIC_SetPriorityGrouping(5);
NVIC_EnableIRQ(USART1_IRQn);		//使能USART1的全局中断
NVIC_SetPriority(USART1_IRQn, 10);	//设置USART1的中断优先级是10

//因为我们最后需要一个串口中断来接收数据,所以通过前几篇文章我们可以知道使用中断的话需要使用到NVIC(中断嵌套向量控制器)。

//具体怎样设置看前几篇文章就清楚了~

//6 使能接受数据中断寄存器
USART1->CR1 |= 1 << 5;
USART1->CR1 |= 1 << 4;			    //开启IDLE中断

//因为我们需要一个中断来接收数据,所以需要开启接收中断。
//
在这里插入图片描述//
在这里插入图片描述//在这里插入图片描述//
在这里插入图片描述串口接收数据部分

很重要

u8 buf = 0;

//因为我们从上面已经得知,串口数据收发时的数据长度为8位,所以这里定义为8位,且buf作为串口接收数据的缓冲区。

if (USART1->SR & 0x20) {	//当接收中断产生

//
在这里插入图片描述//由上图得知,当RDR移位寄存器中的数据被转移到USART_DR寄存器中,该位被硬件置位,也就是该位被置一,代表收到数据,可以读出。

//在这里插入图片描述//那么我们这里判断是否有接收中断产生,则只需要判断该位的状态即可。
//
在这里插入图片描述u8 USART1_REC[USART1_REC_MAX];
//USART1_STA[15] 标识是否接收完成状态位,1:标识接收完成;0:未接收完成;
//USART1_STA[14] 标识是否为第一个数据,1:非第一个数据;0:表示第一个数据;
//USART1_STA[13:0] 标识有效的数据接收长度
u16 USART1_STA = 0;

if (!(USART1_STA & 0x4000)) {	//第一个数据
	USART1_STA = 0x4000;	    //表示下次接收,就不再是第一个数据,
	                            //清除掉了数据接收长度
	memset(USART1_REC, 0, USART1_REC_MAX);
}

//由上面我们可以得知USART1_STA为16位,所以这里就比较奇妙了,大家注意看,因为串口收发数据是一直进行的,如果不进行接下来的操作,那么buf缓冲区迟早会溢出的,所以我们为了避免这个问题,我们直接将16位数据分成三个区域,第15位(也就是最高位)为是否接收完成标志。第14位为接收的数据是否为第一个数据(为什么我们会增加这一个判断条件呢),那是因为串口收发数据总是从开始接收到接收完毕的,总是从开始到结束的,如果我们此时接收的数据不是第一个数据的话,那么我们为了接下来的接收数据过程没有太大的问题,必须在接收之前先清空【13:0】位的缓冲区数据,全部清零之后才能开始新一轮的接收数据!!!

//这里为了大家更好的理解,这里我给大家画张不太好看的图,大家凑合看吧~
//
在这里插入图片描述//所以我们先判断接收的数据是否为第一个数据。如果是的话则第15位为0,第14位为0,其余位为0。所以我们这里只需要取反即可。
//
在这里插入图片描述//如果是第一个数据,那么我们在下一次接收数据的时候就应该将第14位置1,代表此时不是第一位数据。

//在上图中我们知道,不是第一个数据,计算机算出来为0x4000。

//其实这里起到了一举两得的作用,一方面证明了此时接收的数据不是第一个数据,另一方面还对【13:0】位置0。相当于清空了。

//memset这个函数的作用也是清空数组。

buf = USART1->DR;

//上面我们只是判断接收的数据是不是第一个数据,接下里我们才开始正式接收数据。
//USART1->DR为数据寄存器。
//
在这里插入图片描述

USART1_REC[USART1_STA & 0x3FFF] = buf;
USART1_STA++;

//USART1_REC这个数组也是数据缓冲区,只不过buf是临时变量而已。
//因为我们知道“&”与运算的话不会改变我们输入的值,比如说1&1=1;0&1=0;
//所以我们这里就是&0x3FFF。相当于第15位为0,第14位为0,其他位全为1。
//然后我们接收数据是从最低位开始接收的,所以USART1_STA++
//
在这里插入图片描述

if ((USART1_STA & 0x3FFF) >= USART1_REC_MAX) {	//达到将要满载状态
	USART1_STA = 0;	//覆盖原本数据

//USART1_REC_MAX这个变量代表的就是表示可以接收的最大数据长度。
//如果最终接收的数据已经占满了我们设置的可以接收的最大数据长度的话,那么我们 就必须得清空数据缓存区里的数据了。

if (USART1->SR & 0x10) //总线空闲

//
在这里插入图片描述//我们需要判断数据是否接收完成的标志就是总线是否空闲,如果总线空闲的话,那么此时代表的就是串口接收完成,反之,则没有接收完成。
//
在这里插入图片描述

USART1_STA |= 0x8000;	//标识接收成功

//当总线空闲时,那么此时我们的USART1_STA第15位则置1。代表此时接收完成。
//
在这里插入图片描述

USART1_STA &= ~0x4000;	//清除14位

//在这之前的第14位为1,因为已经不是第一个数据了,但是此时我们已经接收完成数据了,所以我们这一步就是为了清除第14位,使得第14位为0,回到最初状态。

USART1_REC[USART1_STA & 0x3FFF] = '\0';	//保证有效字符串

//这里我们就是在接收数据的最后一位加一个字符串,代表有效。

buf = USART1->DR;	//清空状态

//因为此时数据寄存器里面已经没有需要我们接收的数据了,所以此时数据寄存器里面的数据为0,正好将数据寄存器赋值给buf这个临时缓冲区里。

串口发送数据部分

data:char *,字符串;len:u8,长度;

u8 i = 0;

//定义临时变量i,并且数据 长度为8位。

for (i = 0; i < len; i++) {
	if (*(data + i) == '\0')	//空白符号无需发送
		return;

//因为这个函数的功能是发送len长度的char数据,并且每次发送的数据长度为8位,所以我们发送数据需要使用到for循环。
//如果我们这里就需要判断下是否发送的是没有实际意义的符号,如果发送的是空白符号的话,那么我们就离直接退出此函数。

//判断是否允许发送数据
while ((USART1->SR & 0x40) == 0);

//
在这里插入图片描述//由前几篇文章我们可以知道串口发送数据时,数据是先发送到发送数据寄存器,然后再发送到移位寄存器,所以我们只需要判断该位的状态来得知是否允许发送数据。
//
在这里插入图片描述//如果为0的话则一直在循环中,反之,则直接退出循环。

USART1->DR = *(data + i);	

//等到允许发送数据之后,我们直接将需要发送的数据发送到数据寄存器即可。

printf重定向部分

int fputc(int ch, FILE* f) {
	while ((USART1->SR & 0x40) == 0)
		;
	USART1->DR = (u8) ch;	//等效于下面的函数
	return ch;
}

//这里给大家推荐一篇文章,应该能看得懂吧,如果有侵权,请私聊我,我会自行删除。
添加链接描述

结束语

个人认为大家如果细心看完这篇文章,并且结合上一篇文章一起看(在文章的刚开始会将前几篇关于USART原理部分的文章链接发出来),我相信大家会彻底掌握USART了!!!如果觉得这篇文章还不错的话,记得点赞 ,支持下!!!

以后我会继续推出关于嵌入式(stm32)的协议方面的讲解,下一讲会推出DMA部分的文章!敬请期待!!!

**我先休息去了~~╭(╯^╰)╮

发布了32 篇原创文章 · 获赞 63 · 访问量 6203

猜你喜欢

转载自blog.csdn.net/qq_40544107/article/details/105051255