鱼香ROS社区
    • 版块
    • 最新
    • 未解决
    • 已解决
    • 群组
    • 注册
    • 登录
    紧急通知:禁止一切关于政治&VPN翻墙等话题,发现相关帖子会立马删除封号
    提问前必看的发帖注意事项: 社区问答规则(小鱼个人)更新 | 高质量帖子发布指南

    第九章 学完后运行没问题,我加入显示屏显示、时间和电压测试功能后小车不受控制到处跑,请教下该如何处理?

    已定时 已固定 已锁定 已移动
    ROS2机器人开发:从入门到实践
    显示屏显示,时间显示 电压测量
    2
    6
    266
    正在加载更多帖子
    • 从旧到新
    • 从新到旧
    • 最多赞同
    回复
    • 在新帖中回复
    登录后回复
    此主题已被删除。只有拥有主题管理权限的用户可以查看。
    • O
      onedream 年度VIP
      最后由 编辑

      这是第九章完成测试没有问题的代码(下文称为第一代码块):

      #include <Arduino.h>
      #include <Esp32PcntEncoder.h> 
      #include <Esp32McpwmMotor.h> // 导入电机驱动库
      #include <PidController.h>           // 包含 PID 控制器库,用于实现 PID 控制
      #include <Kinematics.h> // 包含运动学库,用于计算运动轨迹
      
      #include <micro_ros_platformio.h>    // 包含用于 ESP32 的 micro-ROS PlatformIO 库
      #include <WiFi.h>                    // 包含 ESP32 的 WiFi 库
      #include <rcl/rcl.h>                 // 包含 ROS 客户端库 (RCL)
      #include <rclc/rclc.h>               // 包含用于 C 的 ROS 客户端库 (RCLC)
      #include <rclc/executor.h>           // 包含 RCLC 执行程序库,用于执行订阅和发布消息
      #include <geometry_msgs/msg/twist.h> // 包含 ROS 消息库,用于发布/订阅 Twist 消息
      #include <nav_msgs/msg/odometry.h>
      #include <micro_ros_utilities/string_utilities.h>
      
      Esp32McpwmMotor motor;
      Esp32PcntEncoder encoders[2]; // 创建一个数组用于存储两个编码器
      PidController pid_controller[2];   // 创建PidController的两个对象,用于控制两个电机的速度
      Kinematics kinematics; // 创建一个Kinematics对象,用于计算运动轨迹
      
      // float target_linear_speed = 50.0; // 设置目标直线速度为50mm/s
      // float target_angular_speed = 0.1f; // 设置目标角速度为0.1rad/s
      float out_left_speed; // 用于存储左电机速度
      float out_right_speed; // 用于存储右电机速度
      
      rclc_executor_t executor;
      rclc_support_t support;
      rcl_allocator_t allocator;
      rcl_node_t node;
      rcl_subscription_t subscriber; // 定义订阅句柄
      
      geometry_msgs__msg__Twist sub_msg; // 定义接收到的消息结构体
      
      rcl_publisher_t odom_publisher;   // 用于发布机器人的里程计信息(Odom)
      nav_msgs__msg__Odometry odom_msg; // 机器人的里程计信息
      rcl_timer_t timer; // 定义计时器句柄
      
      void callback_publisher(rcl_timer_t * timer, int64_t last_call_time) 
      {
        odom_t odom = kinematics.get_odom(); // 获取机器人的里程计信息
        int64_t stamp = rmw_uros_epoch_millis(); // 获取当前时间戳
        odom_msg.header.stamp.sec = static_cast<uint32_t>(stamp / 1000); // 设置时间戳的秒部分
        odom_msg.header.stamp.nanosec = static_cast<uint32_t>((stamp % 1000) * 1e6); // 设置时间戳的纳秒部分
        odom_msg.pose.pose.position.x = odom.x; // 设置机器人的x坐标
        odom_msg.pose.pose.position.y = odom.y; // 设置机器人的y坐标
        odom_msg.pose.pose.orientation.w = cos(odom.angle * 0.5); // 设置机器人的朝向
        odom_msg.pose.pose.orientation.x = 0; // 设置机器人的朝向的x分量
        odom_msg.pose.pose.orientation.y = 0; // 设置机器人的朝向的y分量
        odom_msg.pose.pose.orientation.z = sin(odom.angle * 0.5); // 设置机器人的朝向的z分量
        odom_msg.twist.twist.angular.z = odom.angle_speed; // 设置机器人的角速度
        odom_msg.twist.twist.linear.x = odom.linear_speed; // 设置机器人的线速度
        if(rcl_publish(&odom_publisher, &odom_msg, NULL)!=RCL_RET_OK)
        { // 发布机器人的里程计信息
          Serial.printf("error: odom publisher failed!\n");
        }
      }
      // 回调函数,当接收到新的 Twist 消息时会被调用
      void twist_callback(const void *msg_in)
      {
        // 将接收到的消息指针转化为 geometry_msgs__msg__Twist 类型
        const geometry_msgs__msg__Twist *twist_msg = (const geometry_msgs__msg__Twist *)msg_in;
        kinematics.kinematic_inverse(twist_msg->linear.x*1000, twist_msg->angular.z, out_left_speed, out_right_speed); // 计算运动轨迹
        pid_controller[0].update_target(out_left_speed); // 设置第一个电机的目标速度
        pid_controller[1].update_target(out_right_speed); // 设置第二个电机的目标速度
      }
      void micro_ros_task(void *parameter)
      {
        IPAddress agent_ip;
        agent_ip.fromString("192.168.43.228"); // 设置代理IP地址
        set_microros_wifi_transports("HONOR 9X", "123456789", agent_ip, 8888); // 设置WiFi传输和代理IP地址和端口
        delay(2000);
        allocator = rcl_get_default_allocator();
        rclc_support_init(&support, 0, NULL, &allocator); // 初始化ROS支持
        rclc_node_init_default(&node, "fishbot_motion_control", "", &support); // 初始化ROS节点
      
        unsigned int num_handles = 0+2; // 计算需要添加的句柄数量
        rclc_executor_init(&executor, &support.context, num_handles, &allocator); // 初始化执行程序
        
        rclc_subscription_init_best_effort(&subscriber, &node, ROSIDL_GET_MSG_TYPE_SUPPORT(geometry_msgs, msg, Twist), "/cmd_vel"); // 初始化订阅句柄,并设置消息类型和主题名称
        rclc_executor_add_subscription(&executor, &subscriber, &sub_msg, &twist_callback, ON_NEW_DATA); // 添加订阅句柄
      
        odom_msg.header.frame_id = micro_ros_string_utilities_set(odom_msg.header.frame_id, "odom"); // 设置里程计信息中的frame_id
        odom_msg.child_frame_id = micro_ros_string_utilities_set(odom_msg.child_frame_id, "base_footprint"); // 设置里程计信息中的child_frame_id
        rclc_publisher_init_best_effort(&odom_publisher, &node, ROSIDL_GET_MSG_TYPE_SUPPORT(nav_msgs, msg, Odometry), "/odom"); // 初始化里程计信息发布器
        while (!rmw_uros_epoch_synchronized())
        {
          rmw_uros_sync_session(1000); // 等待ROS网络同步
          delay(10); // 等待10毫秒
        }
        rclc_timer_init_default(&timer, &support, RCL_MS_TO_NS(50), callback_publisher); // 初始化计时器
        rclc_executor_add_timer(&executor, &timer); // 将计时器添加到执行程序中
        rclc_executor_spin(&executor); // 开始执行程序
      }
      void setup()
      {
        // 1.初始化串口
        Serial.begin(115200); // 初始化串口通信,设置通信速率为115200
      
        // 2.设置编码器
        encoders[0].init(0, 32, 33); // 初始化第一个编码器,使用GPIO 32和33连接
        encoders[1].init(1, 26, 25); // 初始化第二个编码器,使用GPIO 26和25连接
      
        motor.attachMotor(0, 22, 23); // 连接电机到MCPWM 0通道,使用GPIO 12和13连接
        motor.attachMotor(1, 12, 13); // 连接电机到MCPWM 1通道,使用GPIO 27和36连接
      
        // 3.设置PID控制器
        pid_controller[0].update_pid(0.625, 0.125, 0.00); // 设置第一个电机的PID参数
        pid_controller[1].update_pid(0.625, 0.125, 0.00); // 设置第二个电机的PID参数
        pid_controller[0].out_limit(-100, 100); // 设置第一个电机的输出限幅
        pid_controller[1].out_limit(-100, 100); // 设置第二个电机的输出限幅
      
        kinematics.set_wheel_distance(175); // 设置轮距为175mm,即轮子之间的距离
        kinematics.set_motor_param(0, 0.1051566); // 设置第一个电机的每脉冲距离
        kinematics.set_motor_param(1, 0.1051566); // 设置第二个电机的每脉冲距离
      
        // kinematics.kinematic_inverse(target_linear_speed, target_angular_speed, out_left_speed, out_right_speed); // 根据目标数度计算左右轮的速度
        // pid_controller[0].update_target(out_left_speed); // 设置第一个电机的目标速度
        // pid_controller[1].update_target(out_right_speed); // 设置第二个电机的目标速度
      
        xTaskCreate(micro_ros_task, "micro_ros", 10240, NULL, 1, NULL); // 创建micro-ROS任务
      }
      
      void loop()
      {
        delay(10); // 等待10毫秒
        kinematics.update_motor_speed(millis(), encoders[0].getTicks(), encoders[1].getTicks()); // 更新电机速度
        motor.updateMotorSpeed(0, pid_controller[0].update(kinematics.get_motor_speed(0))); // 控制第一个电机速度
        motor.updateMotorSpeed(1, pid_controller[1].update(kinematics.get_motor_speed(1))); // 控制第二个电机速度
      
        Serial.printf("x=%f, y=%f, angle=%f\n", kinematics.get_odom().x, kinematics.get_odom().y, kinematics.get_odom().angle); // 打印当前位置信息
      }
      

      然后我看显示屏没有点亮,我就在这个代码的基础上加入了显示屏显示、电压测量、WiFi显示和时间显示4个功能,编译和下传都没有问题,导航时就开始随意无规律乱跑。以下是完整代码:

      #include <Arduino.h>
      #include <Esp32PcntEncoder.h> 
      #include <Esp32McpwmMotor.h> // 导入电机驱动库
      #include <PidController.h>           // 包含 PID 控制器库,用于实现 PID 控制
      #include <Kinematics.h> // 包含运动学库,用于计算运动轨迹
      
      #include <micro_ros_platformio.h>    // 包含用于 ESP32 的 micro-ROS PlatformIO 库
      #include <WiFi.h>                    // 包含 ESP32 的 WiFi 库
      #include <rcl/rcl.h>                 // 包含 ROS 客户端库 (RCL)
      #include <rclc/rclc.h>               // 包含用于 C 的 ROS 客户端库 (RCLC)
      #include <rclc/executor.h>           // 包含 RCLC 执行程序库,用于执行订阅和发布消息
      #include <geometry_msgs/msg/twist.h> // 包含 ROS 消息库,用于发布/订阅 Twist 消息
      #include <nav_msgs/msg/odometry.h>
      #include <micro_ros_utilities/string_utilities.h>
      
      #include <TimeLib.h>          // 加载时间库,提供setTime\year\month...函数
      #include <Wire.h>             // 加载Wire库,用于I2C通信 
      #include <Adafruit_GFX.h>     // 加载Adafruit_GFX库
      #include <Adafruit_SSD1306.h> // 加载Adafruit_SSD1306库
      Adafruit_SSD1306 display(128, 64, &Wire); //创建OLED显示屏对象,使用Wire总线
      
      const int timeout_ms = 1000;  // 设置同步时间时的全局变量4个参数分别是:同步时间,同步周期,同步偏移量,同步时间源。
      static int64_t time_ms;
      static time_t time_seconds;
      char time_str[25];
      
      Esp32McpwmMotor motor;
      Esp32PcntEncoder encoders[2]; // 创建一个数组用于存储两个编码器
      PidController pid_controller[2];   // 创建PidController的两个对象,用于控制两个电机的速度
      Kinematics kinematics; // 创建一个Kinematics对象,用于计算运动轨迹
      
      float out_left_speed; // 用于存储左电机速度
      float out_right_speed; // 用于存储右电机速度
      
      rclc_executor_t executor;
      rclc_support_t support;
      rcl_allocator_t allocator;
      rcl_node_t node;
      rcl_subscription_t subscriber; // 定义订阅句柄
      
      geometry_msgs__msg__Twist sub_msg; // 定义接收到的消息结构体
      
      rcl_publisher_t odom_publisher;   // 用于发布机器人的里程计信息(Odom)
      nav_msgs__msg__Odometry odom_msg; // 机器人的里程计信息
      rcl_timer_t timer; // 定义计时器句柄
      
      void callback_publisher(rcl_timer_t * timer, int64_t last_call_time) 
      {
        odom_t odom = kinematics.get_odom(); // 获取机器人的里程计信息
        int64_t stamp = rmw_uros_epoch_millis(); // 获取当前时间戳
        odom_msg.header.stamp.sec = static_cast<uint32_t>(stamp / 1000); // 设置时间戳的秒部分
        odom_msg.header.stamp.nanosec = static_cast<uint32_t>((stamp % 1000) * 1e6); // 设置时间戳的纳秒部分
        odom_msg.pose.pose.position.x = odom.x; // 设置机器人的x坐标
        odom_msg.pose.pose.position.y = odom.y; // 设置机器人的y坐标
        odom_msg.pose.pose.orientation.w = cos(odom.angle * 0.5); // 设置机器人的朝向
        odom_msg.pose.pose.orientation.x = 0; // 设置机器人的朝向的x分量
        odom_msg.pose.pose.orientation.y = 0; // 设置机器人的朝向的y分量
        odom_msg.pose.pose.orientation.z = sin(odom.angle * 0.5); // 设置机器人的朝向的z分量
        odom_msg.twist.twist.angular.z = odom.angle_speed; // 设置机器人的角速度
        odom_msg.twist.twist.linear.x = odom.linear_speed; // 设置机器人的线速度
        if(rcl_publish(&odom_publisher, &odom_msg, NULL)!=RCL_RET_OK)
        { // 发布机器人的里程计信息
          Serial.printf("error: odom publisher failed!\n");
        }
      }
      // 回调函数,当接收到新的 Twist 消息时会被调用
      void twist_callback(const void *msg_in)
      {
        // 将接收到的消息指针转化为 geometry_msgs__msg__Twist 类型
        const geometry_msgs__msg__Twist *twist_msg = (const geometry_msgs__msg__Twist *)msg_in;
        kinematics.kinematic_inverse(twist_msg->linear.x*1000, twist_msg->angular.z, out_left_speed, out_right_speed); // 计算运动轨迹
        pid_controller[0].update_target(out_left_speed); // 设置第一个电机的目标速度
        pid_controller[1].update_target(out_right_speed); // 设置第二个电机的目标速度
      }
      void micro_ros_task(void *parameter)
      {
        IPAddress agent_ip;
        agent_ip.fromString("192.168.43.228"); // 设置代理IP地址
        set_microros_wifi_transports("HONOR 9X", "123456789", agent_ip, 8888); // 设置WiFi传输和代理IP地址和端口
        delay(2000);
        allocator = rcl_get_default_allocator();
        rclc_support_init(&support, 0, NULL, &allocator); // 初始化ROS支持
        rclc_node_init_default(&node, "fishbot_motion_control", "", &support); // 初始化ROS节点
      
        unsigned int num_handles = 0+2; // 计算需要添加的句柄数量
        rclc_executor_init(&executor, &support.context, num_handles, &allocator); // 初始化执行程序
      
        rclc_subscription_init_best_effort(&subscriber, &node, ROSIDL_GET_MSG_TYPE_SUPPORT(geometry_msgs, msg, Twist), "/cmd_vel"); // 初始化订阅句柄,并设置消息类型和主题名称
        rclc_executor_add_subscription(&executor, &subscriber, &sub_msg, &twist_callback, ON_NEW_DATA); // 添加订阅句柄
      
        odom_msg.header.frame_id = micro_ros_string_utilities_set(odom_msg.header.frame_id, "odom"); // 设置里程计信息中的frame_id
        odom_msg.child_frame_id = micro_ros_string_utilities_set(odom_msg.child_frame_id, "base_footprint"); // 设置里程计信息中的child_frame_id
        rclc_publisher_init_best_effort(&odom_publisher, &node, ROSIDL_GET_MSG_TYPE_SUPPORT(nav_msgs, msg, Odometry), "/odom"); // 初始化里程计信息发布器
        while (!rmw_uros_epoch_synchronized())
        {
          rmw_uros_sync_session(1000); // 等待ROS网络同步
          delay(10); // 等待10毫秒
        }
        rclc_timer_init_default(&timer, &support, RCL_MS_TO_NS(50), callback_publisher); // 初始化计时器
        rclc_executor_add_timer(&executor, &timer); // 将计时器添加到执行程序中
        rclc_executor_spin(&executor); // 开始执行程序
      }
      void setup()
      {
        // 1.初始化串口
        Serial.begin(115200); // 初始化串口通信,设置通信速率为115200
      
        Wire.begin(18, 19); // 初始化I2C通信
        display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // 初始化OLED显示屏,使用GPIO 18和19连接
        display.clearDisplay(); // 清空屏幕
        display.setTextSize(1); // 设置字体大小
        display.setCursor(0, 0); // 设置开始显示文字的坐标
        display.setTextColor(SSD1306_WHITE); // 设置字体颜色
        display.println("hello code001!"); // 输出的字符
        display.display(); // 显示字符
      
        pinMode(34, INPUT);             // 设置测量电池电压引脚模式为输入
        analogSetAttenuation(ADC_11db); // 设置ADC的衰减为11db
      
        encoders[0].init(0, 32, 33); // 初始化第一个编码器,使用GPIO 32和33连接
        encoders[1].init(1, 26, 25); // 初始化第二个编码器,使用GPIO 26和25连接
      
        motor.attachMotor(0, 22, 23); // 连接电机到MCPWM 0通道,使用GPIO 12和13连接
        motor.attachMotor(1, 12, 13); // 连接电机到MCPWM 1通道,使用GPIO 27和36连接
      
        // 3.设置PID控制器
        pid_controller[0].update_pid(0.625, 0.125, 0.00); // 设置第一个电机的PID参数
        pid_controller[1].update_pid(0.625, 0.125, 0.00); // 设置第二个电机的PID参数
        pid_controller[0].out_limit(-100, 100); // 设置第一个电机的输出限幅
        pid_controller[1].out_limit(-100, 100); // 设置第二个电机的输出限幅
      
        kinematics.set_wheel_distance(175); // 设置轮距为175mm,即轮子之间的距离
        kinematics.set_motor_param(0, 0.1051566); // 设置第一个电机的每脉冲距离
        kinematics.set_motor_param(1, 0.1051566); // 设置第二个电机的每脉冲距离
      
        xTaskCreate(micro_ros_task, "micro_ros", 10240, NULL, 1, NULL); // 创建micro-ROS任务
      }
      
      void loop()
      {
        kinematics.update_motor_speed(millis(), encoders[0].getTicks(), encoders[1].getTicks()); // 更新电机速度
        motor.updateMotorSpeed(0, pid_controller[0].update(kinematics.get_motor_speed(0))); // 控制第一个电机速度
        motor.updateMotorSpeed(1, pid_controller[1].update(kinematics.get_motor_speed(1))); // 控制第二个电机速度
        Serial.printf("x=%f, y=%f, angle=%f\n", kinematics.get_odom().x, kinematics.get_odom().y, kinematics.get_odom().angle); // 打印当前位置信息
      
        display.clearDisplay(); // 清空屏幕
      
        if (WiFi.status() == WL_CONNECTED) {
          display.setCursor(0, 0); 
          display.print("WiFi: ");
          display.println(WiFi.SSID()); // 显示WiFi名称
        } else {
          display.setCursor(0, 0); 
          display.println("Connection failed"); // 显示WiFi未连接状态
        }
      
        int analogValue = analogRead(34);                     // 读取原始值0-4096
        int analogVolts = analogReadMilliVolts(34);           // 读取模拟电压,单位毫伏
        float realVolts = 5.02 * ((float)analogVolts * 1e-3); // 计算实际电压值
        Serial.printf("realVolts value = %.2f\n", realVolts); // 串口输出实际电压值
        display.setCursor(0, 16); // 设置开始显示文字的坐标
        display.print("realVolts value:");
        display.printf("%.2f\n", realVolts); // 显示实际电压值
      
        display.setCursor(0, 32);                           
        display.print("123456789012345678901"); // 输出任意英文和数字字符的
      
        while (!rmw_uros_epoch_synchronized()) // 判断时间是否同步,如果没有同步就等待时间同步,直到同步为止。启动bringup.launch.py后,等待一段时间,直到时间同步。
        {
          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到北京时间然后设置到系统
          }
          return;
        }
        /*sprintf 函数的作用就是将格式化的数据写入到time_str变量中*/
        sprintf(time_str, "%04d-%02d-%02d %02d:%02d:%02d ", year(), month(), day(), hour(), minute(), second());
        display.setCursor(0, 48);  // 设置开始显示文字的坐标
        display.println(time_str); // 输出的字符
      
        display.display(); // 
        delay(1000); //
      }
      

      经过2天的测试,发现问题很奇怪:
      1、首先认为导航问题,我把导航关闭后用键盘控制,一样乱跑;
      2、其次测试时间代码部分可能因为共用WiFi接口冲突,注释掉所有显示时间的功能,结果还是乱跑;
      3、接着在注释掉电压测量部分继续乱跑;
      4、然后注释掉显示屏显示功能依然乱跑;
      5、最后将已改动的功能包上把所有的变化删除还原到第一个代码块,还是乱跑。

      如果用完全没有经历过上述代码修改的第一个代码块测试,任然没有问题。
      我想请教下小鱼老师我该怎么啦?实在是测试不出来了,还望小鱼老师空闲时指导一二。

      小鱼小 1 条回复 最后回复 回复 引用 0
      • 小鱼小
        小鱼 技术大佬 @onedream
        最后由 编辑

        @onedream delay1000 去掉把,休眠了会影响程序正常运行的,你参考下fishbot固件的源码,里面有一起运行的实现的方法。

        新书配套视频:https://www.bilibili.com/video/BV1GW42197Ck/

        O 1 条回复 最后回复 回复 引用 0
        • O
          onedream 年度VIP @小鱼
          最后由 编辑

          @小鱼 感谢小鱼老师,太神奇了,我把所有的都测试了就是没有删除delay(1000),因为我看到课本上也有个delay(10),永远也没想到那里去。结果把delay一删就恢复正常了,完全没有问题。顺便我问下你提到的固件源码哪里下载啦?专门的小车界面都是根据配置助手直接下载到小车的.bin文件,我没有找到源码。

          小鱼小 1 条回复 最后回复 回复 引用 0
          • 小鱼小
            小鱼 技术大佬 @onedream
            最后由 编辑

            @onedream 资料汇总页最下面:https://github.com/fishros/fishbot_motion_control_microros

            新书配套视频:https://www.bilibili.com/video/BV1GW42197Ck/

            O 2 条回复 最后回复 回复 引用 0
            • O
              onedream 年度VIP @小鱼
              最后由 onedream 编辑

              此回复已被删除!
              1 条回复 最后回复 回复 引用 0
              • O
                onedream 年度VIP @小鱼
                最后由 编辑

                @小鱼 收到,小鱼老师,感谢!感谢!我继续去学习。

                1 条回复 最后回复 回复 引用 0
                • 第一个帖子
                  最后一个帖子
                皖ICP备16016415号-7
                Powered by NodeBB | 鱼香ROS