作者: 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上按顺序把固件发送完,结束。