nordic——NCS下的DFU(ble加uart)升級(基於NCS)

2023-06-14 18:01:21

一、簡介

  在NCS中有多種的DFU選擇,強烈推薦使用MCUboot,當然如果你需要選擇傳統的nrf_DFU也是可以的,但是要用到官方修改的原始檔。

關於mcuboot,原理性的東西在官網和官方部落格中有講,可以自行檢視,後面只是簡單的提一下:MCUmgr — Zephyr Project Documentation (nordicsemi.com)

  為什麼要使用mcubooot,原因是在NCS使用mcuboot,可以很快的實現DFU的加入,可以同時加入ble-dfu和uart-dfu。就是加入幾個宏定義的事情,就完成了在SDK中可能要花很久才能完成的工程移植,既有ble也有uart的升級。

二、nRF bootloader升級程式製作

2.1、在nrf的庫中加入nRF bootloader庫檔案

這裡是為了便於後續工程直接呼叫:

第一步:在nrf\subsys目錄下的CMakeLists.txt檔案中加入我們需要加入的庫的目錄檔案

開啟CMakeLists.txt檔案在最後加入:

add_subdirectory_ifdef(CONFIG_NRF_DFU nrf_dfu)

add_subdirectory_ifdef(CONFIG_NRF_DFU_RPC_NET nrf_dfu)

第二步:在nrf\subsys目錄下的Kconfig檔案中加入對即將新增到nrf庫的庫檔案nrf_dfu的宏定義組態檔應用:

開啟Kconfig:加入rsource "nrf_dfu/Kconfig",如果不加入,當我們在工程專案的.conf檔案中加如參照檔案的宏時會找不的相關設定宏定義而報錯:

第三步:在nrf\subsys目錄下建立一個資料夾:名字和我們剛剛寫到CMakeLists.txtKconfig檔案中的名字一致,名為:nrf_dfu

 

第四步:把我們需要的.c.h檔案匯入,那匯入的是什麼檔案呢,也就是SDK中的DFU使用到的相關原始檔,但是請注意,由於需要移植到zephyr中,不在是傳統的SDK了,有些底層檔案可能要做部分修改,當然這一部分已經不需要我們來做了,原廠已經幫我們修改好了這一部分程式碼,我們可以直接拿來用。

主要修改的有nrf_dfu_ble.cnrf_dfu_types.c等檔案,當然不是全部修改,但是修改起來很麻煩,你需要知道整個原理,還需要了解nordicnrf bootloader升級流程和相關函數呼叫。本著已經有輪子,我們就不要去造輪子的原則,我直接拿來使用。那麼如何獲取官方開發人員已經修改好的檔案呢?相關檔案工程已經同步到了github上,連線如下:aiminhua/ncs_samples: nRF connect SDK samples (github.com)

 

Ncs_samples目錄下可以下載整個專案,包含了不止DFU的範例,下載後可以進入到2所顯示的目錄,看到3處顯示的目錄,這個是NCS2.2版本已經新增好的nrf bootloader方式升級的檔案,你可以選擇直接把這個nrf_dfu檔案加入,然後替換掉nrf\subsys目錄下的CMakeLists.txtKconfig檔案,就直接使用。

但是我這還是麻煩的一下,不直接使用,只是獲取到官方修改過的.c.h檔案然後再NUS的透傳例程上進行直接新增得到一個BLEOTA例子。

(總結:雖然我們不會造輪子,但是有了輪子,我們要知道這麼把輪子更好的安在車上,讓車跑起來,而不是隻知道把輪子粗暴安上把車開起來就行,那麼可能出問題的時候就一臉蒙了)

 

nrf_dfu檔案中建立兩個資料夾,一個是inc,放置nrf bootloader所有使用到的.h檔案,還有一個就是放置.c檔案的src資料夾。並把從github上下載下來的檔案中的.c.h檔案都放到對應的檔案中,注意把有幾個.c檔案就放置在了nrf_dfu目錄下,如果你想把他們也放到src中,一定要更改CMakeLists.txt中的路徑設定,每一個庫都要有CMakeLists.txtKconfig,便於編譯時編譯鏈找到你的檔案。

所以整理一下目錄結構如下:

第五步:新增我們nrf_dfu庫檔案下的CMakeLists.txtKconfig檔案內容。

1)、對於CMakeLists.txt有 如下定義:

可以看到在1處我們使用zephyr_include_directories加入了相關標頭檔案存放的資料夾(就和keil中加入標頭檔案一樣),在2處使用add_subdirectory_ifdef加入了會使用到的.c檔案存放的資料夾;3處即為要在src資料夾中去找slip檔案(或者在src資料夾中的CMakeLists.txt中那slip.c給加入,兩種方式任選一種)。

 

2)、對Kconfig設定定義了我們可以使用到的一些kconfig,主要關注的是在CMakeLists.txt中我們定義的相關紅定於是否在kconfig中做了申明:

編寫Kconfig的部分規則:

config xxxx /*本次設定的名稱*/

xxxx /*表示改設定的型別(bool、int、hex(十六進位制)、string、tristate(三太型別))*/

type 「xxxxxx」 /*簡單描述*/

default x /*初始值*/

depends on xxxx /*依賴的選項*/

help 「xxxx」 /*幫助資訊,一般是註釋*/

menu 語法以 menu 開始,endmenu 結束。中間包含若干項config設定, 當然也可以包含其他語法。

例如:

menu "test menu"

    config TEST_MENU_A

        tristate "menu test A"

    config TEST_MENU_B

        bool "menu test B"

        default n                          

endmenu

 

第六步:製作.c資料夾裡面的Kconfig檔案:

src資料夾中所有的檔案都加入到Kconfig中,

