1. 项目概述当小船遇上水质检测与触屏遥控如果你对电子制作和开源硬件有点兴趣大概率听说过Arduino。它就像电子世界的乐高积木让普通人也能轻松搭建出各种有趣的智能设备。今天要聊的这个项目就是把Arduino、无线通信、传感器和一个小船平台结合起来做成一艘能实时检测水质、还能用触屏遥控的智能监测船。想象一下你有一个小池塘、一段景观河道或者只是想监测一下家里的鱼缸水质总不可能每次都划船或者伸手去测吧这艘小船就能派上用场。它本质上是一个移动的水质监测平台核心功能有两个一是通过搭载的传感器实时读取水体的关键参数比如酸碱度、浊度、溶解氧等二是通过无线模块将数据传回岸上的控制端而你可以在一个带触摸屏的控制器上像玩遥控车一样操控它去你想测的地方同时屏幕上实时显示水质数据。这个项目听起来复杂但拆解开来就是几个成熟模块的组合拳Arduino作为大脑负责逻辑控制TFT Shield提供本地显示和触控界面APC220这类无线数传模块负责建立船与岸的通信链路再加上相应的水质传感器和电机/舵机驱动小船。它融合了嵌入式开发、无线通信、传感器技术和简单的机械结构是一个综合性很强的DIY项目非常适合有一定动手能力的爱好者、学生或者相关领域的入门者来实践不仅能学到东西做出来的成品也相当有成就感。2. 项目整体设计与核心思路拆解2.1 为什么选择“船”作为载体首先得明确我们做的是“水质检测”目标环境是水体。固定式的监测站虽然稳定但部署成本高且只能获得单点数据。而无人船USV方案的优势在于其移动性和灵活性。你可以遥控它到达池塘、湖泊、河道的不同位置甚至沿着预设航线巡航从而获取一片水域的水质分布图这对于环境评估、水产养殖监测来说价值远高于单点数据。对于DIY级别我们不需要复杂的船体设计和昂贵的推进器。一个现成的双电机遥控船模平台就足够了它的任务就是承载我们的电子系统在水面平稳航行。选择船模时重点考虑其载重能力要能放下Arduino主板、传感器、电池等、密封性电子设备怕水以及双电机差速转向的灵活性。2.2 系统架构一分为二无线互联整个系统自然分为两大部分水上移动端船体和岸基控制端遥控器。两者之间通过无线模块进行双向通信。水上移动端船主控核心一块Arduino Uno或类似的板子如Nano负责读取所有传感器数据控制电机驱动模块来指挥小船前进、后退、转向并通过无线模块发送传感器数据、接收控制指令。感知模块水质传感器套件。常见组合包括pH传感器测酸碱度、TDS/电导率传感器测溶解性固体总量间接反映纯度、浊度传感器测清澈度。更专业的还可以加溶解氧传感器但成本较高。所有传感器需要做好防水处理仅将探针部分浸入水中。执行模块电机驱动板如L298N、TB6612FNG用来驱动船体的两个直流电机实现差速转向。通信模块一对APC220无线数传模块中的一个设置为透明传输模式与岸上配对。电源模块一块大容量锂电池如11.1V 2200mAh航模电池为整个系统供电可能需要通过降压模块如LM2596为Arduino和传感器提供5V或3.3V电压。岸基控制端遥控器主控与显示核心另一块Arduino Uno搭配一块TFT LCD Shield触摸屏扩展板。这块板子直接插在Arduino上提供了显示屏和触摸功能是我们的人机交互界面。交互模块在TFT屏幕上我们需要绘制一个遥控界面虚拟摇杆或方向按钮来发送控制指令同时开辟区域实时显示从船上传回的水质数据数值、曲线等。通信模块另一块APC220模块与船上的模块配对实现双向数据传输。通信协议设计思路为了在一条无线链路上同时传输控制指令和传感器数据需要定义一个简单的串口通信协议。例如可以规定岸端发送的指令以特定字符开头如“C:100,80”表示左电机速度100、右电机速度80船端回传的数据以“D:”开头后面跟上pH、TDS等数值用逗号分隔如“D:7.2,350,12.5”。双方的程序都按照这个格式进行解析。2.3 核心器件选型背后的考量Arduino Uno vs 其他板子Uno接口标准、资料丰富、生态完善是入门和教学的首选。如果考虑功耗和体积Nano更小如果需要更强的处理能力或更多IO口Mega也行。但对于本项目Uno的资源和性能完全足够。TFT Shield的选择市面上常见的2.4寸、2.8寸、3.2寸电阻屏或电容屏Shield都可以。电阻屏成本低用触笔或指甲操作电容屏体验更好。关键是要确认其驱动库如Adafruit_GFX, Adafruit_ILI9341等在Arduino IDE中易于安装和使用并且有足够的示例代码来绘制UI。无线模块为什么是APC220APC220是一款经典的ISM频段433MHz无线数传模块传输距离在空旷地带可达1000米以上完全满足小范围水域监测。它采用透明传输编程时就像操作一个额外的串口Serial1或SoftwareSerial非常简单。相比于蓝牙距离短、Wi-Fi功耗高、复杂APC220在距离、功耗和易用性上取得了很好的平衡。当然你也可以用HC-12、SI4463等类似模块原理相通。水质传感器的选择与校准这是数据的源头至关重要。pH传感器通常由pH复合电极和变送器板组成。变送器板将电极的毫伏信号放大并转换成Arduino能读取的模拟电压。必须定期用标准缓冲液pH4.0, 7.0, 10.0进行校准否则数据毫无意义。TDS传感器实际上测量的是电导率再通过系数换算成TDS值。使用时要注意温度补偿有的传感器自带并且不同液体的换算系数不同对于淡水有一个大致通用的系数。浊度传感器利用光散射原理。使用时要注意避开直射光干扰且传感器窗口需要保持清洁。重要提示大部分水质传感器探针部分可以防水但其电路接口和线缆连接处极其怕水必须用防水胶、热缩管、硅胶等材料做好严格的防水密封处理否则一次溅水就可能报废。3. 硬件搭建与核心电路解析3.1 船载端电路连接详解船载端的核心是接线务必保证牢固和防水。建议使用杜邦线焊接并套上热缩管。Arduino Uno与电机驱动板以L298N为例Arduino的5V、GND接L298N的供电端如果电机电压不高也可由此取电给Arduino但更推荐独立供电。Arduino的数字引脚如D5, D6接L298N的IN1、IN2控制电机A左电机正反转D9, D10接IN3、IN4控制电机B右电机。L298N的OUT1、OUT2接左电机OUT3、OUT4接右电机。电机电源使用一块独立的电池如7.4V锂电池接在L298N的电源输入端为电机提供动力。务必与Arduino的逻辑电源共地即L298N的GND与Arduino的GND相连。Arduino与传感器连接pH传感器变送器板通常输出模拟信号。将其VCC、GND接Arduino的5V和GND模拟输出线接Arduino的A0引脚。TDS传感器类似模拟输出接A1引脚。浊度传感器模拟输出接A2引脚。每个传感器的供电最好经过一个独立的开关或由Arduino的IO口控制通断以降低功耗和减少电极极化。Arduino与APC220模块连接APC220的VCC接5VGND接GND。关键步骤APC220的TXD接Arduino的RXD0RXD接Arduino的TXD1。但注意Arduino Uno的D0、D1也是串口监视器用的引脚下载程序时需要断开。更稳妥的做法是使用SoftwareSerial库将APC220连接到其他数字引脚如D2为RXD3为TX这样就不会与编程串口冲突。电源系统推荐双电源方案一块大容量11.1V锂电池通过降压模块降到5V给Arduino、传感器、APC220供电另一块7.4V或11.1V电池专门给电机驱动板供电。所有电源的“地”GND必须连接在一起。3.2 岸基控制端硬件搭建岸基端简单很多因为TFT Shield是直接插在Arduino Uno上的所以物理连接几乎免焊接。将TFT LCD Shield对准Arduino Uno的引脚轻轻按压确保所有引脚都插入到位。将另一个APC220模块连接到这块Arduino上。同样为了避免占用硬件串口建议使用SoftwareSerial连接到Shield未占用的引脚例如D8和D9。为Arduino Uno供电可以通过USB线连接电脑或移动电源也可以通过Shield上的直流电源接口接入7-12V电源。硬件搭建的注意事项船体防水是生命线所有电路板包括Arduino、驱动板最好用防水盒封装。传感器线缆穿过船壳的地方要用防水接头或打上厚厚的防水胶。可以在船体内放置一些干燥剂。电源隔离与滤波电机在启停时会产生很大的电流尖峰和电磁干扰可能影响传感器读数甚至导致Arduino复位。在电机电源线上加磁环在Arduino的电源入口处加一个大的电解电容如1000uF和一个小的高频瓷片电容0.1uF能有效滤波。天线放置APC220的天线应竖直向上放置远离金属船体和电池以获得最佳信号。4. 软件设计与核心代码实现4.1 船载端Arduino程序框架船载端的程序主要完成三件事循环读取传感器、解析无线指令、控制电机执行。#include SoftwareSerial.h // 定义引脚 #define PH_PIN A0 #define TDS_PIN A1 #define TURB_PIN A2 #define MOTOR_L1 5 #define MOTOR_L2 6 #define MOTOR_R1 9 #define MOTOR_R2 10 // 设置软件串口与APC220通信 SoftwareSerial mySerial(2, 3); // RX, TX // 电机速度变量接收自岸端 int motorLSpeed 0; int motorRSpeed 0; void setup() { Serial.begin(9600); // 用于调试 mySerial.begin(9600); // APC220通信波特率需与模块设置一致 // 初始化电机控制引脚为输出 pinMode(MOTOR_L1, OUTPUT); pinMode(MOTOR_L2, OUTPUT); pinMode(MOTOR_R1, OUTPUT); pinMode(MOTOR_R2, OUTPUT); // 停止所有电机 stopMotors(); } void loop() { // 1. 读取传感器数据 float phValue readPH(); float tdsValue readTDS(); float turbValue readTurbidity(); // 2. 打包数据并通过APC220发送 String dataPacket D: String(phValue) , String(tdsValue) , String(turbValue); mySerial.println(dataPacket); // 使用println自动添加换行符作为帧结束标记 // 3. 检查是否收到控制指令 if (mySerial.available() 0) { String command mySerial.readStringUntil(\n); // 以换行符为命令结束 command.trim(); if (command.startsWith(C:)) { parseMotorCommand(command); } } // 4. 根据解析出的速度值控制电机 setMotorSpeed(MOTOR_L1, MOTOR_L2, motorLSpeed); setMotorSpeed(MOTOR_R1, MOTOR_R2, motorRSpeed); delay(200); // 主循环延时控制数据发送频率 } // 读取PH值需根据传感器手册和校准公式计算 float readPH() { int analogValue analogRead(PH_PIN); float voltage analogValue * (5.0 / 1024.0); // 假设经过变送器后电压与pH值呈线性关系pH k * voltage b // k和b需要通过两点校准法求得 float ph 3.5 * voltage 0.5; // 此为示例公式必须替换为你的校准公式 return ph; } // 解析电机控制命令格式如 C:150,130 void parseMotorCommand(String cmd) { cmd cmd.substring(2); // 去掉“C:” int commaIndex cmd.indexOf(,); if (commaIndex ! -1) { motorLSpeed cmd.substring(0, commaIndex).toInt(); motorRSpeed cmd.substring(commaIndex 1).toInt(); // 限制速度值在有效范围例如 -255 到 255 (PWM范围) motorLSpeed constrain(motorLSpeed, -255, 255); motorRSpeed constrain(motorRSpeed, -255, 255); } } // 设置单个电机速度speed为负值时反转 void setMotorSpeed(int pin1, int pin2, int speed) { if (speed 0) { analogWrite(pin1, speed); digitalWrite(pin2, LOW); } else if (speed 0) { digitalWrite(pin1, LOW); analogWrite(pin2, -speed); } else { digitalWrite(pin1, LOW); digitalWrite(pin2, LOW); } } void stopMotors() { setMotorSpeed(MOTOR_L1, MOTOR_L2, 0); setMotorSpeed(MOTOR_R1, MOTOR_R2, 0); }代码关键点解析SoftwareSerial库让我们可以任意指定两个数字引脚作为串口避免了与USB编程串口的冲突。数据打包时加上前缀“D:”和分隔符“,”便于岸端程序解析。使用println()发送自动添加的换行符\n作为数据帧的结束标志这是一种简单有效的帧同步方法。电机控制函数setMotorSpeed通过两个引脚的高低电平组合和PWM值来实现正转、反转和调速。传感器读数函数如readPH()中的计算公式是示例必须根据你实际使用的传感器变送器手册和校准结果进行替换校准是获得准确数据的前提。4.2 岸基控制端TFT界面与程序岸基端程序主要负责绘制UI、处理触摸事件、发送控制指令和接收显示数据。#include Adafruit_GFX.h // 核心图形库 #include Adafruit_ILI9341.h // TFT屏幕驱动库 #include TouchScreen.h // 触摸屏库 #include SoftwareSerial.h // 定义TFT屏幕和触摸屏引脚根据你的Shield型号修改 #define TFT_CS 10 #define TFT_DC 9 #define TFT_RST 8 Adafruit_ILI9341 tft Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST); // 触摸屏引脚定义示例需核对 #define YP A2 #define XM A3 #define YM 4 #define XP 5 TouchScreen ts TouchScreen(XP, YP, XM, YM, 300); // 300为电阻 // 定义软件串口连接APC220 SoftwareSerial mySerial(8, 9); // RX, TX // 定义UI元素位置和大小 #define JOYSTICK_X 60 #define JOYSTICK_Y 180 #define JOYSTICK_R 40 #define BTN_STOP_X 200 #define BTN_STOP_Y 180 #define BTN_STOP_W 80 #define BTN_STOP_H 40 // 数据显示区域 #define DATA_X 20 #define DATA_Y 20 // 电机速度值 int leftSpd 0, rightSpd 0; void setup() { Serial.begin(9600); mySerial.begin(9600); tft.begin(); tft.setRotation(3); // 根据屏幕方向调整 tft.fillScreen(ILI9341_BLACK); drawUI(); } void loop() { // 1. 处理触摸输入 TSPoint p ts.getPoint(); if (p.z ts.pressureThreshhold) { // 如果被按下 // 将触摸点坐标转换为屏幕像素坐标需要根据库函数调整 int16_t x map(p.x, TS_MINX, TS_MAXX, 0, tft.width()); int16_t y map(p.y, TS_MINY, TS_MAXY, 0, tft.height()); // 判断触摸区域 if (isInCircle(x, y, JOYSTICK_X, JOYSTICK_Y, JOYSTICK_R)) { // 虚拟摇杆逻辑计算触摸点相对于摇杆中心的偏移 int dx x - JOYSTICK_X; int dy y - JOYSTICK_Y; // 将偏移量转换为左右电机速度差速模型 leftSpd constrain(dy dx, -255, 255); // 简化模型 rightSpd constrain(dy - dx, -255, 255); sendMotorCommand(leftSpd, rightSpd); } else if (isInRect(x, y, BTN_STOP_X, BTN_STOP_Y, BTN_STOP_W, BTN_STOP_H)) { // 停止按钮 leftSpd rightSpd 0; sendMotorCommand(0, 0); drawButton(BTN_STOP_X, BTN_STOP_Y, BTN_STOP_W, BTN_STOP_H, ILI9341_RED, STOP); delay(200); drawButton(BTN_STOP_X, BTN_STOP_Y, BTN_STOP_W, BTN_STOP_H, ILI9341_GRAY, STOP); } } // 2. 接收并显示水质数据 if (mySerial.available() 0) { String data mySerial.readStringUntil(\n); data.trim(); if (data.startsWith(D:)) { displaySensorData(data); } } delay(50); // 主循环延时 } // 绘制初始用户界面 void drawUI() { tft.setTextColor(ILI9341_WHITE); tft.setTextSize(2); tft.setCursor(DATA_X, DATA_Y); tft.println(pH: --); tft.setCursor(DATA_X, DATA_Y 30); tft.println(TDS: -- ppm); tft.setCursor(DATA_X, DATA_Y 60); tft.println(Turb: -- NTU); // 绘制虚拟摇杆底座 tft.fillCircle(JOYSTICK_X, JOYSTICK_Y, JOYSTICK_R, ILI9341_DARKGREY); // 绘制停止按钮 drawButton(BTN_STOP_X, BTN_STOP_Y, BTN_STOP_W, BTN_STOP_H, ILI9341_GRAY, STOP); } // 发送电机控制命令 void sendMotorCommand(int l, int r) { String cmd C: String(l) , String(r); mySerial.println(cmd); } // 解析并显示传感器数据 void displaySensorData(String data) { data data.substring(2); // 去掉“D:” int firstComma data.indexOf(,); int secondComma data.indexOf(,, firstComma 1); if (firstComma ! -1 secondComma ! -1) { String phStr data.substring(0, firstComma); String tdsStr data.substring(firstComma 1, secondComma); String turbStr data.substring(secondComma 1); // 更新屏幕显示 tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK); // 白字黑底覆盖式更新 tft.setTextSize(2); tft.setCursor(DATA_X, DATA_Y); tft.print(pH: ); tft.println(phStr); tft.setCursor(DATA_X, DATA_Y 30); tft.print(TDS: ); tft.print(tdsStr); tft.println( ppm); tft.setCursor(DATA_X, DATA_Y 60); tft.print(Turb: ); tft.print(turbStr); tft.println( NTU); } } // 判断点是否在圆内 boolean isInCircle(int x, int y, int cx, int cy, int r) { return (sq(x - cx) sq(y - cy) sq(r)); } // 判断点是否在矩形内 boolean isInRect(int x, int y, int rx, int ry, int w, int h) { return (x rx x (rx w) y ry y (ry h)); } // 绘制带文字的按钮 void drawButton(int x, int y, int w, int h, uint16_t color, String label) { tft.fillRoundRect(x, y, w, h, 5, color); tft.setTextColor(ILI9341_WHITE); tft.setTextSize(2); int textX x (w - label.length() * 12) / 2; // 粗略居中 int textY y (h - 16) / 2; tft.setCursor(textX, textY); tft.print(label); }UI与交互设计要点Adafruit_GFX和ILI9341库是驱动这类TFT屏的标配功能强大。TouchScreen库用于读取电阻屏坐标。坐标映射触摸屏读取的原始坐标需要map()函数转换到屏幕像素坐标这个映射关系需要根据实际接线和库的设定进行校准。虚拟摇杆算法上述代码中的dy dx和dy - dx是一个非常简化的差速转向模型。dy控制前进/后退同向dx控制转向差速。你可以根据需要调整算法使其操控更符合直觉。局部刷新在displaySensorData函数中我们使用ILI9341_BLACK作为背景色重新打印数据而不是清屏重绘这可以避免屏幕闪烁。按钮反馈按下停止按钮时改变颜色再恢复提供了简单的视觉反馈提升用户体验。5. 系统集成、调试与水上实测5.1 分步调试策略不要试图一次性把所有东西连好再调试那会让人崩溃。务必分步进行基础通信测试先将两个APC220模块分别连接到两个Arduino或直接USB转TTL模块连接到电脑用串口助手工具测试它们之间能否正常收发字符串。确保波特率、信道等参数设置一致。船载端电机测试断开无线模块单独测试电机驱动。写一个简单程序让两个电机正转、反转、调速确保硬件连接和基础代码正确。船载端传感器测试单独测试每个传感器在串口监视器中查看原始模拟值并应用校准公式确认读数合理。例如将pH传感器插入pH7.0的标准缓冲液看计算出的pH值是否接近7.0。岸基端TFT显示测试单独测试TFT屏幕能否正常显示文字、图形触摸坐标是否准确。单向链路测试先将船载端的传感器数据发送功能与岸基端的数据接收显示功能联调。让船静止看岸上屏幕能否稳定显示数据。单向控制测试再测试岸基端发送控制指令船载端接收并控制电机可将船抬起空转测试。双向联调最后进行全功能测试在安全的水域如大水盆进行初步航行。5.2 水上实测要点与问题排查第一次下水心情激动又紧张。记住以下要点安全第一选择无风或微风、水面平静、无船只来往的封闭水域进行测试如小型景观池、游泳池。永远准备好打捞工具长杆、绳子、另一艘船。循序渐进先测试短距离5米内的通信和控制确保基本功能正常。再逐步拉远距离观察控制延迟和数据丢包情况。重心与平衡将较重的电池放在船体底部中央确保小船不会侧翻。所有设备固定牢靠防止晃动影响航行。天线姿态确保APC220的天线竖直向上完全展开。水上实测常见问题速查表问题现象可能原因排查思路与解决方案船体不受控制乱跑1. 电机线接反。2. 左右电机定义与程序控制逻辑相反。3. 无线信号受到严重干扰或断开。1. 交换任意一个电机的两根线。2. 检查程序setMotorSpeed函数中引脚对应关系或交换motorLSpeed和motorRSpeed的赋值。3. 检查天线拉近距离测试避开强干扰源。触屏控制不灵敏或错乱1. 触摸屏坐标映射不准。2. 触摸屏库的压力阈值设置不当。3. 屏幕有污渍或水渍。1. 编写一个简单的坐标打印程序触摸屏幕四个角校准TS_MINX, TS_MAXX, TS_MINY, TS_MAXY这几个宏定义的值。2. 调整TouchScreen初始化时的压力阈值参数。3. 清洁屏幕。水质数据读数异常如pH值固定不变或剧烈跳动1. 传感器未校准或校准参数错误。2. 传感器探头未浸入水中或接触不良。3. 电源噪声干扰模拟信号。4. 传感器线缆或接口进水。1.重新校准传感器这是最常见原因。2. 确保探头测量部分完全浸入水面以下。3. 为模拟引脚增加一个0.1uF的电容到地进行滤波。尝试用独立稳压模块为传感器供电。4.立即断电彻底烘干检查。控制距离远小于预期1. APC220模块天线未安装或损坏。2. 模块供电不足电压低于3.3V会严重影响功率。3. 船体金属或电池遮挡信号。4. 环境干扰如附近有大型Wi-Fi路由器、对讲机。1. 安装并确保天线完好。2. 用万用表测量模块VCC脚电压确保在3.3V-5V之间且稳定。3. 尽量将天线引至船体高处。4. 更换APC220的信道频率试试。船载端Arduino偶尔自动重启1. 电机启动瞬间电流过大导致系统电压被拉低“掉电复位”。2. 电源线或接头接触不良。1.电机电源与控制系统电源分离是治本方法。在Arduino的VIN和GND之间并联一个大电容如1000uF电解电容缓冲电压突变。2. 检查所有电源接头确保焊接或插接牢固。5.3 数据记录与功能扩展思路基础功能实现后可以考虑增加一些实用功能数据记录在岸基端的Arduino上插一张SD卡 Shield将收到的时间戳和水质数据保存为CSV文件便于后续在电脑上用Excel或Python进行分析。自动巡航在船载端程序中加入简单的GPS模块如Neo-6M和航点导航逻辑实现按照预设路径自动巡航采样你只需要在岸上监控。视频回传增加一个微型摄像头和Wi-Fi模块如ESP32-CAM可以第一视角观察水面情况或寻找污染源但这会大幅增加系统复杂度和功耗。数据可视化将岸基端Arduino通过USB连接树莓派或旧手机运行一个简单的Python或App脚本将数据以更美观的图表形式展示甚至上传到私有服务器。这个项目从构思到实现每一步都充满了挑战和乐趣。硬件连接要细心软件调试要耐心水上测试要小心。最深的体会是模块化设计和分步调试是成功的关键。先让每个部分独立工作起来再把它们像拼图一样组合遇到问题也就容易定位了。当看到自己制作的小船在水面上划出涟漪而手中的屏幕实时跳动着水质数据时那种将想法变为现实的满足感正是DIY最大的魅力所在。