發(fā)布日期:2022-04-28 點擊率:207
Other Parts Discussed in Post: CC2640
作者: TI 技術(shù)應(yīng)用工程師 張彥
CC2640 R2是一款面向 Bluetooth Smart 應(yīng)用的低功耗無線 MCU。該芯片運行TI的BLE協(xié)議棧,并支持OAD(Over the Air Download)空中固件升級功能,此空中固件升級功能就是利用Android或者iOS的產(chǎn)品對應(yīng)app通過BLE對CC2640R2的產(chǎn)品進行固件升級。同時,TI其實提供了Anroid和iOS的源碼對其支持:http://www.ti.com/tool/SENSORTAG-SW?keyMatch=sensortag&tisearch=Search-EN-Everything。這部分的源碼是基于TI的SensorTag硬件進行開發(fā)的,包含了很多內(nèi)容,對于客戶來說,基本上不適合直接拿去使用,但是其中的OAD部分代碼,卻是可以通用的。但是對于客戶的iOS或者Android app工程師來說,往往對BLE協(xié)議不熟,那就更不用說TI的OAD協(xié)議了,所以即使提供了源碼,客戶在這一部分開發(fā)起來還是很困難。本文就針對這一點,針對app開發(fā)過程中對OAD功能做一個流程和代碼解讀,用以幫助客戶更方便完成此功能開發(fā)。
首先,第一步是從上面的鏈接下載到最新的android和iOS源碼,上述鏈接最終會指引到github的下載地址。
有了源碼之后,我們就可以解讀了。
從app的角度上來看TI的OAD協(xié)議,大致是這樣的:
App連接上設(shè)備之后,就能發(fā)現(xiàn)OAD的service和characteristics(服務(wù)和特征值):
服務(wù)的UUID:0xFFC0,對應(yīng)的128bit UUID:
F000FFC0-0451-4000-B000-000000000000
服務(wù)下面有兩個主要特征值:(其他特征值可以暫且忽略)
OAD Image Identify,UUID:0xFFC1;OAD Image Block,UUID:0xFFC2,對應(yīng)的128bit UUID:
– OAD Image Identify F000FFC1-0451-4000-B000-000000000000,用于交互確認(rèn)固件版本信息。
– OAD Image Block F000FFC2-0451-4000-B000-000000000000,用于傳送新的固件。
固件更新的所有操作都是對上面這兩個特征值進行操作。
用TI的SensorTag app可以看到:

用第三方app比如light blue也能看到,安卓手機的情況也是一樣。

在TI提供的示例代碼中,在Android中,客戶唯一要用到的文件其實就是FwUpdateActivity_CC26xx.java,所有的OAD流程基本全部都在這里。另外有一個相關(guān)的文件BluetoothLeService.java,這個是TI的SensorTag App封裝的BLE相關(guān)API接口集合,主要用于一些特征值的操作,比如write,read等,OAD用到的主要就是write,通過write來神奇地實現(xiàn)各種流程。在iOS源碼中,流程相關(guān)的主要是BLETIOAD2Profile.m(注意,另外一個BLETIOADProfile.m,這是針對舊版的CC254x的,和CC26xx略有不同,這里不做討論,有興趣可以自己去看),基本就在這里,另外對應(yīng)也有一個BLE相關(guān)接口集合BLEUtility.m。
第一步
從app來講,開始OAD的第一步就是先使能上面兩個特征值的notification功能。
簡單來說就是調(diào)用android或者iOS提供的現(xiàn)有API,分分鐘完成。(藍(lán)牙協(xié)議上來說就是往這兩個特征值的CCC句柄上寫01:00,我們這里盡量不討論具體藍(lán)牙協(xié)議,從簡考慮,有興趣的人可以自己去研究一下)。

