站内搜索
发作品签到
WaterManager
专业版

WaterManager

542
0
0
6

简介

无线水塔水量管理系统,实时采集远端水塔剩余水量,通过无线传输到本地显示和控制水泵

简介:无线水塔水量管理系统,实时采集远端水塔剩余水量,通过无线传输到本地显示和控制水泵
星火计划2025
复刻成本:174.29

开源协议

LGPL 3.0

创建时间:2025-09-22 14:32:59更新时间:2025-10-23 10:58:01

描述

演示视频

bilibili:https://www.bilibili.com/video/BV13cW1zXEVh/

开源链接

铝合金外壳:https://gf.jlcfa.com/machine-detail/492509687445581826?source=myDrawing
软件代码:https://gitee.com/mcx1144/water-mgr.git

项目简介

本项目是基于STM32F103单片机的水塔水量管理系统,系统采用分体式设计。TX端负责采集水量信息,通过无线协议发送给RX端。RX端接收到信息后,控制水泵的启停,并实时显示当前水量信息和水泵状态。

系统方案

  • 系统框图
    image.png

MCU使用的是STM32F103RET6,64个引脚,完全够用,当然也有其他便宜的芯片,但习惯了用STM32的芯片,初始化配置直接用STM32CubeMX生成,大大降低了软件的开发难度。

1.TX端设计方案

1.1水量采集方案

水量信息采集,主要是获取当前水塔中水位的高度,来等比例的换算成剩余水量。研究对比了不同的采集方案,主要有以下几种:

前两种价格高昂,适合工业用的范畴。第三种量程最高只有200mm,虽然可以定制,但价格肯定要高,也不太适合。第四种需要在水箱底部开孔,且需要配套的信号解码板。
我的宗旨是在满足需求的前提下,要尽可能便宜,这也是我们搞DIY的追求。然后我想到水位变化时液面的变化不就是一个线性的位置变化么,那我在液面放一个磁铁,用干簧管检测磁铁的位置不就好了。要想获取到不同位置的结果,就需要布置多个干簧管,如果都直接接入到PCB上,那么线会非常多,所以这里参考了ADC按键的思路:

一般水塔直径比较大,浮子会在水面随机漂移,就无法触发干簧管,我的方案是在出水口加一个三通,并立一根PVC或PPR的水管,做成连通器,将浮子放在水管里面,干簧管贴在管外壁。
这个方案虽然不能得到精细的连续的液位变化,但能够获取分段的结果,想要得到更精细的结果,只要增加干簧管的数量即可。
这个方案的成本可谓是非常的低,主要的成本就是干簧管。缺点是在水位介于两个干簧管之间时是不无法直接获取水位的,必需记录之前的状态。比如设备异常重启后就需要等水位来到检测点,在抽水时可能很快,但在用水时,可能需要很久。
然后我又研究了一番,找到了这种液位传感器,输出是开漏形式的,可以代替干簧管的位置。液位之下的始终导通,这样任何时刻都可以立即计算出水位,nice。

这种传感器应该是与电容式触摸按键是一样的原理,只不过是做了小型化,并且感应面是柔性材料,可以适应不同曲率的容器。这个方案成本会高一些,但优点是能随时获取到水量。

1.2电源方案

因为TX端是布置在楼顶水塔边上,环境较为恶劣,且没有交流电的供应,所以这里考虑使用太阳能进行供电。

常规方案是通过太阳能降压充电的芯片来为电充电,再通过一个降压芯片来稳3.3V。但用下来电池很容易鼓包,怀疑是边充电边放电会导致电池过充。
所以这个版本改用了带电源路径管理的降压充电芯片BQ25622,保证电池不会过充过放。

1.3 射频通信方案

早期预研的时候使用的是RF2401系列的模组,但发现通信距离不行,即使加了PA的模组也只能穿一堵墙。而水塔往往在2楼或3楼楼顶,而水泵在一楼地面,需要穿3-4堵墙。主要问题是RF2401使用的是2.4G的频率,并且发射功率只有10mW。理论知识告诉我,无线电的特性就是频率越低,穿透性越好。然后我就往433M的方向去找,让我发现了si4463系列的模组,就是这款号称通信距离可以到2km的,实测的确很强,隔5层楼都可以正常通信。

