ROS2-嵌入式开发基础
单片机(单芯片微型计算机,微控制器,Microcontroller Unit,MCU)是一种集成了CPU、存储器和输入输出接口的芯片,广泛应用于嵌入式系统中。
实验采用的MicroROS开发板采用的单片机是ESP32芯片。ESPressif(乐鑫科技)开发的32位单片机,内置Wi-Fi和蓝牙功能。
对于ESP32芯片的开发,常用的开发环境有
- ESP-IDF(Espressif IoT Development Framework,乐鑫物联网开发框架):官方提供的开发环境,安全稳定,但教程少,工程复杂
- Arduino IDE:极具人气的开源软硬件开发平台,简单易用,教程丰富,适合初学者
- MicroPython:一种运行在单片机上的Python解释器,适合快速开发和测试,但性能较低。MicroROS不支持
PlatformIO是一款跨平台、跨架构、多框架的专业工具,可在VSCode中使用,支持Arduino、ESP-IDF、STM32等多种开发框架和芯片平台。
本实验安装PlatformIO for VSCode开发环境,使用Arduino框架进行开发。
安装PlatformIO开发环境
- 在VSCode扩展中安装
PlatformIO IDE插件。 - 点击左侧菜单栏的PlatformIO图标,进入PlatformIO主页,等待PlatformIO Core初始化完成。
WSL上运行PlatformIO在打开项目时存在路径解析问题:GitHub Issue
缓解方法:
- 用编辑器打开
/home/$USER/.platformio/packages/contrib-piohome/main.*********.min.js(也可能是/root/.platformio/packages/contrib-piohome/main.*********.min.js,根据安装时所用用户而定),其中*********类似3c12dd800fcfebb8bb4f6530e66a1eb60af2b278。 - 在代码中搜索
"\\":"/",将其替换为"/":"/"
编译测试
单片机开发分为四步:
- 编写程序
- 编译工程
- 烧录二进制文件到单片机
- 运行程序
新建一个项目,开发板选择Adafruit ESP32 Feather,框架选择Arduino。


第一次新建项目时,PlatformIO会自动下载所需的工具链和库文件,时间视网络环境而定,约15分钟。
一个PlatformIO项目结构如下:
.
├── .pio // PlatformIO的工作目录
│ └── build // 编译输出目录
├── .vscode // VSCode的配置目录
│ ├── c_cpp_properties.json
│ ├── extensions.json
│ └── launch.json
├── include // 头文件目录
│ └── README
├── lib // 库文件目录,将被编译链接
│ └── README
├── platformio.ini // PlatformIO项目的配置文件
├── src // 源代码目录
│ └── main.cpp
└── test // 测试代码目录
└── README
在platformio.ini中提高单片机主频以便后续使用:
; 编译环境
[env:featheresp32]
; 单片机平台
platform = espressif32
; 开发板型号
board = featheresp32
; 开发框架-arduino
framework = arduino
; 提高主频至240MHZ
board_build.f_cpu = 240000000L
src/main.cpp为项目的主程序文件
#include <Arduino.h>
// 函数声明:
int myFunction(int, int);
void setup() {
// 启动代码,运行一次
int result = myFunction(2, 3);
}
void loop() {
// 主循环代码,重复运行
}
// 函数定义:
int myFunction(int x, int y) {
return x + y;
}
PlatformIO IDE的操作按钮可在最下方的状态栏中找到,鼠标悬停查看说明。点击“√”按钮编译项目测试:

终端输出绿色success,表示编译成功。
串口测试
Hello World!
串口通信(Serial Communication)是指数据在计算机与设备之间按位(bit)顺序逐位传输的一种通信方式。
将开发板通过USB Type-C线连接到电脑(WSL连接USB)
在linux终端中输入lsusb命令(可使用sudo apt install usbutils安装),识别到CH340字样,则设备成功连接到电脑

使用ls /dev/ttyUSB*命令将其列出:

修改对其的读写权限:
# 临时修改
sudo chmod 666 /dev/ttyUSB0
# 永久修改
sudo usermod -a -G dialout $USER
sudo usermod -a -G plugdev $USER
编写源代码
#include <Arduino.h>
void setup() {
// 启动代码
Serial.begin(115200); /*设置波特率为115200比特每秒*/
}
void loop() {
// 循环代码
delay(1000); /*延迟1000ms*/
Serial.printf("Hello World!\n"); /*串口输出*/
}
编译(√按钮)并烧录(→按钮)到开发板,打开串口监视器(🔌按钮),观察输出:

