【STM32】USART串口通讯

1.USART简介

STM32芯片具有多个USART外设用于串口通讯,它是 Universal Synchronous Asynchronous Receiver and Transmitter的缩写, 即通用同步异步收发器可以灵活地与外部设备进行全双工数据交换。有别于USART, 它还有具有UART外设(Universal Asynchronous Receiver and Transmitter),它是在USART基础上裁剪掉了同步通信功能, 只有异步通信。简单区分同步和异步就是看通信时需不需要对外提供时钟输出,我们平时用的串口通信基本都是UART。

USART在STM32应用最多莫过于"打印"程序信息,一般在硬件设计时都会预留一个USART通信接口连接电脑, 用于在调试程序是可以把一些调试信息"打印"在电脑端的串口调试助手工具上,从而了解程序运行是否正确、指出运行出错位置等等。

USART只需两根信号线(TX和RX)即可完成双向通信,对硬件要求低,使得很多模块都预留USART接口来实现与其他模块或者控制器进行数据传输, 比如GSM模块,WIFI模块、蓝牙模块等等。在硬件设计时,注意还需要一根"共地线"。

2.USART标准库简介

c 复制代码
typedef struct {
    uint32_t USART_BaudRate;            // 波特率
    uint16_t USART_WordLength;          // 字长
    uint16_t USART_StopBits;            // 停止位
    uint16_t USART_Parity;              // 校验位
    uint16_t USART_Mode;                // USART模式
    uint16_t USART_HardwareFlowControl; // 硬件流控制
} USART_InitTypeDef;
  1. USART_BaudRate:波特率设置。一般设置为2400、9600、19200、115200。标准库函数会根据设定值计算得到USARTDIV值, 并设置USART_BRR寄存器值。

  2. USART_WordLength:数据帧字长,可选8位或9位。它设定USART_CR1寄存器的M位的值。如果没有使能奇偶校验控制, 一般使用8数据位;如果使能了奇偶校验则一般设置为9数据位。

  3. USART_StopBits:停止位设置,可选0.5个、1个、1.5个和2个停止位, 它设定USART_CR2寄存器的STOP[1:0]位的值,一般我们选择1个停止位。

  4. USART_Parity:奇偶校验控制选择,可选USART_Parity_No(无校验)、 USART_Parity_Even(偶校验)以及USART_Parity_Odd(奇校验),它设定USART_CR1寄存器的PCE位和PS位的值。

  5. USART_Mode:USART模式选择,有USART_Mode_Rx和USART_Mode_Tx, 允许使用逻辑或运算选择两个,它设定USART_CR1寄存器的RE位和TE位。

  6. USART_HardwareFlowControl:硬件流控制选择,只有在硬件流控制模式才有效, 可选有使能RTS、使能CTS、同时使能RTS和CTS、不使能硬件流。

当使用同步模式时需要配置SCLK引脚输出脉冲的属性,标准库使用一个时钟初始化结构体USART_ClockInitTypeDef来设置, 因此该结构体内容也只有在同步模式才需要设置。

c 复制代码
typedef struct {
    uint16_t USART_Clock;    // 时钟使能控制
    uint16_t USART_CPOL;     // 时钟极性
    uint16_t USART_CPHA;     // 时钟相位
    uint16_t USART_LastBit;  // 最尾位时钟脉冲
} USART_ClockInitTypeDef;
  1. USART_Clock:同步模式下SCLK引脚上时钟输出使能控制,可选禁止时钟输出(USART_Clock_Disable)或开启时钟输出(USART_Clock_Enable); 如果使用同步模式发送,一般都需要开启时钟。它设定USART_CR2寄存器的CLKEN位的值。

  2. USART_CPOL:同步模式下SCLK引脚上输出时钟极性设置,可设置在空闲时SCLK引脚为低电平(USART_CPOL_Low)或高电平(USART_CPOL_High)。 它设定USART_CR2寄存器的CPOL位的值。

  3. USART_CPHA:同步模式下SCLK引脚上输出时钟相位设置,可设置在时钟第一个变化沿捕获数据(USART_CPHA_1Edge)或在时钟第二个变化沿捕获数据。 它设定USART_CR2寄存器的CPHA位的值。USART_CPHA与USART_CPOL配合使用可以获得多种模式时钟关系。

  4. USART_LastBit:选择在发送最后一个数据位的时候时钟脉冲是否在SCLK引脚输出,可以是不输出脉冲(USART_LastBit_Disable)、 输出脉冲(USART_LastBit_Enable)。它设定USART_CR2寄存器的LBCL位的值。

