解码串口通信与蓝牙模块

串口通信原理与应用

串行通信与并行通信

  • 串行通信:数据逐位按顺序传输
    • 适合远距离传输(可达数十米以上)
    • 抗干扰能力强
    • 布线简单,成本低
  • 并行通信:多个数据位同时传输
    • 适合短距离传输(通常在数米内)
    • 传输速率高
    • 抗干扰能力弱,易受串扰影响

通信方向分类

  • 单工:数据只能单向传输(如广播)

    image

  • 半双工:数据可双向传输,但不能同时进行(如对讲机)

    image

  • 全双工:数据可同时双向传输(如电话)

    image

同步方式分类

  • 同步通信
    • 通过时钟信号或同步字符实现严格同步
    • 数据以帧为单位连续传输
    • 传输效率高,适合大数据量传输
    • 对时钟同步要求严格,硬件复杂
  • 异步通信
    • 通过起始位和停止位实现字符级同步
    • 数据以字符为单位独立传输
    • 硬件简单,抗干扰能力强
    • 传输效率较低,有额外开销

串口硬件接口

  • 常用接口类型

    • DB-9(9针串口):常用引脚包括TXD、RXD、GND

    image

    • 4针串口:TXD、RXD、GND、VCC,通过CH340芯片转为USB
  • 硬件连接

    • 有线通信:使用CH340转换芯片,UART1跳线帽1-3、2-4短接
    • 无线通信:传感器TX连接MCU RX,传感器RX连接MCU TX,UART1跳线帽3-5、4-6短接

    image

串口通信参数

  • 字符格式(数据帧)

    进行串口通信的时候,需要通信双方在协议层规定好传输的数据包(字符帧)的格式,字符帧由起始位、数据位、校验位、停止位组成。

    image

    • 最常用格式:1起始位 + 8数据位 + 无校验 + 1停止位(8N1)
  • 通信速率

    • 波特率:单位时间内传输的码元数量(bps)
    • 比特率:单位时间内传输的二进制位数(bits/s)
    • 常用波特率:9600、19200、38400、57600、115200 bps

计算示例

  • 格式:1起始位 + 8数据位 + 1停止位 = 10位/字符
  • 波特率9600 bps时,每秒可传输:9600/10 = 960个字符

STM32串口外设

  • USART:通用同步异步收发器(支持同步和异步)
  • UART:通用异步收发器(仅异步)
  • STM32F407ZET6提供:4个USART + 2个UART

image


串口通信程序设计

核心步骤:时钟使能→GPIO复用配置→GPIO参数初始化→UART参数配置→中断配置(可选)→UART使能

UART初始化函数

/**
 * @brief 配置USART外设参数并初始化
 * @param baud 波特率设置,单位bps(如9600、115200)
 * @retval None
 * @note 本函数配置GPIO复用、串口参数、中断使能等
 *       使用前需确保系统时钟正确配置
 *       引脚复用:PA9->USART1_TX, PA10->USART1_RX
 */
void USART_Config(u32 baud)
{
  USART_InitTypeDef USART_InitStructure;
  NVIC_InitTypeDef NVIC_InitStructure;
  GPIO_InitTypeDef GPIO_InitStructure;

  // 1. 使能GPIOA时钟(引脚PA9、PA10所在端口)
  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);

  // 2. 使能USART1时钟(USART1挂载在APB2总线)
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);

  // 3. 配置引脚复用功能
  GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1);  // PA9复用为USART1_TX
  GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1); // PA10复用为USART1_RX

  // 4. 配置GPIO引脚参数
  GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF;   // 复用模式
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; // 输出速度
  GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;  // 推挽输出
  GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_UP;   // 上拉电阻
  GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_9 | GPIO_Pin_10; // 同时配置两个引脚
  GPIO_Init(GPIOA, &GPIO_InitStructure);

  // 5. 配置USART参数
  USART_InitStructure.USART_BaudRate = baud;      // 波特率
  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);

  // 6. 配置USART接收中断
  NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;         // 中断通道
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 抢占优先级
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;        // 子优先级
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;           // 使能中断
  NVIC_Init(&NVIC_InitStructure);

  // 7. 使能接收中断(RXNE:准备好读取接收到的数据)
  USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

  // 8. 使能USART外设
  USART_Cmd(USART1, ENABLE);
}

波特率计算(寄存器开发需要)

  • 框图

    image

  • 寄存器关键位

    • 状态寄存器 (USART_SR)
      • 位 7 TXE:发送数据寄存器为空
        • 硬件置 1:TDR 数据传输到移位寄存器
        • 软件清零:向 USART_DR 写入数据
        • 置 1 时若 TXEIE=1 触发中断
      • 位 6 TC:发送完成
        • 硬件置 1:帧发送完成且 TXE=1
        • 软件清零:读 SR 后写 DR,或直接写 0(多缓冲区推荐)
        • 置 1 时若 TCIE=1 触发中断
      • 位 5 RXNE:读取数据寄存器不为空
        • 硬件置 1:移位寄存器数据传输到 RDR
        • 软件清零:读取 USART_DR 数据
        • 置 1 时若 RXNEIE=1 触发中断
      • 位 4 IDLE:检测到空闲线路
        • 硬件置 1:检测到空闲帧
        • 软件清零:读 SR 后读 DR
        • 置 1 时若 IDLEIE=1 触发中断
      • 位 3 ORE:上溢错误
        • 硬件置 1:RXNE=1 时新数据又传入移位寄存器
        • 软件清零:读 SR 后读 DR
        • 多缓冲区下 EIE=1 时触发中断
      • 位 0 PE:奇偶校验错误
        • 硬件置 1:接收数据奇偶校验失败
        • 软件清零:读 SR 后读 / 写 DR(需等待 RXNE=1)
        • 置 1 时若 PEIE=1 触发中断
    • 数据寄存器 (USART_DR)
      • 位 8:0 DR [8:0]:收发数据存储位
        • 写操作:存入待发送数据(TDR 功能)
        • 读操作:读取接收数据(RDR 功能)
        • 奇偶校验使能时,MSB 位为奇偶校验位(发送时覆盖、接收时读取)
    • 波特率寄存器 (USART_BRR)
      • 位 15:4 DIV_Mantissa [11:0]:USARTDIV 除数尾数
        • 配合小数位配置波特率,公式:USARTDIV=16×baudfPCLK(16 倍过采样)
      • 位 3:0 DIV_Fraction [3:0]:USARTDIV 除数小数
        • 8 倍过采样时(OVER8=1),DIV_Fraction [3] 必须置 0
    • 控制寄存器 1 (USART_CR1)
      • 位 15 OVER8:过采样模式
        • 0:16 倍过采样
        • 1:8 倍过采样
      • 位 13 UE:USART 使能
        • 0:关闭 USART 预分频器和输出,降低功耗
        • 1:使能 USART 功能
      • 位 12 M:字长选择
        • 0:1 起始位 + 8 数据位 + n 停止位
        • 1:1 起始位 + 9 数据位 + n 停止位
        • 数据传输过程中禁止修改
      • 位 10 PCE:奇偶校验控制使能
        • 0:禁止奇偶校验
        • 1:使能奇偶校验(发送插入、接收校验)
      • 位 9 PS:奇偶校验选择(PCE=1 时有效)
        • 0:偶校验
        • 1:奇校验
      • 位 7 TXEIE:TXE 中断使能
        • 0:禁止 TXE 中断
        • 1:允许 TXE 中断
      • 位 5 RXNEIE:RXNE 中断使能
        • 0:禁止 RXNE/ORE 中断
        • 1:允许 RXNE/ORE 中断
      • 位 3 TE:发送器使能
        • 0:关闭发送器
        • 1:开启发送器,置 1 后有 1 位时间延迟
      • 位 2 RE:接收器使能
        • 0:关闭接收器
        • 1:开启接收器,开始搜索起始位
    • 控制寄存器 2 (USART_CR2)
      • 位 13:12 STOP [1:0]:停止位配置
        • 00:1 个停止位
        • 01:0.5 个停止位(UART4/5 不支持)
        • 10:2 个停止位
        • 11:1.5 个停止位(UART4/5 不支持)
      • 位 14 LINEN:LIN 模式使能
        • 0:禁止 LIN 模式
        • 1:使能 LIN 模式,支持断路检测与发送
      • 位 3:0 ADD [3:0]:多处理器通信地址
        • 配置 USART 节点地址,静音模式下通过地址标记唤醒
    • 控制寄存器 3 (USART_CR3)
      • 位 7 DMAT:发送器 DMA 使能
        • 0:禁止发送 DMA
        • 1:使能发送 DMA,支持多缓冲区数据发送
      • 位 6 DMAR:接收器 DMA 使能
        • 0:禁止接收 DMA
        • 1:使能接收 DMA,支持多缓冲区数据接收
      • 位 3 HDSEL:半双工模式选择
        • 0:禁止半双工(双线模式)
        • 1:使能半双工(单线模式)
      • 位 1 IREN:IrDA 模式使能
        • 0:禁止 IrDA 模式
        • 1:使能 IrDA 红外通信模式
      • 位 0 EIE:错误中断使能(多缓冲区)
        • 0:禁止 FE/ORE/NF 错误中断
        • 1:允许 FE/ORE/NF 错误中断(需 DMAR=1)
  • 过采样

    过采样是 USART 异步接收时,以高于波特率的频率对 RX 信号多次采样,靠多数表决或中点采样提升抗噪能力时钟同步精度,是无时钟线异步通信的关键技术。STM32 USART 支持16 倍(默认,OVER8=0)8 倍(OVER8=1) 两种模式,通过 USART_CR1 的 OVER8 位配置

    • 16 倍过采样(OVER8=0)
      • 采样频率:每个周期(Tb)采样 16 次,间隔 1/16 Tb。
      • 时钟计算:USARTDIV(分频系数) = f_ck/(16×BaudRate),最大波特率 f_ck/16。
      • 起始位检测:下降沿触发后,验证前 4 个采样为 1110,后续中点区域连续低电平确认。
      • 数据判决:取第 8、9、10 次采样(位中点)多数表决,抗噪与时钟容错更强(约 ±4.5% 偏差)。
      • 适用场景:常规 / 高噪声 / 时钟精度一般,优先选此模式。
    • 8 倍过采样(OVER8=1)
      • 采样频率:每个周期采样 8 次,间隔 1/8 Tb。
      • 时钟计算:USARTDIV(分频系数) = f_ck/(8×BaudRate),最大波特率 f_ck/8(同时钟下比 16 倍快一倍)。
      • 起始位检测:下降沿触发,前 4 个采样 1110,第 4 次采样确认起始位。
      • 数据判决:取第 4、5、6 次采样多数表决,抗噪与时钟容错略降(约 ±2% 偏差)。
      • 适用场景:需超高波特率且噪声可控的场景。

    通过设置USARTDIV(分频系数) 设置比特率,即先确定要设置的比特率,代入USARTDIV(分频系数) = f_ck/(8×(2-(OVER8))BaudRate)计算出USARTDIV,写入波特率寄存器USART_BRR;

    注意:USARTDIV要分尾数(整数)部分和小数部分分别写入USART_BRR的位 15:4(DIV_Mantissa[11:0]:USARTDIV 的尾数)和位 3:0(DIV_Fraction[3:0]:USARTDIV 的小数)。

