> 技术文档 > 物联网农业大棚系统

物联网农业大棚系统


该系统是作者上手嵌入式的第一个小项目,代码完成周期大概花了一周,使用的是RT-Thread的操作系统,有需要的小伙伴可以选择性摘取,部分博主认为写得不够好的地方,博主也会放上去,但是这些地方博主会备注谨慎使用,实在需要的朋友可以看看思路或者截取该片段私信我讨论,同时代码中涉及到的模块初始化函数博主后续会一一发表博客在该专栏,有需要的朋友可以耐心等待,也可直接私信博主,大家共同成长。

一、系统功能需求

二、主函数介绍

int main(void){LED_Init();//LED初始化函数BH1750_InitI2C();//光照传感器初始化SHT30_Init();//温湿度传感器初始化MOTOR_Init();//电机初始化MYIIC_Init();//IIC初始化My_RTC_Init();//RTC时钟初始化initQueue(&q);//初始化队列,用于断网储存数据light_value=MYAT24CXX_ReadLenByte(0,2);//读取寄存器上的值T_value1=MYAT24CXX_ReadLenByte(2,2);T_value2=MYAT24CXX_ReadLenByte(4,2);TIM2_PWM_Init(500-1,84-1);LED2=0;STEP_MOTOR_Init();//步进电机初始化LCD_Init();//LCD初始化LCD_Clear(WHITE);atk_8266_wifista_config();//esp8266wifi模块初始化NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置系统中断优先级分组2rt_thread_delay(100);RTC_Set_WakeUp(RTC_WakeUpClock_CK_SPRE_16bits,0);lcd_thread_init();//lcd显示线程data_thread_init();//数据处理线程data_collecte();//数据收集线程thread_wificmd();//wifi指令解析线程return 0;}

主函数中共有四个rtthread处理线程,每个线程按优先级轮询有秩序的处理工作,保证的了实时性,这是RT-Thread和FreeRTOS两大操作系统的特点之一。

三、LCD显示线程介绍

rt_thread_t lcd = RT_NULL;int lcd_thread_init(void){lcd = rt_thread_create(\"lcd\",//线程名称 lcd_entry,//线程入口函数 RT_NULL,线程入口函数参数 512,线程栈大小,单位字节 2,//线程优先级大小 20);//线程的时间片大小rt_thread_startup(lcd);//启动线程return 0;}void lcd_entry(void *parameter){while(1){RTC_GetTime(RTC_Format_BIN,&RTC_TimeStruct);//RTC时间获取 sprintf((char*)temp,\"%02d:%02d:%02d\",RTC_TimeStruct.RTC_Hours,RTC_TimeStruct.RTC_Minutes, RTC_TimeStruct.RTC_Seconds); LCD_ShowString(10,55,200,24,24,(u8*)temp);RTC_GetDate(RTC_Format_BIN, &RTC_DateStruct);sprintf((char*)temp,\"20%02d-%02d- %02d\",RTC_DateStruct.RTC_Year,RTC_DateStruct.RTC_Month,RTC_DateStruct.RTC_Date);//显示年月日 LCD_ShowString(10,30,200,24,24,(u8*)temp);sprintf((char*)temp,\"Weekday:%d\",RTC_DateStruct.RTC_WeekDay); //显示星期几LCD_ShowString(10,80,200,24,24,(u8*)temp);sprintf(temp,\"Lvalue:%d \",light_value);//显示光照阈值LCD_ShowString(10,120,200,24,24,(u8*)temp);sprintf(temp,\"light:%d \",light);//显示采集的光照值LCD_ShowString(10,145,200,24,24,(u8*)temp);sprintf(temp,\"Tvalue:%.1f-%.1f \",T_value1,T_value2);//显示温度阈值LCD_ShowString(10,180,200,24,24,(u8*)temp);sprintf(temp,\"temperature:%.2f \",Humi[0]);//显示采集温度值1LCD_ShowString(10,205,200,24,24,(u8*)temp);sprintf(temp,\"humility:%.2f \",Humi[1]);//显示采集温度值2LCD_ShowString(10,230,200,24,24,(u8*)temp);sprintf(temp,\"%s\",Mode[mode]);//显示自动或手动模式LCD_ShowString(10,270,200,24,24,(u8*)temp);rt_thread_mdelay(10);//线程挂起} }

四、数据处理线程介绍

int data_thread_init(void){data = rt_thread_create(\"data\", data_entry,RT_NULL, 2048,、 1, 20);rt_thread_startup(data); //3秒定时器线程timer1 = rt_timer_create(\"timer1\", timeout1, RT_NULL, 3000, RT_TIMER_CTRL_SET_PERIODIC);rt_timer_start(timer1); //1秒定时器线程timer2 = rt_timer_create(\"timer2\", timeout2, RT_NULL, 1000, RT_TIMER_CTRL_SET_PERIODIC);rt_timer_start(timer2);}void data_entry(void *parameter){while(1){u3_receive();//解析通过mqtt协议下发的指令light = BH1750_I2C_Read();//采集光照值SHT30_read_result(Humi);//采集温度值rt_thread_mdelay(20);}}//断网存储数据void timeout1(void *parameter){if(net==1){if(isQueueNotEmpty(&q)){num1++;str1 = dequeue(&q);u3_printf(\"%s\\r\\n\",str1);printf(\"出队数据%d次\\r\\n\",num1);num=0;}else{sprintf((char *)text, \"AT+MQTTPUB=0,\\\"%s\\\",\\\"\\\\\\{\\\\\\\"humidity\\\\\\\":\\\\\\\"%.2f\\\\\\\"\\\\\\,\\\\\\\"temperature\\\\\\\":\\\\\\\"%.2f\\\\\\\"\\\\\\,\\\\\\\"light\\\\\\\":\\\\\\\"%d\\\\\\\"\\\\\\,\\\\\\\"datatime\\\\\\\":\\\\\\\"20%02d-%02d-%02d %02d:%02d:%02d\\\\\\\"\\\\\\,\\\\\\\"data_type\\\\\\\":\\\\\\\"1\\\\\\\"\\\\\\}\\\",0,0\", \"receive\", Humi[1], Humi[0], light,RTC_DateStruct.RTC_Year,RTC_DateStruct.RTC_Month,RTC_DateStruct.RTC_Date,RTC_TimeStruct.RTC_Hours,RTC_TimeStruct.RTC_Minutes,RTC_TimeStruct.RTC_Seconds);u3_printf(\"%s\\r\\n\",text);//wifi发送到服务器}}else{num++;num1 = 0;sprintf((char *)str2, \"AT+MQTTPUB=0,\\\"%s\\\",\\\"\\\\\\{\\\\\\\"humidity\\\\\\\":\\\\\\\"%.2f\\\\\\\"\\\\\\,\\\\\\\"temperature\\\\\\\":\\\\\\\"%.2f\\\\\\\"\\\\\\,\\\\\\\"light\\\\\\\":\\\\\\\"%d\\\\\\\"\\\\\\,\\\\\\\"datatime\\\\\\\":\\\\\\\"20%02d-%02d-%02d %02d:%02d:%02d\\\\\\\"\\\\\\,\\\\\\\"data_type\\\\\\\":\\\\\\\"0\\\\\\\"\\\\\\}\\\",0,0\", \"receive\", Humi[1], Humi[0], light,RTC_DateStruct.RTC_Year,RTC_DateStruct.RTC_Month,RTC_DateStruct.RTC_Date,RTC_TimeStruct.RTC_Hours,RTC_TimeStruct.RTC_Minutes,RTC_TimeStruct.RTC_Seconds);enqueue(&q,str2);printf(\"入队数据%d次\\r\\n\",num);}}rt_timer_t timer2;//每一秒在mqtt客户端上发一次心跳void timeout2(void *parameter){sprintf((char*)text2,\"AT+MQTTPUB=0,\\\"%s\\\",\\\"{\\\\\\\"heartbeat\\\\\\\":\\\\\\\"111\\\\\\\"}\\\",0,0\", \"heartbeat\" );u3_printf(\"%s\\r\\n\",text2);//wifi发送到服务器}

1、data_entry线程

这个线程中主要做对数据的采集工作,因为该项目系统涉及的逻辑的并不复杂,所以我将所有关于数据的工作全部放在此线程中进行处理,但如果涉及的逻辑复杂,应该分别做处理,正常的线程管理应该是一个线程处理一个数据,这样便于管理线程,出现问题也更容易找出bug。

2、定时器timer1

定时器timer1主要用于在遇到断网情况的时候,可以将断网时采集的数据保存下来,等检测到网络连接上时再继续上传至mqtt,数据每3秒存储一次,采集的数据里会放在队列中,注意,这里我的队列是顺序队列,会存在队满的情况,由于该场景只是做模拟,所以我模拟断网的时间并不长,我的队列能存大概两个小时就会满,在真实的应用场景下,最好使用环形队列进行存储,如果存储时间过长可能会导致前面存储的数据的被覆盖掉,但至少不会有队满的情况出现。

3、定时器timer2

这个定时器的作用用于定时给mqtt发送心跳,让服务器知道我们的设备还在线。

五、数据收集处理线程

int data_collecte(void){rt_err_t result,result1;result = rt_event_init(&event,\"event\",RT_IPC_FLAG_FIFO);result1 = rt_event_init(&event1,\"event1\",RT_IPC_FLAG_FIFO);rt_thread_init(&data_recv, \"data_recv\",data_recv_event,RT_NULL,&data1_stack[1],sizeof(data1_stack),2,20);rt_thread_startup(&data_recv); rt_thread_init(&data_send, \"data_send\",data_send_event,RT_NULL,&data2_stack[0],sizeof(data2_stack),3,20);rt_thread_startup(&data_send);step_Motor = rt_thread_create(\"step_Motor\", step_Motor_entry,RT_NULL, 512,4,20);rt_thread_startup(step_Motor);timer = rt_timer_create(\"timer\", timeout, RT_NULL, 5000, RT_TIMER_FLAG_PERIODIC);rt_timer_start(timer);return 0;}void data_recv_event(void *param){while(1){rt_uint32_t e;while(1){rt_event_recv(&event,(EVENT_FLAG1 | EVENT_FLAG2 | EVENT_FLAG3), RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR, RT_WAITING_FOREVER,&e);switch(e){case EVENT_FLAG1: Motor_Control_1(0);Motor_Control_2(0);break;case EVENT_FLAG2:Motor_Control_1(2);Motor_Control_2(0);break;case EVENT_FLAG3:Motor_Control_1(2);Motor_Control_2(2);break;}rt_thread_mdelay(20);}}}ALIGN(RT_ALIGN_SIZE)char data2_stack[1024];struct rt_thread data_send;void data_send_event(void *param){while(1){if(mode==0){if(Humi[0]T_value1&&Humi[0]T_value2){if(control_flag3==1){rt_event_send(&event,EVENT_FLAG3);}control_flag3=0;control_flag2=1;control_flag1=1;}}rt_thread_mdelay(5);}}rt_thread_t step_Motor = RT_NULL;void step_Motor_entry(void *parameter){while(1){if(mode==0){if(light>light_value){led_flag=0;while(light_flag==1){STEP++;if(STEP>7)STEP=0;Stepper_SingleStep(STEP,2);}light_flag=2;}else{led_flag=1;while(light_flag==2){if(STEP==0)STEP=8;STEP--;Stepper_SingleStep(STEP,2);}light_flag=1;}}rt_thread_mdelay(1);}}rt_timer_t timer;void timeout(void *parameter){if(mode==0){if(led_flag==0){TIM_SetCompare1(TIM2,0);}if(led_flag==1){TIM_SetCompare1(TIM2,500);}light_flag=0;}}

备注:谨慎使用

此处的逻辑处理我使用了事件集的方式,每达到一个条件我就发送对应的事件,一个事件集对应一个动作,但是这里我让有需要的小伙伴谨慎使用的原因是我认为这里的逻辑写得并不好,容易出现bug,当时博主在运行系统时,这个地方的bug最难改,改到最后就改成了这坨不能动的屎山。按照我们的正常的逻辑,应该是遇到一个条件就执行相应的动作,但是我却使用事件集的方式,这是因为运行事件集的方式是操作系统的一大特点,虽然看起来更复杂了,但是实际上代码运行起来会更流畅,实时性更好。但是需要运用的当才能产生事半功倍的效果,运用不当就会像博主这样写成屎山,话虽如此,但是不管是什么,也一定要用起来,用起来才能对这些有更深刻的理解,现在我还有试错的机会,以后参加工作了肯定就没有这么多试错的机会了。这里的主要内容就是对步进电机正转和反转、两个风扇的控制,以及led的亮灭控制,参考价值不高,但是使用事件集的方式可以看看。

六、wifi指令解析

int thread_wificmd(void){wificmd = rt_thread_create(\"wificmd\",wificmd_entry, RT_NULL,512,1,20);rt_thread_startup(wificmd);}void wificmd_entry(void *parameter){while(1){if(cmd_flag==1){STEP++;if(STEP>7)STEP=0;STEP_MOTOR_8A(STEP,2);}if(cmd_flag==2){if(STEP==0)STEP=8;STEP--;STEP_MOTOR_8A(STEP,2);}if(cmd_flag==3){STEP_MOTOR_OFF();}rt_thread_mdelay(2);}}void u3_receive(void){u8 Res;static int uart_state = 0;u16 len = 0;if(USART3_RX_STA & 0x8000){//接收到了数据 len = USART3_RX_STA & 0X7FFF; USART3_RX_BUF[len] = 0; //添加结束符 USART3_RX_STA = 0;//printf(\"接收%s\\r\\n\",USART3_RX_BUF);if(strstr((char*)USART3_RX_BUF, \"mode\") != NULL) {s_start = strchr((char*)USART3_RX_BUF, \':\');if(strstr((char*)s_start, \"auto\") != NULL){printf(\"手动模式开启,自动模式关闭\\r\\n\");cmd_flag=0;mode=1;Motor_Control_1(0);Motor_Control_2(0);}}if(mode==1){if(s_start=strstr((char*)USART3_RX_BUF, \"LED\")){s_start += 5;s_send = strchr((char*)USART3_RX_BUF,\'}\');s_send -= 0;strncpy(jieshou,s_start,s_send-s_start);jieshou[s_send-s_start]=\'\\0\';printf(\"光照阈值:%d\\r\\n\",led);led = atoi(jieshou);TIM_SetCompare1(TIM2,500-led);printf(\"可调灯:%s\\r\\n\",jieshou);}if(strstr((char*)USART3_RX_BUF, \"mode\") != NULL){s_start = strchr((char*)USART3_RX_BUF, \':\');if(strstr((char*)s_start, \"AF1\") != NULL){Motor_Control_1(1);printf(\"风扇一:1档开启\\r\\n\");}}if(strstr((char*)USART3_RX_BUF, \"mode\") != NULL){s_start = strchr((char*)USART3_RX_BUF, \':\');if(strstr((char*)s_start, \"DF1\") != NULL){Motor_Control_1(0);printf(\"风扇一:关闭\\r\\n\");}}if(strstr((char*)USART3_RX_BUF, \"mode\") != NULL){s_start = strchr((char*)USART3_RX_BUF, \':\');if(strstr((char*)USART3_RX_BUF, \"AF2\") != NULL){Motor_Control_1(2);printf(\"风扇一:2档开启\\r\\n\");}}if(strstr((char*)USART3_RX_BUF, \"mode\") != NULL){s_start = strchr((char*)USART3_RX_BUF, \':\');if(strstr((char*)USART3_RX_BUF, \"AF3\") != NULL){Motor_Control_1(3);printf(\"风扇一:3档打开\\r\\n\");}}if(strstr((char*)USART3_RX_BUF, \"mode\") != NULL){s_start = strchr((char*)USART3_RX_BUF, \':\');if(strstr((char*)s_start, \"BF1\") != NULL){Motor_Control_2(1);printf(\"风扇二:1档开启\\r\\n\");}}if(strstr((char*)USART3_RX_BUF, \"mode\") != NULL){s_start = strchr((char*)USART3_RX_BUF, \':\');if(strstr((char*)s_start, \"DF2\") != NULL){Motor_Control_2(0);printf(\"风扇二:关闭\\r\\n\");}}if(strstr((char*)USART3_RX_BUF, \"mode\") != NULL){s_start = strchr((char*)USART3_RX_BUF, \':\');if(strstr((char*)USART3_RX_BUF, \"BF2\") != NULL){Motor_Control_2(2);printf(\"风扇二:2档开启\\r\\n\");}}if(strstr((char*)USART3_RX_BUF, \"mode\") != NULL){s_start = strchr((char*)USART3_RX_BUF, \':\');if(strstr((char*)USART3_RX_BUF, \"BF3\") != NULL){Motor_Control_2(3);printf(\"风扇二:3档打开\\r\\n\");}}if(strstr((char*)USART3_RX_BUF, \"mode\") != NULL){s_start = strchr((char*)USART3_RX_BUF, \':\');if(strstr((char*)USART3_RX_BUF, \"CFR\") != NULL){cmd_flag=1;printf(\"步进电机正转\\r\\n\");}}if(strstr((char*)USART3_RX_BUF, \"mode\") != NULL){s_start = strchr((char*)USART3_RX_BUF, \':\');if(strstr((char*)USART3_RX_BUF, \"CFL\") != NULL){cmd_flag=2;printf(\"步进电机反转\\r\\n\");}}if(strstr((char*)USART3_RX_BUF, \"mode\") != NULL){s_start = strchr((char*)USART3_RX_BUF, \':\');if(strstr((char*)USART3_RX_BUF, \"DF3\") != NULL){cmd_flag=3;printf(\"步进电机停止\\r\\n\");}}if(strstr((char*)USART3_RX_BUF, \"mode\") != NULL){s_start = strchr((char*)USART3_RX_BUF, \':\');if(strstr((char*)s_start, \"stop\") != NULL){printf(\"手动模式关闭,自动模式开启\\r\\n\");mode=0;}}}//接收字符解析,改变阈值if(s_start=strstr((char*)USART3_RX_BUF,\"temp1\")){s_start += 7;s_send = strchr((char*)USART3_RX_BUF,\'}\');s_send -= 0;strncpy(jieshou,s_start,s_send-s_start);jieshou[s_send-s_start]=\'\\0\';printf(\"温度阈值1:%s\\r\\n\",jieshou);T1 = (uint16_t)atoi(jieshou);if(T1>T_value2){printf(\"error\\r\\n\");}else{T_value1=T1;MYAT24CXX_WriteLenByte(2,(uint32_t)T_value1,2);}}if(s_start=strstr((char*)USART3_RX_BUF,\"temp2\")){s_start += 7;s_send = strchr((char*)USART3_RX_BUF,\'}\');s_send -= 0;strncpy(jieshou,s_start,s_send-s_start);jieshou[s_send-s_start]=\'\\0\';printf(\"温度阈值2:%s\\r\\n\",jieshou);T2 = (uint16_t)atoi(jieshou);if(T2<T_value1){printf(\"error\\r\\n\");}else{T_value2=T2;MYAT24CXX_WriteLenByte(4,(uint32_t)T_value2,2);}}if(s_start=strstr((char*)USART3_RX_BUF,\"light\")){s_start += 7;s_send = strchr((char*)USART3_RX_BUF,\'}\');s_send -= 0;strncpy(jieshou,s_start,s_send-s_start);jieshou[s_send-s_start]=\'\\0\';printf(\"光照阈值:%s\\r\\n\",jieshou);light_value = (float)atoi(jieshou);MYAT24CXX_WriteLenByte(0,(uint32_t)light_value,2);}if(s_start=strstr((char*)USART3_RX_BUF, \"time\")){s_start += 7;s_send = strchr((char*)USART3_RX_BUF,\'}\');s_send -= 1;strncpy(jieshou,s_start,s_send-s_start);jieshou[s_send-s_start]=\'\\0\';printf(\"%s\\r\\n\",jieshou);h_get[0]=jieshou[0];h_get[1]=jieshou[1];m_get[0]=jieshou[3];m_get[1]=jieshou[4];s_get[0]=jieshou[6];s_get[1]=jieshou[7];printf(\"%s %s %s\\n\",h_get,m_get,s_get);//sscanf(jieshou,\"%s:%s:%s\\n\",&h_get,&m_get,&s_get);h_get1=atoi(h_get);m_get1=atoi(m_get);s_get1=atoi(s_get);printf(\"%d %d %d\\r\\n\",h_get1,m_get1,s_get1);RTC_Set_Time(h_get1,m_get1,s_get1,RTC_H12_AM);}memset(USART3_RX_BUF,0,sizeof(USART3_RX_BUF));}}

该部分的指令解析我并没有完全放在线程中,而是大部分解析内容是封装在u3_receive函数中,而线程中只是做了对步进电机正转和反转的一个解析,该部分最重要的部分是解析方式,但是我认为解析指令的方式还可以写得更简单一些,如上代码,因为mqtt中得JSON格式是比较复杂的,不是我们直接看到的样子,其中还存在很多转义符,有兴趣的小伙伴可以通过一步一步调试,打开串口的窗口可以看到串口里接收的字符,里面的内容就是完整的json内容。关于解析指令,我是直接通过strstr函数找到关键词所在的位置,然后用指针指向关键词后的冒号,再让指针加1,取出指令解析。

七、成品展示

成品正面演示:

成品内部接线:

客户端界面:

备注:客户端界面是由博主队友做的,有需要的小伙伴可以私信博主索要源码。