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页面切换,按键&温度控制电机转速,蓝牙&光照强度控制电灯的亮度。

相关推荐
省四收割者35 分钟前
一文详解信号完整性(1)
python·嵌入式硬件·数学建模·信息与通信·信号处理·智能硬件
崇山峻岭之间2 小时前
单片机无感FOC驱动之ADC
单片机·嵌入式硬件
Ligocious2 小时前
1.点亮一颗小小的LED
单片机·嵌入式硬件
KaifuZeng6 小时前
通信与接口协议面试八、CAN通信
单片机·嵌入式硬件·面试·通信与接口协议
智源单片机设计7 小时前
基于单片机的直流电机双闭环调速系统设计
单片机·嵌入式硬件
kebidaixu7 小时前
两轮车换电 BMS 的模拟前端中颖 SH367306 AFE 芯片详解
stm32
kebidaixu8 小时前
两轮车换电 BMS 的模拟前端中颖 SH367306 AFE 芯片调试实录:四个关键问题与解决方案
stm32
KaifuZeng20 小时前
通信与接口协议面试一、基本概念
嵌入式硬件·通信与接口协议
wuyk55521 小时前
24. C 语言模块化:不是拆几个.c 文件那么简单
c语言·开发语言·stm32·单片机