【Let‘s do第四期】DIY液体流量检测仪
DIY液体流量检测仪
- 开箱
- 前言
- 实物
- 主控板
- 流量传感器
- 继电器模块
- 抽水泵
- 过程
- 点亮板载LED
- 移植OLED程序
- 接线
- 驱动模块
- 流量检测
- 裸机代码
- 使用FreeRTOS
- 任务说明
- 任务函数
- 头文件
- 成果
开箱
前言
非常有幸参加EEPW举办的【Let’s do第四期】DIY液体流量检测仪的活动。
我参加了一次免费的DIY活动,本次DIY通过小抽水泵和继电器实现流量控制,结合霍尔效应传感器,准确测量液体的传播时间,实现瞬时流量和累计流量的监测。该技术广泛运用于咖啡机、饮水机等定量取水场景~
实物
收到东西已经非常久了,今天才来发开箱贴,首先看一下我的全部配套的东西。
首先是两张集体照:
介绍一下我购买的物料清单:
- NUCLEO-64 STM32F103RB EVAL BRD核心板1块
- MINI SUBMERSIBLE WATER PUMP 抽水泵一个
- GRAVITY DIGITAL 5A RELAY MODULE 继电器模块一个
- EVAL BOARD FOR LM1117 降压模块一个
- SENSOR FLOW 0.3-6LPM ML G18" 流量传感器一个
主控板
主控板介绍:NUCLEO-64 STM32F103RB核心板采用的主控芯片是STM32F103RBT6,系统频率:72MHz,RAM:20KB、Flash:128KB,另外,核心板板载有ST-Link调试器,方便烧录程序开发调试。可支持Keil、IAR、STM32CubeIDE、AuduinoIDE等工具进行开发。图片如下
流量传感器
- YF-S401
- 感应范围:0.3 ~ 6 LPM
- 描述:5~12V供电,由塑料阀体 、水流转子组件和霍尔传感器组成
- 工作原理:当水通过水流转子组件时,磁性转子转动并且转速随着流量变化而变化,霍尔传感器输出相应脉冲信号,反馈给控制器,控制器通过触发的脉冲信号数获得水流量。
继电器模块
- 描述:3.3~5V供电,实测高电平导通
抽水泵
- 描述:5V供电,功率0.91W
- 防水等级:IP68
本次DIY活动可以学习到
- IIC 通讯协议的了解
- OLED 显示信息
- 硬件接线继电器控制抽水泵
- 软件环境搭建与硬件测试
- 流量计实现对液体的测量
过程
点亮板载LED
拿到新的开发板,第一个任务就是点亮板载LED,励志成为点灯大师!
板载自带了stlink,不需要额外的烧录器了,需要一根mini-B线连接电脑和开发板烧录程序。
使用CubeMX快速配置一下工程
然后设置工程路径和工程名称,这里我使用Keil-MDK开发
参照原理图
板载的LED是PA5引脚,配置引脚PA5为输出模式
点击Generate Code 生成工程。
在while(1)里写这两段代码即可成为点灯大师!
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // LED
Hal_Delay(500);
现象就是LED以1S为间隔闪烁一次。
移植OLED程序
我移植了之前用的程序,具体的代码会放在Gitee里,链接在文章最后
先来个效果展示
汉字显示:汉字取模步骤:软件PCtoLCD2002
- 模式:字符模式
- 配置取模方式:点击选项按钮或者齿轮按钮
点阵格式:阴码(正常色显示)阳码(反色显示)
取模方式:列行式
取模走向:逆向(低位在前)
输出数制:十六进制
自定义格式:C51格式,把行前缀的’{‘和行后缀的’}'删掉
点击确定,然后输入想取模的汉字,复制下面的十六进制代码到程序里即可
接线
接线其实是比较简单的,用电池或者充电头12V DC输入给降压模块,降压模块输出5V和3.3V
OLED供电3.3V,PC6 - SCL、PC8 - SDA
代码的宏定义如下所示:
#define OLED_PROT GPIOC
#define OLED_PIN_SCL GPIO_PIN_6
#define OLED_PIN_SDA GPIO_PIN_8
抽水泵是5V供电,尽量不用板载USB的5V,抽水泵的两根线,一根接到降压模块的GND,另一根接到继电器的COM口,然后再拿一根线连接继电器的NO和降压模块输出的5V
继电器的另一端有三根线,供电接3.3~5V,实测高电平导通,第三根是信号线,我接了PC10
- 继电器在不通电或者没导通情况下COM端默认打到NC即相通,继电器开关导通后COM和NO打通,COM和NC断开。
流量传感器接5V供电,信号线接PC2,使用外部中断检测脉冲
PC13是板载的按键,输入模式
对应的CubeMX引脚配置
驱动模块
接线成功后,先测试是否能驱动继电器
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_10); //继电器
Hal_Delay(1000); //延时时间尽量长,不要频繁开关继电器
在while(1)里测试这段代码,如果继电器1S吸合,1S断开,那就是驱动成功了!
检测外部中断的脉冲信号 - 检测流量传感器的数据
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_2)
{
flow_cnt ++;
}
}
流量检测
流量检测的思路
累积流量
:用按键控制继电器的开关,在OLED上显示脉冲计数,实际测试时候,打开继电器,抽水1000ml,得到一个计数值x1,多次测量x2,x3,取x1,x2,x3的平均值x,x/1000 = 每毫升水的计数值,这个数值可以定义成一个宏,供电电压不同,这个数值不同,数值需要自己实测!!!
#define CNT_Flow_1ML 355.357f
瞬时流量
:我取的20ms的计数值,计数值*50 = 1S的计数值,然后用这个值除以CNT_Flow_1ML,得到1S的流量,单位是ml/S
裸机代码
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2025 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "tim.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "OLED.h"
#include "myoldkey.h"
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
uint32_t OLED_Tick;
uint32_t Func_Tick;
uint32_t flow_cnt;
uint32_t key_tick;
float Moment_Flow = 1.2f; // 瞬间流量
float Cumulative_Flow = 0.0f; // 累计流量
int8_t textnum;
uint16_t TargetFlow = 0; //目标水量 - 100ML
uint32_t Target_CNT = 0;
void OLED_Show_Func(void);
void JDQ_Func(void);
void key_proc(void);
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
// CNT:164157 --- 650ML
#define CNT_Flow_1ML 355.357f
//#define CNT_Flow_1ML 533.295f
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_TIM3_Init();
/* USER CODE BEGIN 2 */
OLED_Init();
OLED_Clear();
OLED_ShowChinese(8*6, 0, "得捷");
OLED_Printf(0, 16, OLED_8X16, " Waiting... ");
OLED_Update();
HAL_Delay(1800);
OLED_Clear();
HAL_TIM_Base_Start_IT(&htim3);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_10, GPIO_PIN_RESET); // 继电器 off
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
OLED_Show_Func();
// HAL_GPIO_WritePin(GPIOC, GPIO_PIN_10, GPIO_PIN_SET); // 继电器 on
// HAL_GPIO_WritePin(GPIOC, GPIO_PIN_10, GPIO_PIN_RESET); // 继电器 off
JDQ_Func();
key_proc();
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
}
/* USER CODE BEGIN 4 */
void OLED_Show_Func(void)
{
if (uwTick - OLED_Tick <= 200)
return;
OLED_Tick = uwTick;
OLED_Clear();
char tempStr[20];
OLED_ShowChinese(8 * 2, 16 * 0, "瞬间流量");
OLED_ShowChar(8 * 10, 16 * 0 + 8, ':', OLED_6X8); // :
// sprintf(tempStr, "%d %d ", flow_cnt, TargetFlow);
sprintf(tempStr, " %.1f mL/s %d ", Moment_Flow, TargetFlow);
OLED_Printf(8 * 4, 16 * 1 + 5, OLED_6X8, tempStr);
OLED_ShowChinese(8 * 2, 16 * 2, "累计流量");
OLED_ShowChar(8 * 10, 16 * 2 + 8, ':', OLED_6X8); // 显示:
Cumulative_Flow = (float)flow_cnt / CNT_Flow_1ML;
sprintf(tempStr, " %.2f mL", Cumulative_Flow);
OLED_Printf(8 * 4, 16 * 3 + 5, OLED_6X8, tempStr);
OLED_Update();
}
void JDQ_Func(void)
{
static uint32_t Last_CNT; //上次计数
static uint32_t error_Flow; //前后两次的流量差
if (uwTick - Func_Tick <= 20) //如果要修改这个20ms,就需要重新计算瞬间流量
return;
Func_Tick = uwTick;
Target_CNT = TargetFlow*CNT_Flow_1ML;
if (flow_cnt <= TargetFlow*CNT_Flow_1ML) // 当前小于目标,未到,开继电器
{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_10, GPIO_PIN_SET); // 继电器 on
}
else if (flow_cnt >= TargetFlow*CNT_Flow_1ML) //如果当前计数值 >= 目标计数值,关闭继电器
{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_10, GPIO_PIN_RESET); // 继电器 off
}
error_Flow = flow_cnt - Last_CNT; //前后两次的流量差 20ms
Moment_Flow = (float)(error_Flow * (1000/20)) / CNT_Flow_1ML; //计算瞬间流量 // error_Flow * (1000/20) === 20ms的流量*50, 等于1S的流量
Last_CNT = flow_cnt;
}
void key_proc(void)
{
if (uwTick - key_tick <= 10)
return;
key_tick = uwTick;
if (Key[1].short_flag == 1)
{
Key[1].short_flag = 0;
TargetFlow += 100; //目标值+=100
// HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_10); //继电器
// HAL_GPIO_WritePin(GPIOC, GPIO_PIN_10, GPIO_PIN_SET); // 继电器 on
}
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_2)
{
flow_cnt ++;
}
}
/* 定时器回调函数 1ms */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
static uint32_t tim3_cnt;
static uint8_t keyscan_cnt;
if (htim->Instance == TIM3)
{
if (tim3_cnt ++ >= 500)
{
// LED T
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // LED
tim3_cnt = 0;
}
if (keyscan_cnt ++ >= 20)
{
key_serv_long();
keyscan_cnt = 0;
}
}
}
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
使用FreeRTOS
任务说明
一共创建了四个任务,CubeMX创建的默认任务,用来当做LED指示灯,代表系统正在运行中,系统卡死则LED不再闪烁。
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
/* Infinite loop */
for(;;)
{
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // LED
vTaskDelay(500);
}
/* USER CODE END StartDefaultTask */
}
手动创建按键任务,继电器任务,OLED任务
TaskHandle_t xTaskOLED_Handle; // OLED显示任务句柄
TaskHandle_t xTaskKey_Handle; // 按键任务句柄
TaskHandle_t xTaskRelay_Handle; // 继电器任务句柄
BaseType_t xRelay_Handle = xTaskCreate(Relay_Task, "Relay_Task", 128, NULL, (UBaseType_t)osPriorityNormal, &xTaskRelay_Handle);
BaseType_t xKey_Handle = xTaskCreate(KEY_Task, "KEY_Task", 128, NULL, (UBaseType_t)osPriorityNormal + 1, &xTaskKey_Handle);
BaseType_t xOLED_Handle = xTaskCreate(OLED_Show_Func, "OLED_Task", 256, NULL, (UBaseType_t)osPriorityNormal + 2, &xTaskOLED_Handle);
创建一个软件定时器,10ms扫描一次按键,刷新按键任务的键值
xTimerKey = xTimerCreate(
"KeyScanTimer", // 定时器名称
(10), // 定时器周期,10 ms
pdTRUE, // 自动重装
(void *)0, // 定时器 ID,这里没有特殊用途
(TimerCallbackFunction_t)vScanKeyCallback // 定时器到期时调用的函数
);
任务函数
完整代码可以见最下方的Gitee链接
#include "TaskFunction.h"
TimerHandle_t xTimerKey;
uint32_t flow_cnt = 0;
uint8_t OLED_View_Flag = 0; //OLED界面切换FLAG
float Moment_Flow = 0.0f; // 瞬间流量
float Cumulative_Flow = 0.0f; // 累计流量
uint16_t TargetFlow = 0; //目标水量 - 100ML
uint32_t Target_CNT = 0;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_2)
{
flow_cnt ++;
}
}
// 软件定时器-扫描按键-回调函数
void vScanKeyCallback(TimerHandle_t xTimer)
{
// 定时 10ms获取一次按键键值
// static unsigned char led;
key_serv_double();
// 测试软件定时器是否正常工作
// if (++led >= 100) // 1S
// {
// HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // LED
// led = 0;
// }
}
void OLED_Show_Func(void *params)
{
OLED_Init();
OLED_Clear();
OLED_ShowChinese(8*6, 0, "得捷");
OLED_Printf(0, 16, OLED_8X16, " Waiting... ");
OLED_Update();
vTaskDelay(1800);
OLED_Clear();
for(;;)
{
char tempStr[20];
if (OLED_View_Flag == 0)
{
OLED_ShowChinese(8 * 2, 16 * 0, "瞬间流量");
OLED_ShowChar(8 * 10, 16 * 0 + 8, ':', OLED_6X8); // 显示:
// sprintf(tempStr, "%d %d ", flow_cnt, TargetFlow);
sprintf(tempStr, " %.1f mL/s %d ", Moment_Flow, TargetFlow);
OLED_Printf(8 * 4, 16 * 1 + 5, OLED_6X8, tempStr);
OLED_ShowChinese(8 * 2, 16 * 2, "累计流量");
OLED_ShowChar(8 * 10, 16 * 2 + 8, ':', OLED_6X8); // 显示:
Cumulative_Flow = (float)flow_cnt / CNT_Flow_1ML;
sprintf(tempStr, " %.2f mL", Cumulative_Flow);
OLED_Printf(8 * 4, 16 * 3 + 5, OLED_6X8, tempStr);
}
else
{
OLED_ShowChinese(8*6, 16*1, "得捷");
sprintf(tempStr, "Target:%d ", TargetFlow);
OLED_Printf(8 * 4, 16 * 2 + 5, OLED_6X8, tempStr);
}
OLED_Update();
vTaskDelay(100);
}
}
void KEY_Task(void *argument)
{
xTimerStart(xTimerKey, 0);
KEY_GPIO_Init(); // 按键引脚的初始化
for(;;)
{
if (Key[1].short_flag == 1)
{
TargetFlow += 100; //目标值+=100
Key[1].short_flag = 0;
}
if (Key[1].long_flag == 1)
{
TargetFlow += 10;
// Key[1].long_flag = 0;
}
if (Key[1].double_flag == 1)
{
OLED_Clear();
OLED_View_Flag++; // 界面的切换
if (OLED_View_Flag >= 2) // 2个界面
OLED_View_Flag = 0;
Key[1].double_flag = 0;
}
vTaskDelay(100);
}
}
void Relay_Task(void *argument)
{
static uint32_t Last_CNT; //上次计数
static uint32_t error_Flow; //前后两次的流量差
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_10, GPIO_PIN_RESET); // 默认 继电器 off
// HAL_GPIO_WritePin(GPIOC, GPIO_PIN_10, GPIO_PIN_SET); // 继电器 on
for(;;)
{
Target_CNT = TargetFlow*CNT_Flow_1ML;
if (flow_cnt <= TargetFlow*CNT_Flow_1ML) // 当前小于目标,未到,开继电器
{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_10, GPIO_PIN_SET); // 继电器 on
}
else if (flow_cnt >= TargetFlow*CNT_Flow_1ML) //如果当前计数值 >= 目标计数值,关闭继电器
{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_10, GPIO_PIN_RESET); // 继电器 off
}
error_Flow = flow_cnt - Last_CNT; //前后两次的流量差 20ms
Moment_Flow = (float)(error_Flow * (1000/20)) / CNT_Flow_1ML; //计算瞬间流量 // error_Flow * (1000/20) === 20ms的流量*50, 等于1S的流量
Last_CNT = flow_cnt;
vTaskDelay(20); // 不能修改这个20ms,需要和计算公式同步修改
}
}
头文件
#ifndef __TASKFUNCTION_H
#define __TASKFUNCTION_H
#include "stm32f1xx_hal.h"
#include "OLED.h"
#include "myoldkey.h"
#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"
#define CNT_Flow_1ML 355.357f
extern TimerHandle_t xTimerKey;
void vScanKeyCallback(TimerHandle_t xTimer); // 软件定时器-扫描按键-回调函数
void OLED_Show_Func(void *params); // OLED显示任务
void KEY_Task(void *params); // 按键任务
void Relay_Task(void *argument); // 继电器任务
#endif
成果
- 演示视频:
- 代码: