【STM32】基于STM32F103C8T6的水质检测系统设计(声光报警、多级菜单)

需求

1.检测参数:水温、TDS、浊度、PH
2.超出阈值声光报警
3.LCD显示目标参数的测量结果
4.测量模式:单参数测量、所有参数表同时测量
切换方式:按键切换

原理

单总线技术

单总线技术采用单根信号线实现时钟、数据的传输,且数据的传输是双向的,能够控制一个或多个从机设备。主机发送复位脉冲、从机响应应答脉冲即为单总线的初始化过程。主机检测到从机的应答脉冲后,发出ROM命令。

单总线的初始化时序

dan在这里插入图片描述

主机通过拉低总线至少480us以产生复位脉冲,之后主机释放总线,进入接收模式,4.7K上拉电阻将总线拉高。从机DS18B20等单总线器件检测到上升沿后,等待1560us,接着拉低总线60240us以产生从机应答脉冲。该应答脉冲向主机表明它处于总线上,且准备就绪工作。

主机读/写时隙的时序

每个时隙只能传输一位数据。

所有写时隙至少需要60us,两次独立的写时隙之间至少需要1us的恢复时间。主机拉低总线后,写时隙开始,在15us内释放总线,4.7K上拉电阻将总线拉至高电平,则为写1时隙;保持60us以上低电平则为写0时隙。

同写时隙一样,所有读时隙至少需要60us,两次独立的读时隙之间至少需要1us的恢复时间。每个读时隙都由主机发起,至少拉低总线1us。从机发送1时,总线保持高电平;从机发送0时,拉低总线,且在该时隙结束后释放总线,由4.7K上拉电阻将总线拉回至空闲高电平状态。从机发送的数据在起始时隙之后,保持15us有效时间,因此,主机在读时隙期间必须释放总线,并且在时隙起始后的15us内采样总线状态。

PH复合电极

设计选用的复合电极是通过电位法测量目标水体中的氢离子的浓度来确定PH值的。参比电极和指示电极是复合电极的两个组成部分,外管为参比电极,内管为指示电极。本设计选用的参比电极由Ag和AgCl组成,指示电极为玻璃电极。电极接触到目标水体后,溶液中的氢离子与敏感玻璃膜发生离子交换反应,产生膜电位。

浊度透射法

通过吸光率检测浊度的大小,单位NTU。

系统设计框图

硬件

主控MCU:STM32F103C8T6

电源模块:2串 3.7V 锂电池降压至 5V 和 3.3V
MCU主控板使用5V和3.3V供电,DS18B20、TDS传感器模块、有源蜂鸣器模块和LCD显示屏采用3.3V供电,浊度和PH传感器模块则采用5V供电。

传感器:
DS18B20 —— 水温

TWS-30 —— 浊度


左边数字信号输出电路,右边模拟信号输出电路,仅使用右边部分。信号经过R6、R3电阻后通过MCP6002T-I/SN运算放大器组成的跟随器,最后RC滤波输出,经分压电路分压后与MCU的PA2引脚相连。

E-201-C —— PH

模块使用5V电源供电,3个104规格的电容滤波,并通过LED2指示模块的工作状态。J3选用BNC(Q9)接口作值为PH电极信号的输入,输出则分为两路输出:上模拟信号输出,输出电压最大值5V;下数字信号输出。仅使用模拟信号输出部分电路。
参比电极的电压固定,为参考电压,即PH-信号电压;玻璃电极的电压随PH值的变化而变化,即PH+信号电压。TLC4502芯片是双通道运算放大器,PH+信号通过其中一路放大器2倍放大后,一路通过排针经分压电路分压后与MCU的PA1引脚相连;另一路则作为数字信号处理部分的输入,后经LM358运算放大器比较后输出高低电平, 阈值(高低电平临界值)通过调节R23电位器设置。TLC4502芯片的另外一路放大器则作为PH-信号的跟随器使用。

TDS

语音报警:基于 PNP 型三极管的开关电路驱动有源蜂鸣器
3.3V供电,低电平触发

分压电路

PH传感器模块和浊度传感器模块必需。由于单片机ADC无法采集5V电压,在扩展板上增加了1K和2K电阻分压电路,使得ADC输入电压范围在0~3.3V之间。

软件

!!!注释不一定对,懒得改了!!!

总体流程图

系统的多级菜单程序采用3级结构:0级菜单、1级菜单和2级菜单。main函数首先调用各种初始化函数,然后显示0级菜单,之后通过变量Menu_Change顺序进行菜单级数的判断,进行对应的函数处理,最后循环菜单级数的判断。

main函数设计

#include "speaker.h"
#include "sys.h"
#include "led.h"
#include "delay.h"
#include "key.h"
#include "ds18b20.h"
#include "adc.h"
#include "usart.h"
#include "alarm.h"
#include "menu.h"
#include "lcd.h"
#include "Dis_Picture.h" 
#include "Text.h"	
#include "GBK_LibDrive.h"	


//STM32F103核心板例程--1.8寸液晶屏驱动例程
//库函数版本例程
//字号12,1个字符步进6
 	
 int main(void)
 {	 
	extern uint8_t List_Number;
	
	delay_init();	    	     //延时函数初始化	  
	NVIC_Configuration(); 	 //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
	uart_init(115200);	     //串口初始化为115200
 	LED_Init();			         //LED端口初始化 
	Speaker_Init(); 
	LCD_Init();           //初始化LCD SPI 接口
	GBK_Lib_Init();       //硬件GBK字库初始化--(如果使用不带字库的液晶屏版本,此处可以屏蔽,不做字库初始化)
	Adc_Init();
	KEY_Init();
	
	 
	LCD_Clear(WHITE); //清屏 
	LCD_Fill(0,0,LCD_X,LCD_H,WHITE);
	Display_Initial_Menu(10, 3, BLUE, 15);		//注册一个0级菜单,防止刚开机不显示东东西
	LCD_ShowxNum(110, 140, List_Number, 1, 12, BLUE, 1);

	
	while(1)
	{  	
		Menu_Display_Control();		
	}	
}

