返回目录

ROS2-嵌入式开发基础

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开发环境

  1. 在VSCode扩展中安装PlatformIO IDE插件。
  2. 点击左侧菜单栏的PlatformIO图标,进入PlatformIO主页,等待PlatformIO Core初始化完成。

WSL上运行PlatformIO在打开项目时存在路径解析问题:GitHub Issue

缓解方法:

  1. 用编辑器打开/home/$USER/.platformio/packages/contrib-piohome/main.*********.min.js(也可能是/root/.platformio/packages/contrib-piohome/main.*********.min.js,根据安装时所用用户而定),其中*********类似3c12dd800fcfebb8bb4f6530e66a1eb60af2b278
  2. 在代码中搜索"\\":"/",将其替换为"/":"/"

编译测试

单片机开发分为四步:

  1. 编写程序
  2. 编译工程
  3. 烧录二进制文件到单片机
  4. 运行程序

新建一个项目,开发板选择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字样,则设备成功连接到电脑

lsusb输出

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

lsttyusb

修改对其的读写权限:

# 临时修改
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

  1. 控制引脚模式
void pinMode(uint8_t pin, uint8_t mode);
  • pin:引脚编号
  • mode:引脚模式(INPUT、OUTPUT、INPUT_PULLUP、INPUT_PULLDOWN)
  1. 控制输出模式的引脚输出电平
void digitalWrite(uint8_t pin, uint8_t val);
  • pin:引脚编号
  • val:输出电平(HIGH、LOW)
  1. 控制读取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闪烁

配合串口通信,可通过串口输入命令控制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开发板上有两个按键,分别是BOOTRST,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

自锁按键代码,每按一次切换一次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,所以使用分压扩大测量范围:

ADC原理

由图可知,测量电压=引脚电压*5.02

Arduino ADC API

  1. ADC衰减系数

通过将输入引脚的电压衰减,再输入ADC模块,可扩大测量电压范围,我们采用最高的衰减比例ADC_11db,通过analogSetAttenuation(adc_attenuation_t attenuation)函数设置ADC衰减系数。

typedef enum {
    ADC_0db,
    ADC_2_5db,
    ADC_6db,
    ADC_11db,
} adc_attenuation_t;
  1. 读取ADC原始值
int analogRead(uint8_t pin);
  • pin:引脚编号
  • 返回值:读取到的ADC原始值,十二位分辨率,范围为0~4095,越高则表示电压越高
  1. 读取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);
}

编译烧录并打开串口监视器,观察输出:

ADC输出

测得开发板供电电压约为5V,符合预期。

安装第三方库

  1. 可通过PIO安装:

Libraries -> 搜索库名称 -> 点击库 -> Add to Project -> 选择项目 -> Add

PIO安装第三方库

此时,platformio.ini文件中会自动添加库的依赖:

lib_deps = adafruit/Adafruit SSD1306@^2.5.17

安装的库位于项目目录下.pio/libdeps/featheresp32

  1. 可通过git安装:

将git地址添加到platformio.ini文件lib_deps中:

lib_deps = 
    adafruit/Adafruit SSD1306@^2.5.7
    https://github.com/fishros/MPU6050_light.git
  1. 可手动安装到项目的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数据,移动主板,数据随之变化,符合右手定则:

MPU6050输出

其中:
- 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的角度数据,移动开发板,数据随之变化。

MPU6050+OLED输出

另外,可直接通过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输出一次测量的距离,用手遮挡超声波模块,距离会随之变化。

超声波测距

留言