一起学nRF51xx 21 - 蓝牙项目工程的初始化流程解读

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/zhanglifu3601881/article/details/101171797

前言

本节主要工作是对《一起学nRF51xx 20 -  移植SDK蓝牙例程》章节程序进行解读。

示例详解

基于硬件平台:nrf51822ek_tm开发板。

本示例所用的最小系统板原理图:

 

    1. 程序启动分析:

 

注,以下相关内容来自nrf51822ek_tm开发板的官方解析,个人觉得很不错,故在此引用,官方作者:不离不弃qq574912883

/**********************************************************************************************/

int main(void)

{

//初始化 LED 指示灯,用来指示广播和连接状态

leds_init();

//初始化软件定时器模块

timers_init();

//设置按键作为 DETECT signal 用来唤醒 system off 模式,具体参看数据手册 power 章节

buttons_init();

//主要设置 uart 的引脚,波特率。接收,发送中断等。并开启 uart 模块中断

uart_init();

//协议栈初试化, 设置时钟, demo 里面设置为外部时钟。并且注册事件派发函数

ble_stack_init();

//GAP 一些参数的设置,设置设备名,设置 PPCP(外围设备首选链接参数)(手机连上某个蓝

牙设备后可以从 Generic Access Service 中看到设置的这些参数)

gap_params_init();

//服务初始化。添加 uart 的串口服务。主要提供两个特征值来供手机和板子以及电脑的通信

services_init();

//设置广播数据以及扫描响应数据

advertising_init();

//链接参数设置。主要设置什么时候发起更新链接参数请求以及间隔和最大尝试次数。

conn_params_init();

//安全参数初始化。

sec_params_init();

simple_uart_putstring(START_STRING);

//设置广播类型,白名单,间隔,超时等特性。并开始广播。

advertising_start();

for (;;)

{

//电源管理,调用 arm0 的指令__WFE();进入睡眠

power_manage();

}

}

二:函数单独解析:

1 leds_init

static void leds_init(void)

{

nrf_gpio_cfg_output(ADVERTISING_LED_PIN_NO);

nrf_gpio_cfg_output(CONNECTED_LED_PIN_NO);

}

设置的 PIN_CONFIG 寄存器使能两个引脚的作为输出功能。用来当做指示灯指示广播和链接的状态。

2 timers_init

static void timers_init(void)

{

// Initialize timer module

APP_TIMER_INIT(APP_TIMER_PRESCALER, APP_TIMER_MAX_TIMERS,

APP_TIMER_OP_QUEUE_SIZE, false);

}

初始化软件定时器模块,该定时器模块并不是使用 timer0-2来实现定时功能。而是使用 51822中的 RTC1 来软件模拟出定时器模块。 RTC1 使用 32.768K 时钟经过分频后是时钟来作为时钟源。 所以该函数内部实现就是设置 RTC1 相关的寄存器和做一些初始化。 其原理和 timer 定时/计数器模块类似。具体细节参考芯片数据手册。

 

APP_TIMER_PRESCALER:设置分频系数。 (32.768K 来分频)

APP_TIMER_MAX_TIMERS:设置可以创建的最大定时器个数

APP_TIMER_OP_QUEUE_SIZE:定时器操作队列,因为是用 RTC 模拟的软件定时器,因此内部是维护了一个软件定时器的操作队列

False:不使用调度,调度模块没有细看。貌似 51822 关于调度的都是传 False 不使用调度。51822 的协议栈实现是基于异步事件驱动的。

3buttons_init

static void buttons_init(void)

{

nrf_gpio_cfg_sense_input(WAKEUP_BUTTON_PIN,

BUTTON_PULL,

NRF_GPIO_PIN_SENSE_LOW);

}

这里的按键设置比较简单,主要通过 PIN_CNF 寄存器来设置一个 IO 口来作为来作为 sensingmechanism 机制的引脚。这里是设置了 WAKEUP_BUTTON_PIN 这个引脚来作为这个功能,设置成低电平时触发这个机制。而这个机制类似一个 wakeup 机制,当其被触发时会产生一个DETECT signal 而这个信号会将 cpu system off 模式中唤醒。

 

4 uart_init

static void uart_init(void)

{

simple_uart_config(RTS_PIN_NUMBER, TX_PIN_NUMBER, CTS_PIN_NUMBER, RX_PIN_NUMBER,

HWFC);

NRF_UART0->INTENSET

UART_INTENSET_RXDRDY_Enabled<<UART_INTENSET_RXDRDY_Pos;

NVIC_SetPriority(UART0_IRQn, APP_IRQ_PRIORITY_LOW);

NVIC_EnableIRQ(UART0_IRQn);

/**@snippet [UART Initialization] */

}

初始化 uart 设置输入输出引脚,是否关闭流控。一般使用官方例子的时候都要先将流控关掉, HWFC 为 False。然后打开 uart 的接收中断,打开 uart 模块的中断功能,以及设置优先级。 波特率在 simple_uart_config 中设置,该函数设置完引脚后使能 uart 开启 uart 的接收和发送功能。

 

5 ble_stack_init

static void ble_stack_init(void)

{

// Initialize SoftDevice.

SOFTDEVICE_HANDLER_INIT(NRF_CLOCK_LFCLKSRC_XTAL_20_PPM, false);

// Subscribe for BLE events.

uint32_t err_code = softdevice_ble_evt_handler_set(ble_evt_dispatch);

APP_ERROR_CHECK(err_code);

}

//设置 LFCLK(32.768K)的时钟源(协议栈需要使用),这里设置为外部晶振。 False 为不使用调度。 softdevice_ble_evt_handler_set(ble_evt_dispatch);注册事件派发程序,基础 1-协议栈概述说明过,当 BLE 收到广播,链接请求,对端设备数据等后底层处理完会上抛给上册 app 一个事件,这个事件的上抛过程是协议栈触发 SWI 中断,在中断内部将事件放入队列,然后调用 app 中的 SWI 中断。 App 中的 SWI 中断会 get 队列中的事件,并最终会调用注册的ble_evt_dispatch 函数,这个函数再将事件发给各个服务以及模块的事件处理函数来处理各个服务及模块自己感兴趣的事件。相关原理基础 1-协议栈概述视频教程中有说明。

 

 

6gap_params_init

设置必要的设备的 GAP 参数。

static void gap_params_init(void)

{

uint32_t err_code;

ble_gap_conn_params_tgap_conn_params;

ble_gap_conn_sec_mode_tsec_mode;

//设置设备名的写权限为普通模式,则手机扫描到设备连接上后可以在第一个服务 GeneicAccess Service(有的只显示 UUID 1800)中改写 Device name.(有的 app 可能本身未实现改写功能)

BLE_GAP_CONN_SEC_MODE_SET_OPEN(&sec_mode);

//设置设备名,该设备名就是在手机 app 扫描蓝牙设备时显示的名字。

err_code = sd_ble_gap_device_name_set(&sec_mode,

(const uint8_t *) DEVICE_NAME,

strlen(DEVICE_NAME));

APP_ERROR_CHECK(err_code);

memset(&gap_conn_params, 0, sizeof(gap_conn_params));

//设置外围设备连接首选参数。同 device name一样,手机连上某个蓝牙设备后可以从 GenericAccess Service 中看到设置的这些参数。这个参数主要是让中央设备在首次连接外设时可以读取他们以及时调整连接参数。或者当中央设备以后重连该外设,并且之前保留了这些参数那么就免去了连接后可能需要的修改连接参数的麻烦。

//当然,外围设备也可以之后通过 sd_ble_gap_ppcp_get 来获取之前设置的参数然后通过

连接参数跟新请求函数向中央设备请求更改连接参数。

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);

}

7 services_init

static void services_init(void)

{

uint32_t err_code;

ble_nus_init_tnus_init;

memset(&nus_init, 0, sizeof(nus_init));

//注册数据处理函数,这里处理的数据是收到手机发来的数据

// nus_data_handler 就是将板子收到的数据通过串口打印到电脑上

//实现了手机->开发板->电脑方向的数据流传输。

nus_init.data_handler = nus_data_handler;

err_code =ble_nus_init(&m_nus, &nus_init);

APP_ERROR_CHECK(err_code);

}

1ble_nus_init 该函数中实现添加服务以及添加特征值

uint32_t ble_nus_init(ble_nus_t * p_nus, constble_nus_init_t * p_nus_init)

{

uint32_t err_code;

ble_uuid_tble_uuid;

//设置基准 uuid

ble_uuid128_t nus_base_uuid = {0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9,

0xE0,0x93, 0xF3, 0xA3, 0xB5, 0x00, 0x00, 0x40, 0x6E};

if ((p_nus == NULL) || (p_nus_init == NULL))

{

return NRF_ERROR_NULL;

}

// 初始化连接句柄,因为现在并未与手机连接所以先赋值无效。

//赋值数据处理函数,就是上面刚提到的打印收到的手机数据

//设置 notify 是否使能的标志量,该标志量在手机连上板子并且使能了具

//notfify 的特征值时(这里是 rx 特征值后面会讲到),该标志会被设

// 置。这个标志量仅仅只是一个类似 flag 的作用,甚至可能并未被

// 用到。

p_nus->conn_handle = BLE_CONN_HANDLE_INVALID;

p_nus->data_handler = p_nus_init->data_handler;

p_nus->is_notification_enabled = false;

// 因为是自己定义的 uuid,所以需要调用该函数来赋值 p_nus->uuid_type

//该函数会将这个 nus_base_uuid 放到协议栈内部的表中

err_code = sd_ble_uuid_vs_add(&nus_base_uuid, &p_nus->uuid_type);

if (err_code != NRF_SUCCESS)

{

returnerr_code;

}

//设置服务 uuid 以及 uuid_type(就是上面调用的函数或得的)

ble_uuid.type = p_nus->uuid_type;

ble_uuid.uuid = BLE_UUID_NUS_SERVICE;

// 到这里就添加服务到协议栈内部表中了

err_code = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY,

&ble_uuid,

&p_nus->service_handle);

if (err_code != NRF_SUCCESS)

{

returnerr_code;

}

// 一个服务通常有几个特征值

//这里在上面注册的服务中添加了两个特征值。

err_code = rx_char_add(p_nus, p_nus_init);

if (err_code != NRF_SUCCESS)

{

returnerr_code;

}

// Add TX Characteristic.

err_code = tx_char_add(p_nus, p_nus_init);

if (err_code != NRF_SUCCESS)

{

returnerr_code;

}

return NRF_SUCCESS;

}

本篇教程主要说明协议栈的整体框架和。关于服务的创建和特征值的添加。这里不做细将,主要因为特征值的添加涉及到很多的概念和参数设置。后面分单独发一篇教程针对如何创建自己的服务并添加特征值。

 

8advertising_init

广播参数的初始化

static void advertising_init(void)

{

uint32_t err_code;

ble_advdata_tadvdata;

ble_advdata_tscanrsp;

//该标志主要设置广播类型为有限可发现模式,并且设置不支持经典蓝牙

//相比于一般可发现模式的广播,有限可发现模式的广播平率更快,但是只能最多维持

//30s

uint8_t flags = BLE_GAP_ADV_FLAGS_LE_ONLY_LIMITED_DISC_MODE;

//设置需要广播的 uuid, 就是上面主测的服务 uuid

ble_uuid_tadv_uuids[] = {{BLE_UUID_NUS_SERVICE, m_nus.uuid_type}};

//这里设置广播的名字为全名,设置标志,就是上面提到的。

//appearance 外观,他就是一个整形值,代表设备是一个手环,手机什么的。

memset(&advdata, 0, sizeof(advdata));

advdata.name_type = BLE_ADVDATA_FULL_NAME;

advdata.include_appearance = false;

advdata.flags.size = sizeof(flags);

advdata.flags.p_data = &flags;

//这里设置的是扫描响应数据。该数据在设备收到扫描请求的时候才会发出去。

//有时候需要广播的数据可能太多,广播包中放不下,那么就可以放在扫描响应

//数据中,这样对端设备便可以通过扫描请求来或得剩下的数据。

memset(&scanrsp, 0, sizeof(scanrsp));

scanrsp.uuids_complete.uuid_cnt = sizeof(adv_uuids) / sizeof(adv_uuids[0]);

scanrsp.uuids_complete.p_uuids =adv_uuids;

err_code = ble_advdata_set(&advdata, &scanrsp);

APP_ERROR_CHECK(err_code);

}

 

 

9 conn_params_init

设置连接参数

static void conn_params_init(void)

{

uint32_t err_code;

ble_conn_params_init_tcp_init;

memset(&cp_init, 0, sizeof(cp_init));

//这里连接参数设置为 NULL 的原因是前面的 gap_params_init 函数中已经设置了连接

//参数并调用了 sd_ble_gap_ppcp_set 将参数设置到了协议栈中。所以这里既是不设置,

//下面的 ble_conn_params_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);

}

10 sec_params_init

安全参数的初始化。 主要设置超时时间:比如配对过程中某一步的确认超过这个时间还未收到那么便是超时。 APP 会收到SD 上抛的状态事件,状态为超时

Bond: 是否绑定。如果需要绑定,配对过程会有第三步的秘钥分发,然后 app 将秘钥存储在falsh 这样下次就可以避免了下次重复配对的过程。

MITM: 是否需要中间人保护。

Io_caps:本设备的 I/O 能力。比如有显示屏,有键盘。

:当使能了 MITM 并且两端设备一个有键盘,一个有显示屏时,配对过程中就会显示一个配对码,对端设备通过键盘再输入。

如果没有 MITM 保护配对过程中的信息是很容易被监听到的。但是如果有了 MITM 因为这个配对码信息是一端显示一端输入,并不会通过链路传输。因为除了两端设备不会有第三个设备知道。因此后续的链路加密就很难被破解。

OOB:与 MITM 类似,只是配对码不是通过键盘输入而是通过两端设备别的通信通道传输,比如 NFC,当然前提是该通信链路是安全的。不如也没必要绕个弯而不直接用 BLE来传输了。后面就是设置加密秘钥的最大和最小值。 加密秘钥的大小在 7-16 字节之间配对的过程相对比较复杂,这里不做理论解释。后期需要的话会单独做一片配对的详细教程,群文件中有我上传了一个作为从机的配对历程也是基于 uart,当主机在使能有第一个特征值的 notify 时便会触发配对,配对码是通过串口打印的。使用的随机产生的。当然也可以设置为静态的。

