STM32毕设项目分享:水产运输水质监控系统设计(GPS、蓝牙)
一、成品简介1.实物成品成品展示2.手机蓝牙APP蓝牙APP界面二、项目简介1.功能详解水产运输水质监控系统设计(GPS、蓝牙)水质参数采集使用 PH 检测模块采集水体酸碱度DS18B20 数字温度传感器采集车厢水体温度MQ-2 气体检测模块采集车厢内一氧化碳、烟雾等有害气体浓度NEO-6M GPS 模块实时获取运输车辆经纬度定位信息。本地实时显示0.96 寸 OLED 屏幕分页实时展示水温、PH 值、有害气体浓度、GPS 经纬度定位数据支持按键切换监控界面与参数设置界面。本地阈值自定义配置搭载 4 路独立按键并配套软件消抖程序可切换调节 PH、水温、一氧化碳三类参数报警阈值。本地定时增氧控制每 3 分钟自动启动打氧泵持续供氧 30 秒自动化维持水体溶氧量。蓝牙无线数据传输JDY31 蓝牙透传模块通过串口与主控通信传输实时水质、定位数据参数超标时同步推送对应 ERROR 报警手机蓝牙串口助手可实时接收监测信息。全程位置溯源记录NEO-6M GPS 模块解析持续输出车辆北纬、东经坐标实现水产运输全程位置追踪。异常超限双重报警系统实时比对自定义安全阈值当水温、PH、一氧化碳浓度超出设定区间时通过蓝牙向手机端推送文字告警信息标记故障状态。硬件框图流程图2.主要器件STM32F103C8T6 单片机PH 水质检测模块采集水体酸碱度DS18B20 数字温度传感器温度采集MQ-2 气体检测模块检测车厢一氧化碳、烟雾有害气体NEO-6M GPS 定位模块采集车辆经纬度位置信息JDY31 蓝牙透传模块实现无线数据传输0.96 寸 OLED 显示屏数据显示4 路独立按键用于参数阈值修改、模式切换3.原理图设计该系统供电通过四节干电池干电池电压为1.5V四节为6V先通过AMS1117-5V降压为5V给5V的外设供电。之后通过AMS1117-3.3V把5V电压降低为3.3V给单片机以及外设供电。4.PCB设计三、主要模块1.PH检测模块PH计模块主要由PH电极和参比电极组成。溶液的PH值取决于溶液中氢离子的浓度通过测量电极系统与被测溶液构成的测量电池的电动势而得到被测溶液氢离子活度的。PH电极的敏感玻璃膜与溶液中的氢离子发生反应产生微弱的电信号而参比电极提供稳定的参考电位。通过使用单片机去测量这两个电极之间的电位差就可以计算出溶液的PH值。2.GPS定位模块GPSGlobal Positioning System, 全球定位系统是一个以接收导航卫星信号为基础的无线电空间定位系统它可以利用卫星采集到观测点的经纬度、高度和时间定位信息地球表面和上方任何地点可以无源方式同时接收用于定位系统。GPS 在民用领域中发现其在定位、导航和计时等方面的价值现广泛运用于运输、公共资源管理、精确测绘、公共安全等领域。3.蓝牙模块JDY-31蓝牙如图2-5所示,基于蓝牙3.0 SPP设计可以支持Windows、Linux、android数据透传工作频段2.4GHZ调制方式GFSK最大发射功率8db最大发射距离30米支持用户通过AT命令修改设备名、波特率等指令方便快捷使用灵活。具有传输速度快、稳定性高、功耗低等优点。广泛应用于物联网、智能家居、智能穿戴等领域。4.气体检测模块MQ-2 气体传感器模块是种广谱的气体检测元件模块使用二氧化锡为核心材料导电率随被测气体浓度变化而改变当有害气体浓度升高时SnO₂表面吸附气体分子导致导电率上升反之则下降。四、写作内容参考1.摘要参考内容在我国水产冷链物流快速发展、鲜活水产运输品质管控需求日益提升的背景下传统水产运输依赖人工定时检测水质的模式存在效率低、数据滞后、误差大等问题易造成水产品存活率下降与经济损失。为解决上述行业痛点本文设计一套基于单片机的鲜活水产运输车辆车厢水质监控系统。系统以STM32F103C8T6为核通过 PH 检测模块、DS18B20 温度模块、MQ-2 气体检测模块、NEO-6M GPS 定位模块、JDY31 蓝牙模块及 0.96 寸 OLED 显示模块完成硬件电路与整体结构设计实现采集水温、PH 值、有害气体浓度等关键参数以及 GPS 定位追踪、数据本地显示、蓝牙远程传输当参数超标时触发报警。系统具备低成本、实时性强、自动化等特点弥补传统水产运输水质监控的短板降低水产运输损耗、提升存活率为鲜活水产运输的智能化、精细化管控提供了可行的技术方案。2.目录参考内容五、项目主要源码5.1.单片机源码5.1.1主程序#include bsp.h #include math.h /* DS18B20-- PA4 OLED-iic1-pb7 sda scl-9b6 PH pa0 CO pa1 GPS pa10-rx pa9-tx UART1 BLE pa2-tx pa3-rx UART2 key PA5-KEY1 PA6-KEY2 PA7-KEY3 PB0-KEY4 */ /* 要求 屏幕温度 PH CO 以及经纬度 按键调试温度阈值CO阈值PH阈值 串口1解析GPS 串口2蓝牙 发出当前温度 湿度 PH CO 经纬度 adc:读取CO PH阈值 异常报警温度、PH值、含氧量等超标 */ /* key1:切换设置模式修改的数值 key2:数值 key3:数值-- key4:进入或推出设置模式 显示 //---Setting--- //PH CO Temp //x.x xx xx // -------------------- //Temp CO PH //xx 99(ppm) 6.1 // GPS //xxx xxx xxx 正常蓝牙发送: PH:X.X CO:XXPPM Temp:xx.x GPS:XXX.XXX.XXX 错误报警发送PH_ERROR;CO_ERROR;_TEMP_ERROR; */ //PH阈值指的是在这个范围的-1.5之内 //温度阈值是指在该温度的-5之内 //CO阈值指的是大于 uint8_t mode;//设置模式0数值显示模式1 uint8_t configue_index;//修改的指引 PH0 CO1 Temp2 char oled_buffer[25];//OLED显示数值 char ble_send_buf[50];//蓝牙发送数组 float PH;float PH_Thre7; uint16_t CO;uint16_t CO_Thre200; uint16_t Temp;uint16_t Temp_Thre12; uint8_t error_state;//PH-1 CO-2 Temp-3 uint8_t rx1,rx2; uint16_t ph_adc,co_adc; float gps_n,gps_e;extern _SaveData Save_Data; /* */ void key_task(); /* */ void bsp_init() { delay_init(); OLED_Init(); HAL_TIM_Base_Start_IT(htim1); HAL_UART_Receive_IT(huart1,rx1,1); HAL_UART_Receive_IT(huart2,rx2,1); //测试 MY_ADC_Init(); } /* */ void loop() { //key // key_task();//放进中断了 这儿实时性不太行 //OLED if(mode0)//设置模式 { OLED_NewFrame(); sprintf(oled_buffer,----Setting----); OLED_PrintString(5, 0, oled_buffer, font16x16, OLED_COLOR_NORMAL); OLED_PrintString(0, 16, PH, font16x16,configue_index0?OLED_COLOR_REVERSED:OLED_COLOR_NORMAL); OLED_PrintString(42, 16, co, font16x16, configue_index1?OLED_COLOR_REVERSED:OLED_COLOR_NORMAL); OLED_PrintString(84, 16, Temp, font16x16, configue_index2?OLED_COLOR_REVERSED:OLED_COLOR_NORMAL); sprintf(oled_buffer,%.1f,PH_Thre); OLED_PrintString(0, 32, oled_buffer, font16x16, OLED_COLOR_NORMAL); sprintf(oled_buffer,%03d,CO_Thre); OLED_PrintString(42, 32, oled_buffer, font16x16, OLED_COLOR_NORMAL); sprintf(oled_buffer,%02d,Temp_Thre); OLED_PrintString(84, 32, oled_buffer, font16x16, OLED_COLOR_NORMAL); OLED_ShowFrame(); } if(mode1)//显示模式 { OLED_NewFrame(); OLED_PrintString(0, 0, Temp, font16x16, OLED_COLOR_NORMAL); OLED_PrintString(42, 0, co, font16x16, OLED_COLOR_NORMAL); OLED_PrintString(84, 0, PH, font16x16, OLED_COLOR_NORMAL); sprintf(oled_buffer,%.02d,Temp); OLED_PrintString(0, 16, oled_buffer, font16x16, OLED_COLOR_NORMAL); sprintf(oled_buffer,%03d,CO); OLED_PrintString(42, 16, oled_buffer, font16x16, OLED_COLOR_NORMAL); sprintf(oled_buffer,%.1f,PH); OLED_PrintString(84, 16, oled_buffer, font16x16, OLED_COLOR_NORMAL); sprintf(oled_buffer,-----GPS-----); OLED_PrintString(5, 32, oled_buffer, font16x16, OLED_COLOR_NORMAL); //N:xx.xxE:xxx.xx gps_n gps_e sprintf(oled_buffer,N:%.2fE%.2f,gps_n,gps_e); OLED_PrintString(0, 48, oled_buffer, font16x16, OLED_COLOR_NORMAL); OLED_ShowFrame(); } //Scan_Task if(mode1) { //BLE ble_send_buf[50] UART2 sprintf(ble_send_buf,Temp:%.02d\r\nco:%.03dppm\r\nPH:%.1f\r\n,Temp,CO,PH); HAL_UART_Transmit(huart2,(uint8_t *)ble_send_buf,strlen(ble_send_buf),1000); sprintf(ble_send_buf,N:%f\r\nE:%f\r\n,gps_n,gps_e); HAL_UART_Transmit(huart2,(uint8_t *)ble_send_buf,strlen(ble_send_buf),1000); //Alarm uint8_t error_state;//PH-1 CO-2 Temp-3 //PH阈值指的是在这个范围的-1.5之内 //温度阈值是指在该温度的-5之内 //CO阈值指的是大于 if(COCO_Thre){error_state2;HAL_UART_Transmit(huart2,(uint8_t *)CO:ERROR!,strlen(CO:ERROR!),1000);} if(PH(PH_Thre1.5)||(PHPH_Thre-1.5)){error_state1;HAL_UART_Transmit(huart2,(uint8_t *)PH:ERROR!,strlen(PH:ERROR!),1000);} if((TempTemp_Thre5)||(TempTemp_Thre-5)){error_state3;HAL_UART_Transmit(huart2,(uint8_t *)Temp:ERROR!,strlen(Temp:ERROR!),1000);} } //GPS gps_n,gps_e; parseGpsBuffer();//维度Save_Data.latitude ,Save_Data.N_S--N //经度,Save_Data.longitude Save_Data.E_W -E gps_ngps_str_to_float(Save_Data.latitude, 0); gps_egps_str_to_float(Save_Data.longitude, 1); MY_ADC_ReadValues(); ph_adc MY_ADC_GetValue(0); co_adc MY_ADC_GetValue(1); float PH_DAT (ph_adc/4096.0)*3.3; PH_DAT -5.7541*PH_DAT16.654; if(PH_DAT14.0)PH_DAT14.0; if(PH_DAT0)PH_DAT0.0; PHPH_DAT; //CO uint16_t CO;uint16_t CO_Thre;,co_adc; float Vol (co_adc*5.0/4096); float RS (5-Vol)/(Vol*0.5); float R06.64; float ppm pow(11.5428*R0/RS, 0.6549f)1.4; COppm;HAL_ADC_Stop(hadc1); //Temp uint16_t Temp;uint16_t Temp_Thre; TempDS18B20_Get_Temp()/10; } extern uint16_t key_dly;uint8_t key0; void key_task() { if(key_dly10) return; key_dly0; keykey_scan(); switch(key) { case 1:if(mode0){configue_index(configue_index1)%3;}break; case 2: if(configue_index0mode0){PH_ThrePH_Thre0.1;;if(PH_Thre12)PH_Thre2;} if(configue_index1mode0){CO_Thre;if(CO_Thre999)CO_Thre50;} if(configue_index2mode0){Temp_Thre;if(Temp_Thre39)Temp_Thre10;} break; case 3: if(configue_index0mode0){PH_ThrePH_Thre-0.1;;if(PH_Thre2)PH_Thre12;} if(configue_index1mode0){CO_Thre--;if(CO_Thre50)CO_Thre999;} if(configue_index2mode0){Temp_Thre--;if(Temp_Thre10)Temp_Thre39;} break; case 4:mode(mode1)%2; break; } }5.1.2 GPS代码void BSP_GPS_IRQHandler(uint8_t dat) { // 第一步严格边界检查避免数组越界核心修复 if(GPSRX_LEN (GPSRX_LEN_MAX - 1)) // 留1位冗余避免越界 { GPSRX_LEN 0; memset((void*)GPSRX_BUFF, 0, GPSRX_LEN_MAX); } // 第二步缓存数据 if(dat $) // 帧头重置缓存 { GPSRX_LEN 0; memset((void*)GPSRX_BUFF, 0, GPSRX_LEN_MAX); } GPSRX_BUFF[GPSRX_LEN] dat; // 第三步判断GPRMC帧接收完成\n为帧尾 if(GPSRX_BUFF[0] $ strncmp((char*)GPSRX_BUFF[1], GPRMC, 4) 0) { if(dat \n GPSRX_LEN GPRMC_FRAME_MAX_LEN) // 限制帧长度避免超长 { // 中断中仅标记数据就绪解析放到主循环减少中断耗时 memcpy(Save_Data.GPS_Buffer, (char*)GPSRX_BUFF, GPSRX_LEN); Save_Data.GPS_Buffer[GPSRX_LEN] \0; // 强制加字符串结束符 Save_Data.isGetData 1; // 重置缓存 GPSRX_LEN 0; memset((void*)GPSRX_BUFF, 0, GPSRX_LEN_MAX); } } } /****************************************************************** * 函 数 名 称parseGpsBuffer * 函 数 说 明解析GPS发送过来的数据 * 函 数 形 参无 * 函 数 返 回无 * 作 者LC * 备 注无 ******************************************************************/ void parseGpsBuffer(void) { char *subString; char *subStringNext; char i 0; if (Save_Data.isGetData) { Save_Data.isGetData 0; // printf(**************\r\n); // printf(%s\r\n,Save_Data.GPS_Buffer); for (i 0 ; i 6 ; i) { if (i 0) { if ((subString strstr(Save_Data.GPS_Buffer, ,)) NULL) HAL_Delay(1); // printf(1\r\n); // errorLog(1); //解析错误 } else { subString; if ((subStringNext strstr(subString, ,)) ! NULL) { char usefullBuffer[2]; switch(i) { case 1:memcpy(Save_Data.UTCTime, subString, subStringNext - subString);break; //获取UTC时间 case 2:memcpy(usefullBuffer, subString, subStringNext - subString);break; //获取UTC时间 case 3:memcpy(Save_Data.latitude, subString, subStringNext - subString);break; //获取纬度信息 case 4:memcpy(Save_Data.N_S, subString, subStringNext - subString);break; //获取N/S case 5:memcpy(Save_Data.longitude, subString, subStringNext - subString);break; //获取经度信息 case 6:memcpy(Save_Data.E_W, subString, subStringNext - subString);break; //获取E/W default:break; } subString subStringNext; Save_Data.isParseData 1; if(usefullBuffer[0] A) Save_Data.isUsefull 1; else if(usefullBuffer[0] V) Save_Data.isUsefull 0; } else { // errorLog(2); //解析错误 // printf(2\r\n); HAL_Delay(1); } } } } } /****************************************************************** * 函 数 名 称printGpsBuffer * 函 数 说 明输出解析后的数据 * 函 数 形 参无 * 函 数 返 回无 * 作 者LC * 备 注无 ******************************************************************/ void printGpsBuffer(void) { // uint8_t buff[100]{0}; if (Save_Data.isParseData) { Save_Data.isParseData 0; printf(Save_Data.UTCTime ); printf(%s,Save_Data.UTCTime); printf(\r\n); if(Save_Data.isUsefull) { Save_Data.isUsefull 0; //串口显示纬度 printf(Save_Data.latitude ); printf(%s,Save_Data.latitude); //串口显示 printf(Save_Data.N_S ); printf(%s\r\n,Save_Data.N_S); //串口显示经度 printf(Save_Data.longitude ); printf(%s,Save_Data.longitude); printf(\r\n); //串口显示 printf(Save_Data.E_W ); printf(%s,Save_Data.E_W); printf(\r\n); } else { printf(GPS DATA is not usefull!\r\n); } } } /** * brief GPS度分格式字符串转十进制浮点数核心函数 * param dm_str: 度分格式字符串如经度11257.61501、纬度3438.64999 * param is_longitude: 标记是否为经度1经度0纬度 * retval 转换后的十进制浮点数如经度返回112.9602501纬度返回34.6441665 * note 经度规则前3位度剩余分纬度规则前2位度剩余分 */ float gps_str_to_float(char *dm_str, uint8_t is_longitude) { // 1. 异常处理空字符串/NULL直接返回0.0 if(dm_str NULL || strlen(dm_str) 0) { return 0.0f; } float degree 0.0f; // 度 float minute 0.0f; // 分 char degree_str[4] {0}; // 存度的字符串经度最多3位纬度最多2位 char minute_str[15] {0}; // 存分的字符串预留足够长度 uint16_t dm_len strlen(dm_str); // 2. 按经度/纬度拆分度和分核心逻辑 if(is_longitude) // 经度前3位为度剩余为分 { // 经度字符串长度至少3位如112否则格式错误 if(dm_len 3) { return 0.0f; } // 提取度的部分前3位 memcpy(degree_str, dm_str, 3); // 提取分的部分3位之后的所有字符 strncpy(minute_str, dm_str 3, dm_len - 3); } else // 纬度前2位为度剩余为分 { // 纬度字符串长度至少2位如34否则格式错误 if(dm_len 2) { return 0.0f; } // 提取度的部分前2位 memcpy(degree_str, dm_str, 2); // 提取分的部分2位之后的所有字符 strncpy(minute_str, dm_str 2, dm_len - 2); } // 3. 字符串转浮点数度和分 degree atof(degree_str); // 度转数字 minute atof(minute_str); // 分转数字 // 4. 计算十进制经纬度十进制 度 分/60 return (degree minute / 60.0f); }头文件#ifndef _GPS_H #define _GPS_H #include bsp_config.h //定义数组长度 #define GPS_Buffer_Length 80 #define UTCTime_Length 11 #define latitude_Length 11 #define N_S_Length 2 #define longitude_Length 12 #define E_W_Length 2 #define GPSRX_LEN_MAX 255 typedef struct SaveData { char GPS_Buffer[GPS_Buffer_Length]; char isGetData; //是否获取到GPS数据 char isParseData; //是否解析完成 char UTCTime[UTCTime_Length]; //UTC时间 char latitude[latitude_Length]; //纬度 char N_S[N_S_Length]; //N/S char longitude[longitude_Length]; //经度 char E_W[E_W_Length]; //E/W char isUsefull; //定位信息是否有效 } _SaveData; void BSP_GPS_IRQHandler(uint8_t dat); void parseGpsBuffer(void); void printGpsBuffer(void); float gps_str_to_float(char *dm_str, uint8_t is_longitude); #endif5.2手机app蓝牙采用蓝牙调试器开源软件详细的使用流程以及下载连接可以参考我的下面一篇文章的的连接。手把手教你一步步使用基于HAL库的STM32的蓝牙手机控制以及蓝牙调试器APP的使用_蓝牙调试器,有按钮操控的-CSDN博客蓝牙调试器手机APP链接https://share.weiyun.com/DH1jVmez 密码uphi7m六、工程主要内容工程的具体内容包括但不限于原理图、程序代码、流程图等等。完整工程、原理图、APP 源码我已经开源到 Gitee需要的同学可以评论区留言或者私信005-水产运输水质监控系统设计(GPS、蓝牙).