CC2640R2: TI BLE OAD(OTA)协议在Android和iOS上的APP流程和代码解读

作者: TI 技术应用工程师 张彦

CC2640 R2是一款面向 Bluetooth Smart 应用的低功耗无线 MCU。该芯片运行TI的BLE协议栈,并支持OAD(Over the Air Download)空中固件升级功能,此空中固件升级功能就是利用Android或者iOS的产品对应app通过BLE对CC2640R2的产品进行固件升级。同时,TI其实提供了Anroid和iOS的源码对其支持:http://www.ti.com/tool/SENSORTAG-SW?keyMatch=sensortag&tisearch=Search-EN-Everything。这部分的源码是基于TI的SensorTag硬件进行开发的,包含了很多内容,对于客户来说,基本上不适合直接拿去使用,但是其中的OAD部分代码,却是可以通用的。但是对于客户的iOS或者Android app工程师来说,往往对BLE协议不熟,那就更不用说TI的OAD协议了,所以即使提供了源码,客户在这一部分开发起来还是很困难。本文就针对这一点,针对app开发过程中对OAD功能做一个流程和代码解读,用以帮助客户更方便完成此功能开发。

首先,第一步是从上面的链接下载到最新的android和iOS源码,上述链接最终会指引到github的下载地址。

有了源码之后,我们就可以解读了。

从app的角度上来看TI的OAD协议,大致是这样的:

App连接上设备之后,就能发现OAD的service和characteristics(服务和特征值):

服务的UUID:0xFFC0,对应的128bit UUID:

F000FFC0-0451-4000-B000-000000000000

服务下面有两个主要特征值:(其他特征值可以暂且忽略)

OAD Image Identify,UUID:0xFFC1;OAD Image Block,UUID:0xFFC2,对应的128bit UUID:

– OAD Image Identify F000FFC1-0451-4000-B000-000000000000,用于交互确认固件版本信息。

– OAD Image Block       F000FFC2-0451-4000-B000-000000000000,用于传送新的固件。

固件更新的所有操作都是对上面这两个特征值进行操作。

用TI的SensorTag app可以看到:

用第三方app比如light blue也能看到,安卓手机的情况也是一样。

 

在TI提供的示例代码中,在Android中,客户唯一要用到的文件其实就是FwUpdateActivity_CC26xx.java,所有的OAD流程基本全部都在这里。另外有一个相关的文件BluetoothLeService.java,这个是TI的SensorTag App封装的BLE相关API接口集合,主要用于一些特征值的操作,比如write,read等,OAD用到的主要就是write,通过write来神奇地实现各种流程。在iOS源码中,流程相关的主要是BLETIOAD2Profile.m(注意,另外一个BLETIOADProfile.m,这是针对旧版的CC254x的,和CC26xx略有不同,这里不做讨论,有兴趣可以自己去看),基本就在这里,另外对应也有一个BLE相关接口集合BLEUtility.m。

第一步

从app来讲,开始OAD的第一步就是先使能上面两个特征值的notification功能。

简单来说就是调用android或者iOS提供的现有API,分分钟完成。(蓝牙协议上来说就是往这两个特征值的CCC句柄上写01:00,我们这里尽量不讨论具体蓝牙协议,从简考虑,有兴趣的人可以自己去研究一下)。

对应到代码里,
Android在FwUpdateActivity_CC26xx.java中,新的固件文件装载到手机内存后:public void onLoad(View v)中调用:

 

mLeService.setCharacteristicNotification(mCharIdentify, true);

mLeService.setCharacteristicNotification(mCharBlock, true);

实际上可以根据客户真实代码在适当位置加上面两个函数就行,这样第一步其实就完成了。

*更多说明:

上面两个函数,其实就是BluetoothLeService.java中的API,最终追踪下去的话是调用Android SDK 的BLE API来使能notification:

mBluetoothGatt.setCharacteristicNotification(request.characteristic, request.notifyenable)

其实我们也可以直接调用这个来实现上面的功能。

iOS在BLETIOAD2Profile.m里的-(void) configureProfile 函数里调用:

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对应的是TI_OAD_IMAGE_NOTIFY和TI_OAD_IMAGE_BLOCK_REQUEST。同样,在iOS里适当位置调用这个函数就行。

*更多说明:

对于iOS TI给出的源码,是BLEUtility.m中封装了Apple的BLE API来使能notification:

- (void)setNotifyValue:(BOOL)enabled forCharacteristic:(CBCharacteristic *)characteristic;

一样,我们也可以直接调用这个来实现上面的功能。

空中sniffer抓包看的话,就能看到这两个使能notification的流程:

 

第二步

 

从app角度来讲,第二步就是要把新固件的版本信息从手机传送到外设上,让外设进行判断是否要升级。

这一步就要用到OAD Image Identify,这个特征值在整个OAD过程中只会用到一次,就是在这里。

  • App这边先从获得到的新固件里把固件的image header (16个字节) 读出来,image header就是固件的二进制文件的开始16个字节,很容易获取到。

体现在代码里,

Android:还是在FwUpdateActivity_CC26xx.java中,在打开新固件文件的时候:

private boolean loadFile(String filepath, boolean isAsset) {

 

顺便创建要发送的16个字节image header结构:

mFileImgHdr = new ImgHdr(mFileBuffer,readLen);

这个构造函数就会把image header部分给组织好放到16字节的一个buffer中去,用以接下来发送给设备。这个类的定义也在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;
       ……
   }

可以看到构造函数里面,虽然有读取到的实际固件文件的image header作为输入参数,但我们实际的代码里面为了演示,只是写死了一些内容。这里可以根据实际情况修改一下,根据前面提到的16字节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;
       ……
   }

这样就基本能拿到实际的真实数据了。

 

iOS:就很简单了,直接把新固件文件开始的16字节header复制到代码里就可以了,还是在BLETIOAD2Profile.m里,在-(void) uploadImage 函数中:

   img_hdr_t2 imgHeader;

  

uint32_t pages = ((uint32_t)self.imageFile.length / 4096);

//做个CRC校验,放到image header中去,同时把image header的16字节内容补完整。

[self calcImageInfo:0 pages:pages imageHeader:&imgHeader buf:imageFileData];

  

      memcpy(requestData, &imgHeader, 16);

