STM32 - 串口(USART)通信详解

STM32 - 串口(USART)通信


文章目录

  • STM32 - 串口(USART)通信
  • 1、STM32 串口简介
  • 2、串口的工作方式
  • 3、串口通信协议
    • 3.1: 物理层
      • 3.1.1: TTL、RS-232 标准
      • 3.1.2:USB 转串口(划重点!)
      • 3.1.3:串口到串口
    • 3.2 协议层
  • 4、USART 框图功能解析(重要!!!)
    • 4.1 USART 功能框图
    • 4.2 引脚功能
    • 4.3 数据寄存器(划重点!)
    • 4.4 控制单元
      • 4.4.1 控制寄存器1(USART_CR1)
      • 4.4.2 发送器
      • 4.4.3 接收器
      • 4.4.4 相关中断
      • 4.4.5 USART 初始化结构体
  • 5、USART 配置一般步骤
  • 6、USART1收发通信实验(精细分析!!)
      • 6.1 `usart.h` —相关宏定义与函数声明
      • 6.2 `usart.c`- 函数实现与相关初始化操作
      • 6.3 `main.c` 主程序
  • 7、总结
  • 8、参考博客


1、STM32 串口简介

参考 - 【1】

  • 在 STM32 的参考手册中,串口被描述成通用同步异步收发器(USART),它提供了一种灵活的方法与使用工业标准 NRZ 异步串口数据格式的外部设备之间进行全双工数据交换。

    • USART(Universal Synchronous Asynchronous Receiver and Transmitter)是一个串行通信设备。它利用分数波特率发生器提供宽范围的波特率选择。USART 支持同步单向通信半双工单线通信,也支持 LIN(局部互联网),智能卡协议和 IrDA (红外数据组织),以及调制解调器(CTS/RTS)操作。它还允许多处理器通信与使用 DMA 方式实现高速数据通信。
    • UART(Universal Asynchronous Receiver and Transmitter)是在 USART 的基础上裁剪掉了同步通信的功能(时钟同步),只有异步通信。
  • 简单区分同步和异步就是看通信时需不需要对外提供时钟输出。

  • 串行通信一般是以帧格式传输数据,即是一帧一帧的传输,每帧包含有起始信号、数据信息、校验信息(自己设置)、停止信号。

2、串口的工作方式

参考 - 【3】

  • 操作串口一般有两种方式:查询与中断;STM32 还支持第三种 DMA 方式。
    1. 查询:串口程序不断地循环查询标志,看看当前有没有数据要进行传输(发送或接收)。如果有,就进行相应的写操作与读操作进行发送或接收数据。
    2. 中断:平时串口只要打开中断即可。如果发现有一个中断来,则意味着有数据进行传输(发送中断或接收中断)。
    3. DMA 方式:设置好 DMA 工作方式,由 DMA 来自动接收或发送数据。
  • 一般来说,查询方式的效率是比较低的,并且由于 STM32 的 UART 硬件上没有 FIFO,如果程序功能比较多,查询不及时的话很容易出现数据丢失的现象, 故实际项目中这种方式用的并不多。
  • 中断方式的话我们可以分别设置接收中断和发送中断。当串口有数据需要接收时才进入中断程序进行读操作。这种方式占用 CPU 资源比较少,实际项目中比较常用。但需要注意中断程序不要太复杂使执行时间太长,如果执行时间超过一个字符的时间的话也会出现数据丢失的现象,这在波特率比较高的串口编程中比较容易出现。可以考虑用循环BUF方法,在中断程序中只负责实时地接收数据和发送数据,其它操作则放在中断外处理。
  • STM32 还提供了第三种 DMA 方式用来支持高速地串口传输。这种方式只要设置好接收和发送缓冲位置,可以由 DMA 来自动接收和发送数据,这可以最小化占用 CPU 时间。

3、串口通信协议

参考 - 【1】

通信协议分为 物理层协议层

3.1: 物理层

规定通信系统中具有机械、电子功能部分特性,确保原始数据在物理媒体的传输,通俗一点就是硬件部分

