完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
本帖最后由 正点原子运营官 于 2020-4-17 16:10 编辑
1)实验平台:ALIENTEK NANO STM32F411 V1开发板 2)摘自《正点原子STM32F4 开发指南(HAL 库版》关注官方微信号公众号,获取更多资料:正点原子 第九章 串口通信实验 前面几章介绍了 STM32F4 的 IO 口操作。这一章我们将学习 STM32F4 的串口,教大家如何使用 STM32F4 的串口来发送和接收数据。本章将实现如下功能:STM32F4 通过串口和上位机的对话,STM32F4 在收到上位机发过来的字符串后,原原本本的返回给上位机。本章分为如下几个小节: 9.1 STM32 串口简介 9.2 硬件设计 9.3 软件设计 9.4 下载验证 9.5 STM32CubeMX 配置串口 9.1 STM32F4 串口简介 串口作为 MCU 的重要外部接口,同时也是软件开发重要的调试手段,其重要性不言而喻。 现在基本上所有的 MCU 都会带有串口,STM32 自然也不例外。 STM32F4 的串口资源相当丰富的,功能也相当强劲。ALIENTEK NANO STM32F4 开发板所使用的 STM32F411RCT6 最多可提供 3 路串口,有分数波特率发生器、支持同步单线通信和半双工单线通讯、支持 LIN、支持调制解调器操作、智能卡协议和 IrDA SIR ENDEC 规范、具有 DMA 等。 接下来我们先从寄存器层面,告诉你如何设置串口,以达到我们最基本的通信功能。本章,我们将实现利用串口 1 不停的打印信息到电脑上,同时接收从串口发过来的数据,把发送过来的数据直接送回给电脑。NANO STM32F4 开发板板载了一个 USB 虚拟串口,我们本章介绍通过 USB 串口和电脑通信。 串口最基本的设置,就是波特率的设置。STM32 的串口使用起来还是蛮简单的,只要你开启了串口时钟,并设置相应 IO 口的模式,然后配置一下波特率,数据位长度,奇偶校验位等 信息,就可以使用了。下面,我们就简单介绍下这几个与串口基本配置直接相关的寄存器。 1,串口时钟使能。串口作为 STM32F4 的一个外设,其时钟由外设时钟使能寄存器控制,这里我们使用的串口 1 是在 APB2ENR 寄存器的第 4 位。APB2ENR 寄存器在之前已经介绍过了,这里不再介绍。只是说明一点,就是除了串口 1 和串口 6 的时钟使能在 APB2ENR 寄存器, 其他串口的时钟使能位都在 APB1ENR 寄存器。 2, 串 口 波 特 率 设 置 。 STM32F4 的 每 个 串 口 都 有 一 个 自 己 独 立 的 波 特 率 寄 存 器USART_BRR,通过设置该寄存器就可以达到配置不同波特率的目的。其各位描述如图 9.1.1 所 示: 图 9.1.1 寄存器 USART_BRR 各位描述 相对于 STM32F1 来说,STM32F4 多了一个接收器过采样设置位:OVER8 位,该位在USART_CR1 寄存器里面设置,当 OVER8=0 的时候,采用 16 倍过采样,可以增加接收器对时 钟的容差。当 OVER8=1 的时候,可以获得更高的速度。简单说,就是 OVER8=0 时精度高, 容错性好,OVER8=1 的时候,容错差,但是速度快。这里我们一般设置 OVER8=0,以得到更 好的容错性,一下皆以 OVER8=0 进行介绍。关于 OVER8 的详细介绍,请看《STM32F411xC/E 参考手册》第 19.6.3 节。前面提到 STM32 的分数波特率概念,其实就是在这个寄存器(USART_BRR)里面体现的。 USART_BRR 的最低 4 位(位[3:0])用来存放小数部分 DIV_Fraction,紧接着的 12 位(位[15: 4])用来存放整数部分 DIV_Mantissa,最高 16 位未使用。 这里,我们简单介绍一下波特率的计算,STM32 的串口波特率计算公式如下: 上式中,是给串口的时钟(PCLK1 用于 USART2~5,PCLK2 用于 USART1 和 USART6);USARTDIV 是一个无符号定点数。我们只要得到 USARTDIV 的值,就可以得到串口波特率寄存器 USART1->BRR 的值,反过来,我们得到 USART1->BRR 的值,也可以推导出USARTDIV 的值。但我们更关心的是如何从 USARTDIV 的值得到 USART_BRR 的值,因为一 般我们知道的是波特率,和 PCLKx 的时钟,要求的就是 USART_BRR 的值。 下面我们来介绍如何通过 USARTDIV 得到串口 USART_BRR 寄存器的值。假设我们的串口 1 要设置为 115200 的波特率,而 PCLK2 的时钟为 96M。这样,我们根据上面的公式有: USARTDIV=96000000/(115200*16)= 52.0833那么得到: DIV_Fraction=16*0. 0833=1=0X01; DIV_Mantissa=52=0X34; 这样,我们就得到了 USART1->BRR 的值为 0X0341。只要设置串口 1 的 BRR 寄存器值为 0X0341 就可以得到 115200 的波特率。 当然,并不是任何条件下都可以随便设置串口波特率的,在某些波特率和 PCLK2 频率下, 还是会存在误差的,具体可以参考《STM32F411xC/E 参考手册》的第 520 页的表 73。 3,串口控制。STM32 的每个串口都有 3 个控制寄存器 USART_CR1~3,串口的很多配置都是通过这 3 个寄存器来设置的。这里我们只要用到 USART_CR1 就可以实现我们的功能了, 该寄存器的各位描述如图 9.1.2 所示: 图 9.1.2 USART_CR 寄存器各位描述 该寄存器的高 16 位没有用到,低 16 位用于串口的功能设置。OVER8 为过采样模式设置位,我们一般设置位 0,即 16 倍过采样已获得更好的容错性;UE 为串口使能位,通过该位置 1,以使能串口;M 为字长选择位,当该位为 0 的时候设置串口为 8 个字长外加 n 个停止位,停止位的个数(n)是根据 USART_CR2 的[13:12]位设置来决定的,默认为 0;PCE 为校验使能位,设置为 0,则禁止校验,否则使能校验;PS 为校验位选择位,设置为 0 则为偶校验,否则为奇校验;TXIE 为发送缓冲区空中断使能位,设置该位为 1,当 USART_SR 中的 TXE 位为 1 时,将产生串口中断;TCIE 为发送完成中断使能位,设置该位为 1,当 USART_SR 中的 TC 位为 1 时,将产生串口中断;RXNEIE 为接收缓冲区非空中断使能,设置该位为 1,当 USART_SR 中的 ORE 或者 RXNE 位为 1 时,将产生串口中断;TE 为发送使能位,设置为 1,将开启串口的发送功能;RE 为接收使能位,用法同 TE。 其他位的设置,这里就不一一列出来了,大家可以参考《STM32F411xC/E 参考手册》第551 页有详细介绍,在这里我们就不列出来了。 4,数据发送与接收。STM32F4 的发送与接收是通过数据寄存器 USART_DR 来实现的,这是一个双寄存器,包含了 TDR 和 RDR。当向 DR 寄存器写数据的时候,实际是写入 TDR,串口就会自动发送数据;当收到数据,读 DR 寄存器的时候,实际读取的是 RDR。TDR 和 RDR对外是不可见的,所以我们操作的就只有 DR 寄存器,该寄存器的各位描述如图 9.1.3 所示: 可以看出,虽然是一个个 32 位寄存器,但是只用了低 9 位(DR[8:0]),其他都是保留。DR[8:0]为串口数据,包含了发送或接收的数据。由于它是由两个寄存器组成的,一个给 发送用(TDR),一个给接收用(RDR),该寄存器兼具读和写的功能。TDR 寄存器提供了内部总线和输出移位寄存器之间的并行接口。RDR 寄存器提供了输入移位寄存器和内部总线之间的并行接口。 当使能校验位(USART_CR1 中 PCE 位被置位)进行发送时,写到 MSB 的值(根据数据的长度不同,MSB 是第 7 位或者第 8 位)会被后来的校验位取代。 当使能校验位进行接收时,读到的 MSB 位是接收到的校验位。 5,串口状态。串口的状态可以通过状态寄存器 USART_SR 读取。USART_SR 的各位描述 如图 9.1.4 所示: 图 9.1.4 USART_SR 寄存器各位描述 这里我们关注一下两个位,第 5、6 位 RXNE 和 TC。 RXNE(读数据寄存器非空),当该位被置 1 的时候,就是提示已经有数据被接收到了,并且可以读出来了。这时候我们要做的就是尽快去读取 USART_DR,通过读 USART_DR 可以将该位清零,也可以向该位写 0,直接清除。 TC(发送完成),当该位被置位的时候,表示 USART_DR 内的数据已经被发送完成了。如果设置了这个位的中断,则会产生中断。该位也有两种清零方式:1)读 USART_SR,写 USART_DR。2)直接向该位写 0。 通过以上一些寄存器的操作外加一下 IO 口的配置,我们就可以达到串口最基本的配置了,关于串口更详细的介绍,请参考《STM32F411xC/E 参考手册》第 505 页至 557 页,通用同步异 步收发器一章。对于怎么直接使用寄存器配置串口收发,请参考我们寄存器版本教程和源码。接下来我们 将着重讲解使用 HAL 库实现串口配置和使用的方法。HAL 库中,串口相关的函数和定义主要在文件 stm32f1xx_hal_uart.c 和 stm32f1xx_hal_uart.h 中。接下来我们看看 HAL 库提供的串口相关操作函数。 1)串口参数初始化(波特率/停止位等),并使能串口。 串口作为 STM32 的一个外设,HAL 库为其配置了串口初始化函数。接下来我们看看串口初始化函数 HAL_UART_Init 相关知识,定义如下: HAL_StatusTypeDef HAL_UART_Init(UART_HandlerTypeDef *huart); 该函数只有一个入口参数 huart,为 UART_HandleTypeDef 结构体指针类型,我们俗称其为 串口句柄,它的使用会贯穿整个串口程序。一般情况下,我们会定义一个 UART_HandleTypeDef 结构体类型全局变量,然后初始化各个成员变量。接下来我们看看结构体 UART_HandleTypeDef 的定义: typedef struct { USART_TypeDef *Instance; UART_InitTypeDef Init; uint8_t *pTxBuffPtr; uint16_t TxXferSize; __IO uint16_t TxXferCount; uint8_t *pRxBuffPtr; uint16_t RxXferSize; __IO uint16_t RxXferCount; DMA_HandleTypeDef *hdmatx; DMA_HandleTypeDef *hdmarx; HAL_LockTypeDef Lock; __IO HAL_UART_StateTypeDef gState; __IO HAL_UART_StateTypeDef RxState; __IO uint32_t ErrorCode; }UART_HandleTypeDef; 该结构体成员变量非常多,一般情况下载调用函数 HAL_UART_Init 对串口进行初始化的 时候,我们只需要先设置 Instance 和 Init 两个成员变量的值。接下来我们依次解释一下各个成 员变量的含义。 Instance 是 USART_TypeDef 结构体指针变量类型变量,它是执行寄存器基地址,实际上这 个基地址 HAL 库已经定义好了,如果是串口 1,取值为 USART1 即可。 Init 是 UART_InitTypeDef 结构体类型变量,它是用来设置串口的各个参数,包括波特率, 停止位等,它的使用方法非常简单。UART_InitTypeDef 结构体定义如下: typedef struct { uint32_t BaudRate; //波特率 uint32_t WordLength; //字长 uint32_t StopBits; //停止位 uint32_t Parity; //奇偶校验 uint32_t Mode; //收/发模式设置 uint32_t HwFlowCtl; //硬件流设置 uint32_t OverSampling; //过采样设置 }UART_InitTypeDef; 该结构第一个参数 BaudRate 为串口波特率,波特率可以说是串口最重要的参数了,它用来 确定串口通信的速率。第二个参数 WordLength 为字长,可以设置为 8 位字长或者 9 位字长, 这里我们设置为 8 位字长数据格式 UART_WORDLENGTH_8B。第三个参数 StopBits 为停止位 设 置 , 可 以 设 置 为 1 个 停 止 位 或 者 2 个 停 止 位 , 这 里 我 们 设 置 为 1 位 停 止 位 UART_STOPBOTS_1。第四个参数 Parity 设定是否需要奇偶校验,我们设定为无奇偶检验位。 第五个参数 Mode 为串口模块,可以设置为只收模式,只发模式,或者收发模式。这里我们设 置为全双工收发模式。第六个参数 HwFlowCtl 为是否支持硬件流控制,我们设置为无硬件流控 制。第七个参数 OverSampling 用来设置过采样为 16 倍还是 8 倍。 pTxBuddPtr,TxXferSize 和 TxXferCout 三个变量分别用来设置串口发送的数据缓存指针, 发送的数据量和还剩余的要发送的数据量。而接下来的三个变量 pRxBuffPtr,RxXferSize 和 RxXferCount 则是用来设置接收的数据缓存指针,接收的最大数据量以及还剩余的要接收的数 据量。这六个变量是 HAL 库处理中间变量,详细使用方法在我们讲解中断服务函数的时候给 大家讲解。 hdmatx 和 hdmarx 是串口 DMA 相关的变量,指向 DMA 句柄,这里我们先不讲解。 其他的三个变量就是一些 HAL 库处理过程状态标志位和串口通信的错误码。 函数 HAL_UART_Init 使用的一般格式为: UART_HandleTypeDef UART1_Handler; //UART 句柄 UART1_Handler.Instance=USART1; //USART1 UART1_Handler.Init.BaudRate=bound; //波特率 UART1_Handler.Init.WordLength=UART_WORDLENGTH_8B; //字长为 8 位数据格式 UART1_Handler.Init.StopBits=UART_STOPBITS_1; //一个停止位 UART1_Handler.Init.Parity=UART_PARITY_NONE; //无奇偶校验位 UART1_Handler.Init.HwFlowCtl=UART_HWCONTROL_NONE; //无硬件流控 UART1_Handler.Init.Mode=UART_MODE_TX_RX; //收发模式 HAL_UART_Init(&UART1_Handler); //HAL_UART_Init()会使能 UART1 这里我们需要说明的是,函数 HAL_UART_Init 内部会调用串口使能函数使能相应串口, 所以调用了该函数之后我我们就不需要重复使能串口了。当然,HAL 库也提供了具体的串口使 能和关闭方法,具体使用方法如下: __HAL_UART_ENABLE(handler);//使能句柄 handler 指定的串口 __HAL_UART_DISABLE(handler);//关闭句柄 hander 指定的串口 这里还需要提醒大家,串口作为一个重要外设,在调用的初始化函数 HAL_UART_Init 内 部,会先调用 MSP 初始化回调函数进行 MCU 相关的初始化,函数为: void HAL_UART_MspInit(UART_HandleTypeDef*huart); 我们在程序中,只需要重写该函数即可。一般情况下,该函数内部用来编写 IO 口初始化, 时钟使能以及 NVIC 配置。 2)使能串口和 GPIO 口时钟 我们要使用串口,所以我们必须使能串口时钟和使用到的 GPIO 口时钟。例如我们要使用 串口 1,我们必须使能串口 1 时钟和 GPIOA 时钟(串口 1 使用的是 PA9 和 PA10)。具体方法 如下: __HAL_RCC_USART1_CLK_ENABLE();//使能 USART1 时钟 __HAL_RCC_GPIOA_CLK_ENABLE();//使能 GPIOA 时钟 使能相关方法我们在时钟系统相关章节有讲解,操作方法也非常简单,这里我们就不重复 讲解。 3)GPIO 口初始化设置(速度,上下拉等)以及复用映射配置 我们在跑马灯实验中讲解过,在 HAL 库中 IO 口初始化参数设置和复用映射配置是在函数 HAL_GPIO_Init 中一次性完成的。这里大家只需要注意,我们要复用 PA9 和 PA10 为串口 发送接收相关引脚,我们需要配置 IO 口为复用,同时复用映射到串口 1。配置源码如下: GPIO_InitTypeDef GPIO_Initure; GPIO_Initure.Pin=GPIO_PIN_9; //PA9 GPIO_Initure.Mode=GPIO_MODE_AF_PP; //复用推挽输出 GPIO_Initure.Pull=GPIO_PULLUP; //上拉 GPIO_Initure.Speed=GPIO_SPEED_FAST; //高速 GPIO_Initure.Alternate=GPIO_AF7_USART1; //复用为 USART1 HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA9 GPIO_Initure.Pin=GPIO_PIN_10; //PA10 HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA10 4)开启串口相关中断,配置串口中断优先级 HAL 库中定义了一个使能串口中断的标识符__HAL_UART_ENABLE_IT,大家可以把它当 一个函数来使用,具体定义请参考 HAL 库文件 stm32f1xx_hal_uart.h 中该标识符定义。例如我 们要使能接收完成中断,方法如下: __HAL_UART_ENABLE_IT(huart,UART_IT_RXNE); //开启接收完成中断 第一个参数为我们步骤 1 讲解的串口句柄,类型为 UART_HandleTypeDef 结构体类型。第 二个参数为我们要开启的中断类型值,可选值在头文件 stm32f1xx_hal_uart.h 中有宏定义。 有开启中断就有关闭中断,操作方法为: __HAL_UART_DISABLE_IT(huart,UART_IT_RXNE); //关闭接收完成中断 对于中断优先级配置,方法就非常简单,详细知识请参考 4.5 小节相关知识。参考方法为: HAL_NVIC_EnableIRQ(USART1_IRQn); //使能 USART1 中断通道 HAL_NVIC_SetPriorty(USART1_IRQn,3,3); //抢占优先级 3,子优先级 3 5)编写中断服务函数 串口 1 中断服务函数为: void USART1_IRQHandler(void); 当发生中断的时候,程序就会执行中断服务函数。然后我们在中断服务函数中编写们相应 的逻辑代码即可。HAL 库实际上对中断处理过程进行了完整的封装,具体内容我们在 9.3 小节 通过结合实验源码给大家详细讲解。 6)串口数据接收和发送 STM32F4 的发送与接收是通过数据寄存器 USART_DR 来实现的,这是一个双寄存器,包 含了 TDR 和 RDR。当向该寄存器写数据的时候,串口就会自动发送,当收到数据的时候,也 是存在该寄存器内。HAL 库操作 USART_DR 寄存器发送数据的函数是: HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef*huart uint8_t*pData,uint16_t Size,uint32_t Timeout); 通过该函数向串口寄存器 USART_DR 写入一个数据。 HAL 库操作 USART_DR 寄存器读取串口接收到的数据的函数是: HAL_StatusTypeDef HAL_UART_Receive(UART_Handler TypeDef*huart, uint8_t*pData,uint16_t Size,uint32_t Timeout); 通过该函数可以读取串口接收到的数据。 9.2 硬件设计 本实验需要用到的硬件资源有: 1) 指示灯 DS0 2) 串口 1 串口 1 之前还没有介绍过,本实验用到的串口 1 与 USB 串口并没有在 PCB 上连接在一起, 需要通过跳线帽来连接一下。这里我们把 P5 的 RXD 和 TXD 用跳线帽与 PA9 和 PA10 连接起 来。如图 9.2.1 所示: 图 9.2.1 硬件连接图示意图 连接上这里之后,我们在硬件上就设置完成了,可以开始软件设计了。 9.3 软件设计 本小节,我们首先会讲解使用 HAL 库配置串口的一般步骤。然后我们会具体讲解我们串 口实验程序实现。ALIENTEK 编写的串口相关的源码在 SYSTEM 分组之下的 usart.c 和 usart.h 中。 9.1 小节我们讲解了 HAL 库中串口操作的一般步骤以及操作函数。在使用 HAL 库配置串 口的时候,HAL 库为我们封装了串口配置步骤。接下来我们以串口接收中断为例讲解 HAL 库 串口程序执行流程。 和 其 他 外 设 一 样 , HAL 库 为 串 口 的 使 用 开 放 了 MSP 函 数 。 在 串 口 初 始 化 函 数 HAL_UART_Init 内部,会调用串口 MSP 函数 HAL_UART_MspInit 来设置与 MCU 相关的配置。 根据前面的讲解,函数 HAL_UART_Init 主要用来初始化与串口相关的函数(这些参数与 MCU 无关),包括波特率,停止位等。而串口 MSP 函数 HAL_UART_MspInit 用来设置 GPIO 初始 化,NVIC 配置等于 MCU 相关的配置。 这里我们定义了一个函数 uart_init 用来调用 HAL_UART_Init 初始化串口参数配置,具体 函数如下: UART_HandleTypeDef UART1_Handler; //UART 句柄 //初始化 IO 串口 1 //bound:波特率 void uart_init(u32 bound) { //UART 初始化设置 UART1_Handler.Instance=USART1; //USART1 UART1_Handler.Init.BaudRate=bound; //波特率 UART1_Handler.Init.WordLength=UART_WORDLENGTH_8B; //字长为 8 位数据格式 UART1_Handler.Init.StopBits=UART_STOPBITS_1; //一个停止位 UART1_Handler.Init.Parity=UART_PARITY_NONE; //无奇偶校验位 UART1_Handler.Init.HwFlowCtl=UART_HWCONTROL_NONE; //无硬件流控 UART1_Handler.Init.Mode=UART_MODE_TX_RX; //收发模式 HAL_UART_Init(&UART1_Handler); //HAL_UART_Init()会使能 UART1 HAL_UART_Receive_IT(&UART1_Handler, (u8 *)aRxBuffer, RXBUFFERSIZE); //该函数会开启接收中断:标志位 UART_IT_RXNE,并且设置接收缓冲以及接收缓冲 接收最大数据量 } 该函数实现的是我们 9.1 小节讲解的步骤 1 的内容。同时这里大家需要注意,最后一行代 码调用函数 HAL_UART_Receive_IT,作用是开启接收中断,同时设置接收的缓存区以及接收 的数据量,对于这个缓冲我们在湖面会给大家讲解它的作用。 串口 MSP 函数 HAL_UART_MspInit 函数我们自定义了其内容,代码如下: void HAL_UART_MspInit(UART_HandleTypeDef *huart) { //GPIO 端口设置 GPIO_InitTypeDef GPIO_Initure; if(huart->Instance==USART1)//如果是串口 1,进行串口 1 MSP 初始化 { __HAL_RCC_GPIOA_CLK_ENABLE(); //使能 GPIOA 时钟 __HAL_RCC_USART1_CLK_ENABLE(); //使能 USART1 时钟 __HAL_RCC_AFIO_CLK_ENABLE(); GPIO_Initure.Pin=GPIO_PIN_9; //PA9 GPIO_Initure.Mode=GPIO_MODE_AF_PP; //复用推挽输出 GPIO_Initure.Pull=GPIO_PULLUP; //上拉 GPIO_Initure.Speed=GPIO_SPEED_FAST; //高速 GPIO_Initure.Alternate=GPIO_AF7_USART1; //复用为 USART1 HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA9 GPIO_Initure.Pin=GPIO_PIN_10; //PA10 HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA10 #if EN_USART1_RX HAL_NVIC_EnableIRQ(USART1_IRQn); //使能 USART1 中断通道 HAL_NVIC_SetPriority(USART1_IRQn,3,3); //抢占优先级 3,子优先级 3 #endif } } 该函数代码实现的是我们 9.1 小节讲解的步骤 2 到 4 的内容。这里大家需要注意,在该段 代码中,通过判断宏定义标识符 EN_USART1_RX 的值来确定是否开启串口中断通道和设置串 口 1 中断优先级。标识符 EN_USART1_RX 在头文件 usart.h 中有定义,默认情况下我们设置为 1。 #define EN_USART1_RX 1 //使能(1)/禁止(0)串口 1 接收 通过上面两个函数,我们就配置了串口相关设置。接下来就是编写中断服务函数 USART1_IRQHandler。而 HAL 库中,对中断服务函数的编写有非常严格的讲究。 首先 HAL 库定义了一个串口中断处理通用函数 HAL_UART_IRQHandler,该函数声明如 下: void HAL_UART_IRQHandler(UART_HandleTypeDef*huart); 该函数只有一个入口参数就是 UART_HandleTypeDef 结构体指针类型的串口句柄 huart,使用我们在调用 HAL_UART_Init 函数时需要设置的同一个变量即可。该函数一般在中断服务函 数中调用,作为串口中断处理的通用入口。一般调用方法为: void USART1_IRQHandler(void) { HAL_UART_IRQHandler(&UART1_Handler);//调用 HAL 库中断处理公用函数 ....//中断处理完成后的结束工作 } 也就是说,真正的串口中断处理逻辑我们会最终在函数HAL_UART_IRQHandler内部执行。 而该函数是 HAL 库已经定义好,而且用户一般不能随意修改。这个时候大家会问,那么我们 的 中 断 控 制 逻 辑 编 写 在 哪 里 呢 ? 为 了 把 这 个 问 题 讲 清 楚 , 我 们 要 来 看 看 函 数 HAL_UART_IRQHandler 内部具体实现过程。因为本章实验,我们主要实现的是串口中断接收, 也就是每次接收到一个字符后进入中断服务函数来处理。所以我们就以中断接收为例给大家讲 解。这里为了篇幅考虑,我们仅仅列出串口中断执行流程中与接收相关的源码。 函数 HAL_UART_IRQHandler 关于串口接收相关源码如下: void HAL_UART_IRQHandler(UART_HandleTypeDef *huart) { uint32_t isrflags = READ_REG(huart->Instance->SR); uint32_t cr1its = READ_REG(huart->Instance->CR1); uint32_t cr3its = READ_REG(huart->Instance->CR3); uint32_t errorflags = 0x00U; uint32_t dmarequest = 0x00U; errorflags = (isrflags & (uint32_t)(USART_SR_PE | USA RT_SR_FE | USART_SR_ORE | USART_SR_NE)); if(errorflags == RESET) { if(((isrflags & USART_SR_RXNE) != RESET) && ((cr1its & USART_CR1_RXNEIE) != RESET)) { UART_Receive_IT(huart); return; } } ...//此处省略部分代码 } 从代码逻辑可以看出,在函数 HAL_UART_IRQHandler 内部通过判断中断类型是否为接收 完成中断,确定是否调用 HAL 另外一个函数 UART_Receive_IT()。函数 UART_Receive_IT()的 作用是把每次中断接收到的字符保存在串口句柄的缓存指针 pRxBuffPtr 中,同时每次接收一个 字符,其计数器 RxXferCount 减 1,直到接收完成 RxXferSize 个字符之后 RxXferCount 设置为 0,同时调用接收完成回调函数 HAL_UART_RxCpltCallback 进行处理。为了篇幅考虑,这里我 们仅列出 UART_Receive_IT()函数调用回调函数 HAL_UART_RxCpltCallback 的处理逻辑,代 码如下: static HAL_StatusTypeDef UART_Receive_IT(UART_HandleTypeDef*huart) { ...//此处省略部分代码 if(--huart->RxXferCount == 0U) { ...//此处省略部分代码 HAL_UART_RxCpltCallback(huart); } ...//此处省略部分代码 } 最后我们列出串口接收中断的一般流程,如图 9.3.1 所示: 图 9.3.1 串口接收中断执行流程图 这里,我们再把串口接收中断的一般流程进行概括:当接收到一个字符之后,在函数 UART_Receive_IT中会把数据保存在串口句柄的成员变量pRxBuffPtr缓存中,同时RxXferCount 计数器减 1。如果我们设置 RxXferSize=10,那么当接收到 10 个字符之后,RxXferCount 会由 10 减到 0(RxXferCount 初始值等于 RxXferSize),这个时候再调用接收完成回调函数 HAL_UART_RxCpltCallback 进行处理。接下来我们看看我们的配置。 首先,我们回到用户函数 uart_init 定义可以看到,在 uart_init 函数中调用完 HAL_UART_Init 后我们还调用 HAL_UART_Receive_IT 开启接收中断,并且初始化串口句柄的缓存相关参数。 代码如下: HAL_UART_Receive_IT(&UART1_Handler, (u8 *)aRxBuffer, RXBUFFERSIZE); 而 aRXBuffer 是我们定义的一个全局数据变量,RXBUFFERSZIE 是我们定义的一个标识 符:#define RXBUFFERSIZE 1 u8 aRxBuffer[RXBUFFERSIZE]; 所以调用 HAL_UART_Receive_IT 函数后,除了开启接收中断外还确定了每次接收 RXBUFFERSIZE 个字符后标示接收结束从而进入回调函数 HAL_UART_RxCpltCallback进行相 应处理。最后我们看看 HAL_UART_RxCpltCallback 函数定义: void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart->Instance==USART1)//如果是串口 1 { if((USART_RX_STA&0x8000)==0)//接收未完成 { if(USART_RX_STA&0x4000)//接收到了 0x0d { if(aRxBuffer[0]!=0x0a)USART_RX_STA=0;//接收错误,重新开始 else USART_RX_STA|=0x8000; //接收完成了 } else //还没收到 0X0D { if(aRxBuffer[0]==0x0d)USART_RX_STA|=0x4000; else { USART_RX_BUF[USART_RX_STA&0X3FFF]=aRxBuffer[0] ; USART_RX_STA++; if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0; //接收数据错误,重新开始接收 } } } } } 因为我们设置了串口句柄成员变量 RxXferSize 为 1,也就是每当串口 1 发生了接收完成中 断后(接收到一个字符),就会跳到该函数执行。当串口接收到一个字符后,它会保存在缓存 aRxBuffer 中,由于我们设置了缓存大小为 1,而且 RxXferSize=1,所以每次接受一个字符,会 直接保存到 RxXferSize[0]中,我们直接通过读取 RxXferSize[0]的值就是本次接收到的字符。这 里我们设计了一个小小的接收协议:通过这个函数,配合一个数组 USART_RX_BUF[],一个 接收状态寄存器 USART_RX_STA(此寄存器其实就是一个全局变量,由作者自行添加。由于 它 起 到 类 似 寄 存 器 的 功 能 , 这 里 暂 且 称 之 寄 存 器 ) 实 现 对 串 口 数 据 的 接 收 管 理 。 USART_RX_BUF 的大小由 USART_REC_LEN 定义,也就是一次接收的数据最大不能超过 USART_REC_LEN 个字节。USART_RX_STA 是一个接收状态寄存器其各的定义如表 9.3.2 所 示: 表 9.3.2 接收状态寄存器位定义表 设计思路如下: 当接收到从电脑发过来的数据,把接收到的数据保存在 USART_RX_BUF 中,同时在接收状态 寄存器(USART_RX_STA)中计数接收到的有效数据个数,当收到回车(回车的表示由 2 个 字节组成:0X0D 和 0X0A)的第一个字节 0X0D 时,计数器将不再增加,等待 0X0A 的到来, 而如果 0X0A 没有来到,则认为这次接收失败,重新开始下一次接收。如果顺利接收到 0X0A, 则标记 USART_RX_STA 的第 15 位,这样完成一次接收,并等待该位被其他程序清除,从而 开始下一次的接收,而如果迟迟没有收到 0X0D,那么在接收数据超过 USART_REC_LEN 的时 候,则会丢弃前面的数据,重新接收。 在函数 USART1_IRQHandler 的结尾还有几行行代码,其中部分代码是超时退出逻辑,关 键逻辑代码如下: while (HAL_UART_GetState(&UART1_Handler) != HAL_UART_STATE_READY); while(HAL_UART_Receive_IT(&UART1_Handler, (u8 *)aRxBuffer, RXBUFFERSIZE) != HAL_OK); 这两行代码作用非常简单,第一行代码是判断串口是否就绪,如果没有就绪就等待就绪。 第二行代码是继续调用 HAL_UART_Receive_IT 函数来开启中断和重新设置 RxXferSize 和 RxXferCount 的初始值为 1,也就是开启新的接收中断。 学到这里大家会发现,HAL 库定义的串口中断逻辑确实非常复杂,并且因为处理过程繁 琐所以效率不高。这里我们需要说明的是,在中断服务函数中,大家也可以不用调用 HAL_UART_IRQHandler 函数,而是直接编写自己的中断服务函数。串口实验我们之之所以 遵循 HAL 库写法,是为了让大家对 HAL 库有一个更清晰的理解。 如果我们不用中断处理回调函数,那么就不用初始化串口句柄的中断接收缓存,所以我们 HAL_UART_Receive_IT 函数就不用出现在初始化函数 uart_init 中,而是直接在要开启中断的 地方调用__HAL_UART_ENABLE_IT 单独开启中断即可。如果不用中断回调函数处理,中断服 务函数内容为: //串口 1 中断服务程序 void USART1_IRQHandler(void) { u8 Res; #if SYSTEM_SUPPORT_OS //使用 OS OSIntEnter(); #endif if((__HAL_UART_GET_FLAG(&UART1_Handler,UART_FLAG_RXNE)!=RESET)) //接收中断(接收到的数据必须是 0x0d 0x0a 结尾) { Res=USART1->DR; if((USART_RX_STA&0x8000)==0)//接收未完成 { if(USART_RX_STA&0x4000)//接收到了 0x0d { if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始 else USART_RX_STA|=0x8000; //接收完成了 } else //还没收到 0X0D { if(Res==0x0d)USART_RX_STA|=0x4000; else { USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ; USART_RX_STA++; if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0; //接收数据错误,重新开始接收 } } } } HAL_UART_IRQHandler(&UART1_Handler); #if SYSTEM_SUPPORT_OS //使用 OS OSIntExit(); #endif } 这段代码逻辑跟上面的中断回调函数类似,只不过这里还需要通过 HAL 库串口接收函数 HAL_UART_Receive 来获取接收到的字符进行相应的处理,这里我们就不做过多的讲解。在我 们后面很多实验,为了效率和处理逻辑方便,我们会选择将接收控制逻辑直接编写在中断函数 内部。 HAL 库一共提供了 5 个中断处理回调函数: void HAL_UART_TxCpltCallback(UART_HandleTypeDef*huart);//发送完成回调函数 void HAL_USART_TxHalfCpltCallback(USART_HandleTypeDef *husart);//发送完成过半 void HAL_UART_RxCpltCallback(UART_HandleTypeDef*huart);//接收完成回调函数 Void HAL_UART_RxHalfCpltCallbackk(UART_HandleTypeDef*huart);//接收完成过半 Void HAL_UART_ErrorCallback(UART_HandleTypeDef*huart);//错误处理回调函数 有兴趣的同学可以自行测试每个回调函数的使用方法,这里我们就不做过多讲解。最后我们来 看看主函数: int main(void) { u8 len; u16 times=0; HAL_Init(); //初始化 HAL 库 Stm32_Clock_Init(96,4,2,4); //设置时钟,96Mhz delay_init(96); //初始化延时函数 LED_Init(); //初始化 LED uart_init(115200); //初始化串口 115200 while(1) { if(USART_RX_STA&0x8000) { len=USART_RX_STA&0x3fff;//得到此次接收到的数据长度 printf("rn 您发送的消息为:rn"); HAL_UART_Transmit(&UART1_Handler,(uint8_t*)USART_RX_BUF,len,1000); //发送接收到的数据 while(__HAL_UART_GET_FLAG(&UART1_Handler, UART_FLAG_TC)!=SET);//等待发送结束 printf("rnrn");//插入换行 USART_RX_STA=0; }else { times++; if(times%5000==0) { printf("rnALIENTEK NANO STM32 开发板 串口实验rn"); printf("正点原子@ALIENTEKrnrnrn"); } if(times%200==0)printf("请输入数据,以回车键结束rn"); if(times%30==0)LED0=!LED0;//闪烁 LED,提示系统正在运行. delay_ms(10); } } } 这段代码逻辑比较简单,首先判断全局变量 USART_RX_STA 的最高位是否为 1,如果为 1 的话,那么代表前一次数据接收已经完成,接下来就是把我们自定义接收缓冲的数据发送到 串口。接下来我们重点以下两句。 HAL_UART_Transmit(&UART1_Handler,(uint8*)USART_RX_BUF,len,1000); While(__HAL_UART_GET_FLAG(&UART1_Handler,UART_FLAG_TC)!=SET); 第一句,其实就是调用 HAL 串口发送函数 HAL_UART_Transmit 来发送一个字符到串口。 第二句呢,就是我们发送一个字节之后,要检测这个数据是否已经被发送完成了。 9.4 下载验证 我们把程序下载到到 NANO STM32F4 V1 板子,可以看到板子上的 DS0 开始闪烁,说明程 序已经在跑了。串口调试助手,我们用 XCOM V2.0,该软件在光盘有提供,且无需安装,直接 可以运行,但是需要你的电脑安装有.NET Framework 4.0(WIN7 及以上系统直接自带了)或以上 版本的环境才可以,该软件的详细介绍请看:http://www.openedv.com/posts/list/22994.htm 这个 帖子。 接着我们打开 XCOM V2.0,设置串口为开发板的 USB 虚拟串(ST 虚拟串口,得根据你自 己的电脑选择,我的电脑是 COM56,另外,请注意:波特率是 115200),可以看到如图 9.4.1 所示信息: 图 9.4.1 串口调试助手收到的信息 从图 9.4.1 可以看出,STM32F1 的串口数据发送是没问题的了。但是,因为我们在程序上 面设置了必须输入回车,串口才认可接收到的数据,所以必须在发送数据后再发送一个回车符, 这里 XCOM 提供的发送方法是通过勾选发送新行实现,如图 9.4.1,只要勾选了这个选项,每 次发送数据后,XCOM 都会自动多发一个回车(0X0D+0X0A)。设置好了发送新行,我们再在发 送区输入你想要发送的文字,然后单击发送,可以得到如图 9.4.2 所示结果: 图 9.4.2 发送数据后收到的数据 可以看到,我们发送的消息被发送回来了(图中圈圈内)。大家可以试试,如果不发送回 车(取消发送新行),在输入内容之后,直接按发送是什么结果。 9.5 STM32CubeMX 配置串口 前面章节我们详细讲解了使用 STM32CubeMX 配置 IO 口输入输出,本小节我们将讲解使 用 STM32CubeMX 配置串口方法。同样,大家直接复制光盘的 STM32CubeMX 配置的工程模 板,目录为:“4,程序源码标准例程-HAL 库函数版本实验 0-3 Template 工程模板-使用 STM32CubeMX 配置”。然后使用 STM32CubeMX 打开该工程(点击工程目录的 Template.ioc)。 这里我们同样不再讲解 RCC 相关配置,我们仅仅讲解串口相关配置方法。 这里我们要配置串口 1,所以首先我们要使能串口 1 然后设置相应通信模式。打开 Pinout 选项卡界面,左侧依次进入 Configuration->Peripherals->USART1 配置栏,如下图 9.5.1 所示: 图 9.5.1 进入 Configuration->Peripherals->USART1 配置栏 USART1 配置栏有 2 个选项。第一个选项 Mode 用来设置串口 1 的模式或者关闭串口 1。 第二个选项 Hardware Flow Control(RS232)用来开启/关闭串口 1 的硬件流控制,该选项只有在 Mode 选项值为 Asynchronous(异步通信)模式的前提下才有效。这里我们要开启串口 1 的异步模 式,并且不使用硬件流控制,所以这里我们直接选择 Mode 值为 Asynchronous 即可。配置好的 USART1 界面如下图 9.5.2 所示: 图 9.5.2 USART1 配置 配置好串口 1 为异步通信模式后,那么在硬件上会使用到 PA9 和 PA10 作为串口 1 的发送 接收引脚。在 STM32CubeMX 中,当我们选择好外设的工作模式之后,软件会自动配置 GPIO 口的相关模式和参数。在 Pinout 界面我们看看芯片引脚图会发现,PA9 和 PA10 端口的模式会 自动复用为发送和接收模式,如下图 9.5.3 所示: 图 9.5.3 PA9/PA10 配置 同时,进入 GPIO 配置详细界面会发现,IO 口的模式等参数都做了相应的修改。参考 6.5 小节方法,依次进入 Configuration->GPIO Setting,该选项卡界面便是用来配置和查看串口引脚 PA9 和 PA10 配置参数的。如下图 9.5.4 所示: 图 9.5.4 USART1 引脚详细配置界面 对于外设的功能引脚,在我们使能相应的外设(比如 USART1)之后,STM32CubeMX 会 自动设置 GPIO 相关配置,一般情况下用户不再需要再修改。所以这里,对于 PA9 和 PA10 的 配置我们就保留软件配置即可。 接下来我们需要配置 USART1 外设相关的参数,包括波特率,停止位等。我们直接进入 Parameter Settings 选项卡,如下图 9.5.5 所示: 图 9.5.5 Parameter Setting 选项卡 在 Parameter Settings 选项卡用来配置 USART1 的初始化参数,包括波特率停止位等等。这 里我们将 USART1 配置为:波特率 115200,8 位字节模式,无奇偶校验位,发送/接收均开启。 User Constats 是用来配置用户常量。 NVIC 选项卡用来使能 USART1 中断。这里我们勾上 Enabled 选项。 DMA Setting 是在使用 USART1 DMA 的情况才需要配置,这里我们不配置。 GPIO Setting 便是查看和配置 USART1 参数之后,如果我们使用到串口中断,那么我们还 需要设置中断优先级分组。接下来便是配置 NVIC 相关参数。同样的方法,进入 System Core 选项卡,点击 NVIC 按钮,弹出 NVIC 配置界面 NVIC Configuration,如下图 9.5.6 所示: 图 9.5.6 NVIC Configuration 配置界面 在弹出的 NVIC Configuration 界面,我们首先设置中断优先级分组级别,我们系统初始化 设置为分组 2,那么就是 2 位抢占优先级和 2 位响应优先级。所以这里的参数我们选择“2 bits for pre-emption prioriy”,也就是 2 位抢占优先级。 配置完中断优先级分组之后,接下来我们要配置的是 USART1 的抢占优先级和响应优先级 值,这里我们设置抢占和响应优先级为 3 即可。 进行完上面的操作之后,接下来我们便是生成工程代码。 打开生成的工程可以看到,在 main.c 文件中生成了如下串口初始化关键代码: static void MX_USART1_UART_Init(void) { 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(__FILE__, __LINE__); } } 同时在 stm32f4xx_hal_msp.c 中,生成了串口 MSP 函数 HAL_UART_MspInit 内容如下: void HAL_UART_MspInit(UART_HandleTypeDef* huart) { GPIO_InitTypeDef GPIO_InitStruct; if(huart->Instance==USART1) { __HAL_RCC_USART1_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate = GPIO_AF7_USART1; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); HAL_NVIC_SetPriority(USART1_IRQn, 3, 3); HAL_NVIC_EnableIRQ(USART1_IRQn); } } 函 数 MX_USART1_UART_Init 的 内 容 和 本 章 串 口 实 验 源 码 中 函 数 uart_init 中 调 用 HAL_UART_Init 函数作用类似,只不过波特率是通过入口参数动态设置。而生成的 MSP 函数 HAL_UART_MspInit 内容和实验中该函数的作用就几乎是一模一样了。 关于使用 STM32CubeMX 配置串口的方法就给大家介绍到这里。 |
|
相关推荐
|
|
STM32CUBEMX(12)--IIC,12864OLED(0.96寸)移植
238 浏览 0 评论
STM32CUBEMX(5)--自定义红外NEC解码,定时器TIM捕获方式
518 浏览 0 评论
stm32f407vet6+LAN8720A调试以太网遇到软件复位失败问题
897 浏览 0 评论
使用STM32CubeMX对红外波形进行GPIO输出,并LED显示
435 浏览 0 评论
479 浏览 0 评论
浏览过的版块 |
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-9-12 03:57 , Processed in 0.635454 second(s), Total 66, Slave 48 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191