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标准库简介
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;
-
USART_BaudRate:波特率设置。一般设置为2400、9600、19200、115200。标准库函数会根据设定值计算得到USARTDIV值, 并设置USART_BRR寄存器值。
-
USART_WordLength:数据帧字长,可选8位或9位。它设定USART_CR1寄存器的M位的值。如果没有使能奇偶校验控制, 一般使用8数据位;如果使能了奇偶校验则一般设置为9数据位。
-
USART_StopBits:停止位设置,可选0.5个、1个、1.5个和2个停止位, 它设定USART_CR2寄存器的STOP[1:0]位的值,一般我们选择1个停止位。
-
USART_Parity:奇偶校验控制选择,可选USART_Parity_No(无校验)、 USART_Parity_Even(偶校验)以及USART_Parity_Odd(奇校验),它设定USART_CR1寄存器的PCE位和PS位的值。
-
USART_Mode:USART模式选择,有USART_Mode_Rx和USART_Mode_Tx, 允许使用逻辑或运算选择两个,它设定USART_CR1寄存器的RE位和TE位。
-
USART_HardwareFlowControl:硬件流控制选择,只有在硬件流控制模式才有效, 可选有使能RTS、使能CTS、同时使能RTS和CTS、不使能硬件流。
当使用同步模式时需要配置SCLK引脚输出脉冲的属性,标准库使用一个时钟初始化结构体USART_ClockInitTypeDef来设置, 因此该结构体内容也只有在同步模式才需要设置。
typedef struct {
uint16_t USART_Clock; // 时钟使能控制
uint16_t USART_CPOL; // 时钟极性
uint16_t USART_CPHA; // 时钟相位
uint16_t USART_LastBit; // 最尾位时钟脉冲
} USART_ClockInitTypeDef;
-
USART_Clock:同步模式下SCLK引脚上时钟输出使能控制,可选禁止时钟输出(USART_Clock_Disable)或开启时钟输出(USART_Clock_Enable); 如果使用同步模式发送,一般都需要开启时钟。它设定USART_CR2寄存器的CLKEN位的值。
-
USART_CPOL:同步模式下SCLK引脚上输出时钟极性设置,可设置在空闲时SCLK引脚为低电平(USART_CPOL_Low)或高电平(USART_CPOL_High)。 它设定USART_CR2寄存器的CPOL位的值。
-
USART_CPHA:同步模式下SCLK引脚上输出时钟相位设置,可设置在时钟第一个变化沿捕获数据(USART_CPHA_1Edge)或在时钟第二个变化沿捕获数据。 它设定USART_CR2寄存器的CPHA位的值。USART_CPHA与USART_CPOL配合使用可以获得多种模式时钟关系。
-
USART_LastBit:选择在发送最后一个数据位的时候时钟脉冲是否在SCLK引脚输出,可以是不输出脉冲(USART_LastBit_Disable)、 输出脉冲(USART_LastBit_Enable)。它设定USART_CR2寄存器的LBCL位的值。
3.USART示例
3.1硬件设计
为利用USART实现开发板与电脑通信,需要用到一个USB转USART的IC,我们选择CH340芯片来实现这个功能,这个芯片需要安装驱动。
3.2编程要点
- 使能RX和TX引脚GPIO时钟和USART时钟;
- 初始化GPIO,并将GPIO复用到USART上;
- 配置USART参数;
- 配置中断控制器并使能USART接收中断;
- 使能USART;
- 在USART接收中断服务函数实现数据接收和发送。
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);
}
void USART1_IRQHandler(void)
{
uint8_t ucTemp;
if(SET==USART_GetITStatus(USART1,USART_IT_RXNE))
{
ucTemp = USART_ReceiveData(USART1);
USART_SendData(USART1,ucTemp);
}
}
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函数中省略自己配置系统时钟函数了。
int main(void)
{
//设置中断分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
Init_USART();
/* Infinite loop */
while (1)
{
}
}
同时也发现无乱码了。
4.使用USART打印日志
#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
#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);
}
#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头文件。