拨弹盘模块使用说明

1 基本知识
电控队员在阅读本说明前,需基本了解拨弹盘、弹链的机械机构(不清楚的问机械队员或者老队员),至少搞明白以下几点:
- 拨弹盘电机在哪里?电机的型号、减速比?
- 拨弹盘是几爪的?拨一发弹要转多少角度?
- 拨弹盘正转向是顺时针还是逆时针?
- 为什么有弹丸时,拨弹盘反转必然会堵转?(仅讨论我队传统拨弹盘)
- 拨弹盘的作用?(将弹链首颗弹丸直接推给摩擦轮加速?将首颗弹丸送到待击发位置,由推杆/气动/弹射击发?)
- 弹链中的 单发限位 是什么?有什么作用?与堵转卡弹、双发有什么关系?
同时,需要清楚 枪口热量 相关的比赛规则。
本模块默认拨弹盘有弹丸时反转必然堵转、拨弹盘将弹链首颗弹丸直接推给摩擦轮加速。
本模块默认采用位置—速度双环级联PID控制器。
整个Feed模块是一个 C++ 类,继承了hello_world::module::ModuleFsm类。Feed类被放置在hello_world::module命名空间下。
Feed模块用到的所有的结构体/枚举类都定义在hello_world::module::feed_impl命名空间中,以Feed开头。该命名空间不暴露给用户。Feed 类中通过类型别名声明,将这些结构体/枚举类绑定到了新的名字上,去掉了开头的 Feed。如下所示:
| C++ |
|---|
| class Feed : public ModuleFsm
{
public:
/* 类型定义 */
// ......
using StuckStatus = FeedStuckStatus; // 卡弹状态 (枚举类)
using StuckDetectionBy = FeedStuckDetectionBy; // 堵转检测方式(枚举类)
using RfrInputData = FeedRfrInputData; // 裁判系统输入数据 (结构体)
using StuckDetection = FeedStuckDetection; // 堵转检测 (结构体)
using StuckBackMethod = FeedStuckBackMethod; // 防卡弹优化 (结构体)
using StuckHandle = FeedStuckHandle; // 堵转处理 (结构体)
using StuckPrevent = FeedStuckPrevent; // 预防堵转 (结构体)
using Config = FeedConfig; // 配置参数 (结构体)
// ......
}
|
当用户想使用其中某个类型时,可以如下编写代码:
| C++ |
|---|
| hello_world::module::Feed::Config kFeedConfig; // 定义一个配置参数结构体变量
|
1.1 拨弹盘配置参数
| C++ |
|---|
| 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 resurrection_pos_err = 5.0f / 180 * PI; // Resurrection 模式反转速度系数, 反转时以它作为位置环PID的误差值, >0, 单位 rad
uint32_t hold_duration_thre = 100; // 角度保持不变检测阈值时间, >0, 单位 ms (在复活模式作为堵转检测的第二手段)
uint32_t default_trigger_interval = 200; // 默认拨弹频率限制间隔时间, >0, 单位 ms
float default_safe_num_blt = 1.5f; // 默认安全热量值对应的弹丸个数, >=0, 可以为小数
FeedStuckDetection stuck_detection; // 堵转检测
FeedStuckHandle stuck_handle; // 堵转处理
FeedStuckPrevent stuck_prevent; // 预防堵转
};
|
| 成员名称 |
默认值 |
说明 |
备注 |
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发弹丸的热量。 |
|
resurrection_pos_err |
5.0f / 180 * PI |
拨弹盘是通过双环PID(位置环+速度环)进行控制的。为实现拨弹盘在复活模式下匀速反转,控制时,将PID位置环的误差值始终设为-resurrection_pos_err。 |
它仅与复活模式下拨弹盘的反转速度有关,并且成正比。 |
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,否则一直等于上次用户的给定值。 |
stuck_detection |
无 |
堵转检测(结构体) |
见 小节1.2 |
stuck_handle |
|
堵转处理(结构体) |
见 小节1.2 |
stuck_prevent |
|
预防堵转(结构体) |
见 小节1.2 |
【使用示例】定义配置结构体变量,并用它初始化 Feed 实例
| C++ |
|---|
| /* 在初始化结构体变量时直接修改成员值 */
hello_world::module::Feed::Config kFeedConfig = {
.ang_ref_offset = 0.05f,
.ang_per_blt = PI / 3,
.heat_per_bullet = 100,
.resurrection_pos_err = 5.0f / 180 * PI,
.hold_duration_thre = 100,
.default_trigger_interval = 200,
.default_safe_num_blt = 1.5f,
.stuck_detection = {
.detection_by =
},
.stuck_handle,
.stuck_prevent,
}
hello_world::module::Feed unique_feed(kFeedConfig);
|
1.2 卡弹堵转相关
卡弹堵转是非正常情况。若拨弹盘运行过程中发生卡弹堵转,而代码缺少处理措施,则可能导致拨弹盘拨爪被扭弯、电机烧毁,造成财产损失和时间浪费。
用一个枚举类变量表示拨弹盘当前的卡弹状态:
| C++ |
|---|
| /* 卡弹状态 (枚举类) */
enum class FeedStuckStatus : uint8_t {
kNone = 0u, // 没有卡住
kForward, // 向前卡住
kBackward, // 向后卡住
};
|
根据拨弹盘电机以及工况的不同,堵转检测有角度检测和电流检测两种方式。顾名思义,即根据拨弹盘反馈角度与期望角度之差或者拨弹电机电流是否持续超阈值一段时间,来判断电机是否堵转。其中的“持续一段时间”,是为了避免因传感器信号的瞬时波动造成误检测。
同一个拨弹盘二选一使用即可。
每次拨弹过程中,拨弹盘反馈角度与期望角度差都从 1 发弹丸对应的角度逐渐减小至 0 ,所以角度差的检测阈值通常需要大于 1 发弹丸角度,才能避免误判。
17mm 弹丸拨弹盘以连发为主,一旦卡弹,角度差迅速积累,超过 1 发弹丸角度,因此可以使用角度检测;
42mm 弹丸拨弹盘以单发为主,两次发射的间隔时间不定,因此卡弹后,角度差可能长时间保持在 1 发弹丸角度以内,不适合使用角度检测。
另外,选用 2006 电机的 17mm 弹丸拨弹盘最好使用角度检测,因为其正常工作时电流值较大,与堵转电流差距不明显。
| C++ |
|---|
| /* 堵转检测方式(枚举类) */
enum class FeedStuckDetectionBy {
kAngle, // 角度检测,建议 17 mm 拨弹盘使用
kCurrent, // 电流检测,建议 42 mm 拨弹盘使用
};
/* 堵转检测(配置参数,结构体) */
struct FeedStuckDetection {
FeedStuckDetectionBy detection_by; // 堵转检测方式:电流超阈值一段时间 或 反馈角度与期望角度差值过大一段时间
float ang_diff_thre; // 用于判断堵转的角度差值阈值, 通常大于 1 发弹丸对应的角度, 单位 rad
float curr_thre; // 用于判断堵转的电流阈值, >0, 单位 A (TODO 根据拨弹电机调整)
uint32_t time_thre; // 堵转检测阈值时间, >0, 单位 ms
};
|
当 detection_by == StuckDetectionBy::kAngle 时,ang_diff_thre 有效而 curr_thre 无效;
当 detection_by == StuckDetectionBy::kCurrent 时,ang_diff_thre 无效而 curr_thre 有效;
拨弹盘卡弹堵转之后如何处理?
如果是后向堵转,就向前转一个小角度(本模块默认 0.05 rad);
如果是前向堵转,就反转,反转又分回到上一次拨弹角度和反转固定角度两种方式,用户可以自选其一。
| C++ |
|---|
| /* 堵转反转方式(枚举类) */
enum class FeedStuckBackMethod {
kToLastAng, // 回退到上一次的角度
kFixedAng, // 回退固定角度
};
/* 堵转处理(配置参数,结构体) */
struct FeedStuckHandle {
FeedStuckBackMethod method; // 反转方式
float back_ang; // 方法二回退的固定角度, > 0, 单位 rad
};
|
其中,若 method == StuckBackMethod::kToLastAng,则 back_ang 无效。
若弹链内弹丸挤压过紧,容易导致拨弹盘前向堵转。一种解决方案是每完成一次拨弹后,就回退一个固定的小角度。这能起到一定预防堵转的作用。
该功能作为优化项,仅适用于工作在单发模式的拨弹盘(一般是 42mm 弹丸拨弹盘)。
| C++ |
|---|
| /* 预防堵转(配置参数,结构体) */
struct FeedStuckPrevent {
bool enabled; // 是否开启预防堵转
float back_ang; // 预防堵转回退角度, > 0, 单位 rad
};
|
若 enabled == false ,则不开启预防堵转功能。
1.2 拨弹盘所需的裁判系统反馈数据(结构体)
| C++ |
|---|
| 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; // 发射机构热量的每秒冷却值
};
|
拨弹盘状态机正常运行需要实时获取裁判系统的反馈数据,提供的接口如下
| 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); // 更新拨弹盘所需的裁判系统数据
|
【注意】
- 如果用户自己不调用
updateRfrData函数,Feed结构体内部会按照默认值或上一次updateRfrData的传入值工作。
- 如何获知是否有新弹丸射出?
| 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;
}
}
|
2 使用方法
2.1 拨弹盘的实例化与初始化
【建议】 自己写一对 ins_feed.hpp 和 ins_feed.cpp 文件,依照全局单例的设计思想,定义拨弹盘的全局单例,并给出接口函数抛出拨弹盘实例指针。
在其中包含feed.hpp文件,并给出接口函数。
| C++ |
|---|
| #include "feed.hpp"
hello_world::module::Feed* CreateFeed();
|
在其中包含ins_feed.hpp,定义拨弹盘全局实例,注册PID和电机指针,实现接口函数。
| C++ |
|---|
| #include "ins_feed.hpp"
hello_world::module::Feed::Config kFeedConfig = {
.ang_ref_offset = 0.05f,
.ang_per_blt = PI / 3,
.heat_per_blt = 100,
.resurrection_pos_err = 5.0f / 180 * PI,
.hold_duration_thre = 100,
.default_trigger_interval = 200,
.default_safe_num_blt = 1.5f,
.stuck_detection = {
.detection_by = hello_world::module::Feed::StuckDetectionBy::kCurrent,
.ang_diff_thre = 0.0f, // 此时无效
.curr_thre = 14.0f,
.time_thre = 100,
},
.stuck_handle = {
.method = hello_world::module::Feed::StuckBackMethod::kToLastAng,
.back_ang = 0.0f, // 此时无效
},
.stuck_prevent = {
.enabled = true,
.back_ang = 0.05f,
},
};
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);
|
【说明】:
updateRfrData()用于更新拨弹盘所需的裁判系统反馈数据,不更新时,采用用户上一次的更新值或者默认值。(详见1.2 拨弹盘所需的裁判系统反馈数据(结构体))
setFricStatus()用于设定摩擦轮运行状态,当且仅当摩擦轮正常运行时,拨弹盘Feed模块状态机才能进入Working模式正常工作。
2.2.2 设定控制指令
用户需要根据自身需求设定控制指令。
- (1)设定控制模式,若不设定,则采用上一次的设定值或者默认值。
| C++ |
|---|
| void setCtrlMode(CtrlMode mode); // 设定控制模式
|
【说明】:形式参数mode的类型CtrlMode是一个定义在hello_world::module命名空间下的枚举类,如下
| C++ |
|---|
| 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++ |
|---|
| 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);
|
【说明】
- 当拨弹盘控制模式
ctrl_mode_是hello_world::module::CtrlMode::Manual时,只听从操作手发弹信号;当控制模式是hello_world::module::CtrlMode::Auto时,同时听从操作手发弹信号和视觉指令发弹信号,且操作手发弹信号优先。
- 拨弹盘在每次控制周期内,处理完这两个发弹信号后,会将它们主动复位,以防止用户下个周期不将发弹信号设置为
false导致一直发弹的情况。
- 视觉的
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();
|
【说明】
update()用于拨弹盘更新电源状态和自身内部数据,run()用于拨弹盘运行。
- 必须先执行
update(),再run()。
同时,提供了以下两个函数,以使拨弹盘待机或复位。
| C++ |
|---|
| void standby();
void reset();
|
【说明】
standby()和run()是同一层级的指令,若某个控制周期内需要调用standby(),最好不要调用run()。若非调用run()不可,则必须让standby()在run()之后执行,以覆盖电机的控制指令,保证拨弹盘处于待机状态。
3 调试技巧
拨弹盘模块提供了以下接口函数供用户在程序运行过程中读取内部数据,或者可以使用Ozone直接观察类内数据。
| C++ |
|---|
| 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++ |
|---|
| uint16_t trigger_cnt_ = 0; // 拨弹计数器
uint16_t stuck_forward_cnt_ = 0; // 前向卡弹计数器
uint16_t stuck_backward_cnt_ = 0; // 后向卡弹计数器
|
附录
版本说明
| 版本号 |
发布日期 |
说明 |
贡献者 |
 |
2024.12.22 |
拨弹盘模块使用说明 |
娄开杨 |
 |
2025.01.11 |
修改热量/频率限制部分;修复参数初值错误 |
娄开杨 |
 |
2025.05.21 |
防卡弹堵转优化 |
娄开杨 |