STM32快速复习(六)USART串口

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

从本章开始,陆续介绍STM32的通讯协议。
上学常用:单总线,IIC,SPI。
现在上班常用:USART,IIC,SPI,RS485,RS232,TCP/IP等。

为了控制或读取外挂模块,stm32需要与外挂模块进行通信,来扩展硬件系统。而这个“通信”的过程就需要遵守相应的“通信协议”,也就是通信双方需要按照协议规则进行数据收发。不同外挂模块的会采用不同的通信协议。

在这里插入图片描述

USART:TX(Transmit Exchange)数据发送脚、RX(Receive Exchange)数据接收脚。
IIC:SCL(Serial Clock)时钟线、SDA(Serial Data)数据线。
SPI:MOSI(Master Output Slave Input)主机输出数据脚、MISO(Master Input Slave Output)主机输入数据脚、CS(Chip Select)片选
USB:DP(Data Postive)差分线正、DM(Data Minus)差分线负

注:上述协议中,单端电平都需要共地。
注:使用差分信号可以抑制共模噪声,可以极大的提高信号的抗干扰特性,所以一般差分信号的传输速度和传输距离都非常高。

总结来讲,就是串口以及串口协议;串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备的互相通信。单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信,极大地扩展了单片机的应用范围,增强了单片机系统的硬件实力。

在这里插入图片描述
下面介绍一些串口引脚的注意事项:
TX与RX:简单双向串口通信有两根通信线(发送端TX和接收端RX),要交叉连接。不过,若仅单向的数据传输,可以只接一根通信线。
GND:一定要共地。由于TX和RX的高低电平都是相对于GND来说的,所以GND严格来说也算是通信线。
VCC:相同的电平才能通信,如果两设备都有单独的供电,VCC就可以不接在一起。但如果某个模块没有供电,就需要连接VCC,注意供电电压要按照模块要求来,必要时需要添加电压转换电路。

电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:

  1. TTL电平【单片机】:+3.3V或+5V表示1,0V表示0。一般低压小型设备,使用的都是TTL电平。传输范围几十米。
  2. RS232电平:-3-15V表示1,+3+15V表示0。一般在大型机器上使用,由于环境比较恶劣,静电干扰比较大,所以通信电压都很大,并且允许波动的范围也很大。传输范围几十米。
  3. RS485电平:两线压差+2+6V表示1,-2-6V表示0(差分信号)。抗干扰能力极强,通信距离可达上千米。
    注:不同电平标准之间的转换只需要加电压转换芯片即可,并不需要修改相应的软件代码。

注:在stm32中,根据发送数据自动转换发送波形、或根据波形自动读取数据,都是由USART外设自动完成的,无需软件控制每一位的发送或读取。


提示:以下是本篇文章正文内容,下面案例可供参考

一、USART是什么?USART结构?USART电路特点?

USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步收发器 是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据 自动生成数据帧时序,从TX引脚发送出去,也可 自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里。USART中的“S”表示同步,只支持时钟输出,不支持时钟输入,是为了兼容别的协议或特殊用途而设计的,并不支持两个USART之间进行同步通信,所以这个功能几乎不会用到,一般更常使用的是UART同步异步收发器。下面是一些参数:

自带波特率发生器,最高达4.5Mbits/s,常用9600/115200。
可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)。
可选校验位:无校验(常用)/奇校验/偶校验。
支持同步模式(一般不用)、硬件流控制(指示从设备准备好接收的信号,一般不用)、DMA、智能卡、IrDA(手机红外通信,但并不是红外遥控,目前很少见)、LIN(局域网的通信协议)。
STM32F103C8T6 USART资源:USART1(APB2)、USART2(APB1)、USART3(APB1)。

TIPS:会涉及到端口复用,前面定时器中断那一章介绍过,端口复用后,需要了解端口的默用,以及如何调用其他功能的函数。

在这里插入图片描述

USART结构:
在这里插入图片描述
在这里插入图片描述
简化后:
在这里插入图片描述
波特率发生器:用于产生约定的通信速率。时钟来源是PCLK2/PCLK1,经过波特率发生器分频后,产生的时钟通向发送控制器、接收控制器。
发送控制器、接收控制器:用于控制发送移位、接收移位。
GPIO:发送端配置成复用推挽输出、接收端配置成上拉输入。
标志位:TXE置位时写入数据、RXNE置位时接收数据。
开关控制:用于开启整个USART外设。

更加详细的数据帧接受和传输,8位校验无校验,9位校验无校验,噪声读取和标志位。建议看视频

二、使用步骤

1.库函数

