STM32 项目实战:温湿度 / 光敏 / 蓝牙 + 风扇 / LED 双闭环控制(一)

一、前言

此项目是在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 PB7 单总线数据引脚
GND GND 电源地

2.2、通信时序简述

主机起始信号:拉低 DATA 至少 18ms,再拉高 20~40μs,等待 DHT11 应答;
从机应答:DHT11 拉低 DATA 80μs,再拉高 80μs,表示准备就绪;
数据传输:连续发送 40bit 数据(5 字节):
复制代码
 ###### 字节 1:湿度整数
复制代码
 ###### 字节 2:湿度小数(DHT11 固定为 0)
复制代码
 ##### 字节 3:温度整数
复制代码
 ##### 字节 4:温度小数(DHT11 固定为 0)
复制代码
 ##### 字节 5:校验和 = 前 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. Data5: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页面切换,按键&温度控制电机转速,蓝牙&光照强度控制电灯的亮度。

相关推荐
贤哥哥yyds12 小时前
【无标题】
stm32
崇山峻岭之间15 小时前
单片机步进电机实验
单片机·嵌入式硬件
xiangw@GZ17 小时前
802.11全系列标准调制编码与速率档对应关系
网络·单片机·嵌入式硬件·架构
希希之光17 小时前
Aurix Tc3xx Port&Dio模块总结
单片机·嵌入式硬件
三品吉他手会点灯17 小时前
STM32F103 学习笔记-24-I2C-读写EEPROM(第1节)-I2C物理层介绍
笔记·stm32·学习
日拱一卒的小田17 小时前
ZYNQ学习笔记2-ZYNQ的UART控制器1
单片机·嵌入式硬件
我想走路带风18 小时前
OPENWRT-Day01
stm32·单片机·嵌入式硬件
ACP广源盛1392462567318 小时前
GSV2221@ACP#DP 1.4 MST 多屏转换芯片,物理 AI 多模态交互的视觉中枢
大数据·人工智能·嵌入式硬件·gpt·spark
云栖梦泽19 小时前
Linux内核与驱动:pinctl子系统和GPIO子系统
linux·单片机·嵌入式硬件