1. 项目概述:当《易经》遇见微控制器
作为一名在嵌入式开发领域摸爬滚打了十多年的老玩家,我经手过各种稀奇古怪的项目,但将古老的《易经》占卜与现代的微控制器编程结合,确实是个让人眼前一亮的创意。这个项目基于 Adafruit 的 CLUE 开发板,利用其内置的加速度计实现“摇签”动作,通过 CircuitPython 编程,在小小的屏幕上动态绘制出《易经》六十四卦的卦象,并给出对应的卦名。它不仅仅是一个简单的电子玩具,更是一个融合了硬件交互、软件逻辑、图形显示和传统文化概念的综合性嵌入式系统实践案例。
对于刚接触嵌入式开发的朋友来说,这个项目堪称“麻雀虽小,五脏俱全”。它涵盖了从传感器数据读取(加速度计)、用户输入判断(按键)、随机数生成(结合物理熵源)、到图形界面渲染(DisplayIO库)的完整流程。而对于有经验的开发者,其中利用位运算映射卦象、使用 TileGrid 和缩放(Scale)来高效渲染图形的技巧,也颇具借鉴意义。无论你是想学习 CircuitPython,还是对如何将抽象文化概念具象化为软硬件交互感兴趣,这个项目都能提供一条清晰的实践路径。
2. 核心思路与方案选型解析
2.1 为什么选择 CircuitPython 和 CLUE 开发板?
在启动任何嵌入式项目前,工具链的选择至关重要。这个项目选择了 CircuitPython 和 Adafruit CLUE 开发板,背后有非常实际的考量。
CircuitPython 的优势:与传统的 C/C++ 开发(如 Arduino)相比,CircuitPython 是 MicroPython 的一个分支,其最大特点是“即写即运行”。你不需要复杂的编译、烧录环境,只需将代码文件(code.py)拖入开发板识别出的CIRCUITPYU盘,代码便会自动执行。这对于快速原型开发、教学以及非计算机专业背景的爱好者来说,门槛极低。调试也异常方便,可以直接通过串口终端(如 Mu 编辑器、Thonny 或screen/putty)看到print()语句的输出。在这个项目中,我们需要快速实现逻辑并频繁调整图形显示参数,CircuitPython 的交互性和易用性成为了首选。
CLUE 开发板的硬件契合度:Adafruit CLUE 是一款基于 Nordic nRF52840 的强大开发板,它几乎为这个项目“量身定做”:
- 集成显示屏:拥有一块 240x240 的彩色 TFT 屏幕,无需额外接线,为卦象和文字的显示提供了完美的输出设备。
- 丰富的传感器:内置 LSM6DS33 惯性测量单元(包含加速度计和陀螺仪),这正是实现“摇签”交互的核心硬件。我们通过读取加速度计数据来判断用户是否进行了摇晃动作。
- 按钮与蜂鸣器:板载的 A、B 按钮用于确认操作,内置的小型蜂鸣器可以播放提示音效,增强了交互的仪式感和反馈。
- 足够的计算与存储资源:nRF52840 拥有充足的 RAM 和 Flash,足以流畅运行 CircuitPython 及相关的图形库。
选择这两者组合,意味着开发者可以将绝大部分精力集中在应用逻辑和创意实现上,而非纠缠于底层驱动和硬件调试。
2.2 《易经》卦象的数字化建模逻辑
将古老的卦象转化为计算机可处理的数据,是整个项目的逻辑核心。这里采用了一种巧妙且高效的二进制映射方法。
卦象的二进制本质:《易经》卦象由六条“爻”组成,每条爻有两种状态:阴爻(- -)和阳爻(---)。如果我们把阴爻看作0,阳爻看作1,那么一个完整的六爻卦象,从最底下的初爻到最上面的上爻,正好构成一个 6 位的二进制数。例如,阳阴阳阴阳阴(从下至上)就是101010,对应的十进制数是 42。
六十四卦的数组映射:既然每个卦象对应一个 0 到 63 的唯一数字,那么最直接的存储方式就是一个长度为 64 的数组(或元组)。数组的索引(0-63)就是卦象的编号,数组元素就是该卦象对应的英文名称(或任何其他描述)。在代码中,HEXAGRAMS这个元组就承担了这个角色。当程序通过随机过程得到一个数字reading(例如 42)后,只需执行HEXAGRAMS[reading]就能立刻获取卦名 “STRIDE”。
爻序与位序的对应关系:这里有一个关键细节:卦象的绘制顺序是从下往上,但二进制数的位权是从右向左(LSB 在最右)。在代码实现时,需要处理好这个对应关系。show_hexagram(number)函数中的tile_grid[5-i] = (number >> i) & 0x01这行代码精妙地解决了这个问题:通过右移i位并取最低位,依次获取从最高位(对应最上爻)到最低位(对应最下爻)的二进制值,然后将其赋值给TileGrid中从下往上数(索引 5-i)的图块。这种位操作是嵌入式编程中处理状态标志、编码解码的常用高效手段。
注意:这种二进制映射是一种非常工程化的简化理解,便于编程实现。在传统的《易经》哲学体系中,卦象的生成和演变(如“变爻”)有其复杂的规则。本项目聚焦于“摇签”这一随机获取卦象的交互形式,因此采用了这种简洁的数学模型。
3. 硬件准备与开发环境搭建
3.1 CLUE 开发板 CircuitPython 固件烧录
拿到一块全新的 CLUE 开发板,第一步是让它“学会”说 CircuitPython 的语言。
- 下载固件:访问 CircuitPython 官网,找到 Adafruit CLUE 的页面,下载最新的
.uf2格式固件文件。务必选择与你的硬件版本完全匹配的固件。 - 进入引导加载模式:使用一条可靠的数据线(很多手机充电线只能充电,无法传输数据,务必确认)将 CLUE 连接到电脑。快速双击 CLUE 板上的RESET按钮。此时,板载的 NeoPixel LED 会亮起绿色(如果亮红色,通常意味着 USB 供电或连接有问题),并且电脑上会出现一个名为CLUEBOOT的U盘。
- 拖入固件:将下载好的
adafruit-circuitpython-clue-...uf2文件直接拖入CLUEBOOT盘符。CLUE 会自动开始烧录,LED 会闪烁。完成后,CLUEBOOT盘符会消失,取而代之出现一个名为CIRCUITPY的新盘符。这表明 CircuitPython 系统已经成功启动。
3.2 必备库文件的安装
CircuitPython 的强大功能依赖于各种库文件。库文件需要被放置在CIRCUITPY盘符下的lib文件夹内。
- 创建 lib 文件夹:如果
CIRCUITPY盘根目录下没有lib文件夹,请手动创建一个。 - 获取库文件:
- 推荐方式(Adafruit 库捆绑包):前往 Adafruit 的 CircuitPython 库页面,下载与你的 CircuitPython 版本对应的“库捆绑包”(Library Bundle)。这是一个压缩文件,里面包含了几乎所有常用的库。
- 手动下载:根据项目代码开头列出的清单,逐一从 Adafruit 的 GitHub 仓库下载对应的
.mpy或文件夹。
- 安装库:将捆绑包解压,找到本项目需要的库文件(如
adafruit_bmp280.mpy,adafruit_display_text,neopixel.mpy等),将它们复制到CIRCUITPY/lib/目录下。对于像adafruit_display_text这样的库,它是一个文件夹,需要将整个文件夹复制进去。
关键库说明:
adafruit_clue:这是 CLUE 板的“一站式”库,封装了所有传感器、屏幕、按钮的访问接口,极大简化了代码。adafruit_displayio/adafruit_bitmap_font/adafruit_display_text:用于图形显示和文本渲染的核心库。adafruit_lsm6ds:提供加速度计的直接驱动(虽然adafruit_clue已经封装,但了解其独立存在有助于理解底层)。
完成后,你的CIRCUITPY目录结构应类似于:
CIRCUITPY/ ├── code.py (你的主程序,稍后创建) ├── christopher_done_24.bdf (字体文件) └── lib/ ├── adafruit_clue.mpy ├── adafruit_display_text/ ├── adafruit_bitmap_font/ ├── adafruit_lsm6ds.mpy └── ... (其他所需库文件)3.3 项目文件部署
- 获取字体文件:从项目源地址下载
christopher_done_24.bdf字体文件,直接放在CIRCUITPY盘的根目录。 - 创建主程序:在
CIRCUITPY盘根目录下,用任何文本编辑器(推荐 Mu Editor、VS Code 或记事本)创建一个新文件,将提供的项目代码完整复制进去,并保存命名为code.py。CircuitPython 会自动运行根目录下的code.py文件。
至此,硬件和软件环境就全部准备就绪了。按下 CLUE 板的 RESET 按钮,程序就会开始运行。
4. 代码深度解析与核心功能实现
4.1 程序骨架与初始化流程
让我们深入code.py,看看这个占卜器是如何“活”起来的。程序的开头部分主要进行环境设置和资源初始化。
#--| User Config |------------------------------- BACKGROUND_COLOR = 0xCFBC17 HEXAGRAM_COLOR = 0xBB0000 FONT_COLOR = 0x005500 SHAKE_THRESHOLD = 20 MELODY = ( (1000, 0.1), # (频率Hz, 持续时间秒) (1200, 0.1), (1400, 0.1), (1600, 0.2)) #--| User Config |-------------------------------用户配置区:这是项目中我最欣赏的设计之一,将可定制参数集中放在开头。你可以轻松修改屏幕背景色、卦象线条颜色、文字颜色。SHAKE_THRESHOLD是判断摇晃动作灵敏度的关键,值越大,需要摇晃得越用力才能触发。MELODY定义了占卜完成后播放的一段简单音效,你可以修改频率和时长来创造不同的声音。
HEXAGRAMS = ( "EARTH", "RETURN", ... ) # 64个卦名卦名数据:这个长达 64 个元素的元组,是项目的“知识库”。它按照二进制顺序(0 到 63)存储了每个卦象对应的英文名称。有些名称中包含\n换行符,是为了在屏幕上更好地排版显示。
display = clue.display # 创建背景 bg_bitmap = displayio.Bitmap(display.width, display.height, 1) bg_palette = displayio.Palette(1) bg_palette[0] = BACKGROUND_COLOR background = displayio.TileGrid(bg_bitmap, pixel_shader=bg_palette)显示系统初始化:这是 CircuitPythondisplayio库的标准操作流程。
- 获取显示对象
clue.display。 - 创建一个与屏幕同尺寸的位图(
Bitmap),颜色深度为 1(即单色索引模式)。 - 创建一个调色板(
Palette),并设置其第 0 号颜色为用户定义的背景色。 - 创建一个平铺网格(
TileGrid),将整个位图作为一张“大图块”铺满屏幕,并应用刚才的调色板。这样就构成了一个纯色的背景层。
4.2 卦象绘制引擎:Sprite Sheet 与 TileGrid 的魔法
这是本项目图形显示部分最精妙的设计,用极小的资源实现了灵活的图形组合。
创建精灵图(Sprite Sheet):
sprite_sheet = displayio.Bitmap(11, 4, 2) palette = displayio.Palette(2) palette.make_transparent(0) palette[0] = 0x000000 palette[1] = HEXAGRAM_COLOR for x in range(11): sprite_sheet[x, 0] = 1 # - - 0 YIN sprite_sheet[x, 1] = 0 sprite_sheet[x, 2] = 1 # --- 1 YANG sprite_sheet[x, 3] = 0 sprite_sheet[5, 0] = 0- 创建一个 11 像素宽、4 像素高、2 种颜色深度的位图。这就像一个微小的画布。
- 创建一个 2 色的调色板,索引 0 为黑色(透明),索引 1 为卦象线条色。
- 通过循环,在这个小位图上“绘制”出两条线:第 0 行(Yin,阴爻)除了最中间一个像素(
sprite_sheet[5,0]=0)置为透明(索引0),其余像素都置为线条色(索引1),形成一条中间断开的虚线。第 2 行(Yang,阳爻)全部像素置为线条色,形成一条实线。第 1 行和第 3 行全部留空(透明),作为行间距。 这个自生成的精灵图,其内存占用极小,是嵌入式图形编程中“空间换时间(或复杂度)”的经典反例——这里是用“代码逻辑换存储空间”。
构建卦象 TileGrid:
tile_grid = displayio.TileGrid(sprite_sheet, pixel_shader=palette, width=1, height=6, tile_width=11, tile_height=2)这里创建了一个 1 列、6 行的TileGrid。关键参数是tile_width=11和tile_height=2。这意味着这个网格的每个单元格(即每一爻的位置)都会去sprite_sheet这个源位图上,截取一块 11x2 像素的区域来显示。通过指定不同的“图块索引”(0 或 1),就能让每个单元格显示阴爻或阳爻的图案。
应用缩放(Scale):
hexagram = displayio.Group(x=60, y=15, scale=10) hexagram.append(tile_grid)原始的爻线只有 11x2 像素,在 240x240 的屏幕上太小了。displayio.Group的scale参数解决了这个问题。scale=10意味着这个组(Group)内的所有元素在渲染时,每个像素都会被放大 10 倍。于是,11 像素宽的线条变成了 110 像素宽,2 像素高的行(加上间距)变成了 20 像素高,最终整个卦象以 110x120 的醒目尺寸显示在屏幕上。这种缩放是“最近邻”算法,对于这种纯色块图形效果完美,且计算开销极低。
4.3 主循环与交互逻辑剖析
程序的主逻辑清晰体现了状态机的思想,分为“等待摇签”、“生成卦象”、“等待确认”、“显示结果”四个状态。
# 状态1:等待摇签 print("shake") while not clue.shake(shake_threshold=SHAKE_THRESHOLD): passclue.shake()是adafruit_clue库提供的便利函数,它内部持续读取加速度计数据,计算综合加速度变化,并与阈值比较。这个循环会一直阻塞在这里,直到用户摇晃开发板达到预设力度。print(“shake”)在串口终端输出,是调试的好帮手。
# 状态2:生成真随机数(卦象编号) x, y, z = clue.acceleration random.seed(int(time.monotonic() + abs(x) + abs(y) + abs(z))) reading = random.randrange(64)这是项目的“玄学”核心,也是工程上的一个亮点。单纯的random.randrange(64)使用的是伪随机数算法,如果每次上电的种子一样,序列就一样。为了让每次摇签更具“随机性”,这里用了一个巧妙的办法来生成随机种子:
time.monotonic():获取一个从开机起持续递增的时间戳(浮点数)。clue.acceleration:获取摇动停止后瞬间的加速度值(x, y, z)。- 将时间戳和三个加速度的绝对值相加,取整,作为随机数生成器的种子。 这样,种子由“摇动发生的时间点”和“摇动停止时的姿态”共同决定,这两个因素都充满了物理世界的不确定性,从而极大地增强了随机数的不可预测性,更贴合“占卜”的初衷。
# 状态3:提示用户按按钮查看 display.auto_refresh = False hexname.text = " GOT IT\n\nPRESS BUTTON\n TO SEE" display.auto_refresh = True while not clue.button_a and not clue.button_b: pass在显示卦象前,先给用户一个确认环节。这里有一个重要技巧:display.auto_refresh = False。在更新多个显示对象(如改变文本、添加图形组)时,关闭自动刷新,等所有修改完成后一次性刷新(= True),可以避免屏幕中间状态的闪烁,提升视觉体验。
# 状态4:显示卦象和卦名 display.auto_refresh = False splash.append(hexagram) # 将卦象图形组加入主显示组 show_hexagram(reading) # 根据数字设置 TileGrid 的每个图块 show_name(reading) # 更新标签文本为对应卦名 display.auto_refresh = Trueshow_hexagram(number)函数是位运算的舞台:
def show_hexagram(number): for i in range(6): tile_grid[5-i] = (number >> i) & 0x01循环i从 0 到 5。number >> i将数字右移i位,& 0x01取最低位。这样,i=0时取的是最低位(二进制最右边),对应最下面的爻(初爻),但tile_grid[5]是 TileGrid 最下面的一行吗?不,这里索引5-i实现了反转:当i=0(LSB) 时,设置tile_grid[5](最下面一行);当i=5(MSB) 时,设置tile_grid[0](最上面一行)。这完美匹配了卦象从下往上绘制的顺序。
最后,程序进入while True: pass的无限循环,等待硬件复位以开始新一轮占卜。
5. 功能扩展与深度定制指南
原项目已经提供了一个完整可用的占卜器,但它的代码结构清晰,为我们留下了丰富的自定义空间。以下是一些可以尝试的进阶玩法。
5.1 视觉与听觉效果定制
颜色主题:直接修改BACKGROUND_COLOR、HEXAGRAM_COLOR和FONT_COLOR这三个变量即可。颜色值采用 16 进制 RGB 格式0xRRGGBB。你可以创建古典的墨色竹简风格(深褐背景、金色文字),或者科幻的赛博风格(黑色背景、霓虹青色线条)。
动画效果:目前的显示是瞬间完成的。可以增加动画,让卦象从下往上一爻一爻地绘制出来。在show_hexagram函数中,每设置一爻后,可以加入time.sleep(0.3)并刷新显示,创造一种“卦象逐渐显现”的仪式感。
音效增强:MELODY元组定义了简单的提示音。你可以:
- 修改音调:调整频率值。可以参考简单的音乐频率表(如中音 C 是 262Hz)。
- 增加音效:在摇动触发时、显示卦象时加入不同的短促音效。
- 实现简单音乐:编写更长的序列,播放一小段与卦象意境相关的旋律片段(这需要预先为 64 卦定义 64 段旋律,工作量较大,但概念很酷)。
5.2 交互逻辑的优化与扩展
灵敏度校准:SHAKE_THRESHOLD是一个全局阈值。更高级的做法是在程序启动后,让设备静止 2 秒,采集这段时间内的加速度计数据,计算其基线噪声水平,然后动态设置一个基于该噪声水平的阈值(例如,基线值的 3 倍标准差)。这样能适应不同的放置环境(如放在软垫上还是硬桌面上)。
多轮占卜与变爻:《易经》占卜中,有时会涉及“变爻”,即摇出的卦象中某些爻会发生变化(从阴变阳或从阳变阴),从而得到另一个“之卦”。我们可以扩展交互:
- 摇出本卦后,屏幕提示“Shake for changing lines?”。
- 用户再次摇晃,程序根据第二次摇动生成的随机数,决定哪些爻是“变爻”(例如,随机数每一位对应一爻,若为1则变)。
- 屏幕用不同颜色(如闪烁或高亮)标出变爻,并同时显示本卦和之卦的卦名。 这需要扩展状态机,并修改图形显示逻辑来高亮特定爻线。
保存历史记录:CLUE 开发板上的文件系统是可写的。可以增加一个功能,将每次摇出的卦象编号、时间戳保存到一个log.csv文件中。这样用户就可以回顾自己的占卜历史。需要注意的是,频繁写入可能会影响 Flash 寿命,可以设定一个最大记录条数或仅在用户主动要求时保存。
5.3 硬件扩展可能性
外接按钮:CLUE 的 GPIO 引脚可以连接外部大按钮,打造更具仪式感的“占卜按钮”,替代板载的小按钮。
环境感知:CLUE 本身还集成了温度、湿度、气压、磁场、色彩、手势等传感器。可以设计更复杂的“随机”种子生成算法。例如,将摇动时的环境温度、光照颜色值也纳入随机种子计算,让卦象与“天时地利”产生更玄妙的联系。
无线传输:CLUE 板载蓝牙。可以编写一个配套的手机 App(使用 MIT App Inventor 或 BLE 库),将摇出的卦象结果通过蓝牙发送到手机。手机 App 可以显示更详细的卦辞、爻辞解释,甚至链接到在线的《易经》数据库,形成一个“智能占卜终端”。
6. 常见问题排查与调试心得
在实际动手制作和调试的过程中,你可能会遇到以下问题。这里分享一些我踩过的坑和解决方法。
6.1 程序无法启动或屏幕无显示
- 问题现象:连接 USB 后,
CIRCUITPY盘符出现,但屏幕一片漆黑或没有出现“SHAKE FOR READING”提示。 - 排查步骤:
- 检查
code.py文件名:确保主程序文件名为code.py,且位于CIRCUITPY根目录,而不是在子文件夹里。CircuitPython 只自动执行根目录下的code.py。 - 检查库文件:打开串口终端(如 Mu Editor 的串口模式)。如果库缺失或版本不兼容,CircuitPython 会在启动时在终端输出详细的错误信息,例如
ImportError: no module named 'adafruit_clue'。根据提示,检查lib文件夹内的库是否齐全、版本是否匹配。 - 检查字体文件:确认
christopher_done_24.bdf文件已正确放置在CIRCUITPY根目录,且文件名拼写无误。如果字体加载失败,文本可能不显示,但图形部分(卦象)可能正常。 - 硬复位:尝试长按 CLUE 板上的 RESET 按钮,或者拔插 USB 线进行硬重启。
- 检查
6.2 摇动无法触发或过于灵敏
- 问题现象:使劲摇晃也没反应,或者轻轻一碰就触发。
- 解决方案:
- 调整
SHAKE_THRESHOLD:这是最直接的参数。默认值是 20。如果你觉得不灵敏,尝试将其调小,例如设为 10 或 15。如果过于灵敏,则将其调大,例如设为 30 或 40。这个值没有绝对标准,取决于你的使用习惯和环境震动。 - 理解原理:
clue.shake()函数内部计算的是加速度变化的幅度。你可以添加调试代码来观察这个值:在while循环内打印clue.acceleration或计算其向量和,看看你正常摇晃时的数值范围,从而更科学地设定阈值。 - 检查放置状态:确保开发板在静止时是平稳的。如果放在不平或易晃动的表面上,可能会产生误触发。
- 调整
6.3 卦象显示错乱或文字重叠
- 问题现象:屏幕上显示的线条不是完整的爻,或者文字显示不全、位置不对。
- 排查步骤:
- 检查精灵图逻辑:回顾
sprite_sheet的生成代码。确保阴爻(索引0)的中间像素被正确设置为透明(sprite_sheet[5,0]=0),否则阴爻会显示为实线。 - 检查 TileGrid 索引:
show_hexagram函数中的tile_grid[5-i]是关键。确保索引计算正确对应了从下到上的六行。你可以手动测试,例如在显示前直接设置tile_grid[0]=1和tile_grid[5]=0,看最上面是阳爻还是阴爻,以验证映射关系。 - 检查坐标和缩放:
hexagram = displayio.Group(x=60, y=15, scale=10)这行决定了卦象组在屏幕上的起始位置和放大倍数。如果x, y坐标不合适,卦象可能显示在屏幕外。如果scale设置过大(比如 20),卦象可能会超出屏幕边界。 - 文本锚点:
hexname.anchor_point = (0.5, 0.5)表示文本的中心点作为定位基准。hexname.anchored_position = (120, 120)表示文本中心点位于屏幕坐标 (120, 120) 处(屏幕中心)。如果文字显示偏了,检查这两个坐标值。
- 检查精灵图逻辑:回顾
6.4 性能优化与内存管理心得
对于更复杂的扩展项目,需要注意 CircuitPython 在微控制器上的资源限制。
- 避免在循环中创建对象:例如,不要在
while True主循环里反复创建新的Bitmap、Palette或Label对象。这会导致内存碎片和最终的内存分配错误(MemoryError)。正确的做法是在程序初始化时创建好所有需要的图形对象,在循环中只修改它们的属性(如text,[index])。 - 谨慎使用大字体或图片:
.bdf字体文件是位图字体,字号越大,占用内存越多。高分辨率的图片也会消耗大量内存。在 CLUE 的 240x240 屏幕上,使用适中的字体大小(如本项目中的24点阵)是安全的。 - 利用
display.auto_refresh:如前所述,在批量更新显示内容时,先关闭自动刷新,所有更新完成后一次性打开,这是提升视觉流畅性和减少不必要的屏幕刷新开销的标准做法。 - 使用
.mpy格式的库:.mpy是预编译的字节码格式,比直接使用.py源文件加载更快,占用内存更少。在部署项目时,尽量使用库的.mpy版本。