LCD显示

系统采用的1.8寸TFT-LCD使用SPI协议与单片机之间进行通信。横向点阵数量为128,纵向点阵数量160,每个12号字体的字符占6个像素点宽度。

LCD显示程序设计如下:首先进行GPIO口设置并使能时钟,其中PB13、PB14、PB15设置为推挽复用输出。然后使能SPI2端口时钟,注意SPI2端口是挂载在APB1总线上的。接着初始化SPI2并进行SPI2的具体设置,使能SPI2,发送0xff指令启动传输,最后进行其余GPIO口的配置以及LCD功能函数的设置。

void SPI2_Init(void)
{	 
	GPIO_InitTypeDef  GPIO_InitStructure;
	SPI_InitTypeDef  SPI_InitStructure;
	
	RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB ,ENABLE);
	
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//推挽复用输出
	GPIO_Init(GPIOB, &GPIO_InitStructure);
			
	//SPI2配置选项
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2 ,ENABLE);
 
	//这里只针对SPI口初始化
	RCC_APB1PeriphResetCmd(RCC_APB1Periph_SPI2,ENABLE);    //复位SPI2
	RCC_APB1PeriphResetCmd(RCC_APB1Periph_SPI2,DISABLE);   //停止复位SPI2

	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  //设置SPI单向或者双向的数据模式:全速双工模式
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;		                    //设置SPI工作模式:设置为主SPI
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;		                //设置SPI的数据大小:SPI发送接收8位帧结构
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;		                      //串行同步时钟的空闲状态为高电平
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;	                      //串行同步时钟的第二个跳变沿(上升或下降)数据被采样
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;		                        //NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;	//定义波特率预分频的值:波特率预分频值
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;	                //指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始
	SPI_InitStructure.SPI_CRCPolynomial = 7;	                          //CRC值计算的多项式, >1即可
	SPI_Init(SPI2, &SPI_InitStructure);                                 //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器
 
	SPI_Cmd(SPI2, ENABLE); //使能SPI外设

	SPI2_ReadWriteByte(0xff);//启动传输		  
	
}

void LCD_GPIO_Init(void)
{

	GPIO_InitTypeDef  GPIO_InitStructure;
	      
	RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOC ,ENABLE);
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12 |GPIO_Pin_1;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;   //PC6  命令/数据--切换引脚  //PC7    	  //背光控制
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;       //100MHz
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;        //推挽输出
	GPIO_Init(GPIOA, &GPIO_InitStructure);   

  
		
  SPI2_Init();           //初始化硬件SPI2

//  LCD_HardwareRest();   //硬复位--如果IO连接,硬件复位可控有效
	
	LCD_BLK_On;           //开启背光
	
//	LCD_BLK_Off;   //关闭背光
//	LCD_BLK_On;    //开启背光
      
}

按键

系统硬件设置按键按下松开,IO口输入低电平。具体程序设计如下:首先进行GPIO口配置,注意PB3引脚默认JTDO功能,PB4引脚默认JNTRST功能,因此需要开启AFIO复用时钟,并关闭JTAG,使能SWD。然后进行按键扫描函数的设置,按下UP键返回1,按下DOWN键返回2,按下CONFIRM键返回3,按下CANCEL键返回4。

#include "stm32f10x.h"
#include "key.h"
#include "sys.h" 
#include "delay.h"


//STM32F103核心板例程
//库函数版本例程



//	 

//STM32开发板
//按键驱动代码	   
						  
//  
								    
//按键初始化函数
void KEY_Init(void) //IO初始化
{ 
 	GPIO_InitTypeDef GPIO_InitStructure;
	//初始化KEY1-->PA0上拉输入
 	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB |RCC_APB2Periph_AFIO,ENABLE);//使能PA时钟
	GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);//关闭jtag,使能SWD,可以用SWD模式调试
	GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_3 |GPIO_Pin_4 |GPIO_Pin_8 |GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置成上拉输入
 	GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化PA


}
//按键处理函数
//返回按键值
//mode:0,不支持连续按;1,支持连续按;
//0,没有任何按键按下


static u8 key_up=1;//按键按松开标志

u8 KEY_Scan(u8 mode)
{	 
	
	if(mode)key_up=1;  //支持连按
	
	if(key_up&&(UP==0||DOWN==0||CONFIRM==0||CANCEL==0))
	{
		delay_ms(10);//去抖动 
		key_up=0;
		if(UP==0) return 1;
		if(DOWN==0) return 2;
		if(CONFIRM==0) return 3;
		if(CANCEL==0) return 4;
	}
	else if(UP==1&&DOWN==1&&CONFIRM==1&&CANCEL==1)key_up=1; 	    
 	
	return 0;// 无按键按下
}


多级菜单

参考大佬的程序:https://blog.csdn.net/qq_43940175/article/details/123396075?spm=1001.2014.3001.5506


程序设计如下:首先进行各级菜单界面显示设置,然后进行上滑、下滑动作的当前选项序号List_Number的更新设置,再设置菜单级数的切入、切出动作,其中包括当前菜单级数Menu_Change的更新设置以及List_Number的保存与加载设置。接着设置选项框绘制函数以及选项框的上滑、下滑显示,最后进行多级菜单控制函数的设计。