對應(yīng)到代碼里,
Android在FwUpdateActivity_CC26xx.java中,新的固件文件裝載到手機內(nèi)存后:public void onLoad(View v)中調(diào)用:
mLeService.setCharacteristicNotification(mCharIdentify, true);
和
mLeService.setCharacteristicNotification(mCharBlock, true);
實際上可以根據(jù)客戶真實代碼在適當(dāng)位置加上面兩個函數(shù)就行,這樣第一步其實就完成了。
*更多說明:
上面兩個函數(shù),其實就是BluetoothLeService.java中的API,最終追蹤下去的話是調(diào)用Android SDK 的BLE API來使能notification:
mBluetoothGatt.setCharacteristicNotification(request.characteristic, request.notifyenable)
其實我們也可以直接調(diào)用這個來實現(xiàn)上面的功能。
iOS在BLETIOAD2Profile.m里的-(void) configureProfile 函數(shù)里調(diào)用:
CBUUID *sUUID = [CBUUID UUIDWithString:TI_OAD_SERVICE];
CBUUID *cUUID = [CBUUID UUIDWithString:TI_OAD_IMAGE_NOTIFY];
[BLEUtility setNotificationForCharacteristic:self.d.p sCBUUID:sUUID cCBUUID:cUUID enable:YES];
cUUID = [CBUUID UUIDWithString:TI_OAD_IMAGE_BLOCK_REQUEST];
if (self.notifications)[BLEUtility setNotificationForCharacteristic:self.d.p sCBUUID:sUUID cCBUUID:cUUID enable:YES];
注意,iOS代碼里面OAD Image Identify和OAD Image Block對應(yīng)的是TI_OAD_IMAGE_NOTIFY和TI_OAD_IMAGE_BLOCK_REQUEST。同樣,在iOS里適當(dāng)位置調(diào)用這個函數(shù)就行。
*更多說明:
對于iOS TI給出的源碼,是BLEUtility.m中封裝了Apple的BLE API來使能notification:
- (void)setNotifyValue:(BOOL)enabled forCharacteristic:(CBCharacteristic *)characteristic;
一樣,我們也可以直接調(diào)用這個來實現(xiàn)上面的功能。
空中sniffer抓包看的話,就能看到這兩個使能notification的流程:

第二步
從app角度來講,第二步就是要把新固件的版本信息從手機傳送到外設(shè)上,讓外設(shè)進行判斷是否要升級。
這一步就要用到OAD Image Identify,這個特征值在整個OAD過程中只會用到一次,就是在這里。
App這邊先從獲得到的新固件里把固件的image header (16個字節(jié)) 讀出來,image header就是固件的二進制文件的開始16個字節(jié),很容易獲取到。

