USART教程(轮询和中断实现)

79 阅读10分钟

一、USART基础概念与区别

1. 什么是usart?

  • 全称:Universal Synchronous/Asynchronous Receiver/Transmitter,通用同步/异步接发器。
  • 定义:它是微控制器(MCU)内部的一个外设模块,用于串行通行。
  • 关键点:它既支持同步通信(带时钟线),也支持异步通信(不带时钟线)。

2. USART 与 UART 的区别

特性UARTUSART
同步方式仅支持异步支持同步和异步
时钟线无(依靠波特率约定)同步模式下需要时钟线
应用场景调试打印、GPS模块、蓝牙模块驱动SPI设备、SmartCard、高可靠性通信
包含关系USART 包含了 UART的功能USART是UART的超集

二、 物理层

1. 引脚连接

  • TX (Transmit) :发送端
  • RX(Receive):接收端
  • GND:共地(必须连接,否则电平无参考,无法通信)
  • 连接法则:交叉连接(TX连RX,RX连TX)

2. 电平标准

MCU出来的TTL电平不能直接接电脑的USB,也不能直接远距离传输,需要转换。

  • TTL电平:

    • 逻辑1:VCC(3.3V或5V)
    • 逻辑0:GND(0V)
    • 特点:抗干扰差,距离短,MCU直接使用。
  • RS-232电平:

    • 逻辑1:-3V~-15V(负电压)
    • 逻辑0:+3V~+15V(正电压)、
    • 特点:负逻辑,抗干扰强于TTL,用于工业设备,需使用 MAX232芯片进行转换。
  • RS-485(虽然物理层不同,常与UART协议配合):

    • 差分信号(A-B电压差)
    • 特点:抗干扰极强,距离可达1200米,半双工

三、协议层(数据帧格式)

UART通信不需要时钟线,依靠约定的波特率和帧格式来解析数据。

数据帧组成:

空闲状态下通常为高电平。

  1. 起始位:1位,拉低电平,标志传输开始。

  2. 数据位:5~9位(通常是8位)。低位在前

  3. 校验位:可选。

    • 奇校验:保证数据位+校验位中的“1“的个数为奇数。
    • 偶校验:保证”1“的个数为偶数。
    • 无校验:最常用。
  4. 停止位:1位、1.5位或2位。拉高电平,标志结束。

典型配置:115200,8,N,1(波特率115200,8位数据位,无校验,1停止位)。

四、嵌入式软件设计(核心)

在嵌入式开发中,如何高效接收数据很重要。

1. 三种接收方式

1. 轮询:
  • 原理:while(!Flag);Read();死等数据
  • 缺点:浪费CPU,容易丢数据,工程中几乎不用。
2. 中断:
  • 原理:收到一个字节触发一次中断
  • 缺点:如果波特率很高(如921600),中断频率太高,频繁压栈出栈会把CPU累死。
3. DMA + 空闲中断:
  • 原理:硬件自动把数据搬到内存,只有在一帧数据发完(总线空闲)时,才触发一次中断通知CPU处理。
  • 优点:极大释放CPU资源。

五、代码实现

轮询代码:
#include "usart1.h"#define USART1_GPIO_PORT    GPIOA
#define USART1_TX_GPIO_PIN  GPIO_Pin_9
#define USART1_RX_GPIO_PIN  GPIO_Pin_10
​
​
void usart1_init(uint32_t bound) {
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
    
    GPIO_InitTypeDef GPIO_InitStructure;
    
    // PA9 -> TX (复用推挽输出)
    GPIO_InitStructure.GPIO_Pin     =   USART1_TX_GPIO_PIN;
    GPIO_InitStructure.GPIO_Speed   =   GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode    =   GPIO_Mode_AF_PP;
    GPIO_Init(USART1_GPIO_PORT, &GPIO_InitStructure);
    
    // PA10 -> RX(浮空输入)
    GPIO_InitStructure.GPIO_Pin     =   USART1_RX_GPIO_PIN;
    GPIO_InitStructure.GPIO_Mode    =   GPIO_Mode_IN_FLOATING;
    GPIO_Init(USART1_GPIO_PORT, &GPIO_InitStructure);
    
    // USART配置
    USART_InitTypeDef USART_InitStructure;
    
    USART_InitStructure.USART_BaudRate      =   bound;                  //  波特率
    USART_InitStructure.USART_WordLength    =   USART_WordLength_8b;    //  8位数据
    USART_InitStructure.USART_StopBits      =   USART_StopBits_1;       //  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_Cmd(USART1, ENABLE);
    
}
​
// =======================================
//         接口函数实现(轮询模式)
// =======================================/**
 * @brief 发送一个字节
 * @param byte:要发送的数据
 * @note  重点:这里要检查 TXE (Transmit Data Register Empty) 标志位
 *        TXE置位表示数据寄存器空,可以写入下一个数据,但上一帧可能还在移位寄存器发送中。
          这样效率比检查TC (Transmission Complete)更高。
 */
