感谢博主的无私分享:https://blog.csdn.net/SIR_wkp/article/details/80548262
看到很多教程中讲到PX4飞控的协处理器的作用,包括各类遥控器信号的输入、PWM信号输出、以及与FMU的通讯。但是究竟如何实现的,最近几天自己看代码总算搞懂了,在这里分享一下。
一,代码的组成和编译过程。
原始代码的主要代码位于 Firmware/src/modules/px4iofirmware文件夹中。打开CMakeList.txt文件,可以看到包含的文件还有位于其他文件夹中的部分源文件,以及apps、nuttx、nosys等等库文件。其实在编译的过程中,针对具体对象的make指令会新建一个build_XXX文件夹,然后将需要编译的源代码和产生的中间过程都放进去,而一些独立的模块会编译成库文件,比如libnuttx.a 就是针对nuttx系统生成的库文件,在CMakeList中在使用add_link_libraries实现功能块的链接。
以上截图来自Firmware/src/modules/px4iofirmwareCMakeList.txt文件
指定编译目标的指令是add_executable,最后再使用自定义的px4_nuttx_create_bin的CMake自定义function来生成px4io-v2.bin文件。整个编译过程结束之后果然可以在Firmware/build_px4fmu-v2_default/src/modules/px4iofirmware 文件夹中找到这两个文件。
以上截图来自Firmware/src/modules/px4iofirmware/CMakeList.txt文件
二、源码分析。
要分析源码首先需要知道各函数的执行顺序,打开链接文件ld.script,可以看到整个代码使用的flash空间是从0x08001000开始的60K空间,之前的空间被bootloader占据。代码的入口函数是__start,与PX4FMU相同;熟悉PX4FMU启动过程的朋友都知道,接下来会进行一系列OS的启动过程,最后到os_bringup建立第一个用户任务。这也就是nuttx模块可以作为链接库来复用的原因,因为具有相同的执行过程。利用CONFIG_USER_ENTRYPOINT宏进入用户定义的函数中,对PX4FMU来说这个入口是nsh_main()函数;对PX4IO来说,这个入口是usr_start()函数,位于Firmware/src/modules/px4iofirmware/px4io.c文件中。
以上截图来自ld.script文件
以上截图例子os_bingup()函数
usr_start()函数首先进行了相关的初始化过程,然后进入了一个for( ; ; )循环。也就是说,PX4IO除了系统自带的idle任务之外,只会运行这一个任务,非常类似于简单的裸机嵌入式程序。这也是由IO芯片相对简单的功能属性决定的。
IO芯片的功能,最关键的就是遥控输入采集和PWM输出,但这都与FMU的通讯关系密切。usr_main()的初始化中调用interface_init()函数初始化了与FMU通讯的串口,设置了串口的波特率,串口的发送和接收都设置为DMA方式;循环部分的controls_tick()函数则用来进行通讯,在这里面实现遥控输入采集和PWM输出的调用。
controls_tick()函数依次检查DSM、SBUS、PPM三种接收机的数据更新情况,将遥控器数据保存到r_page_raw_rc_input[]数组中,这个数组的结构挺复杂,同时保存了各种遥控输入的状态和原始遥控输入数据,一言半语讲不清楚,有兴趣的朋友可以自己去看代码;然后进行了死区检测、归一化(其实是归10000化,看代码)、重定向、失控保护检测等,将计算结果转存到对应的数组中。
死区检测+归一化,截图来自Firmware/src/modules/px4iofirmware/controls.c文件
失控保护检测,截图来自Firmware/src/modules/px4iofirmware/controls.c文件
与FMU通讯中的数据接收,采用了中断方式。在interface_init()中定义了串口的中断服务函数为serial_interrupt(),并开启中断。serial_interrupt()接收数据、清楚中断后,再调用rx_dma_callback()进行DMA数据的处理。从这里可以分成了接收和发送两部分,接收部分调用了rx_handle_packet()函数处理对应包的内容,并准备好发送的内容;发送部分则直接使能DMA发送功能完成数据发送。截图中rx_代表数据接收,tx_代表数据发送,tx_dma是串口发送数据的句柄,首先计算CRC写入dma_packet里,然后设置DMA的地址和包大小,然后stm32_dmastart()使能DMA完成数据发送。
PX4IO在中断中进行数据接收和发送处理的过程,截图来自Firmware/src/modules/px4iofirmware/serial.c文件
从以上处理过程可以看出,PX4IO不会主动向PX4FMU发送任何数据,所有的发送都出于接收到数据之后的回复。protocol.h文件中定义了串口通讯的帧结构,是按照page来分别发送不同的数据的。
参考文章:https://blog.csdn.net/czyv587/article/details/51445308
这篇后面有一张每个page的列表。
除此之外,usr_start()的大循环中还会执行check_reboot(),这与PX4IO的固件升级有关。因为固件升级必须重新进入bootloader,因此必须重新芯片。
三、PX4FMU中与PX4IO通讯的代码。
这部分代码位于Firmware/src/drivers/px4io文件夹中,也有一个名为px4io的文件,但是后缀是.cpp不是.c。与FMU中所有任务的入口函数类似都是XXX_main()的形式,在px4io.cpp文件中的函数名是px4io_main,从这里可以带参数调用各种与IO芯片相关的操作。下面介绍从rcS文件中使用到的参数出发,来看其中几个。
rcS文件,从上往下,第一次调用px4io 是这里。px4io checkcrc ,在往前一点可以看到,这是对之前编译生成的px4io-v2.bin文件进行CRC检测,目的是在进行固件烧写的串口通讯中,可以让PX4IO芯片的bootloader确认完整无误的收到了整个用于升级固件的文件。
截图来自rcS文件
第二次使用是 px4io forceupdate 14662 ${IO_FILE},这是将固件烧写进PX4IO芯片。具体实现过程中,与PX4IO中check_reboot()函数关系密切。
然后就是 px4io start,作用是创建一个新的任务,实现与PX4IO的通讯。落实到代码中就是调用start()函数,在start()函数中,get_interface()获取与IO芯片通讯的串口的设备指针,new一个PX4IO类,在构造函数中将this指针赋值给当前namespace中的g_dev指针变量,以后就可以用这个指针来直接调用新建PX4IO类实例中的所有成员。
接下来使用g_dev指针调用g_dev->init()来完成所需的初始化工作,在init()函数的最后,创建一个task,入口函数是PX4IO::task_main_trampoline,更进一步,转入实际的入口PX4IO::task_main()。
PX4IO::init()函数中建立的任务
在task_mian()的主循环中,主要的工作有:根据msg的内容设置控制组,按照IO_POLL_INTERVAL的时长从IO芯片获取状态、遥控输入、PWM输出。同时发布对应的orb消息,以供飞控其他功能模块使用。
PX4IO::task_main()循环中实现的一些功能