✔零知IDE 是一个真正属于国人自己的开源软件平台,在开发效率上超越了Arduino平台并且更加容易上手,大大降低了开发难度。零知开源在软件方面提供了完整的学习教程和丰富示例代码,让不懂程序的工程师也能非常轻而易举的搭建电路来创作产品,测试产品。快来动手试试吧!
✔访问零知实验室,获取更多实战项目和教程资源吧!
目录
[1.1 硬件清单](#1.1 硬件清单)
[1.2 接线方案表](#1.2 接线方案表)
[1.3 具体接线图](#1.3 具体接线图)
[1.4 连接实物图](#1.4 连接实物图)
[2.1 初始化设置](#2.1 初始化设置)
[2.2 数据读取与映射](#2.2 数据读取与映射)
[2.3 防闪烁更新机制](#2.3 防闪烁更新机制)
[2.4 条形图表绘制](#2.4 条形图表绘制)
[2.5 完整代码](#2.5 完整代码)
[3.1 操作流程](#3.1 操作流程)
[3.2 界面展示](#3.2 界面展示)
[3.3 视频演示](#3.3 视频演示)
[4.1 基本原理](#4.1 基本原理)
[4.2 工作过程](#4.2 工作过程)
[4.3 模数转换原理](#4.3 模数转换原理)
[Q1: 为什么雨量百分比显示不正确?](#Q1: 为什么雨量百分比显示不正确?)
[Q2: 如何调整报警阈值?](#Q2: 如何调整报警阈值?)
[Q3: 历史数据图表不更新怎么办?](#Q3: 历史数据图表不更新怎么办?)
(1)项目概述
本项目基于STM32F407VET6主控芯片,结合雨滴传感器和240×240分辨率ST7789 TFT显示屏,开发了一套智能降雨监测系统。系统能够实时监测降雨情况,通过三种不同的UI界面展示数据,具备数据可视化、历史趋势分析和报警功能。
(2)项目功能及亮点
功能描述:数据映射关系处理,实时雨量百分比监测与显示
系统亮点:正确处理雨滴传感器的电阻特性,实现从模拟值到百分比的合理映射
一、硬件连接部分
1.1 硬件清单
组件 | 规格 | 数量 |
---|---|---|
主控板 | STM32F407VET6 | 1 |
雨滴传感器 | 模拟输出型 | 1 |
TFT显示屏 | ST7789 240×240 | 1 |
报警LED | 5mm LED | 1 |
连接线 | 杜邦线 | 若干 |
1.2 接线方案表
根据代码定义的引脚分配:
组件 | 引脚功能 | 零知增强板引脚 |
---|---|---|
雨滴传感器 | 模拟输入 | A1 |
报警LED | 数字输出 | 9 |
ST7789 | 片选信号 | 53 |
ST7789 | 复位信号 | 6 |
ST7789 | 数据/命令 | 7 |
ST7789 | SPI时钟 | 52 |
ST7789 | SPI数据 | 51 |
1.3 具体接线图

1.4 连接实物图

二、核心代码解析
2.1 初始化设置
cpp
void setup() {
pinMode(RAIN_SENSOR, INPUT);
pinMode(ALERT, OUTPUT);
Serial.begin(9600);
// 初始化显示屏
tft.init(240, 240);
tft.setRotation(1);
tft.fillScreen(BACKGROUND_COLOR);
tft.setTextWrap(false);
// 显示启动画面
showStartupScreen();
delay(2500);
// 绘制初始界面
drawCurrentPage();
Serial.println("Rain Sensor with TFT Display Started");
}
tft.setRotation(1)设置屏幕方向为横屏、tft.setTextWrap(false)禁止文本自动换行
2.2 数据读取与映射
cpp
void readSensorData() {
rawValue = analogRead(RAIN_SENSOR) / 4; // 适配4096的模拟值
// 反转映射关系:无雨时电阻大,模拟值大,我们希望无雨时百分比小
sensorValue = map(rawValue, 0, 1023, 100, 0); // 反转映射
// 保存历史数据
historyValues[historyIndex] = sensorValue;
historyIndex = (historyIndex + 1) % 15;
// 设置报警状态(阈值可调整)
alertState = (sensorValue > 70); // 大于70%认为有雨(因为映射已反转)
}
模拟值除以4适配4096分辨率ADC、使用map()函数实现数值范围映射
2.3 防闪烁更新机制
cpp
void updateCurrentPage() {
// 只有在数据变化时才更新显示,避免闪烁
if (sensorValue != lastSensorValue || alertState != lastAlertState || rawValue != lastRawValue) {
switch(currentPage) {
case 0: updatePage1(); break;
case 1: updatePage2(); break;
case 2: updatePage3(); break;
}
// 更新上一次的值
lastSensorValue = sensorValue;
lastAlertState = alertState;
lastRawValue = rawValue;
}
// 页面2(图表页面)需要持续更新
if (currentPage == 1 && millis() - lastChartUpdate > 1000) {
updatePage2Chart();
lastChartUpdate = millis();
}
}
数据变化检测避免不必要刷新、图表页面单独处理,保证实时性、时间戳控制刷新频率
2.4 条形图表绘制
cpp
void updatePage2Chart() {
// 计算条形图参数(居中显示,增加间隔)
int barCount = 12;
int barWidth = 10;
int barSpacing = 6;
int totalWidth = barCount * barWidth + (barCount - 1) * barSpacing;
int startX = 130 - totalWidth / 2; // 居中计算
// 绘制条形图
for (int i = 0; i < barCount; i++) {
int valueIndex = (historyIndex - barCount + i + barCount) % barCount;
int barHeight = map(historyValues[valueIndex], 0, 100, 0, chartHeight);
// 选择颜色 - 使用渐变效果
uint16_t barColor;
if (historyValues[valueIndex] > 70) {
barColor = WARNING_COLOR; // 红色报警
} else if (historyValues[valueIndex] > 40) {
barColor = ST77XX_ORANGE; // 橙色警告
} else {
barColor = BAR_COLOR; // 蓝色正常
}
tft.fillRect(x, y, barWidth, barHeight, barColor);
}
}
渐变颜色指示不同状态、时间轴标签显示
2.5 完整代码
cpp
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <SPI.h>
// 引脚定义
#define ST77XX_DARKGREY 0x7453
#define RAIN_SENSOR A1
#define ALERT 9
#define TFT_CS 53
#define TFT_RST 6
#define TFT_DC 7
// 创建显示屏对象
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);
// 变量定义
int sensorValue = 0;
int rawValue = 0;
bool alertState = false;
unsigned long lastDisplayUpdate = 0;
unsigned long lastSensorRead = 0;
unsigned long lastChartUpdate = 0;
int currentPage = 0;
const int TOTAL_PAGES = 3;
const int PAGE_DURATION = 10000; // 页面切换时间8秒
// 颜色定义
#define BACKGROUND_COLOR ST77XX_BLACK
#define TEXT_COLOR ST77XX_WHITE
#define WARNING_COLOR ST77XX_RED
#define NORMAL_COLOR ST77XX_GREEN
#define BAR_COLOR ST77XX_BLUE
#define ACCENT_COLOR ST77XX_CYAN
#define GRID_COLOR 0x4208 // 深灰色
// 页面切换动画相关
int animationOffset = 0;
bool isAnimating = false;
unsigned long animationStartTime = 0;
// 历史数据记录
int historyValues[15] = {0};
int historyIndex = 0;
// 防止闪烁的变量
int lastSensorValue = -1;
bool lastAlertState = false;
int lastRawValue = -1;
void setup() {
pinMode(RAIN_SENSOR, INPUT);
pinMode(ALERT, OUTPUT);
Serial.begin(9600);
// 初始化显示屏
tft.init(240, 240);
tft.setRotation(1);
tft.fillScreen(BACKGROUND_COLOR);
tft.setTextWrap(false);
// 显示启动画面
showStartupScreen();
delay(2500);
// 绘制初始界面
drawCurrentPage();
Serial.println("Rain Sensor with TFT Display Started");
}
void loop() {
unsigned long currentTime = millis();
// 每800ms读取一次传感器数据
if (currentTime - lastSensorRead >= 800) {
readSensorData();
lastSensorRead = currentTime;
}
// 每8秒切换页面
if (currentTime - lastDisplayUpdate >= PAGE_DURATION) {
switchToNextPage();
lastDisplayUpdate = currentTime;
}
// 处理页面切换动画
if (isAnimating) {
handlePageAnimation();
} else {
// 更新当前页面数据(局部刷新)
updateCurrentPage();
}
// 控制报警LED
digitalWrite(ALERT, alertState ? HIGH : LOW);
}
void readSensorData() {
rawValue = analogRead(RAIN_SENSOR) / 4; // 适配4096的模拟值
// 反转映射关系:无雨时电阻大,模拟值大,我们希望无雨时百分比小
sensorValue = map(rawValue, 0, 1023, 100, 0); // 反转映射
// 保存历史数据
historyValues[historyIndex] = sensorValue;
historyIndex = (historyIndex + 1) % 15;
// 设置报警状态(阈值可调整)
alertState = (sensorValue > 70); // 大于70%认为有雨(因为映射已反转)
Serial.print("Raw: ");
Serial.print(rawValue);
Serial.print(" - Rain Level: ");
Serial.print(sensorValue);
Serial.print("% - Alert: ");
Serial.println(alertState ? "ON" : "OFF");
}
void switchToNextPage() {
currentPage = (currentPage + 1) % TOTAL_PAGES;
isAnimating = true;
animationOffset = 240; // 从右侧开始
animationStartTime = millis();
// 重置上一次的值,确保新页面完全绘制
lastSensorValue = -1;
lastAlertState = !alertState;
lastRawValue = -1;
}
void handlePageAnimation() {
unsigned long currentTime = millis();
unsigned long elapsed = currentTime - animationStartTime;
// 动画持续时间400ms
if (elapsed < 400) {
// 计算动画偏移量(缓动效果)
float progress = (float)elapsed / 400.0;
animationOffset = 240 - (int)(240 * easeOutCubic(progress));
// 绘制动画帧
drawPageAnimation();
} else {
isAnimating = false;
animationOffset = 0;
drawCurrentPage(); // 最终绘制完整页面
}
}
float easeOutCubic(float x) {
return 1 - pow(1 - x, 3);
}
void drawPageAnimation() {
tft.fillScreen(BACKGROUND_COLOR);
// 绘制新页面(带偏移)
switch(currentPage) {
case 0: drawPage1(animationOffset); break;
case 1: drawPage2(animationOffset); break;
case 2: drawPage3(animationOffset); break;
}
}
void drawCurrentPage() {
switch(currentPage) {
case 0: drawPage1(0); break;
case 1: drawPage2(0); break;
case 2: drawPage3(0); break;
}
}
void updateCurrentPage() {
// 只有在数据变化时才更新显示,避免闪烁
if (sensorValue != lastSensorValue || alertState != lastAlertState || rawValue != lastRawValue) {
switch(currentPage) {
case 0: updatePage1(); break;
case 1: updatePage2(); break;
case 2: updatePage3(); break;
}
// 更新上一次的值
lastSensorValue = sensorValue;
lastAlertState = alertState;
lastRawValue = rawValue;
}
// 页面2(图表页面)需要持续更新
if (currentPage == 1 && millis() - lastChartUpdate > 1000) {
updatePage2Chart();
lastChartUpdate = millis();
}
}
void showStartupScreen() {
tft.fillScreen(ST77XX_BLACK);
// 主标题
tft.setTextColor(ST77XX_GREEN);
tft.setTextSize(3);
tft.setCursor(25, 80);
tft.println("RAIN SENSOR");
// 副标题
tft.setTextColor(ST77XX_WHITE);
tft.setTextSize(2);
tft.setCursor(85, 120);
tft.println("SYSTEM");
// 进度条 - 粗进度条
tft.drawRoundRect(40, 160, 160, 20, 10, ST77XX_WHITE);
for(int i = 0; i <= 160; i += 8) {
tft.fillRoundRect(40, 160, i, 20, 10, ST77XX_BLUE);
delay(30);
}
}
// 页面1: 简洁数据展示
void drawPage1(int offset) {
tft.fillScreen(BACKGROUND_COLOR);
// 标题 - 左上角
tft.setTextColor(ACCENT_COLOR);
tft.setTextSize(2);
tft.setCursor(10 + offset, 10);
tft.println("RAIN LEVEL");
// 分隔线
tft.drawFastHLine(10 + offset, 35, 220, GRID_COLOR);
// 绘制静态内容
drawPage1Static(offset);
}
void drawPage1Static(int offset) {
// 状态指示标签
tft.setTextColor(TEXT_COLOR);
tft.setTextSize(2);
tft.setCursor(20 + offset, 160);
tft.print("Raw: ");
// 页面指示
tft.setCursor(80 + offset, 220);
tft.print("Page 1/");
tft.print(TOTAL_PAGES);
}
void updatePage1() {
// 清除数据区域
tft.fillRect(10, 50, 220, 100, BACKGROUND_COLOR);
// 大字体显示百分比
tft.setTextColor(sensorValue > 70 ? WARNING_COLOR : TEXT_COLOR);
tft.setTextSize(4);
tft.setCursor(85, 70);
tft.print(sensorValue);
tft.setTextSize(2);
tft.println("%");
// 状态指示
tft.setTextSize(2);
tft.setCursor(75, 120);
if (sensorValue > 70) {
tft.setTextColor(WARNING_COLOR);
tft.println("RAINING!");
} else if (sensorValue > 30) {
tft.setTextColor(ST77XX_YELLOW);
tft.println("CLOUDY");
} else {
tft.setTextColor(NORMAL_COLOR);
tft.println("CLEAR");
}
// 原始值显示
tft.setTextColor(ACCENT_COLOR);
tft.fillRect(50, 150, 100, 30, BACKGROUND_COLOR);
tft.setTextSize(2);
tft.setCursor(20, 160);
tft.print("Raw: ");
tft.print(rawValue);
}
// 页面2: 现代化图表展示
void drawPage2(int offset) {
tft.fillScreen(BACKGROUND_COLOR);
// 标题 - 左上角
tft.setTextColor(ACCENT_COLOR);
tft.setTextSize(2);
tft.setCursor(10 + offset, 10);
tft.println("RAIN HISTORY");
// 分隔线
tft.drawFastHLine(10 + offset, 35, 220, GRID_COLOR);
// 绘制图表框架
drawChartFrame(offset);
// 绘制静态标签
tft.setTextColor(TEXT_COLOR);
tft.setTextSize(1);
tft.setCursor(10 + offset, 200);
tft.print("Time");
// 页面指示
tft.setTextSize(2);
tft.setCursor(80 + offset, 220);
tft.print("Page 2/");
tft.print(TOTAL_PAGES);
}
void drawChartFrame(int offset) {
// 绘制坐标轴
tft.drawFastVLine(30 + offset, 50, 140, TEXT_COLOR); // Y轴
tft.drawFastHLine(30 + offset, 190, 190, TEXT_COLOR); // X轴
// Y轴标签
tft.setTextColor(TEXT_COLOR);
tft.setTextSize(1);
tft.setCursor(5 + offset, 45);
tft.print("100%");
tft.setCursor(10 + offset, 90);
tft.print("75%");
tft.setCursor(10 + offset, 135);
tft.print("50%");
tft.setCursor(10 + offset, 180);
tft.print("25%");
// X轴箭头
tft.drawLine(220 + offset, 190, 215 + offset, 185, TEXT_COLOR);
tft.drawLine(220 + offset, 190, 215 + offset, 195, TEXT_COLOR);
// Y轴箭头
tft.drawLine(30 + offset, 50, 25 + offset, 55, TEXT_COLOR);
tft.drawLine(30 + offset, 50, 35 + offset, 55, TEXT_COLOR);
}
void updatePage2() {
// 更新当前值显示
tft.fillRect(120, 205, 100, 12, BACKGROUND_COLOR);
tft.setTextColor(ACCENT_COLOR);
tft.setTextSize(1);
tft.setCursor(120, 205);
tft.print("Now: ");
tft.print(sensorValue);
tft.print("%");
}
void updatePage2Chart() {
// 计算条形图参数(居中显示,增加间隔)
int barCount = 12;
int barWidth = 10;
int barSpacing = 6;
int totalWidth = barCount * barWidth + (barCount - 1) * barSpacing;
int startX = 130 - totalWidth / 2; // 居中计算
int chartBottom = 190;
int chartHeight = 130;
// 清除图表区域(只清除条形区域,保留坐标轴)
tft.fillRect(startX, 60, totalWidth + 5, chartHeight, BACKGROUND_COLOR);
// 绘制条形图
for (int i = 0; i < barCount; i++) {
int valueIndex = (historyIndex - barCount + i + barCount) % barCount;
int barHeight = map(historyValues[valueIndex], 0, 100, 0, chartHeight);
int x = startX + i * (barWidth + barSpacing);
int y = chartBottom - barHeight;
// 选择颜色 - 使用渐变效果
uint16_t barColor;
if (historyValues[valueIndex] > 70) {
barColor = WARNING_COLOR;
} else if (historyValues[valueIndex] > 40) {
barColor = ST77XX_ORANGE;
} else {
barColor = BAR_COLOR;
}
// 绘制条形(带阴影效果)
tft.fillRect(x, y, barWidth, barHeight, barColor);
// 条形顶部边框
tft.drawFastHLine(x, y, barWidth, TEXT_COLOR);
// 数值标签(只显示较高的条形)
if (barHeight > 20) {
tft.setTextColor(TEXT_COLOR);
tft.setTextSize(1);
tft.setCursor(x + 1, y - 10);
tft.print(historyValues[valueIndex]);
}
// 时间标签(底部)
if (i % 3 == 0) {
tft.setTextColor(GRID_COLOR);
tft.setTextSize(1);
tft.setCursor(x - 3, chartBottom + 5);
tft.print("-");
tft.print(barCount - i);
}
}
}
// 页面3: 详细信息
void drawPage3(int offset) {
tft.fillScreen(BACKGROUND_COLOR);
// 标题 - 左上角
tft.setTextColor(ACCENT_COLOR);
tft.setTextSize(2);
tft.setCursor(10 + offset, 10);
tft.println("SENSOR INFO");
// 分隔线
tft.drawFastHLine(10 + offset, 35, 220, GRID_COLOR);
// 绘制静态内容
drawPage3Static(offset);
}
void drawPage3Static(int offset) {
// 静态标签
tft.setTextColor(TEXT_COLOR);
tft.setTextSize(2);
tft.setCursor(5 + offset, 60);
tft.print("Rain Level");
tft.setCursor(5 + offset, 90);
tft.print("Raw Value: ");
tft.setCursor(5 + offset, 120);
tft.print("Alert: ");
tft.setCursor(5 + offset, 150);
tft.print("Threshold: ");
// 映射说明
tft.setTextColor(GRID_COLOR);
tft.setTextSize(1);
tft.setCursor(5 + offset, 180);
tft.println("Dry=Low%, Wet=High%");
// 页面指示
tft.setTextColor(ACCENT_COLOR);
tft.setTextSize(2);
tft.setCursor(80 + offset, 220);
tft.print("Page 3/");
tft.print(TOTAL_PAGES);
}
void updatePage3() {
// 更新雨量百分比
tft.fillRect(130, 60, 80, 20, BACKGROUND_COLOR);
tft.setTextColor(sensorValue > 70 ? WARNING_COLOR : NORMAL_COLOR);
tft.setTextSize(2);
tft.setCursor(130, 60);
tft.print(sensorValue);
tft.println("%");
// 更新原始值
tft.fillRect(130, 90, 100, 20, BACKGROUND_COLOR);
tft.setTextColor(ACCENT_COLOR);
tft.setCursor(130, 90);
tft.print(rawValue);
tft.println("/1023");
// 更新报警状态
tft.fillRect(130, 120, 100, 20, BACKGROUND_COLOR);
if (alertState) {
tft.setTextColor(WARNING_COLOR);
tft.setCursor(130, 120);
tft.println("ACTIVE");
} else {
tft.setTextColor(NORMAL_COLOR);
tft.setCursor(130, 120);
tft.println("INACTIVE");
}
// 更新阈值信息
tft.fillRect(130, 150, 80, 20, BACKGROUND_COLOR);
tft.setTextColor(TEXT_COLOR);
tft.setCursor(130, 150);
tft.println(">70%");
}
数据展示思维导图
三、项目结果演示
3.1 操作流程
①系统启动显示启动界面
②自动进入主监测界面(页面1)
③每10秒自动切换至下一页面

历史雨量数据柱状统计图模拟界面设计,根据时间戳显示雨量大小
④页面1: 实时雨量百分比显示、页面2: 历史数据趋势图表、页面3: 详细传感器信息
⑤零知IDE串口打印输出
串口输出示例:
Rain Sensor with TFT Display Started
Raw: 245 - Rain Level: 76% - Alert: ON
3.2 界面展示

页面1: 大字体显示当前雨量百分比和状态指示

页面2: 条形柱状图展示历史数据趋势

页面3: 详细的传感器参数和报警状态
3.3 视频演示
雨滴传感器的多界面TFT降雨监测显示系统
系统从启动到运行的完整流程,包括三种界面的自动切换、模拟降雨检测、报警触发等关键功能
四、雨滴传感器工作原理
4.1 基本原理
雨滴传感器基于电阻变化原理工作。传感器表面有交错排列的导电线,当雨水落到表面时,水分的导电性会在导线之间形成电阻通路

模拟电压输出特性:模拟雨量增大之后,导通电阻降低,输出电压下降
4.2 工作过程
(1)干燥状态: 导线间电阻极大(兆欧级),输出高电压
(2)湿润状态: 水分形成导电通路,电阻显著降低,输出电压下降
(3)雨量检测: 通过ADC读取电压值,映射为雨量百分比,雨量阈值超过70%亮红灯报警
4.3 模数转换原理
零知增强板的STM32F407VET6主控芯片内置12位ADC,将0-3.3V模拟电压转换为0-4095数字值:

五、常见问题解答
Q1: 为什么雨量百分比显示不正确?
A: 检查雨滴传感器的接线:
确保模拟输入引脚正确。调整
map()
函数的参数以适应具体传感器特性
Q2: 如何调整报警阈值?
A: 修改代码:
调整
alertState = (sensorValue > 70);
的阈值数值,根据实际需求设置合适的报警点
Q3: 历史数据图表不更新怎么办?
A: 检查historyValues
数组的索引管理:
确保新数据正确覆盖旧数据。验证
updatePage2Chart()
函数的调用频率