如何使用ROS 2与STM32进行串口通信,并实现通过键盘按键‘1’来控制LED灯开关

系统架构概述

本项目的目标是构建一个分布式控制系统:一台运行ROS 2的计算机(上位机)通过串口向一颗STM32微控制器(下位机)发送指令,从而控制连接在STM32上的LED灯。键盘作为用户输入设备,由ROS 2节点监听,最终将按键事件转化为通过串口发送的指令。

整个系统的数据流可以清晰地用下图表示:

复制代码

核心组件

  • ROS2节点 ​ (keyboard_control_node):负责监听键盘事件,并通过串口发送控制指令。

  • 串口通信:基于UART协议,是ROS2与STM32之间的物理和数据链路。

  • STM32固件:负责解析指令并直接控制GPIO引脚。

硬件准备与连接

  1. 所需硬件

    • STM32开发板(如STM32F103C8T6)

    • USB转TTL串口模块(如CH340、CP2102)

    • LED灯及合适阻值的限流电阻(如220Ω)

    • 跳线、面包板若干

    • 运行Ubuntu和ROS2(Humble或更新版本)的电脑

  2. 硬件连接至关重要,务必准确

    • 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。

  1. 核心代码逻辑(主循环)

    以下代码展示了一个简单的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"
    }
  2. 串口与GPIO初始化

    关键步骤包括使能时钟、配置GPIO为复用功能、设置串口参数(波特率115200、8位数据位、无校验位、1位停止位)以及使能中断。

ROS 2上位机程序实现

ROS 2节点负责监听键盘事件,并通过串口发送约定的指令。

  1. 创建功能包

    bash 复制代码
    cd ~/ros2_ws/src
    ros2 pkg create --build-type ament_python stm32_keyboard_control --dependencies rclpy
  2. 核心代码逻辑(键盘控制节点)

    创建一个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()
  3. 配置、编译与运行

    • 修改 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端解析的字符串完全一致。

调试技巧与常见问题排查

  1. STM32端独立测试

    • 烧录程序后,使用串口调试助手(如minicompicocomcutecom)直接向STM32发送LED_ONLED_OFF字符串,验证STM32硬件和固件是否正常工作。

    • 在STM32端添加"回声"功能,将收到的数据原样发回,验证数据通路。

  2. ROS 2节点调试

    • _send_command函数中添加详细日志,确认指令是否已生成并尝试发送。

    • 使用 ros2 topic listros2 topic echo命令检查节点是否正常运行。

  3. 常见问题

    • LED无反应:检查硬件连接(TX/RX是否接反、共地)、串口权限、波特率是否一致。

    • 按键无响应:确保运行节点的终端窗口处于焦点状态。本方案使用原始终端模式,不依赖图形界面,可靠性更高。

    • 数据错误:检查协议一致性,确保STM32端正确解析换行符。

总结与扩展

通过本项目,我们成功搭建了一个典型的ROS 2与微控制器通信的框架。这个框架可以轻松扩展:

  • 控制更多设备:如电机、舵机、传感器等。

  • 实现双向通信:让STM32将传感器数据(如温度、距离)发送回ROS 2,并在ROS 2中发布为话题。

  • 引入更复杂的协议:如加入校验和确保数据可靠性,或使用二进制协议提高传输效率。

相关推荐
hazy1k2 小时前
51单片机基础-PWM、频率与占空比
stm32·单片机·嵌入式硬件·51单片机
逆小舟2 小时前
【STM32】智能排队控制系统
stm32·单片机·嵌入式硬件
GilgameshJSS3 小时前
STM32H743-ARM例程38-UART-IAP
c语言·arm开发·stm32·单片机·嵌入式硬件
清风6666664 小时前
基于单片机的交流功率测量仪设计与实现
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
做一道光6 小时前
6、foc控制——IF控制
笔记·单片机·嵌入式硬件·电机控制
Jerry丶Li7 小时前
二十三、STM32的ADC(三)(ADC多通道)
stm32·单片机·嵌入式硬件
d111111111d8 小时前
STM32外设学习--TIM定时器--编码器接口(程序)
笔记·stm32·嵌入式硬件·学习
辰哥单片机设计8 小时前
STM32项目分享:水质检测系统(升级版)
stm32·单片机·嵌入式硬件
straw_hat.11 小时前
32HAL——RTC时钟
stm32·学习