基于CircuitPython与蓝牙的智能小车:全栈嵌入式开发入门实践
2026/5/16 3:05:10 网站建设 项目流程

1. 项目概述与核心价值

如果你对硬件编程感兴趣,但又对C/C++的复杂性和Arduino的配置感到头疼,那么这个基于CircuitPython和蓝牙的智能小车项目,可能就是为你量身定做的“敲门砖”。我花了几个周末的时间,从零开始复现并深度优化了这个项目,它完美地展示了如何用Python这门我们最熟悉的语言,去直接操控真实的物理世界——让一个小车听从你手机APP的指令,前进、转向、变换灯光,并在屏幕上实时反馈状态。

这个项目的核心在于Adafruit CLUE这块开发板。它内置了Nordic nRF52840芯片,不仅支持CircuitPython,还原生集成了蓝牙低功耗(BLE)。这意味着我们无需额外模块,就能轻松实现与手机的无线通信。另一个主角是Ring:Bit Car V2小车底盘,它本质上是一个兼容micro:bit生态的智能小车套件,通过三个简单的排针接口(GPIO, V+, GND)就能控制两个舵机轮子和车底的RGB灯带。将CLUE插上去,它就从一个普通的开发板,变成了一个机器人的“大脑”。

整个系统的逻辑非常清晰:CLUE运行我们编写的CircuitPython程序,通过BLE广播自己;你的手机安装Adafruit Bluefruit Connect应用,搜索并连接这个“机器人”;之后,你在APP控制面板上的每一次点击(比如方向键),都会通过BLE协议打包成一个数据包发送给CLUE;CLUE上的程序解析这个数据包,然后调用相应的函数去控制舵机的转速(实现移动)和NeoPixel灯珠的颜色(实现灯光效果),同时更新板载屏幕的图形化状态。这整个过程,从无线信号到物理运动,全部由不到300行的Python代码驱动。

我认为这个项目的价值远不止于组装一辆能跑的小车。它提供了一个极其友好的全栈嵌入式开发微缩样板:涵盖了硬件接口(GPIO/PWM)、外设驱动(舵机、LED、屏幕)、无线通信协议(BLE)、事件驱动编程、以及面向对象的软件架构。你学到的不是某个孤立的知识点,而是一套完整的、可迁移的开发范式。无论是想入门物联网,还是为更复杂的机器人项目打基础,这个实践都能让你避开初期的诸多陷阱,直接触摸到核心的工作流。

2. 硬件选型、清单与替代方案解析

原项目清单非常精简,但每一件都有其不可替代性。这里我结合自己的采购和调试经验,为你详细拆解。

2.1 核心硬件详解

  1. Adafruit CLUE开发板:这是项目的“大脑”。选择它而非其他CircuitPython板(如Feather nRF52840 Express)的关键原因有三点:

    • 集成屏幕:板载1.3英寸彩色TFT屏幕,省去了额外连接显示器的麻烦,非常适合用于状态反馈和调试信息显示。
    • 丰富的内置传感器:虽然本项目未使用,但它自带加速度计、陀螺仪、磁力计、温湿度、气压、光线和手势传感器,为项目后期扩展(如避障、巡线)预留了巨大空间。
    • 优质的社区支持:Adafruit为其提供了极其完善的CircuitPython库和教程,几乎你遇到的任何问题都能在社区找到答案。
  2. micro:bit Ring:Bit Buggy Car Robot (V2):这是项目的“身体”。需要注意,务必确认你购买的是V2版本。V1版本的金手指接口排列可能不同,直接套用代码会导致电机控制混乱。这个小车套件包含:

    • 底盘、轮子、两个180度连续旋转舵机(用作电机)、车头万向轮。
    • 一块扩展板,将micro:bit的GPIO口以更易插拔的3Pin接口(信号、VCC、GND)形式引出。
    • 一小条(2颗)WS2812B可寻址RGB LED灯珠,用于底盘氛围灯。
    • 所需的螺丝、螺母和组装工具。
  3. 电源:3节AAA电池。这里有个重要经验:务必使用碱性电池或可充电的镍氢电池。我曾尝试使用廉价的碳性电池,在电机启动瞬间会因为电压骤降导致CLUE重启。舵机在堵转时电流很大,优质的电池是系统稳定的基础。

  4. USB数据线:强调“数据线”而非“充电线”。很多手机充电线只有电源线,没有数据线。你需要一根可靠的Micro-B USB数据线用于给CLUE刷写固件和上传代码。识别方法很简单:用它连接电脑和手机,如果能传文件,就是数据线。

