點此查看>>從空中數(shù)據(jù)收發(fā)理解BLE(上)在使用帶BLE功能的MCU進(jìn)行應(yīng)用開發(fā)的時候,需要先熟悉BLE的API. 然而,各廠家的BLE API風(fēng)格差異很大,要比不同器件平臺硬件驅(qū)動庫HAL之間的差別更大。底層無線電部分的硬件,各家自有獨立的設(shè)計(硬件寄存器也不一定開放),況且BLE協(xié)議棧有很大一部分是軟件實現(xiàn),它不光涉及無線電部分,還需要定時器和中斷管理、電源管理,甚至用到動態(tài)內(nèi)存分配。于是要用BLE通信,協(xié)議棧部分幾十上百kB的代碼占用是很常見的(有的平臺把API實現(xiàn)放到ROM里能省部分),但難處在于不容易預(yù)測它的軟件行為,如一個API調(diào)用的執(zhí)行時間、什么時候會用回調(diào)函數(shù)、什么時候需要切換低功耗模式等等。每當(dāng)接觸一個新的BLE MCU平臺時,對BLE API的學(xué)習(xí)時間要遠(yuǎn)多于GPIO、UART這些基礎(chǔ)硬件。如果對BLE技術(shù)缺乏認(rèn)識,學(xué)習(xí)這些API更容易一頭霧水。BLE協(xié)議棧包含的內(nèi)容太多了,一下弄明白太難。作為MCU應(yīng)用開發(fā),又不一定需要了解那么多,只要能實現(xiàn)需要的數(shù)據(jù)通信就夠了。跟手機(jī)用BLE通信會麻煩一點,但如果是MCU和MCU之間通信呢?用過NRF24L01嗎?它的空中數(shù)據(jù)包和BLE的數(shù)據(jù)包很相似,因為協(xié)議簡單了,沒有BLE的Profile, Service那些概念,對MCU工程師友好很多。BLE應(yīng)用如果只做一個beacon的話,就是只管定期發(fā)出數(shù)據(jù),不需要建立連接的那種,其實是用不著協(xié)議棧的,甚至可能BLE API都不用到——這么說是不是一下子簡單了?比如,我只需要定時廣播一個溫度信息,真沒必要那么復(fù)雜啊。理解了BLE的數(shù)據(jù)包,就可以用不復(fù)雜的辦法來做。這還有一個條件,就是能直接訪問MCU上的無線電部分硬件:得有一個開放的硬件環(huán)境,有手冊。本帖將用nRF51822來演示怎么直接操作硬件進(jìn)行數(shù)據(jù)包的收發(fā)。nRF51822是比較老的BLE MCU了,很容易從拆機(jī)的手環(huán)類電路板上找到,它后一代的nRF52xxx系列性能更好,無線部分硬件變化不大。除了nRF51xxx之外,看手里的板子能不能直接操作無線電部分硬件,就查參考手冊看對應(yīng)有沒有詳細(xì)的寄存器描述。剛結(jié)束的RSL10大賽用的板子也是可以玩的。下面是nRF51xxx手冊中RADIO部分的硬件結(jié)構(gòu)框圖:接收和發(fā)送部分大致是獨立的,但不能同時工作,就是半雙工的意思。要發(fā)送的數(shù)據(jù)包存放在RAM中,硬件通過DMA自動讀取,然后會加上地址、CRC、同步頭等,并經(jīng)過whitening步驟,然后用GFSK調(diào)制發(fā)送出去。接收過程是類似的,硬件通過包頭檢測、地址匹配、CRC校驗過程篩選合法的數(shù)據(jù)包,由DMA寫到RAM中指定的地址。nRF51822支持的數(shù)據(jù)包格式是這樣的:這和BLE spec中基礎(chǔ)數(shù)據(jù)包格式是兼容的(不然怎么支持BLE),所以我們將它配置成BLE的格式,就可以直接收發(fā)數(shù)據(jù)了。Preamble部分是0/1交錯的同步碼,0xAA或者0x55,取決于地址部分的LSB(最先發(fā)送的那一bit),硬件負(fù)責(zé)。地址部分,nRF51822的地址長度可以是3~5字節(jié),分被BASE和PREFIX兩部分。BLE的Access Address是4字節(jié),因此設(shè)置BASE長度為3字節(jié)。接下來的S0、LENGTH、S1字段是可選的(長度可以設(shè)成0),如果用了,則需要看成BLE數(shù)據(jù)包的PDU的一部分。和后面的PAYLOAD部分一起組成PDU.最后CRC部分由硬件負(fù)責(zé),需要設(shè)置為24-bit, 要按照BLE要求設(shè)置。先試驗?zāi)芊駨目罩胁蹲降紹LE的數(shù)據(jù)包。需要提供給RADIO硬件的參數(shù)還有:(1)信道,(2)地址,(3)包長度。關(guān)于信道,為了捕捉advertising類型的包,可以設(shè)置成37、38、39信道當(dāng)中的一個。設(shè)成其它信道捕捉連接數(shù)據(jù)包,除了要根據(jù)跳頻算法不斷更改信道外,還需要知道Access Address才可以。BLE 37、38、39信道使用固定的Access Address: 0x8E89BED6, 但建立連接后用的Access Address是主設(shè)備隨機(jī)生成的,在CONNECT_REQ包中提供給從設(shè)備。包長度在BLE包PDU的第2個字節(jié),也就是把上面的S0字段長度設(shè)置為1字節(jié)后,LENGTH字段就可以對應(yīng)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)轉(zhuǎn)移圖: 在DISABLED狀態(tài)通過TXEN或RXEN task啟動硬件,到TXIDLE或RXDILE的準(zhǔn)備狀態(tài),然后用START來進(jìn)行一次傳輸。從接收切換到發(fā)送,以及從發(fā)送切換到接收,必須先轉(zhuǎn)回DISABLED狀態(tài)。在接收狀態(tài)下,硬件會監(jiān)聽指定地址的數(shù)據(jù)包,接收完成后轉(zhuǎn)到RXIDLE狀態(tài),并產(chǎn)生END event.
當(dāng)收到END event時,表示收到了一個數(shù)據(jù)包(地址匹配有效),然后可以訪問CRCSTATUS寄存器判斷CRC校驗是否正確。若CRC有錯,可能是數(shù)據(jù)包被干擾破壞,或者格式不正確。接收數(shù)據(jù)包的S0、LENGTH、S1、PAYLOAD字段存放到RAM中,稍有變化的是LENGTH和S1字段都被擴(kuò)展成了字節(jié)存儲。 我寫了一個循環(huán)來持續(xù)接收數(shù)據(jù)包,進(jìn)行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ù)記錄下來,待收集一段時間后進(jìn)行統(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設(shè)備了。現(xiàn)在我的程序只是接收,沒有主動發(fā)起“掃描”。但是我的程序收到了許多主動掃描的包,表明附近有設(shè)備在持續(xù)進(jìn)行瘋狂掃描……以上演示的是單向接收。單向發(fā)送也是容易實現(xiàn)的,只要填充一個advertising包,把要發(fā)送的數(shù)據(jù)包含在內(nèi),用TX模式發(fā)送出去就是了。發(fā)送的設(shè)置和接收基本一樣。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ù),調(diào)用上面的函數(shù)。設(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ā)送一次,在手機(jī)上用BLE掃描工具可以發(fā)現(xiàn)這個設(shè)備。當(dāng)然現(xiàn)在僅僅廣播了一個名稱而已,要添加自定的傳感器數(shù)據(jù)也很簡單,不過要注意31個字節(jié)長度的限制。 以上只是最初級的直接操作硬件進(jìn)行BLE數(shù)據(jù)包收發(fā)的演示,只用了單向數(shù)據(jù),因此簡單了。如果要兩個設(shè)備有應(yīng)答地交互,就需要發(fā)送方在數(shù)據(jù)包發(fā)送之后切換到接收狀態(tài),等待一小段時間看是否有應(yīng)答。BLE的連接建立起來后,主從雙方的收發(fā)方向就是在不斷地切換,如果要自己編程操作硬件實現(xiàn)這些,而不使用協(xié)議棧的API, 理論上是可以做的,問題在于有沒有必要了。利用BLE MCU的無線電硬件部分,做一些調(diào)試工具是可行而且有用的。還可以做自己的私有協(xié)議通訊,那樣就不能再叫做BLE了。
|