3.USART示例

3.1硬件设计

为利用USART实现开发板与电脑通信,需要用到一个USB转USART的IC,我们选择CH340芯片来实现这个功能,这个芯片需要安装驱动。

3.2编程要点
  1. 使能RX和TX引脚GPIO时钟和USART时钟;
  2. 初始化GPIO,并将GPIO复用到USART上;
  3. 配置USART参数;
  4. 配置中断控制器并使能USART接收中断;
  5. 使能USART;
  6. 在USART接收中断服务函数实现数据接收和发送。
c 复制代码
void Init_USART(void)
{
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);//使能GPIOA时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//使能USART1时钟
	
    //USART1对应引脚复用映射
	GPIO_PinAFConfig(GPIOA, GPIO_PinSource9,GPIO_AF_USART1);//PA9复用为USART1
	GPIO_PinAFConfig(GPIOA, GPIO_PinSource10,GPIO_AF_USART1);//PA10复用为USART1
	
	//USART1端口配置
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_9|GPIO_Pin_10;
	GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AF;//复用功能
	GPIO_InitStruct.GPIO_OType=GPIO_OType_PP;//推挽复用输出
	GPIO_InitStruct.GPIO_PuPd=GPIO_PuPd_UP;//上拉
	GPIO_InitStruct.GPIO_Speed=GPIO_Fast_Speed;//速度50MHz
	GPIO_Init(GPIOA,&GPIO_InitStruct);//初始化PA9,PA10
		
	//配置USART参数
	USART_InitTypeDef USART_Init_Struct;
	USART_Init_Struct.USART_BaudRate=115200;
	USART_Init_Struct.USART_HardwareFlowControl=USART_HardwareFlowControl_None;
	USART_Init_Struct.USART_Mode=USART_Mode_Rx|USART_Mode_Tx;
	USART_Init_Struct.USART_Parity=USART_Parity_No;
	USART_Init_Struct.USART_StopBits=USART_StopBits_1;
	USART_Init_Struct.USART_WordLength=USART_WordLength_8b;
	USART_Init(USART1,&USART_Init_Struct);
	
	//配置中断控制器并使能USART接收中断
	NVIC_InitTypeDef NVIC_Init_Struct;
	NVIC_Init_Struct.NVIC_IRQChannel=USART1_IRQn;
	NVIC_Init_Struct.NVIC_IRQChannelCmd=ENABLE;
	NVIC_Init_Struct.NVIC_IRQChannelPreemptionPriority=1;
	NVIC_Init_Struct.NVIC_IRQChannelSubPriority=0;
	NVIC_Init(&NVIC_Init_Struct);
	USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
	
	//使能USART
	USART_Cmd(USART1,ENABLE);
}
c 复制代码
void USART1_IRQHandler(void)
{
	uint8_t ucTemp;
	if(SET==USART_GetITStatus(USART1,USART_IT_RXNE))
	{
		ucTemp = USART_ReceiveData(USART1);
        USART_SendData(USART1,ucTemp);
	}
}
c 复制代码
int main(void)
{
	//重新设置系统时钟
	HSE_SetSysClock(8, 336, 2, 7);

	//设置中断分组
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
	
	Init_USART();
	
  /* Infinite loop */
  while (1)
  {

  }
}

但是,出问题了!!!

出现乱码了!!!到底是哪里出问题了呢???

最后,我们在USART_Init函数中发现蹊跷了。关于波特率的设置,它通过配置值115200,然后通过系统时钟去计算应该配置的寄存器的值。但是,系统时钟的获取函数好像有点问题,因为之前那个宏定义的值不对,所以我们在main函数中又自己实现了系统时钟配置函数。详细可以看【STM32】时钟树系统-CSDN博客。现在看来,我们还是的修改标准库文件了,因为它的函数接口都在使用那个错误的宏定义。

所以,修改了标准库文件中的两个宏定义后,我们就可以在main函数中省略自己配置系统时钟函数了。

c 复制代码
int main(void)
{
	//设置中断分组
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
	
	Init_USART();
	
  /* Infinite loop */
  while (1)
  {

  }
}

同时也发现无乱码了。

4.使用USART打印日志

c 复制代码
#ifndef __BSP_USART_H
#define __BSP_USART_H

#ifdef __cplusplus
extern "C"{

#endif

#include "stm32f4xx.h"
#include "stdio.h"

#define LOGGER_USART              USART1
#define LOGGER_USART_BAUDRATE     115200
#define LOGGER_USART_CLK          RCC_APB2Periph_USART1
#define LOGGER_USART_IRQHandler   USART1_IRQHandler
#define LOGGER_USART_IRQ          USART1_IRQn

#define USART1_TX_PIN             GPIO_Pin_9
#define USART1_TX_GPIO_Port       GPIOA
#define USART1_TX_GPIO_CLK        RCC_AHB1Periph_GPIOA
#define USART1_TX_AF              GPIO_AF_USART1
#define USART1_TX_SOURCE          GPIO_PinSource9

#define USART1_RX_PIN             GPIO_Pin_10
#define USART1_RX_GPIO_Port       GPIOA
#define USART1_RX_GPIO_CLK        RCC_AHB1Periph_GPIOA
#define USART1_RX_AF              GPIO_AF_USART1
#define USART1_RX_SOURCE          GPIO_PinSource10


void Init_USART(void);
	
#ifdef __cplusplus
}
#endif

#endif
c 复制代码
#include "bsp_usart.h"

void Init_USART(void)
{
	RCC_AHB1PeriphClockCmd(USART1_TX_GPIO_CLK|USART1_RX_GPIO_CLK,ENABLE);//使能GPIOA时钟
	RCC_APB2PeriphClockCmd(LOGGER_USART_CLK,ENABLE);//使能USART1时钟
	
    //USART1对应引脚复用映射
	GPIO_PinAFConfig(USART1_TX_GPIO_Port, USART1_TX_SOURCE,USART1_TX_AF);//PA9复用为USART1
	GPIO_PinAFConfig(USART1_RX_GPIO_Port, USART1_RX_SOURCE,USART1_RX_AF);//PA10复用为USART1
	
	//USART1端口配置
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Pin=USART1_TX_PIN;
	GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AF;//复用功能
	GPIO_InitStruct.GPIO_OType=GPIO_OType_PP;//推挽复用输出
	GPIO_InitStruct.GPIO_PuPd=GPIO_PuPd_UP;//上拉
	GPIO_InitStruct.GPIO_Speed=GPIO_Fast_Speed;//速度50MHz
	GPIO_Init(USART1_TX_GPIO_Port,&GPIO_InitStruct);//初始化PA9
	
	GPIO_InitStruct.GPIO_Pin=USART1_RX_PIN;
	GPIO_Init(USART1_RX_GPIO_Port,&GPIO_InitStruct);//初始化PA10
		
	//配置USART参数
	USART_InitTypeDef USART_Init_Struct;
	USART_Init_Struct.USART_BaudRate=LOGGER_USART_BAUDRATE;
	USART_Init_Struct.USART_HardwareFlowControl=USART_HardwareFlowControl_None;
	USART_Init_Struct.USART_Mode=USART_Mode_Rx|USART_Mode_Tx;
	USART_Init_Struct.USART_Parity=USART_Parity_No;
	USART_Init_Struct.USART_StopBits=USART_StopBits_1;
	USART_Init_Struct.USART_WordLength=USART_WordLength_8b;
	USART_Init(LOGGER_USART,&USART_Init_Struct);
	
	//配置中断控制器并使能USART接收中断
	NVIC_InitTypeDef NVIC_Init_Struct;
	NVIC_Init_Struct.NVIC_IRQChannel=LOGGER_USART_IRQ;
	NVIC_Init_Struct.NVIC_IRQChannelCmd=ENABLE;
	NVIC_Init_Struct.NVIC_IRQChannelPreemptionPriority=1;
	NVIC_Init_Struct.NVIC_IRQChannelSubPriority=0;
	NVIC_Init(&NVIC_Init_Struct);
	USART_ITConfig(LOGGER_USART,USART_IT_RXNE,ENABLE);
	
	//使能USART
	USART_Cmd(LOGGER_USART,ENABLE);
}

