本篇文章,也是在總結自己使用 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.c 和 kendryte-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 實現軟串口的芯片內部接收實現的時候,如何才能有效的結合硬件特性,讓代碼在不確定的環境中符合預期的工作。