USART串口通信

一、串口物理层结构

1.1 RS232

USART串口通信-1

RS232标准串口一般用于工业控制领域,在使用该标准时,数据传输使用该逻辑电平,在发送或接收到此电平时,会利用电平转换芯片将232电平转换为标准的TTL电平,最后传入到控制器当中处理。

1.2 原生的串口到串口通信(TTL to TTL)

USART串口通信-2

两个设备之间直接使用TTL逻辑电平进行通信,不需要经过电平转换芯片转换电平,主要是控制器和串口设备或者传感器通信。

1.3 USB转TTL通信

USART串口通信-3

单片机与电脑之间进行通信就需要USB转串口。

USB接口电源VCC是5V,一般而言为USB设备提供的最大电流为500mA。

其中D+和D-两根数据线在+400mV~-400mV之间变化,数据线传输的信号为差分信号:

  • 传输高电平时:D+为高电平,D-为低电平
  • 传输低电平时:D+为低电平,D-为高电平

二、STM32串口数据处理

2.1串口通信当中的数据处理

(图源自STM32F10x中文编程手册)

USART串口通信-4

以发送字符“A”为例:

当要发送数据时,将字符“A”发送到发送数据寄存器(该寄存器只能写不能读),再将发送数据寄存器当中的"A"放入发送移位寄存器当中。通过添加起始位停止位等数据构成一个完整的数据帧,最后通过引脚将数据发送出去。当“A”送到移位寄存器时,发送数据寄存器没有数据,就可以将接下来的字符“B”放入到发送寄存器,以此类推就可以连续发送了,数据接收同理。

硬件数据“流控制”:

数据在两个串口之间传输常常会出现数据丢失的现象,或者两台计算机之间的处理速度不同时,接收端的缓冲区已满,则此时继续发送来的数据就会丢失。

当接收端的数据处理不过来时,就可以发出“不再接收”的信号,此时发送端就停止发送,直到接收端发出“可继续发送”信号时再发送数据。因此流控制可以控制两个串口之间的数据传输进程。PC中常用的两种流控制有硬件流控制(RTS/CTS、DTR/CTS)和软件流控制(xon/xoff(继续/停止))

2.2串口数据帧的基本组成

USART串口通信-5

起始位:由1个逻辑0的数据位表示 有效数据位:8位 校验位:(可选)为的是数据的抗干扰性 停止位:由0.5、1、1.5或2个逻辑1的数据位表示

三、STM32串口编程

USART串口通信-6

3.1 USART的代码实现:

1、在STM32F103C8T6上,USART1的GPIO管脚是在PA9和PA10上,可以从开发板的原理图可以找到:

USART串口通信-7

2、USART1的时钟线也是在APB2,和GPIO在一条时钟总线上。(中文编程手册)

USART串口通信-8

3、对串口数据收发的两个GPIO口初始化,可通过中文参考手册,设置对应的模式:

USART串口通信-9

4、调用USART_Init();函数对串口进行初始化。

5、如果是主动发送数据的情况就不需要使用中断,如果是在接收到数据的情况下,就可以使用中断来处理受到数据的情况或处理接收到的数据

6、调用USART_Cmd();函数使能串口

7、清除发送完成标志位(TXE发送数据寄存器空/TC发送完成) USART发送端有两个寄存器,分别是USART_DR寄存器,另一个是移位寄存器。对应到USART数据发送有

两个标志:TXE发送数据寄存器空/TC发送完成

USART串口通信-10

TC即Transmission Complete,发送完成。每发送完一个字节,该标志位会自动为1,如果有开中断的话,当这个标志位为1就会进入中断,也可以通过查询该标志位来查询是否发送完成。

所以当发送下一个字节时,就需要将这个标志位清零。单片机复位后,该标志位为1,所以需要手动清零,避免还没发送数据单片机就误以为发完,这样就有可能丢失数据。

调用USART_SendData();函数发送数据,并判断TC标志位是否为1来判断数据是否发送完成

(软件复位:先读取串口的状态寄存器,再写入数据寄存器)

USART串口通信-11

根据程序理解:

发送完数据后读取标志位(读取状态寄存器),在下一次调用函数时,再次发送数据(写入数据寄存器)如此往复,这样单片机就会自动将TC清零。

USART串口通信-12

#include "Usart.h"
void Usart_Init() {
    GPIO_InitTypeDef GPIO_InitStruct_PA9;
    GPIO_InitTypeDef GPIO_InitStruct_PA10;

    //打开GPIO和串口时钟
    //有些情况下使用复用功能还需要打开复用时钟(RCC_APB2Periph_AFIO):
    //AFIO的事件控制寄存器、AFIO的重映射功能以及外部中断(EXTI)控制寄存器
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);

    //设置PA9,串口的发送引脚
    GPIO_InitStruct_PA9.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStruct_PA9.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStruct_PA9.GPIO_Speed = GPIO_Speed_50MHz;

    //设置PA10,串口的接收引脚
    GPIO_InitStruct_PA10.GPIO_Mode = GPIO_Mode_IPU; //带上拉输入或浮空输入 
    GPIO_InitStruct_PA10.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStruct_PA10.GPIO_Speed = GPIO_Speed_50MHz;

    //GPIO初始化
    GPIO_Init(GPIOA, &GPIO_InitStruct_PA9);
    GPIO_Init(GPIOA, &GPIO_InitStruct_PA10);

    USART_InitTypeDef SART_InitStruct;
    SART_InitStruct.USART_BaudRate = 115200; //一般取1200整数倍
    SART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //设置硬件流控制
    SART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //同时开启发送和接收
    SART_InitStruct.USART_Parity = USART_Parity_No; //设置串口数据校验位
    SART_InitStruct.USART_StopBits = USART_StopBits_1; //设置停止位
    SART_InitStruct.USART_WordLength = USART_WordLength_8b; //设置串口数据的字长

    //USART1串口初始化
    USART_Init(USART1, &SART_InitStruct);

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

    //清除标志位
    USART_ClearFlag(USART1, USART_FLAG_TC);
}

