一、前言
此项目是在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 系统时钟与调试
- SYS
- Debug:选择
Serial Wire(串口调试,占用 PA13/PA14,保留下载 + 在线调试) - Timebase Source:选择
SysTick(系统滴答定时器,用于delay延时)
- Debug:选择
- RCC 时钟配置
- High Speed Clock (HSE):选择
Crystal/Ceramic Resonator(外部晶振)
- High Speed Clock (HSE):选择
- 时钟树配置 外部晶振 8MHz,配置系统主频 72MHz (F1 标准最高主频):
- PLL 倍频:9 倍频
- AHB 预分频:1
- APB1 预分频:2、APB2 预分频:1
- 最终:
HCLK=72MHz,PCLK1=36MHz,PCLK2=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
- 绑定 DHT11 数据线:PA7
- 封装 3 个宏:一键拉高、拉低、读取引脚电平,屏蔽底层 HAL 库函数,业务代码更简洁。
- Data5:DHT11 固定返回 5 字节原始数据
- temp/humidity:最终可用的温湿度数值
- index:接收数据时做计数辅助
- extern:声明全局结构体变量,多文件共享数据(.c文件里定义实体)
- DHT11_Task(void):任务函数,外部直接调用即可完成采集 + 解析温湿度,模块化设计。
- 硬件引脚统一宏定义,后期换引脚只改一行;
- 用结构体整合原始数据 + 解析结果,数据管理清晰;
- 对外只暴露一个任务函数,调用简单,符合嵌入式工程规范。
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配置
- 左侧
Connectivity→ 点击I2C1 - Mode :选择
I2C - Parameter Settings :
I2C Speed Mode:Standard Mode(标准模式,100kHz)Clock Speed:100000 Hz(100kHz)
- 自动分配引脚:
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页面切换,按键&温度控制电机转速,蓝牙&光照强度控制电灯的亮度。