⼲货|BLE⼊门谈:从空中数据收发理解BLE(下)
点此查看>>从空中数据收发理解BLE(上)在使⽤带BLE功能的MCU进⾏应⽤开发的时候,需
要先熟悉BLE的API. 然⽽,各⼚家的BLE API风格差异很⼤,要⽐不同器件平台硬件驱动库HAL
之间的差别更⼤。底层⽆线电部分的硬件,各家⾃有独⽴的设计(硬件寄存器也不⼀定开
放),况且BLE协议栈有很⼤⼀部分是软件实现,它不光涉及⽆线电部分,还需要定时器和中
断管理、电源管理,甚⾄⽤到动态内存分配。于是要⽤BLE通信,协议栈部分⼏⼗上百kB的代
码占⽤是很常见的(有的平台把API实现放到ROM⾥能省部分),但难处在于不容易预测它的
软件⾏为,如⼀个API调⽤的执⾏时间、什么时候会⽤回调函数、什么时候需要切换低功耗模式
等等。每当接触⼀个新的BLE MCU平台时,对BLE API的学习时间要远多于GPIO、UART这些
基础硬件。如果对BLE技术缺乏认识,学习这些API更容易⼀头雾⽔。
BLE协议栈包含的内容太多了,⼀下弄明⽩太难。作为MCU应⽤开发,⼜不⼀定需要了解那么
多,只要能实现需要的数据通信就够了。跟⼿机⽤BLE通信会⿇烦⼀点,但如果是MCU和MCU
之间通信呢?⽤过NRF24L01吗?它的空中数据包和BLE的数据包很相似,因为协议简单了,没
有BLE的Profile, Service那些概念,对MCU⼯程师友好很多。
BLE应⽤如果只做⼀个beacon的话,就是只管定期发出数据,不需要建⽴连接的那种,其实是
⽤不着协议栈的,甚⾄可能BLE API都不⽤到——这么说是不是⼀下⼦简单了?⽐如,我只需要
定时⼴播⼀个温度信息,真没必要那么复杂啊。理解了BLE的数据包,就可以⽤不复杂的办法
来做。
这还有⼀个条件,就是能直接访问MCU上的⽆线电部分硬件:得有⼀个开放的硬件环境,有⼿
册。本帖将⽤nRF51822来演⽰怎么直接操作硬件进⾏数据包的收发。nRF51822是⽐较⽼的
BLE MCU了,很容易从拆机的⼿环类电路板上到,它后⼀代的nRF52xxx系列性能更好,⽆
线部分硬件变化不⼤。除了nRF51xxx之外,看⼿⾥的板⼦能不能直接操作⽆线电部分硬件,就
查参考⼿册看对应有没有详细的寄存器描述。刚结束的RSL10⼤赛⽤的板⼦也是可以玩的。
下⾯是nRF51xxx⼿册中RADIO部分的硬件结构框图:
接收和发送部分⼤致是独⽴的,但不能同时⼯作,就是半双⼯的意思。要发送的数据包存放在
RAM中,硬件通过DMA⾃动读取,然后会加上地址、CRC、同步头等,并经过whitening步
骤,然后⽤GFSK调制发送出去。接收过程是类似的,硬件通过包头检测、地址匹配、CRC校
验过程筛选合法的数据包,由DMA写到RAM中指定的地址。
nRF51822⽀持的数据包格式是这样的:
这和BLE spec中基础数据包格式是兼容的(不然怎么⽀持BLE),所以我们将它配置成BLE的
格式,就可以直接收发数据了。
Preamble部分是0/1交错的同步码,0xAA或者0x55,取决于地址部分的LSB(最先发送的那⼀
bit),硬件负责。
地址部分,nRF51822的地址长度可以是3~5字节,分被BASE和PREFIX两部分。BLE的Access Address是4字节,因此设置BASE长度为3字节。
接下来的S0、LENGTH、S1字段是可选的(长度可以设成0),如果⽤了,则需要看成BLE数据包的PDU的⼀部分。和后⾯的PAYLOAD部分⼀起组成PDU.
最后CRC部分由硬件负责,需要设置为24-bit, 要按照BLE要求设置。
先试验能否从空中捕捉到BLE的数据包。需要提供给RADIO硬件的参数还有:(1)信道,(2)地址,(3)包长度。关于信道,为了捕捉advertising类型的包,可以设置成37、38、39信道当中的⼀个。设成其它信道捕捉连接数据包,除了要根据跳频算法不断更改信道外,还需要知道Access Address才可以。BLE 37、38、39信道使⽤固定的Access Address: 0x8E89BED6, 但建⽴连接后⽤的Access Address是主设备随机⽣成的,在CONNECT_REQ包中提供给从设备。包长度在BLE包PDU的第2个字节,也就是把上⾯的S0字段长度设置为1字节后,LENGTH 字段就可以对应BLE PDU长度。nRF51822的RADIO使⽤LENGTH字段的信息(接收时来⾃空中数据,发送时来⾃RAM数据)来决定收发数据长度,不然就只能采⽤固定长度了。
为了接收37信道(中⼼频率2402MHz)的advertising类型数据包,⽤这样的配置:
NRF_RADIO->RXADDRESSES = 1; // enable address 0NRF_RADIO->FREQUENCY = 2; // 2402MHz, CH37NRF_RADIO->DATAWHITEIV = 37;NRF_RADIO->MODE =
(RADIO_MODE_MODE_Ble_1Mbit << RADIO_MODE_MODE_Pos);NRF_RADIO->PREFIX0 = 0x8E;NRF_RADIO->BASE0 = 0x89BED600;// LFLEN=6 bits, S0LEN=1Byte,
S1LEN=2bitNRF_RADIO->PCNF0 = 0x00020106;// STATLEN=6, MAXLEN=37, BALEN=3, ENDIAN=0 (little), WHITEEN=1NRF_RADIO->PCNF1 = 0x02030025;NRF_RADIO-
>CRCCNF = 0x103; // only PDU, 3 octetsNRF_RADIO->CRCINIT = 0x555555; // for advertising packetNRF_RADIO->CRCPOLY = 0x100065b;// set receive bufferNRF_RADIO->PACKETPTR = (uint32_t)pkt_buf;
启动接收过程或发送过程要通过nRF51的task型寄存器。先看下RADIO部分的状态转移图:
在DISABLED状态通过TXEN或RXEN task启动硬件,到TXIDLE或RXDILE的准备状态,然后⽤START来进⾏⼀次传输。从接收切换到发送,以及从发送切换到接收,必须先转回DISABLED状态。
在接收状态下,硬件会监听指定地址的数据包,接收完成后转到RXIDLE状态,并产⽣END event.
当收到END event时,表⽰收到了⼀个数据包(地址匹配有效),然后可以访问CRCSTATUS 寄存器判断CRC校验是否正确。若CRC有错,可能是数据包被⼲扰破坏,或者格式不正确。接收数据包的S0、LENGTH、S1、PAYLOAD字段存放到RAM中,稍有变化的是LENGTH和S1字段都被扩展成了字节存储。
我写了⼀个循环来持续接收数据包,进⾏37信道的监听。使⽤双缓冲区轮流存放收到的数据包,以便⼀边解析数据⼀边接收。
for(;;){NRF_RADIO->PACKETPTR = (uint32_t)pkt_buf1;NRF_RADIO->EVENTS_END =
0;NRF_RADIO->TASKS_START =
1;if(crcok2)show_pkt(pkt_buf2);elseuart_wstr(".");if(NRF_RADIO-
>EVENTS_END)uart_wstr("!");while(! NRF_RADIO->EVENTS_END){}crcok1=NRF_RADIO->CRCSTATUS;NRF_RADIO->CRCINIT = 0x555555;// for advertising packetNRF_RADIO-
>PACKETPTR = (uint32_t)pkt_buf2;NRF_RADIO->EVENTS_END = 0;NRF_RADIO-
>TASKS_START = 1;if(crcok1)show_pkt(pkt_buf1);elseuart_wstr(".");if(NRF_RADIO-
>EVENTS_END)uart_wstr("!");while(! NRF_RADIO->EVENTS_END){}crcok2=NRF_RADIO->CRCSTATUS;  }
通过对PDU第⼀个字节的低4位,可以判断数据包类型,然后识别余下数据。
static inline void show_pkt(volatile uint8_t *buf){switch(buf[0]&0xF){case 6: //
ADV_SCAN_INDuart_wstr("s");add_log(buf);break;case 0: //
htmlradio传输数据
ADV_INDuart_wstr("A");add_log(buf);break;case 2: //
ADV_NONCONN_INDuart_wstr("n");add_log(buf);break;case 4: //
SCAN_RESPuart_wstr("R");break;case 1: // ADV_DIRECT_INDuart_wstr("i");break;case 3: // SCAN_REQuart_wstr("+");break;case 5: //
CONN_REQuart_wstr("C");break;default:uart_wstr("?");break;}}
如果是包含advertising数据的包,可以将地址、数据记录下来,待收集⼀段时间后进⾏统计。
void add_log(uint8_t *buf){int i;for(i=0;i<32;i++){if(unt)// not
blank{if(memcmp(adv_log.addr, buf+3, 6)==0 && pe==buf[0])//
match{unt++;return;}}else// add entry{memcpy(adv_log.addr, buf+3, 6);pe = buf[0];adv_log.len = buf[1]-6;memcpy(adv_log.payload, buf+9,
adv_log.len);unt=1;return;}}}
这样就可以发现周围的⼀部分BLE设备了。现在我的程序只是接收,没有主动发起“扫描”。但是我的程序收到了许多主动扫描的包,表明附近有设备在持续进⾏疯狂扫描……
以上演⽰的是单向接收。单向发送也是容易实现的,只要填充⼀个advertising包,把要发送的数据包含在内,⽤TX模式发送出去就是了。发送的设置和接收基本⼀样。
void radio_adv_tx(uint8_t *pdu, uint8_t len){uint8_t txpkt[40];NRF_RADIO->EVENTS_READY = 0;NRF_RADIO->TASKS_TXEN = 1;while (NRF_RADIO->EVENTS_READY == 0);// now in TXIDLE statetxpkt[0]=0x42;// private TX address, non-
connectableif(len>31)len=31;txpkt[1]=len+6;txpkt[2]=0;txpkt[3]=0x37; txpkt[4]=0x5A;
txpkt[5]=0x29;txpkt[6]=0xC6; txpkt[7]=0x8B; txpkt[8]=0x04;memcpy(txpkt+9, pdu,
len);NRF_RADIO->PACKETPTR = (uint32_t)txpkt;NRF_RADIO->EVENTS_END =
0;NRF_RADIO->TASKS_START = 1;while(! NRF_RADIO->EVENTS_END){}}
使⽤⼀个包含名称的advertising数据,调⽤上⾯的函数。设备地址是04:8B:C6:29:5A:37, 写在发送函数中了。
const uint8_t dummy_adv[]={0x02,0x01,0x06,// flags15,0x09,'A','D','V','_','D','e','m','o','
','5','1','8','2','2'};radio_adv_tx(dummy_adv,sizeof(dummy_adv));
定期(⽐如1秒)发送⼀次,在⼿机上⽤BLE扫描⼯具可以发现这个设备。当然现在仅仅⼴播了⼀个名称⽽已,要添加⾃定的传感器数据也很简单,不过要注意31个字节长度的限制。
以上只是最初级的直接操作硬件进⾏BLE数据包收发的演⽰,只⽤了单向数据,因此简单了。如果要两个设备有应答地交互,就需要发送⽅在数据包发送之后切换到接收状态,等待⼀⼩段
时间看是否有应答。BLE的连接建⽴起来后,主从双⽅的收发⽅向就是在不断地切换,如果要⾃⼰编程操作硬件实现这些,⽽不使⽤协议栈的API, 理论上是可以做的,问题在于有没有必要了。
利⽤BLE MCU的⽆线电硬件部分,做⼀些调试⼯具是可⾏⽽且有⽤的。还可以做⾃⼰的私有协议通讯,那样就不能再叫做BLE了。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。