以優化 MaixPy 的啟動速度為例,說說 K210 的雙核使用及原子操作。


本篇文章,也是在總結自己使用 K210 芯片的過程中留下的一些痕跡,如果覺得有幫助,不清楚的地方都可以直接留言告訴我。

文章大綱

這次我編寫了一個大綱說明,方便查閱的朋友得知本篇主題

  • 優化 MaixPy 的 SD 卡掛載和啟動問題。
  • 示范一下雙核的使用。
  • 雙核中需要的原子操作。

MaixPy 進入系統很慢,慢在哪里了?

現在 MaixPy 項目也進入到了穩定階段,它遲早會成為一個優秀的 MCU 項目參考,至少我們可以從中學會很多觀念,運用很多復雜代碼。

但還有很多優化空間,如一直存在的啟動速度過慢的問題,我們理一下 MaixPy 的啟動過程。

  • 調用 maixpy_main 經過標准的 K210 BSP 啟動過程,設置相應的芯片初始化,再配置 PLL \ RTC \ FLASH \ CORE 等硬件資源。

MaixPy/components/micropython/port/src/maixpy_main.c#L646-L703

  • 再來進入到 mp_task 導入 MicroPython 的環境代碼,選擇執行的核心,並設置 Gc HeapSize 區域,它就類似於一個死循環,維持 MicroPython 的執行,在進入 REPL 之前,要完成 內存、外設、系統 的初始化。

MaixPy/components/micropython/port/src/maixpy_main.c#L488-L578

  • 才到執行內置的 _boot.py 和 main.py 等內置代碼。

經過了調試確認 SD 卡的 spi 驅動初始化占用了至少 3s 的循環時間,主要影響的地方在我上次修改 SD 的讀取超時導致的時間過長,但這個問題不是因為靠改變等待時間長度來解決的。

此前 MaixPy 的 SD 卡啟動存在問題,會在運行一段時間后出現不穩定情況,從而在 Python 層面丟失 SD 卡的內容,后來量測數據確認為 K210 與 SPI 之間出現了死鎖,就主機和從機都在等對方給應答,理論上只要保證 SD 卡每次的讀寫失敗都會退回上一層 SPI 初始化就可以從根本上解決這個問題,但這並不在本文中繼續討論。

這是在這之前的代碼,我們可以看到如下邏輯:

  • sdcard_is_present 是為了確認 spi 與 sdcard 能夠通信成功。
  • init_sdcard_fs 是為了進一步初始化 SD 插入 MicroPython 環境中。

這里介紹一下關於 MicroPython 的 VFS 注入 SD 卡操作,實時上可以用這樣的代碼去完成 os 模塊的盤符加載的:

import esp

class FlashBdev:

    SEC_SIZE = 4096
    RESERVED_SECS = 1
    START_SEC = esp.flash_user_start() // SEC_SIZE + RESERVED_SECS
    NUM_BLK = 0x6b - RESERVED_SECS

    def __init__(self, blocks=NUM_BLK):
        self.blocks = blocks

    def readblocks(self, n, buf):
        #print("readblocks(%s, %x(%d))" % (n, id(buf), len(buf)))
        esp.flash_read((n + self.START_SEC) * self.SEC_SIZE, buf)

    def writeblocks(self, n, buf):
        #print("writeblocks(%s, %x(%d))" % (n, id(buf), len(buf)))
        #assert len(buf) <= self.SEC_SIZE, len(buf)
        esp.flash_erase(n + self.START_SEC)
        esp.flash_write((n + self.START_SEC) * self.SEC_SIZE, buf)

    def ioctl(self, op, arg):
        #print("ioctl(%d, %r)" % (op, arg))
        if op == 4:  # BP_IOCTL_SEC_COUNT
            return self.blocks
        if op == 5:  # BP_IOCTL_SEC_SIZE
            return self.SEC_SIZE