2.2 硬件连接与引脚定义

组装小车按照说明书即可,这里重点讲电路连接,这是最容易出错的地方。

Ring:Bit扩展板背面有三个并排的3Pin接口,分别标有0VG,1VG,2VG。这个命名规则是:数字代表GPIO引脚号,V代表电源正极(VCC),G代表接地(GND)

根据代码中的定义,我们需要:

  • GPIO0 (D0):连接车底LED灯带。信号线(通常是黄色或绿色线)接在标有“0”的那一排。
  • GPIO1 (D1):连接左侧舵机(以小车尾部朝向自己时的左侧为准)。
  • GPIO2 (D2):连接右侧舵机

实操心得:插接时,务必确保杜邦线的黄色信号线朝向GPIO数字标号一侧,红色(VCC)和黑色(GND)在中间和另一侧。如果插反了,舵机不会动,甚至可能损坏。如果组装好后小车运动方向相反,最快速的解决方法不是重接线,而是直接在代码里交换LEFT_MOTORRIGHT_MOTOR的引脚定义。

2.3 可行的硬件替代方案

万一你手头没有完全相同的硬件,这个项目仍有很高的可移植性。

  • CLUE的替代:任何搭载nRF52840芯片并支持CircuitPython的Adafruit板子都可以,例如Feather nRF52840 ExpressCircuit Playground Bluefruit。但需要注意,它们没有屏幕,你需要注释掉或修改代码中所有与displayio相关的部分,或者通过串口打印状态。同时,引脚定义(如板载NeoPixel、LED)也需要相应调整。
  • 小车的替代:任何由两个连续旋转舵机驱动的小车底盘都可以。你只需要将舵机的信号线连接到开发板的任意两个支持PWM输出的GPIO口,并在代码中修改引脚定义即可。底盘灯带如果是WS2812B,则连接任意一个数字IO口。
  • 电源的替代:对于Feather这类板子,可以通过其电池接口连接一个3.7V锂电池,这样续航和稳定性更好。

3. 软件环境搭建与CircuitPython固件烧录

这是让硬件“活”起来的第一步。CircuitPython的魅力在于,它让嵌入式开发变得像在电脑上操作U盘一样简单。

3.1 下载与安装CircuitPython

  1. 访问下载页面:打开浏览器,访问 circuitpython.org 。在搜索框或板卡列表中找到“Adafruit CLUE”

  2. 选择最新稳定版:点击进入CLUE的页面,你会看到多个版本的.uf2文件。通常选择下载列表中最新的“稳定版”。文件命名类似adafruit-circuitpython-clue-version.uf2

  3. 进入Bootloader模式

    • 用USB数据线连接CLUE和电脑。
    • 快速双击CLUE板载的复位按钮(Reset)。这个按钮位于板子顶部边缘,靠近USB口,是一个小小的黑色按钮。
    • 成功标志:板载的RGB NeoPixel LED(在复位按钮旁边)会发出绿色光。同时,你的电脑会弹出一个名为CLUEBOOT的可移动磁盘驱动器。如果LED亮红色,说明USB线或端口可能有问题,请更换重试。
  4. 拖放固件

    • 将刚才下载的.uf2文件直接拖拽或复制到CLUEBOOT磁盘中。
    • 此时,NeoPixel LED会快速闪烁。等待几秒钟,CLUEBOOT盘符会消失,随后出现一个新的名为CIRCUITPY的磁盘驱动器。这表明CircuitPython固件已经烧录成功!

3.2 认识CIRCUITPY磁盘

打开CIRCUITPY驱动器,你会看到以下初始内容:

  • boot_out.txt:包含板卡信息和CircuitPython版本号。
  • code.py:这是主程序文件。CircuitPython启动后会自动执行这个文件。
  • lib/文件夹:用于存放项目依赖的第三方库文件。

