【小项目】STM32环境监测 | MQ2可燃气体传感器+雨滴传感器+DHT11温湿度传感器+OLED屏幕

概述

前些阵子参加了广东省电赛,因为疫情的原因比赛不能在线下进行,甚至连回学校调试也不行,于是乎就水了一个省三。
备赛的时候,队长给我布置了这个小项目,说有可能会用在比赛的作品中,但实际就不知道了,所以在这里分享一下代码。

固件编写

这个需求对性能要求不高,像人见人爱、人手一个的STM32F103C8T6也能胜任,但因为我没带回家,所以用了野火的霸道V2开发板。
野火霸道V2
这块开发板用的是STM32F103ZET6芯片。

DHT11温湿度传感器

概述

温湿度传感器玩过单片机的小伙伴都应该特别熟悉了,在这里就不再赘述了。但还是放几张时序图解释一下。
总体来说DHT11是串行通信,只用一根数据线进行通信,通信的形式有点像IIC,都是先给一个响应信号,然后传感器应答后开始传输数据,直到从机或主机发送结束信号。
总时序图
下面是主机发送响应信号的时序。
主机发送起始信号
从机接收到主机的响应信号后会按以下时序产生应答信号,表示接受响应。
从机应答信号
实际发送的数据都是以一低一高的形式表示,“0”和“1”的区别在于高电平的持续时间,利用这一点主机可以区分“0”和“1”比特。
0/1数据格式

代码

下面是接收一个字节和接收温湿度数据的代码:

/* 
 * 从DHT11读取一个字节,MSB先行
 */
static uint8_t DHT11_ReadByte ( void )
{
	uint8_t i, temp=0;
	

	for(i=0;i<8;i++)    
	{	 
		/*每bit以50us低电平标置开始,轮询直到从机发出 的50us 低电平 结束*/  
		while(DHT11_Dout_IN() == RESET);

		/*DHT11 以26~28us的高电平表示“0”,以70us高电平表示“1”,
		 *通过检测 x us后的电平即可区别这两个状 ,x 即下面的延时 
		 */
		DHT11_DELAY_10US(4); //延时x us 这个延时需要大于数据0持续的时间即可	   	  

		if(DHT11_Dout_IN() == SET)/* x us后仍为高电平表示数据“1” */
		{
			/* 等待数据1的高电平结束 */
			while(DHT11_Dout_IN() == SET);

			temp|=(uint8_t)(0x01<<(7-i));  //把第7-i位置1,MSB先行 
		}
		else	 // x us后为低电平表示数据“0”
		{			   
			temp&=(uint8_t)~(0x01<<(7-i)); //把第7-i位置0,MSB先行
		}
	}
	
	return temp;
	
}

/*
 * 一次完整的数据传输为40bit,高位先出
 * 8bit 湿度整数 + 8bit 湿度小数 + 8bit 温度整数 + 8bit 温度小数 + 8bit 校验和 
 */
uint8_t DHT11_Read_TempAndHumidity(DHT11_Data_TypeDef *DHT11_Data)
{  
	/*输出模式*/
	DHT11_Mode_Out_PP();
	/*主机拉低*/
	DHT11_Dout_0;
	/*延时18ms*/
	DHT11_DELAY_MS(18);

	/*总线拉高 主机延时30us*/
	DHT11_Dout_1; 

	DHT11_DELAY_10US(3);   //延时30us

	/*主机设为输入 判断从机响应信号*/ 
	DHT11_Mode_IPU();

	/*判断从机是否有低电平响应信号 如不响应则跳出,响应则向下运行*/   
	if(DHT11_Dout_IN() == RESET)     
	{
		/*轮询直到从机发出 的80us 低电平 响应信号结束*/  
		while(DHT11_Dout_IN() == RESET);

		/*轮询直到从机发出的 80us 高电平 标置信号结束*/
		while(DHT11_Dout_IN() == SET);

		/*开始接收数据*/   
		DHT11_Data->humi_int= DHT11_ReadByte();

		DHT11_Data->humi_deci= DHT11_ReadByte();

		DHT11_Data->temp_int= DHT11_ReadByte();

		DHT11_Data->temp_deci= DHT11_ReadByte();

		DHT11_Data->check_sum= DHT11_ReadByte();


		/*读取结束,引脚改为输出模式*/
		DHT11_Mode_Out_PP();
		/*主机拉高*/
		DHT11_Dout_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;
	}
	
	else
		return ERROR;
	
}