3.1.1: TTL、RS-232 标准

  • TTL:晶体管-晶体管逻辑电平。很多单片机内部如 STM32 等,以及一些传感器一般都是TTL电平。

  • RS-232:是一种串行数据传输形式,称其为串行连接。最经典的标志就是9针孔的 DB9 电缆(如下图所示)。RS-232 电压表示逻辑1,0的范围大,极大的增强了容错率,主要用于工业设备通信。
    STM32 - 串口(USART)通信详解_第1张图片
    TTL 与 RS-232 的区别:
    STM32 - 串口(USART)通信详解_第2张图片
    由上表可知,TTL 和 RS-232 标准逻辑大不相同,若单片机与其他 TTL 设备采用 RS-232 通信(DB9),需要进行电平转化 TTL->RS-232,RS-232->TTL。如下图所示
    STM32 - 串口(USART)通信详解_第3张图片

  • 两个通信设备的 DB9 接口 之间通过串口信号线建立起连接,串口信号线中使用 RS-232 标准 传输数据信号。但是呢,由于 RS-232 标准 的电平信号不能直接被控制器识别,所以这些信号会经过一个 电平转换芯片(一般有 MAX3232,SP3232)转换成控制器能够识别的 TTL标准 的电平信号,才能实现通信。

3.1.2:USB 转串口(划重点!)

USB 转串口:主要用于设备(STM32)与电脑通信。其中电平转换芯片一般有 CH340(野火的用这个)、PL2303、CP2102(本人使用的板子采用的就是此芯片)、FT232。使用的时候,电脑会按照电平转换芯片的驱动虚拟出一个串口。

STM32 - 串口(USART)通信详解_第4张图片

3.1.3:串口到串口

原生的串口通信主要是控制器跟采用串口通信的设备或者与传感器通信(如 GPS 模块、GSM 模块、串口转 WIFI 块、HC04 蓝牙模块)。这些设备之间的通信都是采用 TTL标准,因此不需要额外的电平转换芯片来转换电平。

3.2 协议层

主要规定通讯逻辑,统一收发双方的数据打包、解包标准(软件)。规定了数据帧是由起始位、主体数据、校验位以及停止位组成的。并且通信双方的数据帧格式要约定一致(一样的起始位、数据主体、校验位和停止位)才能正常收发数据。

STM32 - 串口(USART)通信详解_第5张图片

  1. 通信的起始信号和停止信号
    • 串口通信的一个数据包从 起始信号 开始,直到 停止信号 结束。而数据包的 停止信号 可由 0.5、1、1.5 或 2 个逻辑 “1” 的数据位表示:
      • 0.5 个停止位:在智能卡模式下接收数据时使用;
      • 1 个停止位:停止位位数的默认值;
      • 1.5 个停止位:在智能卡模式下发送和接收数据时使用;
      • 2 个停止位:可用于常规USART模式,单线模式以及调制解调器模式。
  2. 数据帧(有效数据)
    • 在数据包的起始位之后紧接着就是传输的主体数据内容(数据帧),也称为有效数据。有效数据的长度常被约定为5、6、7 或 8 位长。
  3. 数据校验
    • 奇偶校验是用来检查数据传输的正确性的方法。奇偶校验能检测出传输数据的部分错误(然而仅仅是1位误码是能检测出,2 位及 2 位以上检测不出来),而且不能纠错。在发现错误后,只能要求重发。由于简单所以被广泛应用。

    • 数据传输之前通常会确定是奇校验还是偶校验,以保证发送端和接收端采用相同的校验方式进行数据校验。若是校验位不符合,则误认为传输错误。

      1. 奇校验:有效数据加校验位中 “1” 的个数为奇数。
      2. 偶校验:有效数据加校验位中 “1” 个数为偶数。
    • 校验原理:假如采用奇校验,发送端发送的一个字符编码(含校验位)中,“1” 的个数一定为奇数个。在接收端对接收字符二进制位中的 “1” 的个数进行统计,若是统计出 “1” 的个数位偶数个,则意味着传输过程中有1位发生错误。

    • 事实上,在传输中偶尔—位出错的机会最多,故奇偶校验法常常采用。

4、USART 框图功能解析(重要!!!)

参考 - 【1】

4.1 USART 功能框图

STM32 - 串口(USART)通信详解_第6张图片

4.2 引脚功能

下面三个常用:

  • TX:发送数据输出引脚;

  • RX:接收数据输入引脚;

  • SCLK:发送器时钟输出引脚。(仅仅适用于同步通信)。

下面三个不常用:

  • SW_RX:数据接收引脚,只用单线和智能卡模式,属于内部引脚,没有具体外部引脚。

  • nRTS:请求以发送(Request To Send),n 表示低电平有效。如果硬件流控制为 RTS 流控制,当 USART 接收器准备好接收新数据时就会将 nRTS 变成低电平;当接收寄存器已满时,nRTS 将被设置位高电平。该引脚只适用于硬件流控制。

  • nCTS:清楚以发送(Clear To Send),n 表示低电平有效。如果硬件流控制为 CTS 流控制,USART 发送器在发送下一帧数据之前会检测 nCTS 引脚,如果为低电平,表示可以发送数据;如果为高电平,则在发送完当前数据帧之后停止发送。该引脚只适用于硬件流控制。

