STM32串口通信(基于缓冲区)编程及遇到的问题总结

2023-05-16

        在写串口通信前阅读了STM32中文参考手册,然后满心澎湃地写代码。在这个过程中遇一些让人郁闷的事情,目前这些问题目前已经解决了(嘿嘿嘿),特此来总结一番。串口的使用步骤大概如下(51单片机、STM32、QT或VS编写PC串口上位机都是如此)

1、初始化串口参数(波特率、数据位、停止位、校验位、流控制、开启接收/发送)

2、配置串口中断

3、数据传输

        那么STM32怎么使用串口呢?上面已经说了嘛,所以按照以上步骤即可。可是由于STM32的引脚是可以复用的,我们还需要设置串口通信引脚(GPIO)的工作方式,然后再设置串口参数、串口中断。

        下文函数基于STM32固件库V3.5,以STM32F103RC的串口1的使用为例。

一、串口的初始化和中断设置

1、初始化GPIO:

        根据手册的8.1.11节,我们可以找到下表:


        在全双工的模式下,发送引脚需要设置为推挽复用输出,接收引脚则设置为浮空输入或带上拉的输入。因为一般不用同步和流量控制的方式,所以CK、RST、CTS引脚不作配置。当然啦,在使用STM32外设的时候不要忘记打开外设时钟(GPIO和USART的RCC)。

GPIO_InitTypeDef GPIO_InitStructure;
	
//开启串口和GPIO的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
	
//配置发送引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
//发送引脚设置为推挽复用
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
	
//配置接收引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
//接收引脚设置为浮空输入
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);


2、配置串口参数

        有专门用于初始化串口的库函数(USART_Init)和对应的结构体(USART_InitTypeDef),好像每个外设都有这样的配套,具体内容可参看《STM32F10xxx固件库_3.xx.pdf》。

USART_InitTypeDef USART_InitStructure;
//波特率
USART_InitStructure.USART_BaudRate = 9600;					
//数据长度
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
//停止位
USART_InitStructure.USART_StopBits = USART_StopBits_1;
//校验位
USART_InitStructure.USART_Parity = USART_Parity_No;
//流控制
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
//打开发送和接收模式
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
//初始化串口1
USART_Init(USART1, &USART_InitStructure);

3、中断配置

        在使用STM32的中断前,要对NVIC中断控制器进行配置,设置中断的优先级。

//配置中断优先级
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

4、使能串口及串口中断

        注意1:初始化时不要随意打开TXE中断!只要TX-DR寄存器为空,TX和TXE标志都会马上被置位而立即会产生中断(参考《STM32中文参考手册》的25.3.2节),即使中断标志被清除,也会被重新置位。因此,我采用的是TC中断而不是采用TXE中断。


        注意2:不要采用在一个中断配置函数中同时打开两个中断!例如:USART_ITConfig(USART1, USART_IT_TC | USART_IT_RXNE, ENABLE);    咋眼一看,明明只打开TC中断和RX中断,然而却会同时TXE中断也打开

//串口1使能
USART_Cmd(USART1, ENABLE);
//清除接收中断标记
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
//清除发送完成中断标记
USART_ClearITPendingBit(USART1, USART_IT_TC);
//打开串口1发送完中断
USART_ITConfig(USART1, USART_IT_TC, ENABLE);
//打开串口1接收中断		两个中断不能在一个函数中同时打开!!!太坑了T_T
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

        这样,串口1配置好了。但代码一运行就会发现不妥!为什么每次初始化完成就马上进入中断了呢???遇到这种现象千万不要大惊小怪,我很淡(dan)定(teng)地做了个实验,发现处理器复位后,串口的SR寄存器中的TC标志会被置位。而根《STM32中文参考手册》25.3.2节,在串口使能后会自动发送一个空闲帧,发送完毕后TC也会置位,所以初始化将导致串口初始化完毕后马上进入TC中断。为了避免这种情况,可以在串口使能后等待空闲帧发送完毕,再打开TC中断。

具体看下面完整的初始化代码:

//配置串口1
void USART1_Config()
{
	GPIO_InitTypeDef GPIO_InitStructure;
	USART_InitTypeDef USART_InitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
	
	//配置发送引脚
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	//发送引脚设置为推挽复用
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	//配置接收引脚
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	//接收引脚设置为浮空输入
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	//波特率
	USART_InitStructure.USART_BaudRate = 9600;					
	//数据长度
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;
	//停止位
	USART_InitStructure.USART_StopBits = USART_StopBits_1;
	//校验位
	USART_InitStructure.USART_Parity = USART_Parity_No;
	//流控制
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	//打开发送和接收模式
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
	//初始化串口1
	USART_Init(USART1, &USART_InitStructure);

	//USART1->SR寄存器复位后TC位为1,在此清零
	USART_ClearFlag(USART1, USART_FLAG_TC);		
	//串口1使能
	USART_Cmd(USART1, ENABLE);
	
	//使能后串口发送一个空闲帧,等待空闲帧发送完毕后将TC标记位清零
 	while(USART_GetFlagStatus(USART1, USART_FLAG_TC) != SET);
	//否则开启TC中断后会马上中断
 	USART_ClearFlag(USART1, USART_FLAG_TC);
		
	//配置中断优先级
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);
	
	/***************************************************************
		注意:初始化时不要随意打开TXE中断,
		因为只要TX-DR寄存器为空,TX和TXE都会马上被置位而立即会产生中断
	***************************************************************/
	
	//清除接收中断标记
	USART_ClearITPendingBit(USART1, USART_IT_RXNE);
	//清除发送完成中断标记
	USART_ClearITPendingBit(USART1, USART_IT_TC);
	//打开串口1发送完中断
	USART_ITConfig(USART1, USART_IT_TC, ENABLE);
	//打开串口1接收中断		两个中断不能在一个函数中同时打开!!!太坑了T_T
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
}

二、数据的发送和接收(注:以下代码有bug,博主至今还未找到原因T-T,仅供思路以参考!)

        为了提高代码的效率,我使用基于环形缓冲的串口通信方式。

        发送数据原理:把要发送的数据全部加入到缓冲区中,让处理器开始发送。一个数据发送结束后,即会产生TC中断,此时在中断服务程序中发送下一个数据。像吃饭看电视,在夹菜(发数据)的时候才要把注意力放到菜盘子上,嚼饭的时候(数据发送中)可以看电视,在开始发送数据到数据发送完毕触发中断的这段时间里,处理器可以去做别的事情。

        接收数据原理:当一个数据接收完毕后,将数据立存入缓冲区而不处理,并在未处理数据的计数器上加1。等到处理器空闲,再从缓冲区读取这些数据做并处理(不在中断函数中)。

        如此一来,串口的收发速率并不受影响,还能保证处理器在数据收发的过程中并行执行其他任务。

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

//发送缓冲区
u8 Usart1_SendBuffer[USART_SendBufferSize];
//接收缓冲区
u8 Usart1_RecvBuffer[USART_RecvBufferSize];

//发送缓冲区指针
int Usart1_SendPointer = 0;
//接收缓冲区指针
int Usart1_RecvPointer = 0;

//发送字符队列的长度
int Usart1_SendDataSize = 0;
//接收未处理字符数
int Usart1_RecvDataSize = 0;

//串口1发送状态
int Usart1_SendStatus = USART_Status_Idle;

//生成字符串的缓冲区
char StringBuffer[100];

//发送字符串
void USART1_SendString(char *str)
{
// 	while(*str)
// 	{
// 		USART1_SendByte(*str++);
// 	}
	USART1_SendArray((u8*)str, strlen(str));
}

//发送字节队列
void USART1_SendArray(u8 *DataArray, int count)
{
	if(count <= 0)
	{
		return;
	}
	
	while(count)
	{
		USART1_SendByte(*DataArray++);
		count --;
	}
}

//发送一个字节
void USART1_SendByte(u8 data)
{
	int pos;

	//如果缓冲区满了,要等待
	while(Usart1_SendDataSize >= USART_SendBufferSize);

	//计算数据在缓冲区的位置
	pos = Usart1_SendPointer + Usart1_SendDataSize;
	
	//数据位置超过缓冲区尾地址
	if(pos >= USART_SendBufferSize)
	{	
		//重新计算位置
		pos = pos - USART_SendBufferSize;
	}
	Usart1_SendBuffer[pos] = data;
	Usart1_SendDataSize ++;
	
	//如果串口空闲,立即发送
	if(Usart1_SendStatus == USART_Status_Idle)
	{
		Usart1_SendStatus = USART_Status_Busy;
		USART_SendData(USART1, Usart1_SendBuffer[Usart1_SendPointer++]);
		
		//指针移动到缓冲区尾地址后,循环到缓冲区首地址
		if(Usart1_SendPointer == USART_SendBufferSize)
		{
			Usart1_SendPointer = 0;					
		}
			
		Usart1_SendDataSize --;
	}
}


