天天看點

Treading了解ArduPilot線程

了解ArduPilot線程

當你學習了ArduPilot庫的基礎結構,你應該了解ArduPilot是如何處理線程的了。從Arduino中繼承的setup()/loop()結構可能是ArduPilot看起來是單線程的,但是實際上并不是;

ArduPilot中的線程方式依賴與它所運作的闆子;有一些闆子(像APM1和APM2)不支援線程,是以使用一個簡單的timer和callbacks。一些闆子(像PX4和Linux)支援帶有實時優先級的Posix線程模型,并且廣泛的被ArduPilot所使用;

POSIX:可移植作業系統接口(Portable Operating System Interface of UNIX,縮寫為 POSIX ),POSIX标準定義了作業系統應該為應用程式提供的接口标準。

以下是你需要了解的關于線程的一些關鍵概念:

  • The timer callbacks
  • HAL specific threads
  • driver specific threads
  • ardupilot drivers versus platform drivers
  • platform specific threads and tasks
  • the AP_Scheduler system
  • semaphores
  • lockless data structures

The timer callbacks

每個平台在AP_HAL中提供了一個1kHz的定時器;在ArduPilot中的任何代碼都可以注冊一個定時器函數,然後以1kHz的頻率被調用;所有被注冊的定時器函數都可以被頻繁的調用;這是一種十分原始的機制因為它是十分輕便的也是十分有用的;你可以通過調用hal.scheduler->register_timer_process()注冊一個時鐘回調函數:

這個例子來自于MS5611壓力傳感器驅動;AP_HAL_MEMBERPROC()宏将C++成員函數壓縮為一個回調參數(将對象内容捆綁為一個函數指針)

在新版的ArduPilot項目中沒有找到上述程式,應該是更新之後重寫的這個宏定義,如下所示,道理應該是一樣的:

當程式想以小于1kHz的頻率運作時,那麼它應該定義一個屬于它自己的“last_called”變量,并且如果在沒有經過足夠時間的時候,立即傳回;

使用hal.scheduler->millis()和hal.scheduler->micros()函數以毫秒和微秒為機關擷取從程式啟動開始的時間;

你可以修改一個sketch(或者自己建立一個)來添加一個時鐘回調函數;使定時器增加然後在loop()函數中每分鐘列印一個計數器的值;

修改你的函數使計數器每25毫秒增加一次;

HAL特定線程

在支援線程的平台上,平台的AP_HAL将會建立多個線程支援基礎操作;例如,在Pixhawk平台上,将建立以下線程:

  • UART線程:讀寫UART(和USB)
  • 定時器線程:支援像上面描述的1kHz的定時器功能
  • IO線程:支援向microSD、EEPROM、FRAM寫資料操作

去看一下在每個AP_HAL中的Scheduler.cpp檔案,了解被建立的線程和每個線程的優先級;

在libraries/AP_HAL_ChibiOS/Scheduler.h中可以看見在ChibiOS平台上所定義的線程和優先級;

//優先級定義
#define CHIBIOS_SCHEDULER_MAX_TIMER_PROCS 8

#define APM_MONITOR_PRIORITY    183
#define APM_MAIN_PRIORITY       180
#define APM_TIMER_PRIORITY      181
#define APM_RCIN_PRIORITY       177
#define APM_UART_PRIORITY        60
#define APM_STORAGE_PRIORITY     59
#define APM_IO_PRIORITY          58
#define APM_STARTUP_PRIORITY     10
#define APM_SCRIPTING_PRIORITY  LOWPRIO
...
           
//建立的線程
static void _timer_thread(void *arg);
static void _rcin_thread(void *arg);
static void _io_thread(void *arg);
static void _storage_thread(void *arg);
static void _uart_thread(void *arg);
static void _monitor_thread(void *arg);
...
           

可以使用debug工具檢視程序,此部分暫時沒有硬體平台,不做叙述;線程的一種通用的用途是提供一種驅動方式來排程速度更緩慢的任務而不用中斷飛行程式;例如,AP_Terrain庫需要使用IO接口将檔案讀取到microSD卡上(存儲和擷取地形資料);使用線程來處理此類需要可以調用以下函數進行:

通過以上程式,AP_Terrain::io_timer函數可以被規則的調用;這個被闆子的IO線程所調用,這意味着它使用更低的優先級,适用于存儲的IO任務;像這樣更慢的IO任務不使用timer線程調用是重要的,因為它将導緻更重要的處理高速傳感器資料的任務變慢;

驅動特指線程

它是可能的建立驅動特指線程,來支援特指驅動的異步處理;目前你可以根據相關平台建立驅動指定線程,如果你想要驅動僅僅運作在一個類型的平台上,這種方式是友善的;如果你想要運作在多個平台上,你有以下兩種選擇:

  • 你可以使用register_io_process()和register_timer_process()來使用timer和io線程;
  • 你可以添加新的硬體抽象層接口來提供一種通用的方式在多個平台上建立新的線程(請貢獻更新檔)

    AP_HAL_Linux/ToneAlarmDriver.cpp提供了一個驅動指定線程的例子;

ArduPilot驅動相對于平台驅動

