干貨|BLE入門談:從空中數(shù)據(jù)收發(fā)理解BLE(下)



點此查看>>從空中數(shù)據(jù)收發(fā)理解BLE(上)在使用帶BLE功能的MCU進行應用開發(fā)的時候,需要先熟悉BLE的API. 然而,各廠家的BLE API風格差異很大,要比不同器件平臺硬件驅動庫HAL之間的差別更大。底層無線電部分的硬件,各家自有獨立的設計(硬件寄存器也不一定開放),況且BLE協(xié)議棧有很大一部分是軟件實現(xiàn),它不光涉及無線電部分,還需要定時器和中斷管理、電源管理,甚至用到動態(tài)內存分配。于是要用BLE通信,協(xié)議棧部分幾十上百kB的代碼占用是很常見的(有的平臺把API實現(xiàn)放到ROM里能省部分),但難處在于不容易預測它的軟件行為,如一個API調用的執(zhí)行時間、什么時候會用回調函數(shù)、什么時候需要切換低功耗模式等等。每當接觸一個新的BLE MCU平臺時,對BLE API的學習時間要遠多于GPIO、UART這些基礎硬件。如果對BLE技術缺乏認識,學習這些API更容易一頭霧水。
  
BLE協(xié)議棧包含的內容太多了,一下弄明白太難。作為MCU應用開發(fā),又不一定需要了解那么多,只要能實現(xiàn)需要的數(shù)據(jù)通信就夠了。跟手機用BLE通信會麻煩一點,但如果是MCU和MCU之間通信呢?用過NRF24L01嗎?它的空中數(shù)據(jù)包和BLE的數(shù)據(jù)包很相似,因為協(xié)議簡單了,沒有BLE的Profile, Service那些概念,對MCU工程師友好很多。
 
BLE應用如果只做一個beacon的話,就是只管定期發(fā)出數(shù)據(jù),不需要建立連接的那種,其實是用不著協(xié)議棧的,甚至可能BLE API都不用到——這么說是不是一下子簡單了?比如,我只需要定時廣播一個溫度信息,真沒必要那么復雜啊。理解了BLE的數(shù)據(jù)包,就可以用不復雜的辦法來做。
  
這還有一個條件,就是能直接訪問MCU上的無線電部分硬件:得有一個開放的硬件環(huán)境,有手冊。本帖將用nRF51822來演示怎么直接操作硬件進行數(shù)據(jù)包的收發(fā)。nRF51822是比較老的BLE MCU了,很容易從拆機的手環(huán)類電路板上找到,它后一代的nRF52xxx系列性能更好,無線部分硬件變化不大。除了nRF51xxx之外,看手里的板子能不能直接操作無線電部分硬件,就查參考手冊看對應有沒有詳細的寄存器描述。剛結束的RSL10大賽用的板子也是可以玩的。

干貨|BLE入門談:從空中數(shù)據(jù)收發(fā)理解BLE(下)的圖1

下面是nRF51xxx手冊中RADIO部分的硬件結構框圖:

干貨|BLE入門談:從空中數(shù)據(jù)收發(fā)理解BLE(下)的圖2

接收和發(fā)送部分大致是獨立的,但不能同時工作,就是半雙工的意思。要發(fā)送的數(shù)據(jù)包存放在RAM中,硬件通過DMA自動讀取,然后會加上地址、CRC、同步頭等,并經(jīng)過whitening步驟,然后用GFSK調制發(fā)送出去。接收過程是類似的,硬件通過包頭檢測、地址匹配、CRC校驗過程篩選合法的數(shù)據(jù)包,由DMA寫到RAM中指定的地址。
  
nRF51822支持的數(shù)據(jù)包格式是這樣的:

干貨|BLE入門談:從空中數(shù)據(jù)收發(fā)理解BLE(下)的圖3

這和BLE spec中基礎數(shù)據(jù)包格式是兼容的(不然怎么支持BLE),所以我們將它配置成BLE的格式,就可以直接收發(fā)數(shù)據(jù)了。
  
Preamble部分是0/1交錯的同步碼,0xAA或者0x55,取決于地址部分的LSB(最先發(fā)送的那一bit),硬件負責。
  
地址部分,nRF51822的地址長度可以是3~5字節(jié),分被BASE和PREFIX兩部分。BLE的Access Address是4字節(jié),因此設置BASE長度為3字節(jié)。
  
接下來的S0、LENGTH、S1字段是可選的(長度可以設成0),如果用了,則需要看成BLE數(shù)據(jù)包的PDU的一部分。和后面的PAYLOAD部分一起組成PDU.
  
最后CRC部分由硬件負責,需要設置為24-bit, 要按照BLE要求設置。
 