MQ2可燃气体传感器和雨滴传感器

概述

因为可燃气体检测和雨滴感应的传感器原理较相似,所以放到一起讲了。
可燃气体传感器用的是MQ2。
MQ2雨滴模块
下面是雨滴模块的样子(简单到连名字都没有)
雨滴模块
两个模块都是用模拟信号通信,所以使用STM32的ADC外设就可以轻松读取数据。

ADC外设

根据STM32的用户手册知道我们使用的这款芯片有3个ADC外设,为了让单片机能更快处理转换模拟信号,所以使用了双ADC模式;为了让我们能随时调整采样的时间,我还使用了ADC外设的外部触发功能;为了保证数据传输的效率我还使用了DMA进行传输。
解释一下双ADC模式,通俗讲就是正常是一个ADC处理2个模拟信号的转换,现在2个ADC分别负责2个模拟信号的转换,并且是硬件控制同时开始转换的,因此效率会高很多。
双ADC模式框图
STM32支持的双ADC模式有好几个,具体可参考用户文档,在这里我们使用最常用的同步规则模式即可。
但需要注意的是双ADC模式只能使用ADC1和ADC2外设

代码

接下来放核心代码吧,对于STM32的ADC玩法还挺多的,计划在另外写一篇文章来讲一下。
adc.c文件:

#include "adc/adc.h"

static uint32_t ADC_RAW_DATA = 0;

