把一滩水搬到行空板K10上:FluidBox 移植实录

2026-10-117

前言

初期看到这个项目,我就被惊艳到了:一块没有 GPU、没有浮点加速器、连整帧显存都放不下的 ESP32-S3 小掌机,屏幕里居然养着一滩会晃、会溅、会沉底的 3D 流体——倾斜板子,水就跟着流;甩一下,水花撞上壁面再荡回来。画面的流畅程度完全不像这块芯片该有的水平。

第一反应是好奇:到底使用了什么魔法,才能在几百 KB 内存、240 MHz 主频的芯片上得到如此流畅的效果?

带着这个问题把源码通读了一遍,答案逐渐清晰。看懂之后,头自然冒出来:这么好的东西,能不能搬到更多人的板子上?于是有了这次移植,也就有了这篇文章。

把一滩水搬到行空板K10上:FluidBox 移植实录_image_1.webp

https://github.com/V4C38/esp32-fluidbox


一、移植的目的与意义

原项目本身运行稳定,做这次移植并不只是简单把 Demo 在新硬件跑通。原 FluidBox 项目深度绑定特定进口掌机,硬件获取门槛较高,不少感兴趣的开发者只能观看演示视频,而 DFRobot K10 在国内教育和创客社群保有量很大,同样搭载 ESP32‑S3,移植之后普通爱好者只需一条烧录命令就能运行这套粒子流体效果,让开源项目落地到更多人的开发板上;同时这次移植也是对项目宣称的 “算法与硬件完全解耦” 架构最严苛的实测,更换屏幕、移除陀螺仪、重写全部按键输入后,sim.c、render.c 实现零改动,印证前期架构设计的有效性;除此之外,整个移植过程沉淀下来的硬件差异梳理、BSP 复用、IMU 标定、受限硬件下图形方案取舍这一套可复用工作流,也可以给后续其他物理仿真、小游戏、可视化项目的跨板移植提供参考。

本文记录完整移植过程,聊清楚哪些代码必须修改、哪些逻辑绝对不能动,还有调试周期里最棘手的坐标轴标定问题。本文面向 ESP32 创客与嵌入式开发者,文中包含代码和数学公式,尽量侧重工程实践思路,即便不写代码,也能读懂一套完整的嵌入式项目移植工作方法。


把一滩水搬到行空板K10上:FluidBox 移植实录_image_2.webp

二、盘点差异,分点击破

动手改代码,切忌直接打开工程,遇到报错就到处修补。第一件事,整理两块开发板全部外围硬件差异,逐项处理验证。

源项目 xiaocheng‑fluidbox 运行在 Waveshare ESP32‑S3 掌机,一共 400 个流体粒子,双核分工,核 1 负责流体求解,核 0 执行分条 DMA 屏幕渲染。
目标硬件 DFRobot K10,主控同样是 ESP32‑S3(16MB Flash / 8MB PSRAM),但是所有外设全部不同。

部件 原板(waveshare) 目标板(DFRobot K10) 是否需要修改
屏幕 ST7789(AMOLED 排线) ILI9341(SPI,竖屏物理贴装) ✅ 更换面板驱动
IMU 传感器 加速度计 + 陀螺仪 SC7A20H,仅三轴加速度计 ✅ 移除陀螺依赖,软件估算角速度
LCD 上电 / 复位控制 普通 GPIO 直驱 TCA9555 IO 扩展器输出 ✅ 新增 I2C 扩展器上电时序
LCD 硬件复位引脚 物理引脚可用 无硬件复位脚 ✅ 改用寄存器软件复位
背光控制 GPIO 可控开关亮度 无硬件控制脚,永久常亮 ✅ 删除背光控制逻辑
按键输入 独立 PWR 电源按键 TCA9555 扩展的 10 键矩阵键盘 ✅ 重写按键扫描、消抖逻辑
I2C 总线引脚 板载固定引脚 SDA=47 / SCL=48,速率 400kHz ✅ 修改 I2C 硬件引脚配置
流体求解 sim.c — — ❌ 一行代码未改动
粒子渲染 render.c — — ❌ 一行代码未改动
双核任务调度 main.c — — ❌ 仅改动外设初始化片段

全部硬件层面改动,收敛在 main/config.h 的板级配置段。仿真参数、物理常数、渲染逻辑和底层硬件彻底隔离开。移植本质是替换硬件抽象层,而不是重写上层业务。


三、移植思路

整理完硬件差异,在移植之前,要确定屏幕画面输出方案。ESP32 做图形开发一般有三条路径:Arduino 图形库、LVGL GUI 框架、乐鑫原生 esp_lcd API 自行实现渲染。

1. 为什么放弃 LVGL?

移植的初期,尝试过保留LVGL漂亮的界面,最终导致全面失败,耽搁了好一段时间,差点失去复刻的兴趣。
LVGL 擅长处理按钮、列表、弹窗这类复杂交互 UI。但本项目没有任何控件,只有 400 个粒子持续输出 60Hz 动画。LVGL 的场景图、脏区刷新、布局计算都会产生额外开销,对流体动画没有实际作用。

显存压力更加棘手,ILI9341 屏幕 320×240,RGB565 的完整帧缓冲就要 150KB。ESP32‑S3 的内部 SRAM 总共只有三百多 KB,仿真、任务栈、系统协议栈都要占用高速内存。单一个帧缓存就占掉一半 SRAM,方案不可行。

PSRAM 的访问速度远低于内部 SRAM,流体仿真的热路径绝对不能放在 PSRAM 上运行。

把一滩水搬到行空板K10上:FluidBox 移植实录_image_3.webp

2. 为什么不用 TFT_eSPI 这类 Arduino 图形库?

TFT_eSPI 默认维护一整块完整帧缓存再推送到屏幕,同样会遇到 150KB 的内存瓶颈。就算开启分块缓冲模式,库自带的画圆、填充等通用绘制逻辑,和项目现有的分条 DMA、像素直写的渲染链路完全两套体系。我们移植的核心是流体算法,没必要额外引入一套通用图形栈。

3. esp_lcd 原生接口 + 自绘渲染,才是适配这个项目的形态

esp_lcd 只做很薄的一层封装:把一段内存缓冲区的数据输出到屏幕指定矩形区域。

esp_lcd_panel_draw_bitmap(panel, x0, y0, x1, y1, buf);

缓冲区内部像素内容完全交给开发者自己实现。

项目内部的渲染逻辑,包含针孔投影、颜色 LUT 查表、圆盘 Span 填充,直接向 320×16 的窄条缓冲写入像素,写完直接交给 DMA 搬运刷新,不存在多余图形抽象层,开销压到最低。


厘清我们的工作方向后,如果白手起家,难度也是挺大的,最好你有适当的研究基础,才下手。

  1. 构建好可用的 ESP-IDF 环境——装好对应版本(本项目用 ESP-IDF 6.1.0),idf.py build 能在命令行完整跑通,能正常烧录、能开 monitor 看日志。环境没调通之前,任何方案讨论都是纸上谈兵;
  2. 有一个点亮屏幕的基础工程——从最简单的例程起步:初始化 SPI 总线 + 面板驱动,往屏幕上刷一块纯色。这个"Hello Screen"工程是后面所有工作的试验台:上电时序对不对、引脚配置对不对、像素时钟稳不稳,都在这一步验证。

把一滩水搬到行空板K10上:FluidBox 移植实录_image_4.webp

Agent 友好开发范式--以行空板 k10 为例

行空板 K10 扩展板 ESP‑IDF 完整 BSP 驱动开源实践

在这两篇基础参考文章基础上继续推进我的项目,似乎就没那么难了。

四、分点击破

差异清单里的每一项,按"先让屏幕亮,再让输入通,最后对齐传感器"的顺序逐项击破。

击破点①:先让屏幕成功点亮

ILI9341 有官方维护组件 espressif/esp_lcd_ili9341,在idf_component.yml添加一行依赖就可以引入驱动。真正耗时间的,是 DFRobot K10 这块板子特殊的上电时序。

1. 藏在 IO 扩展器里面的上电脉冲

K10 的 LCD 电源不受普通 GPIO 控制,由 I2C 接口的 TCA9555 16 位 IO 扩展芯片的 P0.0、P0.1 引脚控制。屏幕上电必须严格执行对应的脉冲时序,跳过这一步,无论怎么写寄存器,屏幕始终黑屏。

// TCA9555 P0.0/P0.1 —— LCD上电脉冲时序掩码
#define TCA_PIN_PULSE_MASK 0x03

移植有一个很实用的经验,不要凭空猜测时序。优先查阅厂商官方 BSP 代码。这里直接参考 K10 官方示例 esp‑claw 里 setup_device.c 的复位序列,省去大量反复试错。

2. 没有硬件复位脚、没有背光控制脚

没有物理复位引脚,无法电平操作复位屏幕面板,改用 ILI9341 自带的软件复位寄存器完成复位;硬件没有背光控制引脚,屏幕始终常亮,直接删掉背光渐变的相关任务。

3. 竖屏面板 swap_xy,埋下坐标系大坑

这块 ILI9341 面板物理是 240×320 竖屏贴装,项目需要横向输出画面,代码开启坐标交换:

esp_lcd_panel_swap_xy(panel, true);

写这行代码的时候看不出问题,它只是旋转屏幕输出。但是流体仿真内部坐标系没有同步跟着旋转,这个细节,给后续坐标轴调试埋下很大隐患。


击破点②:保留按键重置流体的行为

原版掌机按下 PWR 电源键,流体全部重置。K10 的所有按键全部挂在 TCA9555 扩展器上,属于 10 键矩阵键盘,低电平有效。

拆解实际需求,我们并不限定某一个特定按键,只需要一个外部触发信号来重置流体模拟。逻辑可以大幅简化:读取扩展器 port1 的按键掩码,任意按键拉低就判定按键按下,叠加 50ms 软件消抖,一百多行 button.c 就实现全部功能。

移植不要死板复刻旧硬件的实现细节,优先抓住底层行为需求。旧硬件需要指定按键,新板子只需要任意输入事件,代码完全可以做减法。


击破点③:丢掉陀螺仪,旋涡效果怎么救回来?

原设备 IMU 同时带加速度计与陀螺仪,甩动板子产生旋涡,直接读取陀螺仪输出的角速度。
K10 搭载的 SC7A20H 只有三轴加速度计,没有陀螺仪。第一反应,旋涡功能只能直接砍掉。

但通过数学推导,可以复原大部分旋涡效果。加速度计测不出绕重力轴的自转,却可以持续拿到重力向量 \hat{g}。刚体旋转状态下重力向量满足:
\frac{d\hat{g}}{dt} = \omega \times \hat{g}
等式两边左叉乘重力单位向量 \hat{g},就可以解算出垂直重力方向的角速度分量:
\omega_\perp = \hat{g} \times \frac{d\hat{g}}{dt}

板子抬头、低头、左右倾斜,这类改变重力朝向的旋转动作,都可以通过加速度的微分反推角速度。有一个客观局限:拿着板子绕竖直重力轴原地转动,这个运动原理上无法观测,对应角速度只能固定为 0,这点硬件局限需要接受。

微分运算会放大传感器噪声,项目增加三层参数做噪声抑制:

参数 功能说明
GYROLESS_GAIN 总增益开关,置 0 直接关闭旋涡模拟
GYROLESS_OMEGA_LP_HZ 估算角速度后,经过 6Hz 低通滤波,压制高频噪声
GYROLESS_SHAKE_GATE_G 0.35g 甩动门限;剧烈晃动时重力观测失效,角速度做半衰衰减,而不是直接清零

为什么选择半衰而不是直接清零?旋涡属于物理惯性量,如果瞬间清零,流体视觉效果会非常生硬,逐步消散更符合真实观感。

移植不等于照搬功能清单,读懂每个参数背后的物理逻辑,才能在硬件变更时做出合理取舍。

这里还有意外收获:原版带陀螺仪时,经常要处理赝矢量跟随屏幕镜像翻转带来的符号错误。现在角速度直接在仿真坐标系内部完成估算,镜像变换带来的符号问题直接消失,去掉硬件传感器,反而解决一类难缠 bug。


五、坐标轴映射

整个移植过程,调试耗时最多的不是屏幕、不是按键,是 IMU 坐标轴映射。表面看只是三行宏,背后横跨三层坐标系,任意一层方向、符号出错,倾斜板子水流方向就会错乱。

①芯片传感器坐标系(ax/ay/az) → ②仿真坐标系(x向右/y向下/z穿入壳体) → ③屏幕显示坐标系
        由芯片贴片硬件决定        IMU_MAP_*映射宏        swap_xy / mirror镜像

项目这里有一处隐蔽陷阱:render.c 的粒子投影逻辑本身不带坐标旋转,而 display.c 开启了 swap_xy(true)。叠加之后,仿真内部 X 轴,对应屏幕画面竖直方向。

人的直觉习惯屏幕上下对应 Y 轴,如果跟着直觉来回修改正负符号,怎么调效果都是错的。

经过多轮上板实测标定,最终得到正确映射,写在main/config.h:

#define IMU_MAP_X(ax, ay, az) (ay)    // 屏幕【竖直轴】 ← 芯片 ay
#define IMU_MAP_Y(ax, ay, az) (ax)    // 屏幕【水平轴】 ← 芯片 ax
#define IMU_MAP_Z(ax, ay, az) (az)    // 壳体深度轴 ← 芯片 az

三组映射全部直接取用原始数值,没有取反;重力反向处理已经写在imu.c内部 dx = -s_lp,映射层不再叠加负号,做到职责分离。

调试坐标轴,不要依靠观察水面现象猜正负。打开main.c的AXIS_CALIB_LOG串口打印开关,直接输出传感器原始读数。把板子摆到四个典型姿态,每种姿态静止 3 秒,就可以反解完整坐标轴映射,完整数学推导参考仓库文档 docs/IMU_AXIS_MAPPING.md。


六、流体力学模拟:超纲了

前面移植工作介绍完毕。sim.c八百五十行代码是整套流体的物理内核,全程没有改动。看完这一节就能明白:仿真代码完全不关心自己跑在哪块板子上,只接收外部传入的重力、角速度向量。

1. 水不是算力,是算粒子位置

现实水体分子数量极其庞大,嵌入式设备不可能逐个模拟。本项目用 400 个粒子代表一小团团水体,算法是 PBF Position‑Based Fluids,基于位置的流体。

这套算法思路和传统物理模拟不太一样:优先修正粒子位置,而不是一步步计算受力。

传统仿真流程:计算合力→积分加速度→积分速度→更新位置,每一步都会不断累积计算误差。

PBF 的处理流程,先用惯性预测粒子下一时刻位置;之后校验粒子之间是过于拥挤还是过度分散,直接修正粒子坐标,再反向推导出速度。这套逻辑稳定性很强,就算大幅度甩动板子,也不会出现粒子飞散爆炸。

2. 高效寻找邻居:格子划分 + 计数排序

流体只和附近粒子产生相互作用,如果两两暴力计算距离,400 粒子每帧要做 16 万次运算,MCU 负担太重。

解决方案,把仿真空间切分为 15×12×3 = 540 个空间格子,每个粒子只检索自身格子和周边 26 个相邻格子。

做完格子检索之后,再执行计数排序,三轮 O (n) 循环把粒子按照格子编号在内存重新排布。这一步带来三重收益:

  1. 同一格子粒子内存连续,CPU 缓存命中率明显提升;
  2. 相邻格子的数据在内存地址上大多靠近,硬件预取可以生效;
  3. 格子编号以 z 深度为主序,排序完成天然按深度有序;渲染反向遍历,直接实现从后往前绘制。

在缓存只有几百 KB 的 MCU 上,内存排布优化带来的性能提升,很多时候比算法本身修改效果还要明显。

3. 一聚一散,成就液态效果

通过平滑核函数算出每个粒子的局部密度,分离两种压力项:

// 普通压力,可正可负
press = K_PRESSURE * (density - rest_density);
// 近距排斥压力,永远大于0
press_near = K_NEAR_PRESSURE * density_near;
符号 作用
普通压力 有符号 密度过高推开粒子,密度过低拉近粒子;水面张力属于系统涌现行为,不需要手写表面张力公式
近压力 恒正 只做排斥,避免多个粒子互相堆叠

普通压力负责把水体聚拢,近压力负责粒子互相弹开,两项平衡,才能呈现出水的视觉效果。

4. 看不见的物理容器

屏幕画面看不到容器边框,但物理模拟内部存在一个圆角盒子。边界检测没有写大量 if‑else 区分墙面、拐角,统一用一套数学处理逻辑。
把粒子坐标夹持到圆角圆心构成的内矩形,剩余偏移指向轮廓最近点;x、y 压缩为径向坐标,再在径向‑深度平面再做一次夹持,一套逻辑处理全部边界几何形态。

5. 旋转参考系:三种伪力带来真实晃动手感

倾斜、甩动板子,流体处于加速旋转的非惯性参考系。除重力之外,还需要叠加三种伪力:

acc += gravity;                              // 重力
acc += ROTATION_GAIN * (r * w2 - ...);       // 离心力:快速甩圈水贴向容器边缘
acc -= ROTATION_GAIN * (alpha × r);          // 欧拉力:板子急停,水体产生切向冲劲
acc -= ROTATION_GAIN * 2.0f * (omega × v);   // 科里奥利力:旋转参考系内粒子偏转

只单纯施加重力,晃动板子水会像一堆硬质颗粒生硬摆动。三种伪力一起参与运算,才能得到贴近真实的晃动手感。

6. 时间缩放:慢放真实物理

