【物联网开发】基于STM32和阿里云的室内温、湿、亮度监测系统(四)—— 设备端的其他模块开发

这是我的物联网开发系列文章,将介绍如何从嵌入式开发、云平台开发、Android端开发来实现一个简单的物联网应用开发,体验物联网全栈开发的过程,积累开发的经验。
本篇文章为系列文章第四篇,主要介绍OLED模块、ESP-12S的开发,使用MQTT协议连接至阿里云,实现与云端的上下行通信。
系列文章第一篇:物联网介绍和系统初步设计
系列文章第二篇:配置阿里云物联网平台及设备端连接测试
系列文章第三篇:设备端的传感器开发(DHT11和光敏电阻传感器)

一、OLED模块开发

1.简介

OLED现在是市面上最常用的显示设备,许多电视和液晶显示器都是采用OLED技术的。我们这里使用的OLED模块是0.96寸OLED显示屏(SPI IIC 7针),既可以使用SPI方式进行控制,也可以使用IIC方式进行控制。我们这里使用的是SPI进行开发。这款OLED的点阵是128×64的,我们显示的内容要控制在它的长宽之间。要控制OLED的显示,实际上就是将我们要显示的内容转换为列行式,这些列行式就代表了点阵中的那些点是需要点亮的,将列行式的数据写入OLED的寄存器中,就可以达到控制其显示的内容的目的。
OLED的七个引脚的连线如下:

  • GND —> GND
  • VCC —> 3.3V
  • DO —> SPI_SCK
  • D1 —> SPI_MOSI
  • RES —> OLED_RES
  • DC —> OLED_DC
  • CS —> OLED_CS

2.使用字模软件生成字模数据

字模软件可以在这里下载:下载链接。字模软件的使用比较简单,里面有对字模软件使用的介绍,这里我就不赘述了。生成的字模数据要保存在oledfont.h文件的数组中,方便我们调用。我放在码云Gitee上的驱动代码(代码链接见下面)已经有这个项目所需要的文字的字模数据。
在这里插入图片描述

3.开发代码

由于OLED的文件代码较多,所以我放到了码云Gitee上面,需要的小伙伴可以到这里下载:https://gitee.com/peanuo/OLED.git

下面这个代码是oled.h中的,里面定义了OLED的各个端口以及使用的SPI,也声明了各种OLED的使用的基本函数。

#ifndef __OLED_H
#define __OLED_H 
 
#include "main.h"
#include "gpio.h"
#include "stdlib.h"	
#include "stm32f1xx_hal.h"
//-----------------OLED端口定义---------------- 
 
 
#define OLED_RES_Clr() HAL_GPIO_WritePin(OLED_RES_GPIO_Port,OLED_RES_Pin,GPIO_PIN_RESET)//RES
#define OLED_RES_Set() HAL_GPIO_WritePin(OLED_RES_GPIO_Port,OLED_RES_Pin,GPIO_PIN_SET)
 
#define OLED_DC_Clr() HAL_GPIO_WritePin(OLED_DC_GPIO_Port,OLED_DC_Pin,GPIO_PIN_RESET)//DC
#define OLED_DC_Set() HAL_GPIO_WritePin(OLED_DC_GPIO_Port,OLED_DC_Pin,GPIO_PIN_SET)
 		     
#define OLED_CS_Clr()  HAL_GPIO_WritePin(OLED_CS_GPIO_Port,OLED_CS_Pin,GPIO_PIN_RESET)//CS
#define OLED_CS_Set()  HAL_GPIO_WritePin(OLED_CS_GPIO_Port,OLED_CS_Pin,GPIO_PIN_SET)
 
#define OLED_CMD  0	//写命令
#define OLED_DATA 1	//写数据
 
#define uint8_t unsigned char
#define uint32_t unsigned int
#define WHICH_SPI &hspi2//根据使用的SPI更改
 