//串口1中断服务程序
void USART1_IRQHandler()
{
	//判断发送完成中断
	if(USART_GetITStatus(USART1, USART_IT_TC) == SET)
	{	
		//清空发送完成TC标记位
		USART_ClearFlag(USART1, USART_FLAG_TC);
		//清空串口发送完成中断TCIE标记
		USART_ClearITPendingBit(USART1, USART_IT_TC);
		
		if(Usart1_SendDataSize > 0)
		{
			//发送下一个数据
			USART_SendData(USART1, Usart1_SendBuffer[Usart1_SendPointer++]);
			
			//指针移动到缓冲区尾地址后,循环到缓冲区首地址
			if(Usart1_SendPointer == USART_SendBufferSize)
			{
				Usart1_SendPointer = 0;					
			}
			
			//待发送数据减1
			Usart1_SendDataSize --;
		}
		else
		{
			//发送完毕,串口1发送状态:空闲
			Usart1_SendStatus = USART_Status_Idle;
		}		
	}
	
	//接收中断
	if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
	{	
		//清空串口接收标记
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);
		
		//获取缓冲区数据	  
		Usart1_RecvBuffer[Usart1_RecvPointer++] = USART_ReceiveData(USART1);
		
		//如果没有溢出,待处理数据+1。否则丢弃该数据
		if(Usart1_RecvDataSize < USART_RecvBufferSize)
		{
			Usart1_RecvDataSize ++;
		}
		
		//指针移动到缓冲区尾地址后,循环到缓冲区首地址
		if(Usart1_RecvPointer == USART_RecvBufferSize)
		{	
			Usart1_RecvPointer = 0;					
		}
	}
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

STM32串口通信(基于缓冲区)编程及遇到的问题总结 的相关文章

