目录
一、什么是串口
1、介绍
USART:(Universal Synchronous/Asynchronous Receiver/Transmitter) 通用同步/异步串行接收/发送器
UART:(Universal Asynchronous Receiver/Transmitter) 通用异步串行接收/发送器
串口是设备双方互相进行数据传输的一种通信协议,是众多通信协议中的其中一种,并且是使用频率比较高的一种通讯协议;串口中最重要的是两个设备间需要设置同一个波特率,否则无法进行同步,从而导致数据传输失败;串口通信通过转换芯片可以转换成 USB通信/232通信/485通信.
2、USART的通信方式
异步串行全双工,并且串口通信只能一个设备对应另一个设备(一主一从),不能一对多
3、USART的数据帧格式
数据帧格式:起始位(1位)+数据位(8位)+校验位(1位)+停止位(0.5位/1位/1.5位/2位)
或数据帧格式(不用校验位):起始位(1位)+数据位(8位)+停止位(0.5位/1位/1.5位/2位)
串口数据帧最多包含12个位
4、USART通信原理图
5、串口通信的四要素
波特率+数据位+检验位+停止位
6、USART和UART的区别
USART可以在同步或者异步通信状态下使用;而UART仅可以在异步通信状态下使用。
故当在异步通信的情况下,可以用USART.
二、如何配置串口
1、串口控制器框图
1.1 数据传输框图部分
①发送部分框图
发送数据中遇到的问题:
发送数据过程:内核写入数据到TDR,TDR并行自动载入到发送移位寄存器,发送移位寄存器再串行发送出去。
封装串口发送一字节函数思路:
void 串口发送一字节函数(要发送的数据)
{
//等待之前发送的数据发送完成(等待状态寄存器的发送完成位(TC)置1)
①该位由软件序列清零(读取 USART_SR 寄存器,然后写入USART_DR 寄存器)
②TC 位也可以通过向该位写入‘0’来清零
//将数据写入到发送TDR
}
②接收部分框图
接收数据中遇到的问题:
数据接收过程:数据串行接收到接收移位寄存器,接收移位寄存器再并行自动载入到RDR,最后内核读取RDR。
封装串口接受一字节函数思路:
返回值类型 串口接受一字节函数(void)
{
//等待之前接收的数据接收完成(等待状态寄存器的接收完成位(RXNE)置1)
①通过对 USART_DR 寄存器执行读入操作将该位清零。
② RXNE 标志也可以通过向该位写入零来清零。
//读取RDR并写入变量中
//返回变量
}
1.2 波特率部分框图
红色框:整数 紫色框:小数
fCK : 串口所挂载在总线上的时钟频率
USARTDIV= fCK/(8*(2-OVER8))/bps
波特率写入值的计算:需要将通过波特率计算出来的结果写入USART_BRR寄存器
方法1:
整数部分:
DIV_M=(强转取整数)USARTDIV
小数部分:
小数计算方法
化简后得:DIV_F= (强转取整数)(USARTDIV - DIV_M)*8*(2-OVER8)
波特率写入的值=DIV_M+DIV_F
示例代码:
方法2:
注意:此时得到的值带有小数部分,要想写入寄存器(寄存器只能存整数),二进制要整体左移四位,故十进制要扩大16倍
变形得到:USARTDIV= (fCK/(8*(2-OVER8))/bps)*16
波特率写入的值 = fCK / 波特率 //注意此时是16倍过采样
波特率写入的值 = fCK / 波特率 * 2; //注意此时是8倍过采样
示例代码:
1.3 通信协议控制部分
看下面的串口寄存器描述
2、串口寄存器描述
串口相关的寄存器
需要根据寄存器手册知道标准串口配置用哪个寄存器的哪些位
2.1 状态寄存器 (USART_SR)
--------------指示UART控制器运行的状态
位6 TC:发送完成 (Transmission complete)
通过检测此为是0表示之前的数据发送还没有完成,不能再次写数据到DR,需要等待
如果此位是1,表示之前的数据发送完成,可以再次写数据到DR,
位5 RXNE:读取数据寄存器不为空 (Read data register not empty)
在接收外界数据的时候,通过此位判断是否可以读数据
0 表示没有接收到数据 需要等待
1 表示已经接收到完整的数据 可以读
2.2 数据寄存器 (USART_DR)
---------包括发送数据寄存器(只读)和接收数据寄存器(只写)
2.3 波特率寄存器 (USART_BRR)
波特率写入的值 = fCK / 波特率 //注意此时是16倍过采样
波特率写入的值 = fCK / 波特率 * 2; //注意此时是8倍过采样
具体方法在1.2波特率部分框图
2.4 控制寄存器 1 (USART_CR1)
位15 过采样倍数 一般选择16倍
位13 使能UART控制器
位12 字长 8位数据位
位3 发送使能
位2 接收使能
2.5 控制寄存器 2 (USART_CR2)
位12~位13 停止位 默认一个停止位
以上就是标准串口初始化的配置
3、程序设计思路
①总体思路
串口初始化函数
串口发送一字节函数
串口接收一字节函数
②详细思路
串口初始化函数:
void Usartx_init(u32 bps)
{
/*IO口控制器配置*/
//1、打开GPIOx的时钟
//2、配置GPIOX的模式---复用
//3、配置复用功能(IO复用)
/*Usartx控制器配置*/
//1、打开Usartx的时钟
//CR1
//1、字长--8位数据位(无奇偶检验位)
//2、发送器使能
//3、接收器使能
//CR2
//4、1个停止位
/*波特率配置*/
//波特率写入的值 = fCK / 波特率 16倍过采样
/*USARTx使能*/
}
串口发送一字节函数
{
//等待之前发送的数据发送完成(等待状态寄存器的发送完成位(TC)置1)
①该位由软件序列清零(读取 USART_SR 寄存器,然后写入USART_DR 寄存器)
②TC 位也可以通过向该位写入‘0’来清零
//将数据写入到发送TDR
}
串口接受一字节函数
{
//等待之前接收的数据接收完成(等待状态寄存器的接收完成位(RXNE)置1)
①通过对 USART_DR 寄存器执行读入操作将该位清零。
② RXNE 标志也可以通过向该位写入零来清零。
//读取RDR并写入变量中
//返回变量
}
三、具体使用串口
需求1:MCU与PC之间互相进行通信
分析:
通过原理图用到的串口控制器是串口1
PA9-----USART1_Tx
PA10----USART1_Rx
复用模式
标准串口的初始化:
/*
函数名: Usart1_init
函数功能:串口Usart1初始化
返回值:void
形参:u32 bps
函数说明:
USART1_TX --- PA9
USART1_RX --- PA10
GPIOx->AFR[0] 低
GPIOx->AFR[1] 高
*/
void Usart1_init(u32 bps)
{
//打开GPIOA、Usart1的时钟
RCC->AHB1ENR |= 1 << 0;
/*IO口控制器配置*/
//配置GPIOA的模式---复用
GPIOA->MODER &= ~(0xf << 18);
GPIOA->MODER |= 0xa << 18;
//端口输出类型(输出)
GPIOA->OTYPER &= ~(1<<9);//可不配置
//端口输出速度 (输出)
GPIOA->OSPEEDR &= ~(3<<18);//可不配置
//上下拉配置(输入和输出)
GPIOA->PUPDR &= ~(0xf << 18);//可不配置
//配置复用功能(IO复用)
GPIOA->AFR[1]|= 7 << 4;//将USART1_TX复用到PA9
GPIOA->AFR[1]|= 7 << 8;//将USART1_RX复用到PA10
//打开Usart1的时钟
RCC->APB2ENR |= 1 << 4;
/*Usart1控制器配置*/
//CR1
USART1->CR1 &= ~(1 << 15);//过采样模式 0:16 倍过采样 1:8 倍过采样
USART1->CR1 &= ~(1 << 12);//字长--8位数据位(无奇偶检验位)
USART1->CR1 |= 1 << 3;//发送器使能
USART1->CR1 |= 1 << 2;//接收器使能
//CR2
//设置1位停止位
USART1->CR2 &= ~(3 << 12);
//波特率配置
USART1->BRR = 84000000 / bps ;
//USART使能
USART1->CR1 |= 1 << 13;
}
/*
函数名: Usart1_sendbyte
函数功能:串口发送一字节字符数据函数
返回值:void
形参:u8 byte
函数说明:
*/
void Usart1_sendbyte(u8 byte)
{
//等待之前发送的数据发送完成(等待状态寄存器的发送完成位(TC)置1)
while(!(USART1->SR & (1 << 6)));
//①该位由软件序列清零(读取 USART_SR 寄存器,然后写入USART_DR 寄存器)
//②TC 位也可以通过向该位写入‘0’来清零
//将数据写入到发送TDR
USART1->DR = byte;
}
/*
函数名: Usart1_recbyte
函数功能:串口接受一字节字符数据函数
返回值:u8
形参:void
函数说明:
*/
u8 Usart1_recbyte(void)
{
u8 data;
//等待之前接收的数据接收完成(等待状态寄存器的接收完成位(RXNE)置1)
while(!(USART1->SR & (1 << 5)));
//①通过对 USART_DR 寄存器执行读入操作将该位清零。
//② RXNE 标志也可以通过向该位写入零来清零。
//读取RDR并写入变量中
data = USART1->DR ;
//返回变量
return data;
}
需求2:PC发送字符控制LED
PC机发送’O’ LED灯亮
PC机发送’N’ LED灯灭
分析:
电脑与407传输数据,选择串口通信
主控芯片接收字符,并且判断字符,决定灯的亮灭
主函数:
u8 Data = Usart1_recbyte();
if(Data == 'O')
{
LED_ON;
}
else if(Data == 'N')
{
LED_OFF;
}
需求3:PC端上位机显示按键键值
PC端显示所按按键编号
407按哪个按键,就通过串口把对应的按键编号字符发送过去
分析:
按键扫描函数会返回所按按键的键值,键值十进制形式的数据,由于上位机(串口软件)不能识别十进制的数据(除非使用printf重定向函数),只能识别字符或十六进制的数据,所以把键值的十进制数据转换成字符的形式 (4----’4’),把转换后的字符发送出去,数据+48
主函数:
u8 Keynum;
Keynum = Key_scan();
switch(Keynum)
{
case 1:Usart1_sendbyte(Keynum+48);break;
case 2:Usart1_sendbyte(Keynum+48);break;
case 3:Usart1_sendbyte(Keynum+48);break;
case 4:Usart1_sendbyte(Keynum+48);break;
}
需求4:串口发送一个字符串,封装成函数
分析:
通过循环结构将字符串中的每个字符发出去
什么时候表示要发送的字符串发送完成
字符串的特点是在结束的位置有一个结束字符 ‘\0’, 通过结束字符决定发送完成
最后最好补上个‘\0’
发送一个字符串函数:
/*
函数名: Usart1_sendstr
函数功能:串口发送一字节字符串函数
返回值:void
形参:u8 *str
函数说明:
*/
void Usart1_sendstr(u8 *str)
{
while(*str != '\0')
{
Usart1_sendbyte(*str);
str++;
}
*str = '\0';
}
需求5:串口接收一个字符串,封装成函数
分析:
接收一个字符串要一个字符一个字符的接收,通过循环结构调用接收一个字符函数
思考:如何知道接收完成了?
方案1:人为设定一个特殊字符,比如#
接收完成后人为加结束字符,形成一个完整字符串
方案2:接收中断和空闲中断
中断篇补充
发送一个字符串函数:
/*
函数名: Usart1_recstr
函数功能:串口接收一字节字符串函数
返回值:void
形参:u8 *str
函数说明:
*/
void Usart1_recstr(u8 *str)
{
while(1)
{
*str = Usart1_recbyte();
if(*str == '#')
{
break;
}
str++;
}
*str = '\0';
}
需求6:PC控制开发板的灯、流水灯
STM32F407(MCU)接收字符串命令开关灯
Usart1_recstr(buff);
if(strcmp((char const*)buff,"OPEN") == 0)
{
LED_ON;
}
if(strcmp((char const*)buff,"OFF") == 0)
{
LED_OFF;
}
//strcmp(数组名1,数组名2) 字符串比较(按照字符的ASCLL码比较'\0'之前的字符)
//数组名1>数组名2 1 数组名1=数组名2 0 数组名1<数组名2 -1
为什么不需要memset清数组?
//strcmp(数组名1,数组名2) 字符串比较(按照字符的ASCLL码比较'\0'之前的字符)
//数组名1>数组名2 1 数组名1=数组名2 0 数组名1<数组名2 -1
需求7:板A按键控制板B的LED灯
板A按键控制器板B的LED灯,
板B将开状态的LED灯编号反馈给上位机
分析:
分清楚哪个是发送板,哪个是接收板,串口1是板子与PC通讯的,串口2是两个板子之间的通讯。
紧接着把程序一段段分别烧录到发送板和接收板里。
//发送板
#ifdef First
u8 Keynum;
Keynum = Key_scan();
switch(Keynum)
{
case 1:Usart2_sendbyte(Keynum);break;
case 2:Usart2_sendbyte(Keynum);break;
case 3:Usart2_sendbyte(Keynum);break;
case 4:Usart2_sendbyte(Keynum);break;
}
//接收板
#else
u8 data;
data = Usart2_recbyte();
switch(data)
{
case 1:LED1_OVERTURN;Usart1_sendbyte(data+48);break;
case 2:LED2_OVERTURN;Usart1_sendbyte(data+48);break;
case 3:LED3_OVERTURN;Usart1_sendbyte(data+48);break;
case 4:LED4_OVERTURN;Usart1_sendbyte(data+48);break;
}
#endif
四、补充内容
1、printf重定向函数
1.1 printf函数添加方法
方法1:
①勾选微库 如果不勾选,主函数不执行
小补充:
什么是微库?---MicroLib是针对以C语言编写的基于ARM嵌入式应用程序的高度优化的库。
MicroLib 和标准 C 库之间的主要区别在于:
MicroLib 专为深度嵌入式应用而设计。
MicroLib 经过优化,与使用 ARM 标准库相比,使用更少的代码和数据存储器。
MicroLib 最大程度优化代码量,可能会导致有些库函数运行速度更慢
MicroLib 和 ARM 标准库都包含在 Keil MDK-ARM 中。
我们如果要使用,直接在keil中使用即可。
②包含”stdio.h”后编译
③Ctrl+F查找 fputc
④找到fputc函数的外部声明
⑤修改并调用fputc函数
方法2:
①直接粘贴这段代码到Usartx.c里
#if 1
#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{
int handle;
/* Whatever you require here. If the only file you are using is */
/* standard output using printf() for debugging, no file handling */
/* is required. */
};
/* FILE is typedef’ d in stdio.h. */
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
int _sys_exit(int x)
{
x = x;
}
//重定义fputc函数
int fputc(int ch, FILE *f)
{
while((USART1->SR&(1<<6))==0);//循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
}
#endif
②包含”stdio.h”后编译
1.2 怎么用
需要添加一个printf重定向函数,跟C语言的用法一样。
在C语言中使用的是printf打印到输出窗口。而我们用keil进行编程的时候也是使用的C语言所以也可以使用printf,但是我们知道,keil中没有输出窗口。那我们如何使用printf呢?这里我们只需要重定向一下,将printf重定向到USART1(串口1)–这样我们就能通过串口1将信息打印到上位机(串口助手)。这里也有一个小知识点:下载程序也是通过串口1下载到单片机的内存中。
1.3 什么时候用
使用printf函数仅能向串口助手发送,并且printf函数可在串口助手上打印任意字符或数字
当串口与电脑同通信时,串口软件不能识别十进制的数据(除非使用printf重定向函数),只能识别字符或十六进制的数据;当两个设备通信互相传输数据时,用串口发送。
1.4 有什么用
开发产品的时候调试程序使用
通过打印一句话证明CPU是否执行这段程序;通过打印产生的数据显示在串口软件上,分析数据。
1.5 说明
①fputc函数的作用是从指定的文件中读取一个字符, 读取成功时会返回读取到的字符,读取到文件末尾或读取失败时返回EOF
②fputc不需要声明和调用并且printf换行需要”\r\n”
2、复用与重映射
2.1 复用与重映射的区别
①IO复用:IO引脚身兼多职的现象 同一个IO口可以被多个模块(片上外设)使用(不同时)
②重映射:将某个模块(片上外设)的复用功能移动到其它IO口上
芯片上某一个引脚可能具有两种或两种以上的复用功能时,当这两种复用功能发生冲突时,将IO口上的其中一个功能映射到其它IO口上就能解决这个冲突了,这就是重映射的作用
2.2 复用功能怎么配置
USART属于片上外设
如果片上外设需要与外界建立通信需要借助GPIO,故需要IO复用
说明:
每个IO口都有自己固定的复用功能,并不是每个IO口都可以复用任何功能
如何知道IO口具体的复用功能------------查表(数据手册)
如何配置复用功能
①确定IO(根据具体的复用功能确定IO)
根据自己需要的复用功能去查看哪个管脚支持这个复用功能(STM32F407VET6) 通常通过原理图就可以确定/数据手册--->以USART1为例子
USART1_TX --- PA9 --- 复用模式
USART1_RX --- PA10 --- 复用模式
②确定复用功能低位寄存器 和 复用功能高位寄存器
低位
高位
看复用功能高位寄存器 PA9:[7-4] PA10:[11-8]
③确定标识
USART1对应标识为AF7
PA9:[7-4] --> AF7
PA10:[11-8] --> AF7
④确定数值 AF7 : 7
高位AFR[1]
低位AFR[0]
去查看具体寄存器下面的说明(中文参考手册)
GPIOA-> AFR[1]|= 7 << 4; //将PA9复用到USART1
GPIOA-> AFR[1] |= 7 << 8; //将PA10复用到USART1
3、STM32F103与GD32F407串口的配置
3.1 STM32F103串口初始化函数:
以USART1为例子
void Usartx_init(u32 bps)
{
/*IO口控制器配置*/
复用重映射和调试I/O配置寄存器(AFIO_MAPR):
//1、打开GPIOA/GPIOB的时钟和AFIO的时钟 开不开AFIO时钟取决于是否使用重映射
//2、配置GPIOA/GPIOB的模式
// 配置PA9/PB6为复用推挽输出(USART1_TX)
// 配置PA10/PB7为浮空输入(USART1_RX)
//3、配置复用功能(IO复用)
PA9、PA10
复用重映射和调试I/O配置寄存器 (AFIO_MAPR) USART1_REMAP = 0 (本身复用功能---重映像配置可不配置)
PB6、PB7
复用重映射和调试I/O配置寄存器 (AFIO_MAPR) USART1_REMAP = 1 (重映射复用功能)
/*波特率配置*/
//波特率写入的值 = fCK / 波特率 16倍过采样
/*Usart1控制器配置*/
//1、打开Usart1的时钟
//CR1
//1、字长--8位数据位(无奇偶检验位)
//2、发送器使能
//3、接收器使能
//CR2
//4、1个停止位
/*USART1使能*/
}
发送一字节函数与接收一字节函数的与STM32F407的一样
总结:STM32F103的串口配置只有IO口控制器配置部分与STM32F407不一样,其它的都差不多
3.2 GD32F407串口初始化函数:
串口初始化、发送数据、接收数据的配置方法与STM32F407一样,注意串口号是从0到5,共6个串口。