2.RX端设计方案

2.1电源方案

RX端是接收水量信息,然后控制水泵的启停。我家水泵是早就已经装好的,用的是86型的开关手动控制的,现在做的是取代这个手动开关。因为开关边是没有预留插座的,所以我是设计的直接从电线上取电的,而且控制水泵就是要通过继电器控制220V的电,故这里使用的是一个成品的AC-DC隔离电源模块,直接安装在PCB上

2.2水泵控制方案

水泵使用的是220V交流电,而MCU是3.3V低压直流电,自然是需要使用继电器来控制。但水泵是电机类的设备,启停电流很大,对继电器的触点冲击较大,会影响继电器的使用寿命。我设计的是再增加一个交流接触器,来间接控制。(如果使用更大负载电流的继电器,应该也可以直接控制)

2.3 显示方案

主要是显示水量信息和水泵工作状态,外加电池的充放电状态和温度,信息内容并不多。因为家里主要是父母在,当然要显示大一点的字体,但一块3.5寸的TFT液晶还是比较贵的。退而求其次,我选用的是数码管,一个大尺寸的数码管才一两块。


硬件原理图解析

1.TX端硬件

1.1电源部分

太阳能降压充电选用的是TI的BQ25622芯片,其使用开关降压充电的方案,效率更高,最大开关3.5A,并具有电源路径管理,可以通过IIC调节充电参数,可以保证电池不出现过充过放。手册链接:https://www.ti.com.cn/product/cn/BQ25622
也可以使用BQ25622E芯片,Pin2Pin的,最大电流3A,砍掉了OTG功能。对我来说没区别


二次降压芯片选用的是TI的TLV62568,同步降压芯片,效率更高


输入保护电路,避免输入电压过高损坏电源芯片。太阳能板的特性是开路电压比最大功率电压要高很多,而一般标参数的时候都是写最大功率电压。BQ25622最大工作电压推荐为18V,绝对最大值为26V,这个芯片还不便宜,小心点为妙。
设计的这个电路会限制输入电压,超过18V时会关闭三极管Q1,保护后面的电路,

1.2水量采集部分

因为需要把水位传感器串起来,故设计了一个传感器小板,将传感器接在右侧H1上,再使用3P的连接线将所有的传感器小板首位串接起来,这样就可以适配不同高度的水塔


因为传感器之间是使用电阻串联结构,按照正常的电阻分压结构,每个节点的电压是一个非线性的关系,对软件设计和噪声容忍度不太好。所以这里使用了恒流二极管,产生0.3mA的电流,这样每个节点的电压就能近似于线性的关系。
D13的电流注入传感器链路,D14下面接一个固定的电阻,用来校准温度漂移,因为半导体对温度都比较敏感。

1.3 射频通信部分

SI4463模组与MCU使用SPI总线通信,按照手册进行连接就行,注意模组的GPIO0/GPIO1需要连到MCU的GPIO上,软件上需要使用的。

1.4其他部分

这个是带灯的按钮,用来在清洗水塔时暂停水泵的控制


预留的拨码开关,可以用来配置某些功能参数或开关


MCU部分就不展开了,按照demo画就行


2.RX端硬件

2.1电源部分

HLK-5M24是成品的AC-DC模块,输出5V1A,供继电器和数码管等工作在5V的器件使用
使用AMS1117降压3.3V,供MCU和射频部分使用。
因为是要装外壳,开关是要装在外壳上的,两个插片用来安装电源开关

2.2显示部分

使用MAX7219芯片来驱动数码管,两位数码管显示水量信息,6颗LED用来指示水泵和电池状态


为了能够在晚上和无人的时候关闭LED,降低功耗,配置了光敏电阻和人体存在传感器。可以根据环境光强度来调节LED亮度。光敏电阻也可以换成光敏三极管

2.3水泵控制系统

