⑦ STM32基础学习— 串口

[TOC]

1 串口UART

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

串行通信一般是以帧格式传输数据,即是一帧一帧的传输,每帧包含有起始信号、数据信息、停止信息,可能还有校验信息。USART 就是对这些传输参数有具体规定,当然也不是只有唯一一个参数值,很多参数值都可以自定义设置,只是增强它的兼容性。

USART 满足外部设备对工业标准 NRZ 异步串行数据格式的要求,并且使用了小数波特率发生器,可以提供多种波特率,使得它的应用更加广泛。USART 支持同步单向通信和半双工单线通信;还支持局域互连网络 LIN、智能卡 (SmartCard) 协议与 lrDA(红外线数据协会) SIR ENDEC 规范。

通用同步异步收发器(USART)提供一种灵活的方法与外部设备之间进行全双工数据交换,USART利用分数波特率发生器提供宽范围的波特率选择,使用多缓冲器配置的DMA方式,可以实现高速数据通信。串口作为 MCU 的重要外部接口,同时也是软件开发重要的调试手段,现在基本上所有的 MCU 都会带有串口。STM32 的串口资源相当丰富的,STM32F103C8T6 最多可提供 3 路串口,有分数波特率发生器、支持同步单线通信和半双工单线通讯、支持 LIN、支持调制解调器操作、智能卡协议和 IrDA SIR ENDEC 规范、具有 DMA等。

串口设置的一般步骤可以总结为如下几个步骤

  • 串口时钟使能,GPIO 时钟使能
  • 串口复位
  • GPIO 端口模式设置
  • 串口参数初始化
  • 开启中断并且初始化 NVIC(如果需要开启中断才需要这个步骤)
  • 使能串口
  • 编写中断处理函数

与串口基本配置直接相关的几个固件库函数和定义主要分布在 stm32f10x_usart.h 和 stm32f10x_usart.c 文件中。

接线方式

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

  • RX:接收数据串行输。通过过采样技术来区别数据和噪音,从而恢复数据
  • TX:发送数据输出。当发送器被禁止时,输出引脚恢复到它的I/O端口配置。当发送器被激活,

并且不发送数据时,TX引脚处于高电平。

2 USART框架图

2.1 引脚说明

功能引脚

  • TX:发送数据输出引脚。
  • SW_RX:数据接收引脚,属于内部引脚。
  • RX:接收数据输入引脚。
  • SW_RX:数据接收引脚,只用于单线和智能卡模式,属于内部引脚,没有具体外部引脚。
  • nCTS:清除以发送 (Clear To Send),n 表示低电平有效。如果使能 CTS 流控制,发送器在发送下一帧数据之前会检测 nCTS 引脚,如果为低电平,表示可以发送数据,如果为高电平则在发送完当前数据帧之后停止发送。该引脚只适用于硬件流控制。
  • SCLK:发送器时钟输出引脚。这个引脚仅适用于同步模式。
  • nRTS:请求以发送,n表示低电平有效。如果使能 RTS 流控制,当USART接收器准备好接收新数据时就会将nRTS变成低电平;当接收寄存器已满时,nRTS将被设置为高电平。该引脚只适用于硬件流控制。
  • nCTS:清除以发送(Clear To Send),n表示低电平有效。如果使能 CTS流控制,发送器在发送下一帧数据之前会检测 nCTS 引脚,如果为低电平,表示可以发送数据,如果为高电平则在发送完当前数据帧之后停止发送。该引脚只适用于硬件流控制。
  • SCLK:发送器时钟输出引脚。这个引脚仅适用于同步模式。

STM32F103C8T6芯片的 _USART_ 引脚

STM32F103C8T6 系统控制器有三个 USART,其中 USART1 和时钟来源于 APB2 总线时钟,其最大频率为 72MHz,其他四个的时钟来源于 APB1 总线时钟,其最大频率为 36MHz。如果是UART 只是异步传输功能,所以没有 SCLK、nCTS 和 nRTS 功能引脚。

2.2 接收发送数据寄存器

USART数据寄存器(USART_DR)只有低 9 位有效

2.3 串口时钟

USART1的时钟来源于APB2总线时钟,最大频率为72MHZ,其他4个时钟来源于APB1总线时钟,最大频率36MHZ。UART只有异步传输功能,没有SCLK、nCTS和nRTS功能引脚。

2.4 波特率发生器

接收器和发送器的波特率在USARTDIV的整数和小数寄存器中的值应设置成相同。

  • 其中,fPLCK 为 USART 时钟,USARTDIV 是一个存放在波特率寄存器 (USART_BRR) 的一个无符号定点数。其中 DIV_Mantissa[11:0] 位定义 USARTDIV 的整数部分,DIV_Fraction[3:0] 位定义USARTDIV 的小数部分。
  • 例如:DIV_Mantissa=24(0x18),DIV_Fraction=10(0x0A),此时 USART_BRR 值为 0x18A;那么 USARTDIV 的小数位 10/16=0.625;整数位 24,最终 USARTDIV 的值为 24.625。
  • 如果知道 USARTDIV 值为 27.68,那么 DIV_Fraction=16*0.68=10.88,最接近的正整数为 11,所以 DIV_Fraction[3:0] 为 0xB;DIV_Mantissa= 整数(27.68)=27,即为 0x1B。
  • 波特率的常用值有 2400、9600、19200、115200。

2.5 串口中断事件

2.6 printf 函数支持

通过 加入printf 函数支持的代码,printf 函数向串口发送我们需要的内容,方便开发过程中查看代码执行情况以及一些变量值。

//加入以下代码,支持 printf 函数,而不需要选择 use MicroLIB
#if 1
#pragma import(__use_no_semihosting) 
//标准库需要的支持函数 
struct __FILE 
{ 
    int handle;
}; 
FILE __stdout; 
//定义_sys_exit()以避免使用半主机模式 
_sys_exit(int x) 
{ 
    x = x; 
} 
//重定义 fputc 函数
int fputc(int ch, FILE *f)
{ 
    while(USART_GetFlagStatus(USART1,USART_FLAG_TC)==RESET); 
    USART_SendData(USART1,(uint8_t)ch);  //printf 定义串口1
    return ch;
}
#endif

==串口时钟使能== 使能相应的 GPIO 时钟,然后使能复用功能时钟和内置外设时钟,串口是挂载在 APB2 下面的外设。

RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE); //使能 USART1,GPIOA 时钟

==串口复位==

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

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

例如复位串口1:

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

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

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

==GPIO 端口为特定的状态==

所以接下来的两段代码就是将 TX(PA9)设置为推挽复用输出模式,将 RX(PA10)设置为浮空输入模式:

//USART1_TX PA.9
 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9 复用推挽输出
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
 GPIO_Init(GPIOA, &GPIO_InitStructure);

 //USART1_RX PA.10 浮空输入
 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
 GPIO_Init(GPIOA, &GPIO_InitStructure);

==GPIO中断==

//Usart1 NVIC 中断配置 配置
 NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级 3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级 3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化 VIC 寄存器

==置串口 1 的初始化参数==

//USART 初始化设置
USART_InitStructure.USART_BaudRate = bound;//波特率设置;
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_Init()函数结构体==

typedef struct
{ 
    uint32_t USART_BaudRate; 
    uint16_t USART_WordLength; 
    uint16_t USART_StopBits; 
    uint16_t USART_Parity; 
    uint16_t USART_Mode; 
    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:硬件流控制选择,只有在硬件流控制模式才有效,可选有

==USART 时钟初始化结构体==

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 位的值。

完成上面的操作后就可以写中断函数了

USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启中断
USART_Cmd(USART1, ENABLE); //使能串口

==USART1_IRQHandler函数==

void USART1_IRQHandler(void)函数是串口 1 的中断响应函数,当串口 1 发生了相应的中断后,就会跳到该函数执行。中断相应函数的名字是不能随便定义的,一般我们都遵循 MDK定义的函数名。在启动文件 startup_stm32f10x_hd.s 文件中可以找到。

if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)

判断是否接受中断,如果是串口接受中断,则读取串口接受到的数据:

Usart1Data =USART_ReceiveData(USART1);//(USART1->DR); //读取接收到的数据

后面就可以根据自己接收需求制定收发协议。