你可能注意到一些驅動的副本存在于ArduPilot中;例如:有一個MPU6000驅動在libraries/AP_InertialSensor/AP_InertialSensor_MPU6000.cpp,還有一個MPU6000驅動在PX4Firmware/src/drivers/mpu6000.

存在一個副本的原因是PX4項目在Pixhawk闆子上已經提供了一系列測試好的驅動程式;并且我們和PX4團隊有着一個良好的合作關系在開發和強化驅動的工作上;是以當我們在PX4平台上建構ArduPilot的時候,我們使用PX4驅動通過寫

一個小的shim驅動表示帶有标準ArduPilot庫接口的PX4驅動;在libraries/AP_InertialSensor/AP_InertialSensor_PX4.cpp中,你可以看見一個小shim驅動通路在這個闆子上可獲得的PX4IMU驅動并且自動的使它們成為ArduPilot AP_InertialSensor庫中的一部分;

這裡部落客暫時了解為調用關系,大部分的驅動程式被PX4所開發,ArduPilot程式調用已經實作好的PX4驅動,在新版程式中,已經不存在這種結構,Pixhawk闆子的驅動程式被完成的寫在ChibiOS目錄下;

平台指定線程和任務

在一些平台上,通過啟動程式,将有很多的基礎任務和線程被建立;這是和平台類型十分相關的,在此教程中,将主要專注于基于PX4闆子類型的任務;

在使用debug工具的"ps"指令輸出内容中,我們可以看到一些不是被AP_HAL_PX4排程程式所啟動的線程;如下所示:

idle task//當沒有其他任務運作的時候被調用
init//啟動系統
px4io//處理與PX4協處理器之間的通信
hpwork//處理基于PX4驅動線程(主要是I2C驅動)
lpwork//處理低優先級線程(IO)
fmuservo//處理在FMU上與輔助PWM輸出間的通信
uavcan//處理uavcan CANBUS協定
           

這些線程的啟動被PX4指定的rc.APM腳本所控制;當PX4啟動時,運作這個腳本,這個腳本檢測所使用的PX4闆子的類型并且加載正确的任務和驅動;它是個“nsh”腳本,和"bourne shell"腳本相似;

作為練習,你可以嘗試編輯rc.APM腳本并且添加一些sleep和echo指令;然後加載到固件上并且啟動時連接配接debug控制台,你可以看見echo指令将顯示在控制台上;

學習PX4啟動過程的另一種方式是不插microSD卡啟動;rcS腳本在rc.APM腳本之前啟動,它的功能是檢測是否有microSD卡插入,如果沒有SD卡插入,隻提供給你一個nsh控制台;你可以在USB控制台上手動運作rc.APM中的步驟學習它是如何工作的;

連接配接USB控制台,在Pixhawk啟動以後,嘗試以下練習:

tone_alarm stop
uorb start
mpu6000 start
mpu6000 info
mpu6000 test
mount -t binfs /dev/null /bin
ls /bin
perf
           

嘗試運作一下其他驅動,在/bin中檢視可獲得的驅動;這些指令的源碼在PX4Firmware/src/drivers中。

如果你檢視mpu6000驅動中的内容,你可以看見如下内容:

這個和AP_HAL中的hal.scheduler->register_timer_process()函數内容是相同的,但是是PX4中特有的内容并且是更靈活的(是以在Pixhawk中是不是沒卵用?)

對于在驅動中運算很快的周期性事件,使用hrt_call_every()是一種通用方法;這個運算的特點是不可中斷,至多執行幾十微秒(SPI裝置驅動)

和hmc5883驅動相比,你可以看以下内容:

是另外一種應用于周期性事件的機制,适用于運算更慢以下的裝置,像I2C裝置;以上程式将cycle_trampoline函數添加到hpwork線程的工作隊列中;hpwork中調用的内容可以中斷,執行時間大約為幾百微秒;對于執行時間更長的任務,應該使用lpwork工作隊列,在更低優先級的lpwork線程中運作;

AP_Scheduler系統

ArduPilot線程和任務的下一方面是了解AP_Scheduler系統;AP_lirary庫用于分割機器人主線程的時間,提供了一個簡單的機制控制每個運算所使用的時間(一個運算即是調用一個任務)

這種工作方式可以表示為:在每個人機器人實作中的loop()函數中包含一些程式執行以下内容:

  • 等待新的IMU樣本到達
  • 在每個IMU樣本中調用一組任務

    維護Scheduler的是一張表,每個機器人類型都有一個AP_Scheduler::Task表;閱讀AP_Scheduler/examples/Scheduler_test.cpp例子學習Scheduler如何工作;

    你可以看見有一個小的表中有三個被排程的程式,每個任務後有兩個數值,如下所示:

/*
  scheduler table - all regular tasks are listed here, along with how
  often they should be called (in 20ms units) and the maximum time
  they are expected to take (in microseconds)
 */
const AP_Scheduler::Task SchedTest::scheduler_tasks[] = {
    SCHED_TASK(ins_update,             50,   1000),
    SCHED_TASK(one_hz_print,            1,   1000),
    SCHED_TASK(five_second_call,      0.2,   1800),
};
           

每個函數名後面的第一個數值表示調用頻率,在這裡例子中ins_update()使用50Hz的頻率,這意味着每次調用使用20ms時間;

信号量

繼續閱讀