0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Ardupilot移植的深入细节分享

RTThread物联网操作系统 来源:惊觉嵌入式 作者:惊觉嵌入式 2021-04-30 16:14 次阅读

接Ardupilot移植经验分享(2)

深入细节

是时候深入具体的HAL接口了。笔者并不打算一一讲解所有的接口,而是挑选一些有代表性的来分析,主要的内容是:

分析HAL接口的含义,包括功能,入参及返回值的具体含义。

分析HAL_PX4的实现,看看有没有可借鉴之处。

调度接口

AP_HAL::Scheduler提供了程序调度相关的接口。主要分为两类:

延时函数

注册回调

延时函数有3个,1个毫秒级延时,2个微秒级延时。这里的延时可不是死等,而是睡眠一段时间,在此期间让出CPU的使用权以执行其他的线程。

virtual void delay(uint16_t ms) = 0;

virtual void delay_microseconds(uint16_t us) = 0;

virtual void delay_microseconds_boost(uint16_t us) { delay_microseconds(us); }

看似简单的delay

大家可能会觉得,delay是最简单的,delay_microseconds会比较复杂。因为通常来说,毫秒级延时是RTOS的基础API,比如RT-Thread的rt_thread_mdelay。

rt_err_t rt_thread_mdelay(rt_int32_t ms);

不过Scheduler::delay除了简单的睡眠延时,还多了一项任务。这就要先提到注册回调接口中的一个:

typedef void(*Proc)(void);

virtual void register_delay_callback(AP_HAL::Proc, uint16_t min_time_ms) = 0;

这接口的功能是:注册一个延时回调。当某个线程调用delay进行延时时,若延时的时间大于min_time_ms,则delay函数将会调用这个延时回调。

这延时回调的意义是,当你睡眠的时候,正好可以执行某一个指定的任务,不要浪费时间。你可能会想,RTOS的延时本就会让出CPU,本就会让别的线程得以执行,何必多此一举呢?我们看看是谁注册了这个回调。

其中之一是mavlink模块。

hal.scheduler-》register_delay_callback(mavlink_delay_cb_static, 5);

7b10b000-a7f7-11eb-9728-12bb97331649.png

在这里插入图片描述

注释里面说明了原因,这是为了在长时间的初始化函数(setup)中能进行MAVLink交互。主要的MAVLink任务是在loop()中的AP_Scheduler调度系统中执行,就是红圈部分(不过红圈里面没提到MAVLink,不要介意哈)。setup()是顺序执行一系列的初始化函数,想在它里面去进行MAVLink任务,就靠这register_delay_callback了。

7b43b7ca-a7f7-11eb-9728-12bb97331649.png

我们再看看PX4的实现。延时的功能依赖于一个微秒级延时的接口delay_microseconds_semaphore,不过它每次只延时1000微秒,多余的时间会去执行延时回调。

7b7388a6-a7f7-11eb-9728-12bb97331649.png

微秒级延时delay_microseconds

笔者当初看到这接口时,有些头疼。因为RT-Thread并没有微秒级别的延时函数,再强调一遍,这不是死等,是要睡眠让权的。

直接看PX4的实现:

void PX4Scheduler::delay_microseconds(uint16_t usec)

{

perf_begin(_perf_delay);

delay_microseconds_semaphore(usec);

perf_end(_perf_delay);

}

这是上节提到的delay_microseconds_semaphore的马甲,脱了它:

7b82352c-a7f7-11eb-9728-12bb97331649.png

该函数使用hrt_call_after和信号量来完成微秒级睡眠的功能。hrt是High-resolution timer的缩写,高精度定时器

调用hrt_call_after注册一个定时器回调,定时时间是usec,单位微秒。回调函数是信号量发送函数sem_post,回调参数是信号量wait_semaphore。

使用sem_wait等待信号量wait_semaphore,此时当前线程会堵塞,进入睡眠状态。

到达定时时间后,底层就执行这个回调,即sem_post(&wait_semaphore)。

sem_wait接收到了信号量wait_semaphore,该线程被唤醒。

结合时序图来理解:

7bb9300e-a7f7-11eb-9728-12bb97331649.png

微秒级的延时函数,依赖于微秒级的定时回调,也就是hrt_call_after:

/**

* Call callout(arg) after delay has elapsed.

*

* If callout is NULL, this can be used to implement a timeout by testing the call

* with hrt_called()。

*/

__EXPORT extern void hrt_call_after(struct hrt_call *entry, hrt_abstime delay, hrt_callout callout, void *arg);

hrt_call_after属于pixhawk底层的接口。笔者一度以为是Nuttx提供的功能,并且为RT-Thread没有相应功能而烦恼。关于定时器,RT-Thread有类似的功能,那就是rt_timer,不过这是毫秒级的。

rt_timer_t rt_timer_create(const char *name,

void (*timeout)(void *parameter),

void *parameter,

rt_tick_t time,

rt_uint8_t flag)

可能你会想,一般单片机里面不是都有硬件定时器吗?实现微秒级的定时功能很简单啊。确实简单,也不简单。因为hrt_call_after提供的定时功能是要支持并发的,千言万语不如一图:

7bde807a-a7f7-11eb-9728-12bb97331649.png

至于pixhawk的实现以及笔者的移植,将专门出一篇文章来讲解。

delay_microseconds_boost

这个同样依赖于hrt_call_after,只是在睡眠的时候提高了优先级,以使得自己可在第一时间被唤醒。

注册回调

virtual void register_timer_process(AP_HAL::MemberProc) = 0;

virtual void register_io_process(AP_HAL::MemberProc) = 0;

相对来说,这两接口就简单的多了。它们用于注册在timer线程和io线程中运行的回调函数。

7c070de2-a7f7-11eb-9728-12bb97331649.png

注册函数将回调指针加入到数组中。

7c17077e-a7f7-11eb-9728-12bb97331649.png

在相应线程中,定时的一一执行。

7c46c72a-a7f7-11eb-9728-12bb97331649.png

那么,这线程是怎么创建的呢?PX4Scheduler的初始化函数中,会创建许多线程。

void PX4Scheduler::init()

{

_main_task_pid = getpid();

// setup the timer thread - this will call tasks at 1kHz

pthread_attr_t thread_attr;

struct sched_param param;

pthread_attr_init(&thread_attr);

pthread_attr_setstacksize(&thread_attr, 2048);

param.sched_priority = APM_TIMER_PRIORITY;

(void)pthread_attr_setschedparam(&thread_attr, ¶m);

pthread_attr_setschedpolicy(&thread_attr, SCHED_FIFO);

pthread_create(&_timer_thread_ctx, &thread_attr, &PX4Scheduler::_timer_thread, this);

// the UART thread runs at a medium priority

pthread_attr_init(&thread_attr);

pthread_attr_setstacksize(&thread_attr, 2048);

param.sched_priority = APM_UART_PRIORITY;

(void)pthread_attr_setschedparam(&thread_attr, ¶m);

pthread_attr_setschedpolicy(&thread_attr, SCHED_FIFO);

pthread_create(&_uart_thread_ctx, &thread_attr, &PX4Scheduler::_uart_thread, this);

// the IO thread runs at lower priority

pthread_attr_init(&thread_attr);

pthread_attr_setstacksize(&thread_attr, 2048);

param.sched_priority = APM_IO_PRIORITY;

(void)pthread_attr_setschedparam(&thread_attr, ¶m);

pthread_attr_setschedpolicy(&thread_attr, SCHED_FIFO);

pthread_create(&_io_thread_ctx, &thread_attr, &PX4Scheduler::_io_thread, this);

// the storage thread runs at just above IO priority

pthread_attr_init(&thread_attr);

pthread_attr_setstacksize(&thread_attr, 1024);

param.sched_priority = APM_STORAGE_PRIORITY;

(void)pthread_attr_setschedparam(&thread_attr, ¶m);

pthread_attr_setschedpolicy(&thread_attr, SCHED_FIFO);

pthread_create(&_storage_thread_ctx, &thread_attr, &PX4Scheduler::_storage_thread, this);

}

pthread_attr_init,pthread_create,这些是POSIX的线程接口,由Nuttx提供。POSIX 是 “Portable Operating System Interface”(可移植操作系统接口) 的缩写,POSIX 是 IEEE Computer Society 为了提高不同操作系统的兼容性和应用程序的可移植性而制定的一套标准。

好消息是,RT-Thread从3.0版本开始提供POSIX接口。所以,当我们移植的时候,很多地方可以参考AP_HAL_PX4的代码,甚至是直接复制它的代码。

串口驱动

AP_HAL的串口驱动框架由4个类组成,层层分工明确。

Print的功能,如其名称,负责打印输出。write系列为最底层的字节输出接口,需要由具体的HAL平台实现。print和println系列提供打印功能。所谓的打印,比如打印float,就是以字符格式输出float。笔者只列了几个print接口,实际上远比这个多。

Stream定义了输入接口,available返回接收缓存中的字节数,read用于读取输入。

BetterStream添加了格式化输出的接口,即大家熟悉的printf系列。

UARTDriver引入了与串口相关的接口,begin用于配置波特率、输入输出缓存,set_flow_control和get_flow_control与流控相关。

7c5553ee-a7f7-11eb-9728-12bb97331649.png

在这里插入图片描述

除了print系列函数由AP_HAL中的类实现,其他的功能,比如read, write, begin, flow_control,都要由具体的HAL平台实现。PX4UARTDriver就是具体的实现类。

串口绑定

Ardupilot有6个串口,定义在AP_HAL::HAL之中。

AP_HAL::UARTDriver* uartA;

AP_HAL::UARTDriver* uartB;

AP_HAL::UARTDriver* uartC;

AP_HAL::UARTDriver* uartD;

AP_HAL::UARTDriver* uartE;

AP_HAL::UARTDriver* uartF;

PX4UARTDriver构造函数和set_device_path用于与具体的底层串口相绑定。HAL_PX4_Class.c中定义了绑定关系:

// 3 UART drivers, for GPS plus two mavlink-enabled devices

static PX4UARTDriver uartADriver(UARTA_DEFAULT_DEVICE, “APM_uartA”);

static PX4UARTDriver uartBDriver(UARTB_DEFAULT_DEVICE, “APM_uartB”);

static PX4UARTDriver uartCDriver(UARTC_DEFAULT_DEVICE, “APM_uartC”);

static PX4UARTDriver uartDDriver(UARTD_DEFAULT_DEVICE, “APM_uartD”);

static PX4UARTDriver uartEDriver(UARTE_DEFAULT_DEVICE, “APM_uartE”);

static PX4UARTDriver uartFDriver(UARTF_DEFAULT_DEVICE, “APM_uartF”);

read和write的实现

大家可能认为串口的读写是非常简单的操作。其实不然,Ardupilot作为一个对实时要求很高的飞控程序,在应用层调用串口读写函数时,是不允许堵塞的。这需要一些额外的工作来实现。

PX4UARTDriver使用接收缓存和发送缓存来实现异步读写。

write是将数据写入到发送缓存_writebuf之中。_writebuf是一个队列,实质上是环形数组RingBuffer。

/*

write one byte to the buffer

*/

size_t PX4UARTDriver::write(uint8_t c)

{

if (_uart_owner_pid != getpid()){

return 0;

}

if (!_initialised) {

try_initialise();

return 0;

}

while (_writebuf.space() == 0) {

if (_nonblocking_writes) {

return 0;

}

hal.scheduler-》delay(1);

}

return _writebuf.write(&c, 1);

}

read从接收缓存中提取数据,若没有则返回-1。

/*

read one byte from the read buffer

*/

int16_t PX4UARTDriver::read()

{

if (_uart_owner_pid != getpid()){

return -1;

}

if (!_initialised) {

try_initialise();

return -1;

}

uint8_t byte;

if (!_readbuf.read_byte(&byte)) {

return -1;

}

return byte;

}

将发送缓存的数据写入串口和从串口接收数据以填充接收缓存的工作,在PX4UARTDriver::_timer_tick函数中实现。而所有串口的_timer_tick由一个统一的串口线程来调度。

void *PX4Scheduler::_uart_thread(void *arg)

{

PX4Scheduler *sched = (PX4Scheduler *)arg;

pthread_setname_np(pthread_self(), “apm_uart”);

while (!sched-》_hal_initialized) {

poll(nullptr, 0, 1);

}

while (!_px4_thread_should_exit) {

sched-》delay_microseconds_semaphore(1000);

// process any pending serial bytes

((PX4UARTDriver *)hal.uartA)-》_timer_tick();

((PX4UARTDriver *)hal.uartB)-》_timer_tick();

((PX4UARTDriver *)hal.uartC)-》_timer_tick();

((PX4UARTDriver *)hal.uartD)-》_timer_tick();

((PX4UARTDriver *)hal.uartE)-》_timer_tick();

((PX4UARTDriver *)hal.uartF)-》_timer_tick();

}

return nullptr;

}

看过前面高清大图的,应该对这个有印象:

7c61c5b6-a7f7-11eb-9728-12bb97331649.png

SPI和I2C驱动

我们再看两个驱动,SPI驱动和I2C驱动。这两个驱动有很多共同之处:

都有总线的概念,一条总线挂接许多设备。

都有主从概念,每次传输由主机发起,由从机应答。

正是由于它们非常相似,Ardupilot提取出它们的共同之处,抽象成一个基类AP_HAL::Device。下图是Device的类图,并非包含其所有内容,仅列出了一些重要的元素。

7c6e6f3c-a7f7-11eb-9728-12bb97331649.png

在这里插入图片描述

UML图示说明

上图为UML类图。前面提到类图的语法,这里做一点补充。

变量和函数左边有颜色的符号表示访问权限,绿色圆圈是public,黄色菱形是protected。

斜体函数为纯虚函数,需要由子类实现。

transfer

Ardupilot的I2C和SPI驱动主要是用于与传感器通信,所以Device类提供了两个常用的接口:read_register和write_register,并且实现了它们。当然,这是基于transfer接口实现的,而transfer交由子类来实现,毕竟SPI和I2C的实现是不同的。

/*

* Core transfer function. This does a single bus transaction which

* sends send_len bytes and receives recv_len bytes back from the slave.

*

* Return: true on a successful transfer, false on failure.

*/

virtual bool transfer(const uint8_t *send, uint32_t send_len,

uint8_t *recv, uint32_t recv_len) = 0;

对于I2C来说,transfer实现的是先写后读。而对于SPI来说,transfer内部是同时读写。

SPIDevice和I2CDevice

7c8186d0-a7f7-11eb-9728-12bb97331649.png

它们添加了自身独有的接口。

SPIDevice添加了全双工的传输接口transfer_fullduplex,与transfer接口所不同之处在于发送和接收缓存的长度一致。

I2CDevice中,set_address用于设置地址,set_split_transfers指定在先写后读的中间是否传输停止位。

Periodic Callback

Device的功能远不只是为SPI和I2C定义了统一的transfer接口。最重要的,是实现了应用层访问总线的串行化。SPI和I2C都是由一条总线挂接许多设备,无论是SPI或I2C,都不允许在同一时刻访问多个设备。因此,Device提供了get_semaphore接口,以锁定总线。当然,这并不算是串行化,真正的串行化,是通过register_periodic_callback来实现。

virtual PeriodicHandle register_periodic_callback(uint32_t period_usec, PeriodicCb) = 0;

各传感器驱动通过register_periodic_callback注册定时回调,在回调之中访问对应的传感器。同一总线的所有定时回调是在同一个线程中被执行的,这就是串行化。

7d495e94-a7f7-11eb-9728-12bb97331649.png

PX4在实现时,使用DeviceBus实现这个串行化的功能,其会为每一条总线创建一个线程。

7d9b24d6-a7f7-11eb-9728-12bb97331649.png

其内部实现,无非是创建线程,将回调添加到一个链表之中。在函数中,POSIX接口的调用清晰可见。笔者的意思是,可以直接拿来用啦。

7db1b5d4-a7f7-11eb-9728-12bb97331649.png

Manager

应用层通过Device来访问I2C和SPI设备,那么Device对象是哪来的呢?由I2CDeviceManager和SPIDeviceManager提供,而这两个Manager的实例可通过HAL引用访问。

7df3cc9e-a7f7-11eb-9728-12bb97331649.png

小结

SPI和I2C传输的具体实现,没啥好说的。最值得说的,是Ardupilot抽象出了Device基类,为应用层提供串行化的访问功能。而这串行化,是靠创建线程和回调链表来实现。

是时候放一张高清大图了:

7e8275b6-a7f7-11eb-9728-12bb97331649.png

总结

到目前为止,我们看了调度接口,串口驱动,SPI和I2C驱动。调度接口中的微秒级延时接口非常关键,因为很多地方使用了它,并且它的实现有些困难。至于串口、SPI等驱动接口,只要我们理清了它们的层级关系,明确了各接口的作用,移植时不会有什么大问题。并且,这些驱动接口的实现,很多地方可以参考PX4的实现,甚至是直接复制过来用。

原文标题:Ardupilot移植经验分享(3)

文章出处:【微信公众号:RTThread物联网操作系统】欢迎添加关注!文章转载请注明出处。

责任编辑:haq

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 驱动
    +关注

    关注

    12

    文章

    1816

    浏览量

    85100
  • 串口
    +关注

    关注

    14

    文章

    1540

    浏览量

    76022

原文标题:Ardupilot移植经验分享(3)

