NXP

BLE核心模块FS-QN9021模块开发-linux版

2019-07-12 11:50发布

这段时间又参与了一个新的小项目,简单概括为蓝牙、智能、家居吧,虽然时间有点紧,还是希望能把这一些东西记录下来。
####BLE 什么是BLE?参考这篇文章做如下总结。 
中文名称为蓝牙低功耗。主要特点为低成本、超低功耗、短距离、标准接口和可互操作性强,并且工作在免许可的2.4GHz ISM射频段,需要支持蓝牙4.0(系统为Android4.3及以上)的主机设备才能与其连接。 目前生产BLE芯片的厂家主要有CSR、TI、Nodic和NXP(QN902x),各个厂家芯片对比如下图 s 从如上图对比可以看出,NXP的QN902x在功耗方面比CSR和TI更省电,在接收灵敏度和模式方面比Nodic的胜一筹,它的从设备相比其它几家可以连接的更多,共有8个,这也算是蓝牙4.0的一大特 {MOD}吧,并且NXP的芯片已经过了MFI认证,直接能与苹果设备相连接,因为这种认证也是挺贵的。 因为BLE的低功耗、低成本及强大的处理能力,并且随着iPhone的设备支持蓝牙4.0,BLE的终端设备在我们的生活当中将会越来越多,在未来将会有爆发式增长。 QN902X是一款内核为M0的蓝牙BLE SOC芯片,其SDK对蓝牙BLE的profile都有实现,并提供源码,SDK也提供很多工具以便使用芯片,比如引脚配置,NVDS读写,串口USB Dongle(配合上位机可以调试各profile,没有手机也可以调试),ISP下载等,但QN902X不提供数据手册,所有的外设操作都以库的方式提供,SDK说明比较全面但全是英文的。 与NODRDIC的51822和TI的CC2540不同QN902X的架构是M0+ROM+FLASH+SRAM的方式,其中ROM放的是蓝牙协议和内部一个小的调度核,FLASH放的是用户程序和数据,RAM用于跑程序。其中ROM:96K,SRAM:64K,FLASH:64K/128K。因为QN902X程序是跑到SRAM中,所以它的深度睡眠电流比较大些。
####环境搭建 老生长谈,开发一款产品,第一步当然是搭建开发环境咯。 #####wine安装 由这里可知,keil不支持linux环境,所以必须自己想办法了。这是在linux环境下运行windows的程序,本身使用windows的请自动忽略这一步。具体请看官网/博文~ $ sudo add-apt-repository ppa:ubuntu-wine/ppa ~ $ sudo apt-get update # => 我的是64为的系统,所有安装64位版 ~ $ sudo apt-get install wine1.8-amd64 # => 配置 ~ $ winecfg # => 中文路径:~/ .wine/drive_c/windows/Fonts/ ~ $ winetricks corefonts #####keil安装 首先得下载keil,可以自己去官网下载最新的,也可以直接点击mdk5.17下载地址,参考这篇博文并实践做如下记录。 # => 进入下载目录安装 ~ $ wine mdk517.exe
提示:卸载程序可以用wine uninstaller 
如编译有限制,下载破解注册机Keygen:http://pan.baidu.com/s/1hqGSRqs
安装完成后,会弹出来一个安装器件(pack installer)如下的界面,也就是说,你要用它来开发哪个芯片(此项目可以忽略,后面步骤导入DB)。 packin
或者打开keil界面会看到如下图标 packicon
要开发哪一款芯片,点击install即可,或者先网页端下载好在导入,具体参考上面提供的博文地址。 #####MCU DB库安装 下载Quintic最新的SDKQBlue1.3.7,安装: ~ $ wine QBlue-1.3.7.exe 会弹出窗口如下,点击安装即可,或者打开桌面QBlue里的QN9020DevDBforIDE工具安装。 sdk #####keil使用 首先获取开源代码 ~ $ cd ~ $ git clone git@bitbucket.org:T-Firefly/fireble.git 桌面打开keil,假如我们希望开始proxr工程:
  1. 在keil的Project菜单中选择Open Project…
  2. 弹出文件选择框中,打开/home/xxx/fireble/BLE/prj_proxr/keil/proxr.uvproj工程文件(linux可能在z磁盘里)
  3. 配置DB如下,如果没发现库,请重启软件 dbset
  4. 编译代码,成功后如下,具体配置选项请参考这里 compile
  5. 下载程序,下载的时候需要按复位键,如下载出错,可先下载到SRAM。 dl
