总结这两天研究的蓝牙串口。人话版资料不多,主要靠翻别人的仓库和文档。
如果本文有什么问题,或仍有不理解的地方,可以私信交流。
蓝牙部分已经由硬件厂商完成,对外只暴露了几根铁丝,与主机通信。 HC08与主机通信的协议是串口。 控制蓝牙串口模块,不需要轮询0011,只需要通过串口的方式,向从机HC08发送命令即可。
连接与断开交由外设完成。连接成功之后就是一个串口,对蓝牙通过串口发送的数据会透传到另一端,传入的数据也会被串口响应。 配置HC08,其实就是配置UART。也可以通过USB转TTL连接到电脑上。
现在原理图中找到引脚所在的位置。
PA9和PA10也是USART的输入输出引脚。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_USART1|RCC_APB2Periph_TIM1,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_SetBits(GPIOA,GPIO_Pin_10);
这款stm32已经集成了USART的硬件,只需要调用库函数初始化。 具体的参数含义在之前的文章中有介绍。
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate=9600;
USART_InitStructure.USART_HardwareFlowControl=ENABLE;
USART_InitStructure.USART_Mode=USART_Mode_Rx|USART_Mode_Tx;
USART_InitStructure.USART_Parity=USART_Parity_No;
USART_InitStructure.USART_StopBits=USART_StopBits_1;
USART_InitStructure.USART_WordLength=USART_WordLength_8b;
USART_Init(USART1,&USART_InitStructure);
硬件只是完成了读入读出操作,在收到串口发来的电平变化时,自动把1个字节的数据放入移位寄存器,将USART_IT_RXNE
标志位置为高电平。
在设置为高电平时,触发中断,读出一个字节的数据,并清除中断标志。如果不清除,会导致无法接收下一个字节的数据。
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel=USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitStructure);
发送来的数据往往是多个字节的,如何判断消息是否结束?
通常的做法包括,约定好消息尾,比如当结尾为\r\n
时标注当前消息已结束。
在本文中,采用的方法是:定时器中断。如果一段时间都没有新数据,那么表面当前数据已经结束。
void TIM1_UP_IRQHandler(){
if(rxBufferPointer&&millis-lastTime>10){
rxBufferPointer=0;
isOK=1;
}
millis++;
TIM_ClearITPendingBit(TIM1,TIM_IT_Update);
}
每毫秒触发一次定时器中断,存储一个定时值。
rxBufferPointer
是指向下一个字节数据的指针。
当前消息结束时,该指针应复位为0
,标志isOK
置一。外界判断消息是否结束,就是通过查看isOK
标志的状态。
void USART1_IRQHandler(){
if(USART_GetFlagStatus(USART1,USART_IT_RXNE)==SET){
lastTime=millis;
rxBuffer[rxBufferPointer++]=USART_ReceiveData(USART1);
rxSize=rxBufferPointer;
USART_ClearITPendingBit(USART1,USART_IT_RXNE);
}
}
如果消息没有结束,自动将当前接收的数据存入rxBufferPointer
指向的下一个字节位置。
将extern修饰的变量放到头文件中,之后可以在导入这个头文件后直接读取。
数组大小256,指针为8位,最多指向256个内存地址。
传递的消息没有结束标志,为了标注结束位置,需要通过rxSize
存储结束读取时的消息长度。
字符串比较需要用strcmp
,而不能用简单的==
。
这一部分简单带过,配置蓝牙串口其实就是配置USART,因为stm32与HC08的通信方式就是串口。更详细的配置过程可以翻看我之前的博客。
有小程序搭建经验的,可以跳过这一部分。
创建一个微信小程序
没有AppID的可以去注册一个,配置成什么样子,几乎不影响之后开发。 我的选择是:不使用云服务、JS基础模板。
设置全局统一样式
把这段代码CV到app.wxss
中:
page {
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica,
Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei',
sans-serif;
}
通过这段代码,实现在不同设备上一样的显示情况。
干掉用不到的页面
删除logs
文件夹。
在app.json
中删除"pages/logs/logs"
这一行。
(其实不删也不影响使用)
一个纯净的App()和Page()
删除app.js
、index.wxml
、index.wxss
、index.js
中的全部内容。
然后选择带有方块□的初始化模板。,初始化app.js
和index.js
。
本文一共配置了三个页面,另外两个页面的初始化同上。
"pages": [
"pages/index/index",
"pages/BLE/Services/Services",
"pages/BLE/control/control"
],
这一步的目标是,在index页面,显示扫描到的蓝牙设备。 根据微信官方的要求,流程为:
蓝牙的可用状态和扫描状态可以在wx.onBluetoothAdapterStateChange()
回调中获取。
扫描的设备可以在wx.onBluetoothDeviceFound()
回调中获取。
为了方便调试:
fail
。onBluetoothAdapterStateChange
的通知结果打印在页面上。app.js挂载全局的fail处理方法
App({
fail:(res)=>{
wx.showToast({
title: res.errMsg,
icon:"none"
})
}
})
index.js响应适配器状态改变事件
这里把onBluetoothAdapterStateChange
单独封装,挂载在this下。是为了使代码结构更清晰,避免在onLoad()
下出现层层嵌套。
Page({
data: {
available: false,
discovering: false
},
onLoad: function (options) {
this.onBluetoothAdapterStateChange();
},
onBluetoothAdapterStateChange() {
wx.onBluetoothAdapterStateChange(({
available,
discovering
}) => {
this.setData({
available,
discovering
})
})
}
})
在前端显示部分值,方便后续调试。
开启适配器之后,需要开始扫描。在扫描之前,先设置设备发现后的处理函数。
这里的处理方案是:把发现的设备添加到数组中。如果报告了重复的设备,那么需要通过数组的.splice()
方法,替换为新的设备。
为了方便判断是否重复,可以创建一个数组_deviceIds
挂载在this
下,存储设备的唯一标识deviceId
。
为了简化代码结构,避免层层嵌套,将代码实现单独封装,挂载在this下。
开始搜索的点击事件为onTapDiscover
。
<button bind:tap="onTapDiscover">{{discovering?"结束搜索":"开始搜索"}}</button>
这一事件要根据当前情况执行不同的策略:
对于前两种情况,在执行前需要清空已扫描到的设备列表,以保证扫描到的设备都是最新有效的。
onTapDiscover() {
if (this.data.discovering) {
wx.stopBluetoothDevicesDiscovery();
} else {
this.setData({
devices: []
})
this._deviceIds = []
if (this.data.available) {
wx.startBluetoothDevicesDiscovery({
allowDuplicatesKey: true
})
} else {
this.openBluetoothAdapter();
}
}
},
openBluetoothAdapter() {
wx.openBluetoothAdapter({
success: () => {
wx.startBluetoothDevicesDiscovery({
allowDuplicatesKey: true
})
},
fail: getApp().fail
})
}
对于前端界面,这不是本文的重点,粗略带过,具体的wxss
设置可翻代码,根据需求自定义。
通过onTapDevice
函数,处理连接事件,通过data-deviceId
传入。通过deviceId
获取服务列表。
在成功连接之后,应停止扫描,关闭这一耗费资源的操作。
服务列表操作在新的页面完成。
onTapDevice(e){
let deviceId=e.currentTarget.dataset.deviceid
wx.showModal({
title: 'Connected or not',
content: deviceId,
success (res) {
if (res.confirm) {
getApp().Toast("connecting");
wx.createBLEConnection({
deviceId,
success:()=>{
wx.stopBluetoothDevicesDiscovery();
wx.navigateTo({
url: `/pages/BLE/Services/Services?deviceId=${deviceId}`
})
}
})
}
}
})
}
这一步的操作比较少,所以可以直接将获取服务列表的方法定义在onLoad
里。
如果返回上一页面,意味着中断连接。所以需要在onUnload
方法中断开当前连接。
onUnload
方法会在当页面的生命周期结束时自动执行。
具体的代码将在之后的源代码中呈现。本项目未使用第三方组件库,为原生的微信小程序,兼容大多数环境。
这是本文中最复杂的部分。(理解之后不复杂)
在一开始,我扫描到多个服务,每个服务又有多个特征,对此不知道该怎么做。 尽管有些特征携带了notify属性,但在尝试notify的时候还是报错。或者read、write没有任何响应。 目前的解决方案是,遍历服务特征,尝试read/write/notify,在success回调中设置服务特征为当前成功的这个。 目前在HC08上可以正常通信。 我之前的理解是,在一个特征上同时进行read/write/notify。但实际可能是分散在多个特征上的,共同完成同一个服务。
为了简化代码结构,采用Command命令模式,每个按钮执行的是同一个方法,只是传入的命令参数不同。
HC08发送来的数据在onBLECharacteristicValueChange
中处理。而不是read,目前read是干什么的我也不清除。
发送来的是ArrayBuffer,发出去的时候也要转换成ArrayBuffer,需要实现:
str就是字符串,hex就是十六进制,最终表现形式也是字符串,ab是ArrayBuffer,这种数据流传输的形式。2
就是to
,为了省事,读音相同,就简写作了2
。
具体过程可翻看源代码。
代码仓库:https://github.com/WuShFeng/BLE
年轻人的第一辆新能源四驱
本文正值开学季,中断了很多次,有好多想写的都忘了。想起来的时候再补充。