前言

这一次我们尝试创建一个串口服务来实现手机与开发板的数据透传,其实无非就跟学习创建LED读写服务的时候一样创建一个私有服务而已。废话少说,我们直接开始实验。

实验分析

首先,我们要知道NRF51822的串口通信引脚:RX:P0.11,TX:P0.9,RTS:P0.10,CTS:P0.8。

接着,新建串口驱动文件:ble_nus.c和ble_nus.h

java 蓝牙连接串口传输数据_java 蓝牙连接串口传输数据

                

然后,写入串口初始化函数并添加到main函数中:

/**@brief  Function for initializing the UART module.
 */
/**@snippet [UART Initialization] */
static void uart_init(void)
{
    uint32_t                     err_code;
    const app_uart_comm_params_t comm_params =
    {
        RX_PIN_NUMBER,
        TX_PIN_NUMBER,
        RTS_PIN_NUMBER,
        CTS_PIN_NUMBER,
        APP_UART_FLOW_CONTROL_DISABLED,
        false,
        UART_BAUDRATE_BAUDRATE_Baud38400
    };//配置串口参数

    APP_UART_FIFO_INIT( &comm_params,
                       UART_RX_BUF_SIZE,
                       UART_TX_BUF_SIZE,
                       uart_event_handle,
                       APP_IRQ_PRIORITY_LOW,
                       err_code);
    APP_ERROR_CHECK(err_code);//配置串口FIFO缓冲,并初始化串口
}
/**@snippet [UART Initialization] */
/**@brief UART communication structure holding configuration settings for the peripheral.
 */
typedef struct
{
    uint8_t                 rx_pin_no;    /**< RX pin number. */
    uint8_t                 tx_pin_no;    /**< TX pin number. */
    uint8_t                 rts_pin_no;   /**< RTS pin number, only used if flow control is enabled. */
    uint8_t                 cts_pin_no;   /**< CTS pin number, only used if flow control is enabled. */
    app_uart_flow_control_t flow_control; /**< Flow control setting, if flow control is used, the system will use low power UART mode, based on CTS signal. */
    bool                    use_parity;   /**< Even parity if TRUE, no parity if FALSE. */
    uint32_t                baud_rate;    /**< Baud rate configuration. */
} app_uart_comm_params_t;

这里我们可以看到这里定义了一个app_uart_comm_params_t类型的变量,这个类型在app_uart.h文件内,是官方提供用户的初始化参数结构体,提高开发效率。这里我们在路径中包含:项目目录\components\libraries\uart。

串口设置里,首先配置串口端口,波特率等串口参数,再使用函数APP_UART_FIFO_INIT配置串口缓冲并且初始化串口,APP_UART_FIFO_INIT函数中调用了app_uart_init初始化函数,把我们配置的串口参数传导设置中,函数具体如下,有4个形参,分别为1:串口通用参数;2:串口缓冲;3:串口中断事件;4:串口中断优先级;

uint32_t app_uart_init(const app_uart_comm_params_t * p_comm_params,
                             app_uart_buffers_t *     p_buffers,
                             app_uart_event_handler_t event_handler,
                             app_irq_priority_t       irq_priority)
{
    uint32_t err_code;

    m_event_handler = event_handler;

    if (p_buffers == NULL)
    {
        return NRF_ERROR_INVALID_PARAM;
    }

    // Configure buffer RX buffer.
    err_code = app_fifo_init(&m_rx_fifo, p_buffers->rx_buf, p_buffers->rx_buf_size);
    if (err_code != NRF_SUCCESS)
    {
        // Propagate error code.
        return err_code;
    }

    // Configure buffer TX buffer.
    err_code = app_fifo_init(&m_tx_fifo, p_buffers->tx_buf, p_buffers->tx_buf_size);
    if (err_code != NRF_SUCCESS)
    {
        // Propagate error code.
        return err_code;
    }

    nrf_drv_uart_config_t config = NRF_DRV_UART_DEFAULT_CONFIG;
    config.baudrate = (nrf_uart_baudrate_t)p_comm_params->baud_rate;
    config.hwfc = (p_comm_params->flow_control == APP_UART_FLOW_CONTROL_DISABLED) ?
            NRF_UART_HWFC_DISABLED : NRF_UART_HWFC_ENABLED;
    config.interrupt_priority = irq_priority;
    config.parity = p_comm_params->use_parity ? NRF_UART_PARITY_INCLUDED : NRF_UART_PARITY_EXCLUDED;
    config.pselcts = p_comm_params->cts_pin_no;
    config.pselrts = p_comm_params->rts_pin_no;
    config.pselrxd = p_comm_params->rx_pin_no;
    config.pseltxd = p_comm_params->tx_pin_no;

    err_code = nrf_drv_uart_init(&config, uart_event_handler);

    if (err_code != NRF_SUCCESS)
    {
        return err_code;
    }

    nrf_drv_uart_rx_enable();
    return nrf_drv_uart_rx(rx_buffer,1);
}

接下来,就是协议栈相关初始化。首先,在services_init函数中添加自己的服务:

/**@brief Function for initializing services that will be used by the application.
 */
static void services_init(void)
{
    uint32_t       err_code;
    ble_nus_init_t nus_init;
    
    memset(&nus_init, 0, sizeof(nus_init));

    nus_init.data_handler = nus_data_handler;
    
    err_code = ble_nus_init(&m_nus, &nus_init);
    APP_ERROR_CHECK(err_code);
}

在这里,我们将nus_data_handler作为串口传输处理句柄,作为回调函数。这个函数用来处理串口UART发送数据和从接收串口端来的数据。这个句柄函数也非常简单,直接调用外设函数:

/**@snippet [Handling the data received over BLE] */
static void nus_data_handler(ble_nus_t * p_nus, uint8_t * p_data, uint16_t length)
{
    for (uint32_t i = 0; i < length; i++)
    {
        while(app_uart_put(p_data[i]) != NRF_SUCCESS);
    }
    while(app_uart_put('\n') != NRF_SUCCESS);
}
/**@snippet [Handling the data received over BLE] */

然后添加串口蓝牙服务声明ble_nus_init函数。

接着,我们来设计服务驱动文件:

ble_nus.h:
/**@brief Nordic UART Service structure. *
 * @details This structure contains status information related to the service.
 */
struct ble_nus_s
{
    uint8_t                  uuid_type;               /**< UUID type for Nordic UART Service Base UUID. */
    uint16_t                 service_handle;          /**< Handle of Nordic UART Service (as provided by the S110 SoftDevice). */
    ble_gatts_char_handles_t tx_handles;              /**< Handles related to the TX characteristic (as provided by the S110 SoftDevice). */
    ble_gatts_char_handles_t rx_handles;              /**< Handles related to the RX characteristic (as provided by the S110 SoftDevice). */
    uint16_t                 conn_handle;             /**< Handle of the current connection (as provided by the S110 SoftDevice). BLE_CONN_HANDLE_INVALID if not in a connection. */
    bool                     is_notification_enabled; /**< Variable to indicate if the peer has enabled notification of the RX characteristic.*/
    ble_nus_data_handler_t   data_handler;            /**< Event handler to be called for handling received data. */
};

uint32_t ble_nus_init(ble_nus_t * p_nus, const ble_nus_init_t * p_nus_init);
void ble_nus_on_ble_evt(ble_nus_t * p_nus, ble_evt_t * p_ble_evt);
uint32_t ble_nus_string_send(ble_nus_t * p_nus, uint8_t * p_string, uint16_t length);
这里用到一个ble_nus_t结构体指针,这个类型用于引用这个服务实例;ble_nus_init_t用于初始化参数;ble_evt是常见的事件类型,在官方ble.h头文件中定义。
ble_nus_string_send用于串口连接状态变化,并进行反馈。
这里再着重看一下ble_nus_t结构体中添加ble_gatts_char_handles_t类型的tx_handles和rx_handles特征值操作句柄:
/**@brief GATT Characteristic Definition Handles. */typedef struct
{
  uint16_t          value_handle;       /**< 处理的特征值. */
  uint16_t          user_desc_handle;   /**< 用户描述句柄:句柄向用户说明,或ble_gatt_handle_invalid不存在. */
  uint16_t          cccd_handle;        /**< 客户的特征描述符配置(CCCD)句柄. */
  uint16_t          sccd_handle;        /**< 服务器的特征描述符配置(SCCD)句柄. */
} ble_gatts_char_handles_t;


客户端特性配置描述符(CCCD),这个描述符是给任何支持通知或指示功能的特性额外增加的。在CCCD中写入“1”使能通知功能,写入“2”使能指示功能,写入“0”同时禁止通知和指示功能。

一个特性至少包含2个属性:一个属性用于声明,一个属性用于存放特性的值。

所有通过GATT服务传输的数据必须映射成一些列的特性,可以把特性中的这些数据看成是一个个捆绑起来的数据,每个特性就是一个自我包容而独立的数据点。例如,如果几个数据总是一起变化,那么我们可以把他们集中在一个特性里。

任何在特性中的属性不是定义为属性值就是描述符。描述符是一个额外的属性以提供更多特性的信息,它提供一个用户可以识别的特性描述的实例。

接着,我们在应用层中把串口的事件处理函数加入ble_evt_dispatch函数中:

static void ble_evt_dispatch(ble_evt_t * p_ble_evt)
{
    ble_conn_params_on_ble_evt(p_ble_evt);
    ble_nus_on_ble_evt(&m_nus, p_ble_evt);
    on_ble_evt(p_ble_evt);
    ble_advertising_on_ble_evt(p_ble_evt);
    bsp_btn_ble_on_ble_evt(p_ble_evt);
    
}

最后,我们将uart_init注册的uart_event_handle函数实体加上:

//timer handler
uint8_t clean_index_cnt = 0;
uint8_t uart_index = 0;
static void time_timeout_handler(void *p_context)
{
	UNUSED_PARAMETER(p_context);
	if(clean_index_cnt < 20)
		clean_index_cnt++;
	if(clean_index_cnt >= 10)
	{
		uart_index = 0;
	}
}

static bool checksum(uint8_t *pbuffer)
{
	uint8_t i;
	uint8_t sum = 0;
	for(i=0;i<(pbuffer[1]+2);i++)
	{
		sum += pbuffer[i];
	}
	if(sum == pbuffer[(pbuffer[1]+2)])
		return true;
	else
		return false;
}
void uart_event_handle(app_uart_evt_t * p_event)
{
    static uint8_t data_array[BLE_NUS_MAX_DATA_LEN];
    uint32_t       err_code;

    switch (p_event->evt_type)
    {
        case APP_UART_DATA_READY:
            UNUSED_VARIABLE(app_uart_get(&data_array[uart_index]));
            uart_index++;

			clean_index_cnt = 0;
			if(uart_index == (data_array[1] + 3))
			{
				if(checksum(data_array))
				{
					err_code = ble_nus_string_send(&m_nus, data_array, uart_index);
					if (err_code != NRF_ERROR_INVALID_STATE)
					{
						APP_ERROR_CHECK(err_code);
					}
				}
				uart_index = 0;
			}
            break;

        case APP_UART_COMMUNICATION_ERROR:
            APP_ERROR_HANDLER(p_event->data.error_communication);
            break;

        case APP_UART_FIFO_ERROR:
            APP_ERROR_HANDLER(p_event->data.error_code);
            break;

        default:
            break;
    }
}

APP_TIMER_DEF(m_timer_id);
static void timers_init(void)
{
	uint32_t err_code;
	
	APP_TIMER_INIT(APP_TIMER_PRESCALER, APP_TIMER_OP_QUEUE_SIZE, false);
	err_code = app_timer_create(&m_timer_id, APP_TIMER_MODE_REPEATED, time_timeout_handler);
	APP_ERROR_CHECK(err_code);
}

这里,我为了可以兼容项目的数据帧,将处理函数加了一些处理,同时开一个定时器做串口的超时处理。

定时器在main函数中启动:

java 蓝牙连接串口传输数据_串口_02

到这里,就已经完成了串口服务的所有操作。

结果验证

  • 手机连接蓝牙,我们可以看到一个UUID为FFF0的服务
  • 这个服务包含notity和write特性
  • 打开write特性,往特性里写入任意小于20个字节的数据,可以发现PC的串口调试助手正常收到数据

总结

通过这个实验,我们学会了如何开发一个蓝牙串口透传功能。