Nuttx 驱动开发手册
目录
-
Nuttx 代码获取编译
-
Nuttx 启动流程
-
Nuttx BootLoader 开发之源码分析
-
gpio 驱动分析
-
I2c驱动分析
-
PX4 框架分析
-
UORB 进程间通讯分析
-
PX4应用层驱动分析并实现例程
-
串口驱动GPS 驱动分析
-
mavlink 飞行控制协议分析
1.Nuttx
代码获取编译
可以参考官网安装过程
https://nuttx.apache.org/docs/latest/quickstart/install.html
1.1 ubuntu 下载依赖
#sudo apt install \ bison flex gettext texinfo libncurses5-dev libncursesw5-dev \ gperf automake libtool pkg-config build-essential gperf genromfs \ libgmp-dev libmpc-dev libmpfr-dev libisl-dev binutils-dev libelf-dev \ libexpat-dev gcc-multilib g++-multilib picocom u-boot-tools util-linux
1.2 KConfig frontend
#sudo apt install kconfig-frontends
1.3 Toolchain
#sudo apt install gcc-arm-none-eabi binutils-arm-none-eabi
1.4 Download NuttX
#mkdir nuttxspace #cd nuttxspace #git clone https://github.com/apache/incubator-nuttx.git nuttx #git clone https://github.com/apache/incubator-nuttx-apps apps
1.5 查看nuttx支持的所有硬件平台的配置,可以先查看stm32 的配置
#./tools/configure.sh -L | less | grep stm32
1.6 选择一个stm32的配置,你可以选择其他型号的配置
#cd nuttx #./tools/configure.sh -l stm32f4discovery:nsh
1.7 然后你可以通过使用基于菜单的配置系统来定制这种配置
#cd nuttx #make menuconfig
1.8 编译nuttx
#cd nuttx
#make -j4
1.10 使用openocd
烧写固件
#sudo apt install openocd
连接好你的USB 线进行烧写
#cd nuttx/
#openocd -f interface/stlink-v2.cfg -f target/stm32f1x.cfg -c 'init' \
-c 'program nuttx/nuttx.bin verify reset' -c 'shutdown'
1.11 使用nuttshell 进行交互
#picocom -b 115200 /dev/ttyUSB0
你可能必须把自己加入到Linux上的dialout组,才能有权限访问串口。
#gpasswd -a <user> dialout
1.12 NuttX构建。
#cd nuttx
#./tools/configure.sh -l sim:nsh
Copy files
Select CONFIG_HOST_LINUX=y
Refreshing...
1.13 Build & run
#make clean; make #./nuttx login: 登录用户名称和密码 用户名: admin 密码: Administrator
1.14 从另一个终端窗口,杀死模拟器。 #sudo pkill nuttx
2. Nuttx 启动流程
2.1 nuttx 的启动流程一共有五个阶段
1 OSINIT_POWERUP()
|
2 OSINIT_BOOT()
|
3 OSINIT_TASKLISTS()
|
4 OSINIT_MEMORY()
|
5 OSINIT_HARDWARE()
|
6 OSINIT_HARDWARE()
|
2.2下面根据源码进行分析
vi ./include/nuttx/init.h +53
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
enum nx_initstate_e { OSINIT_POWERUP = 0, 上电初始化BSS段 OSINIT_BOOT = 1, bootloader 初始化完成,但是系统中的服务还未启动 OSINIT_TASKLISTS = 2, 准备分配任务列表 OSINIT_MEMORY = 3, 内存管理初始化 OSINIT_HARDWARE = 4, MCU 初始化完成,并初始化了驱动 OSINIT_OSREADY = 5 操作系统完全初始化完成 }; |
2.3 MCU 执行到 nx_start
系统起来之后即为完成 OSINIT_BOOT
阶段。
2.4在 OSINIT_TASKLISTS
阶段会去初始化任务列表。
2.5在 OSINIT_MEMORY
阶段需要率先初始化信号量(因为接下来很多系统组件需要使用信号量),然后初始化内存管理。
2.6在 OSINIT_HARDWARE
阶段需要先初始化文件系统(因为后面的设备驱动需要使用到文件系统,这涉及到了 nuttx 的驱动管理方式,后面我会再讲。),然后配置 中断向量表、看门狗、时钟、系统 tick 、系统信号、系统消息队列、网络、线程栈内容初始化、注册标准的设备(如:/dev/null
,/dev/zero
,/dev/loop
./dev/random
)。
2.7在 OSINIT_OSREADY
阶段需要完成多系统相关的初始化(如启动 IDLE 线程)。
2.8下面是 启动过程的分析 vi ./sched/init/nx_start.c +260
2.9除了上面的分析,网上还有其他博主对启动流程分析图,这个更为详细。如下所示:
按照stm32 启动例程来说
vi arch/arm/src/stm32/stm32_start.c +185
vi
boards/arm/stm32/axoloti/src/stm32_boot.c +53
对上面的流程加以分析
__start-- #处理器执行的第一条指令 | vi arch/arm/src/stm32/stm32_start.c +185 硬件开始初始化位置 v stm32_clockconfig()------ #初始化时钟 | v rcc_reset() #复位rcc stm32_stdclockconfig() #初始化标准时钟 rcc_enableperipherals() #使能外设时钟 | -------------------- | v stm32_fpuconfig() #配置fpu,shenzhou/nsh未调用 stm32_lowsetup() #基本初始化串口,之后可以使用up_lowputc() stm32_gpioinit() #初始化gpio,只是调用stm32_gpioremap()设置重映射 up_earlyserialinit() #初始化串口,之后可以使用up_putc() stm32_boardinitialize()-- #板级初始化 | v stm32_spiinitialize() #初始化spi,只是调用stm32_configgpio()设置gpio stm32_usbinitialize() #初始化usb,只是调用stm32_configgpio()设置gpio board_led_initialize() #初始化led,只是调用stm32_configgpio()设置gpio | -------------------- os_start()--------------- #初始化操作系统 vi sched/init/nx_start.c +313 开始初始化操作系统位置 | dq_init() #初始化各种状态的任务列表(置为null) g_pidhash[i]= #初始化唯一可以确定的元素--进程ID g_pidhash[PIDHASH(0)]= #分配空闲任务的进程ID为0 g_idletcb= #初始化空闲任务的任务控制块 sem_initialize()-- #初始化信号量 | v dq_init() #将信号量队列置为null sem_initholders() #初始化持有者结构以支持优先级继承,shenzhou/nsh未调用 | -------- | v up_allocate_heap() #分配用户模式的堆(设置堆的起点和大小) kumm_initialize() #初始化用户模式的堆 up_allocate_kheap() #分配内核模式的堆,shenzhou/nsh未调用 kmm_initialize() #初始化内核模式的堆,shenzhou/nsh未调用 task_initialize() #初始化任务数据结构,shenzhou/nsh未调用 irq_initialize() #将所有中断向量都指向同一个异常中断处理程序 wd_initialize() #初始化看门狗数据结构 clock_initialize() #初始化rtc timer_initialize() #配置POSIX定时器 sig_initialize() #初始化信号 mq_initialize() #初始化命名消息队列 pthread_initialize() #初始化线程特定的数据,空函数 fs_initialize()--- #初始化文件系统 | v sem_init() #初始化节点信号量为1 files_initialize() #初始化文件数组,空函数 | -------- | v net_initialize()-- #初始化网络 | v uip_initialize() #初始化uIP层 net_initroute() #初始化路由表,shenzhou/nsh未调用 netdev_seminit() #初始化网络设备信号量 arptimer_init() #初始化ARP定时器 | -------- | v up_initialize()--- #处理器特定的初始化 | v up_calibratedelay()#校准定时器 up_addregion() #增加额外的内存段 up_irqinitialize() #设置中断优先级,关联硬件异常处理函数 up_pminitialize() #初始化电源管理,shenzhou/nsh未调用 up_dmainitialize() #初始化DMA,shenzhou/nsh未调用 up_timerinit() #初始化定时器中断 devnull_register() #注册/dev/null devzero_register() #注册/dev/zero,shenzhou/nsh未调用 up_serialinit() #注册串口控制台/dev/console和串口/dev/ttyS0 up_rnginitialize() #初始化并注册随机数生成器,shenzhou/nsh未调用 up_netinitialize() #初始化网络,是arch/arm/src/chip/stm32_eth.c中的 up_usbinitialize() #初始化usb驱动,shenzhou/nsh未调用 board_led_on() #打开中断使能led,但很快会被其它地方的led操作改变状态 | -------- | v lib_initialize() #初始化c库,空函数 group_allocate() #分配空闲组 group_setupidlefiles() #在空闲任务上创建stdout、stderr、stdin group_initialize() #完全初始化空闲组 os_bringup()------ #创建初始任务 | v KEKERNEL_THREAD() #启动内核工作者线程 board_initialize() #最后一刻的板级初始化,shenzhou/nsh未调用 TASK_CREATE() #启动默认应用程序 | -------- | v for up_idle() #空闲任务循环 | -------------------- | v for(;;) #不应该到达这里 |
|
3.Nuttx BootLoader 开发之源码分析
3.1下面主要是以STM32为例子,首先分析一下stm32 的3种启动方式启动方式
Cortex M3的内核有三种启动方式,其分别是:
3.1.1.通过boot引脚设置可以将中断向量表定位于SRAM区,即起始地址为0x2000000,同时复位后PC指针位于0x2000000处;
3.1.2.通过boot引脚设置可以将中断向量表定位于FLASH区,即起始地址为0x8000000,同时复位后PC指针位于0x8000000处;
3.1.3.通过boot引脚设置可以将中断向量表定位于内置Bootloader区,M3单片机复位后,从0x00000000取栈指针(SP),从0x00000004取复位向量(PC),有了栈指针和复位向量后,单片机就按照正常流程运行了,单片机启动默认先运行BootLoader,所以默认的中断向量表位置是BootLoader的中断向量表。
3.1.4 Cortex-M3单片机中断向量表的重要性
Cortex-M3单片机有一个管理中断向量表的寄存器,叫做向量表偏移量寄存器(VTOR)(地址:0xE000_ED08)。
STM32的BootLoader程序一般是在0x08000000,不是在0x00000000是因为STM32的重映射技术(不符合Cortex-M3),所以BootLoader的中断向量表在0x08000000那里。如果我们新下载的程序在0x08060000,并且新程序的中断向量表在起始地址,那么下载完程序,为了新程序能正确运行,我们需要把中断向量表重定位到0x08060000那里,再跳转到新程序运行。
Cortex-M3内核规定,起始地址必须存放堆顶指针,而第二个地址则必须存放复位中断入口向量地址,这样在Cortex-M3内核复位后,会自动从起始地址的下一个32位空间取出复位中断入口向量,跳转执行复位中断服务程序。Cortex-M3内核是固定了中断向量表的位置而起始地址是可变化的.
3.2 Bootloader源码:CPU启动过程
3.2.1上电启动:EXTERN (vector_table)
1)初始化堆栈指针 .initial_sp_value = &_stack,
2)硬件错误为阻塞 .hard_fault = hard_fault_handler,
3)中断控制器 .irq = { IRQ_HANDLERS }
4)系统的复位入口函数 .reset = reset_handler,
3.2.2入口函数:ENTRY(reset_handler)
1)定义数据段 .data和.bss
2)pre_main()(开启协处理器)
3)main() //分为main_f1.c和main_f4.c
3.2.3.main函数:main(void)
1)board_init()(开发板的初始化)
2)bootloader()(nuttx系统的设置)
3)jump_to_app()(测试引导nuttx系统)
3.2.4.bootloader引导进入.vectors向量表:stm32_vectors.S
1)定义堆栈的大小
2)定义STM32的中断向量表
3)入口函数是ENTRY(__start)
3.2.5.入口函数是ENTRY(__start)
1)stm32的配置和初始化
2)nuttx系统的入口函数os_start() //\Firmware\NuttX\nuttx\sched\os_start.c启动Nuttx系统
3.2.6.系统入口函数os_start()
1)nuttx系统的初始化
2)nuttx系统的启动进程os_bringup()
3.2.7.系统的启动进程os_bringup()
1)创建内核进程
2)创建用户进程
3.2.8创建init进程(main_t)CONFIG_USER_ENTRYPOINT
IO板 : CONFIG_USER_ENTRYPOINT =user_start
Fmu板:CONFIG_USER_ENTRYPOINT = nsh_main
3.2.9.下面以 stm32 F1 为例子,从main_f1.c 启动初始化中断向量表,在bl.c中 启动nuttx,剩下的可以主要看一下串口虚拟代码路径kinetis 和os烧写拷贝实现过程。
├── bl.c
├── bl.h
├── board_types.txt
├── Bootloader.sublime-project
├── cdcacm.h
├── crypto_hal
│ ├── crypto.h
│ ├── image_toc.c
│ ├── image_toc.h
│ ├── monocypher
│ │ ├── crypto.c
│ │ ├── Makefile.include
│ │ └── public_key.h
│ ├── README.txt
│ └── test_key
│ └── key0.pub
├── hw_config.h
├── Jenkinsfile
├── jig_px4fmu.cfg
├── kinetis //这个路径里面是USB CDC 串口虚拟代码
│ ├── cdcacm.c
│ ├── usart.c
│ ├── usb_device_descriptor.c
│ ├── usb_device_descriptor.h
│ └── virtual_com.h
├── kinetis.c
├── kinetis.h
├── kinetisk66.ld
├── lib
│ └── kinetis
│ └── NXP_Kinetis_Bootloader_2_0_0
├── libopencm3
├── LICENSE.md
├── main_f1.c
├── main_f3.c
├── main_f4.c
├── main_f7.c
├── main_k66.c
├── Makefile
├── Makefile.f1
├── Makefile.f3
├── Makefile.f4
├── Makefile.f7
├── Makefile.k66
├── monocypher
├── px_mkfw.py
├── px_uploader.py
├── README.md
├── rules.mk
├── stm32
│ ├── cdcacm.c
│ └── usart.c
├── stm32f102.cfg
├── stm32f1.ld
├── stm32f1x.cfg
├── stm32f3.ld
├── stm32f3x.cfg
├── stm32f4.ld
├── stm32f4x.cfg
├── stm32f7.ld
├── Tools
│ ├── astylerc
│ ├── check_code_style_all.sh
│ ├── check_code_style.sh
│ ├── check_submodules.sh
│ ├── docker_run.sh
│ ├── files_to_check_code_style.sh
│ ├── fix_code_style.sh
│ └── pre-commit
└── uart.h
4.gpio 驱动分析
gpio分析以drivers/ioexpander/gpio.c为例子
4.1首先要实现文件读写的结构体,实现基本的 read open close 接口函数
static const struct file_operations g_gpio_drvrops =
{
NULL, /* open */
NULL, /* close */
gpio_read, /* read */
gpio_write, /* write */
gpio_seek, /* seek */
gpio_ioctl, /* ioctl */
NULL /* poll */
#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS
, NULL /* unlink */
#endif
};
4.2.下面的结构体是实现是硬件寄存器直接操作,是给上面 g_gpio_drvrops 使用
struct gpio_operations_s
{
/* Interface methods */
CODE int (*go_read)(FAR struct gpio_dev_s *dev, FAR bool *value);
CODE int (*go_write)(FAR struct gpio_dev_s *dev, bool value);
CODE int (*go_attach)(FAR struct gpio_dev_s *dev,
pin_interrupt_t callback);
CODE int (*go_enable)(FAR struct gpio_dev_s *dev, bool enable);
CODE int (*go_setpintype)(FAR struct gpio_dev_s *dev, enum gpio_pintype_e pintype);
};
static const struct gpio_operations_s gpin_ops =
{
.go_read = gpin_read,
.go_write = NULL,
.go_attach = NULL,
.go_enable = NULL,
};
static const struct gpio_operations_s gpout_ops =
{
.go_read = gpout_read,
.go_write = gpout_write,
.go_attach = NULL,
.go_enable = NULL,
};
static const struct gpio_operations_s gpint_ops =
{
.go_read = gpint_read,
.go_write = NULL,
.go_attach = gpint_attach,
.go_enable = gpint_enable,
};
4.3 这是驱动的入口函数,用来获取
int gpio_pin_register(FAR struct gpio_dev_s *dev, int minor)
4.4.通过register_driver函数申请设备描述符,”/dev/gpiox”,方便应用程序的调用
register_driver(devname, &g_gpio_drvrops, 0666, dev);
→inode_reserve(path, mode, &node);
5.I2c驱动分析
gpio分析以drivers/i2c/i2c_driver.c为例子,主要实现以下接口
1.入口函数
i2c_register(FAR struct i2c_master_s *i2c, int bus)
2.实现 file_operations的基本接口函数
static const struct file_operations i2cdrvr_fops
3.申请设备描述符
register_driver(devname, &i2cdrvr_fops, 0666, priv);
4.跟硬件直接交互的接口函数
I2C_TRANSFER(priv->i2c, transfer->msgv, transfer->msgc);
6.PX4 框架分析
6.1 PX4自动驾驶仪软件可分为三大部分:实时操作系统、中间件和飞行控制栈。
6.2 NuttX实时操作系统,上文对nuttx 已经做了深析,它提供POSIX-style的用户操作环境(如printf(), pthreads,/dev/ttyS1,open(),write(),poll(),ioctl()),进行底层的任务调度。
6.3 PX4中间件,PX4中间件运行于操作系统之上,提供设备驱动和一个微对象请求代理(micro object request broker,uORB)用于驾驶仪上运行的单个任务之间的异步通信。Px4被3DR开源后,整个代码结构被⼤改,原先的系统被摒弃,进而采用Nuttx,但是核心思想没变-为了简化开发而采用牺牲部分效率的消息传递机制,这是Px4 与ArduPilot 最本质的差别。
6.4 PX4飞行控制栈。飞行控制栈可以使用PX4的控制软件栈,也可以使用其他的控制软APM:Plane、APM:Copter,但必须运行于PX4中间件之上。
6.4.1决策导航部分:根据飞行器自身安全状态和接收到的命令,决定工作于什么模式,下一步应该怎么做。
6.4.2位置姿态估计部分:根据传感器得到自身的位置和姿态信息,此部分算法含金量最高,算法也相当多。
6.4.3位置姿态控制部分:根据期望位置和姿态设计控制结构,尽可能快、稳的达到期望位置和姿态。
7.UORB 进程间通讯分析
7.1 uORB(Micro Object Request Broker,微对象请求代理器)是PX4/Pixhawk系统中非常重要且关键的一个模块,它肩负了整个系统的数据传输任务,所有的传感器数据、GPS、PPM信号等都要从芯片获取后通过uORB进行传输到各个模块进行计算处理。实际上uORB是一套跨「进程」 的IPC通讯模块。在Pixhawk中, 所有的功能被独立以进程模块为单位进行实现并工作。而进程间的数据交互就由为重要,必须要能够符合实时、有序的特点。
7.2 Pixhawk使用的是NuttX实时ARM系统,uORB实际上是多个进程打开同一个设备文件,进程间通过此文件节点进行数据交互和共享。进程通过命名的「总线」交换的消息称之为「主题」(topic),在Pixhawk 中,一个主题仅包含一种消息类型,通俗点就是数据类型。每个进程可以「订阅」或者「发布」主题,可以存在多个发布者,或者一个进程可以订阅多个主题,但是一条总线上始终只有一条消息。
7.3 应用层中操作基础飞行的应用之间都是隔离的,这样提供了一种安保模式,以确保基础操作独立的高级别系统状态的稳定性。而沟通它们的就是uORB。
topics : 系统通用接口定义的标准主题,比如电池电量转态、GPS的位置参数等
module.mk : uORB模块makefile文件
objects_common.cpp: 通用接口标准主题定义集合,如添加新主题在这里定义
ORBMap.hpp : 对象请求器节点链表管理(驱动节点)
ORBSet.hpp : 对象请求器节点管理(非驱动节点)
Publication.cpp : 在不同的发布中遍历使用
Publication.hpp : 在不同的发布中遍历使用
Subscription.cpp : 在不同的订阅中遍历使用
Subscription.hpp : 在不同的订阅中遍历使用
uORB.cpp : uORB的实现
uORB.h : uORB头文件
uORBCommon.hpp : uORB公共部分变量定义实现
uORBCommunicator.hpp : 远程订阅的接口实现,实现了对不同的通信通道管理,如添加/移除订阅者,可以基于TCP/IP或fastRPC;传递给通信链路的实现,以提供在信道上接收消息的回调。
uORBDevices.hpp :
uORBDevices_nuttx.cpp : 节点操作,close,open,read,write
uORBDevices_nuttx.hpp :
uORBDevices_posix.cpp :
uORBDevices_posix.hpp :
uORBMain.cpp : uORB入口
uORBManager.hpp : uORB功能函数实现头文件
uORBManager_nuttx.cpp : uORB功能函数实现(Nuttx)
uORBManager_posix.cpp : uORB功能函数实现(Posix)
uORBTest_UnitTest.cpp : uORB测试
uORBTest_UnitTest.hpp : uORB测试头文件,包括主题定义和声明等
8.PX4应用层驱动分析并实现例程
8.1下面例子是一个发布订阅的例子,可供参考和使用
sensor_combined
主题是官方提供的通用接口标准主题。
vehicle_attitude
主题是官方提供的通用接口标准主题。
8.2 程序流程图如下:
/**
*
@file px4_simple_app.c
*
Minimal application example for PX4 autopilot
*/
#include
<nuttx/config.h>
#include
<unistd.h>
#include
<stdio.h>
#include
<poll.h>
#include
<uORB/uORB.h>
#include
<uORB/topics/sensor_combined.h>
#include
<uORB/topics/vehicle_attitude.h>
__EXPORT
int px4_simple_app_main(int argc, char *argv[]);
int
px4_simple_app_main(int argc, char *argv[])
{
printf("Hello
Sky!\n");
/*
订阅
sensor_combined
主题
*/
int
sensor_sub_fd = orb_subscribe(ORB_ID(sensor_combined));
orb_set_interval(sensor_sub_fd,
1000);
/*
公告
attitude
主题
*/
struct
vehicle_attitude_s att;
memset(&att,
0, sizeof(att));
int
att_pub_fd = orb_advertise(ORB_ID(vehicle_attitude), &att);
/*
一个应用可以等待多个主题,在这里只等待一个主题
*/
struct
pollfd fds[] = {
{
.fd = sensor_sub_fd, .events = POLLIN },
/*
there could be more file descriptors here, in the form like:
*
{ .fd = other_sub_fd, .events = POLLIN },
*/
};
int
error_counter = 0;
while
(true) {
/*
wait for sensor update of 1 file descriptor for 1000 ms (1 second) */
int
poll_ret = poll(fds, 1, 1000);
/*
handle the poll result */
if
(poll_ret == 0) {
/*
this means none of our providers is giving us data */
printf("[px4_simple_app]
Got no data within a second\n");
}
else if (poll_ret < 0) {
/*
this is seriously bad - should be an emergency */
if
(error_counter < 10 || error_counter % 50 == 0) {
/*
use a counter to prevent flooding (and slowing us down) */
printf("[px4_simple_app]
ERROR return value from poll(): %d\n"
,
poll_ret);
}
error_counter++;
}
else {
if
(fds[0].revents & POLLIN) {
/*
obtained data for the first file descriptor */
struct
sensor_combined_s raw;
/*
copy sensors raw data into local buffer */
orb_copy(ORB_ID(sensor_combined),
sensor_sub_fd, &raw);
printf("[px4_simple_app]
Accelerometer:\t%8.4f\t%8.4f\t%8.4f\n",
(double)raw.accelerometer_m_s2[0],
(double)raw.accelerometer_m_s2[1],
(double)raw.accelerometer_m_s2[2]);
/*
赋值
att
并且发布这些数据给其他的应用
*/
att.roll
= raw.accelerometer_m_s2[0];
att.pitch
= raw.accelerometer_m_s2[1];
att.yaw
= raw.accelerometer_m_s2[2];
orb_publish(ORB_ID(vehicle_attitude),
att_pub_fd, &att);
}
/*
there could be more file descriptors here, in the form like:
*
if (fds[1..n].revents & POLLIN) {}
*/
}
}
return
0;
}
8.3
sensor_combined
主题是官方提供的通用接口标准主题。
/**
* @file px4_simple_app.c
* Minimal application example for PX4 autopilot
*/
#include <nuttx/config.h>
#include <unistd.h>
#include <stdio.h>
#include <poll.h>
#include <uORB/uORB.h>
#include <uORB/topics/sensor_combined.h>
__EXPORT int px4_simple_app_main(int argc, char *argv[]);
int px4_simple_app_main(int argc, char *argv[])
{
printf("Hello Sky!\n");
/*订阅sensor_combined 主题*/
int sensor_sub_fd = orb_subscribe(ORB_ID(sensor_combined));
/*一个应用可以等待多个主题,在这里只等待一个主题*/
struct pollfd fds[] = {
{ .fd = sensor_sub_fd, .events = POLLIN },
/* 这里可以添加更多的文件描述符;
* { .fd = other_sub_fd, .events = POLLIN },
*/
};
int error_counter = 0;
while (true) {
/*poll函数调用阻塞的时间为1s*/
int poll_ret = poll(fds, 1, 1000);
/*处理poll返回的结果 */
if (poll_ret == 0) {
/* 这表示时间溢出了,在1s内没有获取到发布者的数据 */
printf("[px4_simple_app] Got no data within a second\n");
} else if (poll_ret < 0) {
/* 出现问题 */
if (error_counter < 10 || error_counter % 50 == 0) {
/* use a counter to prevent flooding (and slowing us down) */
printf("[px4_simple_app] ERROR return value from poll(): %d\n"
, poll_ret);
}
error_counter++;
} else {
if (fds[0].revents & POLLIN) {
/*从文件描述符中获取订阅的数据*/
struct sensor_combined_s raw;
/* copy sensors raw data into local buffer */
orb_copy(ORB_ID(sensor_combined), sensor_sub_fd, &raw);
printf("[px4_simple_app] Accelerometer:\t%8.4f\t%8.4f\t%8.4f\n",
(double)raw.accelerometer_m_s2[0],
(double)raw.accelerometer_m_s2[1],
(double)raw.accelerometer_m_s2[2]);
}
/* 如果有更多的文件描述符,可以这样:
* if (fds[1..n].revents & POLLIN) {}
*/
}
}
return 0;
}
9.串口驱动GPS 驱动分析
9.1 GPS 通过串口获取数据,可以选择不同数据协议,我这边选用NEMA协议,不断轮训获取新数据,并且通过UORB将读到的数据放到队列中,被其他进程获取到。
10.mavlink 飞行控制协议分析
Mavlink是目前最常见的无人机飞控协议之一。PX4对Mavlink协议提供了良好的原生支持。该协议既可以用于地面站(GCS)对无人机(UAV)的控制,也可用于UAV对GCS的信息反馈。其飞控场景一般是这样的:
a)
手工飞控:GCS
-> (MavLink) -> UAV
b) 信息采集:GCS
<- (Mavlink) <- UAV
c) 自治飞控:User
App -> (MavLink) ->
UAV
也就是说,如果你想实现地面站控制飞行,那么由你的地面站使用Mavlink协议,通过射频信道(或
wifi
etc.)给无人机发送控制指令就可以了。如果你想实现无人机自主飞行,那么就由你自己写的应用(运行在无人机系统上)使用Mavlink协议给无人机发送本地的控制指令就可以了。
然而,为实现飞控架构的灵活性,避免对底层实现细节的依赖,在PX4中,并不鼓励开发者在自定义飞控程序中直接使用Mavlink,而是鼓励开发者使用一种名为uORB((Micro
Object Request Broker,微对象请求代理)的消息机制。其实uORB在概念上等同于posix里面的命名管道(named
pipe),它本质上是一种进程间通信机制。由于PX4实际使用的是NuttX实时ARM系统,因此uORB实际上相当于是多个进程(驱动级模块)打开同一个设备文件,多个进程(驱动级模块)通过此文件节点进行数据交互和共享。
在uORB机制中,交换的消息被称之为topic,一个topic仅包含一种message类型(即数据结构)。每个进程(或驱动模块)均可“订阅”或“发布”多个topic,一个topic可以存在多个发布者,而且一个订阅者可也订阅多个topic。而正因为有了uORB机制的存在,上述飞控场景变成了:
a)
手工飞控:GCS
-> (MavLink) -> (uORB topic) -> UAV
b) 信息采集:GCS
<- (Mavlink) <- (uORB topic) <- UAV
c) 自治飞控:User
App -> (uORB topic) -> (MavLink) ->
UAV
有了以上背景基础,便可以自写飞控逻辑了,仅需在PX4源码中,添加一个自定义module,然后使用uORB订阅相关信息(如传感器消息等),并发布相关控制信息(如飞行模式控制消息等)即可。具体的uORB
API、uORB消息定义可参考PX4文档与源码,所有控制命令都在firmware代码的msg里面,不再敷述。
最后值得一提的是,在PX4系统中,还提供了一个名为mavlink的专用module,源码在firmware的src/modules/mavlink中,这货与linux的控制台命令工具集相当相似,其既可以作为ntt控制台下的命令使用,又可作为系统模块加载后台运行。其所实现的功能包括:1)uORB消息解析,将uORB消息实际翻译为具体的Mavlink底层指令,或反之。2)通过serial/射频通信接口获取或发送Mavlink消息,既考虑到了用户自写程序的开发模式,也适用于类似linux的脚本工具链开发模式,使用起来很灵活,有兴趣的可以看看。
→GPSDriverNMEA::receive//此时传递超时时间去解析下面的数据,超时时间200ms 5HZ
->read(buf,sizeof(buf),timeout);//此处传参给回调
->_callback(GPSCallbackType::readDeviceData,buf,buf_length, _callback_user); //回调接收
→pollOrRead //读取数据
→parseChar(buf[i]); //检查协议逗号
→handleMessage(l); //开始解析
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通