之前写过蓝牙控制芯片nRF52832的一篇概述,里面主要记录了蓝牙的分层结构,需要的话可参考:nRF52832蓝牙概述_路溪非溪的博客-CSDN博客
这篇文章记录的是蓝牙模组的基本使用。
二者有何区别呢?
nRF52832是一款基于蓝牙的主控芯片,灵活度较高,能够实现更多的功能,它几乎和STM32芯片类似,有各种各样实现功能的外设,蓝牙只是其主打的一个外设而已,蓝牙协议实现得很全面。
而BLE模组,基本上就是作为一个透传的模块来使用,等同于一根导线,试想下,如果是一个传感器需要跟主控芯片通信,通过导线就可以了,但是蓝牙因为是无线的,没有导线,那怎么传输信号呢?这时候就需要一个透传模块。而CC2640R2所起的就是这个作用。
这个概念,就是透传。
假如我现在有个蓝牙从机在板子上和单片机通过串口连接,这时,蓝牙主机通过蓝牙协议连接上蓝牙从机,此时,双方如何通信呢?
蓝牙主机发消息给蓝牙从机,蓝牙从机模组不会对这个数据进行任何额外的处理,而是会直接将这个数据转发给串口,此时单片机通过串口读到数据;当单片机需要发数据给主机时,直接将数据通过串口发送给蓝牙从机模组,同样的,蓝牙从机模组不会对数据进行任何的额外处理,而是直接将数据转发给主机。
从上面描述的过程来看,是不是蓝牙从机就好像不存在一样,单片机通过串口就能直接跟主机进行通信,我们也不需要在蓝牙从机上做任何数据操作,就好像这个蓝牙从机就是一根导线。
这种传输模式,就叫做透明传输。
理解透传之后,我们再来了解一下蓝牙模组的模式。
其实,市面上的各种集成模组,大部分都是采样这样的方式来实现,即存在两种模式,那就是AT指令模式和数据透传模式。
为什么有这两种模式呢?就以BLE模组CC2640来讲一下。
首先,如果要让模组实现功能,肯定要对其进行一些设置,设置成功之后,才能进行正确的数据传输,任何外设都是如此,一定是要先初始化,设置需要的一些参数,之后才能正常通信。
蓝牙模组也不例外,所以,模组通常就集成了相应的AT指令,我们只要发送指令给模组,就能对模组进行设置,此时,蓝牙模组就处于AT指令模式,在这种模式下,单片机发送的指令,模组在接收之后,并不会透传出去,而是会自己执行;当我们在AT指令模式下将模组的参数设置好,蓝牙也连接上了之后,就可以进入透传模式,这时,单片机给模组发的数据,就会透传出去,模组本身并不会对其进行任何额外处理。
示例如下:
对于这两种模式的切换,不同的模组有不同的方式,有的模组会通过一个引脚的电平切换来实现模式的转换,而有的模组,在连接之前处于AT指令模式,只要连接上了蓝牙,就会自动进入到透传模式,不用进行额外的操作。
BLE模组CC2640采用的就是第二种自动切换的方式。
透传模式,是模组内部就实现了数据的转发,而不需要我们去实现。
注意:
如果处于透传模式,即使发了AT指令,模组也不会执行,而是会当做数据透传出去。
说明下:
nRF52832不是蓝牙模组,而是一个蓝牙主控芯片,所以除了蓝牙的协议,其他部分需要我们自行设置和实现,就比如透传,就需要我们自己写代码去实现数据的转发。
蓝牙通信中,分为主机和从机,有的模组实现了从机功能,就只能作为从机;有的模组实现了主机功能,就只能作为主机;有的模组既实现了主机功能,又实现了从机功能,所以是主从一体的,既能配置为主机,也能配置为从机。所以在购买蓝牙模组时,就需要注意自己到底需要买的是哪种类型的模组。像nRF52832蓝牙主控芯片,购买时并不存在是主机还是从机一说,而是我们自己用代码去实现主机还是从机的功能。
通常来说,蓝牙协议是一个通用协议,只要主从双方,都是使用的蓝牙协议,并且,从机是在主机的连接范围之内,就可以连接。就像单片机的程序,只要是同一款芯片,不管是烧到哪个芯片里,作用都是一样的,要做的,可能就是改变一下引脚定义,应用软件部分是不用修改的。
基本概念
蓝牙版本
由此可见
蓝牙协议是一个通用协议;
BLE是属于蓝牙4.0以上版本的规范。
设备类型
一般来说,手机都是双模设备,既支持蓝牙低功耗,又支持传统蓝牙,所以,不管蓝牙模组使用的是BLE还是传统蓝牙,手机都能和他们建立连接并通信。
CC2640就是一个单模的设备,只支持BLE。
BLE体系结构
从对链路层的描述我们可以知道:
1、一个设备,同一时刻,要么是主机,要么是从机,不可能同时既是主机又是从机。
2、只有建立连接之后,才会互传数据,所以为什么要建立连接之后模组才能进入透传模式,就是这个原因。
3、为什么需要AT指令模式,就是因为设备上电后,不可能直接进入连接态,必须先设置,然后经由广播态或者发起态才能进入连接态。因此,我们在编写程序时,一定是先设置AT指令,开启广播,连接成功后才会发送透传数据。
链路层信道映射
BLE广播、扫描和连接事件
广播事件
广播是从设备发起的,可以被主设备扫描到,并能对扫描进行响应,即扫描响应。
扫描事件
扫描是主机的行为。
连接事件
注意,建立连接之前,走的都是广播信道,建立连接之后,就会走数据信道。
Profile、Service、Characteristic和UUID
CC2640 R2 透传分为蓝牙主机版本,以及蓝牙从机版本。
本文以蓝牙从机为例,场景是:MCU和手机APP的通信。
透传模式,又叫桥接模式。
桥接模式:这是常用模式,搭建传统 MCU 与手机蓝牙之间的通信桥梁。用户MCU 可以通过模块的通用串口和移动设备进行双向通讯,用户也可以通过特定的串口 AT 指令,对某些通讯参数进行管理控制。用户数据的具体含义由上层应用程序自行定义。移动设备可以通过 APP 对模块进行写操作,写入的数据将通过串口发送给用户的 MCU。模块收到来自用户 MCU 串口的数据包后,将自动转发给移动设备。此模式下的开发,用户必须负责主 MCU 的代码设计,以及智能移动设备端 APP 代码设计。
模块通过初始设置后会自动进行广播,与打开特定 APP 的手机会对其进行扫描和对接,成功之后便可以通过 BLE 协议对其进行监控。实物图展示:引脚说明:这里面除了串口引脚之外,还有几个比较重要的引脚。Wakeup引脚,用来唤醒模组,之后才能进行串口传输。BleCtrl引脚,用来控制广播的开启和关闭。BleState引脚,用来监测模组当前处于连接还是断开状态。一般的工作步骤是这样的。上电后,先要发指令,发指令时,要Wakeup唤醒模组,然后才能发,指令发完之后,根据实际情况,在必要时通过BleCtrl打开广播,打开广播后,主机就可以连接模组了,连接成功后,模组就会进入透传模式,此时,就可以收发数据了,发送数据时,也要先唤醒模组。然后,通过BleState实时监测蓝牙的连接和断开,以便做不同的处理。桥接模式可以看到,这里的通道分为BLE参数配置通道和用户数据通道,就是分别在AT指令模式和透传模式下使用的信道。AT指令说明此处仅部分AT指令,更多查看数据手册。需要注意的是,AT指令的结尾就是回车换行符,也就是\r\n指令格式如下:模组主动发出的提示信息关于指令的详细内容,自行查看数据手册。以下仅以设置模组名称举例说明。有的指令只能写,有的指令只能读,有的只能既能写也能读。比如设置模组名称指令,既能写,也能读。写的时候,就是给模组取名。读的时候,就是读当前模组的名称。比如上述的回复:AT+OKTTC-BLE首先,总是会返回AT+OK+回车换行,之后如果有内容,再返回内容+回车换行。
先附上代码,然后再进行说明。
#include "ble.h" #include "a_board.h" //定义串口接收数据相关的变量 uint8_t g_Uart1RecvBuf[200];//接收缓冲,一帧数据 uint16_t g_Uart1RecvCount = 0;//接收数目 uint8_t USART1_FRAME_RX_STA = 0;//接收状态标记 void ble_uart_send_data(uint8_t *data, uint16_t len); void wakeup_ble_usart(void); //初始化蓝牙模块的IO(除串口外) //PA8 BLE-WAKEUP-MCU输出 //PC7 BLECtrl-MCU输出 //PC8 BLEState-MCU输入 //PC9 BLE-INT-MCU输入 static void ble_io_init(void) { GPIO_InitTypeDef GPIO_InitStructure; //配置时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC, ENABLE); //使能PA端口时钟 //IO通用参数配置 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO口速度为50MHz //PA8 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; //PA8 端口配置 GPIO_Init(GPIOA, &GPIO_InitStructure); //根据设定参数初始化GPIOA.8 //PC7 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; //PC7端口配置 GPIO_Init(GPIOC, &GPIO_InitStructure); //根据设定参数初始化GPIOC.7 //PC89 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入 GPIO_Init(GPIOC, &GPIO_InitStructure); } //蓝牙的通信串口初始化 //PA9 BLE-TX-串口1 //PA10 BLE-RX-串口1 static void ble_usart_init() { //GPIO端口设置 GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); //使能USART1时钟和GPIOA时钟 //USART1_TX GPIOA.9 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 GPIO_Init(GPIOA, &GPIO_InitStructure); //USART1_RX GPIOA.10初始化 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入 GPIO_Init(GPIOA, &GPIO_InitStructure); //USART 初始化设置 USART_InitStructure.USART_BaudRate = 256000;//串口波特率 USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式 USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位 USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位 USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制 USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式 USART_Init(USART1, &USART_InitStructure); //初始化串口1 //USART1中断配置 NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级3 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能 NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化NVIC寄存器 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接收中断 USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);//开启串口空闲中断 USART_Cmd(USART1, ENABLE); //使能串口1 } //蓝牙串口1数据发送函数 void usart1_send_data(uint8_t *txData, uint8_t txLength) { for(uint8_t i = 0; i < txLength; i++) { USART_SendData(USART1, txData[i]);//向串口1发送数据 while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) != SET);//等待发送结束 } } //蓝牙-串口1数据接收函数 void USART1_IRQHandler(void) //串口1中断服务程序 { uint8_t clear;//清除空闲中断 if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断 { USART_ClearFlag(USART1, USART_IT_RXNE); g_Uart1RecvBuf[g_Uart1RecvCount++] = USART_ReceiveData(USART1); } else if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) { clear = USART1->SR; // 清除空闲中断 clear = USART1->DR; // 清除空闲中断 USART1_FRAME_RX_STA = 1;//一帧数据接收完成 } } //唤醒蓝牙串口传输 void wakeup_ble_usart(void) { GPIO_ResetBits(GPIOA, GPIO_Pin_8);//置低电平 } //关闭蓝牙串口传输 void close_ble_usart(void) { GPIO_SetBits(GPIOA, GPIO_Pin_8);//置高电平 } //开启从机广播 void open_slave_broadcast(void) { GPIO_ResetBits(GPIOC, GPIO_Pin_7);//置低电平 } //关闭从机广播 void close_slave_broadcast(void) { GPIO_SetBits(GPIOC, GPIO_Pin_7);//置高电平 } //发送指令 void ble_uart_send_cmd(char *cmd) { int len = strlen(cmd); wakeup_ble_usart();//唤醒蓝牙串口传输 delay_ms(2); //发送指令数据 usart1_send_data((uint8_t *)cmd, len); } //发送透传数据 void ble_uart_send_data(uint8_t *data, uint16_t len) { wakeup_ble_usart();//唤醒蓝牙串口传输 delay_ms(2); //发送透传数据 usart1_send_data(data, len); } //蓝牙初始化 void ble_init(void) { ble_io_init();//蓝牙io初始化 ble_usart_init();//蓝牙串口初始化 //设置蓝牙名称 ble_uart_send_cmd("AT+NAME=TEST\r\n"); //初始先关闭从机广播 close_slave_broadcast(); } //蓝牙透传接收到数据后的处理函数 void ble_received_data_handler(void) { if(USART1_FRAME_RX_STA) { //接收到一帧数据后,就直接发给模拟板 SendDataToAnBoard(g_Uart1RecvBuf, g_Uart1RecvCount); USART1_FRAME_RX_STA = 0; g_Uart1RecvCount = 0; } } //检测蓝牙当前状态 uint8_t CheckBleState(void) { if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_8) == SET) { return 0;//断开状态 } else { return 1;//连接状态 } }
注意,这里从机取的蓝牙名称,要在主机的过滤器有效范围内。
补充说明:
这里需要对串口做一些补充。
其实,不管是不是串口,或者其他接收数据的外设,都有一个值得重点注意的问题。
那就是,接收数据时,不要一个一个地去处理,而是必须先将其存储在一个缓存中,然后需要的时候再去缓存里拿数据进行处理转发等操作。
为什么?因为底层数据接收的速度非常快,而通常数据处理的速度就慢得多,慢的操作绝对无法跟上快的操作,所以必定导致一种现象:数据来得太快,我拿数据处理时,当前数据还没处理完,就已经过去好多数据没被接收到,这样,就会漏数据。而且,可能导致数据位错乱,导致拿到的数据是乱码。
可以看到,上面单次处理的方式,在第四次处理时,你以为你处理的只是第4个点的数据,其实,可能已经过去成百上千个数据了,如果用一个变量来接收,那早就被覆盖了。
所以,必须先缓存下来,然后需要的时候去处理。
注意,我这里没有说建议,而是必须,因为如果不这么做的话,数据就不对。
还有一个问题就是,很多时候,我们都必须知道接收了多少字节的数据,这样才方便处理,如果接收的是定长数据就比较好办,但如果是不定长数据就比较麻烦。之前提到过一种串口空闲中断+DMA的方式,因为DMA提供了一个函数,可以间接算出接收了多少字节。但是如果不用DMA呢?那就可以通过串口接收中断+空闲中断的方式来处理。
举例说明
这里假如接收到一帧5个字节的数据,那么,接收中断就会进入5次,在第5次接收中断之后,就会出现一个空闲中断,此时,就不会再进入接收中断,而是空闲中断,这样,就能知道到底接收了多少个字节的数据。
这一点非常非常重要。
可以参考这篇文章:STM32使用串口IDLE中断的两种接收不定长数据的方式