這裡為什麼少了一個起那麼第6步移動的slip.c檔案的加入,因為起那麼說了,兩種方式任選一種加入即可,這已經在外層檔案的CMakeLists.txt中要應用時,加入了改檔案,所以可以不用寫slip.c了。

到這就算已經加入了nrf bootloader的相關庫了。

2.2、程式新增

2.2.1、設定.conf檔案

開啟vs code,找到peripheral_uart例子然後開啟(如何開啟相關操作可以看nordic的入門教學),然後我們編譯一下,可以看到肯定是能編譯成功的。

NUS例程中已經有一個原始的.conf檔案了,那麼只要往裡面計入宏定義即可,沒有的話,自己新增一個和板子型號一致的名字命名的.conf檔案。

這已經有了,我們開啟後加入如下語句:

在截圖中我們加入了兩個宏定義:

CONFIG_NRF_DFU=y

CONFIG_NRF_DFU_BT=y

那他們是哪裡來的,為什麼是這兩個宏定義?回答是他們是我們剛剛製作nrf_duf庫時自己親手設定的。在第2.1的第六步中的新增的CMakeLists.txt檔案中,有如下兩條語句:

#新增使用的的.c檔案所在的在的目錄

add_subdirectory_ifdef(CONFIG_NRF_DFU src)

 

#如果定義 CONFIG_NRF_DFU_BT ,就加入ble的DFU

zephyr_library_sources_ifdef(CONFIG_NRF_DFU_BT nrf_dfu_ble.c)

可以看到分分別使用是兩個語句加入了標頭檔案和加入了nordicbleDFU.c檔案,他們都是說如果定義了什麼什麼就加入什麼。到這就相信大家可以很好理解了,那麼我們可以改嘛,當然可以,但是記住,如這裡改了名字請在相應的Kconfig中也改對應,否則編譯器還是不能正確找到並編譯連結,以上兩個宏定義在Kconfig中對應的定義如下:

加入後我們直接進行編譯,記住儲存後,點選全編譯:

編譯後沒有編譯正確,報了一個錯:

zephyr\include\zephyr\dfu\flash_img.h:32目錄下的flash_img.h檔案的32行居然沒有定義,那直接在.config中加入該宏定義,定義為4K。發現居然打了波浪線,懷疑肯定有其依賴項沒有加入,我們先編譯一下看一下是否通過,編譯後依然一樣的報錯,且檢視編譯後的autoconf.h檔案也確實沒有(他包含了工程中所有需要用到宏定義),每次加入,要從新全部構建,具體檢視路徑是在我們建立的工程目錄下如下路徑:

 

因此我們要確定其依賴項,報錯說是flash_img.h檔案找不到,且編譯器也告訴我們路徑在哪裡了,那我們直接過去看一下該路徑下有沒有Kconfig檔案:

 

發現居然我沒有,卻其上一級目錄也沒有,那可能是在flash_img.c檔案處了。而不是在flash_img.h檔案處,我直接在整個資料夾中去找flash_img.c(這裡一般使用工具查詢,很快就可以找到)。

找到flash_img.c檔案:

 

在去上一級的目錄果然找到一個對應的Kconfig檔案:

 

開啟該檔案,然後全域性搜尋CONFIIG_IMG_BLOCK_BUF_SIZE,注意這裡要去掉config進行搜尋,即使用IMG_BLOCK_BUF_SIZE進行搜尋,果然我們找到了該定義:

可以看到CONFIIG_IMG_BLOCK_BUF_SIZE有一個依賴項MCUBOOT_IMG_MANAGER,所以我們還要加入MCUBOOT_IMG_MANAGER這一個依賴項,即加入CONFIG_MCUBOOT_IMG_MANAGER.conf中,大家可以看出來了,在定義Kconfig中的定義時時要去掉前面的CONFIG

其中depends on xxx表示需要依賴於xxx的意思,前面有對kconfig語法進行了基本接收,不知道的請翻看前面。

在加入後依然沒有去掉紅色波浪線,且編譯依然有問題,那可能CONFIG_MCUBOOT_IMG_MANAGER依然有依賴或者這個Kcongig檔案有依賴。

查詢後可以看到如圖的定義:

需要定義IMG_MANAGER才能啟用MCUBOOT_IMG_MANAGER,那麼我們把CONFIG_MCUBOOT_IMG_MANAGER加入到.cofig中去,然後再次全編譯。

 

沒有報錯,說明到目前為止我們加入正確,那麼下面可以開始main.c中加入相關的DFU程式碼了。

 

2.2.2、在main中加入DFU功能函數

新增標頭檔案:

#include "nrf_dfu.h"

#include "nrf_dfu_validation.h"

#include <drivers/nrfx_errors.h>

// 加入重啟系統的標頭檔案

#include <zephyr/sys/reboot.h>

加入系統重啟的原因是,如果在DFU中出現錯誤而終止,應該復位系統,等待再次的DFU

程式碼加入:

/**@brief Function for handling DFU events.
 */
static void dfu_observer(nrf_dfu_evt_type_t evt_type)
{
    switch (evt_type)
    {
        case NRF_DFU_EVT_DFU_STARTED:
        case NRF_DFU_EVT_OBJECT_RECEIVED: 
            break;
        case NRF_DFU_EVT_DFU_COMPLETED:
        case NRF_DFU_EVT_DFU_ABORTED:
            LOG_INF("resetting...");
            // 處理一條掛起的log,列印完成後才進行系統復位
            while(log_process());
                sys_reboot(SYS_REBOOT_WARM);
            break;
        case NRF_DFU_EVT_TRANSPORT_DEACTIVATED:
            // Reset the internal state of the DFU settings to the last stored state.
            LOG_INF("NRF_DFU_EVT_TRANSPORT_DEACTIVATED");
            nrf_dfu_settings_reinit();
            break;
        default:
            break;
    }
}
int dfu_init(void)
{
    int ret_val;
    ret_val = nrf_dfu_settings_init(true);
    if (ret_val != NRF_SUCCESS)
    {
        LOG_WRN("dfu settings init err %d", ret_val);
    }
    ret_val = nrf_dfu_init(dfu_observer);
    if (ret_val != NRF_SUCCESS)
    {
        LOG_WRN("dfu init err %d", ret_val);
    }
    return ret_val;
}

