STM32USART串口通信
Posted 甜心猛男
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了STM32USART串口通信相关的知识,希望对你有一定的参考价值。
目录
一、串口通信协议
1、UART简介
嵌入式开发中,UART串口通信协议是我们常用的通信协议(UART、I2C、SPI等)之一,全称叫做通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),是异步串口通信协议的一种,工作原理是将传输数据的每个字符一位接一位地传输,它能将要传输的资料在串行通信与并行通信之间加以转换,能够灵活地与外部设备进行全双工数据交换。
注:在此开发板中,是有USART(Universal Synchronous Asynchronous Receiver and Transmitter通用同步异步收发器)串口的,USART相当于UART的升级版,USART支持同步模式,因此USART 需要同步始终信号USART_CK(如STM32 单片机),通常情况同步信号很少使用,因此一般的单片机UART和USART使用方式是一样的,都使用异步模式。因为USART的使用方法上跟UART基本相同,所以在此就以UART来讲该通信协议了。
2、 UART通信协议
(1)起始位
当未有数据发送时,数据线处于逻辑“1”状态;先发出一个逻辑“0”信号,表示开始传输字符。
(2)数据帧
紧接着起始位之后。资料位的个数可以是4、5、6、7、8等,构成一个字符。通常采用ASCII码。从最低位开始传送,靠时钟定位。
(3)奇偶校验位
资料为加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验),以此来校验资料传送的正确性。
(4)停止位
它是一个字符数据的结束标志。可以是1位、1.5位、2位的高电平。 由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟同步的机会。适用于停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率同时也越慢。
(5)下个起始位
处于逻辑“1”状态,表示当前线路上没有资料传送,进入空闲状态。
处于逻辑“0”状态,表示开始传送下一数据段。
(6)波特率
表示每秒钟传送的码元符号的个数,是衡量数据传送速率的指标,它用单位时间内载波调制状态改变的次数来表示。
常用的波特率有:9600、115200……
时间间隔计算:1秒除以波特率得出的时间,例如,波特率为9600的时间间隔为1s / 9600(波特率) = 104us。
这些就是UART对通信协议的一些理解,如果想详细了解请参考:基于STM32之UART串口通信协议(一)详解 - LLLIN000 - 博客园 (cnblogs.com)
二、STM32的USART串口通信(中断)
3、要求
2、完成一个STM32的USART串口通讯程序,要求:
1)设置波特率为115200,1位停止位,无校验位;
2)STM32系统给上位机(win10)连续发送“hello windows!”。win10采用“串口助手”工具接收。
2、工程的建立
在我们前面的学习中已经介绍过两种建立工程的方式,分别是STM32CubeMX生成基础代码和使用大佬的工程模板文件。这里我们还是选择用大佬"洋桃电子"编写好的工程文件来完善即可。
我们要实现的功能是USART串口通信,所以我们也照葫芦画瓢创建一个usart.c文件并添加到我们的Basic文件夹里,然后我们下一步该做什么呢,相信你心中一定有答案了,初始化!接下来我们就来看看我们的代码怎么写。
usart.c:
#include "usart.h"
//使UASRT串口可用printf函数发送
//在usart.h文件里可更换使用printf函数的串口号
#if 1
#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE {
int handle;
};
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
_sys_exit(int x){
x = x;
}
//重定义fputc函数
int fputc(int ch, FILE *f){
while((USART_n->SR&0X40)==0);//循环发送,直到发送完毕
USART_n->DR = (u8) ch;
return ch;
}
#endif
/*
USART1串口相关程序
*/
#if EN_USART1 //USART1使用与屏蔽选择
u8 USART1_RX_BUF[USART1_REC_LEN]; //接收缓冲,最大USART_REC_LEN个字节.
//接收状态
//bit15, 接收完成标志
//bit14, 接收到0x0d
//bit13~0, 接收到的有效字节数目
u16 USART1_RX_STA=0; //接收状态标记
/*
USART1专用的printf函数
当同时开启2个以上串口时,printf函数只能用于其中之一,其他串口要自创独立的printf函数
调用方法:USART1_printf("123"); //向USART2发送字符123
*/
void USART1_printf (char *fmt, ...){
char buffer[USART1_REC_LEN+1]; // 数据长度
u8 i = 0;
va_list arg_ptr;
va_start(arg_ptr, fmt);
vsnprintf(buffer, USART1_REC_LEN+1, fmt, arg_ptr);
while ((i < USART1_REC_LEN) && (i < strlen(buffer))){
USART_SendData(USART1, (u8) buffer[i++]);
while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
}
va_end(arg_ptr);
}
void USART1_Init(u32 bound){ //串口1初始化并启动
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE); //使能USART1,GPIOA时钟
//USART1_TX PA.9
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);
//USART1_RX PA.10
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
//Usart1 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0 ;//抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器
//USART 初始化设置
USART_InitStructure.USART_BaudRate = bound;//一般设置为9600;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
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; //收发模式
USART_Init(USART1, &USART_InitStructure); //初始化串口
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启ENABLE/关闭DISABLE中断接收到数据时中断 读寄存器DR清零,也可软件手动清零
USART_Cmd(USART1, ENABLE); //使能串口
}
void USART1_IRQHandler(void){ //串口1中断服务程序(固定的函数名不能修改)
u8 Res;
//以下是字符串接收到USART1_RX_BUF[]的程序,(USART1_RX_STA&0x3FFF)是数据的长度(不包括回车)
//当(USART1_RX_STA&0xC000)为真时表示数据接收完成,即超级终端里按下回车键。
//在主函数里写判断if(USART1_RX_STA&0xC000),然后读USART1_RX_BUF[]数组,读到0x0d 0x0a即是结束。
//注意在主函数处理完串口数据后,要将USART1_RX_STA清0
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET){ //接收中断(接收到的数据必须是0x0d 0x0a结尾)
Res =USART_ReceiveData(USART1);//(USART1->DR); //读取接收到的数据
printf("%c",Res); //把收到的数据以 a符号变量 发送回电脑
if((USART1_RX_STA&0x8000)==0){//接收未完成
if(USART1_RX_STA&0x4000){//接收到了0x0d
if(Res!=0x0a)USART1_RX_STA=0;//接收错误,重新开始
else USART1_RX_STA|=0x8000; //接收完成了
}else{ //还没收到0X0D
if(Res==0x0d)USART1_RX_STA|=0x4000;
else{
USART1_RX_BUF[USART1_RX_STA&0X3FFF]=Res ; //将收到的数据放入数组
USART1_RX_STA++; //数据长度计数加1
if(USART1_RX_STA>(USART1_REC_LEN-1))USART1_RX_STA=0;//接收数据错误,重新开始接收
}
}
}
}
}
#endif
可以看到咱们的代码中用了 NVIC ,那么这个 NVIC 是个什么玩意儿呢,这就是我们之前说到过的中断,这时候肯定就有小伙伴要问了,我们为什么要用到中断呢,中断的作用是啥呢。首先我们要知道我们的串口发送数据可以用中断方式和扫描方式,那么二者的区别在哪儿呢。在这里我举一个通俗易懂的例子大家就可以明白了。咱们班上有人捡到100块钱,我们把钱交给辅导员希望辅导员找到失主并归还给他。
扫描方式:扫描方式就相当于辅导员找到咱们班然后一个一个问,这钱是不是你的,丢了多少,一个一个排查下去最后找到失主并完成我们的任务(归还财务)。这种方式听起来很傻而且很麻烦,所以接下来我们就要讲到中断的方式啦。
中断方式:辅导员不再主动,而是被动地等着失主自己去办公室询问他,确认失主以后归还钱财。是不是觉得这种方式才是我们的惯性思维,好了,相信大家也能了解中断和扫描的却别了吧。
还记得我们上次说过的咱们的板子上TX是PA.9,RX是PA.10吗,所以我们需要做些啥呢,是的,就是要初始化这两个管脚并开启USART功能,配置好我们需要用的管脚以后我们就需要开启中断,设置中断模式并编写中断函数。相信学过51单片机的小伙伴都接触过中断,当咱们的程序收到中断的信号时就会跳到咱们的中断函数中并执行中断函数,咱们这次需要完成的功能是串口通信,所以咱们的中断信号是接收完成或者是接收错误,大家可以看看上面的注释,也可以参考一下:浅谈USART_RX_STA各位的描述以及是如何实现数据接收的_JackCrum的博客-CSDN博客
好了,说了这么多,既然咱们有了usart.c文件就要有一个usart.h文件来声明咱们刚刚定义的函数,然后方便我们在main.c文件中调用,那么现在我们就来创建这个文件吧。
usart.h:
#ifndef __USART_H
#define __USART_H
#include <string.h>
#include "stdio.h"
#include "sys.h"
#define USART_n USART1 //定义使用printf函数的串口,其他串口要使用USART_printf专用函数发送
#define USART1_REC_LEN 200 //定义USART1最大接收字节数
#define EN_USART1 1 //使能(1)/禁止(0)串口1
extern u8 USART1_RX_BUF[USART1_REC_LEN]; //接收缓冲,最大USART_REC_LEN个字节.末字节为换行符
extern u16 USART1_RX_STA; //接收状态标记
//函数声明
void USART1_Init(u32 bound);//串口1初始化并启动
void USART1_printf(char* fmt,...); //串口1的专用printf函数
#endif
这个文件里装的都是一些函数声明以及一些变量的定义。现在俺们就需要把这两个文件保存并添加进俺们的Basic文件夹里啦,上次我忘了说如何将.h文件添加进咱们的工程中,有的小伙伴就向我反应咱们的编译有问题,今天我就来教教大家如何将.h文件添加进来。
首先咱们还是点击“仙女棒”,然后选择 C/C++ 。
看到下面有个路径选择没有(Include Paths), 咱们点击后面的省略号。
可以看到此时我们就需要选择路径了,我们只需要点击添加并找到我们放.h的文件夹添加进来就OK啦。
在介绍完我们的usart文件之后,我们再来讲讲我们的main.c文件该如何编写,相信大家现在都知道咱们的第一步是啥啦,没错,就是调用我们的初始化函数,然后剩下的只需要将咱们的串口输出函数写在while循环中就OK啦。
main.c:
#include "stm32f10x.h" //STM32头文件
#include "delay.h"
#include "usart.h"
int main (void){//主程序
//初始化程序
RCC_Configuration(); //时钟设置
USART1_Init(115200); //串口初始化(参数是波特率)
//主循环
while(1){
printf("hello windows "); //纯字符串发送数据到串口
delay_ms(1000); //延时
}
}
大家可以看到我们将波特率设置为了题目要求的“115200” ,这也可以根据大家的需求来更改也是没问题的,但是在这里我们一定要记住这个数字,因为后面要用到。各位小伙伴一定发现了一个问题,这里咱们的串口输出用的函数是“printf”,是不是大家对这个函数格外亲切呀。在这里我要说的是,我们尤其要注意这个“printf”函数,当我们只开启一个串口通信时,我们可以直接用“printf”函数输出到我们的串口助手界面。但是如果我们要使用多个串口时,咱们的芯片就无法区分我们要输出的是哪个了,所以这个时候我们就要自己创建每个串口对应的输出函数了。但是在这里我们只开启了USART1,所以可以直接用“printf”函数就足够啦。
现在我们编译并运行咱们的程序,准备好烧录进咱们的板子中,烧录过程如果还有不清楚的小伙伴可以参考我之前的博客(25条消息) 用STM32F103C8T6制作流水灯_txmnQAQ的博客-CSDN博客,由于我们这次实现的功能不像之前的流水灯能通过咱们的LED直接表现出来,我们要借助一定的工具才能观察到我们的实验结果。
三、建立STM32与PC之间的通信基础
在我们之前的学习中就已经建立过板子与我们的PC之间的通信了,也许你还没反应过来,但是这是我们已经做过的事情啦。仔细回想一下我们是怎样将USB-TTL与板子连接并且烧录程序的呢,实际上这就是一种通信连接啊,将板子的发送接到PC的接收,板子的接收接到PC的发送。只是我们之前没有实现UASAT串口发送功能而已。
1、串口助手的使用
所以我们现在暂时不要动烧录时的接线,咱们直接打开咱们的串口助手。(我这里用的是“洋桃电子”家的串口助手,我将它放入百度网盘供大家下载。
链接:https://pan.baidu.com/s/1QsM4UWR557wzmJqYCL2hcw
提取码:1111
咱们现在打开安装好的串口助手,如下图:
端口号我们可以参考咱们FlyMCU中烧录程序那个端口,我的是COM4端口,大家根据自己的来。是否还记得之前程序编写的时候我让大家记得波特率为 115200 ,我说后面咱们要用的,现在就派上用场啦 。然后我们是在PC上接收来自板子上的“hello windows”,所以我们选择接受模式中的“字符”,设置完成后如下图:
2、效果呈现
现在我们只需要最后一步,打开端口,神奇的事情就会发生啦!
咱们的对话框中会间隔一秒发送一个“hello windows” ,咱们本次实验就完成啦!
注:在我们使用串口助手观察时,咱们的COM4端口,也就是接板子那个端口是被占用了的,这时候如果发现咱们的程序有问题想重新烧录进去,一定要记得先在串口助手中将端口关闭!!!要不然我们直接使用FlyMCU的话就会报错,报告端口被占用。如下图:
参考资料:
(25条消息) 浅谈USART_RX_STA各位的描述以及是如何实现数据接收的_JackCrum的博客-CSDN博客
基于STM32之UART串口通信协议(一)详解 - LLLIN000 - 博客园 (cnblogs.com)
百度网盘链接:
链接:https://pan.baidu.com/s/1QsM4UWR557wzmJqYCL2hcw
提取码:1111
以上是关于STM32USART串口通信的主要内容,如果未能解决你的问题,请参考以下文章