前言
在嵌入式开发中,串口通信是最基础且常用的通信方式之一。本文将记录一次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)
大会时间: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 相连
使用stty和cat简单测试:
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中的中断服务函数 |
| 接收不到完整字符串 | 回调逻辑问题 | 按本文修改回调函数 |
七、总结
通过这次调试,我们深刻体会到:
-
中断配置是使用HAL库时容易忽略的关键点;
-
回调函数的边界条件处理要严谨;
-
调试时回声功能能快速验证链路;
-
上位机程序的异常处理同样重要。
希望这篇文章能帮助你快速上手STM32与树莓派的串口通信。如有疑问,欢迎在评论区交流讨论!