void OLED_ClearPoint(uint8_t x,uint8_t y);
void OLED_ColorTurn(uint8_t i);
void OLED_DisplayTurn(uint8_t i);
void OLED_WR_Byte(uint8_t dat,uint8_t cmd);
void OLED_DisPlay_On(void);
void OLED_DisPlay_Off(void);
void OLED_Refresh(void);
void OLED_Clear(void);
void OLED_DrawPoint(uint8_t x,uint8_t y);
void OLED_DrawLine(uint8_t x1,uint8_t y1,uint8_t x2,uint8_t y2);
void OLED_DrawSquare(uint8_t x1,uint8_t y1,uint8_t x2,uint8_t y2);
void OLED_DrawCircle(uint8_t x,uint8_t y,uint8_t r);
void OLED_ShowChar(uint8_t x,uint8_t y,uint8_t chr,uint8_t size1);
void OLED_ShowString(uint8_t x,uint8_t y,uint8_t *chr,uint8_t size1);
void OLED_ShowNum(uint8_t x,uint8_t y,uint32_t num,uint8_t len,uint8_t size1);
void OLED_ShowChinese(uint8_t x,uint8_t y,uint8_t num,uint8_t size1);
void OLED_ScrollDisplay(uint8_t num,uint8_t space);
void OLED_ShowData(uint8_t temperature, uint8_t humidity, uint16_t light);
void OLED_WR_BP(uint8_t x,uint8_t y);
void OLED_ShowPicture(uint8_t x0,uint8_t y0,uint8_t x1,uint8_t y1,uint8_t BMP[]);
void OLED_Printf(uint8_t str[]);
void OLED_Init(void);

#endif

其中我们主要使用的是OLED_ShowData()这个函数(代码内容如下)。这个函数也是在各个基本函数上面建立起来的,主要功能就是把设备传感器数据和时间显示出来。

//将设备数据与时间显示
void OLED_ShowData(uint8_t temperature, uint8_t humidity, uint16_t light, uint64_t timestamp)
{
	OLED_Clear();
	
	//显示时间
	struct tm* info;
	time_t unix = timestamp/1000 + 8*60*60;//转换为秒级时间戳并添加北京时区的8小时
	char time_text[20];
	info = localtime(&unix);
	sprintf(time_text, "%4d-%02d-%02d %02d:%02d", info->tm_year+1900, info->tm_mon+1, info->tm_mday, (info->tm_hour)%24, info->tm_min);
	OLED_ShowString(19, 0, (uint8_t *)time_text, 12);
	
	//显示传感器数据
	for (int i=0;i<3;i++)
	{
		OLED_ShowChinese(2, 17*i+13, 2*i, 16);
		OLED_ShowChinese(20, 17*i+13, 2*i+1, 16);
		OLED_ShowChinese(38, 17*i+13, 11, 16);
	}
	OLED_ShowChinese(92, 13, 10, 16);
	OLED_ShowString(92, 30, (uint8_t *)"%RH", 16);
	OLED_ShowString(92, 47, (uint8_t *)"LUX", 16);
	char temp[8] = "";
	sprintf(temp, "%d", temperature);
	OLED_ShowString(54, 13, (uint8_t *)temp, 16);
	sprintf(temp, "%d", humidity);
	OLED_ShowString(54, 30, (uint8_t *)temp, 16);
	sprintf(temp, "%d", light);
	OLED_ShowString(54, 47, (uint8_t *)temp, 16);
	
	OLED_Refresh();
}

注意在调用相关函数显示之前,需要调用下面的四个函数对OLED进行初始化的配置。其中OLED_ColorTurn(0)是设置正常显示还是反色显示的, OLED_DisplayTurn(0)是控制是否反转显示的。

  OLED_Init();
  OLED_ColorTurn(0);
  OLED_DisplayTurn(0);
  OLED_Refresh();

最后,注意以下的两点:

  1. 每次要更新OLED显示的内容,在写入相关数据之后,要调用OLED_Refresh()函数才能使得写入的内容显示。
  2. 如果不是要在原先显示的图案基础上修改显示内容,就需要调用OLED_Clear()函数清除掉原先写入的数据再写入新的数据。

显示的效果大概就是下面这样子。

在这里插入图片描述

二、通信模块开发

1.简介

通信模块的开发主要的思路就是下图。实际上我们在第二篇文章中已经成功过连接阿里云了,现在要做的与之前做的连接区别在于:之前是使用串口调试助手在PC端我们手动发送AT指令给ESP12S,现在我们是要在STM32中写入发送AT指令的程序,由STM32发送AT指令给ESP12S。
在这里插入图片描述
开发主要分为三个步骤:

  1. 配置STM32的串口USART和时钟TIM,定义好接收缓存和发送缓存,实现USART和TIM的中断函数。
  2. 定义发送AT指令和接收确认应答的函数,实现AT指令连接WiFi网络。
  3. 定义MQTT相关参数和AT指令,连接阿里云。