Menu_Change的更新和List_Number的保存、加载过程的状态解析

Button_Menu_Enter()函数是切入控制按键,Button_Menu_Exit()是切出控制按键,select是切入切出状态标志位,切入为0,切出为1。List和List2用于保存切入前所在选项序号,List保存的是0级的选项序号,List2则保存1级的选项序号。0级切入后当前选项序号List_Number归1,即选项框从头开始,位于新页面起始位置。切出后List_Number从对应来源重新加载。

多级菜单控制函数
首先在函数外部定义全局变量mode作为Alarm()声光报警程序的标志位,具体模式设置在后续展开,此处不作讨论。函数主体通过3个if语句判断当前菜单级数Menu_Change,具体设置如下:

0级显示模式选择界面及选项框,配置UP、DOWN、CONFIRM按键。

1级判断List变量,为1时显示参数选择界面及选项框,UP、DOWN、CONFIRM、CANCEL按键均配置;为2时则显示全部参数测量结果界面,设置mode=5,采集所有参数的数据,然后调用Alarm()声光报警函数。此界面没有下一级结构,且未设置选项,因此仅配置CANCEL按键,触发后更新Menu_Change并重置mode为0,最后切出上一级菜单,重新显示跳转前的界面及选项框所在位置。选项未触发则跳回当前界面显示,进行下一轮循环。

2级判断List2变量,根据变量值选择对应的分支结构处理,分支结构一共有4条:为1时显示温度测量结果界面,设置mode=1,进行DS18B20初始化成功与否的检查,成功则采集温度数据;为2时显示PH测量结果界面,设置mode=2,然后采集PH值;为3时显示浊度测量结果界面,设置mode=3,然后采集浊度数据;为4时显示TDS测量结果界面,设置mode=4,然后采集TDS数据。该条分支结构处理完毕后,主线调用Alarm()声光报警函数。同样没有下一级结构,且无选项设置,所以仅配置CANCEL按键,触发后更新Menu_Change并重置mode为0,最后切出上一级菜单,重新显示跳转前的界面及选项框所在位置。选项未触发则跳回当前界面显示,进行下一轮循环。

List = = 2时的界面显示及数据处理流程

结束后进行相应按键处理。

#include "lcd.h"
#include "menu.h"
#include "stdio.h"
#include "led.h"
#include "ds18b20.h"
#include "adc.h"
#include "alarm.h"

//多级菜单的级数
#define		Menu_Select		3

//选项显示缓存区
char buf[100];

//当前选项序号
uint8_t List_Number = 1;

//当前菜单级数   0到(Menu_Select-1)
static uint8_t Menu_Change = 0;
//菜单刷新标志位
static uint8_t Menu_Refresh_Flag = 0;
//up/down控制按键标志位,1下2上
static uint8_t UD_Action_Flag = 0;




/******************************************************************************
  * @brief  绘制窗体
  * @param  x1,y1 起始坐标
			x2,y2 终点坐标
			Box_Height 选项框的高度
			Menu_List	选项框个数
  * @retval 无
 ******************************************************************************/

void LCD_Draw_Windows(void)
{
	LCD_Draw_Box(0, 0, LCD_X, LCD_H, LBBLUE, 5);
	LCD_Draw_Box(0, 10, LCD_X, 15, LBBLUE, 5);
	sprintf(buf, "   WINDOWS1      ");//发送格式化输出到指针所指向的字符串
	LCD_ShowString(5, 5, 100, 12, 12, LBBLUE, (uint8_t*)buf);
}



/******************************************************************************
      * @brief	显示初始菜单,自己任意配置
      * @param  x,y 显示坐标
				fc 字的颜色
				Box_Height 选项框的高度
      * @retval 无
******************************************************************************/
void Display_Initial_Menu(u16 x,u16 y,u16 fc,u8 Box_Height)
{
		sprintf(buf,"Single Parameter");
		LCD_ShowString(x, y + ( Box_Height * 0 ), 200, 12, 12, fc, (uint8_t *)buf );
		sprintf(buf,"All Parameters");
		LCD_ShowString(x, y + ( Box_Height * 1 ), 200, 12, 12, fc, (uint8_t *)buf );
		//sprintf(buf,"NOTICE");
		//LCD_ShowString(x, y + ( Box_Height * 2 ), 200, 12, 12, fc, "NOTICE" );
		
}



/******************************************************************************
	  * @brief	显示二级菜单,自己任意配置
	  * @param  x,y 显示坐标
				fc 字的颜色
				Box_Height 选项框的高度
	  * @retval 无
******************************************************************************/
void Display_Second_Menu(u16 x,u16 y,u16 fc,u8 Box_Height)
{
		sprintf(buf,"Temperature");
		LCD_ShowString(x, y + ( Box_Height * 0 ), 200, 12, 12, fc, (uint8_t *)buf );
		sprintf(buf,"PH");
		LCD_ShowString(x, y + ( Box_Height * 1 ), 200, 12, 12, fc, (uint8_t *)buf );
		sprintf(buf,"Turbidty");
		LCD_ShowString(x, y + ( Box_Height * 2 ), 200, 12, 12, fc, (uint8_t *)buf );
		sprintf(buf,"TDS");
		LCD_ShowString(x, y + ( Box_Height * 3 ), 200, 12, 12, fc, (uint8_t *)buf );
}




/******************************************************************************
	  * @brief	显示三级菜单,自己任意配置
	  * @param  x,y 显示坐标
				fc 字的颜色
				bc 字的背景色
				sizey 字号
				mode:  0非叠加模式  1叠加模式
				Box_Height 选项框的高度
	  * @retval 无
******************************************************************************/
void Display_Third_Menu(void)
{
	LCD_ShowString( 5, 20, 200, 12, 12, BLUE, "PH_VOL:0.000V");	
	LCD_ShowString( 5, 35, 200, 12, 12, BLUE, "PH:  .  ");	
	LCD_ShowString( 5, 50, 200, 12, 12, BLUE, "TBD_VOL:0.000V");	
	LCD_ShowString( 5, 65, 200, 12, 12, BLUE, "TBD:    NTU");	
	LCD_ShowString( 5, 80, 200, 12, 12, BLUE, "TDS_VOL:0.000V");
	LCD_ShowString( 5, 95, 200, 12, 12, BLUE, "TDS:    ppm");	

	
	LCD_ShowString(5,5,100,12,12,BLUE,"DS18B20:   . C");
}

void Display_Temp_Menu(void)
{
	LCD_ShowString(5,5,100,12,12,BLUE,"DS18B20:   . C");
}

void Display_PH_Menu(void)
{
	LCD_ShowString( 5, 20, 200, 12, 12, BLUE, "PH_VOL:0.000V");	
	LCD_ShowString( 5, 35, 200, 12, 12, BLUE, "PH:  .  ");
}

void Display_TBD_Menu(void)
{
	LCD_ShowString( 5, 50, 200, 12, 12, BLUE, "TBD_VOL:0.000V");	
	LCD_ShowString( 5, 65, 200, 12, 12, BLUE, "TBD:    NTU");
}

void Display_TDS_Menu(void)
{
	LCD_ShowString( 5, 80, 200, 12, 12, BLUE, "TDS_VOL:0.000V");
	LCD_ShowString( 5, 95, 200, 12, 12, BLUE, "TDS:    ppm");
}

/******************************************************************************
  * @brief  注册up控制按键
  * @param  Menu_List:控制的列表项数目
  * @retval 无
  * eg:一个页面有6个选项,就让 Menu_List = 6
 ******************************************************************************/
void Button_Up_Click(uint8_t Menu_List)
{
	
		LED0=!LED0;
		if (List_Number > 1)
			List_Number = List_Number - 1;
		else if (List_Number == 1)
			List_Number = Menu_List;
		else
			List_Number = List_Number;

		UD_Action_Flag = 2;
		
		LCD_ShowxNum(10, 140, List_Number, 1, 12, BLUE, 0);//当前选项序号
	
}



/******************************************************************************
  * @brief  注册down控制按键
  * @param  控制的列表项数目
  * @retval 无
 ******************************************************************************/
void Button_Down_Click(uint8_t Menu_List)
{
	
	
		LED0=!LED0;
		if (List_Number < Menu_List)
			List_Number = List_Number + 1;
		else if (List_Number == Menu_List)
			List_Number = 1;
		else
			List_Number = List_Number;

		UD_Action_Flag = 1;
		
		LCD_ShowxNum(10, 140, List_Number, 1, 12, BLUE, 0);//当前选项序号
	
}



/******************************************************************************
  * @brief  注册菜单切入控制按键
  * @param  Desk_Num 菜单级数
  * @retval 无
  ****************************************************************************/
void Button_Menu_Enter(u8 Desk_Num)
{
	
	
	//切入菜单,即进入下一级菜单
	
		LED0=!LED0;
		Action_Menu_Change(0);		//List_Number的保存与重载
		Menu_Refresh_Flag = 1;

		if (Menu_Change < Desk_Num - 1)
			Menu_Change = Menu_Change + 1;
		else if (Menu_Change == Desk_Num - 1)
			Menu_Change = Desk_Num - 1;

		//显示菜单级标号
		LCD_ShowxNum(10, 140, List_Number, 1, 12, BLUE, 0);
		LCD_ShowxNum(110, 140, Menu_Change, 1, 12, BLUE, 0);//显示当前菜单级数
}



/******************************************************************************
  * @brief  注册菜单切出控制按键
  * @param  Desk_Num 菜单级数
  * @retval 无
  ****************************************************************************/
void Button_Menu_Exit(u8 Desk_Num)
{
	
	
	//切出菜单,即返回上一级菜单
	
		LED0=!LED0;
		Action_Menu_Change(1);		//List_Number的保存与重载
		Menu_Refresh_Flag = 1;
		
		if (Menu_Change > 0)
			Menu_Change = Menu_Change - 1;
		if (Menu_Change < Desk_Num - 2)
			Menu_Change = 0;

		//显示菜单级标号
		LCD_ShowxNum(10, 140, List_Number, 1, 12, BLUE, 0);
		LCD_ShowxNum(110, 140, Menu_Change, 1, 12, BLUE, 0);
}



/******************************************************************************
  * @brief  注册菜单切换控制按键搭配的动作
  * @param  select 事件标志
  * @retval 无
  ****************************************************************************/
u8 List = 0,List2 = 0;
void Action_Menu_Change(u8 select)              //确认键select=0,返回键select=1,List_Number的保存与重载
{
	switch (select)
	{
	case 0:
		switch (Menu_Change)
		{
		case 0:
			List = List_Number;                       //储存跳转前选项序号
			List_Number = 1;                          //跳转后选项序号从头开始
			LCD_Fill(0, 0, LCD_X, LCD_H, WHITE);
			break;
		case 1:
			List2 = List_Number;
			List_Number = 1;
			LCD_Fill(0, 0, LCD_X, LCD_H, WHITE);
			break;
		//case ...3级、4级菜单
		}
		break;

	case 1:
		switch (Menu_Change)
		{
		case 1:
			List_Number = List;                      //恢复跳转前选项序号 
			LCD_Fill(0, 0, LCD_X, LCD_H, WHITE);
			break;
		case 2:
			List_Number = List2;	                   //恢复跳转前选项序号 
			LCD_Fill(0, 0, LCD_X, LCD_H, WHITE);
			break;
		//case ...3级、4级菜单
		}
	default:
		break;
	}
	
}



/******************************************************************************
  * @brief  注册选项框上滑动作
  * @param  x1,y1 起始坐标
			x2,y2 终点坐标
			Box_Height 选项框的高度
			Menu_List	选项框个数
  * @retval 无
 ******************************************************************************/
void Action_Box_Up(u16 x1,u16 y1,u16 x2,u16 y2,uint8_t Menu_List, uint8_t Box_Height)
{
	if(UD_Action_Flag == 2)
	{
		UD_Action_Flag = 0;
		if(List_Number >= 1 && List_Number < Menu_List)
		{
			LCD_DrawLine(x1, y2+Box_Height*(List_Number), x2, y2+Box_Height*(List_Number), WHITE);
			LCD_DrawLine(x1, y1+Box_Height*(List_Number), x1, y2+Box_Height*(List_Number), WHITE);
			LCD_DrawLine(x2, y1+Box_Height*(List_Number), x2, y2+Box_Height*(List_Number), WHITE);
			printf("%d",List_Number);
		}
		if(List_Number == Menu_List)
		{
			LCD_DrawRectangle(x1, y1 + Box_Height*(0), x2, y2+Box_Height*(0), WHITE);
		}
	}
	
}



/******************************************************************************
  * @brief  注册选项框下滑动作
  * @param  x1,y1 起始坐标
			x2,y2 终点坐标
			Box_Height 选项框的高度
			Menu_List	选项框个数
  * @retval 无
 ******************************************************************************/
void Action_Box_Down(u16 x1, u16 y1, u16 x2, u16 y2, uint8_t Menu_List, uint8_t Box_Height)
{
	if (UD_Action_Flag == 1)
	{
		UD_Action_Flag = 0;
		if (List_Number > 1 && List_Number <= Menu_List)
		{
			LCD_DrawLine(x1, y1 + Box_Height * (List_Number - 2), x2, y1 + Box_Height * (List_Number - 2), WHITE);
			LCD_DrawLine(x1, y1 + Box_Height * (List_Number - 2), x1, y2 + Box_Height * (List_Number - 2), WHITE);
			LCD_DrawLine(x2, y1 + Box_Height * (List_Number - 2), x2, y2 + Box_Height * (List_Number - 2), WHITE);
			printf("%d", List_Number);
		}
		if (List_Number == 1)
		{
			LCD_DrawRectangle(x1, y1 + Box_Height * (Menu_List - 1), x2, y2 + Box_Height * (Menu_List - 1), WHITE);
		}
	}
}


/******************************************************************************
  * @brief  绘制选项框
  * @param  x1,y1 起始坐标
			x2,y2 终点坐标
			Box_Height 选项框的高度
			Menu_List	选项框个数
  * @retval 无
 ******************************************************************************/
void Draw_Option_Box(u16 x1, u16 y1, u16 x2, u16 y2, uint8_t Menu_List, uint8_t Box_Height)
{
	LCD_DrawRectangle(x1, y1 + Box_Height * (List_Number - 1), x2, y2 + Box_Height * (List_Number - 1), BLUE);
}



/******************************************************************************
  * @brief  注册菜单
  * @param  无
  * @retval 无
 ******************************************************************************/
int mode=0;//用于Alarm判断模式
void Menu_Display_Control(void)
{
		
	if (Menu_Change==0)
	{
		if (Menu_Refresh_Flag == 1)
		{
			Display_Initial_Menu(10, 3, BLUE, 15);		//注册一个0级菜单
			Menu_Refresh_Flag = 0;
		}
		Draw_Option_Box(3, 0, 200, 15, 2, 15);							//注册选项框
		
		switch(KEY_Scan(0))
		{
			case 1:
				Button_Up_Click(2);										//注册up控制按钮
				Action_Box_Up(3, 0, 200, 15, 2, 15); 					//添加选项框的上滑动作
				break;
			
			case 2:
				Button_Down_Click(2);											//注册down控制按钮
				Action_Box_Down(3, 0, 200, 15, 2, 15); 					//添加选项框的下滑动作
				break;
			
			case 3:
				Button_Menu_Enter(Menu_Select);
				break;
			
			
			
			default:
				Menu_Refresh_Flag = 0;
		}
	}	
	
	if(Menu_Change==1)		
	{
		switch(List)
		{
			case 1:
				if (Menu_Refresh_Flag == 1)
				{
					Display_Second_Menu(10, 3, BLUE, 15);			//注册一个1级菜单
					Menu_Refresh_Flag = 0;
				}
				
				Draw_Option_Box(3, 0, 150, 15, 4, 15);							//注册选项框
				
				switch(KEY_Scan(0))
				{
					case 1:
						Button_Up_Click(4);										//注册up控制按钮
						Action_Box_Up(3, 0, 150, 15, 4, 15); 					//添加选项框的上滑动作
						break;
					
					case 2:
						Button_Down_Click(4);											//注册down控制按钮
						Action_Box_Down(3, 0, 150, 15, 4, 15); 					//添加选项框的下滑动作
						break;
					
					case 3:
						Button_Menu_Enter(Menu_Select);
						break;
					
					case 4:
						Button_Menu_Exit(Menu_Select);
						break;
					
					default:
						Menu_Refresh_Flag = 0;
				}	
				break;	

			case 2:
				if (Menu_Refresh_Flag == 1)
				{
					Display_Third_Menu();
					Menu_Refresh_Flag = 0;
				}
				mode=5;			
				PH();
				TBD();	
				TDS();
				
				while(DS18B20_Init()) 
				{		 		
					LCD_ShowString(5,5,100,12,12,BLUE,"DS18B20 Error!");
					delay_ms(200);
					LCD_Fill(5,5,88,20,WHITE);
					delay_ms(200);
				}
				DS18B20();
				Alarm();
						
				switch(KEY_Scan(0))
				{
					

					case 4:
						Button_Menu_Exit(2);
						mode=0;
						break;
					
					default:
						Menu_Refresh_Flag = 0;
				}
				
				break;														
		}
			
	}
		
	
	if(Menu_Change==2)		
	{
		switch(List2)	
		{
			case 1:
				if (Menu_Refresh_Flag == 1)
				{
					Display_Temp_Menu();
					Menu_Refresh_Flag = 0;
				}
				mode=1;
				while(DS18B20_Init()) 
				{		 		
					LCD_ShowString(5,5,100,12,12,BLUE,"DS18B20 Error!");
					delay_ms(200);
					LCD_Fill(5,5,88,20,WHITE);
					delay_ms(200);
				}
				DS18B20();
				break;
				
				
			case 2:
				if (Menu_Refresh_Flag == 1)
				{
					Display_PH_Menu();
					Menu_Refresh_Flag = 0;
				}
				mode=2;
				PH();
				break;
					
					
			case 3:
				if (Menu_Refresh_Flag == 1)
				{
					Display_TBD_Menu();
					Menu_Refresh_Flag = 0;
				}
				mode=3;
				TBD();
				break;
				
			case 4:
				if (Menu_Refresh_Flag == 1)
				{
					Display_TDS_Menu();
					Menu_Refresh_Flag = 0;
				}
				mode=4;
				TDS();
				break;
		}
		
		Alarm();	
				
		switch(KEY_Scan(0))
		{
			case 4:
				Button_Menu_Exit(Menu_Select);
				mode=0;
				break;
			
			default:
				Menu_Refresh_Flag = 0;
		}
	}		
		
}


DS18B20

程序设计显示精度为0.1℃。

首先主机发送复位脉冲,从机发送应答脉冲初始化序列,因为系统仅使用一个DS18B20设备,即单节点,所以主机发送0xcc跳越ROM命令,然后发送0x44温度转换功能命令,至此温度转换功能开启。因为单片机每次发功能指令前都必须复位DS18B20并写ROM指令,所以主机再次发送复位脉冲,从机发送应答脉冲初始化序列,然后主机发送0xcc跳越ROM命令和0xbe读暂存器功能指令,最后进行数据处理并显示。

#include "ds18b20.h"
#include "delay.h"
#include "lcd.h"

short temperature;
//	 
//本程序只供学习使用,未经作者许可,不得用于其它任何用途
//ALIENTEK miniSTM32开发板
//DS18B20驱动代码	   
//正点原子@ALIENTEK
//技术论坛:www.openedv.com
//修改日期:2012/9/12
//版本:V1.0
//版权所有,盗版必究。
//Copyright(C) 广州市星翼电子科技有限公司 2009-2019
//All rights reserved									  
//
  

