【ARM Cortex-M 开发实战指南(基础篇)】第10章 串口通信

开发环境:
MDK:Keil 5.30
STM32CubeMX:V6.4.0
MCU:STM32F103ZET6

10.1串口简介

USART(Universal Synchronous Asynchronous Receiver and Transmitter,通用同步-异步接收发射器)提供了一种灵活的方法与使用工业标准NRZ异步串行数据格式的外部设备之间进行全双工数据交换。USART利用分数波特率发生器提供宽范围的波特率选择。它支持同步单向通信和半双工单线通信,也支持LIN(局部互连网),智能卡协议和IrDA(红外数据组织)SIR ENDEC规范,以及调制解调器(CTS/RTS)操作。它还允许多处理器通信。使用多缓冲器配置的DMA方式,可以实现高速数据通信。

虽然USART既可以同步又可以异步,但是常见的最常用的就是使用功能的异步功能,如果作为异步通信就是UART(Universal Asynchronous Receiver and Transmitter),可以说,UART是USART的子集,但是同步通信相比异步通信多了一根时钟同步信号线。

下面简单介绍下同步和异步。

在同步通讯中,收发设备双方会使用一根信号线表示时钟信号,在时钟信号的驱动下双方进行协调,同步数据,见下图。通讯中通常双方会统一规定在时钟信号的上升沿或下降沿对数据线进行采样。

Xd30ER.md.png

在异步通讯中不使用时钟信号进行数据同步,它们直接在数据信号中穿插一些同步用的信号位,或者把主体数据进行打包,以数据帧的格式传输数据,见下图,某些通讯中还需要双方约定数据的传输速率,以便更好地同步。

Xd3D4x.md.png

在同步通讯中,数据信号所传输的内容绝大部分就是有效数据,而异步通讯中会包含有帧的各种标识符,所以同步通讯的效率更高,但是同步通讯双方的时钟允许误差较小,而异步通讯双方的时钟允许误差较大。

从上面的介绍可以看出,USART以同步方式通信需要时钟同步信号,但不需要额外的起始、停止位,可以实现更快的传输速度。但USART控制起来更复杂,因此本文主要讲解以异步通信。

异步串行通信以字符为单位,即一个字符一个字符地传送 。

Xd36gO.md.png

串口外设的架构图看起来十分复杂,实际上对于软件开发人员来说,我们只需要大概了解串口发送的过程即可。从下至上,我们看到串口外设主要由三个部分组成,分别是波特率控制、收发控制和数据存储转移。

 波特率控制
波特率,即每秒传输的二进制位数,用 b/s (bps)表示,通过对时钟的控制可以改变波特率。在配置波特率时,我们向波特比率寄存器 USART_BRR 写入参数,修改了串口时钟的分频值USARTDIV。USART_BRR 寄存器包括两部分,分别是 DIV_Mantissa(USARTDIV 的整数部分)和 DIV_Fraction(USARTDIV 的小数)部分,最终,计算公式为 USARTDIV=DIV_Mantissa+(DIV_Fraction/16)。

USARTDIV 是对串口外设的时钟源进行分频的,对于USART1,由于它挂载在 APB2总线上,所以它的时钟源为 PCLK2 ;而 USART2、3 挂载在 APB1上,时钟源则为 PCLK1,串口的时钟源经过 USARTDIV 分频后分别输出作为发送器时钟及接收器时钟,控制发送和接收的时序。

Xd3cvD.md.png

 收发控制
围绕着发送器和接收器控制部分,有好多个寄存器 :CR1、CR2、CR3 和 SR,即USART 的三个控制寄存器(Control Register)及一个状态寄存器(Status Register)。通过向寄存器写入 各种控制参数来控制发送和接收,如奇偶校验位、停止位等,还包括对USART 中断的控制 ;串口的状态在任何时候都可以从状态寄存器中查询得到。其中停止位的配置如下图所示。