void LOGGER_USART_IRQHandler(void)
{
	uint8_t ucTemp;
	if(SET==USART_GetITStatus(LOGGER_USART,USART_IT_RXNE))
	{
		ucTemp = USART_ReceiveData(LOGGER_USART);
        USART_SendData(LOGGER_USART,ucTemp);
	}
}

//重定向c库函数printf到串口,重定向后可使用printf函数
int fputc(int ch, FILE *f)
{
    /* 发送一个字节数据到串口 */
    USART_SendData(LOGGER_USART, (uint8_t) ch);

    /* 等待发送完毕 */
    while (USART_GetFlagStatus(LOGGER_USART, USART_FLAG_TXE) == RESET);

    return (ch);
}
c 复制代码
#include "bsp_usart.h"

void delay(uint32_t cnt)
{
	while(cnt--);
}

int main(void)
{
	//设置中断分组
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
	
	Init_USART();
	
  /* Infinite loop */
  while (1)
  {
	  printf("hello,i am logger!\r\n");
	  delay(0xfffffff);
  }
}

这样,我们就可以使用printf函数直接输出到串口上了。

在C语言标准库中,fputc函数是printf函数内部的一个函数,功能是将字符ch写入到文件指针f所指向文件的当前写指针位置, 简单理解就是把字符写入到特定文件中。我们使用USART函数重新修改fputc函数内容,达到类似"写入"的功能。

fgetc函数与fputc函数非常相似,实现字符读取功能。在使用scanf函数时需要注意字符输入格式。

还有一点需要注意的,使用fput和fgetc函数达到重定向C语言标准库输入输出函数必须在MDK的工程选项把"Use MicroLIB"勾选上, MicoroLIB是缺省C库的备选库,它对标准C库进行了高度优化使代码更少,占用更少资源。

为使用printf、scanf函数需要在文件中包含stdio.h头文件。

相关推荐
小达爱学习6 小时前
verilog实现PID控制
fpga开发
远望创客学堂7 小时前
【单片机毕业设计选题24047】-基于阿里云的工地环境监测系统
stm32·单片机·嵌入式硬件·阿里云·云计算·课程设计·arduino
极客小张7 小时前
利用 STM32 实现多协议物联网网关:Modbus/Zigbee 到以太网/Wi-Fi 的数据桥接
stm32·单片机·嵌入式硬件·物联网·网络协议·https·硬件工程
FPGAmaster创新者8 小时前
基于AGX ORIN与FPGA K7实现PCIE高速数据通信/Orin与FPGA高速数据传输/XDMA在linux系统使用教程
linux·嵌入式硬件·fpga开发
XD7429716368 小时前
【TB作品】电子琴,ATMEGA16单片机,Proteus仿真
单片机·proteus·电子琴·atmega
maybe_YX8 小时前
51单片机基础8——单片机控制超声波模块
c语言·单片机·嵌入式硬件·51单片机
9527华安9 小时前
Zynq系列FPGA实现SDI视频编解码,基于GTX高速接口,提供5套工程源码和技术支持
fpga开发·视频编解码·sdi·gtx
@一二三四五9 小时前
STM32 看门狗 HAL
stm32·单片机·嵌入式硬件
不会倒的鸡蛋9 小时前
单片机与FPGA的关系及其在嵌入式系统中的应用
fpga开发
maybe_YX9 小时前
51单片机基础10——串口实验
c语言·单片机·嵌入式硬件·51单片机