RT-Thread STM32F4 自制 BootLoader 的製作和使用(線上升級上位機篇Android)

鴛鴦冰筆發表於2020-11-14

前言說明

  前兩篇文章分別介紹了通用BootLoader的編寫和自制BootLoader的編寫,本篇文章用來介紹串列埠YModem升級的上位機實現方法。

大致介紹

  在上位機實現在下位機的線上升級,最關鍵的就是實現串列埠YModem的通訊協議,由於很多嵌入式設計的多樣性的需求,不可能有完美的第三方庫滿足需求,因此我們需要在第三方庫的基礎上進行修改。
採用的第三方庫的地址為: https://github.com/ArdWang/YModemlib_Android.

思路分析

1.為什麼採用YModem協議進行串列埠傳送和接收呢?是不是有別的更好的方案?

  這是我一開始就思考的問題。在慢慢的除錯和使用過程中,有了答案,串列埠通訊的協議方式有很多,甚至自己造一個都是可以的,但是採用YModem協議進行通訊,使用的人比較多,並且通訊準確可靠,帶CRC校驗,像xshell或者別的串列埠除錯工具自帶YModem協議,即使上位機的程式碼沒有寫,也可以採用串列埠除錯工具來直接除錯下位機的程式,比較方便。當然,如果也可以用來除錯上位的程式碼。

2.,如果將600多kb的檔案內容直接下發是否合適?

  線上升級的檔案大小基本上都在幾百KB以上,因此如果將檔案一次性下發,是可以的,Linux系統對於輸入和輸出流的大小是有限制的,因此不僅僅Linux系統對輸出流有快取,Android系統下app 程式內部也要對發出的檔案內容進行快取,串列埠的輸出速度是固定的,因此即使app程式將資料傳送完成,資料真正到達到下位機的時間並不受APP控制。直接將幾百KB大小的檔案下發給RAM很小的微控制器是不合適的,微控制器無法快速的處理完畢這麼多資料。

除錯建議

  除錯YModem協議可以採用Xshell自帶的YModem通訊功能來進行除錯,使用串列埠網路除錯都比較方便。

綜上:

  因此採用一個帶CRC校驗和分批傳送資料的協議進行線上的升級是十分必要的。並且考察了RT-thread 通用BootLoader的升級方式就可以明白,RT-thread採用的是YModem-1K通訊協議。因此如果自制BootLoader,最好也採用YModem-1k協議,這樣與通用BootLoader可以保持一定的相容,也方便以後除錯和移植。並且這種通訊協議可以很方便的更換通訊硬體,如網路,藍芽等。

啟動通訊

下面的程式碼是一個簡單的資料傳送啟動示例。

 private void startTransmission(){
        //String md5 = MD5Util.MD5(mCurrentFilePath);
        yModem = new YModem.Builder()
                .with(this)
                .filePath(mCurrentFilePath)
                .fileName(mCurrentFileName)
                .checkMd5("")
                .callback(new YModemListener() {
                    @Override
                    public void onDataReady(byte[] data) { //資料傳送
                        if(sendData) {
                            if (bluetoothService.writeCharacteristic(mSendOTACharacteristic, data)) {
                                Log.i("==Send Data is:", bytesToHexFun(data));
                            }
                        }
                    }
 
                    @Override
                    public void onProgress(int currentSent, int total) { //傳送進度
                        float currentPt = (float)currentSent/total;
                        int a = (int)(currentPt*100);
                        mUpgradeBar.setProgress(currentSent);   // Main Progress
                        mUpgradeBar.setMax(total); // Maximum Progress
                        if(a<=100){
                            mUpgradeBar.setProgressText(""+a+"%");
                        }else{
                            mUpgradeBar.setProgressText("100%");
                        }
                    }
                    @Override
                    public void onSuccess() { //傳送完畢
                        Toast.makeText(OTAActivity.this,"韌體升級完成",Toast.LENGTH_LONG).show();
                        finish();
                    }
                    @Override
                    public void onFailed(String reason) {//傳送失敗
                        Toast.makeText(OTAActivity.this,reason,Toast.LENGTH_LONG).show();
                    }
                }).build();
        yModem.start(); //啟動
    }

關鍵點說明

控制一次讀取資料大小

  下面的程式碼是讀取傳送檔案的方法,在FileStreamThread.java檔案中,其中

byte[] block = new byte[1024];