一般性配置和正常收发数据功能:
USART_DeInit :将USART结构体恢复成默认值。
USART_Init 【必需】:初始化USART结构体。
USART_StructInit :给USART结构体赋一个初值。
USART_Cmd 【常用】:USART的开关控制。
USART_ITConfig 【常用】:USART的中断配置。
USART_SendData 【常用】:发送数据,写DR寄存器。
USART_ReceiveData 【常用】:接收数据,读DR寄存器。
USART_GetFlagStatus :主函数调用,获取USART的各种标志位。
USART_ClearFlag :主函数调用,清除USART指定的标志位。
USART_GetITStatus :中断函数调用,获取中断状态。
USART_ClearITPendingBit :中断函数调用,清除中断状态。
其他单独的配置:
USART_SetGuardTime :在两个连续字符之间添加一个额外的时间间隔,默认为1位。
USART_SetPrescaler :设置波特率发生器的预分频值DIV,以产生相应的比特率。
USART_HalfDuplexCmd :启用或禁用半双工模式。
USART_OverSampling8Cmd :启用或禁用USART的8倍过采样模式。在8倍过采样模式下,USART对串行数据线的采样次数将达到8次,从而提高数据传输的精度和可靠性。
USART_OneBitMethodCmd :启用或禁用单线半双工通信模式。
DMA触发源:
USART_DMACmd :开启USART的DMA硬件触发源。
时钟同步功能:
USART_ClockInit :时钟输出功能,时钟输出初始化。
USART_ClockStructInit :时钟输出功能,时钟输出结构体初始化。
流控功能(总线通信):
USART_SetAddress :总线通信功能,设置USART的地址。
USART_WakeUpConfig :总线通信功能,启用USART唤醒功能,检测到特定事件就会被唤醒。
USART_ReceiverWakeUpCmd :总线通信功能,启用USART的接收唤醒功能,接收到特定的数据包就会被唤醒。
LIN总线通信:
USART_LINBreakDetectLengthConfig :LIN模式,检测到BREAK信号的长度。
USART_LINCmd :开启LIN模式。
USART_SendBreak :发送一个BREAK信号。
智能卡模式:
USART_SmartCardCmd :启用或禁用智能卡模式。
USART_SmartCardNACKCmd :在USART智能卡模式中,启用或禁用非应答模式。
红外接收IrDA模式:
USART_IrDAConfig :配置USART通信中的红外数据通信模式。
USART_IrDACmd :启用或禁用红外数据通信模式(IrDA)。

2.库函数示例代码

代码如下(示例):

- main.c
#include "stm32f10x.h" // Device header
#include "SerialPort.h"
int main(void){
uint8_t send_byte = 0x42;
uint8_t send_array[6] = {0x30,0x31,0x32,0x33,0x34,0x35};
//串口初始化
SerialPort_Init();
//发送单个字节
SerialPort_SendByte('A');//可以直接发送字符
SerialPort_SendByte(send_byte);
SerialPort_SendByte('\r');
SerialPort_SendByte('\n');
//发送数组
SerialPort_SendArray(send_array,6);
SerialPort_SendByte('\r');
SerialPort_SendByte('\n');
//发送字符串
SerialPort_SendString("Hello World!\r\n");
//发送数字的每一位
SerialPort_SendNum(65535, 5);
SerialPort_SendString("\r\n");
while(1){
 // //循环发送数字
 // SerialPort_SendByte(send_byte);
 // OLED_ShowHexNum(1,9,send_byte,2);
 // send_byte++;
 // Delay_ms(1000);
 };
}
- SerialPort.h

#ifndef __SERIALPORT_H
#define __SERIALPORT_H
void SerialPort_Init(void);
void SerialPort_SendByte(uint8_t send_byte);
void SerialPort_SendArray(uint8_t *send_array, uint16_t size_array);
void SerialPort_SendString(char *send_string);
void SerialPort_SendNum(uint32_t send_num, uint16_t send_len);
#endif
- SerialPort.c
#include "stm32f10x.h" // Device header
//串口初始化-USART1
void SerialPort_Init(void){
//1.开启RCC外设时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//2.初始化GPIO-PA9
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//3.初始化USART结构体
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStructure);
//4.配置中断,开启NVIC(接收数据使用)
//5.开启外设
USART_Cmd(USART1, ENABLE);
}
//串口发送1字节数据
void SerialPort_SendByte(uint8_t send_byte){
//向发送数据寄存器TDR中写入数据
USART_SendData(USART1, send_byte);
//确认数据被转移到发送移位寄存器(等待标志位TXE置位)
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE)==RESET);
}
//发送一个数组
void SerialPort_SendArray(uint8_t *send_array, uint16_t size_array){
uint8_t i=0;
for(i=0;i<size_array;i++){
SerialPort_SendByte(send_array[i]);
}
}
//发送一个字符串
void SerialPort_SendString(char *send_string){
uint8_t i=0;
for(i=0; send_string[i]!='\0'; i++){
SerialPort_SendByte(send_string[i]);
}
}
//非外部调用函数-幂次函数
uint32_t SerialPort_Pow(uint32_t X, uint32_t Y){
uint32_t result = 1;
while(Y--){
result *= X;
}
return result;
}
//发送数字的每一位-先发高位
void SerialPort_SendNum(uint32_t send_num, uint16_t send_len){
uint16_t i;
for(i=0;i<send_len;i++){
SerialPort_SendByte((send_num/SerialPort_Pow(10,send_len-i-1))%10+'0');
}
}

3.对C语言自带的函数printf 函数进行移植和封装(常用)

移植 printf 函数
需求:将C语言自带函数 printf 进行封装,默认成将需要打印的数据发送到串口,进而可以显示在电脑端串口助手上。接线图、函数调用(非库函数) 与实验“串口发送”相同
方法一:

  1. 首先点击“魔术棒”,在Target界面的“Code Generation”方框中勾选“USE MicroLIB”。MicroLIB是Keil为嵌入式平台优化的一个精简库,要使用 printf 函数就会用到这个MicroLIB精简库。
  2. 对 printf 函数重定向,将 printf 函数打印的东西输出到串口。于是在 SerialPort.c 模块中添加下列代码:
#include <stdio.h>
//对printf函数重定向-将fputc函数原型重定向到串口
//注:ptintf函数本质上就是循环调用fputc,将字符一个一个输出
int fputc(int ch, FILE *f){
SerialPort_SendByte(ch);
return ch;
}

在 SerialPort.h 模块中添加下列代码:
#include <stdio.h>
于是就可以在 main.c 中调用 printf 函数,将数据输出到串口了。
//使用重定向的printf函数
printf(“%d\r\n”,666);
但注意这个方法直接将 printf 函数重定向到了USART1,别的USART外设(USART2、USART3等)想用就没办法了。所以有如下的改进方法。

方法二:
若多个串口都想使用 printf 函数,那么就可以使用 sprintf 函数。 sprintf 函数可以将格式化字符输出到一个字符串里,然后再调用相应的“串口发送字符串”函数发送这个
字符串,整个过程不涉及重定向,于是就实现了所有USART外设都可以打印信息到串口了。所以下面可以直接在 main.c 中定义:
char String[100];//定义一个足够长的字符串数组
sprintf(String, “Num=%d\r\n”, 666);//将格式化字符串存储在String中
SerialPort_SendString(String);//串口发送字符串

方法三:
方法二的 sprintf 函数很方便,但是直接在主函数中写比较麻烦,于是本方法就是来封装“方法二”。具体方法如下:
3. C语言可变参数。在串口模块 SerialPort.c 中添加下面代码:

 #include <stdarg.h>
//对sprintf函数进行封装
void SerialPort_Printf(char *format,...){
char String[100];//定义输出的字符串
va_list arg;//定义参数列表变量
va_start(arg, format);//从format位置开始接收参数表,放在arg里面
vsprintf(String, format, arg);//sprintf只接收直接写的参数,对于封装格式改用vsprintf
va_end(arg);//释放参数表
SerialPort_SendString(String);
}
//注意上述函数要在头文件中声明,但头文件就不需要再添加<stdarg.h>了。

直接在主函数中调用:
SerialPort_Printf(“Num=%d\r\n”, 888);

特殊说明:显示汉字
使用上面几种 printf 函数时,有可能会出现乱码,要解决这个问题,关键是要保持Keil编译器(“扳手”图标Editor界面的Encoding下拉菜单)和“串口助手”的文本编码一致。
可以选择以下两种情况:
(1). 两者都选择UTF-8编码。但是直接在字符串写汉字,编译器有可能报错,解决方法是点击“魔术棒”–>C/C+±->最下面的Controls输入 --no-multibyte-chars 即可。
(2). 有些串口助手软件可能不兼容UTF-8,所以两者都需要选择GB2312编码。
注:更改编译器文本编码格式后,需要将文件全部关闭,重新打开,否则编码方式不会改变。


总结

1-2引用,我常出现的问题,但是我没想到这是个问题,看了别人的博客才发觉。

  1. 关于接线。注意串口通信的两个设备是交叉连接的,所以“USB转串口模块”的TX应该接在stm32串口的RX(PA10)、RX应该接在stm32串口的TX(PA9)!
  2. 关于 sizeof 。相信很多人也想到,在封装发送数组的函数时,为什么不直接在函数中使用 sizeof 函数来计算数组的大小呢?这样能少传递一个参数,更简洁。但实际上,这里面的
    水很深,这样操作是不能得到正确结果的,具体原理可以参考CSDN博文“数组名不等于指针”。最终结论就是,数组名在传参过程中,会退化成一个指针,此时所表示的大小不是数组的
    实际大小,而是这个指针的大小,所以要想在函数中使用到数组大小,最好还是多传递一个参数。
  3. 串口这里,发送数据帧与发送数据包,不同软件,波特率等等,涉及到足够多的代码。我把STM32写完原理后,打算开几章示例代码讲解与说明,例子来源于当时学的江科大自化协。方便理解
  • 32
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值