XdsqMj.md.png

 发送配置步骤:

1.通过在USART_CR1寄存器上置位UE位来激活USART
2.编程USART_CR1的M位来定义字长。
3.在USART_CR2中编程停止位的位数。
4.如果采用多缓冲器通信,配置USART_CR3中的DMA使能位(DMAT)。按多缓冲器通信中的描述配置DMA寄存器。
5.利用USART_BRR寄存器选择要求的波特率。
6.设置USART_CR1中的TE位,发送一个空闲帧作为第一次数据发送。
7.把要发送的数据写进USART_DR寄存器(此动作清除TXE位)。在只有一个缓冲器的情况下,对每个待发送的数据重复步骤7。
8.在USART_DR寄存器中写入最后一个数据字后,要等待TC=1,它表示最后一个数据帧的传输结束。当需要关闭USART或需要进入停机模式之前,需要确认传输结束,避免破坏最后一次传输。

 接收配置步骤:

1.将USART_CR1寄存器的UE置1来激活USART。
2.编程USART_CR1的M位定义字长
3.在USART_CR2中编写停止位的个数
4.如果需多缓冲器通信,选择USART_CR3中的DMA使能位(DMAR)。按多缓冲器通信所要求的配置DMA寄存器。
5.利用波特率寄存器USART_BRR选择希望的波特率。
6.设置USART_CR1的RE位。激活接收器,使它开始寻找起始位。

 数据存储转移

收发控制器根据我们的寄存器配置,对数据存储转移部分的移位寄存器进行控制。当我们需要发送数据时,内核或 DMA 外设(一种数据传输方式,在后面介绍)把数据从内存(变量)写入到发送数据寄存器 TDR 后,发送控制器将适时地自动把数据从 TDR 加载到发送移位寄存器,然后通过串口线 Tx,把数据一位一位地发送出去,当数据从 TDR转移到移位寄存器时,会产生发送寄存器 TDR 已空事件 TXE,当数据从移位寄存器全部发送出去时,会产生数据发送完成事件 TC,这些事件可以在状态寄存器中查询到。而接收数据则是一个逆过程,数据从串口线 Rx 一位一位地输入到接收移位寄存器,然后自动地转移到接收数据寄存器 RDR,最后用内核指令或 DMA 读取到内存(变量)中。

以上对串口通信进行了简单介绍,为了方便各位读者朋友更好的理解,在这里笔者将引入一个新的思想–系统分层思想。既然各位对着有意于嵌入式,那么必须得有对整个系统的架构要有一定的认知。对STM32裸机开发,我们可以将分为三层:物理层、协议层和应用层。前文讲了这么多也是对串口协议进行分析,常用的物理层的串口通信标准有232和485。

【注】UART和USART的区别
USART(universal synchronous asynchronous receiver and transmitte): 通用同步异步收发器,USART是一个串行通信设备,可以灵活地与外部设备进行全双工数据交换。

UART(universal asynchronous receiver and transmitter): 通用异步收发器,异步串行通信口(UART)就是我们在嵌入式中常说的串口,它还是一种通用的数据通信议。从名字上可以看出,USART在UART基础上增加了同步功能,即USART是UART的增强型。

当我们使用USART在异步通信的时候,它与UART没有什么区别,但是用在同步通信的时候,区别就很明显了:大家都知道同步通信需要时钟来触发数据传输,也就是说USART相对UART的区别之一就是能提供主动时钟。如STM32的USART可以提供时钟支持ISO7816的智能卡接口。

USART是指单片机的一个端口模块,可以根据需要配置成同步模式(SPI,I2C),也可以将其配置为异步模式,后者就是UART。所以说UART姑且可以称之为一个与SPI,I2C对等的“协议”,而USART则不是一个协议,而是更应该理解为一个实体。相比于同步通讯,UART不需要统一的时钟线,接线更加方便。但是,为了正常的对信号进行解码,使用UART通讯的双方必须事先约定好波特率,即每个码元的长度。