随机推荐

  • 记 - PC视频播放最强画质教程(Potplayer + madVR)

    PC视频播放最强画质教程 前言 xff1a 本次使用到的软件 工具 Potplayer播放器 Potplayer是目前我用到的最好用的宝藏视频播放软件 xff1a 内存占用低 无广告 支持视频格式多 功能强大 扩展性高 界面唯美 xff08
  • 【三维深度学习】多视角立体视觉 MVSNet代码解读

    MVSNet通过将相机几何参数编码到网络中 xff0c 实现了端到端的多视角三维重建 xff0c 并在性能和视觉效果上超越了先前算法 xff0c 并在eccv2018 oral中发表 模型主要包含四个主要步骤 xff1a 图像特征抽取 多视
  • 【pandas】删除满足条件元素所在的行

    在数据清洗时 xff0c 需要按照一定条件删除某些数据样本 xff0c 利用布尔表达式 索引和drop方法可以实现 1 pandas drop df 61 df drop df lt some boolean condition gt in
  • 【AI视野·今日Robot 机器人论文速览 第八期】Wed, 16 Jun 2021

    AI视野 今日CS Robotics 机器人学论文速览 Wed 16 Jun 2021 Totally 13 papers x1f449 上期速览 更多精彩请移步主页 Daily Robotics Papers Constrained Mo
  • 【CVPR2022】论文列表与下载——PartTwo

    CVPR2022将于6月22日召开 x1f389 x1f389 x1f389 xff0c 本次会议共收录了2067篇论文 由于数量较多 xff0c 本文将分四个子文章呈现 xff0c 可直接点击论文标题获取文档 x1f4c3 第一部分 x1
  • 【CVPR2022】论文列表与下载——PartThree

    CVPR2022将于6月22日召开 x1f389 x1f389 x1f389 xff0c 本次会议共收录了2067篇论文 由于数量较多 xff0c 本文将分四个子文章呈现 xff0c 可直接点击论文标题获取文档 x1f4c3 第一部分 x1
  • 【CVPR2022】论文列表与下载——PartFour

    CVPR2022将于6月22日召开 x1f389 x1f389 x1f389 xff0c 本次会议共收录了2067篇论文 由于数量较多 xff0c 本文将分四个子文章呈现 xff0c 可直接点击论文标题获取文档 x1f4c3 第一部分 x1
  • Python三维绘图--Matplotlib

    Python三维绘图 在遇到三维数据时 xff0c 三维图像能给我们对数据带来更加深入地理解 python的matplotlib库就包含了丰富的三维绘图工具 1 创建三维坐标轴对象Axes3D 创建Axes3D主要有两种方式 xff0c 一
  • 【深度学习】三维点云数据集总结

    点云数据集总结 三维点云数据 xff0c 三维深度学习 1 ShapeNet ShapeNet是一个丰富标注的大规模点云数据集 xff0c 其中包含了55中常见的物品类别和513000个三维模型 2 ShapeNetSem 这是一个小的数据
  • git push代码到远程新分支

    Git push 获取远程代码修改后 想要push到远端与原来不同的新分支 xff0c 可以使用下面的命令实现 xff1a git push origin 本地分支 远端希望创建的分支 例如git下来的分支为master span clas
  • 【numpy求和】numpy.sum()求和

    numpy sum a axis 61 None dtype 61 None out 61 None keepdims 61 initial 61 source 用于计算array元素的和 python中常用的numpy进行数学计算 xff
  • Nuttx romfs与启动脚本rcS

    ARM系统上电后 xff0c 系统将flash地址映射到零地址处 xff0c 处理器从零地址处开始运行第一条指令 而在零地址处 xff0c 一般是系统复位中断向量 xff0c 此处存放的是一条跳转指指令 xff0c 通过该条换指令 xff0
  • 在终端/命令行下打开文件浏览器窗口--Win cmd &Ubuntu terminal

    在命令行下想要可视化查看文件 xff0c 可以使用命令直接打开图形化窗口 1 Windows windows上可以使用explorer exe打开资源管理器 xff1a explorer exe span class token keywo
  • 127.0.0.0与0.0.0.0的区别

    1 IP地址分类 ref https tools ietf org html rfc1700 page 3 IP地址表示 IP地址由两个部分组成 xff0c net id和host id xff0c 即网络号和主机号 net id 表示ip
  • 【python】代码换行的几种方法

    代码太长怎么办 xff0c 反斜杠 引号 34 34 34 39 来帮忙 xff01 在写list或者较长的字符串时候 xff0c 或者多个循环造成IDE不够用时 xff0c 就需要代码换行了 主要的代码换行有通用的反斜杠 和针对字符串起作
  • 自己动手写C++迭代器

    综述 关于STL iterator和 iterator adapter 的部分我已在先前的博客 stl源码剖析笔记之iterator 中有所提及 xff0c 下面我们可以试着自己动手写一个简单的迭代器工具 step iterator xff
  • 【STM32F0】Keil 查看局部变量显示<not in scope>

    现象 xff1a 在进行STM32F0开发的时候出现了 调试代码 xff0c 添加变量Watch时 xff0c 显示not in scope 处理方式 xff1a 因为代码开了优化的处理 xff0c 把优化改到Level0 就可以解决问题
  • error: undefined reference to '__dso_handle'解决方案

    error undefined reference to 39 dso handle 39 解决方案 home NDK android ndk r9 sources cxx stl gnu libstdc 43 43 4 7 libs ar
  • ARM里的大端格式和小端格式分别是什么意思?

    当前的存储器 xff0c 多以byte为访问的最小单元 xff0c 当一个逻辑上的地址必须分割为物理上的若干单元时就存在了先放谁后放谁的问题 于是端 endian 的问题应运而生了 对于不同的存储方法 就有大端 big endian 和小端
  • STM32串口通信(基于缓冲区)编程及遇到的问题总结

    在写串口通信前阅读了STM32中文参考手册 xff0c 然后满心澎湃地写代码 在这个过程中遇一些让人郁闷的事情 xff0c 目前这些问题目前已经解决了 xff08 嘿嘿嘿 xff09 xff0c 特此来总结一番 串口的使用步骤大概如下 xf