数据发送函数

/**
 * @brief 通过USART发送字符串
 * @param str 要发送的字符串指针,以'\0'结尾
 * @retval None
 * @note 使用轮询方式等待发送寄存器为空
 *       适用于非实时性要求场景
 *       中断中不建议调用此函数
 */
void UART_SendString(char *str)
{
    // 遍历字符串直到结束符
    while (*str != '\0')
    {
        // 等待发送数据寄存器为空(TXE:Transmit Data Register Empty)
        while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
        
        // 发送当前字符
        USART_SendData(USART1, *str++);
    }
    
    // 等待最后一字节发送完成(TC:Transmission Complete)
    while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
}

中断服务函数

/**
 * @brief USART1中断服务函数,处理接收数据
 * @param None
 * @retval None
 * @note 当USART1接收到数据时触发此中断
 */
void USART1_IRQHandler(void)
{
    uint8_t data = 0;

    // 检查是否为接收中断(RXNE:Receive Data Register Not Empty)
    if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
    {
        //清除中断标志位
        USART_ClearITPendingBit(USART2,USART_IT_RXNE);

        // 读取接收到的数据(读取后RXNE自动清零)
        data = USART_ReceiveData(USART1);

        // 示例:将接收到的数据原样发送回PC(回声功能)
        USART_SendData(USART1, data);

        // 等待发送完成(可选,确保数据发送完毕)
        while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
    }
}

