1. 从硬件选型到开发体验:为什么是XIAO nRF52840与CircuitPython?
如果你最近在寻找一款既能玩转蓝牙、功耗又低、体积还足够小巧的开发板,大概率会看到Seeed Studio XIAO nRF52840这个名字。它属于“XIAO”系列,这个系列的特点就是“小”,但功能不缩水。而CircuitPython,则是MicroPython的一个分支,由Adafruit主导开发,以其对初学者极其友好、开箱即用、硬件抽象层完善而闻名。把这两者结合起来,你会发现一条通往物联网和嵌入式创意的捷径,尤其适合那些不想在环境配置和底层驱动上耗费太多精力的开发者、教育者以及硬件爱好者。
我最初接触这个组合,是因为一个需要低功耗蓝牙通信的小型传感器项目。传统的开发流程,从搭建ARM GCC工具链、配置Nordic的nRF5 SDK、再到处理SoftDevice(蓝牙协议栈),每一步都可能遇到版本兼容、环境变量设置等“拦路虎”。而XIAO nRF52840预装了支持CircuitPython的UF2引导程序,你只需要一根USB线,把它当作一个U盘,把编好的CircuitPython固件拖进去,瞬间就拥有了一个交互式的Python解释器环境。这种“即插即用”的体验,极大地降低了嵌入式开发的门槛,让你能快速将想法转化为可以运行的代码。
核心的吸引力在于效率。nRF52840是一颗性能相当不错的Cortex-M4F芯片,主频64MHz,拥有1MB Flash和256KB RAM,并且集成了蓝牙5.0/蓝牙Mesh、NFC等射频功能。CircuitPython则提供了一个高级的、事件驱动的编程模型,内置了丰富的硬件库(board、digitalio、analogio、busio等),让你可以用几行Python代码就控制GPIO、读取传感器、通过I2C/SPI/UART与外部设备通信,甚至创建蓝牙外设(通过_bleio库)。这个组合解决的核心问题是:让开发者专注于应用逻辑和创新,而非底层细节。无论是制作一个可穿戴的蓝牙心率监测器,一个无线控制的智能灯带,还是一个简单的环境数据记录器,这个平台都能提供快速原型开发的能力。
2. 开箱第一步:固件烧录与开发环境搭建
拿到XIAO nRF52840开发板后,第一步不是急着写代码,而是确保它运行着最新、最稳定的CircuitPython固件。虽然有些板子可能预装了,但自己动手更新一遍是最稳妥的做法,也能熟悉整个流程。
2.1 获取与烧录CircuitPython固件
首先,访问CircuitPython官方网站的下载页面。你需要找到对应“Seeed Studio XIAO nRF52840”的固件文件(通常是一个.uf2文件)。这里有个关键点:务必确认你下载的是稳定版而非每日构建版,除非你想体验最新但可能不稳定的特性。
烧录过程简单得令人惊讶:
- 用USB Type-C线连接XIAO nRF52840和电脑。
- 快速双击板子上的复位按钮(RST)。此时,板载的RGB LED会变成绿色,并且电脑上会弹出一个名为“XIAO-SENSE”或类似的可移动磁盘(UF2引导程序模式)。
- 将下载好的
.uf2固件文件直接拖拽或复制到这个磁盘里。 - 等待几秒钟,磁盘会自动弹出,板子会自动重启。重启后,电脑上会再次出现一个新的可移动磁盘,名称变成了“CIRCUITPY”。恭喜,你的CircuitPython环境已经就绪了!
这个“CIRCUITPY”磁盘就是你的代码存储和运行空间。你可以直接在里面创建、编辑Python文件(比如code.py或main.py),板子会在每次启动或保存文件时自动运行它。
2.2 选择代码编辑器与串口工具
虽然你可以用任何文本编辑器编辑CIRCUITPY磁盘里的文件,但为了更好的体验,我推荐使用专为CircuitPython/MicroPython设计的编辑器,比如Mu Editor或Visual Studio Code 搭配 CircuitPython插件。
- Mu Editor:这是最“官方”推荐的选择,尤其适合初学者。它内置了串口REPL(交互式解释器)、代码检查、图形化绘图等功能,开箱即用,无需复杂配置。安装后,选择“CircuitPython”模式,它就能自动识别并连接到你的板子。
- VS Code + CircuitPython插件:如果你已经是VS Code的用户,那么安装“CircuitPython”插件后可以获得代码自动补全、库管理、串口监视器等强大功能,开发效率更高。
无论用哪种编辑器,串口REPL都是你调试和交互的利器。在Mu中直接点击“串口”按钮;在VS Code中通过插件打开串口终端。连接成功后,你会看到Python提示符>>>,在这里你可以直接输入Python命令并立即看到结果,例如import board、print(“Hello XIAO!”),这对于快速测试硬件引脚、库函数非常方便。
2.3 管理第三方库
CircuitPython的强大之处在于其丰富的硬件驱动库生态系统。这些库通常以.mpy(预编译的MicroPython字节码)文件的形式提供,你需要将它们放置到CIRCUITPY磁盘的lib文件夹内。
如何获取这些库?最规范的方式是通过CircuitPython库捆绑包(Library Bundle)。在CircuitPython官网,你可以下载对应你固件版本的库捆绑包,解压后里面按类别存放了数百个库文件,从传感器驱动(adafruit_bme280)、显示器驱动(adafruit_ssd1306)到网络协议(adafruit_requests)应有尽有。根据你的项目需要,将对应的.mpy文件复制到CIRCUITPY盘的lib文件夹即可。
注意:库的版本需要与CircuitPython固件版本大致匹配。使用过旧或过新的库可能会导致
ImportError。当遇到奇怪的导入错误时,检查库版本是首要的排查步骤。
3. 硬件资源映射与基础GPIO控制
要让代码控制硬件,首先得知道如何“称呼”板子上的各个引脚。CircuitPython通过board模块提供了对硬件引脚的抽象,使用预定义的常量来引用它们,这比直接记引脚编号直观得多。
3.1 理解XIAO nRF52840的引脚定义
XIAO nRF52840板子虽小,但引出了14个GPIO引脚,排列在两侧。在CircuitPython中,这些引脚有固定的名称。你可以通过REPL快速查看所有可用的引脚:
import board dir(board)你会看到一串列表,其中与物理引脚相关的主要有:D0到D13(对应板载丝印的A0-A10,以及一些特殊功能引脚)。需要特别注意以下几点:
- 模拟输入:
A0到A6(对应board.A0到board.A6)引脚支持模拟输入(ADC),可用于读取电位器、模拟传感器等的电压值。nRF52840的ADC分辨率是12位(0-4095),对应0-3.6V(参考电压)。 - 数字IO与PWM:几乎所有
Dx引脚都支持数字输入输出,其中很多也支持PWM(脉冲宽度调制)输出,用于控制LED亮度、舵机角度等。 - 特殊功能引脚:
board.LED或board.LED_RED:控制板载的红色用户LED。board.NEOPIXEL:控制板载的RGB NeoPixel LED。board.SDA和board.SCL:默认的I2C总线引脚。board.MOSI,board.MISO,board.SCK:默认的SPI总线引脚。board.RX和board.TX:默认的UART(串口)引脚。
3.2 数字输入输出实战
控制一个LED闪烁是嵌入式世界的“Hello World”。假设我们在D2引脚上外接了一个LED(记得串联一个约220欧姆的限流电阻)。
import board import digitalio import time # 1. 初始化D2引脚为数字输出 led = digitalio.DigitalInOut(board.D2) led.direction = digitalio.Direction.OUTPUT # 2. 闪烁循环 while True: led.value = True # 输出高电平,LED亮 time.sleep(0.5) led.value = False # 输出低电平,LED灭 time.sleep(0.5)将这段代码保存为CIRCUITPY盘根目录下的code.py,板子会自动运行,LED开始闪烁。这里的关键是digitalio模块,它提供了清晰的面向对象接口来操作数字引脚。direction属性设置为OUTPUT或INPUT,value属性用于读取或设置电平状态。
3.3 模拟输入与PWM输出实战
读取模拟值:我们可以用A0引脚连接一个电位器(中间脚接A0,两侧分别接3.3V和GND)。
import board import analogio import time # 初始化A0为模拟输入 pot = analogio.AnalogIn(board.A0) while True: # 读取原始ADC值 (0-65535, CircuitPython内部已转换为16位) raw_value = pot.value # 转换为电压值 (假设参考电压为3.3V) voltage = (raw_value / 65535) * 3.3 print(f"Raw: {raw_value}, Voltage: {voltage:.2f}V") time.sleep(0.1)PWM控制LED亮度:使用D3引脚实现呼吸灯效果。
import board import pwmio import time # 初始化D3为PWM输出 led_pwm = pwmio.PWMOut(board.D3, frequency=5000, duty_cycle=0) # 呼吸灯效果 while True: # 亮度逐渐增加 for i in range(0, 65535, 256): # duty_cycle范围是0-65535 led_pwm.duty_cycle = i time.sleep(0.005) # 亮度逐渐减小 for i in range(65535, 0, -256): led_pwm.duty_cycle = i time.sleep(0.005)pwmio模块使得生成PWM信号变得非常简单。duty_cycle决定了占空比,最大值65535对应100%占空比(常亮)。通过循环改变这个值,就能实现平滑的亮度变化。
4. 通信协议:I2C、SPI与UART的应用
与外部传感器、显示屏、模块通信是项目的核心。CircuitPython的busio模块统一了这些标准通信协议的操作。
4.1 I2C总线驱动OLED显示屏
I2C因其接线简单(仅需SDA和SCL两根线)而被广泛使用。我们以驱动一个128x64的SSD1306 OLED屏幕为例。
首先,确保adafruit_ssd1306和adafruit_framebuf库已放入CIRCUITPY/lib/文件夹。
import board import busio import displayio import adafruit_ssd1306 # 1. 释放任何现有的显示资源(重要,避免冲突) displayio.release_displays() # 2. 创建I2C总线对象,使用默认的SDA和SCL引脚 i2c = busio.I2C(board.SCL, board.SDA) # 3. 创建SSD1306 I2C显示对象 display = adafruit_ssd1306.SSD1306_I2C(128, 64, i2c) # 4. 清屏并显示内容 display.fill(0) # 0代表黑色,1代表白色 display.text('Hello XIAO!', 0, 0, 1) # 在坐标(0,0)处显示白色文字 display.show()注意:
displayio.release_displays()是一个关键步骤。CircuitPython的显示系统使用displayio驱动,它在初始化时会占用相关硬件资源。如果之前运行过其他显示程序或库没有正确释放,直接创建新的I2C对象可能会失败。养成在初始化显示前调用此函数的习惯,可以避免很多“莫名其妙”的I2C总线锁死问题。
4.2 SPI总线读取SD卡数据
SPI通常用于需要高速数据传输的设备,如SD卡、某些传感器和全彩显示屏。XIAO nRF52840的默认SPI引脚是board.SCK(时钟)、board.MOSI(主机输出从机输入)、board.MISO(主机输入从机输出)。我们还需要一个片选引脚board.D10。
首先,确保adafruit_sdcard库已安装。
import board import busio import digitalio import storage import adafruit_sdcard import os # 1. 创建SPI总线对象 spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) # 2. 创建片选(CS)引脚对象 cs = digitalio.DigitalInOut(board.D10) # 3. 创建SD卡对象并挂载文件系统 sdcard = adafruit_sdcard.SDCard(spi, cs) vfs = storage.VfsFat(sdcard) storage.mount(vfs, "/sd") # 将SD卡挂载到“/sd”目录 # 4. 现在可以像操作本地文件一样操作SD卡 try: with open("/sd/test.txt", "a") as f: f.write("Data logged from XIAO!\n") print("Data written to SD card.") except OSError as e: print("SD Card error:", e)通过storage.mount,我们将SD卡的文件系统挂载到了/sd路径下,之后所有针对/sd目录的文件操作(open,read,write,listdir)都会作用在SD卡上。这对于数据记录项目至关重要。
4.3 UART串口通信
UART是最简单的双向异步串行通信,常用于与电脑、GPS模块、老式传感器或另一个微控制器通信。
import board import busio import time # 创建UART对象,波特率设为9600 uart = busio.UART(board.TX, board.RX, baudrate=9600) # 发送数据 uart.write(b"Hello from XIAO!\r\n") # 注意要发送bytes类型 # 非阻塞式读取数据 if uart.in_waiting > 0: data = uart.read(uart.in_waiting) # 读取所有可用数据 if data: print("Received:", data) # 更常见的用法是在循环中读取 buffer = bytearray(32) while True: bytes_read = uart.readinto(buffer) # 将数据读入预分配的缓冲区 if bytes_read: received_text = buffer[:bytes_read].decode('utf-8', errors='ignore') print("Echo:", received_text) # 回传接收到的数据 uart.write(b"Echo: " + buffer[:bytes_read]) time.sleep(0.1)busio.UART提供了基础的读写功能。对于复杂的协议解析(如GPS NMEA语句),你可能需要结合string的方法或使用专门的库(如adafruit_gps)。
5. 低功耗蓝牙开发实战
nRF52840的看家本领就是蓝牙,而CircuitPython通过_bleio模块(注意前面的下划线,表示它仍在发展中)提供了蓝牙支持。这可能是整个平台中最令人兴奋也最具挑战性的部分。
5.1 BLE基础概念与CircuitPython模型
在开始编码前,需要理解几个核心概念:
- 外设(Peripheral):通常指发送数据或提供服务的设备,比如心率带、温度传感器。XIAO nRF52840通常扮演这个角色。
- 中央设备(Central):扫描并连接外设的设备,比如手机、电脑。
- 服务(Service):一个功能单元,包含一个或多个特征值。
- 特征值(Characteristic):服务中的具体数据点,它包含一个值(Value)和一组属性(读、写、通知等)。
CircuitPython的_bleio采用了事件驱动模型。你定义服务、特征值,并设置回调函数来处理连接、断开、数据写入等事件。
5.2 创建一个简单的BLE温度计外设
假设我们有一个连接到A0引脚的温度传感器(模拟输出),我们想通过BLE广播它的读数。
import board import analogio import time import _bleio from adafruit_ble import BLERadio from adafruit_ble.services.nordic import UARTService from adafruit_ble.advertising.standard import ProvideServicesAdvertisement import adafruit_thermistor # 假设使用一个模拟温度传感器 # 初始化BLE无线电和UART服务(一种常见的服务,便于与手机App通信) ble = BLERadio() uart_service = UARTService() advertisement = ProvideServicesAdvertisement(uart_service) # 初始化温度传感器(这里用模拟热敏电阻示例) thermistor = adafruit_thermistor.Thermistor(board.A0, series_resistor=10000, nominal_resistance=10000, b_coefficient=3950) print("BLE Temperature Peripheral Starting...") while True: # 开始广播,等待连接 ble.start_advertising(advertisement) print("Advertising...") while not ble.connected: # 在等待连接时,可以做一些其他事情,比如读取传感器值 # 但这里简单等待 time.sleep(0.1) print("Connected!") # 进入连接状态循环 while ble.connected: # 读取温度 temp_c = thermistor.temperature # 通过UART服务发送温度数据 # 注意:uart_service.rx和tx是缓冲区,需要以bytes形式写入 data_str = f"Temp: {temp_c:.2f}C\n" uart_service.write(data_str.encode('utf-8')) time.sleep(2) # 每2秒发送一次 print("Disconnected.")这个例子使用了adafruit_ble库(需要安装),它是对底层_bleio的高级封装,简化了服务和广播的创建。我们创建了一个UARTService,它模拟了串口通信,许多通用的BLE手机App(如nRF Connect、LightBlue)都能识别并连接它,然后收发数据。
5.3 连接与数据交换的注意事项
- 连接间隔与功耗:BLE连接参数(如连接间隔)会极大影响功耗和响应速度。在
_bleio中,这些参数通常在连接建立时由中央设备(如手机)提议,外设可以接受或拒绝。对于低功耗数据采集,可以尝试在手机端App设置更长的连接间隔(如100ms以上)。 - MTU大小:最大传输单元,决定了单次能发送的数据包大小。默认可能只有23字节。如果你需要发送更长的数据(比如一幅小图片),可能需要协商更大的MTU。这在
adafruit_ble中可能需要更底层的操作。 - 广播数据:在广播包中可以包含设备名称、厂商数据等。你可以自定义广播数据,让特定的中央设备能识别你的设备。
- 安全性与配对:对于需要安全性的应用,可以启用配对和加密。这涉及到更复杂的密钥管理和IO能力(如显示、按键)交互,在资源受限的微控制器上实现起来比较有挑战性。
6. 项目管理、调试与性能优化
当项目代码越来越复杂,管理多个文件、调试问题、优化性能和功耗就变得重要起来。
6.1 代码组织与多文件管理
CircuitPython允许你将代码模块化。除了自动运行的code.py或main.py,你可以在CIRCUITPY盘上创建其他.py文件作为模块,然后在主程序中导入。
例如,创建一个sensor_utils.py文件:
# sensor_utils.py def read_temperature(pin): # 模拟读取温度的复杂逻辑 # ... return temperature def calibrate_sensor(offset): # 校准逻辑 # ... return calibrated_value然后在code.py中导入并使用:
# code.py import board import time import sensor_utils # 导入自定义模块 while True: temp = sensor_utils.read_temperature(board.A0) print(f"Temperature: {temp}") time.sleep(1)这种方式使得代码更清晰,也便于复用。你还可以将硬件初始化的代码(如创建I2C对象、传感器对象)放在一个单独的配置模块中,避免主程序过于臃肿。
6.2 常见问题排查与调试技巧
MemoryError(内存错误):这是CircuitPython开发者最常遇到的错误。nRF52840虽然有256KB RAM,但CircuitPython运行时和堆内存占用后,留给用户程序的空间可能只有几十KB。- 对策:使用
gc.mem_free()和gc.mem_alloc()(需要import gc)来监控内存使用。避免创建巨大的列表或字符串。及时使用del删除不再需要的大对象。对于重复使用的缓冲区,使用bytearray预分配。 - 案例:我曾经在循环中不断拼接字符串来生成日志,很快就耗尽了内存。改为使用
bytearray缓冲区和uart.write()直接流式输出后,问题解决。
- 对策:使用
库导入失败(
ImportError):- 检查
lib文件夹路径和库文件名是否正确。 - 确认库版本与CircuitPython固件版本兼容。
- 确保库文件本身没有损坏(可以重新下载)。
- 检查
硬件通信失败(I2C/SPI设备无响应):
- 首先检查接线:电源、地线、信号线是否接好。这是最常见的问题。
- 使用REPL和
busio模块进行手动探测。对于I2C,可以运行以下代码扫描设备:import board import busio i2c = busio.I2C(board.SCL, board.SDA) while not i2c.try_lock(): pass print("I2C addresses found:", [hex(addr) for addr in i2c.scan()]) i2c.unlock() - 检查设备地址是否正确。许多传感器的I2C地址可以通过拉高或拉低某个引脚来改变。
程序卡死或无响应:
- 检查是否有死循环,或者
time.sleep()时间过长导致看门狗(Watchdog)超时(如果启用)。 - 尝试在REPL中按
Ctrl+C中断当前运行的程序。 - 如果完全无响应,可能需要硬复位(按RST按钮)或重新上电。
- 检查是否有死循环,或者
6.3 功耗优化策略
XIAO nRF52840设计用于低功耗场景,但不当的代码会使其持续消耗数mA甚至十几mA的电流。优化目标是降至微安级别。
- 禁用未使用的硬件和外设:在初始化后,如果不使用板载NeoPixel LED、传感器等,确保将其关闭或置于低功耗模式。例如,将NeoPixel亮度设为0。
- 利用
time.sleep()和深度睡眠:最简单的省电方法是在无事可做时让CPU休眠。普通的time.sleep()会降低功耗。对于更极致的省电,可以使用alarm模块进入深度睡眠(light或deepsleep),此时大部分电路关闭,仅由RTC或外部中断唤醒。但要注意,深度睡眠时RAM内容可能丢失,程序会从头开始执行。 - 优化BLE连接参数:如前所述,增加BLE连接间隔是降低射频部分功耗的最有效手段。
- 降低CPU频率:CircuitPython允许动态调整CPU频率(通过
microcontroller.cpu.frequency),降低频率可以减少动态功耗,但也会降低性能。 - 断开调试接口:当通过USB供电和通信时,功耗会较高。对于最终部署的产品,应使用电池供电,并移除所有调试输出(如
print语句,它们会通过USB串口工作)。
一个简单的低功耗数据采集循环示例:
import board import digitalio import time import alarm from adafruit_ble import BLERadio # ... 其他导入 # 初始化时关闭不必要的硬件 led = digitalio.DigitalInOut(board.LED) led.direction = digitalio.Direction.OUTPUT led.value = False # 关闭板载LED sensor_power = digitalio.DigitalInOut(board.D5) # 用一个GPIO控制传感器电源 sensor_power.direction = digitalio.Direction.OUTPUT ble = BLERadio() # ... 初始化BLE和外设 while True: # 1. 唤醒,给传感器上电 sensor_power.value = True time.sleep(0.01) # 等待传感器稳定 # 2. 采集数据 data = read_sensor() # 3. 如果BLE已连接,发送数据 if ble.connected: send_data_via_ble(data) # 4. 关闭传感器电源 sensor_power.value = False # 5. 进入轻睡眠一段时间(例如10秒) time_alarm = alarm.time.TimeAlarm(monotonic_time=time.monotonic() + 10) alarm.light_sleep_until_alarms(time_alarm)7. 从原型到项目:实战案例与进阶思路
掌握了基础,我们可以构思更完整的项目。这里分享一个我实际搭建过的环境监测节点的思路,它融合了传感器读取、BLE数据传输和低功耗管理。
7.1 项目构想:低功耗蓝牙环境监测节点
目标:制作一个由电池供电的微型设备,每隔5分钟测量一次温度和湿度,并通过BLE将数据发送到附近的手机或网关进行记录。要求续航时间达到数周。
硬件清单:
- Seeed Studio XIAO nRF52840(主控)
- AHT20或SHT31温湿度传感器(I2C接口,精度高,功耗低)
- 3.7V锂聚合物电池(500mAh以上)
- 电池充电/保护模块(可选,如果XIAO底板没有)
- 杜邦线、面包板或定制PCB
软件设计要点:
- 电源管理:使用XIAO nRF52840的
ENABLE引脚(如果底板引出)或一个MOSFET来控制传感器的电源,仅在测量时通电。主控大部分时间处于深度睡眠。 - 数据采集:使用
adafruit_ahtx0或adafruit_sht31d库读取传感器数据。确保I2C总线在睡眠前被正确释放(i2c.deinit()或i2c = None)。 - BLE通信策略:采用“连接-发送-断开”模式,而不是保持长连接。设备唤醒后,快速广播并尝试连接预设的中央设备(如手机),连接成功后立即发送缓存的数据,然后主动断开连接并进入睡眠。这比维持长连接更省电。
- 数据缓存:考虑到可能无法立即连接,设备需要具备在本地缓存若干次测量数据的能力(例如使用
json模块将数据存储在CIRCUITPY盘上的文件里,但要注意Flash写入寿命和功耗)。连接成功后再将缓存的数据一并发送。 - 唤醒源:使用
alarm.time.TimeAlarm进行定时唤醒,这是实现周期性任务最直接的方式。
7.2 核心代码结构示例
import board import time import alarm import digitalio import busio import json import _bleio from adafruit_ble import BLERadio from adafruit_ble.services.nordic import UARTService from adafruit_ble.advertising.standard import ProvideServicesAdvertisement # 假设使用AHT20 import adafruit_ahtx0 # 配置 MEASUREMENT_INTERVAL = 300 # 秒,5分钟 SENSOR_POWER_PIN = board.D6 DATA_FILE = "/data_log.json" # 初始化电源控制引脚 sensor_power = digitalio.DigitalInOut(SENSOR_POWER_PIN) sensor_power.direction = digitalio.Direction.OUTPUT sensor_power.value = False # 初始化BLE ble = BLERadio() uart_service = UARTService() advertisement = ProvideServicesAdvertisement(uart_service) def measure(): """上电传感器,进行测量,然后断电""" sensor_power.value = True time.sleep(0.05) # 等待传感器启动 i2c = busio.I2C(board.SCL, board.SDA) sensor = adafruit_ahtx0.AHTx0(i2c) temperature = sensor.temperature humidity = sensor.relative_humidity # 释放I2C资源 i2c.deinit() sensor_power.value = False return temperature, humidity def save_data(temp, hum): """将数据追加保存到文件""" try: with open(DATA_FILE, "r") as f: data_list = json.load(f) except (OSError, ValueError): data_list = [] data_list.append({"t": time.monotonic(), "temp": temp, "hum": hum}) # 只保留最近N条数据,防止文件过大 if len(data_list) > 100: data_list = data_list[-50:] with open(DATA_FILE, "w") as f: json.dump(data_list, f) def send_stored_data(): """尝试连接并发送所有存储的数据""" if not ble.connected: ble.start_advertising(advertisement, timeout=10) # 广播10秒 while not ble.connected and ble.advertising: time.sleep(0.1) if not ble.connected: return False # 连接失败 try: with open(DATA_FILE, "r") as f: data_list = json.load(f) for data in data_list: msg = json.dumps(data) + "\n" uart_service.write(msg.encode('utf-8')) time.sleep(0.01) # 小延迟,避免缓冲区溢出 # 清空已发送的数据文件 with open(DATA_FILE, "w") as f: json.dump([], f) return True except Exception as e: print("Send error:", e) return False finally: if ble.connected: ble.stop_advertising() # 注意:这里为了省电,我们主动断开连接 # 实际可能需要更优雅的断开方式 ble._adapter._bleio_adapter._disconnect() # 这是一个底层方法,谨慎使用 # 主循环(实际上每次唤醒都会从头执行) def main(): print(f"Woke up at {time.monotonic()}") # 1. 测量 temp, hum = measure() print(f"Measured: {temp:.1f}C, {hum:.1f}%") # 2. 保存数据 save_data(temp, hum) # 3. 尝试发送数据(例如每第6次测量尝试发送一次,降低射频活动频率) if (int(time.monotonic() / MEASUREMENT_INTERVAL) % 6) == 0: if send_stored_data(): print("Data sent successfully.") else: print("Failed to send data.") # 4. 设置下一次唤醒闹钟并进入深度睡眠 next_alarm = alarm.time.TimeAlarm(monotonic_time=time.monotonic() + MEASUREMENT_INTERVAL) alarm.exit_and_deep_sleep_until_alarms(next_alarm) # 程序入口 if __name__ == "__main__": # 检查是否是深度睡眠后的唤醒 if alarm.wake_alarm: print("Woken by alarm") else: print("Cold boot") main()这个例子提供了一个框架,实际应用中还需要处理更多细节,比如BLE连接超时、断开重连策略、文件系统的错误处理、以及更精确的功耗测量。
7.3 进阶探索方向
当你熟悉了基础操作后,可以探索更多可能性:
- 使用
asyncio进行并发编程:CircuitPython支持asyncio库,允许你在单个线程中“同时”处理多个任务,例如同时监听按钮事件、更新显示屏和进行网络通信,而无需复杂的多线程。 - 连接Wi-Fi(需外接模块):虽然nRF52840不支持Wi-Fi,但你可以通过UART或SPI连接一个ESP32或ESP8266模块,使用
adafruit_esp32spi库来接入互联网,实现数据上报到云平台。 - 开发自定义BLE服务:不局限于UART服务,你可以使用
_bleio定义自己的服务和特征值,实现更定制化的蓝牙协议,与专用的手机App配合。 - 使用外部Flash存储:
CIRCUITPY的文件系统是基于内部Flash的,频繁写入可能影响寿命。对于大量数据记录,可以考虑通过SPI连接外部Flash芯片(如W25Q系列),并使用相应的库进行管理。 - 参与CircuitPython社区:Adafruit的Discord和论坛非常活跃。遇到棘手的问题时,去那里搜索或提问,经常能得到核心开发者的直接帮助。你也可以为自己常用的传感器编写或贡献驱动库。
从我个人的使用经验来看,XIAO nRF52840与CircuitPython的组合,最大的优势在于其极快的迭代速度。你想测试一个传感器?几分钟内就能看到数据。想改个逻辑?保存文件立刻生效。这种即时反馈对于学习和原型开发是无价的。当然,它也有其边界,比如对实时性要求极高的应用、需要复杂数字信号处理(DSP)的任务、或者对内存和计算资源极度苛刻的场景,可能还是需要回归到C/C++和传统的RTOS开发环境。但对于绝大多数物联网原型、教育项目、艺术装置和个人DIY项目而言,这个组合提供了一个强大、友好且充满乐趣的起点。