SYD8801是一款低功耗高性能蓝牙低功耗SOC,集成了高性能2.4GHz射频收发机、32位ARM Cortex-M0处理器、128kB Flash存储器、以及丰富的数字接口。SYD8801片上集成了Balun无需阻抗匹配网络、高效率DCDC降压转换器,适合用于可穿戴、物联网设备等。具体可咨询:http://www.sydtek.com/
BLE_SendData函数gap_att_report结构体设置
BLE_SendData函数具体代码如下:
void BLE_SendData(uint8_t *buf, uint8_t len) { if(start_tx == 1) { struct gap_att_report report; report.primary = BLE_SERVICE_UUID_UART; report.uuid = BLE_SERVICE_UUID_UART_NOTIFICATION; report.hdl = 0x0001F; report.value = BLE_GATT_NOTIFICATION; GATTDataSend(BLE_GATT_NOTIFICATION, &report, len, buf) ; } }
其中最关键的设置就是gap_att_report 结构体,该结构体定义如下(请看:http://blog.csdn.net/chengdong1314/article/details/60870526):
struct gap_att_report { uint16_t primary; //主要服务的uuid uint16_t uuid; //上报信息的特性的uuid uint16_t hdl; //上报信息的特性的handle uint16_t config; uint16_t value; };
这里要注意的一点:report.hdl代表的是可以notify或者indicate的特性
蓝牙广播的实现
在ble_init函数中有调用:setup_adv_data();设置广播,其具体实现如下:
static void setup_adv_data() { struct gap_adv_params adv_params; adv_params.type = ADV_IND; adv_params.channel = 0x07; // advertising channel : 37 & 38 & 39 adv_params.interval = 0x6b; // advertising interval : 66.8ms (107 * 0.625ms) adv_params.timeout = 0x1e; // timeout : 30s SetAdvParams(&adv_params); ADV_DATA[0x02] = 0x05; // LE Limited Discoverable Mode & BR/EDR Not Supported SetAdvData(ADV_DATA, ADV_DATA_SZ, SCAN_DATA, SCAN_DATA_SZ); }
注意:这里设置了广播结构体:gap_adv_params,该结构体定义了广播的间隔与超时时间‘’但是在SYD8801中这些设置是没有使用的,主要原因是在广播发送数据包出去的时候底层硬件没有关掉RF,所以要应用层来关掉RF,但是连接的时候底层硬件已经有做了在发送数据包之后立即关掉RF的功能,所以在连接的时候不需要应用层去关掉R,所以SYD8801的广播机制如下:
1.在程序初始化的时候开始广播,并且设置一个0.5s的定时器:
ble_init(); timer_0_enable(19200, timer0_adv_callback); StartAdv();
2.当rf把广播包发送出去的时候协议栈回调GAP_EVT_ADV_END事件,这时候MCU设置停止RF模块:
void ble_evt_callback(struct gap_ble_evt *p_evt) { if(p_evt->evt_code == GAP_EVT_ADV_END) { RFSleep(); }
RFSleep();函数使射频模块进入休眠状态!
3.当0.5s定时器中断发生的时候唤醒RF再次初始化协议栈并且开始广播:
static void timer0_adv_callback() { RFWakeup(); ble_init(); StartAdv(); }
其中RFWakeup();函数将唤醒射频模块!
这样一来只要设置定时器的周期就能够设置广播的间隔!当然因为定时器回调函数是用户自己写的代码,什么时候广播就由用户决定!
蓝牙广播数组ADV_DATA的定义:
该数组的定义符合蓝牙规范中提及的《EXTENDED INQUIRY RESPONSE DATA FORMAT》定义(具体请看:http://blog.csdn.net/chengdong1314/article/details/55051653中的《EXTENDED INQUIRY RESPONSE DATA FORMAT》章节):
下面是SYD8801手环方案的广播数据数组定义:
注意:该数组的最大长度必须小于31(原因请看:http://blog.csdn.net/chengdong1314/article/details/55051653)如果想要在数组长度已经大于31的情况下增加蓝牙广播名称的长度就必须舍弃前面的某些数据类型,按照蓝牙规范,“0x19”和“0x03”以及“0XFF”是可选的参数,所以这里可以把蓝牙的广播参数删减成:
注意:IOS并不会显示蓝牙广播中底层的MAC地址,所以如果想在广播中获取到蓝牙设备的MAC地址,必须在制造商的类型中填入正确的MAC地址,也就是说如果想在IOS设备中从广播获取到蓝牙设备的MAC地址,必须增加上“0XFF”这个类型,最后数组如下:
具体要删减那个部分,请用户根据自己的需求酌情删减!
协议栈回调事件
enum _GAP_EVT_{ GAP_EVT_ADV_END = 0x0001, GAP_EVT_CONNECTED = 0x0002, GAP_EVT_DISCONNECTED = 0x0004, GAP_EVT_ENC_KEY = 0x0008, // 如果device需要配對,配對完後會收到此事件,協議棧會將配對相關key傳給上層知道 (可不處理) GAP_EVT_PASSKEY_REQ = 0x0010, GAP_EVT_SHOW_PASSKEY_REQ = 0x0020, GAP_EVT_CONNECTION_INTERVAL = 0x0040, //每次 connection interval 前的事件 (可不處理) GAP_EVT_CONNECTION_SLEEP = 0x0080, //每次 connection interval 後的事件,代表RF可進入睡眠模式 (由於RF是Auto-Sleep,所以可不處理) GAP_EVT_ATT_READ = 0x0100, GAP_EVT_ATT_WRITE = 0x0200, GAP_EVT_ATT_PREPARE_WRITE = 0x0400, GAP_EVT_ATT_EXECUTE_WRITE = 0x0800, GAP_EVT_ATT_HANDLE_CONFIRMATION = 0x1000, GAP_EVT_ATT_HANDLE_CONFIGURE = 0x2000, GAP_EVT_ENC_START = 0x4000, GAP_EVT_CONNECTION_UPDATE_RSP =0x8000, };
自动睡眠
在SYD8801的串口打印中有如下代码:
void dbg_printf(char *format,...) { uint8_t iWriteNum = 0; va_list ap; if(!format) return; *((uint8_t*)(0x50001000 + 0x24)) = 0x00; va_start(ap,format); iWriteNum = vsprintf((char *)s_formatBuffer,format,ap); va_end(ap); if(iWriteNum > MAX_FORMAT_BUFFER_SIZE) iWriteNum = MAX_FORMAT_BUFFER_SIZE; _uart_0_write(s_formatBuffer, iWriteNum); *((uint8_t*)(0x50001000 + 0x24)) = 0x01; }
其中*((uint8_t*)(0x50001000 + 0x24))寄存器置1的时候打开自动睡眠,清零的时候关闭自动睡眠,在串口发送数据之前关掉自动睡眠,发送数据之后打开自动睡眠,如果在自动睡眠的时候调用串口模块这时候串口打印将会出现乱码!
使用内部晶振
SYD8801的方案中,去掉外部的32KHz晶振转而使用芯片内部的32KHz的RC晶振可以减少产品成本,下面就是使用内部32KHz晶振的方法:
首先在《config.h》文件中打开“USER_32K_CLOCK_RCOSC”这个宏,如下图:
然后在“ble_init”函数的时钟选择中选择32KHz内部RC晶振:
注意这里必须调用“LPOCalibration”函数来进行内部32KHz RC晶振的校准,否则定时器将不能正常工作,蓝牙连接也会存在问题!
接下来在主函数中每3分钟(最多3分钟)必须进行内部32KHz RC晶振的校准,如下:
这样内部32KHz RC晶振就能够正常使用了!
注意:另外如果要更加准确的时钟精准度就要把ble_init函数里的关于时钟源选择的操作提到主函数来,具体请看:http://blog.csdn.net/chengdong1314/article/details/73929998的【时钟源准确度的优化】相关博文
这里上传本小节的使用代码:http://download.csdn.net/detail/chengdong1314/9906419
内部晶振校准花时间比较长的解决办法:
如果担当按照上面的方法校准内部晶振,虽然能够达到校准的目的,但是仔细测试的时候发现在校准的时候所花的时间很长,这里分处于广播状态和处于连接状态来分析!
蓝牙处于广播状态下:
如果程序使用的事SYD官方提供的广播方式,那么调用StartAdv函数之后程序惊人main函数的主循环执行,当调用LPOCalibration函数进行校准的时候蓝牙处于正在处于发送广播数据包的状态。如果蓝牙处于发送广播数据包的时候调用LPOCalibration校准,那么就要等到下次RFWakeup函数被调用的时候才能够校准成功,如果广播间隔是1S,那么校准的时间就长达1s钟。
所以如果处于广播状态下要进行内部RC的校准的话只能够等待广播数据包发送完成后再校准,实现步骤如下:
1.调用了开始广播StartAdv函数后设置start_adv变量为2,当广播数据包发送完成后设置start_adv变量为3:
2.在将要调用LPOCalibration函数进行校准的时候先要等待广播数据包发送完成,也就是start_adv变量为3,如下红色方框中的内容:
蓝牙处于连接状态下:
在连接的时候校准必须要等到接下来的第二个连接事件来临的时候才会完成,所以想要提高连接状态时校准的时间,在校准前要把latency关闭,这样能够更好的提高校准的时间,操作方法入上图的蓝色方框中的内容,这里在校准完成后再次把latency打开!
波形区别:
蓝牙处于 广播状态下当发送蓝牙广播数据包的时候校准的波形如下,当不处于发送数据包的时候校准的波形基本上看不出来了!
蓝牙处于连接状态下不关闭latency进行校准的波形如下左图,关闭latency进行校准的波形如下右图:
这里上传上面内容涉及的代码:http://download.csdn.net/download/chengdong1314/9966723
注意:上面涉及的功能必须建立在SYD官方2017年7月11日之后提供的《syd_ble_keil.lib》协议栈库的基础上
蓝牙状态机的说明
所谓的蓝牙状态机就是在蓝牙的状态发生变化的时候(比如由广播态变成了连接态)设置相应的标志位并且保存下这时候蓝牙的数据(比如接收到的数据),在主函数的主循环体中把这些状态通过串口或者OLED显示出来,这就是状态机制!
状态机的意义:
通过串口打印状态是一件十分耗时的事情,如果把十分耗时的事情放在蓝牙回调事件的处理函数中很有可能阻塞了蓝牙协议栈的运行,造成蓝牙出现了异常!所以这里要通过状态机制把该在蓝牙协议栈回调事件中的串口打印工作转移到主函数中来运行!还有一种更糟糕的情况串口会造成死机,请看:http://write.blog.csdn.net/postlist?ticket=ST-238386-cKTLPedZte23nc2Fg6Zw-passport.csdn.net博客中串口会造成死机的章节
状态机的使能:
打开《config.h》文件中的“USER_MARCHE_STATE”宏将使能状态机,如下:
状态机状态的定义:
这里状态机其实就是一个结构体,该结构体具体如下:
typedef struct{ uint8_t state; union { uint8_t datau8; uint16_t uuid; uint16_t hdl; gap_att_array att_evt; }data; } MARCHE_STATE;
其中的“state”代表着当前程序所处的状态,状态的具体定义如下:
注意该字节的最高位代表着蓝牙的状态是否有变化!
状态机数据的定义:
蓝牙状态机中的数据在每一个状态下都有可能不同,如下写状态下数据就代表着对等设备对该蓝牙写的内容,但是断线状态下,数据却是代表着断线的原因,当然有一些状态是没有数据的,所以状态机的数据其实是一个联合体:
状态机的声明(设置状态标志位和数据的方法):
在蓝牙的状态发生变化的时候只要正确的设置状态机结构体“march_state”中的状态标志位“state”即可,比如在蓝牙连接上的时候这样设置标志位:
在对等设备对该蓝牙进行写的时候这样设置标志位和数据:
蓝牙状态机的显示和打印:
这里在主函数的主循环中判断状态机标志位的最高位是否为1就知道了状态机的状态是否改变了,再根据标志位的剩下七位来判断当前蓝牙的状态,这里在主循环中调用“march_state_process”函数来处理状态机,如下:
该函数的源码如下:
#ifdef USER_MARCHE_STATE void march_state_process(void){ if(march_state.state & 0x80){ march_state.state &=0x7f; led_turn(LED1); // ·×ªLED1 ָʾÀ¶ÑÀ״̬·¢ÉúÁ˱仯 switch(march_state.state){ case 1: #ifdef _DEBUG_ DBGPRINTF(("GAP_EVT_CONNECTED")); #endif oled_8x16str(0,2,"CONNECTED "); break; case 2: #ifdef _DEBUG_ DBGPRINTF(("GAP_EVT_DISCONNECTED(x)\r\n",march_state.data.datau8)); #endif oled_8x16str(0,2,"DISCONNECTED "); break; // case 3: // #ifdef _DEBUG_ // DBGPRINTF(("GAP_EVT_ATT_HANDLE_CONFIGURE uuid:(x)\r\n",march_state.data.uuid)); // #endif // break; // case 4: // #ifdef _DEBUG_ // DBGPRINTF(("GAP_EVT_ATT_WRITE uuid:(x)\r\n",march_state.data.uuid)); // #endif // break; // case 5: // #ifdef _DEBUG_ // DBGPRINTF(("GAP_EVT_ATT_READ uuid:(x)\r\n",march_state.data.uuid)); // #endif // break; // case 6: // #ifdef _DEBUG_ // DBGPRINTF(("GAP_EVT_ATT_HANDLE_CONFIRMATION uuid:(x)\r\n",march_state.data.uuid)); // #endif // break; case 7: #ifdef _DEBUG_ DBGPRINTF(("ancs_find_ervice\r\n")); #endif oled_8x16str(0,2,"Security end "); break; // case 8: // #ifdef _DEBUG_ // DBGPRINTF(("GAP_EVT_ENC_START\r\n")); // #endif // break; case 9: #ifdef _DEBUG_ DBGPRINTF(("update rsp:x\r\n",march_state.data.datau8)); #endif break; case 10: #ifdef _DEBUG_ dbg_printf("ancs end\r\n"); #endif oled_8x16str(0,2,"ancs end "); break; case 11: #ifdef _DEBUG_ dbg_printf("not ancs\r\n"); #endif oled_8x16str(0,2,"not ancs "); break; case 12: #ifdef _DEBUG_ dbg_printf("not band\r\n"); #endif oled_8x16str(0,2,"not band "); break; case 13: #ifdef _DEBUG_ dbg_printf("Security start\r\n"); #endif oled_8x16str(0,2,"Security start "); break; case 14: #ifdef _DEBUG_ dbg_hexdump("msg:\r\n",march_state.data.att_evt.data,march_state.data.att_evt.sz); // dbg_printf("c:%x l:%x\r\n",march_state.data.att_evt.data[0],march_state.data.att_evt.data[1]); #endif oled_8x16str(0,2,"Writing "); oled_hexdump(0,4,"msg:",march_state.data.att_evt.data,march_state.data.att_evt.sz); break; default: oled_8x16str(0,2,"advertising "); break; } } } #endif
这里就根据状态机中的状态标志位来进行不一样的显示!
这里上传本小节的使用代码:http://download.csdn.net/detail/chengdong1314/9865161
断线时重新配置IO口
SYD8801的程序在蓝牙断线的时候协议栈底层硬件会把所有的GPIO的配置设置成默认的配置(gpio输出配置),但是这个配置可能不是程序所需要的,所以这里在断线的时候代码必须调用gpio_init函数再次进行所有gpio口的配置。如果代码中还有其他的地方重新配置了GPIO口,那么在断线的时候必须要重新进行配置,比如gsensor模块中会把I2C模块相关的gpio口配置成I2C模式,那么在断线的时候也必须要把该IO口配置成GPIO模式,如下示意代码: