返回目录

ROS2-MicroROS

ROS2

Micro-ROS 即运行在微控制器上的 ROS 2,架构图如下:

Micro-ROS 架构图

上位机运行Agent代理,微控制器可以通过串口,蓝牙、以太网、Wifi等多种协议将数据传递给Agent,Agent再将其转换成ROS2的话题等数据,以此完成通信。

第一个节点

在docker中运行Micro-ROS的Agent:

sudo docker run -it --rm -v /dev:/dev -v /dev/shm:/dev/shm --privileged --net=host microros/micro-ros-agent:$ROS_DISTRO serial --dev /dev/ttyUSB0 -v6

Agent与烧录二进制文件不能同时进行

首次运行时会自动拉取microros/micro-ros-agent:$ROS_DISTRO镜像并运行,使用串口/dev/ttyUSB0与微控制器通信。

Micro-ROS Agent

新建一个PlatformIO项目,开发板为Adafruit ESP32 Feather,在platformio.ini中添加如下依赖:

lib_deps = 
    https://gitee.com/ohhuo/micro_ros_platformio.git

使用RCLC-API创建一个空节点,代码风格如下:

#include <Arduino.h>
#include <micro_ros_platformio.h>

#include <rcl/rcl.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>

rclc_executor_t executor;
rclc_support_t support;
rcl_allocator_t allocator;
rcl_node_t node;

void setup()
{
  Serial.begin(115200);
  // 设置通过串口进行MicroROS通信
  set_microros_serial_transports(Serial);
  // 延时时一段时间,等待设置完成
  delay(2000);
  // 初始化内存分配器
  allocator = rcl_get_default_allocator();
  // 创建初始化选项
  rclc_support_init(&support, 0, NULL, &allocator);
  // 创建节点 hello_microros
  rclc_node_init_default(&node, "hello_microros", "", &support);
  // 创建执行器
  rclc_executor_init(&executor, &support.context, 1, &allocator);
}

void loop()
{
  delay(100);
  // 循环处理数据
  rclc_executor_spin_some(&executor, RCL_MS_TO_NS(100));
}

编译并上传代码到开发板,再次运行Micro-ROS Agent,查看Agent的输出(若是没有输出,则按下开发板上RST按钮):

Micro-ROS Agent 输出

此时在上位机中可以看到/hello_microros节点。

Micro-ROS Node

Topic订阅

通过Topic控制LED灯的开关,代码如下:

#include <Arduino.h>
#include <micro_ros_platformio.h>

#include <rcl/rcl.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>

#include <std_msgs/msg/int32.h>

rclc_executor_t executor;
rclc_support_t support;
rcl_allocator_t allocator;
rcl_node_t node;
// 声明话题订阅者
rcl_subscription_t subscriber;
// 声明消息文件
std_msgs__msg__Int32 sub_msg;
// 定义话题接收回调函数
void callback_subscription_(const void *msgin)
{
  const std_msgs__msg__Int32 *msg = (const std_msgs__msg__Int32 *)msgin;
  if (msg->data == 0)
  {
    digitalWrite(2, HIGH); // 关灯
  }
  else
  {
    digitalWrite(2, LOW);  // 开灯
  }
}

void setup()
{
  Serial.begin(115200);
  // 设置通过串口进行MicroROS通信
  set_microros_serial_transports(Serial);
  // 延时时一段时间,等待设置完成
  delay(2000);
  // 初始化内存分配器
  allocator = rcl_get_default_allocator();
  // 创建初始化选项
  rclc_support_init(&support, 0, NULL, &allocator);
  // 创建节点 topic_sub_test
  rclc_node_init_default(&node, "topic_sub_test", "", &support);
  // 订阅者初始化
  rclc_subscription_init_default(
      &subscriber,
      &node,
      ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Int32),
      "led_control");
  // 创建执行器
  rclc_executor_init(&executor, &support.context, 1, &allocator);
  // 为执行器添加一个订阅者
  rclc_executor_add_subscription(&executor, &subscriber, &sub_msg, &callback_subscription_, ON_NEW_DATA);
  // 初始化LED
  pinMode(2, OUTPUT);
}

void loop()
{
  delay(100);
  // 循环处理数据
  rclc_executor_spin_some(&executor, RCL_MS_TO_NS(100));
}

该代码实现了一个订阅者节点topic_sub_test,订阅话题led_control,接收到的消息为std_msgs/msg/Int32类型,若接收到的消息为0,则关闭LED灯,否则打开LED灯。

编译并上传代码到开发板,运行Micro-ROS Agent,此时在上位机中可以看到led_control话题,使用如下命令发布消息控制LED灯:

# 关闭LED灯
ros2 topic pub /led_control std_msgs/msg/Int32 "{data: 0}" --once
# 打开LED灯
ros2 topic pub /led_control std_msgs/msg/Int32 "{data: 1}" --once

Micro-ROS Topic

Topic发布

编写代码发布电池电压到话题battery_voltage,代码如下:

#include <Arduino.h>
#include <micro_ros_platformio.h>

#include <rcl/rcl.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>
// 消息类型头文件
#include <std_msgs/msg/float32.h>

rclc_executor_t executor;
rclc_support_t support;
rcl_allocator_t allocator;
rcl_node_t node;
rcl_timer_t timer;

// 声明话题发布者
rcl_publisher_t publisher;
// 声明消息文件
std_msgs__msg__Float32 pub_msg;

// 定义定时器接收回调函数
void timer_callback(rcl_timer_t *timer, int64_t last_call_time)
{
  RCLC_UNUSED(last_call_time);
  if (timer != NULL)
  {
    rcl_publish(&publisher, &pub_msg, NULL);
  }
}

void setup()
{
  Serial.begin(115200);
  // 设置通过串口进行MicroROS通信
  set_microros_serial_transports(Serial);
  // 延时时一段时间,等待设置完成
  delay(2000);
  // 初始化内存分配器
  allocator = rcl_get_default_allocator();
  // 创建初始化选项
  rclc_support_init(&support, 0, NULL, &allocator);
  // 创建节点 topic_pub_test
  rclc_node_init_default(&node, "topic_pub_test", "", &support);
  // 发布者初始化,发布std_msgs/msg/Float32消息到/battery_voltage
  rclc_publisher_init_default(
      &publisher,
      &node,
      ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Float32),
      "battery_voltage");

  // 创建定时器,200ms发一次
  const unsigned int timer_timeout = 200;
  rclc_timer_init_default(
      &timer,
      &support,
      RCL_MS_TO_NS(timer_timeout),
      timer_callback);

  // 创建执行器
  rclc_executor_init(&executor, &support.context, 1, &allocator);
  // 给执行器添加定时器
  rclc_executor_add_timer(&executor, &timer);
  // 初始化ADC
  pinMode(34, INPUT);
  analogSetAttenuation(ADC_11db);
}

void loop()
{
  delay(100);
  // 循环处理数据
  rclc_executor_spin_some(&executor, RCL_MS_TO_NS(100));
  // 通过ADC获取电压值
  // int analogValue = analogRead(34);                     // 读取原始值0-4096
  int analogVolts = analogReadMilliVolts(34);           // 读取模拟电压,单位毫伏
  float realVolts = 5.02 * ((float)analogVolts * 1e-3); // 计算实际电压值
  pub_msg.data = realVolts; // 填充到发布消息结构
}

在该代码中,创建了一个发布者节点topic_pub_test,发布话题battery_voltage,消息类型为std_msgs/msg/Float32,通过ADC读取电压值并使用定时器发布。

编译并上传代码到开发板,运行Micro-ROS Agent,Agent日志中可看到持续接收到的数据,此时在上位机中可以看到battery_voltage话题,使用topic echo查看该话题消息:

ros2 topic echo /battery_voltage

Micro-ROS Topic Echo

电压约为5V,符合预期。

Service

为节点添加两数相加示例服务,代码如下:

#include <Arduino.h>
#include <micro_ros_platformio.h>

#include <rcl/rcl.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>

// 添加接口
#include <example_interfaces/srv/add_two_ints.h>

rclc_executor_t executor;
rclc_support_t support;
rcl_allocator_t allocator;
rcl_node_t node;

// 定义服务
rcl_service_t service;

// 服务请求和返回消息定义
example_interfaces__srv__AddTwoInts_Request req;
example_interfaces__srv__AddTwoInts_Response res;

// 服务回调函数
void service_callback(const void *req, void *res)
{
  example_interfaces__srv__AddTwoInts_Request *req_in = (example_interfaces__srv__AddTwoInts_Request *)req;
  example_interfaces__srv__AddTwoInts_Response *res_in = (example_interfaces__srv__AddTwoInts_Response *)res;
  // 计算sum
  res_in->sum = req_in->a + req_in->b;
}

void setup()
{
  Serial.begin(115200);
  // 设置通过串口进行MicroROS通信
  set_microros_serial_transports(Serial);
  // 延时时一段时间,等待设置完成
  delay(2000);
  // 初始化内存分配器
  allocator = rcl_get_default_allocator();
  // 创建初始化选项
  rclc_support_init(&support, 0, NULL, &allocator);
  // 创建节点 hello_microros
  rclc_node_init_default(&node, "service_test", "", &support);
  // 使用默认配置创建服务
  rclc_service_init_default(&service, &node, ROSIDL_GET_SRV_TYPE_SUPPORT(example_interfaces, srv, AddTwoInts), "/addtwoints");
  // 创建执行器
  rclc_executor_init(&executor, &support.context, 1, &allocator);
  // 执行器添加服务
  rclc_executor_add_service(&executor, &service, &req, &res, service_callback);
}

void loop()
{
  delay(100);
  // 循环处理数据
  rclc_executor_spin_some(&executor, RCL_MS_TO_NS(100));
}

在该代码中,创建了一个服务节点service_test,提供服务/addtwoints,其中请求与响应的消息类型分别定义为

typedef struct example_interfaces__srv__AddTwoInts_Request
{
  int64_t a;
  int64_t b;
} example_interfaces__srv__AddTwoInts_Request;

typedef struct example_interfaces__srv__AddTwoInts_Response
{
  int64_t sum;
} example_interfaces__srv__AddTwoInts_Response;

此时在上位机中可以看到/addtwoints服务,使用如下命令调用该服务:

ros2 service call /addtwoints example_interfaces/srv/AddTwoInts "{a: 1, b: 2}"

Micro-ROS Service Call

Interface

本节尝试自定义接口,并使用该接口创建服务控制OLED显示指定文字

添加依赖

创建一个新的PlatformIO项目,开发板为Adafruit ESP32 Feather,在platformio.ini中添加如下依赖:

lib_deps = 
    https://gitee.com/ohhuo/micro_ros_platformio.git
    adafruit/Adafruit SSD1306@^2.5.7

创建自定义接口功能包

在工程目录下创建名为extra_packages的文件夹,该文件夹中的ros包会被添加进编译过程。在extra_packages中创建ros功能包:

mkdir extra_packages
cd extra_packages 
ros2 pkg create fishbot_interfaces

添加自定义接口文件extra_packages/fishbot_interfaces/srv/OledControl.srv,内容如下:

int32 px
int32 py
string data
---
int32 result

修改功能包CMakeLists.txt,添加如下内容:

find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "srv/OledControl.srv"
 )

修改功能包package.xml,添加如下内容:

<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>

编译功能包:

cd extra_packages 
colcon build --packages-select fishbot_interfaces

重新编译

删除工程目录下的.pio/libdeps/featheresp32/micro_ros_platformio/libmicroros文件夹,重新编译,以添加extra_packages中的自定义接口

若过程中fishros服务器返回503错误

fatal: unable to access 'http://github.fishros.org/https://github.com/ament/ament_cmake/': The requested URL returned error: 503

可尝试换用micro-ROS-platformio官方源,并进行如下修改:

  1. 修改依赖中的micro_ros_platformio库地址为官方源:https://github.com/micro-ROS/micro_ros_platformio.git
  2. 出现SetuptoolsDeprecationWarning,要屏蔽该警告可降级Setuptools版本:
# 使用实际的penv路径执行
/root/.platformio/penv/bin/pip install "setuptools<70.0.0"
  1. 出现rmw_test_fixture CMake Error,要跳过该错误可跳过编译该桌面端测试包
# 方法一:
touch .pio/libdeps/featheresp32/micro_ros_platformio/build/dev/src/ament_cmake_ros/rmw_test_fixture/COLCON_IGNORE
# 方法二:在所有名为rmw_test_fixture的文件夹中创建COLCON_IGNORE文件
find .pio -name "rmw_test_fixture" -type d -exec touch {}/COLCON_IGNORE \;
  1. 再次重新编译,若出错则需删除已编译build文件夹后重新编译。

创建服务节点

创建服务节点hello_interface,提供服务/oled_control,代码如下:

#include <Arduino.h>
#include <micro_ros_platformio.h>

#include <rcl/rcl.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>
#include <micro_ros_utilities/string_utilities.h> // string工具类,用于为string类型消息分配空间

#include "Wire.h"
#include <Adafruit_GFX.h>     // 加载Adafruit_GFX库
#include <Adafruit_SSD1306.h> // 加载Adafruit_SSD1306库

#include <fishbot_interfaces/srv/oled_control.h> // 接口文件

rclc_executor_t executor;
rclc_support_t support;
rcl_allocator_t allocator;
rcl_node_t node;
// 定义服务
rcl_service_t service;

// 服务请求和返回消息定义
fishbot_interfaces__srv__OledControl_Request req;
fishbot_interfaces__srv__OledControl_Response res;

Adafruit_SSD1306 display;

// 服务回调函数
void service_callback(const void *req, void *res)
{
  // 将void指针转换为具体类型
  fishbot_interfaces__srv__OledControl_Request *req_in = (fishbot_interfaces__srv__OledControl_Request *)req;
  fishbot_interfaces__srv__OledControl_Response *res_in = (fishbot_interfaces__srv__OledControl_Response *)res;
  // 执行显示操作
  display.clearDisplay();                    // 清空屏幕
  display.setCursor(req_in->px, req_in->py); // 设置开始显示文字的坐标
  display.println(req_in->data.data);        // 输出的字符
  display.display();
  res_in->result = 0;
}

void setup()
{
  Serial.begin(115200);
  // 设置通过串口进行MicroROS通信
  set_microros_serial_transports(Serial);
  // 延时时一段时间,等待设置完成
  delay(2000);
  // 初始化内存分配器
  allocator = rcl_get_default_allocator();
  // 创建初始化选项
  rclc_support_init(&support, 0, NULL, &allocator);
  // 创建节点 hello_interface
  rclc_node_init_default(&node, "hello_interface", "", &support);
  // 使用默认配置创建服务
  rclc_service_init_default(&service, &node, ROSIDL_GET_SRV_TYPE_SUPPORT(fishbot_interfaces, srv, OledControl), "/oled_control");
  // 创建执行器
  rclc_executor_init(&executor, &support.context, 1, &allocator);
  // 执行器添加服务
  rclc_executor_add_service(&executor, &service, &req, &res, service_callback);
  // 重要,为string类型消息分配空间
  req.data = micro_ros_string_utilities_init_with_size(100);

  /*========================OLED初始化====================================*/
  Wire.begin(18, 19);
  display = Adafruit_SSD1306(128, 64, &Wire);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // 设置OLED的I2C地址
  display.clearDisplay();                    // 清空屏幕
  display.setTextSize(1);                    // 设置字体大小
  display.setCursor(0, 0);                   // 设置开始显示文字的坐标
  display.setTextColor(SSD1306_WHITE);       // 设置字体颜色
  display.println("hello fishros!");         // 输出的字符
  display.display();
}

void loop()
{
  delay(100);
  // 循环处理数据,等待服务调用
  rclc_executor_spin_some(&executor, RCL_MS_TO_NS(100));
}

该代码创建了一个服务节点hello_interface,提供服务/oled_control,其中micro_ros_string_utilities_init_with_size为string类型消息分配了空间

编译烧录代码到开发板,运行Micro-ROS Agent,此时在上位机中可以看到/oled_control服务,使用如下命令调用该服务:

source 项目路径/extra_packages/install/setup.bash # 加载自定义接口环境
ros2 service call /oled_control fishbot_interfaces/srv/OledControl "{px: 0, py: 0 , data: 'nihao'}" # 以(0,0)为起点显示文字"nihao"

Micro-ROS Service Call

Micro-ROS OLED Display

Time库

本节尝试使用Time库获取时间,并将时钟显示在OLED屏幕上。

