一、前言

此项目是在B站UP“技术探索者”的环境检测项目的基础上,进行扩展改动的。

本项目硬件由 STM32F103C8T6 为主控,搭配 DHT11、光敏电阻、MQ135 三类环境传感器,HC-05 蓝牙模块实现无线通信;外设包含 0.96 寸 OLED 屏、独立按键、TB6612 驱动模块、5V 风扇、LED 灯珠,整套器件均可使用 5V USB 供电,体积小巧。

实现通过环境温度&手动按键控制电机转速,环境光暗&蓝牙控制LED的亮度,OLED的页面切换等功能。

二、DHT11驱动

2.1、引脚接口

DHT11 引脚 STM32 引脚 说明
VCC 3.3/5V 电源正极
DATA PA7 单总线数据引脚
GND GND 电源地

2.2、通信时序简述

  1. 主机起始信号:拉低 DATA 至少 18ms,再拉高 20~40μs,等待 DHT11 应答;

  2. 从机应答:DHT11 拉低 DATA 80μs,再拉高 80μs,表示准备就绪;

  3. 数据传输:连续发送 40bit 数据(5 字节):

    • 字节 1:湿度整数

    • 字节 2:湿度小数(DHT11 固定为 0)

    • 字节 3:温度整数

    • 字节 4:温度小数(DHT11 固定为 0)

    • 字节 5:校验和 = 前 4 字节相加

  4. 校验通过后,读取有效温湿度数据。

DHT11高低电平

DHT11 每次拉低数据线 50μs 表示一位数据的起始,然后拉高的时间决定是 0 还是 1:

  • 拉高 26~28μs → 代表 0
  • 拉高 70μs 左右 → 代表 1

2.3、CubeMX配置

2.2.1 选择芯片

打开 CubeMX → 芯片选型栏输入:STM32F103C8T6,选中进入配置界面。

2.2.2 系统时钟与调试

  1. SYS
    • Debug:选择 Serial Wire(串口调试,占用 PA13/PA14,保留下载 + 在线调试)
    • Timebase Source:选择 SysTick(系统滴答定时器,用于delay延时)
  2. RCC 时钟配置
    • High Speed Clock (HSE):选择 Crystal/Ceramic Resonator(外部晶振)
  3. 时钟树配置外部晶振 8MHz,配置系统主频 72MHz(F1 标准最高主频):
    • PLL 倍频:9 倍频
    • AHB 预分频:1
    • APB1 预分频:2、APB2 预分频:1
    • 最终:HCLK=72MHzPCLK1=36MHzPCLK2=72MHz

2.2.3TIM1配置

1. 模式设置

  • Clock Source: Internal Clock(内部时钟),用于基本定时,不做 PWM / 输入捕获
  • Channels: 全部 Disable,因为只做纯定时延时,不需要通道输出 / 输入功能
  • 其他模式(Slave Mode/Trigger Source 等): 全部保持 Disable,不开启额外功能

2. 关键时基参数

  • APB2 为 72MHz,TIM1 挂在 APB2 上
    • 预分频器 PSC = 72-1 → 分频后时钟频率 = 72MHz / 72 = 1MHz
    • 计数器周期 ARR = 65535 → 单次最大定时时间 = 65535 / 1MHz = 65535μs

 

 

2.4、程序

2.4.1、dht11.h文件

#ifndef __DHT11_H__
#define __DHT11_H__

#include"main.h"

/*数据类型*/
#define u8 unsigned char
#define u16 unsigned short
#define u32 unsigned int

/*DHT11数据引脚定义*/
#define DATA_PIN			 GPIO_PIN_7
#define DATA_GPIO_Port GPIOA

/*DHT11数据电平控制*/
#define DATA_SET()				HAL_GPIO_WritePin(DATA_GPIO_Port,DATA_PIN,GPIO_PIN_SET)
#define DATA_RESET()			HAL_GPIO_WritePin(DATA_GPIO_Port,DATA_PIN,GPIO_PIN_RESET)
#define DATA_READ()				HAL_GPIO_ReadPin(DATA_GPIO_Port,DATA_PIN)

/*定义一个结构体保存DHT11的数据*/
typedef struct
{
	u8 Data[5];				//存储DHT11传输的40位数据(5字节)
	u8 index;					//数据计数标志(用于统计采集次数)
	u8 temp;					//解析后的温度值
	u8 humidity;			//解析后的湿度值
}DHT11_DATA;

//全局变量,供外部使用
extern DHT11_DATA DHT11_data;
//函数申明
void DHT11_Task(void);

