这是我的物联网开发系列文章,将介绍如何从嵌入式开发、云平台开发、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();
最后,注意以下的两点:
- 每次要更新OLED显示的内容,在写入相关数据之后,要调用
OLED_Refresh()
函数才能使得写入的内容显示。 - 如果不是要在原先显示的图案基础上修改显示内容,就需要调用
OLED_Clear()
函数清除掉原先写入的数据再写入新的数据。
显示的效果大概就是下面这样子。
二、通信模块开发
1.简介
通信模块的开发主要的思路就是下图。实际上我们在第二篇文章中已经成功过连接阿里云了,现在要做的与之前做的连接区别在于:之前是使用串口调试助手在PC端我们手动发送AT指令给ESP12S,现在我们是要在STM32中写入发送AT指令的程序,由STM32发送AT指令给ESP12S。
开发主要分为三个步骤:
- 配置STM32的串口USART和时钟TIM,定义好接收缓存和发送缓存,实现USART和TIM的中断函数。
- 定义发送AT指令和接收确认应答的函数,实现AT指令连接WiFi网络。
- 定义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的大致流程如下:
- 发送指令
AT+CWJAP?
检查是否已经连接至WiFi,如果是,则return 0;如果不是,则继续下一步。 - 发送指令
AT+CWMODE=1
设置ESP12S的运行模式为STA模式,如果失败,则return 1;如果成功,则继续下一步。 - 发送指令
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 */