bdev = FlashBdev((size - 20480) // FlashBdev.SEC_SIZE - FlashBdev.START_SEC)

我們只需要能夠構建這個 vfs 對象並提供 readblocks 和 writeblocks 給到 os 去執行該對象所映射的接口就可以添加到 MicroPython 中的 vfs 中,所以理解了這一層,我們就知道 Python 是如何連接到具體的 SD 卡讀寫操作的,關於這個的代碼,我也留個標記 MaixPy/components/micropython/port/src/standard_lib/machine/machine_sdcard.c#L84-L102

那么我們回到主題上,排除了上層代碼的問題,將目光定位到 SD 卡驅動上,我們不難發現 spi 的驅動方式只存在於單線程流程,也就是 K210 先使用 SPI 對目標的通信和等待后再做后續操作。

從整體上來看問題,單獨拿出來,這個再正常不過了,從大局來看,如果沒有 sd 的卡的芯片豈不是也要等待?

因為從該邏輯只考慮軟件,而不考慮硬件。如果從硬件上考慮問題,則需要硬件上做一個引腳識別或協議判斷,從而不超時等待來確認 SD 卡是否存在,但目前來看,只能軟件默默的等待應答。

而不使用 RTOS 的情況下,也就沒有辦法讓出當前的時間片給其他線程的話,我們還能怎么做?

可以多靠硬件的資源來解決問題,一方面可以透過定時器中斷,另一方面則可以使用雙核,而不需要總是想要通過軟件的邏輯來解決問題,有時候結合硬件可以輕松解決問題。

所以我最后選擇了使用多核,主要的修改記錄如下。

MaixPy/commit/450f20b9956d88107109499a7eabfaf24021f4ed

最終的優化結果為復位芯片后 250ms 進入 repl 接口,這就保證了 MaixPy IDE 連接可以在 2s 內完成,在這之前需要等待 10 秒才能完成連接,而使用雙核就是為了不想在底層代碼引入 RTOS 的代碼依賴。

雙核要如何使用?

K210 的雙核使用相關的 API 示范在 bsp 的 kendryte-standalone-sdk/lib/bsp/entry_user.ckendryte-standalone-sdk/lib/bsp/include/entry.h

我們看一下的雙核接口調用的方法,且不說初始化,我們可以看如下代碼:


typedef int (*dual_func_t)(int);
corelock_t lock;
volatile dual_func_t dual_func = 0;
void *arg_list[16];

void core1_task(void *arg)
{
    while (1)
    {
        if (dual_func)
        { //corelock_lock(&lock);
            (*dual_func)(1);
            dual_func = 0;
            //corelock_unlock(&lock);
        }

        //usleep(1);
    }
}
int core1_function(void *ctx)
{
    // vTaskStartScheduler();
    core1_task(NULL);
    return 0;
}

使用的方法很簡單,只需要這樣一行。

dual_func = sd_preload; // int sd_preload(int core)

當然,這樣看起來很草率,如果封裝一下會更好看。

不過這樣修改運行后 MaixPy 就變成白屏了,這是為什么呢?

使用雙核出現了問題?

通常這是因為雙核的使用上在使用同一個資源的時候,出現了沖突,如 MaixPy 啟動后就會使用 lcd.display 進行 lcd_draw_picture 繪圖操作,那么它做了什么呢?

這是由於 lcd 在 K210 上的繪圖前需要對緩沖區進行翻轉的,因此可以在這做一層簡單的兩路重復操作的優化。

    g_pixs_draw_pic_half_size = g_pixs_draw_pic_size/2;
    g_pixs_draw_pic_half_size = (g_pixs_draw_pic_half_size%2) ? (g_pixs_draw_pic_half_size+1) : g_pixs_draw_pic_half_size;
    g_pixs_draw_pic = p+g_pixs_draw_pic_half_size;

    dual_func = swap_pixs_half; // 注冊函數

    for(i=0; i< g_pixs_draw_pic_half_size; i+=2)
    {
        #if LCD_SWAP_COLOR_BYTES
            g_lcd_display_buff[i] = SWAP_16(*(p+1));
            g_lcd_display_buff[i+1] = SWAP_16(*(p));
        #else
            g_lcd_display_buff[i] = *(p+1);
            g_lcd_display_buff[i+1] = *p;
        #endif
        p+=2;
    }

    while(dual_func){} // 等待注冊的函數執行完成

這也就是為什么啟動后會白屏,因為它上電后雙核掛載 SD 卡的期間進行雙核加速的繪圖就鎖死了,那么怎么辦呢?有兩個方法分別是 信號量 和 臨界區 的方式去讓資源互斥,雖然最終我選擇了信號量的方式,但也會提及臨界區的原子操作進行說明。

加入一個 maixpy_sdcard_loading 的變量,可裝飾為 volatile 變量(但我沒有這樣使用它),設置 volatile 保證了永遠都是讀取變量的實時值,也就避開變量緩存的情況,此時繪圖函數只需要保證在 SD 卡掛載釋放雙核操作才可以使用雙核加速,問題得到解決。

        if (maixpy_sdcard_loading) {
            for(i=0; i< g_pixs_draw_pic_size; i+=2)
            {
                #if LCD_SWAP_COLOR_BYTES
                    g_lcd_display_buff[i] = SWAP_16(*(p+1));
                    g_lcd_display_buff[i+1] = SWAP_16(*(p));
                #else
                    g_lcd_display_buff[i] = *(p+1);
                    g_lcd_display_buff[i+1] = *p;
                #endif
                p+=2;
            }
        } else {

但這樣做是不優雅,且有些亂來的操作,如果不是 LCD 調用雙核呢?其他的模塊難道也要等它?

所以想要真正解決這個問題,最好就是封裝在資源的訪問操作之間,進行臨界區的加鎖形成資源互斥的形式,也就是所謂的線程安全函數,最好保證函數均可分時復用退出,否則和單核無異,不然就是新一代 MTK 8核圍觀傳說 XD 。

原子操作要如何使用?

雙核中需要的原子操作,有類似於臨界區的 lock 和 unlock 的實現,具體的實現可以繼續往深處查看,但本質不變,我們可以從 BSP 中獲取這個定義。

我們在這個頭文件 kendryte-standalone-sdk/lib/bsp/include/atomic.h 中獲取它的使用方法如下。


#include "atomic.h"

spinlock_t lock = SPINLOCK_INIT;

int set_call_back() {
      spinlock_lock(&lock);

      // your operator

      spinlock_unlock(&lock);
}

實際上這是很基礎的東西,在 BSP 這里提供了自旋鎖 spinlock 的實現,並加入了 CAS 的 try_lock 的方式,形成如下原子操作的雛形。

  • 試圖獲取鎖,如果失敗則退出,確保了雙核在並行執行時調取互斥資源的時候不會沖突。
      spinlock_lock(&lock);
      // your operator
      spinlock_unlock(&lock);
  • 試圖設置變量到目標值,如果變量持續不滿足則失敗,類似 CAS 操作。
    int var = 0;
    atomic_add(&(var), 1);

實際上使用起來並不困難,取決於自己的場景需求,如果想要在 MicroPython 層面上開放該函數接口,恐怕還需要考慮,尤其是不熟悉的開發者調用,系統隨時可能崩潰。

后記

在沒有 RTOS 的時候,也並非沒有多線程的方法,如使用一些定時器中斷、外部觸發中斷等接口,或者像雙核這類硬件提供的資源,也是可以達到目的。

不過代碼並不會因為使用多線程而提高性能,想要用好多線程,應該要在思維上一定要保持狀態機多路並行的方式,用異步並行的方式來思考問題,如在硬件中存在多和物理世界中不斷循環的執行單元,而你要做的就是等待這個單元的狀態發生改變再進行下一段操作,所以代碼里的設計思維可以如下:


def loop():

      if state si 0:
            pass
      if state is 1:
            pass

保持住狀態機的思維方式,讓代碼都可以分時執行,那么在接入 RTOS 的時候代碼架構上也不會發生任何改變,只需要注意分配到各個函數在整個芯片中執行周期的比例即可。

這次的內容很基礎,所以就到這里吧。

junhuanchen 2020年10月5日

這次為了不引入 RTOS 環境,借助雙核操作跳過了 SD 卡的阻塞運行,使得 MaixPy 進入 MPY 的 REPL 快了從而執行了對 SD 卡的訪問,導致了 SD 卡的讀寫操作沖突,由於硬件掛載設備的通信需要時間,所以一旦出現 SPI SDCard 不穩定,那么上層就得不到 SD 卡的路徑進行訪問。
在發現 SPI SDCard 工作不穩定的情況下,通過打開日志的 printf 后可以克服,這就說明是時序問題,做嵌入式軟件需要對硬件的特性十分敏感,硬件電路不一定會輸出我們想要的結果,所以我們必須能夠量測到這樣的現象,但實在量測不到怎么辦?我們可以通過邏輯上的思考來想象代碼的執行過程,這個內容想在講解 ESP8285 實現軟串口的芯片內部接收實現的時候,如何才能有效的結合硬件特性,讓代碼在不確定的環境中符合預期的工作。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM