一、nRF52840蓝牙芯片简介
自从nordic在2018年强势推出nRF52840这颗重磅级芯片后,蓝牙5.0技术开始在业界流行起来,随后蓝牙5.0技术开始成为了各大品牌的旗舰手机标配功能。
1. 芯片优势
这个芯片最强大的优势就是低速远距离模式,及LE_CODED。
官方宣称最远能实现300米的传输距离,在开启低速(125kbps/s)远距离模式的情况下,并把发射功率调到最大8db。
但是更让人吃惊的人有一次我和同事用在某宝买来某蓝牙模块商家自己设计的52840蓝牙开发板进行远距离测试,我们的测试方法是用两块开发板一块用来当从机角色,以200ms的广播间隔不停的对外广播,另一块用来当主机角色,不停的扫描蓝牙5的广播包,扫描成功后就用串口把数据发送到笔记本的串口调试助手里。
通过在比较空旷的地带,我们实现了最远400多米的距离还能断断续续的扫描到蓝牙5的广播包。
2. 那么蓝牙的5.0远距离是怎么实现的呢?
答案是编码的方式!
通过一个二分之一码率和八分之一码率的代码,其中,二分之一码率的代码在500kbps下,提供了大约4-5dB的灵敏度以及接近两倍的范围;至于八分之一码率的代码则可以125kbps下达到12dB的灵敏度,并将传输距离扩大至四倍。
这种实现远距离传输的方式与LORA扩频有异曲同工之妙!
蓝牙天线一般可以采用板载PCB天线,贴片陶瓷天线,和棒状天线。其中PCB天线天线实现起来成本最低,是目前普遍的一种实现方式,而贴片陶瓷天线是最节省模块尺寸面积的一种方式,棒状天线是信号增益和数据传输最可靠的一种方式,各位可以根据自己的实际需求选择适合自己应用场景的天线种类。
3. 芯片特点:
nRF52840芯片特点:
● 支持Thread无线通信协议
● 支持蓝牙5.0,支持多协议
● 支持蓝牙5.0的数据速率:2Mbs,1Mbs,500kbs,125kbs
● 32位ARM Cortex M4F 处理器,64MHz时钟
● 高速2Mbs数据速率
● 高达104dB的链路预算
● 全速12Mbs USB控制器
● 片上NFC-A标签
● 独立于协议栈之外的应用程序开发
● 输出功率-20dBm ~ 8dBm
● 接收灵敏度-96dBm
● 与nRF52、nRF51、nRF24L、nRF24AP系列空中包兼容
● ARM TrustZone Cryptocell 310密码加速器
● 宽供电电压范围:1.7v ~ 5.5v
● SPI/UART/PWM接口
● 32M高速SPI接口
● 所有的数字接口都支持EasyDMA
● 12bit/200k SPS ADC
● 128位AES/ECB/CCM/AAR协处理器
● 单端天线输出(片上balun)
● 片上DC-DC降压变换器
● 正交解码器(QDEC)
● 所有的外设模块都支持独立电源管理
● 为外部元器件提供高达25mA的稳定电流
nRF52840是一种先进的、高度灵活的单芯片解决方案,它的设计为蓝牙5的主要功能进步做好准备,并利用了蓝牙5增强的性能,包括远程和高性能。
nRF52840为CortexTM-M系列增加了最佳的安全级别,并配备了片上ARM密码加速器。
nRF52840采用与现有nRF 52系列SoC相同的硬件和软件架构。其核心是ARM Cortex-m4处理器,它允许更快、更高效地计算复杂。
功能的DSP和那些需要浮点数学。FLASH和RAM都有广泛的内存可用度,分别为1MB/256KB。
Cortex-M4与浮点和内存可用性的结合为真正的单芯片应用程序提供不错的功能。片内集成全速(12Mbps)USB2.0控制器。
广泛的外围设备可利用能够与许多高性能的数字接口,如高速SPI(32MHz)和四电平SPI(32MHz),允许直接接口到显示器和外部存储源。
nRF52840从5.5V到1.7V供电电压,允许从充电电池和USB电源直接供电。NRF52840已经准备好利用蓝牙5规范的到来对蓝牙LE的相当大的性能改进。
最重要的是支持范围更广(与蓝牙4.x相比最多可达x4),空中数据速率翻了一番,从蓝牙4.x中的1Mbps提高到2Mbps。
无线网络支持802.15.4物理层和MAC层,适合于使用802.15.4的其他堆栈,如Zigbee。它既支持蓝牙5的远程功能,也支持蓝牙5的远程功能。
还有802.15.4,它已经是家庭网络协议的一种流行技术。
最大输出功率为8dBm,可实现>111dBm的总链路预算。NRF52840具有片上ARMCryptoCell310密码硬件加速器。
CryptoCell提供了广泛的密码和安全功能,用于将实体安全构建到应用程序中从头到尾。
与CPU在软件中执行的等效操作相比,使用CryptoCell还可以使相关的安全操作运行得更快,使用的处理时间和功率也更少。
4. 内部模块图:
5. 芯片引脚参考电路
以上是nordic官方提供蓝牙最小系统原理图,这里需要特别强调的是这颗芯片供电方式。52840内部集成了高性能的电源管理芯片PMU和电压调节器。
可以支持LDO和DC-DC方式对CPU和射频部分进行供电,LDO方式电压纹波最小能带来更好的射频性能,但是其电源效率较低,不适合对功耗要求特别高的场合。
DC-DC方式正好弥补了LDO方式的缺点,但是其纹波较大,但是在芯片外围需要添加LC振荡电路进行滤波。
官方提供的工程代码框架默认使用的是LDO供电方式。以下就是52840电源模块示意图:
6. MQ-2气体传感器
芯片参数:
下图显示的是整个产品模块功能示意图,电路结构较为简单,便于大规模普及。
nRF52840气体传感器上面集成了MQ-2气体传感器,可以用来检测工业区和居民小区周围可能泄露的可燃性气体,然后通过串口通信把气体数据传输给nRF52840芯片,然后nRF52840芯片通过蓝牙5.0数据帧把气体数据按照一定的格式广播出去,手机APP或者蓝牙报警设备就可以随时检测到气体数据,并发出报警功能,从而避免更大的安全事故发生。
MQ气体传感器使用的气敏材料是在清洁空气中电导率较低的二氧化锡(SnO2)。
当传感器所处环境中存在可疑气体时,传感器的电导率随空气中可燃气体浓度的增加而增大。
使用简单的电路即可将电导率的变化转换为与该气体浓度相对应的输出信号。MQ气体传感器对甲烷的灵敏度高,对丙烷、丁烷也有较好的灵敏度。这种传感器可检测多种可燃性气体,特别是天然气,是一款适合多种营养的低成本传感器。
7. 气体传感器工作模式介绍
由于传感器采用了100MA的纽扣电池供电, NRF 52840作为主控芯片每1S读取5次传感器的数据,通过一定的滤波算法,计算出正确的数据。当52840不采集数据的时候,也自动进入休眠模式,从而达到节省功耗的目的。
正常情况下,52840每5分中通过串口接收气体传感器的数据,再以广播的形式把数据广播出去,由于要保证手机能扫描到52840的数据,因此广播次数不能随便设,次数设多了,功耗又大了,次数设少了,手机有时又不能扫描到,这与手机开启的扫描占空比有关,还有与扫描通道有关,蓝牙共有3个广播通道,一次只能扫一个,所以存在一定的概率。
经过综合测试,我们决定正常情况下广播次数设为30次,异常情况下,广播次数设为50次。
二、蓝牙协议栈工作流程介绍
1. BLE优势
自从蓝牙4.0BLE诞生以来,以蓝牙为基础的物联网设备开启了爆发式增长,其原因在于BLE具备下面几点优势:
- BLE具备相当于WIFI 10分之1的极低功耗,一颗纽扣电池就能维持设备工作很长时间。
- BLE芯片成本很低,其价格已经做到和MCU相当了,这大大的促进了BLE设备的快速增长。
- BLE通信和工作稳定性极强,由于蓝牙超强的跳频机制,可以让BLE轻松避开被干扰了的信道,保持高速通信。
- BLE通信距离远,BLE特别是到了蓝牙5.0时代,通信距离达到了400米,比一般的WIFI距离都远。
- BLE通信速度有了明显的提升,特别是到了蓝牙5.0时代,通信速度达到了2Mb/s,这个速度可以传输音频信号了。
2. BLE启动工作流程顺序如下:
- 启动后进入main函数,首先初始化串口,波特率为9600,用来和MQ-2进行通信。
- log打印初始化,软件定时器初始化,然后蓝牙协议栈初始化,主要是完成协议相关标签设置,以及RAM初始地址设置,使能协议栈,开启蓝牙观察者模式。
- GAP角色初始化,里面进行了蓝牙开放连接模式设置,广播器件名设置,然后最小连接间隔设为10MS,最大连接间隔设为50MS,从机延迟设为0,连接超时设为1S。
- GATT初始化,GATT是传输层,用来传输真正的数据,主要是进行MTU最大传输数据单元的设置,蓝牙4.0是31个字节,而蓝牙5.0可以设置253个字节,大大的提升了数据的吞吐量。
- 服务的初始化,这里添加了串口收发服务,向蓝牙协议栈注册了串口数据接收回调函数。
- 连接参数更新初始化,因为连接参数主机和从机是可以协商的,
比如有大数据量要传输的时候,主机就可以发起参数更新请求,从机根据自己的配置,可以响应或者拒绝主机的更新请求,初始化工作主要做了更新连接参数的相关规则,比如首次更新连接参数时间,第二次更新连接参数时间,最大协商更新连接参数次数。接着是发射功率设置,由于工业区周围环境非常复杂,干扰性很大,干扰会造成距离的大大的降低,所以我们采用了最大的发射功率为8db,空旷地带测试,传输距离可达400M,完全满足大部分客户的使用场景要求。接着是开启低速远距离广播模式,模式设为通用可发现模式。
蓝牙的大部分应用都是以服务的形式提供的,比如心率采集服务,血压采集服务,温湿度采集服务,每个服务都有一个唯一的128位UUID,UUID可以是蓝牙联盟定义的,也可以是芯片厂商定义的,还可以是个人定义的,以来区分服务的内容,以便接收方能正确的解析服务数据。
这点和can2.0B很相似,都是采用协议数据单元PDU。接着是广播初始化,广播模式设为快速广播,广播间隔设为100ms,注册广播超时回调函数,设置物理层的广播速度为1M,蓝牙协议规定,广播速度只能设为1M,但连接后传输速度可以设为2M。
3. 蓝牙建立过程
上图显示的是蓝牙建立连接的过程。
4. 蓝牙协议栈启动过程
上图显示的是蓝牙协议栈启动流程,本次代码工程框架使用的协议栈版本是FLASH_S140_NRF52_7.01_SOFTDEVICE.工程版本使用的是SDK17.0。
这方面的资料大家可以去NORDIC官网进行下载,或者后台回复【nRF52840】。
四、代码实现
下面开始介绍52840部分关键性的代码
1. 启动流程
芯片启动流程流程如下:
int main(void)
{
bool erase_bonds;
// Initialize.
uart_init();
log_init();
timers_init();
buttons_leds_init(&erase_bonds);
power_management_init();
ble_stack_init();
gap_params_init();
gatt_init();
services_init();
advertising_init();
conn_params_init();
sd_ble_gap_tx_power_set(BLE_GAP_TX_POWER_ROLE_ADV,m_advertising.adv_handle,8);
// Start execution.
// printf("\r\nUART started.\r\n");
NRF_LOG_INFO("Debug logging for UART over RTT started.");
advertising_start();
nrf_gpio_cfg_input(6,NRF_GPIO_PIN_PULLUP);
// Enter main loop.
for (;;)
{
if(start_adv_flag == 0xa5)
{
start_adv_flag = 0x5a;
(void)sd_ble_gap_adv_stop(m_advertising.adv_handle);
nrf_uart_disable(NRF_UART0);
advertising_init();
advertising_start();
}
if(start_adv_flag == 0x5a)
{
idle_state_handle();
}
else
{
if(nrf_gpio_pin_read(6) == 1)
{
nrf_gpio_cfg_sense_input(6,NRF_GPIO_PIN_PULLUP,NRF_GPIO_PIN_SENSE_LOW);
sd_power_system_off();
}
}
}
}
以上这段代码循环处理动态的向外广播气体传感器数据,
if(start_adv_flag == 0xa5) 这个变量是uart_event_handle 串口接收中断里进行赋值的,因为52840和气体传感器通过串口进行相连,当此变量等于0xa5时,说明传感器又发来数据了,
(void)sd_ble_gap_adv_stop(m_advertising.adv_handle); 这句的意思是停止当前的广播,
nrf_uart_disable(NRF_UART0);这句的意思是暂时关了串口中断,
advertising_init();这句的意思是重新配置广播数据,其中就包含在uart_event_handle串口中断中接收的传感器数据。
advertising_start();然后就开始新的广播。
2. 核心函数详解
1)advertising_init()
功能:
此函数主要实现的是如何设置蓝牙的广播数据内容,
company_identifier:代表的是厂商ID,可以自己随意设置
adv_interval:表示的是广播间隔
adv_duration:表示的是广播超时
ble_adv_primary_phy:表示的是主广播物理通道
ble_adv_extended_enabled = true;表示的是开启蓝牙5.0扩展广播。
本次设置的是BLE_GAP_PHY_CODED,即低速远距离编码模式。
ble_adv_secondary_phy= BLE_GAP_PHY_CODED 表示的是第二广播物理通道。
/**@brief Function for initializing the Advertising functionality.
*/
static void advertising_init(void)
{
uint32_t err_code;
ble_advertising_init_t init;
memset(&init, 0, sizeof(init));
init.advdata.name_type = BLE_ADVDATA_FULL_NAME;
init.advdata.include_appearance = false;
init.advdata.flags = BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE;
m_manuf_data.company_identifier = company_identifier; //0xaa55;
m_manuf_data.data.size = manuf_data_size;
m_manuf_data.data.p_data = manuf_data;
init.advdata.p_manuf_specific_data = &m_manuf_data;
// init.srdata.uuids_complete.uuid_cnt = sizeof(m_adv_uuids) / sizeof(m_adv_uuids[0]);
// init.srdata.uuids_complete.p_uuids = m_adv_uuids;
init.config.ble_adv_fast_enabled = true;
init.config.ble_adv_fast_interval = adv_interval;
init.config.ble_adv_fast_timeout = adv_duration;
init.config.ble_adv_primary_phy = BLE_GAP_PHY_CODED; //BLE_GAP_PHY_1MBPS;
init.config.ble_adv_secondary_phy = BLE_GAP_PHY_CODED; //BLE_GAP_PHY_2MBPS;
init.config.ble_adv_extended_enabled = true;
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);
}
设置完成后,就可以使用支持蓝牙5.0的智能手机使用nrf_connectAPP进行扫描广播包,我们测试用的是华为Mate20,几乎支持蓝牙5.0的所有特性。
此函数主要设置的是蓝牙器件名,连接间隔和从机延时等等相关的参数,设置完成后,也可以使用nrf_connect APP进行查看。
2)services_init()
功能:
此函数实现的是蓝牙服务初始化,向蓝牙协议栈底层注册了nrf_qwr_error_handler和nus_data_handler这两个回调函数,其中nus_data_handler是一个非常核心的函数,当52840接收到蓝牙数据时,此回调函数立即被调用,协议栈底层会上抛出应用数据的指针,以及数据的长度。
/**@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;
nrf_ble_qwr_init_t qwr_init = {0};
// Initialize Queued Write Module.
qwr_init.error_handler = nrf_qwr_error_handler;
err_code = nrf_ble_qwr_init(&m_qwr, &qwr_init);
APP_ERROR_CHECK(err_code);
// Initialize NUS.
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);
}
3)nus_data_handler()
功能:
函数里进行解析相关的功能。
具体如何解析由手机APP发送的数据格式决定,也就是开发者制定的应用层协议。
/**@brief Function for handling the data from the Nordic UART Service.
*
* @details This function will process the data received from the Nordic UART BLE Service and send
* it to the UART module.
*
* @param[in] p_evt Nordic UART Service event.
*/
/**@snippet [Handling the data received over BLE] */
static void nus_data_handler(ble_nus_evt_t * p_evt)
{
if (p_evt->type == BLE_NUS_EVT_RX_DATA)
{
uint32_t err_code;
NRF_LOG_DEBUG("Received data from BLE NUS. Writing data on UART.");
NRF_LOG_HEXDUMP_DEBUG(p_evt->params.rx_data.p_data, p_evt->params.rx_data.length);
for (uint32_t i = 0; i < p_evt->params.rx_data.length; i++)
{
do
{
err_code = app_uart_put(p_evt->params.rx_data.p_data[i]);
if ((err_code != NRF_SUCCESS) && (err_code != NRF_ERROR_BUSY))
{
NRF_LOG_ERROR("Failed receiving NUS message. Error 0x%x. ", err_code);
APP_ERROR_CHECK(err_code);
}
} while (err_code == NRF_ERROR_BUSY);
}
if (p_evt->params.rx_data.p_data[p_evt->params.rx_data.length - 1] == \'\r\')
{
while (app_uart_put(\'\n\') == NRF_ERROR_BUSY);
}
}
}
4) ble_evt_handler()
功能:
此函数主要用来检测蓝牙的连接或断开等相关事件。
/**@brief Function for handling BLE events.
*
* @param[in] p_ble_evt Bluetooth stack event.
* @param[in] p_context Unused.
*/
static void ble_evt_handler(ble_evt_t const * p_ble_evt, void * p_context)
{
uint32_t err_code;
switch (p_ble_evt->header.evt_id)
{
case BLE_GAP_EVT_CONNECTED:
NRF_LOG_INFO("Connected");
err_code = bsp_indication_set(BSP_INDICATE_CONNECTED);
APP_ERROR_CHECK(err_code);
m_conn_handle = p_ble_evt->evt.gap_evt.conn_handle;
err_code = nrf_ble_qwr_conn_handle_assign(&m_qwr, m_conn_handle);
APP_ERROR_CHECK(err_code);
break;
case BLE_GAP_EVT_DISCONNECTED:
NRF_LOG_INFO("Disconnected");
// LED indication will be changed when advertising starts.
m_conn_handle = BLE_CONN_HANDLE_INVALID;
break;
case BLE_GAP_EVT_PHY_UPDATE_REQUEST:
{
NRF_LOG_DEBUG("PHY update request.");
ble_gap_phys_t const phys =
{
.rx_phys = BLE_GAP_PHY_AUTO,
.tx_phys = BLE_GAP_PHY_AUTO,
};
err_code = sd_ble_gap_phy_update(p_ble_evt->evt.gap_evt.conn_handle, &phys);
APP_ERROR_CHECK(err_code);
} break;
case BLE_GAP_EVT_SEC_PARAMS_REQUEST:
// Pairing not supported
err_code = sd_ble_gap_sec_params_reply(m_conn_handle, BLE_GAP_SEC_STATUS_PAIRING_NOT_SUPP, NULL, NULL);
APP_ERROR_CHECK(err_code);
break;
case BLE_GATTS_EVT_SYS_ATTR_MISSING:
// No system attributes have been stored.
err_code = sd_ble_gatts_sys_attr_set(m_conn_handle, NULL, 0, 0);
APP_ERROR_CHECK(err_code);
break;
case BLE_GATTC_EVT_TIMEOUT:
// Disconnect on GATT Client timeout event.
err_code = sd_ble_gap_disconnect(p_ble_evt->evt.gattc_evt.conn_handle,
BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
APP_ERROR_CHECK(err_code);
break;
case BLE_GATTS_EVT_TIMEOUT:
// Disconnect on GATT Server timeout event.
err_code = sd_ble_gap_disconnect(p_ble_evt->evt.gatts_evt.conn_handle,
BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
APP_ERROR_CHECK(err_code);
break;
default:
// No implementation needed.
break;
}
}
5)uart_event_handle()
功能:
uart_event_handle也是一个常用的回调函数,在串口初始化时对协议栈底层进行注册,当52840串口接收到数据时,此回调函数立即被调用。
大家可以在里面添加解析串口命令相关的功能代码,其实完全可以把它当作STM32串口中断服务函数来用。
只是特别说明的是52840的串口设计得更强大一些,带有256字节的硬件FIFO BUFF,而且支持Easy DMA进行数据搬移,这大大的减轻了CPU 内核的负担,节省了功耗。
所谓Easy DMA就是串口专用的DMA,只是不能用来传输flash的数据。
/**@brief Function for handling app_uart events.
*
* @details This function will receive a single character from the app_uart module and append it to
* a string. The string will be be sent over BLE when the last character received was a
* \'new line\' \'\n\' (hex 0x0A) or if the string has reached the maximum data length.
*/
/**@snippet [Handling the data received over UART] */
void uart_event_handle(app_uart_evt_t * p_event)
{
static uint8_t data_array[BLE_NUS_MAX_DATA_LEN];
static uint8_t index = 0;
uint32_t err_code;
switch (p_event->evt_type)
{
case APP_UART_DATA_READY:
UNUSED_VARIABLE(app_uart_get(&data_array[index]));
if(data_array[0] == 0xfa)
{
index++;
if((data_array[1] == 0x01) && (data_array[2] > 3) && (index >= 12))
{
if(data_array[2] > 10)
{
adv_duration = data_array[2] * 19;
adv_interval = 320;
}
else
{
adv_duration = data_array[2] * 9;
adv_interval = 160;
}
company_identifier = ((uint16_t)*(data_array + 3) << 8) | *(data_array + 4) ;
memcpy(manuf_data,data_array + 5,6);
memset(data_array,0,12);
index = 0;
start_adv_flag = 0xa5;
}
}
if(index >= 12)
{
memset(data_array,0,12);
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;
}
}
void uart_event_handle(app_uart_evt_t * p_event), 这个函数是串口接收中断回调函数,由NRF串口底层驱动库上抛的,
APP_UART_DATA_READY,表示串口数据已经准备好
UNUSED_VARIABLE(app_uart_get(&data_array[index])); 表示读取串口数据,并放到数组data_array,每读取一次,index+1次,此中断回调函数,每收到一个字节,就会被调用一次。
if(data_array[0] == 0xfa) 解析协议传感器数据头,传感器串口发送过来的数据是以0xfa开头的,后面就是长度,传感器数据,和校验码。
adv_duration代表广播数次数,具体广播多少次由传感器发送过来的数据指定的,一般是数据比较异常,广播次数就会比较多,
adv_interval = 320; 这个是设置广播间隔,数值越大,越省电。company_identifier表示公司ID,也会体现在广播数据内容里,手机APP可以观察到。
memcpy(manuf_data,data_array + 5,6);这句是提取数组里的传感器数据到manuf_data里,然后广播初始化的时候会提取manuf_data这个变量的数据,
start_adv_flag = 0xa5;启动新的广播标致,主循环里会不断检查这个标致。
if(index >= 12) index = 0; 由于协议数据长度是12个字节,接收完成了,就要清0。
文中项目完整代码、用到的蓝牙调试程序,公众号后台回复:【nrf52840】即可获取。