先試驗能否從空中捕捉到BLE的數(shù)據(jù)包。需要提供給RADIO硬件的參數(shù)還有:(1)信道,(2)地址,(3)包長度。關于信道,為了捕捉advertising類型的包,可以設置成37、38、39信道當中的一個。設成其它信道捕捉連接數(shù)據(jù)包,除了要根據(jù)跳頻算法不斷更改信道外,還需要知道Access Address才可以。BLE 37、38、39信道使用固定的Access Address: 0x8E89BED6, 但建立連接后用的Access Address是主設備隨機生成的,在CONNECT_REQ包中提供給從設備。包長度在BLE包PDU的第2個字節(jié),也就是把上面的S0字段長度設置為1字節(jié)后,LENGTH字段就可以對應BLE PDU長度。nRF51822的RADIO使用LENGTH字段的信息(接收時來自空中數(shù)據(jù),發(fā)送時來自RAM數(shù)據(jù))來決定收發(fā)數(shù)據(jù)長度,不然就只能采用固定長度了。
  
為了接收37信道(中心頻率2402MHz)的advertising類型數(shù)據(jù)包,用這樣的配置:



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

  

啟動接收過程或發(fā)送過程要通過nRF51的task型寄存器。先看下RADIO部分的狀態(tài)轉移圖:

干貨|BLE入門談:從空中數(shù)據(jù)收發(fā)理解BLE(下)的圖4


在DISABLED狀態(tài)通過TXEN或RXEN task啟動硬件,到TXIDLE或RXDILE的準備狀態(tài),然后用START來進行一次傳輸。從接收切換到發(fā)送,以及從發(fā)送切換到接收,必須先轉回DISABLED狀態(tài)。
  
在接收狀態(tài)下,硬件會監(jiān)聽指定地址的數(shù)據(jù)包,接收完成后轉到RXIDLE狀態(tài),并產(chǎn)生END event.


干貨|BLE入門談:從空中數(shù)據(jù)收發(fā)理解BLE(下)的圖5


當收到END event時,表示收到了一個數(shù)據(jù)包(地址匹配有效),然后可以訪問CRCSTATUS寄存器判斷CRC校驗是否正確。若CRC有錯,可能是數(shù)據(jù)包被干擾破壞,或者格式不正確。接收數(shù)據(jù)包的S0、LENGTH、S1、PAYLOAD字段存放到RAM中,稍有變化的是LENGTH和S1字段都被擴展成了字節(jié)存儲。

  

我寫了一個循環(huán)來持續(xù)接收數(shù)據(jù)包,進行37信道的監(jiān)聽。使用雙緩沖區(qū)輪流存放收到的數(shù)據(jù)包,以便一邊解析數(shù)據(jù)一邊接收。



  
  
  • 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第一個字節(jié)的低4位,可以判斷數(shù)據(jù)包類型,然后識別余下數(shù)據(jù)。


  
  
  • 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數(shù)據(jù)的包,可以將地址、數(shù)據(jù)記錄下來,待收集一段時間后進行統(tǒng)計。


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

  • }

  • }

  • }

 

這樣就可以發(fā)現(xiàn)周圍的一部分BLE設備了。現(xiàn)在我的程序只是接收,沒有主動發(fā)起“掃描”。但是我的程序收到了許多主動掃描的包,表明附近有設備在持續(xù)進行瘋狂掃描……

干貨|BLE入門談:從空中數(shù)據(jù)收發(fā)理解BLE(下)的圖6

以上演示的是單向接收。單向發(fā)送也是容易實現(xiàn)的,只要填充一個advertising包,把要發(fā)送的數(shù)據(jù)包含在內,用TX模式發(fā)送出去就是了。發(fā)送的設置和接收基本一樣。


  
  
  • 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數(shù)據(jù),調用上面的函數(shù)。設備地址是04:8B:C6:29:5A:37, 寫在發(fā)送函數(shù)中了。


  
  
  • 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秒)發(fā)送一次,在手機上用BLE掃描工具可以發(fā)現(xiàn)這個設備。當然現(xiàn)在僅僅廣播了一個名稱而已,要添加自定的傳感器數(shù)據(jù)也很簡單,不過要注意31個字節(jié)長度的限制。

干貨|BLE入門談:從空中數(shù)據(jù)收發(fā)理解BLE(下)的圖7


以上只是最初級的直接操作硬件進行 BLE 數(shù)據(jù)包收發(fā)的演示,只用了單向數(shù)據(jù),因此簡單了。 如果要兩個設備有應答地交互,就需要發(fā)送方在數(shù)據(jù)包發(fā)送之后切換到接收狀態(tài),等待一小段時間看是否有應答。 BLE 的連接建立 起來后,主從雙方的收發(fā)方向就是在不斷地切換,如果要自己編程操作硬件實現(xiàn)這些,而不使用協(xié)議棧的 API,   理論上是可以做的,問題在于有沒有必要了。
  
利用BLE MCU的無線電硬件部分,做一些調試工具是可行而且有用的。還可以做自己的私有協(xié)議通訊,那樣就不能再叫做BLE了。
 
登錄后免費查看全文
立即登錄
App下載
技術鄰APP
工程師必備
  • 項目客服
  • 培訓客服
  • 平臺客服

TOP

2