注意事项:请勿随意删除或重命名CIRCUITPY驱动器,也不要使用磁盘修复工具。它不是一个普通的U盘,而是一个微控制器的文件系统。安全弹出后再拔线是个好习惯。

3.3 安装必要的库文件

原项目提供了一个“项目包”,但为了更深入理解,我建议你从Adafruit的库仓库手动安装,这能让你更清楚每个库的作用。

你需要将以下库文件(.mpy.py)下载并放入CIRCUITPY驱动器的lib文件夹内:

  • adafruit_ble/:蓝牙低功耗通信的核心库。
  • adafruit_bluefruit_connect/:定义了与Bluefruit Connect APP通信的数据包格式(如按钮包、颜色包)。
  • adafruit_motor/:包含伺服电机(舵机)的控制类。
  • adafruit_display_text/adafruit_display_shapes/adafruit_displayio_*:用于屏幕显示(本项目使用内置vectorio,但某些板子可能需要这些)。
  • neopixel.mpy:控制WS2812B LED灯带。
  • adafruit_pixelbuf.mpy:NeoPixel库的依赖。

获取库文件的最佳途径

  1. 访问 Adafruit CircuitPython Library Bundle 页面。
  2. 下载与你的CircuitPython版本匹配的“最新版本库包”(Library Bundle)。
  3. 解压后,在lib文件夹中找到上述库文件,复制到你的CIRCUITPY/lib中。

4. 项目代码深度解析与编写

理解了硬件和基础环境后,我们来看代码。项目的代码结构清晰,分为一个主程序文件code.py和一个自定义的类库文件robot.py。这种分离让主逻辑非常简洁,而将复杂的硬件控制和状态管理封装在Robot类中。

4.1 主程序 (code.py):极简的事件循环

import board import neopixel from robot import Robot # 硬件引脚定义 UNDERLIGHT_PIXELS = board.D0 LEFT_MOTOR = board.D1 RIGHT_MOTOR = board.D2 # 初始化硬件对象 underlight_neopixels = neopixel.NeoPixel(UNDERLIGHT_PIXELS, 2) robot = Robot(LEFT_MOTOR, RIGHT_MOTOR, underlight_neopixels) # 主循环 while True: robot.wait_for_connection() # 阻塞,直到手机连接 while robot.is_connected(): # 连接保持时 robot.check_for_packets() # 检查并处理来自手机的数据包

代码逻辑解读

  1. 引脚定义:将物理引脚(D0, D1, D2)映射到程序中的变量名,提高可读性和可维护性。
  2. 对象初始化
    • neopixel.NeoPixel():初始化车底的两个RGB LED。
    • Robot():实例化机器人对象,传入电机和灯光控制对象。
  3. 事件驱动主循环
    • wait_for_connection():启动BLE广播,并阻塞在此处,直到手机APP连接。此时屏幕显示“Waiting for connection”。
    • 一旦连接成功,进入内层while循环,不断调用check_for_packets()。这个方法非阻塞地检查蓝牙串口(UART)是否有新数据,有则解析处理。
    • 如果连接断开(比如APP关闭),is_connected()返回False,跳出内层循环,回到外层,重新开始广播等待连接。

这种结构非常经典和高效,避免了在循环中空转消耗CPU资源。

4.2 机器人核心类 (robot.py):面向对象的硬件抽象

Robot类是这个项目工程化的精髓。它将近400行代码组织得井井有条,我们将分模块拆解。

4.2.1 初始化与硬件设置 (__init__)
def __init__(self, left_pin, right_pin, underlight_neopixel): self.left_motor = self._init_motor(left_pin) self.right_motor = self._init_motor(right_pin) self._init_display() self._init_ble() self.under_pixels = underlight_neopixel self.neopixel = neopixel.NeoPixel(board.NEOPIXEL, 1) # CLUE板载LED self.direction = STOP self.release_color = None self.headlights = digitalio.DigitalInOut(board.WHITE_LEDS) # CLUE板载白光LED self.headlights.switch_to_output() self.set_underglow(PURPLE) # 设置初始底盘灯颜色 self.set_speed(STOP) # 确保电机初始为停止状态
  • _init_motor():封装了舵机初始化的细节。它使用pwmio.PWMOut在指定引脚生成50Hz的PWM信号,然后用adafruit_motor.servo.ContinuousServo将其包装成一个连续旋转舵机对象。min_pulsemax_pulse参数(600和2400微秒)是校准舵机中位点(停止)和速度范围的关键,不同品牌舵机可能需要微调。
  • _init_ble():初始化BLE无线电、UART服务和广播包。UARTService是BLE中一种模拟串口的服务,让手机和开发板可以像通过串口一样收发数据。
  • _init_display():初始化屏幕,创建一个显示组(displayio.Group),并绘制一个黄色的背景矩形。所有后续的图形(箭头、圆圈)都将作为“子图层”添加到这个组里。
4.2.2 运动控制逻辑:差速转向的实现

这是小车运动的核心算法。代码巧妙地处理了两种转向模式:

def rotate_right(self): self.release_color = self.get_underglow() self.set_underglow(YELLOW, True) # 转向时亮黄灯 if self.direction == STOP: # 模式1:原地旋转 self._set_status_rotate_cw() speed = FWD self._set_left_throttle(speed) self._set_right_throttle(-1 * speed) # 右轮反转 else: # 模式2:行进中转向(以一侧轮子为轴心) self._set_status_right() speed = self.direction # 保持当前前进/后退方向 self._set_left_throttle(speed) # 左轮保持速度 self._set_right_throttle(STOP) # 右轮停止
  • 原地旋转:当小车静止时按下左/右键,左右轮以相同速度、相反方向转动,实现原地掉头。屏幕上显示旋转箭头。
  • 行进中转向:当小车前进或后退时按下左/右键,一侧轮子停止,另一侧继续转动,实现绕轴转弯。屏幕上显示方向箭头。
  • _set_right_throttle(speed)函数中有一个关键操作:-1 * speed。这是因为两个舵机在车体上是镜像对称安装的,它们的“正转”物理方向相反。通过软件取反,使得代码中相同的speed值能让两个轮子产生相同的物理前进方向。
4.2.3 蓝牙数据包解析与事件响应

_process_packet()方法是连接手机指令和机器人动作的桥梁。

def _process_packet(self, packet): if isinstance(packet, ColorPacket): self._handle_color_packet(packet) elif isinstance(packet, ButtonPacket) and packet.pressed: self._handle_button_press_packet(packet) elif isinstance(packet, ButtonPacket) and not packet.pressed: self._handle_button_release_packet(packet) def _handle_button_press_packet(self, packet): if packet.button == ButtonPacket.UP: self.set_throttle(FWD) # 前进 elif packet.button == ButtonPacket.DOWN: self.set_throttle(REV) # 后退 elif packet.button == ButtonPacket.RIGHT: self.rotate_right() # 右转 ... elif packet.button == ButtonPacket.BUTTON_4: self.toggle_headlights() # 开关大灯
  • adafruit_bluefruit_connect.packet库定义了标准的数据包结构。当你在APP上按下按钮,手机会发送一个ButtonPacket,其中包含按钮ID和按压状态。
  • isinstance()用于判断数据包类型,这是Python多态的优雅体现。
  • 按钮释放事件:代码特别处理了方向键的释放(_handle_button_release_packet)。当松开左/右键时,小车会从转向状态恢复为直线行驶(set_throttle(self.direction)),同时底盘灯颜色恢复为转向前的颜色。这个细节提升了交互的流畅感。
4.2.4 图形化状态显示:使用Vectorio绘图

项目没有使用图片资源,所有屏幕图形(箭头、停止方块、旋转图标)都是用vectorio模块在代码中实时绘制的。这减少了文件依赖,提高了绘制效率。