Time库函数常见用法:
- year()/month()/day()/hour()/minute()/second():获取当前时间的年/月/日/时/分/秒,返回值为整数int
- rmw_uros_epoch_synchronized():判断时间是否同步
- rmw_uros_sync_session(timeout_ms):使用NTP协议同步时间,参数为超时时间
- rmw_uros_epoch_millis():获取当前时间,单位为毫秒
- setTime(time_t t):设置当前时间,参数为秒

时钟同步测试

添加依赖paulstoffregen/Time@^1.6.1

lib_deps = 
    https://gitee.com/ohhuo/micro_ros_platformio.git
    adafruit/Adafruit SSD1306@^2.5.7
    paulstoffregen/Time@^1.6.1

编写代码:

#include <Arduino.h>
#include <micro_ros_platformio.h>

#include <rcl/rcl.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>

#include <TimeLib.h>          // 加载时间库,提供setTime\year\month...函数
#include <Adafruit_GFX.h>     // 加载Adafruit_GFX库
#include <Adafruit_SSD1306.h> // 加载Adafruit_SSD1306库
Adafruit_SSD1306 display;     // 声明对象

rclc_executor_t executor;
rclc_support_t support;
rcl_allocator_t allocator;
rcl_node_t node;

const int timeout_ms = 1000;  // 超时时间:1s
static int64_t time_ms;       // 毫秒
static time_t time_seconds;   // 秒
char time_str[25];            // 时间显示的字符串

void setup()
{
  Serial.begin(115200);
  // 设置通过串口进行MicroROS通信
  set_microros_serial_transports(Serial);
  // 延时时一段时间,等待设置完成
  delay(2000);
  // 初始化内存分配器
  allocator = rcl_get_default_allocator();
  // 创建初始化选项
  rclc_support_init(&support, 0, NULL, &allocator);
  // 创建节点 time_sync
  rclc_node_init_default(&node, "time_sync", "", &support);
  // 创建执行器
  rclc_executor_init(&executor, &support.context, 1, &allocator);

  Wire.begin(18, 19);
  display = Adafruit_SSD1306(128, 64, &Wire);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // 设置OLED的I2C地址,默认0x3C
  display.setTextSize(2);                    // 设置字体大小,最小为1
  display.clearDisplay();                    // 清空屏幕
  display.setCursor(0, 0);                   // 设置开始显示文字的坐标
  display.setTextColor(SSD1306_WHITE);       // 设置字体颜色
  display.println("hello oled!");            // 输出的字符
}

void loop()
{
  /*=========================同步时间=====================================*/
  while (!rmw_uros_epoch_synchronized()) // 判断时间是否同步
  {
    rmw_uros_sync_session(timeout_ms); //  同步时间
    if (rmw_uros_epoch_synchronized())
    {
      time_ms = rmw_uros_epoch_millis(); // 获取当前时间(毫秒)
      time_seconds = time_ms / 1000;     // 转换为秒
      setTime(time_seconds + 8 * 3600); // 将当前时间+8H到北京时间然后设置到系统
    }
    delay(10);
    return;
  }

  /*========================获取时间与显示==================================*/
  sprintf(time_str, "%04d-%02d-%02d %02d:%02d:%02d ", year(), month(), day(), hour(), minute(), second());

  display.clearDisplay();    // 清空屏幕
  display.setCursor(00, 0);  // 设置开始显示文字的坐标
  display.println(time_str); // 输出的字符
  display.display();
  delay(100);
}

编译并上传代码到开发板,运行Micro-ROS Agent,此时在OLED屏幕上可以看到当前时间,看不到可点击RST按钮。

Micro-ROS OLED Time

无线通信-WiFi

本节尝试使用WiFi替代串口进行Micro-ROS通信。

相较于串口通信的set_microros_serial_transports函数,WiFi通信需要在代码中设置Agent的IP地址和端口号,函数声明如下

// 设置wifi名称、密码,Agent IP、端口号
void set_microros_wifi_transports(char * ssid, char * pass, IPAddress agent_ip, uint16_t agent_port)

无线通信测试

修改platformio.ini,添加依赖并配置WiFi通信:

board_microros_transport = wifi
lib_deps = 
    https://gitee.com/ohhuo/micro_ros_platformio.git
    adafruit/Adafruit SSD1306@^2.5.7
    paulstoffregen/Time@^1.6.1

使用WiFi通信同步时钟,示例代码如下:

#include <Arduino.h>
#include <micro_ros_platformio.h>

#include <rcl/rcl.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>

#include <WiFi.h>   // 无线通信库

#include <TimeLib.h>          // 加载时间库,提供setTime\year\month...函数
#include <Adafruit_GFX.h>     // 加载Adafruit_GFX库
#include <Adafruit_SSD1306.h> // 加载Adafruit_SSD1306库
Adafruit_SSD1306 display;     // 声明对象

rclc_executor_t executor;
rclc_support_t support;
rcl_allocator_t allocator;
rcl_node_t node;

const int timeout_ms = 1000;  // 超时时间:1s
static int64_t time_ms;       // 毫秒
static time_t time_seconds;   // 秒
char time_str[25];            // 时间显示的字符串

void setup()
{
  Serial.begin(115200);
  delay(1000); // 等待串口监视器打开
  Serial.println("\n===== Micro-ROS Time Sync OLED =====");

  // ---------- WiFi 连接与调试 ----------
  IPAddress agent_ip;
  agent_ip.fromString("10.28.8.94");  // 替换为实际的Agent运行主机的IP地址
  Serial.print("目标 Agent IP: ");
  Serial.println(agent_ip);

  Serial.println("正在启动 WiFi 传输...");
  // 设置 WiFi 名称,密码,Agent IP,端口号
  set_microros_wifi_transports("fish", "12345678", agent_ip, 8888);

  // 等待 WiFi 连接,并输出状态
  int connect_attempts = 0;
  while (WiFi.status() != WL_CONNECTED && connect_attempts < 20) {
    delay(1000);
    Serial.print(".");
    connect_attempts++;
  }
  Serial.println();

  if (WiFi.status() == WL_CONNECTED) {
    Serial.print("当前 IP 地址: ");
    Serial.println(WiFi.localIP());
  } else {
    Serial.println("WiFi 连接失败");
    while (1) {
      delay(1000);
    }
  }

  // 初始化内存分配器
  allocator = rcl_get_default_allocator();
  // 创建初始化选项
  rclc_support_init(&support, 0, NULL, &allocator);
  // 创建节点 wifi_test
  rclc_node_init_default(&node, "wifi_test", "", &support);
  // 创建执行器
  rclc_executor_init(&executor, &support.context, 1, &allocator);

  Wire.begin(18, 19);
  display = Adafruit_SSD1306(128, 64, &Wire);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // 设置OLED的I2C地址,默认0x3C
  display.setTextSize(2);                    // 设置字体大小,最小为1
  display.clearDisplay();                    // 清空屏幕
  display.setCursor(0, 0);                   // 设置开始显示文字的坐标
  display.setTextColor(SSD1306_WHITE);       // 设置字体颜色
  display.println("hello oled!");            // 输出的字符
  display.display();

  Serial.println("初始化完成,进入 loop...");
}

void loop()
{
  /*=========================同步时间=====================================*/
  if (!rmw_uros_epoch_synchronized()) {       // 判断时间是否同步
    rmw_uros_sync_session(timeout_ms);        // 同步时间
    if (rmw_uros_epoch_synchronized()) {
      time_ms = rmw_uros_epoch_millis();      // 获取当前时间(毫秒)
      time_seconds = time_ms / 1000;          // 转换为秒
      setTime(time_seconds + 8 * 3600);       // 转换为北京时间
      Serial.println("时间同步成功");
    }
    delay(10);
    return;
  }

  /*========================获取时间与显示==================================*/
  sprintf(time_str, "%04d-%02d-%02d %02d:%02d:%02d ", year(), month(), day(), hour(), minute(), second());

  display.clearDisplay();    // 清空屏幕
  display.setCursor(0, 0);   // 设置开始显示文字的坐标
  display.println(time_str); // 输出的字符
  display.display();
  delay(100);
}

将Agent的IP地址修改为实际的Agent运行主机的IP地址,编译并上传代码到开发板,

运行Micro-ROS Agent的命令与串口不同,需切换通信方式(UDP,IPv4,监听端口8888):

docker run -it --rm -v /dev:/dev -v /dev/shm:/dev/shm --privileged --net=host microros/micro-ros-agent:$ROS_DISTRO udp4 --port 8888 -v6

Agent会接收到来自开发板的WiFi通信数据,并同步时间,OLED屏幕上会显示当前时间。