4.3 数据寄存器(划重点!)

STM32 - 串口(USART)通信详解_第7张图片

  • 如下图:TDR(发送数据寄存器) 和 RDR(接收数据寄存器) 都是介于系统总线和移位寄存器之间。串行通信都是一位一位传输的。发送时把 TDR 内数据并行传输到发送移位寄存器,然后发送移位寄存器把数据一位一位(串行)发送出去。接收时把接收到的每一位数据顺序保存在接收移位寄存器内,然后并行传输到 RDR。
    STM32 - 串口(USART)通信详解_第8张图片
    STM32 - 串口(USART)通信详解_第9张图片
  • 相关实现代码。在 STM32Fxxx_usart.c 库函数中,无需自己编写(添加了头文件,直接调用即可,下面第 6 小节中会调用这个函数)。
    STM32 - 串口(USART)通信详解_第10张图片

4.4 控制单元

4.4.1 控制寄存器1(USART_CR1)

STM32 - 串口(USART)通信详解_第11张图片
STM32 - 串口(USART)通信详解_第12张图片

4.4.2 发送器

  • 发送器根据 M 位的状态发送 8 位或 9 位的数据字。当发送使能位(TE)被设置时,发送移位寄存器中的数据在 TX 脚上输出,相应的时钟脉冲在 CK 脚上输出。

  • 一个字符帧发送需要三个部分:起始位 + 数据帧(可能带有校验位)+ 停止位。每一个数据帧之前都有一个低电平的起始位,数据帧之后跟着的是停止位(停止位位数可配置),停止位是一定时间周期的高电平。数据帧就是我们需要发送的 8 位或 9 位数据,数据是从最低位开始传输的

  • 配置步骤:

    1. 通过在 USART_CR1 寄存器上置位 UE 位来激活 USART。
      在这里插入图片描述

    2. 编程 USART_CR1 的 M 位来定义字长。
      在这里插入图片描述

    3. USART_CR2 中编程停止位的位数。
      STM32 - 串口(USART)通信详解_第13张图片

    4. 如果采用多缓冲通信,配置 USART_CR3 中的 DMA 使能位 (DMAT)。按多缓冲器通信中的描述配置 DMA 寄存器。
      在这里插入图片描述

    5. 利用 USART_BRR 寄存器选择要求的波特率。
      STM32 - 串口(USART)通信详解_第14张图片

    发送和接收都是用的波特率发生器驱动,当发送器和接收器的使能位分别置位时,分别为其产生时钟。

STM32 - 串口(USART)通信详解_第15张图片STM32 - 串口(USART)通信详解_第16张图片

  • 以波特率为 115200 为例
    STM32 - 串口(USART)通信详解_第17张图片
  1. 设置 USART_CR1 中的 TE 位,发送一个空闲帧(一个数据长度的高电平)作为第一次数据发送。
    STM32 - 串口(USART)通信详解_第18张图片

  2. 把要发送的数据写进 USART_DR 寄存器(此动作清楚 SR 中的 TXE位)。在只有一个缓冲器的情况下,对每个待发送的数据重复步骤 7。
    STM32 - 串口(USART)通信详解_第19张图片

  3. **在 USART_DR 寄存器中写入最后一个数据字后,要等待 SR 中的 TC=1,它表示最后一个数据帧的传输结束(移位寄存器中的数据全部发送完毕)。**当需要关闭 USART 或需要进入停机模式之前,需要确认传输结束,避免破坏最后一次传输。
    STM32 - 串口(USART)通信详解_第20张图片

深入理解 SR 中的 TXE 位与 TC 位:

  • 清零 TXE 位总是通过对数据寄存器的写操作( CPU 或 DMA)来完成的,当 TXE 位已经被硬件置 1 它表明:

    1. 数据已经从 TDR 移送到移位寄存器,数据发送已经开始(发送移位寄存器 正在一位一位向外传输数据);
    2. TDR 寄存器被清空;
    3. 下一个数据可以被写进 USART_DR 寄存器而不会覆盖先前的数据。如果 TXEIE 位被设置,此标志将产生一个中断。
    
  • 如果此时 USART 正在发送数据(发送移位寄存器正在一位一位向外传输数据)。对 USART_DR 寄存器的写操作把数据存进 TDR 寄存器,并在 当前传输结束时把数据复制进移位寄存器,也就是说移位寄存器里面的数据并不会被覆盖。所以只要发送一帧数据等待 TXE 置 “1” 即可,就算时发送多帧数据时最后也不用等待 TC=1。

  • 如果此时 USART 没有在发送数据,处于空闲状态。对 USART_DR 寄存器的写操作直接把数据放进移位寄存器,数据传输开始,TXE 位立即被置 “1”。

  • 当一帧发送完成时(停止位发送后)并且设置了 TXE 位,TC 位被置 “1”。如果 USART_CR1寄存器中的 TCIE 位被置 “1” 时,则会产生中断。

  • 使用下列软件过程清楚 TC 位:

    1. 读一次 USART_SR寄存器;
    2. 写一次 USART_DR寄存器。
    
  • TC 位也可以通过软件对它写 ‘0’来清除。此清零方式只推荐在多缓冲器通信模式下使用。

4.4.3 接收器

如果将 USART_CR1寄存器的 RE 位置 “1”,使能 USART 接收,使得接收器在 RX 线开始搜索起始位。确定起始位后,就根据 RX 线电平状态把数据存放在接收移位寄存器内。接收完成后就把接收移位寄存器数据移传输到 RDR 内,并把 USART_SR 寄存器的 RXNE 位置 “1” ,同时如果 USART_CR2寄存器的 RXNEIE 置 “1” 就可以产生中断。

  • 当一个字符被接收到时

    1. RXNE 位被置 “1”。它表明移位寄存器的内容被转移到RDR。即数据已经被接收并且可以被读出。
    2. 如果 RXNEIE 位被设置,则产生中断。
    3. 在多缓冲器通信时,RXNE 在每个字节被接收后被置 “1”,并由 DMA 对数据寄存器的读操作而清零。
    4. 在单缓冲器模式里,由软件读 USART_DR 寄存器完成对 RXNE 位清除,RXNE 标志也可以通过对它写 “0” 来清除。
  • 溢出错误:

    • 如果 RXNE 还没有被复位(还没有读出 DR 寄存器的数据),又接收到一个字符,则会发生溢出错误。**数据只有当 RXNE 位被清零后才能从移位寄存器转移到 RDR 寄存器。**RXNE 是在接收到每个字节后被置 “1” 的。如果写一个数据已经被收到或是 DMA 请求还没被服务时,RXNE 标志仍是 “1”,溢出错误产生。

    • 当移除错误产生时

       1. ORE 位被置位。
       2. RDR 内容将不会丢失。读 USART_DR 寄存器仍能得到先前的数据。
       3. 移位寄存器中以前的内容将被覆盖,随后接收到的数据都将丢失。(!)
       4. 如果 RXNEIE 位被设置或 EIE 和 DMAR 位都被设置,中断产生。
       5. 顺序执行对 USART_SR 和 USART_DR 寄存器的读操作,可复位 ORE 位。
      

STM32 - 串口(USART)通信详解_第21张图片

4.4.4 相关中断

USART 的中断请求:其时间标志常用于防止误触发中断。在第 6 节的中断服务函数中有使用

STM32 - 串口(USART)通信详解_第22张图片

USART 中断映像图
STM32 - 串口(USART)通信详解_第23张图片

4.4.5 USART 初始化结构体

USART 结构体代码实现,同样是库函数里面已经定义了。引用了 32 的头文件就可以直接调用

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 个停止位。
  4. USART_Parity : 奇偶校验控制选择 ,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、⑷ 使能硬件流。

5、USART 配置一般步骤

参考 - 【3】
STM32 - 串口(USART)通信详解_第24张图片

  1. **RCC 配置 **

    • 由于 UART 的 TX、RX 和 AFIO 都挂在 APB2 桥上,因此采用固件库函数 RCC_APB2PeriphClockCmd();进行初始化。UARTx 需要分情况讨论,如果是 UART1,则挂在 APB2 桥上;其余的 UART2~5 均挂在 APB1 上。
  2. GPIO 配置 (GPIO 了解可参考这篇文章【4】)

    • GPIO 的属性包含在结构体 GPIO_InitTypeDef中。
      • 其中对于 TX 引脚,GPIO_Mode 字段设置为 GPIO_Mode_AF_PP (复用推挽输出),GPIO_Speed 切换速率 设置为 GPIO_Speed_50MHZ
      • 对于 RX 引脚,GPIO_Mode字段设置为 GPIO_IN_FLOATING(浮空输入),不需要设置切换速率。最后通过 GPIO_Init() 使能 IO 口。

    注:AHB 总线、APB2 总线、APB1 总线 介绍参考【5】

  3. USART 配置

    • STM32 在只有一个中断的情况下,仍然需要优先配置优先级,这样才能 **使能某条中断的触发通道。**STM32 的中断有至多两个层次,分别是 主优先级从优先级,通过 NVIC_PriorityGroupConfig()实现。

6、USART1收发通信实验(精细分析!!)

参考 - 【1】、【6】、【8】、【9】

使用 STM32 标准库,使用的功能如串口 USART 功能,DMA 功能、NVIC 功能、I2C 功能、SPI 功能等都有一个类似的配置流程。

  • 编程要点:

    1. 初始化串口所用到的 GPIO;
    2. 初始化串口 USART;
    3. 配置中断(中断接收,中断优先级);
    4. 使能串口;
    5. 编写发送和接收函数;
    6. 编写中断服务函数。

实际使用中,前四步都是一样的(因此前四部理解即可)。后面的第 5、6步就需要根据自己的需求来实现。

下面 在 usart.h 中进行函数声明与宏的定义;在 usaert.c 中进行函数实现与相应初始化操作;在 main.c 调用。

6.1 usart.h —相关宏定义与函数声明

/* 注:
	1.XX初始化结构体属于标准库函数的部分已经定义好(用户不用编写),具体的配置包含在初始化配置函数中(用户自己编写)。
   	  如: USART 初始化结构体 USART_InitTypeDef;  USART 时钟初始化结构体 USART_Clock_InitTypeDef 或     USART_ClockInitTypeDef。
   	2.下面串口 1~5(USART1~5)需要用到哪几个就上对应的宏即可,无需全部写上。
*/
#ifndef _USART_H
#define _USART_H

#include "stm32f4xx.h" // 我用的是 f4 的芯片,如果为其他的更换为其他 32 的头文件
#include 
#define  DEBUG_USART1   1
#define  DEBUG_USART2   2

/* 串口宏定义(起个别名,方便移植),不同的串口挂载的总线和 I/O 不一样 */
#if  DEBUG_USART1  

/* 串口 1-USART1 */ 
#define  DEBUG_USARTx                   USART1                   // 串口 1
#define  DEBUG_USART_CLK                RCC_APB2Periph_USART1    // 串口 1 的时钟
#define  DEBUG_USART_APBxClkCmd         RCC_APB2PeriphClockCmd   // 由于 UART1 的 TX、RX 和 AFIO 都挂在 APB2 桥上,因此采用固件库函数 RCC_APB2PeriphClockCmd() 进行初始化。后面会用到该代码 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); 把 USART1 外设的 TX、RX 映射到其它 I/O 口上。即被映射的 I/O 口与内置外设原来所对应的 I/O 口的作用相同( I/O 管脚的复用功能)。 
#define  DEBUG_USART_BAUDRATE           115200                   // 设置串口 1 的波特率为 115200

/* USART GPIO 引脚宏定义 */
#define  DEBUG_USART_GPIO_CLK           (RCC_APB2Periph_GPIOA)   // GPIO 的时钟
#define  DEBUG_USART_GPIO_APBxClkCmd    RCC_APB2PeriphClockCmd   // GPIO 外设是挂在 APB2 总线上的。APB2 的时钟 = GPIO 外设的时钟
    
#define  DEBUG_USART_TX_GPIO_PORT       GPIOA   				 // TX 复用在 GPIOA 上
#define  DEBUG_USART_TX_GPIO_PIN        GPIO_Pin_9				 // GPIOA 的 9 引脚 即 A9
#define  DEBUG_USART_RX_GPIO_PORT       GPIOA                    // RX 复用在 GPIOA 上
#define  DEBUG_USART_RX_GPIO_PIN        GPIO_Pin_10				 // GPIOA 的 10 引脚 即 A10

#define  DEBUG_USART_IRQ                USART1_IRQn				 // 设置 串口 USART1 为中断源
#define  DEBUG_USART_IRQHandler         USART1_IRQHandler

/* 串口 2-USART2 */ 
#elif   DEBUG_USART2 

#define  DEBUG_USARTx                   USART2
#define  DEBUG_USART_CLK                RCC_APB1Periph_USART2
#define  DEBUG_USART_APBxClkCmd         RCC_APB1PeriphClockCmd
#define  DEBUG_USART_BAUDRATE           115200

// USART GPIO 引脚宏定义
#define  DEBUG_USART_GPIO_CLK           (RCC_APB2Periph_GPIOA)
#define  DEBUG_USART_GPIO_APBxClkCmd    RCC_APB2PeriphClockCmd
    
#define  DEBUG_USART_TX_GPIO_PORT       GPIOA   
#define  DEBUG_USART_TX_GPIO_PIN        GPIO_Pin_2
#define  DEBUG_USART_RX_GPIO_PORT       GPIOA
#define  DEBUG_USART_RX_GPIO_PIN        GPIO_Pin_3

#define  DEBUG_USART_IRQ                USART2_IRQn
#define  DEBUG_USART_IRQHandler         USART2_IRQHandler

/* 串口 3-USART3 */ 
#elif DEBUG_USART3

#define  DEBUG_USARTx                   USART3
#define  DEBUG_USART_CLK                RCC_APB1Periph_USART3
#define  DEBUG_USART_APBxClkCmd         RCC_APB1PeriphClockCmd
#define  DEBUG_USART_BAUDRATE           115200
 
// USART GPIO 引脚宏定义
#define  DEBUG_USART_GPIO_CLK           (RCC_APB2Periph_GPIOB)
#define  DEBUG_USART_GPIO_APBxClkCmd    RCC_APB2PeriphClockCmd
    
#define  DEBUG_USART_TX_GPIO_PORT       GPIOB   
#define  DEBUG_USART_TX_GPIO_PIN        GPIO_Pin_10
#define  DEBUG_USART_RX_GPIO_PORT       GPIOB
#define  DEBUG_USART_RX_GPIO_PIN        GPIO_Pin_11
 
#define  DEBUG_USART_IRQ                USART3_IRQn
#define  DEBUG_USART_IRQHandler         USART3_IRQHandler
 
/* 串口 4-USART4 */ 
#elif DEBUG_USART4

#define  DEBUG_USARTx                   UART4
#define  DEBUG_USART_CLK                RCC_APB1Periph_UART4
#define  DEBUG_USART_APBxClkCmd         RCC_APB1PeriphClockCmd
#define  DEBUG_USART_BAUDRATE           115200
 
// USART GPIO 引脚宏定义
#define  DEBUG_USART_GPIO_CLK           (RCC_APB2Periph_GPIOC)
#define  DEBUG_USART_GPIO_APBxClkCmd    RCC_APB2PeriphClockCmd
    
#define  DEBUG_USART_TX_GPIO_PORT       GPIOC   
#define  DEBUG_USART_TX_GPIO_PIN        GPIO_Pin_10
#define  DEBUG_USART_RX_GPIO_PORT       GPIOC
#define  DEBUG_USART_RX_GPIO_PIN        GPIO_Pin_11
 
#define  DEBUG_USART_IRQ                UART4_IRQn
#define  DEBUG_USART_IRQHandler         UART4_IRQHandler
 
/* 串口5-UART5 */
#elif DEBUG_USART5

#define  DEBUG_USARTx                   UART5
#define  DEBUG_USART_CLK                RCC_APB1Periph_UART5
#define  DEBUG_USART_APBxClkCmd         RCC_APB1PeriphClockCmd
#define  DEBUG_USART_BAUDRATE           115200
 
// USART GPIO 引脚宏定义
#define  DEBUG_USART_GPIO_CLK           (RCC_APB2Periph_GPIOC|RCC_APB2Periph_GPIOD)
#define  DEBUG_USART_GPIO_APBxClkCmd    RCC_APB2PeriphClockCmd
    
#define  DEBUG_USART_TX_GPIO_PORT       GPIOC   
#define  DEBUG_USART_TX_GPIO_PIN        GPIO_Pin_12
#define  DEBUG_USART_RX_GPIO_PORT       GPIOD
#define  DEBUG_USART_RX_GPIO_PIN        GPIO_Pin_2
 
#define  DEBUG_USART_IRQ                UART5_IRQn
#define  DEBUG_USART_IRQHandler         UART5_IRQHandler
 
#endif
 
void USART_Config(void);                                                   // 配置初始化 函数
void Usart_SendByte(USART_TypeDef* pUSARTx, uint8_t data);				   // 发送一个字节
void Usart_SendHalfWord(USART_TypeDef* pUSARTx, uint16_t data);			   // 发送一个 16 位的数据
void Usart_SendArray(USART_TypeDef* pUSARTx, uint8_t *array,uint8_t num);  // 发送一个 8 位的数组
void Usart_SendString(USART_TypeDef* pUSARTx, char *str);				   // 发送字符串
#endif  /* __BSP_USART_H */

6.2 usart.c- 函数实现与相关初始化操作

#include "usart.h"