def _add_centered_polygon(self, points, x_offset=0, y_offset=0, color=None): # 计算多边形包围盒的宽高,用于居中定位 width = max(points, key=lambda item:item[0])[0] - min(points, key=lambda item:item[0])[0] height = max(points, key=lambda item:item[1])[1] - min(points, key=lambda item:item[1])[1] polygon = vectorio.Polygon( pixel_shader=self._make_palette(color), points=points, x=(self.display.width // 2 - width // 2) + x_offset - 1, y=(self.display.height // 2 - height // 2) + y_offset - 1 ) self.display_group.append(polygon)
  • _add_centered_polygon函数接收一个多边形顶点坐标列表(例如[(20, 0), (60, 0), (80, 100), (0, 100)]代表一个梯形),自动计算其尺寸并居中显示在屏幕上。
  • _remove_shapes()函数在绘制新图形前,会清空之前除背景外的所有图形元素,实现了屏幕的刷新。
  • 这种编程式绘图的方式,非常适用于需要动态变化、且资源受限的嵌入式界面。

5. 项目组装、配置与实操全流程

现在,让我们把硬件、软件和代码结合起来,完成整个项目的搭建和运行。

5.1 硬件组装步骤

  1. 组装Ring:Bit小车:按照套件说明书,将底盘、轮子、舵机、万向轮和扩展板组装好。注意螺丝不要拧得过紧,以免塑料件开裂。
  2. 连接舵机和灯带:使用杜邦线(母对母),将左侧舵机信号线(黄)接扩展板1VG1脚,右侧接2VG2脚,底盘灯带接0VG0脚。务必核对VCC(红)和GND(黑)的连接。
  3. 安装电池:装入3节AAA电池。先不要打开电源开关
  4. 插入CLUE开发板:将已烧录好CircuitPython的CLUE板,像插入micro:bit一样,插入小车扩展板顶部的金手指插座。确保方向正确,CLUE的USB口朝向车尾方向。
  5. 连接USB线(用于初次上传代码):用USB线连接CLUE和电脑。此时由USB供电,可以暂时不开电池开关。

5.2 软件文件部署

  1. 确保你的电脑上能看到CIRCUITPY盘符。
  2. 将编写好的code.pyrobot.py两个文件,直接复制到CIRCUITPY盘的根目录。
  3. 将之前准备好的所有必要库文件(adafruit_ble,adafruit_bluefruit_connect等),复制到CIRCUITPY/lib/目录下。
  4. 安全弹出CIRCUITPY盘符,然后拔掉USB线。

5.3 手机APP连接与控制

  1. 安装APP:在手机应用商店搜索“Adafruit Bluefruit Connect”并安装。
  2. 给机器人上电:将小车底部的电源开关拨到ON。CLUE屏幕应点亮,并显示“Waiting for connection”,板载RGB LED亮蓝色。
  3. 蓝牙连接
    • 打开手机蓝牙。
    • 打开Bluefruit Connect APP。首次使用可能需要授予位置权限(用于蓝牙扫描)。
    • APP会自动扫描附近的BLE设备。在列表中找到名称以“CIRCUITPY”开头的设备,点击“Connect”
    • 连接成功后,CLUE屏幕提示消失,板载LED变为绿色。
  4. 进入控制模式:在APP主界面,选择“Controller”模式。然后选择“Control Pad”。你会看到一个带有方向键(上下左右)和四个功能按钮(1-4)的虚拟手柄。

5.4 功能测试与操作

  • 移动:点击上箭头,小车应开始前进,屏幕显示前进箭头,底盘灯为紫色。点击下箭头后退,屏幕显示后退图标。
  • 转向:在小车移动中,按住左箭头右箭头,对应一侧轮子会减速或停止,实现转弯,同时底盘灯变为黄色,屏幕显示转弯箭头。松开按键,恢复直行。
  • 停止:点击Button 1,小车会急停,底盘灯瞬间变红(模拟刹车灯)0.5秒后恢复原色,屏幕显示方块。
  • 灯光控制
    • Button 2:底盘灯变绿色。
    • Button 3:底盘灯变蓝色。
    • Color Picker:在APP的Controller页面选择“Color Picker”,可以调色盘选择任意颜色发送给小车,底盘灯会实时变化。
    • Button 4:开关CLUE板载的两颗白色LED(充当车头大灯)。

6. 常见问题排查与深度调试技巧

即使按照步骤操作,你也可能会遇到一些问题。这里是我在多次搭建和教学中总结的“排坑指南”。

6.1 电源与电机问题

