控制模式中的定点模式(需配合光流模块),此模式下,通过光电定点,激光(2m以内)定高的方式实现四轴稳定悬停,如果挂载光流模块,此模式作用相同于定高模式。注意:此模式关闭空翻功能。
通信链路
MiniFly 遥控器、四轴主机、上位机之间通信链路关系如图所示:
- Radiolink 链路:无线通信方式
- Usblink 链路:STM32 USB 虚拟串口通信方式
- WiFilink 链路:手机控制命令数据通过 WiFi 摄像头模块接收并转成串口方式输出与四轴通信
当遥控器与四轴 NRF51822 通信成功后,遥控器会不断发送数据包给 NRF51822,NRF51822 接收并解析,然后再转发给 STM32F411,STM32F411 接收到后会立即向 NRF51822返回一条数据包,一应一答模式。
如果遥控器连接了上位机,遥控器将四轴返回的数据转发给上位机,即遥控器会将接收到的应答数据包先解析,解析完成后转发给上位机。同样,上位机发下来的数据遥控器也会先解析再转发给四轴。Radiolink 链路中,遥控器定周期发送控制命令,四轴定周期返回姿态和其他数据。
程序框架
MiniFly 遥控器固件 Firmware_F103 工程分组:
可以看到工程分有 14 个分组,其中
- CONFIG 分组主要是配置参数保存至 Flash 相关的代码。
- COMMUNICATE 分组主要是跟通信相关的代码。
- GUI_APP分组主要是界面显示相关的代码。
- COMMON 分组主要是调试相关的代码。
- HARWARE 分组主要是硬件底层驱动相关代码。
- GUI_DRV分组主要是界面驱动相关代码。
部分代码关系图:
adc.c 主要实现采集摇杆电位器电压 AD 值。
joystick.c 主要实现将 AD值转为 THRUST、YAW、PITCH、ROLL 对应百分比。
remoter.c 主要实现将百分比乘以设定速度值并打包成 ATKP 包格式,然后以 10ms 周期性发送到 radiolink.c 的发送队列中,即 上述的 commanderTask。
radiolink.c 主要实现实时发送 ATKP 数据包给四轴,发送成功四轴会返回一个应答包,即上述的 radiolinkTask。
usblink.c 主要实现发送 ATKP 格式数据包到上位机,接收上位机串口数据并打包为 ATKP 格式,即上述的 usblinkTxTask 和 usblinkRxTask。
aktp.c 主要实现解析四轴通过 radiolink.c 返回的应答包并通过 usblink.c 转发给上位机,上位
机 通 过 usblink.c 发下来 的 数 据 包 将 通 过 radiolink 转 发 给 四 轴 , 即 上 述 的radiolinkDataProcessTask 和 usblinkDataProcessTask。
usblink.c 是 USB_LIB 驱动代码上构建的,USB_LIB 即 USB_CONFIG 分组和 USB_CORE 分组的驱动代码。
USB_LIB 主要实现了USB 虚拟串口的功能。
main_ui.c、menu_ui.c、debug_ui.c 及其他 xx_ui.c 均是在 GUI_DRV 分组里的驱动代码上构建的,GUI_DRV 驱动代码主要是 oled 驱动代码、oled 显示字符串及汉字代码、oled 基本绘图代码等。
mian_ui.c 主要实现主界面显示、低电量报警、解锁加锁、一键起飞降落、紧急停机、一键翻滚、切换至调试界面、切换至微调界面等功能。
menu_ui.c 主要实现菜单显示功能。
debug_ui.c 主要实现显示调试界面。界面切换是通过按键来实现,所以几乎所有的界面代码 xx_ui.c 都调用了 keyTask.c。部分界面需要蜂鸣器报警功能,所以调用了beepTask.c。
源码解析
USER 分组的 main.c包含了所有硬件初始化和任务的创建的代码。
int main(void)
{
NVIC_SetVectorTable(FIRMWARE_START_ADDR,0);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);/*中断配置初始化*/
delay_init(); /*delay初始化*/
configParamInit(); /*配置参数初始化*/
ledInit(); /*led初始化*/
oledInit(); /*oled初始化*/
beepInit(); /*蜂鸣器初始化*/
keyInit(); /*按键初始化*/
joystickInit(); /*摇杆初始化*/
usb_vcp_init(); /*usb虚拟串口初始化*/
radiolinkInit(); /*无线通信初始化*/
usblinkInit(); /*usb通信初始化*/
displayInit(); /*显示初始化*/
xTaskCreate(startTask, "START_TASK", 100, NULL, 2, &startTaskHandle);/*创建起始任务*/
vTaskStartScheduler();/*开启任务调度*/
while(1){};/* 任务调度后不会执行到这 */
}
任务创建代码如下:
void startTask(void *param)
{
taskENTER_CRITICAL(); /*进入临界区*/
xTaskCreate(radiolinkTask, "RADIOLINK", 100, NULL, 6, &radiolinkTaskHandle);/*创建无线连接任务*/
xTaskCreate(usblinkTxTask, "USBLINK_TX", 100, NULL, 5, NULL); /*创建usb发送任务*/
xTaskCreate(usblinkRxTask, "USBLINK_RX", 100, NULL, 5, NULL); /*创建usb接收任务*/
xTaskCreate(commanderTask, "COMMANDER", 100, NULL, 4, NULL); /*创建飞控指令发送任务*/
xTaskCreate(keyTask, "BUTTON_SCAN", 100, NULL, 3, NULL); /*创建按键扫描任务*/
xTaskCreate(displayTask, "DISPLAY", 200, NULL, 1, NULL); /*创建显示任务*/
xTaskCreate(configParamTask, "CONFIG_TASK", 100, NULL, 1, NULL);/*创建参数配置任务*/
xTaskCreate(radiolinkDataProcessTask, "DATA_PROCESS", 100, NULL, 6, NULL); /*创建无线通信数据处理任务*/
xTaskCreate(usblinkDataProcessTask, "DATA_PROCESS", 100, NULL, 6, NULL); /*创建USB通信数据处理任务*/
vTaskDelete(startTaskHandle); /*删除开始任务*/
taskEXIT_CRITICAL(); /*退出临界区*/
}
功能任务作用
初始化代码主要是硬件底层驱动的初始化,初始化完毕先创建一个起始任务。任务创建代码主要是在起始任务里再创建系统功能任务,任务创建完毕之后删除起始任务并启动任务调度。
- radiolinkTask 主要功能是发送 ATKP 数据包给四轴,并接收四轴返回的应答包。
radiolinkTask 函数在 radiolink.c 中。 - usblinkTxTask 主要功能是给 ATKP 数据包加上帧头和校验并发送给上位机。
usblinkTxTask 函数在 usblink.c 中。 - usblinkRxTask 主要功能是接收上位机发下来的串口数据,按照 ATKP 格式打包。usblinkRxTask 函数在 usblink.c 中。
- commanderTask 主要功能是将采集摇杆电位器的 AD 值转换为姿态控制命令,并以 10ms的周期通过 radiolink 链路发送给四轴。
commanderTask 函数在 remoter_ctrl.c 中。 - keyTask 主要功能是扫描按键,根据按键按下的时间来区分长按和短按。
keyTask 函数在 keyTask.c 中。 - displayTask 主要功能是显示界面,50ms 刷新一次界面。
displayTask 函数在 display.c 中。 - configParamTask 主要功能是保存参数。任务中 1000ms 判断一次配置参数有无改变,当有参数改变后 6S 内不再改变则将新参数写入 Flash。这样做的目的是避免在微调四轴时频繁写 Flash,Flash 擦写次数是有限的。
configParamTask 函数在 config_param.c 中。 - radiolinkDataProcessTask 主要功能是处理四轴返回的应答包数据,处理完之后再通过usblink 链路转发给上位机。
radiolinkDataProcessTask 函数在 atkp.c 中。 - usblinkDataProcessTask 主要功能是处理上位机发下来的 ATKP 数据包,处理完之后通过radiolink 链路转发给四轴。上位机发下来的 ATKP 数据包由 usblinkRxTask 打包。
usblinkDataProcessTask 函数在 atkp.c 中。
Bootloader+Firmware程序烧录问题
bootloader起始地址(BOOTLOADER_START_ADDR) : 0x8000000
固件起始地址(FIRMWARE_START_ADDR) : 0x8002400
① 中断向量表设置函数 NVIC_SetVectorTable() 的定义与实现
void NVIC_SetVectorTable(uint32_t NVIC_VectTab, uint32_t Offset)
{
/* Check the parameters */
assert_param(IS_NVIC_VECTTAB(NVIC_VectTab));
assert_param(IS_NVIC_OFFSET(Offset));
SCB->VTOR = NVIC_VectTab | (Offset & (uint32_t)0x1FFFFF80);
}
/**
* @brief Sets the vector table location and Offset.
* @param NVIC_VectTab: specifies if the vector table is in RAM or FLASH memory.
* This parameter can be one of the following values:
* @arg NVIC_VectTab_RAM
* @arg NVIC_VectTab_FLASH
* @param Offset: Vector Table base offset field. This value must be a multiple
* of 0x200.
* @retval None
*/
NVIC_SetVectorTable() 在固件程序中的misc.c 中给出了定义与实现,主要用于设置向量表的位置和偏移。其中,Offset为向量表基偏移字段,注意此值必须是0x200的倍数。
实现代码首先是对参数进行正确性判断,然后就是对寄存器进行赋值。
② keil 中如何查看代码大小?
编译一次工程后,双击工程名,如Bootloader_F103,此时会打开.map文件,滚动到文件最底部会有内存占用情况的详细信息。
- Code:代码和数据占用空间。
- RO size:表示程序占用Flash空间的大小。
- RW size:表示运行时占用RAM的大小。
- ROM size:下载程序到ROM即 Flash时,所占用的最小空间。
由上面编译的结果可知,Bootloader 程序占用 8.52kB的 Flash和 10.57kB的 RAM。
同理可得,Firmware 程序占用 54.78kB的 Flash和 17.14kB的 RAM。
而遥控器的STM32F103C8T6的Flash为64K,SRAM为20K。RAM是覆盖的,Firmware启动后可以占用全部的RAM,Bootload的使命在跳转到 Firmware后已经结束了,只是还占着 Flash而已,两个程序分别占用 RAM的大小均小于20K,加起来共占用 Flash的大小为 63.3K < 64K,由此可得,芯片能装得下 Bootloader与 Firmware的程序代码。
Bootloader占用 8.52kB的 Flash,转换成十六进制即为 0x2148。
在config_param.h 中,
固件起始地址FIRMWARE_START_ADDR 定义为:
#define FIRMWARE_START_ADDR (FLASH_BASE + BOOTLOADER_SIZE)
BOOTLOADER_SIZE为:
#define BOOTLOADER_SIZE (9*1024) /*9K bootloader*/
而Flash 基址为(stm32f10x.h):
#define FLASH_BASE ((uint32_t)0x08000000) /*!< FLASH base address in the alias region */
最终得出固件起始地址为起始地址 0x8002400,还给Bootloader程序预留了一定空间。
(0x2400为 0x200的整数倍)
③ NVIC_SetVectorTable() 的调用
在main.c 主函数中一开始就调用:
NVIC_SetVectorTable(FIRMWARE_START_ADDR,0);
设置了固件起始地址为起始地址NVIC_VectTab为 0x8002400,偏移量 Offset为 0。
④ Bootloader程序中的 isUpgradeFirmware() 函数
//判断进行固件升级还是跳转APP
void isUpgradeFirmware(void)
{
if(READ_KEY_L() == 0)
{
delay_ms(1500);
if(READ_KEY_L() != 0)
{
iap_load_app(FIRMWARE_START_ADDR);
}
}
else
{
iap_load_app(FIRMWARE_START_ADDR);
}
LED_BLUE = 0;
}
遥控器开机时,先运行 Bootloader 程序,判断当前KEY_L 键是否按下,如果按下大于 3S 则留在 Bootloader 程序等待固件升级,否则跳转至固件程序。
我们可以在Bootloader程序中的 iap.c中查看到 iap_load_app() 函数的定义与实现:
//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(u32 appxaddr)
{
if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000) //检查栈顶地址是否合法.
{
jump2app=(iapfun)*(vu32*)(appxaddr+4); //用户代码区第二个字为程序开始地址(复位地址)
MSR_MSP(*(vu32*)appxaddr); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
for(int i = 0; i < 8; i++)
{
NVIC->ICER[i] = 0xFFFFFFFF; /* 关闭中断*/
NVIC->ICPR[i] = 0xFFFFFFFF; /* 清除中断标志位 */
}
jump2app(); //跳转到APP.
}
}
⑤ Bootloader程序中的 IAP_Response() 函数
//用来响应上位机
void IAP_Response()
{
TransportProtocol.Device_Address = 0x01; //设备地址
TransportProtocol.Sequence = TransportProtocol.Sequence; //帧序列和收到的一致,这里不改变
TransportProtocol_Manager.Packed(); //打包
usbsendData(TransportProtocol_Manager.Buf, TransportProtocol_Manager.FrameTotalLength);
}
Bootloader程序有专门用来响应上位机的函数 IAP_Response(),而Firmware程序没有,且Bootloader程序有 iap_load_app() 函数来引导固件程序,这就是为什么要先烧录Bootloader程序,再烧录用户程序的原因了。
这里涉及到一个很重要的设计方法——IAP(应用编程)
IAP是In Application Programming的首字母缩写,IAP是用户自己的程序在运行过程中对User Flash的部分区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信口对产品中的固件程序进行更新升级。
通常在用户需要实现IAP功能时,即用户程序运行中作自身的更新操作,需要在设计固件程序时编写两个项目代码,第一个项目程序不执行正常的功能操作,而只是通过某种通信管道(如USB、USART)接收程序或数据,执行对第二部分代码的更新;第二个项目代码才是真正的功能代码。这两部分项目代码都同时烧录在User Flash中,当芯片上电后,首先是第一个项目代码开始运行,它作如下操作:
1)检查是否需要对第二部分代码进行更新
2)如果不需要更新则转到 4)
3)执行更新操作
4)跳转到第二部分代码执行
第一部分代码必须通过其它手段,如JTAG或ISP烧入;第二部分代码可以使用第一部分代码IAP功能烧入,也可以和第一部分代码一道烧入,以后需要程序更新是再通过第一部分IAP代码更新。
对于STM32来说,因为它的中断向量表位于程序存储器的最低地址区,为了使第一部分代码能够正确地响应中断,通常会安排第一部分代码处于Flash的开始区域,而第二部分代码紧随其后。
在第二部分代码开始执行时,首先需要把CPU的中断向量表映像到自己的向量表,然后再执行其他的操作。
如果IAP程序被破坏,产品必须返厂才能重新烧写程序,这是很麻烦并且非常耗费时间和金钱的。针对这样的需求,STM32在对Flash区域实行读保护的同时,自动地对用户Flash区的开始4页设置为写保护,这样可以有效地保证IAP程序(第一部分代码)区域不会被意外地破坏。
(引用自 百度百科)