NRF52840开发笔记:定向直连

记录在 NRF52840 项目中使用定向直连广播模式实现设备定向连接的思路与代码。

一、需求

毕设项目里面需要实现在每个设备成功交换信息之后,从机需要选择一个综合指标最好的设备进行连接,一开始想的是在主机端进行处理,但大规模的设备组网对从机和主机的负担都很大。最简单最好的办法是从机使用定向直连的广播模式与目标主机设备进行连接。

二、代码实现

1. 广播初始化

SDK中的广播方式是递进的,顺序为 Direct High Duty -> Direct -> Fast -> Slow -> Idle。如果开启广播时选择 Direct 模式,即代码 ble_advertising_start(&m_advertising, BLE_ADV_MODE_DIRECTED),但是未在初始化时设置 Direct 模式的相关参数,那么它就会尝试 Fast 模式,如果初始化时 Fast 模式的相关信息也没设置,就会再尝试 Slow 模式,如果初始化时 Slow 模式相关信息也没设置最后就直接进入到 Idle 模式了。在我的广播初始化中,我只初始化了 Direct 和 Fast 两个模式的参数。

/**
 * @brief 用于初始化广告功能的函数.
 */
static void advertising_init(void)
{
    ret_code_t             err_code;
    ble_advertising_init_t init;
    // 发送功率值
    int8_t tx_power_value = -12;
    // service uuid
    ble_uuid_t adv_uuids[] = {{SNS_UUID_SERVICE, BLE_UUID_TYPE_BLE}};
    // 自定义广播数据
    uint8_t my_data[3] = {0x01, 0x02, 0x03};
    ble_advdata_manuf_data_t manuf_data;
    manuf_data.company_identifier = 0x0059;
    manuf_data.data.p_data = my_data;
    manuf_data.data.size = sizeof(my_data);

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

    // Advertising data: name, appearance, discovery flags, and more. 广播数据
    init.advdata.name_type               = BLE_ADVDATA_FULL_NAME;   // 广播名字信息
    init.advdata.include_appearance      = true;                    // 广播图标信息
    init.advdata.flags                   = BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE;   // 通用广播模式
    init.advdata.uuids_complete.uuid_cnt = sizeof(adv_uuids) / sizeof(adv_uuids[0]);
    init.advdata.uuids_complete.p_uuids  = adv_uuids;
    init.advdata.p_tx_power_level        = &tx_power_value;         // 显示的发送功率值
    init.advdata.p_manuf_specific_data   = &manuf_data;

    // 选择将使用的广告模式和时间间隔
    // 广播流程是Direct High Duty -> Direct -> Fast -> Slow -> Idle
    // interval 广播一次的间隔(uint 0.625ms)  timeout 广播持续事件(uint 10ms)
    init.config.ble_adv_directed_enabled  = true;
    init.config.ble_adv_directed_interval = 1600;
    init.config.ble_adv_directed_timeout  = 1800;
    init.config.ble_adv_fast_enabled  = true;
    init.config.ble_adv_fast_interval = 1600;
    init.config.ble_adv_fast_timeout  = 1800;

    // 广播事件的回调函数
    init.evt_handler = on_adv_evt;

    err_code = ble_advertising_init(&m_advertising, &init);
    APP_ERROR_CHECK(err_code);

    ble_advertising_conn_cfg_tag_set(&m_advertising, APP_BLE_CONN_CFG_TAG);

    // 实际修改的功率
    // BLE_GAP_TX_POWER_ROLE_ADV        = 1     广播角色
    // BLE_GAP_TX_POWER_ROLE_SCAN_INIT  = 2     扫描和发起者角色
    // BLE_GAP_TX_POWER_ROLE_CONN       = 3     连接角色
    err_code = sd_ble_gap_tx_power_set(BLE_GAP_TX_POWER_ROLE_ADV, m_advertising.adv_handle, tx_power_value);
    APP_ERROR_CHECK(err_code);
}

2. 广播回调函数

在程序的 while(1) 开始前,我调用 ble_advertising_start(&m_advertising, BLE_ADV_MODE_DIRECTED) 开启广播,在广播回调函数中会抛出 BLE_ADV_EVT_PEER_ADDR_REQUEST 的请求。然而在开始时,还没有获得需要连接的设备的对等地址,所以全局变量 direct_peer_adr 还未赋值,则不会执行 ble_advertising_peer_addr_reply(&m_advertising, &direct_peer_adr)。此时会跳过 Direct 模式的广播,直接进入 Fast 广播模式。

/**
 * @brief 用于处理广告事件的功能,广播观察者回调函数
 * @param[in] ble_adv_evt  广告活动.
 */