void usart1_send_byte(uint8_t byte) {
    // 发送数据
    USART_SendData(USART1, byte);
    
    // 等待发送数据寄存器为空(TXE = 1)
    // 如果不等待直接发下一个,会直接覆盖当前数据
    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
​
/**
 * @brief 发送字符串
 * @param str:字符串指针
 * @note  利用循环调用单字节发送函数,直到遇到字符串结束符
 */
void usart1_send_string(char* str) {
    while (*str != '\0') {
        usart1_send_byte(*str);
        str++;
    }
}
​
/**
 * @brief 发送指定长度的数组(用于发送Hex数据/通信协议包)
 * @param arr:数组指针
 * @param len:数据长度
 */
void usart1_send_array(uint8_t *arr, uint16_t len) {
    uint16_t i;
    for (i = 0; i < len; i++) {
        usart1_send_byte(arr[i]);
    }
}
​
/**
 * @brief  接收一个字节(死等模式)
 * @return 接收到的字节
 * @note   缺点:如果没有数据发送过来,程序就会卡死在这里,blocking。
 */
uint8_t usart1_receive_byte(void) {
    // 等待接收数据寄存器非空 (RXNE = 1)
    while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
    
    // 读取数据 (读取操作会自动清除 RXNE 标志)
    return (uint8_t)USART_ReceiveData(USART1);
}
​
/**
 * @brief  接收一个字节
 * @param  data:接收数据存放地址
 * @param  timeout:超时计数值(非标准时间,取决于CPU主频)
 * @return 0:成功,1:超时失败
 * @note   在实际工程中,不能让程序永远卡死在while死循环中,必须要有超时机制推出
 */
uint8_t usart1_receive_byte_with_timeout(uint8_t *data, uint32_t timeout) {
    // 轮询标志位
    while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET) {
        timeout--;
        if (timeout == 0) {
            return 1;
        }
    }
    
    *data = (uint8_t)USART_ReceiveData(USART1);
    return 0;
}
​
// 重定向fputc函数,让printf输出到串口
// 注意:需要在keil的Target设置中勾选 “Use MicroLIB”
int fputc(int ch, FILE *f) {
    // 发送一个字节
    USART_SendData(USART1, (uint8_t) ch);
    // 等待发送完成(TC = Transmission Complete)
    while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
    return ch;
}
​
#include "stm32f10x.h"
#include "usart1.h"int main(void)
{
    systick_init();
    bsp_led_init();
    
    uint8_t rxData;
    
    usart1_init(115200);
    
    printf("STM32 USART1 Polling Test Start...\r\n");
    
    usart1_send_string("Please send a byte to me.\r\n");
    
    while(1) {
        if (usart1_receive_byte_with_timeout(&rxData, 100000) == 0) {
            printf("Recv: %c (Hex: 0x%02X)\r\n", rxData, rxData);
        } else {
        
        }
    }
}

我这里简单进行一个回显打印,通过串口软件发送数据后,重新发送回去,这里就需要不停的轮询,来判断有没有数据过来,非常简单,但是非常不好用。

[14:30:18.430]发→◇A[14:30:18.434]收←◆Recv: A (Hex: 0x41)

中断代码

核心思路
  1. 接收(RX):使用中断模式

    • 开启RXNE中断,读取数据寄存器非空。
    • 数据到达时,触发中断,CPU跳入 USART2_IRQHandler。
    • 在中断里把数据存入一个环形缓冲区,并判断是否接收到了结束符(如\n)、
  2. 发送(TX):使用阻塞方式(或重定向printf)。

    • 发送通常是主动行为,且数据量不大时,轮询等待发送完成(TC或者TXE标志)最简单稳定。
#include "usart2.h"
#include "ring_buffer.h"#define BUF_SIZE    16
​
uint8_t usart2_buffer[BUF_SIZE];
​
ring_buffer_t usart2_rb;
​
void usart2_init(uint32_t bound) {
    
    rb_init(&usart2_rb, usart2_buffer, BUF_SIZE);
    
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2 , ENABLE); 
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    GPIO_InitTypeDef GPIO_InitStructure;
    
    // TX
    GPIO_InitStructure.GPIO_Pin = USART2_TX_GPIO_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(USART2_GPIO_PORT, &GPIO_InitStructure);
    
    // RX
    GPIO_InitStructure.GPIO_Pin = USART2_RX_GPIO_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(USART2_GPIO_PORT, &GPIO_InitStructure);
    
    // 初始化usart2
    USART_InitTypeDef USART2_InitStructure;
    
    USART2_InitStructure.USART_BaudRate = bound;
    USART2_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART2_InitStructure.USART_StopBits = USART_StopBits_1;
    USART2_InitStructure.USART_Parity = USART_Parity_No;
    USART2_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART2_InitStructure.USART_Mode = USART_Mode_Rx|USART_Mode_Tx;
    
    USART_Init(USART2, &USART2_InitStructure);
    
    
    USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);
    
    NVIC_InitTypeDef NVIC_InitStructure;
    
    // 配置USASR1为中断源
    NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
    // 抢占优先级
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    // 子优先级
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    // 使能中断
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    
    NVIC_Init(&NVIC_InitStructure);
    
    USART_Cmd(USART2, ENABLE);
}
​
void usart2_send_byte(uint8_t byte) {
    USART_SendData(USART2, byte);
​
    while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET);   
}
​
void usart2_send_string(char *str) {
    while (*str != '\0') {
        usart2_send_byte(*str);
        str++;
    }
}
​
uint8_t usart2_read_byte_from_buffer(uint8_t *pData) {
    if (rb_get(&usart2_rb, pData) == RB_SUCCESS) {
        return 1;
    }
    return 0;
}
    