void USART1_IRQHandler(void) //串口 1 中断服务程序
{
    u8 Usart1Data;

    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) 
        //接收中断(接收到的数据必须是 0x0d 0x0a 结尾)
    {
        Usart1Data =USART_ReceiveData(USART1);//(USART1->DR); //读取接收到的数据
        // 下面根据接收需求制定接收协议
    }
}

==通过寄存器数据发送与接收==

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);

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

==串口状态==串口的状态可以通过状态寄存器 USART_SR 读取

  • RXNE(读数据寄存器非空),当该位被置 1 的时候,就是提示已经有数据被接收到了,并且可以读出来了。这时候我们要做的就是尽快去读取 USART_DR,通过读 USART_DR 可以将该位清零,也可以向该位写 0,直接清除。

  • TC(发送完成),当该位被置位的时候,表示 USART_DR 内的数据已经被发送完成了。如果设置了这个位的中断,则会产生中断。该位也有两种清零方式:

    • 1、读 USART_SR,写USART_DR。

    • 2、直接向该位写 0。

==读取串口状态的函数==

FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG);

==要判断读寄存器是否非空(RXNE)==

USART_GetFlagStatus(USART1, USART_FLAG_RXNE);

==判断发送是否完成(TC)==

USART_GetFlagStatus(USART1, USART_FLAG_TC);
//这些标识号在 MDK 里面是通过宏定义定义的:
#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)

==串口使能==串口使能是通过函数 USART_Cmd()来实现

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

==开启串口响应中断==使能串口中断的函数

void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, 
FunctionalState NewState)

详细可以请参考《STM32 参考手册》第 516 页至 548 页!!!!!!!!!

2.7 串口初始化代码

void uart_init(u32 bound){
    //GPIO 端口设置
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA
                           , ENABLE); //使能 USART1,GPIOA 时钟
    //USART1_TX PA.9
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9 复用推挽输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
    GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.0 发送端

    //USART1_RX PA.10 浮空输入
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
    GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.10 接收端
    //Usart1 NVIC 中断配置 配置
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //对应中断通道
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级 3
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级 3
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道使能
    NVIC_Init(&NVIC_InitStructure); //中断优先级配置

    //USART 初始化设置
    USART_InitStructure.USART_BaudRate = bound;//波特率设置;
    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_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启中断
    USART_Cmd(USART1, ENABLE); //使能串口
}

因为蓝牙使用串口操作,我们可以通过蓝牙去测试串口收发情况,下一个文章就是进行蓝牙的收发实验。

DMA连续通信

串口DMA发送

使用串口DMA方式将SRAM的数据通过串口发送出发

#define USART1_DR_ADDR (USART1_BASE+4)
#define BUFSIZE 28

char tx_buf[BUFSIZE] = {\
    'a','b','c','d','e',\
    'f','g','h','i','j',\
    'k','l','m','n','o',\
    'p','q','r','s','t',\
    'u','v','w','x','y',\
    'z','z','z'};

void uart_dma_init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;
    USART_InitTypeDef USART_InitStruct;

    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA | \
                           RCC_APB2Periph_AFIO, ENABLE);

    //GPIO Init(PA9‐>TX,PA10‐>RX)
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    //usart init
    USART_InitStruct.USART_BaudRate = 115200;
    USART_InitStruct.USART_HardwareFlowControl = \
        USART_HardwareFlowControl_None;
    USART_InitStruct.USART_Parity = USART_Parity_No;
    USART_InitStruct.USART_StopBits = USART_StopBits_1;
    USART_InitStruct.USART_WordLength = USART_WordLength_8b;
    USART_InitStruct.USART_Mode = USART_Mode_Tx|USART_Mode_Rx;
    USART_Init(USART1, &USART_InitStruct);

    //DMA init
    DMA_InitStruct.DMA_PeripheralBaseAddr = USART1_DR_ADDR;
    DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)tx_buf;
    DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralDST;
    DMA_InitStruct.DMA_BufferSize = BUFSIZE;
    DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
    DMA_InitStruct.DMA_Priority = DMA_Priority_High;
    DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;
    DMA_Init(DMA1_Channel4, &DMA_InitStruct); //usart1_tx

    USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
    USART_Cmd(USART1, ENABLE);
    DMA_Cmd(DMA1_Channel4, ENABLE);

}