基于串口的BLE模组CC2640R2使用总结

之前写过蓝牙控制芯片nRF52832的一篇概述,里面主要记录了蓝牙的分层结构,需要的话可参考:nRF52832蓝牙概述_路溪非溪的博客-CSDN博客

这篇文章记录的是蓝牙模组的基本使用。

二者有何区别呢?

nRF52832是一款基于蓝牙的主控芯片,灵活度较高,能够实现更多的功能,它几乎和STM32芯片类似,有各种各样实现功能的外设,蓝牙只是其主打的一个外设而已,蓝牙协议实现得很全面。

而BLE模组,基本上就是作为一个透传的模块来使用,等同于一根导线,试想下,如果是一个传感器需要跟主控芯片通信,通过导线就可以了,但是蓝牙因为是无线的,没有导线,那怎么传输信号呢?这时候就需要一个透传模块。而CC2640R2所起的就是这个作用。

这个概念,就是透传。

什么是透传?

假如我现在有个蓝牙从机在板子上和单片机通过串口连接,这时,蓝牙主机通过蓝牙协议连接上蓝牙从机,此时,双方如何通信呢?

蓝牙主机发消息给蓝牙从机,蓝牙从机模组不会对这个数据进行任何额外的处理,而是会直接将这个数据转发给串口,此时单片机通过串口读到数据;当单片机需要发数据给主机时,直接将数据通过串口发送给蓝牙从机模组,同样的,蓝牙从机模组不会对数据进行任何的额外处理,而是直接将数据转发给主机。

从上面描述的过程来看,是不是蓝牙从机就好像不存在一样,单片机通过串口就能直接跟主机进行通信,我们也不需要在蓝牙从机上做任何数据操作,就好像这个蓝牙从机就是一根导线。

基于串口的BLE模组CC2640R2使用总结_第1张图片

这种传输模式,就叫做透明传输。

理解透传之后,我们再来了解一下蓝牙模组的模式。

AT指令模式和透传模式

其实,市面上的各种集成模组,大部分都是采样这样的方式来实现,即存在两种模式,那就是AT指令模式和数据透传模式。

为什么有这两种模式呢?就以BLE模组CC2640来讲一下。

首先,如果要让模组实现功能,肯定要对其进行一些设置,设置成功之后,才能进行正确的数据传输,任何外设都是如此,一定是要先初始化,设置需要的一些参数,之后才能正常通信。

蓝牙模组也不例外,所以,模组通常就集成了相应的AT指令,我们只要发送指令给模组,就能对模组进行设置,此时,蓝牙模组就处于AT指令模式,在这种模式下,单片机发送的指令,模组在接收之后,并不会透传出去,而是会自己执行;当我们在AT指令模式下将模组的参数设置好,蓝牙也连接上了之后,就可以进入透传模式,这时,单片机给模组发的数据,就会透传出去,模组本身并不会对其进行任何额外处理。

示例如下:

基于串口的BLE模组CC2640R2使用总结_第2张图片

对于这两种模式的切换,不同的模组有不同的方式,有的模组会通过一个引脚的电平切换来实现模式的转换,而有的模组,在连接之前处于AT指令模式,只要连接上了蓝牙,就会自动进入到透传模式,不用进行额外的操作。

BLE模组CC2640采用的就是第二种自动切换的方式。

透传模式,是模组内部就实现了数据的转发,而不需要我们去实现。

注意:

如果处于透传模式,即使发了AT指令,模组也不会执行,而是会当做数据透传出去。

说明下:

nRF52832不是蓝牙模组,而是一个蓝牙主控芯片,所以除了蓝牙的协议,其他部分需要我们自行设置和实现,就比如透传,就需要我们自己写代码去实现数据的转发。

蓝牙主机和蓝牙从机

蓝牙通信中,分为主机和从机,有的模组实现了从机功能,就只能作为从机;有的模组实现了主机功能,就只能作为主机;有的模组既实现了主机功能,又实现了从机功能,所以是主从一体的,既能配置为主机,也能配置为从机。所以在购买蓝牙模组时,就需要注意自己到底需要买的是哪种类型的模组。像nRF52832蓝牙主控芯片,购买时并不存在是主机还是从机一说,而是我们自己用代码去实现主机还是从机的功能。

通常来说,蓝牙协议是一个通用协议,只要主从双方,都是使用的蓝牙协议,并且,从机是在主机的连接范围之内,就可以连接。就像单片机的程序,只要是同一款芯片,不管是烧到哪个芯片里,作用都是一样的,要做的,可能就是改变一下引脚定义,应用软件部分是不用修改的。

再次补充介绍下蓝牙协议

基本概念

基于串口的BLE模组CC2640R2使用总结_第3张图片

蓝牙版本

基于串口的BLE模组CC2640R2使用总结_第4张图片

由此可见

蓝牙协议是一个通用协议;

BLE是属于蓝牙4.0以上版本的规范。

设备类型

基于串口的BLE模组CC2640R2使用总结_第5张图片

一般来说,手机都是双模设备,既支持蓝牙低功耗,又支持传统蓝牙,所以,不管蓝牙模组使用的是BLE还是传统蓝牙,手机都能和他们建立连接并通信。

CC2640就是一个单模的设备,只支持BLE。

BLE体系结构

基于串口的BLE模组CC2640R2使用总结_第6张图片

基于串口的BLE模组CC2640R2使用总结_第7张图片

从对链路层的描述我们可以知道:

1、一个设备,同一时刻,要么是主机,要么是从机,不可能同时既是主机又是从机。

2、只有建立连接之后,才会互传数据,所以为什么要建立连接之后模组才能进入透传模式,就是这个原因。

3、为什么需要AT指令模式,就是因为设备上电后,不可能直接进入连接态,必须先设置,然后经由广播态或者发起态才能进入连接态。因此,我们在编写程序时,一定是先设置AT指令,开启广播,连接成功后才会发送透传数据。

链路层信道映射

基于串口的BLE模组CC2640R2使用总结_第8张图片

基于串口的BLE模组CC2640R2使用总结_第9张图片

基于串口的BLE模组CC2640R2使用总结_第10张图片

基于串口的BLE模组CC2640R2使用总结_第11张图片

BLE广播、扫描和连接事件

广播事件

基于串口的BLE模组CC2640R2使用总结_第12张图片

广播是从设备发起的,可以被主设备扫描到,并能对扫描进行响应,即扫描响应。

扫描事件

基于串口的BLE模组CC2640R2使用总结_第13张图片

基于串口的BLE模组CC2640R2使用总结_第14张图片

扫描是主机的行为。

连接事件

注意,建立连接之前,走的都是广播信道,建立连接之后,就会走数据信道。

基于串口的BLE模组CC2640R2使用总结_第15张图片

基于串口的BLE模组CC2640R2使用总结_第16张图片

Profile、Service、Characteristic和UUID

基于串口的BLE模组CC2640R2使用总结_第17张图片

基于串口的BLE模组CC2640R2使用总结_第18张图片

基于串口的BLE模组CC2640R2使用总结_第19张图片

CC2640使用概述

CC2640 R2 透传分为蓝牙主机版本,以及蓝牙从机版本

本文以蓝牙从机为例,场景是:MCU和手机APP的通信。

透传模式,又叫桥接模式。

桥接模式:这是常用模式,搭建传统 MCU 与手机蓝牙之间的通信桥梁。用户MCU 可以通过模块的通用串口和移动设备进行双向通讯,用户也可以通过特定的串口 AT 指令,对某些通讯参数进行管理控制。用户数据的具体含义由上层应用程序自行定义。移动设备可以通过 APP 对模块进行写操作,写入的数据将通过串口发送给用户的 MCU。模块收到来自用户 MCU 串口的数据包后,将自动转发给移动设备。此模式下的开发,用户必须负责主 MCU 的代码设计,以及智能移动设备端 APP 代码设计。

模块通过初始设置后会自动进行广播,与打开特定 APP 的手机会对其进行扫描和对接,成功之后便可以通过 BLE 协议对其进行监控。
实物图展示:
基于串口的BLE模组CC2640R2使用总结_第20张图片
引脚说明:
基于串口的BLE模组CC2640R2使用总结_第21张图片
这里面除了串口引脚之外,还有几个比较重要的引脚。
Wakeup引脚,用来唤醒模组,之后才能进行串口传输。
BleCtrl引脚,用来控制广播的开启和关闭。
BleState引脚,用来监测模组当前处于连接还是断开状态。
一般的工作步骤是这样的。
上电后,先要发指令,发指令时,要Wakeup唤醒模组,然后才能发,指令发完之后,根据实际情况,在必要时通过BleCtrl打开广播,打开广播后,主机就可以连接模组了,连接成功后,模组就会进入透传模式,此时,就可以收发数据了,发送数据时,也要先唤醒模组。
然后,通过BleState实时监测蓝牙的连接和断开,以便做不同的处理。
桥接模式
基于串口的BLE模组CC2640R2使用总结_第22张图片
可以看到,这里的通道分为BLE参数配置通道和用户数据通道,就是分别在AT指令模式和透传模式下使用的信道。
AT指令说明
基于串口的BLE模组CC2640R2使用总结_第23张图片
此处仅部分AT指令,更多查看数据手册。
需要注意的是,AT指令的结尾就是回车换行符,也就是\r\n
指令格式如下:
基于串口的BLE模组CC2640R2使用总结_第24张图片
模组主动发出的提示信息
基于串口的BLE模组CC2640R2使用总结_第25张图片
关于指令的详细内容,自行查看数据手册。
以下仅以设置模组名称举例说明。
基于串口的BLE模组CC2640R2使用总结_第26张图片
有的指令只能写,有的指令只能读,有的只能既能写也能读。
比如设置模组名称指令,既能写,也能读。
写的时候,就是给模组取名。
读的时候,就是读当前模组的名称。
比如上述的回复:
AT+OK
TTC-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;//连接状态
    }
}

注意,这里从机取的蓝牙名称,要在主机的过滤器有效范围内。

补充说明:

这里需要对串口做一些补充。

其实,不管是不是串口,或者其他接收数据的外设,都有一个值得重点注意的问题。

那就是,接收数据时,不要一个一个地去处理,而是必须先将其存储在一个缓存中,然后需要的时候再去缓存里拿数据进行处理转发等操作。

为什么?因为底层数据接收的速度非常快,而通常数据处理的速度就慢得多,慢的操作绝对无法跟上快的操作,所以必定导致一种现象:数据来得太快,我拿数据处理时,当前数据还没处理完,就已经过去好多数据没被接收到,这样,就会漏数据。而且,可能导致数据位错乱,导致拿到的数据是乱码。

基于串口的BLE模组CC2640R2使用总结_第27张图片

可以看到,上面单次处理的方式,在第四次处理时,你以为你处理的只是第4个点的数据,其实,可能已经过去成百上千个数据了,如果用一个变量来接收,那早就被覆盖了。

所以,必须先缓存下来,然后需要的时候去处理

注意,我这里没有说建议,而是必须,因为如果不这么做的话,数据就不对。

还有一个问题就是,很多时候,我们都必须知道接收了多少字节的数据,这样才方便处理,如果接收的是定长数据就比较好办,但如果是不定长数据就比较麻烦。之前提到过一种串口空闲中断+DMA的方式,因为DMA提供了一个函数,可以间接算出接收了多少字节。但是如果不用DMA呢?那就可以通过串口接收中断+空闲中断的方式来处理。

举例说明

基于串口的BLE模组CC2640R2使用总结_第28张图片

这里假如接收到一帧5个字节的数据,那么,接收中断就会进入5次,在第5次接收中断之后,就会出现一个空闲中断,此时,就不会再进入接收中断,而是空闲中断,这样,就能知道到底接收了多少个字节的数据。

这一点非常非常重要。

可以参考这篇文章:STM32使用串口IDLE中断的两种接收不定长数据的方式

你可能感兴趣的:(stm32,单片机)