通过前面的课程大家已经对飞控源码有了更深刻的认识,明白了飞控源码级是如何和地面站互动的,这一节课我们要明白飞控是如何和外设互动的,掌握这个可以让大家给自己的无人机添加实现特别功能的外部设备。

一 概念理解

外设:故名思意,即外部设备。外部设备大致可分为三类:
1.人机交互设备,如打印机,显示器,绘图仪,语言合成器。
2.计算机信息的存储设备,如磁盘,光盘,磁带。

  1. 机-机通信设备,如两台计算机之间可利用电话线进行通信,它们可以通过调制解调器完成。

外设可以简单的理解为输入设备和输出设备。如显示器只是用来显示电脑信息的输出设备, 鼠标键盘是用来输入信息的输入设备,都属于外设。计算机系统中输入、输出设备和外存储器的统称。对数据和信息起着传输、转送和存储的作用。是计算机系统中的重要组成部分。
外围设备涉及到主机以外的任何设备。外围设备是附属的或辅助的与计算机连接起来的设备。外围设备能扩充计算机系统。
对于飞控系统来说,除去飞控系统内部完成自己控制飞行的主要任务的那部分必要设备外,人们用于完成特定任务的那部分设备称为外设。无人机离开了外设也就是离开了任务,也就失去了无人机的特色,与航模无异。航拍无人机的机载航拍设备就是外设。
OpenMV:OpenMV是一个开源,低成本,功能强大的机器视觉模块。以STM32F767CPU为核心,集成了OV7725摄像头芯片,在小巧的硬件模块上,用C语言高效地实现了核心机器视觉算法,提供Python编程接口。使用者们(包括发明家、爱好者以及智能设备开发商)可以用python语言使用OpenMV提供的机器视觉功能,为自己的产品和发明增加有特色的竞争力。
OpenMV上的机器视觉算法包括寻找色块、人脸检测、眼球跟踪、边缘检测、标志跟踪等。可以用来实现非法入侵检测、产品的残次品筛选、跟踪固定的标记物等。使用者仅需要写一些简单的Python代码,即可轻松的完成各种机器视觉相关的任务。小巧的设计,使得OpenMV可以用到很多创意的产品上。比如,可以给自己的机器人提供周边环境感知能力;给智能车增加视觉巡线功能;给智能玩具增加识别人脸功能,提高产品趣味性等;甚至,可以给工厂产品线增加残次品筛选功能等。
OpenMV采用的STM32F427拥有丰富的硬件资源,引出UART,I2C,SPI,PWM,ADC,DAC以及GPIO等接口方便扩展外围功能。USB接口用于连接电脑上的集成开发环境OpenMVIDE,协助完成编程、调试和更新固件等工作。TF卡槽支持大容量的TF卡,可以用于存放程序和保存照片等。
(链接:https://www.jianshu.com/p/8ff6a7223ebb 来源:简书)
OpenMV是当下智能机器人方面比较火热的一个机器视觉模块,用小巧精悍来说明不为过。(具体可以看看星瞳科技的官网:https://singtown.com/openmv/)这一课我们先看看如何将openmv这个好东西添加到我们的飞控当中去。

二 添加外设驱动OpenMV

步骤一:添加驱动文件

利用前面的知识拷贝一份源码(版本稍微高一点)
进入ardupilot/libraries目录,新建OpenMV的底层驱动目录文件,命名为AP_OpenMV。进入该文件中新建一个AP_OpenMV.cpp文件和AP_OpenMV.h文件。
首先,在AP_OpenMV.h文件中添加如下代码。
#pragma once #include #include #include class AP_OpenMV { public: AP_OpenMV(); AP_OpenMV(const AP_OpenMV &other) = delete; AP_OpenMV &operator=(const AP_OpenMV&) = delete; // 初始化外设 void init(const AP_SerialManager& serial_manager); // 定时更新外设数据 bool update(void); uint8_t cx;//定义x坐标变量 uint8_t cy;//定义y坐标变量 uint32_t last_frame_ms;//最后一次从openmv接受数据的时间,单位是ms private: AP_HAL::UARTDriver _port; // openmv接到飞控上的串口 uint8_t _step; //数据贞的步骤记录 //存储x坐标和y坐标的临时变量 uint8_t _cx_temp; uint8_t _cy_temp; };
其次,在AP_OpenMV.cpp文件中添加如下代码。
/
OpenMV library */ #define AP_SERIALMANAGER_OPEN_MV_BAUD 115200 //串口波特率定义 #define AP_SERIALMANAGER_OPENMV_BUFSIZE_RX 64//接受缓存的容量 #define AP_SERIALMANAGER_OPENMV_BUFSIZE_TX 64//发送缓存容量 #include “AP_OpenMV.h” extern const AP_HAL::HAL& hal; //constructor AP_OpenMV::AP_OpenMV(void) { _port = NULL;//串口默认为空 _step = 0;//解析贞步骤默认为0 } void AP_OpenMV::init(const AP_SerialManager& serial_manager) { // 查找串口为_port附值,并判断openmv是否接入 if ((_port = serial_manager.find_serial(AP_SerialManager::SerialProtocol_OPEN_MV, 0))) { //注意,这里出现了SerialProtocol_OPEN_MV这个值,代码段结束后将设置这个值 _port->set_flow_control(AP_HAL::UARTDriver::FLOW_CONTROL_DISABLE); // 初始化与openmv链接的串口设置 _port->begin(AP_SERIALMANAGER_OPEN_MV_BAUD, AP_SERIALMANAGER_OPENMV_BUFSIZE_RX, AP_SERIALMANAGER_OPENMV_BUFSIZE_TX); } } bool AP_OpenMV::update() //此函数用于解析openmv传输数据的贞 { if(_port == NULL) return false; //这里用于判断串口是否为空,即是否与openmv链接正常,这一步非常重要,否则程序会奔溃 int16_t numc = _port->available();//读取已经收到的数据字节个数 uint8_t data; uint8_t checksum = 0;//校验和 for (int16_t i = 0; i < numc; i++) { //遍历读取到的所有字节数据 data = _port->read();//获取一个数据 switch(_step) { case 0: if(data == 0xA5) //判断该数据是否是贞头 _step = 1; //若是贞头,那么下一次就允许进行下一步 break; case 1: if(data == 0x5A) //判断这个数据是否是贞头2 _step = 2; //是,允许进入下一步 else _step = 0;//否,回到最上面的步骤重新判断数据 break; case 2://从这里开始正式接受openmv的有效数据(表示图像信息的数据) _cx_temp = data; _step = 3; break; case 3: _cy_temp = data; _step = 4; break; case 4://有效数据接受完毕,步骤归0,为下一次数据解析做准备 _step = 0; checksum = _cx_temp + _cy_temp; //计算校验和,就是求出两次有效数据的和与最后收到的校验和数据进行对比,用来判断接受的数据是否正确无误 if(checksum == data) { //无误,将两个有效数据确认为openmv实际的反馈,并作为飞控的处理数据 cx = _cx_temp; cy = _cy_temp; last_frame_ms = AP_HAL::millis();//记录这贞接受成功的时间戳 return true; //与openmv通信成功 } break; default: _step = 0; } } return false; }
保存上述文件,在ardupilot/libraries/AP_SerialManager/AP_SerialManager.h文件中第80行附近的AP_SerialManager类里的SerialProtocol枚举类型末尾添加一个openmv成员设为19。意思是在MP地面站的全部参数表里面串口类型的地方设置为19就表示链接openmv
enum SerialProtocol { SerialProtocol_None = -1, SerialProtocol_Console = 0, // unused SerialProtocol_MAVLink = 1, SerialProtocol_MAVLink2 = 2, // do not use - use MAVLink and provide instance of 1 SerialProtocol_FrSky_D = 3, // FrSky D protocol (D-receivers) SerialProtocol_FrSky_SPort = 4, // FrSky SPort protocol (X-receivers) SerialProtocol_GPS = 5, SerialProtocol_GPS2 = 6, // do not use - use GPS and provide instance of 1 SerialProtocol_AlexMos = 7, SerialProtocol_SToRM32 = 8, SerialProtocol_Rangefinder = 9, SerialProtocol_FrSky_SPort_Passthrough = 10, // FrSky SPort Passthrough (OpenTX) protocol (X-receivers) SerialProtocol_Lidar360 = 11, // Lightware SF40C, TeraRanger Tower or RPLidarA2 SerialProtocol_Aerotenna_uLanding = 12, // Ulanding support - deprecated, users should use Rangefinder SerialProtocol_Beacon = 13, SerialProtocol_Volz = 14, // Volz servo protocol SerialProtocol_Sbus1 = 15, SerialProtocol_ESCTelemetry = 16, SerialProtocol_Devo_Telem = 17, SerialProtocol_OpticalFlow = 18, SerialProtocol_OPEN_MV = 19, //!!!!这里!!!! };
到这里只是表示修改代码的人知道了有openmv的加入,但是对于编译器来说并不知道,所以我们需要在ardupilot/ArduCopter/wscript中第38行附近加入
def build(bld): vehicle = bld.path.name bld.ap_stlib( name=vehicle + ‘_libs’, ap_vehicle=vehicle, ap_libraries=bld.ap_common_vehicle_libraries() + [ ‘AP_ADSB’, ‘AC_AttitudeControl’, ‘AC_InputManager’, ‘AC_Fence’, ‘AC_Avoidance’, ‘AC_PID’, ‘AC_PrecLand’, ‘AC_Sprayer’, ‘AC_WPNav’, ‘AP_Camera’, ‘AP_IRLock’, ‘AP_InertialNav’, ‘AP_LandingGear’, ‘AP_Menu’, ‘AP_Motors’, ‘AP_Parachute’, ‘AP_RCMapper’, ‘AP_Avoidance’, ‘AP_AdvancedFailsafe’, ‘AP_SmartRTL’, ‘AP_Stats’, ‘AP_Beacon’, ‘AP_Arming’, ‘AP_WheelEncoder’, ‘AP_Winch’, ‘AP_Follow’, ‘AP_Devo_Telem’, ‘AP_OSD’, ‘AP_OpenMV’,//将我们的驱动加入编译 ], )
到此我们的驱动添加完毕,接下来是调用,就是让飞控定时调用我们的驱动。

步骤二:在源码中添加顶层调用

在ardupilot/ArduCopter/ArduCopter.cpp文件中第132行附近的任务表中加入openmv任务
#if LOGGING_ENABLED == ENABLED SCHED_TASK(fourhundred_hz_logging,400, 50), #endif SCHED_TASK_CLASS(AP_Notify, &copter.notify, update, 50, 90), SCHED_TASK(one_hz_loop, 1, 100), SCHED_TASK(update_OpenMV, 400, 100), //这里加入openmv的调用任务,表示运行频率400hz,最大运行时间不超过100um SCHED_TASK(ekf_check, 10, 75), SCHED_TASK(gpsglitch_check, 10, 50), SCHED_TASK(landinggear_update, 10, 75), SCHED_TASK(lost_vehicle_check, 10, 50), SCHED_TASK(gcs_check_input, 400, 180), SCHED_TASK(gcs_send_heartbeat, 1, 110), SCHED_TASK(gcs_send_deferred, 50, 550), SCHED_TASK(gcs_data_stream_send, 50, 550),
在ardupilot/ArduCopter/Copter.h文件中第673行附近写出openmv任务函数的声明
// ArduCopter.cpp void fast_loop(); void rc_loop(); void throttle_loop(); void update_batt_compass(void); void update_OpenMV(void); //openmv任务函数声明 void fourhundred_hz_logging(); void ten_hz_logging_loop(); void twentyfive_hz_logging(); void three_hz_loop(); void one_hz_loop(); void update_GPS(void); void init_simple_bearing(); void update_simple_mode(void); void update_super_simple_bearing(bool force_update); void read_AHRS(void); void update_altitude();
函数的具体实现在ardupilot/ArduCopter/ArduCopter.cpp中第317行附近
//openmv模拟输入数据函数 void Copter::update_OpenMV(void) { // simulation,在实际飞行时需要将这以下代码(到end of simulation code 处结束) bool sim_openmv_new_data = false; static uint32_t last_sim_new_data_time_ms = 0; if(control_mode != GUIDED) { last_sim_new_data_time_ms = millis(); openmv.cx = 80; openmv.cy = 60; } else if (millis()- last_sim_new_data_time_ms < 15000) { sim_openmv_new_data = true; openmv.last_frame_ms = millis(); openmv.cx = 1; openmv.cy = 1; } else if (millis()- last_sim_new_data_time_ms < 30000) { sim_openmv_new_data = true; openmv.last_frame_ms = millis(); openmv.cx = 160; openmv.cy = 120; } else { sim_openmv_new_data = false; openmv.cx = 80; openmv.cy = 60; } // end of simulation code(这里之上的是制造openmv模拟数据的代码) static uint32_t last_set_pos_target_time_ms = 0; Vector3f target = Vector3f(0, 0, 0); if(openmv.update() || sim_openmv_new_data) { //在实际飞行时需要将 sim_openmv_new_data 变量给删掉 Log_Write_OpenMV(); if(control_mode != GUIDED) return; int16_t target_body_frame_y = (int16_t)openmv.cx - 80; // QQVGA 160 120 int16_t target_body_frame_z = (int16_t)openmv.cy - 60; float angle_y_deg = target_body_frame_y 60.0f / 160.0f; float angle_z_deg = target_body_frame_z 60.0f / 120.0f; Vector3f v = Vector3f(1.0f, tanf(radians(angle_y_deg)), tanf(radians(angle_z_deg))); v = v / v.length(); const Matrix3f &rotMat = copter.ahrs.get_rotation_body_to_ned(); v = rotMat v; target = v * 10000.0f; // distance 100m target.z = -target.z; // ned to neu Vector3f current_pos = inertial_nav.get_position(); target = target + current_pos; if(millis() - last_set_pos_target_time_ms > 500) { // call in 2Hz // wp_nav->set_wp_destination(target, false); mode_guided.set_destination(target, false, 0, true, 0, false); last_set_pos_target_time_ms= millis(); } } }

若有收获,就点个赞吧