#define TIME_SCALE 0.100f    // 时间缩放系数,0.1倍速运行

现实中水运动速度很快,当前模拟盒子的尺寸条件下,重力会让水几毫秒就撞击盒底,人眼看不清流动细节。项目直接把全局时间放慢十倍。全部物理常数维持真实数值,只拉长时间尺度。

对比调小重力:修改重力会直接改变流体本身特性;时间缩放只是播放慢动作,所有物理关系完整保留。


把一滩水搬到行空板K10上:FluidBox 移植实录_image_5.webp

七、烧录、验收

移植合格标准不是屏幕有水跑起来,核心验收条件:流体运行行为和原版硬件保持一致。

粒子数量、平滑半径、压力系数、时间缩放,全部沿用原项目仿真参数。两块板子做相同倾斜、甩动操作,水面涟漪表现应当完全一致,这就是软硬件解耦带来的好处。

工程版本存在差异,原项目基于 ESP‑IDF 5.5;本次 K10 移植版本使用 ESP‑IDF 6.1.0。

编译烧录命令:

idf.py set-target esp32s3
idf.py build
idf.py -p <你的串口设备> flash monitor

sdkconfig.defaults里面几项配置直接影响流体热路径性能,需要逐项核对:

CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y   # CPU拉满240MHz,仿真需要充足算力
CONFIG_FREERTOS_HZ=1000                 # RTOS节拍1000Hz,保证仿真渲染时序精确
CONFIG_SPIRAM=n                         # 仿真热路径完全运行在内部SRAM,不走PSRAM
CONFIG_COMPILER_OPTIMIZATION_PERF=y     # 开启性能级别编译优化

串口每两秒输出诊断日志,包含帧率、流体求解分段耗时、传感器原始数据。判断移植有没有完整复现原版行为优先看串口数值,不要只靠肉眼观察屏幕画面。


把一滩水搬到行空板K10上:FluidBox 移植实录_image_6.webp

写在最后:移植完成不是终点

必须承认,把项目成功移植到 K10 上,看着水流在另一块板子的屏幕里晃出同样的涟漪,是挺有成就感的。但这份成就感是短暂的——烧录成功的那个瞬间,成就感就开始贬值了。

真正的意义不在"跑起来了",而在往深处走:

  1. 吃透流体模拟的实现原理。 位置约束为什么比力的积分更稳定?双密度松弛里一聚一散怎么平衡?计数排序为什么能一石三鸟?这些设计背后是计算物理和实时系统的交叉知识,值得逐行读懂 sim.c,甚至动手改参数、破坏它、再修好它。
  2. 理解整个项目的构思细节。 无陀螺仪怎么用数学救回角速度、没有帧缓冲怎么把 DMA 藏进渲染循环、圆角容器怎么用两次夹持零分支解决——这套"在资源上限下做取舍"的思维方式,才是能带走的东西。下次面对的不是流体,可能是任何受约束的实时系统,思路是相通的。
  3. 把 K10 实现当作随手可得的实验平台。 这次移植真正的产出,是在一块保有量很大的板子上搭好了一个可视化的沙盒:调参立刻能看到画面变化,串口日志随手可读。有了它,学习流体力学、验证新想法、做自己的物理仿真实验,都不再需要任何门槛。

移植是手段,平台是载体,深入理解原理才是目的。 屏幕上那滩水跑起来只是开始——愿你把它拆开、改坏、再修好,从"它能动"走到"我懂它为什么能动"。


📎 项目代码仓库:https://gitee.com/genvex/dfk10-fluidbox

可以直刷固件体验:

硬件清单

创作许可协议

本项目采用 CC BY(署名) 进行许可。

所需材料/产品

加载全部

    行空板K10 AI智能体编程主板,支持ESP-Claw/DeepSeek/MCP/边缘计算

    行空板K10 AI智能体编程主板,支持ESP-Claw/DeepSeek/MCP/边缘计算

    行空板K10 AI智能体编程主板,支持ESP-Claw/DeepSeek/MCP/边缘计算

相关推荐

评论(0)
- 没有更多了 -

创作许可协议

本项目采用 CC BY(署名) 进行许可。

相关推荐

所需材料/产品

行空板K10 AI智能体编程主板,支持ESP-Claw/DeepSeek/MCP/边缘计算

行空板K10 AI智能体编程主板,支持ESP-Claw/DeepSeek/MCP/边缘计算

行空板K10 AI智能体编程主板,支持ESP-Claw/DeepSeek/MCP/边缘计算