體現(xiàn)在代碼里,
Android:還是在FwUpdateActivity_CC26xx.java中,在打開新固件文件的時候:
private boolean loadFile(String filepath, boolean isAsset) {
順便創(chuàng)建要發(fā)送的16個字節(jié)image header結(jié)構(gòu):
mFileImgHdr = new ImgHdr(mFileBuffer,readLen);
這個構(gòu)造函數(shù)就會把image header部分給組織好放到16字節(jié)的一個buffer中去,用以接下來發(fā)送給設(shè)備。這個類的定義也在FwUpdateActivity_CC26xx.java中:
private class ImgHdr {
    ……
    ImgHdr(byte[] buf, int fileLen) {
        this.len = (fileLen / (16 / 4));
        this.ver = 0;
        this.uid[0] = this.uid[1] = this.uid[2] = this.uid[3] = 'E';
        this.addr = 0;
        this.imgType = 1; //EFL_OAD_IMG_TYPE_APP
        this.crc0 = calcImageCRC((int)0,buf);
        crc1 = (short)0xFFFF;
        ……
    }
可以看到構(gòu)造函數(shù)里面,雖然有讀取到的實際固件文件的image header作為輸入?yún)?shù),但我們實際的代碼里面為了演示,只是寫死了一些內(nèi)容。這里可以根據(jù)實際情況修改一下,根據(jù)前面提到的16字節(jié)header的順序來就行,比如:
private class ImgHdr {
    ……
    ImgHdr(byte[] buf, int fileLen) {
        this.len = (fileLen / (16 / 4));
        this.ver = buf[5];
       this.ver = (this.ver << 8) | buf[4];
        this.uid[0] = buf[8];this.uid[1] = buf[9];
       this.uid[2] = buf[10]; this.uid[3] = buf[11];
        this.addr = buf[13];
       this.addr = (this.addr << 8) | buf[12];
        this.imgType = buf[14];//EFL_OAD_IMG_TYPE_APP
        this.crc0 = calcImageCRC((int)0,buf);
        crc1 = (short)0xFFFF;
        ……
    }
這樣就基本能拿到實際的真實數(shù)據(jù)了。
iOS:就很簡單了,直接把新固件文件開始的16字節(jié)header復(fù)制到代碼里就可以了,還是在BLETIOAD2Profile.m里,在-(void) uploadImage 函數(shù)中:
img_hdr_t2 imgHeader;
uint32_t pages = ((uint32_t)self.imageFile.length / 4096);
//做個CRC校驗,放到image header中去,同時把image header的16字節(jié)內(nèi)容補完整。
[self calcImageInfo:0 pages:pages imageHeader:&imgHeader buf:imageFileData];
memcpy(requestData, &imgHeader, 16);
需要注意的是iOS給的源碼里面,image header內(nèi)容的補充是在crc校驗函數(shù)里面一并完成的:-(void) calcImageInfo里:
imageHeader buf:(uint8_t *)buf {
imageHeader->len = (pages * FLASH_PAGE_SIZE) / (OAD_BLOCK_SIZE / FLASH_WORD_SIZE);
imageHeader->ver = 0;
imageHeader->uid[0] = imageHeader->uid[1] = imageHeader->uid[2] = imageHeader->uid[3] = 'E';
imageHeader->addr = (firstpage * FLASH_PAGE_SIZE) / (OAD_BLOCK_SIZE / FLASH_WORD_SIZE);
imageHeader->imgType = EFL_OAD_IMG_TYPE_APP;
imageHeader->res[0] = 0xff;
imageHeader->crc0 = [self calcImageCRC:firstpage imageHeader:imageHeader buf:buf];
imageHeader->crc1 = 0xffff;
}
這樣iOS這里也得到完整的image header信息了。
接下來app將通過OAD Image Identify這個特征值首先把前面得到的新的固件的版本信息發(fā)送給設(shè)備(CC2640R2),這個版本信息包含在前面得到的image header的16個字節(jié)buffer里,把這個發(fā)出去給設(shè)備就行了。
體現(xiàn)在代碼里,
Android,還是在FwUpdateActivity_CC26xx.java中,開始流程函數(shù):
private void startProgramming() {
獲取image identify(其實就是image header):
mCharIdentify.setValue(mFileImgHdr.getRequest());
并通過BLE的write characteristic動作從app發(fā)送到設(shè)備端:
mLeService.writeCharacteristic(mCharIdentify);
*更多說明:
上面write characteristic函數(shù)其實就是BluetoothLeService.java中的API,最終追蹤下去的話是調(diào)用Android SDK 的BLE API來進行BLE的write command操作:
mBluetoothGatt.writeCharacteristic(request.characteristic);
其實我們也可以直接調(diào)用這個來實現(xiàn)上面的功能。
iOS代碼中,在BLETIOAD2Profile.m里,還是在-(void) uploadImage 函數(shù)里:
 CBUUID *cUUID = [CBUUID UUIDWithString:TI_OAD_IMAGE_NOTIFY];
[BLEUtility writeNoResponseCharacteristic:self.d.p sCBUUID:sUUID cCBUUID:cUUID data:[NSData dataWithBytes:requestData length:16]];
通過 write characteristic來把image header發(fā)送到設(shè)備端。
*更多說明:
這個的write函數(shù),其實也是在BLEUtility.m里面封裝了Apple的BLE標(biāo)準(zhǔn)API:
- (void)writevalue:(NSData *)data forCharacteristic:(CBCharacteristic *)characteristic type:(CBCharacteristicWriteType)type;
我們也可以直接調(diào)用這個來實現(xiàn)上面的功能。
空中sniffer抓包能看到write command發(fā)送image header:

最后,設(shè)備收到image header之后,會進行和自己本身的固件版本號進行比較。如果發(fā)現(xiàn)image header中的版本號沒有自己的版本號新,那么就直接以notification (這里notification是在前面第一步使能)的形式在OAD Image Identify上回復(fù)自己的版本號給手機,表示拒絕此次固件升級,此次升級就此結(jié)束。
空中sniffer抓包,其中標(biāo)黃的部分就是設(shè)備回復(fù)自身版本號,表示拒絕此次固件升級:

如果發(fā)現(xiàn)image header中的版本號確實比自己本身的固件版本號要新,那么就同意這次固件更新,會在OAD Image Block這個特征值的notification (這里notification是在前面第一步使能)上回復(fù)0x0000,表示準(zhǔn)備接受序列號第0個固件內(nèi)容包。注意實在OAD Image Block這個特征值上,不是OAD Image Identify這個特征值上,OAD Image Identify這個特征值的使命在前面已經(jīng)完成,后面不會再使用。
從sniffer看就會看到0x0000從外設(shè)發(fā)回手機:

總結(jié)下來第二步,就是app要在OAD Image Identify特征值上發(fā)送新固件的image header到設(shè)備端,然后設(shè)備端進行判斷是否要接收新的固件進行升級,如果要升級,則在OAD Image Block特征值上向手機回復(fù)0x0000,不然回復(fù)自己目前的固件版本號表示拒絕更新。總結(jié)起來就是下面的兩個流程圖:

第三步
這一步從app來講就是按照順序發(fā)送固件內(nèi)容了。
手機在OAD Image Block特征值上收到0x0000之后就代表設(shè)備端愿意接收新固件,并且意思是對方準(zhǔn)備好接收第0個固件包。每個固件包的內(nèi)容長度是16個字節(jié),并且需要在頭部放上兩個字節(jié)的序列號,序列號從0x0000開始累加,一直到最后一個固件包。
*注意這個序列號是小端在前的,所以后面是0x0100,0x0200,0x0300,0x0400,…,0xFF00,0x0001,0x0101,0x0201,…這樣累加上去。
從空中sniffer上看,很容易就能看到整個流程的交互:
首先是手機發(fā)送第0包的固件內(nèi)容,通過BLE的Write方式在OAD Image Block特征值上發(fā)送,固件block內(nèi)容是16個字節(jié),注意下圖標(biāo)黃的頭兩個字節(jié),是固件包序列號,第0包是0x0000,所以包的總長是16+2=18個字節(jié)。

那么設(shè)備收到APP發(fā)送過去的第0包固件后,回復(fù)請求下一個固件包,通過Notification的方式在OAD Image Block特征值上回復(fù)0x0100:

APP收到設(shè)備發(fā)回的0x0100,就知道對方已經(jīng)等待接收下一包固件,于是就發(fā)送下一包0x0100的固件包,以此類推,一直到固件發(fā)送結(jié)束。

具體到代碼里,
在Android中,就是在programBlock()函數(shù)里,這里我們只關(guān)注核心部分:
private void programBlock() {
    …
…
   省略前面邏輯相關(guān)代碼,工程里面很容易看懂。
        // Prepare block
   首先自然是包序列號,buffer的頭兩個字節(jié):
        mOadBuffer[0] = Conversion.loUint16(mProgInfo.iBlocks);
        mOadBuffer[1] = Conversion.hiUint16(mProgInfo.iBlocks);
        然后是16個字節(jié)的固件block包內(nèi)容:
   System.arraycopy(mFileBuffer, mProgInfo.iBytes, mOadBuffer, 2, OAD_BLOCK_SIZE);
 
        // Send block
   接著把這18個字節(jié)通過write方式在OAD Image Block特征值上發(fā)送:
        mCharBlock.setValue(mOadBuffer);
        boolean success = mLeService.writeCharacteristicNonBlock(mCharBlock);
        如果發(fā)送成功,那么就順移相關(guān)的包序列號還有固件文件中的位移標(biāo)志:
        if (success) {
            // Update stats
            packetsSent++;
            mProgInfo.iBlocks++;
            mProgInfo.iBytes += OAD_BLOCK_SIZE;
            mProgressBar.setProgress((mProgInfo.iBlocks * 100) / mProgInfo.nBlocks);
       如果最后一個固件block成功完成,那么就恭喜,OAD順利成功!
            if (mProgInfo.iBlocks == mProgInfo.nBlocks) {
                …
                b.setTitle("Programming finished");
                b.setPositiveButton("OK",null);
 
                alertDialog d = b.create();
                d.show();
                mProgramming = false;
               mLog.append("Programming finished at block " + (mProgInfo.iBlocks + 1) + "");
            }
        } else {
            mProgramming = false;
            msg = "GATT writeCharacteristic failed";
        }
        …
    …
}
*更多說明:
上面boolean success = mLeService.writeCharacteristicNonBlock(mCharBlock);這個函數(shù),其實就是BluetoothLeService.java中的API,最終追蹤下去的話是調(diào)用Android SDK 的BLE API來實現(xiàn)的:mBluetoothGatt.writeCharacteristic(request.characteristic);
iOS的代碼里,體現(xiàn)在BLETIOAD2Profile.m里的-(void) sendonePacket 函數(shù),只看核心部分,和Android很像的,因為流程一樣:
-(void) sendonePacket {
…
…
//Prepare Block
首先自然是包序列號,buffer的頭兩個字節(jié):
uint8_t requestData[2 + OAD_BLOCK_SIZE];
requestData[0] = LO_UINT16(self.iBlocks);
requestData[1] = HI_UINT16(self.iBlocks);
然后是16個字節(jié)的固件block包內(nèi)容:
memcpy(&requestData[2] , &imageFileData[self.iBytes], OAD_BLOCK_SIZE);
CBUUID *sUUID = [CBUUID UUIDWithString:TI_OAD_SERVICE];
CBUUID *cUUID = [CBUUID UUIDWithString:TI_OAD_IMAGE_BLOCK_REQUEST];
接著把這18個字節(jié)通過write方式在OAD Image Block特征值上發(fā)送:
[BLEUtility writeNoResponseCharacteristic:self.d.p sCBUUID:sUUID cCBUUID:cUUID data:[NSData dataWithBytes:requestData length:2 + OAD_BLOCK_SIZE]];
如果發(fā)送成功,那么就順移相關(guān)的包序列號還有固件文件中的位移標(biāo)志:
dataWithBytes:requestData length:2 + OAD_BLOCK_SIZE]);
self.sndDataCount ++;
self.iBlocks++;
self.iBytes += OAD_BLOCK_SIZE;
self.sentPackets++;
如果最后一個固件block成功完成,那么就恭喜,OAD順利成功!
if(self.iBlocks == self.nBlocks) {
self.inProgramming = NO;
[self.oadDelegate didFinishUploading];
return;
}
流程和Android完全一樣,我連注釋都直接復(fù)制過來了J。
這樣就是完整的OAD流程了。總結(jié)就是分三步走:使能OAD Image Identify和OAD Image Block 的notification,在OAD Image Identify上發(fā)送新固件版本(image header)進行確認(rèn),最后在OAD Image Block上按順序把固件發(fā)送完,結(jié)束。
下一篇: PLC、DCS、FCS三大控
上一篇: 在AM335X平臺上運行ub