文章出处:【微信号:RTThread,微信公众号:RTThread物联网操作系统】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    深入解析Zephyr RTOS的技术细节

    ,Zephyr OS在嵌入式开发中的知名度逐渐增加,新的微控制器和开发板都支持Zephyr。本文将深入讨论Zephyr RTOS的技术细节
    的头像 发表于 10-22 16:47 192次阅读
    <b class='flag-5'>深入</b>解析Zephyr RTOS的技术<b class='flag-5'>细节</b>

    Nordic-RT-Thread5.1.0移植笔记

    Nordic-RT-Thread5.1.0移植笔记
    的头像 发表于 10-16 08:09 246次阅读
    Nordic-RT-Thread5.1.0<b class='flag-5'>移植</b>笔记

    基于机智云移植STM32L496G代码移植

    前言最近我拿到了STM32L496AGMCU,发现其扩展版可以连接ESP-01S。我想尝试将开发板连接到我们的机智云上,并根据机智云提供的文档进行程序移植。STM32CubeMX移植机智云自动生成
    的头像 发表于 09-20 08:05 258次阅读
    基于机智云<b class='flag-5'>移植</b>STM32L496G代码<b class='flag-5'>移植</b>

    CC2500和CC1101移植说明

    主要通过如何移植移植注意、关于芯片配置、如何生成导出配置四大步骤来说明CC2500和CC1101移植
    的头像 发表于 06-15 14:32 539次阅读
    CC2500和CC1101<b class='flag-5'>移植</b>说明

    如何移植FFmpeg

    的全方位解决方案,深入掌握FFmpeg已成为每一位多媒体开发工程师的必修课。今天就跟各位小伙伴分享一篇共创社的学习笔记,探讨一下如何移植FFmpeg。配置交叉编译环
    的头像 发表于 06-07 15:28 1588次阅读
    如何<b class='flag-5'>移植</b>FFmpeg

    深入了解目标检测深度学习算法的技术细节

    本文将讨论目标检测的基本方法(穷尽搜索、R-CNN、FastR-CNN和FasterR-CNN),并尝试理解每个模型的技术细节。为了让经验水平各不相同的读者都能够理解,文章不会使用任何公式来进行讲解
    的头像 发表于 04-30 08:27 293次阅读
    <b class='flag-5'>深入</b>了解目标检测深度学习算法的技术<b class='flag-5'>细节</b>

    鸿蒙开发实战:基于【Markwon】移植和开发

    本项目是基于开源项目**Markwon**进行适用harmonyos的移植和开发的。
    的头像 发表于 03-25 16:27 764次阅读
    鸿蒙开发实战:基于【Markwon】<b class='flag-5'>移植</b>和开发

    鸿蒙ArkUI【开发移植Carbon】

    本项目是基于开源项目[Carbon] 进行harmonyos化的移植和开发的。
    的头像 发表于 03-25 15:41 474次阅读
    鸿蒙ArkUI【开发<b class='flag-5'>移植</b>Carbon】

    【鸿蒙】标准系统移植指南

    标准系统移植指南 本文描述了移植一块开发板的通用步骤,和具体芯片相关的详细移植过程无法在此一一列举。后续社区还会陆续发布开发板移植的实例供开发者参考。 定义开发板 本文以
    的头像 发表于 02-27 14:36 781次阅读
    【鸿蒙】标准系统<b class='flag-5'>移植</b>指南

    纳雷科技成为开源飞控ArduPilot平台官方合作伙伴

    近日,纳雷科技正式成为全球知名开源飞控平台的官方合作伙伴,与ArduPilot平台完美兼容,无缝对接,提供从传感到飞控全流程服务支持,客户即插即用,能够更好的推动国内外无人机应用创新。
    的头像 发表于 01-15 10:52 1000次阅读

    stm32必须要移植系统吗

    电子等。在使用STM32进行开发时,是否需要进行系统移植是一个常见的问题。 系统移植是指将操作系统或者驱动程序从一个硬件平台移植到另一个硬件平台的过程。由于不同的硬件平台可能存在差异,例如处理器架构、外设接口等,因此需要对原有的
    的头像 发表于 01-08 14:17 785次阅读

    【收藏】大牛总结的30个PCB布局的细节与心得

    【收藏】大牛总结的30个PCB布局的细节与心得
    的头像 发表于 12-06 15:17 574次阅读

    TIA博途V13移植PLC的基本步骤

    在V13中打开S7-300/400项目,编译项目à开始移植PLC (移植PLC硬件及程序)
    的头像 发表于 11-18 17:22 4483次阅读
    TIA博途V13<b class='flag-5'>移植</b>PLC的基本步骤

    Android ART在玄铁C910上的移植和性能优化

    在过去的3年里,我们在玄铁C910上分别移植了Android 10和Android 12,在ART上我们也做了一些更加深入的优化。在今年 RISC-V 美国峰会上,我们介绍了在ART上移植和优化过程和最近进展,在本文中也将分享这
    的头像 发表于 11-16 09:43 1380次阅读
    Android ART在玄铁C910上的<b class='flag-5'>移植</b>和性能优化

    光伏逆变系统细节知多少

    电子发烧友网站提供《光伏逆变系统细节知多少.doc》资料免费下载
    发表于 11-15 11:13 3次下载
    光伏逆变系统<b class='flag-5'>细节</b>知多少