可以看到,首先會使用nrf_dfu_settings_init()函數初始化校驗頁。

 

2.2.3、製作新的韌體APP和升級

改名字後編譯一個APP,在我們的build_5340\zephyrbuild_5340是我給我的編譯目錄起的名字,你也需要在你自己的建立目錄在找到)目錄下找到merged.hex名字的hex檔案,複製一下,然後雜工程的首目錄下建議一個檔案件(我建立的名字為updata

 

然後把剛剛的merged.hex放到該檔案中,使用nrfutil進行升級韌體制作。

建立一個.bat的指令碼,或者在該目錄(updata)中直接執行命令視窗,執行如下

nrfutil pkg generate --application merged.hex --application-version 2 --hw-version 52 --sd-req 0x00 new.zip

pause

由此生成了一個壓縮檔案:

把這個檔案給到手機,然後連線上藍芽,點選DFU,選擇ZIP格式,然後選擇檔案,我們就可以看到如下介面了:

 

等待更新完成即可,這裡注意的是使用nrf bootloader最高速度只到18KB/s,由於還需要校驗等步驟,只會更慢,在我的截圖上最高只到了14kB/s,平均只有5.9KB/s。因此一般不建議使用這種升級協定。建議使用MCUboot

三、MCUboot

簡單一點來說,一個bootloader應該包括兩個部分,一個是DFU時資料傳輸協定部分、一個是對image的管理部分。

注意一點,使用MCUboot時,傳輸的升級韌體是app_update.bin,後續生成韌體後,路徑如下: 編譯生成檔案件\zephyr 下。後續就不再說明了。

3.1DFU資料傳輸協定

DFU協定一般都是把升級的image檔案分成一塊一塊的傳給bootloader,在傳輸過程中,DFU會完成校驗每一塊的資料對不對,出現錯誤處理等工作。值得注意的是DFU協定不管你物理通道是什麼,你可以選擇bleuartUSB等。同時在zephyr中有許多的協定,只對SMP做一下簡單介紹。

3.1.1SMP dfu協定

SMP-simple management protoco(簡單管理協定),nordicNCS支援包中,mcumgr模組就是使用了SMP協定。

Mcumgr模組使用命令組(我的立即這就是一組函數)的方式去操作SMP協定進行資料的傳輸。這種方式對於mcumgr來說,只管把他需要SMP做的事高數SMPSMP會去完成組包,解包等操作,mcumgr等著SMP返回的結果就。

mcumgr中有兩個命令組是和DFU有關的:

1)img_mgmtimage管理命令組,包括3個三個命令集4個具體的命令(4個函數):

 

2)os_mgmtOS(系統)管理命令組,包括三個命令集4個具體命令(函數):

 

在後續的例程中我們會使用兩條宏定義去啟動上面的兩個命令組。

3.2image管理部分

接下來是在NCS中推薦使用的DFU方式——MCUboot,其功能強大,相容的晶片多,不僅僅是nordic,是一個開源的第三方bootloader。MCUboot的存放地址是從0x00000000開始。MCUboot把儲存區域分為了兩塊,分別是主記憶體儲區(Primary slot)和第二儲存區(Secondary slot),對於nordic的NCS中,應用程式(app)只能在主記憶體儲區執行,第二儲存區(Secondary slot)可以位於晶片內(flash),也可以位於外部的flash(external flash)。

 

 

MCUboot中是根據其定義的一個變數swap_type,其確定了是等待升級還是跳轉APPswap_type的值由三個引數決定,官網上有這樣一張圖,通過它們不同的組合,導致swap_type6種不同的值,如下圖所示:

 

1)、BOOT_SWAP_TYPE_TEST:

在升級後,必須呼叫boot_write_img_confirmed()來把image_ok這個引數寫為1。再次啟動時MCUboot才會認為新的image正確,否則回滾。

 

2)、BOOT_SWAP_TYPE_ PERM:

在升級後不管image_ok的值,預設新的image是正確的,沒有回滾。

 

3)、BOOT_SWAP_TYPE_ REVERT

一旦檢測到,就直接進行回滾。

 

4)、BOOT_SWAP_TYPE_ NONE

不進行DFUMCU直接跳轉到app執行。

 

5)、BOOT_SWAP_TYPE_ FAIL

MCUboot在進行對primary slot進行校驗時沒有通過,程式將一直死在MCUboot

 

6)、BOOT_SWAP_TYPE_ PANIC

MCUboot啟動時出現致命錯誤時,出現也將死在MCUboot我們要程式能執行到APP,那麼就要保證swap_type的值為BOOT_SWAP_TYPE_TEST和BOOT_SWAP_TYPE_ PERM,那麼我們怎麼設定這兩種型別呢?需要使用函數boot_request_upgrade(bool choose)。當引數choose = flash時為BOOT_SWAP_TYPE_TEST當引數choose = true時為BOOT_SWAP_TYPE_ PERM

三個引數的取值如下:

magic:全為FF和0x96f3b83d(Good

image_ok全為FF(或者Any、Unset)0x01

copy_done:全為FF(或者Any、Unset)或者0x01

(any表示可能是0x11,等非0x01的值)

如下圖就為MCUboot列印的資訊,我們可以通過其判斷現在是那種模式:

  對應前面的第四種模式,紅色框部分為當前MCUboot標誌位的值,也不用我們一個一個去對,然後查表確定當前MCUboot的狀態。只需要看Swap_type,也就是黃色框部分就知道是處於第幾種模式,然後就可以對應當前是屬於6中狀態中的那種狀態了。如果狀態不對,就可以根據前面的狀態解釋來進行修改。

 

3.3、一級不可升級的安全bootloader

為什麼要加一個安全,在MCUboot官網中有這樣一個解釋,為什麼你在出門期間你信任你的家是安全的,因為你家只有一道門,由此有這樣一個邏輯:

  1. 你信任一扇門,因為你信任鎖
  2. 你信任鎖,是因為你信任唯一的鑰匙
  3. 你信任鑰匙,是因為它正在你的口袋裡

由此如果你口袋中的鑰匙不見了,那麼你將失去對門的信任。

在每次復位或者上電後執行安全bootloader,會通過驗證bootloader中的下一個image的簽名和後設資料來建立一個信任根本,就像確定鑰匙是不是唯一且在自己的口袋中,如果任意一次驗證不通過,那麼bootloader就好停止,由此保證bootloader中的下一個image在被篡改後不會啟動。由此杜絕攻擊者通過更改韌體來入侵接管裝置,以保證安全性。

總結:

1)、不可變的:bootloaderflash鎖定的,如果不擦除整個裝置,無法進行修改或者刪除。

2)、安全的:擁有一套驗證機制,保證自己和使用者app的安全,不會被攻擊者篡改。因此要提供自己的祕鑰,讓bootloader進行簽名驗證(下一個image是否被篡改)和後設資料(Metadata)校驗(檢測image是否相容)。

注意本章節新增的,不管是ble的還是uart的,在升級的時候都是後臺式的,升級是你可以正常使用。

3.3.1、加入MCUboot

依然選擇在peripheral_uart這個NUS例子中加入MCUbootDFU功能。那具體要用到那些宏定義設定,如何檢視:

1)、一是有參考例程:zephyr\samples\subsys\mgmt\mcumgr\smp_svr,該路徑的例子中有全部的config宏定義,只要對比加入就行

2)、參考官方網站的說明進行加入:Adding a bootloader chain — nRF Connect SDK 2.3.99 documentation (nordicsemi.com)

3.3.2、工程新增修改(ble-OTA)

這裡值得注意的是,不同版本之間這些CONFIG設定是會變的,所以在自己加入時一定要去看上面提到的參考,而不是無腦的複製貼上。不然很可能你的版本和本文的版本不一樣,導致你初始的工程編譯都通過不了,稍後我會列舉一下NCS2.1NCS2.3的區別。

本次在NUS(透傳)程式的基礎上新增MCUboot,例子路徑:nrf\samples\bluetooth\peripheral_uart。使用VS code建立一個該工程的映像,並且編譯通過後,開始DFU的新增。

第一步:.config中加入全域性宏:

1:該全域性宏定義版本是NCS2.1

# #~~~~~~~~MCUboot加入~~~~~~~~~~~~

#確保生成與MCUboot相容的二進位制檔案

CONFIG_BOOTLOADER_MCUBOOT=y
 

#啟用mcumgr

CONFIG_MCUMGR=y

#~~~~~ 啟用大多數的核心程式,就是前面說的對SMP的命令組的啟用,用於DFU傳輸~~~~~~

##  用於DFU支援已新增到應用程式中,因此需要重置晶片,以將控制權交給MCUboot,MCUboot將驗證新上傳的影象並將其與舊映像交換。最後,BOOTLOADER_MCUBOOT將MCUboot作為子映像包含在內  ##

#啟用用於imager管理的處理程式(傳輸/列舉/校驗確認imager)

CONFIG_MCUMGR_CMD_IMG_MGMT=y

#啟用用於作業系統管理的處理程式(用於校驗出錯時的回滾,以及復位元運算)

CONFIG_MCUMGR_CMD_OS_MGMT=y


# 允許傳輸大容量的封包

CONFIG_BT_L2CAP_TX_MTU=252

CONFIG_BT_BUF_ACL_RX_SIZE=256


# 啟用mcumgr SMP 傳輸,SMP是傳輸協定。

CONFIG_MCUMGR_SMP_BT=y


#同時確保禁用掉加密傳輸和連線時的身份驗證(如果有繫結等功能的一定要關閉)

CONFIG_MCUMGR_SMP_BT_AUTHEN=n


# 把堆疊從原來的2048改為4096,為某些需要大堆疊的命令提供空間

CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=4096

 

#由於要加入DFU到主執行緒,增加一點主執行緒的堆疊空間

CONFIG_MAIN_STACK_SIZE=2348

# #~~~~~~~~MCUboot加入結束~~~~~~~~~~~~

 

2:這對於NCS2.3及之後版本(本文編寫時更新到NCS2.3版本):

除了在NCS2.1中加入的全域性宏,還應該多加入一條全域性宏。

## NCS為 NCS3.1 以及以後需要加入,啟用並註冊一個SPM 程序,就不用在在main中新增程式碼了。

CONFIG_MCUMGR_CMD_STAT_MGMT=y

 

第二步:標頭檔案加入:

3:針對於NCS2.1

標頭檔案加入:

#include <zephyr/mgmt/mcumgr/smp_bt.h>

#include "os_mgmt/os_mgmt.h"

#include "img_mgmt/img_mgmt.h"

4:如果是NCS2.0標頭檔案加入是:

#include <mgmt/mcumgr/smp_bt.h>

#include "os_mgmt/os_mgmt.h"

#include "img_mgmt/img_mgmt.h"

5:如果你的是NCS2.3版本以後,已經不需要在加入標頭檔案了。

 

第三步:DFU程式加入:

6:針對於NCS2.NCS2.1

官方例程中需要加入的是函數有三個,只有加入他們到main()進行初始化,才可以在初始化時把SMP相關的程式碼加入,如果你不加入,會導致下面的第五步升級包選擇後出現錯誤,由於錯誤無法進行資料的傳輸:

smp_bt_register();

os_mgmt_register_group();

img_mgmt_register_group();

7針對於NCS2.3及以後

由於官方對底層驅動又做了升級,單獨使用一個程序對SMP進行初始化,只要使用一條宏定義開啟程序,就不用新增這一步分程式碼了,已經通過——注2 所列出的全域性宏加入了程序,如下圖所示,使用MCUMGR_HANDLER_DEFINE(),新增了一個程序用於初始化,而且想對應於NCS2.1使用了static定義了

img_mgmt_register_group();函數,也無法在main()進行新增了。

第三步:編譯下載

然後就可以編譯了,編譯成功後,直接下載,就可以使用nordic的官方APP——nrf connect搜尋藍芽廣播的名字,連線後,可以看到已經有了DFU的服務。

 

第四步、韌體制作與升級

在工程中修改一下藍芽廣播的名字改為Nordic_NEW,然後重新構建工程:

 

然後你可以在你的工程檔案的編譯子檔案中找到升級的bin檔案。如我在建立工程時,給編譯的子工程起的名字為bulid_5340:

 

那麼在我的工程目錄下的zephyr中會找到一個app_updata.bin檔案,這就是我們新的app

傳到手機上,在nrf connect中連線裝置,然後點選右上腳的DFU圖示,選擇app_updata.bin檔案,然後,如圖進行操作:

 

等待升級完成,,斷開連線,復位一下,就可以看到廣播名字改變了。

 

列印資訊也沒有問題,先是test,進入DFU升級,升級完成後,復位板子,MCUboot模式為none,表示正常啟動,升級成功。

特別注意:

如果你使用的版本和我本文的不同,且按照上述步驟不能升級,很有可能就是官方做了升級,改變了部分流程,請一定一定去參看一下路徑:zephyr\samples\subsys\mgmt\mcumgr\smp_svr下的例子,我以上的更改也是根據這個來更改的。

 

3.3.3、使用自定義祕鑰進行韌體進行簽名

如果你使用過SDK開發,那麼在DFU時第一步就是生成自己的祕鑰並進行替換。同樣在NCS中實際專案中也請使用自己的祕鑰。

在前面的升級中使用了NCS中自帶的祕鑰,這是不安全的,你可以在編譯結果中找到如下一個警告:「MCUBOOT的祕鑰使用的是預設祕鑰,不能用於生產使用。」

 

祕鑰一定要使用自己生成的,而不是使用NCS中自帶的,而且最好保證祕鑰儲存的位置不在你的專案中。

還有一點就是:測試的時候你如果使用預設祕鑰,那麼在每一次刪除專案進行重新構建預設祕鑰都是可能被更改的,由此導致你不能使用新的APP對使用老版本韌體的裝置進行升級,因為你的祕鑰已經在構建過程中改變了。所以最開始的韌體也最好使用新建立的工程從新燒錄。

在開始前,我先把官方檔案參考連線給出(注意,如果你使用的版本和我的(NCS2.3)不一樣,那麼請切換到和你版本一致的問的檔案):

Firmware updates — nRF Connect SDK 2.3.99 documentation (nordicsemi.com)

版本切換如下圖:

第一步:祕鑰生成

值的注意的是雖然生成祕鑰的加密演演算法有許多中,但是NCS中的只支援部分,所以我們並不能隨意的選擇。

NCS中支援三種方式生成祕鑰,分別是:

  • 使用openSSL生成祕鑰
  • l使用imgtool生成祕鑰
  • 使用python生成祕鑰

如下截圖是官方檔案中給出的不同的bootloader支援所支援的祕鑰加密演演算法。生成的時候一定不能使用不支援。否則DFU會失敗。

1)OpenSSL方式

要使用openssl生成祕鑰,那麼我們就需要有openssl這個外掛,你可以在網路上直接搜尋openssl安裝,在你的PC端安裝一個openssl。如果你電腦安裝的有Git、VMware、Strawberry等,那麼由於這些軟體都自帶openssl,因此可以直接使用。

首先可以直接執行一下win+R鍵執行CMD,開啟命令列視窗,執行一下如下命令

openssl help

結果:

居然提示不能執行,但是我電腦確定已經裝了git的,並且在git執行相同的命令是可以得到響應的。由此應該由於沒有把openssl的路徑加到環境變數中,導致電腦找不到openssl。因此開啟git的安裝目錄在usr/bin檔案下成功找到了opensslexe檔案:

 

注意如果你也是這種方式,那麼請確定好你git安裝的路徑。接下來我複製該路徑,開啟環境變數並把該路徑加入帶系統變數中,流程看下列截圖,按照箭頭的指示一路下來:

新增並確認完畢後,再次開啟cmd命令列視窗,記得一定要在新增完成後再次開啟,不然原本是視窗由於沒有重新整理,依然不能執行openssl.exe  

新增完畢再次執行命令:

openssl help

成功:

 

環境沒有問題了,那麼就可以開始製作祕鑰了。

在我的工程檔案中新建立一個名字為key的檔案,然後再該檔案中開啟cmd視窗:

 

在開啟的視窗執行一下指令確定依然可以在該路徑下執行openssl.exe

 

MCUboot支援的幾種加密演演算法都是非對稱加密的演演算法,會生成一對祕鑰對,分別為私鑰和公鑰,公鑰直接寫入到MCUboot中,在NCS中我們只用把生成的私鑰加入到我們的工程中,私鑰或在程式的執行下生成公鑰,並寫入到hex中。同時也對韌體進行加密。

選擇RSA-2048演演算法來製作祕鑰對:

私鑰生成命令:

openssl genrsa -out private.pem 2048

然後就可以在資料夾中看到生成的祕鑰了:

 

2)、使用imgtool

imgtool是一個NCS庫中已經包含了的指令碼工具,具體路徑為:

NCS目錄/bootloader/mcuboot/scripts/imgtool.py

因此要在想生成祕鑰的資料夾中加入該指令碼的絕對路徑,如我在前一節的工程先建立的key資料夾下建立另一個檔案imgtool,然後確定絕對路徑,之後開始製作祕鑰

命令如下:

python ../../../../v2.3.0/bootloader/mcuboot/scripts/imgtool.py keygen -t ecdsa-p256 -k priv.pem

 

由於我電腦是python,沒有python3,所以我直接使用python,需要根據自己的環境進行選擇:

Python3 ../../../../v2.3.0/bootloader/mcuboot/scripts/imgtool.py keygen -t ecdsa-p256 -k priv.pem

3)、使用python

命令如下:

python ../../../../v2.3.0/nrf/scripts/bootloader/keygen.py --private -o priv.pem

或者:

Python3 ../../../../v2.3.0/nrf/scripts/bootloader/keygen.py --private -o priv.pem

主要路徑是nrf/scripts/bootloader/keygen.py,你要根據你需要生成的祕鑰的文字去定位該路徑的絕對路徑。

 

第二步:替換工程編譯時的祕鑰

然後可以在工程目錄下建立一個資料夾(可以任意,只要找得到祕鑰的即可),該資料夾名字必須定義為child_image,並且在其中定義一個mcuboot.conf檔案,為什麼要求這麼嚴格,因為自動建立指令碼已經把響應的子檔案的名字都確定好了,所以為了可以在建立時被應用到,必須按照要求來,加入完畢目錄結構如下(peripheral_uart_23為專案的根目錄):

然後再mcuboot.conf中加入如下的宏定義:

CONFIG_BOOT_SIGNATURE_KEY_FILE="D:/nordic_NCS/work/peripheral_uart_23/key/priv.pem"

其中D:\nordic_NCS\work\peripheral_uart_23\key為我放置私鑰的檔案位置。

建立編譯後可以在編譯結果中找到如下的編譯資訊,證明替換沒有問題。

 

第三步:驗證升級

先燒錄剛剛的韌體,然後改一改藍芽廣播名字,再次建立工程。然後依然是在建立的工程目錄下build_5340\zephyrbuild_5340是我自定義的編譯輸出檔案的名字,和自己定義的進行匹配),找到app_update.bin,傳送到手機上,然後開啟手機連線上裝置。

可以看到裝置現在名字是NORDIC_TEST,點選DFU,開始選擇升級韌體

 

選擇韌體:

 

然後開始升級等待,這個過程手機最好不要退出去,保持在這個介面,不然很可能導致失敗:

 

升級完成,斷開連線再次掃描,會看到裝置的ble名字已經變了:

 

Rtt列印資訊如下:

由此測試完畢。

問題解決:在升級的過程中,有遇到有些手機會由於檔案系統原因,導致傳送失敗,如果你有同樣的現象,可以換一個檔案管理系統進行測試。

3.3.4、UART-DFU工程新增修改

這裡要說一下,我真正需要的僅僅是mcumgr.exe,如果說你已經有了那麼直接在環境變數中新增好mcumgr.exe的路徑,就可以在PC端的任意路徑下cmd命令啟動,並使用mcuboot的串列埠升級。如果你是需要其他平臺上可以執行的mcumgr的可執行檔案,也要自己去生成一個對應平臺的可執行的檔案。打個比方說,如果我想在Linux系統執行mcumgr.exe去進行升級,那麼我怎麼獲取一個在可以在Linux上可執行的mcumgr.exe。是用linux版本的go去製作。

由於我使用的是Windows,所以我下面製作的是Windows上的mcumgr.exe

第一步:PC端上位機環境搭建

需要使用mcumgr的功能,那麼上位機端也應該支援mcumgr的傳輸協定SMP,否則沒有辦法進行傳輸。有由於支援mcumgr的上位機命令列工具是使用 go語言開發的,所以第一步就是應該在Windows端安裝go

1)、下載go的安裝包

https://golang.google.cn/dl/ 開啟go的官網進行下載,根據你的系統選擇對應的安裝包進行下載。

由於我的是win1164位機,所以下載第一個,如果你的系統是macOS或者是linux,那麼請根據你自己最終執行的平臺去選擇適合的go

 

  

下載完成後點選msi檔案進行安裝,可以選擇你的安裝路徑,然後一路next下去就行。

安裝好後,確定系統環境變數中是否已經把go安裝目錄下的bin檔案路徑包含進去了:

 

然後我們win+R輸入cmd後在命令列視窗輸入go後回車:看到如下列印,說明go環境安裝完成:

 

2) 、下載mcumgr

開始使用go安裝mcumgr的上位機工具(相關命令參考為zephyr官網給出的檔案,連線地址如下:

MCUmgr — Zephyr Project Documentation (nordicsemi.com)):

獲取MCUmgr命令(需要在有go.mod的目錄執行,請看問題解決)

go get github.com/apache/mynewt-mcumgr-cli/mcumgr

如果你無法獲取,並報錯,請參看後續問題解決,主要是網路原因。

下載完成後:還是無法執行mcumgr,請使用如下命令進行安裝

go install github.com/apache/mynewt-mcumgr-cli/mcumgr@latest

如果mcumgr安裝成功,那麼在gobin目錄下會有mcumgr.exe檔案

 

安裝完成請確保已經把go的安裝目錄下的bin檔案所在目錄加到了環境變數中。

然後我們任意啟用一個cmd命令視窗,輸入mcumgr,回車,如下圖,證明安裝成功了。

問題解決1-缺少go.mod報錯:

如果你安裝的go改過預設路徑或者版本原因,那麼可能導致在安裝檔案中沒有go.mod檔案,在執行命令時會導致無法安裝。

解決方式:在go安裝路徑下:

建立一個新的資料夾並命名為mod

 

然後我們進入這個資料夾,並輸入一個cmd,開啟命令列視窗:

然後執行指令:

go mod init mod 

 

 

然後就生成了執行時缺少的go.mod檔案了:

 

然後繼續執行mcumgr的安裝命令進行安裝即可。

 

問題解決2-網路原因下載失敗:

解決了問題1,到這一步還可能會安裝失敗:

具體原因:預設go的代理網站是GOPROXY=https://proxy.golang.org,direct,是一個外網地址,國記憶體取不到VPN可能存取到,但是我測試時,開VPN也沒有用,改代理網站沒有問題

1)、第一種方式時修改代理映象

代理網站更改命令:

go env -w GOPROXY=https://goproxy.cn,direct

更改後,再去執行go set命令下載mcumgr

go get github.com/apache/mynewt-mcumgr-cli/mcumgr

2)、第二種方式——直接拉取(前提是安裝的有git環境)

先進入到go的安裝目錄下的src資料夾,然後再在src資料夾下開啟cmd命令視窗,然後使用git

直接在github上拉取工具的原始碼。

拉取命令如下:

Git clone github.com/apache/mynewt-mcumgr-cli/mcumgr

拉取完成,安裝即可。

 

第二步:建立一個工程

正常建立工程,只需要使用如下幾個宏定義就行,就是加入到工程的.conf組態檔中設定就行了。

# #確保生成與MCUboot相容的二進位制檔案
CONFIG_BOOTLOADER_MCUBOOT=y
# #啟用mcumgr
CONFIG_MCUMGR=y

# #啟用用於作業系統管理的處理程式(用於校驗出錯時的回滾,以及復位元運算)
CONFIG_MCUMGR_CMD_OS_MGMT=y
 
# #啟用用於imager管理的處理程式(傳輸/列舉/校驗確認imager)
CONFIG_MCUMGR_CMD_IMG_MGMT=y 

# #啟動uart傳輸的宏定義
CONFIG_MCUMGR_SMP_UART=y
CONFIG_CONSOLE=y

  這裡的正常是什麼意思呢?就是說你APP中沒有使用到UART去做資料通訊,NUS透傳的例子(peripheral_uart),原本已經使用了UART0作為應用中的資料通訊外設,這個時候不能再用UART0來作為mcuboot的升級傳輸口了,至少是現在的NCS_2.3版本是不行的,如果要做,那麼工作量可能也會很大,不建議自己再去改底層。

  由於我使用的測試工程就是peripheral_uart,已經默然使用uart0作為透傳歷程了,我只能進行更多的修改。根據不同的需求有兩種更改方式:分別是使用UART0作為mcuboot的升級串列埠,使用UART1或者UART2等其餘串列埠作為mcuboot的串列埠,二種方式都需要晶片支援多個uart,直接在app中觸發升級。還有就是直接使用一個串列埠,升級必須在mcuboot中。

1)、使用雙bank+UART升級(uart0smp傳輸通道)

這個的好處是可以實現回滾。

a、修改app中的UARTuart1

在工程中修改或者新增一個可參照的overlay檔案,新增時可以選擇命名為app.overlay或者_Board_.overlay(_Board_表示板子的名字)

開啟我工程下的app.overlay,使能uart1,設定引腳,並進行參照,修改完成後如下:

/ {
    chosen {
        nordic,nus-uart = &uart1;
    };
    pinctrl: pin-controller {
       nus_uart: nus_uart {
           phandle = < 0xb >;
            group1{
                psels = < NRF_PSEL(UART_TX, 1,8 ) >;
            };
            group2{
                psels = < NRF_PSEL(UART_RX, 1,6) >;
                bias-pull-up;
            };
        };
    };
}; 

&uart1 {
    status = "okay";
    pinctrl-0 = < &nus_uart >;
};

如果你直接修改,注意 phandle = < 0xb >;要確定和最終的zephyr.dts的不重複,就是一個檔案不要有兩個都為0xb。

以上新增方式不理解可以看如下官方給出的裝置樹說明部落格:詳解Zephyr裝置樹(DeviceTree)與驅動模型 - jayant97 - 部落格園 (cnblogs.com)

由以上程式碼我們啟動了uart1,並設定的P1.08和P1.06作為uart1 的通訊引腳。

 

b、加入宏定義,改變資料通訊UART的通訊方式

在前面提到的prj.conf檔案中加入如下宏定義:

CONFIG_UART_ASYNC_API=y
CONFIG_UART_1_ASYNC=y
CONFIG_UART_1_INTERRUPT_DRIVEN=n

用於啟動uart1的非同步API,禁止中斷方式,使用非同步的方式通訊,如果你不加入這個三個宏定義,還是採用原本例程預設的,那麼在編譯下載後,通過RTT列印,你會看到如下報錯:

 

 

這是說,在設定uart 的回撥時,返回-88這個錯誤,表示系統不支援。具體原因我沒有仔細確定,所以一定要進行修改。到這一步第一種uart升級就算加入完成了,直接下載就行。

2)、使用雙bank+uart升級(uart1smp傳輸)

這種方式修改的就比較多了,首先是你需要修改MCUboot,讓其把預設的為UART0的傳輸,改變為UART1

 

a、工程目錄下.conf中宏定義的修改

首先是.conf檔案的修改,依然是在啟動了mcuboot的基礎上設定傳輸方式為UART,並且啟動非同步API,同時禁用掉UART0的中斷回撥方式,啟動為非同步回撥:

CONFIG_MCUMGR_SMP_UART=y
CONFIG_CONSOLE=y

CONFIG_UART_ASYNC_API=y
CONFIG_UART_0_ASYNC=y
CONFIG_UART_0_INTERRUPT_DRIVEN=n

bapp(應用程式)的.overlay的修改

然後是APP中的.overlay的修改,(也就是我們的主應用,把其他mcuboot程式,網路核程式叫做子image)把app中觸發升級的串列埠改為UART1,在工程根目錄下的.overlay中加入設定程式碼:

/ {
    chosen {
        zephyr,uart-mcumgr = &uart1;
        nordic,nus-uart = &uart0;
    };
    pinctrl: pin-controller {
        dfu_uart: dfu_uart {
            phandle = < 0xb >;
            group1{
                psels = < NRF_PSEL(UART_TX, 1,8 ) >;
            };
            group2{
                psels = < NRF_PSEL(UART_RX, 1,6) >;
                bias-pull-up;
            };
        };
    };
};

&uart1 {
    status = "okay";
    pinctrl-0 = < &dfu_uart >;
};

啟動了UART1,並設定uart1mcubootuart傳輸通道。

c、修改mcuboot——子imgeroverlay

開啟工程目錄,建立一個名字為child_image的資料夾:

然後建立一個名字為mcuboot的資料夾:

在進入並建立一個名字為boards的資料夾:

在再此資料夾中建立一個-board-.overlay的檔案,其中-board-表示的是專案需要使用的板卡檔案,可以簡單理解為要使用那個系列的晶片就使用對應的名字:

我使用的是5340,且app執行在應用核,所以使用nrf5340dk_nrf5340_cpuapp這個名字就行。如果你不知道你使用的晶片應該怎麼命名,可以在VS code中進行檢視:

完整的overlay檔案存放目錄如下:

專案檔案\child_image\mcuboot\boards

然後在nrf5340dk_nrf5340_cpuapp.overlay目錄中加入如下的程式碼:

/ {

    chosen {

        zephyr,uart-mcumgr = &uart1;

    };

    pinctrl: pin-controller {

        dfu_uart: dfu_uart {

            phandle = < 0xb >;

            group1{

                psels = < NRF_PSEL(UART_TX, 1,8 ) >;

            };
            group2{
                psels = < NRF_PSEL(UART_RX, 1,6) >;
                bias-pull-up;
            };
        };
    };
};
&uart1 {
    status = "okay";
    pinctrl-0 = < &dfu_uart >;
};

到這一步修改就完成了,我們全編譯一下,然後需要檢視最終的dts檔案,看我們進行的修改是否生效,只有生效了才說明是正確的。

 

d、檢視設定是否生效

在全編譯完整後,在如下目錄先可以找到主image(也就是app)的dts檔案——zephay.dts檢視:

其中build_5340是我建立工程時選擇的生成檔案所在目錄的名字(根據你自己設定的檔名確認),可以看到在:建立目錄\zephyr 路徑下的zephyr.dts檔案中按照我們的修改正確設定了。

 

Mcubootinage修改的檢視:

建立目錄\mcuboot\zephyr 路徑下的zephyr中也是修改成功。

 

到此為止app就算製作完了。

第三步:使用串列埠下載升級包

在製作完成韌體後,你可以在生成一個app——修改一下藍芽的廣播名,然後獲得升級韌體

命令:

nrfjprog --com
mcumgr --conntype serial --connstring "COM24,baud=115200" echo hello mcumgr --conntype serial --connstring "COM23,baud=115200" image upload -e app_update.bin

把板子連上nrf裝置後,在命令列視窗執行命令:

nrfjprog --com

獲取埠的COM號,如圖:

 

 

由於我使用的是nrf5340,其有三個虛擬串列埠,從UART列印埠確定,使用的串列埠COM23

在開始傳輸前必須使用測試命令確保uart已經準備好,

命令:

 

mcumgr --conntype serial --connstring "COM12,baud=115200" echo hello

 

 

如果你是使用了第二中新增串列埠的方式,使用nrfjprog --com命令後可能不會把你串列埠列舉出來,可以通過串列埠助手參看,確定好埠後:使用更行命令:

命令:

mcumgr --conntype serial --connstring "COM23,baud=115200" image upload -e app_update.bin

等待升級完成:

 

傳輸完成後使用如下命令進行啟用:

mcumgr --conntype serial --connstring "COM12,baud=115200" image list

由返回可以確定,我們新上傳了一個韌體,放在bank2中,我們需要用到這個韌體的hash值,本次測試升級韌體的hash73edc83fe5a2536bc0a5b9b3015841f6f7a88444ebd75d2dfe0248f6bb3323f4

即執行:

mcumgr --conntype serial --connstring "COM12,baud=115200" image test 73edc83fe5a2536bc0a5b9b3015841f6f7a88444ebd75d2dfe0248f6bb3323f4

然後復位測試確定一下,是否可以跳轉到新韌體執行:

使用命令:

mcumgr --conntype serial --connstring "COM12,baud=115200" reset

執行完成後要等待一下:

可以看到新的APP執行成功。但是這不是最終的韌體修改,需要如下指令來固化

mcumgr -c acm0 image confirm 73edc83fe5a2536bc0a5b9b3015841f6f7a88444ebd75d2dfe0248f6bb3323f4

然後使用命令復位一下

nrfjprog --reset

等待一會,等程式執行起來:

使用命令:

mcumgr -c acm0 image list

可以看到啟用的韌體的hash是我們升級韌體的hash,升級成功。