/* 配置 嵌套向量中断控制器 NVIC */
static void NVIC_Configuration(void)
{
     // 创建一个 NVIC 对象,用于后面初始化。NVIC_InitTypeDef 为标准库函数,已经定义好了,直接用即可。
	NVIC_InitTypeDef  NVIC_InitStructure;                       
	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);              // 嵌套向量中断控制器组选择
    
	NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQ;        // 配置 USART 为中断源
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x01; // 主优先级(抢断优先级)配置
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01;        // 子优先级配置
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;              // 使能中断
   
	NVIC_Init(&NVIC_InitStructure);								 // 初始化配置 NVIC
}

/* 配置 USART、GPIO */
void USART_Config(void)
{
 	// 分别定义 GPIO 与 USART 对象,用于后面初始化。GPIO_InitTypeDef 与 USART_InitTypeDef 同样为标准库函数,直接用即可
	GPIO_InitTypeDef GPIO_InitStructure;
	USART_InitTypeDef USART_InitStructure;

	// 打开串口GPIO的时钟
	DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);
	
	// 打开串口外设的时钟
	DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE);
	
    /* 配置 GPIO 的工作参数 */ 
	// 将 USART Tx 的 GPIO 配置为推挽复用模式
	GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;     // 设置 GPIO 的输出引脚
    
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;			   // 设置 GPIO 的工作模式为 推挽输出模式(常用输出设置)
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;          // 设置 GPIO 的输出速度
	GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);  // 初始化配置 GPIO 的输出

    // 将 USART Rx 的 GPIO 配置为浮空输入模式
	GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;     // 设置 GPIO 的输入引脚
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;      // 设置 GPIO 的工作模式为 浮空输入模式
	GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);  // 初始化配置 GPIO 的输入
	
	/* 配置串口的工作参数 */ 
	USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;       // 配置波特率
	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(DEBUG_USARTx, &USART_InitStructure);                  // 完成串口的初始化配置
	
	NVIC_Configuration(); //中断配置                                           
	
	USART_ITConfig(DEBUG_USARTx,USART_IT_RXNE, ENABLE); //开启串口接收中断
	
	USART_Cmd(DEBUG_USARTx, ENABLE); //使能串口
}

/* 发送一个字节的数据 */
void Usart_SendByte(USART_TypeDef* pUSARTx, uint8_t date) 
{
    USART_SendData(pUSARTx,date); // 调用 STM32 的库函数 USART_SendData(串口,数据),来发送数据。
    /* 等待发送寄存器发送完成 */
	while( USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);  
    // 关于 TXE 的知识点在上面 4. USART 功能框图 -> 4.4 控制单元(划重点!)-> 4.4.2.发送器 -> 第 7 点关于控制寄存器 SR 中 TXE(发送寄存器空)的介绍。
    // 概括来讲 就是 TDR 中的数据还没被转移到 移位寄存器中。TXE 位就一直为 “0”。反过来,就为 “1”.
    // USART_GetFlagStatus()(该函数的返回值 只有两种:RESET = 0, SET = !RESET)函数在此处就是判断 串口 pUSARTx 的  USART_FLAG_TXE(发送数据寄存器空标志位)。如果 TXE 为 “0”,则与 返回 RESET,反之,返回 SET。
    // 因此,还没发送完,就通过 while(...) 函数进行等待,等待数据发送完成。
}

// 发送一个16位的数据
void Usart_SendHalfWord(USART_TypeDef * pUSARTx, uint16_t date)
{
	uint16_t tmp_h;
	uint16_t tmp_l;
	tmp_h = date>>0x08; // 向右移动 8 位,获取高 8 位。 
	tmp_l = date & 0xff; // 通过与 1111 1111 进行 “与” 操作,获得数据的低八位。
	Usart_SendByte(pUSARTx,tmp_h); // 先发送高 8 位
	Usart_SendByte(pUSARTx,tmp_l); // 后发送低 8 位 
    // 为什么不能直接发送 16 位数据,而分别发送高 8 位 与 低 8 位?
    // 这是因为串口最大支持数据长度为 9bit。
}

// 发送一个8位的数组
void Usart_SendArray(USART_TypeDef * pUSARTx, uint8_t *arr, uint16_t num)
{ 
   while(num--)
	 {
		 Usart_SendByte( pUSARTx ,*arr++);
	 }
 while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TC) == RESET); // 等待发送成功,TXE 被置 “1”.
}