//发送数据函数
void USART_SentByte(USART_TypeDef* USARTx, u8 Data) {
    //发送串口数据
    USART_SendData(USARTx,  (u16)Data);

    //获取标志位判断数据是否发送完成
    while(USART_GetFlagStatus(USARTx, USART_FLAG_TC) == 0);
}

//发送字符串函数
void USART_SentString(USART_TypeDef* USARTx, u8 *Str) {
    u32 pos = 0;
    while(*(Str+pos) != '\0') {
        USART_SentByte(USARTx, *(Str+pos));
        pos++;
    }
}

四、STM32串口接收

串口接收需要用到中断来处理受到数据的情况或处理接收到的数据,因此需要做中断的配置。

4.1 设置中断分组

使用中断需要对中断进行分组,因此需要使用中断分组函数,在设置完中断分组后,所有中断都会依照设置的分组来运行。

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

不同的分组其子优先级也不同,中断的优先级一共用4位二进制数来表示,4位二进制数一共有0-15种表示方式,将这16种表示方式分为0-4共5组。

USART串口通信-13

  • 主优先级也称为抢占优先级,子优先级也称为次占优先级(响应优先级)
  • 分组值决定他们的取值范围,取值越小级别越高
  • 优先级分组决定中断的取值范围,取值越小级别越高
  • 主优先级高(值越小级别越高)的中断可以打断低优先级的中断
  • 相同优先级中断不能相互打断,这时候才考虑子优先级,子优先级高的中断优先响应

总结:先用优先级分组为中断们分配中断数量,主优先级相同时不能相互打断,主优先级相同的中断同时请求时,用子优先级判断。

4.2 调用函数设置串口的中断优先级

NVIC_InitTypeDef NVIC_InitStruct;

NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;  //中断通道号没有在函数定义中,可以在stm32f10x.h中找到,搜索“IRQn”,查找串口的中断通道号
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2;

//中断优先级的初始化
NVIC_Init(&NVIC_InitStruct);

4.3 打开串口中断

//打开串口中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

这里串口有很多中断,函数的第二个参数是选择打开串口中断类型,我们选择的是USART_IT_RXNE,即串口接收中断。

4.4 设置串口中断处理函数

#define size 256
u8 USART1_Word[size];
u16 pos = 0;

//该函数在启动文件当中查找,负责接收到数据后进入中断并存储数据
void USART1_IRQHandler(void) {
    u8 Rdata;
    //检查中断标志位以判断是否进入到串口接收中断
    if(USART_GetITStatus(USART1, USART_IT_RXNE) == 1) {
        Rdata = (u8)USART_ReceiveData(USART1);
        USART1_Word[pos++] = Rdata;
        //清除中断标志位
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    }
}

中断的处理函数都是固件库内有定义好的,可以直接到单片机的启动文件当中查询。

这里判断串口接收标志位来判断是否真的有进入中断,然后再用串口接收数据函数来处理接收的数据,最后用字符串数组来存储数据。

五、串口字符串收发

通过接收时间的间隔长短来判断字符串是否接收完成。

USART串口通信-14

一个字符占八个位,当串口在发送一个字符时,除了发送字符数据,还需要加上起始位和停止位来构成一个完整的数据帧,8+2=10个位

除以设置的115200可以得到发送这一个字符所需要的时间

由此我们可以设置一个10ms以内的定时器,由计算可知接收一个字符的时间是0.087ms,如果在等待了10ms还没收到数据的话,就可以说明这整个字符串已经全部接收完成。

在中断处理函数中实现接收数据后将该数据重新发送出去:

//设置中断处理函数,在启动文件中查找
void USART1_IRQHandler() {
    u16 Rdata;
    //检查中断标志位以判断是否进入中断
    if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET) {
        Rdata = USART_ReceiveData(USART1); //接收收到的数据
        UsartSendData(USART1, Rdata);
        //清除中断标志位
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    }
}

六、使用printf函数发送字符串

需要引入“stdio.h”头文件,并添加一下函数:

int fputc(int ch,FILE *f)
{
    USART_SendData(USART1,(u16)ch);
    while(USART_GetFlagStatus(USART1,USART_FLAG_TC)==RESET);
    return (ch);
}

七、串口功能的重映射

如果USART0对应的GPIO端口被占用作其他功能用,那么需要调用重映射函数将USART0的功能转移到其他的GPIO端口上

使用重映射功能还需要开启AFIO时钟

//串口重映射
GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE);

//设置PB7,串口的发送引脚,全双工:推挽复用模式
GPIO_InitStruct_PA9.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct_PA9.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStruct_PA9.GPIO_Speed = GPIO_Speed_50MHz;

//设置PB8,串口的接收引脚,全双工:浮空输入或上拉输入
GPIO_InitStruct_PA10.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStruct_PA10.GPIO_Pin = GPIO_Pin_7;