Tip:在ubuntu 上串口识别为ttyS0或ttyUSB0之类,在wine上识别不到,可用:
# =>将其该为小写的com1,如果不行,将其改为大写的COM1 ~ $ sudo ln -s /dev/ttyUSB0 ~/.wine/dosdevices/com1 ~ $ sudo usermod -a -G dialout $USER ~ $ sudo chmod 777 ~/.wine/dosdevices/com1
即可在wine的应用程序使用串口

####项目实践 #####点亮LED 对于新的芯片与开发板,从LED实验开始。首先下载该开发板的原理图,对该LED部分的电路进行分析。 led 本程序非常简单,复制gpio demo代码 gpio 实现让开发板D1灯闪烁如下: /* Set pin D1/P2_7 */ gpio_set_direction_field(GPIO_P27, (uint32_t)GPIO_OUTPUT); while(1) { gpio_write_pin_field(GPIO_P27, GPIO_LOW); delay(100000); gpio_write_pin_field(GPIO_P27, GPIO_HIGH); delay(100000); } 程序流程:系统初始化–>GPIO配置–>各驱动模块初始化–>主循环实现功能 
效果如下: led #####UART实验 串口通信可以用来打印数据,调试程序,有必要实验一下。 
同样复制uart demo代码 
改程序如下: //Print out "Hello NXP! " thought uart. uart_printf(QN_UART0, (uint8_t *)"Hello NXP! "); uart_printf(QN_UART0, (uint8_t *)"This is nephen's test! "); 波特率设置为115200,现象如下: uart_tst #####PWM实验 由于这个项目会控制到电机什么的,所以pwm少不了,这个实验是通过pwm控制陶瓷蜂鸣器报警和呼吸灯。通过PWM方式调节脉冲频率和占空比,变换LED亮度,渐变亮度实现呼吸灯效果。FireBLE板载的贴片蜂鸣器是压电式陶瓷蜂鸣器,压电陶瓷蜂鸣器要想响起来,需要满足四个条件:多谐振荡器、压电蜂鸣片、阻抗匹配器及共鸣箱,压电蜂鸣片由锆钛酸铅或铌镁酸铅压电陶瓷材料制成,在陶瓷片的两面镀上银电极,经极化和老化处理后,再与黄铜片或不锈钢片粘在一起。板载的蜂鸣器缺少的只是多谢振荡器,这里我们用PWM来代替,输出1.5KHz-2.5KHz的方波信号推动压电蜂鸣片发声,频率在1.5KHz-2.5KHz才会响,太高太低都不响,直接加3.3V更不会响,直接加3.3V响的那是电磁式蜂鸣器,有源和无源。 
首先看下电路连接 buzz 同样复制pwm代码,改写如下: int main (void) { SystemInit(); pwm_init(PWM_CH0); pwm_io_config(); //P2.7 will output pwm wave with period for 1000us and pulse for 400us pwm_config(PWM_CH0, PWM_PSCAL_DIV, PWM_COUNT_US(1000, PWM_PSCAL_DIV), PWM_COUNT_US(500, PWM_PSCAL_DIV)); pwm_enable(PWM_CH0, MASK_ENABLE); pwm_io_dis_config(); pwm_init(PWM_CH1); pwm_io_config(); //P2.6 will output pwm wave with period for 1000us and pulse for 500us pwm_config(PWM_CH1, 119, PWM_COUNT_US(1000, 119), PWM_COUNT_US(500, 119)); pwm_enable(PWM_CH1, MASK_ENABLE); while (1) /* Loop forever */ { int i; for(i=0;i<=1000;i++) { pwm_config(PWM_CH1, 119, PWM_COUNT_US(1000-i, 119), PWM_COUNT_US(500, 119)); if(i%2) pwm_config(PWM_CH0, PWM_PSCAL_DIV, PWM_COUNT_US(1000-i, PWM_PSCAL_DIV), PWM_COUNT_US(500, PWM_PSCAL_DIV)); else pwm_config(PWM_CH0, PWM_PSCAL_DIV, PWM_COUNT_US(i, PWM_PSCAL_DIV), PWM_COUNT_US(500, PWM_PSCAL_DIV)); delay(2000); } } } 呼吸灯效果: breath #####按键广播 看到有些人对买的fireBLE按键广播不懂,其实刚开始我也是这样的,摸不着头脑,现在至少不会太迷糊,就给大家记录下吧。 
首先按键按下属于gpio中断,找到中断初始化函数:SystemInit(); ——》 
gpio_init(gpio_interrupt_callback); ——》 
void gpio_interrupt_callback(enum gpio_pin pin) ——》 
void usr_button1_cb(void) ——》 
ke_evt_set(1UL « EVENT_BUTTON1_PRESS_ID); 找到EVENT_BUTTON1_PRESS_ID对应的事件,搜索 EVENT_BUTTON1_PRESS_ID,找到void usr_init(void)里的if(KE_EVENT_OK != ke_evt_callback_set(EVENT_BUTTON1_PRESS_ID, app_event_button1_press_handler))可知为app_event_button1_press_handler ——》 
ke_timer_set(APP_KEY_SCAN_TIMER,TASK_APP,2); 同样通过搜索APP_KEY_SCAN_TIMER可知定时器事件为app_key_scan_timer_handler ——》 
app_key_scan_timer_handler,这里进行adc采集,完成后由adc_read(&read_cfg, adc_key_value, KEY_SAMPLE_NUMBER, adc_test_cb);可知进入adc_test_cb,在这里设置了EVENT_ADC_KEY_SAMPLE_CMP_ID,搜索找到对应事件app_event_adc_key_sample_cmp_handler ——》 
app_event_adc_key_sample_cmp_handler,判断按键朝哪个方向按,定时回调 ——》 
app_key_process_timer_handler,如下: int app_key_process_timer_handler(ke_msg_id_t const msgid, void const *param, ke_task_id_t const dest_id, ke_task_id_t const src_id) { ke_evt_clear(1UL << EVENT_ADC_KEY_SAMPLE_CMP_ID); switch (key_value0) { case key_up: if (APP_IDLE == ke_state_get(TASK_APP)) { // start adv //QPRINTF("you press up key !"); app_gap_adv_start_req(GAP_GEN_DISCOVERABLE | GAP_UND_CONNECTABLE, app_env.adv_data, app_set_adv_data(GAP_GEN_DISCOVERABLE), app_env.scanrsp_data, app_set_scan_rsp_data(app_get_local_service_flag()), GAP_ADV_FAST_INTV1, GAP_ADV_FAST_INTV2); 这里一看就明白,向上按时,进入广播。其实在系统上电的时候,已经进行了一系列的gap建立连接的初始话,就差广播了,所以这里只要广播出去就能和别的蓝牙建立连接。 #####QTool使用及蓝牙了解 使用QBlueStudio中QTool工具进行蓝牙开发分析,可以方便的对各个蓝牙操作的过程进行细致的研究,并结合具体的源代码进行查看,能够更加深入的了解到蓝牙协议的实现过程,大部分的API接口在GAP和GATT。具体的使用文档请查看QBlueStudio里的Document。Linux里可以采取这种方法查看:地址见/home/username/.wine/drive_c/QBlue/QN9020/QBlue-1.3.7/Documents,然后使用evince命令打开。 在这之前,还需对蓝牙的一些术语做一个大概的了解,比如什么是master,slave,主机。客户端与服务器又是什么。GAP与GATT有什么区边。蓝牙各个协议层都有哪些分工。下面参考做一些归纳:
  1. BLE规范中定义了GAP(Generic Access Profile)和GATT(Generic Attribute)两个基本配置文件。 
    a.协议中的GAP层负责设备访问模式和进程,包括设备发现,建立连接,终止连接。初始化安全特性和设备配置。 
    b.GATT层用于已连接的蓝牙设备之间的数据通信。GATT通俗理解为用于主从机之间的客户端和服务器端的数据交互,以Attribute Table来体现。
  2. BLE低功耗蓝牙中有四种设备类型,Central主机,Peripheral从机,Observer观察者,Broadcaster广播者。通常Central主机,Peripheral从机一起使用,Observer观察者,Broadcaster广播者一起使用。Central和Peripheral连接交换数据,平时我们使用到的基本上是这种模式。而像多温度采集器,通常使用Observer和Broadcaster这种无连接形式。 
    主机和从机是这样开机工作的:从机开始广播,然后主机扫描广播的从机,当从机收到主机的扫描请求后,会向主机发送扫描回应数据。然后主机发起连接,然后开始通讯。所以从机需要设置广播内容和扫描回应内容。这方面的代码可以查看App_gap.c和App_gap_task.c。
    • profile:可以理解为一种规范,一个标准的通信协议,profile存在于从机中。蓝牙组织规定了一系列的标准profile,如防丢计、心率计。每个profile中包含多个service,每个service代表从机的一种能力。
    • Service:可以理解为一种服务,在BLE从机里,通过有多个服务,例如电量信息服务、系统信息服务等。每个service又包含多个characteristic特征值。每个具体的characteristic特征值才是BLE通讯的主体,比如当前电量是80%。所以会通过电量的characteristic特征值特征值保存在从机的profile里,这样主机就可以通过这个characteristic来读取80%这个数。
    • characteristic:characteristic特征值,BLE主从机均是通过characteristic来实现,可以理解为一个标签,通过这个标签可以获取或者写入想要的内容。
    • UUID:统一标识吗,我们刚提到的characteristic和service,都需要一个唯一的UUID来标识。
    每个从机都会有个叫做profile的东西存在,不管自定义的还是标准的profile,他们都是由一系列的Service组成,然后每个service又包含多个characteristic,主机和从机之间的通信,均是通过characteristic来实现。 
    BLE协议栈中传输数据分为两方面,一个GATT的client主动向service发送数据。另一个是GATT的service主动向client发送数据。即主从之前相互传数据。 
    主机向从机发送数据,使用GATT_Write 
    从机向主机发送数据,使用GATT_Notification
  3. GATT有Service和Client,Service作为服务器端,对GATT Client提供read/write接口,一般情况下,Central作为Client,Peripheral作为Service。所以主机会调用read/write来和作为Service端的Peripheral从机通讯。而Peripheral则通过notify的方式即调用GATT_Notifycation发起和主机通讯。
  4. 特征值声明值可以有五种属性:Read(可读) Write(可靠的可写,带响应) Write without resp(不可靠的可写,不带响应) Indicate(可靠通知,带响应) Notify(不可靠通知,不带响应)
更多请查看蓝牙设计问与答 /Android 蓝牙4.0 BLE 理解 BLE中主从机建立连接,到配对和绑定的过程如下图。  #####QPPS工程 在此之前,建议先了解一下Firefly的QPPS介绍/对QPPS profile中服务和特征实现的分析理解/对profile QPPS的分析理解。关于蓝牙的数据收发部分可以看Firefly的协议栈介绍。数据帧格式如下:
Tip:技术案例中串口透传案例,实现的是将蓝牙模组串口所接收到的数据透传到app,并且把app的数据通过蓝牙透传到串口输出,案例本身的实现是针对串口通信和蓝牙透传的结合。
  1. 其实BLE完成初始化的条件并不是进入main函数的while(1)中(系统在调度了几个消息之后才完成的初始化,前几次进入while(1)时初始化都是没完成的),真正完成初始化并且可以运行是在打印BLE is Ready这句话的地方。那个地方你可以看到只要开启QN_DEMO_AUTO宏定义就可以上电广播了。
  2. 设备名默认从NVDS中写入,但是软件也可以修改,可以用app_gap_set_devname_req修改设备名。
  3. 如果你非要用软件指定,参考广播内容设定,取消NVDS读取,直接利用app_gap_set_devname_req函数指定设备名,然后将QN_LOACL_NAME广播出去,那么设备名和广播中的设备名都会是QN_LOCAL_NAME了。 具体实施如下 修改自动广播,无需向上拨动按键: c #=> App_config.h中,取消如下的注释即可 #define QN_DEMO_AUTO 1 在广播之前改变设备的名字,注意app_set_adv_data函数 c //Set remote device name app_gap_set_devname_req("nephen",6); // Created DB should has been finished by each profile service, // Start Adv mode automatically here app_gap_adv_start_req(GAP_GEN_DISCOVERABLE|GAP_UND_CONNECTABLE, app_env.adv_data, app_set_adv_data(GAP_GEN_DISCOVERABLE), app_env.scanrsp_data, app_set_scan_rsp_data(app_get_local_service_flag()), GAP_ADV_FAST_INTV1, GAP_ADV_FAST_INTV2);
  4. 接收手机app中9600可写属性发送的数据。 
    由下面Qpps_task.c可知,当有数据到达蓝牙时,会触发gatt_write_cmd_ind_handler,同理,发送数据触发qpps_data_send_req_handler,得到通知触发gatt_notify_cmp_evt_handler,app层对应的api是app_qpps_data_send_cfm_handlerapp_qpps_data_ind_handlerapp_qpps_cfg_indntf_ind_handler,下文会提到,只会对这些做修改 c /// Connected State handler definition. const struct ke_msg_handler qpps_connected[] = { {QPPS_DATA_SEND_REQ, (ke_msg_func_t) qpps_data_send_req_handler}, {GATT_WRITE_CMD_IND, (ke_msg_func_t) gatt_write_cmd_ind_handler}, {GATT_NOTIFY_CMP_EVT, (ke_msg_func_t) gatt_notify_cmp_evt_handler}, }; 而在gatt_write_cmd_ind_handler函数里,会对QPPS_IDX_RX_DATA_VAL属性进行处理 ```c else if (param->handle == (qpps_env.shdl + QPPS_IDX_RX_DATA_VAL)) { if (param->length <= QPP_DATA_MAX_LEN) { //inform APP of configuration change struct qpps_data_val_ind * ind = KE_MSG_ALLOC_DYN(QPPS_DAVA_VAL_IND, qpps_env.appid, TASK_QPPS, qpps_data_val_ind, param->length); memcpy(&ind->conhdl, &(qpps_env.conhdl), sizeof(uint16_t)); //Send received data to app value ind->length = param->length; memcpy(ind->data, param->value, param->length); ke_msg_send(ind); } else { status = QPPS_ERR_RX_DATA_EXCEED_MAX_LENGTH; } } ``` 再由上面的QPPS_DAVA_VAL_IND可知会跳到下面这个函数,在这里对接收到的数据进行处理 ```c int app_qpps_data_ind_handler(ke_msg_id_t const msgid, struct qpps_data_val_ind *param, ke_task_id_t const dest_id, ke_task_id_t const src_id) { uint8_t i; if (param->length > 0) { QPRINTF(“len=%d, I%02X”, param->length, param->data[0]); QPRINTF(“ ”); QPRINTF(“the receive data is :”); for(i=0;ilength;i++) { QPRINTF("%x",param->data[i]); } } QPRINTF(" "); return (KE_MSG_CONSUMED); } ``` 当发送数据为29时,CuteCom蓝牙接收数据为 QN BLE is ready. Set device name complete Advertising start. Connection with 1FDA7E9F8E30 result is 0x0. LTK request indication idx is 0, auth_req is 0. Start encryption complete, idx 0, status 0, key_size 0, sec_prop 1, bonded 0. Slave update success. Update parameter complete, interval: 0xc, latency: 0x0, sup to: 0x12c. len=1, I29 the receive data is :29
  5. App收取蓝牙通知。通过点击app上5个特征的通知,能获取蓝牙模块的实时通知。Gatt层与通知相关的函数是qpps_data_send_req_handler,但我们只需要修改app开头的api即可完成相应的开发,现象及分析如下: 
    当点击app上的通知时,会进入如下的函数,为了调试将其调整为如下: ```c int app_qpps_cfg_indntf_ind_handler(ke_msg_id_t const msgid, struct qpps_cfg_indntf_ind *param, ke_task_id_t const dest_id, ke_task_id_t const src_id) { if (app_qpps_env->conhdl == param->conhdl) { QPRINTF(“enter app_qpps_cfg_indntf_ind_handler”); QPRINTF(“ ”); if (param->cfg_val == PRF_CLI_START_NTF) { QPRINTF(“enter PRF_CLI_START_NTF”); QPRINTF(“ ”); app_qpps_env->features |= (QPPS_VALUE_NTF_CFG « param->char_index); QPRINTF(“app_qpps_env->features is %d, param->char_index is %d”,app_qpps_env->features,param->char_index); QPRINTF(“ ”); // App send data if all of characteristic have been configured if (get_bit_num(app_qpps_env->features) == app_qpps_env->tx_char_num)//num is 5 { QPRINTF(“enter app_qpps_env->features”); QPRINTF(“ ”); app_qpps_env->char_status = app_qpps_env->features; app_test_send_data(app_qpps_env->tx_char_num - 1); } } else { app_qpps_env->features &= ~(QPPS_VALUE_NTF_CFG « param->char_index); app_qpps_env->char_status &= ~(QPPS_VALUE_NTF_CFG « param->char_index); } } return (KE_MSG_CONSUMED); } ``` 其中,param->char_index代表第几个可通知特征,app_qpps_env->tx_char_num为特征的数量5,只有当所有特征都进入通知状态时蓝牙模块才会发数据到手机app。 数据发送的过程见如下函数,这个函数中的数据val可以改为项目中的需求,如IO口状态 ```c static void app_test_send_data(uint8_t max) { uint8_t cnt; QPRINTF("enter app_test_send_data"); QPRINTF(" "); for (cnt = 0; (max != 0) && cnt < app_qpps_env->tx_char_num; cnt++) { if ((app_qpps_env->char_status >> cnt) & QPPS_VALUE_NTF_CFG) { static uint8_t val[] = {0, '0', '1', '2','3','4','5','6','7','8','9','8','7','6','5','4','3','2','1','0'}; // Increment the first byte for test val[0]++; max--; // Allow next notify until confirmation received in this characteristic app_qpps_env->char_status &= ~(QPPS_VALUE_NTF_CFG << cnt); app_qpps_data_send(app_qpps_env->conhdl, cnt, sizeof(val), val); } } } ``` 数据发送后,有一个发送确认函数app_qpps_data_send_cfm_handler,发送成功后手机上看到的现象是这样的
至此完成的功能为,实时反应蓝牙通知信息,app发数据控制蓝牙模块。
####QTool使用 QTool是一个运行在PC端的应用软件,允许用户启动两个BLE设备之间的连接,它能帮助用户分析BLE蓝牙。它是通过串口与BLE设备进行通信,通过ACI命令扮演网络处理器的作用。 打开pdf使用文档 ~ $ cd /home/nephne/.wine/drive_c/QBlue/QN9020/QBlue-1.3.7/Documents ~ $ evince QBlue ISP Studio Manual v1.0.pdf 插入USB连接蓝牙模块,下载np_controller_B2.bin,然后打开QTool进入设备连接。关于界面说明见系统文档。 例如:点击Setting-Server-QPPS里的Create DB,然后进入Setting-Mode里点击Advertising,即可实现如上QPPS工程的效果。而在旁边的Local Device Traces窗口可以看到程序的大概运行过程。
####参考文档 - FireBLE-wiki - BLE编程API参考手册 - BLE软件开发手册 - 数据手册 - QN9020快速入门 - QN902x周立功版 - 防丢器项目 - MDK-ARM PRO SET - 总有“一道菜”适合你——BLE - 【FireBLE试用体验】体验报告汇总 -FireBLE 开发板试用汇总贴(2015.11.29更新 共收录145篇) [板块置顶] [精华] - FireBLE驱动第一篇:呼吸灯 -FireBLE低功耗蓝牙开发板评测 > 可穿戴手环+蓝牙防丢器 - esp8266