跳转至

拨弹盘模块使用说明

1 基本知识

整个Feed模块是一个类,继承了hello_world::module::ModuleFsm类。

Feed类被放置在hello_world::module命名空间下。

hello_world::module::feed_impl命名空间下定义了以下三个枚举类/结构体,并在Feed类中通过typedef的方式引入。

1.1 卡弹状态(枚举类)

C++
1
2
3
4
5
6
7
8
// 被放在 hello_wrold::module::feed_impl 里
enum class FeedStuckStatus : uint8_t {
  kNone = 0u,    // 没有卡住
  kForward,      // 向前卡住
  kBackward,     // 向后卡住
};
// 被放在 Feed 类里
typedef FeedStuckStatus StuckStatus;

Feed::StuckStatus枚举类用于表示拨弹盘当前的卡弹状态。

Feed类提供了读取卡弹状态的函数接口,定义如下

C++
1
2
3
4
5
6
StuckStatus getStuckStatus() const;

// 【使用示例】判断当前卡弹状态是否为向前卡弹
if (feed_ptr->getStuckStatus() == hello_world::module::Feed::StuckStatus::kForward) {
  /* do something */
}

1.2 拨弹盘所需的裁判系统反馈数据(结构体)

C++
// 被放在 hello_world::module::feed_impl 里
struct FeedRfrInputData {
  bool is_rfr_on          = false;    // 裁判系统是否在线
  bool is_power_on        = false;    // 发射机构电源是否开启
  bool is_new_bullet_shot = false;    // 是否有新弹丸射出
  float heat_limit        = 100.0;    // 发射机构热量限制
  float heat              = 0;        // 发射机构热量
  float heat_cooling_ps   = 10;       // 发射机构热量的每秒冷却值
};
// 被放在 Feed 类里
typedef FeedRfrInputData RfrInputData;

拨弹盘状态机正常运行需要实时获取裁判系统的反馈数据,提供的接口如下

C++
void updateRfrData(const RfrInputData &inp_data);

// 【使用示例】更新拨弹盘所需的裁判系统反馈数据
hello_world::module::Feed::RfrInputData feed_rfr_input_data; // 初始化结构体变量,成员全为默认值

// 获取裁判系统数据,略

feed_rfr_input_data.is_rfr_on = true;                        // 修改结构体变量成员值
feed_rfr_input_data.is_power_on = true;                      // 修改结构体变量成员值
/* more ... */

feed_ptr_->updateRfrData(feed_rfr_input_data);               // 更新拨弹盘所需的裁判系统数据

【注意】

  1. 如果用户自己不调用updateRfrData函数,Feed结构体内部会按照默认值上一次updateRfrData的传入值工作。
  2. 如何获知是否有新弹丸射出?
C++
hello_world::referee::Referee *referee_ptr;
hello_world::referee::RobotShooterPackage *rfr_shooter_pkg_ptr;

// 通讯过程与数据更新过程略

bool is_new_bullet_shot = false;
if (!referee_ptr_->isOffline()) {
  if (!rfr_shooter_pkg_ptr_->isHandled()) {  // 检测到了一颗新的弹丸发射
    rfr_shooter_pkg_ptr_->setHandled();
    is_new_bullet_shot = true;
  }
}

1.3 拨弹盘配置参数(结构体)