//复位DS18B20
void DS18B20_Rst(void)	   
{                 
	DS18B20_IO_OUT(); //SET PA0 OUTPUT
    DS18B20_DQ_OUT=0; //拉低DQ
    delay_us(750);    //拉低750us,>480us
    DS18B20_DQ_OUT=1; //DQ=1 
	delay_us(15);     
}
//等待DS18B20的回应
//返回1:未检测到DS18B20的存在
//返回0:存在
u8 DS18B20_Check(void) 	   
{   
	u8 retry=0;
	DS18B20_IO_IN();//SET PA15 INPUT
	
//应答操作,判断引脚低电平时间是否在60s和240s之间,返回应答结果,此处取200s-240s	
    while (DS18B20_DQ_IN&&retry<200)   //&&逻辑与,两个表达式都为真(true)时才为真,优先级低于关系运算符<
	{
		retry++;
		delay_us(1);
	};	 
	if(retry>=200)return 1;
	else retry=0;
	
    while (!DS18B20_DQ_IN&&retry<240)
	{
		retry++;
		delay_us(1);
	};
	if(retry>=240)return 1;	    
	return 0;
}
//从DS18B20读取一个位
//返回值:1/0
u8 DS18B20_Read_Bit(void) 			 // read one bit
{
    u8 data;
	DS18B20_IO_OUT();//设置IO方向为输出
    DS18B20_DQ_OUT=0; //主机拉低总线
	delay_us(2);
    DS18B20_DQ_OUT=1; //拉高总线
	DS18B20_IO_IN();//SET PA0 INPUT
	delay_us(12);
	if(DS18B20_DQ_IN)data=1;//输入判断
    else data=0;	 
    delay_us(50);           
    return data;
}
//从DS18B20读取一个字节
//返回值:读到的数据
u8 DS18B20_Read_Byte(void)    // read one byte
{        
    u8 i,j,dat;
    dat=0;
	for (i=1;i<=8;i++) 
	{
        j=DS18B20_Read_Bit();
        dat=(j<<7)|(dat>>1);
    }						    
    return dat;
}
//写一个字节到DS18B20
//dat:要写入的字节
void DS18B20_Write_Byte(u8 dat)     
 {             
    u8 j;
    u8 testb;
	DS18B20_IO_OUT();//设置IO方向为输出
    for (j=1;j<=8;j++) 
	{
        testb=dat&0x01;
        dat=dat>>1;
        if (testb) 
        {
					// Write 1
            DS18B20_DQ_OUT=0;//主机拉低总线
            delay_us(2);// >1us                            
            DS18B20_DQ_OUT=1;//拉高总线
            delay_us(60); //>15us            
        }
        else 
        {
					// Write 0
            DS18B20_DQ_OUT=0;//主机拉低总线
            delay_us(60); //60-120us            
            DS18B20_DQ_OUT=1;//拉高总线
            delay_us(2); // >1us                         
        }
    }
}
//开始温度转换
void DS18B20_Start(void)
{   						               
    DS18B20_Rst();	//复位   
	DS18B20_Check();	//检测 
    DS18B20_Write_Byte(0xcc);// 仅适用单节点,跳越ROM
    DS18B20_Write_Byte(0x44);// 温度转换
} 
//初始化DS18B20的IO口 DQ 同时检测DS的存在
//返回1:不存在
//返回0:存在    	 
u8 DS18B20_Init(void)
{
 	GPIO_InitTypeDef  GPIO_InitStructure;
 	
 	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	 //使能PORTA口时钟 
	GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);
 	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15;				
 	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 		  
 	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
 	GPIO_Init(GPIOA, &GPIO_InitStructure);

 	GPIO_SetBits(GPIOA,GPIO_Pin_15);    //输出1

	DS18B20_Rst();

	return DS18B20_Check();
}  
//从ds18b20得到温度值
//精度:0.1C
//返回值:温度值 (-550~1250) 
short DS18B20_Get_Temp(void)
{
    u8 temp;
    u8 TL,TH;
	short tem;
    DS18B20_Start();         //开始温度转换
    DS18B20_Rst();
    DS18B20_Check();	 
    DS18B20_Write_Byte(0xcc);// skip rom
    DS18B20_Write_Byte(0xbe);// 读暂存器    
    TL=DS18B20_Read_Byte(); // LSB   
    TH=DS18B20_Read_Byte(); // MSB  
	    	  
    if(TH>7)
    {
        TH=~TH;
        TL=~TL; 
        temp=0;//温度为负  
    }else temp=1;//温度为正	  	  
    tem=TH; //获得高八位
    tem<<=8;    
    tem+=TL;//获得底八位
    tem=(float)tem*0.625;//转换     
	if(temp)return tem; //返回温度值
	else return -tem;    
} 
 
void DS18B20(void)
{
	
	temperature=DS18B20_Get_Temp();	
	if(temperature<0)
	{
		LCD_ShowChar(5+48,5,'-',12,RED,0);			//显示负号
		temperature=-temperature;					//转为正数
	}
	else
	{
		LCD_ShowChar(5+48,5,' ',12,RED,0);			//去掉负号
	}
			
	LCD_ShowNum(5+48+6,5,temperature/10,2,12,RED);	//显示正数部分	    
  LCD_ShowNum(5+48+24,5,temperature%10,1,12,RED);	//显示小数部分,保留1位小数
}

端口设置方向:https://zhiguoxin.blog.csdn.net/article/details/112070047?spm=1001.2014.3001.5506

ADC

PH

PH电极在使用前需使用标准缓冲溶液进行校准,每次测量后均需用蒸馏水洗净。

校准过程中的测试结果

模块输出电压和PH值的关系


解得:PH=-5.23×U+26.66

浊度

模块输出电压和浊度值的关系


浊度 = -865.68×U+3291.3

TDS

标准曲线


 #include "adc.h"
 #include "delay.h"
 #include "lcd.h"
 #include "led.h"
 #define K 3291.3
 #define offset 26.39

float PH_Value,TBD_Vol,TDS_Vol;
//初始化ADC
//这里我们仅以规则通道为例
//我们默认将开启通道0~3	
//总转换时间21us
void  Adc_Init(void)
{ 	
	ADC_InitTypeDef ADC_InitStructure; 
	GPIO_InitTypeDef GPIO_InitStructure;

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |RCC_APB2Periph_ADC1	, ENABLE );	  //使能ADC1通道时钟
 

	RCC_ADCCLKConfig(RCC_PCLK2_Div6);   //设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M

	//PA1 作为模拟通道输入引脚                         
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1| GPIO_Pin_2| GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;		//模拟输入引脚
	GPIO_Init(GPIOA, &GPIO_InitStructure);	

	ADC_DeInit(ADC1);  //复位ADC1,将外设 ADC1 的全部寄存器重设为缺省值

	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;	//ADC工作模式:ADC1和ADC2工作在独立模式
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;	//模数转换工作在单通道模式
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;	//模数转换工作在单次转换模式
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;	//转换由软件而不是外部触发启动
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;	//ADC数据右对齐
	ADC_InitStructure.ADC_NbrOfChannel = 1;	//顺序进行规则转换的ADC通道的数目
	ADC_Init(ADC1, &ADC_InitStructure);	//根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器   

  
	ADC_Cmd(ADC1, ENABLE);	//使能指定的ADC1
	
	ADC_ResetCalibration(ADC1);	//使能复位校准  
	 
	while(ADC_GetResetCalibrationStatus(ADC1));	//等待复位校准结束
	
	ADC_StartCalibration(ADC1);	 //开启AD校准
 
	while(ADC_GetCalibrationStatus(ADC1));	 //等待校准结束
 
