干货|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 0

  • NRF_RADIO->FREQUENCY = 2; // 2402MHz, CH37

  • NRF_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=2bit

  • NRF_RADIO->PCNF0 = 0x00020106;

  • // STATLEN=6, MAXLEN=37, BALEN=3, ENDIAN=0 (little), WHITEEN=1

  • NRF_RADIO->PCNF1 = 0x02030025;

  • NRF_RADIO->CRCCNF = 0x103; // only PDU, 3 octets

  • NRF_RADIO->CRCINIT = 0x555555; // for advertising packet

  • NRF_RADIO->CRCPOLY = 0x100065b;

  • // set receive buffer

  • NRF_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);

  • else

  • uart_wstr(".");

  • if(NRF_RADIO->EVENTS_END)

  • uart_wstr("!");

  • while(! NRF_RADIO->EVENTS_END)

  • {}

  • crcok1=NRF_RADIO->CRCSTATUS;

  • NRF_RADIO->CRCINIT = 0x555555;// for advertising packet

  • NRF_RADIO->PACKETPTR = (uint32_t)pkt_buf2;

  • NRF_RADIO->EVENTS_END = 0;

  • NRF_RADIO->TASKS_START = 1;

  • if(crcok1)

  • show_pkt(pkt_buf1);

  • else

  • uart_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_IND

  • uart_wstr("s");

  • add_log(buf);

  • break;

  • case 0: // ADV_IND

  • uart_wstr("A");

  • add_log(buf);

  • break;

  • case 2: // ADV_NONCONN_IND

  • uart_wstr("n");

  • add_log(buf);

  • break;

  • case 4: // SCAN_RESP

  • uart_wstr("R");

  • break;

  • case 1: // ADV_DIRECT_IND

  • uart_wstr("i");

  • break;

  • case 3: // SCAN_REQ

  • uart_wstr("+");

  • break;

  • case 5: // CONN_REQ

  • uart_wstr("C");

  • break;

  • default:

  • uart_wstr("?");

  • break;

  • }

  • }

  

如果是包含advertising数据的包,可以将地址、数据记录下来,待收集一段时间后进行统计。

  • void add_log(uint8_t *buf)

  • {

  • int i;

  • for(i=0;i<32;i++)

  • {

  • if(adv_log.count)// not blank

  • {

  • if(memcmp(adv_log.addr, buf+3, 6)==0 && adv_log.type==buf[0])// match

  • {

  • adv_log.count++;

  • return;

  • }

  • }

  • else// add entry

  • {

  • memcpy(adv_log.addr, buf+3, 6);

  • adv_log.type = buf[0];

  • adv_log.len = buf[1]-6;

  • memcpy(adv_log.payload, buf+9, adv_log.len);

  • adv_log.count=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 state

  • txpkt[0]=0x42;// private TX address, non-connectable

  • if(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,// flags

  • 15,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了。
(0)

相关推荐