ROS2系列教程:嵌入式开发从点灯开始(原理)

🕒 阅读时间:16 分钟📝 字数:4023👀 阅读量:Loading...

本文是 ROS2 系列教程的第 26 篇

本文是 ROS2 系列教程的第 26 篇:嵌入式开发从点灯开始(原理)。第 25 篇我们完成了 Nav2 导航阶段(21-25)的全部内容,前面的教程全都是在仿真环境里运行:Gazebo 里的小车、RViz 里的模型,都是“虚拟的”。但真实的机器人要动起来,必须有硬件:单片机控制电机、传感器采集数据、串口与上位机通信。从本篇开始进入硬件控制阶段(第 26-30 篇),从嵌入式开发的最基础操作——“点灯”(GPIO 控制 LED)讲起,建立嵌入式开发的完整认知,为后续 MicroROS、ROS2-Control 打下基础。

一、为什么机器人需要嵌入式开发

1.1 上位机与下位机的分工

一台完整的机器人,通常由两大部分组成:

  • 上位机:性能更强的处理器,通常运行 Linux 和 ROS2,负责感知融合、路径规划、人机交互等“大脑级”工作。常见有树莓派、Jetson Nano、工控机、x86 迷你主机。
  • 下位机:嵌入式微控制器(MCU),负责电机驱动、传感器采集、执行器等“肌肉级”工作。常见有 STM32、ESP32、Arduino。

为什么要分工?因为两者的强项完全不同:

对比项 上位机(Linux + ROS2) 下位机(MCU)
计算能力 强,可跑复杂算法 弱,只适合简单逻辑
实时性 一般(非实时系统) 强(可做硬实时控制)
功耗 高 极低
启动时间 秒级 毫秒级
稳定性 依赖系统环境 极可靠,不易死机
成本 高 低

真实机器人常见的架构:上位机跑 ROS2,通过串口 / USB / CAN 与下位机通信,下位机直接控制电机和读取传感器。这种“大脑 + 肌肉”的分工,是机器人工程的主流做法。

1.2 为什么从“点灯”开始

几乎所有嵌入式开发教程的第一个程序都是“点灯”(控制 LED 亮灭),原因很朴素:

  1. 点灯用到了嵌入式开发最核心的资源:GPIO(通用输入输出引脚)。
  2. 点灯的程序虽然简单,却覆盖了完整的开发流程:写代码 → 编译 → 下载(烧录)→ 运行 → 调试。
  3. 任何复杂的机器人控制(电机 PWM、舵机、传感器读取),本质都是 GPIO 或基于 GPIO 扩展的外设操作。

可以说:会点灯,就摸到了嵌入式的大门。

二、认识单片机(MCU)

2.1 单片机是什么

单片机(Microcontroller Unit,MCU)是把 CPU、内存、Flash 存储、各种外设(GPIO、定时器、串口、ADC 等)集成在一个芯片上的微型计算机。它不像电脑需要外接内存和硬盘,一个芯片就是一个完整的微型系统。

在 ROS2 机器人领域,最常见的有两类:

  • STM32:ARM Cortex-M 内核,性能和生态都非常优秀,是机器人底盘、机械臂控制器的主流选择。使用 C 语言 + HAL 库或 LL 库开发。
  • ESP32:集成了 Wi-Fi 和蓝牙,价格便宜,适合做无线通信节点(如遥控手柄、传感器节点)。支持 C/C++(ESP-IDF、Arduino)和 MicroPython。

还有一类特殊的存在:Arduino。Arduino 严格说是“单片机 + 开发板 + 简化框架”的完整生态,封装极简,初学者最容易上手,很多 ROS 机器人项目(如 TurtleBot 的电机板)也用 Arduino。

2.2 开发板与芯片的关系

开发板(如 STM32F103C8T6 最小系统板、ESP32-DevKitC)是在芯片周围加上电源电路、晶振、USB 接口、调试接口等,方便我们直接使用。开发时我们面对的是“板子”,但编程时操作的是“芯片内部的寄存器与外设”。

一个典型的 STM32 最小系统板包含:

组成部分 作用
STM32 芯片 核心处理单元
晶振 提供时钟基准
USB 接口 供电 + 下载程序 + 虚拟串口
复位按键 复位程序
BOOT 跳线 选择启动模式
LED(通常 PC13 或 PA1) 点灯实验的标准对象

2.3 开发流程总览

不管用哪家芯片,嵌入式开发流程都一样:

编写代码(C/C++/MicroPython)
↓
编译(把源码变成机器码)
↓
烧录(把机器码下载进芯片 Flash)
↓
运行(上电后从 Flash 执行程序)
↓
调试(串口打印、调试器断点)

对应到工具上:STM32 用 STM32CubeIDE 或 VS Code + arm-none-eabi-gcc,ESP32 用 ESP-IDF 或 Arduino IDE,MicroPython 用 Thonny。

三、GPIO 与点灯原理

3.1 GPIO 是什么

GPIO(General Purpose Input/Output,通用输入输出)是单片机上最基础也最重要的引脚资源。每个 GPIO 引脚都可以配置为:

  • 输出模式:引脚输出高电平(如 3.3V)或低电平(0V),用来驱动 LED、继电器、蜂鸣器。
  • 输入模式:读取引脚电平(高或低),用来读取按键、传感器开关信号。
  • 复用功能:引脚被分配给串口、SPI、I2C、PWM 定时器等外设使用。

点灯实验,就是把某个引脚配置为推挽输出,然后交替输出高低电平,LED 就闪烁了。

3.2 LED 与限流电阻

LED(发光二极管)是有极性的元件:长脚是阳极(接正极),短脚是阴极(接负极或地)。LED 导通电压约 1.8-2.2V,工作电流约 5-20mA。如果直接接 3.3V 电源,电流过大,LED 会烧毁,所以必须串联一个限流电阻(通常 220Ω-1kΩ)。

电路很简单:

GPIO 引脚 ----[限流电阻 R]----|>|---- GND
LED(阳极朝 GPIO 侧)

电阻阻值计算:R = (Vcc - Vled) / I,例如 3.3V 供电、LED 压降 2V、电流 10mA:R = (3.3 - 2) / 0.01 = 130Ω,取常见值 220Ω 即可。

3.3 点灯的完整逻辑

初始化 GPIO(使能时钟、配置模式为输出、设置速度)
↓
循环:
输出高电平(LED 亮)
延时 500ms
输出低电平(LED 灭)
延时 500ms

这段逻辑,无论是 STM32 的 HAL 库、ESP32 的 Arduino 框架还是 MicroPython,都是一样的,只是 API 不同。

四、示例 1:STM32 寄存器版点灯(原理级)

在理解 HAL 库之前,先看寄存器版——直接操作芯片寄存器,这是最底层、最能理解原理的方式。以 STM32F103(Cortex-M3)为例,LED 接在 PC13 引脚(很多最小系统板板载 LED 就是 PC13)。

STM32 的 GPIO 操作分三步:使能 GPIOC 时钟 → 配置 PC13 为输出 → 写数据寄存器。

// 寄存器版点灯:直接操作 STM32F103 寄存器
#include "stm32f10x.h"
// 简化的寄存器地址定义(实际工程在头文件中已定义)
// RCC 时钟控制寄存器
#define RCC_APB2ENR (*(volatile unsigned int *)0x40021018)
// GPIOC 相关寄存器
#define GPIOC_CRH (*(volatile unsigned int *)0x40011004) // 配置寄存器高
#define GPIOC_ODR (*(volatile unsigned int *)0x4001100C) // 输出数据寄存器
// 简易延时(不精确,仅示意)
void delay(volatile unsigned int n) {
while (n--) { __asm volatile("nop"); }
}
int main(void) {
// 1. 使能 GPIOC 时钟(RCC_APB2ENR 的 bit4 = IOPCEN)
RCC_APB2ENR |= (1 << 4);
// 2. 配置 PC13 为推挽输出,最大速度 2MHz
// PC13 属于 CRH(引脚 8-15),每引脚占 4 位
// 模式位(MODE)=0b10(输出2MHz),配置位(CNF)=0b00(推挽输出)
GPIOC_CRH &= ~(0xF << (4 * (13 - 8))); // 先清空 PC13 的 4 位配置
GPIOC_CRH |= (0x2 << (4 * (13 - 8))); // 设为推挽输出
while (1) {
GPIOC_ODR |= (1 << 13); // PC13 输出高电平,LED 灭(板载 LED 低电平点亮)
delay(500000);
GPIOC_ODR &= ~(1 << 13); // PC13 输出低电平,LED 亮
delay(500000);
}
}

这段代码的价值在于:它展示了 GPIO 控制的三个本质步骤——使能时钟、配置模式、读写数据。HAL 库不过是把这些操作封装成了更友好的函数。

五、示例 2:STM32 HAL 库点灯(工程级)

寄存器直接操作虽然原理清晰,但工程开发中我们使用 HAL 库(Hardware Abstraction Layer),它把寄存器操作封装成函数,可读性和可移植性更好。这是目前 STM32 开发的主流方式。

