STM32与树莓派USART通信实战:从零开始调试与回声功能实现

前言

在嵌入式开发中,串口通信是最基础且常用的通信方式之一。本文将记录一次STM32F103RCT6与树莓派5通过USART通信的完整调试过程,包括问题排查、代码修复、以及如何实现回声功能。无论你是初学者还是有一定经验的开发者,都能从中获得实用的调试技巧。

目录

前言

一、项目背景

二、初版代码与遇到的问题

[2.1 STM32端(CubeMX生成+用户代码)](#2.1 STM32端(CubeMX生成+用户代码))

[2.2 树莓派端Python脚本](#2.2 树莓派端Python脚本)

[2.3 问题现象](#2.3 问题现象)

三、问题排查与解决

[3.1 硬件连接检查](#3.1 硬件连接检查)

[3.2 软件层面排查](#3.2 软件层面排查)

[3.2.1 中断未使能](#3.2.1 中断未使能)

[3.2.2 重复定义错误](#3.2.2 重复定义错误)

[3.2.3 数据格式问题](#3.2.3 数据格式问题)

[3.2.4 发送响应格式](#3.2.4 发送响应格式)

[3.3 树莓派端优化](#3.3 树莓派端优化)

四、实现"回声"功能

[4.1 逐字节实时回声](#4.1 逐字节实时回声)

[4.2 带格式的回声(调试用)](#4.2 带格式的回声(调试用))

[4.3 测试回声](#4.3 测试回声)

五、完整代码示例

[5.1 STM32端(最终版本)](#5.1 STM32端(最终版本))

[5.2 树莓派端Python脚本](#5.2 树莓派端Python脚本)

六、常见问题与解决

七、总结


论文投稿:
第七届计算机工程与应用国际学术会议 (ICCEA 2026)

大会官网:https://ais.cn/u/memANb

大会时间:2026年5月15-17日

大会地点:中国-重庆


一、项目背景

  • 下位机:STM32F103RCT6(Cortex-M3)

  • 上位机:树莓派5(Raspberry Pi OS)

  • 通信接口:USART1(PA9 TX, PA10 RX)

  • 波特率:115200,8数据位,1停止位,无校验

  • 目标 :树莓派发送d111命令,STM32接收并返回OK


二、初版代码与遇到的问题

2.1 STM32端(CubeMX生成+用户代码)

cs 复制代码
// usart.c 关键部分
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart->Instance == USART1)
  {
    if (rx_data == '\n' || rx_data == '\r' || rx_index >= RX_BUFFER_SIZE - 1)
    {
      rx_buffer[rx_index] = '\0';
      command_received = 1;
      rx_index = 0;
    }
    else
    {
      rx_buffer[rx_index++] = rx_data;
    }
    HAL_UART_Receive_IT(&huart1, &rx_data, 1);
  }
}

void process_command(void)
{
  if (command_received)
  {
    if (strcmp((char*)rx_buffer, "d111") == 0)
      send_response("OK");
    else
      send_response("UNKNOWN");
    command_received = 0;
  }
}

2.2 树莓派端Python脚本

python 复制代码
class SimpleSTM32Communicator:
    def send_d111(self):
        command = "d111\n"
        self.serial.write(command.encode())
        # ... 接收响应

2.3 问题现象

运行树莓派脚本后,STM32没有返回任何响应,按下Ctrl+C时Python程序卡住并报错:

bash 复制代码
Traceback (most recent call last):
  ...
KeyboardInterrupt
Exception ignored in: Serial<...>
OSError: [Errno 9] Bad file descriptor

三、问题排查与解决

3.1 硬件连接检查

首先确认物理连接正确:

  • STM32 PA9 (TX) → 树莓派 GPIO15 (RX)

  • STM32 PA10 (RX) → 树莓派 GPIO14 (TX)

  • GND 相连

使用sttycat简单测试:

bash 复制代码
stty -F /dev/ttyAMA2 115200
echo "test" > /dev/ttyAMA2
timeout 1 cat /dev/ttyAMA2

若无响应,则需检查STM32程序是否运行。

3.2 软件层面排查

3.2.1 中断未使能

初版代码虽然在MX_USART1_UART_Init()中调用了HAL_UART_Init(),但没有使能USART1中断 。CubeMX生成的代码默认会在stm32f1xx_it.c中提供中断服务函数,但NVIC配置可能缺失。

解决 :在MX_USART1_UART_Init()中添加NVIC配置:

cs 复制代码
HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);

或者在stm32f1xx_it.c中确保已包含:

cs 复制代码
void USART1_IRQHandler(void)
{
    HAL_UART_IRQHandler(&huart1);
}
3.2.2 重复定义错误

编译时出现USART1_IRQHandler multiply defined,是因为我们在main.c中也定义了该函数。正确做法是只在stm32f1xx_it.c中保留,main.c中删除。

3.2.3 数据格式问题

我们测试了多种换行符格式:\n\r\n\r。最终发现STM32的中断回调中检测到\n\r时才会结束接收。但回调逻辑存在缺陷:当收到换行符时,没有存储它 ,而rx_index被重置,导致缓冲区中的字符串可能不完整。

优化后的回调

cs 复制代码
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart->Instance == USART1)
  {
    if (rx_data == '\n' || rx_data == '\r')
    {
      if (rx_index > 0)
      {
        rx_buffer[rx_index] = '\0';
        command_received = 1;
      }
      rx_index = 0;
    }
    else if (rx_index < RX_BUFFER_SIZE - 1)
    {
      rx_buffer[rx_index++] = rx_data;
    }
    HAL_UART_Receive_IT(&huart1, &rx_data, 1);
  }
}
3.2.4 发送响应格式

STM32发送响应时,使用\r\n作为结束符,确保树莓派能正确识别一行。

cs 复制代码
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart->Instance == USART1)
  {
    if (rx_data == '\n' || rx_data == '\r')
    {
      if (rx_index > 0)
      {
        rx_buffer[rx_index] = '\0';
        command_received = 1;
      }
      rx_index = 0;
    }
    else if (rx_index < RX_BUFFER_SIZE - 1)
    {
      rx_buffer[rx_index++] = rx_data;
    }
    HAL_UART_Receive_IT(&huart1, &rx_data, 1);
  }
}

3.3 树莓派端优化

  • 将串口超时从1秒缩短到0.5秒,避免长时间阻塞

  • 使用readline()简化接收逻辑

  • 添加KeyboardInterrupt的优雅退出处理


四、实现"回声"功能

为了调试方便,我们想实现:无论STM32收到什么,都原样返回(回声)。这有助于验证通信链路是否正常。

4.1 逐字节实时回声

最简单的方式是在接收中断中立即回传字符:

cs 复制代码
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart->Instance == USART1)
  {
    // 立即回显
    HAL_UART_Transmit(&huart1, &rx_data, 1, 10);
    // 处理回车换行
    if (rx_data == '\r')
    {
      uint8_t lf = '\n';
      HAL_UART_Transmit(&huart1, &lf, 1, 10);
    }
    HAL_UART_Receive_IT(&huart1, &rx_data, 1);
  }
}

4.2 带格式的回声(调试用)

如果需要更多信息,可以在process_command()中发送详细数据:

cs 复制代码
void process_command(void)
{
  if (command_received)
  {
    char info[128];
    snprintf(info, sizeof(info), "\r\nReceived: \"%s\"\r\n", rx_buffer);
    HAL_UART_Transmit(&huart1, (uint8_t*)info, strlen(info), 100);
    command_received = 0;
  }
}

4.3 测试回声

使用树莓派minicom或Python脚本发送任意数据,观察是否返回相同内容。


五、完整代码示例

5.1 STM32端(最终版本)

usart.c

cs 复制代码
#include "usart.h"
#include <string.h>

#define RX_BUFFER_SIZE 128

UART_HandleTypeDef huart1;
uint8_t rx_data = 0;
uint8_t rx_buffer[RX_BUFFER_SIZE];
uint16_t rx_index = 0;
uint8_t command_received = 0;

void MX_USART1_UART_Init(void)
{
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  HAL_UART_Init(&huart1);

  // 使能中断
  HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(USART1_IRQn);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart->Instance == USART1)
  {
    if (rx_data == '\n' || rx_data == '\r')
    {
      if (rx_index > 0)
      {
        rx_buffer[rx_index] = '\0';
        command_received = 1;
      }
      rx_index = 0;
    }
    else if (rx_index < RX_BUFFER_SIZE - 1)
    {
      rx_buffer[rx_index++] = rx_data;
    }
    HAL_UART_Receive_IT(&huart1, &rx_data, 1);
  }
}

void send_response(const char *response)
{
  HAL_UART_Transmit(&huart1, (uint8_t*)response, strlen(response), 1000);
  HAL_UART_Transmit(&huart1, (uint8_t*)"\r\n", 2, 1000);
}

void process_command(void)
{
  if (command_received)
  {
    if (strcmp((char*)rx_buffer, "d111") == 0)
      send_response("OK");
    else
      send_response("UNKNOWN");
    command_received = 0;
  }
}
main.c 部分

c

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();

  HAL_UART_Receive_IT(&huart1, &rx_data, 1);
  HAL_UART_Transmit(&huart1, (uint8_t*)"STM32 Ready\r\n", 13, 1000);

  while (1)
  {
    process_command();
    HAL_Delay(10);
  }
}

5.2 树莓派端Python脚本

python 复制代码
import serial
import time
import sys

class STM32Comm:
    def __init__(self, port='/dev/ttyAMA2', baud=115200):
        self.ser = serial.Serial(port, baud, timeout=1)
        time.sleep(0.5)
        self.ser.reset_input_buffer()

    def send_cmd(self, cmd):
        self.ser.write(f"{cmd}\n".encode())
        resp = self.ser.readline().decode().strip()
        return resp

    def close(self):
        self.ser.close()

def main():
    comm = STM32Comm()
    try:
        while True:
            cmd = input("Enter command (d111): ")
            if cmd == "quit":
                break
            resp = comm.send_cmd(cmd)
            print(f"Response: {resp}")
    except KeyboardInterrupt:
        pass
    finally:
        comm.close()

if __name__ == "__main__":
    main()

六、常见问题与解决

问题 可能原因 解决方法
无响应 硬件连接错误 检查TX/RX交叉连接,共地
数据乱码 波特率不匹配 确保两边一致
程序卡死 中断未使能 添加NVIC配置
编译错误 重复定义 删除main.c中的中断服务函数
接收不到完整字符串 回调逻辑问题 按本文修改回调函数

七、总结

通过这次调试,我们深刻体会到:

  1. 中断配置是使用HAL库时容易忽略的关键点;

  2. 回调函数的边界条件处理要严谨;

  3. 调试时回声功能能快速验证链路;

  4. 上位机程序的异常处理同样重要。

希望这篇文章能帮助你快速上手STM32与树莓派的串口通信。如有疑问,欢迎在评论区交流讨论!

相关推荐
勾股导航3 小时前
集成运放.比例电路
嵌入式硬件·具身智能·集成放大器
lisw053 小时前
单片机:概念、历史、内容与发展战略!
人工智能·单片机·机器学习
困死,根本不会3 小时前
Windows下模拟树莓派:使用ble-serial创建虚拟串口实现手机蓝牙通信
windows·python·单片机·嵌入式硬件·树莓派
ytttr8734 小时前
F3U源码STM32仿三菱PLC底层实现
stm32·plc
我在人间贩卖青春4 小时前
单片机的时钟源
单片机·嵌入式硬件·时钟源
promising-w4 小时前
线性电源和开关电源
单片机·嵌入式硬件
BackCatK Chen4 小时前
TMC2240 芯片数据手册解读|第二篇MC2240 芯片电气规格与封装信息
嵌入式硬件·步进电机驱动·tmc2240·tmc2240 数据手册解读·电气参数·封装信息·硬件选型
老师用之于民4 小时前
【DAY29】DS18B20 传感器特性、时序协议及 51 单片机驱动开发
c语言·驱动开发·单片机·嵌入式硬件
-Try hard-4 小时前
单片机 | 温度传感器(DS18B20)
单片机·嵌入式硬件