问题现象可能原因解决方案
上电后CLUE不断重启电池电量不足或质量差,电机启动时电压被拉低。更换为全新的碱性电池或充满电的镍氢电池。
电机不转或抖动1. 杜邦线接触不良。
2. 舵机线序接反。
3. PWM脉冲参数不匹配。
1. 重新插紧所有连接线。
2. 检查信号线(黄)是否接在GPIO数字引脚一侧。
3. 尝试在_init_motor函数中微调min_pulsemax_pulse值(如550和2450)。
小车直线跑偏两个舵机的中位点(零速点)存在细微差异。在代码中为两个电机设置微调偏移量。例如:self.left_motor.throttle = speed + 0.02。需要实验确定。

6.2 蓝牙连接与通信问题

问题现象可能原因解决方案
APP中找不到“CIRCUITPY”设备1. 手机蓝牙未开。
2. CLUE未进入广播状态。
3. 之前已配对,但未在APP内连接。
1. 打开手机蓝牙。
2. 确认CLUE屏幕显示“Waiting for connection”。
3. 在手机系统蓝牙设置中,忘记“CIRCUITPY”设备,然后在APP内重新扫描连接。
APP显示已连接,但控制无反应1. 代码未运行或出错。
2. 库文件缺失或版本不对。
3. 手机APP未切换到“Control Pad”模式。
1. 通过USB连接电脑,用串口工具(如Mu编辑器)查看CLUE的错误输出。
2. 检查lib文件夹是否包含所有必要库。
3. 确保在APP中进入了“Controller” -> “Control Pad”界面。
控制延迟高或断连环境中有强无线干扰(如Wi-Fi路由器),或距离过远。尽量在开阔无干扰环境下使用,保持手机与小车在5米范围内。

6.3 代码与软件问题

  • 错误提示ImportError: no module named 'adafruit_ble': 这是最常见的问题。说明库文件没有正确放置。请确保:

    1. CIRCUITPY/lib/目录下存在adafruit_ble文件夹及其内部文件。
    2. 库的版本与你的CircuitPython版本兼容。建议使用从Adafruit官网下载的最新版库包。
  • 如何查看调试信息: CircuitPython板在运行时,会通过USB虚拟出一个串口(COM端口)。你可以使用Mu编辑器Thonny或任何串口终端工具(如Putty、Arduino IDE的串口监视器)连接到这个端口,波特率通常为115200。当代码出现运行时错误时,详细的错误信息会打印在这里,这是调试的黄金通道。

  • 修改代码后不生效: 在CircuitPython中,修改并保存code.py后,板子会自动软复位并重新运行新代码。但有时缓存可能导致问题。最彻底的方法是:按一下板子的硬件复位按钮,或者安全弹出CIRCUITPY盘符后再重新接入

6.4 功能扩展与自定义

这个项目的框架非常利于扩展,以下是几个方向:

  1. 增加传感器:CLUE板载了大量传感器。例如,在代码中导入adafruit_clue库,读取clue.acceleration(加速度计),可以实现碰撞检测,遇到障碍自动停止。
  2. 修改控制逻辑:在_handle_button_press_packet函数中,你可以自由映射按钮功能。比如把Button 2改成“旋转360度”或“跳个舞”(让电机按特定序列转动)。
  3. 添加声音:CLUE没有扬声器,但可以通过PWM引脚连接一个无源蜂鸣器,用pwmio产生不同频率的方波,实现简单的音效。
  4. 改用其他控制方式:Bluefruit Connect APP还提供了“加速度计控制”(用手机倾斜控制方向)和“终端控制”(发送文本命令)模式。你可以修改代码来解析这些不同的数据包。
  5. 优化图形界面:利用displayiovectorio,你可以绘制更复杂的UI,比如电池电量图标、传感器数据曲线等。

这个项目就像一棵技能树的根节点,从这里出发,你可以探索嵌入式Python编程、实时控制、无线通信、传感器融合等多个分支。最重要的是,它让你看到,从一行代码到一个能动起来的实物,中间的道路是清晰且充满乐趣的。动手去试,遇到问题就去解决,这正是硬件编程最大的魅力所在。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询