这段时间又参与了一个新的小项目,简单概括为蓝牙、智能、家居
吧,虽然时间有点紧,还是希望能把这一些东西记录下来。
BLE
什么是BLE?参考这篇文章做如下总结。
中文名称为蓝牙低功耗。主要特点为低成本、超低功耗、短距离、标准接口和可互操作性强,并且工作在免许可的2.4GHz ISM射频段,需要支持蓝牙4.0(系统为Android4.3及以上)的主机设备才能与其连接。
目前生产BLE芯片的厂家主要有CSR、TI、Nodic和NXP(QN902x),各个厂家芯片对比如下图
从如上图对比可以看出,NXP的QN902x在功耗方面比CSR和TI更省电,在接收灵敏度和模式方面比Nodic的胜一筹,它的从设备相比其它几家可以连接的更多,共有8个,这也算是蓝牙4.0的一大特色吧,并且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的请自动忽略这一步。具体请看官网/博文。
1 | ~ $ sudo add-apt-repository ppa:ubuntu-wine/ppa |
keil安装
首先得下载keil,可以自己去官网下载最新的,也可以直接点击mdk5.17下载地址,参考这篇博文并实践做如下记录。
1 | # => 进入下载目录安装 |
提示
:卸载程序可以用wine uninstaller
如编译有限制,下载破解注册机Keygen:http://pan.baidu.com/s/1hqGSRqs
安装完成后,会弹出来一个安装器件(pack installer)如下的界面,也就是说,你要用它来开发哪个芯片(此项目可以忽略,后面步骤导入DB)。
或者打开keil界面会看到如下图标
要开发哪一款芯片,点击install即可,或者先网页端下载好在导入,具体参考上面提供的博文地址。
MCU DB库安装
下载Quintic最新的SDKQBlue1.3.7,安装:
1 | ~ $ wine QBlue-1.3.7.exe |
会弹出窗口如下,点击安装即可,或者打开桌面QBlue里的QN9020DevDBforIDE工具安装。
keil使用
首先获取开源代码
1 | ~ $ cd |
桌面打开keil,假如我们希望开始proxr工程:
- 在keil的Project菜单中选择Open Project…
- 弹出文件选择框中,打开/home/xxx/fireble/BLE/prj_proxr/keil/proxr.uvproj工程文件(linux可能在z磁盘里)
配置DB如下,如果没发现库,请重启软件
编译代码,成功后如下,具体配置选项请参考这里
下载程序,下载的时候需要按复位键,如下载出错,可先下载到SRAM。
Tip
:在ubuntu 上串口识别为ttyS0或ttyUSB0之类,在wine上识别不到,可用:
1
2
3
4 ># =>将其该为小写的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部分的电路进行分析。
本程序非常简单,复制gpio demo代码
实现让开发板D1灯闪烁如下:
1 | /* Set pin D1/P2_7 */ |
程序流程:系统初始化–>GPIO配置–>各驱动模块初始化–>主循环实现功能
效果如下:
UART实验
串口通信可以用来打印数据,调试程序,有必要实验一下。
同样复制uart demo代码
改程序如下:
1 | //Print out "Hello NXP!\n" thought uart. |
波特率设置为115200,现象如下:
PWM实验
由于这个项目会控制到电机什么的,所以pwm少不了,这个实验是通过pwm控制陶瓷蜂鸣器报警和呼吸灯。通过PWM方式调节脉冲频率和占空比,变换LED亮度,渐变亮度实现呼吸灯效果。FireBLE板载的贴片蜂鸣器是压电式陶瓷蜂鸣器,压电陶瓷蜂鸣器要想响起来,需要满足四个条件:多谐振荡器、压电蜂鸣片、阻抗匹配器及共鸣箱,压电蜂鸣片由锆钛酸铅或铌镁酸铅压电陶瓷材料制成,在陶瓷片的两面镀上银电极,经极化和老化处理后,再与黄铜片或不锈钢片粘在一起。板载的蜂鸣器缺少的只是多谢振荡器,这里我们用PWM来代替,输出1.5KHz-2.5KHz的方波信号推动压电蜂鸣片发声,频率在1.5KHz-2.5KHz才会响,太高太低都不响,直接加3.3V更不会响,直接加3.3V响的那是电磁式蜂鸣器,有源和无源。
首先看下电路连接
同样复制pwm代码,改写如下:
1 | int main (void) |
呼吸灯效果:
按键广播
看到有些人对买的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,如下:
1 | int app_key_process_timer_handler(ke_msg_id_t const msgid, void const *param, |
这里一看就明白,向上按时,进入广播。其实在系统上电的时候,已经进行了一系列的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有什么区边。蓝牙各个协议层都有哪些分工。下面参考做一些归纳:
BLE规范中定义了GAP(Generic Access Profile)和GATT(Generic Attribute)两个基本配置文件。
a.协议中的GAP层负责设备访问模式和进程,包括设备发现,建立连接,终止连接。初始化安全特性和设备配置。
b.GATT层用于已连接的蓝牙设备之间的数据通信。GATT通俗理解为用于主从机之间的客户端和服务器端的数据交互,以Attribute Table来体现。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
GATT有Service和Client,Service作为服务器端,对GATT Client提供read/write接口,一般情况下,Central作为Client,Peripheral作为Service。所以主机会调用read/write来和作为Service端的Peripheral从机通讯。而Peripheral则通过notify的方式即调用GATT_Notifycation发起和主机通讯。
- 特征值声明值可以有五种属性:Read(可读) Write(可靠的可写,带响应) Write without resp(不可靠的可写,不带响应) Indicate(可靠通知,带响应) Notify(不可靠通知,不带响应)
更多请查看蓝牙设计问与答 /Android 蓝牙4.0 BLE 理解
BLE中主从机建立连接,到配对和绑定的过程如下图。
QPPS工程
在此之前,建议先了解一下Firefly的QPPS介绍/对QPPS profile中服务和特征实现的分析理解/对profile QPPS的分析理解。关于蓝牙的数据收发部分可以看Firefly的协议栈介绍。数据帧格式如下:
Tip
:技术案例中串口透传案例,实现的是将蓝牙模组串口所接收到的数据透传到app,并且把app的数据通过蓝牙透传到串口输出,案例本身的实现是针对串口通信和蓝牙透传的结合。
- 其实BLE完成初始化的条件并不是进入main函数的while(1)中(系统在调度了几个消息之后才完成的初始化,前几次进入while(1)时初始化都是没完成的),真正完成初始化并且可以运行是在打印BLE is Ready这句话的地方。那个地方你可以看到只要开启QN_DEMO_AUTO宏定义就可以上电广播了。
- 设备名默认从NVDS中写入,但是软件也可以修改,可以用app_gap_set_devname_req修改设备名。
如果你非要用软件指定,参考广播内容设定,取消NVDS读取,直接利用app_gap_set_devname_req函数指定设备名,然后将QN_LOACL_NAME广播出去,那么设备名和广播中的设备名都会是QN_LOCAL_NAME了。
具体实施如下
修改自动广播,无需向上拨动按键:
1
2#=> App_config.h中,取消如下的注释即可
#define QN_DEMO_AUTO 1在广播之前改变设备的名字,注意
app_set_adv_data
函数1
2
3
4
5
6
7
8//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);接收手机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_handler
、app_qpps_data_ind_handler
、app_qpps_cfg_indntf_ind_handler
,下文会提到,只会对这些做修改1
2
3
4
5
6
7/// 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属性进行处理1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22else 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可知会跳到下面这个函数,在这里对接收到的数据进行处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20int 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("\r\n");
QPRINTF("the receive data is :");
for(i=0;i<param->length;i++)
{
QPRINTF("%x",param->data[i]);
}
}
QPRINTF("\r\n");
return (KE_MSG_CONSUMED);
}当发送数据为29时,CuteCom蓝牙接收数据为
1
2
3
4
5
6
7
8
9
10QN 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 :29App收取蓝牙通知。通过点击app上5个特征的通知,能获取蓝牙模块的实时通知。Gatt层与通知相关的函数是qpps_data_send_req_handler,但我们只需要修改app开头的api即可完成相应的开发,现象及分析如下:
当点击app上的通知时,会进入如下的函数,为了调试将其调整为如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34int 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("\r\n");
if (param->cfg_val == PRF_CLI_START_NTF)
{
QPRINTF("enter PRF_CLI_START_NTF");
QPRINTF("\r\n");
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("\r\n");
// 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("\r\n");
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口状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22static void app_test_send_data(uint8_t max)
{
uint8_t cnt;
QPRINTF("enter app_test_send_data");
QPRINTF("\r\n");
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使用文档
1 | ~ $ cd /home/nephne/.wine/drive_c/QBlue/QN9020/QBlue-1.3.7/Documents |
插入USB连接蓝牙模块,下载np_controller_B2.bin,然后打开QTool进入设备连接。关于界面说明见系统文档。
例如:点击Setting-Server-QPPS里的Create DB,然后进入Setting-Mode里点击Advertising,即可实现如上QPPS工程的效果。而在旁边的Local Device Traces窗口可以看到程序的大概运行过程。