1. 项目概述一个会追激光的桌面电子宠物猫如果你玩过上世纪90年代的电脑可能对“桌面宠物”这个概念不陌生。那些在屏幕角落里自顾自玩耍、睡觉的小猫小狗是许多人的童年记忆。今天我们要把这个经典概念用现代的开源硬件和软件技术“复活”让它在一个实体的、可触摸的智能显示屏上跑起来。这个项目叫做“Neko Kitty”一个基于CircuitPython和Displayio图形库的交互式动画猫咪精灵。它的核心是一只32x32像素的像素风小猫能够在Adafruit PyPortal这类带触摸屏的开发板上自由漫步。更有趣的是它不再是完全“自闭”的宠物——当你触摸屏幕时会出现一个红色的激光点这只小猫会像真实的猫咪一样被激光点吸引并追过去。当然它也有自己的“猫格”会随机停下来舔爪子、打盹走到屏幕边缘时会挠墙完全模拟了一只真实猫咪的任性。对于嵌入式开发者和硬件爱好者来说这个项目远不止是一个怀旧玩具。它实际上是一个微型嵌入式图形交互应用的绝佳教学范例。通过拆解它你能学到如何在资源极其有限的微控制器MCU上高效地管理动画精灵、处理触摸输入、实现一个健壮的行为状态机并最终构建出一个响应灵敏、行为有趣的完整应用。无论你是想为你的物联网设备添加一个有趣的UI还是学习嵌入式图形编程的基本功这个项目都能提供从理论到实践的完整路径。2. 核心原理与架构设计如何在单片机上“养猫”在开始动手写代码之前我们必须先理解背后的核心思想。在内存可能只有几百KB的单片机上实现一个流畅的动画精灵系统不能像在PC上那样“暴力”渲染需要精巧的设计。2.1 Displayio图形栈分层渲染与硬件加速Displayio是CircuitPython为图形显示抽象出的一套核心库。它的设计哲学是分层Group与分块TileGrid。你可以把整个显示屏想象成一个画布displayio.Group就是一个透明的图层。我们可以在一个主Group里叠加多个子Group比如一个做背景一个放猫咪精灵一个放激光点。displayio库会负责将这些图层按照顺序合成最终输出到屏幕。这种设计带来了巨大的灵活性比如我们可以独立移动、缩放某个图层而不影响其他元素。displayio.TileGrid瓷砖网格是承载图像的核心。它并不直接存储整个大位图而是存储一个对“精灵表”Sprite Sheet的引用并通过索引来显示精灵表中的某一块“瓷砖”。我们的猫咪所有动作帧都打包在一张neko_cat_spritesheet.bmp图片里TileGrid通过改变索引值就能快速切换显示奔跑、睡觉、挠墙等不同姿态这比频繁加载和释放多个小图片要高效得多极大地节省了宝贵的内存和处理器时间。2.2 状态机猫咪行为的“大脑”猫咪下一秒是走是停是追激光还是睡大觉这种决策逻辑我们用状态机State Machine来实现。这是嵌入式系统和游戏开发中管理复杂行为模式的经典模式。在这个项目中猫咪被定义了多种状态移动状态向左、向右、向上、向下以及四个斜向移动。静止状态坐着、舔爪子、睡觉。边界状态挠上、下、左、右四面墙。每个状态都是一个简单的元组Tuple包含三个信息(状态ID, 动画帧序列, (x方向步进, y方向步进))。例如STATE_MOVING_RIGHT (3, (12, 13), (10, 0))表示状态ID是3动画在精灵表的第12和13帧之间循环每步向右移动10像素。猫咪的“大脑”update函数在每个主循环周期里都会检查当前状态、周围环境是否碰到边缘激光点在哪然后根据一系列规则如随机数判定决定是保持状态还是切换到下一个状态。这种设计将复杂的行为逻辑分解为离散的状态和清晰的转换条件使得代码非常易于理解、调试和扩展。2.3 资源优化策略在螺蛳壳里做道场PyPortal等开发板性能强大但资源依然有限。项目中有几个关键的优化点值得学习背景缩放背景是一个纯色矩形代码中创建了一个很小的位图20x15像素然后通过设置Group的scale16将其放大到全屏320x240像素。这比直接创建一个全屏大小的位图节省了超过90%的内存。调色板与透明色精灵表使用索引颜色模式而非RGB。displayio.Palette调色板对象存储了有限的一组颜色。通过make_transparent(0)将第一个颜色通常是背景色设为透明这样在渲染时就能实现非矩形精灵的效果让猫咪融入背景。时间驱动动画动画更新不是每帧都进行而是基于时间戳。通过time.monotonic()获取单调递增的时间与预设的ANIMATION_TIME如0.3秒比较决定是否切换到下一帧。这保证了动画速度不受主循环执行快慢的影响在不同性能的设备上都能有一致的表现。3. 硬件准备与项目环境搭建3.1 硬件选型为什么是PyPortal项目原作者指定了Adafruit PyPortal系列开发板这并非偶然。PyPortal是一个高度集成、开箱即用的物联网显示终端它为我们省去了大量底层硬件调试工作内置显示屏与触摸集成了分辨率清晰的TFT屏幕和电容触摸屏无需额外接线和驱动调试。强大的主控使用Microchip原Atmel的ATSAMD51或类似系列MCU运行CircuitPython绰绰有余。丰富的接口板载Wi-Fi模块、MicroSD卡槽、扬声器接口等为未来功能扩展留足空间。如果你手头没有PyPortal理论上任何支持CircuitPython且带有足够引脚驱动SPI或并行接口显示屏的开发板如ESP32-S3、RP2040系列板卡搭配显示屏都可以尝试移植但需要自行处理屏幕和触摸的驱动初始化复杂度会高不少。对于初学者强烈建议从PyPortal开始它能让你专注于应用逻辑而非硬件兼容性。3.2 软件环境部署从零到一安装CircuitPython访问Adafruit官网找到对应你的PyPortal型号的CircuitPython固件.uf2文件。用USB线连接PyPortal和电脑快速双击板载复位按钮直到出现一个名为PORTALBOOT或RPI-RP2的U盘将下载的.uf2文件拖入即可完成刷机。完成后会出现一个名为CIRCUITPY的U盘。获取项目文件从项目页面下载“Project Bundle”项目捆绑包。这是一个压缩文件里面包含了核心的code.py主程序、猫咪精灵表图片neko_cat_spritesheet.bmp以及必要的库文件。文件系统部署解压下载的捆绑包。将code.py和neko_cat_spritesheet.bmp直接复制到CIRCUITPY驱动器的根目录。然后将lib文件夹内含adafruit_imageload、adafruit_touchscreen等依赖库也整体复制到CIRCUITPY驱动器的根目录。如果lib文件夹已存在合并即可。自动运行CircuitPython设备上电后会自动执行根目录下的code.py。复制完成后按下PyPortal的复位键稍等片刻你就能看到蓝色背景上出现一只静止的小猫了。注意务必使用数据线而非仅能充电的线缆连接电脑和PyPortal。如果CIRCUITPY盘符没有出现可能是驱动问题或固件未正确安装需要重新执行刷机步骤。4. 代码深度解析与核心模块实现现在让我们深入代码腹地看看这只电子猫是如何被“赋予生命”的。我将按照代码的执行流程和逻辑模块进行拆解并补充原始代码注释中未提及的细节和设计考量。4.1 全局配置与初始化设定舞台规则程序开头定义了一系列配置变量这是调整猫咪行为和外观的“总开关”。理解它们你就能定制属于自己的宠物。BACKGROUND_COLOR 0x00AEF0 # 背景色十六进制蓝绿色 ANIMATION_TIME 0.3 # 动画帧间隔秒值越小动画越快 USE_TOUCH_OVERLAY True # 是否启用触摸交互 TOUCH_COOLDOWN 0.1 # 触摸事件冷却时间秒防误触 LASER_DOT_COLOR 0xFF0000 # 激光点颜色红色在NekoAnimatedSprite类内部还有控制猫咪行为的参数CONFIG_STEP_SIZE 10猫咪每步移动的像素数。增大此值会让猫咪移动更快但可能显得“瞬移”降低动画平滑度减小则移动更慢更细腻。CONFIG_STOP_CHANCE_FACTOR 30移动时随机停下清洁或睡觉的概率因子。算法是random.randint(0, FACTOR-1)结果为0时触发。因此这个值越小停下的概率反而越大。例如设为10则每步有1/10的概率停下设为100则概率为1/100。CONFIG_START_CHANCE_FACTOR 10挠墙后重新开始移动的概率因子逻辑同上。CONFIG_MIN_SCRATCH_TIME 2最短挠墙时间秒。防止猫咪刚碰到墙就立刻回头增加行为真实性。初始化部分程序首先检测是否启用触摸并初始化触摸屏对象。这里有一个关键细节adafruit_touchscreen.Touchscreen的校准参数calibration((5200, 59000), (5800, 57000))。这些数值是针对PyPortal屏幕的原始触摸ADC值范围用于将触摸坐标映射到屏幕像素坐标。如果你更换了屏幕型号这些值可能需要重新校准。4.2 NekoAnimatedSprite类猫咪的躯体与灵魂这个类继承自displayio.TileGrid意味着每个猫咪实例本身就是一个可显示的图形对象。这是面向对象设计的巧妙之处数据精灵帧和行为移动、状态切换被封装在同一个对象里。精灵表加载与透明处理sprite_sheet, neko_palette adafruit_imageload.load( /neko_cat_spritesheet.bmp, bitmapdisplayio.Bitmap, palettedisplayio.Palette, ) neko_palette.make_transparent(0)adafruit_imageload.load函数一次性加载位图和其调色板。make_transparent(0)是关键调用它告诉渲染引擎调色板中索引为0的颜色在精灵表中通常是粉色或绿色背景应被渲染为透明。这样我们看到的就只有猫咪像素而不是一个矩形色块。状态系统的实现状态是用元组定义的。以STATE_MOVING_RIGHT (3, (12, 13), (CONFIG_STEP_SIZE, 0))为例3是状态ID用于内部标识和区分。(12, 13)是动画帧索引列表。猫咪向右走时会在这两帧之间循环形成踏步动画。(CONFIG_STEP_SIZE, 0)是移动向量。(10, 0)表示每动画帧在X轴向右正方向移动10像素Y轴不变。核心update()函数行为决策引擎这是整个类最复杂也最核心的方法它每帧被主循环调用驱动猫咪的所有行为。其逻辑可以概括为以下流程图所表示的优先级判断目标驱动最高优先级检查self.moving_to属性由触摸事件设置。如果存在目标激光点猫咪会计算目标相对于自身中心点的方位并切换到对应的移动状态8个方向之一向目标前进。如果猫咪的矩形区域已经包含了目标点则认为“抓住”了激光随机切换到睡觉或清洁状态并清空目标。动画与随机行为尝试执行动画self.animate()。如果成功播放了一帧则根据当前状态进行随机行为判定如果正在移动有1/CONFIG_STOP_CHANCE_FACTOR的概率随机切换到睡觉或清洁。如果正在挠墙且已挠够最短时间则有1/CONFIG_START_CHANCE_FACTOR的概率随机切换到一个移动状态。如果正在睡觉或清洁且整个动画序列已播放完一遍CURRENT_ANIMATION_INDEX 0则切换到一个随机移动状态。移动与碰撞检测如果当前是移动状态则尝试沿移动向量前进一步。但在移动前会进行碰撞预测计算下一步的新坐标判断是否超出屏幕边界0 new_x screen_width - cat_width。如果会撞墙则不让它移动并立即切换到面向墙壁的“挠墙”状态同时将其位置“吸附”到紧贴墙壁的位置防止穿模。这种基于优先级的决策链目标 动画/随机 移动/碰撞是游戏AI和交互逻辑的常见模式确保了行为的响应性和合理性。4.3 主循环与触摸交互连接虚拟与现实的桥梁主循环while True是程序的心跳它通常以设备能承受的最快速度运行通常几十到上百FPS。while True: neko.update() # 更新猫咪状态、动画和位置 if USE_TOUCH_OVERLAY: if not neko.moving_to: circle.x -10 # 没有目标时隐藏激光点移出屏幕 circle.y -10 _now time.monotonic() if _now LAST_TOUCH_TIME TOUCH_COOLDOWN: touch_location ts.touch_point if touch_location: LAST_TOUCH_TIME _now circle.x touch_location[0] # 显示激光点 circle.y touch_location[1] neko.moving_to (touch_location[0], touch_location[1]) # 设定猫咪目标这里有几个重要的工程细节触摸防抖通过TOUCH_COOLDOWN默认0.1秒和LAST_TOUCH_TIME确保不会因一次触摸而触发过多事件避免猫咪行为抽搐。激光点视觉反馈激光点是一个vectorio.Circle图形对象。当没有目标时将其坐标设为负值如-10移出屏幕是一种简单高效的“隐藏”方法比从Group中移除再添加更高效。目标坐标钳制在moving_to属性的setter方法中对传入的触摸坐标做了处理确保目标点不会太靠近屏幕边缘小于半个猫咪精灵的宽度/高度。这是因为如果目标点紧贴边缘猫咪为了“到达”该点会不断尝试向边缘移动并触发挠墙陷入逻辑循环。这个处理体现了对边界条件的周密考虑。5. 功能扩展与自定义改造指南原项目已经非常完整但开源项目的乐趣在于改造。以下是一些可以尝试的扩展方向我将提供具体的实现思路和代码片段。5.1 更换精灵与主题你想养一只狗、一只恐龙甚至一个机器人只需要替换精灵表文件。准备精灵表使用Aseprite、PyxelEdit或在线工具制作一张位图。关键要求必须是索引颜色模式通常保存为8位或更低的BMP或PNG背景色你希望透明的颜色必须放在调色板的第一个索引位置索引0。每个精灵帧大小必须一致这里是32x32像素并按行排列。修改代码将adafruit_imageload.load函数加载的文件名改为你的新图片。同时你需要根据新精灵表的布局重新定义所有状态元组中的动画帧索引列表。例如如果你的新精灵表第一行是站立帧第二行是向右走的两帧那么STATE_MOVING_RIGHT的动画列表可能就要改成(1, 2)假设索引从0开始。5.2 添加声音反馈PyPortal板载有音频输出接口。我们可以让猫咪在特定动作时发出声音。准备音频文件将简短的WAV格式声音文件如“喵呜”、抓挠声转换为适合单片机播放的较低采样率和位深例如22kHz16位命名为meow.wav、scratch.wav放入CIRCUITPY盘。引入音频库在代码开头导入audiocore和audioio或对应板卡的音频库。触发播放在状态切换的地方添加播放逻辑。例如在current_state属性的setter方法中或直接在update函数里状态改变后。import audiocore import audioio # 初始化音频输出以PyPortal为例 audio audioio.AudioOut(board.AUDIO_OUT) def play_sound(filename): try: with open(filename, rb) as f: wav audiocore.WaveFile(f) audio.play(wav) except OSError: pass # 忽略文件找不到的错误 # 在STATE_SCRATCHING_LEFT等状态被设置时 if new_state in (self.STATE_SCRATCHING_LEFT, self.STATE_SCRATCHING_RIGHT, ...): play_sound(/scratch.wav) elif new_state self.STATE_SLEEPING: play_sound(/snore.wav) # 添加打鼾声5.3 实现多点触摸或手势当前代码只处理单点触摸。我们可以扩展它实现更丰富的交互。双击唤醒记录两次触摸的时间间隔。如果间隔很短如300毫秒内且猫咪在睡觉则强制将其状态切换为移动。滑动手势记录触摸按下和释放的坐标计算位移向量。根据滑动方向和速度可以抛出一个“玩具”一个新的移动目标让猫咪去追而不是简单的点触。last_touch_time -1 last_touch_pos None # 在主循环的触摸处理部分 if touch_location: _now time.monotonic() if last_touch_pos and (_now - last_touch_time 0.3): # 双击检测 if neko.current_state neko.STATE_SLEEPING: neko.current_state random.choice(neko.MOVING_STATES) last_touch_time _now last_touch_pos touch_location5.4 连接网络与API高级PyPortal内置Wi-Fi我们可以让它变得更“智能”。显示天气在屏幕角落添加一个label对象定期从网络API如OpenWeatherMap获取天气信息并显示。注意这需要异步操作避免阻塞主循环导致猫咪动画卡顿。可以使用asyncio库或简单的状态机来管理网络请求周期。时间同步与作息通过网络同步时间让猫咪在“夜晚”例如屏幕显示晚上10点到早上6点更倾向于睡觉在“白天”更活跃。这只需要在随机行为判定的概率因子中加入时间权重即可。6. 调试技巧与常见问题排查实录即使完全按照指南操作你也可能会遇到一些“坑”。这里分享我实践中总结的经验和解决方案。6.1 猫咪不显示或显示为色块问题现象屏幕只有背景色看不到猫咪或者猫咪显示为一个纯色矩形。排查步骤检查文件路径确认neko_cat_spritesheet.bmp文件确实在CIRCUITPY根目录且文件名拼写完全一致包括大小写。检查库文件确认lib文件夹内存在adafruit_imageload库。CircuitPython 8.x及以上版本可能需要单独安装此库。验证精灵表格式用电脑上的图片查看器打开精灵表BMP文件。确认其颜色模式。CircuitPython的imageload通常对索引色BMP支持最好。如果图片是24位真彩色尝试用图像处理软件如GIMP、Photoshop将其转换为“索引颜色”模式并将颜色数减少到256色以下。检查透明色代码中neko_palette.make_transparent(0)意味着精灵表中索引0的颜色是透明色。请确保你的精灵表背景色确实是调色板中的第一个颜色。一个简单的测试方法是将make_transparent(0)注释掉如果猫咪显示为一个有背景色的矩形说明图片加载成功但透明设置未生效需要检查图片格式。6.2 触摸没有反应或激光点位置不准问题现象触摸屏幕激光点不出现或者出现的位置与手指位置偏差很大。排查步骤确认触摸屏初始化首先检查代码顶部USE_TOUCH_OVERLAY是否设置为True。检查硬件连接如果是自己连接的触摸屏请仔细检查SPI或I2C引脚连接是否正确、牢固。校准触摸屏PyPortal的校准参数((5200, 59000), (5800, 57000))是针对其特定型号的。如果你使用的是其他屏幕或觉得触摸不准需要重新校准。一个简单的校准方法是在代码中添加调试输出打印ts.touch_point的原始值然后触摸屏幕四个角记录下对应的ADC值再重新计算映射关系。Adafruit通常提供专门的触摸校准示例代码。检查冷却时间TOUCH_COOLDOWN设置过长比如1秒会导致触摸响应迟钝。可以暂时将其设为0.01进行测试。6.3 动画卡顿或猫咪移动不流畅问题现象猫咪动画像幻灯片移动一跳一跳的。排查步骤检查主循环负载在while True循环末尾添加time.sleep(0.01)或一个很小的延时可以主动让出CPU有时反而能使系统调度更稳定。但注意延时太大会导致整体帧率下降。优化update()逻辑update()函数中的条件分支非常多。确保没有在循环内进行非常耗时的操作比如复杂的数学计算或文件读取。所有常量计算如CONFIG_STEP_SIZE // 2最好在初始化时完成并存储起来。监视内存CircuitPython可以通过import gc; print(gc.mem_free())来打印剩余内存。如果内存持续减少可能存在内存泄漏虽然Python有垃圾回收但循环引用需注意。确保没有在循环内不断创建新的显示对象如Bitmap、TileGrid。6.4 想移植到其他非触摸屏设备需求我只有一块普通显示屏没有触摸功能如何运行这个项目解决方案非常简单。将代码开头的USE_TOUCH_OVERLAY True改为False。这样程序会跳过所有触摸初始化和处理逻辑。猫咪将完全按照内置的AI逻辑自主行动 wandering around, sleeping, and scratching at the edges成为一个纯粹的桌面伴侣。你还可以考虑外接一个摇杆或按钮通过读取GPIO来模拟触摸事件设定猫咪的移动目标。7. 项目总结与进阶思考通过这个“Neko Kitty”项目我们完成了一次从硬件准备、环境搭建、代码解析到功能扩展的完整嵌入式图形应用开发旅程。它麻雀虽小五脏俱全涵盖了显示驱动、资源管理、状态机设计、用户交互、实时行为模拟等多个核心主题。这个项目的价值在于其清晰的架构和良好的可扩展性。NekoAnimatedSprite类是一个独立的、封装良好的精灵实体你可以轻易地在自己的项目中复用它。主程序逻辑干净地分离了显示初始化、事件循环和精灵更新这种模式可以套用到任何需要动态图形和用户交互的嵌入式应用中比如一个简单的游戏、一个数据可视化仪表盘或者一个智能家居的控制界面。更进一步思考你可以尝试多精灵管理创建多个NekoAnimatedSprite实例让一群猫咪在屏幕上互动。更复杂的AI引入“心情值”、“饥饿值”等隐藏属性影响猫咪的行为概率让它更像一个真正的电子宠物。与物理世界交互利用PyPortal的传感器如光线传感器让猫咪在环境变暗时更容易睡觉。嵌入式开发不仅仅是点亮一个LED或读取一个传感器它关乎于创造有生命力的、能与用户情感共鸣的交互体验。这个会追激光的小猫正是这种理念的一个可爱起点。希望你在实现它的过程中不仅收获了代码运行的快乐也点燃了对创造更丰富嵌入式应用的热情。