关于串口的深入理解,请参看笔者文章:

深入理解淳口通信

10.2串口通信的寄存器描述

串口常用的寄存器有状态寄存器(USART_SR)、数据寄存器(USART_DR)、波特比率寄存器(USART_BRR)、控制寄存器 (USART_CR)。

XdsLss.md.png

XdsOLn.md.png

XdsjZq.md.png

Xdsvd0.md.png

10.3串口硬件

串口的接口通过三个引脚与其他设备连接在一起。任何USART双向通信至少需要两个脚:接收数据输入(RX)和发送数据输出(TX)。

RX:接收数据串行输入。通过采样技术来区别数据和噪音,从而恢复数据。
TX :发送数据输出。当发送器被禁止时,输出引脚恢复到它的I/O端口配置。当发送器被激活,并且不发送数据时,TX引脚处于高电平。在单线和智能卡模式里,此I/O 口被同时用于数据的发送和接收。

XdsxoV.png

10.4串口发送(重定向printf)

10.4.1串口发送实现-标准库

下面笔者就用标准库来操作串口1。

1.串口配置

 串口1时钟使能

串口1是挂载在 APB2 下面的外设,所以使能函数为:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1);

值得注意的是,不仅要打开串口的时钟,还需要打开相应GPIO的时钟,最终的代码如下:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);

 串口复位

当外设出现异常的时候可以通过复位设置,实现该外设的复位,然后重新配置这个外设达到让其重新工作的目的。一般在系统刚开始配置外设的时候,都会先执行复位该外设的操作。复位的是在函数 USART_DeInit()中完成:

void USART_DeInit(USART_TypeDef* USARTx);//串口复位

比如我们要复位串口1,方法为:

USART_DeInit(USART1); //复位串口 1

 配置串口GPIO
这个比较简单,前面的章节已经讲过了,只需要注意的是,这里的GPIO不再是普通GPIO,要配置成复用功能,因此TX和RX分别配置成GPIO_Mode_AF_PP和GPIO_Mode_IN_FLOATING。

 串口参数初始化

串口初始化是通过 USART_Init()函数实现的,

void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);

这个函数的第一个入口参数是指定初始化的串口标号,这里选择 USART1。第二个入口参数是一个 USART_InitTypeDef 类型的结构体指针, 这个结构体指针的成员变量用来设置串口的一些参数。 一般的实现格式为:

USART_InitStructure.USART_BaudRate = 115200; //波特率设置;
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; //收发模式
USART_Init(USART1, &USART_InitStructure); //初始化串口

从上面的初始化格式可以看出初始化需要设置的参数为:波特率,字长,停止位,奇偶校验位,硬件数据流控制,模式(收,发)。 我们可以根据需要设置这些参数。

 串口使能
串口使能是通过函数 USART_Cmd()来实现的,这个很容易理解,使用方法是:

USART_Cmd(USART1, ENABLE); //使能串口

到此,串口初始化的基本配置就算完成了,完整初始化代码如下:

/**
  * @brief  USART1 GPIO 配置,工作模式配置。115200 8-N-1
  * @param  None
  * @retval None
  */
void USART_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;

    /* config USART1 clock */
     RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);

    USART_DeInit(USART1); //复位串口 1

    /* USART1 GPIO config */
    /* Configure USART1 Tx (PA.09) as alternate function push-pull */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* Configure USART1 Rx (PA.10) as input floating */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* USART1 mode config */
    USART_InitStructure.USART_BaudRate = 115200;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    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;
    USART_Init(USART1, &USART_InitStructure); 
    USART_Cmd(USART1, ENABLE);
}

2.数据发送与接收
STM32 的发送与接收是通过数据寄存器 USART_DR 来实现的,这是一个双寄存器,包含了 TDR 和 RDR。当向该寄存器写数据的时候,串口就会自动发送,当收到数据的时候,也是存在该寄存器内。
STM32 库函数操作 USART_DR 寄存器发送数据的函数是:

void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);

通过该函数向串口寄存器 USART_DR 写入一个数据。

STM32 库函数操作 USART_DR 寄存器读取串口接收到的数据的函数是:

uint16_t USART_ReceiveData(USART_TypeDef* USARTx);

通过该函数可以读取串口接受到的数据。

3.串口状态
串口的状态可以通过状态寄存器 USART_SR 读取。

状态寄存器的其他位我们这里就不做过多讲解,大家需要可以查看中文参考手册。

在我们固件库函数里面,读取串口状态的函数是:

FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG);

这个函数的第二个入口参数非常关键, 它是标示我们要查看串口的哪种状态, 比如上面讲解的RXNE(读数据寄存器非空)以及 TC(发送完成)。例如我们要判断读寄存器是否非空(RXNE), 操作库函数的方法是:

USART_GetFlagStatus(USART1, USART_FLAG_RXNE);

我们要判断发送是否完成(TC),操作库函数的方法是:

USART_GetFlagStatus(USART1, USART_FLAG_TC);

这些标识号是通过宏定义定义的:

#define USART_IT_PE ((uint16_t)0x0028)
#define USART_IT_TXE ((uint16_t)0x0727)
#define USART_IT_TC ((uint16_t)0x0626)
#define USART_IT_RXNE ((uint16_t)0x0525)
#define USART_IT_IDLE ((uint16_t)0x0424)
#define USART_IT_LBD ((uint16_t)0x0846)
#define USART_IT_CTS ((uint16_t)0x096A)
#define USART_IT_ERR ((uint16_t)0x0060)
#define USART_IT_ORE ((uint16_t)0x0360)
#define USART_IT_NE ((uint16_t)0x0260)
#define USART_IT_FE ((uint16_t)0x0160)

另外,笔者在此给出输出格式的说明,请读者朋友参考。

格式 说明
%d 按照十进制整型数打印
%6d 按照十进制整型数打印,至少6个字符宽
%f 按照浮点数打印
%6f 按照浮点数打印,至少6个字符宽
%.2f 按照浮点数打印,小数点后有2位小数
%6.2f 按照浮点数打印,至少6个字符宽,小数点后有2位小数
%x 按照十六进制打印
%c 打印字符
%s 打印字符串

接下来就可以实现串口的发送了,这里对发送函数进行封装。

/**
  * @brief  串口发送一个字节数据 
  * @param  ch:待发送字符
  * @retval None
  */