是控制一次讀取和傳送資料的大小的,預設是1024位元組。

    private void prepareData() throws IOException {
        initStream();
        byte[] block = new byte[1024]; //用於修改 一次輸出的資料大小,128位元組或者1024位元組
        int dataLength;
        byte blockSequence = 1;//The data package of a file is actually started from 1
        isDataAcknowledged.set(true);
        isKeepRunning = true;
        while (isKeepRunning) {

            if (!isDataAcknowledged.get()) {
                try {
                    //We need to sleep for a while as the sending 1024 bytes data from ble would take several seconds
                    //In my circumstances, this can be up to 3 seconds.
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                continue;
            }

            if ((dataLength = inputStream.read(block)) == -1) {
                L.f("The file data has all been read...(檔案所有內容讀取完畢!)");
                if (listener != null) {
                    onStop();
                    listener.onFinish();
                }
                break;
            }

            byte[] packige = YModemUtil.getDataPackage(block, dataLength, blockSequence);

            if (listener != null) {
                listener.onDataReady(packige);
            }

            blockSequence++;
            isDataAcknowledged.set(false);
        }

    }

流程控制

  下面的程式碼是主要用於控制整個通訊過程中的流程的,這與標準的YModem-1K的協議流程是一致的,但是具體到應用中還需要進行除錯測試,我在使用的過程中就發現在與XShell工具進行YModem通訊時是沒問題的,但是到了與微控制器進行YModem通訊時有時候會出現不響應的情況,請大家在使用時根據標準的YModem協議流程進行單步除錯,測試。適當時要具體進行修改,提高程式碼的相容性。

    /**
     * Method for the outer caller when received data from the terminal
     */
    public void onReceiveData(byte[] respData) {
        if (respData != null && respData.length > 0) {
            switch (CURR_STEP) {
                case STEP_HELLO:
                    handleHello(respData);
                    break;
                case STEP_FILE_NAME:
                    handleFileName(respData);
                    break;
                case STEP_FILE_BODY:
                    timerHelper.stopTimer();
                    handleFileBody(respData);
                    break;
                case STEP_EOT:
                    timerHelper.stopTimer();
                    handleEOT(respData);
                    break;
                case STEP_END:
                    timerHelper.stopTimer();
                    handleEnd(respData);
                    break;
                default:
                    break;
            }
        } else {
            L.f("The terminal do responsed something, but received nothing??");
        }
    }

資料傳送的YModem協議

  這個YModem的協議與標準的YModem的協議有一點差異,主要是加入了檔案的Md5校驗和Hello握手資料的傳送。這是為了方便把傳送資料的主動權控制在傳送端手中。如果有必要可以根據實際的需求來修改握手和MD5校驗這部分程式碼。

/**
 * THE YMODEM:

 * HELLO BOOTLOADER ---------------------------------------------->* //主機傳送HELLO資料並等待從機的回應,失敗達到一定次數,則判斷失敗,從機收到後會回應ok(ox0C)
 * <---------------------------------------------------------------* C //從機回覆OK
 * SOH 00 FF filename0fileSizeInByte0MD5[90] ZERO[38] CRC CRC----->* //主機傳送 檔名和檔案大小資訊並附帶傳送的這段資料的CRC檢驗碼
 * <---------------------------------------------------------------* ACK C //從機回覆應答,並帶ok
 * STX 01 FE data[1024] CRC CRC ---------------------------------->* //主機開始傳送資料給從機 每次資料1024位元組,並帶著資料的CRC校驗值,注意程式中計算資料傳送進度的時候把CRC校驗的資料也計算了進去,導致計算出的進度比實際進度快,目前沒有修復這個問題(如果確實有必要可以考慮修改下程式碼進行修復)
 * <---------------------------------------------------------------* ACK //從機回覆應該
 * STX 02 FF data[1024] CRC CRC ---------------------------------->*
 * <---------------------------------------------------------------* ACK
 * ...
 * ...
 * <p>
 * STX 08 F7 data[1000] CPMEOF[24] CRC CRC ----------------------->*
 * <---------------------------------------------------------------* ACK
 * EOT ----------------------------------------------------------->* //主機傳送EOT指令給從機,表示要結束傳輸
 * <---------------------------------------------------------------* ACK //從機回覆應答
 * SOH 00 FF ZERO[128] ------------------------------------------->* //傳送結束資料幀
 * <---------------------------------------------------------------* ACK //從機回覆應答
 * <---------------------------------------------------------------* MD5_OK //從機會MDK5檢驗ok

 */

標準YModem協議說明

基本流程

  YModem分成YModem-1K與YModem-g。
  YModem-1K用1024位元組資訊塊傳輸取代標準的128位元組傳輸,資料的傳送回使用CRC校驗,保證資料傳輸的正確性。它每傳輸一個資訊塊資料時,就會等待接收端回應ACK訊號,接收到回應後,才會繼續傳輸下一個資訊塊,保證資料已經全部接收。
  YModem-g傳輸形式與YModem-1K差不多,但是它去掉了資料的CRC校驗碼,同時在傳送完一個資料塊資訊後,它不會等待接收端的ACK訊號,而直接傳輸下一個資料塊。正是它沒有涉及錯誤校驗,才使得它的傳輸速度比YModem-1K來得塊。
  一般都會選擇YModem-1K傳輸,平時所說的YModem也是指的是YModem-1K。下面就講講它的傳輸協議。

1、起始幀的資料格式

  YModem的起始幀並不直接傳輸檔案的資料,而是將檔名與檔案的大小放在資料幀中傳輸,它的幀長=3位元組資料首部+128位元組資料+2位元組CRC16校驗碼=33位元組。它的資料結構如下:

SOH  00 FF  filename[ ] filezise[ ]  NUL[ ] CRCH CRCL

  其中SOH=0x01,表示這個資料幀中包含著128位元組的資料部分;在SOH後面的00 FF,00表示資料幀序號,因為是起始幀,所以它的幀序為00,至於FF,它是幀序的取反,YModem特地這麼做是 為了給資料是否正確提供一種判斷依據 ,通過判斷這兩個位元組是否為取反關係,就可以知道資料是否傳輸出錯;
  filename[ ]就是檔名,如檔名foo.c,它在資料幀中存放格式為:66 6F 6F 2E 63 00,一定要在檔名最後跟上一個00,表示檔名結束;
  filesize[ ]就是檔案大小,如上面的foo.c的大小為1KByte,即1024Byte,需要先將它轉化成16進位制,即0x400,所以它在資料幀的存放格式為:34 30 30 00,即“400”,同樣的檔案大小最後需要跟上00,表示結束;
  NUL[ ]表示剩下的位元組都用00填充,資料部分大小為128位元組,除去檔名與檔案大小佔用的空間外,剩餘的位元組全部用00填充;CRCH CRCL分別表示16位CRC校驗碼的高8位與低8位。

2、資料幀的資料格式

  YModem的資料幀中會預留1024位元組空間用來傳輸檔案資料,它跟起始幀接收差不多,如下:

STX 01 FE data[1024] CRCH CRCL

  其中STX=0x02,表示這幀資料幀後面包含著1024位元組的資料部分;STX後面的01 FE,01表示第一幀資料幀,FE則是它的取反,當然如果是第二幀資料的話就是:01 FD;data[1024]表示存放著1024位元組的檔案資料;CRCH與CRCL是CRC16檢驗碼的高8位與低8位。
  如果檔案資料的最後剩餘的資料在128~1024之前,則還是使用STX的1024位元組傳輸,但是剩餘空間全部用0x1A填充,如下結構:

STX [num] [~num] data[ ] 1A ...1A CRCH CRCL

  有一種特殊的情況:如果檔案大小小於等於128位元組或者檔案資料最後剩餘的資料小於128位元組,則YModem會選擇SOH資料幀用128位元組來傳輸資料,如果資料不滿128位元組,剩餘的資料用0x1A填充這是資料正的結構就變成了:
檔案大小小於128位元組:

SOH 01 FE data[ ] 1A ...1A CRCH CRCL  

檔案最後剩餘資料小於128位元組:

 SOH [num] [~~num] data[ ] 1A...1A CRCH CRCL

3、結束幀資料結構

  YModem的結束幀資料也採用SOH的128位元組資料幀,它的結構如下:

SOH 00 FF NUL[128] CRCH CRCL

結束幀同樣以SOH開頭,表示後面跟著128位元組大小的資料;結束幀的幀序也認為是00 FF;結束幀的128位元組的資料部分不存放任何資訊,即NUL[128]全部用00填充。

4、檔案傳輸過程

  檔案的傳輸過程,以具體的例子說明。把foo.c,大小為4196Byte(16進製為0x1064)的檔案作為傳輸的物件,則它的傳輸過程如下:
  下面是對標準的YModem的協議進行說明,這個協議的流程是要熟悉的,這是為了方便後續程式碼除錯。

步驟序號傳送端資料資料方向接收端資料
1-<<<<C
2SOH 00 FF “foo.c” "1064’’ NUL[118] CRC CRC>>>>>-
3-<<<<ACK
4-<<<<C
5STX 01 FE data[1024] CRC CRC>>>>>-
6-<<<<ACK
7STX 02 FD data[1024] CRC CRC>>>>-
8-<<<<ACK
9STX 03 FC data[1024] CRC CRC>>>>-
10-<<<<<ACK
11STX 04 FB data[1024] CRC CRC>>>>-
12-<<<<ACK
13SOH 05 FA data[100] 1A[28] CRC CRC>>>>-
14-<<<<ACK
15EOT>>>>>-
16-<<<<<NAK
17EOT>>>>>-
18-<<<<<ACK
19-<<<<<C
20SOH 00 FF NUL[128] CRC CRC>>>>>-
21-<<<<<ACK

5、CRC的計算

  YModem的採用的是CRC16-CCITT歐洲版本的CRC校驗,它的生成多項式為:x16+x12+x5+1,具體的CRC的計算可以檢視原始碼中內容。

關鍵點說明

  YModem的傳輸過程就是上面所示。但是上面傳輸過程中存在許多通訊訊號,它們的數值與意義如下表所示:

符號數值含義說明
SOH0x01128位元組資料包
STX0x021024位元組資料包
EOT0x04結束傳輸EOT訊號由傳送端傳送
ACK0x06回應
NAK0x15不回應
CA0x18傳輸中止CA中止傳輸訊號由傳送端傳送
C0x43請求資料包

總結

  GitHub提供的程式碼只是方便使用而已,其實線上升級的功能根據需求的不同有很多細節上的區別,關鍵是要理解了YModem協議後,根據具體的要求來修改自己的程式碼。

相關文章