void ADC_Config(void)
{
	/* DMA初始化 */
	DMA_InitTypeDef DMA_Init_Struct = {0};
	
	RCC_AHBPeriphClockCmd(DMA_PERI_CLK_PORT, ENABLE);
	
	DMA_DeInit(ADC_DMA_CHAL);
	DMA_Init_Struct.DMA_PeripheralBaseAddr = ADC1_DR_Address;  // 外设基地址
	DMA_Init_Struct.DMA_MemoryBaseAddr = (uint32_t)&ADC_RAW_DATA;  // 内存基地址
	DMA_Init_Struct.DMA_DIR = DMA_DIR_PeripheralSRC;  // 外设到内存
	DMA_Init_Struct.DMA_BufferSize = 1;  // 数据块数量
	DMA_Init_Struct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;  // 关闭外设地址自增
	DMA_Init_Struct.DMA_MemoryInc = DMA_MemoryInc_Disable;  // 关闭内存地址自增
	DMA_Init_Struct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;  // 字
	DMA_Init_Struct.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;  // 字
	DMA_Init_Struct.DMA_Mode = DMA_Mode_Circular;  // 循环模式
	DMA_Init_Struct.DMA_Priority = DMA_Priority_High;  // 优先级高
	DMA_Init_Struct.DMA_M2M = DMA_M2M_Disable;  // 关闭内存至内存模式
	DMA_Init(ADC_DMA_CHAL, &DMA_Init_Struct);

	DMA_Cmd(ADC_DMA_CHAL, ENABLE);
	DMA_ClearFlag(DMA1_FLAG_TC1);

	/* 初始化计时器 */
	TIM_TimeBaseInitTypeDef TIM_BaseInit_Struct = {0};
	
	RCC_APB1PeriphClockCmd(TIM_PERI_CLK_PORT, ENABLE);
	
	TIM_BaseInit_Struct.TIM_ClockDivision = TIM_CKD_DIV1;  // 输入捕获才用到,默认值即可
	TIM_BaseInit_Struct.TIM_CounterMode = TIM_CounterMode_Up;  // 向上计数
	TIM_BaseInit_Struct.TIM_Period = TIM_PERIOD_VAL;   // 周期,即在(1 / 2kHz) * (TIM_PERIOD_VAL + 1) = CONV_TIME(ms)后产生上溢,这也是ADC的触发周期
	TIM_BaseInit_Struct.TIM_Prescaler = (36000 - 1);   // 预分频,即分频后的时钟频率为72Mhz / (35999 + 1) = 2kHz
	TIM_BaseInit_Struct.TIM_RepetitionCounter = 0;  // 不使用重装载寄存器

	TIM_TimeBaseInit(TIM_PORT, &TIM_BaseInit_Struct);
	TIM_SelectOutputTrigger(TIM_PORT, TIM_TRGOSource_Update);  // 使能输出触发
	TIM_ClearFlag(TIM_PORT, TIM_FLAG_Update);

	/* 初始化ADC */
	ADC_InitTypeDef ADC_Init_Struct = {0};
	
	RCC_APB2PeriphClockCmd(MQ2_ADC_PERI_CLK_PORT | WaterSensor_ADC_PERI_CLK_PORT, ENABLE);
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);  // ADC 6分频,即采样频率为72 / 6 = 12MHz
	
	ADC_Init_Struct.ADC_ContinuousConvMode = DISABLE;  // 关闭连续采样模式
	ADC_Init_Struct.ADC_DataAlign = ADC_DataAlign_Right;  // 数据右对齐
	ADC_Init_Struct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO;  // ADC1外部触发,由计时器3 TRGO事件负责
	ADC_Init_Struct.ADC_Mode = ADC_Mode_RegSimult;  // 同步扫描模式
	ADC_Init_Struct.ADC_NbrOfChannel = 1;  // 1个通道
	ADC_Init_Struct.ADC_ScanConvMode = DISABLE;  // 关闭扫描模式
	
	ADC_Init(MQ2_ADC_PORT, &ADC_Init_Struct);

	ADC_Init_Struct.ADC_ContinuousConvMode = DISABLE;  // 关闭连续采样模式
	ADC_Init_Struct.ADC_DataAlign = ADC_DataAlign_Right;  // 数据右对齐
	ADC_Init_Struct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;  // ADC2关闭外部触发
	ADC_Init_Struct.ADC_Mode = ADC_Mode_RegSimult;  // 同步扫描模式
	ADC_Init_Struct.ADC_NbrOfChannel = 1;  // 1个通道
	ADC_Init_Struct.ADC_ScanConvMode = DISABLE;  // 关闭扫描模式
	
	ADC_Init(WaterSensor_ADC_PORT, &ADC_Init_Struct);
	
	ADC_DMACmd(MQ2_ADC_PORT, ENABLE);  // 使能ADC DMA功能
	
	ADC_Cmd(MQ2_ADC_PORT, ENABLE);
	ADC_Cmd(WaterSensor_ADC_PORT, ENABLE);
	
	ADC_ExternalTrigConvCmd(MQ2_ADC_PORT, ENABLE);  // 使能ADC1外部触发转换
	ADC_ExternalTrigConvCmd(WaterSensor_ADC_PORT, ENABLE);  // 使能ADC2外部触发转换
	
	/* 转换时间 (1 / 12MHz) * (55.5 + 12.5) ≈ 5.7us */
	ADC_RegularChannelConfig(MQ2_ADC_PORT, MQ2_ADC_CHAL, 1, ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(WaterSensor_ADC_PORT, WaterSensor_ADC_CHAL, 1, ADC_SampleTime_55Cycles5);
	
	ADC_ResetCalibration(MQ2_ADC_PORT);  // 复位ADC1
	while(ADC_GetResetCalibrationStatus(MQ2_ADC_PORT));
	ADC_StartCalibration(MQ2_ADC_PORT);  // 校正ADC1
	while(ADC_GetCalibrationStatus(MQ2_ADC_PORT));
	
	ADC_ResetCalibration(WaterSensor_ADC_PORT);  // 复位ADC2
	while(ADC_GetResetCalibrationStatus(WaterSensor_ADC_PORT));
	ADC_StartCalibration(WaterSensor_ADC_PORT);  // 校正ADC2
	while(ADC_GetCalibrationStatus(WaterSensor_ADC_PORT));
	
	TIM_Cmd(TIM_PORT, ENABLE);  // 开启计时器,开启ADC转换
}

uint16_t ADC_GetMQ2RawData(void)
{
	uint32_t tmp = ADC_RAW_DATA;
	return (uint16_t)(tmp & 0x0000ffff);
}

uint16_t ADC_GetWaterSensorRawData(void)
{
	uint32_t tmp = ADC_RAW_DATA;
	return (uint16_t)(tmp >> 16);
}

adc.h文件:

/* 相关配置 */
#define CONV_TIME 100  // 采样周期,单位ms

/* 其他宏定义 */
#define TIM_PERIOD_VAL (CONV_TIME * 2 - 1)

/* 管脚宏定义 */
#define TIM_PORT TIM3
#define TIM_PERI_CLK_PORT RCC_APB1Periph_TIM3

#define DMA_PERI_CLK_PORT RCC_AHBPeriph_DMA1
#define ADC1_DR_Address ((uint32_t)0x4001244C)
#define ADC_DMA_CHAL DMA1_Channel1

void ADC_Config(void);
uint16_t ADC_GetMQ2RawData(void);
uint16_t ADC_GetWaterSensorRawData(void);