因为后面会加交流接触器,注意这里继电器的选择比较宽松,小体积的也够用了

2.4射频通信部分

同TX部分,略


软件代码

软件设计围绕射频模组的状态机来设计的
TX端核心代码:

switch (eState)
    {
        case FSM_CHIP_INIT:
        {
            if (Si446xConfig())
            {
                u32InitFail++;
                HAL_Delay(1000);

                // Reset BQ25622 WatchDog
                u8RegWDRst = 0xA7;//WD_RST
                HAL_I2C_Mem_Write(&hi2c1, u8DevAddr << 1, 0x16, I2C_MEMADD_SIZE_8BIT, &u8RegWDRst  , 1, 200);
            }
            else
            {
                u32InitFail = 0;
                loss_pkt = 0;
                eState = FSM_UPDATE_COLLECT;
            }
            break;
        }
        case FSM_UPDATE_COLLECT:
        {
            // Reset BQ25622 WatchDog
            u8RegWDRst = 0xA7;//WD_RST
            HAL_I2C_Mem_Write(&hi2c1, u8DevAddr << 1, 0x16, I2C_MEMADD_SIZE_8BIT, &u8RegWDRst  , 1, 200);

            // Start BQ25622 ADC
            HAL_I2C_Mem_Write(&hi2c1, u8DevAddr << 1, 0x26, I2C_MEMADD_SIZE_8BIT, u8BQInit + 30, 2, 200);

            u32WDRstDiff = HAL_GetTick() - u32WDRstTick;
            // Start&Read STM32 ADC
            HAL_ADC_Start(&hadc1);     //启动ADC1
            HAL_ADC_PollForConversion(&hadc1, 100);//
            if (HAL_IS_BIT_SET(HAL_ADC_GetState(&hadc1), HAL_ADC_STATE_REG_EOC))
            {
                u16AdcVal = HAL_ADC_GetValue(&hadc1);
            }
            HAL_ADC_Stop(&hadc2);
            HAL_ADC_Start(&hadc2);     //启动ADC2
            HAL_ADC_PollForConversion(&hadc2, 100);//
            if (HAL_IS_BIT_SET(HAL_ADC_GetState(&hadc2), HAL_ADC_STATE_REG_EOC))
            {
                u16AdcRef = HAL_ADC_GetValue(&hadc2);
            }
            HAL_ADC_Stop(&hadc2);

            // Read BQ25622 ADC
            HAL_I2C_Mem_Read(&hi2c1, u8DevAddr << 1, 0x28, I2C_MEMADD_SIZE_8BIT, u8AdcData    , 10, 2000);

            // Read BQ25622 Charger Status
            HAL_I2C_Mem_Read(&hi2c1, u8DevAddr << 1, 0x1d, I2C_MEMADD_SIZE_8BIT, u8RegData,  6, 2000);
            if(u8RegData[3]&0x01)
            {
                HAL_I2C_Mem_Write(&hi2c1, u8DevAddr<<1, 0x02, I2C_MEMADD_SIZE_8BIT, u8BQInit, 20, 2000);
            }

            u32WDRstTick = HAL_GetTick();
            UpdateWaterLevel();
            UpdateBattery();
            u8TxSeq++;
            u32TimeOutCnt = 0;
            eState = FSM_SEND_PACKET;
            break;
        }
        case FSM_SEND_PACKET:  //????
        {
            //tx_conter++;
            //HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
            //HAL_Delay(50);
            if (Si446xSendWaterLevel())
            {
                eState = FSM_CHIP_INIT;
                break;
            }
            //HAL_Delay(100);
            eState = FSM_WAIT_RESPONSE;
            SI446x_Start_RX(0, 0, PACKET_LENGTH, 0, 3, 3);
            u32SendTick = HAL_GetTick();
            break;
        }
        case FSM_SEND_CONFIRM:  //????
        {
            //if (Si446xSendConfirmAck())
            //{
            //    eState = FSM_CHIP_INIT;
            //    break;
            //}
            eState = FSM_SLEEP;
            SI446x_Start_RX(0, 0, PACKET_LENGTH, 0, 3, 3);
            break;
        }
        case FSM_WAIT_RESPONSE: //????
        {
            if ((HAL_GetTick() - u32SendTick) > 500 * (u32TimeOutCnt + 1))
            {//??1s
                u32TimeOutCnt++;
                if (u32TimeOutCnt <= 2)
                {
                    //????
                    eState = FSM_SEND_PACKET;
                }
                else
                {   //??????
                    eState = FSM_SLEEP;
                    loss_pkt++;
                    u32TimeOutCnt = 0;

                    if (0 == loss_pkt % 30) {
                        eState = FSM_CHIP_INIT;
                        break;
                    }
                }
            }
            if (!Si446xPacketRecv())
            {
                u32TimeOutCnt = 0;
                loss_pkt = 0;
                eState = FSM_SEND_CONFIRM;
                break;
            }
            HAL_Delay(20);
            break;
        }
        case FSM_SLEEP: //???????
        {
            SI446x_Change_State(STATE_SLEEP);   //ready??,1.8mA
            if(u8WaterRising)
            {
                HAL_Delay(1000);
            }
            else
            {
                HAL_Delay(5000);
                //RTC_AlarmStart();
                //CLEAR_BIT(SysTick->CTRL, SysTick_CTRL_ENABLE_Msk); //暂停systick
                //__HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);  //清理唤醒标志 防止立刻唤醒
                //HAL_PWR_EnterSTOPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFE);
                //SET_BIT(SysTick->CTRL, SysTick_CTRL_ENABLE_Msk); //打开systick
            }
            eState = FSM_UPDATE_COLLECT;
            break;
        }
        default:
            eState = FSM_UPDATE_COLLECT;
            break;
    }

TX端状态机:

TX端软件状态机迁移图。上电后进行芯片的初始化,然后进入状态机循环,每5s更新一次状态信息,然后发送给TX并等待响应。因为模块有概率会异常,所以增加了超时重置芯片的逻辑。

RX端核心代码:

    {
        case FSM_RECV_PKT:
        {
            if (!Si446xRecvPacket())
            {
                eState = FSM_SEND_REPLY;
                Si446xRecvPaser();
                if (u8WaterLevel == 100)
                    u8WaterLevel = 99;
                //dump_WORD(u8RxSeq);
                u32LastRecvTime = HAL_GetTick();
                u8LinkFail = 0;
                u32PktLoss = 0;
            }
            if (HAL_GetTick() - u32LastRecvTime > 30000)
            {
                eState = FSM_CHIP_INIT;
                //printf("recv time out\r\n");
                u32LastRecvTime = HAL_GetTick();
                u32PktLoss++;
                if(u32PktLoss >= 2)
                {
                    u8LinkFail = 1;
                }
            }
            break;
        }
        case FSM_SEND_REPLY:    //????
        {
            SI446x_Change_State(STATE_TX_TUNE);
            if (Si446xWaitState(STATE_TX_TUNE))
            {
                eState = FSM_CHIP_INIT;
                break;
            }
            HAL_Delay(10);
            if (Si446xSendReply())
            {
                eState = FSM_CHIP_INIT;
                break;
            }
            eState = FSM_RECV_PKT;
            u32TxPktCnt++;
            //LED_Toggle;
            HAL_UART_Transmit(&huart1, (uint8_t*)"send done\r\n", 12, 100);
            break;
        }
        case FSM_CHIP_INIT:
        {
            if (Si446xConfig())
            {
                u8InitFail++;
                HAL_UART_Transmit(&huart1, (uint8_t*)"RF module init failed\r\n", 24, 200);
                if (u8InitFail >= 10)
                {
                    HAL_UART_Transmit(&huart1, (uint8_t*)"module may be not connected or damaged\r\n", 40, 200);
                    HAL_Delay(500);
                }
                eState = FSM_CHIP_INIT;
            }
            else
            {
                u8InitFail = 0;
                HAL_UART_Transmit(&huart1, (uint8_t*)"Si4463 init success\r\n", 24, 200);
                eState = FSM_RECV_PKT;
                u32LastRecvTime = HAL_GetTick();
            }
            break;
        }
        default:
        {
            eState = FSM_RECV_PKT;
            break;
        }
    }

