资讯动态

百元水火箭嵌入式数据采集:从传感器选型到飞行数据解析

发布时间:2026/9/10 12:09:04 来源:尧图企业网站定制
简介这是一份基于开源组件的百元水火箭设计工程面向水火箭爱好者、嵌入式开发者及科技教育场景旨在解决水火箭从结构设计、数据采集到主动控制的完整实现问题。压缩包共216个文件体积约11.5MB包含C语言源码76个头文件、71个源文件、SolidWorks三维模型25个零件图、3个装配图、STL打印模型、Python脚本、HTML页面及Keil工程配置等代码、结构与调试配置齐全可直接用于二次开发或3D打印验证。项目按三阶段推进第一阶段完成旋转自稳和加速度、温湿度、气压采集并实现试车台推力采集确定外壳设计、开伞机构和发射架方案第二阶段引入可控鸭翼、主动控制与数传功能实现箭载离线录像第三阶段重点实现图传、降低成本并做整体优化完整呈现了水火箭的迭代演进思路。目前已有336人学习下载适合想从零跟做水火箭或借鉴其低成本软硬件协同方案的开发者。资源明确声明严禁商用请仅用于个人学习研究。1. 百元水火箭的原理用开源组件把测量做到火箭上花一百块钱做一枚能飞的水火箭真正难的不是让它飞起来而是让你知道它飞了多高、姿态如何、喷射过程有多久。发射瞬间只有 0.3 到 0.6 秒整个过程从离架到落地不到 5 秒用秒表和肉眼根本抓不住有效数据。我见过很多做水火箭的朋友PVC 管和可乐瓶做得再漂亮最后都卡在同一个问题上没有任何一条可回放的飞行数据。这个项目的价值在于它并不是一个单纯的航模制作而是一个典型的低预算嵌入式数据采集原型。板子用 Arduino Nano 或者 ESP32传感器用 MPU6050 和 BMP280 这类普及程度极高的开源组件代码量不大但覆盖了传感器驱动、二进制帧协议、串口回读、离线解析的完整链路。预算全部加起来确实能压到一百元以内前提是你愿意自己焊接排针、自己处理供电和密封并且接受飞行数据可能只有几秒钟这个现实。适合动手做的人不只是玩模型的爱好者还有平时写业务代码、想碰一碰硬件数据链路的开发者。做一次这种项目你会把 I2C 时序、量程上限、数据丢帧、时间同步这些概念全部踩一遍比单纯看开发板例程要直观得多。2. 器件选型与量程匹配决定飞行数据质量的三个参数2.1 传感器怎么选先看量程再看精度水火箭的传感器选型和普通气象气球完全不同。气象气球要的是长时间、低噪声、高精度而水火箭要的是高速变化、短时冲击、抗饱和。这是两个方向选错了传感器代码写得再好也没有用。加速度计是第一个要决策的器件。MPU6050 的加速度量程最高支持 ±16g这里有个常见误区很多人以为水火箭的加速度不会超过几个 g实际上发射车架上瞬间冲击很大。当发动机舱压力达到 5 bar、水质量 300 毫升以上时离架瞬间加速度峰值可以超过 12g而如果喷嘴被异物堵住或者安装角度不正峰值冲到 20g 以上也不罕见。±16g 量程是底线如果预算允许直接用 ADXL375它支持 ±200g后面做撞击检测和开伞触发计算都有余量。气压计的选择则要考虑采样率和绝对精度之间的平衡。BMP280 的典型噪声水平在 1 Pa 左右换算成高度大约是 8 厘米这对水火箭来说完全够用。它的问题在于采样率普通模式下最高能跑到 157 Hz但实际使用中受到 I2C 总线读取耗时的影响能稳定做到 50 Hz 就已经不错。飞行全程只有 4 秒50 Hz 意味着只有 200 个采样点所以代码里不能有任何阻塞等待。陀螺仪在这里的主要作用不是测姿态——水火箭飞行时间太短姿态解算做不了什么复杂控制——而是辅助判断翻滚。喷射结束后火箭进入弹道飞行如果重心和压心设计得不好会在空中翻跟头陀螺仪的角速度积分能把这个过程忠实记录下来。MPU6050 内置 DMP 可以直接输出四元数省去自己写互补滤波的麻烦但要注意 DMP 初始化在低功耗后偶尔不成功需要在上电后加延时。2.2 主控和存储Nano 还是 ESP32主控的定义是“够用就好”。我做过两版第一版用 Arduino Nano第二版用 ESP32。Nano 的优点是便宜、稳定、烧录简单但它的 2KB SRAM 是硬约束。飞行数据如果按照一帧 20 字节、100 Hz 采样率来计算每秒产生 2KB 数据存储必须外挂串口回传在飞行中也不可能实时处理。ESP32 有 320KB 以上的可用 SRAM可以先把数据攒在内存环形缓冲里落地后再写 SD 卡或者通过蓝牙回传。从成本角度考虑其实还有第三条路用带有 SPI Flash 的 ATtiny 或者 STM32 最小系统板。不过这意味着引入更底层的寄存器操作调试时间会拉长。我的建议是第一次做就选 ESP32原因不是性能而是它的 I2C 硬件支持更好内置的 RTC 可以打时间戳后续如果你想加无线数据回传直接用内置蓝牙不用再接一个模块。存储方面SD 卡模块必须选 3.3V 供电版本5V 的能在实验室跑上了发射架之后稳压纹波一大就容易掉卡。在代码初始化里检测 SD 卡失败不应该直接 halt而是继续飞把数据留在内存里落地后再尝试落盘。2.3 供电与结构低压差才是稳定供电的关键电池选择 18650 锂电池配合低压差线性稳压器LDO输出 3.3V。千万别用 9V 叠层电池加 AMS1117 的方案9V 电池在低温下内阻飙升水火箭发射前通常要灌冰水增加气压电池温度一降整个系统直接掉电重启。AMS1117 的压差在 1V 左右输入一旦跌破 4.5V输出就不稳了。结构上有一件事必须提前做传感器舱的密封。BMP280 有一个透气孔水雾渗进去之后湿度传感器读数会漂移直接影响高度换算。我一般用热熔胶把传感器板子背面和排针根部全部封死舱体底部打一个直径 1mm 的排水孔防止水到火箭喷出之后在舱内积存。部件参考型号参考单价说明主控ESP32 DevKit约 25 元双核、内置蓝牙和 4MB Flash加速度陀螺仪MPU6050约 10 元六轴量程±16g支持 DMP气压计BMP280约 12 元精度足够功耗低采样率 50Hz存储MicroSD 模块 卡约 10 元3.3V 版本落地后写入电池18650 单节约 10 元充放电板负载到 2A 即可发射底座铝型材 电磁阀约 30 元远程泄压保障安全距离表里的总价大约在百元以内前提是你已经有电烙铁和基础工具并且愿意用 USB 线供电进行大部分地面调试电池作为外场备选电源。3. 源码结构采集、协议与地面站实现的完整链路3.1 固件状态机从待发射到回收只有四个状态水火箭飞行时间太短固件里不适合用复杂的调度器或者 RTOS 来组织代码但也不能像 Arduino 例程那样在 loop 里堆 if。我一般把整个飞行流程建模为一个四状态状态机待发射、倒计时、飞行中、落地回收。状态之间的切换条件必须依赖传感器信号而不是毫秒延时这是保证数据时间戳可靠性的关键。enum FlightState { ST_IDLE 0, // 待发射等待开始命令 ST_COUNTDOWN, // 倒计时连续记录加速度 ST_FLIGHT, // 飞行中检测离架 ST_RECOVERY // 落地数据落盘 }; FlightState state ST_IDLE; // 在 loop 中轮询状态机 void update_state() { static float last_accel 0; float accel_mag read_accel_magnitude(); switch (state) { case ST_IDLE: // 串口收到 F 后进入倒计时 if (Serial.available() Serial.read() F) { state ST_COUNTDOWN; log_time_mark(countdown_start); } break; case ST_COUNTDOWN: // 相对加速度超过 1.5g 时判定离架 if (accel_mag - 1.0f 0.5f) { state ST_FLIGHT; log_time_mark(liftoff); } break; case ST_FLIGHT: // 连续 1 秒加速度接近 1g 且海拔不再上升判定落地 if (accel_mag 0.9f accel_mag 1.1f) { static uint32_t stable_ms 0; if (millis() - stable_ms 1000) { state ST_RECOVERY; log_time_mark(landed); } } else { stable_ms millis(); } break; case ST_RECOVERY: // 执行存储写入、关闭传感器 flush_log_to_sd(); state ST_IDLE; break; } }核心逻辑说明状态迁移的条件全部写成“量变触发“而不是定时触发。”离架“判断使用加速度幅值与重力的差值这样做的好处是无论火箭立着发射还是稍微倾斜都能识别倾斜时 Z 轴的分量变化不会造成误判。落地判断加了一个 1 秒的稳定窗口避免伞还没开或者落地弹跳导致状态机反复切换。时间标记函数log_time_mark会在对应的数据帧里写入毫秒级的时间戳这个时间戳是后续离线解析阶段对齐数据的锚点。参数注意1.5g 这个离架阈值不是死的。如果你的火箭发射架有很强的导轨摩擦实际离架加速度会低于这个值需要在地面实测后微调。做法是连续采集启动瞬间的 2 秒数据取峰值的 70% 作为阈值。落地窗口的 1 秒同理如果火箭最终落到草地上弹跳幅度会很大可以适当增加到 1.5 秒。3.2 传感器读取用寄存器操作替代库函数很多现成的 MPU6050 库函数在这个场景里都不适用它们的读取间隔里加了大量的校验和延时导致采样率上不去。我在项目里只保留了两段寄存器读写逻辑一是初始化配置二是高速读取。#define MPU6050_ADDR 0x68 void mpu6050_init() { Wire.begin(); Wire.beginTransmission(MPU6050_ADDR); Wire.write(0x6B); // PWR_MGMT_1电源管理寄存器 Wire.write(0x00); // 退出休眠使用内部时钟 Wire.endTransmission(); Wire.beginTransmission(MPU6050_ADDR); Wire.write(0x1C); // ACCEL_CONFIG加速度量程配置 Wire.write(0x18); // ±16g 量程低通滤波 Wire.endTransmission(); Wire.beginTransmission(MPU6050_ADDR); Wire.write(0x1B); // GYRO_CONFIG陀螺仪量程配置 Wire.write(0x18); // ±2000dps适合高速翻滚 Wire.endTransmission(); } void mpu6050_read_accel(int16_t *ax, int16_t *ay, int16_t *az) { Wire.beginTransmission(MPU6050_ADDR); Wire.write(0x3B); // 加速度数据起始寄存器 Wire.endTransmission(false); Wire.requestFrom(MPU6050_ADDR, 6); // 高位在前大端排列按寄存器顺序读取 *ax ((int16_t)Wire.read() 8) | Wire.read(); *ay ((int16_t)Wire.read() 8) | Wire.read(); *az ((int16_t)Wire.read() 8) | Wire.read(); }这段代码的逻辑说明MPU6050 的电源管理寄存器是 0x6B写入 0x00 退出休眠这里没有额外配置时钟源用内部振荡器在短时间飞行中够用。加速度量程配置写在 0x1C0x18 的二进制是 0b11000对应±16g 且开启了数字低通滤波能够滤掉部分机械振动噪声。读取时为什么从 0x3B 连读 6 个字节而不是单寄存器轮询——因为 I2C 每次事务都有起始位和停止位连续读只付一次开销在 400kHz 时钟下可以显著减少总线占用。数据返回是大端格式所以必须做移位拼接直接用 memcpy 在 Arduino 平台上可能因为字节序问题得到错误结果。BMP280 的读取稍微复杂一些它需要先用 0x88 地址读出一组校准系数然后才能把原始气压 ADC 值换算成真实的 Pa 数值。换算公式在网上能查到但一个容易踩的坑是校准系数数组用的是有符号还是无符号整型——温度校准的 5 个系数全部是有符号气压校准的 9 个系数中最后两个也是写错了高度曲线会漂移几百米。3.3 下行数据协议二进制帧比 JSON 更适合存储外场环境下串口波特率通常设定在 115200 或更高这个速度下传 JSON 完全可行但飞行时数据是从内存环形缓冲写入 SD 卡不是走串口所以帧格式要考虑的是编码效率和排错便利而不是“人类可读”。我在这个项目里定义了一个 20 字节的固定长度帧// 帧格式定义共 20 字节 typedef struct { uint8_t sync; // 0xAA 帧同步字 uint8_t state; // 状态机当前状态 uint16_t seq; // 帧序号用于检测丢帧 uint32_t timestamp; // 毫秒时间戳 int16_t ax, ay, az; // 加速度原始值 int16_t gx, gy, gz; // 角速度原始值 int32_t pressure_q; // 气压原始 ADC 值 uint16_t crc; // CRC16 校验 } flight_frame_t;固定长度的好处是地面站解析时不需要处理粘包和断包每次读取 20 字节后先检查同步字和 CRC不合格直接丢弃该帧并递增错误计数。CRC 实现用查表法代价是 256 字节的 Flash对 ESP32 来说无所谓。CRC 校验在这里的作用不只是查错。水火箭落地时冲击很大SD 卡在写入过程中如果恰好发生断电或者卡松动最后一个扇区多半是坏的有 CRC 之后地面站可以直接跳过损坏帧不影响前后数据的连贯性。地面站部分用 Python 写逻辑很简单读取串口或二进制文件按帧格式解析后转换成 CSVimport struct import csv def parse_frame(raw: bytes): # 固定 20 字节一帧前 5 字节分别对应 sync/state/seq if raw[0] ! 0xAA: return None sync, state, seq, ts struct.unpack(BBHI, raw[0:8]) accel struct.unpack(3h, raw[8:14]) gyro struct.unpack(3h, raw[14:20]) return { timestamp_ms: ts, ax: accel[0], ay: accel[1], az: accel[2], gx: gyro[0], gy: gyro[1], gz: gyro[2], } def file_to_csv(src: str, dst: str): with open(src, rb) as f, open(dst, w) as out: writer csv.writer(out) writer.writerow([timestamp_ms, ax, ay, az, gx, gy, gz]) while True: chunk f.read(20) if len(chunk) ! 20: break frame parse_frame(chunk) if frame: writer.writerow([frame[timestamp_ms], frame[ax], frame[ay], frame[az], frame[gx], frame[gy], frame[gz]])这段脚本的含义是这样解析首字节检查同步字避免把文件中间位置当成帧起始点struct.unpack使用小端模式和底层 C 结构体保持一致加速度和角速度目前保留原始量纲需要除以各自的量程系数才能得到重力加速度和度每秒。这一步没有做死是刻意留到分析阶段的因为原始值可以在之后重新换算而一旦缩放成物理单位丢掉了整数部分的原始精度就再也回不来了。4. 实现过程中的坑从电磁阀干扰到数据削顶的排查4.1 发射架电磁阀干扰烧掉两次数据后我才加了光耦隔离这个项目的发射底座用电磁阀作为远程泄压开关12V 供电通过串口命令控制开阀。第一次外场测试时电磁阀通电的瞬间ESP32 的 I2C 总线直接锁死传感器读回来的数据全是 0xFF整帧数据完全不可用。问题的根源不是电磁阀本身而是它的线圈在断电瞬间会产生很高的反向感应电动势这个尖峰脉冲通过共用的电源地线耦合进信号线。在实验室里用示波器能看到地线上的毛刺超过 3VI2C 的高电平阈值在 3.3V 系统中只有 2.5V 左右每一次电磁阀动作都是一次总线复位信号。修复方案是两层第一层是在电磁阀两端并联一个反向续流二极管吸收断电尖峰第二层是让发射端的控制器和传感器舱之间的供电完全隔离用光耦传递开关信号。前者是硬件改造后者是更干净的工程方案。如果你的发射架和传感器舱通过长电缆连接强烈建议直接走光耦成本不到两块钱换回来的是一条不会被干扰的总线。现场判断电磁阀干扰的方法很简单。给系统上电后不打开传感器电源只让电磁阀连续开关十次然后把传感器接上读一段空白数据检查 CRC 错误率。如果错误率不是零说明干扰路径没有彻底切断排查方向不是换传感器而是查电源和地线走线。4.2 加速度削顶量程设成±16g 还是会在某次发射中失效我前面提到量程要设置成±16g但实测中仍然遇到过削顶。那一次发射的喷嘴用了口径小一档的塑料瓶盖水喷射速度更快反冲力更猛点火瞬间读到的加速度原始值打到满幅 32767 并保持了几毫秒。满幅不是正常的峰值而是传感器量程上限截断点——你看到的是一个平顶矩形波而不是真实的冲击曲线。削顶问题在代码层面有两个补救思路。第一是启动时做一次自检通过串口读取全量程范围内的噪声基底如果噪声明显偏高说明传感器安装处的机械共振太强需要加缓冲垫而不是改代码。第二是建立削顶检测规则在帧解析阶段直接把 ax、ay、az 任一值等于 ±32767 的帧标记为CLIPPED后续分析峰值加速度时自动排除这段数据避免把截断值当真值去计算。更根治的办法是换用 ±100g 或者 ±200g 的加速度计但这会增加预算和体积。如果你的水火箭设计成可复用多次并且你对不同喷嘴下的推力曲线感兴趣那么换 ADXL377 是值得的如果只是一次性验证飞多高±16g 加削顶标记足够。4.3 数据搬到 SD 卡时的时序陷阱中断与写卡抢占ESP32 的 Arduino 内核是跑 FreeRTOS 的SD 卡写入如果使用默认的 SPI 模式会在写操作期间长时间占用总线。而传感器读取走的是 I2C如果两者共用同一个硬件 SPI 总线写卡的一个扇区操作会造成传感器数据采集的长时间暂停。这个坑在日志里表现为时间戳出现几百毫秒的断层而帧序号是连续的排除丢帧后很难定位。我把 SD 卡写入挪到了ST_RECOVERY阶段一次性完成飞行中只往内存环形缓冲写数据。环形缓冲区的大小用 ESP32 的 320KB 可用内存分配 256KB按照每帧 20 字节、100 Hz 采样率计算可以存下 128 秒的数据远超实际飞行时间。这样做彻底避免了飞行中写卡带来的时序抖动代价是如果火箭坠毁后模块损坏内存里的数据也就丢了。作为折中也可以每 5 秒写一次缓存但考虑到水火箭飞行只有几秒一次性写是更简单的方案。4.4 传感器标定与地面验证不等于做一次就算成功传感器装在火箭舱里和放在桌面上读出的加速度零漂完全不同因为焊接应力会让 MEMS 传感器的零点发生偏移。所以在装舱后必须做一次六位置标定至少做三轴正反六个朝向记录每个面的加速度读数均值计算出偏移量写进配置项。地面验证最基础的是落体实验。把组装好的传感器舱从 1 米高度平抛出去读取加速度曲线理论上自由下落阶段三个轴的合加速度应该接近 0g触地瞬间会有一个明显的冲击峰。如果你的曲线在下落过程中显示 1g说明传感器还受安装方向的约束需要检查是否绑得太紧、轴向被外力拉偏了。这个实验成本极低但能排查掉传感器焊接、方向定义、姿态解算初始化三个层面的问题。5. 飞行数据离线解析用时间戳对齐还原火箭的完整姿态数据落盘之后最后一步是把几十秒的原始数据变成有意义的曲线。这里有一个经常被忽略的细节传感器数据和事件标记的时间基准必须统一。固件里所有时间都来自millis()而地面站的解析脚本里如果把帧序号乘以 10 毫秒当时间戳用遇到偶尔丢帧就会让整条曲线后半段逐渐错位。正确做法是直接用帧内的timestamp字段做 X 轴丢帧的位置在曲线上自然形成缺口而不是错误地对齐到错误时刻。具体分析时我一般会分三段看曲线。第一段是倒计时到离架重点看气压曲线是否平稳、加速度是否有异常预震第二段是喷射段从离架标记到高压气体排空气压快速下降加速度出现第一个大峰第三段是弹道段和回收段气压变化趋于平缓加速度回到 1g 附近。把三段画在同一张图上配合事件标记竖线整个飞行的物理过程就一目了然了。关于喷射结束时刻的确定可以用气压计原始值的差分来计算。BMP280 在飞行中记录的压力下降率在喷射阶段显著高于弹道阶段通过计算一阶差分并找到斜率突变点就能比较可靠地确定切水结束的时刻def find_jet_end(pressure_list, time_ms): # 计算气压差分取绝对值 delta [abs(pressure_list[i] - pressure_list[i-1]) for i in range(1, len(pressure_list))] # 找出差分值下降到首段峰值 20% 以下的时间点 threshold max(delta) * 0.2 for i, d in enumerate(delta): if d threshold: return time_ms[i] return None这个算法的逻辑是喷射段因高压气体快速排出气压每秒下降可达上百帕进入弹道段后气压只随高度变化变化率骤降。取首段峰值的 20% 作为切换阈值比较稳但不同喷嘴直径和发射压强下的曲线形态不一样建议你把自己实测的那次数据跑一遍把阈值调到能看到明显台阶的位置。喷射结束时刻配合加速度峰值能算出喷水效率这是我们自己评估火箭设计是否合理的重要指标。姿态维度上DMP 输出的四元数可以直接转成欧拉角但转之前记得对 Z 轴做方向校正。水火箭发射时有一定倾斜角如果传感器安装坐标系和火箭坐标系没有对齐那么横滚角的绝对数值没有参考意义只有相对变化量可以用于判断火箭是否发生了翻转。判断是否翻转更直接的办法是看角速度的 Z 分量有没有突变的符号翻转这比欧拉角更灵敏。数据解析脚本建议直接写到 Jupyter Notebook 里保存为模板每次外场回来只需要替换文件路径就能生成完整的图表。把加速度幅值、气压高度、角速度 Z 轴三条曲线打印在同一张图里事件标记用竖线画出每次发射后花五分钟看一遍图就能替代过去用肉眼看轨迹和秒表掐时间的全部工作。这套脚本本身就是项目开源源码的一部分比传感器驱动代码的使用频率高得多。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价