C++
// 被放在 hello_world::module::feed_impl 里
struct FeedConfig {
  float ang_ref_offset              = 0.0f;          // 拨盘电机目标角度偏移量, >=0, 单位 rad(TODO 根据弹链长度调整)
  float ang_per_blt;                                 // 1发弹丸拨盘的转动角度, >0, 单位 rad (TODO 根据拨弹盘设计调整)
  float heat_per_blt;                                // 1发弹丸的耗热量, >0 (TODO 根据兵种调整)
  float stuck_curr_thre;                             // 用于判断堵转的电流阈值, >0, 单位 A (TODO 根据拨弹电机调整)
  float resurrection_pos_err        = 5.0f / 180 * PI;  // Resurrection 模式反转速度系数, 反转时以它作为位置环PID的误差值, >0, 单位 rad
  uint32_t stuck_duration_thre      = 200;           // 堵转检测阈值时间, >0, 单位 ms
  uint32_t hold_duration_thre       = 100;           // 角度保持不变检测阈值时间, >0, 单位 ms (在复活模式作为堵转检测的第二手段)
  uint32_t default_trigger_interval = 200;           // 默认拨弹频率限制间隔时间, >0, 单位 ms
  float default_safe_num_blt        = 1.5f;          // 默认安全热量值对应的弹丸个数, >=0, 可以为小数
};
// 被放在 Feed 类里
typedef FeedConfig Config;
成员名称 默认值 说明 备注
ang_ref_offset 0.0f 我们默认拨弹盘电机没有绝对编码。在复活阶段,拨弹盘会反转至堵转,并将反向堵转位置设定为电机的角度零点。
为了使云台机械的单发限位发挥出最佳效果,我们希望拨弹盘转动之前,弹链首颗弹丸处在接触到摩擦轮一点点的位置;而电机反向堵转后的角度大概率不是这一最佳角度。
我们将最佳角度和角度零点之差记为ang_ref_offset
Feed模块会保证拨弹时,电机的期望角度都为AngleNormRad(ang_ref_offset + k * ang_per_blt),其中AngleNormRad()表示归一化到[-π, π)k为正整数。
【1】由于弹丸尺寸和正常拨弹频率的差异,ang_ref_offset对英雄机器人的影响较大,对步兵机器人的影响不大。
【2】可通过给电机发0力矩指令的情况下,分别转动电机至反向堵转位置和弹链首颗弹丸接触到摩擦轮一点点的位置,记录两次角度,作差即得ang_ref_offset
ang_per_blt 1发弹丸拨盘的转动角度。
heat_per_blt 1发弹丸的热量。
stuck_curr_thre 堵转电流阈值。 该值为堵转电流阈值的绝对值。
若电机电流反馈值< -stuck_curr_thre,且持续一段时间,则认为反向堵转。
resurrection_pos_err 5.0f / 180 * PI 拨弹盘是通过双环PID(位置环+速度环)进行控制的。为实现拨弹盘在复活模式下匀速反转,控制时,将PID位置环的误差值始终设为-resurrection_pos_err 它仅与复活模式下拨弹盘的反转速度有关,并且成正比。
stuck_duration_thre 200 为了防止拨弹电机电流短时间超阈值而被误认为卡弹,规定电流超阈值持续stuck_duration_thre时间后,才认为是堵转。
hold_duration_thre 100 在复活模式反向旋转至堵转时,由于位置环误差恒定为resurrection_pos_err,可能导致拨弹盘电流输出值小于stuck_curr_thre。因此需要另一种方法判断堵转。
在复活模式下,当拨弹盘保持静止持续时间超过hold_duration_thre后,认为拨弹盘已经反转到位。
仅在复活模式下有用。
default_trigger_interval 200 默认的拨弹间隔时间。 在用户通过setTriggerLimit函数设置前,拨弹盘一直按照default_triggger_interval限制拨弹频率;否则,将按照上次用户的给定值限制频率。
default_safe_num_blt 1.5 默认安全热量值对应的弹丸个数,可以是小数。
若当前热量值加上safe_num_blt * heat_per_blt会超限,且未使用setTriggerLimit关闭热量限制时,则不允许前向拨动拨弹盘。
在用户通过setTriggerLimit函数设置前,safe_num_blt等于default_safe_num_blt,否则一直等于上次用户的给定值。

【使用示例】定义配置结构体变量,并用它初始化 Feed 实例

C++
// 在初始化结构体变量时直接修改成员值
hello_world::module::Feed::Config kFeedConfig = {
  .ang_ref_offset           = 0.05f,
  .ang_per_blt              = PI / 3,
  .heat_per_bullet          = 100,
  .stuck_curr_thre          = 14.5f,
  .resurrection_pos_err     = 5.0f / 180 * PI,
  .stuck_duration_thre      = 200,
  .hold_duration_thre       = 100,
  .default_trigger_interval = 200,
  .default_safe_num_blt     = 1.5f,
}
hello_world::module::Feed unique_feed(kFeedConfig);

