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

发布者:心灵舞者最新更新时间:2018-09-09 来源: eefocus关键字:STM32  串口通信  缓冲 手机看文章 扫描二维码
随时随地手机看文章

       在写串口通信前阅读了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;

}

}

}


关键字:STM32  串口通信  缓冲 引用地址:STM32串口通信(基于缓冲区)编程及遇到的问题总结

上一篇:matlab与stm32之间利用串口通信记录
下一篇:STM32 USART2发送数据笔记

推荐阅读最新更新时间:2024-03-16 16:13

stm32 GPIO口配置操作
stm32里面最基本的思路就是使用外设相应寄存器之前,必须开启控制对应寄存器的时钟,读者可到技术手册中查询相应的时钟控制的相应的寄存器。 这里首先开启stm32普通io口的时钟。 GPIO 作为通用输入输出口使用时,当有外部中断设置时才需要开启AFIO时钟,否则不需要开启AFIO 时钟。 然后就是进行gpio结构体的初始化设置 GPIO 常用设置里包括三个结构体的使用如下: 1、GPIO_InitTypeDef为GPIO的基本参数设置结构体,其中GPIO_Pin表示引脚号,GPIO_Speed表示引脚的速度,GPIO_Mode表示引脚的输入输出模式选择。通过这三个基本设置实现了
[单片机]
单片机入门学习十三 STM32单片机学习十 通用定时器
本篇重点记录的是STM32F1的通用定时器。 STM32F103ZE有8个定时器,其中2个高级定时器(TIM1、TIM8),4个通用定时器(TIM2、TIM3、TIM4、TIM5),2个基本定时器(TIM6、TIM7)。下表是对这8个定时器的详细描述。 定时器种类 位数 计数器模式 产生DMA请求 捕获/比较通道 互补输出 特殊应用场景 高级定时器 (TIM1,TIM8) 16 向上、向下、向上/下 可以 4 有 带死区控制盒紧急刹车, 可应用于PWM电机控制 通用定时器 (TIM2~TIM5) 16 向上、向下、向上/下 可以 4 无 通用。定时计数,PWM输出, 输入捕获,输出比较 基本定时器 (TIM6,TIM7) 1
[单片机]
单片机入门学习十三 <font color='red'>STM32</font>单片机学习十 通用定时器
stm32位带操作,实现51类似的GPIO控制功能
新建一个system.h文件,包含以下内容 #ifndef _system_H #define _system_H #include stm32f10x.h //位带操作,实现51类似的GPIO控制功能 //具体实现思想,参考 CM3权威指南 第五章(87页~92页). //IO口操作宏定义 #define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF) 5)+(bitnum 2)) #define MEM_ADDR(addr) *((volatile unsigned long *)(addr)) #define BIT_ADDR(ad
[单片机]
RyanMqtt移植指南
测试环境:stm32F401RCT6、RT-Thread版本: v4.1.0、RT-Thread Studio版本: 2.2.6、网络硬件使用ec800m移植at_socket使用sal框架。 1、移植介绍 RyanMqtt 库希望应用程序为以下接口提供实现: system 接口 RyanMqtt 需要 RTOS 支持,必须实现如下接口才可以保证 mqtt 客户端的正常运行 network 接口 RyanMqtt 依赖于底层传输接口 API,必须实现该接口 API 才能在网络上发送和接收数据包 MQTT 协议要求基础传输层能够提供有序的、可靠的、双向传输(从客户端到服务端 和从服务端到客户端)的字节流 time 接口
[单片机]
RyanMqtt移植指南
STM32寄存器操作方式学习-通用定时/计数器之影子寄存器
在这幅图中细心的朋友可能会发现,有些寄存器的方框下面有阴影,这就是表示该寄存器有相应的影子寄存器。 这种寄存器表示在物理上这个寄存器对应2个寄存器,一个是程序员可以写入或读出的寄存器,称为preload register(预装载寄存器),另一个是程序员看不见的、但在操作中真正起作用的寄存器,称为shadow register(影子寄存器);正如手册上的14.3.1节所说,根据TIMx_CR1寄存器中APRE位的设置,preload register的内容可以随时传送到shadow register,即两者是连通的(permanently),或者在每一次更新事件(UEV)时才把preload register的内容传送到sha
[单片机]
<font color='red'>STM32</font>寄存器操作方式学习-通用定时/计数器之影子寄存器
STM32开发笔记69: 外设启动的先后次序
单片机型号:STM32F070F6P6 今天,在程序框架中增加了Timer16定时器驱动,但程序不能正常运行,本篇日志记录其原因。 驱动程序框架,定义了回调函数Timer16_InterruptFunction,写在main.cpp中用于逻辑层设计。Timer16_InterruptFunction调用的间隔为1ms,具体程序如下: void Timer16_InterruptFunction(void) { Target.HAL.L2.Turn(); } 此程序完成以1ms为间隔L2闪烁的程序,但是将此程序烧写到目标板后,程序不能正常运行。经过调试,最后将问题锁定在启动顺序上,看一下程序外设的启动顺序,具体程序
[单片机]
STM32中DMA模块的使用
DMA(Direct Memory Access)常译为“存储器直接存取”。早在Intel的8086平台上就有了DMA应用了。 一个完整的微控制器通常由CPU、存储器和外设等组件构成。这些组件一般在结构和功能上都是独立的,而各个组件的协调和交互就由CPU完成。如此一来,CPU作为整个芯片的核心,其处理的工作量是很大的。如果CPU先从A外设拿到一个数据送给B外设使用,同时C外设又需要D外设提供一个数据。。。这样的数据搬运工作将使CPU的负荷显得相当繁重。 严格的说,搬运数据只是CPU的比较不重要的一种工作。CPU最重要的工作室进行数据运算,从加减乘除到一些高级的运算,包括浮点、积分、微分、FFT等。CPU还需要负责复杂的中断
[单片机]
<font color='red'>STM32</font>中DMA模块的使用
STM32之的GPIO推挽输出与开漏输出的区别
首先看以下STM32的GPIO的原理图如下: 当端口配置为输出时: 开漏模式:输出 0 时,N-MOS 导通,P-MOS 不被激活,输出0。 输出 1 时,N-MOS 高阻, P-MOS 不被激活,输出1(需要外部上拉电路);此模式可以把端口作为双向IO使用。 推挽模式:输出 0 时,N-MOS 导通,P-MOS 高阻,输出0。 输出 1 时,N-MOS 高阻,P-MOS 导通,输出1(不需要外部上拉电路)。
[单片机]
<font color='red'>STM32</font>之的GPIO推挽输出与开漏输出的区别
小广播
添点儿料...
无论热点新闻、行业分析、技术干货……
设计资源 培训 开发板 精华推荐

最新单片机文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件
随便看看
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved