学用系列丨如何用Workbuddy将AlphaPi开发板改造成蓝牙翻页器?
上周接了一个两小时的内部技术分享,翻页笔在抽屉里躺了半年,电池漏液了。临时买蓝牙翻页器又碰上电脑蓝牙死活连不上,看着桌上那块吃灰的AlphaPi开发板,我说干脆自己动手做一个吧。于是用Workbuddy这个AI辅助开发工作台,花了一个晚上,把一块基于ESP32-S3的AlphaPi开发板改造成了支持蓝牙BLE的PPT翻页器。这篇文章就是完整的改造记录,原理、接线、代码、烧录、踩坑全在里面。适合手上有类似开发板、又想搞个实用小外设的嵌入式入门玩家,也适合经常做PPT汇报、不想随身带遥控器的朋友参考。
把开发板变成翻页器,听起来像是个偏门需求,但做下来你会发现,这个项目把蓝牙HID协议、按键去抖、低功耗配置、外设调试这些嵌入式常见技能点全串起来了。一个晚上能搞定的事情,性价比极高。
1. 项目思路与原理浅析
1.1 蓝牙翻页器的本质是什么
翻页器在电脑眼里就是一个键盘,只是它只会按两个键:上一页和下一页。具体到蓝牙场景,它就是一个BLE HID键盘设备。所谓HID,全称Human Interface Device,人机交互设备,鼠标、键盘、触摸板都走这套标准。只要设备按HID协议上报按键码,操作系统就会把它当成键盘用,完全不需要装驱动。
这里有个关键点,很多人第一次接触时容易绕晕:蓝牙键盘和蓝牙耳机不一样,它用的是GATT(通用属性协议)下的HID Service,而不是A2DP音频流。也就是说,翻页器本质上是在蓝牙协议栈上跑了一个"键盘服务",电脑端通过这个服务收到按键事件,解析成PageUp和PageDown指令。
我们要发送的按键码也很固定,在USB HID Usage Table里,PageUp键的Usage ID是0x4B,PageDown键的Usage ID是0x4E。后面写代码的时候会用到,这里先有个印象。
1.2 为什么选择AlphaPi开发板
AlphaPi这类开发板在市面上很多,核心芯片是乐鑫ESP32-S3。选它做翻页器,我看中的是以下几点。
第一,原生支持蓝牙5.0,BLE协议栈完整,官方SDK里就带了BLE HID的示例工程,等于最难的协议部分官方已经给铺好了路。
第二,GPIO引脚充足,板载USB转串口芯片,Type-C线一插就能烧录,不需要额外买下载器。
第三,功耗控制灵活。ESP32-S3支持Modem Sleep和Light Sleep,翻页器这种低频使用场景,设置好睡眠模式,一节18650电池或一个小锂电池能撑很久。
你可能会问,市面上几十块钱的翻页器不也挺好吗?确实,但自制的意义在于可控。市售翻页器按键功能固定,你没法自己加按键,也没法改造成激光笔、演示遥控器或者智能家居遥控器。用开发板做,固件完全自己掌握,想加什么功能改代码就行。
1.3 Workbuddy在整个流程里扮演什么角色
Workbuddy在这里的作用,不是聊天工具,而是一个AI辅助开发工作台。它的用法是:把需求用自然语言描述清楚,让它生成工程骨架和核心代码,再由你审查、调整、烧录验证。这个项目里,我主要用Workbuddy做了三件事:
- 让它生成ESP32-S3的BLE HID工程代码,包括HID报告描述符和键盘上报逻辑;
- 让它帮我把按键去抖和长按判定的代码整理成状态机,省去自己查资料拼代码的时间;
- 在调试阶段,通过工作台的串口终端直接看日志,排查蓝牙连接问题。
有一点必须说清楚:AI生成的代码不是拿来就能跑。开发板引脚、协议栈版本、SDK配置这些细节,AI不一定知道你手里这块板子的实际接线,所以最终还是要人工核对。Workbuddy的价值是把80%的重复工作干掉,剩下的20%是把关和适配。
2. 硬件准备与按键电路设计
2.1 需要的物料清单
翻页器本身结构很简单,物料也很少,大多数东西你手边可能已经有了。
| 物料 | 数量 | 说明 |
|---|---|---|
| AlphaPi开发板 | 1块 | ESP32-S3核心,带USB和蓝牙 |
| 轻触按键 | 2个 | 6x6mm或12x12mm均可,手感差异而已 |
| 面包板 | 1块 | 用于快速原型验证 |
| 杜邦线 | 若干 | 母对母、公对母都备一点 |
| Type-C数据线 | 1根 | 要求支持数据传输,不是纯充电线 |
| 锂电池 | 可选 | 3.7V锂电池,带保护板,容量1000mAh左右够用 |
按键建议选带帽的轻触开关,按压手感好一些。做原型验证时用面包板最省事,但如果你打算长期用,建议后面焊一块小板,把按键和电池座固定在一起,做成一个真正握在手里的翻页器。
2.2 按键电路怎么接
按键电路在这个项目里几乎是最简单的一环,但引脚的选型有个坑,必须单独拎出来说。
我最初的设想是两个按键各接一个GPIO,按下时引脚接地,读取到低电平就触发翻页。AlphaPi开发板没有板载按键,所以需要外接。连接方式如下:
- 按键A(上一页):一端接GPIO4,另一端接GND;
- 按键B(下一页):一端接GPIO5,另一端接GND。
用杜邦线把按键、面包板、开发板连起来就行,不需要焊接。代码里把GPIO4和GPIO5配置为输入模式并启用内部上拉,这样按键悬空时读到高电平,按下时被拉低,逻辑清晰且不需要额外电阻。
这里有个小细节:我特意避开了GPIO0。ESP32-S3的GPIO0是BOOT引脚,上电时的电平状态会影响启动模式。如果你把按键接在GPIO0上,每次按翻页键的同时可能会触发下载模式,导致开发板黑屏或重新启动。这个坑我踩过一次,不会建议你再踩。
为什么选GPIO4和GPIO5?这两个引脚在ESP32-S3上默认就是普通GPIO,不涉及JTAG或USB功能,也不影响启动,用作按键输入非常稳妥。当然,如果你用的开发板引脚定义不同,以你板子上的丝印和原理图为准。
2.3 上电测试与固件准备
电路接好之后,先不要急着写代码。把开发板通过Type-C线连到电脑,正常情况下电脑会出现一个新的串口设备,Windows系统下通常是COM3或COM4,Linux/macOS下一般是/dev/ttyACM0或/dev/ttyUSB0。这个串口就是开发板的烧录通道和日志输出通道。
我建议在Workbuddy里新建一个"嵌入式串口终端"任务,把串口打开,看看开发板启动日志是否正常输出。如果日志正常,说明板子供电和USB转串口链路没问题,可以进入下一步。如果没日志,大概率是USB线不支持数据传输,换一根线再试。这个排查顺序很重要,别一上来就怀疑代码有问题。
3. 用Workbuddy生成核心代码
3.1 在Workbuddy中创建项目
硬件就绪后,我开始在Workbuddy里创建项目。它的工作方式不是直接打开一个IDE,而是通过对话和指令管理整个工程流程。
我给Workbuddy输入的第一条需求是这样描述的:
在ESP32-S3开发板上,使用ESP-IDF和NimBLE协议栈,创建一个BLE HID键盘设备。两个GPIO按键分别发送PageUp和PageDown键码,GPIO4对应PageUp,GPIO5对应PageDown。按键需要做10ms去抖,长按800ms以上发送ESC键。请生成完整的工程代码。
Workbuddy很快给出建议,让我基于官方示例ble_hid_device_demo来改,而不是从零手写。这个建议很关键,因为官方示例已经包含了完整的HID Service注册、报告映射、连接管理代码,我们需要做的只是简化设备功能,把它变成只发几个键的翻页器。
我按它的引导,在本地创建了工程目录,拷贝官方示例,然后用Workbuddy生成了一份修改清单,重点改了三个文件:main.c里的按键处理、hid_dev.c里的报告映射、以及ble_hid_device_demo.c里的广播名称和连接参数。
3.2 蓝牙HID键盘的关键配置
BLE HID键盘最核心的就是HID报告描述符。它规定了设备能上报哪些类型的输入数据,操作系统据此解析按键。下面是标准的键盘HID Report Map,我直接抄了官方示例,略作删减:
static const uint8_t hid_report_map[] = { 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x06, // Usage (Keyboard) 0xA1, 0x01, // Collection (Application) 0x05, 0x07, // Usage Page (Key Codes) 0x19, 0xE0, // Usage Minimum (Keyboard Left Control) 0x29, 0xE7, // Usage Maximum (Keyboard Right GUI) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x75, 0x01, // Report Size (1) 0x95, 0x08, // Report Count (8) 0x81, 0x02, // Input (Data, Variable, Absolute) 0x95, 0x01, // Report Count (1) 0x75, 0x08, // Report Size (8) 0x81, 0x03, // Input (Constant) 0x95, 0x05, // Report Count (5) 0x75, 0x01, // Report Size (1) 0x05, 0x08, // Usage Page (LEDs) 0x19, 0x01, // Usage Minimum (Num Lock) 0x29, 0x05, // Usage Maximum (Kana) 0x91, 0x02, // Output (Data, Variable, Absolute) 0x95, 0x01, // Report Count (1) 0x75, 0x03, // Report Size (3) 0x91, 0x03, // Output (Constant) 0x95, 0x06, // Report Count (6) 0x75, 0x08, // Report Size (8) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x65, // Logical Maximum (101) 0x05, 0x07, // Usage Page (Key Codes) 0x19, 0x00, // Usage Minimum (0) 0x29, 0x65, // Usage Maximum (101) 0x81, 0x00, // Input (Data, Array) 0xC0, // End Collection };这段描述符的意思是:本设备是一个键盘,有8个修饰键(Ctrl、Shift、Alt等),后面跟6个普通按键槽位。操作系统会自动处理这个协议,我们只需要往指定的数据数组里填键码就可以翻页了。
我让Workbuddy帮我确认了PageUp和PageDown的Usage ID。它给出的结果是:PageUp是0x4B,PageDown是0x4E。这个数据来自USB-IF的HID Usage Table,属于行业标准,可以直接用。
3.3 按键扫描与去抖逻辑
物理按键按下和释放时,触点会产生机械抖动,通常持续几毫秒到十几毫秒。如果不做去抖,一次按键可能会被识别成多次触发,翻页会一下跳好几页。我的处理方式是典型的软件去抖:周期性扫描按键状态,连续3次读到相同电平才认为状态有效。
以下是按键扫描任务的核心代码,由Workbuddy生成后我做了注释和调整:
#define KEY_UP_PIN GPIO_NUM_4 #define KEY_DOWN_PIN GPIO_NUM_5 static bool key_saved[2] = {true, true}; static uint8_t key_debounce[2] = {0, 0}; static bool key_state[2] = {false, false}; void key_scan_task(void *arg) { const gpio_num_t key_pins[2] = {KEY_UP_PIN, KEY_DOWN_PIN}; while (1) { for (int i = 0; i < 2; i++) { bool level = gpio_get_level(key_pins[i]); if (level != key_saved[i]) { if (++key_debounce[i] >= 3) { key_saved[i] = level; key_debounce[i] = 0; key_state[i] = !level; // 按键按下时引脚为低电平 } } else { key_debounce[i] = 0; } } vTaskDelay(10 / portTICK_PERIOD_MS); } }这段代码用了一个10ms的定时扫描周期。连续3次(即约30ms)读到相反电平,才确认状态翻转,既简单又可靠。
长按800ms发送ESC的逻辑,是在主循环里判断按键按下的持续时间:
uint32_t press_start[2] = {0, 0}; for (int i = 0; i < 2; i++) { if (key_state[i]) { if (press_start[i] == 0) { press_start[i] = xTaskGetTickCount(); } } else if (press_start[i] != 0) { uint32_t duration = (xTaskGetTickCount() - press_start[i]) * portTICK_PERIOD_MS; if (duration > 800) { sendKey(0x29); // ESC } else { sendKey(i == 0 ? 0x4B : 0x4E); // PageUp / PageDown } press_start[i] = 0; } }sendKey的逻辑是先发送包含键码的键盘报告,再发送一个全零的空报告,表示按键释放。两次报告之间间隔约20ms,太短的间隔有时候会被系统吞掉,也不要太长,否则会感觉按键不跟手。
3.4 编译烧录与日志验证
代码调整完成后,开始编译烧录。Workbuddy会调用本地安装的ESP-IDF工具链,本质上就是执行几条命令。你也可以直接在当前工程目录手动执行:
idf.py set-target esp32s3 idf.py build idf.py -p /dev/ttyACM0 flash monitorWindows系统把端口换成类似COM6的编号即可。如果烧录时提示连接失败,按住开发板上的BOOT键再点烧录,同时观察串口日志是否出现"Downloading"字样。有些开发板带自动下载电路,不需要手动按键,但AlphaPi这种板子还是建议手动按住BOOT更稳。
烧录完成后,串口终端里会出现类似下面的日志:
I (401) BLE_HID: BLE HID device demo started I (411) BLE_HID: Advertising with name: alphapi-ppt I (421) BLE_HID: Advertising start successfully看到"Advertising start successfully"就说明蓝牙已经开始广播,可以用电脑搜索设备了。
4. 连接调试与常见问题排查
4.1 电脑收不到蓝牙广播
这是最常见的问题,很多人烧录后打开电脑的蓝牙设置,刷新半天就是看不到设备。
先从广播参数排查。确认代码里的广播名称不是空的,且你搜索时用的名称一致。常用做法是在广播数据里设置设备名,同时设置一个可被发现的标志:
static const char ble_device_name[] = "alphapi-ppt";如果广播名正常,还是搜不到,多半是广播类型或间隔问题。NimBLE默认的广播间隔一般是40ms到100ms,这在大多数情况下没问题,但如果周围蓝牙设备很多,存在信道拥挤,把广播间隔调大一点有时反而能提高被发现概率。当然,广播间隔太大会增加连接延迟,建议不要超过200ms。
还有一个容易被忽略的点:如果之前你已经把这台电脑的蓝牙和开发板配对过,然后改了代码重新烧录,电脑可能还保留着旧设备的缓存,导致新设备名搜不到。此时去Windows的蓝牙设置里删除那个旧设备再重新搜索就行。
4.2 配对之后没有翻页效果
能搜到设备、也能配对,但按按键电脑没反应。这个问题我花了最多时间排查,最后的结论很有意思:很多系统把"蓝牙键盘"和"蓝牙其他设备"分开管理。
在Windows系统里,你打开蓝牙设置添加设备时,如果选的是"其他设备"类别,系统可能只是建立了BLE连接,但没有把它识别为键盘输入设备。正确做法是添加设备时选择"蓝牙键盘"或"输入设备"分类,让系统按HID设备处理。
另外,电脑系统对蓝牙键盘有"配对"和"仅连接"两种状态。从手机或者电脑发出的普通BLE连接,可能只是GATT连接,但HID键盘必须走完整的配对流程。最简单的排查方法是:Windows控制面板里删除该设备,然后重新走"添加蓝牙键盘"流程。在macOS里则是"系统设置-蓝牙"里点一下"忘记此设备",再重新添加。
4.3 按键偶尔失效或响应慢
翻页器按下去之后,PPT有时过一两秒才有反应,甚至偶尔丢键。这个问题的根源在BLE连接间隔。
BLE的通信不是持续不断的,而是在约定的连接间隔内传输数据。如果连接间隔设置得太大,比如50ms以上,按键数据就有明显的传输延迟。推荐把连接间隔设置在15ms到30ms之间,这样按键延迟基本无感。在NimBLE的ble_gap_conn_params_t结构里配置:
ble_gap_conn_params_t conn_params = { .scan_itvl = 0x20, .scan_window = 0x20, .itvl_min = 0x18, // 30ms .itvl_max = 0x28, // 50ms .latency = 0, .supervision_timeout = 400, };除了连接间隔,还要检查按键的GPIO配置。ESP32-S3启用了内部上拉后,按键悬空电平稳定,但如果你把按键接到了其他复用引脚上,比如GPIO36之类的ADC引脚,可能因为内部下拉或浮空导致误触发。此时在代码里显式配置上拉:
gpio_config_t io_cfg = { .pin_bit_mask = (1ULL << KEY_UP_PIN) | (1ULL << KEY_DOWN_PIN), .mode = GPIO_MODE_INPUT, .pull_up_en = GPIO_PULLUP_ENABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, }; gpio_config(&io_cfg);4.4 耗电过快问题
翻页器是移动设备,功耗是最难绕的问题。做完之后我发现如果一直开着蓝牙广播,小锂电池撑不到一整天。
合理的做法是设置广播超时。翻页器在使用时才对码,平时完全可以停止广播。可以在连接成功后就关闭广播,断开连接后重新开启一段时间的广播,比如只广播60秒,这样不用的时候系统不会再持续广播耗电。
代码里可以用定时器实现:
static void stop_advertising_after_timeout(void) { // 连接成功后调用 ble_gap_stop_advertising(); }此外,按键扫描任务运行时也会持续唤醒CPU。可以在没有按键操作的10秒后进入Light Sleep模式,让CPU在大部分时间里处于低功耗状态:
esp_sleep_enable_timer_wakeup(10 * 1000 * 1000); esp_light_sleep_start();实测下来,优化后整机待机电流从原来的十几毫安降到了两三毫安,翻页器放在桌面上隔两三周用一次都没问题。如果要进一步降低功耗,可以考虑用深度睡眠加外部GPIO唤醒,但那样配对状态可能会丢失,每次都要重新连接,我觉得得不偿失。
5. 还能怎么升级这个翻页器
翻页器做完之后,我顺手在Workbuddy里跟了一句"能不能给这个设备加个电量上报功能",它给了我几个方案,虽然最后没全部实现,但思路值得记录。
方向一是加LED指示灯。连接成功时点亮蓝色LED,按键时闪一下白色,低电量时闪红色。AlphaPi开发板上一般有可编程LED,稍微加两行代码就能做。
方向二是增加手势翻页。利用ESP32-S3的I2S接口接一个小型加速度传感器,检测手腕左右晃动来翻页。不过这个功能对算法要求不低,误触率需要慢慢调。
方向三是改成双模。目前是纯BLE键盘,如果你把开发板切换到USB模式配置HID,同一个按键既能通过蓝牙用,也能通过USB有线用,适合办公室和家里两种场景切换。
整个项目下来,我最深的体会是:蓝牙翻页器看着是个小东西,但把它做扎实了,方案选型、协议配置、外设处理、功耗优化这些环节一个都少不了。Workbuddy确实帮我省了不少查文档的时间,但真正能不能跑起来,还得靠一遍遍烧录和调试去验证。如果你也想试试,照着这篇文章的步骤来,周末花一个下午,你也能拥有一块"会翻页的开发板"。