#endif
  1. 绑定 DHT11 数据线:PA7
  2. 封装 3 个宏:一键拉高、拉低、读取引脚电平,屏蔽底层 HAL 库函数,业务代码更简洁。
  3. Data[5]:DHT11 固定返回 5 字节原始数据
  4. temp/humidity:最终可用的温湿度数值
  5. index:接收数据时做计数辅助
  6. extern:声明全局结构体变量,多文件共享数据(.c文件里定义实体)
  7. DHT11_Task(void):任务函数,外部直接调用即可完成采集 + 解析温湿度,模块化设计。
  8. 硬件引脚统一宏定义,后期换引脚只改一行;
  9. 用结构体整合原始数据 + 解析结果,数据管理清晰;
  10. 对外只暴露一个任务函数,调用简单,符合嵌入式工程规范。

2.4.2、ht11.c文件

1、精准延时

    #include"dht11.h"
    
    extern TIM_HandleTypeDef htim1;//声明定时器1
    
    
    void Delay_us(uint16_t us)
    {		//微妙延时
    	uint16_t differ = 0xffff-us-5;
    	__HAL_TIM_SET_COUNTER(&htim1,differ);//设定Time1计时器的起始值
    	HAL_TIM_Base_Start(&htim1);//开启定时器
    	
    	while(differ < 0xffff-5)
    	{
    		differ = __HAL_TIM_GET_COUNTER(&htim1);//查询计数器的计数值
    	}
    	HAL_TIM_Base_Stop(&htim1);//停止Time1计数
    }
    • 原理:利用 TIM1 基本定时器 做精准微秒延时,DHT11 单总线通信必须依赖μs级延时;
    • 逻辑:手动设置定时器初值启动计数循环等待计数溢出关闭定时器;
    • 补偿值-5:修正函数执行本身带来的微小误差,保证延时精度。

    2、DHT11输出

    将DATA切换为推挽输出模式,后面用来给DHT11发送起始信号。

    static void DATA_OUTPUT(u8 flag);//DATA输出
    /***配置PA7作为数据线,单线模式***/
    static void DATA_OUTPUT(u8 flag)
    {//__HAL_RCC_GPIOA_CLK_ENABLE();
    		GPIO_InitTypeDef GPIO_InitStruct = {0};  // GPIO 初始化结构体
        // 配置 PA7 为推挽输出模式
        GPIO_InitStruct.Pin = DATA_PIN;
        GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;  // 推挽输出
        GPIO_InitStruct.Pull = GPIO_NOPULL;          // 无上下拉(输出模式无需)
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;// 高速模式
        HAL_GPIO_Init(DATA_GPIO_Port, &GPIO_InitStruct);
    		/***传入flag参数来控制数据线的电平***/
    		if(flag == 0)
    		{
    			DATA_RESET();/*拉低数据线*/
    		}
    		else
    		{
    			DATA_SET();/*拉高数据线*/
    		}
    }static void DATA_OUTPUT(u8 flag);//DATA输出

    3、DATA输入

    DATA切换为上拉输入模式。

    static u8 DATA_INPUT(void);//DATA输入
    /***配置PA7为上拉输入模式,读取电平并返回***/
    static u8 DATA_INPUT(void)
    {//__HAL_RCC_GPIOA_CLK_ENABLE();
    		u8 flag = 0;//存储读取的电平状态
    		GPIO_InitTypeDef GPIO_InitStruct = {0};  
        GPIO_InitStruct.Pin = DATA_PIN;
        GPIO_InitStruct.Mode = GPIO_MODE_INPUT;  // 输入模式
        GPIO_InitStruct.Pull = GPIO_PULLUP;          // 上拉
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;// 高速模式
        HAL_GPIO_Init(DATA_GPIO_Port, &GPIO_InitStruct);
    		
    		/***将读取到的电平返回出去***/
    		if (DATA_READ() == GPIO_PIN_RESET)
    		{
    			flag = 0;
    		}
    		else
    		{
    			flag = 1;
    		}
    		return flag;
    }

    4、读取DHT11单个字节数据

    一个字节有8位数据,循环读取8次,根据DHT11的高低电平特点,判断出DATA接收到的电平是‘0’/‘1‘,逐位将数据存储在ReadDat中。

    /***读取DHT11 8位(1字节)数据***/
    static u8 DHT11_Read_Byte(void)
    {
    	u8 ReadDat = 0;	//存储字节数据
    	u8 temp = 0;		//存储位数据
    	u8 retry = 0;		//防超时计数变量
    	u8 i = 0;				//循环变量
    	/***循环读取8次,1字节***/
    	for(i = 0; i < 8;i ++)
    	{		/***等待DHT11拉高电平***/
    		while(DATA_READ() == 0 && retry < 100)
    		{
    			Delay_us(1);
    			retry ++;
    		}
    		retry = 0;//重置超时计数变量
    		//延时40us,如果是低电平(高电平持续26~28us)此时DHT11已经拉低了电平
    		Delay_us(40);
    		//当前电平为高电平则数据位是1,为低电平则数据位是0
    		if(DATA_READ() == 1)
    		{
    			temp = 1;
    		}
    		else
    		{
    			temp = 0;
    		}
    		//等待当前位数据传输完毕
    		while(DATA_READ() == 1 && retry < 100)
    		{
    			Delay_us(1);
    			retry ++;
    		}
    		retry = 0;
    		//将数据左移一位,将当前数据位移入
    		ReadDat <<= 1;
    		ReadDat |= temp;
    	}
    	return ReadDat;//将读取到的字节数据返回
    }

    5、采集DHT11数据完整时序

    起始信号-->切换为输入模式 --> 读取响应信号 --> 循环读取5字节数据将数据逐位放进DHT11_data.Data[] --> 验证数据将验证结果返回’0‘失败’1‘成功。

    DHT11_DATA DHT11_data;//结构体
    /***完整采集DHT11数据***/
    static u8 DHT11_Read(void)
    {
    	u8 retry = 0;		//防超时计数变量
    	u8 i = 0;				//循环变量
    	
    	//发送起始信号,拉低18us(>=18us),拉高20us~40us
    	DATA_OUTPUT(0);
    	HAL_Delay(18);
    	DATA_OUTPUT(1);
    	Delay_us(20);
    	
    	//切换为输入模式,接收应答信号
    	DATA_INPUT();
    	Delay_us(20);//等待信号稳定
    	
    	//判断是否接收到应答,先低电平80us,后高电平80us
    	if(DATA_READ() == 0)
    	{	//等待应答低电平结束
    		while(DATA_READ() == 0 && retry < 100)
    		{
    			Delay_us(1);
    			retry ++;
    		}
    		retry = 0;//重置超时计数变量
    		//等待应答高电平结束
    		while(DATA_READ() == 1 && retry < 100)
    		{
    			Delay_us(1);
    			retry ++;
    		}
    		retry = 0;//重置超时计数变量
    		//读取五个字节
    		for(i = 0; i < 5;i ++)
    		{
    			DHT11_data.Data[i] = DHT11_Read_Byte();
    		}
    		Delay_us(50);//数据读取后延时时,确保稳定
    	}
    	//校验数据,前4个字节之和等于第5个字节
    	u32 sum = DHT11_data.Data[0] + DHT11_data.Data[1] + DHT11_data.Data[2] + DHT11_data.Data[3];
    	if(sum == DHT11_data.Data[4])
    	{
    		//通过校验,解析数据
    		DHT11_data.humidity = DHT11_data.Data[0];
    		DHT11_data.temp = DHT11_data.Data[2];
    		return 1;//成功采集
    	}
    	else
    	{
    		return 0;//采集失败
    	}
    }

    6、将函数提供给外部文件使用

    /***采集成功,记录采集次数,防止index溢出***/
    void Test(void)
    {
    	if(DHT11_Read())
    	{
    		DHT11_data.index++;
    		if(DHT11_data.index >=128)
    		{
    			DHT11_data.index = 0;
    		}
    	}
    }
    
    
    /***供外部调用***/
    void DHT11_Task(void)
    {
    	Test();
    	
    }
    

    2.4.3、mian调用

    #include "dht11.h"
    
    int main(void)
    {
    
      
     
      while (1)
      {
    
    		DHT11_Task();
      }

    三、ADC采取光敏&MQ135

    3.1、引脚接口

    外设通道 引脚 采集对象 用途
    ADC1_IN0 PA1 光敏电阻 环境光照检测,自动调节 LED 亮度
    ADC1_IN1 PA2 MQ135 室内空气质量检测,屏幕显示状态

    3.2、CubeMX配置

    • 模式与通道

      • 工作模式:独立模式(Independent mode)
      • 开启通道:ADC1_IN1、ADC1_IN2
      • 规则转换序列长度:2
      • 序列触发方式:软件触发(Regular Conversion launched by software)
      • 序列顺序:Rank1=Channel1,Rank2=Channel2
      • 采样时间:两个通道均设置为 71.5 Cycles
    • 核心设置项

      • 数据对齐方式:右对齐(Right alignment)
      • 扫描转换模式:Enabled(扫描模式开启)
      • 连续转换模式:Disabled(连续转换模式关闭)
      • 间断转换模式:Enabled(间断转换模式开启)
      • 间断转换数:1(每次转换1个通道)

    3.3、程序

    循环两次转换完两个通道

    uint16_t adc_val[2] = {0};//存放MQ135和光敏的数据
    int i = 0;								//循环变量
    
    
    int main(void)
    {
    
      while (1)
      {
        // 采集两路ADC数据,存到adc_val数组
        for(i = 0; i < 2; i++)
        {
            // 1. 启动ADC转换
            HAL_ADC_Start(&hadc1);
    
            // 2. 轮询等待转换完成,超时时间10ms
            HAL_ADC_PollForConversion(&hadc1, 10);
    
            // 3. 读取转换结果,存入数组
            adc_val[i] = HAL_ADC_GetValue(&hadc1);
        }
        }

    四、蓝牙串口通信

    4.1、引脚接口

    接口名称 STM32 引脚 外设资源 功能说明 备注
    蓝牙 TX PA10 USART1_RX 蓝牙发送 → 单片机接收数据 串口异步通信,波特率 9600
    蓝牙 RX PA9 USART1_TX 单片机发送 → 蓝牙接收数据 电平 3.3V/5V 兼容
    VCC 5V 电源 模块供电 推荐 5V 供电,工作更稳定
    GND GND 电源地 共地 必须共地,否则通信异常

    4.2、CubeMX配置

    • Mode(工作模式):选择 Asynchronous(异步串口模式)
    • Hardware Flow Control (RS232):保持默认 Disable(不开启硬件流控)
    参数项 配置值 对应英文选项
    Baud Rate(波特率) 9600 Bits/s 9600 Bits/s
    Word Length(数据位) 8 Bits(包含校验位) 8 Bits (including Parity)
    Parity(校验位) None(无校验) None
    Stop Bits(停止位) 1 1

    • 切换到 NVIC Settings 选项卡
    • 勾选 USART1 global interrupt(开启串口全局中断)
    • 中断优先级保持默认即可,无需额外调整

    4.3、printf重定向

    1、开启Use MicroLIB(微库)

    2、在usart.c文件中

     

    4.4、main

    uint8_t Rx_date = 0;				//接收串口数据
    uint8_t USART_Flag = 0;			//接收标志位
    
    int main(void)
    {
    	HAL_UART_Receive_IT(&huart1, &Rx_date, 1);//开启接收中断,接收一字节
    ..........
        while(1)
    {
    .........
    }
    
    }
    void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
    {
    	if(huart->Instance == USART1)
    	{
    		USART_Flag = 1;
    	
    	}
    	//重启串口
    	HAL_UART_Receive_IT(&huart1, &Rx_date, 1);
    }

    五、OLED

    5.1CubeMX配置

    1. 左侧 Connectivity → 点击 I2C1
    2. Mode:选择 I2C
    3. Parameter Settings
      • I2C Speed ModeStandard Mode(标准模式,100kHz)
      • Clock Speed100000 Hz(100kHz)
    4. 自动分配引脚:PB6 (I2C1_SCL)PB7 (I2C1_SDA),确认无冲突后保存配置。

    5.2、调用函数显示数据

    OLED的文件获取方式在B站UP“技术探索者”的微信公众号“优质程序猿”回复‘3’。

    void Env_OLED(void);
    
    //显示环境
    void Env_OLED(void)
    {
    	
    	 char buf[20];
    	 // 页面1:环境参数
    		sprintf((char *)buf, "Light:%d", adc_val[0]);
    		OLED_ShowString(0, 0, (uint8_t *)buf, 16);
    
    		sprintf((char *)buf, "Air:%d", adc_val[1]);
    		OLED_ShowString(0, 2, (uint8_t *)buf, 16);
    
    		sprintf((char *)buf, "Temp:%d C", DHT11_data.temp);
    		OLED_ShowString(0, 4, (uint8_t *)buf, 16);
    
    		sprintf((char *)buf, "Hum:%d %%RH", DHT11_data.humidity);
    		OLED_ShowString(0, 6, (uint8_t *)buf, 16);
    	
    }

    六、结尾

    剩下的TB6512驱动电机、输出PWM控制电机转速&电灯亮度、RTC实时时钟这些内容放在下一篇博客,后面将实现时间&环境状况的OLED页面切换,按键&温度控制电机转速,蓝牙&光照强度控制电灯的亮度。

     

     

     

    Logo

    免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

    更多推荐