出于个人模型爱好,我一直想尝试VTOL垂直起落,夏天试了几种离题太远就不说了。在地铁冥想之前,一个计划突然闪现。与鱼鹰基本相似V22, 双发垂直起飞,然后电机转90度提供前进推力,固定翼提供升力。有一个问题:电机转90度后飞控板XYZ方向传感器没有相应的旋转,不能发挥自稳定作用。当两个电机的速差在垂直起落时控制左右翼的平衡,但当转向水平方向时,应转向前进方向来控制差异,并发挥类似方向舵的作用。我想到了几个方案,包括机械旋转飞控板,这并非不可能,但最终我觉得更改代码是最简单的。
使用的飞行控制是F4 开源INAV 2.0.0.第一个问题是编译。我去了。网上搜索的信息不多。简书的连接写得很清楚,https://www.jianshu.com/p/9a13eee41b48 我应该感谢你,但我真的很想给作者几次,因为这些实际操作远远不够,其他的CSDN还有一篇关于上面的文章inav但是写的太笼统了,不懂linux中间有太多的坑:Linxu的版本选择、编译飞控代码需要依赖的软件包。编译命令。前后花了将近2个星期的个人时间。中间还放弃过,
有人说可以用docker编译,实际试了下我靠,不是不行,docker下载有多恶心,安装有多恶心docker要先安装windows server,如此不可操作的建议...你知道我以前安装过windows server迷失了多少次自我?
这本书的解释是平时不怎么写的Linux还有一些修改INAV需要的人,对Linxu熟客会觉得很常识,可以直接跳过。
第一部分 编译 第一步:安装虚拟机。我用的版本是VMware? Workstation 14 Pro。配置虚拟硬盘至少20G空间,其他默认情况就足够了
第二步:网上下载Ubuntu 18AMD64.我想在这里这么简单的信息, 没人提到我前后下载了不少于5、6个版本linux安装包,包Ubuntu包的大小没那么吓人。更多的用户。然后想当然地使用它X在86版本中,编译时出现了32位error,找了几天,发现没有对应的。X86-64位版本, 或者包的尺寸有几个G大,最后尝试的态度是AMD64版,其实是可以用的。
第三步:更新Ubuntu软件,命令 sudo apt-get update (这个命令写起来也很简单,但是中国还涉及到更新源的问题。修改更新源本身就是一个猜测和盲目测试的过程,费时费力。我参考了一下CSDN里面有很多文章,具体自己搜索,这里没必要展开)
安装make, gcc, git等 sudo apt install git sudo apt install gcc sudo apt install ruby
第四步:github下载INAV2.0.0, 耐心多试两次还是可以直接从github成功下载。 git clone https://github.com/iNavFlight/inav.git
请注意,最新代码是通过这种方式获得的。如果你想得到指定版本,你应该先在左边选择tag如果使用标签,最简单的是直接下载标签版本git命令要checkout指定版本。
事实上,你可以直接尝试编译INAV但确保你几乎肯定会遇到各种各样的编辑。包括这个:
第五步: 安装gcc-arm-none-eabi, 我使用INAV2.0.0 下载的arm编译器是gcc-arm-none-eabi-7-2018-q2-update 仅供参考的版本。 这个包比较大,远远大于编译inav代码确定下载成功,然后继续下一步 可以用inav脚本在目录中 ./install-toolchain.sh
也可手动下载, 参考文章:https://blog.csdn.net/yk150915/article/details/80117082 搜索gcc arm eabi, 同时参考多篇文章,这里不展开,否则又是一大坨。
第六步: 在代码目录下编译: make TARGET=OMNIBUSF4PRO 这里Tareget 参数是你的飞控板型号,即使是F4飞控也分V3版本和pro但现在淘宝基本上都是山寨pro版本。具体参考inav/docs 以下说明,以及Makefile。
如果没有编译,并且安装在上面列出的版本中,现在应该只有缺乏支持包。看看缺少什么,根据提示在线搜索,一般可以给出方法。
若通过,在 inav/obj应该有目录.hex文件编译成功。可以和解release的hex确认字节大小。然后上板测试。
第二部分, 修改INAV代码(混控部分) 我以为需要很长时间,但当我坐下来打开它时,我发现INAV写得很清楚,没有太多炫目的句子技巧,个人非常厌恶使用非常罕见的句子,看起来很困难,也没有看到提高任何可读性。 我的目的是暂时调整混合控制通道,因为它是直接的inav 该功能在配置器中无法实现。 跳过驱动什么的,直奔主题,有与混控通道相关的文件src/main/flight/mixer.c src/main/flight/servos.c
舵机和电机可见PWM控制的混合控制是分开处理的。INAV配置器的混控界面里一样,分成电机混控和舵机混控
存储混合控制信息的数据结构:
static servoMixer_t currentServoMixer[MAX_SERVO_RULES]; static motorMixer_t currentMixer[MAX_SUPPORTED_MOTORS];
typedef struct servoMixer_s { uint8_t targetChannel; // servo that receives the output of the rule uint8_t inputSource; // input channel for this rule int8_t rate; // range [-125; 125] ; can be used to adjust a rate 0-125% and a direction uint8_t speed; // reduces the speed of the rule, 0=unlimited speed } servoMixer_t;
typedef struct motorMixer_s { float throttle; float roll; float pitch; float yaw; } motorMixer_t;
存储混合控制信息的数据结构并不复杂,例如motormix里边的throttle,roll,pitch,yaw,自然是相应配置界面中的相应项。
如果获得遥控辅助通道,那就简单了。PWM信息,即代表电机倾斜角度的信息RC通道, 重新设置相应的混合控制规则的通道值。 这里说说我做的。VT0L, 只有两个电机 两个舵机和两个舵机不仅负责垂直起落时电机的前后控制,还负责水平飞行时左右副翼的操作和类似水平尾翼的俯仰操作。 垂直/水平飞行的转换是通过新设置的辅助通道实现的,我使用CH7,CH7混合控制电机的角度,扳动或旋转,看看预期的操作模式。 说起来难,可以参考视频演示
https://www.bilibili.com/video/av70709777?from=search&seid=16415844640983981290
双发固定翼 VTOL 操纵转换演示
其实关于身体本身有很多说法,可以参考5imx相关帖子。
http://bbs.5imx.com/forum.php?mod=viewthread&tid=1551588&extra=page=1
2轴VTOL垂直起降固定翼 INAV 修改编译飞控代码
这里还有一个细节,就是开始想从角度不断调整混控比例,然后改为直接在45度以上调整几个通道的混控值。因为在45度的方向上,无论是电机转速还是舵机的滚动偏转,都有水平和垂直的重量力,所以没有必要继续调整比例。
继续找到通道输入量的存储位置servos.c的 servoMixer()函数中放置遥控器通道输入量 rcData[AUX3]里边的,
这个值是从0 ~ 500的PWM因此,我设置了连续变化值 260基本上是45度以上的位置。
添加代码在----VTOL Custom Code----- 其他的都是注释INAV原有代码, 修改后编译,最后要解释的是,由于个人使用,通道和混合控制值都是写死的INAV配置器还应配置相应的初始混合控制规则,并注意。
如下: servos.c:
void servoMixer(float dT) { int16_t input[INPUT_SOURCE_COUNT]; // Range [-500: 500] if (FLIGHT_MODE(MANUAL_MODE)) { input[INPUT_STABILIZED_ROLL] = rcCommand[ROLL];
input[INPUT_STABILIZED_PITCH] = rcCommand[PITCH];
input[INPUT_STABILIZED_YAW] = rcCommand[YAW];
} else {
// Assisted modes (gyro only or gyro+acc according to AUX configuration in Gui
input[INPUT_STABILIZED_ROLL] = axisPID[ROLL];
input[INPUT_STABILIZED_PITCH] = axisPID[PITCH];
input[INPUT_STABILIZED_YAW] = axisPID[YAW];
// Reverse yaw servo when inverted in 3D mode only for multirotor and tricopter
if (feature(FEATURE_3D) && (rcData[THROTTLE] < PWM_RANGE_MIDDLE) &&
(mixerConfig()->platformType == PLATFORM_MULTIROTOR || mixerConfig()->platformType == PLATFORM_TRICOPTER)) {
input[INPUT_STABILIZED_YAW] *= -1;
}
}
input[INPUT_FEATURE_FLAPS] = FLIGHT_MODE(FLAPERON) ? servoConfig()->flaperon_throw_offset : 0;
if (IS_RC_MODE_ACTIVE(BOXCAMSTAB)) {
input[INPUT_GIMBAL_PITCH] = scaleRange(attitude.values.pitch, -1800, 1800, -360, +360);
input[INPUT_GIMBAL_ROLL] = scaleRange(attitude.values.roll, -1800, 1800, -360, +360);
} else {
input[INPUT_GIMBAL_PITCH] = 0;
input[INPUT_GIMBAL_ROLL] = 0;
}
input[INPUT_STABILIZED_THROTTLE] = motor[0] - 1000 - 500; // Since it derives from rcCommand or mincommand and must be [-500:+500]
// center the RC input value around the RC middle value
// by subtracting the RC middle value from the RC input value, we get:
// data - middle = input
// 2000 - 1500 = +500
// 1500 - 1500 = 0
// 1000 - 1500 = -500
input[INPUT_RC_ROLL] = rcData[ROLL] - PWM_RANGE_MIDDLE;
input[INPUT_RC_PITCH] = rcData[PITCH] - PWM_RANGE_MIDDLE;
input[INPUT_RC_YAW] = rcData[YAW] - PWM_RANGE_MIDDLE;
input[INPUT_RC_THROTTLE] = rcData[THROTTLE] - PWM_RANGE_MIDDLE;
input[INPUT_RC_CH5] = rcData[AUX1] - PWM_RANGE_MIDDLE;
input[INPUT_RC_CH6] = rcData[AUX2] - PWM_RANGE_MIDDLE;
input[INPUT_RC_CH7] = rcData[AUX3] - PWM_RANGE_MIDDLE;
input[INPUT_RC_CH8] = rcData[AUX4] - PWM_RANGE_MIDDLE;
input[INPUT_RC_CH9] = rcData[AUX5] - PWM_RANGE_MIDDLE;
input[INPUT_RC_CH10] = rcData[AUX6] - PWM_RANGE_MIDDLE;
input[INPUT_RC_CH11] = rcData[AUX7] - PWM_RANGE_MIDDLE;
input[INPUT_RC_CH12] = rcData[AUX8] - PWM_RANGE_MIDDLE;
input[INPUT_RC_CH13] = rcData[AUX9] - PWM_RANGE_MIDDLE;
input[INPUT_RC_CH14] = rcData[AUX10] - PWM_RANGE_MIDDLE;
input[INPUT_RC_CH15] = rcData[AUX11] - PWM_RANGE_MIDDLE;
input[INPUT_RC_CH16] = rcData[AUX12] - PWM_RANGE_MIDDLE;
//---------------VTOL Custom Code--------------------
if(input[INPUT_RC_CH7] > 260) {
//swap roll and Yaw channel
//currentServoMixer[1].targetChannel = 3;
currentServoMixer[1].inputSource = INPUT_STABILIZED_ROLL ;
currentServoMixer[1].rate = 50;
//currentServoMixer[3].targetChannel = 4;
currentServoMixer[3].inputSource = INPUT_STABILIZED_ROLL ;
currentServoMixer[3].rate = 50;
}
else {
//currentServoMixer[1].targetChannel = 3;
currentServoMixer[1].inputSource = INPUT_STABILIZED_YAW ;
currentServoMixer[1].rate = 50;
//currentServoMixer[3].targetChannel = 4;
currentServoMixer[3].inputSource = INPUT_STABILIZED_YAW ;
currentServoMixer[3].rate = 50;
}
//---------------VTOL Custom Code--------------------
for (int i = 0; i < MAX_SUPPORTED_SERVOS; i++) {
servo[i] = 0;
}
// mix servos according to rules
for (int i = 0; i < servoRuleCount; i++) {
const uint8_t target = currentServoMixer[i].targetChannel;
const uint8_t from = currentServoMixer[i].inputSource;
/*
* Apply mixer speed limit. 1 [one] speed unit is defined as 10us/s:
* 0 = no limiting
* 1 = 10us/s -> full servo sweep (from 1000 to 2000) is performed in 100s
* 10 = 100us/s -> full sweep (from 1000 to 2000) is performed in 10s
* 100 = 1000us/s -> full sweep in 1s
*/
int16_t inputLimited = (int16_t) rateLimitFilterApply4(&servoSpeedLimitFilter[i], input[from], currentServoMixer[i].speed * 10, dT);
servo[target] += servoDirection(target, from) * ((int32_t)inputLimited * currentServoMixer[i].rate) / 100;
}
for (int i = 0; i < MAX_SUPPORTED_SERVOS; i++) {
/*
* Apply servo rate
*/
servo[i] = ((int32_t)servoParams(i)->rate * servo[i]) / 100L;
/*
* Perform acumulated servo output scaling to match servo min and max values
* Important: is servo rate is > 100%, total servo output might be bigger than
* min/max
*/
if (servo[i] > 0) {
servo[i] = (int16_t) (servo[i] * servoMetadata[i].scaleMax);
} else {
servo[i] = (int16_t) (servo[i] * servoMetadata[i].scaleMin);
}
/*
* Add a servo midpoint to the calculation
*/
servo[i] += servoParams(i)->middle;
/*
* Constrain servo position to min/max to prevent servo damage
* If servo was saturated above min/max, that means that user most probably
* allowed the situation when smix weight sum for an output was above 100
*/
servo[i] = constrain(servo[i], servoParams(i)->min, servoParams(i)->max);
}
}
mixer.c
void mixTable(const float dT)
{
int16_t input[3]; // RPY, range [-500:+500]
// Allow direct stick input to motors in passthrough mode on airplanes
if (STATE(FIXED_WING) && FLIGHT_MODE(MANUAL_MODE)) {
// Direct passthru from RX
input[ROLL] = rcCommand[ROLL];
input[PITCH] = rcCommand[PITCH];
input[YAW] = rcCommand[YAW];
}
else {
input[ROLL] = axisPID[ROLL];
input[PITCH] = axisPID[PITCH];
input[YAW] = axisPID[YAW];
if (motorCount >= 4 && mixerConfig()->yaw_jump_prevention_limit < YAW_JUMP_PREVENTION_LIMIT_HIGH) {
// prevent "yaw jump" during yaw correction
input[YAW] = constrain(input[YAW], -mixerConfig()->yaw_jump_prevention_limit - ABS(rcCommand[YAW]), mixerConfig()->yaw_jump_prevention_limit + ABS(rcCommand[YAW]));
}
}
// Initial mixer concept by bdoiron74 reused and optimized for Air Mode
int16_t rpyMix[MAX_SUPPORTED_MOTORS];
int16_t rpyMixMax = 0; // assumption: symetrical about zero.
int16_t rpyMixMin = 0;
//---------------VTOL Custom Code--------------------
if(rcData[AUX3] - PWM_RANGE_MIDDLE < 260) {
//swap roll and Yaw channel
currentMixer[0].roll = 1;
currentMixer[1].roll = -1;
currentMixer[0].yaw = 0;
currentMixer[1].yaw = 0;
}
else {
currentMixer[0].roll = 0;
currentMixer[1].roll = 0;
currentMixer[0].yaw = 1;
currentMixer[1].yaw = -1;
}
//---------------VTOL Custom Code--------------------
// motors for non-servo mixes
for (int i = 0; i < motorCount; i++) {
rpyMix[i] =
input[PITCH] * currentMixer[i].pitch +
input[ROLL] * currentMixer[i].roll +
-mixerConfig()->yaw_motor_direction * input[YAW] * currentMixer[i].yaw;
if (rpyMix[i] > rpyMixMax) rpyMixMax = rpyMix[i];
if (rpyMix[i] < rpyMixMin) rpyMixMin = rpyMix[i];
}
int16_t rpyMixRange = rpyMixMax - rpyMixMin;
int16_t throttleRange, throttleCommand;
int16_t throttleMin, throttleMax;
static int16_t throttlePrevious = 0; // Store the last throttle direction for deadband transitions
// Find min and max throttle based on condition.
if (feature(FEATURE_3D)) {
if (!ARMING_FLAG(ARMED)) throttlePrevious = PWM_RANGE_MIDDLE; // When disarmed set to mid_rc. It always results in positive direction after arming.
if ((rcCommand[THROTTLE] <= (PWM_RANGE_MIDDLE - rcControlsConfig()->deadband3d_throttle))) { // Out of band handling
throttleMax = flight3DConfig()->deadband3d_low;
throttleMin = motorConfig()->minthrottle;
throttlePrevious = throttleCommand = rcCommand[THROTTLE];
} else if (rcCommand[THROTTLE] >= (PWM_RANGE_MIDDLE + rcControlsConfig()->deadband3d_throttle)) { // Positive handling
throttleMax = motorConfig()->maxthrottle;
throttleMin = flight3DConfig()->deadband3d_high;
throttlePrevious = throttleCommand = rcCommand[THROTTLE];
} else if ((throttlePrevious <= (PWM_RANGE_MIDDLE - rcControlsConfig()->deadband3d_throttle))) { // Deadband handling from negative to positive
throttleCommand = throttleMax = flight3DConfig()->deadband3d_low;
throttleMin = motorConfig()->minthrottle;
} else { // Deadband handling from positive to negative
throttleMax = motorConfig()->maxthrottle;
throttleCommand = throttleMin = flight3DConfig()->deadband3d_high;
}
} else {
throttleCommand = rcCommand[THROTTLE];
throttleMin = motorConfig()->minthrottle;
throttleMax = motorConfig()->maxthrottle;
// Throttle compensation based on battery voltage
if (feature(FEATURE_THR_VBAT_COMP) && feature(FEATURE_VBAT) && isAmperageConfigured())
throttleCommand = MIN(throttleMin + (throttleCommand - throttleMin) * calculateThrottleCompensationFactor(), throttleMax);
}
throttleRange = throttleMax - throttleMin;
#define THROTTLE_CLIPPING_FACTOR 0.33f
motorMixRange = (float)rpyMixRange / (float)throttleRange;
if (motorMixRange > 1.0f) {
for (int i = 0; i < motorCount; i++) {
rpyMix[i] /= motorMixRange;
}
// Allow some clipping on edges to soften correction response
throttleMin = throttleMin + (throttleRange / 2) - (throttleRange * THROTTLE_CLIPPING_FACTOR / 2);
throttleMax = throttleMin + (throttleRange / 2) + (throttleRange * THROTTLE_CLIPPING_FACTOR / 2);
} else {
throttleMin = MIN(throttleMin + (rpyMixRange / 2), throttleMin + (throttleRange / 2) - (throttleRange * THROTTLE_CLIPPING_FACTOR / 2));
throttleMax = MAX(throttleMax - (rpyMixRange / 2), throttleMin + (throttleRange / 2) + (throttleRange * THROTTLE_CLIPPING_FACTOR / 2));
}
// Now add in the desired throttle, but keep in a range that doesn't clip adjusted
// roll/pitch/yaw. This could move throttle down, but also up for those low throttle flips.
if (ARMING_FLAG(ARMED)) {
for (int i = 0; i < motorCount; i++) {
motor[i] = rpyMix[i] + constrain(throttleCommand * currentMixer[i].throttle, throttleMin, throttleMax);
if (failsafeIsActive()) {
motor[i] = constrain(motor[i], motorConfig()->mincommand, motorConfig()->maxthrottle);
} else if (feature(FEATURE_3D)) {
if (throttlePrevious <= (PWM_RANGE_MIDDLE - rcControlsConfig()->deadband3d_throttle)) {
motor[i] = constrain(motor[i], motorConfig()->minthrottle, flight3DConfig()->deadband3d_low);
} else {
motor[i] = constrain(motor[i], flight3DConfig()->deadband3d_high, motorConfig()->maxthrottle);
}
} else {
motor[i] = constrain(motor[i], motorConfig()->minthrottle, motorConfig()->maxthrottle);
}
// Motor stop handling
if (ARMING_FLAG(ARMED) && (getMotorStatus() != MOTOR_RUNNING)) {
if (feature(FEATURE_MOTOR_STOP)) {
motor[i] = (feature(FEATURE_3D) ? PWM_RANGE_MIDDLE : motorConfig()->mincommand);
} else {
motor[i] = motorConfig()->minthrottle;
}
}
}
} else {
for (int i = 0; i < motorCount; i++) {
motor[i] = motor_disarmed[i];
}
}
/* Apply motor acceleration/deceleration limit */
applyMotorRateLimiting(dT);
}