出于个人模型爱好,一直想尝试VTOL垂直起落,夏天时候尝试了几种离题太远就不说了。11前做地铁冥想时忽然闪出个方案。基本上类似于鱼鹰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包的大小不那么吓人。用户也比较多。然后就是想当然用了X86版本,结果编译时出现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%3D1
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);
}