系统架构概述
本项目的目标是构建一个分布式控制系统:一台运行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。
objectivec#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节点负责监听键盘事件,并通过串口发送约定的指令。
-
创建功能包
bashcd ~/ros2_ws/src ros2 pkg create --build-type ament_python stm32_keyboard_control --dependencies rclpy -
核心代码逻辑(键盘控制节点)
创建一个Python文件(如
keyboard_control_node.py)。这个节点使用pyserial库进行串口通信,并监听键盘输入。python#!/usr/bin/env python3 import serial import rclpy from rclpy.node import Node import termios import tty import sys import select import threading import time class 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').value baudrate = 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 = False self.is_running = True # 在独立线程中启动键盘监听,避免阻塞ROS2 self.keyboard_thread = threading.Thread(target=self._keyboard_listener) self.keyboard_thread.daemon = True self.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 = False self.get_logger().info("Turning LED OFF") else: command = "LED_ON" self.led_state = True self.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 = False termios.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: pass finally: 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中发布为话题。
-
引入更复杂的协议:如加入校验和确保数据可靠性,或使用二进制协议提高传输效率。