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