需要注意的是iOS给的源码里面,image header内容的补充是在crc校验函数里面一并完成的:-(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这个特征值首先把前面得到的新的固件的版本信息发送给设备(CC2640R2),这个版本信息包含在前面得到的image header的16个字节buffer里,把这个发出去给设备就行了。

体现在代码里,

Android,还是在FwUpdateActivity_CC26xx.java中,开始流程函数:

private void startProgramming() {

 

获取image identify(其实就是image header):

mCharIdentify.setValue(mFileImgHdr.getRequest());

 

并通过BLE的write characteristic动作从app发送到设备端:

mLeService.writeCharacteristic(mCharIdentify);

 

*更多说明:

上面write characteristic函数其实就是BluetoothLeService.java中的API,最终追踪下去的话是调用Android SDK 的BLE API来进行BLE的write command操作:

mBluetoothGatt.writeCharacteristic(request.characteristic);

其实我们也可以直接调用这个来实现上面的功能。

iOS代码中,在BLETIOAD2Profile.m里,还是在-(void) uploadImage 函数里:
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发送到设备端。

*更多说明:

这个的write函数,其实也是在BLEUtility.m里面封装了Apple的BLE标准API:

- (void)writeValue:(NSData *)data forCharacteristic:(CBCharacteristic *)characteristic type:(CBCharacteristicWriteType)type;

我们也可以直接调用这个来实现上面的功能。

空中sniffer抓包能看到write command发送image header:

 

 

  • 最后,设备收到image header之后,会进行和自己本身的固件版本号进行比较。如果发现image header中的版本号没有自己的版本号新,那么就直接以notification (这里notification是在前面第一步使能)的形式在OAD Image Identify上回复自己的版本号给手机,表示拒绝此次固件升级,此次升级就此结束。

空中sniffer抓包,其中标黄的部分就是设备回复自身版本号,表示拒绝此次固件升级:

 

如果发现image header中的版本号确实比自己本身的固件版本号要新,那么就同意这次固件更新,会在OAD Image Block这个特征值的notification (这里notification是在前面第一步使能)上回复0x0000,表示准备接受序列号第0个固件内容包。注意实在OAD Image Block这个特征值上,不是OAD Image Identify这个特征值上,OAD Image Identify这个特征值的使命在前面已经完成,后面不会再使用。

从sniffer看就会看到0x0000从外设发回手机:

总结下来第二步,就是app要在OAD Image Identify特征值上发送新固件的image header到设备端,然后设备端进行判断是否要接收新的固件进行升级,如果要升级,则在OAD Image Block特征值上向手机回复0x0000,不然回复自己目前的固件版本号表示拒绝更新。总结起来就是下面的两个流程图:

第三步

这一步从app来讲就是按照顺序发送固件内容了。

手机在OAD Image Block特征值上收到0x0000之后就代表设备端愿意接收新固件,并且意思是对方准备好接收第0个固件包。每个固件包的内容长度是16个字节,并且需要在头部放上两个字节的序列号,序列号从0x0000开始累加,一直到最后一个固件包。

*注意这个序列号是小端在前的,所以后面是0x0100,0x0200,0x0300,0x0400,…,0xFF00,0x0001,0x0101,0x0201,…这样累加上去。

从空中sniffer上看,很容易就能看到整个流程的交互:

首先是手机发送第0包的固件内容,通过BLE的Write方式在OAD Image Block特征值上发送,固件block内容是16个字节,注意下图标黄的头两个字节,是固件包序列号,第0包是0x0000,所以包的总长是16+2=18个字节。

那么设备收到APP发送过去的第0包固件后,回复请求下一个固件包,通过Notification的方式在OAD Image Block特征值上回复0x0100:

APP收到设备发回的0x0100,就知道对方已经等待接收下一包固件,于是就发送下一包0x0100的固件包,以此类推,一直到固件发送结束。

具体到代码里,

在Android中,就是在programBlock()函数里,这里我们只关注核心部分:

private void programBlock() {
  

   省略前面逻辑相关代码,工程里面很容易看懂。
       // Prepare block

   首先自然是包序列号,buffer的头两个字节:
      
mOadBuffer[0] = Conversion.loUint16(mProgInfo.iBlocks);
       mOadBuffer[1] = Conversion.hiUint16(mProgInfo.iBlocks);
       然后是16个字节的固件block包内容:

   System.arraycopy(mFileBuffer, mProgInfo.iBytes, mOadBuffer, 2, OAD_BLOCK_SIZE);

       // Send block

   接着把这18个字节通过write方式在OAD Image Block特征值上发送:
      
mCharBlock.setValue(mOadBuffer);
       boolean success = mLeService.writeCharacteristicNonBlock(mCharBlock);
       如果发送成功,那么就顺移相关的包序列号还有固件文件中的位移标志:
      
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) + "\n");
           }
       } else {
           mProgramming = false;
           msg = "GATT writeCharacteristic failed\n";
       }
      
  

}

*更多说明:

上面boolean success = mLeService.writeCharacteristicNonBlock(mCharBlock);这个函数,其实就是BluetoothLeService.java中的API,最终追踪下去的话是调用Android SDK 的BLE API来实现的:mBluetoothGatt.writeCharacteristic(request.characteristic);

 

iOS的代码里,体现在BLETIOAD2Profile.m里的-(void) sendOnePacket 函数,只看核心部分,和Android很像的,因为流程一样:

-(void) sendOnePacket {

            …

            …

   //Prepare Block

   首先自然是包序列号,buffer的头两个字节:

   uint8_t requestData[2 + OAD_BLOCK_SIZE];

  

   requestData[0] = LO_UINT16(self.iBlocks);

   requestData[1] = HI_UINT16(self.iBlocks);

   然后是16个字节的固件block包内容:

   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个字节通过write方式在OAD Image Block特征值上发送:

[BLEUtility writeNoResponseCharacteristic:self.d.p sCBUUID:sUUID cCBUUID:cUUID data:[NSData dataWithBytes:requestData length:2 + OAD_BLOCK_SIZE]];

   如果发送成功,那么就顺移相关的包序列号还有固件文件中的位移标志:

   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完全一样,我连注释都直接复制过来了J。

 

 

这样就是完整的OAD流程了。总结就是分三步走:使能OAD Image Identify和OAD Image Block 的notification,在OAD Image Identify上发送新固件版本(image header)进行确认,最后在OAD Image Block上按顺序把固件发送完,结束。