串口调试工具(PortHelper.exe

image


蓝牙模块原理与应用

无线通信技术对比

特性 BLE(蓝牙低功耗) Wi-Fi Zigbee LoRa
传输距离 10-100米 30-300米(室内) 10-100米(室内) 2-30公里
数据速率 2Mbps(BLE 5.3) 9.6Gbps(Wi-Fi 6) 250kbps 0.3-50kbps
工作频段 2.4GHz 2.4/5/6GHz 2.4GHz/868/915MHz Sub-GHz
功耗 极低 极低
主要应用 穿戴设备、IoT 互联网接入 智能家居、工业控制 远距离IoT

蓝牙4.0模块概述

image

  • 蓝牙4.0 BLE模块
    • 低功耗、低成本、小体积
    • 采用TI CC2541芯片
    • 支持AT指令配置
    • 主从一体模式
  • 技术参数
    • 协议:Bluetooth 4.0 BLE
    • 工作频率:2.4GHz ISM频段
    • 调制方式:GFSK(高斯频移键控)
    • 传输速率:异步6kbps
    • 供电:3.3V DC,50mA
    • 灵敏度:≤-84dBm @ 0.1% BER

蓝牙模块状态指示

模式 LED显示 模块状态
主模式 慢闪(300ms开/关) 搜索及连接中
主模式 常亮 连接已建立
从模式 慢闪(800ms开/关) 等待配对
从模式 常亮 连接已建立

蓝牙AT指令配置

/**
 * @brief 配置蓝牙模块参数示例
 * @param None
 * @retval None
 * @note AT指令需以\r\n结尾
 *       每条指令后需适当延时等待模块响应
 *       配置完成后需复位使设置生效
 */
void Bluetooth_ConfigExample(void)
{
    // 1. 测试连接(应返回OK)
    UART2_SendStr("AT\r\n");
    delay_ms(500);
    
    // 2. 修改蓝牙名称(最大20字符)
    UART2_SendStr("AT+NAMEYouEmbedded\r\n");
    delay_ms(500);
    
    // 3. 修改配对码(4位或6位数字)
    UART2_SendStr("AT+PIN123456\r\n");
    delay_ms(500);
    
    // 4. 修改波特率(需与MCU配置一致)
    UART2_SendStr("AT+BAUD4\r\n");  // 4对应9600bps
    delay_ms(500);
    
    // 5. 复位模块使设置生效
    UART2_SendStr("AT+RESET\r\n");
    delay_ms(1000);
}

常用AT指令表

指令 功能 响应
AT 测试连接 OK
AT+NAME<name> 设置设备名称 OK
AT+PIN<pin> 设置配对码 OK
AT+BAUD<n> 设置波特率(1-8) OK
AT+ROLE<n> 设置主从模式 OK
AT+RESET 软件复位 OK
AT+VERSION 查询版本 +VERSION:<ver>

波特率对应表

  • 1: 1200bps
  • 2: 2400bps
  • 3: 4800bps
  • 4: 9600bps(默认)
  • 5: 19200bps
  • 6: 38400bps
  • 7: 57600bps
  • 8: 115200bps

蓝牙数据透传

  • 透传模式:蓝牙连接建立后自动进入,透明传输串口数据
  • 应用场景:手机APP通过蓝牙控制MCU外设
  • 数据格式:自定义应用层协议

自定义数据协议设计

协议帧结构设计

/**
 * @brief 自定义串口通信协议帧结构
 * @note 一帧数据包含:包头 + 数据 + 校验 + 包尾
 *       和校验算法:所有字节相加取低8位
 */

// 协议定义
#define PACKET_HEADER  0xAA    // 包头标志
#define PACKET_TAIL    0xBB    // 包尾标志
#define BUFFER_SIZE    4       // 数据包总长度

// 状态机状态定义
typedef enum {
    WAIT_HEADER = 0,    // 等待包头
    WAIT_DATA,          // 等待数据
    WAIT_CHECK,         // 等待校验
    WAIT_TAIL           // 等待包尾
} PacketState;

// 数据包结构体
typedef struct {
    uint8_t buffer[BUFFER_SIZE];  // 接收缓冲区
    PacketState state;            // 当前状态
    uint8_t index;                // 缓冲区索引
} PacketParser;

状态机解析实现

/**
 * @brief 状态机方式解析数据包
 * @param parser 数据包解析器指针
 * @param byte 接收到的单个字节
 * @retval bool 解析完成返回true,否则false
 * @note 每收到一个字节调用一次
 *       校验失败时丢弃整个数据包
 *       支持断包、粘包处理
 */
bool ParsePacket(PacketParser* parser, uint8_t byte)
{
    switch (parser->state) {
        case WAIT_HEADER:
            if (byte == PACKET_HEADER) {
                parser->buffer[0] = byte;
                parser->index = 1;
                parser->state = WAIT_DATA;
            }
            break;

        case WAIT_DATA:
            parser->buffer[1] = byte;  // 数据字节
            parser->state = WAIT_CHECK;
            break;

        case WAIT_CHECK:
            parser->buffer[2] = byte;  // 校验字节
            parser->state = WAIT_TAIL;
            break;

        case WAIT_TAIL:
            if (byte == PACKET_TAIL) {
                parser->buffer[3] = byte;

                // 计算校验和(包头+数据+包尾)
                uint8_t checksum = (parser->buffer[0] +
                                   parser->buffer[1] +
                                   parser->buffer[3]) & 0xFF;

                // 校验通过
                if (checksum == parser->buffer[2]) {
                    parser->state = WAIT_HEADER;  // 重置状态
                    return true;                  // 解析成功
                }
            }
            // 校验失败或包尾错误,重置状态机
            parser->state = WAIT_HEADER;
            break;
    }

    return false;  // 解析未完成
}

应用层指令定义

/**
 * @brief 应用层指令码定义
 * @note 指令码为单字节,可扩展至多字节
 *       实际应用中可根据需求定义更多指令
 */
typedef enum {
    CMD_LED_ON    = 0xA1,  // 打开LED
    CMD_LED_OFF   = 0xA2,  // 关闭LED
    CMD_BEEP_ON   = 0xB1,  // 打开蜂鸣器
    CMD_BEEP_OFF  = 0xB2,  // 关闭蜂鸣器
    CMD_SERVO_SET = 0xC1,  // 设置舵机角度
    CMD_GET_TEMP  = 0xD1,  // 获取温度
    CMD_GET_HUMI  = 0xD2   // 获取湿度
} CommandCode;

指令处理示例

/**
 * @brief 处理解析完成的数据包
 * @param packet 完整的数据包缓冲区
 * @retval None
 * @note 根据指令码执行相应操作
 *       可扩展为支持参数化指令
 */
void ProcessPacket(uint8_t* packet)
{
    uint8_t command = packet[1];  // 数据字节即指令码

    switch (command) {
        case CMD_LED_ON:
            // LED亮
            break;

        case CMD_LED_OFF:
            // LED灭
            break;

        case CMD_BEEP_ON:
            // 蜂鸣器控制代码
            break;

        case CMD_BEEP_OFF:
            // 蜂鸣器控制代码
            break;

        case CMD_SERVO_SET:
            // 舵机角度设置,需解析参数
            break;

        default:
            // 未知指令处理
            break;
    }
}

完整控灯示例

/* Includes ------------------------------------------------------------------*/
#include "stm32f4xx.h"
#include <stdbool.h>
#include <string.h>

/* Private typedef -----------------------------------------------------------*/
// 数据包解析状态枚举
typedef enum {
    WAIT_HEADER = 0,  // 等待包头
    WAIT_DATA,        // 等待数据
    WAIT_CHECK,       // 等待校验
    WAIT_TAIL         // 等待包尾
} PacketState;

// 数据包解析器结构体
typedef struct {
    uint8_t buffer[4];        // 数据包缓冲区(包头/数据/校验/包尾)
    PacketState state;        // 当前解析状态
    uint8_t index;            // 缓冲区索引(兼容参考风格)
} PacketParser;

// 指令码枚举
typedef enum {
    CMD_LED_ON     = 0xA1,
    CMD_LED_OFF    = 0xA2,
} CmdCode;

/* Private define ------------------------------------------------------------*/
// 系统配置
#define SYS_CLOCK_FREQ_MHZ    21  // SysTick时钟频率(MHz)
#define NVIC_PRIORITY_GROUP   NVIC_PriorityGroup_4

// 数据包格式
#define PACKET_HEADER         0xAA
#define PACKET_TAIL           0xBB
#define PACKET_LEN            4

// 硬件引脚
#define LED_PIN               GPIO_Pin_9
#define LED_PORT              GPIOF

// UART配置
#define UART_PC               USART1
#define UART_BLE              USART2
#define UART_DEFAULT_BAUDRATE 9600

/* Private macro -------------------------------------------------------------*/
#define CALC_CHECKSUM(header, data, tail)  ((header + data + tail) & 0xFF)
#define LED_ON()              GPIO_ResetBits(LED_PORT, LED_PIN)
#define LED_OFF()             GPIO_SetBits(LED_PORT, LED_PIN)

/* Private variables ---------------------------------------------------------*/
PacketParser g_packet_parser = {0};          // 全局数据包解析器
volatile bool g_packet_parsed_ok = false;    // 数据包解析完成标志

/* Private function prototypes -----------------------------------------------*/
static void delay_us(uint32_t nus);
static void delay_ms(uint32_t nms);
static void LED_Init(void);
static void UART_PC_Init(uint32_t baudrate);
static void UART_BLE_Init(uint32_t baudrate);
static void UART_SendString(USART_TypeDef* uart, const char* str);
static void BLE_ConfigDefaultParams(void);
bool ParsePacket(PacketParser* parser, uint8_t byte);
void ProcessPacket(uint8_t* packet);

/* Private functions ---------------------------------------------------------*/

/**
  * @brief  微秒级延时函数
  * @param  nus: 延时时间(us)
  * @retval None
  */
static void delay_us(uint32_t nus)
{
    if (nus == 0) return;
    
    SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
    SysTick->LOAD  = (uint32_t)(nus * SYS_CLOCK_FREQ_MHZ) - 1;
    SysTick->VAL   = 0;
    SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
    
    while (!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk));
    
    SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}

/**
  * @brief  毫秒级延时函数
  * @param  nms: 延时时间(ms)
  * @retval None
  */
static void delay_ms(uint32_t nms)
{
    while (nms--) {
        delay_us(1000);
    }
}

/**
  * @brief  LED初始化
  * @param  None
  * @retval None
  */
static void LED_Init(void)
{
    GPIO_InitTypeDef gpio_init_struct;
    
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);
    
    gpio_init_struct.GPIO_Pin   = LED_PIN;
    gpio_init_struct.GPIO_Mode  = GPIO_Mode_OUT;
    gpio_init_struct.GPIO_OType = GPIO_OType_PP;
    gpio_init_struct.GPIO_Speed = GPIO_Speed_100MHz;
    gpio_init_struct.GPIO_PuPd  = GPIO_PuPd_NOPULL;
    
    GPIO_Init(LED_PORT, &gpio_init_struct);
    LED_OFF();
}

/**
  * @brief  初始化连接PC的UART1
  * @param  baudrate: 波特率
  * @retval None
  */
static void UART_PC_Init(uint32_t baudrate)
{
    GPIO_InitTypeDef gpio_init_struct;
    USART_InitTypeDef uart_init_struct;
    NVIC_InitTypeDef nvic_init_struct;
    
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1);
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1);
    
    gpio_init_struct.GPIO_Pin   = GPIO_Pin_9 | GPIO_Pin_10;
    gpio_init_struct.GPIO_Mode  = GPIO_Mode_AF;
    gpio_init_struct.GPIO_OType = GPIO_OType_PP;
    gpio_init_struct.GPIO_Speed = GPIO_Speed_100MHz;
    gpio_init_struct.GPIO_PuPd  = GPIO_PuPd_UP;
    GPIO_Init(GPIOA, &gpio_init_struct);
    
    uart_init_struct.USART_BaudRate            = baudrate;
    uart_init_struct.USART_WordLength          = USART_WordLength_8b;
    uart_init_struct.USART_StopBits            = USART_StopBits_1;
    uart_init_struct.USART_Parity              = USART_Parity_No;
    uart_init_struct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    uart_init_struct.USART_Mode                = USART_Mode_Rx | USART_Mode_Tx;
    USART_Init(UART_PC, &uart_init_struct);
    
    nvic_init_struct.NVIC_IRQChannel                   = USART1_IRQn;
    nvic_init_struct.NVIC_IRQChannelPreemptionPriority = 1;
    nvic_init_struct.NVIC_IRQChannelSubPriority        = 0;
    nvic_init_struct.NVIC_IRQChannelCmd                = ENABLE;
    NVIC_Init(&nvic_init_struct);
    
    USART_ITConfig(UART_PC, USART_IT_RXNE, ENABLE);
    USART_Cmd(UART_PC, ENABLE);
}