OLED屏幕

概述

OLED屏幕应该也有很多小伙伴玩过了。
OLED屏幕
这个屏幕的通信方式有挺多种的,比如说8080时序、IIC、SPI,具体是哪种通信方式要看买的时候商家的说明。
我买的这个是IIC的接口,我使用了硬件IIC对其通信。

代码

简单贴一下代码:
碍于篇幅就只放初始化和传输一个字节的代码吧。

/**
  * @brief 初始化OLED针脚
  * @param None
  * @retval None
  */
static void OLED_GPIO_Init(void)
{
	GPIO_InitTypeDef GPIO_Init_Struct;
	
	RCC_APB2PeriphClockCmd(OLED_PERI_CLK, ENABLE);
	GPIO_Init_Struct.GPIO_Mode = GPIO_Mode_AF_OD;
	GPIO_Init_Struct.GPIO_Speed = GPIO_Speed_50MHz;

	GPIO_Init_Struct.GPIO_Pin = OLED_SCL_PIN;
	GPIO_Init(OLED_SCL_PORT, &GPIO_Init_Struct);
	
	GPIO_Init_Struct.GPIO_Pin = OLED_SDA_PIN;
	GPIO_Init(OLED_SDA_PORT, &GPIO_Init_Struct);
	
	RCC_APB1PeriphClockCmd(IIC_PERI_CLK, ENABLE);
	
	I2C_InitTypeDef I2C_Init_Struct;
	
	I2C_Init_Struct.I2C_Ack = I2C_Ack_Enable;  // 使能ACk
	I2C_Init_Struct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;  // 7位从机地址
	I2C_Init_Struct.I2C_ClockSpeed = 1000000;  // 速度100kHz
	I2C_Init_Struct.I2C_DutyCycle = I2C_DutyCycle_2;
	I2C_Init_Struct.I2C_Mode = I2C_Mode_I2C;
	I2C_Init_Struct.I2C_OwnAddress1 = 0x00;  // STM32自己的地址,任意值即可
	
	I2C_Init(IIC_PORT, &I2C_Init_Struct);
	I2C_Cmd(IIC_PORT, ENABLE);
}

/**
  * @brief 初始化SSD1306
  * @param None
  * @retval None
  */
void OLED_Init(void)
{
	OLED_GPIO_Init();
	
    OLED_SendCmd(0xAE);//--turn off oled panel
    OLED_SendCmd(0x00);//---set low column address
    OLED_SendCmd(0x10);//---set high column address
    OLED_SendCmd(0x40);//--set start line address  Set Mapping RAM Display Start Line (0x00~0x3F)
    OLED_SendCmd(0x81);//--set contrast control register
    OLED_SendCmd(0xCF); // Set SEG Output Current Brightness
    OLED_SendCmd(0xA1);//--Set SEG/Column Mapping     0xa0左右反置 0xa1正常
    OLED_SendCmd(0xC8);//Set COM/Row Scan Direction   0xc0上下反置 0xc8正常
    OLED_SendCmd(0xA6);//--set normal display
    OLED_SendCmd(0xA8);//--set multiplex ratio(1 to 64)
    OLED_SendCmd(0x3f);//--1/64 duty
    OLED_SendCmd(0xD3);//-set display offset	Shift Mapping RAM Counter (0x00~0x3F)
    OLED_SendCmd(0x00);//-not offset
    OLED_SendCmd(0xd5);//--set display clock divide ratio/oscillator frequency
    OLED_SendCmd(0x80);//--set divide ratio, Set Clock as 100 Frames/Sec
    OLED_SendCmd(0xD9);//--set pre-charge period
    OLED_SendCmd(0xF1);//Set Pre-Charge as 15 Clocks & Discharge as 1 Clock
    OLED_SendCmd(0xDA);//--set com pins hardware configuration
    OLED_SendCmd(0x12);
    OLED_SendCmd(0xDB);//--set vcomh
    OLED_SendCmd(0x40);//Set VCOM Deselect Level
    OLED_SendCmd(0x20);//-Set Page Addressing Mode (0x00/0x01/0x02)
    OLED_SendCmd(0x02);//
    OLED_SendCmd(0x8D);//--set Charge Pump enable/disable
    OLED_SendCmd(0x14);//--set(0x10) disable
    OLED_SendCmd(0xA4);// Disable Entire Display On (0xa4/0xa5)
    OLED_SendCmd(0xA6);// Disable Inverse Display On (0xa6/a7) 
    OLED_SendCmd(0xAF);//--turn on oled panel

    OLED_SendCmd(0xAF); /*display ON*/
    OLED_Clear();
    OLED_Set_Pos(0,0);
}

/**
  * @brief 向SSD1106写入一个字节
  * @param dat 要写入的数据/命令
  * @param cmd 数据/命令标志 0,表示命令;1,表示数据
  * @retval
  */
static void OLED_WR_Byte(uint8_t dat, uint8_t cmd)
{
	// FlagStatus bitstatus = RESET
	while (I2C_GetFlagStatus(IIC_PORT, I2C_FLAG_BUSY));  // 检查I2C总线是否繁忙	
	I2C_GenerateSTART(IIC_PORT, ENABLE);  // 打开IIC总线
	// ErrorStatus status = ERROR, ERROR是个枚举类型, 值为0        
	while(!I2C_CheckEvent(I2C1,  I2C_EVENT_MASTER_MODE_SELECT));  // 检查总线是否打开
	I2C_Send7bitAddress(IIC_PORT, IIC_ADDR, I2C_Direction_Transmitter);  // 配置STM32的IIC设备自己的地址,每个连接到IIC总线上的设备都有一个自己的地址,作为主机也不例外。
	while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));   // 检查地址是否发送
	if (cmd) I2C_SendData(IIC_PORT, 0x40);  // 进入数据模式
	else I2C_SendData(IIC_PORT, 0x00);  // 进入命令模式
	while(!I2C_CheckEvent(I2C1,  I2C_EVENT_MASTER_BYTE_TRANSMITTING));  // 等待发送数据完成
	I2C_SendData(IIC_PORT, dat);   //发送数据
	while(!I2C_CheckEvent(I2C1,  I2C_EVENT_MASTER_BYTE_TRANSMITTING));  // 等待发送数据完成
	I2C_GenerateSTOP(IIC_PORT, ENABLE);  // 关闭IIC总线
}

main.c文件

概述

应队长的要求这个小项目用了FreeRTOS系统,但实际上除了用到最基本的任务调度外,其他的功能都没有用到。
至于FreeRTOS系统的移植网上都有好多的教程了,这里就不赘述了。
简单说主任务做的就是一段时间后切换OLED屏幕的显示内容(因为一块OLED屏幕显示不了所有传感器的数据)

代码

简单贴一下代码:

static void MainTask(void* params)
{
	uint8_t status = 0;
	
	while(1)
	{
		if (status)
		{
			DHT11_Read_TempAndHumidity(&dht11_data);
			OLED_ShowTempAndHumi(dht11_data.temp_int, dht11_data.temp_deci, dht11_data.humi_int, dht11_data.humi_deci);
			printf("Displaying temperature and humidity\n");
		}
		else
		{
			OLED_ShowPpmAndWaterLevel(MQ2_GetPPM(), WaterSensor_GetWaterLevel());
			printf("Displaying PPM and water level\n");
		}
		status = !status;
		vTaskDelay(2000);
	}
}

static void AppInitTask(void* params)
{
	taskENTER_CRITICAL();  // 进入临界段
	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
	USART_Config();
	MQ2_Init();
	WaterSensor_Init();
	ADC_Config();
	TIMER_Init();
	DHT11_Init();
	OLED_Init();
	printf("Init completed\n");
	
	vTaskDelay(100);
	
	xTaskCreate(MainTask, "MainTask", 1024, NULL, 5, NULL);
	printf("Main task created\n");

	vTaskDelete(NULL);  // 删除这个任务
	
	taskEXIT_CRITICAL();  // 退出临界段
}

/**
  * @brief  主函数
  * @param  None
  * @retval None
  */
int main(void)
{
	BaseType_t xReturn = pdPASS;
	
	xReturn = xTaskCreate(AppInitTask, "AppInitTask", 1024, NULL, 5, NULL);
	
	if (xReturn == pdPASS) vTaskStartScheduler();  // 开启任务调度
	else while(1);
}

总结

总的来说,这个项目还是蛮适合对STM32已经入门然后想进阶的小伙伴。
项目包含了对各种常见模块的代码撰写,每种模块对应的通信协议、用到的外设也不尽相同;项目还涉及了对FreeRTOS系统的基本移植和使用,对于想进阶STM32的小伙伴是非常好的。

项目代码下载

链接:https://download.csdn.net/download/JackieCoo/86506760

  • 9
    点赞
  • 122
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

马浩同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值