static void on_adv_evt(ble_adv_evt_t ble_adv_evt)
{
    switch (ble_adv_evt)
    {
        // 开始直连模式的回调
        case BLE_ADV_EVT_DIRECTED:
        {
            NRF_LOG_INFO("Directed advertising.");
            bsp_board_led_on(ADVERTISING_LED);
        }break;
        // 直连模式下对等地址的请求
        case BLE_ADV_EVT_PEER_ADDR_REQUEST:
        {
            NRF_LOG_INFO("peer addr request.");
            if(direct_peer_adr.addr[5] != 0x00)
            {
                ret_code_t err_code;
                err_code = ble_advertising_peer_addr_reply(&m_advertising, &direct_peer_adr);
                APP_ERROR_CHECK(err_code);
            }
        }break;
        // 开始快速广播时的回调
        case BLE_ADV_EVT_FAST:
        {
            NRF_LOG_INFO("Fast advertising.");
            bsp_board_led_on(ADVERTISING_LED);
        } break;
        // 广播超时的回调
        case BLE_ADV_EVT_IDLE:
        {
            NRF_LOG_INFO("advertising time out.");
            bsp_board_led_off(ADVERTISING_LED);

        } break;

        default:
            // No implementation needed.
            break;
    }
}

下面是系统程序开始时,从机端输出的 RTT 日志信息:

从机端 RTT 日志

3. 扫描回调函数

对于从机而言,扫描到可连接的广播数据后,判断广播数据的对等设备是否是自己需要连接的,为了测试,我这里使用了简单的判断 if(p_adv->peer_addr.addr[0] < DEVICE_ID),如果满足条件,就把变量 direct_peer_adr 赋值为需要连接设备的对等地址。关闭后又打开广播是因为让广播从定向广播模式开始,在对等地址请求时会成功执行 ble_advertising_peer_addr_reply(&m_advertising, &direct_peer_adr) 而进入直连广播模式。

对于需要被连接的主机而言,从机发送定向广播后,会收到 directed 定向广播数据。需要注意的是,如果开启了扫描过滤,定向数据是不会出现在 NRF_BLE_SCAN_EVT_FILTER_MATCH 里的,而是出现在 NRF_BLE_SCAN_EVT_NOT_FOUND 里,这里我卡了很久!最后调用连接函数连接即可。此外,直连模式的广播是无法被 Sniffer 捕捉到的,因此我在没有收到直连数据时尝试过这个方法,后来才发现是在 NRF_BLE_SCAN_EVT_NOT_FOUND 里。

/**
 * @brief 扫描观察者回调函数
 * @param p_scan_evt
 */
static void scan_evt_handler(scan_evt_t const * p_scan_evt)
{
    ret_code_t err_code;
    ble_gap_evt_adv_report_t const * p_adv = p_scan_evt->params.filter_match.p_adv_report;
    ble_gap_scan_params_t    const * p_scan_param = p_scan_evt->p_scan_params;
    ble_gap_addr_t                   m_addr = p_adv->peer_addr;
    // uint8_t *data = NULL;
    // uint16_t dat_len;

    switch(p_scan_evt->scan_evt_id)
    {
        // 找到了过滤后的匹配项
        case NRF_BLE_SCAN_EVT_FILTER_MATCH:
        {
            // 可连接的
            if(p_adv->type.connectable)
            {
                NRF_LOG_INFO("receive connectable data!");
                NRF_LOG_INFO("find device id: %d", m_addr.addr[0]);
                if(p_adv->peer_addr.addr[0] < DEVICE_ID)
                {
                    NRF_LOG_INFO("turn to direct adv!");
                    direct_peer_adr.addr_type = m_addr.addr_type;
                    direct_peer_adr.addr_id_peer = m_addr.addr_id_peer;
                    strcpy((char *)direct_peer_adr.addr, (const char *)m_addr.addr);
                    controlAdvertise(CLOSE);
                    controlAdvertise(OPEN);
                    controlScan(CLOSE);
                }
            }

        } break;
        // 不匹配
        case NRF_BLE_SCAN_EVT_NOT_FOUND:
        {
            // 定向的
            if(p_adv->type.directed)
            {
                NRF_LOG_INFO("receive directed data!");
                NRF_LOG_INFO("find device id: %d", m_addr.addr[0]);
                // Initiate connection. 开始连接
                // 参数为目标MAC地址, 扫描参数, 连接参数, 连接配置标签, 这些参数均可以在扫描初始化参数部分获取.
                // 所及基本只需要得到目标的MAC地址就可以进行连接
                err_code = sd_ble_gap_connect(&p_adv->peer_addr,
                                            p_scan_param,
                                            &m_scan.conn_params,
                                            APP_BLE_CONN_CFG_TAG);
                APP_ERROR_CHECK(err_code);
            }
        }break;
        // 扫描超时
        case NRF_BLE_SCAN_EVT_SCAN_TIMEOUT:
        {
            NRF_LOG_INFO("scan timeout.");
            bsp_board_led_off(SCANNING_LED);
        }break;

        default:
            break;
    }
}

下面是直连时,主机和从机输出的 RTT 日志信息,从机通过判断条件设置了需要连接的对等地址,并重新开始广播使得定向广播成功开启。主机在扫描回调函数中,匹配项里发现了可连接非定向的数据,未匹配项里发现了定向数据,选择连接,并发现了自定义的 sns 服务。

主机输出:

主机输出 RTT 日志

从机输出:

从机输出 RTT 日志

使用 Hugo 构建
主题 StackJimmy 设计