本项目使用HAL库实现温湿度检测和甲烷浓度检测并将数据显示在OLED屏幕上,OLED显示多级界面,可以选择所在地,查看当地适宜的环境数值。当温度超过阈值时风扇转动降温;当湿度超过阈值时LED灯闪烁提醒;当甲烷浓度超过阈值时启动风扇通风并且蜂鸣器报警。同时风扇可由旋转编码器控制。阈值存储在W25Q64FLASH中。
目录
前言
项目涉及GPIO、中断、PWM、USART、I2C、SPI,主要用来熟悉stm32的通信,不过也是一个较综合的stm32项目,但是没有应用于FreeRTOS。
在后期的学习中可以不断拓展此项目,比如使用FreeRTOS或者RT-Thread等实时操作系统,提高CPU利用率;也可以用u8g2等图形库美化OLED界面甚至改成触摸屏等等,天高任君飞。
文章记录主体模块的配置与代码实现,从数据手册出发逐个实现,为减少篇幅,常用的简单模块和知识点一带而过。这里仅仅展示作者个人的实现流程,发表拙见,授人以渔不敢说,希望可以给读者带来帮助。如有错误或不足请评论或私信作者。望共勉
效果演示
环境检测系统
一、温湿度
1)TB6612驱动直流电机
1>引脚介绍
引脚 | 说明 |
PWMA | A电机控制信号输入端 |
AIN2 | A电机输入端2,连接IO口 |
AIN1 | A电机输入端1,连接IO口 |
STBY | 正常工作/待机状态控制端 |
BIN1 | B电机输入端1,连接IO口 |
BIN2 | B电机输入端2,连接IO口 |
PWMB | B电机控制信号输入端 |
GND | 接地 |
VM | 电机驱动电压输入端(4.5V~15V) |
VCC | 逻辑电平输入端(2.7V~5.5V) |
AO1 | A电机输出端1,连接电机一端 |
AO2 | A电机输出端2,连接电机另一端 |
BO2 | B电机输出端2,连接电机一端 |
BO1 | B电机输入端1,连接电机另一端 |
GND | 接地 |
简单来说只需要知道它可以控制两个电机驱动,PWMX输入端控制转速,XIN1和2根据输入电平的不同控制电机的正反转,XO作为输出。这里只用来模拟风扇,代码写的较随意,读者有时间可以查阅详细介绍,细致操控转速与方向。
2>配置
开启一个PWM通道设置频率即可
驱动电机的PWM频率一般是10kHz(0.0001ms)左右
3>代码实现
TB6612.c
#include "TB6612.h"
/**
* @brief 驱动左电机
* @param direction 正数正转负数反转
* @param speed 速度
* @retval 无
*/
void motorControlL(int8_t direction,int16_t speed) //左电机驱动
{
HAL_TIM_PWM_Start(&htim4,TIM_CHANNEL_4);
if(direction >= 0){
HAL_GPIO_WritePin(GPIOB,AIN1_Pin,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB,AIN2_Pin,GPIO_PIN_RESET);
}else
{
HAL_GPIO_WritePin(GPIOB,AIN1_Pin,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB,AIN2_Pin,GPIO_PIN_SET);
}
speed < 0 ? (speed = 1) :(speed = speed);
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_4, speed);
}
/**
* @brief 驱动右电机
* @param direction 正数正转负数反转
* @param speed 速度
* @retval 无
*/
void motorControlR(int8_t direction,int16_t speed) //右电机驱动
{
HAL_TIM_PWM_Start(&htim4,TIM_CHANNEL_4);
if(direction >= 0){
//HAL_GPIO_WritePin(GPIOB,AIN3_Pin,GPIO_PIN_SET);
//HAL_GPIO_WritePin(GPIOB,AIN4_Pin,GPIO_PIN_RESET);
}else
{
//HAL_GPIO_WritePin(GPIOB,AIN3_Pin,GPIO_PIN_RESET);
//HAL_GPIO_WritePin(GPIOB,AIN4_Pin,GPIO_PIN_SET);
}
speed < 0 ? (speed = 1) :(speed = speed);
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_4, speed);
}
TB6612.h
#ifndef __TB6612_H__
#define __TB6612_H__
#include "main.h"
extern TIM_HandleTypeDef htim4;
void motorControlL(int8_t direction,int16_t speed); //左电机驱动
void motorControlR(int8_t direction,int16_t speed); //右电机驱动
#endif
2)旋转编码器
1>引脚介绍
这里作者用的是常见的增量编码器,简单来说就是VCC接正极GND接负极,ABC接IO口,其中C接口是按键的功能,因为没有用到所以不接。
2>配置
两个IO口都配置为外部中断,下降沿触发
工作原理也很简单,旋转编码器旋转的时候A、B两个引脚会产生相位差90度的方波。当A引脚的上升沿对应B引脚的低电平时,编码器是正转;当B引脚的上升沿对应A引脚的低电平时,编码器是反转。
因为资源有限,作者这里只用中断进行简单判断,模拟开关调速,毕竟对于风扇来说根本不需要关注方向。感兴趣的小伙伴可以查阅详细资料进行细致操控。另外,其实并不是只靠中断才可以控制编码器,定时器也为增量型编码器准备了专门的编码器接口,这里推荐b站博主keysking,大家可以观看他的教学视频自己实践。
3>代码实现
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == RotateA_Pin)
{
if(HAL_GPIO_ReadPin(GPIOB,RotateB_Pin) == 0) //编码器正转,速度增加
{
rotateNum++;
speed += rotateNum*2;
tFlag = 1;
motorControlL(1,speed);
}
}
if(GPIO_Pin == RotateB_Pin)
{
if(HAL_GPIO_ReadPin(GPIOB,RotateA_Pin) == 0) //编码器反转,速度减慢
{
rotateNum--;
speed -= rotateNum*2;
tFlag = 0;
motorControlL(1,speed);
}
}
if(GPIO_Pin == Key_Next_Pin)
{
OLED_FullyClear();
table_index = table[table_index].next;
}
if(GPIO_Pin == Key_Enter_Pin)
{
OLED_FullyClear();
table_index = table[table_index].enter;
}
}
这里有一个坑,等温湿度检测的代码实现后大家可以返回来检查一下看看哪里有问题。
3)AHT20
1>前言
这里介绍两种温湿度传感器,因为AHT20代码已经实现但是焊接的时候被烧坏了,就换了一个差不多的SHT21,大家可以学习完一个自己实现另一个。
同时AHT20是保持主机模式,所以最好使用SHT21的非主机模式。
- AHT20 采用标准的I2C协议进行通讯。
- 配置只需在stm32Cubemx中打开I2C即可
2>介绍
1.地址
在启动传输后,随后传输的I2C首字节包括7位I2C设备地址 0x38和一个SDA方向位 x(读R:‘1’,写W:‘0’)。
可以看出,除了本身的7位地址以外还需要一位读写位,所以地址需要左移一位变成0x70,之后在发送和接收的命令中直接传入此地址即可,读写位1和0的转换HAL库会根据发送还是接收自动帮我们修改。
2.保持主机
保持主机模式: 保证数据在读取时已经准备好,但会阻塞主机的其他操作。
非保持主机模式: 不阻塞主机的其他操作,但需要主机自己管理延时,以确保数据已准备好。
SHT21可以设置非保持主机模式
3>读取流程
1.上电后要等待40ms,读取温湿度值之前, 首先要看状态字的校准使能位Bit[3]是否为 1(通过发送0x71可以获取一个字节的状态字),如果不为1,要发送0xBE命令(初始化),此命令参数有两个字节, 第一个字节为0x08,第二个字节为0x00。
即先等待40ms然后发送0x71后读取数据,判断第三位是否为1,如果不为1则需要发送0xBE,0x08,0x00
2.直接发送 0xAC命令(触发测量),此命令参数有两个字节,第一个字节为 0x33,第二个字节为0x00。
3.等待75ms待测量完成,忙状态Bit[7]为0,然后可以读取六个字节(发0X71即可以读取)。
SDA的输出数据被转换成湿度温度前后各两个半字节的数据包,高字节MSB在前(左对齐)。即需要合并为一个数据进行后续计算,合并的时候建议用 | 而不是 +
4.计算温湿度值。
在代码中,2的20次幂可以用(1 << 20)表示
4>代码实现
AHT20.c
#include "AHT20.h"
extern I2C_HandleTypeDef hi2c1;
void AHT20_Init(void)
{
uint8_t ReceiveBuffer;
HAL_Delay(40);
HAL_I2C_Master_Receive(&hi2c1,AHT20_ADDRESS,&ReceiveBuffer,1,100);
if((ReceiveBuffer & (1 << 3)) != 1)
{
uint8_t SendBuffer[3] = {0xBE,0x08,0x00};
HAL_I2C_Master_Transmit(&hi2c1,AHT20_ADDRESS,SendBuffer,3,100);
}
}
void AHT20_Read(float *Temperature,float *Humidity)
{
uint8_t SendBuffer[3] = {0xAC,0x33,0x00};
uint8_t ReceiveBuffer[6];
HAL_I2C_Master_Transmit(&hi2c1,AHT20_ADDRESS,SendBuffer,3,100);
HAL_Delay(75);
//这里要注意是6不是1
HAL_I2C_Master_Receive(&hi2c1,AHT20_ADDRESS,ReceiveBuffer,6,100);
if((ReceiveBuffer[0] | (1 << 7)) != 0)
{
uint32_t data;
data = ((uint32_t)ReceiveBuffer[1] << 12) | ((uint32_t)ReceiveBuffer[2] << 4)
| ((uint32_t)ReceiveBuffer[3] >> 4);
*Humidity = data / (1 << 20) * 100.0f;
data = ((uint32_t)(ReceiveBuffer[3] & 0x0f) << 16) | ((uint32_t)ReceiveBuffer[4] << 8)
| (uint32_t)ReceiveBuffer[5];
*Temperature = data / (1 << 20) * 200.0 - 50;
}
}
AHT20.h
#ifndef __AHT20_H__
#define __AHT20_H__
#define AHT20_ADDRESS 0x70
void AHT20_Init(void);
void AHT20_ReadData(float *humidity,float *temperature);
#endif
4)SHT21
1>读取流程
1.上电后,传感器最多需要15毫秒时间(此时SCL为高电平)以达到空闲状态,即做好准备接收由主机(MCU)发送的命令。
2.和AHT20一样,I2C地址为7位设备地址(1000 000)和一个SDA方向位(读R:‘1’,写W:‘0’)
3.命令
二者区别就是在主机模式下测量的过程中SCL线被封锁(由传感器控制),而非主机模式下SCL线仍然保持开放状态,可进行其他通讯。同时在非主机模式下MCU需要对传感器状态进行查询,不过这步操作不由我们完成,简单来说二者就是发送不同的命令决定SCL是否阻塞。
3.时序图
主机模式时序
非主机模式时序
Measurement是保持测量的意思,可以不用管,只需在发送测量指令后等待规定实际即可。
无论哪种传输模式,由于测量的最大分辨率为14位,第二个字节SDA上的后两位LSBs(bit43和44)用来传输相关的状态信息。两个LSBs中的bit1表明测量的类型(‘0’温度;‘1’湿度)。即获取的两个字节的数据需要舍弃最低两位
传感器内部设置的默认分辨率为相对湿度12位和温度14位
2>代码实现
SHT21.c
#include "SHT21.h"
void SHT21_Init(void)
{
SHT21_Start; //上电等待15毫秒
}
/**
* @brief 主机模式下读取温度
* @param 接收温度的变量
* @retval 无
*/
void SHT21_HOLD_ReadT(float *Temperature)
{
uint8_t CMDdata = CMD_HOLDdata_t; //测量命令
uint8_t receiveBuffer[2]; //接收数组
uint16_t Tdata; //温度
HAL_I2C_Master_Transmit(&hi2c1,SHT21_ADDRESS,&CMDdata,1,HAL_MAX_DELAY); //发送测量命令
HAL_Delay(75); //等待测量
HAL_I2C_Master_Receive(&hi2c1,SHT21_ADDRESS,receiveBuffer,2,HAL_MAX_DELAY);
Tdata = ((uint16_t)receiveBuffer[0] << 8) | ((uint16_t)receiveBuffer[1] & 0xFE); //将测得的高低位合并
*Temperature = 175.72f * (float)Tdata / (1 << 16) - 46.85f; //换算
}
/**
* @brief 主机模式下读取湿度
* @param 接收湿度的变量
* @retval 无
*/
void SHT21_HOLD_ReadH(float *Humidity)
{
uint8_t CMDdata = CMD_HOLDdata_h; //测量命令
uint8_t receiveBuffer[2]; //接收数组
uint16_t Hdata; //湿度
HAL_I2C_Master_Transmit(&hi2c1,SHT21_ADDRESS,&CMDdata,1,HAL_MAX_DELAY); //发送测量命令
HAL_Delay(29); //等待测量
HAL_I2C_Master_Receive(&hi2c1,SHT21_ADDRESS,receiveBuffer,2,HAL_MAX_DELAY);
Hdata = ((uint16_t)receiveBuffer[0] << 8) | ((uint16_t)receiveBuffer[1] & 0xFC); //将测得的高低位合并
*Humidity = 125.0f * (float)Hdata / (1 << 16) - 6.0f; //换算
}
/**
* @brief 非主机模式下读取温度
* @param 接收温度的变量
* @retval 无
*/
void SHT21_NOHOLD_ReadT(float *Temperature)
{
uint8_t CMDdata = CMD_NOHOLDdata_t; //测量命令
uint8_t receiveBuffer[2]; //接收数组
uint16_t Tdata; //温度
HAL_I2C_Master_Transmit(&hi2c1,SHT21_ADDRESS,&CMDdata,1,HAL_MAX_DELAY); //发送测量命令
HAL_Delay(85); //等待测量
HAL_I2C_Master_Receive(&hi2c1,SHT21_ADDRESS,receiveBuffer,2,HAL_MAX_DELAY);
Tdata = ((uint16_t)receiveBuffer[0] << 8) | ((uint16_t)receiveBuffer[1] & 0xFE); //将测得的高低位合并
*Temperature = 175.72f * Tdata / (1 << 16) - 46.85f; //换算
}
/**
* @brief 非主机模式下读取湿度
* @param 接收湿度的变量
* @retval 无
*/
void SHT21_NOHOLD_ReadH(float *Humidity)
{
uint8_t CMDdata = CMD_NOHOLDdata_h; //测量命令
uint8_t receiveBuffer[2]; //接收数组
uint16_t Hdata; //湿度
HAL_I2C_Master_Transmit(&hi2c1,SHT21_ADDRESS,&CMDdata,1,HAL_MAX_DELAY); //发送测量命令
HAL_Delay(29); //等待测量
HAL_I2C_Master_Receive(&hi2c1,SHT21_ADDRESS,receiveBuffer,2,HAL_MAX_DELAY);
Hdata = ((uint16_t)receiveBuffer[0] << 8) | ((uint16_t)receiveBuffer[1] & 0xFC); //将测得的高低位合并
*Humidity = 125.0f * (float)Hdata / (1 << 16) - 6.0f; //换算
}
SHT21.h
#ifndef __SHT21_H__
#define __SHT21_H__
#include "main.h"
extern I2C_HandleTypeDef hi2c1;
#define SHT21_ADDRESS 0x80
#define SHT21_Start HAL_Delay(15)
#define CMD_HOLDdata_t 0xE3
#define CMD_HOLDdata_h 0xE5
#define CMD_NOHOLDdata_t 0xF3
#define CMD_NOHOLDdata_h 0xF5
void SHT21_Init(void); //上点等待15ms,在执行while之前启动一次即可
void SHT21_HOLD_ReadT(float *Temperature);
void SHT21_HOLD_ReadH(float *Humidity);
void SHT21_NOHOLD_ReadT(float *Temperature);
void SHT21_NOHOLD_ReadH(float *Humidity);
#endif
二、甲烷浓度
1)MQ-4
1>介绍
- 适用于家庭或工厂的甲烷气体,天然气等监测装置,可测试天然气、甲烷 300 to 10000ppm;
- 具有DO开关信号(TTL)输出和AO模拟信号输出;
- 模拟量输出的电压,浓度越高电压越高
2>引脚及配置
接线方式:
- VCC:接电源正极(5V)
- GND:接电源负极
- DO:TTL开关信号输出
- AO:模拟信号输出
只需将AO引脚接到MCU的ADC接口,在Cubemx界面直接打开一个通道采集数据即可;
作者这里查到的转换公式是:adc_value * 5.0 / 4095.0; adc_value为ADC通道采集的数据
打开ADC后需要在时钟树将ADC时钟配置为6分频(ADC最大频率为14M)
3>代码实现
MQ4.c
#include "MQ4.h"
extern ADC_HandleTypeDef hadc1;
float MQ_4_Getppm(void)
{
int adc_value;
float methane_ppm;
if(HAL_ADC_PollForConversion(&hadc1,100) == HAL_OK)
{
adc_value = HAL_ADC_GetValue(&hadc1);
methane_ppm = adc_value * 3.3 / 4095.0;
}
return methane_ppm;
}
MQ4.h
#ifndef __MQ4_H__
#define __MQ4_H__
#include "main.h"
float MQ_4_Getppm(void);
#endif
三、OLED
1)介绍
我们先来清楚要实现的效果是什么。
界面分为三级,next按键按下,“<”指示箭头向下(在本界面轮询);enter按键按下,切换下一级界面或者上一级界面,当返回上一级界面时,返回到箭头上次指示的位置。
一级界面的区别在于箭头位置不同;二级界面的区别在于返回一级界面的位置不同,三级界面的区别在于返回二级界面的位置不同和显示的阈值不同。
一级界面
二级界面
三级界面
2)代码实现
实际上就是按键按下,执行相应的函数,在函数中显示相应的文字和数据。所以,关注点在于如何知道要执行哪个函数。
先定义一个结构体,结构体里面有四个变量,分别代表当前状态,next键,enter键和指向当前执行函数的指针。
typedef struct
{
uint8_t current; //当前位置
uint8_t next; //下一行
uint8_t enter; //上/下级界面
void (*current_opertion)(void); //要执行的函数(显示的界面)
}menu_table;
然后定义一个数组,代表按键按下的索引
menu_table table[20] =
{
//一级界面
{0,1,2,(*Show0)}, //箭头在HOME处
{1,0,6,(*Show1)}, //箭头在SCHOOL处
//二级界面查看HOME数值
{2,3,10,(*Show2)}, //箭头在温度处
{3,4,11,(*Show3)}, //箭头在湿度处
{4,5,12,(*Show4)},
{5,2,0,(*Show5)}, //返回0(箭头在HOME处)
//二级界面查看SCHOOL数值
{6,7,13,(*Show2)}, //箭头在温度处
{7,8,14,(*Show3)}, //箭头在湿度处
{8,9,15,(*Show4)},
{9,6,1,(*Show5)}, //返回1(箭头在SCHOOL处)
//三级界面查看阈值
{10,11,2,(*Show6)}, //返回2(箭头在HOME下的温度处)
{11,12,3,(*Show7)},
{12,13,4,(*Show8)},
//三级界面查看阈值
{13,14,6,(*Show9)}, //返回6(箭头在SCHOOL下的温度处)
{14,15,7,(*Show10)},
{15,13,8,(*Show11)},
};
数组的索引号要与当前位置相匹配,当按键按下next或者enter时,将next或enter对应的值赋值给索引,因为索引已经和当前状态相同,所以可以直接找到对应要执行的函数。函数中就是你想要显示的界面,因为二级界面显示的只是数值的不同,而数值又是随时采集到的数据,所以指向相同的函数即可。
show():
//在主函数中将获取的数值通过memcpy()转换成字符串
char hum[10],tem[10],met[10];
void Show0(void)
{
OLED_ShowCN(0,0,(uint8_t*)"北京欢迎您");
OLED_ShowCN(0,16,(uint8_t*)"请选择所在地:");
OLED_ShowStr(0,32,(uint8_t*)"HOME",2);
OLED_ShowStr(100,32,(uint8_t*)"<",2);
OLED_ShowStr(0,48,(uint8_t*)"SCHOOL",2);
OLED_RefreshRAM();
}
void Show2(void)
{
OLED_ShowCN(0,0,(uint8_t*)"温度为:");
OLED_ShowStr(64,0,(uint8_t*)tem,2);
OLED_ShowStr(100,0,(uint8_t*)"<",2);
OLED_ShowCN(0,16,(uint8_t*)"湿度为:");
OLED_ShowStr(64,16,(uint8_t*)hum,2);
OLED_ShowCN(0,32,(uint8_t*)"甲烷含量为:");
OLED_ShowStr(64,32,(uint8_t*)met,2);
OLED_ShowCN(0,48,(uint8_t*)"返回");
OLED_RefreshRAM();
}
按键
uint8_t table_index = 0;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == Key_Next_Pin) //引脚重命名
{
OLED_FullyClear();
table_index = table[table_index].next; //更新当前状态
}
if(GPIO_Pin == Key_Enter_Pin)
{
OLED_FullyClear();
table_index = table[table_index].enter;
}
}
while (1)
{
//执行当前状态的函数(show)
(*table[table_index].current_opertion)();
HAL_Delay(0);
}
要注意的一点是,检测的数据都是float类型,而显示屏显示的驱动函数一般是需要char型,所以需要sprintf函数将数据格式化为字符串,只需include <stdint.h>即可。
建议自己画个图,标记一下对应的数组索引,思考跳转界面和索引的关系,相信很快就可以理解掌握。
show.c
#include "Show.h"
#include <stdio.h>
extern float temperature,humidity,methane_ppm; //检测到的数据
extern float temperatureMax,humidityMax,methane_ppmMax; //阈值
char tem[10],hum[10],met[10]; //格式化后的数据
float max[2][3]; //读取到FLASH的阈值
char home_temMax[10],home_humMax[10],home_metMax[10]; //格式化后的HOME阈值
char school_temMax[10],school_humMax[10],school_metMax[10];
extern uint8_t len;
//将测得的float格式化为字符串
void ChangeGetValue(void)
{
sprintf(tem,"%.1f",temperature);
sprintf(hum,"%.1f",humidity);
sprintf(met,"%.2f",methane_ppm);
}
//将存储的阈值格式化为字符串
void ChangeMaxValue(float max[locationNum][len])
{
sprintf(home_temMax,"%.1f",max[0][0]);
sprintf(home_humMax,"%.1f",max[0][1]);
sprintf(home_metMax,"%.2f",max[0][2]);
sprintf(school_temMax,"%.1f",max[1][0]);
sprintf(school_humMax,"%.1f",max[1][1]);
sprintf(school_metMax,"%.2f",max[1][2]);
}
menu_table table[20] =
{
//一级界面
{0,1,2,(*Show0)}, //箭头在HOME处
{1,0,6,(*Show1)}, //箭头在SCHOOL处
//二级界面查看HOME数值
{2,3,10,(*Show2)}, //箭头在温度处
{3,4,11,(*Show3)}, //箭头在湿度处
{4,5,12,(*Show4)},
{5,2,0,(*Show5)}, //返回0(箭头在HOME处)
//二级界面查看SCHOOL数值
{6,7,13,(*Show2)}, //箭头在温度处
{7,8,14,(*Show3)}, //箭头在湿度处
{8,9,15,(*Show4)},
{9,6,1,(*Show5)}, //返回1(箭头在SCHOOL处)
//三级界面查看阈值
{10,10,2,(*Show6)}, //返回2(箭头在HOME下的温度处)
{11,11,3,(*Show7)},
{12,12,4,(*Show8)},
//三级界面查看阈值
{13,13,6,(*Show9)}, //返回6(箭头在SCHOOL下的温度处)
{14,14,7,(*Show10)},
{15,15,8,(*Show11)},
};
void Show0(void)
{
temperatureMax = max[0][0]; //更新阈值
humidityMax = max[0][1];
methane_ppmMax = max[0][2];
OLED_ShowCN(0,0,(uint8_t*)"北京欢迎您");
OLED_ShowCN(0,16,(uint8_t*)"请选择所在地:");
OLED_ShowStr(0,32,(uint8_t*)"HOME",2);
OLED_ShowStr(100,32,(uint8_t*)"<",2);
OLED_ShowStr(0,48,(uint8_t*)"SCHOOL",2);
OLED_RefreshRAM();
}
void Show1(void)
{
temperatureMax = max[1][0];
humidityMax = max[1][1];
methane_ppmMax = max[1][2];
OLED_ShowCN(0,0,(uint8_t*)"北京欢迎您");
OLED_ShowCN(0,16,(uint8_t*)"请选择所在地:");
OLED_ShowStr(0,32,(uint8_t*)"HOME",2);
OLED_ShowStr(0,48,(uint8_t*)"SCHOOL",2);
OLED_ShowStr(100,48,(uint8_t*)"<",2);
OLED_RefreshRAM();
}
void Show2(void)
{
OLED_ShowCN(0,0,(uint8_t*)"温度为:");
OLED_ShowStr(64,0,(uint8_t*)tem,2);
OLED_ShowStr(100,0,(uint8_t*)"<",2);
OLED_ShowCN(0,16,(uint8_t*)"湿度为:");
OLED_ShowStr(64,16,(uint8_t*)hum,2);
OLED_ShowCN(0,32,(uint8_t*)"甲烷含量为:");
OLED_ShowStr(64,32,(uint8_t*)met,2);
OLED_ShowCN(0,48,(uint8_t*)"返回");
OLED_RefreshRAM();
}
void Show3(void)
{
OLED_ShowCN(0,0,(uint8_t*)"温度为:");
OLED_ShowStr(64,0,(uint8_t*)tem,2);
OLED_ShowCN(0,16,(uint8_t*)"湿度为:");
OLED_ShowStr(64,16,(uint8_t*)hum,2);
OLED_ShowStr(100,16,(uint8_t*)"<",2);
OLED_ShowCN(0,32,(uint8_t*)"甲烷含量为:");
OLED_ShowStr(64,32,(uint8_t*)met,2);
OLED_ShowCN(0,48,(uint8_t*)"返回");
OLED_RefreshRAM();
}
void Show4(void)
{
OLED_ShowCN(0,0,(uint8_t*)"温度为:");
OLED_ShowStr(64,0,(uint8_t*)tem,2);
OLED_ShowCN(0,16,(uint8_t*)"湿度为:");
OLED_ShowStr(64,16,(uint8_t*)hum,2);
OLED_ShowCN(0,32,(uint8_t*)"甲烷含量为:");
OLED_ShowStr(64,32,(uint8_t*)met,2);
OLED_ShowStr(100,32,(uint8_t*)"<",2);
OLED_ShowCN(0,48,(uint8_t*)"返回");
OLED_RefreshRAM();
}
void Show5(void)
{
OLED_ShowCN(0,0,(uint8_t*)"温度为:");
OLED_ShowStr(64,0,(uint8_t*)tem,2);
OLED_ShowCN(0,16,(uint8_t*)"湿度为:");
OLED_ShowStr(64,16,(uint8_t*)hum,2);
OLED_ShowCN(0,32,(uint8_t*)"甲烷含量为:");
OLED_ShowStr(64,32,(uint8_t*)met,2);
OLED_ShowCN(0,48,(uint8_t*)"返回");
OLED_ShowStr(100,48,(uint8_t*)"<",2);
OLED_RefreshRAM();
}
void Show6(void)
{
OLED_ShowCN(0,0,(uint8_t*)"该地温度阈值为");
OLED_ShowStr(0,16,(uint8_t*)home_temMax,2);
OLED_ShowCN(0,48,(uint8_t*)"确定");
OLED_ShowStr(100,48,(uint8_t*)"<",2);
OLED_RefreshRAM();
}
void Show7(void)
{
OLED_ShowCN(0,0,(uint8_t*)"该地湿度阈值为");
OLED_ShowStr(0,16,(uint8_t*)home_humMax,2);
OLED_ShowCN(0,48,(uint8_t*)"确定");
OLED_ShowStr(100,48,(uint8_t*)"<",2);
OLED_RefreshRAM();
}
void Show8(void)
{
OLED_ShowCN(0,0,(uint8_t*)"该地甲烷阈值为");
OLED_ShowStr(0,16,(uint8_t*)home_metMax,2);
OLED_ShowCN(0,48,(uint8_t*)"确定");
OLED_ShowStr(100,48,(uint8_t*)"<",2);
OLED_RefreshRAM();
}
void Show9(void)
{
OLED_ShowCN(0,0,(uint8_t*)"该地温度阈值为");
OLED_ShowStr(0,16,(uint8_t*)school_temMax,2);
OLED_ShowCN(0,48,(uint8_t*)"确定");
OLED_ShowStr(100,48,(uint8_t*)"<",2);
OLED_RefreshRAM();
}
void Show10(void)
{
OLED_ShowCN(0,0,(uint8_t*)"该地湿度阈值为");
OLED_ShowStr(0,16,(uint8_t*)school_humMax,2);
OLED_ShowCN(0,48,(uint8_t*)"确定");
OLED_ShowStr(100,48,(uint8_t*)"<",2);
OLED_RefreshRAM();
}
void Show11(void)
{
OLED_ShowCN(0,0,(uint8_t*)"该地甲烷阈值为");
OLED_ShowStr(0,16,(uint8_t*)school_metMax,2);
OLED_ShowCN(0,48,(uint8_t*)"确定");
OLED_ShowStr(100,48,(uint8_t*)"<",2);
OLED_RefreshRAM();
}
其中格式化和更新阈值是下一部分内容
show.h
#ifndef __SHOW_H__
#define __SHOW_H__
#include "main.h"
#include "oled.h"
#include <stdint.h>
#include <string.h>
#define LED_Light HAL_GPIO_WritePin(GPIOA,LED_Pin,GPIO_PIN_SET)
#define LED_Close HAL_GPIO_WritePin(GPIOA,LED_Pin,GPIO_PIN_RESET)
#define Get_KeyNext HAL_GPIO_ReadPin(GPIOB,Key_Next_Pin)
#define Get_KeyEnter HAL_GPIO_ReadPin(GPIOA,Key_Enter_Pin)
typedef struct
{
uint8_t current; //当前位置
uint8_t next; //下一行
uint8_t enter; //上/下级界面
void (*current_opertion)(void); //要执行的函数(显示的界面)
}menu_table;
extern uint8_t locationNum;
extern uint8_t len;
void Show0(void);
void Show1(void);
void Show2(void);
void Show3(void);
void Show4(void);
void Show5(void);
void Show6(void);
void Show7(void);
void Show8(void);
void Show9(void);
void Show10(void);
void Show11(void);
void ChangeGetValue(void);
void ChangeMaxValue(float Max[locationNum][len]);
#endif
四、W25Q64FLASH
这里作者用来作为熟悉SPI的模块,存储各地温湿度和甲烷阈值,显示在OLED上。
1)介绍
- W25Q64是为系统提供一个最小空间、最小引脚,最低功耗的串行Flash存储器,25Q系列比普通的串行Flash存储器更灵活,性能更优越。
- 支持标准串行外围接口(SPI)。
- W25Q64内存为64MBit,也就是8MByte。其他W25Qxx同理。
- 8MByte的存储单元分为块(Block),每块为64KB,即分成8 * 1024 / 64 = 127块
- 块被分成扇区,每个扇区占4KB,即每块分成64KB / 4KB = 16个扇区
- 扇区被分为页(Page),每页占256字节,即每页分成4 * 1024 / 256 = 16页
- 擦除指令分别支持:16页(1扇区)、128页、256页、全片擦除
(作者这里只编写了1扇区擦除) - FLASH存储器芯片的操作特点,写入时只能写0,不能写1。1是靠擦除实现的。
FLASH写之前要先擦除 - FLASH有擦写次数限制(10w),尽量避免写在while中忘记关机
2)配置
3)使能SPI
- 进行SPI通信之前需要先把CS拉低,通信结束CS被拉高
#define W25Q63_CS_Open HAL_GPIO_WritePin(CS_GPIO_Port,CS_Pin,GPIO_PIN_RESET)
#define W25Q63_CS_Close HAL_GPIO_WritePin(CS_GPIO_Port,CS_Pin,GPIO_PIN_SET)
/**
* @brief 使能SPI
* @param Control
* 1:使能
* 0:关闭
* @retval 无
*/
void W25Q64_Init_CS(int Control)
{
if(Control == 1)
{
W25Q63_CS_Open;
}else{
W25Q63_CS_Close;
}
}
//发送一字节数据
uint8_t W25Q64_ReadWriteByte(uint8_t sendData)
{
uint8_t ReceiveData;
HAL_SPI_TransmitReceive_IT(&hspi1,&sendData,&ReceiveData,1);
return ReceiveData;
}
4)读取ID 
/**
* @brief 读取ID
* @param 无
* @retval 16位ID(前八位制造商ID,后八位设备ID)
*/
uint16_t W25Q64_ReadID(void)
{
uint16_t W25Q64ID;
uint16_t ManufacturerID;
uint16_t EquipmentID;
W25Q64_State();
W25Q64_Init_CS(1);
W25Q64_ReadWriteByte(0x90);
W25Q64_ReadWriteByte(0x00);
W25Q64_ReadWriteByte(0x00);
W25Q64_ReadWriteByte(0x00);
//读取和发送同时进行。想读取一个数据就需要发送一个数据,没有规定则可以任意发送
ManufacturerID = W25Q64_ReadWriteByte(0xFF);
EquipmentID = W25Q64_ReadWriteByte(0xff);
W25Q64ID = (ManufacturerID << 8) + EquipmentID;
W25Q64_Init_CS(0);
return W25Q64ID;
}
uint16_t W25Q64ID;
W25Q64ID = W25Q64_ReadID();
printf("ID为:%X\r\n",W25Q64ID);
5)写使能 
/**
* @brief 写使能
* @param 无
* @retval 无
*/
void W25Q64_WriteEnable(void)
{
W25Q64_State();
W25Q64_Init_CS(1);
W25Q64_ReadWriteByte(0x06);
W25Q64_Init_CS(0);
}
6)读取状态寄存器
-
在进行任何一个操作前先查询BUSY状态位命令(状态寄存器1)
-
从机不能主动发送数据,从机的时钟信号是由主机提供的,主机必须发送一字节数据,才会产生时钟信号,然后从机才会将状态寄存器的值发送出来。如图带叉的数据表示可以发送任意数据,主要起到产生时钟信号的作用。一般发送0xff
-
发送05h查询状态寄存器1,35h查询状态寄存器2,15h查询状态寄存器3
这里查看寄存器1 -
接收的一个字节(High Impedance,0xff)无意义,第二个字节才是状态数据
-
1表示忙碌
/**
* @brief 读取状态寄存器,直到空闲(0)
* @param
* @retval 无
*/
void W25Q64_State(void)
{
uint8_t SendBuffer[2],ReceiveBuffer[2];
SendBuffer[0] = 0x05;
SendBuffer[1] = 0xFF;
//判断BUSY是否为1(忙碌),如果为1就继续读取BUSY位
do{
W25Q64_Init_CS(1);
HAL_SPI_TransmitReceive_IT(&hspi1,SendBuffer,ReceiveBuffer,2);
W25Q64_Init_CS(0);
}while((ReceiveBuffer[1] & 0x01) == 1);
}
7)擦除一扇区(4K)空间
-
24位地址表示扇区的起始地址
-
扇区大小为4KB,即4096B
/**
* @brief 擦除一个扇区的数据
* @param 要擦除扇区的地址
* @retval 无
*/
void W25Q64_ClearSector(uint32_t sectorAddress)
{
uint8_t ClearCMD[4];
ClearCMD[0] = 0x20;
ClearCMD[1] = (sectorAddress >> 16) & 0xff;
ClearCMD[2] = (sectorAddress >> 8) & 0xff;
ClearCMD[3] = (sectorAddress >> 0) & 0xff;
W25Q64_State(); //等待空闲
W25Q64_WriteEnable(); //开启写使能
W25Q64_Init_CS(1);
HAL_SPI_Transmit_IT(&hspi1,ClearCMD,4);
W25Q64_Init_CS(0);
}
8)写入一页数据
-
256个字节
-
有回卷特性,即当超过256个字节(0-255)时会覆盖之前的数据
/**
* @brief 写一页数据
* @param 起始地址
* @param 要写入的数据
* @retval 无
*/
void W25Q64_WritePage(uint32_t pageAddress,uint8_t *Data,uint16_t len)
{
uint8_t WriteCMD[4];
WriteCMD[0] = 0x02;
WriteCMD[1] = (pageAddress >> 16) & 0xff;
WriteCMD[2] = (pageAddress >> 8) & 0xff;
WriteCMD[3] = (pageAddress >> 0) & 0xff;
W25Q64_State();
W25Q64_WriteEnable();
W25Q64_Init_CS(1);
HAL_SPI_Transmit_IT(&hspi1,WriteCMD,4);
HAL_SPI_Transmit_IT(&hspi1,Data,len);
W25Q64_Init_CS(0);
}
9)读数据
/**
* @brief 读取数据
* @param 要读取数据的地址
* @param 要读取数据的长度
* @param 存放读取到的数据
* @retval
*/
void W25Q64_ReadData(uint32_t address,uint32_t Len,uint8_t *readBuffer)
{
uint8_t ReadCMD[4];
ReadCMD[0] = 0x03;
ReadCMD[1] = (address >> 16) & 0xff;
ReadCMD[2] = (address >> 16) & 0xff;
ReadCMD[3] = (address >> 16) & 0xff;
W25Q64_State();
W25Q64_Init_CS(1);
HAL_SPI_Transmit_IT(&hspi1,ReadCMD,4);
HAL_SPI_Receive_IT(&hspi1,readBuffer,Len); //发送的数据由hal库自动解决
W25Q64_Init_CS(0);
}
uint8_t ReceiveBuffer[4096];
uint8_t sendBuffer[3] = {0x12,0x34,0x56};
W25Q64_ClearSector(0x00000000);
W25Q64_WritePage(0x00000000,sendBuffer,3);
W25Q64_ReadData(0x00000000,3,ReceiveBuffer);
for(int i = 0;i < 3;i++)
{
printf("%d\r\n",ReceiveBuffer[i]);
}
10)存储和读取阈值
因为不同地方适宜的环境不同,所以将各地的阈值存放在FLASH中,在main.c中调用一次存储和读取的函数即可获取阈值,从而进行相应判断,做出适合的操作。
存储和读取需要注意的是,FLASH中的数据是一字节一字节存储,而float类型的数据是四字节,左右需要通过memcpy进行转换
//存储阈值
float depositBuffer[2][3] =
{
//温度,湿度,甲烷含量
{30.5, 70.0, 2.0}, //HOME
{44.4, 66.6, 3.3} //SCHOOL
};
//地点数量 2
uint8_t locationNum = sizeof(depositBuffer) / sizeof(depositBuffer[0]);
//每个地方存储数据数量 3
uint8_t len = sizeof(depositBuffer[0]) / sizeof(depositBuffer[0][0]);
//起始地点
uint32_t addressStart = 0x000000;
//实际发送到FLASH中的数组长度
uint16_t BufLen = sizeof(depositBuffer[0]) / sizeof(depositBuffer[0][0]) * 4;
void Deposit(void)
{
W25Q64_ClearSector(0x000000);
uint8_t sendBuf[BufLen]; //实际写入的数组 长度为存储数据数量 * 4
for(int i = 0;i < locationNum;i++)
{
int index = 0;
for(int j = 0;j < len;j++)
{
//将deopsitBuffer二维数组中的每个一维数组中的数据依次存储到sendBuf里
memcpy(&sendBuf[index],&depositBuffer[i][j],sizeof(depositBuffer[i][j]));
index += 4;
}
W25Q64_WritePage(addressStart,(uint8_t*)sendBuf,BufLen); //将数据写入
addressStart += 0x000100; //只能一页一页写,所以需要修改起始地址——加一页
}
addressStart = 0x000000;
}
void GetMax(float maxBuf[locationNum][len])
{
uint8_t receiveBuf[BufLen];
for(int i = 0;i < locationNum;i++)
{
int index = 0;
W25Q64_ReadData(addressStart,receiveBuf,BufLen); //将一页的数据读到receiveBuf中
for(int j = 0;j < len;j++)
{
memcpy(&maxBuf[i][j],&receiveBuf[index],sizeof(maxBuf[i][j])); //将每页数据放入float二维数组中
index += 4;
}
addressStart += 0x000100;
}
addressStart = 0x000000;
}
当主函数获取到阈值后,在show函数中显示即可
extern float temperature,humidity,methane_ppm; //检测到的数据
extern float temperatureMax,humidityMax,methane_ppmMax; //阈值
char tem[10],hum[10],met[10]; //格式化后的数据
float max[2][3]; //读取到FLASH的阈值
char home_temMax[10],home_humMax[10],home_metMax[10]; //格式化后的HOME阈值
char school_temMax[10],school_humMax[10],school_metMax[10];
extern uint8_t len;
void Show6(void)
{
OLED_ShowCN(0,0,(uint8_t*)"该地温度阈值为");
OLED_ShowStr(0,16,(uint8_t*)home_temMax,2);
OLED_ShowCN(0,48,(uint8_t*)"确定");
OLED_ShowStr(100,48,(uint8_t*)"<",2);
OLED_RefreshRAM();
}
void Show7(void)
{
OLED_ShowCN(0,0,(uint8_t*)"该地湿度阈值为");
OLED_ShowStr(0,16,(uint8_t*)home_humMax,2);
OLED_ShowCN(0,48,(uint8_t*)"确定");
OLED_ShowStr(100,48,(uint8_t*)"<",2);
OLED_RefreshRAM();
}
void Show8(void)
{
OLED_ShowCN(0,0,(uint8_t*)"该地甲烷阈值为");
OLED_ShowStr(0,16,(uint8_t*)home_metMax,2);
OLED_ShowCN(0,48,(uint8_t*)"确定");
OLED_ShowStr(100,48,(uint8_t*)"<",2);
OLED_RefreshRAM();
}
main.c
Deposit();
GetMax(max);
ChangeMaxValue(max);
show.c中编写了格式化的函数
五、数据判断
在旋转编码器哪里作者一开始犯了一个错:转动旋转编码器直接控制风扇转动。但是由于我们要设置当温度高于阈值时风扇打开,低于阈值时风扇关闭,这样的话,假如温度低于阈值,通过旋转编码器控制风扇只会转动几秒。
作者的解决办法是:通过两个值控制风扇,一个由旋转编码器控制,一个由温度控制。这样就解决了自动和手动的问题。同时也可以用别的方法,比如加一个按键切换自动手动。
//旋转编码器
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == RotateA_Pin)
{
if(HAL_GPIO_ReadPin(GPIOB,RotateB_Pin) == GPIO_PIN_SET) //编码器正转,速度增加
{
if(rotateNum <= 1200 && rotateNum >= 200)
{
rotateNum += 200;
}else
{
rotateNum = 200;
}
}
}
if(GPIO_Pin == RotateB_Pin)
{
if(HAL_GPIO_ReadPin(GPIOB,RotateA_Pin) == GPIO_PIN_SET) //编码器反转,速度减慢
{
if(rotateNum <= 1200 && rotateNum >= 200)
{
rotateNum -= 200;
}else
{
rotateNum = 0;
}
}
}
}
//甲烷浓度检测
void Methane_Detection(void)
{
if(methane_ppm > methane_ppmMax)
{
Beep_Start;
speed = 1000;
}else
{
Beep_Close;
}
}
//温度检测
void Temperature_Detection(void)
{
if(temperature > temperatureMax)
{
speed = 1000;
}else
{
speed = 0;
}
}
//湿度检测
void Humidity_Detection(void)
{
if(humidity > humidityMax)
{
LED_Blink;
}else
{
LED_Close;
}
}
while (1)
{
motorControlL(1,speed + rotateNum);
}
总结
使用stm32Cubemx进行单片机的配置非常的方便,主要就是数据手册的阅读以及某些逻辑转换需要多思考试验,比如风扇的自动和手动,阈值的存储和显示等等。