简介:本资源是一套基于STM32F103系列单片机开发的C语言水质监测系统完整工程,面向嵌入式初学者与课程设计、毕业设计及物联网实践项目开发者,解决水体PH值、TDS(总溶解固体)及温度三项核心参数的实时采集、本地显示与远程上传需求。压缩包含280个文件,总计11.12MB,其中C源文件(46个)与头文件(72个)构成主控逻辑,Keil工程配置文件(uvprojx、uvoptx)、编译中间文件(o、d、crf)及可执行镜像(axf、hex)齐全,配套PDF说明文档与调试配置(dbgconf)便于快速部署。已有2038人学习下载,提供从传感器驱动(ADC、I2C)、数据滤波处理、LCD本地显示到OneNet平台HTTP/MQTT协议对接的全链路实现,代码结构清晰、模块划分合理,适合作为嵌入式C语言综合实训范例深入理解硬件驱动、通信协议与低功耗传感应用。 做水质检测系统这个项目,算是我在嵌入式开发这条路上真正脱了一层皮又长了一层肉的经历。当时手里只有一块STM32F103C8T6核心板、几个传感器模块和一个模糊的需求,硬是靠着查手册、看源码、反复试错,把一套能实时显示、能标定、能报警的水质检测系统给磕了出来。今天不聊虚的,直接把整个项目的设计思路、硬件选型、C语言代码架构、标定过程和踩坑记录全部摊开来讲,给准备做单片机项目或者正在纠结怎么把传感器信号变成稳定数据的你一个完整参考。
先说结论:这套基于STM32的水质检测系统,核心是三条模拟量采集链路加一条数字温度链路,通过ADC多通道扫描加DMA搬运,配合滑动均值滤波和温度补偿算法,最终在OLED上实时刷新TDS(溶解性总固体)、pH值和温度三个指标,同时支持按键设置阈值,超限就触发蜂鸣器报警。整个工程用标准库开发,代码分层清晰,移植性很强,后续想加蓝牙、Wi-Fi模块往云端传数据,也就是多写几个外设驱动的事。
1. 项目背景与整体设计思路
1.1 为什么选STM32做水质检测
水质检测听起来像是一门化学课或者环境工程课的课题,但落到电子设计上,本质就是一个多通道数据采集系统。你需要同时读取好几个传感器的电压信号,做信号调理,然后通过算法把这些电压换算成人能看懂的水质参数。这种活儿,单片机天然比PC更适合,因为传感器接口、ADC采样、实时控制、低功耗这些需求,单片机的片内外设刚好全都能覆盖。
在单片机选型上,我最后敲定STM32F103C8T6,理由很实在:第一,性能足够。72MHz主频,12位ADC,三个ADC外设,可以同时采集多路信号,处理几个传感器的数据绰绰有余。第二,生态成熟。标准外设库的资料满天飞,寄存器手册、参考代码、论坛讨论,遇到问题基本都能搜到解决方案,对新手极其友好。第三,成本低。核心板几十块钱,坏了不心疼,适合反复折腾。
相比用51单片机,STM32的优势在于ADC精度和采样效率。51单片机虽然也能做AD转换,但通常是8位精度,而且需要外扩ADC芯片,电路复杂度和代码复杂度都会上升。STM32内置的12位ADC虽然不能和专用ADC芯片比,但对于TDS、pH这类输出缓慢变化的直流信号来说,精度完全够用。
1.2 系统架构和硬件组成
整套系统的信号链路是这样的:各类水质传感器将水质参数转换为电信号,经过信号调理电路处理后送入STM32内置ADC转换,主控MCU对多通道数据进行采集、滤波、补偿和换算,最终将结果显示在OLED屏幕上,并根据设置的阈值控制报警模块。
硬件清单如下:
- 主控:STM32F103C8T6核心板,板载8MHz晶振,通过PLL倍频到72MHz
- TDS传感器:探针式TDS模块,输出模拟电压,带温度补偿输入接口
- pH传感器:pH电极探头配信号变送板,输出模拟电压
- 温度传感器:DS18B20单总线数字传感器,用于温度补偿
- 显示:0.96寸I2C接口OLED显示屏(SSD1306驱动)
- 交互:三个独立按键(菜单切换、数值加减、确认)
- 报警:有源蜂鸣器
- 电源:5V USB供电,板载AMS1117稳压到3.3V
这套系统中,TDS传感器和pH传感器输出的都是模拟电压信号,所以必须占用ADC通道。温度用DS18B20不走ADC,是用GPIO模拟单总线时序读取数字值。在实际接线中,我特别注重TDS传感器和pH传感器的信号线走线,尽量避免布在电机、继电器等大功率器件附近,因为这类器件启停瞬间会产生很强的电磁干扰,模拟信号线如果离得太近,采集到的电压信号会夹杂大量毛刺,后期软件滤波难度倍增。
1.3 开发环境搭建
开发环境用Keil MDK5,搭配ST-Link V2下载器。工程基于STM32标准外设库V3.5,代码分层为驱动层和应用层。我建议新人在建工程时不要把文件堆在一起,而是按功能模块划分文件夹,比如:
- BSP目录:存放LCD、DS18B20、ADC等板级外设驱动
- APP目录:存放主流程逻辑、菜单逻辑、数据处理逻辑
- Core目录:存放启动文件、系统时钟配置等
共享一份工程模板的价值在于,你做不同项目时可以直接复用大部分驱动代码,只需要改应用层逻辑就行。这个工程我后来整整用了快两年,每次做新东西都从这套骨架开始改,省了大量时间。
2. 硬件选型与电路设计要点
2.1 TDS传感器:电导率测量原理与选型
TDS(Total Dissolved Solids,溶解性总固体)传感器测量的是水中溶解性固体的总含量,单位是ppm(mg/L)。它的工作原理并不复杂:水中的离子浓度越高,导电性越强,通过测量水的电导率,再乘以一个系数就能换算出TDS值。
市面上常见的TDS模块有两种输出方式:一种是直接输出模拟电压,模块内部已经做了信号调理;另一种是厂商自带算法,数字接口输出。我选的是模拟电压输出的模块,因为它给开发者最大的算法定制空间,你可以自己写滤波、自己写温度补偿,而不是被动接受模块内置的处理逻辑。模块输出范围大致是0到2.3V,对应0到1000ppm,负载电阻和探头常数都已经在模块上校准过,直接接ADC就行。
这里要特别说一个很多人忽略的坑:TDS电极极化。测量电导率时,如果电极两端一直加直流电压,溶液中的离子会在电极表面富集,形成极化层,导致测量值随时间漂移。解决办法是让测量工作在交流激励下进行。大部分成品TDS模块内部已经加了交流激励源,所以不用太担心。但如果你自己用裸电极搭电路,一定要设计一个方波发生器去驱动探头,不能直接加直流电平。
2.2 pH传感器:高阻抗信号处理
pH传感器和TDS传感器完全是两种风格。pH电极是一个高阻抗器件,内阻可达几十兆欧甚至上百兆欧,所以它的信号调理电路必须要有高输入阻抗的运算放大器做缓冲,否则信号会被ADC的采样阻抗直接吃掉,测出来的电压完全不准。
我用的pH模块已经在板子上集成了一级高阻抗缓冲放大,输出阻抗很低,可以直接接ADC。但pH模块的精度受温度影响很大,因为能斯特方程里的斜率项和温度有关。所以我要同时采集DS18B20的温度数据,然后在软件里做温度补偿。
选型方面,普通实验室用的pH电极分玻璃电极和复合电极。玻璃电极稳定性高,但响应慢,且不能用在含氟化物的水样里。复合电极把测量电极和参比电极做在一起,使用方便,适合自动化设备。我这个项目用的是通用复合电极,成本低,能满足教学演示和普通水质评估的需求。
2.3 电源与信号调理电路
整机用5V供电,STM32核心板板载3.3V稳压给MCU和传感器模块用。这里有个非常重要的细节:TDS模块和pH模块的参考地必须和MCU共地,否则信号电平没法统一。有些模块有独立的电源引脚,如果供电电压不一致,还要考虑电平匹配问题。我测试时发现,如果TDS模块用5V供电,而MCU的ADC参考电压是3.3V,模块输出0到2.3V虽然不会超过3.3V,但如果模块工作在5V下,输出满量程可能接近4V,此时直接接3.3V的ADC引脚就有烧毁风险,必须用电阻分压或运放电平转换。保险起见,我给所有传感器模块统一用3.3V供电,虽然部分模块手册标注5V供电,但实际上3.3V也能正常工作,只是满量程输出略有降低,通过软件标定就能消除。
ADC的参考电压AVDD我认为也是值得重视的环节。STM32F103的ADC参考电压接VREF引脚,核心板上一般已经接到3.3V。如果追求更高精度,可以给VREF单独接一路基准电压源,比如REF3030,精度能稳定到0.2%以内。不过对于TDS这种本身就有10%误差的传感器,外部参考源带来的提升有限,我没上,直接用板载3.3V。
3. 软件架构与C语言实现核心
3.1 代码分层:模板工程搭建思维
这个项目的代码文件划分,核心的思想就是让每一层只干一件事,绝不跨界。硬件相关的寄存器操作全部封装在BSP层,数据滤波和参数换算是应用层,主循环只调度逻辑任务。
工程结构: - Core/ - main.c 主函数,任务调度 - stm32f10x_it.c 中断服务函数 - BSP/ - bsp_adc.c ADC初始化、DMA采集 - bsp_ds18b20.c DS18B20驱动 - bsp_oled.c OLED驱动 - bsp_key.c 按键驱动 - bsp_beep.c 蜂鸣器驱动 - APP/ - app_task.c 应用逻辑主循环 - app_filter.c 滤波算法(滑动均值) - app_calc.c 物理量换算(TDS/pH补偿计算) - app_menu.c 菜单界面逻辑这样的结构,后期想换个显示屏、换个传感器,只需要修改对应的BSP文件,应用层代码完全不用动。我在做一个项目的时候,可以从之前调好的工程里直接拷贝BSP层代码,整体迁移成本极低。
3.2 ADC多通道采样与DMA搬运
ADC部分是本项目的核心之一。TDS和pH两个通道要同时采集,我用ADC1的通道0和通道1,配置为扫描模式加多通道采样,采样结果通过DMA自动搬运到内存数组里。DMA的意义在于,ADC转换完成后数据不需要CPU干预就能存到指定的变量里,CPU可以腾出手去刷新OLED、处理按键逻辑。
// ADC初始化配置 void BSP_ADC_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; // 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // PA0(TDS) PA1(pH) 配置为模拟输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); // ADC1 配置 ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = ENABLE; // 扫描模式 ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 连续转换 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 2; // 2个通道 ADC_Init(ADC1, &ADC_InitStructure); // 通道转换顺序和采样时间 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); // DMA1 通道1 配置 DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_buf; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = 2; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel1, &DMA_InitStructure); ADC_DMACmd(ADC1, ENABLE); DMA_Cmd(DMA1_Channel1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); }采样时间选55.5个周期,是折算之后单个通道大约20kHz的有效采样率,对水质传感器这种低频信号来说绰绰有余,但又能有效抑制高频干扰。采集到的数据放在全局数组adc_buf里,主循环直接读这个数组就行了。
3.3 滑动均值滤波:拒绝毛刺的实用方案
实际测量中,ADC采到的原始值跳动非常大,TDS输出的模拟电压在探头附近有气泡或者水流动不稳定的情况下,跳变幅度能到几十毫伏,换算成TDS就是几十甚至上百ppm的波动。面对这种问题,我选用的方案是滑动均值滤波。
我在三个硬性条件之间做了折中:一是要能有效平滑短期波动,二是要保留一定的响应速度,三是代码要简单,不能占用太多内存和CPU。滑动均值滤波的窗口取20次采样值,每来一个新数据就丢弃最老的一个数据,然后计算平均值,实质是一个低通滤波器。
#define FILTER_N 20 static uint16_t filter_buf[FILTER_N]; static uint8_t filter_index = 0; static uint8_t filter_count = 0; uint32_t SlidingMeanFilter(uint16_t new_value) { uint32_t sum = 0; uint8_t i = 0; filter_buf[filter_index] = new_value; filter_index = (filter_index + 1) % FILTER_N; if (filter_count < FILTER_N) filter_count++; for (i = 0; i < filter_count; i++) sum += filter_buf[i]; return sum / filter_count; }这段代码有好几个细节值得琢磨。filter_index用模运算做环形缓冲,不用移动数据,时间复杂度固定是O(N),主循环里调用它不会产生不确定的延迟。filter_count的存在是为了解决开机初期的“空窗期”问题,如果只积累了5个数据,就直接除以5而不是除以20,否则平均值会被前几个零拉低,必须等20个数据填满才进入稳定状态。
工程里我也遇到过一个比较棘手的情况:偶然性的尖峰脉冲干扰,比如电机启动瞬间的浪涌耦合到传感器线路上,滑动均值虽然能减轻影响,但一个足够大的尖峰依然能在窗口内残留一段时间。为了处理这类情况,我在滑动均值之前加了一道中值滤波,每次连续采样5次,取中间值作为一次有效输入。两个滤波算法串联之后,曲线平滑度非常理想,实测TDS读数稳定后波动量在±5ppm以内。
3.4 物理量换算与温度补偿标定计算
ADC采集出来的是数字量,要把它变成真实的物理量,必须走完从数字到电压、从电压到电导率、从电导率到TDS这样一条换算链。这一部分是项目的灵魂,也是很多人容易出错的地方。
ADC数字量转电压的公式非常简单:
float voltage = (float)adc_value * 3.3f / 4096.0f;12位ADC的分辨率是4096级,参考电压3.3V,所以每个LSB对应的电压约为0.806mV。
TDS传感器模块的输出电压和电导率存在线性关系,但比例系数受探头常数和模块增益的影响。我不能直接拿厂商给的默认系数就用,因为不同的水样、不同的探头安装位置,实际测量值都会偏离。正确做法是配置标准溶液进行单点标定。
标定过程是这样的:准备一杯TDS已知的标准液(比如500ppm),把探头放进去稳定2分钟,读取模块输出电压,然后反推系数:
// 电压 -> TDS (ppm) tds_value = voltage * tds_coefficient;其中tds_coefficient初始给一个经验值1500,然后通过标准液校准调整。更准确的做法是两点标定,用0ppm(纯水)和500ppm标准液分别测输出电压,线性拟合出斜率和截距,但实际工程中,由于pH传感器和TDS传感器的电压都在零点附近,截距项影响不大,单点标定足够用。
温度补偿是另一个重头戏。溶液的导电率和温度强相关,温度升高1℃,电导率大约增加2%左右。如果不在软件里做补偿,夏天和冬天测同一杯水,TDS读数能差出10%以上。标准做法是把测量值折算到25℃基准下的等效值:
// 温度补偿到25℃ float temp_factor = 1.0f + 0.02f * (current_temp - 25.0f); tds_compensated = tds_raw / temp_factor;pH传感器的温度补偿逻辑稍有不同。pH电极的输出电压随温度变化满足能斯特方程,每pH单位在25℃下对应约59.16mV,但实际斜率会随温度漂移,所以要对斜率项做修正:
// pH = 7 + (7.0 - voltage_mid) / (slope * temp_correction) float slope_25c = 59.16f; // mV/pH float temp_correction = (273.15f + current_temp) / (273.15f + 25.0f); float slope = slope_25c * temp_correction; ph_value = 7.0f + (voltage_mid - voltage) / slope;这里voltage_mid是pH值为7.0时电极在中性缓冲液中的输出电压,一般在标定时测量得到。pH电极的输出电压和pH成线性反比关系,pH越高,输出电压越低。
3.5 按键菜单与显示逻辑
人机交互方面,我用OLED显示主界面和菜单界面,三个按键分别承担“菜单/确认”“加”“减”的功能。菜单逻辑的本质是一个状态机,状态切换的驱动源是按键事件。
typedef enum { DISPLAY_MAIN, // 主界面 MENU_TDS_THRESHOLD, // TDS阈值设置 MENU_PH_THRESHOLD // pH阈值设置 } MENU_STATE; MENU_STATE current_menu_state = DISPLAY_MAIN;主循环里轮询按键,一旦检测到短按KEY1就切换状态。在阈值设置状态下,KEY2和KEY3调整阈值,调整结果实时写回全局变量,并且存入内部Flash,掉电不丢失。用户不想每次开机重新设置阈值。
OLED用的是SSD1306驱动芯片,I2C接口。驱动函数的实现并不复杂,核心是把显存数组通过I2C写入SSD1306的GRAM。注意SSD1306内部有一块128x64的显存,每个像素点对应一个bit,点亮就写1。
我用的OLED一屏能显示4行16列标准ASCII字符,所以在主界面同时显示TDS、pH、温度三个参数绰绰有余,每行一个参数,用sprintf格式化后再显示。
3.6 蜂鸣器报警逻辑
报警逻辑很简单:当TDS值超过阈值,或者pH值超出设定范围时,GPIO输出高电平驱动有源蜂鸣器发声。但真正让我反复调整的是报警的“迟滞”问题。如果阈值设为500ppm,实测值恰好是499ppm,那么蜂鸣器会处于一种临界状态:数据轻微跳动就会导致报警状态反复翻转,不仅烦人,还容易损坏蜂鸣器。
解决方法是加入迟滞比较,实测值要超过设定的上限加上迟滞带才触发报警,低于上限减去迟滞带才解除报警。迟滞带取阈值的5%,既能抑制抖动又不至于让报警变得迟钝。这是工业控制里比较常用的一种思路。
4. 实操过程与核心环节实现
4.1 从零开始搭建工程的过程记录
我这里记录一下从拿到核心板到跑通显示,大约需要经过哪些步骤。新手可能觉得这部分太基础,但它决定了后续所有功能调试的底子,所以我建议按顺序完整操作一遍。
第一步,安装Keil MDK5,并且安装好STM32F1系列的器件支持包。第二步,新建工程,选择芯片型号STM32F103C8T6。第三步,把标准外设库的源码文件复制到工程目录下,并且把全局宏STM32F10X_MD、USE_STDPERIPH_DRIVER加上。第四步,配置时钟。我这里直接用库函数SystemInit,它在启动文件里会被自动调用,把时钟从8MHz外部晶振倍频到72MHz。第五步,写一个简单的GPIO翻转程序,下载到板子里,用LED验证最小系统正常。
这五步走完,才算具备了点灯之外所有功能开发的基础。我不建议直接跳到最后一步写完整程序,因为如果程序跑不起来,你很难判断是硬件问题、驱动问题还是逻辑问题。从最小的功能模块一个一个迭代,每个模块都是可测的,问题定位速度会快很多。
4.2 传感器标定的完整步骤
传感器标定是这个项目里最耗时也最关键的一环,我在这上面踩过的坑比写代码踩过的还多。以TDS传感器为例,整个标定过程是这样的:
准备标准液。这一步很多人图省事直接拿自来水标,但自来水的TDS值本身就不稳定,标出来的系数没有参考价值。我买了TDS标准缓冲液粉剂,按说明配成500ppm的溶液。配制时要注意,粉剂要用天平称量准确,水尽量用去离子水或蒸馏水,不能用自来水,不然配出来的浓度根本不对。
将探头放入标准液中,深度至少要没过探头的两个电极,等待2分钟让读数稳定。这段时间如果发现读数有缓慢漂移,多半是电极表面有油污或气泡。处理方法是先用去离子水冲洗探头,再轻轻甩干,不可用纸巾擦,因为有可能会损伤电极表面镀层。
记录模块输出电压,同时用DS18B20测标准液温度。根据温度补偿公式计算系数,写入程序中的tds_coefficient变量。这样完成之后,再测量另一杯未知水样,和商用TDS笔做对比,误差在5%以内就说明标定成功。如果偏差很大,优先检查标准液温度补偿是否做对,以及探头是否完全浸没。
pH传感器的标定比TDS复杂,常规做法是两点标定。先测pH6.86缓冲液,记录电压;再测pH4.01缓冲液,记录电压。两个点的电压差除以pH差,得到实际斜率。如果实际斜率明显偏离理论值59.16mV/pH,说明电极老化或缓冲液失效。标定结束后把斜率和零点电压保存在程序里,这就是整个项目里最重要的两个校正参数。
4.3 实测数据记录与误差分析
在25℃室温环境下,我用这套系统对几组水样做了实测,误差基本在预期范围内。这里放其中一组数据:
| 水样 | 商用TDS笔读数(ppm) | 本系统读数(ppm) | 误差 |
|---|---|---|---|
| 纯净水 | 4 | 5 | +1 |
| 自来水 | 205 | 211 | +2.9% |
| 标准液 | 500 | 498 | -0.4% |
| 盐水 | 963 | 940 | -2.4% |
纯净水的绝对误差虽然只有1ppm,但相对误差高达25%,这是因为在极低浓度下,探头本身的残余电导率变成了主要影响因素。实际使用中,如果测的是纯净水的TDS,读数为个位数到十几ppm,这个精度其实是够用的,但在报告里应该注明低浓度段的相对误差会明显增大。
pH实测方面,我用标准缓冲液6.86和9.18分别验证,误差都在0.1pH以内。pH传感器的漂移问题在开机前15分钟特别明显,电极电位的建立过程比较漫长。所以我的使用习惯是系统上电后先等5到10分钟,等传感器读数稳定了再记录,对于需要精确测量的场景,这个预热流程必须遵守。
4.4 常见问题与排查技巧实录
ADC采样值一直满量程或一直为零。先量传感器模块输出电压,用手摸探头,看电压有没有变化。如果模块输出正常,问题在STM32的ADC配置上,重点检查ADC通道号是否对应、DMA搬运地址是否正确。如果模块输出异常,优先检查供电和共地。
TDS读数漂移严重,一直在几百ppm范围内大幅跳动。可能原因有三个:探头没完全浸入液面、探头顶部接线处进水短路、水样里有气泡。先用肉眼观察排除可见问题,再用排除法把探头从液体中取出测空气值,如果空气值稳定,说明探头本身没问题,问题在水样或者屏蔽干扰。
OLED显示乱码或白屏。先检查接线顺序,很多OLED模块的I2C引脚标的是SCL、SDA,但有的板子会加电平转换电路,导致引脚定义和丝印不一致。其次检查I2C地址,SSD1306有0x3C和0x3D两种地址,取决于模块的地址选择电阻。最后检查时序,把I2C速率从400K降到100K试试,有些质量一般的杜邦线在400K下波形畸变严重。
DS18B20读不到温度,返回85或者-55度。85度是DS18B20上电复位时的寄存器默认值,说明MCU和DS18B20之间通信没成功。优先检查上拉电阻,DS18B20的数据线必须有4.7K欧上拉到VCC,很多新手直接拉一根杜邦线接MCU,没有上拉电阻,通信时好时坏。再检查时序,单总线的时序非常苛刻,拉低时间、释放时间差几十微秒就会失败,最好用逻辑分析仪对齐时序波形。
4.5 关键代码逻辑解析
主函数的整体调度逻辑,是每100ms执行一次数据采集和界面刷新,每500ms执行一次按键扫描和报警判断。这个时间分配是经过测试的,OLED刷新太快会闪烁,太慢又显得卡顿。数据采集放最前面,因为滤波算法需要样本连续,中间被其他任务打断也没关系,只要每个周期都有新数据进来就行。
int main(void) { SystemInit(); BSP_ADC_Init(); BSP_DS18B20_Init(); BSP_OLED_Init(); BSP_Key_Init(); BSP_Beep_Init(); while (1) { // 1. 采集原始值 adc_tds_raw = adc_buf[0]; adc_ph_raw = adc_buf[1]; temperature = DS18B20_GetTemp(); // 2. 滤波 tds_filtered = SlidingMeanFilter(adc_tds_raw); // 3. 物理量换算 tds_value = TDS_Calc(tds_filtered, temperature); ph_value = PH_Calc(adc_ph_raw, temperature); // 4. 界面刷新 APP_Display(tds_value, ph_value, temperature); // 5. 按键与报警 APP_KeyScan(); APP_AlarmCheck(tds_value, ph_value); // 调度延时 delay_ms(100); } }这段代码的时间分配策略是固定节拍扫一遍所有任务,这是一种最原始的操作系统节拍思想。如果你的项目要扩展更多功能,比如传感器的数量、通信协议栈、复杂按键逻辑,这种裸机轮询方式会越来越吃力,到时候就需要考虑用定时器做任务调度,或者直接上RTOS。
5. 实际使用场景与扩展方向
5.1 系统适合的应用场景
这套水质检测系统做成之后,我主要用在三个场景。第一,家用饮水安全评估,定期测一下自来水、净水器出水、桶装水的TDS和pH值,形成一个简单的趋势记录。第二,课堂教学演示,把单片机项目和数据采集结合起来,学生能看到传感器信号从模拟到数字到用户界面的完整链路,比只讲书本理论直观得多。第三,室内鱼缸水质监控,鱼缸里水的pH值非常关键,但变化是缓慢的,人不可能24小时盯着,有了自动报警功能,水质的突然恶化就能及早发现。
如果需要做成一个产品级的设备,还应该考虑防水外壳、电极的定期维护提醒、数据存储能力。这个系统目前只能算是一个功能原型,从原型到产品之间还隔着可靠性验证、长时间稳定性测试、外观散热设计这些坎。但作为毕业设计、电子设计竞赛或者DIY项目,它已经完全够用了。
5.2 后续功能扩展
我要说,这个项目做完之后,其实只发挥了STM32的很小一部分能力。后续扩展可以往三个方向走:
一是加数据通信模块。比如把ESP8266接在STM32的串口2上,编一个简单协议把采集到的水质数据上传到云平台,手机上随时查看。实现难度不高,但价值感提升非常明显,真正实现了远程监控。
二是加存储功能。在TDS和pH传感器上各接一个模拟开关,外加一个24C02的I2C EEPROM,就可以定时把数据存下来。或者更高端一点的方案,用SPI接口的SD卡模块,把带时间戳的历史数据存成CSV文件,导出来用Excel分析趋势,效果非常好。
三是加自动给水控制。比如鱼缸场景,pH值低于阈值就控制水泵加入缓冲液,TDS过高就自动换水。这时候需要增加一个继电器模块,通过GPIO控制外部设备,同时要对加液泵的启停做防抖处理,避免频繁启停造成设备损坏。
5.3 我对这套系统的几点体会
写了这么多,最后分享一点个人的实际体会。在开发过程中,最容易出问题的部分不是算法,也不是代码语法,而是传感器输出的噪声和波动。软件滤波看似简单,但窗口长度的选择直接影响系统的响应速度和稳定性,我实际调的时候,每次改动都要在真实水样场景下跑够半小时才能确认效果,这是代码质量之外最难量化的一步。
还有一个让我印象深刻的问题是,硬件连接松动导致ADC读数间歇性跳变。一开始我以为是算法问题,花了很多时间反复调滤波参数,最后发现是杜邦线接触不良。做这种模拟信号采集项目,建议所有传感器线都用焊接方式固定,杜邦线只适合做原型验证,不适合长时间运行。如果实在要用杜邦线,也要用质量好的,并且在测试前把所有接口插紧一遍。
项目里最值得回味的不是最后测出的漂亮数据,而是中间不断犯错、定位、修正的过程。这种从混乱中逐渐建立起确定性的体验,恐怕才是做嵌入式项目最大的收获。如果你正在做类似的水质检测系统,或者准备用STM32做传感器采集项目,上面这些经验应该能帮你少走几周弯路。
本文还有配套的精品资源,点击获取