// -------------------------------------------------------
//                  中断服务函数(重点)
// -------------------------------------------------------
void USART2_IRQHandler(void) {
    // 1. 检查是否接收中断(RXNE)
    if (USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) {
        
        // 读取数据(读取数据会自动清楚RXNE标志)
        uint8_t data = (uint8_t)USART_ReceiveData(USART2);
        
        // 2. 判断缓冲区是否已经满了
        if (!rb_is_full(&usart2_rb)) {
            rb_put(&usart2_rb, data);
        } else {
            // 缓冲区满了,丢弃数据
            // 可以在这里做一个全局计数器 g_rx_overflow_count++ 用于调试
        }
        
    }
    
    // 关键
    // 如果CPU处理过慢,导致数据覆盖,ORE会置位,如果不清除,中断会一直触发或卡死
    if (USART_GetFlagStatus(USART2, USART_FLAG_ORE) != RESET) {
        
        // 清除ORE标志,通常需要:读SR寄存器 -》读DR寄存器
        volatile uint8_t temp;
        temp = USART2->SR;
        temp = USART2->DR;
        (void)temp;
    }
}
​

第五部分:嵌入式面试关键点

Q1: 串口通信是全双工还是半双工?

  • :UART/USART通常是全双工的,因为有独立的TX和RX线,可以同时收发。
  • 追问:RS-485呢?
  • :RS-485通常是半双工的,同一时刻只能发或只能收。

Q2: 波特率 115200 是什么意思?传输 1 Byte 需要多久?

  • :115200 表示每秒传输 115200 个 bit。
  • 计算:配置为 8,N,1 时,传输1字节实际包含:1起始 + 8数据 + 0校验 + 1停止 = 10 bits。时间 = 10÷115200≈86.8μs10÷115200≈86.8μs。也就是说,1秒钟理论上最多传输 115200÷10=11520115200÷10=11520 个字节(约11KB/s)。

Q3: 为什么双方波特率必须一致?允许误差是多少?

  • :因为是异步通信,接收端依靠内部时钟对信号进行采样。如果没有时钟线同步,双方必须约定好速率。
  • 误差:通常允许误差在 2% ~ 5% 之间。如果累计误差超过半个bit的时间,采样就会错位,导致乱码。

Q4: 接收端如何检测信号?(采样原理)

  • :这是底层原理题。接收器通常使用过采样 (Oversampling) 技术(如16倍频)。检测到下降沿(起始位)后,计数器开始计数。在第 8, 9, 10 个采样点(即bit的中间位置)读取电平,通过多数表决(3次中有2次是高即为高)来确定该位的值,从而消除噪声干扰。

Q5: 常见的串口错误有哪些?

  • Framing Error (帧错误) :没有读到预期的停止位(通常因波特率不匹配或干扰)。
  • Parity Error (校验错误) :奇偶校验失败。
  • Overrun Error (溢出错误) :RX寄存器里的旧数据还没被CPU取走,新数据又来了,覆盖了旧数据。

Q6: 为什么长距离传输用RS485而不用TTL或RS232?

  • :TTL和RS232是单端信号(信号线对地电压),容易受到共模干扰(地电位波动或电磁辐射)。RS-485采用差分信号(两根线电压之差),干扰同时作用在两根线上,差值不变,因此抗干扰能力极强。

Q7: 编写一个串口接收环形缓冲区的判空和判满逻辑。

#define BUFFER_SIZE 128
typedef struct {
    uint8_t buffer[BUFFER_SIZE];
    volatile uint16_t head; // 写入位置
    volatile uint16_t tail; // 读取位置
} RingBuffer;
​
// 判空
int IsBufferEmpty(RingBuffer *rb) {
    return (rb->head == rb->tail);
}
​
// 判满 (保留一个字节不用,用于区分空和满)
int IsBufferFull(RingBuffer *rb) {
    return ((rb->head + 1) % BUFFER_SIZE == rb->tail);
}

总结建议

  1. 硬件上:搞清楚 TTL vs RS232 vs RS485 的区别。
  2. 软件上:重点掌握 DMA + 空闲中断 的流程,以及 环形缓冲区 的实现。
  3. 调试上:知道如果不通该怎么排查(查共地、查交叉连接、查波特率、查晶振)。