由于串口监视器的波特率默认是9600,与设置的115200不一致,导致乱码。修改platformio.ini文件,添加监视器串口波特率设置:
monitor_speed = 115200
再次烧录并打开串口监视器,输出打印HelloWorld!,表示串口测试成功。

从串口接收单个数据
可使用Serial.read()函数从串口接收单个数据,返回值为读取到的字节数,若没有数据可读,则返回-1。
/**
* @file demo01_read_byte.cpp
* @author fishros@foxmail.com
* @brief 初始化串口,当有数据过来的时候读取并将数据打印出来
* @version 0.1
* @date 2022-12-18
*
* @copyright Copyright (c) 2022
*
*/
#include <Arduino.h>
void setup()
{
// 初始化串口
Serial.begin(115200);
}
void loop()
{
// 判断是否有有效数据,返回值是有效数据的长度
if (Serial.available())
{
// 读取一个数据
int c = Serial.read();
// -1 代表接收失败
if (c != -1)
{
// 以%c字符的格式输出接收的数据
Serial.printf("I receve %c\n", c);
}
}
}
烧录并打开串口监视器,在终端中输入字符。

从串口接收多个数据
使用Serial.readString()函数从串口一次接收多个数据,返回值为读取到的字符串,若没有数据可读,则返回空字符串。
/**
* @file demo01_read_byte.cpp
* @author fishros@foxmail.com
* @brief 初始化串口,当有数据过来的时候读取并将数据打印出来
* @version 0.1
* @date 2022-12-18
*
* @copyright Copyright (c) 2022
*
*/
#include <Arduino.h>
void setup()
{
// 初始化串口
Serial.begin(115200);
}
void loop()
{
// 判断是否有有效数据
if (Serial.available())
{
// 读取一个String字符串数据
String str = Serial.readString();
// 以%s的格式输出接收的数据
Serial.printf("I receve %s\n", str.c_str());
}
}
烧录并打开串口监视器,在终端中输入字符串。

LED测试
LED简介
LED(Light Emitting Diode,发光二极管)是一种半导体器件,具有单向导电性,当电流通过时会发光。
LED有单向导电性,而电流会从高电压端流向低电压端,控制LED灯工作,就是控制两侧的电压差。MicroROS开发板上有一个R2 LED灯,正极端有3.3V电压,设置负极端为低电平时,LED灯亮,高电平时,LED灯灭。
GPIO简介
GPIO(General Purpose Input/Output,通用输入输出)是单片机上最常见的接口之一,将数字世界与物理世界连接,GPIO口的基本模式是输入模式或输出模式:
- 输出模式(Output):芯片通过GPIO口输出高电平或低电平,控制外部设备(如LED)的工作状态。
- 输入模式(Input):芯片通过GPIO口读取外部设备的状态,获取外部设备的输入信号。
另外还有其它的高级模式,如:
- 上拉/下拉(Pull-up/Pull-down):在输入模式下,电平状态可能会受到外部干扰而不稳定。可以启用上拉(默认高电平)或下拉(默认低电平)电阻,使输入状态稳定。
- 中断(Interrupt):在输入模式下,当GPIO口的电平状态发生跳变时,可以触发中断服务程序,及时响应外部事件。
Arduino GPIO控制API
- 控制引脚模式
void pinMode(uint8_t pin, uint8_t mode);
- pin:引脚编号
- mode:引脚模式(INPUT、OUTPUT、INPUT_PULLUP、INPUT_PULLDOWN)
- 控制输出模式的引脚输出电平
void digitalWrite(uint8_t pin, uint8_t val);
- pin:引脚编号
- val:输出电平(HIGH、LOW)
- 控制读取INPUT模式引脚的电平
int digitalRead(uint8_t pin);
- pin:引脚编号
- 返回值:读取到的电平状态(HIGH、LOW)
编写代码控制LED
/**
* @file main.cpp
* @author fishros@foxmail.com
* @brief 使LED灯亮1s关闭1s,持续闪烁
* @version 0.1
* @date 2022-12-19
*
* @copyright Copyright (c) 2022
*
*/
#include <Arduino.h>
void setup()
{
pinMode(2, OUTPUT); // 设置2号引脚模式为OUTPUT模式
}
void loop()
{
digitalWrite(2, LOW); // 低电平,打开LED灯
delay(1000); // 休眠1000ms
digitalWrite(2, HIGH); // 高电平,关闭LED灯
delay(1000); // 休眠1000ms
}
编译烧录,可看到R2 LED灯闪烁。