RX端状态机:

RX端软件状态机迁移图。RX端idle下保持接收状态,等待接收数据包,收到状态信息后回复响应包,用来给TX端确认空中链路正常。如果长时间接收不到TX的信号,会重新初始化模组并再次等待接收。


BQ25622可以在默认参数下运行,不需要访问IIC。但我这个做了一些参数的调整,包括最大充电电流,电池充电截止电压等,目的是延长电池寿命(理论上有作用,实际效果有待检验)。可以根据实际需要进行调整,修改下表的数值就行
image.png
需要注意的是,通过IIC配置了参数,就需要定期通过IIC复位看门狗,不然内部的看门狗会溢出复位寄存器,并在复位后将设置的充电电流值减半,最终会导致充电电流值有几个mA

其他代码细节请自行上gitee查看,WaterMgr

注意事项

  • 恒流二极管的基准值可能需要校准
  • 光敏电阻或光敏三极管在不同亮度下的阈值需要自行校准
  • 设备安装涉及220V交流电,请在专业人士的指导下进行。免责申明:请自行评估风险,本人不对任何后果负责

组装流程

1.TX端安装
先安装侧面板


分别为DC-022B太阳能输入接口、F3充电指示灯、16mm按钮、12mm 3芯航空插头、IPEX转SMA天线接口
螺丝要缩紧,不然安装另外一端插头的时候会跟着转圈

然后安装连接线

按钮4根线,对角的2根为一组。一组是按钮的线,另一组是灯的线,先测一下两组分别是什么,正常按钮和灯都是不分正负极的,跟PCB的插座线序对应就行.
充电指示灯,正极接PCB上V+的焊盘,负极接ST的焊盘。
电源插头,先测一下正负极,然后焊接好就行。

另一侧主要是航空插头,3芯分别是电源/地/信号。但PCB的插座是4P的,中间两条线靠近地的那条不用接,剪掉或者拆掉都行。连接线与航空插头的对应关系没有什么特定顺序,只要与航空插头的另一头能对应就行。

焊接好连接线后,对应的连接好PCB端,插上电池和温度探头,上电测试,检查功能是否完好。

电池后面会换一块大的,粘在上盖板上,温度传感器也要贴在电池表面

2.RX端安装

先在外壳上合适的位置分别按照控制按钮、电源开关、SMA天线接头的开孔尺寸开孔

然后固定PCB,因为这个外壳是买的,固定孔不太合适,需要在底下垫起来或者另外开孔进行固定。
然后安装保险丝,并将开关、按钮、天线等装到外壳上,并通过线缆连接到PCB上
右下角的是要接220V交流电的,这里就不展示了,按照标注接线就行。调试的时候先不接交流电,可以使用左边的TYPE-C进行供电

实物图

RX端组装好之后的样子


TX端组装好之后的样子


系统演示

更新说明

  • 2025/10/23 上传程序源码

设计图

未生成预览图,请在编辑器重新保存一次

BOM

暂无BOM

3D模型

序号文件名称下载次数
暂无数据

附件

序号文件名称下载次数
1
WaterManager.zip
19
克隆工程
添加到专辑
0
0
分享
侵权投诉

工程成员

知识产权声明&复刻说明

本项目为开源硬件项目,其相关的知识产权归创作者所有。创作者在本平台上传该硬件项目仅供平台用户用于学习交流及研究,不包括任何商业性使用,请勿用于商业售卖或其他盈利性的用途;如您认为本项目涉嫌侵犯了您的相关权益,请点击上方“侵权投诉”按钮,我们将按照嘉立创《侵权投诉与申诉规则》进行处理。

请在进行项目复刻时自行验证电路的可行性,并自行辨别该项目是否对您适用。您对复刻项目的任何后果负责,无论何种情况,本平台将不对您在复刻项目时,遇到的任何因开源项目电路设计问题所导致的直接、间接等损害负责。

底部导航