STM32水质水位精准检测方案详解
目录
一 项目需求
项目概述
功能描述
数据采集(输入模块)
数据处理
数据显示
系统总体设计
二 硬件架构及软件架构
硬件选型
水位测量模块
TDS采集模块
外置ADC模块(ADS1115)
水位测量模块使用方法
水位测量原理
读取时序图
TDS采集模块使用方法
TDS采集原理
ADC模块ADS1115使用方法
读写时序
编辑软件架构
三 显示模块
Driver层
FSMC(之前代码)
Interface层
LCD(之前代码)
App层
Display(显示模块)
display.h
display.c
四 水位测量模块
逻辑
文件创建
gpio.h
gpio.c
hx710.h
hx710.c
water_level.h
water_level.c
五 调试模块和延时模块
分层
调试模块:(涉及到驱动层和公共层)
printf函数
debug.h
debug.c
延迟模块:(公共层)
一 项目需求
项目概述
水质测量:TDS(Total Dissolved Solids),中文名总溶解固体,又称溶解性固体总量,表明1升水中溶有多少毫克溶解性固体。一般来说,TDS值越高,表示水中含有的溶解物越多,水就越不洁净。
水位测量通过压强
功能描述
数据采集(输入模块)
系统能够连接到要测的水体。
TDS探针能够捕获水体的TDS数据,并把数据传输到嵌入式系统。
水位传感器可以把数据传输到嵌入式系统。
数据处理
在STM32中进行处理。
数据显示
讲数据显示到LCD屏幕上。
系统总体设计
二 硬件架构及软件架构
硬件选型
水位测量模块
TDS采集模块
外置ADC模块(ADS1115)
水位测量模块使用方法
水位测量原理
压力传感器:水越深压力越大
P= ρgh
F=ps
F=ρgh*s + P大气压
水位测量传感器本质上是一个压力测量传感器。压力的值和传感器产生的电压值是线性关系,压力的值和水深也是线性关系。根据这个原理,我们不需要知道具体的电压,就可以测量出来水位。
假设水位是x,从ADC读取的值y。(y不必转成具体的电压)。则有下面的等式成立:
y= ax +b
当水管没有放入水里时,
x1 = 0 ,y1 = b(测出y1)
当水管放入水里10cm时,
x2 = 10,则 y2 = 10a + b (测出y2)
从而计算出来:
b = y1;a = (y2 – y1)/10
所以有:
x = (y – b)/a*y
我们实时测,从而计算出来 x (水位)的值
读取时序图
TDS采集模块使用方法
TDS采集原理
当水中的导电粒子多时,导电性好,采集到的电压高;导电粒子少时导电性差,采集到的电压低。可以简单的认为水中杂质多时,导电粒子多,杂质少时导电粒子少。所以可以通过采集的电压高低来计算TDS的值。
TDS内部有一个固定的函数,可以通过手册进行查看。
通过这个函数可以看到电压值对应的TDS的值。
ADC模块ADS1115使用方法
ADC模块提供了5个寄存器(查看手册)
读写时序
软件架构
三 显示模块
Driver层
FSMC(之前代码)
Interface层
LCD(之前代码)
App层
Display(显示模块)
display.h
#ifndef __DISPLAY_H__
#define __DISPLAY_H__
#include "lcd.h"
//初始化
void Display_Init(void);
//清屏
void Display_Clear(void);
//显示信息 (英文,指定字体大小和颜色)
void Display_String(uint16_t x,uint16_t y,uint8_t * str);
//显示标题 (LOGO + 汉字)
void Display_Title(void);
#endif /* __DISPLAY_H__ */
display.c
#include "display.h"
//初始化
void Display_Init(void){
LCD_Init();
Display_Clear();
}
//清屏
void Display_Clear(void){
LCD_fillColor(0,0,320,480,WHITE);
}
//显示信息 (英文,指定字体大小和颜色)
void Display_String(uint16_t x,uint16_t y,uint8_t * str){
LCD_ShowAsciiString(x,y,str,WHITE,BLUE,BIG);
}
void Display_Title(void){
//显示LOGO
LCD_ShowBeauty1();
for (uint8_t i = 0; i < 9; i++)
{
LCD_ShowChinereChar(16+i*32,80,WHITE,BLUE,i);
}
}
四 水位测量模块
逻辑
传感器捕捉到的是压力,压力转换成电压值是模拟电压,然后通过ADC转换成数字电压,然后输入给芯片,最后芯片进行运算,然后再lcd屏幕上显示。
通过逻辑梳理,可以知道电压值和水深是成为线性关系,y = a * x + b;需要求得a和b,所以需要两个固定的值来验证,这里用x水深0cm和10cm来,推断a和b。
y = a * x + b
y = a * 0 + b -> b = y 可以测得b值
y = a * 10 + b -> 将b带入可得a
将a和b存储到falsh中,然后每次测量都从中提取可以得到深度。
通过按键进行两次的校准。
文件创建
Driver->创建GPIO->创建gpio.c和gpio.h
Interface->创建HX710 ->创建hx710.c和hx710.h
App->创建WaterLeven->创建water_level.c和water_level.h
Driver->SPI 过去文件迁移(存储文件的协议)
Interface->FLASH 过去文件迁移(存储文件的地址)
gpio.h
这里按键不采用中断,这里的目标是等待采样,所以用whil循环来进行按键操作
#ifndef __GPIO_H
#define __GPIO_H
#include "stm32f10x.h"
#include "delay.h"
// 宏定义 SCK-PB12,OUT-PB13 操作
#define SCK_HIGH (GPIOB->ODR |= GPIO_ODR_ODR12)
#define SCK_LOW (GPIOB->ODR &= ~GPIO_ODR_ODR12)
#define OUT_READ (GPIOB->IDR & GPIO_IDR_IDR13)
// 宏定义 KEY3-PF10 操作
#define KEY_READ (GPIOF->IDR & GPIO_IDR_IDR10)
// 初始化(PB12、PB13、PF10)
void GPIO_Init(void);
// 等待按键按下,方便校准
void GPIO_Wait4KeyPressed(void);
#endif
gpio.c
这里按键不采用中断,这里的目标是等待采样,所以用whil循环来进行按键操作
判断按键按下是GPIOF->IDR & GPIO_IDR_IDR10 那一位为1,所以不能写成GPIOF->IDR & GPIO_IDR_IDR10 = 1。
注意 位=1 ,不是整体等于1,所以这里直接采用真值!= 0来判断。
#include "gpio.h"
//初始化 (PB13、PB13、PF10)
void GPIO_Init(void)
{
//1. 测量模块
//1.1 开启GPIOB时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
//1.2 设置PB12、PB13 工作模式
//PB12 - 通用推挽输出 MODE - 11,CNF - 00
GPIOB->CRH |= GPIO_CRH_MODE12;
GPIOB->CRH &= ~GPIO_CRH_CNF12;
//PB13 - 浮空输入 MODE - 00,CNF - 01
GPIOB->CRH &= ~GPIO_CRH_MODE13;
GPIOB->CRH &= ~GPIO_CRH_CNF13_1;
GPIOB->CRH |= GPIO_CRH_CNF13_0;
//2.校准部分
//2.1 开启GPIOF时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPFEN;
//2.2 设置PF10的工作模式,下拉输入 MODE - 00 ,CNF - 10
GPIOF->CRH &= ~GPIO_CRH_MODE10;
GPIOF->CRH |= GPIO_CRH_CNF10_1;
GPIOF->CRH &= ~GPIO_CRH_CNF10_0;
GPIOF->ODR &= ~GPIO_ODR_ODR10;
}
//等待按键按下,方便校准
void GPIO_Wait4KeyPressed(void)
{
// 使用循环 轮询等待
while (1)
{
//检测按键按下
if (KEY_READ != 0)
{
//防抖延迟
Delay_ms(100);
if (KEY_READ != 0)
{
return;
}
}
}
}
hx710.h
#ifndef __HX710_H__
#define __HX710_H__
#include "gpio.h"
//初始化
void HX710_Init(void);
//读取转换结构
uint32_t HX710_ReadValue(void);
#endif /* __HX710_H__ */
hx710.c
注意:最后返回的值与1异或的原因?
首先与1异或是取反,其次通过查看手册发现输入数据编码是二进制的补码,范围是0x800000~0x7FFFFF,拿8位来讲有符号的范围是-128~127,补码 -0表示-128,因为这个是24位的,没有类型可以表示无符号数,所以直接在这里处理将有符号数转换成无符号数。
#include "hx710.h"
// 初始化
void HX710_Init(void)
{
GPIO_Init();
}
// 读取ADC转换结果
uint32_t HX710_ReadValue(void)
{
// 定义变量保存读取的数据
uint32_t data = 0;
// 1. 拉低时钟
SCK_LOW;
// 2. 等待数据信号变低
while (OUT_READ)
{
}
// 3. 循环读取24位数据
for (uint8_t i = 0; i < 24; i++)
{
// 3.1 拉高时钟
SCK_HIGH;
Delay_us(5);
// 3.2 拉低时钟
SCK_LOW;
// 3.3 左移
data <<= 1;
// 3.4 读取数据
if (OUT_READ)
{
data |= 0x01;
}
Delay_us(5);
}
// 4. 第25个时钟周期
SCK_HIGH;
Delay_us(5);
SCK_LOW;
Delay_us(5);
// 5. 转换成无符号数(移码)返回
return data ^ 0x800000;
}
water_level.h
#ifndef __WATERLEVEL_H__
#define __WATERLEVEL_H__
#include "hx710.h"
#include "W25Q32.h"
#include "string.h"
#include "stdlib.h"
#include <stdio.h>
//宏定义 保存到Flash 的地址
#define ADDR 0x00
//初始化
void WaterLevel_Init(void);
//读取水位值(单位cm)
double WaterLevel_ReadValue(void);
#endif /* __WATERLEVEL_H__ */
water_level.c
#include "waterlevel.h"
#include "display.h"
//定义水位计算a、b(y = ax + b)
double a,b;
//声明校准函数
static void WaterLevel_Calibrate(void);
//初始化
void WaterLevel_Init(void){
//1.测量芯片HX710初始化
HX710_Init();
//2.保存校准参数芯片W25Q32初始化
W25Q32_Init();
//3.对参数(a,b)进行校准
WaterLevel_Calibrate();
}
static void WaterLevel_Calibrate(void){
//0.先做一次擦除防止之前有数据残留
W25Q32_EraseSector((ADDR >> 16) & 0xff, (ADDR >> 12) & 0x0f);
//数据在Flash中存储格式:La#b (9 <= L <= 30)
// 定义保存数据的变量
//保存读到的L
uint8_t len = 0;
//保存读到的数据a#b
uint8_t data[31] = {0};
// 1. 读取Flash,判断是否已经做过校准,如果校准过就直接返回
//1.1 先读取第一个字节
W25Q32_ReadData(0,0,0,0,&len,1);
//1.2.判断L如果在标准范围内,说明校准过,直接解析数据读取a和b
if (len >= 9 && len <= 30)
{
//1.2.1 读取数据
W25Q32_ReadData(0,0,0,1,data,len);
//1.2.2 解析a和b的值stritok
a = strtod(strtok((char *)data, "#"), NULL);
b = strtod(strtok(NULL, "#"), NULL);
//不需要直接返回
return;
}
//2.如果没有校准过,就执行校准流程
//2.1 LCD提示信息:不放入水中,按下按键,第一次读取y1
//2.1.1 第一次提示信息
Display_String(10,150,"Start to calibrate water level...");
Display_String(10,200,"1.Don't put into water, then press the key...");
//2.1.2 等待用户按下按键
GPIO_Wait4KeyPressed();
//2.1.3 读取当前电压值-y1
uint32_t y1 = HX710_ReadValue();
//2.2 LCD提示信息:放入10cm水中,按下按键,第二次读取y2
//2.2.1 第二次提示信息
Display_String(10,250,"2.Put into 10cm water,then press the key...");
//2.2.2 等待用户按下按键
GPIO_Wait4KeyPressed();
//2.2.3 读取当前电压值 - y2
uint32_t y2 = HX710_ReadValue();
//2.3 根据y1、y2的值,计算除直线方程中参数a、b 完成校准
b = y1;
a = (y2 - y1) / 10.0;
//2.3.1 提示完成校准
Display_String(10, 300, "Calibration is done!");
//2.4 清空屏幕信息,准备显示实时的水位信息,记得加延迟,和标头
Delay_s(2);
Display_Clear();
Display_Title();
//2.5 保存参数:讲a和b的值以 La#b的格式存入Flash
//2.5.1 拼接数据 a#b
sprintf((char*)data,"%.2f#%.2f", a , b);
//2.5.2 计算数据长度L
len = strlen((char *)data);
//2.5.3 写入Flash前,进行段擦除
W25Q32_EraseSector( (ADDR >> 16) & 0xff, (ADDR >> 12) & 0x0f );
//2.5.4 将L写入第一个字节
W25Q32_PageWrite(0,0,0,0,&len,1);
//2.5.5 将data写入后面的字节
W25Q32_PageWrite(0,0,0,0,data,len);
}
//读取水位值(单位cm)
double WaterLevel_ReadValue(void){
//1.先读取当前电压值
uint32_t y = HX710_ReadValue();
//2.带入公式y = a * x + b,计算对应的水位值 x
return(y - b) / a;
}
五 调试模块和延时模块
分层
通过软件架构可以进行分层,驱动层Driver、接口层Interface、应用层App、公共层Common、用户层,分别创建这几个层级文件夹
调试模块:(涉及到驱动层和公共层)
1 打开Driver文件夹
2 将之前的USART文件直接复制到新项目的Driver文件夹中,记得keil中添加.c和.h文件
STM32 案例计算机和串口通讯及学习方法和重映像printf函数(逻辑查询以及寄存器代码如何书写)_stm32库函数串口代码-CSDN博客
printf函数
printf函数可以用来调试,但是随着代码的增多,为了更加方便,可以用条件编译来进行编辑
1 打开Common文件夹
2 新建两个文件debug.c和debug.h文件,用来调试
debug.h
新拓展:
format——格式化的占位符
…————定义可变的参数
strrchr——从字符串最右边往左边寻找目标字符,找到后返回目标字符的地址函数
##————连接前后可以用来省略,
__FILE__–文件路径及其
__LINE__–行号
#ifndef __DEBUG_H__
#define __DEBUG_H__
#include "usart1.h"
#include <stdarg.h>
#include <string.h>
//为了方便调试,定义一个宏进行条件编译
#define DEBUG
#ifdef DEBUG //调试Debug)模式
//从__FILE__中提取文件名
#define FLIENAME (strrchr(__FILE__, '\\') ? strrchr(__FILE__, '\\') + 1 : __FILE__)
#define dubug_init() Debug_Init()
#define debug_printf(format, ...) printf("[%s:%d]--" format, FLIENAME, __LINE__, ##__VA_ARGS__)
#define debug_printfln(format, ...) printf("[%s:%d]--" format "\r\n", FLIENAME, __LINE__, ##__VA_ARGS__)
#else //发布(Release)模式
#define dubug_init()
#define debug_printf(format,...)
#define debug_printfln(format,...)
#endif
//初始化
void Debug_Init(void);
#endif /* __DEBUG_H__ */
debug.c
#include "debug.h"
void Debug_Init(void){
Usart1_Init();
}
延迟模块:(公共层)
1 打开Common文件夹
2 用之前滴答定时器的两个文件delay.c 和delay.h 放入新建立的Common文件夹中
流程:系统滴答定时器中的装载值LOAD值是向下计数,VAL计数器值清零它会自动装载LOAD值进行向下计数,每过一个时钟VAL减1。配置采用系统时钟源
STM32 创建和书写delay.c 和delay.h_stm32 delay.c-CSDN博客
作者:雁过留声花欲落