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博客

作者:雁过留声花欲落

物联沃分享整理
物联沃-IOTWORD物联网 » STM32水质水位精准检测方案详解

发表回复