/**
  * @brief  初始化连接蓝牙的UART2
  * @param  baudrate: 波特率
  * @retval None
  */
static void UART_BLE_Init(uint32_t baudrate)
{
    GPIO_InitTypeDef gpio_init_struct;
    USART_InitTypeDef uart_init_struct;
    NVIC_InitTypeDef nvic_init_struct;
    
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
    
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource2, GPIO_AF_USART2);
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource3, GPIO_AF_USART2);
    
    gpio_init_struct.GPIO_Pin   = GPIO_Pin_2 | GPIO_Pin_3;
    gpio_init_struct.GPIO_Mode  = GPIO_Mode_AF;
    gpio_init_struct.GPIO_OType = GPIO_OType_PP;
    gpio_init_struct.GPIO_Speed = GPIO_Speed_100MHz;
    gpio_init_struct.GPIO_PuPd  = GPIO_PuPd_UP;
    GPIO_Init(GPIOA, &gpio_init_struct);
    
    uart_init_struct.USART_BaudRate            = baudrate;
    uart_init_struct.USART_WordLength          = USART_WordLength_8b;
    uart_init_struct.USART_StopBits            = USART_StopBits_1;
    uart_init_struct.USART_Parity              = USART_Parity_No;
    uart_init_struct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    uart_init_struct.USART_Mode                = USART_Mode_Rx | USART_Mode_Tx;
    USART_Init(UART_BLE, &uart_init_struct);
    
    nvic_init_struct.NVIC_IRQChannel                   = USART2_IRQn;
    nvic_init_struct.NVIC_IRQChannelPreemptionPriority = 1;
    nvic_init_struct.NVIC_IRQChannelSubPriority        = 1;
    nvic_init_struct.NVIC_IRQChannelCmd                = ENABLE;
    NVIC_Init(&nvic_init_struct);
    
    USART_ITConfig(UART_BLE, USART_IT_RXNE, ENABLE);
    USART_Cmd(UART_BLE, ENABLE);
}

