简介:一款基于STM32的电导率测量仪完整设计资料,面向电子、自动化、仪器仪表及计算机相关专业的毕设、课设和项目初期验证。项目以STM32F429为主控,外接电导率信号处理模块,设计正弦波驱动、滤波、放大与电压转置电路,并完成温度传感器、ADC、LCD、触摸屏、SD卡及串口打印等驱动,最终实现溶液电导率的采集、显示与保存。包内共101个文件,以C/H源码、SCHDOC/PCBDOC电路设计、HEX固件、Keil工程、MD说明和MP4演示为主,压缩包约72.96MB,目录结构清楚,便于按模块阅读与二次开发。其中MCU驱动、传感器调理、显示与存储代码相互分离,便于理解数据流与功能扩展。作者已实际测试,功能验证通过,可顺利收集数据,适合从传感器调理、模拟电路到嵌入式的完整学习。已有648人学习下载,可作为低成本工业级测量仪器设计的有价值参考。
1. 从浓差极化说起:为什么STM32电导率仪要用正弦波激励
在流体实验里需要连续记录溶液电导率,市面上的产品分两类:食品级便宜但探头精度和寿命都一般,工业级参数漂亮价格却直接跳一个数量级。实验室正好有一块STM32F429开发板,于是决定绕开仪表采购成本,自己搭一台适合项目场景的电导率测量仪。真正动手后才发现,电导率测量的核心难点不在ADC采样,而在激励方式。电极放进导电溶液后,如果施加直流电压,正负离子会向两极定向迁移,在电极表面形成极化层,等效阻抗随时间漂移,数值根本稳不住。因此整套硬件必须用正弦波交流激励,让电场方向不断切换,把浓差极化压到可接受范围。这也决定了后续DDS波形发生、信号放大、DFT幅值提取、温度补偿等一系列设计选择。本文从硬件到固件,完整拆解这套基于STM32F429的低成本工业级电导率仪实现过程。
2. 硬件设计:正弦波发生、信号调理与负电源电路
2.1 DDS激励源:SPI配置与频率设定
电导率传感器需要频率稳定、幅值可调的正弦波激励。市售方案里,用DDS芯片比运放搭文氏桥振荡器更适合仪器场景:频率由寄存器控制,可随时切换激励频率,以适应不同浓度溶液的测量需求。常见的DDS芯片是AD9833,内部集成10位DAC,一路SPI接口即可完成配置,输出端加一个低通滤波器就能得到干净的正弦波。
硬件连接上,AD9833的SCLK、SDATA、FSYNC分别接STM32F429的SPI2引脚,输出接电极驱动电路。初始化时把SPI配置为8位主模式,NSS软件管理:
static void DDS_SPI_Init(void) { hspi2.Instance = SPI2; hspi2.Init.Mode = SPI_MODE_MASTER; hspi2.Init.Direction = SPI_DIRECTION_1LINE; hspi2.Init.DataSize = SPI_DATASIZE_8BIT; hspi2.Init.NSS = SPI_NSS_SOFT; hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB; HAL_SPI_Init(&hspi2); }虽然AD9833的寄存器长度是16位,但SPI数据位配置成8位后,只要连续发送两个字节,芯片仍能正确解析。FSYNC引脚要在字节发送期间保持低电平,发送完成后拉高,这个时序如果没写对,会出现频率偶尔跳变的现象。我把写入操作封装成独立函数,并等待SPI状态机回到ready:
static void AD9833_Write16(uint16_t reg) { uint8_t buf[2]; buf[0] = (reg >> 8) & 0xFF; buf[1] = reg & 0xFF; HAL_GPIO_WritePin(DDS_SYNC_GPIO_Port, DDS_SYNC_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi2, buf, 2, HAL_MAX_DELAY); while (HAL_SPI_GetState(&hspi2) != HAL_SPI_STATE_READY); HAL_GPIO_WritePin(DDS_SYNC_GPIO_Port, DDS_SYNC_Pin, GPIO_PIN_SET); }AD9833频率寄存器是28位,控制字需要先写成B28=1,把28位拆成两个14位段写入。假设参考时钟是25MHz,想输出10kHz正弦波,频率字FREQ = (10e3 × 2^28) / 25e6 ≈ 106.9,取整后拆成低14位和高14位。后续如果要在不同激励频率间切换,只需重新写入两组数据,芯片会自动换频且不产生相位跳跃,这一点对测量一致性很关键。
2.2 信号放大与滤波电路设计要点
电极在交流激励下的等效模型可看作电阻并联双电层电容,响应电流通常只有微安到毫安量级。为了让STM32F429内部12位ADC的分辨率足够,信号必须经过I/V转换、交流放大和带通滤波。I/V转换电阻决定小信号增益,阻值越大灵敏度越高,但响应速度会变慢,所以要根据目标电导率量程折中。
| 设计位置 | 参考参数 | 说明 |
|---|---|---|
| 激励频率 | 1 kHz ~ 10 kHz | 低浓度溶液用低频,高浓度用高频 |
| 激励幅值 | 0.2 Vpp ~ 1.0 Vpp | 幅值过高加速极化,过低降低信噪比 |
| I/V转换电阻 | 10 kΩ ~ 100 kΩ | 对应 1 mS/cm ~ 200 mS/cm 量程 |
| 交流放大增益 | 10 倍 ~ 100 倍 | 让ADC输入在满量程30%~70% |
| 带通滤波器 | 中心频率=激励频率 | Q值取0.7~1.0 |
滤波模块我用了两个运放搭成二阶带通滤波器,中心频率由电阻和电容共同决定。按10kHz中心频率设计时,电容取1nF,电阻按标准值计算,理论Q值约0.78,对高次谐波有20dB以上衰减。运放选型要注意增益带宽积,如果选1MHz带宽的普通运放,放大10kHz信号100倍后会衰减到一半以下,这种情况下必须用到增益带宽积大于10MHz的型号。
2.3 负电源生成与PCB布局
放大电路里的双电源运放需要负电源,项目中的电压转置模块就是解决这个问题。常用的方案是电荷泵型DC-DC转换器,比如TPS60403或LMC7660,将3.3V转换成-3.3V,外围只需要两个陶瓷电容。第一版设计我尝试过用MC34063反压,纹波偏大,后来改成电荷泵,并在负电源输出端加了LC滤波,开关噪声控制在2mV以内。
PCB布局上,数字电路和模拟电路必须分区。STM32F429和DDS属于数字区,I/V转换和放大属于模拟区,两地通过0Ω电阻单点连接。电导率探头信号线尽量短,并且使用屏蔽线,屏蔽层在仪器端接地。电源走线避免与输入信号线平行。这些布局细节如果处理不好,即使芯片选型全部正确,也会出现测量值周期性跳变。
3. 固件驱动:STM32F429外设配置与电导率数据采集
3.1 定时器触发ADC同步采样
电导率信号是正弦波,为了用DFT提取幅值,ADC采样必须和激励频率保持整数倍关系。本项目用定时器4的更新事件作为ADC外部触发源,通过调整PSC和ARR把采样率设为激励频率的64倍,这样每个周期采集64个点,DFT计算量小且能够匹配预期频率点。
ADC开启扫描模式,一次触发采集两个通道:一路采集激励电压参考信号,另一路采集放大后的电极响应信号。这两个通道的数据相除可以抵消激励幅值漂移带来的增益误差。初始化配置如下:
ADC_ChannelConfTypeDef sConfig = {0}; hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode = ENABLE; hadc1.Init.ContinuousConvMode = DISABLE; hadc1.Init.NbrOfConversion = 2; hadc1.Init.ExternalTrigConv = ADC_EXTERNAL_TRIGCONV_T4_TRGO; HAL_ADC_Init(&hadc1); sConfig.Channel = ADC_CHANNEL_0; sConfig.SamplingTime = ADC_SAMPLETIME_84CYCLES; HAL_ADC_ConfigChannel(&hadc1, &sConfig); sConfig.Channel = ADC_CHANNEL_1; HAL_ADC_ConfigChannel(&hadc1, &sConfig);NbrOfConversion = 2,表示每次触发依次扫描两个通道。定时器更新事件频率就是采样频率。需要留意的是,ADC中断处理不要放太多计算任务,否则定时器触发会堆积,导致采样点丢失。实际调参时,我把采样率固定为64kHz,定时器预分频值和重装载值按照公式计算,之后不再改动。
3.2 温度传感器驱动与转换
电导率换算必须依赖温度数据,项目里温度传感器选择了单总线的DS18B20。它只需要一根数据线接到STM32F429普通GPIO,读温度时序简单,精度12位时分辨率0.0625℃,足够实验室使用。驱动时先执行复位,再跳过ROM匹配并启动转换,等待转换完成后读取两个字节。
uint16_t DS18B20_ReadRaw(void) { uint8_t low, high; if (DS18B20_Reset()) return 0xFFFF; DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0x44); while (!DS18B20_ReadBit()); DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); low = DS18B20_ReadByte(); high = DS18B20_ReadByte(); return (uint16_t)((high << 8) | low); }上述代码最关键的是等待转换完成那一步。DS18B20在12位分辨率下转换时间约750ms,如果用固定延时,环境温度较低时转换时间变长,容易出现采样和测量不同步。等待转换时,主循环仍然可以继续做ADC采集,温度值只需要每秒刷新一次。
3.3 从正弦响应中提取幅值:单频DFT算法
电极响应信号经过放大后仍然是正弦波,只是幅值和相位发生变化。从有限个采样点里提取幅值,我推荐做单频DFT,而不是直接用峰值检测,因为DFT能有效抑制随机噪声和直流偏置。
typedef struct { float real; float imag; } Complex; Complex SingleToneDFT(int16_t *samples, uint32_t n, float f_signal, float f_sample) { Complex result = {0.0f, 0.0f}; uint32_t k = (uint32_t)(f_signal / f_sample * n + 0.5f); float step = 2.0f * 3.1415926535f * k / n; for (uint32_t i = 0; i < n; i++) { float angle = step * i; result.real += samples[i] * cosf(angle); result.imag -= samples[i] * sinf(angle); } result.real *= 2.0f / n; result.imag *= 2.0f / n; return result; }调用时n=64,f_sample是实际ADC采样率,f_signal是DDS配置的激励频率。代码里先用四舍五入方式计算对应的频谱bin,避免因除不净产生频谱泄漏。返回的复数幅值通过sqrt(real²+imag²)得到,这个幅值就是后续电导率换算的原始输入。
| 参数 | 设置值 | 说明 |
|---|---|---|
| ADC分辨率 | 12 bit | 内部ADC,满足显示精度 |
| 采样率 | 64 kHz | 64 × 激励频率 |
| 采样点数 | 64 | 一个完整周期 |
| 输入通道 | ADC1_IN0 / IN1 | 激励参考 + 响应信号 |
4. 数据处理与温度补偿算法
4.1 电导池常数与原始电导率换算
电导率不是直接测出来的,而是先测出电极间的电导G,再乘上电极常数K。电极常数由电极几何尺寸决定,常见标称值有0.1、1、10三挡,对应低、中、高量程。由于制造误差,每支电极的K值都有偏差,必须用标准液重新标定。
从DFT幅值得到电极间等效电阻后,当前温度下的电导率可表示为:
float cond_rt = cell_const / resistance;其中resistance单位是Ω,cond_rt单位是S/cm。例如电极常数K=1,电阻10kΩ,那么当前温度电导率为0.0001S/cm,也就是0.1mS/cm。这个计算看起来简单,但前提是激励频率和采样率保持同步,如果两者不同步,resistance会明显偏大,造成显示值错误。
4.2 温度补偿:修正到25℃标准值
溶液电导率受温度影响很大,大多数中性盐溶液温度升高1℃,电导率增加约2%。为了让测量结果可比较,工业仪表统一把实际温度下的电导率折算到25℃。本项目采用线性补偿公式:G25 = Gt / (1 + α(t - 25))。
| 溶液类型 | 温度系数 α(1/℃) | 适用场景 |
|---|---|---|
| 中性盐溶液 | 0.020 | NaCl、KCl等 |
| 稀酸 | 0.014 | 稀硫酸、盐酸 |
| 稀碱 | 0.019 | 稀NaOH溶液 |
| 纯水 | 0.025 | 低电导率监测 |
温度传感器读取的实时温度t直接代入公式。系数α默认写成0.02,如果待测溶液类型明确,可在菜单里调整。计算时把补偿放在最后一步,因为标定标准液通常已经在25℃环境完成,这样可以简化电极常数的迭代过程。
4.3 标定参数与断电保存
标定后得到的电极常数和温度系数必须掉电保存。STM32F429内部Flash空间充足,我直接把它当作EEPROM使用,在最后一个扇区存储参数结构体。保存前计算CRC16,加载时校验,防止在擦写过程中断电导致参数损坏。
typedef struct { float cell_const; float temp_coeff; uint16_t crc; } CalibParam; void Calib_Save(CalibParam *param) { param->crc = (uint16_t)CalcCRC16((uint8_t*)param, sizeof(*param) - 2); HAL_FLASH_Unlock(); FLASH_Erase(FLASH_SECTOR_11); // 写入数据,此处略去逐字写入 HAL_FLASH_Lock(); } void Calib_Load(CalibParam *param) { memcpy(param, (uint8_t*)FLASH_BASE_ADDR, sizeof(*param)); if (param->crc != (uint16_t)CalcCRC16((uint8_t*)param, sizeof(*param) - 2)) { param->cell_const = 1.0f; param->temp_coeff = 0.02f; } }代码逻辑是加载后先做CRC校验,校验失败就恢复默认电极常数1.0。这个默认值能保证在未标定状态下仪器也有输出,只是精度无法保证。需要注意的是,内部Flash擦除按扇区执行,写之前要确认该扇区没有存放代码或片选配置,否则程序可能直接跑飞。
5. 人机交互与数据存储:LCD、触摸屏与SD卡文件系统
5.1 LCD显示与刷新策略
源码包里的bsp_lcd.c已经完成LCD底层寄存器配置,对外提供清屏、画矩形、显示字符串等基础接口,字库则由fonts.c提供,中英文字符都能渲染。显示测量数据时,我建议把数值格式化成固定位数,例如COND:%5.2f mS/cm,避免数字位数变化导致屏幕残留上一帧内容。
char buf[32]; snprintf(buf, sizeof(buf), "COND:%5.2f mS/cm", g_result.cond); LCD_ClearRegion(x, y, x + 220, y + 24); LCD_DisplayString(x, y, (uint8_t*)buf);先调用LCD_ClearRegion只清数值区域,再调用LCD_DisplayString显示新数据。如果为了省事每次都清全屏,画面会明显闪烁。LCD刷新频率不需要太快,仪器类界面每秒刷新两次即可。整个UI任务可放在主循环里,由标志位控制,避免和ADC中断抢CPU。
5.2 触摸屏驱动与参数修改
触摸屏芯片选用GT9xx系列,驱动代码在gt9xx.c里已经实现了固件加载、坐标读取和触点上报。GT9xx通过I2C接口与STM32通信,中断引脚接入EXTI外部中断。每次触摸触发中断后,读取第一有效触点坐标,然后做一次滑动滤波,减轻手指按压不稳定带来的跳动。
void HAL_GPIO_EXTI_Callback(uint16_t Pin) { if (Pin == TOUCH_INT_Pin) { if (GT9xx_ReadOnePoint(&touch_x, &touch_y)) { UI_Task(touch_x, touch_y); } } }中断回调里最好只更新坐标变量,不执行耗时的Flash写入或文件系统操作。UI_Task处理点击区域判断,比如用户按“标定”按钮后,设置一个标定标志,主循环检测到这个标志再执行标准液校准流程。电容屏坐标与LCD分辨率通常不是一一对应的,需要在初始化和LCD屏尺寸匹配时做一次线性变换。
5.3 SD卡日志系统:FATFS挂载与CSV写入
SD卡部分对应源码包里的bsp_sdio_sd.c,负责SDIO底层初始化;ff.c是FATFS文件系统核心;cc936.c提供GBK/中文代码页转换。SD卡上电后需要约几十毫秒稳定时间,我习惯把它放在开机自检函数里,先延时再执行SDIO初始化,否则CMD0命令经常失败。
日志写入采用CSV文本格式,每列包括时间戳、电导率、温度和当前电极常数,方便导入Excel或Python做后处理。追加写入的完整流程如下:
FIL fil; FATFS fs; FRESULT res; res = f_mount(&fs, "SD:", 1); if (res == FR_OK) { res = f_open(&fil, "SD:/LOG.CSV", FA_WRITE | FA_OPEN_ALWAYS); if (res == FR_OK) { f_lseek(&fil, f_size(&fil)); f_printf(&fil, "%.2f,%.2f,%.2f\r\n", conductivity, temp, cell_const); f_close(&fil); } }这段代码中f_lseek到文件末尾是关键,否则每次打开文件都会从头部开始覆盖。FATFS的f_printf支持浮点输出,但前提是配置里_USE_STRFUNC设为1。下面是一组在电导率仪上验证过的FATFS配置:
| 宏名称 | 配置值 | 作用 |
|---|---|---|
_USE_LFN | 2 | 长文件名支持,动态内存 |
_CODE_PAGE | 936 | 中文字符编码 |
_MAX_SS | 4096 | 兼容4KB扇区SD卡 |
_USE_STRFUNC | 1 | 启用f_printf字符串功能 |
频繁写入SD卡会占用较长时间,影响ADC采样实时性。实际项目里我把写入间隔设为2秒,每两次采集结果累积到缓冲区,再一次性追加到CSV文件。这样既保证日志连续,又不会造成采样任务阻塞。
6. 调试、校准与实战:串口监视与标准液标定技巧
6.1 串口打印结构化数据
调试阶段,我会把采集到的原始数据和计算结果全部通过USART3打印出来。用HAL库重定向fputc后,printf直接输出到串口,方便在电脑端用串口助手或Python脚本实时绘图:
int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart3, (uint8_t*)&ch, 1, 10); return ch; }每次数据更新后输出一行CSV:printf("%.3f,%.3f,%.3f\r\n", temperature, raw_adc_v, conductivity);这样可以不动上位机逻辑,直接通过终端重定向保存数据。
6.2 标准液快速标定流程
标定准备一支量程合适的KCl标准液,常用12.88mS/cm。电极插入标准液后等待温度读数稳定,然后进入标定菜单,软件读取当前未补偿电导率并显示。按下“校准”时,程序自动把电极常数调整为当前值,使显示值等于12.88mS/cm。建议连续标定两次,第二次微调后保存到Flash。
如果没有标准液,也可以用精密电阻箱代替电极,模拟已知电导。比如设置1kΩ电阻对应K=1时的1mS/cm,分别测1kΩ、10kΩ、100kΩ三个点,检查线性度。三个点基本呈直线,说明I/V转换和采样链路正常;如果只有高阻端偏差大,重点检查I/V转换电阻的寄生电容和PCB漏电流。
6.3 量程切换与自检
量程切换在软件中通过切换I/V转换电阻挡位实现,每次切换后需要重新初始化ADC和DDS同步关系。我的代码里把量程做成菜单项,每换一次量程就自动保存当前参数,并把默认电极常数重置到1。量程切换后,先用标准液做一次单点校准,防止切换引入的增益误差。这样既不增加硬件复杂度,也能覆盖从纯水到高浓度盐水的测量范围。
本文还有配套的精品资源,点击获取