2 使用方法

2.1 拨弹盘的实例化与初始化

【建议】 自己写一对 ins_feed.hpp 和 ins_feed.cpp 文件,依照全局单例的设计思想,定义拨弹盘的全局单例,并给出接口函数抛出拨弹盘实例指针。

  • ins_feed.hpp

在其中包含feed.hpp文件,并给出接口函数。

C++
#include "feed.hpp"
hello_world::module::Feed* CreateFeed();
  • ins_feed.cpp

在其中包含ins_feed.hpp,定义拨弹盘全局实例,注册PID和电机指针,实现接口函数。

C++
#include "ins_feed.hpp"
hello_world::module::Feed::Config kFeedConfig = {
  .ang_ref_offset           = 0.0f,  
  .ang_per_blt              = PI / 3,   
  .heat_per_bullet          = 100, 
  .stuck_curr_thre          = 13.5f,          
  .resurrection_pos_err     = 5.0f / 180 * PI,  
  .stuck_duration_thre      = 200,           
  .hold_duration_thre       = 100,  
  .default_trigger_interval = 200,
  .default_safe_num_blt     = 1.5f,
};
hello_world::module::Feed unique_feed = hello_world::module::Feed(kFeedConfig);
hello_world::module::Feed* CreateFeed()
{
  static bool is_feed_created = false;
  if (!is_feed_created) {
    unique_feed.registerPidFeed(CreatePidMotorFeed()); // 注册MultiNodesPID指针
    unique_feed.registerMotorFeed(CreateMotorFeed());  // 注册电机指针
    is_feed_created = true;
  }
  return &unique_feed;
};

2.2 更新数据,设定指令

【要求】每个控制周期,用户都需要使用2.1 拨弹盘的实例化与初始化创建的实例,按照顺序调用以下接口函数,更新数据,设定控制指令。

(基本所有程序都做了防呆处理,即使用户没有每个控制周期都调用以下函数,程序也能正常运行,但是极不推荐这样做!!)

2.2.1 更新数据

用户需要通过调用以下两个函数,来更新裁判系统数据和摩擦轮运行状态。

C++
void updateRfrData(const RfrInputData &inp_data);
void setFricStatus(bool is_fric_ok);

// 用法, 后文接口函数的调用方法基本一致,不再重复给出
hello_world::module::Feed* feed_ptr_ = CreateFeed();
hello_world::module::Feed::RfrInputData feed_rfr_input_data;
bool fric_ok_flag;
/* 设置 rfr_input_data, fric_ok_flag, 略 */
feed_ptr_->updateRfrData(feed_rfr_input_data);
feed_ptr_->setFricStatus(fric_ok_flag);

【说明】:

  1. updateRfrData()用于更新拨弹盘所需的裁判系统反馈数据,不更新时,采用用户上一次的更新值或者默认值。(详见1.2 拨弹盘所需的裁判系统反馈数据(结构体)
  2. setFricStatus()用于设定摩擦轮运行状态,当且仅当摩擦轮正常运行时,拨弹盘Feed模块状态机才能进入Working模式正常工作。

2.2.2 设定控制指令

用户需要根据自身需求设定控制指令。

  • (1)设定控制模式,若不设定,则采用上一次的设定值或者默认值。
C++
void setCtrlMode(CtrlMode mode);  // 设定控制模式

【说明】:形式参数mode的类型CtrlMode是一个定义在hello_world::module命名空间下的枚举类,如下

C++
1
2
3
4
enum class CtrlMode : uint8_t {
  kManual,  // 手动控制模式
  kAuto,    // 自动控制模式
};
  • (2)设定热量 / 拨弹频率限制,若不设定,则采用上一次的设定值或者默认值。

使用该函数可实现 1. 禁止拨弹2. 允许拨弹,限制频率但不限制热量3. 允许拨弹,限制频率和热量

C++
/** 
 * @brief       设定拨弹限制
 * @param       is_trigger_allowed: 是否允许拨弹
 * @param       is_heat_limited:    是否限制热量
 * @param       safe_num:           安全热量值对应的弹丸个数
 * @param       interval:           拨弹间隔时间, 单位 ms   
 * @note        该函数用于设定拨弹频率限制和热量限制。
 *              当 is_trigger_allowed 为 false 时, 后续所有参数设置不起作用。
 *              当 is_heat_limited 为 false 时, safe_num 参数设置不起作用。
 */
void setTriggerLimit(bool is_trigger_allowed, bool is_heat_limited, float safe_num, uint32_t interval);

具体用法如下所示

C++
1
2
3
4
hello_world::module::Feed* feed_ptr_ = CreateFeed();
feed_ptr_->setTriggerLimit(false, false, 0, 0);       // 禁止拨弹
feed_ptr_->setTriggerLimit(true, false, 1.5, 100);    // 允许拨弹,不限制热量,限制拨弹间隔 100ms
feed_ptr_->setTriggerLimit(true, true, 1.5, 100);     // 允许拨弹,限制安全热量对应的弹丸颗数为 1.5 颗,限制拨弹间隔 100ms
  • (3)设定操作手 / 视觉指令发弹信号,若不设定,则默认不发弹。
C++
void setManualShootFlag(bool flag);
void setVisionShootFlag(hello_world::vision::Vision::ShootFlag flag);

【说明】

  1. 当拨弹盘控制模式ctrl_mode_hello_world::module::CtrlMode::Manual时,只听从操作手发弹信号;当控制模式是hello_world::module::CtrlMode::Auto时,同时听从操作手发弹信号和视觉指令发弹信号,且操作手发弹信号优先。
  2. 拨弹盘在每次控制周期内,处理完这两个发弹信号后,会将它们主动复位,以防止用户下个周期不将发弹信号设置为false导致一直发弹的情况。
  3. 视觉的hello_world::vision::Vision::ShootFlag::kShootOnce"发一颗弹"是一个上升沿触发的指令,即当且仅当上一时刻视觉指令为hello_world::vision::Vision::ShootFlag::kNoShoot且当前指令为hello_world::vision::Vision::ShootFlag::kShootOnce时,拨弹盘才会向前拨一发弹。因此,为了能准确识别上升沿,在视觉控制时,用户【必须】每个控制周期都调用setVisionShootFlag()

2.3 运行拨弹盘模块状态机

在每个控制周期内,调用以下接口函数以运行拨弹盘模块状态机。

C++
void update();
void run();

【说明】

  1. update()用于拨弹盘更新电源状态和自身内部数据,run()用于拨弹盘运行。
  2. 必须先执行update(),再run()

同时,提供了以下两个函数,以使拨弹盘待机复位

C++
void standby();
void reset();

【说明】

  1. standby()run()是同一层级的指令,若某个控制周期内需要调用standby(),最好不要调用run()。若非调用run()不可,则必须让standby()run()之后执行,以覆盖电机的控制指令,保证拨弹盘处于待机状态。

3 调试技巧

拨弹盘模块提供了以下接口函数供用户在程序运行过程中读取内部数据,或者可以使用Ozone直接观察类内数据。

C++
1
2
3
4
5
6
7
8
9
Config &getConfig();             
const Config &getConfig() const; // 获取当前配置 (适用于 const 实例)
CtrlMode getCtrlMode() const;
StuckStatus getStuckStatus() const;
float getAngRef() const;
float getAngFdb() const;
uint16_t getTriggerCnt() const;
uint16_t getStuckForwardCnt() const;
uint16_t getStuckBackwardCnt() const;

值得注意的是,拨弹盘模块中有三个计数器,专门供用户调试使用

C++
1
2
3
uint16_t trigger_cnt_ = 0;         // 拨弹计数器
uint16_t stuck_forward_cnt_ = 0;   // 前向卡弹计数器
uint16_t stuck_backward_cnt_ = 0;  // 后向卡弹计数器

附录

版本说明

版本号 发布日期 说明 贡献者
2024.12.22 拨弹盘模块使用说明 娄开杨
2025.01.11 修改热量/频率限制部分;修复参数初值错误 娄开杨