
1. 项目缘起为什么选择树莓派与STM32的UART组合最近在折腾一个需要跨平台数据采集与控制的小项目核心需求是让一个运行着复杂应用比如图像识别或者数据可视化的Linux系统与一个负责实时采集传感器数据、控制电机等外设的微控制器进行稳定、高效的通信。这个场景下树莓派4B和STM32的组合就自然而然地浮现在脑海里了。树莓派4B性能足够强劲能跑完整的操作系统如Raspbian/Ubuntu处理上层应用逻辑、网络通信和复杂计算游刃有余。而STM32尤其是像STM32F103这类经典款在实时性、低功耗和精确的定时、PWM、ADC控制方面是专家成本也控制得非常好。但两者一个跑在ARM Cortex-A72上一个跑在Cortex-M内核上架构和开发环境天差地别怎么让它们“说上话”就成了关键。在SPI、I2C、UART这几种常见的板间通信方式里UART通用异步收发传输器往往是首选。原因很简单它足够“通用”和“简单”。SPI和I2C通常需要主从设备共享时钟信号对时序要求更严格在多主设备或长距离通信时配置更复杂。而UART是异步的只需要两根数据线TX和RX就能实现全双工通信物理层逻辑简单几乎所有的微控制器和单板计算机都原生支持。对于树莓派和STM32这种“跨界”组合用UART对接是最快、最直接的方案不需要额外的电平转换芯片如果双方都是3.3V电平的话调试工具串口调试助手也遍地都是出了问题好排查。所以这个实验的目的很明确打通树莓派4B与STM32之间的UART串口通信链路实现双向数据传输并提供一个清晰、可复现的代码框架。无论是想用树莓派给STM32发送指令还是让STM32将传感器数据上报给树莓派做处理这个基础通信层都是必须迈过去的第一道坎。下面我就把从硬件连接到软件调试的完整过程包括我踩过的坑和总结的经验毫无保留地分享出来。2. 硬件连接与引脚定义避开那些“默认”的陷阱硬件连接是第一步也是最容易出错的一步。很多人以为照着引脚图接上TX、RX和GND就行了但实际操作中电平匹配、引脚复用这些细节没处理好后面软件调得再辛苦也是白搭。2.1 树莓派4B的UART引脚选择树莓派4B上有两个可用的UART一个是硬件UART通常称为/dev/ttyAMA0另一个是迷你UART/dev/ttyS0。它们的默认分配在历代树莓派型号中有所变化这是第一个坑。在树莓派4B上硬件UART/dev/ttyAMA0默认被分配给蓝牙模块使用。而那个更稳定、性能更好的硬件UART我们想拿来和STM32通信就需要先从蓝牙那里“抢”过来。对应的物理引脚是GPIO14TX和GPIO15RX也就是板子排针的第8脚和第10脚。注意务必确认你的树莓派GPIO引脚编号。这里指的是BCM编码物理引脚号是8TX和10RX。连接时树莓派的TX要接STM32的RX树莓派的RX接STM32的TX千万别接反了。GND一定要共地这是信号参考的基础。2.2 STM32端的UART配置STM32的UART资源非常丰富以常见的STM32F103C8T6蓝色pill开发板为例我们可以选用USART1。它的引脚是PA9TX和PA10RX。确保你的开发板原理图上这两个引脚没有被其他外设比如USB、JTAG占用。STM32的IO口电压通常是3.3V与树莓派4B的GPIO电平完美兼容这是选择这个组合的一大便利省去了电平转换电路。2.3 连接示意图与关键检查点最终的硬件连接非常简单树莓派 GPIO14 (TX) --- STM32 PA10 (RX)树莓派 GPIO15 (RX) --- STM32 PA9 (TX)树莓派 GND (例如引脚6) --- STM32 GND连接好后在上电前我强烈建议做三件事万用表测电压给树莓派和STM32分别上电先不要连接TX/RX线用万用表测量树莓派TX引脚GPIO14和STM32RX引脚PA10对各自GND的电压。正常应为稳定的3.3V左右高电平。这能排除电源异常或引脚损坏。肉眼复查对照引脚图再确认一遍TX-RX的交叉连接关系。接反了是绝对不通的。确保共地这是很多通信问题的根源。两个系统的GND必须连接在一起否则信号电压没有共同的参考点通信必然失败。3. 树莓派端软件配置释放硬件UART硬件连接无误后树莓派端的配置是下一个重点。我们需要重新配置系统把硬件UART从蓝牙手中解放出来并设置正确的串口参数。3.1 禁用蓝牙对硬件UART的占用树莓派的配置主要通过raspi-config工具和修改/boot/config.txt文件来完成。首先通过SSH或直接接显示器打开终端运行sudo raspi-config在界面中依次选择Interface Options-Serial Port当询问“是否要启用串口登录shell”时选择No。这一步很重要如果启用串口会被getty进程占用我们的程序就无法独占访问了。当询问“是否要启用串口硬件”时选择Yes。退出raspi-config后它主要修改了/boot/config.txt。但我们还需要手动编辑这个文件进行更精确的控制sudo nano /boot/config.txt在文件末尾确保或添加以下几行# 禁用蓝牙将硬件UARTttyAMA0分配给GPIO串口 dtoverlaydisable-bt # 确保迷你UART不被用作主串口可选但建议加上以明确配置 enable_uart1dtoverlaydisable-bt这一行是关键它禁用了蓝牙模块从而释放了硬件UART/dev/ttyAMA0。添加后保存退出。3.2 移除串口控制台getty服务即使我们在raspi-config里说了“No”有时系统可能仍有残留配置。我们需要确保串口没有被用于登录终端sudo systemctl disable serial-gettyttyAMA0.service sudo systemctl stop serial-gettyttyAMA0.service执行完上述步骤后重启树莓派sudo reboot3.3 验证配置与权限重启后首先验证硬件UART设备文件是否存在且权限正确ls -l /dev/ttyAMA0你应该看到类似crw-rw---- 1 root dialout 204, 64 May 1 10:00 /dev/ttyAMA0的输出。注意用户组是dialout。为了允许普通用户如pi访问串口需要将用户加入该组sudo usermod -a -G dialout $USER这个改动需要重新登录或重启才能生效。然后我们可以用一个简单的工具测试串口硬件是否正常。用一个杜邦线短接树莓派的GPIO14TX和GPIO15RX形成一个自发自收的回路。然后安装并运行minicomsudo apt install minicom -y minicom -D /dev/ttyAMA0 -b 115200在minicom界面中打开本地回显通常按Ctrl-A然后E。然后随意按键如果屏幕上能显示你按的字符说明串口硬件和自发自收回路是正常的。这步排除了硬件层面的问题。4. STM32端固件开发从零搭建通信框架STM32端的开发我选择使用STM32CubeIDE它集成了CubeMX配置工具和代码编辑器非常方便。这里以STM32F103C8T6为例。4.1 使用STM32CubeMX进行外设配置新建工程选择正确的芯片型号STM32F103C8Tx。在Pinout Configuration标签页中找到USART1。将Mode设置为Asynchronous异步模式。在下方Configuration标签中进入Parameter Settings子标签设置通信参数Baud Rate波特率: 115200 需与树莓派端一致Word Length字长: 8 BitsParity校验位: NoneStop Bits停止位: 1Hardware Flow Control硬件流控: Disable 其他参数保持默认即可。这里波特率的选择需要权衡速率越高数据传输越快但抗干扰能力越差通信距离也越短。对于板间短距离通信115200是一个兼顾速度和稳定性的常用值。还需要配置一个重要的部分系统时钟。在RCC配置中将High Speed Clock (HSE)设置为Crystal/Ceramic Resonator因为外部晶振能提供更精确的时钟源这是保证UART波特率准确的关键。然后转到Clock Configuration标签页合理配置时钟树确保系统时钟如72MHz和APB2总线时钟USART1挂载在此被正确设置。CubeMX会自动计算并显示USART1的实际波特率它应该非常接近你设置的115200如115203误差在可接受范围内。生成代码。在Project Manager标签页设置好工程名和路径选择Toolchain / IDE为STM32CubeIDE然后点击GENERATE CODE。4.2 编写核心通信代码CubeMX生成的代码已经帮我们初始化好了USART1的硬件。我们只需要在main.c的用户代码区添加应用逻辑。首先我们需要一个函数来发送字符串// 在 /* USER CODE BEGIN 0 */ 区域添加 #include string.h // 用于strlen函数 void USART1_SendString(char *str) { for(uint32_t i 0; i strlen(str); i) { // 等待发送数据寄存器为空即上一字节已发送完毕 while(!(USART1-SR USART_SR_TXE)); // 写入要发送的数据到数据寄存器 USART1-DR (str[i] 0xFF); } // 等待传输完成TC位被置位确保所有数据都已从移位寄存器发出 while(!(USART1-SR USART_SR_TC)); }接下来我们需要接收数据。这里采用中断接收的方式这是处理异步数据流的推荐方法不会阻塞主程序。在CubeMX的USART1配置中NVIC Settings子标签下勾选USART1 global interrupt使能中断。重新生成代码。在stm32f1xx_it.c文件中找到USART1_IRQHandler函数在其中添加中断处理逻辑// 在 /* USER CODE BEGIN USART1_IRQn 0 */ 区域添加 extern uint8_t uart_rx_buffer[256]; // 声明外部定义的接收缓冲区 extern uint16_t uart_rx_index; // 声明外部定义的缓冲区索引 extern uint8_t uart_rx_flag; // 声明外部定义的数据接收完成标志 void USART1_IRQHandler(void) { /* USER CODE BEGIN USART1_IRQn 0 */ // 检查是否是接收寄存器非空中断RXNE if(USART1-SR USART_SR_RXNE) { // 读取接收到的数据这会自动清除RXNE标志 uint8_t received_char USART1-DR; // 简单的协议以换行符\n作为一帧数据的结束 if(received_char \n) { uart_rx_buffer[uart_rx_index] \0; // 字符串结束符 uart_rx_flag 1; // 设置标志位通知主循环 uart_rx_index 0; // 重置索引 } else if(uart_rx_index sizeof(uart_rx_buffer) - 1) { // 将字符存入缓冲区 uart_rx_buffer[uart_rx_index] received_char; } else { // 缓冲区溢出处理这里可以重置索引或做错误处理 uart_rx_index 0; } } /* USER CODE END USART1_IRQn 0 */ HAL_UART_IRQHandler(huart1); /* USER CODE BEGIN USART1_IRQn 1 */ /* USER CODE END USART1_IRQn 1 */ }在main.c文件中定义上面中断处理函数用到的全局变量和缓冲区// 在 /* USER CODE BEGIN PV */ 区域添加 uint8_t uart_rx_buffer[256] {0}; uint16_t uart_rx_index 0; uint8_t uart_rx_flag 0;最后在main函数的while(1)主循环中添加发送和接收处理的逻辑// 在 /* USER CODE BEGIN WHILE */ 之后 char hello_msg[] STM32 Ready!\n; HAL_UART_Transmit(huart1, (uint8_t*)hello_msg, strlen(hello_msg), 1000); // 上电发送问候语 while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ // 检查接收完成标志 if(uart_rx_flag) { uart_rx_flag 0; // 清除标志 // 判断接收到的命令并回应 if(strcmp((char*)uart_rx_buffer, LED_ON) 0) { HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); // 假设LED接在PC13低电平点亮 USART1_SendString(LED is ON\n); } else if(strcmp((char*)uart_rx_buffer, LED_OFF) 0) { HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); USART1_SendString(LED is OFF\n); } else if(strcmp((char*)uart_rx_buffer, GET_DATA) 0) { // 模拟发送传感器数据 char sensor_data[32]; sprintf(sensor_data, Temp:25.6C, Humi:60%%\n); // 模拟数据 USART1_SendString(sensor_data); } else { // 未知命令回应 char echo_msg[64]; sprintf(echo_msg, ECHO: %s\n, uart_rx_buffer); USART1_SendString(echo_msg); } } // 这里可以添加其他任务如ADC采样等 } /* USER CODE END 3 */这段代码实现了一个简单的命令响应机制。STM32上电后发送“STM32 Ready!”然后等待树莓派发送命令。收到以换行符结尾的命令后会解析并执行对应的操作控制LED、返回模拟数据或回显充分展示了双向通信的能力。5. 树莓派端Python程序编写灵活的上位机控制树莓派端我们使用Python的pyserial库它功能强大且易于使用。首先安装库pip install pyserial然后创建一个Python脚本例如uart_comm.py#!/usr/bin/env python3 import serial import time import threading class STM32Communicator: def __init__(self, port/dev/ttyAMA0, baudrate115200, timeout1): 初始化串口通信类 :param port: 串口设备文件 :param baudrate: 波特率必须与STM32端一致 :param timeout: 读超时时间秒 self.ser serial.Serial(portport, baudratebaudrate, bytesizeserial.EIGHTBITS, parityserial.PARITY_NONE, stopbitsserial.STOPBITS_ONE, timeouttimeout) if self.ser.is_open: print(f成功打开串口 {port}) else: print(串口打开失败) exit(1) self.rx_buffer self.running True # 启动一个单独的线程来持续监听来自STM32的数据 self.read_thread threading.Thread(targetself._read_loop, daemonTrue) self.read_thread.start() def _read_loop(self): 后台线程持续读取串口数据并打印 while self.running: if self.ser.in_waiting 0: try: # 读取所有可用字节并解码为字符串 data self.ser.read(self.ser.in_waiting).decode(ascii, errorsignore) self.rx_buffer data # 按行打印接收到的数据 while \n in self.rx_buffer: line, self.rx_buffer self.rx_buffer.split(\n, 1) print(f[STM32] - {line}) except Exception as e: print(f读取数据时发生错误: {e}) def send_command(self, cmd): 发送命令到STM32自动添加换行符作为帧结束标志 full_cmd cmd \n try: self.ser.write(full_cmd.encode(ascii)) print(f[RPi] - {cmd}) except Exception as e: print(f发送命令时发生错误: {e}) def interactive_mode(self): 交互式模式用户输入命令并发送 print(进入交互模式。输入命令如 LED_ON, LED_OFF, GET_DATA或输入 quit 退出。) while True: user_input input( ).strip() if user_input.lower() quit: self.running False self.ser.close() print(退出程序。) break elif user_input: self.send_command(user_input) def auto_test_sequence(self): 自动测试序列演示基本功能 commands [LED_ON, GET_DATA, LED_OFF, HelloSTM32] for cmd in commands: self.send_command(cmd) time.sleep(1) # 等待STM32响应 if __name__ __main__: # 初始化通信器端口根据你的实际设备文件调整 comm STM32Communicator(port/dev/ttyAMA0) # 等待片刻接收STM32上电发送的问候语 time.sleep(2) # 选择运行模式 print(请选择模式) print(1 - 自动测试序列) print(2 - 交互式命令模式) choice input(输入 1 或 2: ) if choice 1: comm.auto_test_sequence() # 等待所有响应返回 time.sleep(3) comm.running False comm.ser.close() elif choice 2: comm.interactive_mode() else: print(无效选择。) comm.running False comm.ser.close()这个Python脚本做了几件关键事情封装串口操作使用pyserial以正确的参数波特率、数据位等打开串口。多线程接收创建了一个后台线程专门负责监听串口数据这样主线程在发送命令或等待用户输入时不会阻塞对STM32响应的接收。接收到的数据会实时打印出来。实现简单协议与STM32端约定以换行符\n作为一帧数据的结束标志。发送命令时自动附加接收时按行分割打印。提供两种模式auto_test_sequence函数演示了一个自动化的测试流程而interactive_mode则允许用户手动输入命令非常适合调试和功能验证。6. 联调实战与深度排错当通信没有如期而至即使按照上述步骤一步步操作第一次联调很可能还是会遇到问题。下面是我在多次实践中总结的排查链路和常见坑点。6.1 基础连通性测试先确保物理层没问题检查电源与共地再次确认树莓派和STM32都已正确供电且GND已可靠连接。可以用万用表测量两者GND之间的电压差应为0V或极小的mV级电压。验证引脚连接确认TX-RX是交叉连接没有接成TX-TX和RX-RX。使用逻辑分析仪或示波器这是最直接的诊断工具。将探头连接到通信线上设置正确的波特率115200观察是否有数据波形。如果树莓派发送了数据你会在其TX线上看到规律的方波STM32回复时则会在树莓派的RX线上看到波形。如果完全没有波形说明软件可能没成功驱动串口发送。6.2 软件配置检查清单树莓派端ls -l /dev/ttyAMA0确认设备文件存在且用户有读写权限在dialout组。运行dmesg | grep tty查看内核启动日志确认没有关于串口的错误信息。尝试用minicom或screen命令手动连接串口screen /dev/ttyAMA0 115200看能否看到STM32上电发送的“STM32 Ready!”信息。如果能看到说明STM32发送和树莓派接收通路基本正常。在minicom或screen中尝试直接输入字符看能否在STM32端收到需要STM32有回显功能。这可以测试树莓派发送通路。STM32端时钟配置这是导致波特率不准、通信乱码的头号杀手。务必在CubeMX的Clock Configuration页面仔细检查。对于STM32F103如果使用8MHz外部晶振HSE通常的配置路径是HSE - PLL输入 - PLL倍频到72MHz - 作为系统时钟SYSCLK。然后APB2总线时钟PCLK2通常等于SYSCLK72MHz而USART1的时钟来源于PCLK2。CubeMX会在USART配置页面显示“Calculated Baud Rate”它应该非常接近你设置的115200。如果误差超过2%通信就可能失败。下载与复位确保新编译的程序已成功下载到STM32并且单片机已复位运行。有时需要按一下板子的复位键。引脚复用确认你使用的UART引脚如PA9, PA10没有被其他功能如JTAG/SWD调试接口占用。在CubeMX的引脚图上如果引脚显示为黄色通常表示已配置如果是绿色表示有冲突。对于STM32F103PA13、PA14、PA15默认是JTAG/SWD引脚可能会影响其他功能但PA9和PA10通常没问题。6.3 通信乱码或数据错位如果双方能收到数据但全是乱码或者字符错位首要怀疑波特率99%的问题出在这里。请双端严格核对波特率、数据位8、停止位1、校验位None是否完全一致。哪怕一方是115200另一方是9600也可能收到一些看似有规律实则错误的数据。检查代码中的延时在STM32的发送函数中如果使用循环等待TXE标志要注意是否因为系统时钟配置错误导致延时计算不准。使用HAL库的HAL_UART_Transmit一般不会有问题。电源噪声如果使用不稳定的电源如劣质USB线可能导致电压波动影响信号质量。尝试更换电源或使用示波器观察信号波形是否干净。6.4 只能单向通信如果树莓派能收到STM32的数据但发送的命令STM32没反应或者反之交叉线检查这是最常见的原因再确认一遍树莓派TX接STM32 RX树莓派RX接STM32 TX。STM32中断未正确触发在STM32代码中确保USART的接收中断RXNEIE已使能。在CubeMX配置中NVIC Settings里要勾选USART全局中断。在代码中__HAL_UART_ENABLE_IT(huart1, UART_IT_RXNE);这句是否被执行可以在中断服务函数里加一个翻转LED的语句来测试中断是否被触发。Python发送格式确认Python脚本发送的字符串是否以\n结尾STM32的中断服务函数是否在等待\n作为帧结束可以尝试在Python端发送一个简单的字符比如A然后在STM32端改为收到任何字符都回显来缩小问题范围。6.5 性能与稳定性考量当基础通信调通后如果考虑长期运行或高速数据传输还需要注意缓冲区管理示例中的简单环形缓冲区在高速数据流下可能溢出。需要实现更健壮的环形缓冲区并考虑在STM32端使用**DMA直接存储器访问**来接收和发送UART数据可以极大减轻CPU负担避免因中断处理不及时导致的数据丢失。错误处理在UART中断中除了检查RXNE还应检查ORE溢出错误、FE帧错误等标志位并在发生错误时进行相应的清理和恢复。协议设计示例中使用换行符作为帧分隔符很简单但不适用于传输二进制数据。在实际项目中通常会设计更完善的协议例如包含帧头、长度、数据、校验和CRC的定长或变长数据包以提高通信的可靠性。通过以上步骤你应该能建立起一个稳定可靠的树莓派与STM32之间的UART通信链路。这个组合的灵活性极高树莓派可以作为强大的服务器处理数据、提供Web界面而STM32则充当可靠的前端执行器两者各司其职能搭建出非常多有趣的物联网或嵌入式项目。