// 发送字符串
void Usart_SendString( USART_TypeDef * pUSARTx, char *str)
{
 while( *str!='\0' )
 {
	 Usart_SendByte( pUSARTx, *str++); 
 }

 while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TC)== RESET); // 等待数据发送成功(与发送数据类型无关).
}


/* c语言中 printf 函数默认输出设备是显示器,如果实现在串口或 LCD 等上显示,必须重定义标准库函数李米娜调用的输出设备定义的相关函数。即使用 printf 输出到串口,需要将 fputc 里面的输出指向串口,这一过程称为 重定向。 */

// 重定向c库函数printf到串口,重定向后可使用printf函数
int fputc(int ch, FILE *f) // 函数默认的,在使用 printf 函数时自动调用
{
		/* 发送一个字节数据到串口 */
		USART_SendData(DEBUG_USARTx, (uint8_t) ch);
		
		/* 等待发送完毕 */
		while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_TXE) == RESET);		
	
		return (ch);
}

// 重定向c库函数scanf到串口,重写向后可使用scanf、getchar等函数
// scanf函数(默认键盘输入,我们要重定向到串口接收)
int fgetc(FILE *f)
{
		/* 等待串口输入数据 */
		while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_RXNE) == RESET);

		return (int)USART_ReceiveData(DEBUG_USARTx);
}



//中断服务函数
void DEBUG_USART_IRQHandler(void)
{
     uint16_t tmp;
	/* 实现 串口在接收到数据后,又把接收到数据通过串口发送出去。 */
    // 该 if 语句表明作用:1、判断是哪个串口产生中断;2、防止误触发中断(即通过 RXNE 进行判断)。
	if(USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_RXNE) != RESET) 
	{
	  tmp = USART_ReceiveData(DEBUG_USARTx); // 将串口接收到的数据保存在 tmp 中
	  USART_SendData(DEBUG_USARTx,tmp);	// 接收到数据后,把接收到的数据通过串口发送出去
	  while( USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_TXE)== RESET); // 等待发送成功
		
   	  // 关于 RXNE 的知识点在上面 4.USART 功能框图 -> 4.4 控制单元(划重点!)-> 4.4.2.接收器。
	}
}

6.3 main.c 主程序

#include "stm32f10x.h"   
#include "usart.h"

int main(void)
{
	
	uint8_t a[8] = {0x0,0x1,0x2,0x3,0x4,0x5,0x6,0x7};
	
	/*串口初始化*/
	USART_Config();
	//printf("\n 单个字符输出测试:");
	Usart_SendByte( DEBUG_USARTx, 'F');
	//printf("\n 8位数字输出测试:");
	Usart_SendByte( DEBUG_USARTx, 0xFF);
	//printf("\n 16位数输出测试:");
	Usart_SendHalfWord( DEBUG_USARTx, 0xFFFF);
	//printf("\n 数组输出测试:");
	Usart_SendArray( DEBUG_USARTx, a, 8);
	//printf("\n 字符串输出测试:");
	Usart_SendString( DEBUG_USARTx, "字符串 hello world\r\n");
	
	while(1);
}

7、总结

目前还在学习阶段,所以还是仅仅在依靠别人博客进行笔记真理的阶段。不过 厚积薄发嘛,在整理过程也 get 了很多。串口通信这方面,差不多基本掌握了,但是也只局限于串口通信。比如里面还有 GPIO、DMA、NVIC 等还没有往下面挖掘。以及上面提到的已经被 32 包含的相应库函数,虽然已经在源码中看明白了,还是需要通过笔记的形式进行输出。嘿嘿,后面再继续加油!道阻且长,我依然选择一步一步前行。

8、参考博客

【1】STM32串口通信详解_rivencode的博客-CSDN博客_stm32串口通信

【2】(5条消息) 数据通信中,奇偶校验原理_又小雪666的博客-CSDN博客

【3】STM32–USART详解_阿槐123456的博客-CSDN博客_usart

【4】GPIO简介_旭日初扬的博客-CSDN博客_gpio

【5】STM32中AHB总线_APB2总线_APB1总线这些是什么_m0_59949484的博客-CSDN博客_apb1总线

【6】STM32——串口通信实验_骤雨南山下的博客-CSDN博客_stm32串口通信实验

【7】嵌套向量中断控制器 (NVIC)_不知名的好人的博客-CSDN博客_nvic

【8】STM32的USART_GetFlagStatus和USART_GetITStatus解析_嵌入式快跑的博客-CSDN博客

【9】printf重定向_旭日初扬的博客-CSDN博客_printf重定向

你可能感兴趣的:(STM32,stm32,单片机,嵌入式硬件,mcu)