前言
——本章为对串口通信的一种(USART)进行总结
————————————————————————————————————————
目录
—— 1.串行通信的背景
—— 2.配置与分析串口
—— 3.控制程序
—————————————————————————————————————————
一、串行通信的背景
1. 处理器 与 外部设备通信有两种方式
方式 | 串行 | 并行 |
原理 | 数据同时传输 | 数据按位顺序传输 |
优点 | 速度快 | 速度慢 |
缺点 | 占用引脚多 | 占用引脚少 |
2. 串行通信的分类
· 单工 ——数据只支持在一个方向上传输
· 半双工——允许在两个方向上传输,但是在某一时刻,只能在一个方向上传输,本质是一种切 换方向的单工
· 全双工——允许在两个方向上传输
——————————————————
· 同步 ——发送方发送数据后,等待对方响应后才可以发下一个数据
· 异步 ——发送方发送数据后,不等待对方响应直接发送下一个数据
下面给出图例,让大家更好理解
——————
我们要说的 USART 就是属于 全双工——异步通讯
——————————
3.对应引脚
名称 | 默认复用功能 |
PA2 | USART2_TX |
PA3 | USART2_RX |
PB10 | USART3_TX |
PB11 | USART3_RX |
PA9 | USART1_TX |
PA10 | USART1_RX |
我们需要注意以下引脚和他对应的通道
———————————————————————————————————————
二、配置与分析串口
1. 配置流程
这个流程是比较统一的,我们就按照这个顺序配置
1 —————— 串口时钟、GPIOA时钟使能
2 —————— GPIOA端口模式设置
3 —————— 串口参数初始化
4 —————— 开启中断并且初始化NVIC
5 —————— 使能串口
6 —————— 编写中断处理函数
————
2.按步分析
定义我们的初始化函数,我们定义一个变量(bound)之后作为我们的波特律
void USART1_Init(u32 bound)
————————————
(1)时钟使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_USART1, ENABLE);
我们的板子使用的是 PA9 与 PA10 我们需要打开时钟与USART通道1
————————————
(2)GPIOA的端口配置
GPIO_InitTypeDef GPIO_InitStruct;
//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_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);
我们的的发送和接受数据的端口所需要的模式并不一样,所以我们把他们分开初始化,说明一下
但要注意他们的模式是固定的,如果弄错了就会出现错误,而 RX 里因为是接受数据,所以就不需要输出的速率,所以我们没有 speed
(这里可能会有人会出现报错,可以在 C/C++里面把 c99 打开)
——————————————————————————————————
(3)串口参数初始化
这一部分在这里比较重要,这是我们涉及的新的部分
USART_InitStructure.USART_BaudRate = bound;
这是设置我们的 波特率 最开始我们所设置的 bound就是填的这个,但是这个并不是乱填的,我们还是要注意注意,他是有固定数字的
————
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; //收发模式
接着这四个比较固定,我们就这么设置即可,以后也不需要怎么更改
————
(4)开启中断并初始化NVIC
这节参考前面的中断文章,不过多说明
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启中断
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);
————————
(5)使能串口
USART_Cmd(USART1, ENABLE); //使能串口
——————
(6)编写中断函数
比较需要注意的是,中断函数的名字不能更改,如果我们不需要中断不用设置他也行
我们接下来看看 洋桃电子 所给出的中断函数
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;//接收数据错误,重新开始接收
}
}
}
}
}
这个中断函数,是我们使用串口发送所使用程序,接下来我们再给出一个串口接收所使用的中断函数
void USART1_IRQHandler(void){ //串口1中断服务程序(固定的函数名不能修改)
u8 a;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET){ //接收中断(接收到的数据必须是0x0d 0x0a结尾)
a =USART_ReceiveData(USART1);//读取接收到的数据
printf("%c",a); //把收到的数据发送回电脑
}
}
——————————
我们来看看他的主程序
int main (void){//主程序
u8 a;
//初始化程序
RCC_Configuration(); //时钟设置
USART1_Init(115200); //串口初始化(参数是波特率)
//主循环
while(1){
//查询方式接收
if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE) != RESET){ //查询串口待处理标志位
a =USART_ReceiveData(USART1);//读取接收到的数据
printf("%c",a); //把收到的数据发送回电脑
}
// delay_ms(1000); //延时
}
}
需要注意的是,如果我们使用的接收字符(接收我们在串口助手上写的),就需要关闭以下我们的中断函数,这样就不会进入中断了(USART_ITConfig)
我们再来看看主函数里 while 循环里调用的两个函数
1.USART_GetFlagStatus 作一个标志位,判断这个位置是否为1,我们查询了数据手册,让我们更深入的了解一下这个函数
而下面就是我们标志位的内容
当这些完成之后,就会置 1 ,所以上面的主函数里,我们就是判断 数据寄存器是否是 1,如果是1的话,我们就 进行下面的操作,把这些数据给发送出去。
而接下来的 USART_ReceiveData 就是用来接收我们发送的数据,他只有一个参数,就是用来选择我们要接收的串口
——————————————————————————————————————————
上面的方法虽然好,但是他有一个比较严重的缺点,也就是失去的实时性,我们需要在查询的时候做我们的任务,所以我们就需要使用中断的方法。
我们打开我们的中断使能,然后把之前的 中断函数改成我们上面所给的接收数据的中断,这样我们既能做主函数,也能马上执行我们所发送的,这里就使用了一个新的函数
USART_GetITStatus 我们看看这个函数
而下面就是他的内容
我们在此处选择的是接收中断事件,我们开启中断之后,就选择这个中断的内容,当接收信息时,我们的标志位置 1 ,然后进入下一步操作,读取我们收到的信息,然后用 printf 函数打印出来
_________________________________________________________________
这样子我们就可以持续的接收我们所发送的字符串了,为了方便,我们再给出已经写好的发送字符串的函数 (函数出自 海创电子)
void USART_SendString( USART_TypeDef * USARTx, char *str)
{
while(*str!='\0')
{
USART_SendByte( USARTx, *str++ );
}
while(USART_GetFlagStatus(USARTx,USART_FLAG_TC)==RESET);
}
uint8_t USART_ReceiveByte(USART_TypeDef* USARTx)
{
while(USART_GetFlagStatus(USARTx,USART_FLAG_RXNE)==RESET);
return (uint8_t)USART_ReceiveData(USART1);
}
——————————————————————————————————————————
当然我们前面还有一种方法没有说,也就是我们的 printf 函数,他能将我们所写的打印在我们的串口上,当然,要想使用 printf函数 就和我们之前说的一样,他需要一个重定向,我们就需要
fputc 和 fgetc
#pragma import(__use_no_semihosting)
struct __FILE
{
int handle;
};
FILE __stdout;
void _sys_exit(int x)
{
x = x;
}
int fputc(int ch, FILE *f)
{
USART_SendData(USART1, (uint8_t) ch);
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
return (ch);
}
int fgetc(FILE *f)
{
while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
return (int)USART_ReceiveData(USART1);
}
在我们编写好的 usart.c 文件下面补充上这个就可以了,还需要注意的是,printf函数所打印出来的东西和 c语言很相似,我们需要对要打印出来的东西进行一个说明
u32 a | 定义32位无符号变量a |
u16 a | 定义16位无符号变量a |
u8 a | 定义8位无符号变量a |
vu32 a | 定义易变的32位无符号变量a |
vu16 a | 定义易变的 16位无符号变量a |
vu8 a | 定义易变的 8位无符号变量a |
uc32 a | 定义只读的32位无符号变量a |
uc16 a | 定义只读 的16位无符号变量a |
uc8 a | 义只读 的8位无符号变量a |
%d 十进制有符号整数
%u 十进制无符号整数
%f 浮点数
%s 字符串
%c 单个字符
%p 指针的值
%e 指数形式的浮点数
%x, %X 无符号以十六进制表示的整数
%o 无符号以八进制表示的整数
%g 自动选择合适的表示法
%p 输出地址符
————————————————————————————————————————-
三、控制程序
使用 串口来远程控制我们的单片机,同时也可以把单片机的状态展示在我们的串口上
这个程序是通过上面的串口接收程序来改写的
我们来看看主程序
#include "stm32f10x.h" //STM32头文件
#include "sys.h"
#include "delay.h"
#include "led.h"
#include "key.h"
#include "buzzer.h"
#include "usart.h"
int main (void){//主程序
u8 a;
//初始化程序
RCC_Configuration(); //时钟设置
LED_Init();//LED初始化
KEY_Init();//按键初始化
BUZZER_Init();//蜂鸣器初始化
USART1_Init(115200); //串口初始化(参数是波特率)
//主循环
while(1){
//查询方式接收
if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE) != RESET){ //查询串口待处理标志位
a =USART_ReceiveData(USART1);//读取接收到的数据
switch (a){
case '0':
GPIO_WriteBit(LEDPORT,LED1,(BitAction)(0)); //LED控制
printf("%c:LED1 OFF ",a); //
break;
case '1':
GPIO_WriteBit(LEDPORT,LED1,(BitAction)(1)); //LED控制
printf("%c:LED1 ON ",a); //
break;
case '2':
BUZZER_BEEP1(); //蜂鸣一声
printf("%c:BUZZER ",a); //把收到的数据发送回电脑
break;
default:
break;
}
}
//按键控制
if(!GPIO_ReadInputDataBit(KEYPORT,KEY1)){ //读按键接口的电平
delay_ms(20); //延时20ms去抖动
if(!GPIO_ReadInputDataBit(KEYPORT,KEY1)){ //读按键接口的电平
while(!GPIO_ReadInputDataBit(KEYPORT,KEY1)); //等待按键松开
printf("KEY1 "); //
}
}
if(!GPIO_ReadInputDataBit(KEYPORT,KEY2)){ //读按键接口的电平
delay_ms(20); //延时20ms去抖动
if(!GPIO_ReadInputDataBit(KEYPORT,KEY2)){ //读按键接口的电平
while(!GPIO_ReadInputDataBit(KEYPORT,KEY2)); //等待按键松开
printf("KEY2 "); //
}
}
// delay_ms(1000); //延时
}
}
我们进入到 while 循环里面,一样的,我们通过 USART1 这个通道来读取我们所发送的字符,并把这个值赋值给 a ,然后再通过 a 的值 ,使用 switch 语句来选择我们要执行的部分
例如 : 我们在串口上面输入 1 ,就可以点亮我们的灯(这是远程控制)
下面的 if 语句就是用到记录我们 单片机的状态,当按键按下(前面是正常的按键程序),我们就会执行后面的 printf 语句
————————————————————————————————————-————