引言
LPC55S69 微控制器內(nèi)部集成了兩個 ARM Cortex-M33 內(nèi)核, 都可以跑在 150MHz 的主頻上 . 通常情況下, 使用其中一個內(nèi)核(core0)就可以完成足夠多的工作 . 但是, 讓一個 150MHz 的 Cortex-M33 內(nèi)核閑在那里實在浪費, 并且在一些對性能有要求的情況下, 使用雙核同時工作確實可以簡化應(yīng)用的開發(fā)過程, 并提升系統(tǒng)整體的工作效率 . 筆者最近就遇到了這么一個案例 .
筆者在 LPCXpresso55s69 開發(fā)板上面做語音關(guān)鍵字識別的項目時, 想在 LCD 顯示屏模塊上顯示點交互信息, 改善用戶體驗 . 筆者使用的是一塊 SPI 總線 320x240 像素的 LCD 屏模塊, 使用 RGB565 的像素格式, 如果使用 DMA+SPI 的方式刷屏自然可以為主 CPU 減負, 但需要提前緩存整張圖片到內(nèi)存中, 而存一幅圖需要連續(xù)的 150KB 內(nèi)存, 占用內(nèi)存空間比較大 . 雖然 LPC55S69 有足夠的內(nèi)存(320KB), 但是使用人工神經(jīng)網(wǎng)絡(luò)模型占用的內(nèi)存規(guī)模也比較大, 在不確定內(nèi)存能否夠用的情況下, 筆者覺得僅僅為刷屏分配這么多內(nèi)存無疑是奢侈的, 并且屏幕交互信息很簡單, 基本上就是一個黑色的背景色加幾行字而已, 現(xiàn)算現(xiàn)刷的方式可以大幅降低內(nèi)存和代碼的占用量 . 但使用主 CPU 輪詢 SPI 會嚴重影響人工神經(jīng)網(wǎng)絡(luò)的計算實時性 . 此時, 使用副 CPU 在主 CPU 計算的時候執(zhí)行刷屏的操作, 哇, 簡直不要太香 .
在本文中, 筆者將介紹筆者從零開始搭建雙核工程的過程 .
精簡 MCUX SDK 工程, 制作工程模板
首先, 筆者從 MCUX SDK 代碼包里提取出了一個 hello_world 工程, 經(jīng)過一番精簡和調(diào)整文件組織結(jié)構(gòu), 改了工程名, 最后變成了這個樣子:
?
根目錄下只有"application", "CMSIS", "device"和"drivers", 大部分源文件直接放在一級目錄下, 最深的工程組織文件路徑也不過只有三層 . 一個詞, "清爽".
?
這個工程作為模板, 將成為后續(xù)所有新建工程的起點 .
為雙核工程籌備足夠的源文件
復(fù)制"applicationhello_world"目錄, 改"hello_world"為"dualcore_basic", 相當于根據(jù)模板創(chuàng)建了一個新工程 . 然后在"applicationdualcore_basiciar"目錄下復(fù)制一份"my_project.eww"和"my_project.ewp", 將兩份工程組織文件改名為"core0_project"和"core1_project", 這兩個工程組織文件將分別用于編譯生成兩個處理器內(nèi)核上運行的程序 .
從模板創(chuàng)建的工程在默認情況下是支持單核的, 其中并沒有包含支持第二個核心的一些必要的源文件 . 如此, 筆者又在 MCUX SDK 軟件包中提取了雙核版的"hello_world" 工程 .
?
從中復(fù)制了與 core1 相關(guān)的相關(guān)文件:
LPC55S69_cm33_core1_ram.icf
startup/startup_LPC55S69_cm33_core1.s
startup/LPC55S69_cm33_core1.h
startup/LPC55S69_cm33_core1_features.h
startup/system_LPC55S69_cm33_core1.c
startup/system_LPC55S69_cm33_core1.h
lib/iar_lib_power_cm33_core1.a
在新工程中, 只要放置在對應(yīng)"xxx_core0.xxx"源文件存放的位置就可以了 . 兩個核心除了啟動代碼, 鏈接命令文件和供電庫文件之外, 其余的驅(qū)動源代碼完全復(fù)用 .
這里特別強調(diào)一個思路, 雙核的工程跟單核的工程本質(zhì)上沒有區(qū)別, 只是原來生成下載的可執(zhí)行文件是一次編譯, 雙核工程需要兩次編譯(先編 core1 再編 core0). 或者也可以將 core1 的程序當成 core0 工程的庫文件 . 先編譯 core1 的二進制可執(zhí)行程序, 然后將 core1 工程編譯生成的 core1_project.bin 包含在 core0 工程中, 就像平時在單核工程中添加一個預(yù)編譯庫那樣簡單 . 實際上, 在實際使用雙核應(yīng)用的時候, 也就是將 core1 當成一個運行著的庫函數(shù)一樣使用 .
配置副核 core1 工程
1. 改頭換面從 core0 到 core1
core1 工程中, 將芯片類型, linker 文件名, 芯片名的宏定義, 都從 core0 改到 core1. 另外, 由于 core1 工程文件和 core0 在同一個目錄下, 為了區(qū)分同 core0 生成中間文件, 特別將輸出文件目錄名中加一個"core1_"的前綴 .
?
?
?
?
?
?
2. 調(diào)整 linker 文件指定運行時空間
唯一需要注意的地方就是調(diào)整 linker 文件中的內(nèi)容 .
雙核系統(tǒng)中, 兩個內(nèi)核都是總線主機, 在執(zhí)行程序和存取變量的時候都需要訪問系統(tǒng)總線, 但如果兩個內(nèi)核要同時訪問同一個總線從機設(shè)備, 那么就會出現(xiàn)訪問沖突, 需要通過總線仲裁, 這樣就降低了兩個內(nèi)核訪問存儲設(shè)備的速度, 降低了雙核系統(tǒng)執(zhí)行程序的性能 . 因此需要合理安排兩個內(nèi)核各自使用的存儲區(qū), 盡量不要讓兩個內(nèi)核在訪問內(nèi)存的時候"打架".
?
在上圖中可以看到, 將 SRAMX 內(nèi)存塊分給 core1 存放代碼, 將 RAM3 內(nèi)存塊分給 core1 存放數(shù)據(jù), 其余的內(nèi)存塊分給 core0, 大家各用各的, 相安無事 .
core1 工程有下載時空間和運行時空間兩個概念 . 下載時空間就是把需要將可執(zhí)行程序下載到 flash 中, 否則掉電之后程序就沒了 . 運行時空間是指, 整個系統(tǒng)的啟動后, 需要把 flash 中的程序搬運到 ram 中, 程序中跳轉(zhuǎn)指令和尋址變量都是在 ram 中的運行時空間中 . 對于 core1 工程, 怎么下載到 flash 和在系統(tǒng)啟動過程中搬運到 ram 中, 它都不管, 這將會交給 core0 工程處理, 由于系統(tǒng)啟動過程是單線程的, 就是把 core1 的程序存放在 core0 的管轄空間內(nèi)也無妨 . 此處, core1 只要告訴自己的程序和變量, 在運行時自己會在內(nèi)存中就可以 . core1 的運行時內(nèi)存就是 sramx 和 ram3.
對應(yīng)的 linker 腳本文件 LPC55S69_cm33_core1_ram.icf 中, 對應(yīng)指定代碼和數(shù)據(jù)存放區(qū)域的代碼如下:
?
在基本的應(yīng)用中不用去管下載選項, 因為實際不大可能直接下載 core1 工程程序到芯片中 . 即使可以通過調(diào)試器直接將程序?qū)懭氲叫酒?RAM 中, 但由于 core1 的啟動開關(guān)和時鐘系統(tǒng)的初始化過程都需要 core0 的代碼去完成, 單獨下載 core1 的工程不能正常啟動, 仍是不能直接調(diào)試的 . 但這里可以考慮到一種特殊的情況, 也是一個比較有意思的設(shè)計, 就是預(yù)先在 core0 的工程中讓 core0 啟動后(電路系統(tǒng)的默認啟動操作)僅僅啟用 core1, 之后什么都不做了直接進入休眠或者死等的狀態(tài) . 此時, 是可以用 IAR 的調(diào)試環(huán)境將程序下載到 RAM 中并調(diào)試 core1 的程序的 . 使用這種方式可以用于專門調(diào)試運行在副核上的功能, 待代碼成熟后, 再集成到有完善功能的主核應(yīng)用工程中 .
3. 生成二進制格式的 bin 文件
配置 core1 工程生成"core0_project.bin"文件(默認只生成 core1_project.out 文件), 這個二進制文件將在 core0 工程中將被直接包含 .
?
配置主核 core0 工程
終于回到主場了 . core0 工程就跟普通的單核工程沒有不同 . 額 ... 除了需要為 core1 留一點空間(下載時空間和運行時空間). 那么在工程中的配置都是圍繞這個預(yù)留空間來的 .
1. 在 IAR 工程中創(chuàng)建新段包容 core1 的 bin 程序文件
在編輯 linker 文件之前, 先要為 core1 的一大塊程序指定一個在 core0 程序空間中的標號 . core0 工程不會關(guān)注 core1_project.bin 里各種細節(jié), 對于 core0 來說, core1_project.bin 只是一塊需要燒寫在 flash 中特定位置的數(shù)據(jù) . 甚至內(nèi)存搬運的工作都是在代碼中完成的, 因此在工程配置中沒有更多隱藏的"黑科技".
?
其中, "Raw binary image"框中的幾個字符框的內(nèi)容分別是:
File ? : "$PROJ_DIR$core1_debugcore1_project.bin"
Symbol : "_lpc5500_cm33_core1_image"
Section: "__sec_core1"
Align ?: "4"
這里的意思是, 將 core1_project.bin 文件指定成 linker 過程中的一個段(section), 段名為"__sec_core1", 并在鏈接過程中使用"_lpc5500_cm33_core1_image"符號指代 . 這個段在 linker 文件中將被用于安置內(nèi)存, 在啟動代碼中將提取段地址從而執(zhí)行將 coer1 程序從 flash 復(fù)制到 sramx 的過程 .
2. 調(diào)整 linker 文件包容 core1 的新段
首先, 將 core0 的程序空間和內(nèi)存空間壓縮, 為 core1 程序的下載時空間和運行時空間讓出地方 .
?
從 linker 腳本代碼中可以看到, 從 0x0009_0000 開始的 32KB flash 空間就是預(yù)留給 core1 的下載時空間 . core0 的數(shù)據(jù)空間也僅僅用到了 160KB.
之后, 在后續(xù)的 linker 腳本代碼中將 core1 的下載時空間包容到 core0 工程的程序文件中 .
?
此處專門為 core1 的下載時空間創(chuàng)建了一個域(region)并制定地址, 然后將之前創(chuàng)建的段"__sec_core1"通過塊"sec_core1_block"包含在"CORE1_region"域中 .
3. 在代碼中復(fù)制 core1 的程序到 ram 中并啟動 core1
筆者總是想盡量把關(guān)鍵的部分放在代碼中, 因為 IDE 總是在更新, 配置可能會變, 但代碼永恒 . 而且代碼是程序員的通用語言, 最容易理解 .
使用 core1 程序的關(guān)鍵部分, 就在于內(nèi)存復(fù)制和啟動內(nèi)核, 一個是軟件的活, 一個是硬件的事 .
筆者在"core1_init.c"文件中實現(xiàn)了 core1_init()函數(shù):
?
這里面用了一點 IAR 編譯器專有的"黑魔法", 通過"_section_end()"函數(shù)配合提取了"_sec_core1"段的地址空間 . 然后就是用 memcpy 函數(shù)直接進行無差別的內(nèi)存復(fù)制 . 此處的"CORE1_BOOT_ADDRESS"不是動態(tài)提取的, 如果想保持代碼風格一致的話, 也可以像"core1_image_start"一樣從鏈接文件中引用 . 此處將兩種方式都展現(xiàn)出來, 只是為了說明兩種方式的作用是一致的 .
start_core1_hardware()函數(shù)的實現(xiàn)內(nèi)容是硬件雙核系統(tǒng)的專有設(shè)計, 根據(jù) LPC55S69 手冊中的說明, 啟動 coer1 首先需要在"SYSCON->CPUCFG"寄存器中啟用 core1, 之后在"SYSCON->CPBOOT"寄存器指定可執(zhí)行程序二進制文件的首地址, 芯片就會自動將其指定為 core1 的向量表地址(啟動地址), 最后在"SYSCON->CPUCTRL"寄存器中執(zhí)行一波"神操作", 需要在特定驗證碼的加持下, 提供時鐘并復(fù)位 core1, 才能最終讓 core1 運行起來 .
至此, 為雙核程序運行準備的所有配置操作都已經(jīng)完成, 在 core0 工程中的 main()函數(shù)中調(diào)用 core1_init()即可啟動為 core1 預(yù)先寫好的程序 .
編寫樣例程序, 測試運行
1. 測試 core1 正常啟動并運行
搭建好雙核運行環(huán)境之后, 筆者先寫了一個簡單的測試程序, 驗證 core1 能夠被正常啟動并運行 . 具體就是讓 core1 控制板子上的一盞小燈閃爍 . core1 的 main()函數(shù)代碼如下:
?
在 core0 的 main()函數(shù)中, 只是調(diào)用"core1_init()"啟動 core1 而已, 沒有同 core1 有進一步的干預(yù) . 下載程序后, 可以看到小燈閃爍, 說明 core1 已經(jīng)按照預(yù)期正常工作了 .
2. 實現(xiàn)簡單的雙核通信
實際上, 筆者希望 core1 能夠幫助 core0 在運行時執(zhí)行更多的輔助工作, 靈活地根據(jù) core0 的需求執(zhí)行多種操作 . 然后筆者就基于共享內(nèi)存的機制, 實現(xiàn)了一個極為簡單的純軟件的雙核通信組件"shmem". 具體原理很簡單, 筆者在 LPC55S69 芯片上的 ram 空間中分出來一塊內(nèi)存, 獨立于 core0 和 core1 工程可自主使用的內(nèi)存, 而是需要兩個內(nèi)核通過絕對地址訪問 . 目前實現(xiàn)的就是兩個核的分別擁有的事件標志位和兩個帶鎖的單向 fifo 數(shù)據(jù)隊列, 事件標志位用于同步事件, 兩個 fifo 數(shù)據(jù)隊列就像串口的收發(fā)一樣建立數(shù)據(jù)的雙向通信通道 .
寫好了"shmem"組件之后, 筆者改進了基本的雙核測試程序, 讓 core1 在 core0 的控制下閃爍小燈 . 筆者設(shè)計了控制小燈的命令碼和參數(shù)格式, 然后通過從 core0 到 core1 的 fifo 傳遞控制命令及參數(shù) . 當然, 在此之前, 筆者還用了事件標志同步了兩個內(nèi)核的工作步調(diào), 一定確保在 core1 已經(jīng)完成了對 fifo 的初始化, 保證 fifo 可用之后才能讓 core0 向 fifo 中下命令 .
這樣, core1 的 main()就增加了 shmem 的內(nèi)容:
?
core0 的 main()函數(shù)在初始化階段, 耐心等待 core1 的各項工作準備完成, 然后時不時向 core1 的 fifo 發(fā)送控制命令及參數(shù) .
?
下載, 運行, core1 接收 core0 的指令控制小燈閃爍, 大功告成 .
?
后記
MCUX SDK 的代碼包中已經(jīng)提供了雙核的樣例工程, 為什么筆者此處還是要做從零開始的工程搭建 . 原因有兩個:
SDK 中的雙核工程略顯臃腫, 像 LPC5500 這種芯片,由于 core0 和 core1 只有 CPU 不同, 整個系統(tǒng)中的外設(shè)是完全一樣的, 因此可以使用同一份驅(qū)動代碼 . 本文中將副核作為主核的一個運行庫安排在應(yīng)用工程中, 這個思路跟在單核工程中添加預(yù)編譯庫的思路基本一致, 便于用戶理解。而 SDK 將兩個核的工程完全獨立出來, 這對于熟悉經(jīng)典單核開發(fā)的用戶來說,始終需要按雙芯片的系統(tǒng)考慮,但好處是兩個工程可以分別用不同版本的 SDK 庫,也可以分別用不同的開發(fā)工具,適合大型項目或多人同時開發(fā)等。
SDK 中提供樣例程序, 對雙核通信部分的實現(xiàn)比較高級(無論是"erpc"還是"rpmsg"), ?在比較簡單的環(huán)境時,不必使用這么復(fù)雜的組件, 可以考慮使用筆者設(shè)計的這個 shmem 組件用于后期開發(fā) .
筆者的初衷, 是用盡量簡單的方式理解雙核工程, 然后才能進一步將雙核系統(tǒng)用起來 . , 筆者希望通過本文的講述, 能夠降低讀者使用 LPC55S69 微控制器雙核系統(tǒng)的難度, 讓廣大的單片機開發(fā)者們了解雙核開發(fā), 善用雙核系統(tǒng), 充分使用 LPC55S69 這款性能強大的芯片 .