引言

在嵌入式开发的广阔天地里,STM32 单片机凭借其强大的性能与丰富的外设,成为众多开发者的得力助手。而串口通信(USART),作为 STM32 与外界沟通的关键桥梁,承载着数据传输的重要使命。无论是与 PC 端的交互调试,还是与各类传感器、执行器的协同工作,串口通信都发挥着不可或缺的作用。

想象一下,智能家居系统中,STM32 如何接收手机 APP 发送的指令来控制家电设备?工业自动化场景里,传感器采集的数据又怎样准确无误地传递给 STM32 进行分析处理?这背后,串口通信如同一位默默耕耘的信使,确保信息的顺畅流转。今天,就让我们一同深入探索串口通信的奥秘,解锁 STM32 与外界高效对话的密码。

常见的几种通信接口

连接对象 通信接口 功能简述
LED 灯 / 蜂鸣器 GPIO 通过通用输入输出接口,处理器可控制 LED 灯的亮灭、蜂鸣器的发声等操作 ,实现简单的信号指示或报警功能。
BT 蓝牙 / GPS UART 利用通用异步收发传输器接口,处理器与蓝牙模块通信可实现无线数据传输、设备配对等功能;与 GPS 模块通信可获取位置、时间等信息 。
加速度传感器 I2C 总线 借助 I2C(Inter-Integrated Circuit)总线,处理器可从加速度传感器获取物体的加速度数据,用于运动检测、姿态识别等场景 。
DS18B20 温度传感器 1-Wired 一线式总线 通过 1-Wired 一线式总线,处理器能够读取 DS18B20 温度传感器测量的温度数据,实现温度监测功能 。
WIFI/Norflash SPI 总线 利用 SPI(Serial Peripheral Interface)总线,处理器与 WIFI 模块通信可实现无线网络连接、数据收发;与 Norflash 通信可进行数据的存储和读取 。

串口定义

通用串行异步收发器

  • 通用:UART 在工控、电力等诸多领域广泛应用,凭借其简单易用和兼容性强的特点,成为嵌入式系统中数据通信的常用选择。

  • 串行:与并行通信不同,UART 通信时处理器与外设仅需一根信号线相连,数据按位依次传输,且从低位开始。并行通信虽传输速度快,但需多根数据线,抗干扰能力弱、传输距离受限,相比之下,串行通信在抗干扰和长距离传输方面更具优势。

  • 异步:在计算机系统中,处理器与外设的数据处理速度差异显著,数据同步至关重要。异步通信通过特定协议实现数据传输,无需额外时钟信号线,而同步通信则需借助时钟信号线来协调双方数据传输节奏,以确保数据准确无误。

  • 收发器:收发器作为数据发送与接收的硬件单元,在数据传输过程中,根据角色不同,处理器和外设可分别作为发送器或接收器。

UART 数据传输的协议

协议要素详解

  • 空闲位:当处理器与外设不进行数据传输时,数据线上维持高电平的空闲位,为后续数据传输做好准备。

  • 起始位:数据传输开始时,以低电平的起始位作为信号,标志着数据帧的开端。

  • 数据位:代表实际传输的数据内容,可设置为 5 - 8 位,一般选用 8 位,通信双方的数据位长度需保持一致。

  • 奇偶校验位:用于检测数据传输过程中是否出现错误,有奇校验、偶校验和无校验三种方式,双方校验方式必须相同。

  • 停止位:数据传输结束时,发送高电平的停止位,停止位位数可为 1 位或 2 位,双方的停止位设置需一致。

  • 波特率:作为衡量数据传输速率的关键参数,常见的波特率有 115200bps、9600bps 等,通信双方的波特率必须匹配,否则将导致数据传输错误。

协议流程示例

以处理器通过 UART 向 BT 发送数据 0x95(10010101)为例,UART 工作参数为 115200(波特率)、8(数据位数)、e(偶校验)、1(停止位位数),即 115200 8e1。其时序图如下:

数据传输协议流程为:空闲位 -> 起始位 -> 数据位 -> (校验位) -> 停止位 -> 空闲位。若要传输多个字节数据,流程则为:空闲位 -> 起始位 -> 低字节的 8 位数据 -> (校验位) -> 停止位 -> 起始位 -> 高字节的 8 位数据 -> (奇偶校验) -> 停止位... -> 空闲位。

UART 的三种工作方式

  • 单工:数据传输仅能朝一个固定方向进行,适用于数据流向单一的场景。

  • 半双工:数据可双向传输,但同一时刻只能进行单向传输,常见于一些对通信实时性要求不高的设备通信中。

  • 全双工:允许数据同时双向传输,STM32 的 UART 通常工作在全双工模式,能高效实现数据的双向交互,满足大多数应用场景的需求。

硬件设计

硬件结构示意图

UART 控制器工作原理

  • 发送数据流程:由于 CPU 核向数据寄存器 DR 写入数据的速度远快于发送移位寄存器将数据发送到 TX 引脚的速度,因此发送数据时,需先判断 TC 位(发送完成标志位),当 TC 为 1 表示发送数据寄存器为空,CPU 方可发送下一个数据,否则需轮询等待。若 TC 为 1,CPU 将数据写入数据寄存器 DR,随后数据自动拷贝至发送移位器,发送移位器按波特率和 UART 协议将数据发送到 TX 引脚上。

  • 接收数据流程:接收移位器依据波特率和 UART 协议从 RX 引脚上接收数据,接收完成后数据自动拷贝至数据寄存器 DR。当 RXNE 位(接收数据寄存器非空标志位)为 1 时,表明数据寄存器 DR 中有数据,CPU 核可从中读取数据,否则 CPU 核需轮询等待。

简化硬件示意图

代码演示

串口发送功能代码

//usart.c
#include "usart.h"
​
void UART_Init(void){
    //1.打开GPIOA/USART1控制器时钟 - APB2
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
    //2.配置PA9 - 推挽复用输出,50MHz
    GPIO_InitTypeDef GPIO_Config;
    GPIO_Config.GPIO_Pin=GPIO_Pin_9;
    GPIO_Config.GPIO_Speed=GPIO_Speed_50MHz;
    GPIO_Config.GPIO_Mode=GPIO_Mode_AF_PP;
    GPIO_Init(GPIOA,&GPIO_Config);
    //3.配置PA10 - 浮空输入
    GPIO_Config.GPIO_Pin=GPIO_Pin_10;
    GPIO_Config.GPIO_Mode=GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA,&GPIO_Config);
    //4.配置工作参数:115200 8n1
    USART_InitTypeDef UART_Config;
    UART_Config.USART_BaudRate=115200;
    UART_Config.USART_WordLength=USART_WordLength_8b;
    UART_Config.USART_Parity=USART_Parity_No;
    UART_Config.USART_StopBits=USART_StopBits_1;
    UART_Config.USART_Mode=USART_Mode_Rx|USART_Mode_Tx;
    UART_Config.USART_HardwareFlowControl=USART_HardwareFlowControl_None;
    USART_Init(USART1,&UART_Config);
    //5.使能串口
    USART_Cmd(USART1,ENABLE);
}
​
void UART_Putc(char c){
    //循环的判断
    //TC,SET,可以发送;RESET,不能发送
    while(USART_GetFlagStatus(USART1,USART_FLAG_TC)==RESET);
    //将字符c放入到串口1的DR中
    USART_SendData(USART1,c);
}
​
void UART_Puts(char* pstr){
    while(*pstr){
        UART_Putc(*pstr);
        pstr++;
    }
}
// main.c
#include "stm32f10x.h"
#include "led.h"
#include "beep.h"
#include "system.h"
#include "systick.h"
#include "key.h"
#include "exti.h"
#include "usart.h"
​
int main(void) {
​
    //优先级分组 --|--
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
​
    LED_Init();  // led初始化
    BEEP_Init(); // beep初始化
    Systick_Init();//systick定时器初始化
    KEY_Init(); //按键初始化
    My_EXTI_Init();//EXTI初始化
    UART_Init();//串口初始化
​
    while(1) {
        UART_Puts("my heart will go on\n");
        delay_ms(1000);
    }
}

中断方式串口接收代码

//usart.c
#include "usart.h"
​
void UART_Init(void){
    //1.打开GPIOA/USART1控制器时钟 - APB2
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
    //2.配置PA9 - 推挽复用输出,50MHz
    GPIO_InitTypeDef GPIO_Config;
    GPIO_Config.GPIO_Pin=GPIO_Pin_9;
    GPIO_Config.GPIO_Speed=GPIO_Speed_50MHz;
    GPIO_Config.GPIO_Mode=GPIO_Mode_AF_PP;
    GPIO_Init(GPIOA,&GPIO_Config);
    //3.配置PA10 - 浮空输入
    GPIO_Config.GPIO_Pin=GPIO_Pin_10;
    GPIO_Config.GPIO_Mode=GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA,&GPIO_Config);
    //4.配置工作参数:115200 8n1
    USART_InitTypeDef UART_Config;
    UART_Config.USART_BaudRate=115200;
    UART_Config.USART_WordLength=USART_WordLength_8b;
    UART_Config.USART_Parity=USART_Parity_No;
    UART_Config.USART_StopBits=USART_StopBits_1;
    UART_Config.USART_Mode=USART_Mode_Rx|USART_Mode_Tx;
    UART_Config.USART_HardwareFlowControl=USART_HardwareFlowControl_None;
    USART_Init(USART1,&UART_Config);
    //5.使能串口
    USART_Cmd(USART1,ENABLE);
​
    //6.配置NVIC支持串口1的中断
    NVIC_InitTypeDef NVIC_Config;
    NVIC_Config.NVIC_IRQChannel=USART1_IRQn;//串口1的中断通道
    NVIC_Config.NVIC_IRQChannelPreemptionPriority=0;
    NVIC_Config.NVIC_IRQChannelSubPriority=2;
    NVIC_Config.NVIC_IRQChannelCmd=ENABLE;
    NVIC_Init(&NVIC_Config);
​
    //7.配置USART控制器支持RXNE中断
    USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
}
​
void UART_Putc(char c){
    //循环的判断
    //TC,SET,可以发送;RESET,不能发送
    while(USART_GetFlagStatus(USART1,USART_FLAG_TC)==RESET);
    //将字符c放入到串口1的DR中
    USART_SendData(USART1,c);
}
​
void UART_Puts(char* pstr){
    while(*pstr){
        UART_Putc(*pstr);
        pstr++;
    }
}
​
char UART_Getc(void){
    // RXNE
    //RESET,DR没有数据,陷入循环;SET,有数据,跳出循环
    while(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)==RESET);
​
    //从串口1中获取一个字符
    return (char)USART_ReceiveData(USART1);
}
//char buf[len];
//上位机 - "xxxx\r\n" -> "xxxx"
//"hello\r\n" -> "hello\0\n" -> "hello"
void UART_Gets(char* buf,u32 len){
    int i=0;
    for(i=0;i<(len-1);i++){
        buf[i]=UART_Getc();
        if(buf[i]=='\n')
            break;
    }
    buf[i-1]='\0'; 
}
//interrupt.c
#include "interrupt.h"
//声明串口1的接收数组
char UART_RxBuff[UART_RXBUFF_SIZE];
​
//用于计数/下标
int UART_RxCounter=0;
​
//声明变量表示数据是否读取结束
int UART_Send_Flag=0;
​
//串口1的中断处理函数
void USART1_IRQHandler(void){
    char c;
    //判断是否是RXNE中断
    if(USART_GetITStatus(USART1,USART_IT_RXNE)!=RESET){ 
        //清除中断到来位
        USART_ClearITPendingBit(USART1,USART_IT_RXNE);
        //从DR寄存器中读取数据
        c=USART_ReceiveData(USART1);
        UART_RxBuff[UART_RxCounter++]=c;
        if('\n'==c){//读取结束
            UART_RxBuff[UART_RxCounter-2]='\0';
            //将UART_RxCounter清0
            UART_RxCounter=0;
            //数据读取结束
            UART_Send_Flag=1;
        }
    }
}
// main.c
#include "stm32f10x.h"
#include "led.h"
#include "beep.h"
#include "system.h"
#include "systick.h"
#include "key.h"
#include "exti.h"
#include "usart.h"
#include "string.h"//strcmp函数
#include "init.h"
#include "cmd.h"
#include "interrupt.h"
​
static cmd_t* pcmd;//定义变量存储find_cmd函数返回值
​
int main(void) {
​
    //优先级分组 --|--
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
​
    //总初始化函数
    DEVICE_Init();
​
    while(1) {
        if(UART_Send_Flag){
            UART_Puts("received command\n");
            //buf="led on"/"led off"/"beep on"/"beep off"
            //命令匹配
            pcmd=find_cmd(UART_RxBuff);
            if(pcmd != NULL)//匹配成功
                pcmd->callback();
            else
                UART_Puts("invalid command");
            UART_Puts("\n");
            UART_Send_Flag=0;//读取完成标志为0
        }
        //业务逻辑
    }
}

总结

通过本文,我们从串口的定义出发,详细剖析了 UART 数据传输协议、工作方式以及硬件设计原理,并通过代码演示展示了串口发送及中断方式接收数据的具体实现。串口通信作为 STM32 与外部设备交互的基础方式,其稳定性和高效性在嵌入式系统开发中至关重要。

在实际应用中,合理配置串口参数、优化数据收发逻辑,能确保数据准确无误地传输,为系统的稳定运行提供有力保障。同时,结合中断机制,可进一步提高系统的实时响应能力,满足多样化的应用需求。

最后

作为技术分享者,我始终希望用通俗易懂的语言和详细的代码示例,为大家呈现技术知识的精髓。但由于技术的复杂性和个人知识的局限性,文中可能存在不足或疏漏之处。真诚期待大家在评论区提出宝贵意见和建议,无论是对内容的疑问,还是对代码优化的见解,都欢迎分享。让我们携手在技术学习的道路上不断探索,共同进步!

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