前言

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

目录

前言

一、项目背景

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

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

2.2 树莓派端Python脚本

2.3 问题现象

三、问题排查与解决

3.1 硬件连接检查

3.2 软件层面排查

3.2.1 中断未使能

3.2.2 重复定义错误

3.2.3 数据格式问题

3.2.4 发送响应格式

3.3 树莓派端优化

四、实现“回声”功能

4.1 逐字节实时回声

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

4.3 测试回声

五、完整代码示例

5.1 STM32端(最终版本)

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生成+用户代码)

// 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脚本

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

2.3 问题现象

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

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简单测试:

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配置:

HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);

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

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被重置,导致缓冲区中的字符串可能不完整。

优化后的回调

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作为结束符,确保树莓派能正确识别一行。

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 逐字节实时回声

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

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()中发送详细数据:

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

#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脚本

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与树莓派的串口通信。如有疑问,欢迎在评论区交流讨论!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