2.串口USART配置代码

  • usart.h

在usart中需要使用到定时器,所以要将"tim.h"文件include,然后要extern定义的接收缓存和发送缓存的数组,以及接收缓存的状态USART3_RX_STA

#define USART3_MAX_RECV_LEN		800					//最大接收缓存字节数
#define USART3_MAX_SEND_LEN		800					//最大发送缓存字节数
extern uint8_t  USART3_RX_BUF[USART3_MAX_RECV_LEN]; 		//接收缓冲,最大USART3_MAX_RECV_LEN字节
extern uint8_t  USART3_TX_BUF[USART3_MAX_SEND_LEN]; 		//发送缓冲,最大USART3_MAX_SEND_LEN字节
extern uint16_t USART3_RX_STA;   						//接收数据状态
extern uint8_t  temp_rx;
  • usart.c

这里主要是定义两个函数void u3_printf(char* fmt,...)void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)u3_printf() 和之前的 u1_printf() 是一样的,只不过换成了usart3。HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) 函数是串口接收到完整的数据时的回调函数,在这个函数中主要是得通过定时器TIM2来控制接收的数据写入接收缓存中。

uint8_t  temp_rx;
uint8_t  USART3_RX_BUF[USART3_MAX_RECV_LEN]; 		//接收缓冲,最大USART3_MAX_RECV_LEN字节
uint8_t  USART3_TX_BUF[USART3_MAX_SEND_LEN]; 		//发送缓冲,最大USART3_MAX_SEND_LEN字节
uint16_t USART3_RX_STA=0;
void u3_printf(char* fmt,...)  
{  
	uint8_t i,j; 
	va_list ap; 
	va_start(ap,fmt);
	vsprintf((char*)USART3_TX_BUF,fmt,ap);
	va_end(ap);
	i=strlen((const char*)USART3_TX_BUF);//此次发送数据的长度
	for(j=0;j<i;j++)//循环发送数据
	{
		while((USART3->SR&0X40)==0);//循环发送,直到发送完毕   
		USART3->DR=USART3_TX_BUF[j];  
	} 
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	if(huart->Instance==USART3)
	{
		if((USART3_RX_STA&(1<<15))==0)//接收完的一批数据,还没有被处理,则不再接收其他数据
		{
			if(USART3_RX_STA<USART3_MAX_RECV_LEN)//还可以接收数据
			{
				__HAL_TIM_SET_COUNTER(&htim2, 0);//计数器清空	
				if(USART3_RX_STA==0)//使能定时器2的中断 
				{
					__HAL_TIM_ENABLE(&htim2);//使能定时器2
				}
				USART3_RX_BUF[USART3_RX_STA++]=temp_rx;	//记录接收到的值	 
			}else 
			{
				USART3_RX_STA|=1<<15;//强制标记接收完成
			} 
		}
	}
}

另外,在USART3反初始化时,需要对TIM2以及USART3_RX_STA也进行重置。

  /* USER CODE BEGIN USART3_MspDeInit 1 */
	__HAL_UART_ENABLE_IT(uartHandle,UART_IT_RXNE);
    HAL_NVIC_EnableIRQ(USART3_IRQn);
	HAL_NVIC_SetPriority(USART3_IRQn, 2, 0);
	USART3_RX_STA=0;//清零
	__HAL_TIM_DISABLE(&htim2);//关闭定时器
  /* USER CODE END USART3_MspDeInit 1 */
  • stm32f1xx_it.c

这个文件是用来管理stm32中的中断函数的。我们找到USART3的中断函数,在里面加上HAL_UART_Receive_IT(&huart3, &temp_rx, 1) 函数。这是将接收到数据1字节1字节地复制到temp_rx 中,然后在我们上面的回调函数HAL_UART_RxCpltCallback 就会将temp_rx 的数据再复制到缓存中。

