文章目录
-
- 一、项目概述
- 二、硬件选型与清单
- 三、硬件接线原理
-
- [3.1 DS18B20温度传感器接线](#3.1 DS18B20温度传感器接线)
- [3.2 YF-S201流量传感器接线](#3.2 YF-S201流量传感器接线)
- [3.3 SSD1306 OLED显示屏接线](#3.3 SSD1306 OLED显示屏接线)
- [3.4 继电器模块接线](#3.4 继电器模块接线)
- [3.5 按键接线](#3.5 按键接线)
- [3.6 蜂鸣器接线](#3.6 蜂鸣器接线)
- [3.7 AT24C02 EEPROM接线](#3.7 AT24C02 EEPROM接线)
- 四、系统整体架构
- 五、开发环境搭建
-
- [5.1 软件工具准备](#5.1 软件工具准备)
- [5.2 创建工程](#5.2 创建工程)
- 六、代码文件创建与编写
-
- [6.1 文件:User/main.h](#6.1 文件:User/main.h)
- [6.2 文件:User/main.c](#6.2 文件:User/main.c)
- [6.3 文件:System/delay.h](#6.3 文件:System/delay.h)
- [6.4 文件:System/delay.c](#6.4 文件:System/delay.c)
- [6.5 文件:System/pid.h](#6.5 文件:System/pid.h)
- [6.6 文件:System/pid.c](#6.6 文件:System/pid.c)
- [6.7 文件:Hardware/ds18b20.h](#6.7 文件:Hardware/ds18b20.h)
- [6.8 文件:Hardware/ds18b20.c](#6.8 文件:Hardware/ds18b20.c)
- [6.9 文件:Hardware/flow_sensor.h](#6.9 文件:Hardware/flow_sensor.h)
- [6.10 文件:Hardware/flow_sensor.c](#6.10 文件:Hardware/flow_sensor.c)
- [6.11 文件:Hardware/oled.h](#6.11 文件:Hardware/oled.h)
- [6.12 文件:Hardware/oled.c](#6.12 文件:Hardware/oled.c)
- [6.13 文件:Hardware/oled_font.h](#6.13 文件:Hardware/oled_font.h)
- [6.14 文件:Hardware/relay.h](#6.14 文件:Hardware/relay.h)
- [6.15 文件:Hardware/relay.c](#6.15 文件:Hardware/relay.c)
- [6.16 文件:Hardware/key.h](#6.16 文件:Hardware/key.h)
- [6.17 文件:Hardware/key.c](#6.17 文件:Hardware/key.c)
- [6.18 文件:Hardware/buzzer.h](#6.18 文件:Hardware/buzzer.h)
- [6.19 文件:Hardware/buzzer.c](#6.19 文件:Hardware/buzzer.c)
- [6.20 文件:Hardware/eeprom.h](#6.20 文件:Hardware/eeprom.h)
- [6.21 文件:Hardware/eeprom.c](#6.21 文件:Hardware/eeprom.c)
- 七、编译与下载
-
- [7.1 工程编译设置](#7.1 工程编译设置)
- [7.2 编译下载步骤](#7.2 编译下载步骤)
- [7.3 串口调试](#7.3 串口调试)
- 八、系统工作流程详解
- 九、实际操作演示
-
- [9.1 首次开机](#9.1 首次开机)
- [9.2 设置目标温度](#9.2 设置目标温度)
- [9.3 设置单价](#9.3 设置单价)
- [9.4 正常工作状态](#9.4 正常工作状态)
- [9.5 PID控温效果](#9.5 PID控温效果)
- [9.6 流量计费验证](#9.6 流量计费验证)
- 十、PID参数整定指南
-
- [10.1 手动整定步骤](#10.1 手动整定步骤)
- [10.2 推荐参数组合](#10.2 推荐参数组合)
- 十一、常见问题排查
-
- [11.1 温度显示异常](#11.1 温度显示异常)
- [11.2 OLED无显示](#11.2 OLED无显示)
- [11.3 流量计不计数](#11.3 流量计不计数)
- [11.4 继电器不动作](#11.4 继电器不动作)
- 十二、扩展功能建议
- 十三、总结
一、项目概述
本项目将带你从零开始打造一台具备温度精确控制和流量计费功能的智能饮水机。系统以STM32F103C8T6为主控芯片,集成DS18B20温度传感器、流量传感器、继电器驱动模块、0.96寸OLED显示屏以及蜂鸣器,实现饮水机出水温度的实时监测、PID恒温控制、用水量精确计量与费用计算。
整个项目的核心功能包括:实时水温与流量显示、目标温度按键调节、PID算法控温、单次用水量及费用统计、历史总费用累计存储。我们将从硬件选型开始,一步步深入到代码编写,确保每一位零基础的开发者都能跟着动手实现。
二、硬件选型与清单
在开始编写代码之前,我们先明确需要的所有硬件模块。合理的硬件选型是项目成功的基础。
| 序号 | 元器件名称 | 型号/规格 | 数量 | 备注 |
|---|---|---|---|---|
| 1 | 主控板 | STM32F103C8T6 最小系统板 | 1 | 俗称"蓝色药丸" |
| 2 | 温度传感器 | DS18B20 防水型 | 1 | 不锈钢封装,耐高温 |
| 3 | 流量传感器 | YF-S201 霍尔流量计 | 1 | 脉冲输出型 |
| 4 | OLED显示屏 | 0.96寸 SSD1306 IIC接口 | 1 | 128×64分辨率 |
| 5 | 继电器模块 | 5V单路光耦隔离 | 1 | 控制加热棒 |
| 6 | 加热棒 | 12V/100W 直流 | 1 | 实际加热执行器 |
| 7 | 按键 | 6×6mm 轻触开关 | 4 | 菜单、加、减、确认 |
| 8 | 蜂鸣器 | 有源蜂鸣器模块 5V | 1 | 提示音 |
| 9 | 电源模块 | 12V 10A开关电源 | 1 | 系统总供电 |
| 10 | 降压模块 | LM2596 DC-DC 可调 | 1 | 12V转5V/3.3V |
| 11 | EEPROM | AT24C02 | 1 | 存储累计费用 |
| 12 | 电阻 | 10KΩ | 4 | 按键上拉 |
| 13 | 电容 | 100μF/25V | 2 | 电源滤波 |
| 14 | 杜邦线 | 公母各一包 | 若干 | 接线用 |
| 15 | 面包板 | 830孔 | 1 | 搭建电路 |
| 16 | 三极管 | S8050 NPN | 1 | 驱动蜂鸣器 |
| 17 | 电阻 | 1KΩ | 1 | 蜂鸣器基极限流 |
三、硬件接线原理
在进行代码编写前,必须把硬件连接清楚。下面用文字详细描述每一根线的连接关系,请务必仔细对照接线。
3.1 DS18B20温度传感器接线
DS18B20采用单总线通信,仅需要三根线。传感器通常引出三根导线:红色为VCC,黑色为GND,黄色为数据线DATA。
- 红色线 → 3.3V
- 黑色线 → GND
- 黄色线 → STM32的PA0引脚
- PA0引脚与3.3V之间并联一个4.7KΩ的上拉电阻(此电阻至关重要,缺失将导致通信失败)
3.2 YF-S201流量传感器接线
YF-S201流量传感器内部为霍尔元件,输出脉冲信号。三根线分别为红色(VCC)、黑色(GND)、黄色(信号输出)。
- 红色线 → 5V(注意是5V供电,不是3.3V)
- 黑色线 → GND
- 黄色线 → STM32的PA1引脚(定时器输入捕获通道)
3.3 SSD1306 OLED显示屏接线
采用IIC通信接口,四根引脚:
- VCC → 3.3V
- GND → GND
- SCL → STM32的PB6引脚(I2C1时钟线)
- SDA → STM32的PB7引脚(I2C1数据线)
3.4 继电器模块接线
继电器模块用于控制加热棒的通断:
- VCC → 5V
- GND → GND
- IN(信号) → STM32的PB0引脚
3.5 按键接线
四个按键分别用于菜单切换、数值增加、数值减少、确认操作。按键一端接GND,另一端分别接到STM32的PA4、PA5、PA6、PA7引脚,同时每个引脚通过10KΩ电阻上拉到3.3V。这样未按下时引脚为高电平,按下时为低电平。
3.6 蜂鸣器接线
蜂鸣器通过S8050三极管驱动,STM32的PB1引脚串联1KΩ电阻到三极管基极,发射极接地,集电极接蜂鸣器负极,蜂鸣器正极接5V。
3.7 AT24C02 EEPROM接线
- VCC → 3.3V
- GND → GND
- SCL → PB6
- SDA → PB7
- A0、A1、A2 → GND(地址设为000)
- WP → GND(允许写入)
四、系统整体架构
在进入代码编写之前,我们先用Mermaid流程图展示系统的整体工作逻辑,帮助理解各模块之间的协作关系。
#mermaid-svg-LPlrZLCPrqSd7t0v{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LPlrZLCPrqSd7t0v .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LPlrZLCPrqSd7t0v .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LPlrZLCPrqSd7t0v .error-icon{fill:#552222;}#mermaid-svg-LPlrZLCPrqSd7t0v .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LPlrZLCPrqSd7t0v .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LPlrZLCPrqSd7t0v .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LPlrZLCPrqSd7t0v .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LPlrZLCPrqSd7t0v .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LPlrZLCPrqSd7t0v .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LPlrZLCPrqSd7t0v .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LPlrZLCPrqSd7t0v .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LPlrZLCPrqSd7t0v .marker.cross{stroke:#333333;}#mermaid-svg-LPlrZLCPrqSd7t0v svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LPlrZLCPrqSd7t0v p{margin:0;}#mermaid-svg-LPlrZLCPrqSd7t0v .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LPlrZLCPrqSd7t0v .cluster-label text{fill:#333;}#mermaid-svg-LPlrZLCPrqSd7t0v .cluster-label span{color:#333;}#mermaid-svg-LPlrZLCPrqSd7t0v .cluster-label span p{background-color:transparent;}#mermaid-svg-LPlrZLCPrqSd7t0v .label text,#mermaid-svg-LPlrZLCPrqSd7t0v span{fill:#333;color:#333;}#mermaid-svg-LPlrZLCPrqSd7t0v .node rect,#mermaid-svg-LPlrZLCPrqSd7t0v .node circle,#mermaid-svg-LPlrZLCPrqSd7t0v .node ellipse,#mermaid-svg-LPlrZLCPrqSd7t0v .node polygon,#mermaid-svg-LPlrZLCPrqSd7t0v .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LPlrZLCPrqSd7t0v .rough-node .label text,#mermaid-svg-LPlrZLCPrqSd7t0v .node .label text,#mermaid-svg-LPlrZLCPrqSd7t0v .image-shape .label,#mermaid-svg-LPlrZLCPrqSd7t0v .icon-shape .label{text-anchor:middle;}#mermaid-svg-LPlrZLCPrqSd7t0v .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LPlrZLCPrqSd7t0v .rough-node .label,#mermaid-svg-LPlrZLCPrqSd7t0v .node .label,#mermaid-svg-LPlrZLCPrqSd7t0v .image-shape .label,#mermaid-svg-LPlrZLCPrqSd7t0v .icon-shape .label{text-align:center;}#mermaid-svg-LPlrZLCPrqSd7t0v .node.clickable{cursor:pointer;}#mermaid-svg-LPlrZLCPrqSd7t0v .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LPlrZLCPrqSd7t0v .arrowheadPath{fill:#333333;}#mermaid-svg-LPlrZLCPrqSd7t0v .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LPlrZLCPrqSd7t0v .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LPlrZLCPrqSd7t0v .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LPlrZLCPrqSd7t0v .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LPlrZLCPrqSd7t0v .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LPlrZLCPrqSd7t0v .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LPlrZLCPrqSd7t0v .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LPlrZLCPrqSd7t0v .cluster text{fill:#333;}#mermaid-svg-LPlrZLCPrqSd7t0v .cluster span{color:#333;}#mermaid-svg-LPlrZLCPrqSd7t0v div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LPlrZLCPrqSd7t0v .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LPlrZLCPrqSd7t0v rect.text{fill:none;stroke-width:0;}#mermaid-svg-LPlrZLCPrqSd7t0v .icon-shape,#mermaid-svg-LPlrZLCPrqSd7t0v .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LPlrZLCPrqSd7t0v .icon-shape p,#mermaid-svg-LPlrZLCPrqSd7t0v .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LPlrZLCPrqSd7t0v .icon-shape .label rect,#mermaid-svg-LPlrZLCPrqSd7t0v .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LPlrZLCPrqSd7t0v .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LPlrZLCPrqSd7t0v .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LPlrZLCPrqSd7t0v :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 菜单键按下
加减键按下
确认键按下
无按键
是
否
是
否
系统上电启动
初始化外设
GPIO/定时器/I2C/DS18B20
读取EEPROM
累计费用数据
显示开机画面
OLED初始化完成
主循环开始
按键检测
切换设置项
目标温度/单价
修改当前设置值
温度±1/单价±0.1
保存设置到EEPROM
执行温控与计量
读取DS18B20
当前水温
读取流量传感器
脉冲累计值
计算瞬时流量
与累计用水量
PID控制器
计算加热输出
PID输出
是否大于阈值
继电器吸合
加热棒通电
继电器断开
停止加热
更新OLED显示
温度/流量/费用
水温是否
达到目标
蜂鸣器短鸣
提示可接水
五、开发环境搭建
5.1 软件工具准备
本项目使用Keil MDK-ARM V5作为开发环境。请确保安装以下组件:
- Keil MDK-ARM V5.36或更高版本
- STM32F1系列芯片支持包(在Keil的Pack Installer中安装)
- ST-Link调试器驱动程序
- 串口调试助手(用于调试输出)
5.2 创建工程
打开Keil软件,点击菜单栏的 Project → New μVision Project,选择合适的文件夹保存工程,命名为 SmartWaterDispenser。
在弹出的芯片选择窗口中,展开 STMicroelectronics,找到 STM32F103C8,点击OK。
在接下来弹出的 Manage Run-Time Environment 窗口中,我们需要勾选以下组件:
- CMSIS → CORE(必选)
- Device → Startup(必选)
- Device → StdPeriph Drivers → GPIO(外设驱动)
- Device → StdPeriph Drivers → RCC(时钟配置)
- Device → StdPeriph Drivers → TIM(定时器)
- Device → StdPeriph Drivers → USART(串口,调试用)
- Device → StdPeriph Drivers → I2C(I2C通信)
点击OK后,Keil会自动生成工程结构。
六、代码文件创建与编写
在工程目录下创建以下文件夹结构:
SmartWaterDispenser/
├── User/
│ ├── main.c
│ ├── main.h
│ ├── stm32f10x_conf.h
│ └── stm32f10x_it.c
├── Hardware/
│ ├── ds18b20.c
│ ├── ds18b20.h
│ ├── flow_sensor.c
│ ├── flow_sensor.h
│ ├── oled.c
│ ├── oled.h
│ ├── oled_font.h
│ ├── relay.c
│ ├── relay.h
│ ├── key.c
│ ├── key.h
│ ├── buzzer.c
│ ├── buzzer.h
│ ├── eeprom.c
│ └── eeprom.h
├── System/
│ ├── delay.c
│ ├── delay.h
│ ├── pid.c
│ └── pid.h
└── Output/
在Keil工程中,右键点击 Target 1 下的 Source Group 1,选择 Add New Group,分别创建 User、Hardware、System 三个组,然后将对应的 .c 文件添加进去。同时需要在工程选项的 C/C++ 标签页的 Include Paths 中添加上述所有文件夹的路径。
6.1 文件:User/main.h
这是主头文件,包含所有全局定义和外部函数声明。
c
/**
* @file main.h
* @brief 智能饮水机主头文件
* @author SmartWater Team
* @date 2026-05-29
*/
#ifndef __MAIN_H
#define __MAIN_H
#include "stm32f10x.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
/* ==================== 系统参数宏定义 ==================== */
/* 温度相关 */
#define DEFAULT_TARGET_TEMP 85 /* 默认目标温度(摄氏度) */
#define TEMP_MIN 40 /* 最低可设温度 */
#define TEMP_MAX 100 /* 最高可设温度 */
#define TEMP_HYSTERESIS 1 /* 温度回差 */
/* 流量计费相关 */
#define FLOW_PULSE_PER_LITER 450 /* YF-S201: 450个脉冲/升 */
#define DEFAULT_PRICE_PER_LITER 50 /* 默认单价(分/升),即0.50元/升 */
#define PRICE_MIN 10 /* 最低单价(分) */
#define PRICE_MAX 500 /* 最高单价(分) */
/* EEPROM地址定义 */
#define EEPROM_ADDR_TOTAL_COST 0x00 /* 累计费用存储起始地址(4字节) */
#define EEPROM_ADDR_PRICE 0x04 /* 单价存储地址(2字节) */
#define EEPROM_ADDR_TARGET_TEMP 0x06 /* 目标温度存储地址(1字节) */
/* 引脚定义 */
/* DS18B20 */
#define DS18B20_PORT GPIOA
#define DS18B20_PIN GPIO_Pin_0
#define DS18B20_RCC RCC_APB2Periph_GPIOA
/* 流量传感器 */
#define FLOW_PORT GPIOA
#define FLOW_PIN GPIO_Pin_1
#define FLOW_RCC RCC_APB2Periph_GPIOA
/* 继电器 */
#define RELAY_PORT GPIOB
#define RELAY_PIN GPIO_Pin_0
#define RELAY_RCC RCC_APB2Periph_GPIOB
/* 蜂鸣器 */
#define BUZZER_PORT GPIOB
#define BUZZER_PIN GPIO_Pin_1
#define BUZZER_RCC RCC_APB2Periph_GPIOB
/* 按键 */
#define KEY_PORT GPIOA
#define KEY_MENU_PIN GPIO_Pin_4
#define KEY_UP_PIN GPIO_Pin_5
#define KEY_DOWN_PIN GPIO_Pin_6
#define KEY_OK_PIN GPIO_Pin_7
#define KEY_RCC RCC_APB2Periph_GPIOA
/* I2C (OLED + EEPROM共用) */
#define I2C_PORT GPIOB
#define I2C_SCL_PIN GPIO_Pin_6
#define I2C_SDA_PIN GPIO_Pin_7
#define I2C_RCC RCC_APB2Periph_GPIOB
/* ==================== 系统状态结构体 ==================== */
/**
* @brief 系统运行状态结构体
*/
typedef struct {
/* 温度相关 */
float currentTemp; /* 当前水温 */
uint8_t targetTemp; /* 目标温度 */
uint8_t tempReached; /* 温度是否达标标志 */
/* 流量相关 */
volatile uint32_t flowPulseCount; /* 脉冲累计计数 */
float flowRate; /* 瞬时流量(L/min) */
float totalVolume; /* 本次用水总量(L) */
float lastVolume; /* 上一次累计量,用于计算单次 */
/* 计费相关 */
uint16_t pricePerLiter; /* 单价(分/升) */
float currentCost; /* 本次费用(元) */
uint32_t totalCostCents; /* 历史累计费用(分) */
/* PID相关 */
float pidOutput; /* PID输出值 */
/* 显示相关 */
uint8_t displayMode; /* 显示模式 0:主界面 1:温度设置 2:单价设置 */
uint8_t displayRefresh; /* 显示刷新标志 */
/* 按键相关 */
uint8_t keyValue; /* 当前按键值 */
uint8_t keyPressed; /* 按键按下标志 */
/* 系统运行 */
uint32_t systemTick; /* 系统运行时间(ms) */
uint8_t heaterStatus; /* 加热器状态 0:关闭 1:开启 */
} SystemState_t;
/* ==================== 全局变量声明 ==================== */
extern SystemState_t g_sysState;
extern volatile uint32_t g_flowPulseIRQ;
/* ==================== 函数声明 ==================== */
void SystemClock_Config(void);
void GPIO_Configuration(void);
void TIM_Configuration(void);
void I2C_Configuration(void);
void USART_Configuration(void);
void SysTick_Configuration(void);
void NVIC_Configuration(void);
#endif /* __MAIN_H */
6.2 文件:User/main.c
这是程序的主入口文件,包含初始化代码和主循环逻辑。
c
/**
* @file main.c
* @brief 智能饮水机主程序
* @author SmartWater Team
* @date 2026-05-29
*/
#include "main.h"
#include "delay.h"
#include "ds18b20.h"
#include "flow_sensor.h"
#include "oled.h"
#include "relay.h"
#include "key.h"
#include "buzzer.h"
#include "eeprom.h"
#include "pid.h"
/* ==================== 全局变量定义 ==================== */
SystemState_t g_sysState; /* 系统状态 */
volatile uint32_t g_flowPulseIRQ = 0; /* 中断中累计的流量脉冲 */
/* ==================== 静态函数声明 ==================== */
static void LoadSystemConfig(void);
static void SaveSystemConfig(void);
static void ProcessKeyInput(void);
static void UpdateDisplay(void);
static void TemperatureControl(void);
static void FlowCalculation(void);
static void CostCalculation(void);
/**
* @brief 主函数
* @param 无
* @retval int
*/
int main(void)
{
/* 系统初始化 */
SystemClock_Config(); /* 配置系统时钟为72MHz */
SysTick_Configuration(); /* 配置SysTick定时器 */
GPIO_Configuration(); /* 配置GPIO */
USART_Configuration(); /* 配置串口(调试用) */
I2C_Configuration(); /* 配置I2C */
TIM_Configuration(); /* 配置定时器 */
NVIC_Configuration(); /* 配置中断优先级 */
/* 外设模块初始化 */
Delay_Init(); /* 延时函数初始化 */
DS18B20_Init(); /* DS18B20初始化 */
FlowSensor_Init(); /* 流量传感器初始化 */
OLED_Init(); /* OLED初始化 */
Relay_Init(); /* 继电器初始化 */
Key_Init(); /* 按键初始化 */
Buzzer_Init(); /* 蜂鸣器初始化 */
EEPROM_Init(); /* EEPROM初始化 */
PID_Init(); /* PID控制器初始化 */
/* 设置PID参数 */
PID_SetTunings(80.0f, 2.0f, 5.0f); /* Kp=80, Ki=2, Kd=5 */
PID_SetOutputLimits(0.0f, 1000.0f); /* 输出限幅0-1000 */
PID_SetSampleTime(1000); /* 采样时间1000ms */
/* 加载系统配置 */
LoadSystemConfig();
/* 初始化系统状态 */
g_sysState.currentTemp = 25.0f;
g_sysState.flowPulseCount = 0;
g_sysState.flowRate = 0.0f;
g_sysState.totalVolume = 0.0f;
g_sysState.lastVolume = 0.0f;
g_sysState.currentCost = 0.0f;
g_sysState.displayMode = 0;
g_sysState.displayRefresh = 1;
g_sysState.heaterStatus = 0;
g_sysState.tempReached = 0;
/* 显示开机画面 */
OLED_Clear();
OLED_ShowString(0, 0, "Smart Water", 16);
OLED_ShowString(0, 2, "Dispenser V1.0", 16);
OLED_ShowString(0, 4, "Initializing...", 16);
OLED_Refresh();
Delay_Ms(2000);
/* 输出调试信息 */
printf("\r\n=================================\r\n");
printf("Smart Water Dispenser Started\r\n");
printf("Target Temp: %d C\r\n", g_sysState.targetTemp);
printf("Price: %d.%02d Yuan/L\r\n",
g_sysState.pricePerLiter / 100,
g_sysState.pricePerLiter % 100);
printf("Total Cost: %ld.%02ld Yuan\r\n",
g_sysState.totalCostCents / 100,
g_sysState.totalCostCents % 100);
printf("=================================\r\n\r\n");
/* 主循环 */
while (1)
{
/* 更新系统时钟 */
g_sysState.systemTick++;
/* 按键扫描与处理 */
Key_Scan();
ProcessKeyInput();
/* 温度读取(每500ms读取一次) */
if (g_sysState.systemTick % 500 == 0)
{
g_sysState.currentTemp = DS18B20_GetTemp();
printf("Current Temp: %.1f C\r\n", g_sysState.currentTemp);
}
/* 温度控制(每1000ms执行一次PID) */
if (g_sysState.systemTick % 1000 == 0)
{
TemperatureControl();
}
/* 流量计算(每100ms更新) */
if (g_sysState.systemTick % 100 == 0)
{
FlowCalculation();
}
/* 费用计算(每500ms更新) */
if (g_sysState.systemTick % 500 == 0)
{
CostCalculation();
}
/* 显示更新(每200ms刷新) */
if (g_sysState.systemTick % 200 == 0)
{
UpdateDisplay();
}
/* 每10秒保存一次累计费用到EEPROM */
if (g_sysState.systemTick % 10000 == 0)
{
SaveSystemConfig();
}
/* 系统时钟溢出处理(约49天后溢出) */
if (g_sysState.systemTick > 0xFFFF0000)
{
g_sysState.systemTick = 10000;
}
}
}
/**
* @brief 加载系统配置(从EEPROM读取)
* @param 无
* @retval 无
*/
static void LoadSystemConfig(void)
{
uint8_t tempData;
uint16_t priceData;
uint32_t costData;
/* 读取目标温度 */
tempData = EEPROM_ReadByte(EEPROM_ADDR_TARGET_TEMP);
if (tempData >= TEMP_MIN && tempData <= TEMP_MAX)
{
g_sysState.targetTemp = tempData;
}
else
{
g_sysState.targetTemp = DEFAULT_TARGET_TEMP;
EEPROM_WriteByte(EEPROM_ADDR_TARGET_TEMP, DEFAULT_TARGET_TEMP);
}
/* 读取单价 */
EEPROM_ReadBuffer(EEPROM_ADDR_PRICE, (uint8_t*)&priceData, 2);
if (priceData >= PRICE_MIN && priceData <= PRICE_MAX)
{
g_sysState.pricePerLiter = priceData;
}
else
{
g_sysState.pricePerLiter = DEFAULT_PRICE_PER_LITER;
EEPROM_WriteBuffer(EEPROM_ADDR_PRICE, (uint8_t*)&g_sysState.pricePerLiter, 2);
}
/* 读取累计费用 */
EEPROM_ReadBuffer(EEPROM_ADDR_TOTAL_COST, (uint8_t*)&costData, 4);
if (costData < 100000000) /* 合理性检查: 不超过100万元 */
{
g_sysState.totalCostCents = costData;
}
else
{
g_sysState.totalCostCents = 0;
EEPROM_WriteBuffer(EEPROM_ADDR_TOTAL_COST, (uint8_t*)&g_sysState.totalCostCents, 4);
}
}
/**
* @brief 保存系统配置到EEPROM
* @param 无
* @retval 无
*/
static void SaveSystemConfig(void)
{
uint16_t priceData = g_sysState.pricePerLiter;
EEPROM_WriteByte(EEPROM_ADDR_TARGET_TEMP, g_sysState.targetTemp);
EEPROM_WriteBuffer(EEPROM_ADDR_PRICE, (uint8_t*)&priceData, 2);
EEPROM_WriteBuffer(EEPROM_ADDR_TOTAL_COST, (uint8_t*)&g_sysState.totalCostCents, 4);
printf("Config Saved to EEPROM\r\n");
}
/**
* @brief 处理按键输入
* @param 无
* @retval 无
*/
static void ProcessKeyInput(void)
{
uint8_t key = g_sysState.keyValue;
if (key == 0) return; /* 无按键按下 */
switch (g_sysState.displayMode)
{
case 0: /* 主界面模式 */
if (key == KEY_MENU)
{
g_sysState.displayMode = 1; /* 进入温度设置 */
g_sysState.displayRefresh = 1;
Buzzer_Beep(50); /* 短鸣50ms */
}
break;
case 1: /* 温度设置模式 */
if (key == KEY_UP)
{
if (g_sysState.targetTemp < TEMP_MAX)
{
g_sysState.targetTemp++;
g_sysState.displayRefresh = 1;
}
}
else if (key == KEY_DOWN)
{
if (g_sysState.targetTemp > TEMP_MIN)
{
g_sysState.targetTemp--;
g_sysState.displayRefresh = 1;
}
}
else if (key == KEY_OK)
{
SaveSystemConfig();
g_sysState.displayMode = 2; /* 进入单价设置 */
g_sysState.displayRefresh = 1;
Buzzer_Beep(100); /* 长鸣确认 */
}
else if (key == KEY_MENU)
{
g_sysState.displayMode = 2; /* 跳到单价设置 */
g_sysState.displayRefresh = 1;
}
break;
case 2: /* 单价设置模式 */
if (key == KEY_UP)
{
if (g_sysState.pricePerLiter < PRICE_MAX)
{
g_sysState.pricePerLiter += 10; /* 每次加0.1元 */
g_sysState.displayRefresh = 1;
}
}
else if (key == KEY_DOWN)
{
if (g_sysState.pricePerLiter > PRICE_MIN)
{
g_sysState.pricePerLiter -= 10; /* 每次减0.1元 */
g_sysState.displayRefresh = 1;
}
}
else if (key == KEY_OK)
{
SaveSystemConfig();
g_sysState.displayMode = 0; /* 返回主界面 */
g_sysState.displayRefresh = 1;
Buzzer_Beep(100);
}
else if (key == KEY_MENU)
{
g_sysState.displayMode = 0; /* 返回主界面 */
g_sysState.displayRefresh = 1;
}
break;
default:
g_sysState.displayMode = 0;
break;
}
g_sysState.keyValue = 0; /* 清除按键值 */
}
/**
* @brief 更新OLED显示
* @param 无
* @retval 无
*/
static void UpdateDisplay(void)
{
char strBuf[22]; /* 显示缓冲区 */
if (g_sysState.displayRefresh == 0) return;
g_sysState.displayRefresh = 0;
OLED_Clear();
switch (g_sysState.displayMode)
{
case 0: /* 主界面 */
/* 第一行: 当前温度 */
sprintf(strBuf, "Temp: %.1f C", g_sysState.currentTemp);
OLED_ShowString(0, 0, strBuf, 16);
/* 第二行: 目标温度和加热状态 */
if (g_sysState.heaterStatus)
{
sprintf(strBuf, "Target:%dC [HEAT]", g_sysState.targetTemp);
}
else
{
sprintf(strBuf, "Target:%dC [IDLE]", g_sysState.targetTemp);
}
OLED_ShowString(0, 1, strBuf, 16);
/* 第三行: 流量和流速 */
sprintf(strBuf, "Flow:%.2fL %.1fL/m",
g_sysState.totalVolume,
g_sysState.flowRate);
OLED_ShowString(0, 2, strBuf, 16);
/* 第四行: 本次费用 */
sprintf(strBuf, "Cost: %.2f Yuan", g_sysState.currentCost);
OLED_ShowString(0, 3, strBuf, 16);
/* 第五行: 累计总费用 */
sprintf(strBuf, "Total: %ld.%02ld Yuan",
g_sysState.totalCostCents / 100,
g_sysState.totalCostCents % 100);
OLED_ShowString(0, 4, strBuf, 16);
/* 第六行: 状态提示 */
if (g_sysState.tempReached)
{
OLED_ShowString(0, 5, ">> READY <<", 16);
}
else
{
OLED_ShowString(0, 5, "Heating...", 16);
}
/* 第七行: 按键提示 */
OLED_ShowString(0, 7, "MENU:Settings", 16);
break;
case 1: /* 温度设置界面 */
OLED_ShowString(0, 0, "== SET TEMP ==", 16);
OLED_ShowString(0, 1, "", 16);
sprintf(strBuf, " Target: %d C", g_sysState.targetTemp);
OLED_ShowString(0, 2, strBuf, 16);
sprintf(strBuf, " Current: %.1f C", g_sysState.currentTemp);
OLED_ShowString(0, 3, strBuf, 16);
OLED_ShowString(0, 4, "", 16);
OLED_ShowString(0, 5, "UP/DOWN:Adjust", 16);
OLED_ShowString(0, 6, "OK:Next MENU:Skip", 16);
OLED_ShowString(0, 7, "Range:40-100 C", 16);
break;
case 2: /* 单价设置界面 */
OLED_ShowString(0, 0, "== SET PRICE ==", 16);
OLED_ShowString(0, 1, "", 16);
sprintf(strBuf, "Price: %d.%02d Yuan/L",
g_sysState.pricePerLiter / 100,
g_sysState.pricePerLiter % 100);
OLED_ShowString(0, 2, strBuf, 16);
sprintf(strBuf, "Total: %ld.%02ld Yuan",
g_sysState.totalCostCents / 100,
g_sysState.totalCostCents % 100);
OLED_ShowString(0, 3, strBuf, 16);
OLED_ShowString(0, 4, "", 16);
OLED_ShowString(0, 5, "UP/DOWN:Adjust", 16);
OLED_ShowString(0, 6, "OK:Save MENU:Back", 16);
OLED_ShowString(0, 7, "Step:0.10 Yuan", 16);
break;
default:
break;
}
OLED_Refresh();
}
/**
* @brief 温度PID控制
* @param 无
* @retval 无
*/
static void TemperatureControl(void)
{
float pidResult;
/* 设置PID目标值 */
PID_SetSetpoint((float)g_sysState.targetTemp);
/* 计算PID输出 */
pidResult = PID_Compute(g_sysState.currentTemp);
g_sysState.pidOutput = pidResult;
/* 根据PID输出控制继电器 */
if (pidResult > 100.0f) /* 输出大于阈值,开启加热 */
{
Relay_On();
g_sysState.heaterStatus = 1;
}
else /* 输出小于阈值,关闭加热 */
{
Relay_Off();
g_sysState.heaterStatus = 0;
}
/* 检查是否达到目标温度(允许±0.5度误差) */
if (g_sysState.currentTemp >= (g_sysState.targetTemp - 0.5f))
{
if (g_sysState.tempReached == 0)
{
g_sysState.tempReached = 1;
Buzzer_Beep(200); /* 温度达标提示音 */
printf("Target temperature reached!\r\n");
}
}
else
{
g_sysState.tempReached = 0;
}
/* 调试输出 */
printf("PID: Target=%d, Current=%.1f, Output=%.1f, Heater=%d\r\n",
g_sysState.targetTemp,
g_sysState.currentTemp,
pidResult,
g_sysState.heaterStatus);
}
/**
* @brief 流量计算(从脉冲数转换为流量值)
* @param 无
* @retval 无
*/
static void FlowCalculation(void)
{
static uint32_t lastPulseCount = 0;
static uint32_t lastCalcTime = 0;
uint32_t currentPulse;
uint32_t pulseDiff;
uint32_t currentTime;
float timeDiffSec;
/* 获取当前累计脉冲数(包含中断中的计数) */
currentPulse = g_sysState.flowPulseCount + g_flowPulseIRQ;
currentTime = g_sysState.systemTick;
/* 计算时间差(秒) */
if (currentTime > lastCalcTime)
{
timeDiffSec = (float)(currentTime - lastCalcTime) / 1000.0f;
}
else
{
timeDiffSec = 0.1f;
}
/* 计算脉冲差值 */
if (currentPulse >= lastPulseCount)
{
pulseDiff = currentPulse - lastPulseCount;
}
else
{
/* 处理计数器溢出 */
pulseDiff = (0xFFFFFFFF - lastPulseCount) + currentPulse + 1;
}
/* 计算瞬时流量(L/min) */
if (timeDiffSec > 0 && pulseDiff > 0)
{
g_sysState.flowRate = ((float)pulseDiff / (float)FLOW_PULSE_PER_LITER)
* (60.0f / timeDiffSec);
}
else if (timeDiffSec > 1.0f)
{
/* 长时间无脉冲,流量归零 */
g_sysState.flowRate = 0.0f;
}
/* 计算累计总用水量(L) */
g_sysState.totalVolume = (float)currentPulse / (float)FLOW_PULSE_PER_LITER;
/* 更新记录 */
lastPulseCount = currentPulse;
lastCalcTime = currentTime;
/* 将中断计数同步到主计数变量 */
__disable_irq();
g_sysState.flowPulseCount += g_flowPulseIRQ;
g_flowPulseIRQ = 0;
__enable_irq();
}
/**
* @brief 费用计算
* @param 无
* @retval 无
*/
static void CostCalculation(void)
{
float newVolume;
float volumeIncrement;
float costIncrement;
uint32_t costIncrementCents;
/* 计算本次增量 */
newVolume = g_sysState.totalVolume;
if (newVolume > g_sysState.lastVolume)
{
volumeIncrement = newVolume - g_sysState.lastVolume;
/* 计算费用增量(元) */
costIncrement = volumeIncrement * ((float)g_sysState.pricePerLiter / 100.0f);
/* 累加到本次费用 */
g_sysState.currentCost += costIncrement;
/* 转换为分,累加到总费用 */
costIncrementCents = (uint32_t)(costIncrement * 100.0f + 0.5f);
g_sysState.totalCostCents += costIncrementCents;
/* 更新上一次累计量 */
g_sysState.lastVolume = newVolume;
/* 调试输出 */
if (volumeIncrement > 0.001f)
{
printf("Volume: +%.3fL, Cost: +%.3f Yuan, Total: %ld.%02ld Yuan\r\n",
volumeIncrement,
costIncrement,
g_sysState.totalCostCents / 100,
g_sysState.totalCostCents % 100);
}
}
}
/**
* @brief 系统时钟配置: HSE 8MHz -> PLL 72MHz
* @param 无
* @retval 无
*/
void SystemClock_Config(void)
{
ErrorStatus HSEStartUpStatus;
/* 复位RCC配置 */
RCC_DeInit();
/* 使能外部高速晶振 */
RCC_HSEConfig(RCC_HSE_ON);
/* 等待HSE就绪 */
HSEStartUpStatus = RCC_WaitForHSEStartUp();
if (HSEStartUpStatus == SUCCESS)
{
/* 使能预取缓冲 */
FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable);
/* 设置FLASH等待周期: 72MHz需要2个等待周期 */
FLASH_SetLatency(FLASH_Latency_2);
/* 设置AHB预分频器: HCLK = SYSCLK */
RCC_HCLKConfig(RCC_SYSCLK_Div1);
/* 设置APB2预分频器: PCLK2 = HCLK */
RCC_PCLK2Config(RCC_HCLK_Div1);
/* 设置APB1预分频器: PCLK1 = HCLK/2 */
RCC_PCLK1Config(RCC_HCLK_Div2);
/* 配置PLL: 8MHz * 9 = 72MHz */
RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9);
/* 使能PLL */
RCC_PLLCmd(ENABLE);
/* 等待PLL就绪 */
while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);
/* 选择PLL作为系统时钟 */
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
/* 等待PLL成为系统时钟 */
while (RCC_GetSYSCLKSource() != 0x08);
}
else
{
/* HSE启动失败,使用HSI */
while (1);
}
}
/**
* @brief GPIO配置
* @param 无
* @retval 无
*/
void GPIO_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* 使能各端口时钟 */
RCC_APB2PeriphClockCmd(DS18B20_RCC | FLOW_RCC | RELAY_RCC |
BUZZER_RCC | KEY_RCC | I2C_RCC, ENABLE);
/* 配置DS18B20引脚(推挽输出,用于单总线通信) */
GPIO_InitStructure.GPIO_Pin = DS18B20_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DS18B20_PORT, &GPIO_InitStructure);
/* 配置流量传感器引脚(浮空输入,用于脉冲捕获) */
GPIO_InitStructure.GPIO_Pin = FLOW_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(FLOW_PORT, &GPIO_InitStructure);
/* 配置继电器引脚(推挽输出) */
GPIO_InitStructure.GPIO_Pin = RELAY_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(RELAY_PORT, &GPIO_InitStructure);
GPIO_ResetBits(RELAY_PORT, RELAY_PIN); /* 初始关闭 */
/* 配置蜂鸣器引脚(推挽输出) */
GPIO_InitStructure.GPIO_Pin = BUZZER_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(BUZZER_PORT, &GPIO_InitStructure);
GPIO_ResetBits(BUZZER_PORT, BUZZER_PIN); /* 初始关闭 */
/* 配置按键引脚(上拉输入) */
GPIO_InitStructure.GPIO_Pin = KEY_MENU_PIN | KEY_UP_PIN |
KEY_DOWN_PIN | KEY_OK_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(KEY_PORT, &GPIO_InitStructure);
/* 配置I2C引脚(开漏输出) */
GPIO_InitStructure.GPIO_Pin = I2C_SCL_PIN | I2C_SDA_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(I2C_PORT, &GPIO_InitStructure);
}
/**
* @brief 定时器配置(用于流量脉冲计数)
* @param 无
* @retval 无
*/
void TIM_Configuration(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
/* 使能TIM2时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
/* 配置TIM2时基: 用于定时读取流量 */
TIM_TimeBaseStructure.TIM_Period = 999; /* 自动重装载值 */
TIM_TimeBaseStructure.TIM_Prescaler = 71; /* 预分频: 72MHz/72=1MHz, 1us计数 */
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
/* 使能TIM2更新中断 */
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
/* 配置NVIC */
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
/* 使能TIM2 */
TIM_Cmd(TIM2, ENABLE);
}
/**
* @brief I2C配置(软件模拟I2C)
* @param 无
* @retval 无
*/
void I2C_Configuration(void)
{
/* I2C引脚已在GPIO配置中初始化 */
/* 设置初始状态为高电平 */
GPIO_SetBits(I2C_PORT, I2C_SCL_PIN | I2C_SDA_PIN);
}
/**
* @brief 串口配置(调试用)
* @param 无
* @retval 无
*/
void USART_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
/* 使能USART1和GPIOA时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
/* 配置USART1 TX(PA9)为复用推挽输出 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 配置USART1 RX(PA10)为浮空输入 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 配置USART1参数 */
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
/* 使能USART1 */
USART_Cmd(USART1, ENABLE);
}
/**
* @brief SysTick配置(1ms中断)
* @param 无
* @retval 无
*/
void SysTick_Configuration(void)
{
/* 配置SysTick为1ms中断 */
if (SysTick_Config(SystemCoreClock / 1000))
{
/* 配置失败,陷入死循环 */
while (1);
}
/* 设置SysTick优先级 */
NVIC_SetPriority(SysTick_IRQn, 0);
}
/**
* @brief NVIC配置
* @param 无
* @retval 无
*/
void NVIC_Configuration(void)
{
/* 设置优先级分组为2: 2位抢占优先级, 2位子优先级 */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
}
/**
* @brief SysTick中断处理函数
* @param 无
* @retval 无
*/
void SysTick_Handler(void)
{
g_sysState.systemTick++;
/* 按键扫描(每10ms) */
if (g_sysState.systemTick % 10 == 0)
{
Key_Scan();
}
}
/**
* @brief TIM2中断处理函数
* @param 无
* @retval 无
*/
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
/* 检查流量传感器引脚状态,统计脉冲 */
if (GPIO_ReadInputDataBit(FLOW_PORT, FLOW_PIN) == Bit_SET)
{
g_flowPulseIRQ++;
}
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
/**
* @brief 重定向printf到USART1
* @param ch: 要发送的字符
* @retval int
*/
int fputc(int ch, FILE *f)
{
/* 等待发送缓冲区为空 */
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
/* 发送字符 */
USART_SendData(USART1, (uint8_t)ch);
return ch;
}
6.3 文件:System/delay.h
延时函数头文件,提供微秒级和毫秒级延时。
c
/**
* @file delay.h
* @brief 延时函数头文件
* @author SmartWater Team
* @date 2026-05-29
*/
#ifndef __DELAY_H
#define __DELAY_H
#include "stm32f10x.h"
void Delay_Init(void);
void Delay_Us(uint32_t nus);
void Delay_Ms(uint32_t nms);
#endif /* __DELAY_H */
6.4 文件:System/delay.c
延时函数实现,利用SysTick实现精确延时。
c
/**
* @file delay.c
* @brief 延时函数实现
* @author SmartWater Team
* @date 2026-05-29
*/
#include "delay.h"
static uint32_t g_fac_us = 0; /* 微秒延时因子 */
/**
* @brief 延时初始化
* @param 无
* @retval 无
*/
void Delay_Init(void)
{
/* 配置SysTick(已在main中配置) */
/* 计算微秒延时因子: SystemCoreClock/1000000 */
g_fac_us = SystemCoreClock / 1000000;
}
/**
* @brief 微秒级延时
* @param nus: 延时的微秒数(最大值约798915)
* @retval 无
*/
void Delay_Us(uint32_t nus)
{
uint32_t temp;
/* 重载值 = 延时因子 * 微秒数 */
SysTick->LOAD = nus * g_fac_us;
/* 清空计数器 */
SysTick->VAL = 0x00;
/* 使能SysTick, 使用HCLK时钟, 不产生中断 */
SysTick->CTRL = SysTick_CTRL_ENABLE_Msk;
/* 等待计数结束 */
do
{
temp = SysTick->CTRL;
}
while ((temp & 0x01) && !(temp & (1 << 16)));
/* 关闭SysTick */
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
SysTick->VAL = 0x00;
}
/**
* @brief 毫秒级延时
* @param nms: 延时的毫秒数
* @retval 无
*/
void Delay_Ms(uint32_t nms)
{
uint32_t i;
for (i = 0; i < nms; i++)
{
Delay_Us(1000);
}
}
6.5 文件:System/pid.h
PID控制器头文件。
c
/**
* @file pid.h
* @brief PID控制器头文件
* @author SmartWater Team
* @date 2026-05-29
*/
#ifndef __PID_H
#define __PID_H
#include "stm32f10x.h"
void PID_Init(void);
void PID_SetTunings(float kp, float ki, float kd);
void PID_SetOutputLimits(float min, float max);
void PID_SetSampleTime(uint32_t sampleTimeMs);
void PID_SetSetpoint(float setpoint);
float PID_Compute(float input);
void PID_Reset(void);
#endif /* __PID_H */
6.6 文件:System/pid.c
PID控制器实现,采用位置式PID算法,带积分分离和输出限幅。
c
/**
* @file pid.c
* @brief PID控制器实现
* @author SmartWater Team
* @date 2026-05-29
*/
#include "pid.h"
#include <string.h>
/* ==================== PID控制器结构体 ==================== */
typedef struct {
float kp; /* 比例系数 */
float ki; /* 积分系数 */
float kd; /* 微分系数 */
float setpoint; /* 目标值 */
float input; /* 当前输入值 */
float output; /* 输出值 */
float outMin; /* 输出下限 */
float outMax; /* 输出上限 */
float integral; /* 积分累加值 */
float lastInput; /* 上一次输入值 */
float lastError; /* 上一次误差 */
uint32_t lastTime; /* 上一次计算时间 */
uint32_t sampleTime; /* 采样时间(ms) */
uint8_t autoMode; /* 自动模式标志 */
} PID_t;
/* ==================== 静态变量 ==================== */
static PID_t g_pid;
/* 积分分离阈值: 误差大于此值时取消积分作用 */
#define PID_INTEGRAL_SEPARATION_THRESHOLD 10.0f
/**
* @brief PID初始化
* @param 无
* @retval 无
*/
void PID_Init(void)
{
memset(&g_pid, 0, sizeof(PID_t));
/* 默认参数 */
g_pid.kp = 50.0f;
g_pid.ki = 1.0f;
g_pid.kd = 3.0f;
g_pid.setpoint = 85.0f;
g_pid.outMin = 0.0f;
g_pid.outMax = 1000.0f;
g_pid.sampleTime = 1000; /* 默认1000ms采样 */
g_pid.autoMode = 1;
g_pid.lastTime = 0;
PID_Reset();
}
/**
* @brief 设置PID调节参数
* @param kp: 比例系数
* @param ki: 积分系数
* @param kd: 微分系数
* @retval 无
*/
void PID_SetTunings(float kp, float ki, float kd)
{
/* 参数有效性检查 */
if (kp < 0 || ki < 0 || kd < 0) return;
g_pid.kp = kp;
g_pid.ki = ki;
g_pid.kd = kd;
}
/**
* @brief 设置输出限幅
* @param min: 输出下限
* @param max: 输出上限
* @retval 无
*/
void PID_SetOutputLimits(float min, float max)
{
if (min >= max) return;
g_pid.outMin = min;
g_pid.outMax = max;
/* 限制当前输出 */
if (g_pid.output > max) g_pid.output = max;
if (g_pid.output < min) g_pid.output = min;
/* 限制积分值 */
if (g_pid.integral > max) g_pid.integral = max;
if (g_pid.integral < min) g_pid.integral = min;
}
/**
* @brief 设置采样时间
* @param sampleTimeMs: 采样时间(毫秒)
* @retval 无
*/
void PID_SetSampleTime(uint32_t sampleTimeMs)
{
if (sampleTimeMs < 1) return;
/* 根据采样时间调整积分和微分系数 */
float ratio = (float)sampleTimeMs / (float)g_pid.sampleTime;
g_pid.ki *= ratio;
g_pid.kd /= ratio;
g_pid.sampleTime = sampleTimeMs;
}
/**
* @brief 设置目标值
* @param setpoint: 目标值
* @retval 无
*/
void PID_SetSetpoint(float setpoint)
{
g_pid.setpoint = setpoint;
}
/**
* @brief PID计算
* @param input: 当前输入值(实际温度)
* @retval float: PID输出值
*/
float PID_Compute(float input)
{
float error;
float dInput;
float pTerm, iTerm, dTerm;
uint32_t now;
if (!g_pid.autoMode) return g_pid.output;
/* 获取当前时间(使用系统tick) */
now = 0; /* 由外部传入,这里简化处理 */
/* 检查采样时间 */
/* 注意: 在实际项目中,now应由外部传入的系统tick替代 */
/* 这里简化处理,假设每次调用都进行计算 */
g_pid.input = input;
/* 计算误差 */
error = g_pid.setpoint - g_pid.input;
/* === 比例项 === */
pTerm = g_pid.kp * error;
/* === 积分项(带积分分离) === */
if (fabs(error) < PID_INTEGRAL_SEPARATION_THRESHOLD)
{
/* 误差较小时启用积分 */
g_pid.integral += g_pid.ki * error;
/* 积分限幅 */
if (g_pid.integral > g_pid.outMax) g_pid.integral = g_pid.outMax;
if (g_pid.integral < g_pid.outMin) g_pid.integral = g_pid.outMin;
iTerm = g_pid.integral;
}
else
{
/* 误差较大时取消积分作用,防止积分饱和 */
iTerm = 0;
g_pid.integral = 0;
}
/* === 微分项(微分先行,避免设定值突变引起的冲击) === */
dInput = g_pid.input - g_pid.lastInput;
dTerm = -g_pid.kd * dInput;
/* 更新上一次输入值 */
g_pid.lastInput = g_pid.input;
/* === 计算总输出 === */
g_pid.output = pTerm + iTerm + dTerm;
/* 输出限幅 */
if (g_pid.output > g_pid.outMax) g_pid.output = g_pid.outMax;
if (g_pid.output < g_pid.outMin) g_pid.output = g_pid.outMin;
/* 输出小于10时强制为0,避免继电器频繁动作 */
if (g_pid.output < 10.0f) g_pid.output = 0.0f;
return g_pid.output;
}
/**
* @brief 重置PID状态
* @param 无
* @retval 无
*/
void PID_Reset(void)
{
g_pid.integral = 0;
g_pid.lastInput = 0;
g_pid.lastError = 0;
g_pid.output = 0;
}
/**
* @brief 浮点数绝对值函数(避免使用math库)
* @param x: 浮点数
* @retval float: 绝对值
*/
static float fabs(float x)
{
return (x >= 0) ? x : -x;
}
6.7 文件:Hardware/ds18b20.h
DS18B20温度传感器驱动头文件。
c
/**
* @file ds18b20.h
* @brief DS18B20温度传感器驱动头文件
* @author SmartWater Team
* @date 2026-05-29
*/
#ifndef __DS18B20_H
#define __DS18B20_H
#include "stm32f10x.h"
/* DS18B20命令定义 */
#define DS18B20_CMD_CONVERT_TEMP 0x44 /* 启动温度转换 */
#define DS18B20_CMD_READ_SCRATCHPAD 0xBE /* 读取暂存器 */
#define DS18B20_CMD_WRITE_SCRATCHPAD 0x4E /* 写入暂存器 */
#define DS18B20_CMD_COPY_SCRATCHPAD 0x48 /* 复制暂存器到EEPROM */
#define DS18B20_CMD_RECALL_E2 0xB8 /* 从EEPROM调用数据 */
#define DS18B20_CMD_SKIP_ROM 0xCC /* 跳过ROM匹配 */
#define DS18B20_CMD_MATCH_ROM 0x55 /* 匹配ROM */
void DS18B20_Init(void);
float DS18B20_GetTemp(void);
uint8_t DS18B20_ReadScratchpad(uint8_t *buf);
void DS18B20_WriteScratchpad(uint8_t th, uint8_t tl, uint8_t config);
#endif /* __DS18B20_H */
6.8 文件:Hardware/ds18b20.c
DS18B20驱动实现,包含单总线复位、读写位、读写字节等基础操作以及温度转换读取功能。
c
/**
* @file ds18b20.c
* @brief DS18B20温度传感器驱动实现
* @author SmartWater Team
* @date 2026-05-29
*/
#include "ds18b20.h"
#include "delay.h"
#include "main.h"
/* ==================== 宏定义 ==================== */
/* 单总线操作宏 */
#define DQ_OUT_HIGH() GPIO_SetBits(DS18B20_PORT, DS18B20_PIN)
#define DQ_OUT_LOW() GPIO_ResetBits(DS18B20_PORT, DS18B20_PIN)
#define DQ_IN() GPIO_ReadInputDataBit(DS18B20_PORT, DS18B20_PIN)
/* 引脚模式切换 */
#define DQ_OUTPUT_MODE() \
do { \
GPIO_InitTypeDef GPIO_InitStructure; \
GPIO_InitStructure.GPIO_Pin = DS18B20_PIN; \
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; \
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; \
GPIO_Init(DS18B20_PORT, &GPIO_InitStructure); \
} while(0)
#define DQ_INPUT_MODE() \
do { \
GPIO_InitTypeDef GPIO_InitStructure; \
GPIO_InitStructure.GPIO_Pin = DS18B20_PIN; \
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; \
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; \
GPIO_Init(DS18B20_PORT, &GPIO_InitStructure); \
} while(0)
/* ==================== 静态函数声明 ==================== */
static uint8_t DS18B20_Reset(void);
static uint8_t DS18B20_ReadBit(void);
static void DS18B20_WriteBit(uint8_t bit);
static uint8_t DS18B20_ReadByte(void);
static void DS18B20_WriteByte(uint8_t data);
static uint16_t DS18B20_CRC8(uint8_t *data, uint8_t len);
/**
* @brief DS18B20初始化
* @param 无
* @retval 无
*/
void DS18B20_Init(void)
{
/* 引脚配置在GPIO_Configuration中已完成 */
DQ_OUT_HIGH();
Delay_Us(100);
/* 进行一次复位操作确认传感器存在 */
if (DS18B20_Reset() == 0)
{
printf("DS18B20 detected!\r\n");
}
else
{
printf("DS18B20 not found!\r\n");
}
}
/**
* @brief 单总线复位脉冲
* @param 无
* @retval uint8_t: 0表示检测到存在脉冲, 1表示未检测到
*/
static uint8_t DS18B20_Reset(void)
{
uint8_t presence;
/* 主机拉低总线至少480us */
DQ_OUTPUT_MODE();
DQ_OUT_LOW();
Delay_Us(500); /* 保持低电平500us */
/* 释放总线,切换到输入模式 */
DQ_INPUT_MODE();
Delay_Us(60); /* 等待15-60us */
/* 读取存在脉冲 */
presence = DQ_IN();
/* 等待复位完成 */
Delay_Us(420); /* 总共延时480us以上 */
return presence; /* 0=存在, 1=不存在 */
}
/**
* @brief 从单总线读取一个位
* @param 无
* @retval uint8_t: 读取的位值(0或1)
*/
static uint8_t DS18B20_ReadBit(void)
{
uint8_t bit;
/* 主机拉低总线启动读时序 */
DQ_OUTPUT_MODE();
DQ_OUT_LOW();
Delay_Us(2); /* 至少保持1us低电平 */
/* 释放总线 */
DQ_INPUT_MODE();
Delay_Us(10); /* 等待DS18B20输出数据 */
/* 采样数据 */
bit = DQ_IN();
/* 等待读时序完成(总共至少60us) */
Delay_Us(50);
return bit;
}
/**
* @brief 向单总线写入一个位
* @param bit: 要写入的位值
* @retval 无
*/
static void DS18B20_WriteBit(uint8_t bit)
{
DQ_OUTPUT_MODE();
if (bit)
{
/* 写1: 拉低1-15us后释放 */
DQ_OUT_LOW();
Delay_Us(2);
DQ_OUT_HIGH();
Delay_Us(60);
}
else
{
/* 写0: 拉低60-120us */
DQ_OUT_LOW();
Delay_Us(60);
DQ_OUT_HIGH();
Delay_Us(2);
}
}
/**
* @brief 从单总线读取一个字节
* @param 无
* @retval uint8_t: 读取的字节
*/
static uint8_t DS18B20_ReadByte(void)
{
uint8_t i;
uint8_t data = 0;
for (i = 0; i < 8; i++)
{
/* 低位在前 */
if (DS18B20_ReadBit())
{
data |= (1 << i);
}
}
return data;
}
/**
* @brief 向单总线写入一个字节
* @param data: 要写入的字节
* @retval 无
*/
static void DS18B20_WriteByte(uint8_t data)
{
uint8_t i;
for (i = 0; i < 8; i++)
{
/* 低位在前 */
DS18B20_WriteBit(data & 0x01);
data >>= 1;
}
}
/**
* @brief 获取温度值
* @param 无
* @retval float: 温度值(摄氏度)
*/
float DS18B20_GetTemp(void)
{
uint8_t tempL, tempH;
int16_t tempRaw;
float temperature;
uint8_t scratchpad[9];
/* 复位 */
if (DS18B20_Reset())
{
printf("DS18B20 reset failed!\r\n");
return -999.0f; /* 返回错误标志 */
}
/* 跳过ROM匹配 */
DS18B20_WriteByte(DS18B20_CMD_SKIP_ROM);
/* 启动温度转换 */
DS18B20_WriteByte(DS18B20_CMD_CONVERT_TEMP);
/* 等待转换完成(12位精度需要750ms) */
Delay_Ms(800);
/* 再次复位 */
if (DS18B20_Reset())
{
printf("DS18B20 reset failed after conversion!\r\n");
return -999.0f;
}
/* 跳过ROM匹配 */
DS18B20_WriteByte(DS18B20_CMD_SKIP_ROM);
/* 读取暂存器 */
DS18B20_WriteByte(DS18B20_CMD_READ_SCRATCHPAD);
/* 读取9字节数据 */
for (uint8_t i = 0; i < 9; i++)
{
scratchpad[i] = DS18B20_ReadByte();
}
/* CRC校验 */
if (DS18B20_CRC8(scratchpad, 8) != scratchpad[8])
{
printf("DS18B20 CRC error!\r\n");
return -999.0f;
}
/* 提取温度值 */
tempL = scratchpad[0];
tempH = scratchpad[1];
/* 组合成16位有符号数 */
tempRaw = (int16_t)((tempH << 8) | tempL);
/* 转换为实际温度(12位精度,分辨率0.0625度) */
temperature = (float)tempRaw / 16.0f;
return temperature;
}
/**
* @brief 读取DS18B20暂存器
* @param buf: 存储缓冲区(至少9字节)
* @retval uint8_t: 0成功, 1失败
*/
uint8_t DS18B20_ReadScratchpad(uint8_t *buf)
{
uint8_t i;
if (DS18B20_Reset()) return 1;
DS18B20_WriteByte(DS18B20_CMD_SKIP_ROM);
DS18B20_WriteByte(DS18B20_CMD_READ_SCRATCHPAD);
for (i = 0; i < 9; i++)
{
buf[i] = DS18B20_ReadByte();
}
/* CRC校验 */
if (DS18B20_CRC8(buf, 8) != buf[8])
{
return 1;
}
return 0;
}
/**
* @brief 写入DS18B20暂存器(设置报警温度)
* @param th: 高温报警值
* @param tl: 低温报警值
* @param config: 配置寄存器(精度设置)
* @retval 无
*/
void DS18B20_WriteScratchpad(uint8_t th, uint8_t tl, uint8_t config)
{
if (DS18B20_Reset()) return;
DS18B20_WriteByte(DS18B20_CMD_SKIP_ROM);
DS18B20_WriteByte(DS18B20_CMD_WRITE_SCRATCHPAD);
DS18B20_WriteByte(th);
DS18B20_WriteByte(tl);
DS18B20_WriteByte(config);
}
/**
* @brief CRC8校验(Dallas/Maxim CRC8多项式: X^8+X^5+X^4+1)
* @param data: 数据缓冲区
* @param len: 数据长度
* @retval uint16_t: CRC校验值
*/
static uint16_t DS18B20_CRC8(uint8_t *data, uint8_t len)
{
uint8_t crc = 0;
uint8_t i, j;
for (i = 0; i < len; i++)
{
crc ^= data[i];
for (j = 0; j < 8; j++)
{
if (crc & 0x01)
{
crc = (crc >> 1) ^ 0x8C; /* 多项式 0x8C */
}
else
{
crc >>= 1;
}
}
}
return crc;
}
6.9 文件:Hardware/flow_sensor.h
流量传感器驱动头文件。
c
/**
* @file flow_sensor.h
* @brief 流量传感器驱动头文件
* @author SmartWater Team
* @date 2026-05-29
*/
#ifndef __FLOW_SENSOR_H
#define __FLOW_SENSOR_H
#include "stm32f10x.h"
void FlowSensor_Init(void);
uint32_t FlowSensor_GetPulseCount(void);
void FlowSensor_ResetCount(void);
float FlowSensor_GetFlowRate(uint32_t pulseCount, uint32_t timeMs);
#endif /* __FLOW_SENSOR_H */
6.10 文件:Hardware/flow_sensor.c
流量传感器驱动实现。
c
/**
* @file flow_sensor.c
* @brief 流量传感器驱动实现
* @author SmartWater Team
* @date 2026-05-29
*/
#include "flow_sensor.h"
#include "main.h"
/* 流量传感器常数: YF-S201, F=7.5*Q (Q单位L/min, F单位Hz) */
/* 即450个脉冲 = 1升 */
/**
* @brief 流量传感器初始化
* @param 无
* @retval 无
*/
void FlowSensor_Init(void)
{
/* GPIO配置已在main中完成 */
/* 定时器配置已在main中完成 */
printf("Flow Sensor initialized (YF-S201)\r\n");
}
/**
* @brief 获取当前脉冲计数值
* @param 无
* @retval uint32_t: 脉冲计数值
*/
uint32_t FlowSensor_GetPulseCount(void)
{
uint32_t count;
__disable_irq();
count = g_sysState.flowPulseCount + g_flowPulseIRQ;
__enable_irq();
return count;
}
/**
* @brief 重置脉冲计数器
* @param 无
* @retval 无
*/
void FlowSensor_ResetCount(void)
{
__disable_irq();
g_sysState.flowPulseCount = 0;
g_flowPulseIRQ = 0;
__enable_irq();
}
/**
* @brief 根据脉冲数和时间计算瞬时流量
* @param pulseCount: 这段时间内的脉冲数
* @param timeMs: 这段时间的毫秒数
* @retval float: 瞬时流量(L/min)
*/
float FlowSensor_GetFlowRate(uint32_t pulseCount, uint32_t timeMs)
{
float flowRate;
if (timeMs == 0) return 0.0f;
/*
* 计算公式:
* 流量(L/min) = 脉冲数 / 450(脉冲/升) * (60000 / 时间ms)
* = 脉冲数 * 60000 / (450 * 时间ms)
* = 脉冲数 * 133.333 / 时间ms
*/
flowRate = ((float)pulseCount * 60000.0f) /
((float)FLOW_PULSE_PER_LITER * (float)timeMs);
return flowRate;
}
6.11 文件:Hardware/oled.h
OLED显示屏驱动头文件。
c
/**
* @file oled.h
* @brief SSD1306 OLED驱动头文件
* @author SmartWater Team
* @date 2026-05-29
*/
#ifndef __OLED_H
#define __OLED_H
#include "stm32f10x.h"
/* OLED尺寸定义 */
#define OLED_WIDTH 128
#define OLED_HEIGHT 64
void OLED_Init(void);
void OLED_Clear(void);
void OLED_Refresh(void);
void OLED_ShowChar(uint8_t x, uint8_t y, char ch, uint8_t size);
void OLED_ShowString(uint8_t x, uint8_t y, const char *str, uint8_t size);
void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size);
void OLED_ShowFloat(uint8_t x, uint8_t y, float num, uint8_t intLen,
uint8_t decLen, uint8_t size);
void OLED_DrawPoint(uint8_t x, uint8_t y, uint8_t color);
void OLED_SetCursor(uint8_t x, uint8_t y);
void OLED_DisplayOn(void);
void OLED_DisplayOff(void);
#endif /* __OLED_H */
6.12 文件:Hardware/oled.c
OLED显示屏驱动实现,基于SSD1306,采用IIC通信协议。
c
/**
* @file oled.c
* @brief SSD1306 OLED驱动实现(IIC接口)
* @author SmartWater Team
* @date 2026-05-29
*/
#include "oled.h"
#include "oled_font.h"
#include "delay.h"
#include "main.h"
#include <string.h>
/* ==================== SSD1306 I2C地址 ==================== */
#define OLED_I2C_ADDR 0x78 /* 7位地址: 0x3C << 1 = 0x78 */
/* ==================== SSD1306命令 ==================== */
#define OLED_CMD_DISPLAY_OFF 0xAE
#define OLED_CMD_DISPLAY_ON 0xAF
#define OLED_CMD_SET_CONTRAST 0x81
#define OLED_CMD_DISPLAY_ALL_ON 0xA5
#define OLED_CMD_NORMAL_DISPLAY 0xA6
#define OLED_CMD_INVERT_DISPLAY 0xA7
#define OLED_CMD_SET_MEMORY_MODE 0x20
#define OLED_CMD_SET_COL_ADDR 0x21
#define OLED_CMD_SET_PAGE_ADDR 0x22
#define OLED_CMD_SET_START_LINE 0x40
#define OLED_CMD_SET_SEG_REMAP 0xA0
#define OLED_CMD_SET_COM_SCAN_DIR 0xC0
#define OLED_CMD_SET_COM_PINS 0xDA
#define OLED_CMD_SET_DISPLAY_OFFSET 0xD3
#define OLED_CMD_SET_CLOCK_DIV 0xD5
#define OLED_CMD_SET_PRECHARGE 0xD9
#define OLED_CMD_SET_VCOM_DETECT 0xDB
#define OLED_CMD_CHARGE_PUMP 0x8D
/* ==================== 显示缓冲区 ==================== */
static uint8_t OLED_Buffer[OLED_WIDTH * OLED_HEIGHT / 8];
/* ==================== 软件I2C操作函数 ==================== */
/**
* @brief 软件I2C起始信号
* @param 无
* @retval 无
*/
static void I2C_Start(void)
{
/* SDA高, SCL高 */
GPIO_SetBits(I2C_PORT, I2C_SDA_PIN);
GPIO_SetBits(I2C_PORT, I2C_SCL_PIN);
Delay_Us(5);
/* SDA拉低 */
GPIO_ResetBits(I2C_PORT, I2C_SDA_PIN);
Delay_Us(5);
/* SCL拉低 */
GPIO_ResetBits(I2C_PORT, I2C_SCL_PIN);
Delay_Us(5);
}
/**
* @brief 软件I2C停止信号
* @param 无
* @retval 无
*/
static void I2C_Stop(void)
{
/* SCL低, SDA低 */
GPIO_ResetBits(I2C_PORT, I2C_SCL_PIN);
GPIO_ResetBits(I2C_PORT, I2C_SDA_PIN);
Delay_Us(5);
/* SCL拉高 */
GPIO_SetBits(I2C_PORT, I2C_SCL_PIN);
Delay_Us(5);
/* SDA拉高 */
GPIO_SetBits(I2C_PORT, I2C_SDA_PIN);
Delay_Us(5);
}
/**
* @brief 软件I2C等待应答
* @param 无
* @retval uint8_t: 0表示收到应答, 1表示未收到
*/
static uint8_t I2C_WaitAck(void)
{
uint8_t ack;
uint16_t timeout = 5000;
/* 释放SDA为输入 */
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = I2C_SDA_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(I2C_PORT, &GPIO_InitStructure);
/* SCL拉高 */
GPIO_SetBits(I2C_PORT, I2C_SCL_PIN);
Delay_Us(1);
/* 等待SDA被拉低(应答)或超时 */
while (GPIO_ReadInputDataBit(I2C_PORT, I2C_SDA_PIN) == Bit_SET)
{
if (--timeout == 0)
{
I2C_Stop();
return 1; /* 未收到应答 */
}
}
ack = GPIO_ReadInputDataBit(I2C_PORT, I2C_SDA_PIN);
/* SCL拉低 */
GPIO_ResetBits(I2C_PORT, I2C_SCL_PIN);
/* 恢复SDA为输出 */
GPIO_InitStructure.GPIO_Pin = I2C_SDA_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(I2C_PORT, &GPIO_InitStructure);
return (ack == Bit_RESET) ? 0 : 1;
}
/**
* @brief 软件I2C发送应答
* @param ack: 0发送应答, 1发送非应答
* @retval 无
*/
static void I2C_SendAck(uint8_t ack)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* 配置SDA为输出 */
GPIO_InitStructure.GPIO_Pin = I2C_SDA_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(I2C_PORT, &GPIO_InitStructure);
if (ack)
{
GPIO_SetBits(I2C_PORT, I2C_SDA_PIN); /* 非应答: SDA高 */
}
else
{
GPIO_ResetBits(I2C_PORT, I2C_SDA_PIN); /* 应答: SDA低 */
}
/* SCL产生脉冲 */
GPIO_SetBits(I2C_PORT, I2C_SCL_PIN);
Delay_Us(5);
GPIO_ResetBits(I2C_PORT, I2C_SCL_PIN);
Delay_Us(5);
}
/**
* @brief 软件I2C发送一个字节
* @param data: 要发送的字节
* @retval 无
*/
static void I2C_SendByte(uint8_t data)
{
uint8_t i;
for (i = 0; i < 8; i++)
{
/* 先输出最高位 */
if (data & 0x80)
{
GPIO_SetBits(I2C_PORT, I2C_SDA_PIN);
}
else
{
GPIO_ResetBits(I2C_PORT, I2C_SDA_PIN);
}
Delay_Us(2);
/* SCL产生一个时钟脉冲 */
GPIO_SetBits(I2C_PORT, I2C_SCL_PIN);
Delay_Us(5);
GPIO_ResetBits(I2C_PORT, I2C_SCL_PIN);
Delay_Us(2);
data <<= 1; /* 左移,准备发送下一位 */
}
}
/**
* @brief 软件I2C读取一个字节
* @param ack: 读取后是否发送应答(0发送应答)
* @retval uint8_t: 读取的字节
*/
static uint8_t I2C_ReadByte(uint8_t ack)
{
uint8_t i;
uint8_t data = 0;
GPIO_InitTypeDef GPIO_InitStructure;
/* 释放SDA为输入 */
GPIO_InitStructure.GPIO_Pin = I2C_SDA_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(I2C_PORT, &GPIO_InitStructure);
for (i = 0; i < 8; i++)
{
data <<= 1;
/* SCL拉高 */
GPIO_SetBits(I2C_PORT, I2C_SCL_PIN);
Delay_Us(5);
/* 读取SDA */
if (GPIO_ReadInputDataBit(I2C_PORT, I2C_SDA_PIN) == Bit_SET)
{
data |= 0x01;
}
/* SCL拉低 */
GPIO_ResetBits(I2C_PORT, I2C_SCL_PIN);
Delay_Us(5);
}
/* 发送应答信号 */
I2C_SendAck(ack);
return data;
}
/* ==================== OLED底层操作 ==================== */
/**
* @brief 向OLED写入一个命令字节
* @param cmd: 命令字节
* @retval 无
*/
static void OLED_WriteCmd(uint8_t cmd)
{
I2C_Start();
I2C_SendByte(OLED_I2C_ADDR); /* 发送设备地址+写 */
I2C_WaitAck();
I2C_SendByte(0x00); /* 控制字节: 0x00表示后面是命令 */
I2C_WaitAck();
I2C_SendByte(cmd); /* 发送命令 */
I2C_WaitAck();
I2C_Stop();
}
/**
* @brief 向OLED写入一个数据字节
* @param data: 数据字节
* @retval 无
*/
static void OLED_WriteData(uint8_t data)
{
I2C_Start();
I2C_SendByte(OLED_I2C_ADDR); /* 发送设备地址+写 */
I2C_WaitAck();
I2C_SendByte(0x40); /* 控制字节: 0x40表示后面是数据 */
I2C_WaitAck();
I2C_SendByte(data); /* 发送数据 */
I2C_WaitAck();
I2C_Stop();
}
/**
* @brief 向OLED写入多个数据字节
* @param data: 数据缓冲区指针
* @param len: 数据长度
* @retval 无
*/
static void OLED_WriteMultiData(uint8_t *data, uint16_t len)
{
uint16_t i;
I2C_Start();
I2C_SendByte(OLED_I2C_ADDR);
I2C_WaitAck();
I2C_SendByte(0x40); /* 数据模式 */
I2C_WaitAck();
for (i = 0; i < len; i++)
{
I2C_SendByte(data[i]);
I2C_WaitAck();
}
I2C_Stop();
}
/**
* @brief 设置OLED显示区域
* @param x: 起始列地址
* @param y: 起始页地址
* @param width: 宽度
* @param height: 高度(页数)
* @retval 无
*/
static void OLED_SetWindow(uint8_t x, uint8_t y, uint8_t width, uint8_t height)
{
OLED_WriteCmd(OLED_CMD_SET_COL_ADDR);
OLED_WriteCmd(x);
OLED_WriteCmd(x + width - 1);
OLED_WriteCmd(OLED_CMD_SET_PAGE_ADDR);
OLED_WriteCmd(y);
OLED_WriteCmd(y + height - 1);
}
/* ==================== OLED初始化与显示函数 ==================== */
/**
* @brief OLED初始化
* @param 无
* @retval 无
*/
void OLED_Init(void)
{
printf("Initializing OLED...\r\n");
Delay_Ms(100); /* 等待OLED上电稳定 */
/* SSD1306初始化序列 */
OLED_WriteCmd(OLED_CMD_DISPLAY_OFF); /* 关闭显示 */
OLED_WriteCmd(OLED_CMD_SET_CLOCK_DIV); /* 设置时钟分频 */
OLED_WriteCmd(0x80); /* 分频系数 */
OLED_WriteCmd(OLED_CMD_SET_MULTIPLEX); /* 设置多路复用比 */
OLED_WriteCmd(0x3F); /* 64路 */
OLED_WriteCmd(OLED_CMD_SET_DISPLAY_OFFSET); /* 设置显示偏移 */
OLED_WriteCmd(0x00); /* 无偏移 */
OLED_WriteCmd(OLED_CMD_SET_START_LINE | 0x00); /* 设置起始行 */
OLED_WriteCmd(OLED_CMD_CHARGE_PUMP); /* 电荷泵设置 */
OLED_WriteCmd(0x14); /* 使能电荷泵(3.3V供电时需要) */
OLED_WriteCmd(OLED_CMD_SET_MEMORY_MODE); /* 内存寻址模式 */
OLED_WriteCmd(0x00); /* 水平寻址模式 */
OLED_WriteCmd(OLED_CMD_SET_SEG_REMAP | 0x01); /* 段重映射 */
OLED_WriteCmd(OLED_CMD_SET_COM_SCAN_DIR | 0x08); /* COM扫描方向 */
OLED_WriteCmd(OLED_CMD_SET_COM_PINS); /* COM引脚配置 */
OLED_WriteCmd(0x12);
OLED_WriteCmd(OLED_CMD_SET_CONTRAST); /* 对比度设置 */
OLED_WriteCmd(0xCF); /* 对比度值 */
OLED_WriteCmd(OLED_CMD_SET_PRECHARGE); /* 预充电周期 */
OLED_WriteCmd(0xF1);
OLED_WriteCmd(OLED_CMD_SET_VCOM_DETECT); /* VCOMH电压 */
OLED_WriteCmd(0x40);
OLED_WriteCmd(OLED_CMD_DISPLAY_ALL_ON_RESUME); /* 正常显示 */
OLED_WriteCmd(OLED_CMD_NORMAL_DISPLAY); /* 非反显 */
OLED_WriteCmd(OLED_CMD_DISPLAY_ON); /* 开启显示 */
/* 清屏 */
OLED_Clear();
OLED_Refresh();
printf("OLED initialized successfully!\r\n");
}
/**
* @brief 清空显示缓冲区
* @param 无
* @retval 无
*/
void OLED_Clear(void)
{
memset(OLED_Buffer, 0x00, sizeof(OLED_Buffer));
}
/**
* @brief 将缓冲区内容刷新到OLED
* @param 无
* @retval 无
*/
void OLED_Refresh(void)
{
uint8_t page;
for (page = 0; page < 8; page++)
{
OLED_SetWindow(0, page, OLED_WIDTH, 1);
OLED_WriteMultiData(&OLED_Buffer[page * OLED_WIDTH], OLED_WIDTH);
}
}
/**
* @brief 在指定位置显示一个字符
* @param x: 列坐标(0-127)
* @param y: 行坐标(0-7, 每行8像素高)
* @param ch: 要显示的字符
* @param size: 字体大小(12: 6x12, 16: 8x16)
* @retval 无
*/
void OLED_ShowChar(uint8_t x, uint8_t y, char ch, uint8_t size)
{
uint8_t i, j;
uint8_t charWidth, charHeight;
uint8_t *pFont;
if (x > OLED_WIDTH - 1 || y > 7) return;
if (size == 12)
{
charWidth = 6;
charHeight = 12;
/* 检查是否为ASCII可见字符 */
if (ch < ' ' || ch > '~') ch = ' ';
pFont = (uint8_t *)&Font6x12[(ch - ' ') * charHeight];
for (i = 0; i < charHeight; i++)
{
if ((y * 8 + i) >= OLED_HEIGHT) break;
for (j = 0; j < charWidth; j++)
{
if ((x + j) >= OLED_WIDTH) break;
if (pFont[i] & (1 << (charWidth - 1 - j)))
{
OLED_Buffer[(y * 8 + i) * OLED_WIDTH + x + j] = 1;
}
else
{
OLED_Buffer[(y * 8 + i) * OLED_WIDTH + x + j] = 0;
}
}
}
}
else if (size == 16)
{
charWidth = 8;
charHeight = 16;
if (ch < ' ' || ch > '~') ch = ' ';
pFont = (uint8_t *)&Font8x16[(ch - ' ') * charHeight];
for (i = 0; i < charHeight; i++)
{
if ((y * 8 + i) >= OLED_HEIGHT) break;
for (j = 0; j < charWidth; j++)
{
if ((x + j) >= OLED_WIDTH) break;
if (pFont[i] & (1 << (charWidth - 1 - j)))
{
OLED_Buffer[(y * 8 + i) * OLED_WIDTH + x + j] = 1;
}
else
{
OLED_Buffer[(y * 8 + i) * OLED_WIDTH + x + j] = 0;
}
}
}
}
}
/**
* @brief 在指定位置显示字符串
* @param x: 列坐标
* @param y: 行坐标
* @param str: 字符串
* @param size: 字体大小
* @retval 无
*/
void OLED_ShowString(uint8_t x, uint8_t y, const char *str, uint8_t size)
{
uint8_t charWidth = (size == 12) ? 6 : 8;
while (*str)
{
if (x + charWidth > OLED_WIDTH)
{
x = 0;
y++;
}
if (y > 7) break;
OLED_ShowChar(x, y, *str, size);
x += charWidth;
str++;
}
}
/**
* @brief 在指定位置显示数字
* @param x: 列坐标
* @param y: 行坐标
* @param num: 数字
* @param len: 数字位数
* @param size: 字体大小
* @retval 无
*/
void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size)
{
char str[12];
uint8_t i;
/* 转换为字符串 */
for (i = 0; i < len; i++)
{
str[len - 1 - i] = '0' + (num % 10);
num /= 10;
}
str[len] = '\0';
OLED_ShowString(x, y, str, size);
}
/**
* @brief 在指定位置显示浮点数
* @param x: 列坐标
* @param y: 行坐标
* @param num: 浮点数
* @param intLen: 整数部分位数
* @param decLen: 小数部分位数
* @param size: 字体大小
* @retval 无
*/
void OLED_ShowFloat(uint8_t x, uint8_t y, float num, uint8_t intLen,
uint8_t decLen, uint8_t size)
{
char str[20];
int32_t intPart;
uint32_t decPart;
uint8_t i;
/* 处理负数 */
if (num < 0)
{
OLED_ShowChar(x, y, '-', size);
num = -num;
x += (size == 12) ? 6 : 8;
}
/* 分离整数和小数部分 */
intPart = (int32_t)num;
num -= (float)intPart;
/* 计算小数部分 */
for (i = 0; i < decLen; i++)
{
num *= 10;
}
decPart = (uint32_t)(num + 0.5f);
/* 显示整数部分 */
OLED_ShowNum(x, y, intPart, intLen, size);
x += intLen * ((size == 12) ? 6 : 8);
/* 显示小数点 */
OLED_ShowChar(x, y, '.', size);
x += (size == 12) ? 6 : 8;
/* 显示小数部分 */
OLED_ShowNum(x, y, decPart, decLen, size);
}
/**
* @brief 在指定位置画点
* @param x: X坐标(0-127)
* @param y: Y坐标(0-63)
* @param color: 1亮, 0灭
* @retval 无
*/
void OLED_DrawPoint(uint8_t x, uint8_t y, uint8_t color)
{
if (x >= OLED_WIDTH || y >= OLED_HEIGHT) return;
if (color)
{
OLED_Buffer[(y / 8) * OLED_WIDTH + x] |= (1 << (y % 8));
}
else
{
OLED_Buffer[(y / 8) * OLED_WIDTH + x] &= ~(1 << (y % 8));
}
}
/**
* @brief 设置光标位置
* @param x: 列坐标
* @param y: 行坐标
* @retval 无
*/
void OLED_SetCursor(uint8_t x, uint8_t y)
{
OLED_WriteCmd(0xB0 + y); /* 设置页地址 */
OLED_WriteCmd(((x & 0xF0) >> 4) | 0x10); /* 设置列地址高4位 */
OLED_WriteCmd(x & 0x0F); /* 设置列地址低4位 */
}
/**
* @brief 开启显示
* @param 无
* @retval 无
*/
void OLED_DisplayOn(void)
{
OLED_WriteCmd(OLED_CMD_DISPLAY_ON);
}
/**
* @brief 关闭显示
* @param 无
* @retval 无
*/
void OLED_DisplayOff(void)
{
OLED_WriteCmd(OLED_CMD_DISPLAY_OFF);
}
6.13 文件:Hardware/oled_font.h
OLED字库头文件,包含6x12和8x16两种ASCII字库。
c
/**
* @file oled_font.h
* @brief OLED ASCII字库
* @author SmartWater Team
* @date 2026-05-29
*/
#ifndef __OLED_FONT_H
#define __OLED_FONT_H
/* ==================== 6x12 ASCII字库 (95个可打印字符) ==================== */
static const uint8_t Font6x12[][12] = {
/* 空格 ' ' (0x20) */
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
/* ! */
{0x00,0x00,0x00,0x10,0x10,0x10,0x10,0x10,0x00,0x10,0x00,0x00},
/* " */
{0x00,0x00,0x00,0x24,0x24,0x24,0x00,0x00,0x00,0x00,0x00,0x00},
/* # */
{0x00,0x00,0x00,0x24,0x24,0x7E,0x24,0x7E,0x24,0x24,0x00,0x00},
/* $ */
{0x00,0x00,0x10,0x3C,0x50,0x50,0x38,0x14,0x14,0x78,0x10,0x00},
/* % */
{0x00,0x00,0x00,0x62,0x64,0x08,0x10,0x26,0x46,0x00,0x00,0x00},
/* & */
{0x00,0x00,0x00,0x30,0x48,0x48,0x30,0x4A,0x44,0x3A,0x00,0x00},
/* ' */
{0x00,0x00,0x00,0x10,0x10,0x10,0x00,0x00,0x00,0x00,0x00,0x00},
/* ( */
{0x00,0x00,0x04,0x08,0x10,0x10,0x10,0x10,0x08,0x04,0x00,0x00},
/* ) */
{0x00,0x00,0x20,0x10,0x08,0x08,0x08,0x08,0x10,0x20,0x00,0x00},
/* * */
{0x00,0x00,0x00,0x00,0x24,0x18,0x7E,0x18,0x24,0x00,0x00,0x00},
/* + */
{0x00,0x00,0x00,0x00,0x10,0x10,0x7C,0x10,0x10,0x00,0x00,0x00},
/* , */
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x10,0x20,0x00},
/* - */
{0x00,0x00,0x00,0x00,0x00,0x00,0x7C,0x00,0x00,0x00,0x00,0x00},
/* . */
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x30,0x00,0x00},
/* / */
{0x00,0x00,0x00,0x02,0x04,0x08,0x10,0x20,0x40,0x00,0x00,0x00},
/* 0 */
{0x00,0x00,0x00,0x38,0x44,0x4C,0x54,0x64,0x44,0x38,0x00,0x00},
/* 1 */
{0x00,0x00,0x00,0x10,0x30,0x10,0x10,0x10,0x10,0x38,0x00,0x00},
/* 2 */
{0x00,0x00,0x00,0x38,0x44,0x04,0x08,0x10,0x20,0x7C,0x00,0x00},
/* 3 */
{0x00,0x00,0x00,0x38,0x44,0x04,0x18,0x04,0x44,0x38,0x00,0x00},
/* 4 */
{0x00,0x00,0x00,0x08,0x18,0x28,0x48,0x7C,0x08,0x08,0x00,0x00},
/* 5 */
{0x00,0x00,0x00,0x7C,0x40,0x78,0x04,0x04,0x44,0x38,0x00,0x00},
/* 6 */
{0x00,0x00,0x00,0x18,0x20,0x40,0x78,0x44,0x44,0x38,0x00,0x00},
/* 7 */
{0x00,0x00,0x00,0x7C,0x04,0x08,0x10,0x10,0x10,0x10,0x00,0x00},
/* 8 */
{0x00,0x00,0x00,0x38,0x44,0x44,0x38,0x44,0x44,0x38,0x00,0x00},
/* 9 */
{0x00,0x00,0x00,0x38,0x44,0x44,0x3C,0x04,0x08,0x30,0x00,0x00},
/* : */
{0x00,0x00,0x00,0x00,0x30,0x30,0x00,0x30,0x30,0x00,0x00,0x00},
/* ; */
{0x00,0x00,0x00,0x00,0x30,0x30,0x00,0x30,0x10,0x20,0x00,0x00},
/* < */
{0x00,0x00,0x00,0x04,0x08,0x10,0x20,0x10,0x08,0x04,0x00,0x00},
/* = */
{0x00,0x00,0x00,0x00,0x00,0x7C,0x00,0x7C,0x00,0x00,0x00,0x00},
/* > */
{0x00,0x00,0x00,0x20,0x10,0x08,0x04,0x08,0x10,0x20,0x00,0x00},
/* ? */
{0x00,0x00,0x00,0x38,0x44,0x04,0x08,0x10,0x00,0x10,0x00,0x00},
/* @ */
{0x00,0x00,0x00,0x38,0x44,0x5C,0x54,0x5C,0x40,0x38,0x00,0x00},
/* A */
{0x00,0x00,0x00,0x10,0x28,0x44,0x44,0x7C,0x44,0x44,0x00,0x00},
/* B */
{0x00,0x00,0x00,0x78,0x24,0x24,0x38,0x24,0x24,0x78,0x00,0x00},
/* C */
{0x00,0x00,0x00,0x38,0x44,0x40,0x40,0x40,0x44,0x38,0x00,0x00},
/* D */
{0x00,0x00,0x00,0x78,0x24,0x24,0x24,0x24,0x24,0x78,0x00,0x00},
/* E */
{0x00,0x00,0x00,0x7C,0x40,0x40,0x78,0x40,0x40,0x7C,0x00,0x00},
/* F */
{0x00,0x00,0x00,0x7C,0x40,0x40,0x78,0x40,0x40,0x40,0x00,0x00},
/* G */
{0x00,0x00,0x00,0x38,0x44,0x40,0x4C,0x44,0x44,0x38,0x00,0x00},
/* H */
{0x00,0x00,0x00,0x44,0x44,0x44,0x7C,0x44,0x44,0x44,0x00,0x00},
/* I */
{0x00,0x00,0x00,0x38,0x10,0x10,0x10,0x10,0x10,0x38,0x00,0x00},
/* J */
{0x00,0x00,0x00,0x1C,0x08,0x08,0x08,0x48,0x48,0x30,0x00,0x00},
/* K */
{0x00,0x00,0x00,0x44,0x48,0x50,0x60,0x50,0x48,0x44,0x00,0x00},
/* L */
{0x00,0x00,0x00,0x40,0x40,0x40,0x40,0x40,0x40,0x7C,0x00,0x00},
/* M */
{0x00,0x00,0x00,0x44,0x6C,0x54,0x54,0x44,0x44,0x44,0x00,0x00},
/* N */
{0x00,0x00,0x00,0x44,0x64,0x54,0x4C,0x44,0x44,0x44,0x00,0x00},
/* O */
{0x00,0x00,0x00,0x38,0x44,0x44,0x44,0x44,0x44,0x38,0x00,0x00},
/* P */
{0x00,0x00,0x00,0x78,0x44,0x44,0x78,0x40,0x40,0x40,0x00,0x00},
/* Q */
{0x00,0x00,0x00,0x38,0x44,0x44,0x44,0x54,0x48,0x34,0x00,0x00},
/* R */
{0x00,0x00,0x00,0x78,0x44,0x44,0x78,0x50,0x48,0x44,0x00,0x00},
/* S */
{0x00,0x00,0x00,0x38,0x44,0x40,0x38,0x04,0x44,0x38,0x00,0x00},
/* T */
{0x00,0x00,0x00,0x7C,0x10,0x10,0x10,0x10,0x10,0x10,0x00,0x00},
/* U */
{0x00,0x00,0x00,0x44,0x44,0x44,0x44,0x44,0x44,0x38,0x00,0x00},
/* V */
{0x00,0x00,0x00,0x44,0x44,0x44,0x44,0x28,0x28,0x10,0x00,0x00},
/* W */
{0x00,0x00,0x00,0x44,0x44,0x44,0x54,0x54,0x6C,0x44,0x00,0x00},
/* X */
{0x00,0x00,0x00,0x44,0x44,0x28,0x10,0x28,0x44,0x44,0x00,0x00},
/* Y */
{0x00,0x00,0x00,0x44,0x44,0x28,0x10,0x10,0x10,0x10,0x00,0x00},
/* Z */
{0x00,0x00,0x00,0x7C,0x04,0x08,0x10,0x20,0x40,0x7C,0x00,0x00},
/* [ */
{0x00,0x00,0x1C,0x10,0x10,0x10,0x10,0x10,0x10,0x1C,0x00,0x00},
/* \ */
{0x00,0x00,0x00,0x40,0x20,0x10,0x08,0x04,0x02,0x00,0x00,0x00},
/* ] */
{0x00,0x00,0x38,0x08,0x08,0x08,0x08,0x08,0x08,0x38,0x00,0x00},
/* ^ */
{0x00,0x00,0x10,0x28,0x44,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
/* _ */
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x7E,0x00,0x00},
/* ` */
{0x00,0x00,0x10,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
/* a */
{0x00,0x00,0x00,0x00,0x00,0x38,0x04,0x3C,0x44,0x3C,0x00,0x00},
/* b */
{0x00,0x00,0x40,0x40,0x40,0x58,0x64,0x44,0x64,0x58,0x00,0x00},
/* c */
{0x00,0x00,0x00,0x00,0x00,0x38,0x44,0x40,0x44,0x38,0x00,0x00},
/* d */
{0x00,0x00,0x04,0x04,0x04,0x34,0x4C,0x44,0x4C,0x34,0x00,0x00},
/* e */
{0x00,0x00,0x00,0x00,0x00,0x38,0x44,0x7C,0x40,0x38,0x00,0x00},
/* f */
{0x00,0x00,0x18,0x24,0x20,0x70,0x20,0x20,0x20,0x20,0x00,0x00},
/* g */
{0x00,0x00,0x00,0x00,0x00,0x34,0x4C,0x44,0x4C,0x34,0x04,0x38},
/* h */
{0x00,0x00,0x40,0x40,0x40,0x58,0x64,0x44,0x44,0x44,0x00,0x00},
/* i */
{0x00,0x00,0x10,0x00,0x00,0x30,0x10,0x10,0x10,0x38,0x00,0x00},
/* j */
{0x00,0x00,0x08,0x00,0x00,0x18,0x08,0x08,0x08,0x48,0x48,0x30},
/* k */
{0x00,0x00,0x40,0x40,0x44,0x48,0x50,0x60,0x50,0x48,0x00,0x00},
/* l */
{0x00,0x00,0x30,0x10,0x10,0x10,0x10,0x10,0x10,0x38,0x00,0x00},
/* m */
{0x00,0x00,0x00,0x00,0x00,0x6C,0x54,0x54,0x54,0x54,0x00,0x00},
/* n */
{0x00,0x00,0x00,0x00,0x00,0x58,0x64,0x44,0x44,0x44,0x00,0x00},
/* o */
{0x00,0x00,0x00,0x00,0x00,0x38,0x44,0x44,0x44,0x38,0x00,0x00},
/* p */
{0x00,0x00,0x00,0x00,0x00,0x58,0x64,0x44,0x64,0x58,0x40,0x40},
/* q */
{0x00,0x00,0x00,0x00,0x00,0x34,0x4C,0x44,0x4C,0x34,0x04,0x04},
/* r */
{0x00,0x00,0x00,0x00,0x00,0x58,0x64,0x40,0x40,0x40,0x00,0x00},
/* s */
{0x00,0x00,0x00,0x00,0x00,0x3C,0x40,0x38,0x04,0x78,0x00,0x00},
/* t */
{0x00,0x00,0x20,0x20,0x20,0x70,0x20,0x20,0x24,0x18,0x00,0x00},
/* u */
{0x00,0x00,0x00,0x00,0x00,0x44,0x44,0x44,0x4C,0x34,0x00,0x00},
/* v */
{0x00,0x00,0x00,0x00,0x00,0x44,0x44,0x44,0x28,0x10,0x00,0x00},
/* w */
{0x00,0x00,0x00,0x00,0x00,0x44,0x44,0x54,0x54,0x28,0x00,0x00},
/* x */
{0x00,0x00,0x00,0x00,0x00,0x44,0x28,0x10,0x28,0x44,0x00,0x00},
/* y */
{0x00,0x00,0x00,0x00,0x00,0x44,0x44,0x44,0x4C,0x34,0x04,0x38},
/* z */
{0x00,0x00,0x00,0x00,0x00,0x7C,0x08,0x10,0x20,0x7C,0x00,0x00},
/* { */
{0x00,0x00,0x0C,0x10,0x10,0x20,0x10,0x10,0x10,0x0C,0x00,0x00},
/* | */
{0x00,0x00,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x00,0x00},
/* } */
{0x00,0x00,0x30,0x08,0x08,0x04,0x08,0x08,0x08,0x30,0x00,0x00},
/* ~ */
{0x00,0x00,0x00,0x00,0x00,0x32,0x4C,0x00,0x00,0x00,0x00,0x00},
};
/* ==================== 8x16 ASCII字库 (95个可打印字符) ==================== */
static const uint8_t Font8x16[][16] = {
/* 空格 ' ' */
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
/* ! */
{0x00,0x00,0x00,0x18,0x18,0x18,0x18,0x18,0x18,0x00,0x18,0x18,0x00,0x00,0x00,0x00},
/* " */
{0x00,0x00,0x00,0x66,0x66,0x66,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
/* # */
{0x00,0x00,0x00,0x24,0x24,0x7E,0x24,0x24,0x24,0x7E,0x24,0x24,0x00,0x00,0x00,0x00},
/* $ */
{0x00,0x00,0x08,0x3E,0x49,0x48,0x3E,0x09,0x09,0x49,0x3E,0x08,0x00,0x00,0x00,0x00},
/* % */
{0x00,0x00,0x00,0x61,0x92,0x64,0x08,0x10,0x26,0x49,0x86,0x00,0x00,0x00,0x00,0x00},
/* & */
{0x00,0x00,0x00,0x38,0x44,0x44,0x38,0x4A,0x44,0x44,0x3A,0x00,0x00,0x00,0x00,0x00},
/* ' */
{0x00,0x00,0x00,0x18,0x18,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
/* ( */
{0x00,0x00,0x04,0x08,0x10,0x10,0x10,0x10,0x10,0x10,0x08,0x04,0x00,0x00,0x00,0x00},
/* ) */
{0x00,0x00,0x20,0x10,0x08,0x08,0x08,0x08,0x08,0x08,0x10,0x20,0x00,0x00,0x00,0x00},
/* * */
{0x00,0x00,0x00,0x00,0x24,0x18,0x7E,0x18,0x24,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
/* + */
{0x00,0x00,0x00,0x00,0x08,0x08,0x3E,0x08,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
/* , */
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x08,0x10,0x00,0x00,0x00},
/* - */
{0x00,0x00,0x00,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
/* . */
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x00,0x00},
/* / */
{0x00,0x00,0x00,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x00,0x00,0x00,0x00,0x00,0x00},
/* 0 */
{0x00,0x00,0x00,0x3C,0x42,0x46,0x4A,0x52,0x62,0x42,0x3C,0x00,0x00,0x00,0x00,0x00},
/* 1 */
{0x00,0x00,0x00,0x08,0x18,0x28,0x08,0x08,0x08,0x08,0x3E,0x00,0x00,0x00,0x00,0x00},
/* 2 */
{0x00,0x00,0x00,0x3C,0x42,0x02,0x04,0x08,0x10,0x20,0x7E,0x00,0x00,0x00,0x00,0x00},
/* 3 */
{0x00,0x00,0x00,0x3C,0x42,0x02,0x1C,0x02,0x02,0x42,0x3C,0x00,0x00,0x00,0x00,0x00},
/* 4 */
{0x00,0x00,0x00,0x04,0x0C,0x14,0x24,0x44,0x7E,0x04,0x04,0x00,0x00,0x00,0x00,0x00},
/* 5 */
{0x00,0x00,0x00,0x7E,0x40,0x40,0x7C,0x02,0x02,0x42,0x3C,0x00,0x00,0x00,0x00,0x00},
/* 6 */
{0x00,0x00,0x00,0x1C,0x20,0x40,0x7C,0x42,0x42,0x42,0x3C,0x00,0x00,0x00,0x00,0x00},
/* 7 */
{0x00,0x00,0x00,0x7E,0x02,0x04,0x08,0x10,0x10,0x10,0x10,0x00,0x00,0x00,0x00,0x00},
/* 8 */
{0x00,0x00,0x00,0x3C,0x42,0x42,0x3C,0x42,0x42,0x42,0x3C,0x00,0x00,0x00,0x00,0x00},
/* 9 */
{0x00,0x00,0x00,0x3C,0x42,0x42,0x42,0x3E,0x02,0x04,0x38,0x00,0x00,0x00,0x00,0x00},
/* : */
{0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x00,0x00},
/* ; */
{0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x18,0x18,0x08,0x10,0x00,0x00,0x00},
/* < */
{0x00,0x00,0x00,0x02,0x04,0x08,0x10,0x20,0x10,0x08,0x04,0x02,0x00,0x00,0x00,0x00},
/* = */
{0x00,0x00,0x00,0x00,0x00,0x7E,0x00,0x00,0x7E,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
/* > */
{0x00,0x00,0x00,0x40,0x20,0x10,0x08,0x04,0x08,0x10,0x20,0x40,0x00,0x00,0x00,0x00},
/* ? */
{0x00,0x00,0x00,0x3C,0x42,0x02,0x04,0x08,0x08,0x00,0x08,0x08,0x00,0x00,0x00,0x00},
/* @ */
{0x00,0x00,0x00,0x3C,0x42,0x5A,0x56,0x5A,0x40,0x42,0x3C,0x00,0x00,0x00,0x00,0x00},
/* A */
{0x00,0x00,0x00,0x18,0x24,0x42,0x42,0x7E,0x42,0x42,0x42,0x00,0x00,0x00,0x00,0x00},
/* B */
{0x00,0x00,0x00,0x7C,0x42,0x42,0x7C,0x42,0x42,0x42,0x7C,0x00,0x00,0x00,0x00,0x00},
/* C */
{0x00,0x00,0x00,0x3C,0x42,0x40,0x40,0x40,0x40,0x42,0x3C,0x00,0x00,0x00,0x00,0x00},
/* D */
{0x00,0x00,0x00,0x78,0x44,0x42,0x42,0x42,0x42,0x44,0x78,0x00,0x00,0x00,0x00,0x00},
/* E */
{0x00,0x00,0x00,0x7E,0x40,0x40,0x7C,0x40,0x40,0x40,0x7E,0x00,0x00,0x00,0x00,0x00},
/* F */
{0x00,0x00,0x00,0x7E,0x40,0x40,0x7C,0x40,0x40,0x40,0x40,0x00,0x00,0x00,0x00,0x00},
/* G */
{0x00,0x00,0x00,0x3C,0x42,0x40,0x4E,0x42,0x42,0x46,0x3A,0x00,0x00,0x00,0x00,0x00},
/* H */
{0x00,0x00,0x00,0x42,0x42,0x42,0x7E,0x42,0x42,0x42,0x42,0x00,0x00,0x00,0x00,0x00},
/* I */
{0x00,0x00,0x00,0x3E,0x08,0x08,0x08,0x08,0x08,0x08,0x3E,0x00,0x00,0x00,0x00,0x00},
/* J */
{0x00,0x00,0x00,0x0F,0x04,0x04,0x04,0x04,0x04,0x44,0x38,0x00,0x00,0x00,0x00,0x00},
/* K */
{0x00,0x00,0x00,0x42,0x44,0x48,0x50,0x60,0x50,0x48,0x44,0x00,0x00,0x00,0x00,0x00},
/* L */
{0x00,0x00,0x00,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x7E,0x00,0x00,0x00,0x00,0x00},
/* M */
{0x00,0x00,0x00,0x42,0x66,0x5A,0x5A,0x42,0x42,0x42,0x42,0x00,0x00,0x00,0x00,0x00},
/* N */
{0x00,0x00,0x00,0x42,0x62,0x52,0x4A,0x46,0x42,0x42,0x42,0x00,0x00,0x00,0x00,0x00},
/* O */
{0x00,0x00,0x00,0x3C,0x42,0x42,0x42,0x42,0x42,0x42,0x3C,0x00,0x00,0x00,0x00,0x00},
/* P */
{0x00,0x00,0x00,0x7C,0x42,0x42,0x42,0x7C,0x40,0x40,0x40,0x00,0x00,0x00,0x00,0x00},
/* Q */
{0x00,0x00,0x00,0x3C,0x42,0x42,0x42,0x42,0x4A,0x44,0x3A,0x00,0x00,0x00,0x00,0x00},
/* R */
{0x00,0x00,0x00,0x7C,0x42,0x42,0x7C,0x48,0x44,0x42,0x42,0x00,0x00,0x00,0x00,0x00},
/* S */
{0x00,0x00,0x00,0x3C,0x42,0x40,0x3C,0x02,0x02,0x42,0x3C,0x00,0x00,0x00,0x00,0x00},
/* T */
{0x00,0x00,0x00,0x7F,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x00,0x00,0x00,0x00,0x00},
/* U */
{0x00,0x00,0x00,0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x3C,0x00,0x00,0x00,0x00,0x00},
/* V */
{0x00,0x00,0x00,0x41,0x41,0x22,0x22,0x14,0x14,0x08,0x08,0x00,0x00,0x00,0x00,0x00},
/* W */
{0x00,0x00,0x00,0x42,0x42,0x42,0x5A,0x5A,0x66,0x42,0x42,0x00,0x00,0x00,0x00,0x00},
/* X */
{0x00,0x00,0x00,0x42,0x42,0x24,0x18,0x18,0x24,0x42,0x42,0x00,0x00,0x00,0x00,0x00},
/* Y */
{0x00,0x00,0x00,0x41,0x22,0x14,0x08,0x08,0x08,0x08,0x08,0x00,0x00,0x00,0x00,0x00},
/* Z */
{0x00,0x00,0x00,0x7E,0x02,0x04,0x08,0x10,0x20,0x40,0x7E,0x00,0x00,0x00,0x00,0x00},
/* [ */
{0x00,0x00,0x1E,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x1E,0x00,0x00,0x00,0x00},
/* \ */
{0x00,0x00,0x00,0x80,0x40,0x20,0x10,0x08,0x04,0x02,0x00,0x00,0x00,0x00,0x00,0x00},
/* ] */
{0x00,0x00,0x78,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x78,0x00,0x00,0x00,0x00},
/* ^ */
{0x00,0x00,0x08,0x14,0x22,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
/* _ */
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0x00,0x00,0x00,0x00},
/* ` */
{0x00,0x00,0x10,0x08,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
/* a */
{0x00,0x00,0x00,0x00,0x00,0x3C,0x02,0x3E,0x42,0x46,0x3A,0x00,0x00,0x00,0x00,0x00},
/* b */
{0x00,0x00,0x40,0x40,0x40,0x5C,0x62,0x42,0x42,0x62,0x5C,0x00,0x00,0x00,0x00,0x00},
/* c */
{0x00,0x00,0x00,0x00,0x00,0x3C,0x42,0x40,0x40,0x42,0x3C,0x00,0x00,0x00,0x00,0x00},
/* d */
{0x00,0x00,0x02,0x02,0x02,0x3A,0x46,0x42,0x42,0x46,0x3A,0x00,0x00,0x00,0x00,0x00},
/* e */
{0x00,0x00,0x00,0x00,0x00,0x3C,0x42,0x7E,0x40,0x42,0x3C,0x00,0x00,0x00,0x00,0x00},
/* f */
{0x00,0x00,0x1C,0x22,0x20,0x78,0x20,0x20,0x20,0x20,0x20,0x00,0x00,0x00,0x00,0x00},
/* g */
{0x00,0x00,0x00,0x00,0x00,0x3A,0x46,0x42,0x46,0x3A,0x02,0x42,0x3C,0x00,0x00,0x00},
/* h */
{0x00,0x00,0x40,0x40,0x40,0x5C,0x62,0x42,0x42,0x42,0x42,0x00,0x00,0x00,0x00,0x00},
/* i */
{0x00,0x00,0x08,0x08,0x00,0x18,0x08,0x08,0x08,0x08,0x3E,0x00,0x00,0x00,0x00,0x00},
/* j */
{0x00,0x00,0x04,0x04,0x00,0x0C,0x04,0x04,0x04,0x04,0x44,0x44,0x38,0x00,0x00,0x00},
/* k */
{0x00,0x00,0x40,0x40,0x44,0x48,0x50,0x60,0x50,0x48,0x44,0x00,0x00,0x00,0x00,0x00},
/* l */
{0x00,0x00,0x18,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x3E,0x00,0x00,0x00,0x00,0x00},
/* m */
{0x00,0x00,0x00,0x00,0x00,0x6C,0x52,0x52,0x52,0x52,0x52,0x00,0x00,0x00,0x00,0x00},
/* n */
{0x00,0x00,0x00,0x00,0x00,0x5C,0x62,0x42,0x42,0x42,0x42,0x00,0x00,0x00,0x00,0x00},
/* o */
{0x00,0x00,0x00,0x00,0x00,0x3C,0x42,0x42,0x42,0x42,0x3C,0x00,0x00,0x00,0x00,0x00},
/* p */
{0x00,0x00,0x00,0x00,0x00,0x5C,0x62,0x42,0x62,0x5C,0x40,0x40,0x40,0x00,0x00,0x00},
/* q */
{0x00,0x00,0x00,0x00,0x00,0x3A,0x46,0x42,0x46,0x3A,0x02,0x02,0x02,0x00,0x00,0x00},
/* r */
{0x00,0x00,0x00,0x00,0x00,0x5C,0x62,0x40,0x40,0x40,0x40,0x00,0x00,0x00,0x00,0x00},
/* s */
{0x00,0x00,0x00,0x00,0x00,0x3C,0x42,0x30,0x0C,0x42,0x3C,0x00,0x00,0x00,0x00,0x00},
/* t */
{0x00,0x00,0x20,0x20,0x20,0x78,0x20,0x20,0x20,0x22,0x1C,0x00,0x00,0x00,0x00,0x00},
/* u */
{0x00,0x00,0x00,0x00,0x00,0x42,0x42,0x42,0x42,0x46,0x3A,0x00,0x00,0x00,0x00,0x00},
/* v */
{0x00,0x00,0x00,0x00,0x00,0x42,0x42,0x24,0x24,0x18,0x18,0x00,0x00,0x00,0x00,0x00},
/* w */
{0x00,0x00,0x00,0x00,0x00,0x42,0x42,0x5A,0x5A,0x66,0x42,0x00,0x00,0x00,0x00,0x00},
/* x */
{0x00,0x00,0x00,0x00,0x00,0x42,0x24,0x18,0x18,0x24,0x42,0x00,0x00,0x00,0x00,0x00},
/* y */
{0x00,0x00,0x00,0x00,0x00,0x42,0x42,0x24,0x24,0x18,0x18,0x10,0x20,0x00,0x00,0x00},
/* z */
{0x00,0x00,0x00,0x00,0x00,0x7E,0x04,0x08,0x10,0x20,0x7E,0x00,0x00,0x00,0x00,0x00},
/* { */
{0x00,0x00,0x0E,0x10,0x10,0x10,0x20,0x10,0x10,0x10,0x10,0x0E,0x00,0x00,0x00,0x00},
/* | */
{0x00,0x00,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x00,0x00,0x00,0x00},
/* } */
{0x00,0x00,0x70,0x08,0x08,0x08,0x04,0x08,0x08,0x08,0x08,0x70,0x00,0x00,0x00,0x00},
/* ~ */
{0x00,0x00,0x00,0x00,0x00,0x32,0x4C,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
};
#endif /* __OLED_FONT_H */
6.14 文件:Hardware/relay.h
继电器驱动头文件。
c
/**
* @file relay.h
* @brief 继电器驱动头文件
* @author SmartWater Team
* @date 2026-05-29
*/
#ifndef __RELAY_H
#define __RELAY_H
#include "stm32f10x.h"
void Relay_Init(void);
void Relay_On(void);
void Relay_Off(void);
void Relay_Toggle(void);
uint8_t Relay_GetStatus(void);
#endif /* __RELAY_H */
6.15 文件:Hardware/relay.c
继电器驱动实现。
c
/**
* @file relay.c
* @brief 继电器驱动实现
* @author SmartWater Team
* @date 2026-05-29
*/
#include "relay.h"
#include "main.h"
static uint8_t g_relayStatus = 0; /* 继电器状态: 0关闭, 1开启 */
/**
* @brief 继电器初始化
* @param 无
* @retval 无
*/
void Relay_Init(void)
{
/* GPIO配置已在main中完成 */
Relay_Off(); /* 初始状态关闭 */
printf("Relay initialized (OFF)\r\n");
}
/**
* @brief 继电器吸合(开启加热)
* @param 无
* @retval 无
*/
void Relay_On(void)
{
GPIO_SetBits(RELAY_PORT, RELAY_PIN);
g_relayStatus = 1;
}
/**
* @brief 继电器断开(停止加热)
* @param 无
* @retval 无
*/
void Relay_Off(void)
{
GPIO_ResetBits(RELAY_PORT, RELAY_PIN);
g_relayStatus = 0;
}
/**
* @brief 继电器状态翻转
* @param 无
* @retval 无
*/
void Relay_Toggle(void)
{
if (g_relayStatus)
{
Relay_Off();
}
else
{
Relay_On();
}
}
/**
* @brief 获取继电器状态
* @param 无
* @retval uint8_t: 0关闭, 1开启
*/
uint8_t Relay_GetStatus(void)
{
return g_relayStatus;
}
6.16 文件:Hardware/key.h
按键驱动头文件。
c
/**
* @file key.h
* @brief 按键驱动头文件
* @author SmartWater Team
* @date 2026-05-29
*/
#ifndef __KEY_H
#define __KEY_H
#include "stm32f10x.h"
/* 按键值定义 */
#define KEY_NONE 0
#define KEY_MENU 1
#define KEY_UP 2
#define KEY_DOWN 3
#define KEY_OK 4
void Key_Init(void);
void Key_Scan(void);
uint8_t Key_GetValue(void);
#endif /* __KEY_H */
6.17 文件:Hardware/key.c
按键驱动实现,包含软件消抖和长按检测。
c
/**
* @file key.c
* @brief 按键驱动实现(带消抖)
* @author SmartWater Team
* @date 2026-05-29
*/
#include "key.h"
#include "main.h"
#include "delay.h"
/* 按键消抖时间(ms) */
#define KEY_DEBOUNCE_MS 20
/* 长按时间阈值(ms) */
#define KEY_LONG_PRESS_MS 800
/* 按键状态结构体 */
typedef struct {
uint8_t currentState; /* 当前状态(未按下:1, 按下:0) */
uint8_t lastState; /* 上一次状态 */
uint8_t stableState; /* 消抖后的稳定状态 */
uint32_t pressTime; /* 按下持续时间(ms) */
uint8_t processed; /* 是否已处理 */
} KeyState_t;
static KeyState_t g_keyStates[4]; /* 四个按键的状态 */
/**
* @brief 按键初始化
* @param 无
* @retval 无
*/
void Key_Init(void)
{
uint8_t i;
/* GPIO配置已在main中完成 */
for (i = 0; i < 4; i++)
{
g_keyStates[i].currentState = 1;
g_keyStates[i].lastState = 1;
g_keyStates[i].stableState = 1;
g_keyStates[i].pressTime = 0;
g_keyStates[i].processed = 0;
}
printf("Keys initialized\r\n");
}
/**
* @brief 按键扫描(需要在定时中断中周期调用, 每10ms一次)
* @param 无
* @retval 无
*/
void Key_Scan(void)
{
uint8_t i;
uint16_t pinMask[4] = {KEY_MENU_PIN, KEY_UP_PIN, KEY_DOWN_PIN, KEY_OK_PIN};
static uint32_t debounceTimer[4] = {0, 0, 0, 0};
for (i = 0; i < 4; i++)
{
/* 读取当前引脚状态 */
g_keyStates[i].currentState = GPIO_ReadInputDataBit(KEY_PORT, pinMask[i]);
/* 状态变化检测 */
if (g_keyStates[i].currentState != g_keyStates[i].lastState)
{
/* 重置消抖计时器 */
debounceTimer[i] = 0;
g_keyStates[i].lastState = g_keyStates[i].currentState;
}
else
{
/* 状态稳定,累加消抖时间 */
debounceTimer[i] += 10; /* 每次调用间隔10ms */
if (debounceTimer[i] >= KEY_DEBOUNCE_MS)
{
/* 消抖完成,更新稳定状态 */
if (g_keyStates[i].stableState != g_keyStates[i].currentState)
{
g_keyStates[i].stableState = g_keyStates[i].currentState;
/* 按键按下(下降沿) */
if (g_keyStates[i].stableState == 0)
{
g_keyStates[i].pressTime = 0;
g_keyStates[i].processed = 0;
/* 设置按键值 */
g_sysState.keyValue = i + 1; /* KEY_MENU=1, KEY_UP=2... */
g_sysState.keyPressed = 1;
}
/* 按键释放(上升沿) */
else
{
/* 如果短按已处理,清除按键值 */
if (g_keyStates[i].processed)
{
/* 不重复设置 */
}
}
}
}
}
/* 按键持续按下计时 */
if (g_keyStates[i].stableState == 0 && !g_keyStates[i].processed)
{
g_keyStates[i].pressTime += 10;
/* 长按检测 */
if (g_keyStates[i].pressTime >= KEY_LONG_PRESS_MS)
{
g_keyStates[i].processed = 1;
/* 长按可以触发特殊功能,这里暂不实现 */
}
}
}
}
/**
* @brief 获取当前按键值
* @param 无
* @retval uint8_t: 按键值
*/
uint8_t Key_GetValue(void)
{
uint8_t value = g_sysState.keyValue;
g_sysState.keyValue = 0; /* 读取后清除 */
return value;
}
6.18 文件:Hardware/buzzer.h
蜂鸣器驱动头文件。
c
/**
* @file buzzer.h
* @brief 蜂鸣器驱动头文件
* @author SmartWater Team
* @date 2026-05-29
*/
#ifndef __BUZZER_H
#define __BUZZER_H
#include "stm32f10x.h"
void Buzzer_Init(void);
void Buzzer_On(void);
void Buzzer_Off(void);
void Buzzer_Beep(uint32_t durationMs);
void Buzzer_BeepTimes(uint8_t times, uint32_t durationMs, uint32_t intervalMs);
#endif /* __BUZZER_H */
6.19 文件:Hardware/buzzer.c
蜂鸣器驱动实现。
c
/**
* @file buzzer.c
* @brief 蜂鸣器驱动实现
* @author SmartWater Team
* @date 2026-05-29
*/
#include "buzzer.h"
#include "main.h"
#include "delay.h"
static uint8_t g_buzzerStatus = 0; /* 蜂鸣器状态 */
/**
* @brief 蜂鸣器初始化
* @param 无
* @retval 无
*/
void Buzzer_Init(void)
{
/* GPIO配置已在main中完成 */
Buzzer_Off();
printf("Buzzer initialized\r\n");
}
/**
* @brief 蜂鸣器开启
* @param 无
* @retval 无
*/
void Buzzer_On(void)
{
GPIO_SetBits(BUZZER_PORT, BUZZER_PIN);
g_buzzerStatus = 1;
}
/**
* @brief 蜂鸣器关闭
* @param 无
* @retval 无
*/
void Buzzer_Off(void)
{
GPIO_ResetBits(BUZZER_PORT, BUZZER_PIN);
g_buzzerStatus = 0;
}
/**
* @brief 蜂鸣器鸣叫指定时间
* @param durationMs: 鸣叫持续时间(毫秒)
* @retval 无
*/
void Buzzer_Beep(uint32_t durationMs)
{
Buzzer_On();
Delay_Ms(durationMs);
Buzzer_Off();
}
/**
* @brief 蜂鸣器鸣叫指定次数
* @param times: 鸣叫次数
* @param durationMs: 每次鸣叫持续时间(毫秒)
* @param intervalMs: 鸣叫间隔时间(毫秒)
* @retval 无
*/
void Buzzer_BeepTimes(uint8_t times, uint32_t durationMs, uint32_t intervalMs)
{
uint8_t i;
for (i = 0; i < times; i++)
{
Buzzer_Beep(durationMs);
if (i < times - 1)
{
Delay_Ms(intervalMs);
}
}
}
6.20 文件:Hardware/eeprom.h
AT24C02 EEPROM驱动头文件。
c
/**
* @file eeprom.h
* @brief AT24C02 EEPROM驱动头文件
* @author SmartWater Team
* @date 2026-05-29
*/
#ifndef __EEPROM_H
#define __EEPROM_H
#include "stm32f10x.h"
/* AT24C02 容量: 256字节 */
#define EEPROM_SIZE 256
#define EEPROM_PAGE_SIZE 8 /* 页写入大小 */
void EEPROM_Init(void);
uint8_t EEPROM_ReadByte(uint16_t addr);
void EEPROM_WriteByte(uint16_t addr, uint8_t data);
void EEPROM_ReadBuffer(uint16_t addr, uint8_t *buf, uint16_t len);
void EEPROM_WriteBuffer(uint16_t addr, uint8_t *buf, uint16_t len);
#endif /* __EEPROM_H */
6.21 文件:Hardware/eeprom.c
AT24C02 EEPROM驱动实现,基于I2C通信。
c
/**
* @file eeprom.c
* @brief AT24C02 EEPROM驱动实现
* @author SmartWater Team
* @date 2026-05-29
*/
#include "eeprom.h"
#include "delay.h"
#include "main.h"
#include <string.h>
/* AT24C02 I2C地址: 0xA0(写), 0xA1(读) */
#define EEPROM_I2C_ADDR_WRITE 0xA0
#define EEPROM_I2C_ADDR_READ 0xA1
/* 软件I2C操作(复用OLED中的I2C函数) */
/* 注意: 这些函数与oled.c中的函数是相同的I2C总线操作 */
/* 为了避免重复定义,这里使用extern声明 */
extern void I2C_Start(void);
extern void I2C_Stop(void);
extern uint8_t I2C_WaitAck(void);
extern void I2C_SendByte(uint8_t data);
extern uint8_t I2C_ReadByte(uint8_t ack);
/**
* @brief EEPROM初始化
* @param 无
* @retval 无
*/
void EEPROM_Init(void)
{
/* I2C初始化已在main中完成 */
printf("EEPROM AT24C02 initialized\r\n");
}
/**
* @brief 向EEPROM指定地址写入一个字节
* @param addr: 写入地址(0-255)
* @param data: 要写入的数据
* @retval 无
*/
void EEPROM_WriteByte(uint16_t addr, uint8_t data)
{
if (addr >= EEPROM_SIZE) return;
/* 起始信号 */
I2C_Start();
/* 发送设备地址(写) */
I2C_SendByte(EEPROM_I2C_ADDR_WRITE);
if (I2C_WaitAck())
{
I2C_Stop();
return;
}
/* 发送内存地址 */
I2C_SendByte((uint8_t)addr);
if (I2C_WaitAck())
{
I2C_Stop();
return;
}
/* 发送数据 */
I2C_SendByte(data);
if (I2C_WaitAck())
{
I2C_Stop();
return;
}
/* 停止信号 */
I2C_Stop();
/* 等待写入完成(AT24C02需要最多5ms) */
Delay_Ms(10);
}
/**
* @brief 从EEPROM指定地址读取一个字节
* @param addr: 读取地址(0-255)
* @retval uint8_t: 读取的数据
*/
uint8_t EEPROM_ReadByte(uint16_t addr)
{
uint8_t data = 0;
if (addr >= EEPROM_SIZE) return 0;
/* 起始信号 */
I2C_Start();
/* 发送设备地址(写) - 设置读取地址 */
I2C_SendByte(EEPROM_I2C_ADDR_WRITE);
if (I2C_WaitAck())
{
I2C_Stop();
return 0;
}
/* 发送内存地址 */
I2C_SendByte((uint8_t)addr);
if (I2C_WaitAck())
{
I2C_Stop();
return 0;
}
/* 重新起始(重复起始条件) */
I2C_Start();
/* 发送设备地址(读) */
I2C_SendByte(EEPROM_I2C_ADDR_READ);
if (I2C_WaitAck())
{
I2C_Stop();
return 0;
}
/* 读取数据(发送非应答) */
data = I2C_ReadByte(1);
/* 停止信号 */
I2C_Stop();
return data;
}
/**
* @brief 从EEPROM读取多个字节
* @param addr: 起始地址
* @param buf: 数据缓冲区
* @param len: 读取长度
* @retval 无
*/
void EEPROM_ReadBuffer(uint16_t addr, uint8_t *buf, uint16_t len)
{
uint16_t i;
if (addr + len > EEPROM_SIZE) return;
/* 起始信号 */
I2C_Start();
/* 发送设备地址(写) - 设置读取地址 */
I2C_SendByte(EEPROM_I2C_ADDR_WRITE);
if (I2C_WaitAck()) { I2C_Stop(); return; }
/* 发送内存地址 */
I2C_SendByte((uint8_t)addr);
if (I2C_WaitAck()) { I2C_Stop(); return; }
/* 重新起始 */
I2C_Start();
/* 发送设备地址(读) */
I2C_SendByte(EEPROM_I2C_ADDR_READ);
if (I2C_WaitAck()) { I2C_Stop(); return; }
/* 连续读取 */
for (i = 0; i < len; i++)
{
/* 最后一个字节发送非应答,其余发送应答 */
buf[i] = I2C_ReadByte((i == len - 1) ? 1 : 0);
}
I2C_Stop();
}
/**
* @brief 向EEPROM写入多个字节(自动处理页边界)
* @param addr: 起始地址
* @param buf: 数据缓冲区
* @param len: 写入长度
* @retval 无
*/
void EEPROM_WriteBuffer(uint16_t addr, uint8_t *buf, uint16_t len)
{
uint16_t i;
uint8_t pageOffset;
uint8_t writeLen;
if (addr + len > EEPROM_SIZE) return;
i = 0;
while (i < len)
{
/* 计算当前页剩余空间 */
pageOffset = addr % EEPROM_PAGE_SIZE;
writeLen = EEPROM_PAGE_SIZE - pageOffset;
/* 不超过剩余数据长度 */
if (writeLen > (len - i))
{
writeLen = len - i;
}
/* 起始信号 */
I2C_Start();
/* 发送设备地址(写) */
I2C_SendByte(EEPROM_I2C_ADDR_WRITE);
if (I2C_WaitAck()) { I2C_Stop(); return; }
/* 发送内存地址 */
I2C_SendByte((uint8_t)addr);
if (I2C_WaitAck()) { I2C_Stop(); return; }
/* 连续写入当前页 */
for (uint8_t j = 0; j < writeLen; j++)
{
I2C_SendByte(buf[i + j]);
if (I2C_WaitAck()) { I2C_Stop(); return; }
}
I2C_Stop();
/* 等待写入完成 */
Delay_Ms(10);
/* 更新地址和索引 */
addr += writeLen;
i += writeLen;
}
}
七、编译与下载
7.1 工程编译设置
在Keil中,点击 Project → Options for Target 'Target 1',进行以下设置:
Target 标签页:
- Xtal(MHz): 8.0
- 勾选 Use MicroLIB(减少代码体积,支持printf)
Output 标签页:
- 勾选 Create HEX File(生成HEX文件)
C/C++ 标签页:
- Optimization: Level 0 (-O0)(调试阶段不优化)
- Include Paths: 添加 User、Hardware、System 文件夹路径
- Define: 添加
STM32F10X_MD,USE_STDPERIPH_DRIVER
Debug 标签页:
- Use: ST-Link Debugger
- 点击Settings,在Flash Download中选择Erase Sectors,Programming Algorithm选择STM32F10x Med-density Flash
7.2 编译下载步骤
- 将ST-Link调试器连接电脑USB口
- 将ST-Link的SWDIO、SWCLK、GND、3.3V连接到STM32最小系统板对应引脚
- 点击Keil工具栏的
Build按钮(或按F7)编译工程 - 确认编译窗口显示
0 Error(s), 0 Warning(s) - 点击
Download按钮(或按F8)下载程序到开发板 - 下载完成后,按下开发板复位键,程序开始运行
7.3 串口调试
- 使用USB转TTL模块连接STM32的PA9(TX)和PA10(RX)
- 打开串口调试助手,设置参数:波特率115200、8数据位、1停止位、无校验
- 复位开发板,即可在串口助手看到系统启动信息和实时运行数据
八、系统工作流程详解
为了让零基础的同学更好地理解整个系统是如何运作的,下面用流程图详细展示系统从开机到正常工作的完整过程。
#mermaid-svg-f6tfg4TMfaXUhiIj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-f6tfg4TMfaXUhiIj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-f6tfg4TMfaXUhiIj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-f6tfg4TMfaXUhiIj .error-icon{fill:#552222;}#mermaid-svg-f6tfg4TMfaXUhiIj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-f6tfg4TMfaXUhiIj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-f6tfg4TMfaXUhiIj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-f6tfg4TMfaXUhiIj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-f6tfg4TMfaXUhiIj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-f6tfg4TMfaXUhiIj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-f6tfg4TMfaXUhiIj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-f6tfg4TMfaXUhiIj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-f6tfg4TMfaXUhiIj .marker.cross{stroke:#333333;}#mermaid-svg-f6tfg4TMfaXUhiIj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-f6tfg4TMfaXUhiIj p{margin:0;}#mermaid-svg-f6tfg4TMfaXUhiIj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-f6tfg4TMfaXUhiIj .cluster-label text{fill:#333;}#mermaid-svg-f6tfg4TMfaXUhiIj .cluster-label span{color:#333;}#mermaid-svg-f6tfg4TMfaXUhiIj .cluster-label span p{background-color:transparent;}#mermaid-svg-f6tfg4TMfaXUhiIj .label text,#mermaid-svg-f6tfg4TMfaXUhiIj span{fill:#333;color:#333;}#mermaid-svg-f6tfg4TMfaXUhiIj .node rect,#mermaid-svg-f6tfg4TMfaXUhiIj .node circle,#mermaid-svg-f6tfg4TMfaXUhiIj .node ellipse,#mermaid-svg-f6tfg4TMfaXUhiIj .node polygon,#mermaid-svg-f6tfg4TMfaXUhiIj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-f6tfg4TMfaXUhiIj .rough-node .label text,#mermaid-svg-f6tfg4TMfaXUhiIj .node .label text,#mermaid-svg-f6tfg4TMfaXUhiIj .image-shape .label,#mermaid-svg-f6tfg4TMfaXUhiIj .icon-shape .label{text-anchor:middle;}#mermaid-svg-f6tfg4TMfaXUhiIj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-f6tfg4TMfaXUhiIj .rough-node .label,#mermaid-svg-f6tfg4TMfaXUhiIj .node .label,#mermaid-svg-f6tfg4TMfaXUhiIj .image-shape .label,#mermaid-svg-f6tfg4TMfaXUhiIj .icon-shape .label{text-align:center;}#mermaid-svg-f6tfg4TMfaXUhiIj .node.clickable{cursor:pointer;}#mermaid-svg-f6tfg4TMfaXUhiIj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-f6tfg4TMfaXUhiIj .arrowheadPath{fill:#333333;}#mermaid-svg-f6tfg4TMfaXUhiIj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-f6tfg4TMfaXUhiIj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-f6tfg4TMfaXUhiIj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-f6tfg4TMfaXUhiIj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-f6tfg4TMfaXUhiIj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-f6tfg4TMfaXUhiIj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-f6tfg4TMfaXUhiIj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-f6tfg4TMfaXUhiIj .cluster text{fill:#333;}#mermaid-svg-f6tfg4TMfaXUhiIj .cluster span{color:#333;}#mermaid-svg-f6tfg4TMfaXUhiIj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-f6tfg4TMfaXUhiIj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-f6tfg4TMfaXUhiIj rect.text{fill:none;stroke-width:0;}#mermaid-svg-f6tfg4TMfaXUhiIj .icon-shape,#mermaid-svg-f6tfg4TMfaXUhiIj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-f6tfg4TMfaXUhiIj .icon-shape p,#mermaid-svg-f6tfg4TMfaXUhiIj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-f6tfg4TMfaXUhiIj .icon-shape .label rect,#mermaid-svg-f6tfg4TMfaXUhiIj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-f6tfg4TMfaXUhiIj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-f6tfg4TMfaXUhiIj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-f6tfg4TMfaXUhiIj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
是
否
菜单键
调节键
确认键
无操作
开机
系统初始化
加载EEPROM配置
读取到
有效配置?
使用已保存的
目标温度/单价
使用默认配置
并写入EEPROM
进入主循环
周期性读取温度
PID计算加热输出
需要加热?
继电器导通
加热棒工作
继电器断开
停止加热
更新显示数据
检测到
水流脉冲?
累计脉冲
计算流量
流量归零处理
更新费用
保持当前费用
更新OLED显示
按键操作?
切换设置界面
修改参数值
保存到EEPROM
九、实际操作演示
9.1 首次开机
系统上电后,OLED会显示开机画面 Smart Water Dispenser V1.0 和 Initializing...,持续2秒后进入主界面。此时如果EEPROM中没有有效数据,系统会使用默认配置:目标温度85°C,单价0.50元/升。
9.2 设置目标温度
- 按下 MENU键,进入温度设置界面
- 屏幕显示当前目标温度和实时水温
- 按 UP键 增加目标温度(每次+1°C,上限100°C)
- 按 DOWN键 降低目标温度(每次-1°C,下限40°C)
- 按 OK键 保存并进入单价设置,或按 MENU键 跳到单价设置
9.3 设置单价
- 在单价设置界面,屏幕显示当前单价和累计总费用
- 按 UP/DOWN键 调整单价(每次±0.10元)
- 按 OK键 保存所有设置并返回主界面
9.4 正常工作状态
主界面显示内容:
- 第一行:当前水温(实时更新)
- 第二行:目标温度和加热状态(HEAT加热中/IDLE待机)
- 第三行:本次用水量和瞬时流量
- 第四行:本次费用
- 第五行:历史累计总费用
- 第六行:状态提示(Heating...加热中/READY温度达标)
- 第七行:按键提示
9.5 PID控温效果
系统采用位置式PID算法控制加热棒:
- 当实际温度远低于目标温度时,PID输出较大,继电器持续导通加热
- 当温度接近目标时,PID输出逐渐减小
- 当温度达到目标±0.5°C时,系统判定温度达标,蜂鸣器短鸣提示
- 输出小于阈值时自动切断加热,防止过冲
9.6 流量计费验证
当水流经传感器时:
- YF-S201输出脉冲信号(450脉冲/升)
- 系统实时计算瞬时流量和累计用水量
- 费用 = 用水量 × 单价,自动累加到本次费用和总费用
- 停止用水后,费用保持不变
十、PID参数整定指南
PID参数的整定直接影响温控效果。以下是针对本项目的整定建议:
10.1 手动整定步骤
第一步:设置纯比例控制
将Ki和Kd设为0,逐步增大Kp直到系统出现轻微振荡:
PID_SetTunings(Kp, 0, 0);
建议从Kp=20开始,每次增加10,观察温度响应曲线。
第二步:加入积分控制
确定Kp后,逐步增大Ki消除稳态误差:
PID_SetTunings(Kp, Ki, 0);
Ki从0.5开始,每次增加0.5,直到稳态误差在1分钟内消除。
第三步:加入微分控制
最后加入Kd抑制超调:
PID_SetTunings(Kp, Ki, Kd);
Kd从1开始,每次增加1,直到温度上升平稳无明显超调。
10.2 推荐参数组合
| 加热功率 | Kp | Ki | Kd | 适用场景 |
|---|---|---|---|---|
| 50W | 60 | 1.5 | 4 | 小容量加热 |
| 100W | 80 | 2.0 | 5 | 中等容量(默认) |
| 200W | 120 | 3.0 | 8 | 大容量快速加热 |
十一、常见问题排查
11.1 温度显示异常
症状: 温度显示-999.0或明显错误的值
排查步骤:
- 检查DS18B20接线是否正确(特别是4.7K上拉电阻)
- 用万用表测量DS18B20的VCC引脚是否为3.3V
- 检查PA0引脚是否与DS18B20数据线正确连接
- 查看串口输出是否有"DS18B20 reset failed"或"CRC error"
11.2 OLED无显示
症状: 屏幕完全不亮或显示乱码
排查步骤:
- 检查OLED的VCC和GND是否正确连接
- 用万用表测量I2C引脚SCL(PB6)和SDA(PB7)是否有信号
- 确认OLED的I2C地址是否正确(通常为0x3C,代码中左移后为0x78)
- 检查OLED模块背面是否有焊接不良
11.3 流量计不计数
症状: 有水流动但计数始终为0
排查步骤:
- 确认YF-S201的供电电压为5V
- 检查信号线是否连接到PA1
- 用示波器或逻辑分析仪查看PA1引脚是否有脉冲信号
- 检查定时器TIM2的中断配置是否正常
11.4 继电器不动作
症状: 加热指示灯亮但加热棒不工作
排查步骤:
- 检查继电器模块的VCC(5V)和GND
- 检查IN信号引脚是否连接到PB0
- 用万用表测量继电器输出端是否导通
- 注意区分继电器模块是高电平触发还是低电平触发
十二、扩展功能建议
本项目已经实现了核心的温控和计费功能,你可以在此基础上扩展以下功能:
- WiFi联网功能: 添加ESP8266模块,实现手机APP远程控制和数据查看
- 语音播报: 集成语音合成模块,播报当前水温、费用等信息
- RFID刷卡: 添加RC522模块,实现刷卡取水、自动扣费
- 水质检测: 集成TDS传感器,检测水质并在OLED上显示
- 数据记录: 扩展EEPROM或添加SD卡模块,记录每日用水数据
- 多路出水: 增加电磁阀控制,实现冷热水分路出水
- 定时功能: 添加RTC时钟模块,实现定时加热和用水统计
十三、总结
通过本教程,我们从零开始完整实现了一台智能饮水机的核心控制系统。项目涵盖了STM32的GPIO操作、定时器中断、I2C通信、单总线通信、PID控制算法、EEPROM数据存储等多个关键技术点。
整个代码架构采用模块化设计,每个外设都有独立的驱动文件,便于维护和移植。PID控制器的加入使得温度控制更加精准和平稳,流量计费功能则为商用场景提供了基础支持。
希望这份详尽的教程能帮助你顺利搭建出属于自己的智能饮水机系统。如果在搭建过程中遇到任何问题,建议先通过串口调试输出来定位问题所在,再针对性地检查硬件连接和代码逻辑。