如何使用ROS 2与STM32进行串口通信,并实现通过键盘按键‘1’来控制LED灯开关
系统架构概述
本项目的目标是构建一个分布式控制系统:一台运行ROS 2的计算机(上位机)通过串口向一颗STM32微控制器(下位机)发送指令,从而控制连接在STM32上的LED灯。键盘作为用户输入设备,由ROS 2节点监听,最终将按键事件转化为通过串口发送的指令。
整个系统的数据流可以清晰地用下图表示:
核心组件:
ROS2节点 (
keyboard_control_node):负责监听键盘事件,并通过串口发送控制指令。串口通信:基于UART协议,是ROS2与STM32之间的物理和数据链路。
STM32固件:负责解析指令并直接控制GPIO引脚。
硬件准备与连接
所需硬件
STM32开发板(如STM32F103C8T6)
USB转TTL串口模块(如CH340、CP2102)
LED灯及合适阻值的限流电阻(如220Ω)
跳线、面包板若干
运行Ubuntu和ROS2(Humble或更新版本)的电脑
硬件连接(至关重要,务必准确)
USB转TTL模块的TX 接 STM32的RX(例如PA10)
USB转TTL模块的RX 接 STM32的TX(例如PA9)
USB转TTL模块的GND 接 STM32的GND(必须共地)
LED正极通过电阻接STM32的GPIO引脚(例如PA8)
LED负极接GND
接线示意图如下:

STM32下位机程序实现
STM32端的任务是初始化串口和GPIO,并不断监听来自串口的指令,根据指令控制LED。
核心代码逻辑(主循环)
以下代码展示了一个简单的STM32程序框架,它持续检查串口是否收到数据,并根据收到的字符控制LED。
#include "stm32f10x.h" #include "stdio.h" #include "string.h"// 定义接收缓冲区和状态 #define RX_BUF_SIZE 64 char uart_rx_buf[RX_BUF_SIZE]; uint8_t rx_index = 0; uint8_t cmd_ready = 0;int main(void) {// 初始化系统时钟、LED GPIO(推挽输出)、串口(波特率115200,8N1)// ... 硬件初始化代码 (基于HAL库或标准库) ...while(1) {// 检查是否收到一行完整的命令(以换行符'\\n'结束)if(cmd_ready) {cmd_ready = 0;process_command(uart_rx_buf); // 处理命令rx_index = 0; // 重置缓冲区索引}// 其他主循环任务...} }// 串口中断服务函数 void USART1_IRQHandler(void) {if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {char received_char = USART_ReceiveData(USART1);// 简单协议:以换行符'\\n'作为一帧命令的结束if(received_char == '\\n') {if(rx_index > 0) {uart_rx_buf[rx_index] = '\\0'; // 添加字符串结束符cmd_ready = 1;}} else {// 将字符存入缓冲区,防止溢出if(rx_index < RX_BUF_SIZE - 1) {uart_rx_buf[rx_index++] = received_char;}}} }// 命令处理函数 void process_command(char* cmd) {if(strcmp(cmd, "LED_ON") == 0) {GPIO_ResetBits(GPIOA, GPIO_Pin_8); // 点亮LED} else if(strcmp(cmd, "LED_OFF") == 0) {GPIO_SetBits(GPIOA, GPIO_Pin_8); // 熄灭LED}// 可以添加更多命令,如"LED_TOGGLE" }串口与GPIO初始化
关键步骤包括使能时钟、配置GPIO为复用功能、设置串口参数(波特率115200、8位数据位、无校验位、1位停止位)以及使能中断。
ROS 2上位机程序实现
ROS 2节点负责监听键盘事件,并通过串口发送约定的指令。
创建功能包
cd ~/ros2_ws/src ros2 pkg create --build-type ament_python stm32_keyboard_control --dependencies rclpy核心代码逻辑(键盘控制节点)
创建一个Python文件(如
keyboard_control_node.py)。这个节点使用pyserial库进行串口通信,并监听键盘输入。#!/usr/bin/env python3 import serial import rclpy from rclpy.node import Node import termios import tty import sys import select import threading import timeclass KeyboardLEDControl(Node):def __init__(self):super().__init__('keyboard_led_control')# 参数声明self.declare_parameter('serial_port', '/dev/ttyUSB0')self.declare_parameter('baudrate', 115200)serial_port = self.get_parameter('serial_port').valuebaudrate = self.get_parameter('baudrate').value# 串口初始化try:self.serial_conn = serial.Serial(serial_port, baudrate, timeout=1)time.sleep(2) # 等待串口稳定self.get_logger().info(f'Successfully connected to {serial_port}')except Exception as e:self.get_logger().error(f'Could not open serial port: {e}')raise e# 保存原始终端设置,并设置为原始模式以立即捕获按键self.old_settings = termios.tcgetattr(sys.stdin)tty.setraw(sys.stdin.fileno())# 状态变量self.led_state = Falseself.is_running = True# 在独立线程中启动键盘监听,避免阻塞ROS2self.keyboard_thread = threading.Thread(target=self._keyboard_listener)self.keyboard_thread.daemon = Trueself.keyboard_thread.start()self.get_logger().info('Keyboard listener started. Press "1" to toggle LED, "Esc" to exit.')def _keyboard_listener(self):"""核心键盘监听循环"""while self.is_running and rclpy.ok():# 使用select非阻塞读取rlist, _, _ = select.select([sys.stdin], [], [], 0.1)if rlist:key = sys.stdin.read(1)self._handle_key_press(key)def _handle_key_press(self, key):"""处理按键"""if key == '1':self.toggle_led()elif key == '\\x1b': # ESC键self.get_logger().info("ESC pressed, shutting down...")self.is_running = False# 清理工作可在destroy_node中完成def toggle_led(self):"""切换LED状态并发送命令"""if self.led_state:command = "LED_OFF"self.led_state = Falseself.get_logger().info("Turning LED OFF")else:command = "LED_ON"self.led_state = Trueself.get_logger().info("Turning LED ON")self._send_command(command)def _send_command(self, command):"""通过串口发送命令"""try:full_command = command + '\\n'self.serial_conn.write(full_command.encode('utf-8'))self.serial_conn.flush()self.get_logger().debug(f'Sent: {command}')except Exception as e:self.get_logger().error(f'Failed to send command: {e}')def destroy_node(self):"""节点清理"""self.is_running = Falsetermios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_settings) # 恢复终端if hasattr(self, 'serial_conn') and self.serial_conn.is_open:self.serial_conn.close()super().destroy_node()def main(args=None):rclpy.init(args=args)node = KeyboardLEDControl()try:rclpy.spin(node)except KeyboardInterrupt:passfinally:node.destroy_node()rclpy.shutdown()if __name__ == '__main__':main()配置、编译与运行
修改
setup.py:确保entry_points中的console_scripts指向正确的节点。设置串口权限:每次重新插拔USB设备后可能需要执行
sudo chmod 666 /dev/ttyUSB0,或将用户加入dialout组永久解决 (sudo usermod -a -G dialout $USER,需重新登录)。编译功能包:
colcon build --packages-select stm32_keyboard_control运行节点:
ros2 run stm32_keyboard_control keyboard_control_node --ros-args -p serial_port:=/dev/ttyUSB0
通信协议设计
一个简单可靠的协议至关重要。本项目采用文本协议,优点是可读性好,便于调试。
指令格式:
"LED_ON\n"和"LED_OFF\n"。换行符\n作为帧结束符,帮助STM32判断一条指令是否接收完整。一致性:务必保证ROS 2节点发送的指令字符串与STM32端解析的字符串完全一致。
调试技巧与常见问题排查
STM32端独立测试
烧录程序后,使用串口调试助手(如
minicom、picocom或cutecom)直接向STM32发送LED_ON和LED_OFF字符串,验证STM32硬件和固件是否正常工作。在STM32端添加"回声"功能,将收到的数据原样发回,验证数据通路。
ROS 2节点调试
在
_send_command函数中添加详细日志,确认指令是否已生成并尝试发送。使用
ros2 topic list和ros2 topic echo命令检查节点是否正常运行。
常见问题
LED无反应:检查硬件连接(TX/RX是否接反、共地)、串口权限、波特率是否一致。
按键无响应:确保运行节点的终端窗口处于焦点状态。本方案使用原始终端模式,不依赖图形界面,可靠性更高。
数据错误:检查协议一致性,确保STM32端正确解析换行符。
总结与扩展
通过本项目,我们成功搭建了一个典型的ROS 2与微控制器通信的框架。这个框架可以轻松扩展:
控制更多设备:如电机、舵机、传感器等。
实现双向通信:让STM32将传感器数据(如温度、距离)发送回ROS 2,并在ROS 2中发布为话题。
引入更复杂的协议:如加入校验和确保数据可靠性,或使用二进制协议提高传输效率。
