1. 项目概述:BLE HID与GATT服务配置的AT命令实战
如果你正在捣鼓智能硬件,想让你的Arduino项目或者ESP32开发板变成一个无线键盘、鼠标,甚至是游戏手柄,那么BLE HID(蓝牙低功耗人机接口设备)绝对是你绕不开的技术。这玩意儿能让你的设备被手机、平板、电脑识别为一个标准的输入设备,直接打字、点鼠标,想想就挺酷的。但当你真正上手时,面对一堆GATT、服务、特征值这些概念,还有厂商提供的厚厚一叠AT命令手册,是不是感觉头大?别急,今天我就以Adafruit Bluefruit LE模块为例,带你把这些AT命令掰开揉碎了讲清楚。
我手头这个Bluefruit LE模块,它内置了完整的BLE协议栈,并且提供了一套非常强大的AT命令集。这意味着你不需要从零开始啃蓝牙协议,只需要通过串口发送几条简单的文本命令,就能配置复杂的HID功能。这就像你拿到了一台功能强大的机器,但厂家给了你一个遥控器,按几个键就能让它干活,而不是让你去重新焊接电路板。本文的核心,就是把这个“遥控器”——也就是AT命令集——的用法、原理和实战中的坑,给你讲明白。无论你是想做个蓝牙遥控器、无障碍输入设备,还是简单的数据透传,这里面的门道都能帮到你。
2. BLE HID与GATT基础概念扫盲
在深入AT命令之前,我们得先统一一下语言。不然我说“特征值”,你以为是产品卖点,那就对不上频道了。
2.1 GATT:蓝牙设备的数据“黄页”
你可以把GATT(通用属性配置文件)想象成一本设备的数据“黄页”。每个BLE设备(作为GATT服务器)都拥有这么一本“书”,里面记录了这个设备能提供哪些“服务”(Services),以及每个服务下具体有哪些“特征值”(Characteristics)可以读写或订阅。
- 服务(Service):一个逻辑功能集合。比如“电池服务”(UUID: 0x180F)专门管理电量信息,“HID服务”(UUID: 0x1812)专门管理人机交互。
- 特征值(Characteristic):服务下的具体数据点。它是实际承载数据的地方,比如电池服务下有一个“电池电量”特征值(UUID: 0x2A19),HID服务下有“键盘输入报告”、“鼠标输入报告”等特征值。每个特征值都有属性(Properties),定义了它能否被读(Read)、写(Write)、通知(Notify)或指示(Indicate)。
当你的手机(作为GATT客户端)连接上这个BLE设备后,它就会去翻阅这本“黄页”,找到它需要的服务(比如HID),然后订阅或读写对应的特征值,从而获取数据或发送指令。
2.2 HID over GATT (HOGP):当蓝牙遇上键盘鼠标
传统的USB HID设备(键盘、鼠标)是通过USB线直接通信的。BLE HID,更准确说是HID over GATT (HOGP),就是把HID协议搬到了BLE的GATT框架上运行。它定义了一套标准的HID服务(UUID: 0x1812)和一系列特征值,用来传输HID报告(比如按下了哪个键、鼠标移动了多少)。
这样做的好处是巨大的:操作系统(Windows, macOS, iOS, Android)内置了对HOGP的支持。这意味着只要你的BLE设备正确实现了HID服务,它就能被系统自动识别为一个标准的键盘或鼠标,无需安装任何额外的驱动。这也是我们能用AT命令轻松模拟键鼠的基础。
2.3 AT命令:与模块对话的“摩斯密码”
AT命令(Attention Commands)是一种历史悠久的、基于文本的指令集,最初用于调制解调器。在嵌入式蓝牙模块中,它被广泛用于通过串口(UART)对模块进行配置和控制。你发送一条像AT+BLEHIDEN=1这样的字符串,模块回复一个OK或具体数据,交互非常简单直接。
对于Bluefruit LE模块,AT命令是你与底层复杂蓝牙协议栈之间的“翻译官”和“指挥官”。你不需要关心底层如何组包、如何建立连接,你只需要告诉它“启用HID”、“模拟按下A键”,它就会帮你处理好一切。
注意:AT命令通常需要在每条命令后加上回车换行符(
\r\n),并且大多数命令执行后需要模块重启(ATZ)才能生效。这是新手最容易忽略的地方,务必记住。
3. HID功能AT命令深度解析与实战
理论铺垫完毕,现在进入实战环节。我们来看看如何用AT命令,一步步把你的模块变成一个听话的无线输入设备。
3.1 启用HID功能:万事开头第一步
在发送任何键盘鼠标指令前,你必须先告诉模块:“嘿,请把你的HID功能打开。” 这就是AT+BLEHIDEN命令的职责。
# 1. 启用HID支持 AT+BLEHIDEN=1 OK # 2. 重启模块使配置生效(关键步骤!) ATZ OK为什么必须重启?因为HID服务的启用涉及到底层协议栈的重新初始化和广播数据(Advertising Data)的更新。模块需要重启才能以“我是一个HID设备”的身份重新开始广播,这样才能被中央设备(如手机)正确发现和识别。
实操心得:很多朋友卡在第一步,手机搜不到设备或者连接后不识别为键盘,十有八九是忘了发送ATZ重启命令。养成习惯:修改任何与服务和广播相关的配置(AT+BLEHIDEN,AT+GAPDEVNAME,AT+GAPSETADVDATA等)后,立即执行ATZ。
3.2 模拟键盘输入:从发送“ABC”开始
键盘输入是HID最常用的功能。核心命令是AT+BLEKEYBOARDCODE。它的参数格式是理解的关键。
命令格式:AT+BLEKEYBOARDCODE=<Modifier>-<Reserved>-<Key1>-<Key2>-<Key3>-<Key4>-<Key5>-<Key6>这是一个由连字符分隔的8字节十六进制数组。
- 字节0 (Modifier):修饰键状态。这是一个位掩码(bitmask),可以同时按下多个修饰键。
- 字节1 (Reserved):保留字节,必须为
00。 - 字节2-7 (Key1-Key6):最多6个普通按键的HID扫描码。如果不足6个键,后面的字节可以填
00或直接留空(但格式位置要保留)。
修饰键(Modifier)位掩码详解: 这是实现组合键(如Ctrl+C)的核心。每个比特位代表一个键是否被按下。
| 比特位 | 十六进制值 | 对应的按键 |
|---|---|---|
| Bit 0 | 0x01 | 左Ctrl (Left Control) |
| Bit 1 | 0x02 | 左Shift (Left Shift) |
| Bit 2 | 0x04 | 左Alt (Left Alt) |
| Bit 3 | 0x08 | 左GUI (Windows键或Command键) |
| Bit 4 | 0x10 | 右Ctrl (Right Control) |
| Bit 5 | 0x20 | 右Shift (Right Shift) |
| Bit 6 | 0x40 | 右Alt (Right Alt) |
| Bit 7 | 0x80 | 右GUI (Right GUI) |
如何计算Modifier值?如果你想同时按下左Shift和左Alt,那么: 左Shift (0x02) + 左Alt (0x04) = 0x06 所以Modifier字节就是06。
HID扫描码(Scan Code): 这是与ASCII码完全不同的另一套编码系统。例如,字母‘a’的HID扫描码是0x04,数字‘1’是0x1E,回车键是0x28。大写‘A’并不是一个独立的扫描码,而是通过按下左Shift(Modifier=0x02)的同时发送‘a’的扫描码(0x04)来实现的。
一个完整的“按下并释放”流程: HID协议要求你必须发送“释放”报告,否则系统会认为按键一直被按住。
# 1. 按下左Shift,并同时按下A键(扫描码0x04) # Modifier: 左Shift (0x02) # Keys: A键 (0x04), 其余补00 AT+BLEKEYBOARDCODE=02-00-04-00-00-00-00-00 OK # 此时电脑会输入一个大写的‘A’ # 2. 发送释放报告(必须做!) # Modifier和所有Key都设置为0x00 AT+BLEKEYBOARDCODE=00-00-00-00-00-00-00-00 OK # 此时系统知道所有键都已释放更复杂的例子:发送组合键 Ctrl+Alt+Delete
# 1. 按下 Ctrl 和 Alt # Modifier: 左Ctrl (0x01) + 左Alt (0x04) = 0x05 # Delete键的扫描码是 0x4C (Keyboard Delete Forward) AT+BLEKEYBOARDCODE=05-00-4C-00-00-00-00-00 OK # 2. 释放所有按键 AT+BLEKEYBOARDCODE=00-00-00-00-00-00-00-00 OK踩坑实录:最常见的错误就是只发送“按下”命令,忘了发送“释放”命令。结果就是系统认为按键卡住了,导致连续输入或功能错乱。务必把“释放”操作当成一个必须的、独立的步骤来对待。在你的代码逻辑里,最好封装一个
keyPress(keycode, modifier)和一个keyRelease()函数。
3.3 模拟鼠标操作:移动、点击与滚动
鼠标控制主要依赖两个命令:AT+BLEHIDMOUSEMOVE和AT+BLEHIDMOUSEBUTTON。
鼠标移动 (AT+BLEHIDMOUSEMOVE): 命令格式:AT+BLEHIDMOUSEMOVE=<X>,<Y>,<Scroll>,<Pan>四个参数都是8位有符号整数(-128 到 +127)。
- X: 水平移动量。正数向右,负数向左。
- Y: 垂直移动量。正数向下,负数向上。
- Scroll: 滚轮滚动。正数向下滚,负数向上滚。
- Pan: 水平滚动(触摸板或某些鼠标的侧滚轮)。正数向右,负数向左。
# 鼠标向右移动100像素,向下移动50像素 AT+BLEHIDMOUSEMOVE=100,50 OK # 仅向上滚动30个单位(保持X, Y为0,用逗号占位) AT+BLEHIDMOUSEMOVE=,,30, OK鼠标按键 (AT+BLEHIDMOUSEBUTTON): 命令格式更灵活:AT+BLEHIDMOUSEBUTTON=<ButtonMask>,<Action>[,<HoldTime>]
- ButtonMask: 按键掩码字符串。
L左键,R右键,M中键,B后退,F前进。可以组合,如LR表示同时按左右键。 - Action: 动作。
PRESS(按下)、CLICK(单击)、DOUBLECLICK(双击)、HOLD(按住)。 - HoldTime(可选): 当动作为
HOLD时,指定按住的时间(毫秒)。
# 双击左键 AT+BLEHIDMOUSEBUTTON=L,DOUBLECLICK OK # 实现拖拽操作:按下左键 -> 移动 -> 释放左键 AT+BLEHIDMOUSEBUTTON=L,PRESS # 按下左键 OK AT+BLEHIDMOUSEMOVE=50,30 # 按住的同时移动鼠标 OK AT+BLEHIDMOUSEBUTTON=0 # 释放所有按键(ButtonMask为0) OK # 按住后退键200毫秒 AT+BLEHIDMOUSEBUTTON=B,HOLD,200 OK实操心得:鼠标移动是相对移动,而不是设置绝对坐标。参数值代表“移动的速度和方向”,系统会将其累加。如果你想实现平滑移动,应该在循环中发送较小的增量值(如每次移动5-10个像素),而不是一次性发送一个大值。对于点击操作,直接使用CLICK或DOUBLECLICK是最可靠的,它封装了按下和释放的完整过程。
3.4 媒体与系统控制:一键静音与调音量
AT+BLEHIDCONTROLKEY命令用于发送消费电子控制码,可以控制媒体播放、音量、亮度等。
# 播放/暂停 AT+BLEHIDCONTROLKEY=PLAYPAUSE OK # 音量增加,并保持按下500毫秒(实现长按快速增音量效果) AT+BLEHIDCONTROLKEY=VOLUME+,500 OK # 静音切换 AT+BLEHIDCONTROLKEY=MUTE OK # 发送原始的16位控制码(0x00E9 = 下一曲) AT+BLEHIDCONTROLKEY=0x00E9 OK这个功能非常实用,可以轻松制作一个蓝牙媒体遥控器。需要注意的是,不同操作系统对某些控制键(如BRIGHTNESS+)的支持程度可能不同,通常媒体和音量键的兼容性最好。
3.5 游戏手柄与MIDI支持
对于需要更多交互的场景,模块还支持游戏手柄和MIDI。
游戏手柄 (AT+BLEHIDGAMEPADEN,AT+BLEHIDGAMEPAD): 游戏手柄默认是禁用的(因iOS/macOS兼容性问题),需要先启用。
# 启用游戏手柄服务 AT+BLEHIDGAMEPADEN=1 OK ATZ OK # 按下“右”方向键和1号按钮(位掩码0x01) AT+BLEHIDGAMEPAD=1,0,0x01 OK # 释放所有按键 AT+BLEHIDGAMEPAD=0,0,0x00 OKMIDI支持 (AT+BLEMIDIEN,AT+BLEMIDITX): MIDI服务可以让你的模块变成一个蓝牙MIDI设备,与音乐软件通信。
# 启用MIDI服务 AT+BLEMIDIEN=1 OK ATZ OK # 发送一个MIDI事件:通道1(0x90),音符60(0x3C),力度127(0x7F) # 这表示在通道1上以最大力度按下中央C(Middle C) AT+BLEMIDITX=90-3C-7F OK4. GAP与GATT服务自定义配置
除了使用预设的HID服务,你经常需要自定义设备行为,比如改个设备名、调整广播参数,甚至创建自己的数据服务。这就涉及到GAP(通用访问配置文件)和GATT的配置命令。
4.1 GAP配置:设备如何“自我介绍”
GAP管理设备的广播和连接参数,决定了设备如何被外界发现和连接。
修改设备名称: 这是最常用的命令之一,让你的设备在手机蓝牙列表里显示一个有意义的名字。
# 读取当前设备名 AT+GAPDEVNAME UART OK # 设置新设备名 AT+GAPDEVNAME=My_BLE_Keyboard OK ATZ # 重启生效 OK调整广播与连接间隔:AT+GAPINTERVALS命令可以精细控制功耗和响应速度。
- 连接间隔:设备与手机通信的频率。间隔越短(如20ms),响应越快,但功耗越高。间隔越长(如100ms),越省电,但延迟可能增加。
- 广播间隔:设备发送“我在这里”信号的频率。同样,间隔短则容易被快速发现,但耗电。
# 读取当前间隔设置 AT+GAPINTERVALS 20,100,100,30 # 含义:最小连接间隔20ms,最大连接间隔100ms,快速广播间隔100ms,快速广播超时30秒 # 修改为更省电的设置(连接慢一点,广播也慢一点) AT+GAPINTERVALS=50,200,500,30 OK ATZ OK重要警告:不要随意修改这些间隔值,尤其是改得过小。某些操作系统(特别是iOS)对连接参数有严格限制,超出范围的参数可能导致无法连接。除非有明确的低功耗或低延迟需求,否则建议使用默认值。
设置原始广播数据:AT+GAPSETADVDATA是高级功能,允许你完全自定义广播包的内容。例如,你可以在广播包里直接声明“我支持心率服务”,这样专门的心率App就能直接过滤和发现你的设备,而不用连接后再去服务列表里查找。
# 广播数据:标志位(可发现、仅BLE),并声明支持心率服务(0x180D)和设备信息服务(0x180A) AT+GAPSETADVDATA=02-01-06-05-02-0D-18-0A-18 OK ATZ OK这个命令非常强大,但使用需谨慎,格式错误会导致设备无法被正常发现。建议先备份原始配置或知道如何用AT+FACTORYRESET恢复出厂设置。
4.2 自定义GATT服务:打造专属数据通道
这是AT命令集里最强大的部分之一。你可以不满足于键盘鼠标,而是创建自己的服务和特征值,用来传输任意自定义数据,比如传感器读数、控制命令等。
创建自定义服务的完整流程:
# 1. 清除之前的自定义配置(重要!) AT+GATTCLEAR OK # 2. 添加一个自定义服务。这里使用标准的16位UUID(0x180F是电池服务,仅作示例) AT+GATTADDSERVICE=UUID=0x180F 1 # 返回服务索引ID为1,记下来! OK # 3. 为这个服务添加一个特征值。 # UUID: 0x2A19 (电池电量特征) # PROPERTIES: 0x10 (Notify,允许客户端订阅通知) # MIN_LEN/MAX_LEN: 值长度1字节 # VALUE: 初始值100 (0x64,表示100%电量) AT+GATTADDCHAR=UUID=0x2A19,PROPERTIES=0x10,MIN_LEN=1,MAX_LEN=1,VALUE=100 1 # 返回特征值索引ID为1,记下来! OK # 4. 重启使服务生效 ATZ OK现在,你的设备就拥有了一个标准的电池服务。手机上的蓝牙调试App(如nRF Connect)连接后,就能看到这个服务,并订阅这个特征值。当你用AT+GATTCHAR=1,50命令更新电量值为50%时,手机会自动收到通知。
创建完全自定义的128位UUID服务: 对于私有协议,你需要使用128位UUID以避免冲突。
AT+GATTCLEAR OK # 添加一个128位UUID的服务 AT+GATTADDSERVICE=UUID128=00-11-22-33-44-55-66-77-88-99-AA-BB-CC-DD-EE-FF 1 OK # 为该服务添加一个可读写的自定义特征值 AT+GATTADDCHAR=UUID=0x0001,PROPERTIES=0x0A,MIN_LEN=4,MAX_LEN=4,VALUE=00-00-00-00 1 OK ATZ OK这里PROPERTIES=0x0A是0x02 (Read) + 0x08 (Write),表示这个特征值既可读又可写。客户端可以向你发送4字节的命令,你也可以向客户端通知4字节的数据。
查看与读写自定义特征值:
# 列出所有自定义服务/特征值 AT+GATTLIST ID=01,UUID=0x180F ID=01,UUID=0x2A19,PROPERTIES=0x10,MIN_LEN=1,MAX_LEN=1,VALUE=0x64 OK # 读取索引为1的特征值当前值 AT+GATTCHAR=1 0x64 OK # 写入新值到索引为1的特征值 AT+GATTCHAR=1,75 OK # 再次读取确认 AT+GATTCHAR=1 0x4B # 75的十六进制 OK5. 实战避坑指南与高级技巧
掌握了基本命令,我们来看看实际项目中会遇到哪些坑,以及如何优雅地跨过去。
5.1 连接、绑定与配对问题
问题:手机能搜索到设备,但连接失败,或者连接后HID功能不工作。排查:
- 确认HID已启用并重启:这是首要检查项。确保执行了
AT+BLEHIDEN=1和ATZ。 - 检查绑定状态:HID通常需要绑定(Pairing/Bonding)。在手机蓝牙设置里,找到你的设备,尝试“忽略”或“取消配对”,然后重新连接并完成配对流程。
- 清除模块绑定信息:如果模块里存储了旧的、错误的绑定信息,可能导致新设备无法连接。使用
AT+GAPDELBONDS清除模块端的所有绑定信息,然后重启再试。 - 检查GAP可连接性:确保设备处于可连接状态
AT+GAPCONNECTABLE=1(默认是1)。
5.2 电源管理与复位策略
问题:设备运行不稳定,偶尔无响应。排查与策略:
- 复位是关键:任何修改了GATT服务、GAP参数、设备名称、HID使能状态的命令,几乎都必须后跟
ATZ重启才能生效。在你的代码中,最好将这些“配置命令”和ATZ打包成一个初始化函数。 - 理解命令的持久化:像
AT+GAPDEVNAME,AT+GAPINTERVALS,AT+GATTADDSERVICE等命令,会将配置保存到模块的非易失性存储器中。即使断电,下次上电配置依然存在。要彻底清除,需使用AT+FACTORYRESET。 - 供电要足:BLE射频部分对电源噪声敏感。确保使用稳定的3.3V电源,并在电源引脚附近放置足够的去耦电容(如10uF电解电容并联0.1uF陶瓷电容)。
5.3 编写健壮的通信代码
直接通过串口发送AT命令,需要考虑通信的可靠性。
// 伪代码示例:一个健壮的发送-等待响应函数 bool sendATCommand(const char* cmd, char* response, int respSize, unsigned long timeout) { serialPort.println(cmd); // 发送命令,带换行 unsigned long startTime = millis(); int index = 0; memset(response, 0, respSize); // 清空响应缓冲区 while (millis() - startTime < timeout) { if (serialPort.available()) { char c = serialPort.read(); if (index < respSize - 1) { response[index++] = c; } // 检查是否收到完整的响应(以OK或ERROR结尾) if (index >= 4 && strstr(response + index - 4, "OK\r\n") != NULL) { response[index] = '\0'; return true; } if (index >= 7 && strstr(response + index - 7, "ERROR\r\n") != NULL) { response[index] = '\0'; return false; } } } response[index] = '\0'; // 超时 return false; }关键点:
- 等待响应:发送命令后一定要等待模块回复
OK或ERROR,不要立即发送下一条。 - 处理超时:设置合理的超时时间(如2秒),防止因模块忙或通信错误导致程序卡死。
- 解析响应:根据响应判断命令是否成功,并据此决定后续逻辑。
5.4 性能优化与多设备兼容性
- 键盘输入防冲突:快速连续发送键盘命令时,确保前一个按键的“释放”报告已发送,再发送下一个按键的“按下”报告。可以在代码中为键盘操作加一个简单的状态锁或队列。
- 鼠标移动平滑处理:不要用
delay来制造移动间隔。使用非阻塞的定时(如Arduino的millis()),在固定时间间隔(如每20ms)发送一次小的位移增量,可以实现非常平滑的光标移动。 - iOS/macOS特殊处理:苹果设备对BLE连接参数和某些服务(如游戏手柄)更挑剔。如果目标平台是苹果系,建议:
- 使用默认或较大的连接间隔。
- 避免启用游戏手柄服务 (
AT+BLEHIDGAMEPADEN)。 - 确保设备名称和广播数据规范。
- 广播超时与低功耗:利用
AT+GAPINTERVALS的第四个参数(快速广播超时)。例如,设置超时为30秒。设备在前30秒以较快的间隔(如100ms)广播,便于快速被发现。30秒后若未连接,自动切换到更慢的间隔(如1秒),显著降低待机功耗。
6. 项目构思与扩展方向
掌握了这些AT命令,你能做的远不止一个简单的键盘。这里提供几个项目思路:
- 蓝牙智能遥控器:结合加速度计和陀螺仪,用
AT+BLEHIDMOUSEMOVE实现空中鼠标。用几个物理按键通过AT+BLEKEYBOARDCODE映射为多媒体键(播放、音量),再用AT+BLEHIDCONTROLKEY控制PPT翻页。 - 无障碍输入设备:为行动不便者制作一个简单的开关扫描式输入器。一个大按钮通过不同的按压时长或次数,组合成
AT+BLEKEYBOARDCODE命令,模拟键盘输入,实现文字沟通。 - 传感器数据中继器:将温湿度传感器(如DHT22)的数据,通过自定义GATT服务(
AT+GATTADDCHAR)发送到手机App。手机App订阅特征值通知,即可实时接收并显示传感器数据。 - 自动化脚本触发器:做一个物理的“宏按键”。按下按钮,模块通过一系列
AT+BLEKEYBOARDCODE命令,模拟输入一串复杂的快捷键组合(如Win+R-> 输入“cmd” -> 回车),一键打开命令行窗口。
最后,再分享一个调试小技巧:务必使用一个串口调试助手(如CoolTerm, Putty)来手动发送AT命令并观察响应。在编写自动控制代码前,先手动把整个流程走通,记录下每一步正确的命令和响应。这能帮你快速定位是命令逻辑问题,还是代码通信问题。BLE的世界很大,AT命令只是为你打开了一扇便捷的门,门后的精彩,就等你用代码去实现了。