/**
  * @brief  UART发送字符串
  * @param  uart: UART外设指针
  * @param  str: 要发送的字符串
  * @retval None
  */
static void UART_SendString(USART_TypeDef* uart, const char* str)
{
    if (str == NULL || uart == NULL) return;
    
    while (*str != '\0') {
        while (USART_GetFlagStatus(uart, USART_FLAG_TXE) == RESET);
        USART_SendData(uart, (uint8_t)*str++);
    }
    while (USART_GetFlagStatus(uart, USART_FLAG_TC) == RESET);
}

/**
  * @brief  配置蓝牙模块默认参数
  * @param  None
  * @retval None
  */
static void BLE_ConfigDefaultParams(void)
{
    UART_SendString(UART_BLE, "AT\r\n");
    delay_ms(500);
    
    UART_SendString(UART_BLE, "AT+NAMEYouEmbedded\r\n");
    delay_ms(500);
    
    UART_SendString(UART_BLE, "AT+RESET\r\n");
    delay_ms(500);
}

/**
  * @brief  状态机方式解析数据包
  * @param  parser 数据包解析器指针
  * @param  byte 接收到的单个字节
  * @retval bool 解析完成返回true,否则false
  * @note 每收到一个字节调用一次
  *       校验失败时丢弃整个数据包
  *       支持断包、粘包处理
  */
bool ParsePacket(PacketParser* parser, uint8_t byte)
{
    if (parser == NULL) return false;

    switch (parser->state) {
        case WAIT_HEADER:
            if (byte == PACKET_HEADER) {
                parser->buffer[0] = byte;
                parser->index = 1;
                parser->state = WAIT_DATA;
            }
            break;

        case WAIT_DATA:
            parser->buffer[1] = byte;
            parser->state = WAIT_CHECK;
            break;

        case WAIT_CHECK:
            parser->buffer[2] = byte;
            parser->state = WAIT_TAIL;
            break;

        case WAIT_TAIL:
            if (byte == PACKET_TAIL) {
                parser->buffer[3] = byte;

                // 计算校验和
                uint8_t checksum = CALC_CHECKSUM(parser->buffer[0], 
                                                 parser->buffer[1], 
                                                 parser->buffer[3]);

                // 校验通过
                if (checksum == parser->buffer[2]) {
                    parser->state = WAIT_HEADER;
                    return true;
                }
            }
            // 校验失败或包尾错误,重置状态机
            parser->state = WAIT_HEADER;
            break;

        default:
            parser->state = WAIT_HEADER;
            break;
    }

    return false;
}

/**
  * @brief  处理解析完成的数据包
  * @param  packet 完整的数据包缓冲区
  * @retval None
  * @note 根据指令码执行相应操作
  *       可扩展为支持参数化指令
  */
void ProcessPacket(uint8_t* packet)
{
    if (packet == NULL) return;

    uint8_t command = packet[1];

    switch (command) {
        case CMD_LED_ON:
            LED_ON();
            break;

        case CMD_LED_OFF:
            LED_OFF();
            break;

        default:
            break;
    }
}

/**
  * @brief  USART1中断服务函数(PC端UART)
  * @param  None
  * @retval None
  */
void USART1_IRQHandler(void)
{
    uint8_t recv_data = 0;
    
    if (USART_GetITStatus(UART_PC, USART_IT_RXNE) == SET)
    {
        recv_data = USART_ReceiveData(UART_PC);
        
        // 数据回显
        while (USART_GetFlagStatus(UART_PC, USART_FLAG_TXE) == RESET);
        USART_SendData(UART_PC, recv_data);
        
        USART_ClearITPendingBit(UART_PC, USART_IT_RXNE);
    }
}

/**
  * @brief  USART2中断服务函数(蓝牙模块UART)
  * @param  None
  * @retval None
  */
void USART2_IRQHandler(void)
{
    uint8_t recv_data = 0;
    
    if (USART_GetITStatus(UART_BLE, USART_IT_RXNE) == SET)
    {       
        recv_data = USART_ReceiveData(UART_BLE);
        
        // 调用封装的解析函数
        if (ParsePacket(&g_packet_parser, recv_data)) {
            g_packet_parsed_ok = true;
        }
        
        // 转发数据到PC端调试
        while (USART_GetFlagStatus(UART_PC, USART_FLAG_TXE) == RESET);
        USART_SendData(UART_PC, recv_data);
        
        USART_ClearITPendingBit(UART_BLE, USART_IT_RXNE);
    }
}

/**
  * @brief  主函数
  * @param  None
  * @retval None
  */
int main(void)
{
    // 1.初始化NVIC优先级分组
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
    
    // 2.初始化解析器
    memset(&g_packet_parser, 0, sizeof(PacketParser));
    g_packet_parser.state = WAIT_HEADER;
    
    // 3.硬件初始化
    LED_Init();
    UART_BLE_Init(UART_DEFAULT_BAUDRATE);
    UART_PC_Init(UART_DEFAULT_BAUDRATE);
    
    // 4.配置蓝牙模块
    BLE_ConfigDefaultParams();
    
    // 主循环
    while (1)
    {
        if (g_packet_parsed_ok)
        {
            g_packet_parsed_ok = false;
            
            // 调用封装的处理函数
            ProcessPacket(g_packet_parser.buffer);
            
            // 清空缓冲区
            memset(g_packet_parser.buffer, 0, PACKET_LEN);
        }
    }
}

注意:手机可以使用蓝牙调试助手app或小程序通过蓝牙模块向MCU发送消息


常见问题与解决方案

串口通信乱码问题

  • 可能原因1:波特率不匹配
    • 检查MCU与PC端波特率设置是否一致
    • 常用波特率:9600、115200
  • 可能原因2:时钟配置错误
    • STM32F4默认HSE=25MHz,假设实际为8MHz(具体查看产品原理图), 需修改配置:
      • 修改system_stm32f4xx.c第316行:PLL_M从25改为8
      • 修改stm32f4xx.h第123行:HSE_VALUE从25000000改为8000000
  • 可能原因3:数据格式不一致
    • 确认数据位、停止位、校验位设置
    • 最常见格式:8位数据、1位停止、无校验
  • 可能原因4:硬件连接问题
    • 检查TX、RX是否交叉连接(设备TX接MCU RX)
    • 确认地线(GND)已连接
    • 检查电源电压是否稳定

蓝牙连接失败

  • 检查步骤
    • 蓝牙模块供电是否正常
    • AT指令是否能正常响应
    • 手机蓝牙是否开启并搜索到设备
    • 配对密码是否正确
    • 模块是否处于可配对状态(LED慢闪)
  • 解决方法
    • 使用AT指令恢复出厂设置:AT+DEFAULT
    • 重新配置名称、密码、波特率
    • 重启模块:AT+RESET

printf函数重定向

重定向原理

  • 问题:嵌入式系统无标准输出设备,printf无法直接使用
  • 解决方案:重定向printf到串口,通过串口调试助手查看输出

实现方法

/**
 * @brief 重定向printf到USART1
 * @param ch 要发送的字符
 * @param f 文件指针(未使用)
 * @retval int 成功返回字符,失败返回EOF
 * @note 需包含stdio.h头文件
 *       在Options for Target中勾选Use MicroLIB
 *       避免在中断中调用printf
 */
#include <stdio.h>

int fputc(int ch, FILE *f)
{
    // 等待发送寄存器为空
    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);

    // 发送字符
    USART_SendData(USART1, (uint8_t)ch);

    return ch;
}

Keil环境配置

启用MicroLIB(减小代码体积):

  • Options for Target → Target → 勾选"Use MicroLIB"

包含标准库

#include <stdio.h>
#include <string.h>

使用示例

int value = 123;
float temp = 25.5;

printf("系统启动成功!\r\n");
printf("整数值:%d\r\n", value);
printf("温度值:%.1f℃\r\n", temp);
printf("字符串:%s\r\n", "Hello World");

image

注意事项

  • 避免在中断中使用printf:执行时间长,可能引起系统不稳定
  • 格式化字符串占用资源:大量使用可能增加代码体积
  • 实时性要求:高实时性场景建议使用直接串口发送函数
posted @ 2026-01-15 18:37  YouEmbedded  阅读(26)  评论(0)    收藏  举报