配合串口通信,可通过串口输入命令控制LED灯的亮灭。
#include <Arduino.h>
void setup()
{
Serial.begin(115200);
pinMode(2, OUTPUT); // 设置2号引脚模式为OUTPUT模式
}
void loop()
{
// LED串口控制代码
if (Serial.available())
{
String command = Serial.readString();
if (command == "on")
{
Serial.printf("Receive: ON\n");
digitalWrite(2, LOW); // 低电平,打开LED灯
}
else if (command == "off")
{
Serial.printf("Receive: OFF\n");
digitalWrite(2, HIGH); // 高电平,关闭LED灯
}
}
}
按键测试

MicroROS开发板上有两个按键,分别是BOOT和RST,BOOT按键与0号引脚ESP_IO0连接。若按下按键,则0号引脚接地线(GND),为低电平,若松开按键,则连接高电压为高电平。
编写代码,按下按键时,LED灯亮,松开按键时,LED灯灭。
#include <Arduino.h>
void setup()
{
Serial.begin(115200);
pinMode(0, INPUT); // 设置0号引脚(连接BOOT)为INPUT
pinMode(2, OUTPUT); // 设置2号引脚(连接LED)为OUTPUT模式
}
void loop()
{
if (digitalRead(0) == LOW) // 若0号引脚低电平
{
Serial.println("LED ON");
digitalWrite(2, LOW); // 低电平,打开LED灯
}
else
{
Serial.println("LED OFF");
digitalWrite(2, HIGH); // 高电平,关闭LED灯
}
}

自锁按键代码,每按一次切换一次LED亮灭:
#include <Arduino.h>
bool status = false;
void setup()
{
Serial.begin(115200);
pinMode(0, INPUT); // 设置0号引脚(BOOT)为INPUT模式
pinMode(2, OUTPUT); // 设置2号引脚(LED)为OUTPUT模式
}
void loop()
{
// 自锁开关
if (digitalRead(0) == LOW)
{
delay(50); // 休眠50ms再次判断,防止误触
if (digitalRead(0) == LOW)
{
status = !status;
while (digitalRead(0) == LOW) // 死循环等待放开按键
;
}
}
if (status == true)
{
digitalWrite(2, LOW); // 低电平,打开LED灯
}
else
{
digitalWrite(2, HIGH); // 高电平,关闭LED灯
}
}
ADC
ADC(Analog to Digital Converter,模数转换器)是将模拟信号转换为数字信号的电子器件。我们可通过ADC模块测量电池电压,当电压低时提醒充电。
本实验所用的MicroROS开发板的ESP_IO34与ADC模块连接,由于该单片机的供电电压为3.3V,测量的电压范围最大不能超过3.3V,但电池电压和板子的供电电压分别是12V和5V,所以使用分压扩大测量范围:

由图可知,测量电压=引脚电压*5.02
Arduino ADC API
- ADC衰减系数
通过将输入引脚的电压衰减,再输入ADC模块,可扩大测量电压范围,我们采用最高的衰减比例ADC_11db,通过analogSetAttenuation(adc_attenuation_t attenuation)函数设置ADC衰减系数。
typedef enum {
ADC_0db,
ADC_2_5db,
ADC_6db,
ADC_11db,
} adc_attenuation_t;
- 读取ADC原始值
int analogRead(uint8_t pin);
- pin:引脚编号
- 返回值:读取到的ADC原始值,十二位分辨率,范围为0~4095,越高则表示电压越高
- 读取ADC电压值
uint32_t analogReadMilliVolts(uint8_t pin);
- pin:引脚编号
- 返回值:读取到的ADC电压值,单位为mV
#include <Arduino.h>
void setup()
{
Serial.begin(115200);
pinMode(34, INPUT); // 设置34号引脚为INPUT模式
analogSetAttenuation(ADC_11db); // 设置ADC衰减系数为ADC_11db,扩大测量电压范围
}
void loop()
{
int analogValue = analogRead(34); // 读取原始值0-4096
int analogVolts = analogReadMilliVolts(34); // 读取模拟电压,单位毫伏
float realVolts = 5.02 * ((float)analogVolts * 1e-3); // 计算实际电压值
Serial.printf("ADC analog value = %d\n", analogValue);
Serial.printf("ADC millivolts value = %d\n", analogVolts);
Serial.printf("realVolts value = %f\n", realVolts);
delay(100);
}
编译烧录并打开串口监视器,观察输出:

测得开发板供电电压约为5V,符合预期。
安装第三方库
- 可通过PIO安装:
Libraries -> 搜索库名称 -> 点击库 -> Add to Project -> 选择项目 -> Add

此时,platformio.ini文件中会自动添加库的依赖:
lib_deps = adafruit/Adafruit SSD1306@^2.5.17
安装的库位于项目目录下.pio/libdeps/featheresp32
- 可通过git安装:
将git地址添加到platformio.ini文件lib_deps中:
lib_deps =
adafruit/Adafruit SSD1306@^2.5.7
https://github.com/fishros/MPU6050_light.git
- 可手动安装到项目的lib目录下
驱动IMU-MPU6050_light库
IMU(Inertial Measurement Unit,惯性测量单元)是一种集成了加速度计、陀螺仪和磁力计的传感器模块,可用于测量物体的加速度、角速度和方向。
实验所用开发板上的MPU6050是一款常用的IMU芯片
项目依赖中通过git添加并下载该库https://github.com/fishros/MPU6050_light.git,使用示例可见该项目examples文件夹,文档见该项目documentation_MPU6050_light.pdf
I2C简介
I2C(Inter-Integrated Circuit,集成电路间通信)是一种串行通信协议,所有设备通过两根线进行通信:SDA(Serial Data Line)和SCL(Serial Clock)。本实验中通过引脚18和19连接MPU6050和OLED,它们都接在同一个I2C总线上。
- SDA:串行数据线,用于传输数据。数据可以在SDA线上双向传输,但同一时间只能朝一个方向发送(半双工)
- SCL:串行时钟线,用于同步数据传输。数据传输由SCL时钟信号同步,因此没有严格的波特率要求
在Arduino中,使用Wire.h库进行I2C通信,使用Wire.begin(sda, scl)函数初始化I2C总线,sda和scl分别为SDA和SCL引脚编号。
获取MPU6050数据
#include "Wire.h" // 导入I2C相关头文件
#include <MPU6050_light.h> // 导入MPU6050库
MPU6050 mpu(Wire); // 新建MPU6050对象mpu
unsigned long timer = 0;
void setup()
{
Serial.begin(115200);
Wire.begin(18, 19); // 初始化I2C,设置sda引脚为GPIO18,SCL引脚为GPIO19
byte status = mpu.begin(); // 检测IMU模块状态
Serial.print(F("MPU6050 status: "));
Serial.println(status);
while (status != 0)
{
delay(1000);
Serial.printf("Couldn't connect MPU6050\n");
} // stop everything if could not connect to MPU6050
Serial.println(F("Calculating offsets, do not move MPU6050"));
delay(1000);
mpu.calcOffsets(true, true); // 校准陀螺仪和加速度计,之后的数据会减去零偏
Serial.println("Done!\n");
}
void loop()
{
mpu.update(); // 读取mpu所有数据
if (millis() - timer > 1000)
{ // 每秒打印数据
Serial.print(F("TEMPERATURE: "));
Serial.println(mpu.getTemp()); // 温度
Serial.print(F("ACCELERO X: "));
Serial.print(mpu.getAccX()); // X轴加速度
Serial.print("\tY: ");
Serial.print(mpu.getAccY()); // Y轴加速度
Serial.print("\tZ: ");
Serial.println(mpu.getAccZ()); // Z轴加速度
Serial.print(F("GYRO X: "));
Serial.print(mpu.getGyroX()); // X轴 角速度
Serial.print("\tY: ");
Serial.print(mpu.getGyroY()); // Y轴 角速度
Serial.print("\tZ: ");
Serial.println(mpu.getGyroZ()); // Z轴 角速度
Serial.print(F("ACC ANGLE X: "));
Serial.print(mpu.getAccAngleX()); // X轴角加速度
Serial.print("\tY: ");
Serial.println(mpu.getAccAngleY()); // Y轴角加速度
Serial.print(F("ANGLE X: "));
Serial.print(mpu.getAngleX()); // X角度
Serial.print("\tY: ");
Serial.print(mpu.getAngleY()); // Y角度
Serial.print("\tZ: ");
Serial.println(mpu.getAngleZ()); // Z角度
Serial.println(F("=====================================================\n"));
timer = millis();
}
}
编译烧录并打开串口监视器,输出MPU数据,移动主板,数据随之变化,符合右手定则:

其中:
- X轴正方向:板上“丘比特箭头”所指方向
- Y轴正方向:箭头指向自己,正面朝上,右方
- Z轴正方向:正面朝上,上方
驱动OLED-SSD1306库
在platformio.ini中添加依赖adafruit/Adafruit SSD1306@^2.5.7
OLED(Organic Light Emitting Diode,有机发光二极管)是一种自发光显示器件,可作为嵌入式系统的人机交互界面,显示文字、图像和动画。
在本实验中,OLED屏幕型号为SSD1306,分辨率为128x64像素,使用I2C通信协议,设备地址为0x3C,18和19号引脚分别连接SDA和SCL。
编写代码,启动时OLED显示“Hello World”,之后显示MPU6050的角度数据。
/* Get tilt angles on X and Y, and rotation angle on Z
* Angles are given in degrees
*
* License: MIT
*/
#include "Wire.h"
#include <MPU6050_light.h>
#include <Adafruit_GFX.h> // 加载Adafruit_GFX库
#include <Adafruit_SSD1306.h> // 加载Adafruit_SSD1306库
Adafruit_SSD1306 display;
MPU6050 mpu(Wire);
unsigned long timer = 0;
void setup()
{
Serial.begin(115200);
Wire.begin(18, 19);
/*========================OLED初始化====================================*/
display = Adafruit_SSD1306(128, 64, &Wire);
display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // 设置OLED的I2C地址
display.clearDisplay(); // 清空屏幕
display.setTextSize(2); // 设置字体大小
display.setCursor(0, 0); // 设置开始显示文字的坐标
display.setTextColor(SSD1306_WHITE); // 设置字体颜色
display.println("hello oled!"); // 输出的字符
display.display();
/*========================IMU初始化====================================*/
byte status = mpu.begin();
Serial.print(F("MPU6050 status: "));
Serial.println(status);
while (status != 0)
{
} // stop everything if could not connect to MPU6050
Serial.println(F("Calculating offsets, do not move MPU6050"));
delay(1000);
// mpu.upsideDownMounting = true; // uncomment this line if the MPU6050 is mounted upside-down
mpu.calcOffsets(); // gyro and accelero
Serial.println("Done!\n");
}
void loop()
{
mpu.update();
if ((millis() - timer) > 100)
{ // print data every 100ms
Serial.print("X : ");
Serial.print(mpu.getAngleX());
Serial.print("\tY : ");
Serial.print(mpu.getAngleY());
Serial.print("\tZ : ");
Serial.println(mpu.getAngleZ());
timer = millis();
/*==========================OLED显示===========================*/
display.clearDisplay(); // 清空屏幕
display.setCursor(0, 0); // 设置开始显示文字的坐标
display.print("X="); // 输出X
display.println(mpu.getAngleX());
display.print("Y="); // 输出Y
display.println(mpu.getAngleY());
display.print("Z="); // 输出Z
display.println(mpu.getAngleZ());
display.display();
}
}
开发板上的OLED屏幕显示MPU6050的角度数据,移动开发板,数据随之变化。

另外,可直接通过I2C发送命令控制OLED显示文字、图像和动画,可参考https://github.com/fishros/fishbot-laser-control/blob/main/components/oled/oled.c
简易雷达-超声波测距
实验使用的超声波模块一共有四个引脚,分别是
- TRIG 即发送引脚,用于发送超声波
- ECHO 即接收引脚,用于接收反射回来的超声波
- VCC 电源接5V
- GND 电源地
当 TRIG 引脚接收到一个高电平脉冲时,会触发模块内部的超声波发射器发出一串 40 kHz 的超声波脉冲
pulseIn(Echo, HIGH) 函数会返回ECHO引脚一次高电平持续的微秒数,即超声波从发射到接收的时间差。
超声波测距公式:
距离 = 声速 * 往返时间 / 2
取声速 = 340m/s,往返时间以微秒us为单位,距离以米m为单位
则距离 = 340 * 时间(us) * 1e-6 / 2 = 时间(us) / 58 / 100
超声波测距
编写测试代码如下:
#include <Arduino.h>
// 定义引脚编号
#define Trig 27
#define Echo 21
void setup()
{
Serial.begin(115200);
pinMode(Trig, OUTPUT); // 发送引脚
pinMode(Echo, INPUT); // 接收引脚
}
void loop()
{
static double mtime;
digitalWrite(Trig, LOW); // 测量距离
delayMicroseconds(2); // 延时2us
digitalWrite(Trig, HIGH);
delayMicroseconds(10); // 产生一个10us的高脉冲去触发SR04
digitalWrite(Trig, LOW);
mtime = pulseIn(Echo, HIGH); // 检测脉冲宽度,注意返回值是微秒us
float detect_distance = mtime / 58.0 / 100.0; // 计算出距离,单位为米
Serial.printf("distance=%f\n", detect_distance);
delay(500);
}
编译下载,打开串口监视器,每隔500ms输出一次测量的距离,用手遮挡超声波模块,距离会随之变化。

留言