简介:这份资源是面向嵌入式开发初学者与电子设计竞赛参赛者的智能饮水机控制系统完整项目包,以STM32F103C8T6为核心主控,配合DS18B20数字温度传感器与液位检测模块,实现水温实时监测、水位自动控制与远程监控功能,可用于课程设计、毕业设计或赛题方案参考。压缩包共291个文件,约23.2MB,涵盖40个C源码与38个头文件构成的主控程序、38个编译中间文件、37个CRF工程索引、27个JS与Vue前端页面、25张PNG界面截图,以及PCB原理图、封装库、烧录固件与APK安装包等,软硬件资料齐全。目前已有38人学习下载。读者可从中获取完整的STM32工程源码、温度采集与液位检测驱动逻辑、上位机与移动端交互界面,以及从原理图到固件的全链路实现思路,便于快速复现与二次开发。
1. 智能饮水机控制系统:从DS18B20到液位检测的工程落地拆解
饮水机这个品类看起来简单,但真正把它做成一台带恒温控制、缺水保护、防干烧的智能设备,涉及的技术点并不少。STM32F103C8T6 作为主控,配合 DS18B20 数字温度传感器和液位检测模块,是一套成本可控、资料丰富、适合从零搭建的方案。我选择这个组合的原因很直接:DS18B20 单总线输出数字量,省去 ADC 校准的麻烦;液位检测用导电式或浮球式都能快速验证;STM32F103C8T6 的 GPIO 和外设资源足够覆盖加热控制、按键交互和显示输出。这套系统适合有 C 语言基础、想从最小系统板开始做一个完整闭环控制项目的工程师,也适合作为嵌入式课程设计或毕业设计的落地参考。接下来我会按硬件选型、DS18B20 时序驱动、液位检测逻辑、加热控制策略、避坑排查、进阶调试的顺序,把每个环节的参数和代码讲清楚。
2. 硬件选型与最小系统搭建:STM32F103C8T6 怎么接线才不翻车
2.1 主控芯片与最小系统板的引脚分配
STM32F103C8T6 是 LQFP48 封装,48 个引脚里可用 GPIO 有 37 个。做饮水机控制,我一般按功能块划分引脚:DS18B20 占用一个 GPIO 做单总线通信,液位检测占用一个 GPIO 做数字输入或 ADC 输入,加热继电器占用一个 GPIO 做输出,按键占用两到三个 GPIO,OLED 或 LCD 显示占用 I2C 或 SPI 的一组引脚。具体分配如下表:
| 功能模块 | 引脚 | 模式 | 备注 |
|---|---|---|---|
| DS18B20 数据线 | PA0 | 开漏输出/浮空输入 | 需外接 4.7k 上拉电阻 |
| 液位检测(数字式) | PA1 | 浮空输入 | 高电平表示有水 |
| 加热继电器控制 | PA2 | 推挽输出 | 驱动继电器或 MOS 管 |
| 按键1(温度+) | PA3 | 上拉输入 | 内部上拉 |
| 按键2(温度-) | PA4 | 上拉输入 | 内部上拉 |
| OLED SCL | PB6 | 复用开漏 | I2C1 |
| OLED SDA | PB7 | 复用开漏 | I2C1 |
接线时注意 DS18B20 的 VDD 和 GND 不要接反,我见过至少三次因为反接导致传感器发烫报废的情况。液位检测模块如果是导电式,探针长期泡水会有电解腐蚀,建议用不锈钢探针或者改用浮球开关。
2.2 新建基于标准库的工程模板
热词里有人搜「stm32f103c8t6 keil4下建立工程」,我建议直接用 Keil MDK5 配合标准外设库,比 HAL 库更贴近寄存器,调试时心里有底。新建工程的步骤:
# 目录结构建议 Project/ ├── CMSIS/ # 内核支持文件 ├── FWLIB/ # 标准外设库 │ ├── inc/ │ └── src/ ├── USER/ │ ├── main.c │ ├── stm32f10x_it.c │ └── system_stm32f10x.c └── HARDWARE/ ├── ds18b20.c ├── ds18b20.h ├── liquid.c └── liquid.h在 Keil 中新建工程后,把 CMSIS 和 FWLIB 的文件按分组添加进去,然后在 Options for Target 的 C/C++ 选项卡里添加头文件路径。关键宏定义要加上USE_STDPERIPH_DRIVER和STM32F10X_MD,否则编译会报错。启动文件选startup_stm32f10x_md.s,对应中容量产品。
注意:STM32F103C8T6 的 Flash 是 64KB,属于中容量,启动文件和宏定义必须匹配,选错会导致中断向量表偏移错误。
3. DS18B20 温度采集:单总线时序与 12 位精度配置
3.1 DS18B20 的复位、写、读时序实现
DS18B20 的通信全靠一根数据线,时序要求比较严格。复位脉冲是主机拉低至少 480us,然后释放,等待 15-60us 后检测从机是否拉低 60-240us。写时隙分写 0 和写 1,写 0 是拉低 60us 以上再释放,写 1 是拉低 1-15us 后释放。读时隙是主机拉低 1us 后释放,在 15us 内采样。
// ds18b20.c 关键时序函数 #include "ds18b20.h" #include "delay.h" #define DS18B20_GPIO_PORT GPIOA #define DS18B20_GPIO_PIN GPIO_Pin_0 // 复位 DS18B20,返回 0 表示存在,1 表示不存在 u8 DS18B20_Reset(void) { u8 presence; GPIO_InitTypeDef GPIO_InitStructure; // 配置为推挽输出 GPIO_InitStructure.GPIO_Pin = DS18B20_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(DS18B20_GPIO_PORT, &GPIO_InitStructure); GPIO_ResetBits(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN); // 拉低 delay_us(480); GPIO_SetBits(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN); // 释放 delay_us(60); // 配置为浮空输入 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(DS18B20_GPIO_PORT, &GPIO_InitStructure); presence = GPIO_ReadInputDataBit(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN); delay_us(420); return presence; // 0 表示检测到存在脉冲 } // 写一个字节 void DS18B20_WriteByte(u8 dat) { u8 i; GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = DS18B20_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(DS18B20_GPIO_PORT, &GPIO_InitStructure); for (i = 0; i < 8; i++) { GPIO_ResetBits(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN); delay_us(2); if (dat & 0x01) GPIO_SetBits(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN); else GPIO_ResetBits(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN); delay_us(60); GPIO_SetBits(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN); delay_us(2); dat >>= 1; } } // 读一个字节 u8 DS18B20_ReadByte(void) { u8 i, dat = 0; GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = DS18B20_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(DS18B20_GPIO_PORT, &GPIO_InitStructure); for (i = 0; i < 8; i++) { dat >>= 1; GPIO_ResetBits(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN); delay_us(2); GPIO_SetBits(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(DS18B20_GPIO_PORT, &GPIO_InitStructure); delay_us(10); if (GPIO_ReadInputDataBit(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN)) dat |= 0x80; delay_us(50); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(DS18B20_GPIO_PORT, &GPIO_InitStructure); GPIO_SetBits(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN); } return dat; }上面三个函数是 DS18B20 驱动的核心。DS18B20_Reset负责检测传感器是否存在,如果返回 1 说明接线有问题或者传感器损坏。DS18B20_WriteByte和DS18B20_ReadByte按位操作,每个时隙的延时用delay_us实现。注意delay_us的精度直接影响通信成功率,如果用的是 SysTick 延时,确保系统时钟配置正确。
3.2 温度转换与 12 位精度读取
DS18B20 上电默认是 12 位精度,转换时间 750ms。读取温度时先发0xCC跳过 ROM,再发0x44启动转换,延时后再发0xCC和0xBE读取暂存器。
// 读取温度值,返回单位为 0.1 摄氏度 short DS18B20_GetTemp(void) { u8 temp_l, temp_h; short temp; DS18B20_Reset(); DS18B20_WriteByte(0xCC); // 跳过 ROM DS18B20_WriteByte(0x44); // 启动温度转换 delay_ms(750); // 12 位精度需要 750ms DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); // 读取暂存器 temp_l = DS18B20_ReadByte(); temp_h = DS18B20_ReadByte(); temp = (temp_h << 8) | temp_l; // 12 位精度下,低 4 位是小数部分 temp = temp * 10 / 16; // 转换为 0.1 摄氏度单位 return temp; }temp_h和temp_l组成 16 位有符号数,低 4 位是小数。乘以 10 再除以 16 得到 0.1 摄氏度的整数表示,比如 235 表示 23.5 摄氏度。负温度时高 5 位是符号位,需要做补码处理,实际项目中水温不会低于 0 度,可以暂时不处理。
提示:如果读取温度一直是 85 度,说明转换还没完成就读取了暂存器,检查延时是否足够。如果一直是 0 度,检查上拉电阻是否接好。
4. 液位检测与加热控制:从缺水保护到 PID 恒温
4.1 液位检测模块的两种接入方式
液位检测常见的有导电式和浮球式。导电式用两个探针,有水时电阻降低,通过比较器输出数字信号;浮球式靠磁簧开关,水位到达时闭合。我一般用导电式接 GPIO 做数字输入,配置成浮空输入,检测高电平表示有水。
// liquid.c #include "liquid.h" #define LIQUID_GPIO_PORT GPIOA #define LIQUID_GPIO_PIN GPIO_Pin_1 void Liquid_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = LIQUID_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(LIQUID_GPIO_PORT, &GPIO_InitStructure); } // 返回 1 表示有水,0 表示缺水 u8 Liquid_Check(void) { return GPIO_ReadInputDataBit(LIQUID_GPIO_PORT, LIQUID_GPIO_PIN); }如果用水位传感器输出模拟量,可以接 ADC 通道,通过阈值判断水位高低。STM32F103C8T6 的 ADC1 有 10 个外部通道,PA1 对应通道 1。配置 ADC 时注意采样时间设为 55.5 个周期,避免阻抗不匹配导致读数跳动。
4.2 加热控制逻辑与继电器驱动
加热控制的核心是:温度低于设定值减回差时开加热,高于设定值时关加热。回差一般设 1-2 度,防止继电器频繁切换。如果要做恒温,可以用 PID 控制 PWM 输出驱动 MOS 管,但继电器方案更简单,适合饮水机这种大惯性系统。
// main.c 中的控制逻辑片段 #define TEMP_SET_DEFAULT 850 // 85.0 摄氏度 #define TEMP_HYSTERESIS 20 // 2.0 摄氏度回差 short temp_set = TEMP_SET_DEFAULT; short temp_current; void Heater_Control(void) { temp_current = DS18B20_GetTemp(); if (!Liquid_Check()) { // 缺水,强制关闭加热 GPIO_ResetBits(GPIOA, GPIO_Pin_2); return; } if (temp_current < (temp_set - TEMP_HYSTERESIS)) { GPIO_SetBits(GPIOA, GPIO_Pin_2); // 开加热 } else if (temp_current > temp_set) { GPIO_ResetBits(GPIOA, GPIO_Pin_2); // 关加热 } }TEMP_HYSTERESIS是回差,设太小会导致继电器咔咔响,设太大温度波动明显。饮水机一般设 2 度回差比较合适。缺水检测必须放在加热控制之前,这是防干烧的关键,我见过因为没做缺水保护把加热管烧红的案例。
注意:继电器线圈要加续流二极管,否则断开瞬间的反向电动势可能击穿三极管或 MCU 引脚。
5. 避坑与排查:DS18B20 读 85 度、液位误判、继电器干扰
5.1 DS18B20 读数异常排查
现象:温度一直显示 85 度。原因:DS18B20 上电默认暂存器温度值是 85 度,如果转换未完成就读取,或者复位失败,就会读到这个默认值。解决:检查delay_ms(750)是否足够,用示波器看数据线是否有应答脉冲。如果复位函数返回 1,检查 4.7k 上拉电阻是否接好,接线是否过长。
现象:温度跳动超过 1 度。原因:电源纹波大或者数据线受干扰。解决:在 DS18B20 的 VDD 和 GND 之间并一个 0.1uF 电容,数据线尽量短,远离继电器和加热管。
5.2 液位检测误判
现象:明明有水却报缺水。原因:导电式探针表面氧化或水垢覆盖,导致电阻变大。解决:定期清洗探针,或者改用浮球开关。软件上可以加消抖,连续检测 3 次都是缺水才触发保护。
现象:水位波动时频繁切换状态。原因:水面晃动导致探针接触不稳定。解决:在软件里加 500ms 的延时确认,或者把探针位置放在水面以下 1cm 处。
5.3 继电器干扰导致 MCU 复位
现象:加热继电器吸合瞬间 MCU 重启。原因:继电器线圈反向电动势通过电源耦合到 MCU。解决:继电器线圈并续流二极管,MCU 电源加 100uF 电解电容和 0.1uF 陶瓷电容,继电器和 MCU 分开供电或者加磁珠隔离。
5.4 按键响应迟钝
现象:按温度加键没反应。原因:主循环里DS18B20_GetTemp延时 750ms,按键扫描被阻塞。解决:把温度转换放到定时器中断里,主循环只读结果,或者用状态机非阻塞方式实现。
5.5 显示乱码或花屏
现象:OLED 显示乱码。原因:I2C 时序不对或者初始化顺序错误。解决:检查 I2C 的 GPIO 模式是否配置为复用开漏,SCL 和 SDA 是否接反,初始化命令是否按手册顺序发送。
6. 进阶调试:用串口输出温度曲线与 PID 参数整定
6.1 串口打印温度数据
调试温度控制时,把温度值通过串口打印出来,用上位机画曲线,比盯着 OLED 看直观得多。配置 USART1 的 PA9 和 PA10,波特率 115200。
// 串口初始化与打印 void USART1_Init(u32 bound) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); 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); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); USART_InitStructure.USART_BaudRate = bound; 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); USART_Cmd(USART1, ENABLE); } void USART1_SendChar(char ch) { while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); USART_SendData(USART1, ch); } void USART1_SendString(char *str) { while (*str) { USART1_SendChar(*str++); } }在主循环里每 1 秒打印一次温度值,格式为T:235表示 23.5 度。用串口助手保存数据,导入 Excel 画曲线,能清楚看到超调量和稳定时间。
6.2 PID 参数整定与 PWM 加热
如果要做更精确的恒温,把继电器换成 MOS 管,用 TIM 输出 PWM 控制加热功率。PID 三个参数先设Kp=20, Ki=0.5, Kd=5,观察曲线。超调大就减小 Kp,震荡就增大 Kd,稳态误差大就增大 Ki。我一般先把 Ki 和 Kd 设 0,只调 Kp 到临界震荡,然后 Kp 减半,再加 Ki 消除静差,最后加 Kd 抑制超调。
| 参数 | 作用 | 调整方向 |
|---|---|---|
| Kp | 响应速度 | 越大越快,太大震荡 |
| Ki | 消除静差 | 越大静差越小,太大超调 |
| Kd | 抑制超调 | 越大阻尼越强,太大噪声敏感 |
调试时注意 PWM 频率选 1-5kHz,太低会有噪音,太高 MOS 管开关损耗大。加热管惯性大,PID 周期设 1-2 秒比较合适。
6.3 一个具体技巧:用定时器中断做非阻塞温度采集
主循环里delay_ms(750)会阻塞按键和显示,实际项目中我习惯用 TIM2 定时 1ms,在中断里做状态机:状态 0 发复位和转换命令,状态 1 计数到 750 后读温度,状态 2 更新显示和控制。这样主循环只处理按键和显示,响应流畅。这个改动花不了多少时间,但体验提升明显。
我在这个项目上最大的教训是:第一版没做缺水保护,测试时加热管干烧了半分钟,塑料支架都变形了。后来加了液位检测和软件双重保护,才敢长时间运行。做硬件控制,安全逻辑永远排在功能前面。希望帮到你。
本文还有配套的精品资源,点击获取