
1. 项目概述一个可交互的蓝牙电子糖果心情人节期间那些印着“BE MINE”、“HUG ME”等短句的糖果心Conversation Hearts总是能传递简单而直接的情感。你有没有想过如果能亲手制作一个可以随时改变文字和颜色的电子版糖果心会是什么体验这个项目就是将这个想法变成了现实。它基于Adafruit的Circuit Playground Bluefruit开发板和TFT Gizmo圆形显示屏使用CircuitPython编程打造了一个既能通过按键随机切换、又能通过手机蓝牙无线控制的智能电子糖果心。对于刚接触硬件编程的朋友来说这个项目是一个绝佳的起点。它不像传统的嵌入式开发那样充斥着复杂的C语言指针和寄存器操作而是用我们熟悉的Python语法来控制硬件。你不需要焊接只需要像搭积木一样将显示屏扣在开发板上然后编写几十行代码就能让一块冰冷的电路板变成一个充满趣味和温情的交互装置。而对于有经验的开发者这个项目深入展示了CircuitPython中displayio图形库的灵活运用、蓝牙低能耗BLE服务的集成以及如何通过软件技巧如调色板操作来高效管理硬件资源。无论你的水平如何都能从中获得动手的乐趣和实用的知识。接下来我将带你从零开始完整复现这个项目。我会详细解释每一个步骤背后的原理分享我在调试过程中踩过的坑和总结的技巧并提供两种实现版本基础按键版和蓝牙控制版的详细代码解析。我们的目标是让你不仅能“抄作业”成功更能理解“为什么这么写”从而具备举一反三的能力去创造属于你自己的硬件互动作品。2. 硬件准备与CircuitPython环境搭建2.1 核心硬件选型解析这个项目的硬件核心非常简洁主要就是两块板子Circuit Playground Bluefruit (CPB)这是项目的大脑。我选择它有几个关键理由。首先它原生支持CircuitPython省去了我们配置复杂开发环境的麻烦。其次它内置了蓝牙低能耗BLE模块这是我们实现手机无线控制的基础。最后它板载了按键、加速度计、麦克风、蜂鸣器和10颗可编程RGB LEDNeoPixels为未来的功能扩展比如通过摇晃或拍手来切换信息留下了巨大空间。如果你手头有更早的Circuit Playground Express它也能运行基础版代码但会缺少蓝牙功能。TFT Gizmo显示屏这是项目的脸面。它是一个1.54英寸、240x240像素的圆形彩色TFT显示屏通过边缘的弹簧针脚直接扣在CPB板上无需焊接连接稳固。其驱动芯片ST7789已被CircuitPython的adafruit_st7789库完美支持而adafruit_gizmo库则对其进行了更高层次的封装让我们用几行代码就能轻松驱动。圆形的外观与糖果心的主题简直是天作之合。提示购买时请认准“TFT Gizmo for Circuit Playground”版本它有专用的连接器。Adafruit也有适用于其他主板如CLUE的Gizmo接口不通用。除了这两大件你还需要一根优质的数据USB线用于供电和编程和一台电脑。很多新手会栽在数据线上——有些USB线只能充电不能传输数据。如果你连接电脑后板子没有任何反应第一个要怀疑的就是数据线。我个人的经验是使用手机原配的数据线或者知名品牌的USB线成功率会高很多。2.2 为开发板刷入CircuitPythonCircuitPython是MicroPython的一个分支由Adafruit积极维护对自家硬件支持极好。为CPB安装CircuitPython的过程非常简单可以理解为给板子“安装操作系统”。详细步骤与原理下载固件访问 circuitpython.org 找到Circuit Playground Bluefruit的页面下载最新的.uf2固件文件。.uf2是微软设计的一种特殊镜像格式主板将其识别为一个可拖放文件的磁盘极大地简化了烧录过程。进入引导加载模式用数据线连接CPB和电脑。正常情况下你会看到一个名为CPLAYBTBOOT的磁盘驱动器出现。如果没出现或者你想更新固件就需要手动进入引导模式。找到板子中央的复位按钮Reset。快速双击它。此时板载的10颗NeoPixel LED会先全部变红然后全部变绿。同时电脑上会弹出CPLAYBTBOOT磁盘。实操心得双击节奏很重要不要太慢也不要太快得像抽搐。如果一次不成功多试几次。有时单次点击也能进入。观察LED颜色是关键全红后变绿说明成功如果一直全红通常是USB线或端口有问题。拖放烧录将下载好的.uf2文件直接拖入CPLAYBTBOOT磁盘。拖入后磁盘会自动弹出并消失。稍等片刻一个新的名为CIRCUITPY的磁盘会出现。恭喜CircuitPython已经成功运行在你的板子上了这个过程之所以简单是因为CPB的引导加载程序Bootloader已经预先烧录好了。它负责检测USB连接和按钮状态并管理.uf2文件的拷贝与刷写。CIRCUITPY磁盘就是我们后续进行编程和存放资源文件的地方你可以像操作U盘一样直接编辑里面的code.py文件板子会自动运行最新的代码。3. 基础版代码实现与核心原理剖析基础版实现了糖果心的核心功能在屏幕上显示一个心形图案和两行文字每次按下板载的A或B键就会随机切换一组预设的消息和心形颜色。让我们深入代码看看这一切是如何运作的。3.1 项目文件结构与库管理在开始编码前我们需要准备好所有依赖。建议直接从Adafruit学习页面的项目仓库下载完整的ZIP包里面包含了代码、字体文件和心形位图。必须的文件code.py主程序文件放在CIRCUITPY磁盘的根目录。Multicolore_36.bdf自定义字体文件放在CIRCUITPY/fonts/目录下。heart_bw.bmp黑白心形位图文件放在CIRCUITPY/images/目录下。必需的CircuitPython库复制到CIRCUITPY/lib/目录adafruit_bus_device/adafruit_display_text/(确保版本 2.2.0否则文本定位功能可能异常)adafruit_gizmo/adafruit_imageload/adafruit_bitmap_font/adafruit_circuitplayground/注意库文件必须正确放置在lib文件夹内并且保持其目录结构。直接从GitHub仓库下载的adafruit-circuitpython-bundle压缩包中可以找到所有这些库。我习惯定期去更新这个包以确保用到最新的功能和修复。3.2 代码逐段解析与自定义让我们打开基础版的code.py分段理解其奥妙。第一部分导入与用户配置import time from random import choice import displayio import adafruit_imageload from adafruit_bitmap_font import bitmap_font from adafruit_display_text import label from adafruit_gizmo import tft_gizmo from adafruit_circuitplayground import cp #---| User Config |-------------------------------------------------- HEART_MESSAGES ( (I LUV, YOU), (SAY, YES), (HUG, ME), (BE, MINE), (TEXT,ME), (OMG,LOL), (PEACE,), ) HEART_COLORS ( 0xEAFF50, # yellow 0xFFAD50, # orange 0x9D50FF, # purple 0x13B0FE, # blue 0xABFF96, # green 0xFF96FF, # pink ) MESSAGE_COLORS ( 0xFF0000, # red ) #---| User Config |--------------------------------------------------导入库displayio是CircuitPython的显示框架核心所有图形操作都基于它。adafruit_imageload用于加载图片bitmap_font和label处理文字tft_gizmo是显示屏驱动cp则提供了访问板载按钮等传感器的便捷接口。用户配置区这是你可以自由发挥的地方。HEART_MESSAGES是一个元组的元组每个子元组代表两行文字。即使你只想显示一行如“PEACE”也需要用空字符串占位第二行。关键限制第一行最多9个字符第二行最多5个字符这是由字体大小和屏幕尺寸决定的超出的部分会被截断。颜色配置颜色使用16进制RGB值表示格式为0xRRGGBB。HEART_COLORS定义心形的颜色池MESSAGE_COLORS定义文字颜色池。代码中文字颜色只预设了红色因为实测中红色在彩色心形上对比度最好。你可以随意增删或修改这些颜色值。第二部分显示初始化与资源加载# Create the TFT Gizmo display display tft_gizmo.TFT_Gizmo() # Load the candy heart BMP bitmap, palette adafruit_imageload.load(/images/heart_bw.bmp, bitmapdisplayio.Bitmap, palettedisplayio.Palette) heart displayio.TileGrid(bitmap, pixel_shaderpalette)创建display对象这是与屏幕通信的入口。adafruit_imageload.load加载位图文件。它返回两个重要对象bitmap像素数据和palette调色板。这里加载的是一张黑白位图。displayio.TileGrid将位图和调色板包装成一个可以在屏幕上显示的“图块网格”对象heart。pixel_shaderpalette意味着这个图块的颜色由palette调色板决定。第三部分文本标签设置与定位# Set up message text font bitmap_font.load_font(/fonts/Multicolore_36.bdf) line1 label.Label(font, text?*9) line2 label.Label(font, text?*5) line1.anchor_point (0.5, 0) # middle top line2.anchor_point (0.5, 1.0) # middle bottom # Set up group and add to display group displayio.Group() group.append(heart) group.append(line1) group.append(line2) display.root_group group加载自定义的.bdf字体文件并创建两个文本标签line1和line2。初始用“?”填充是为了预留空间避免后续更新文本时频繁重新计算布局这很耗时。anchor_point是精确定位的关键这个属性定义了标签的“锚点”在其自身边界框中的相对位置。(0,0)是左上角(1,1)是右下角。(0.5, 0)表示锚点在顶部中点(0.5, 1.0)表示锚点在底部中点。设置锚点后我们通过anchored_position设置的是这个锚点对准屏幕上的哪个坐标。displayio.Group是一个容器可以容纳多个显示元素如图片、文字。我们将心形和两个文本标签添加到这个组里然后将整个组设置为显示的根组。所有后续对组内元素的修改都会最终体现在屏幕上。第四部分主循环与交互逻辑while True: # turn off auto refresh while we change some things display.auto_refresh False # pick a random message line1.text, line2.text choice(HEART_MESSAGES) # update location for new text bounds line1.anchored_position (120, 85) line2.anchored_position (120, 175) # pick a random text color line1.color line2.color choice(MESSAGE_COLORS) # pick a random heart color palette[1] choice(HEART_COLORS) # OK, now turn auto refresh back on to display display.auto_refresh True # wait for button press while not cp.button_a and not cp.button_b: pass # just a little debounce time.sleep(0.25)display.auto_refresh False这是一个重要的性能优化技巧。在修改多个显示属性文字、颜色、位置时如果自动刷新开启每修改一个属性屏幕就会刷新一次导致肉眼可见的闪烁。关闭自动刷新后所有修改都在后台进行最后再一次性刷新 True画面变化会非常平滑。choice()函数从预设的元组中随机选取一项。动态更新调色板palette[1] choice(HEART_COLORS)是改变心形颜色的魔法。我们的黑白位图中白色对应的调色板索引是1。通过直接修改palette[1]的颜色值所有原本是白色的像素瞬间就变成了新的颜色无需重新加载图片效率极高。最后的while循环等待任意按键按下。time.sleep(0.25)是简单的按键消抖防止一次物理按键被误识别为多次按下。3.3 自定义进阶改变交互方式基础版的交互是按键。但CPB板载了丰富的传感器我们可以轻松改变触发方式。只需替换主循环末尾的等待按键代码即可。改为摇晃触发# wait for shake while not cp.shake(shake_threshold20): pass time.sleep(0.5) # 防止连续触发shake_threshold值越小越敏感。你可以通过实际测试调整到一个合适的值。改为定时自动切换# change every 10 seconds time.sleep(10) # 注意这里要移除原来的等待按键循环改为拍手声音触发这需要一些调试# 设置一个声音阈值 while cp.sound_level 200: # 这个阈值需要根据环境调整 pass time.sleep(1) # 长一点的延时防止回声误触发改变交互方式后这个电子糖果心就可以放在桌上通过摇晃来“摇签”或者作为一个自动轮播的电子相框如果你把图片换成照片的话可玩性大大增加。4. 蓝牙高级版实现与无线控制基础版已经很有趣但通过蓝牙用手机控制才是真正的“魔法时刻”。这个版本允许你从手机App上任意输入文字和选择颜色实时同步到糖果心上。4.1 BLE通信原理与代码结构蓝牙低能耗BLE是一种低功耗的无线通信协议。在这个项目中CPB扮演“外围设备”Peripheral手机App扮演“中央设备”Central。它们通过一个叫做“服务”的通道进行通信。我们这里使用的是UART服务它模拟了古老的串口通信非常适合传输简单的文本和命令。BLE版本代码的新增部分解析导入BLE相关库from adafruit_ble import BLERadio from adafruit_ble.advertising.standard import ProvideServicesAdvertisement from adafruit_ble.services.nordic import UARTService from adafruit_bluefruit_connect.packet import Packet from adafruit_bluefruit_connect.color_packet import ColorPacketBLERadio是蓝牙无线电控制器UARTService提供了串口通信通道而ProvideServicesAdvertisement用于广播告知外界“我提供UART服务”。Packet和ColorPacket是Adafruit Bluefruit Connect App定义的数据包格式用于解析App发来的特定指令如颜色数据。BLE初始化与广播ble BLERadio() uart UARTService() advertisement ProvideServicesAdvertisement(uart) ble._adapter.name BLE_NAME # 设置蓝牙设备名称这段代码设置了蓝牙无线电创建了UART服务并准备了一个广播包。BLE_NAME可以自定义你会在手机蓝牙列表中看到这个名字如“My Candy Heart”。主循环中的BLE状态机while True: # 1. 开始广播并等待连接 ble.start_advertising(advertisement) while not ble.connected: pass # 等待连接 # 2. 连接成功停止广播 ble.stop_advertising() # 3. 连接保持期间持续处理数据 while ble.connected: if uart.in_waiting: raw_bytes uart.read(uart.in_waiting) # ... 解析和处理数据 ... # 4. 断开连接后循环回到步骤1重新开始广播 print(DISCONNECTED)这是一个典型的BLE外围设备逻辑广播 - 等待连接 - 连接后处理数据 - 断开后重回广播。这种设计确保了设备可以反复被连接。数据解析逻辑if raw_bytes[0] ord(!): # 这是Adafruit Connect App发来的控制数据包如颜色 packet Packet.from_bytes(raw_bytes) if isinstance(packet, ColorPacket): color packet.color # 提取颜色值 else: # 这是普通文本信息 text raw_bytes.decode(utf-8).strip()Adafruit的App在发送颜色等信息时会以!字符开头后面跟着特定的二进制包结构。ColorPacket能自动解析出颜色值。普通文本消息则直接解码为字符串。代码中通过逗号,来分隔两行文本。4.2 使用Adafruit Bluefruit LE Connect App要让手机控制糖果心你需要在手机上下载“Adafruit Bluefruit LE Connect”应用iOS和Android均有。连接与控制流程连接设备打开App扫描蓝牙设备。你应该能看到你代码中设置的BLE_NAME默认是“Candy Heart”。点击连接。发送文本连接成功后在模块列表中选择UART。在文本输入框中输入你想显示的信息。关键格式用英文逗号分隔两行。例如输入HELLO,WORLD第一行会显示“HELLO”第二行显示“WORLD”。如果只输入一行如PEACE则第二行为空。点击“SEND”信息会立刻显示在糖果心屏幕上。发送颜色在模块列表中选择Controller。选择Color Picker。在调色板上选择你喜欢的颜色点击“SELECT”。糖果心的颜色会立即改变。实操心得有时App会连接失败或发送无反应。请按以下步骤排查① 确保CPB已上电且运行了BLE版代码可通过串口监视器查看打印信息。② 关闭手机蓝牙再重新打开。③ 在App中先断开现有连接重新扫描。④ 检查代码中BLE_NAME是否过长或有特殊字符尽量使用简单的英文和数字。⑤ 确保手机没有同时连接其他蓝牙设备干扰。4.3 代码优化与功能扩展思路当前的BLE代码是一个很好的起点但我们可以让它更健壮、功能更强。1. 增加输入验证与错误处理def update_heart(message, heart_color): # ... 原有代码 ... text1, sep, text2 message.partition(MESSAGE_DELIMITER) # 处理没有逗号的情况全部文本放在第一行 if not sep: text1 message text2 # 截断超长文本并用“…”提示 if len(text1) LINE1_MAX: text1 text1[:LINE1_MAX-1] … if len(text2) LINE2_MAX: text2 text2[:LINE2_MAX-1] … line1.text text1 line2.text text2 # ... 其余代码 ...这样即使用户输入了超长文本或忘记加逗号程序也不会崩溃显示会更友好。2. 扩展更多控制指令Adafruit Connect App的Controller模块还能发送按钮命令、加速度计数据等。你可以解析ButtonPacket来让手机按钮控制消息切换的动画效果或者解析QuaternionPacket来根据手机姿态改变心形的倾斜角度实现更丰富的交互。3. 结合板载传感器即使在使用蓝牙控制时板载传感器依然可用。你可以编写代码使得在蓝牙连接时由手机控制断开连接时自动切换回按键或摇晃控制模式提升用户体验。5. 深度技术解析与常见问题排查5.1 displayio图形系统工作机制理解displayio是编写高效CircuitPython图形程序的关键。它采用了一种基于“图层”和“组”的显示模型。显示总线Display Bus最底层负责与屏幕驱动芯片如ST7789进行硬件通信。显示核心Display管理整个显示区域root_group属性指向顶层的显示组。组Group可以嵌套的容器。一个组可以包含多个其他组或显示元素TileGrid,Label等。组可以设置位置、缩放和旋转这对于创建复杂的UI或动画至关重要。图块网格TileGrid用于显示位图图像。它引用一个Bitmap对象像素数据和一个Palette对象颜色映射。通过修改调色板来改变颜色正是本项目使用的技巧。标签Label用于显示文本。依赖于字体文件。内存优化提示CircuitPython设备的内存RAM通常很有限CPB约256KB。加载大尺寸位图或复杂字体很容易导致MemoryError。务必使用像本项目中的小尺寸位图heart_bw.bmp只有几KB并将颜色深度降至最低如本项目的1位黑白图。如果程序复杂可以使用gc.collect()手动触发垃圾回收来释放内存。5.2 字体处理与文本渲染本项目使用了.bdf格式的位图字体。这种字体为每个字符定义了固定的像素图渲染速度快但缺乏缩放能力。adafruit_bitmap_font库负责加载这种字体。如果你想使用自己的字体找到一款TrueType.ttf字体。使用Adafruit提供的在线工具“Font Converter”可在Adafruit学习网站找到将.ttf转换为CircuitPython可用的.bdf位图字体并指定你需要的像素大小。将生成的.bdf文件放入CIRCUITPY/fonts/目录并在代码中修改加载路径。文本定位的精确计算代码中(120, 85)和(120, 175)这两个坐标是通过试验得出的。如果你想更精确地控制可以计算文本的边界框label.bounding_box。例如要让文本在垂直方向上绝对居中于心形可以动态计算# 假设heart对象有height属性或你知道心形高度 heart_height 100 # 示例值需要根据实际图片测量 text_height line1.bounding_box[3] line2.bounding_box[3] line_spacing total_height heart_height text_height start_y (display.height - total_height) // 2 # 然后根据start_y计算line1和line2的anchored_position这需要你更深入地了解每个显示对象的属性。5.3 常见问题与解决方案速查表问题现象可能原因解决方案连接电脑后无CIRCUITPY磁盘1. USB线非数据线。2. 板子未进入CircuitPython模式还是Arduino模式。3. 驱动问题Windows。1. 更换已知良好的数据线。2. 双击复位键等待绿灯后查看。3. 尝试其他USB口或安装Adafruit Windows驱动。屏幕一片空白或花屏1. 库文件缺失或版本不对。2. 代码语法错误导致未执行。3. 屏幕排线接触不良。1. 检查lib文件夹内库是否齐全特别是adafruit_display_text版本。2. 连接串口监视器如Mu编辑器查看错误输出。3. 重新扣紧TFT Gizmo。按键无反应但屏幕有显示1. 代码中事件循环卡住。2. 消抖延时过长或逻辑错误。3. 按钮损坏罕见。1. 检查while循环条件是否正确。2. 调整time.sleep延时或改用cp.button_a/cp.button_b的value属性结合时间戳判断。3. 使用Mu编辑器的REPL模式输入import touchio; touchio.TouchIn(board.TX).value测试按键需查引脚图。蓝牙版本代码运行后手机搜不到设备1. 代码未正确运行。2. 蓝牙名称包含非法字符或过长。3. 手机蓝牙缓存问题。1. 通过串口监视器查看是否有“WAITING...”等打印信息。2. 简化BLE_NAME为纯英文。3. 关闭手机蓝牙再打开或重启手机蓝牙。手机App发送文本后无变化1. 文本格式错误未用逗号分隔。2. UART服务数据传输错误。3. 代码解析部分有bug。1. 确保文本格式为line1,line2。2. 在代码中增加print(raw_bytes)打印原始数据检查接收是否正常。3. 检查update_heart函数中的字符串处理逻辑。程序运行一段时间后崩溃1. 内存泄漏常见于频繁创建新对象。2. 硬件不稳定供电不足。1. 避免在循环内重复创建Label、TileGrid等对象。应复用对象只修改其属性。2. 使用可靠的5V/1A以上的USB电源供电避免使用老旧的电脑USB口。心形颜色或文字颜色显示异常1. 颜色值格式错误。2. 调色板索引弄错。3. 位图文件不是纯黑白。1. 颜色值必须是0xRRGGBB格式的16进制数。2. 黑白位图中0是黑色1是白色。确认修改的是palette[1]。3. 确保heart_bw.bmp是1位深度的位图。调试利器——串口输出在Mu编辑器或Thonny等支持CircuitPython的IDE中打开串口监视器Serial Console在代码关键位置添加print()语句如print(Connected!),print(Received:, text)是诊断问题最直接有效的方法。CircuitPython的错误回溯信息也会打印在这里。这个项目虽然小巧但涵盖了现代嵌入式交互开发的几个核心要素传感器输入、图形输出、无线通信和事件驱动编程。通过亲手实践和不断调试你收获的将不仅仅是一个会发光的电子玩具更是一套应对更复杂硬件编程项目的思维方法和工具集。