//	ADC_SoftwareStartConvCmd(ADC1, ENABLE);		//使能指定的ADC1的软件转换启动功能

}				  
//获得ADC值
//ch:通道值 0~3
u16 Get_Adc(u8 ch)   
{
  	//设置指定ADC的规则组通道,一个序列,采样时间
	ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5 );	//ADC1,ADC通道,采样时间为239.5周期	  			    
  
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);		//使能指定的ADC1的软件转换启动功能	
	 
	while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束

	return ADC_GetConversionValue(ADC1);	//返回最近一次ADC1规则组的转换结果
}

u16 Get_Adc_Average(u8 ch,u8 times)
{
	u32 temp_val=0;
	u8 t;
	for(t=0;t<times;t++)
	{
		temp_val+=Get_Adc(ch);
		delay_ms(5);
	}
	return temp_val/times;
} 	 

void PH(void)
{
	u16 tp,TEMP;
	float temp,TP,PH_Vol;
	
	tp=Get_Adc_Average(ADC_Channel_1,10);//采样10次,取平均
	temp=(float)tp*(3.3/4096)/0.66;//12位ADC,参考电压3.3V被分成4096份,转换为5V参考
	tp=temp;
	PH_Vol=temp;
	PH_Value=-5.4*PH_Vol+offset;
	
	/***********PH值**********/
	LCD_ShowxNum(5+18,35,PH_Value,2,12,RED,0);//整数部分
	//小数部分
	TEMP=PH_Value;
	TP=PH_Value-TEMP;
	TP*=100;
	LCD_ShowxNum(5+36,35,TP,2,12,RED,0);
	
	/***********电压**********/
	LCD_ShowxNum(5+42,20,temp,1,12,RED,0);//整数部分
	//小数部分
	temp-=tp;
	temp*=1000;
	LCD_ShowxNum(5+54,20,temp,3,12,RED,0X80);
}

void TBD(void)
{
	u16 tp;
	float temp,TBD_Value;
	
	tp=Get_Adc_Average(ADC_Channel_2,10);//采样10次,取平均
	temp=(float)tp*(3.3/4096)/0.66;//12位ADC,参考电压3.3V被分成4096份,转换为5V参考
	tp=temp;
	TBD_Vol=temp;
	TBD_Value=-865.68*temp+K;
	LCD_ShowxNum(5+24,65,TBD_Value,4,12,RED,0);
	LCD_ShowxNum(5+48,50,tp,1,12,RED,0);//整数部分
	//小数部分
	temp-=tp;
	temp*=1000;
	LCD_ShowxNum(5+60,50,temp,3,12,RED,0X80);
	
}

void TDS(void)
{
	u16 tp;
	float temp,TDS_Value;
	
	tp=Get_Adc_Average(ADC_Channel_7,10);//采样10次,取平均
	temp=(float)tp*(3.3/4096);//12位ADC,参考电压3.3V被分成4096份
	tp=temp;
	TDS_Vol=temp;
	TDS_Value=66.71*temp*temp*temp-127.93*temp*temp+428.7*temp;
	LCD_ShowxNum(5+24,95,TDS_Value,4,12,RED,0);
	LCD_ShowxNum(5+48,80,tp,1,12,RED,0);//整数部分
	//小数部分
	temp-=tp;
	temp*=1000;
	LCD_ShowxNum(5+60,80,temp,3,12,RED,0X80);
}



声光报警

水温、PH或浊度超出设定的阈值(水温2532℃,PH值6.58.5,浊度电压2.0V以上)时点亮,触发方式为低电平。

mode标志位模式设置

#include "sys.h"
#include "adc.h"
#include "led.h"
#include "ds18b20.h"
#include "speaker.h"
#include "alarm.h"
#include "delay.h"

void Alarm(void)
{
	extern float PH_Value,TBD_Vol,TDS_Vol;
	extern short temperature;
	extern int mode;
	
	switch(mode)
	{
		case 1:
			if((temperature/10)<25||(temperature/10)>=32)
			{
				LED1=0;
				Speaker=0;
			}
			else 
			{
				LED1=1;
				Speaker=1;
			}
			break;
			
		case 2:
			if(PH_Value<6.5||PH_Value>8.5)
			{
				LED1=0;
				Speaker=0;
			}
			else 
			{
				LED1=1;
				Speaker=1;
			}
			break;
		
		case 3:
			if(TBD_Vol<2.5)
			{
				LED1=0;
				Speaker=0;
			}
			else 
			{
				LED1=1;
				Speaker=1;
			}
			break;
			
		case 5:
			if((temperature/10)<25||(temperature/10)>=32||PH_Value<6.5||PH_Value>8.5||TBD_Vol<2.5)
			{
				LED1=0;
				Speaker=0;
			}
			else 
			{
				LED1=1;
				Speaker=1;
			}
			break;
	}
	
	
	
}

实物

功能

1) 通过UP、DOWN、CONFIRM、CANCEL按键进行单参数测量和所有参数同时测量模式的选择以及多级菜单之间的切换,按键每次按下,LED0翻转一次状态。屏幕左下角显示当前选项序号,右下角显示当前菜单级数。

2) 实时测量目标水体的温度、PH值、浊度、TDS,同时显示对应电压值,动态变化部分以红色字体显示,便于区分。

3) 温度、PH、浊度设置阈值,进入参数显示界面后,超出阈值蜂鸣器报警,同时LED1点亮。参数保持超出阈值状态,退出结果显示界面仍然声光报警,提示用户水质亟待改善。按下复位键可结束报警。

物联沃分享整理
物联沃-IOTWORD物联网 » 【STM32】基于STM32F103C8T6的水质检测系统设计(声光报警、多级菜单)

发表评论