void USART_SendByte(uint8_t ch)
{
    /* 发送一个字节数据到USART1 */
    USART_SendData(USART1,ch);

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

/**
  * @brief  串口发送指定长度的字符串
  * @param  str:待发送字符串缓冲器
  *         strlen:指定字符串长度
  * @retval None
  */
void USART_Send_String_length(uint8_t *str,uint32_t strlen)
{
    uint32_t k=0;
    do 
    {
        USART_SendByte(*(str + k));
        k++;
    } while(k < strlen);
}

/**
  * @brief  串口发送字符串,直到遇到字符串结束符
  * @param  str:待发送字符串缓冲器
  * @retval None
  */
void USART_Send_String(uint8_t *str)
{
    uint32_t k=0;
    do 
    {
        USART_SendByte(*(str + k));
        k++;
    } while(*(str + k)!='\0');
}

这样就方便多了,然后再主函数中调用发送函数。

/**
  * @brief     主函数
  * @param     None
  * @retval    int
  */
int main(void)
{
    char str[20];

    /* USART1 config 115200 8-N-1 */
    USART_Config();

    USART_Send_String((uint8_t*)"串口普通方式实验 \r\n");

    /* sprintf函数把格式化的数据写入某个字符串  */  
    sprintf(str,"20%02d-%02d-%02d",22,05,9); 

    USART_Send_String((uint8_t*)str);

    for(;;)
    {

    }
}

下面笔者还要介绍一种常用的串口打印方式I/O重定向,也就是使用printf打印数据到终端,但是我们的裸机系统没有终端,因此如果想让printf / scanf向USART1发送、获取数据,需要通过代码指定C标准库输入/输出函数的控制终端设备,也就是使用功能I/O重定向。

在stdio.h有相应的接口。

/*
    * dynamically allocates a buffer of the right size for the
    * formatted string, and returns it in (*strp). Formal return value
    * is the same as any other printf variant, except that it returns
    * -1 if the buffer could not be allocated.
    *
    * (The functions with __ARM_ prefixed names are identical to the
    * ones without, but are available in all compilation modes without
    * violating user namespace.)
    */
extern _ARMABI int fgetc(FILE * /*stream*/) __attribute__((__nonnull__(1)));
   /*
    * reads at most one less than the number of characters specified by n from
    * the stream pointed to by stream into the array pointed to by s. No
    * additional characters are read after a new-line character (which is
    * retained) or after end-of-file. A null character is written immediately
    * after the last character read into the array.
    * Returns: s if successful. If end-of-file is encountered and no characters
    *          have been read into the array, the contents of the array remain
    *          unchanged and a null pointer is returned. If a read error occurs
    *          during the operation, the array contents are indeterminate and a
    *          null pointer is returned.
    */
extern _ARMABI int fputc(int /*c*/, FILE * /*stream*/) __attribute__((__nonnull__(2)));

下面我们以实现printf打印数据到USART(即重定义fputc函数)的实现过程。

/**
  * @brief  重定向c库函数printf到USART1
  * @param  None
  * @retval 
  */
int fputc(int ch, FILE *f)
{
    /*清除标志位*/
    USART_ClearFlag(USART1,USART_FLAG_TC);

    /* 发送一个字节数据到USART1 */
    USART_SendData(USART1, (uint8_t) ch);

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

    return (ch);
}

scanf同理。

/**
  * @brief  重定向c库函数scanf到USART1
  * @param  None
  * @retval None
  */
int fgetc(FILE *f)
{
    /* 等待串口1输入数据 */
    while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);

    return (int)USART_ReceiveData(USART1);
}

接下来就可使用printf和scanf函数了。

/**
  * @brief     主函数
  * @param     None
  * @retval    int
  */
int main(void)
{
    char str[20];

    /* USART1 config 115200 8-N-1 */
    USART_Config();

    printf("串口普通方式实验 \r\n");

    /* sprintf函数把格式化的数据写入某个字符串  */  
    sprintf(str,"20%02d-%02d-%02d",22,05,9); 

    /* 调用格式化输出函数打印输出数据 */
    printf("%s\n",str);

    for(;;)
    {

    }
}

完整代码请查看配套程序,另外还需添加微库以便支持printf。具体设置参看本节后文的小贴士部分。

10.4.2串口发送实现-HAL库

10.4.2.1 STM32Cube生成工程

1.设置RCC
设置高速外部时钟HSE,选择外部时钟源。

XdySiT.md.png

2.时钟配置
笔者的板子使用的外部晶振为8MHz,选择外部时钟HSE 8MHz ,PLL锁相环9倍频后为72MHz,系统时钟来源选择为PLL,设置APB2分频器为 /1,这时候定时器的时钟频率为72Mhz。本文笔者使用的定时器是USART1,USART1挂在APB2上,不同的USART挂在不同总线上的。

XdypJU.md.png

【注】APB1上面连接的是低速外设,包括电源接口、备份接口、CAN、USB、I2C1、I2C2、USART2、USART3、UART4、UART5、SPI2、SP3等;而APB2上面连接的是高速外设,包括UART1、SPI1、Timer1、ADC1、ADC2、ADC3、所有的普通I/O口(PA-PE)、第二功能I/O(AFIO)口等。

