
简介本资源是一套完整的基于STM32F103的水质监测系统课程设计实现方案面向电子信息、自动化及物联网方向的本科生与实践开发者解决多参数水质实时检测与可视化反馈的教学与实训需求。系统集成油度、pH、TDS三类传感器信号采集通过HAL库驱动ADC、UART、TIM等外设实现在LCD1602上的动态显示及超限声光报警覆盖嵌入式软硬件协同开发全流程。压缩包含172个文件933KB以49个.h头文件和22个.c源码为主涵盖HAL底层驱动如stm32f1xx_hal_adc.c、stm32f1xx_hal_uart.c、工程配置.ioc、.uvprojx、编译输出.hex、.axf、.map及Proteus仿真文件.pdsbak结构完整、模块清晰便于理解传感器接口逻辑与系统调度机制。已有9360人学习下载提供可直接烧录运行的固件、可仿真验证的电路模型及规范化的Keil5工程框架显著降低课程设计调试门槛。1. 项目缘起为什么我们需要一个自研的水质监测系统几年前我在参与一个社区鱼塘的维护项目时遇到了一个挺头疼的问题。我们想实时了解鱼塘的水质状况比如酸碱度PH值、浑浊度TDS和是否有油污但市面上现成的监测设备要么价格昂贵要么功能单一要么数据传输不灵活很难满足我们低成本、多参数、可定制的需求。当时我就想能不能自己动手用最熟悉的STM32单片机为核心搭建一个灵活、可靠且成本可控的水质监测系统这个想法最终落地成了今天要分享的这个项目一个集成了水质浊度、PH值、TDS总溶解固体检测功能的STM32系统并且全程在Proteus中进行仿真验证在Keil5里完成代码编写与调试。选择STM32是因为它的性能足以处理多路传感器数据外设丰富ADC、UART、定时器等生态成熟资料遍地都是。而Proteus仿真则是在硬件打板前验证电路设计和程序逻辑的绝佳沙盘能提前发现很多低级错误省下不少时间和物料成本。Keil5作为经典的ARM开发环境其调试功能对于STM32项目来说依然是高效可靠的选择。这个系统非常适合电子爱好者、物联网初学者、相关专业的学生或者像我一样有具体应用场景需要DIY的朋友。它不仅仅是一套代码和电路图更是一个完整的、从传感器原理到数据采集、处理、显示的实战案例。通过它你能深入理解模拟信号采集、传感器校准、嵌入式系统设计以及仿真验证的全流程。接下来我就把这个项目的核心设计思路、关键实现细节以及我在仿真和调试中踩过的那些“坑”毫无保留地分享出来。2. 系统核心架构与传感器选型解析一个水质监测系统核心任务就是“感知”和“处理”。我们的架构围绕STM32F103C8T6这颗经典的“蓝桥杯”芯片展开它价格亲民资源足够。系统架构可以清晰地分为三层感知层、控制层和交互层。感知层负责采集水质的三项关键指标这里的选择直接决定了系统的精度和成本。浊度传感器我选用的是基于光学散射原理的浊度传感器模块。它内部有一个红外LED和一个光电接收管。当光线穿过水样时悬浮颗粒会使光线发生散射接收管检测到的光强会发生变化从而输出一个模拟电压信号。水质越浑浊输出电压通常越高具体需看模块输出特性。这个模拟信号将接入STM32的ADC模数转换器引脚。PH传感器PH测量采用玻璃电极法我直接选用了市面上常见的PH计模拟传感器模块。它的核心是一个PH复合电极其产生的电势差与溶液的PH值成线性关系能斯特方程。模块内部通常带有信号调理电路将微弱的电极信号放大并转换成0-3.3V或0-5V的模拟电压输出同样送给STM32的ADC。这里的关键在于两点校准需要用标准缓冲液如PH4.01和PH6.86/9.18来标定以得到准确的斜率灵敏度和截距零点偏移。TDS传感器TDS测量基于电导率。我选用的是带温度补偿的TDS传感器模块。它通过两个电极测量水的电导率再根据经验公式折算成TDS值单位ppm。模块输出同样是模拟电压信号。需要注意的是水的电导率受温度影响很大所以温度补偿是保证TDS测量准确度的必要环节。好的模块会集成温度传感器如DS18B20或热敏电阻并自动补偿。控制层的核心就是STM32F103C8T6。它的任务很明确多通道ADC采样同时或分时采集三个传感器送来的模拟电压。我使用了STM32的ADC1并配置为扫描模式顺序对三个通道进行采样。为了提高效率并减少CPU干预我启用了DMA直接存储器访问将ADC转换结果直接搬运到内存数组中这是处理多路慢变模拟信号的常规高效做法。数据处理与校准将ADC读取到的原始数字量通过各自的校准公式线性或非线性转换为具有物理意义的数值浊度NTU、PH值、TDS ppm。这部分算法在代码中实现。通信与显示处理后的数据可以通过串口UART发送到上位机如电脑串口助手进行记录和分析。同时也可以驱动一个OLED显示屏I2C或SPI接口进行本地实时显示方便现场查看。交互层在本次仿真中主要通过Proteus里的虚拟终端Virtual Terminal模拟串口输出以及虚拟仪器如电压表、示波器来观察信号。在实物中则是真实的OLED屏和串口通信。这个架构的优势在于模块化清晰每一层都可以独立调整或升级。例如你可以轻易地将TDS传感器换成溶解氧传感器只需修改对应的ADC通道和处理算法即可。3. 开发环境搭建Keil5与Proteus的协同作战工欲善其事必先利其器。这个项目涉及嵌入式软件和硬件仿真所以Keil5 MDK和Proteus Professional两个工具缺一不可。它们的安装和基础配置网上教程很多我不再赘述这里重点讲几个让它们“协同作战”的关键配置和容易踩坑的地方。3.1 Keil5工程创建与芯片支持包安装首先在Keil5中新建一个基于STM32F103C8的工程。这里第一个坑就是芯片支持包Device Family Pack。如果你第一次使用某款STM32芯片Keil5可能会提示找不到设备。你需要通过Pack Installer工具栏图标像一个小盒子在线下载或离线安装对应的DFP包。对于F103系列通常需要安装Keil::STM32F1xx_DFP。安装后在新建工程选择设备时才能看到STM32F103C8。工程创建好后需要配置几个关键点目标选项Options for TargetTarget标签页确认晶振频率我用的仿真和常见开发板是8MHz但代码中会通过系统初始化配置到72MHz主频。Output标签页务必勾选Create HEX File。这个HEX文件是最终要加载到Proteus中STM32模型里的可执行文件。Debug标签页如果你有实物调试器如ST-Link可以在这里配置。但纯仿真时这部分不是必须的。C/C标签页在Define里添加预处理宏比如STM32F10X_MD对于中容量产品USE_STDPERIPH_DRIVER如果你使用标准外设库。在Include Paths里添加所有你用到的库文件头文件路径如标准外设库的inc目录。3.2 Proteus电路设计与STM32模型配置在Proteus中绘制原理图。核心元件包括STM32F103C6或STM32F103C8Proteus元件库中可能只有C6其引脚与C8兼容仿真足够。模拟信号源用于模拟三个传感器的输出。可以使用DC VOLTMETER直流电压表连接的可调电位器POT-HG来手动调节电压模拟传感器变化。将电位器的滑动端连接到STM32的ADC输入引脚如PA0, PA1, PA2。虚拟终端VIRTUAL TERMINAL连接到STM32的串口发送引脚如PA9/USART1_TX用于接收STM32打印的数据。需要设置正确的波特率如9600。电源和地给STM32和电位器提供VCC3.3V和GND。最关键的一步双击Proteus中的STM32元件打开属性对话框。在Program File一栏点击文件夹图标选择你在Keil5中编译生成的.hex文件。在Clock Frequency一栏输入你的系统主频比如72MHz。这里必须和你在Keil5代码中配置的系统时钟一致否则串口波特率等所有与时序相关的功能都会出错。Crystal Frequency通常填8MHz外部高速晶振值。3.3 联调技巧如何高效排查问题当你在Keil5中修改代码并重新编译生成新的HEX文件后Proteus并不会自动加载。你需要手动暂停Proteus仿真再次打开STM32属性重新选择一次HEX文件或者直接点击OK如果路径没变然后重新开始仿真。这是一个容易忘记的步骤。在联调时要善用两个工具的自带功能Keil5的软件仿真即使没有Proteus你也可以在Keil5的调试模式下使用Logic Analyzer逻辑分析仪查看GPIO引脚波形使用Serial Window串口窗口查看串口输出。这可以帮助你初步验证代码逻辑特别是定时器、PWM、串口发送等功能的时序是否正确。Proteus的虚拟仪器除了虚拟终端还可以放置OSCILLOSCOPE示波器来观察ADC引脚上的实际电压波形或者观察串口TX引脚上的数字波形看其波特率是否正常。我个人的习惯是先在Keil5中把核心的数据采集、转换算法和串口打印功能调通用软件仿真验证逻辑。然后再放到Proteus中结合模拟电压输入验证整个信号链是否工作正常。这种分步验证的方法能极大提高效率。4. STM32固件开发多通道ADC与数据处理的实战代码硬件框架搭好了环境也配置妥当了接下来就是最核心的“大脑”部分——STM32的固件程序。我将以标准外设库Standard Peripheral Library为例讲解关键模块的配置和代码逻辑。使用HAL库的思路也类似。4.1 ADC多通道扫描与DMA配置我们的目标是让ADC自动、循环地采样三个传感器通道并且不占用CPU时间。这就需要用到ADC的扫描模式Scan mode和连续转换模式Continuous conversion并搭配DMA。// 以ADC1, 通道0,1,2 (PA0, PA1, PA2) 为例 void ADC1_DMA_Init(void) { ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; // 1. 开启时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 2. 配置ADC引脚为模拟输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; // 模拟输入 GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 配置DMA DMA_DeInit(DMA1_Channel1); // ADC1使用DMA1通道1 DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)(ADC1-DR); // 外设地址ADC数据寄存器 DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)ADC_ConvertedValue; // 内存地址自定义数组 DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; // 方向从外设到内存 DMA_InitStructure.DMA_BufferSize 3; // 缓冲区大小3个通道 DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; // 外设地址不递增 DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; // 内存地址递增 DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; // 外设数据宽度半字(16位) 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); DMA_Cmd(DMA1_Channel1, ENABLE); // 4. 配置ADC 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 3; // 规则通道序列长度为3 ADC_Init(ADC1, ADC_InitStructure); // 5. 配置ADC规则通道的转换顺序和采样时间 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); // PA0, 第1个转换 ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); // PA1, 第2个转换 ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5); // PA2, 第3个转换 // 6. 使能ADC的DMA请求 ADC_DMACmd(ADC1, ENABLE); // 7. 使能ADC ADC_Cmd(ADC1, ENABLE); // 8. 复位校准然后执行校准 ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); // 9. 软件触发开始转换 ADC_SoftwareStartConvCmd(ADC1, ENABLE); }这段代码初始化后ADC就会在DMA的控制下永不停歇地按照通道0-1-2的顺序进行转换并将结果依次存放到ADC_ConvertedValue[0],[1],[2]这个数组中。主循环中无需再调用ADC转换函数直接读取这个数组即可获得最新的采样值。4.2 传感器数据转换与校准算法从ADC读到的是0-409512位分辨率的数字量需要转换成实际的物理量。这里以PH传感器为例展示两点校准法的代码实现。假设我们用PH4.01和PH9.18的标准缓冲液进行校准。将传感器分别插入两种缓冲液待读数稳定后记录对应的ADC值adc_ph4和adc_ph9。根据两点确定一条直线的公式PH k * adc_raw b斜率k (9.18 - 4.01) / (adc_ph9 - adc_ph4)截距b 4.01 - k * adc_ph4// 定义校准系数这些值需要实际校准后填入 float ph_slope_k 0.0; float ph_offset_b 0.0; // 校准函数在实际项目中可能需要通过按键或串口命令触发并保存系数到EEPROM void PH_Sensor_Calibrate(float ph_low, uint16_t adc_low, float ph_high, uint16_t adc_high) { ph_slope_k (ph_high - ph_low) / ((float)adc_high - (float)adc_low); ph_offset_b ph_low - ph_slope_k * (float)adc_low; // 这里可以添加将k和b保存到非易失存储器的代码 } // 将ADC原始值转换为PH值 float Convert_ADC_to_PH(uint16_t adc_raw) { float ph_value; ph_value ph_slope_k * (float)adc_raw ph_offset_b; // 可以添加范围限制或滤波 if(ph_value 0) ph_value 0; if(ph_value 14) ph_value 14; return ph_value; }对于浊度和TDS传感器原理类似但公式可能不同。浊度传感器可能需要查表或使用非线性拟合。TDS传感器通常提供线性电压输出但其转换系数ppm/V需要参考传感器手册并且要结合温度补偿公式。例如一个常见的简化公式是TDS (adc_voltage * tds_coefficient) / (1.0 0.02 * (temperature - 25.0))其中tds_coefficient需要根据传感器规格确定。4.3 数据整合与串口输出在主循环中我们定期例如每秒一次从全局数组ADC_ConvertedValue中读取数据进行转换然后通过串口打印出来。同时可以加入简单的软件滤波比如取最近10次采样的平均值以平滑数据。// 全局变量 uint16_t ADC_ConvertedValue[3]; // 存放ADC结果的数组 float filtered_turbidity, filtered_ph, filtered_tds; void main(void) { // 系统时钟、ADC、DMA、串口等初始化 SystemInit(); USART1_Init(9600); // 初始化串口1波特率9600 ADC1_DMA_Init(); // 初始化校准系数应从EEPROM读取 // ph_slope_k ...; ph_offset_b ...; while(1) { // 每秒处理一次数据 Delay_ms(1000); // 读取ADC值由于DMA循环写入这里直接读取即可 uint16_t adc_turb ADC_ConvertedValue[0]; uint16_t adc_ph ADC_ConvertedValue[1]; uint16_t adc_tds ADC_ConvertedValue[2]; // 转换为物理量 (这里省略了滤波和温度补偿的详细代码) filtered_turbidity Convert_ADC_to_Turbidity(adc_turb); filtered_ph Convert_ADC_to_PH(adc_ph); filtered_tds Convert_ADC_to_TDS(adc_tds, current_temperature); // 假设有温度值 // 通过串口打印 printf(Turbidity: %.2f NTU, PH: %.2f, TDS: %.0f ppm\r\n, filtered_turbidity, filtered_ph, filtered_tds); // 也可以同时刷新OLED显示 // OLED_ShowString(...); } }5. Proteus仿真中的关键验证与常见问题排查在Proteus中仿真这个项目不仅仅是看电路能不能跑通更是对硬件设计逻辑和软件驱动代码的一次严格检验。下面我结合自己的仿真经历总结几个关键的验证点和常见的“坑”。5.1 模拟信号源的设置与观察在Proteus中我们用可调电位器来模拟传感器输出的变化电压。这里有几个细节电压范围确保电位器输出的电压在STM32 ADC的输入电压范围内通常是0-3.3V。如果传感器模块输出是0-5V需要在输入端加分压电路仿真时电位器就应模拟分压后的电压。动态变化你可以手动拖动电位器的滑块来改变电压模拟水质参数的变化。更好的方法是使用Proteus的SIMULATION GRAPH仿真图表功能给电位器的控制端施加一个模拟电压信号如正弦波、三角波来观察系统对动态输入的响应。测量验证在ADC输入引脚处连接一个DC VOLTMETER确保你设置的电压值确实如你所想。有时候电位器连接错误会导致电压不对。5.2 串口通信的验证串口是调试的“生命线”。在Proteus中连接VIRTUAL TERMINAL后双击虚拟终端设置波特率Baud Rate、数据位、停止位、校验位必须与STM32代码中的串口配置完全一致。最常见的是9600-8-N-1。运行仿真。如果一切正常虚拟终端窗口会每秒打印出一行格式如Turbidity: 12.34 NTU, PH: 7.05, TDS: 215 ppm的数据。如果没数据首先检查STM32的TX引脚如PA9是否正确连接到了虚拟终端的RXD引脚。检查Keil5生成的HEX文件是否成功加载到Proteus的STM32模型中重新加载一次。检查STM32的系统时钟和串口波特率计算是否正确。一个72MHz系统时钟配置9600波特率是常见的组合。可以在Proteus中用DIGITAL OSCILLOSCOPE数字示波器连接到TX引脚测量一个字节起始位的低电平时间换算一下看是否是预期的1/9600秒约104us。5.3 ADC采样结果的验证这是仿真中最需要仔细核对的一环。由于ADC采样的是模拟电压我们需要验证STM32读到的数字量是否与输入的模拟电压成正确的比例关系。理论计算STM32的ADC是12位参考电压Vref假设接3.3V。那么数字量D (V_in / 3.3V) * 4095。仿真验证在代码中除了打印转换后的物理量也把原始的ADC值通过串口打印出来。例如ADC: %d, %d, %d。在Proteus中将三个电位器分别调到某个固定电压比如1.0V 1.5V 2.0V。查看串口打印的原始ADC值。对于1.0V输入理论值应为(1.0/3.3)*4095 ≈ 1241。允许有一定的量化误差。如果偏差巨大首先检查ADC引脚的配置是否为模拟输入GPIO_Mode_AIN然后检查ADC的采样时间配置是否足够对于信号源内阻55.5个周期通常足够。在Proteus中STM32模型和理想电压源之间的交互是理想的所以偏差应该很小。5.4 常见仿真问题与解决思路仿真运行极慢或卡死原因代码中有死循环或阻塞性延时如while(1)等待某个标志位而该标志位由于仿真模型或代码逻辑问题永远不会被置位。解决检查所有while循环等待的条件如ADC校准完成标志、DMA传输完成标志。在仿真初期可以适当增加超时退出机制。优化代码结构避免在主循环中使用过长的Delay_ms。虚拟终端显示乱码原因几乎100%是波特率不匹配。请双检Keil5代码中的串口初始化函数和Proteus虚拟终端的属性设置。解决确保完全一致。也可以尝试更低的波特率如4800看是否正常。ADC值不变化或始终为0原因DMA或ADC配置错误导致转换没有真正启动或者ADC通道配置错误。解决逐步调试。可以先不用DMA用单次转换模式在代码中手动触发一次ADC转换并读取一个通道的值看是否正常。如果正常再逐步加入扫描模式和DMA。检查ADC_RegularChannelConfig的顺序和通道号是否正确。Proteus中STM32模型无法加载HEX文件原因HEX文件路径包含中文或特殊字符Keil5输出HEX的选项未勾选芯片型号不匹配。解决将工程和HEX文件放在英文路径下确认Keil5的Options for Target - Output - Create HEX File已勾选尝试在Proteus中使用STM32F103C6模型其引脚与C8兼容。通过以上细致的验证和排查你就能在投入硬件成本之前最大限度地保证系统设计的正确性。仿真通过后移植到实物开发板上就会顺利很多剩下的工作主要是解决实际传感器接口、电源噪声、PCB布局等现实问题了。这个从仿真到实物的跨越又是另一个充满挑战和乐趣的过程。本文还有配套的精品资源点击获取