前言:最近在做一个小项目,这个东西也是我在寒假的时候刷到视频,有那种跑腿平台让人上门照顾宠物的,还比较火爆,我也从这里灵感触发,想着做个这个东西,正好参加一个比赛,一举两得,做这个还是学到了许多东西,我在网上查找,确实已经有这个玩意的落地,但是仅限于智能喂食,而我在此基础上进行创新,进行了一些扩展与延伸。
1.主板选用stm32mini,因为我这个其实不用太考虑占不占地方。
2.模块部分
(1)温湿度模块 :DHT11
0011 0101 0000 0000 0001 1000 0000 0100 0101 0001
湿度高 8 位 湿度低 8 位 温度高 8 位 温度低 8 位 校验位
校验位为“8bit 湿度整数数据 + 8bit 湿度小数数据 + 8bit 温度整数数据 + 8bit 温度小数数据” 8bit 校验位等于所得结果的末 8 位
初始化:
u8 DHT11_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //使能PG端口时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12; //PG11端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //初始化IO口
GPIO_SetBits(GPIOB,GPIO_Pin_1); //PG11 输出高
DHT11_Rst(); //复位DHT11
return DHT11_Check();//等待DHT11的回应
}
结构体里设置所需变量
typedef struct
{
uint8_t humi_int; //湿度的整数部分
uint8_t humi_deci; //湿度的小数部分
uint8_t temp_int; //温度的整数部分
uint8_t temp_deci; //温度的小数部分
uint8_t check_sum; //校验和
}DHT11_Data_TypeDef;
将数据输入到对应的变量里面
uint8_t Read_DHT11(DHT11_Data_TypeDef *DHT11_Data)
{
u8 buf[5];
u8 i;
DHT11_Rst();
if(DHT11_Check()==0)
{
for(i=0;i<5;i++)//读取40位数据
{
buf[i]=DHT11_Read_Byte();
}
if((buf[0]+buf[1]+buf[2]+buf[3])==buf[4])
{
DHT11_Data->humi_int=buf[0];
DHT11_Data->humi_deci=buf[1];
DHT11_Data->temp_int=buf[2];
DHT11_Data->temp_deci=buf[3];
DHT11_Data->check_sum=buf[4];
}
}else return 1;
/*检查读取的数据是否正确*/
if(DHT11_Data->check_sum == DHT11_Data->humi_int + DHT11_Data->humi_deci + DHT11_Data->temp_int+ DHT11_Data->temp_deci)
return SUCCESS;
else
return ERROR;
}
(2)MQ-2烟雾传感器
借助ADC将当前获取的24位数据转换为当前电压的模拟信号,然后借助公式转换为当前的室内CH4浓度。
初始化:
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_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的软件转换启动功能
}
计算当前的CH4
float MQ7_PPM_Calibration(void)
{
float RS = 0;
float R0 = 0;
RS = (3.3f - Get_Adc_Average(ADC_Channel_1,30)) / Get_Adc_Average(ADC_Channel_1,30) * RL;
R0 = RS / pow(CAL_PPM / 98.322, 1 / -1.458f);//CAL_PPM 10 // 校准环境中PPM值
return R0;
}
//计算Smog_ppm
float Smog_GetPPM(void)
{
float RS = (3.3f - Get_Adc_Average(ADC_Channel_1,30)) / Get_Adc_Average(ADC_Channel_1,30) * RL;
float ppm = 98.322f * pow(RS/R0, -1.458f);
return ppm;
}
注: RL:每个传感器的数值不一样,需要查看对应的数据手册
算Ro的时候,注销到Smog_GetPPM(void)代码,算出标准的RO后注销掉MQ7代码,然后使用Smog_GetPPM(void)。然后更改define值。
(3)舵机控制[PWM控制]
初始化:
void TIM2_PWM_Init(u16 arr, u16 psc)
{
GPIO_InitTypeDef GPIO_InitStructure; //定义一个引脚初始化的结构体
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStrue; //定义一个定时中断的结构体
TIM_OCInitTypeDef TIM_OCInitTypeStrue; //定义一个PWM输出的结构体
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能GPIOA时钟,在STM32中使用IO口前都要使能对应时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //使能通用定时器2时钟,A0引脚对应TIM2CHN1
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;//引脚0
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP; //复用推挽输出模式,定时器功能为A0引脚复用功能
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz; //定义该引脚输出速度为50MHZ
GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化引脚GPIOA0
TIM_TimeBaseInitStrue.TIM_Period=arr; //计数模式为向上计数时,定时器从0开始计数,计数超过到arr时触发定时中断服务函数
TIM_TimeBaseInitStrue.TIM_Prescaler=psc; //预分频系数,决定每一个计数的时长
TIM_TimeBaseInitStrue.TIM_CounterMode=TIM_CounterMode_Up; //计数模式:向上计数
TIM_TimeBaseInitStrue.TIM_ClockDivision=0; //一般不使用,默认TIM_CKD_DIV1
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStrue); //根据TIM_TimeBaseInitStrue的参数初始化定时器TIM2
TIM_OCInitTypeStrue.TIM_OCMode=TIM_OCMode_PWM1; //PWM模式1,当定时器计数小于TIM_Pulse时,定时器对应IO输出有效电平
TIM_OCInitTypeStrue.TIM_OCPolarity=TIM_OCNPolarity_High; //输出有效电平为高电平
TIM_OCInitTypeStrue.TIM_OutputState=TIM_OutputState_Enable; //使能PWM输出
TIM_OCInitTypeStrue.TIM_Pulse = 0; //设置待装入捕获比较寄存器的脉冲值
TIM_OC1Init(TIM2, &TIM_OCInitTypeStrue); //根TIM_OCInitTypeStrue参数初始化定时器2通道1
TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Disable); //CH1预装载使能
TIM_ARRPreloadConfig(TIM2, ENABLE); //CH1预装载使能
TIM_Cmd(TIM2, ENABLE); //使能定时器TIM2
}
然后通过TIM_SetCompare1(TIM2, PWM)和 TIM_SetCompare1(TIM2, PWM)来设置舵机的角度,实现食物的释放。
(4)HX711模块称重+压力传感器
初始化:
有一个地方困扰了我一个礼拜,害我白白又买了一个,我的IO口初始化,才开始是PB3和PB4,使得传感器传回来的数值一直都是最大值,也就是一直都是错误的。
后来发现这两个是特殊的IO口【手动哭泣】,具体可以看看这篇文章
STM32F1系列PB3,PB4,PA13,PA14,PA15用作普通IO口的特殊配置_stm32 pa14_qhw5279的博客-CSDN博客
void Init_HX711pin(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //使能PF端口时钟
//HX711_SCK
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; // 端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO口速度为50MHz
GPIO_Init(GPIOB, &GPIO_InitStructure); //根据设定参数初始化GPIOB
//HX711_DOUT
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//输入上拉
GPIO_Init(GPIOB, &GPIO_InitStructure);
//GPIO_SetBits(GPIOB,GPIO_Pin_0); //初始化设置为0
}
测量的时候,需要有个起始重量,俗称毛重,任何变化都是基于毛重进行计算的。
所以在程序烧录进去的时候,大家需要保持上面无重量,这样毛重就是0,方便接下来的操作
void Get_Weight(void)
{
HX711_Buffer = HX711_Read();
if(HX711_Buffer > Weight_Maopi)
{
Weight_Shiwu = HX711_Buffer;
Weight_Shiwu = Weight_Shiwu - Weight_Maopi; //获取实物的AD采样数值。
Weight_Shiwu = (s32)((float)Weight_Shiwu/GapValue); //计算实物的实际重量
//因为不同的传感器特性曲线不一样,因此,每一个传感器需要矫正这里的GapValue这个除数。
//当发现测试出来的重量偏大时,增加该数值。
//如果测试出来的重量偏小时,减小改数值。
}
else
Weight_Shiwu = 1;
}
(5)定时器计时
void TIM3_Int_Init(u16 arr,u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //时钟使能
TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值 计数到5000为500ms
TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值 10Khz的计数频率
TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
TIM_ITConfig( //使能或者失能指定的TIM中断
TIM3, //TIM3
TIM_IT_Update ,
ENABLE //使能
);
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; //TIM3中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //先占优先级0级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //从优先级3级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
TIM_Cmd(TIM3, ENABLE); //使能TIMx外设
}
在定时器的中断里面,到设定的时间到了就进行定时喂食操作,所以在中断里面上传的是类似于flag的标志位,然后传入到主函数进行对应的操作。
(6)蜂鸣器【提醒这个程序开始了】
void Beep_init(void)
{
GPIO_InitTypeDef GPIO_INITstrcture;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_INITstrcture.GPIO_Pin = GPIO_Pin_11;
GPIO_INITstrcture.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_INITstrcture.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_INITstrcture);
GPIO_ResetBits(GPIOB,GPIO_Pin_11);
}
void fengmingqi()
{
GPIO_SetBits(GPIOB,GPIO_Pin_11);
delay_us(100);
GPIO_ResetBits(GPIOB,GPIO_Pin_11);
}
(7)esp8266使用MQTT协议连接到onenet平台
因为后续还需要数据的发送,所以需要使用MQTT协议,如果不需要则可以使用HTTP协议
设置AT模式,初始化当前wifi模块
AT+CWMODE == 设置为STA模式
AP模式下,WiFi模块产生热点,提供无线接入服务,允许其它无线设备接入,提供数据访问,一般的无线路由/网桥工作在该模式下。该模式对应TCP传输协议中的服务端(TCP Server)。
STA模式下,WiFi模块为连接到无线网络的终端(站点),可以连接到AP,一般无线网卡工作在STA模式下。该模式对应TCP传输协议中的客户端(TCP Client)。
AT+CWDHCP=x,y dhcp,y=0关闭,1开启;x为0时是ap,1是station, 2是二者同时。
AT+CWJAP="SSID","PWD" 当作为station模式时,加入WIFI热点SSID,PWD是热点密码【这里注意当前的所连接的热点必须为2.4GHZ的频段】
AT+CIPSTART : 建立tcp连接
void ESP8266_Init(void)
{
GPIO_InitTypeDef GPIO_Initure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//ESP8266复位引脚
GPIO_Initure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Initure.GPIO_Pin = GPIO_Pin_13; //GPIOC13-复位
GPIO_Initure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_Initure);
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET);
delay_ms(250);
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);
delay_ms(500);
ESP8266_Clear();
// UsartPrintf(USART_DEBUG, "1. AT\r\n");
printf("1. AT\r\n");
while(ESP8266_SendCmd("AT\r\n", "OK"))
delay_ms(500);
printf("2. CWMODE\r\n");
while(ESP8266_SendCmd("AT+CWMODE=1\r\n", "OK"))
delay_ms(500);
printf( "3. AT+CWDHCP\r\n");
while(ESP8266_SendCmd("AT+CWDHCP=1,1\r\n", "OK"))
delay_ms(500);
printf("4. CWJAP\r\n");
while(ESP8266_SendCmd(ESP8266_WIFI_INFO, "GOT IP"))
delay_ms(500);
printf( "5. CIPSTART\r\n");
while(ESP8266_SendCmd(ESP8266_ONENET_INFO, "CONNECT"))
delay_ms(500);
printf("6. ESP8266 Init OK\r\n");
}
然后将esp8266连接当前的在onenet创建的设备
_Bool OneNet_DevLink(void)
{
MQTT_PACKET_STRUCTURE mqttPacket = {NULL, 0, 0, 0}; //协议包
unsigned char *dataPtr;
_Bool status = 1;
//打印一下信息产品id,鉴权信息,设备ID
printf("OneNet_DevLink\r\nPROID: %s, AUIF: %s, DEVID:%s\r\n", PROID, AUTH_INFO, DEVID);
if(MQTT_PacketConnect(PROID, AUTH_INFO, DEVID, 256, 0, MQTT_QOS_LEVEL0, NULL, NULL, 0, &mqttPacket) == 0)
{
ESP8266_SendData(mqttPacket._data, mqttPacket._len); //上传平台
dataPtr = ESP8266_GetIPD(250); //等待平台响应
printf("\r\ndataPtr=%s\r\n",dataPtr);
if(dataPtr != NULL)//如果平台返回数据不为空则
{
if(MQTT_UnPacketRecv(dataPtr) == MQTT_PKT_CONNACK)// MQTT数据接收类型判断(connack报文)2
{
switch(MQTT_UnPacketConnectAck(dataPtr))//打印是否连接成功及连接失败的原因
{
case 0:printf( "Tips: 连接成功\r\n");status = 0;break;
case 1:printf( "WARN: 连接失败:协议错误\r\n");break;
case 2:printf( "WARN: 连接失败:非法的clientid\r\n");break;
case 3:printf( "WARN: 连接失败:服务器失败\r\n");break;
case 4:printf( "WARN: 连接失败:用户名或密码错误\r\n");break;
case 5:printf( "WARN: 连接失败:非法链接(比如token非法)\r\n");break;
default:printf( "ERR: 连接失败:未知错误\r\n");break;
}
}
}
MQTT_DeleteBuffer(&mqttPacket); //删包
}
else
printf( "WARN: MQTT_PacketConnect Failed\r\n");
return status;
}
对应的鉴权信息,设备ID,以及用户ID,需要可以自行在onenet平台查询。连接之后便可以在发送数据流给平台了。
发送数据给onenet
void OneNet_SendData(void)
{
MQTT_PACKET_STRUCTURE mqttPacket = {NULL, 0, 0, 0}; //协议包
char buf[128];
short body_len = 0, i = 0;
printf( "Tips: OneNet_SendData-MQTT\r\n");
memset(buf, 0, sizeof(buf));//清空数组内容
body_len = OneNet_FillBuf(buf); //获取当前需要发送的数据流的总长度
if(body_len)
{
if(MQTT_PacketSaveData(DEVID, body_len, NULL, 5, &mqttPacket) == 0) //封包
{
for(; i < body_len; i++)
mqttPacket._data[mqttPacket._len++] = buf[i];
ESP8266_SendData(mqttPacket._data, mqttPacket._len); //上传数据到平台
printf( "Send %d Bytes\r\n", mqttPacket._len);
MQTT_DeleteBuffer(&mqttPacket); //删包
}
else
printf( "WARN: EDP_NewBuffer Failed\r\n");
}
}
将OneNet_FillBuf(buf)获取的需要发送给onenet平台数据的数组长度计算出来,传入到ESP8266_SendData(mqttPacket._data, mqttPacket._len)
nsigned char OneNet_FillBuf(char *buf)
{
char text[64];
//LED0_FLAG=GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_5);//读取LED的开关状态(即对应引脚的)
//LED1_FLAG=GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_5);
//printf("LED0_FLAG_TYPE=%d\n",sizeof(LED0_FLAG));
memset(text, 0, sizeof(text));
strcpy(buf, ",;");
memset(text, 0, sizeof(text));
sprintf(text, "Tempreture,%d.%d;",temperatureH,temperatureL);
strcat(buf, text);
memset(text, 0, sizeof(text));
sprintf(text, "Humidity,%d.%d;", humidityH,humidityH);
strcat(buf, text);
// memset(text, 0, sizeof(text));
// sprintf(text, "now_weight;", LED0_FLAG);
// strcat(buf, text);
//
// memset(text, 0, sizeof(text));
// sprintf(text, "LED1,%d;", LED1_FLAG);
// strcat(buf, text);
memset(text, 0, sizeof(text));
sprintf(text, "P_M,%f;",pm);
strcat(buf, text);
memset(text, 0, sizeof(text));
sprintf(text, "W_S,%d;",Weight_Shiwu);
strcat(buf, text);
printf("buf_mqtt=%s\r\n",buf);
return strlen(buf);
}
然后将“发送命令”发送给esp8266,随后当esp8266在串口接收到了“>”的字符,则可以开始发送数据了。如果不使用这个发送命令的指令,当数据比较多的时候以及次数多的时候,这样会比较麻烦,但是这样,只需要一次,就可以一直发送指令和数据,减少了不必要的麻烦。
void ESP8266_SendData(unsigned char *data, unsigned short len)
{
char cmdBuf[32];
ESP8266_Clear(); //清空接收缓存
//先发送要发送数据的指令做准备
sprintf(cmdBuf, "AT+CIPSEND=%d\r\n", len); //发送命令
if(!ESP8266_SendCmd(cmdBuf, ">")) //收到‘>’时可以发送数据
{
//既然准备完毕即可开始发送数据
Usart_SendString(USART2, data, len); //发送设备连接请求数据
}
}
(8)onenet发送json命令,32设备进行接收
设置一个data的数据包,里面存放需要发送的变量的数值,这样可以实现一次性发送多个数据流。而且进行了判错,当用户未发送重量的时候,我也可以根据我设定的初始值传入设备,防止程序卡死。
object=cJSON_GetObjectItem(root,"data");
item=cJSON_GetObjectItem(object,"time"); //客户端定时下发的定时时间
times_ing=item->valueint;
item=cJSON_GetObjectItem(object,"weight1"); //自主喂养的重量
if(item->valueint<-66666666)
{
weight1 = 300; //防止用户没有设置重量
}
else
{
weight1=item->valueint;
}
item=cJSON_GetObjectItem(object,"weight2"); //定时喂养的重量
if(item->valueint<-66666666)
{
weight2 = 300; //防止用户没有设置重量
}
else
{
weight2=item->valueint;
}
item=cJSON_GetObjectItem(object,"flag"); //是否定时
weishi_flag=item->valueint;
//printf("weishi_flag = %d,weight2 = %d\r\n",weishi_flag,weight2);
delay_ms(10);
printf("\r\ntime=%d,weight1=%d\r\n,weight2=%d\r\n,flag = %d",times_ing,weight1,weight2,weishi_flag);
}
onenet发送数据的格式为:
{"data":{"flag":1,"weight1":300,"weight2":288......}}
后续我还会增加功能【未完待续!!!】