void sec_params_init(void)

{

m_sec_params.timeout = SEC_PARAM_TIMEOUT;

m_sec_params.bond = SEC_PARAM_BOND;

m_sec_params.mitm = SEC_PARAM_MITM;

m_sec_params.io_caps = SEC_PARAM_IO_CAPABILITIES;

m_sec_params.oob = SEC_PARAM_OOB;

m_sec_params.min_key_size = SEC_PARAM_MIN_KEY_SIZE;

m_sec_params.max_key_size = SEC_PARAM_MAX_KEY_SIZE;

}

11 advertising_start

static void advertising_start(void)

{

uint32_t err_code;

ble_gap_adv_params_t adv_params;

memset(&adv_params, 0, sizeof(adv_params));

//设置广播类型为通用广播.

广播类型有四种:

通用广播:用途最广的广播方式。可以被扫描到,以及可以被连接

定向广播:用来快速建立和目标设备建立连接。报文中包含自己以及目标地址。不可连接广播:只广播数据,不可以被扫描以及连接。可发现广播;可以被扫描(回复扫描响应数据),不可以被连接。

adv_params.type = BLE_GAP_ADV_TYPE_ADV_IND;

//如果广播方式为定向广播,这里添目标设备的地址

adv_params.p_peer_addr = NULL;

//设置过滤规则。

//可设置为是否过滤掉非白名单中的扫描请以及非白名单中的连接请求或者两者都过滤。

adv_params.fp = BLE_GAP_ADV_FP_ANY;

//设置广播间隔和广播超时,超时时间到期如果设备还未连接那么 app 会收到协议栈上

//抛的广播超时时间。 App 可以做自己想做的处理,比如让设备进入睡眠。

adv_params.interval = APP_ADV_INTERVAL;

adv_params.timeout = APP_ADV_TIMEOUT_IN_SECONDS;

//开启广播

err_code = sd_ble_gap_adv_start(&adv_params);

APP_ERROR_CHECK(err_code);

nrf_gpio_pin_set(ADVERTISING_LED_PIN_NO);

}

/**********************************************************************************************/

注,以上相关内容来自nrf51822ek_tm开发板的官方解析,个人觉得很不错,故在此引用,官方作者:不离不弃qq574912883

 

    1. 应用程序与协议之间的关系

Nordic提供的协蓝牙协议栈形式是HEX文件,那个应用程序如何调用协议栈中的API接口函数呢,答案是应用程序通过SVC指令,解发蓝牙协议栈中的SVC中断,协议栈在SVC中断中通过服务号(执行SVC指令中的带入的一个数字)确定要执行的具体代码/接口/函数,

以ble_stack_init函数为例时行分析,在内部softdevice_enable函数,softdevice_enable又调用了sd_ble_enable接口。

当对sd_ble_enable按快捷键F12时,发现MDK提示找不到相关原型说明通知,

在工程中进行sd_ble_enable全局查找发现在ble.h文件中有相关定义:

SVCALL(SD_BLE_ENABLE, uint32_t, sd_ble_enable(ble_enable_params_t * p_ble_enable_params, uint32_t * p_app_ram_base));

同时,在nrf_svc.h中可以看到关到SVCALL宏的定义为:

#define SVCALL(number, return_type, signature) return_type __svc(number) signature

SVCALL(SD_BLE_ENABLE, uint32_t, sd_ble_enable(ble_enable_params_t * p_ble_enable_params, uint32_t * p_app_ram_base));

可以写成:

uint32_t __svc(SD_BLE_ENABLE) sd_ble_enable(ble_enable_params_t * p_ble_enable_params, uint32_t * p_app_ram_base)

这句话的作用让编译器在程序中看到sd_ble_enable接口时,产生一个SVC调用,SVC服务号为SD_BLE_ENABLE,并按uint32_t  sd_ble_enable(ble_enable_params_t * p_ble_enable_params, uint32_t * p_app_ram_base)形式把相关的参数存放地址放入到堆栈中,实现参数传递功能。这也是SVC调用的基本原理。

 

 

应用程序与协议栈之间的参数传递则是通过堆栈实现。协议栈与应用程序通信是通过SWI软中断来实现的。这里给大家介绍一个相关的博文地址:

https://blog.csdn.net/wulazula/article/details/80847262

 

文中源码资料下载,在公众号里给十三发消息:

下载|一起学nRF51xx 21

 

关注十三公众号

 

猜你喜欢

转载自blog.csdn.net/zhanglifu3601881/article/details/101171797
21