3.串口配置
点击USATR1,设置MODE为异步通信(Asynchronous) ,波特率为115200 Bits/s。传输数据长度为8 Bit。奇偶检验无,停止位1 ,接收和发送都使能。

Xw6JLd.md.png

GPIO引脚设置 USART1_RX/USART_TX,默认即可。

Xw6teA.md.png

10.4.2.2串口发送代码讲解

我们先看如何实现的,再讲解具体的代码。先实现printf重定向函数,在main.c中添加如下函数。

/**
  * @brief 重定向c库函数printf到USARTx
  * @retval None
  */
int fputc(int ch, FILE *f)
{
  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
  return ch;
}

/**
  * @brief 重定向c库函数getchar,scanf到USARTx
  * @retval None
  */
int fgetc(FILE *f)
{
  uint8_t ch = 0;
  HAL_UART_Receive(&huart1, &ch, 1, 0xffff);
  return ch;
}

这里只讲解串口的参数初始化,代码如下:

static void MX_USART1_UART_Init(void)
{

  /* USER CODE BEGIN USART1_Init 0 */

  /* USER CODE END USART1_Init 0 */

  /* USER CODE BEGIN USART1_Init 1 */

  /* USER CODE END USART1_Init 1 */
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart1) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN USART1_Init 2 */

  /* USER CODE END USART1_Init 2 */

}

这是STM32cudeMX自动生成的代码,我们要注意UART_HandleTypeDef结构体,这个结构体就是用来配串口参数的,原型如下:

typedef struct __UART_HandleTypeDef
{
  USART_TypeDef                 *Instance;        /*!< UART registers base address        */

  UART_InitTypeDef              Init;             /*!< UART communication parameters      */

  uint8_t                       *pTxBuffPtr;      /*!< Pointer to UART Tx transfer Buffer */

  uint16_t                      TxXferSize;       /*!< UART Tx Transfer size              */

  __IO uint16_t                 TxXferCount;      /*!< UART Tx Transfer Counter           */

  uint8_t                       *pRxBuffPtr;      /*!< Pointer to UART Rx transfer Buffer */

  uint16_t                      RxXferSize;       /*!< UART Rx Transfer size              */

  __IO uint16_t                 RxXferCount;      /*!< UART Rx Transfer Counter           */

  DMA_HandleTypeDef             *hdmatx;          /*!< UART Tx DMA Handle parameters      */

  DMA_HandleTypeDef             *hdmarx;          /*!< UART Rx DMA Handle parameters      */

  HAL_LockTypeDef               Lock;             /*!< Locking object                     */

  __IO HAL_UART_StateTypeDef    gState;           /*!< UART state information related to global Handle management
                                                       and also related to Tx operations.
                                                       This parameter can be a value of @ref HAL_UART_StateTypeDef */

  __IO HAL_UART_StateTypeDef    RxState;          /*!< UART state information related to Rx operations.
                                                       This parameter can be a value of @ref HAL_UART_StateTypeDef */

  __IO uint32_t                 ErrorCode;        /*!< UART Error code                    */
} UART_HandleTypeDef;

这个结构体很简单,也有英文注释,笔者就不在赘述了。当然啦,除了使用普通方式发送,还可使用中断方式和DMA方式发送数据。这里中断发送数据就不讲了,下面会将中断接收,有兴趣的朋友请自行去看参考手册自行实现,DMA方式会在介绍DMA的时候讲解。

最后在main函数调用printf函数,代码如下:

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */
  char str[20];
  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
  printf("串口普通方式实验 \r\n");
  /* sprintf函数把格式化的数据写入某个字符串  */  
  sprintf(str,"20%02d-%02d-%02d",22,05,9); 

  /* 调用格式化输出函数打印输出数据 */
  printf("%s\n",str);

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

    HAL_Delay(1000);
  }
  /* USER CODE END 3 */
}

好了,这就是串口发送代码实现。

完整代码请查看配套程序,另外还需添加Use MicroLIB以便支持printf。具体设置参看本节后文的小贴士部分。

我们来总结下串口发送的流程:
1.初始化硬件,时钟;
2.USART 的GPIO初始化,USART参数初始化;
3.重定向printf
4.打印输出

10.4.3实验现象

将程序编译好下载到板子中,打开串口助手,按下图设置相应参数,按下板子的复位按键,在接收区可以看到如下信息。

Xw6NdI.md.png

10.5串口接收数据(中断方式)

10.5.1串口接收实现-标准库

中断方式相对于与普通方式,还需要开启中断并且初始化 NVIC以及中断服务函数。

 开启中断

在接收到数据的时候( RXNE 读数据寄存器非空),我们要产生中断,那么我们开启中断的方法是:

USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); /* 使能串口1接收中断 */

在发送数据结束的时候( TC, 发送完成) 要产生中断,那么方法是:

USART_ITConfig(USART1, USART_IT_TC, ENABLE);

 初始化NVIC

NVIC_InitTypeDef NVIC_InitStructure; 
/* Configure the NVIC Preemption Priority Bits */  
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);

/* Enable the USARTy Interrupt */
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;    
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

 中断服务函数

/**
  * @brief  This function handles USART1 Handler.
  * @param  None
  * @retval None
  */
void USART1_IRQHandler(void)
{
    uint8_t ch;

    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
        //ch = USART1->DR;
        ch = USART_ReceiveData(USART1);
        printf( "%c", ch );    //将接受到的数据直接返回打印
    } 
}

在中断服务程序中,接收到数据后立即输出。

主函数代码如下:

/**
  * @brief     主函数
  * @param     None
  * @retval    int
  */
int main(void)
{
    char str[20];

    /* USART1 config 115200 8-N-1 */
    USART_Config();

    printf("串口普通方式实验 \r\n");

    /* sprintf函数把格式化的数据写入某个字符串  */  
    sprintf(str,"20%02d-%02d-%02d",22,05,9); 

    /* 调用格式化输出函数打印输出数据 */
    printf("%s\n",str);

    for(;;)
    {

    }
}

10.5.2串口接收实现-HAL库

10.5.2.1 STM32Cube生成工程

和串口发送的大体一致,主要要配置中断。

NVIC Settings 一栏使能接收中断,默认没打开。

Xw6Uot.md.png

另外,还需配置中断优先级,默认即可。

Xw6dFP.md.png

10.5.2.1串口接收代码讲解

和串口发送数据一样,先看如何实现的,然后再进行代码讲解。

在main.c中添加下列定义:

#define RXBUFFERSIZE  256     //最大接收字节数
char TxBuffer[RXBUFFERSIZE];   //发送缓冲
uint8_t RxBuffer;  //接收中断缓冲
uint8_t Uart1_Rx_Cnt = 0;  //接收缓冲计数

在main()主函数中,调用一次接收中断函数

HAL_UART_Receive_IT(&huart1, (uint8_t *)&RxBuffer, 1);

在main.c下方添加中断回调函数

/**
  * @brief  串口中断回调函数
  * @param huart
  * @retval None
  */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    /* Prevent unused argument(s) compilation warning */
    UNUSED(huart);
    /* NOTE: This function Should not be modified, when the callback is needed,
           the HAL_UART_TxCpltCallback could be implemented in the user file
    */
    if(Uart1_Rx_Cnt >= 255)  //溢出判断
    {
        Uart1_Rx_Cnt = 0;
        memset(TxBuffer,0x00,sizeof(TxBuffer));
        HAL_UART_Transmit(&huart1, (uint8_t *)"数据溢出", 10,0xFFFF); 
    }
    else
    {
        TxBuffer[Uart1_Rx_Cnt++] = RxBuffer;   //接收数据转存

        if((TxBuffer[Uart1_Rx_Cnt-1] == 0x0A)&&(TxBuffer[Uart1_Rx_Cnt-2] == 0x0D)) //判断结束位
        {
            HAL_UART_Transmit(&huart1, (uint8_t *)&TxBuffer, Uart1_Rx_Cnt,0xFFFF); //将收到的信息发送出去
            while(HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_TX);//检测UART发送结束
            Uart1_Rx_Cnt = 0;
            memset(TxBuffer,0x00,sizeof(TxBuffer)); //清空数组
        }
    }
    HAL_UART_Receive_IT(&huart1, (uint8_t *)&RxBuffer, 1);   //开启接收中断
}

好了。接下来看看主函数代码:

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */
  char str[20];
  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
  HAL_UART_Receive_IT(&huart1, (uint8_t *)&RxBuffer, 1);

  printf("串口中断接收回显实验 \r\n");  

  /* sprintf函数把格式化的数据写入某个字符串  */  
  sprintf(str,"20%02d-%02d-%02d",22,05,9); 

  /* 调用格式化输出函数打印输出数据 */
  printf("%s\n",str);

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

总结下串口接收的编程流程:
1.硬件初始化,时钟初始化;
2.串口GPIO初始化,串口参数配置;
3.在main()函数中使能中断接收;
4.编写HAL_UART_RxCpltCallback中断回调函数,处理接收的数据,

【注】中断接收函数只能触发一次接收中断,所以我们需要在中断回调函数中再次调用中断接收函数。这里可以对比下标准库的流程。

10.5.3实验现象

将程序编译好下载到板子中,打开串口助手,按下图设置相应参数,按下板子的复位按键,在接收区可以看到如下信息。

Xw6wJf.md.png

小贴士:printf 函数重定向

要想 printf() 函数工作的话,我们需要把 printf() 重新定向到串口中。重定向是指用户可以自己重写 C 的库函数,当连接器检查到用户编写了与 C 库函数相同名字的函数时,优先采用用户编写的函数,这样用户就可以实现对库的修改了。

为了实现重定向 printf() 函数,我们需要重写 fputc() 这个 C 标准库函数,因为 printf()在 C 标准库函数中实质是一个宏,最终是调用了 fputc() 这个函数。

重定向的这部分工作,由stm32f1_bsp_usar1.c 文件中的 fputc(int ch, FILE * f) 这个函数来完成。重定向时,我们把 fputc( ) 的形参 ch,作为串口将要发送的数据,也就是说,当使用 printf( ) 时,它先调用这个 fputc( ) 函数,然后使用 ST 库的串口发送函数 USART_SendData(),把数据转移到发送数据寄存器 TDR,触发我们的串口向 PC 发送一个相应的数据。调 用 完 USART_SendData( ) 后 , 要 使 用 while( USART_GetFlagStatus(USART1,USART_FLAG_TC)!= SET) 语句不停地检查串口发送是否完成的标志位TC,一直检测到标志为“完成”,才进入下一步的操作,避免出错。在这段 while 循环检测的延时中,串口外设已经由发送控制器以及根 据我们的配置把数据从移位寄存器一位一位地通过串口线 Tx 发送出去了。

【注意】printf函数在“stdio.h”头文件里,使用该函数必须引用“stdio.h”库, 还
要在编译器中设置一个选项 Use MicroLIB (使用微库)。设置方式如下:

xc4oJf.md.png

单击Project,选择option选项,再选择Target 勾选Use MicroLIB 即可。

Xw60W8.png


欢迎访问我的网站

BruceOu的哔哩哔哩
BruceOu的主页
BruceOu的博客
BruceOu的CSDN博客
BruceOu的简书
BruceOu的知乎


资源获取方式

1.关注公众号[嵌入式实验楼]
2.在公众号回复关键词[Cortex-M]获取资料提取码

Related posts

Leave a Comment