void USART3_IRQHandler(void)
{
  /* USER CODE BEGIN USART3_IRQn 0 */

  /* USER CODE END USART3_IRQn 0 */
  HAL_UART_IRQHandler(&huart3);
  /* USER CODE BEGIN USART3_IRQn 1 */
  HAL_UART_Receive_IT(&huart3, &temp_rx, 1);
  /* USER CODE END USART3_IRQn 1 */
}
  • tim.c

在这里我们要添加定时器的周期运行回调函数,当TIM2的计时结束时就会回调这个函数。在这个函数中首先要关闭缓存数组的写入,然后对数据进行初步的处理(处理订阅的MQTT Topic返回的数据),最后清除定时器的更新中断标志并关闭定时器2。

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance == TIM2)
	{	
		USART3_RX_BUF[USART3_RX_STA&0X7FFF] = 0;//添加结束符
		USART3_RX_STA|=1<<15;//标记接收完成

		if(strstr((const char*)USART3_RX_BUF, (const char*)"+MQTTSUBRECV"))//MQTT云端返回的数据
		{
			u1_printf("[TEST]:get +MQTTSUBRECV\r\n");
			int data_len;
			char topic[100], data[256];
			sscanf((const char*)USART3_RX_BUF, "%*[^\"]\"%[^\"]\",%d,%s", topic, &data_len, data);
			u1_printf("[TEST]:data:%s\r\n", data);
			sub_recv_handle(topic, data_len, data);//调用aliyun.c的订阅数据处理函数
		}
		
		__HAL_TIM_CLEAR_FLAG(&htim2,TIM_EVENTSOURCE_UPDATE );//清除TIM7更新中断标志  
		__HAL_TIM_DISABLE(&htim2);//关闭定时器2
	}
}
  • main.c

在main中,外设都已经完成初始化之后,需要将TIM2的中断开启。

  HAL_TIM_Base_Start_IT(&htim2);

3.ESP12S的配置代码

关于ESP12S连接WiFi和阿里云的驱动代码我同样放在了码云上面,可以点击这里下载:https://gitee.com/peanuo/esp12-s.git

esp12.c最基础的两个函数是esp_check_cmd(uint8_t *str)esp_send_cmd(uint8_t *cmd, uint8_t *ack, uint16_t waittime) 。第一个函数是用于从接收缓存中读取数据,查看其中是否由符合AT指令的回复的字符串,第二个函数就是用于发送AT指令的,其中也包含了确认回复的函数。

连接WiFi的函数如下。ESP12S在第一次将连接WiFi之后会将WiFi的SSID和密码保存在flash中,下一次启动时可以自动连接原先记录的WiFi。连接WiFi的大致流程如下:

  1. 发送指令AT+CWJAP? 检查是否已经连接至WiFi,如果是,则return 0;如果不是,则继续下一步。
  2. 发送指令AT+CWMODE=1 设置ESP12S的运行模式为STA模式,如果失败,则return 1;如果成功,则继续下一步。
  3. 发送指令AT+CWJAP="ssid","password" ,发送连接WiFi的的名称和密码,如果连接失败,则return 1;如果连接成功,则return 0。
uint8_t esp_connect_ap(char *ssid, char *pass)
{
	if(esp_send_cmd((uint8_t*)"AT+CWJAP?", (uint8_t*)"+CWJAP:", 1000))//检查是否已经连接WiFi
	{
		if(esp_send_cmd((uint8_t*)"AT+CWMODE=1", (uint8_t*)"OK", 1000))//设置WiFi模式
		{
			u1_printf("[ESP12S]:设置WIFI模式失败");
			return 1;
		}
		else
		{
			char *p = (char*)malloc(100);
			sprintf((char*)p,"AT+CWJAP=\"%s\",\"%s\"", ssid, pass);
			if(esp_send_cmd((uint8_t*)p, (uint8_t*)"WIFI GOT IP", 1000))//连接WiFi
			{
				u1_printf("[ESP12S]:连接WIFI网络失败");
				return 1;
			}
			else
			{
				return 0;
			}
		}
	}
	else
	{
		return 0;
	}
}

4.连接阿里云的配置代码

aliyun.c最基础的三个函数是publish(char* pTopic, char* data)subscribe(char* sTopic)connect(void) ,分别用于发布消息到Topic、订阅Topic和连接MQTT客户端。其他的函数都是基于订阅或者发布消息的函数的基础之上的。
另外,还有函数sub_recv_handle(char* sTopic, int data_len, char* data) 是用于处理订阅的Topic获得的数据的,在上面的tim.c中有进行调用,主要接收的Topic就是关于NTC服务的以及云端下发设备的属性的数据。
这里需要解释一下我们实现使用NTC获取时间的方法。有一些STM32是有RTC实时时钟的,在设备掉电之后这个实时时钟还是可以靠着备用电源继续计时,但是由于我使用的这个STM32板子上面没有备用电源,所以就无法掉电运行(本人亲测,掉电之后只能记住掉电时刻的时间,不能继续计时)。在这里我没有使用RTC,而是通过设备在连接阿里云之后通过NTC服务获取北京时间的毫秒时间戳,减去当前设备的上电时间HAL_GetTick() ,从而获得设备上电时间与北京时间的毫秒差值time_differ ,并保存下来。需要获取时间就使用上电时间再加上毫秒差值就可以得到当前时间。当然,这样的做法在掉电之后也无法继续运行,需要重新联网获得时间。

//处理订阅的Topic接收到的数据
void sub_recv_handle(char* sTopic, int data_len, char* data)
{
	if(strcmp(sTopic, S_Topic_NTC) == 0)//接收到NTC数据
	{
		uint64_t deviceSendTime, serverSendTime, serverRecvTime, deviceRecvTime;
		sscanf(
			data, 
			"{\"deviceSendTime\":\"%lld\",\"serverSendTime\":\"%lld\",\"serverRecvTime\":\"%lld\"}", 
			&deviceSendTime, &serverSendTime, &serverRecvTime
		);
		deviceRecvTime = HAL_GetTick();
		time_differ = (serverRecvTime + serverSendTime + deviceRecvTime - deviceSendTime)/2 - deviceRecvTime;
		u1_printf("[ALIYUN]:NTC时间校准成功\r\n");
	}
	else if(strcmp(sTopic, S_Topic_set) == 0)//接收到云端下发属性的数据
	{
		char param[30];
		int value;
		sscanf(
			data, 
			"{\"method\":\"thing.service.property.set\",\"id\":\"%*ld\",\"params\":{\"%[^\"]\":%d},\"version\":\"1.0.0\"}", 
			param, &value
		);
		u1_printf("[ALIYUN]:获取到云端下发的属性数据%s %d\r\n", param, value);
		if(strcmp(param, "led") == 0)
		{
			if(value)
				LED_Open();
			else LED_Close();
		}
		else if(strcmp(param, "fan") == 0)
		{
			if(value)
				FAN_Open();
			else FAN_Close();
		}
		else if(strcmp(param, "temperature_value") == 0)
		{
			temperature_value = value;
		}
	}
}

三、LED、蜂鸣器和电机模块

LED、蜂鸣器和电机模块都是使用简单的GPIO进行控制的,只不过控制的电平不同。根据电路,板上的LED是低电平时亮,高电平时灭;而蜂鸣器和电机模块都是反过来,低电平时关闭,高电平时开启。基于此,在gpio.c中简单写了LED、蜂鸣器和电机模块的开关函数(如下)。

/* USER CODE BEGIN 2 */
void LED_Open(void)
{
	HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
	if(!led)
		u1_printf("[LED]:LED灯已开启\r\n");
}


void LED_Close(void)
{
	HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
	if(led)
		u1_printf("[LED]:LED灯已关闭\r\n");
}


void FAN_Open(void)
{
	HAL_GPIO_WritePin(FAN_GPIO_Port, FAN_Pin, GPIO_PIN_SET);
	if(!fan)
		u1_printf("[FAN]:风扇已开启\r\n");
}


void FAN_Close(void)
{
	HAL_GPIO_WritePin(FAN_GPIO_Port, FAN_Pin, GPIO_PIN_RESET);
	if(fan)
		u1_printf("[FAN]:风扇已关闭\r\n");
}


void BUZZER_Ring_Times(int times)
{
	while(times)
	{
		times--;
		HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_SET);
		HAL_Delay(300);
		HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_RESET);
		HAL_Delay(200);
	}
}


void BUZZER_Ring(void)
{
	HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_SET);
	HAL_Delay(1000);
	HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_RESET);
	HAL_Delay(1000);
}


/* USER CODE END 2 */
  • 5
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值