Micro-ROS OLED Time WiFi

上位机中可以看到/wifi_test节点。

Micro-ROS OLED Time WiFi

注意事项:
1. 确保开发板与运行Agent的主机连接同一WiFi网络。
2. 确保临时关闭防火墙,或配置规则允许UDP端口8888的通信。
3. 若使用WSL2运行Agent,需配置端口映射或使用mirror网络模式。

双核运行

ESP32单片机有两个内核,所有的外设都通过一个总线连接到两个内核上,也就是说,程序无论在哪个核上运行都可以操作硬件。

常用函数:
- xPortGetCoreID(): 获取当前运行的核心ID,返回值为0或1
- xTaskCreatePinnedToCore():创建任务并绑定到指定核心上运行。

xTaskCreatePinnedToCore()函数原型:

BaseType_t xTaskCreatePinnedToCore(
    TaskFunction_t pvTaskCode, // 任务函数
    const char * const pcName, // 任务名称
    const uint32_t usStackDepth, // 任务栈大小
    void * const pvParameters, // 传递给任务函数的参数
    UBaseType_t uxPriority, // 任务优先级
    TaskHandle_t * const pvCreatedTask, // 返回创建的任务句柄
    const BaseType_t xCoreID // 指定运行的核心ID,0或1
);

双核运行测试

修改platformio.ini,提高主频:

board_build.f_cpu = 240000000L
board_microros_transport = wifi
lib_deps = 
    https://gitee.com/ohhuo/micro_ros_platformio.git

测试双核可用,代码如下:

#include <Arduino.h>

void microros_task(void *param)
{
  while (true)
  {
    delay(1000);
    Serial.printf("microros_task on core:%d\n", xPortGetCoreID());
  }
}

void setup()
{
  Serial.begin(115200);
  xTaskCreatePinnedToCore(microros_task, "microros_task", 10240, NULL, 1, NULL, 0);
}

void loop()
{
  delay(1000);
  Serial.printf("loop on core:%d\n", xPortGetCoreID());
}

以上代码创建了一个任务microros_task,绑定到核心0上运行,而loop()函数默认在核心1上运行。

编译并上传代码到开发板,打开串口监视器,能看到两个核心交替输出的日志:

microros_task on core:0
loop on core:1
microros_task on core:0
loop on core:1
...

在Micro-ROS中使用双核,代码如下:

#include <Arduino.h>
#include <micro_ros_platformio.h>
#include <WiFi.h>
#include <rcl/rcl.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>

rclc_executor_t executor;
rclc_support_t support;
rcl_allocator_t allocator;
rcl_node_t node;

void microros_task(void *param)
{
  // 设置通过WIFI进行MicroROS通信
  IPAddress agent_ip;
  agent_ip.fromString("10.28.8.94");
  // 设置wifi名称,密码,电脑IP,端口号
  set_microros_wifi_transports("fish", "lhysz250213", agent_ip, 8888);
  // 延时时一段时间,等待设置完成
  delay(2000);
  // 初始化内存分配器
  allocator = rcl_get_default_allocator();
  // 创建初始化选项
  rclc_support_init(&support, 0, NULL, &allocator);
  // 创建节点 microros_wifi
  rclc_node_init_default(&node, "microros_wifi", "", &support);
  // 创建执行器
  rclc_executor_init(&executor, &support.context, 1, &allocator);
  while (true)
  {
    delay(100);
    Serial.printf("microros_task on core:%d\n", xPortGetCoreID());
    // 循环处理数据
    rclc_executor_spin_some(&executor, RCL_MS_TO_NS(100));
  }
}

void setup()
{
  Serial.begin(115200);
  xTaskCreatePinnedToCore(microros_task, "microros_task", 10240, NULL, 1, NULL, 0);
}

void loop()
{
  delay(1000);
  Serial.printf("do some control on core:%d\n", xPortGetCoreID());
}

以上代码创建了一个任务microros_task,创建了一个空节点microros_wifi绑定到核心0上运行,而loop()函数默认在核心1上运行,loop()函数中可以执行一些控制逻辑。

编译并上传代码到开发板,可以看到两个核心交替输出的日志:

microros_task on core:0
microros_task on core:0
do some control on core:1
microros_task on core:0
microros_task on core:0
microros_task on core:0

留言