// STM32 HAL 库点灯(CubeMX 生成的工程,仅保留核心逻辑)
#include "main.h"
// 延时 500ms
void delay_ms(uint32_t ms) {
HAL_Delay(ms);
}
int main(void) {
// 1. 初始化系统时钟(CubeMX 自动生成 SystemClock_Config)
HAL_Init();
SystemClock_Config();
// 2. 初始化 GPIO(CubeMX 生成的 MX_GPIO_Init)
MX_GPIO_Init();
// 内部实现大致是:
// __HAL_RCC_GPIOC_CLK_ENABLE(); // 使能时钟
// GPIO_InitStruct.Pin = GPIO_PIN_13;
// GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
// GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
// HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
while (1) {
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); // 低电平,LED 亮
delay_ms(500);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); // 高电平,LED 灭
delay_ms(500);
}
}

对比寄存器版可以看出:HAL 库把“使能时钟 + 配置模式 + 读写数据”封装成了 HAL_GPIO_Init 和 HAL_GPIO_WritePin,逻辑完全一样,但代码更清晰、跨芯片移植更容易(换芯片只需重新配置,不用重写逻辑)。

六、示例 3:ESP32 Arduino 点灯(C++ 风格)

ESP32 是 ROS2 机器人项目中很常见的无线节点。用 Arduino 框架开发时,代码风格是 C++,API 极其简洁,digitalWrite 一行就能控制引脚。

// ESP32 Arduino 框架点灯(Arduino IDE 或 PlatformIO)
// LED 接 GPIO2(很多 ESP32 开发板板载 LED 在 GPIO2)
#define LED_PIN 2
void setup() {
// 配置 GPIO2 为输出模式
pinMode(LED_PIN, OUTPUT);
// 初始化串口,便于调试
Serial.begin(115200);
Serial.println("ESP32 LED blink start");
}
void loop() {
// 输出高电平,LED 亮
digitalWrite(LED_PIN, HIGH);
delay(500); // 延时 500ms
// 输出低电平,LED 灭
digitalWrite(LED_PIN, LOW);
delay(500);
}

在 PlatformIO 工程中,还可以用 C++ 类封装 LED,方便复用:

// led.hpp:可复用的 LED 类
#ifndef LED_HPP
#define LED_HPP
#include <Arduino.h>
class Led {
public:
Led(uint8_t pin, bool active_high = true)
: pin_(pin), active_high_(active_high) {}
void init() {
pinMode(pin_, OUTPUT);
off();
}
void on() { digitalWrite(pin_, active_high_ ? HIGH : LOW); }
void off() { digitalWrite(pin_, active_high_ ? LOW : HIGH); }
void toggle() {
digitalWrite(pin_, !digitalRead(pin_));
}
private:
uint8_t pin_;
bool active_high_;
};
// main.cpp
Led led(2); // GPIO2 上接 LED,高电平点亮
int state = 0;
void setup() {
led.init();
}
void loop() {
state = !state;
if (state) led.on(); else led.off();
delay(500);
}
#endif

这个类的价值在于:init/on/off/toggle 的接口设计,和后面 ROS2 驱动里“控制 LED/指示灯的节点”接口天然一致——机器人上层只关心“开/关/翻转”,不关心具体引脚。

七、示例 4:MicroPython 点灯(Python 风格)

MicroPython 让 Python 直接运行在单片机上,语法简单,非常适合快速原型验证。ESP32 和部分 STM32 都支持 MicroPython。

# MicroPython 点灯(Thonny 编辑器运行,ESP32 为例)
from machine import Pin
from time import sleep
# 配置 GPIO2 为输出模式
led = Pin(2, Pin.OUT)
print("MicroPython LED blink start")
while True:
led.value(1) # 输出高电平,LED 亮
sleep(0.5) # 延时 500ms
led.value(0) # 输出低电平,LED 灭
sleep(0.5)

MicroPython 还有更“Python 风格”的写法,用定时器和回调:

from machine import Pin, Timer
led = Pin(2, Pin.OUT)
def blink(timer):
led.toggle() # 翻转电平
# 创建定时器,每 500ms 触发一次回调
timer = Timer(0)
timer.init(period=500, mode=Timer.PERIODIC, callback=blink)

注意:回调里不要做耗时操作(如 sleep),否则会阻塞其他任务。这种“定时器回调”的思想,和 ROS2 里 create_timer 定时器极其相似——理解一个,另一个就通了。

对于 ROS2 开发者来说,MicroPython 是理解“Python 在硬件上怎么跑”的最快路径,适合做传感器快速验证。

八、串口与调试(嵌入式开发必备技能)

8.1 串口是什么

串口(UART)是嵌入式最常用的调试和通信手段:单片机把调试信息通过串口发送给电脑,电脑用串口助手或 minicom 查看。在 ROS2 机器人里,串口也是上位机与下位机通信的主要方式(下一阶段会大量使用)。

串口通信的核心参数:

参数 常见值 说明
波特率 115200 每秒传输的比特数,两端必须一致
数据位 8 每个字节 8 位数据
停止位 1 帧结束标志
校验位 None 一般不使用

8.2 串口打印调试信息

STM32 HAL 库重定向 printf 到串口:

// 重定向 printf 到 USART1(需要先初始化 USART1)
#include <stdio.h>
int __io_putchar(int ch) {
// 向 USART1 发送一个字节(HAL 库方式)
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 100);
return ch;
}
// 使用示例
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE); // 使能接收中断(可选)
int count = 0;
while (1) {
printf("hello from stm32, count=%d\r\n", count++);
HAL_Delay(1000);
}
}

MicroPython 就简单得多,直接用 print:

from machine import Pin, UART
import time
uart = UART(1, baudrate=115200) # 初始化串口
led = Pin(2, Pin.OUT)
while True:
led.toggle()
uart.write("led toggled\r\n") # 发送到串口
print("count:", time.ticks_ms()) # print 也会走 REPL 串口
time.sleep(1)

调试铁律:嵌入式程序“看不到运行状态”,串口打印是观察程序行为的眼睛。遇到问题先加打印,再定位。

8.3 常见串口问题

  • 波特率不匹配:电脑显示乱码。检查两端波特率是否一致。
  • 串口号不对:Windows 下需要安装 CH340/CP2102 驱动,查看设备管理器确定串口号。
  • TX/RX 接反:下位机 TX 接上位机 RX,下位机 RX 接上位机 TX,交叉连接。
  • 共地:两个设备必须共地(GND 相连),否则通信不稳定。

九、嵌入式与 ROS2 的关系

9.1 嵌入式在 ROS2 生态中的位置

ROS2 本身运行在 Linux 上位机上,但 ROS2 机器人离不开嵌入式:

ROS2 上位机(大脑)
↕ 串口 / USB / CAN / Wi-Fi
嵌入式 MCU(肌肉)
↕ GPIO / PWM / ADC / 串口
电机、舵机、传感器(肢体)

嵌入式负责最底层的“电”的层面:给电机 PWM 信号、读编码器计数、采集 IMU 数据。ROS2 负责“信息”的层面:状态估计、路径规划、任务调度。

9.2 上位机如何“指挥”下位机

最常见的模式是串口协议:上位机通过串口发送命令帧(如 "CMD:FORWARD:SPEED=0.5"),下位机解析后执行电机控制;下位机定时回传传感器数据帧(如 "SENSOR:ENCODER=1234")。后面第 29-30 篇会详细实现这套协议。

9.3 MicroROS 预告

ROS2 官方提供了 MicroROS 方案:让 MCU 也能直接作为 ROS2 节点(通过串口/USB 与上位机的 micro-ROS Agent 桥接)。这样下位机可以直接发布/订阅话题,不用自己写协议。这是第 27 篇的主题,本篇先打好嵌入式基础。

十、常见问题与排障

现象 原因 解决
程序烧录不进芯片 未进 Boot 模式 / 驱动未装 / 接线错误 检查 BOOT0 跳线、安装驱动、检查 SWD/USB 接线
下载成功但 LED 不亮 引脚配错 / LED 极性接反 / 未使能时钟 核对原理图引脚、调换 LED 方向、检查时钟使能代码
串口乱码 波特率不一致 两端统一 115200
编译报错 undefined reference 链接库缺失 检查 Makefile/CMake 是否正确链接 HAL 库
程序运行不稳定、偶尔复位 电源供电不足 / 干扰 外部供电(勿只靠 USB)、加滤波电容
寄存器操作没反应 时钟未使能就操作外设 先使能对应外设时钟再配置寄存器

十一、小结与下一篇预告

本篇核心:

  1. 机器人 = 上位机(ROS2)+ 下位机(MCU)分工协作,嵌入式负责最底层的电机、传感器控制。
  2. 点灯是嵌入式开发的“Hello World”,本质是 GPIO 的时钟使能 → 模式配置 → 电平输出三步。
  3. 四种点灯方式:STM32 寄存器版(原理)、STM32 HAL 库(工程)、ESP32 Arduino(C++ 风格)、MicroPython(Python 风格),逻辑完全一致,只是 API 不同。
  4. 串口是嵌入式调试的必备手段,也是后续上位机/下位机通信的基础,务必掌握波特率、接线、乱码排查。

至此 硬件控制阶段(26-30)正式开启。下一篇(第 27 篇)进入 MicroROS 接入(原理+仿真):让单片机直接变成 ROS2 节点,用 ROS2 的话题和服务去控制 LED、读取传感器。我们会先用仿真环境跑通 MicroROS 完整流程,再为真机接入做准备。嵌入式 + ROS2 的奇妙组合,即将展开。

ROS2系列教程:嵌入式开发从点灯开始(原理)

作者:Wang Shijie

本文链接:https://wsj-blog.pages.dev/posts/ros2-xi-lie-jiao-cheng-qian-ru-shi-kai-fa-cong-dian-deng/

本文采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

Creative Commons

留言评论