蓝牙51822学习笔记三
目前手里面有几个项目用的是nrf51822这款蓝牙芯片。由于我从未接触过蓝牙协议,所以在很多地方磕磕绊绊的,所以最近准备系统学习一下该芯片。并做一下笔记放在我的博客里面。系统学习资料来源于B站青峰电子。视频名称为蓝牙nrf51822视频教程资料 编程开发 协议栈开发 青峰电子例程。
协议栈初始化
ble_stack_init
static void ble_stack_init(void)
{
uint32_t err_code;
//时钟初始化宏
// Initialize the SoftDevice handler module. PPM(百万分之一)
SOFTDEVICE_HANDLER_INIT(NRF_CLOCK_LFCLKSRC_XTAL_20_PPM, NULL);
//协议栈
#if defined(S110) || defined(S130) || defined(S132)
// Enable BLE stack.
ble_enable_params_t ble_enable_params;
memset(&ble_enable_params, 0, sizeof(ble_enable_params));
#if (defined(S130) || defined(S132))
ble_enable_params.gatts_enable_params.attr_tab_size = BLE_GATTS_ATTR_TAB_SIZE_DEFAULT;
#endif
ble_enable_params.gatts_enable_params.service_changed = IS_SRVC_CHANGED_CHARACT_PRESENT;
err_code = sd_ble_enable(&ble_enable_params);
APP_ERROR_CHECK(err_code);
#endif
//调度机制的核心
// Register with the SoftDevice handler module for BLE events.
err_code = softdevice_ble_evt_handler_set(ble_evt_dispatch);//蓝牙事件
APP_ERROR_CHECK(err_code);
// Register with the SoftDevice handler module for BLE events.
err_code = softdevice_sys_evt_handler_set(sys_evt_dispatch);//系统事件
APP_ERROR_CHECK(err_code);
}
回调派发函数
回调派发函数,分两个部分,一个是蓝牙事件派发,一个是系统事件派发。不同于CC240等蓝牙芯片带操作系统,nrf系列蓝牙处理器采用派发方式。当蓝牙协议栈有事件需要处理的时候,就会用到蓝牙事件派发。当系统有事件处理的时候就用到系统事件派发。
蓝牙事件派发
static void ble_evt_dispatch(ble_evt_t * p_ble_evt)
{
dm_ble_evt_handler(p_ble_evt);//设备管理事件处理函数
ble_conn_params_on_ble_evt(p_ble_evt);//连接参数管理处理函数,必要
bsp_btn_ble_on_ble_evt(p_ble_evt);//板级蓝牙事件处理函数
on_ble_evt(p_ble_evt);//通用的事件处理函数,必要
ble_advertising_on_ble_evt(p_ble_evt);//广播蓝牙事件处理函数,必要
/*YOUR_JOB add calls to _on_ble_evt functions from each service your application is using
ble_xxs_on_ble_evt(&m_xxs, p_ble_evt);//添加自己的服务处理函数
ble_yys_on_ble_evt(&m_yys, p_ble_evt);
*/
}
evt_id传递事件。
在ble_ranges.h定义了一组事件ID。事件字节区间不定,有1个字节,2个字节,4个字节等。
协议栈抛出事件ID进来时,例如是BLE_GAP_EVT_CONNECTED。在派发函数采用轮询的方式,看各个事件处理函数是否关注这个事件ID,如果关注的花,也就是在程序总定义事件处理函数需要执行对应的操作。
系统事件派发
static void sys_evt_dispatch(uint32_t sys_evt)
{
pstorage_sys_event_handler(sys_evt);//内部存储器的应用
ble_advertising_on_sys_evt(sys_evt);//系统广播应用
}
pstorage_sys_event_handler将在之后章节详细说明。一样是根据ID来处理事件。通过sd_evt_get获取事件ID,然后由处理事件函数进行相关的处理。
协议栈GAP入门
通用访问配置文件(GAP),该profile保证不通过的Bluetooth产品可以互相发现对方并建立连接。
GAP属于主协议栈层。
GAP初始化
GAP介绍
GAP定义了蓝牙设备如何发现和建立于其他设备的安全(或者不安全)连接。它处理一些一般模式的业务(如询问、命名和搜索)和一些安全性问题(如担保),同时还处理一些有关连接的业务(如链路建立、信道和连接建立)。
GAP规定的是一些一般性的运行任务。因此,它具有强制性,并作为所有其他蓝牙设备应用规范的基础。
GAP是所有其他配置文件的基础,它定义了在蓝牙设备间建立基带链路的通用方法。除此之外,GAP还定义了下列内容:
- 必须在所有蓝牙设备中实施的功能
- 发现和链接设备的通用步骤
- 基本用户界面术语
GAP确保了应用程序和设备间的高度互操作性,还允许开发人员利用现有的定义更加容易的定义新的配置文件。GAP处理未连接的两个设备间的发现和建立连接过程
此配置文件定义了一些通用的操作,这些操作可供引用GAP的配置文件,以及实施多个配置文件的设备使用。GAP确保了两个蓝牙设备可通过蓝牙技术交换信息,以发现彼此支持的应用程序。不符合任何其他蓝牙配置文件的蓝牙设备必须与GAP符合以确保基本的互操作性和共存。
GAP软件设计分析
static void gap_params_init(void)
{
uint32_t err_code;
ble_gap_conn_params_t gap_conn_params;
ble_gap_conn_sec_mode_t sec_mode;
//设置连接的安全模式
BLE_GAP_CONN_SEC_MODE_SET_OPEN(&sec_mode);
err_code = sd_ble_gap_device_name_set(&sec_mode,
(const uint8_t *)DEVICE_NAME,
strlen(DEVICE_NAME));
APP_ERROR_CHECK(err_code);
/* YOUR_JOB: Use an appearance value matching the application's use case.
err_code = sd_ble_gap_appearance_set(BLE_APPEARANCE_);
APP_ERROR_CHECK(err_code); */
//设置连接参数,主要设置连接间隔
memset(&gap_conn_params, 0, sizeof(gap_conn_params));
gap_conn_params.min_conn_interval = MIN_CONN_INTERVAL;
gap_conn_params.max_conn_interval = MAX_CONN_INTERVAL;
gap_conn_params.slave_latency = SLAVE_LATENCY;
gap_conn_params.conn_sup_timeout = CONN_SUP_TIMEOUT;
err_code = sd_ble_gap_ppcp_set(&gap_conn_params);
APP_ERROR_CHECK(err_code);
}
GAP安全模式配置
GAP通常还会负责启动BLE连接的安全功能。只有对通过身份验证的连接而言某些数据是可读可写的。一旦形成一个连接,两个设备可以通过一个过程被称为配对。进行配对时,密钥建立加密和认证的连接。在一个典型的案例中,外围设备需要中央设备提供密钥以完成配对过程。这可能是一个固定值,如“000000”(静态密钥)或可能是一个随机生成的值(随机密钥)被提供给用户。中央设备发送正确的密钥后,两台设备交换安全密钥加密和验证的连接。
在许多情况下,相同的中央设备和外围设备将会经常建立连接和断开连接。BLE具有一个安全功能允许两个设备在配对的时候给对方一个长久的安全密钥。此功能称之为绑定,使得两个重连设备能够迅速重新确立加密和认证而不需要经过充分的配对过程,只要他们存储长期的密钥信息。
typedef struct
{
uint8_t sm : 4; /**< Security Mode (1 or 2), 0 for no permissions at all. */
uint8_t lv : 4; /**< Level (1, 2 or 3), 0 for no permissions at all. */
} ble_gap_conn_sec_mode_t;
GAP连接要求
有0,1,2三种模式,0,1,2,3,4五种等级。
- 0,0:不允许连接
- 1,1:无安全要求
- 1,2:需要加密链接,无MITM保护
- 1,3:加密链接和MITM保护
- 1,4:LESC加密和MITM保护
- 2,1:签名或者加密,无MITM保护
- 2,2:MITM保护签名或者加密连接
MITM为中间人攻击。
设备名称修改
err_code = sd_ble_gap_device_name_set(&sec_mode,
(const uint8_t *)DEVICE_NAME,
strlen(DEVICE_NAME));
连接参数的设置
连接前,外围设备需要先广播,向中央设备通告自己的存在,主要有下面几个参数
在一个典型的蓝牙系统中,外围设备发送具体的广播数据让任何的中央设备知道他是一个可连接的设备。广播内容包含设备地址,还可以包含一些额外的数据,比如设备名称。中央设备接收到广播后发送一个搜索请求给外围设备,外围设备答复一个搜索答复。这就是设备被发现的过程。
连接间隔
在一个BLE连接中是要使用到跳频机制,这样两个设备可以在一个特定的信道上进行数据收发,在一个特定的时间之后会跳到一个新的信道上。LL层负责信道切换。这个遇见设备收发数据被称为时连接事件。尽管没有应用程序需要收发,两个设备依然会交换链路层数据来保持连接。连接间隔时两个连接事件之间的事件,使用一个单元值1.25ms的步进。从最小值6(7.5ms)到3200(4.0s)。
不同的应用也许经过不同的连接间隔,一个长时间的连接间隔将会节约更多的电量,因为设备可以在两个连接时间之间睡眠更长的时间。但是会导致数据发送不及时,如果有数据要发送那么只能在下一个连接事件到来时才能发送。
从机潜伏周期
这个参数描述了从机跳过连接事件的次数。这使外围设备具有一定的灵活性,如果它不具有任何数据传输,它可以选择跳过连接事件,并保持睡眠。
监督超时
这是两个成功连接事件之间间隔的最大值。如果超过这个时间还未出现成功的连接事件,那么设备将会考虑失去连接,返回一个未连接状态。这个参数值为10ms的步进。最小10(100ms)到3200(32 s)同时超时时间必须大于有效连接时间
- 短连接间隔:高功耗,高数据吞吐量,发送等待时间短
- 长连接间隔:低功耗,低数据吞吐量,发送等待时间长
- 低或者0潜伏值:从机在没有数据发送的情况下高功耗,从机可以快速的收到主机数据
- 高潜伏值:从机在没有数据发送的情况下可以低功耗,从机无法及时收到主机的数据,但是主机能及时收到从机的数据
err_code = sd_ble_gap_ppcp_set(&gap_conn_params);
GAP实际上定义了蓝牙设备的基本需要,包括广播功能,连接加密等功能。
广播模式的配置
广播初始化
/**@brief Function for initializing the Advertising functionality.
*/
static void advertising_init(void)
{
uint32_t err_code;
ble_advdata_t advdata;//广播数据
// Build advertising data struct to pass into @ref ble_advertising_init.
memset(&advdata, 0, sizeof(advdata));
advdata.name_type = BLE_ADVDATA_FULL_NAME;//名字类型,长名
advdata.include_appearance = true;//广播包含的应用
advdata.flags = BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE;//广播的方式
advdata.uuids_complete.uuid_cnt = sizeof(m_adv_uuids) / sizeof(m_adv_uuids[0]);//uuid长度
advdata.uuids_complete.p_uuids = m_adv_uuids;//uuid
//广播方式
ble_adv_modes_config_t options = {0};
options.ble_adv_fast_enabled = BLE_ADV_FAST_ENABLED;//快速广播
options.ble_adv_fast_interval = APP_ADV_INTERVAL;//间隔
options.ble_adv_fast_timeout = APP_ADV_TIMEOUT_IN_SECONDS;//超时
err_code = ble_advertising_init(&advdata, NULL, &options, on_adv_evt, NULL);
APP_ERROR_CHECK(err_code);
}
广播的说明
蓝牙nrf51822实际上设置的蓝牙类型是严格限定的。
蓝牙BR/EDR(蓝牙基本速率和增强数据率),低功耗是Bluetooth Smart的亮点之一。Bluetooth Smart设备仅靠一颗纽扣电池就能运行数月甚至数年之久。Bluetooth Smart的灵活配置也让应用能够更好的管理连接间隔(Connection interval),以优化接收机的工作周期。对于蓝牙BR/EDR,由于其数据吞吐量更高,功耗也会相应增加。BR/EDR配置文件包括:耳机(HSP)、对象交换(OBEX)、音频传输(A2DP)、视频传输(VDP)和文件传输(FTP)。也就是一些大的数据的传输。
BlueTooth 4.x核心规格中有一卷是低功耗控制器,还有一卷是蓝牙BR/EDR控制器。通常情况下,如果耳机支持4.x,则兼容4.x BR/EDR规格,而不兼容低功耗规格或者Bluetooth smart。可以通过辨认产品包装上的Bluetooth Smart商标确认是否为Bluetooth smart产品。因此nrf51822是无法传输音频的
广播模式
- Direct:直连模式,利用ble中的直连广播,该模式是为了快速重新脸上刚刚断开的设备,比如利用在快速重连上意外断开的设备,已经达到无缝恢复
- Fast:普通的广播,不过连接间隔可以设置的快一些
- Slow:普通广播,连接间隔设置的慢一些
- Idle:停止广播
ble_advertising_on_ble_evt(p_ble_evt);//广播派发函数
广播初始化的时候使能了快速广播,在广播派发函数中如果不进行连接(超时时间之后)会逐级递减。
连接参数
连接参数函数
/**@brief Function for initializing the Connection Parameters module.
*/
static void conn_params_init(void)
{
uint32_t err_code;
ble_conn_params_init_t cp_init;
memset(&cp_init, 0, sizeof(cp_init));
//连接参数更新值
cp_init.p_conn_params = NULL;
cp_init.first_conn_params_update_delay = FIRST_CONN_PARAMS_UPDATE_DELAY;
cp_init.next_conn_params_update_delay = NEXT_CONN_PARAMS_UPDATE_DELAY;
cp_init.max_conn_params_update_count = MAX_CONN_PARAMS_UPDATE_COUNT;
cp_init.start_on_notify_cccd_handle = BLE_GATT_HANDLE_INVALID;
cp_init.disconnect_on_fail = false;
cp_init.evt_handler = on_conn_params_evt;
cp_init.error_handler = conn_params_error_handler;
err_code = ble_conn_params_init(&cp_init);
APP_ERROR_CHECK(err_code);
}
这个函数设置的并不是连接参数值,连接参数在GAP初始化中已经设置了,这个函数主要关注的是连接参数更新。设置连接更新的方式。
连接参数结构体
/**@brief Connection Parameters Module init structure. This contains all options and data needed for
* initialization of the connection parameters negotiation module. */
typedef struct
{
//GAP连接参数,如果设置为NULL,则代表需要从主机获取连接参数(正在使用的连接参数,GAP中设置的)
ble_gap_conn_params_t * p_conn_params; /**< Pointer to the connection parameters desired by the application. When calling ble_conn_params_init, if this parameter is set to NULL, the connection parameters will be fetched from host. */
//初始化时间(连接或者启动通知)到第一次连接参数更新的时间
uint32_t first_conn_params_update_delay; /**< Time from initiating event (connect or start of notification) to first time sd_ble_gap_conn_param_update is called (in number of timer ticks). */
//第一次更新后,下次发起更新申请的时间间隔。蓝牙4.0协议栈推荐值为30s
uint32_t next_conn_params_update_delay; /**< Time between each call to sd_ble_gap_conn_param_update after the first (in number of timer ticks). Recommended value 30 seconds as per BLUETOOTH SPECIFICATION Version 4.0. */
//放弃协商连接参数前尝试的最大次数
uint8_t max_conn_params_update_count; /**< Number of attempts before giving up the negotiation. */
//如果是当通知开始时启动的这个过程,设置为对应的CCCD的句柄,如果是连接开始时启动的,则设置为BLE_GATT_HANDLE_INVALID
uint16_t start_on_notify_cccd_handle; /**< If procedure is to be started when notification is started, set this to the handle of the corresponding CCCD. Set to BLE_GATT_HANDLE_INVALID if procedure is to be started on connect event. */
//设置为true时,当连接参数更新失败,则会自动断开连接
bool disconnect_on_fail; /**< Set to TRUE if a failed connection parameters update shall cause an automatic disconnection, set to FALSE otherwise. */
//连续参数更新参数对应的处理事件回调函数
ble_conn_params_evt_handler_t evt_handler; /**< Event handler to be called for handling events in the Connection Parameters. */
//发送错误时的错误回调处理函数
ble_srv_error_handler_t error_handler; /**< Function to be called in case of an error. */
} ble_conn_params_init_t;
连接参数更新描述
在连接中,如果从设备希望修改当前的连接参数则可以使用该命令。在低功耗蓝牙中,为了实现极低的功耗,低功耗蓝牙协议栈设置为不需要射频时彻底将空中射频关断。所以,尽可能降低从机监听连接事件所需要的频率对提高电池寿命是至关重要的。例如:如果连接事件间隔太快,呆滞过多的电量浪费。这种情况下在从机设备时延很大时没有问题,否则从机设备将会频繁的监听链路。
连接参数更新请求命令用于从设备向主设备发送,因为主设备随时能够启动链路层链接参数更新控制规程。如果该命令是由主设备发送的,从设备会将其视为一个错误,并返回原因代码为“命令不理解”的指令拒绝命令。
参数first_conn_params_update_delay为初始化后第一次更新请求的事件间隔。如果没有得到主机的连接参数的更新响应,那么开始申请第二次更新请求next_conn_params_update_delay,直到到最大的申请次数max_conn_params_update_count,如果还是申请失败,则断开连接。
连接参数更新应答
该命令用于主设备向从设备进行发送。
从机设备可以在任何时候发送连接参数更新请求命令。收到该信息的主设备如果可以修改连接参数,则将返回连接参数更新应答命令,其中的结果代码设置为接受。然后主设备将会启动链路层连接参数更新控制规程对连接参数进行更新。
如果主设备不同意从设备的请求命令,它可以发送结果代码为拒绝的连接参数更新应答来拒绝请求,此时,设备有两种选择:接受从设备希望的正在使用的连接参数或者终止连接。
修改连接参数时,如果要减少主设备拒绝从设备请求的可能性,可以 在请求里设置一个可接受的参数范围或者从设备提供一个合理的从设备延时。主设备可以选择合适的连接事件间隔,从设备可以使用最佳功耗的从设备延迟参数。
协议栈下板级设备的使用
这个没太多需要做笔记的地方,类似于一些按键和LED的官方demo。对于单片机有些许了解的话,应该是没有什么问题的。在官方例程中的代码前缀时bsp。
协议栈下的定时器的使用
定时器的初始化
static void timers_init(void)
{
// Initialize timer module.//初始化定时器,
APP_TIMER_INIT(APP_TIMER_PRESCALER, APP_TIMER_OP_QUEUE_SIZE, false);
// Create timers.
/* YOUR_JOB: Create any timers to be used by the application.
Below is an example of how to create a timer.
For every new timer needed, increase the value of the macro APP_TIMER_MAX_TIMERS by
one.
uint32_t err_code;//创建一个定时器
err_code = app_timer_create(&m_app_timer_id, APP_TIMER_MODE_REPEATED, timer_timeout_handler);
APP_ERROR_CHECK(err_code); */
}
蓝牙LED的控制(私有服务)
通过私有自建任务实现功能。
定制私有profile服务
只要协议栈支持GATT,就可能为一个应用建立一个他需要的profile和任务。因此该profile必须符合GATT的 规范定义。
UUID
在GATT层中规范定义的所有属性都有一个UUID,UUID时全球唯一的128位的号码,他来识别不同的特性。
UUID蓝牙技术联盟
蓝牙核心规范指定了两种 不同的UUID,一种是基本的UUID,一种是代替基本UUID的16位UUID。
所有的蓝牙技术联盟定义UUID公用一个基本的UUID0000XXXX-0000-1000-8000-00805F9B34FB
虽然蓝牙技术联盟使用相同的基本UUID,但是16位的UUID足够唯一的识别蓝牙技术联盟所定义的各种属性。
蓝牙技术联盟所用的基本UUID不能用于任何定制的属性、服务和特性。对于定制的属性,必须使用另外完整的128位UUID
供应商特定的UUID
softDevice根据蓝牙技术联盟定义UUID类似的定义UUID:先增加一个特定的基本UUID,再定义一个16位的UUID,加载到基本UUID上面。
空中操作和性质
大部分的空中操作事件都是采用句柄来进行的,因为句柄能够唯一识别各个属性。
- 常用性质
- 写
- 没有响应的写
- 读
- 通知:客户端发送请求给服务器,不需要服务器回复一个响应
- 指示:服务器发送指示给客户端,需要客户端发送一个确认给服务器
写和没有回应的写
写和没有回应的写允许GATT客户端写入一个值到GATT服务器的一个特性中。他们之间不同的地方在于没有回应的写事件再任何应用层上的确认和回应
读
读性质表明一个GATT客户端可以读取再GATT服务器中特性的值。
通知和指示
通知和指示性质允许GATT服务器在其某个特性改变的时候对GATT客户端进行提醒。通知和指示之间不同之处在于指示有应用层上的确认,而通知没有 。
私有任务的建立
一个主服务下面:建立多个特征值(相当于多个子服务)
电池服务
官方配置文件ble_bas.c
蓝牙心电
官方配置文件ble_hrs.c
有检测的位置设置,例程设置为手指。
必须有设备初始化信息服务。ble_dis.c
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!