51单片机智能家居系统:从零实现温湿度监测、蓝牙遥控与PWM风扇调速

手把手拆解一个完整的嵌入式小项目,代码、电路、原理全都有

很多初学者学完51单片机的语法和基础外设后,都会面临一个尴尬的问题:单独点亮一个LED、单独驱动一个传感器都会,但怎么把这些东西组合成一个"有点意思"的小系统?

今天这篇文章,我就带你完整拆解一个智能家居原型项目 ------它不复杂,但涵盖了嵌入式开发中最核心的几块内容:传感器数据采集、PWM电机调速、蓝牙串口遥控、OLED屏幕显示、光敏自动控制。我会把每一部分的设计思路、代码细节、硬件连接全部讲清楚。

先看系统框图,对整个项目有个整体印象:

一、这个项目能做什么?

先给你一个直观感受,这套系统实现了以下功能:

功能模块 实现方式 实际用途
温湿度检测 DHT11传感器 采集环境温湿度,显示在OLED和串口
自动补光 光敏传感器 + LED 环境变暗时自动开灯
风扇调速 PWM控制电机 通过蓝牙或串口无级调速
灯光控制 蓝牙串口指令 手机APP远程开关灯
数据监控 OLED屏幕显示 本地实时查看温湿度、风扇转速
数据上传 串口发送 上传到PC或手机APP

一句话概括:采集环境数据 → 本地显示 + 逻辑控制 → 支持远程干预,这就是一个完整物联网节点的雏形。


二、硬件清单与引脚分配

2.1 元器件清单

器件 型号/规格 数量 作用
单片机 STC89C52RC 1 主控
温湿度传感器 DHT11 1 采集温湿度
OLED屏幕 SSD1306 (I²C接口) 1 本地显示
蓝牙模块 HC-05/HC-06 1 无线串口通信
光敏传感器 光敏电阻模块(数字输出) 1 光线检测
LED灯 普通LED + 限流电阻 2 照明指示
直流电机 小功率5V直流电机 1 模拟风扇
电机驱动 L298N或L9110S 1 驱动电机
晶振 11.0592MHz 1 串口通信精准波特率

2.2 引脚连接表

对照代码的main.h,引脚定义如下:

外设 引脚 说明
LED2(灯光) P1.1 蓝牙控制灯光(低电平点亮)
LED4(光控灯) P1.3 光敏传感器自动控制
PWM风扇 P1.0 同时作为PWM输出引脚
DHT11 P1.2 单总线数据线
OLED_SCL P1.5 I²C时钟线
OLED_SDA P1.4 I²C数据线
光敏传感器 P3.7 数字信号输入(0=有光/1=无光)
串口TX P3.1 蓝牙/串口发送
串口RX P3.0 蓝牙/串口接收

三、软件架构分层解读

整个工程的代码结构很清晰,分为以下几层:

复制代码
main.c/h          → 主逻辑、状态管理、中断服务
├── uart.c/h      → 串口初始化、收发(蓝牙通信)
├── timer.c/h     → 定时器初始化(PWM时基)
├── dht11.c/h     → DHT11驱动(时序读取)
├── oled.c/h      → OLED屏幕驱动(I²C指令/数据)
├── iic.c/h       → I²C底层协议(起始、停止、收发)
├── delay.c/h     → 延时函数(微秒/毫秒级)
├── key.c/h       → 按键扫描(预留)
└── main.h        → 全局引脚定义、类型别名

这个分层的好处是:每一层只做一件事,上层调用下层接口,修改底层驱动不影响上层逻辑


四、逐个模块深度解析

4.1 串口与蓝牙通信(uart.c)

串口是这套系统的"神经"------它既要接收蓝牙指令,又要发送温湿度数据。

波特率计算

代码中使用了定时器1工作模式2(8位自动重装),波特率9600:

cpp 复制代码
TH1 = 0xFD;
TL1 = 0xFD;  // 11.0592MHz下,9600波特率

为什么是0xFD?这是计算出来的初值:

cpp 复制代码
定时器溢出速率 = 晶振频率 / (12 × (256 - TH1))
波特率 = 定时器溢出速率 / 32(模式1)

9600 = 11059200 / (12 × (256 - TH1) × 32)
→ TH1 = 256 - 11059200/(9600×12×32) ≈ 256 - 3 = 253 = 0xFD

所以用11.0592MHz晶振,就是为了串口通信时波特率误差极小。

串口中断处理
cpp 复制代码
void UART_Routine(void) interrupt 4
{
    if(RI) {
        RI = 0;
        recv = SBUF;
        switch(recv) {
            case 0x01: LED2 = 0; break;  // 开灯
            case 0x02: LED2 = 1; break;  // 关灯
            case 0x03: level++; break;   // 加速
            case 0x04: level--; break;   // 减速
        }
    }
}

这里定义了一个简单的二进制通信协议

  • 0x01 → 开灯

  • 0x02 → 关灯

  • 0x03 → 风扇加速(占空比+1)

  • 0x04 → 风扇减速(占空比-1)

补充知识 :实际产品中不会用这种单字节协议(易出错、无校验),通常会加上帧头帧尾和校验和,比如 AA 01 01 02 这种格式。但作为学习项目,简洁优先。


4.2 PWM风扇调速(timer.c)

51单片机没有硬件PWM,所以用定时器中断配合软件计数实现。

原理

定时器0每10ms触发一次中断,中断服务函数里维护两个计数器:

cpp 复制代码
void Timer0_Routine(void) interrupt 1
{
    TH0 = 0xDC; TL0 = 0x00;  // 10ms重装值
    pwm_count++;
    pwm_count = pwm_count % 10;  // 周期 = 10 × 10ms = 100ms
    
    if(pwm_count < level) {
        PWM_IO = 1;   // 高电平
    } else {
        PWM_IO = 0;   // 低电平
    }
    
    // 每300个中断 = 3000ms = 3秒读取一次DHT11
    tim_count++;
    tim_count = tim_count % 300;
    if(0 == tim_count) read_dht11_flag = 1;
}

工作机制

level值 高电平占比 风扇转速
0 0/10 = 0% 停转
3 3/10 = 30% 低速
5 5/10 = 50% 中速
8 8/10 = 80% 高速
10 10/10 = 100% 全速

pwm_count从0到9循环,level决定了在一个周期内高电平持续多少次中断。当level=10时,10个时间片全为高电平,相当于直通全速。

为什么周期选100ms? 因为人耳对100ms以上的频率不敏感,电机不会发出可听见的嗡嗡声。实际电机调速通常用1kHz~20kHz,但51单片机用定时器做高频率PWM会占用大量CPU资源,100ms是在资源有限下的折中方案。


4.3 DHT11温湿度读取(dht11.c)

DHT11是单总线传感器,通信时序非常讲究。一次完整读取流程如下:

cpp 复制代码
主机拉低 ≥ 18ms → 拉高 20~40us → DHT11拉低 80us → 拉高 80us → 开始传数据

每个数据位是这样区分的:

  • 位"0":DHT11拉低 50us → 拉高 26~28us(高电平持续时间短)

  • 位"1":DHT11拉低 50us → 拉高 70us(高电平持续时间长)

代码用Delay30us()后读取引脚电平来区分0和1:

cpp 复制代码
Delay30us();
if(1 == DHT11_IO) {
    data_bit = 1;   // 30us后还是高电平 → 位"1"
    while(DHT11_IO && count < 80) count++;
} else {
    data_bit = 0;   // 30us后已变低 → 位"0"
}

DHT11返回的数据格式是40位(5字节)

字节0 字节1 字节2 字节3 字节4
湿度整数 湿度小数 温度整数 温度小数 校验和

校验公式:字节0 + 字节1 + 字节2 + 字节3 == 字节4

代码中只取了整数部分(humi = dht_buf[0]temp = dht_buf[2]),舍弃了小数------因为DHT11的湿度精度也就±5%,小数没实际意义。


4.4 OLED显示(oled.c + iic.c)

OLED用的是SSD1306驱动芯片 ,7位I²C地址是0x78(含读写位是0x78写/0x79读)。

I²C通信的关键是起始条件、停止条件、应答位

cpp 复制代码
void IIC_Start(void) {
    IIC_SCL = 1;
    IIC_SDA = 1;
    Delay5us();
    IIC_SDA = 0;   // SDA在SCL高电平时拉低 → 起始
    Delay5us();
    IIC_SCL = 0;
}

void IIC_Stop(void) {
    IIC_SCL = 1;
    IIC_SDA = 0;
    Delay5us();
    IIC_SDA = 1;   // SDA在SCL高电平时拉高 → 停止
}

向OLED写数据分两步:

  1. 写命令 (控制指令,如设置光标位置):IIC_Send_Byte(0x00)

  2. 写数据 (像素内容):IIC_Send_Byte(0x40)

代码中的OLED_WR_Byte(dat, cmd)封装了这一过程:

cpp 复制代码
void OLED_WR_Byte(unsigned dat, unsigned cmd) {
    IIC_Start();
    IIC_Send_Byte(0x78);  // 设备地址 + 写
    IIC_Recv_Ack();
    if(cmd) {
        IIC_Send_Byte(0x40);  // 数据命令
    } else {
        IIC_Send_Byte(0x00);  // 控制命令
    }
    IIC_Recv_Ack();
    IIC_Send_Byte(dat);
    IIC_Recv_Ack();
    IIC_Stop();
}

显示原理 :OLED内部有8个页(Page0~Page7),每页128列,每列对应一个字节(8个像素)。要显示一个字符,就是把对应的点阵数据(F8X16或F6x8字库)逐字节写入对应位置。

!OLED显示原理示意

比如显示"Fan Speed:",就是循环调用OLED_ShowChar(),每个字符是16×8像素,x坐标每写一个字符+8。


4.5 光敏传感器控制(main.c)

光敏传感器模块通常输出TTL电平

  • 光线足够 → 输出低电平(0)

  • 光线不足 → 输出高电平(1)

代码逻辑极其简单:

cpp 复制代码
if(SUN == 0) {   // 有光
    LED4 = 1;     // 关灯
} else {          // 暗
    LED4 = 0;     // 开灯(低电平点亮)
}

注意代码中LED4是低电平点亮(sbit LED4 = P1^3),所以条件判断是反的:SUN == 0时关灯,SUN == 1时开灯。


五、主程序逻辑流程图

整个main()的执行逻辑:

cpp 复制代码
系统初始化(定时器、串口、OLED)
    ↓
显示标题 "智能家居系统"
    ↓
┌──────────────────────────────────┐
│  循环执行:                      │
│  ├── 每3秒读一次DHT11            │
│  │   ├── 成功 → OLED显示 + 串口发送 │
│  │   └── 失败 → 忽略             │
│  ├── 光敏检测 → 控制LED4         │
│  └── OLED刷新显示风扇速度         │
└──────────────────────────────────┘
    ↑                            │
    └─────────────── 中断驱动 ────┘
    定时器中断:PWM输出 + DHT11触发标志
    串口中断:蓝牙指令响应

状态标志位 read_dht11_flag起到了关键作用------DHT11读取需要时间,不能在主循环里阻塞,所以用定时器每3秒置位一次标志,主循环检测到标志才去读取,实现了非阻塞的定时采集。


六、代码中隐藏的小细节

6.1 数据类型重定义

cpp 复制代码
typedef unsigned char uchar;
typedef unsigned int uint;

后面所有的代码都用ucharuint,比直接写unsigned char更简洁。这是51开发中的常见习惯。

6.2 延时函数的精度

cpp 复制代码
void Delay30us(void) {
    unsigned char data i;
    i = 11;
    while (--i);
}

注意这里用了data关键字,把变量放在片内RAM 中,访问速度更快,延时更准确。如果不加data,编译器可能把它分配到xdata区,访问变慢导致延时不准。

Delay5us()是空函数,因为5us太短,用空操作_nop_()积累,I²C时序是靠Delay5us()配合IIC_SCL/IIC_SDA电平切换来实现的。

6.3 中断服务函数的注意事项

cpp 复制代码
void Timer0_Routine(void) interrupt 1
{
    TH0 = 0XDC; TL0 = 0X00;
    // ...
}

定时器0工作在模式1(16位),每次溢出都需要手动重装初值。如果用模式2(8位自动重装)就不用手动重装,但最大计数值只有256,不够10ms。

6.4 sprintf的使用风险

cpp 复制代码
sprintf(buf, "H:%u%% T:%uC", (uint)humity, (uint)temperature);

sprintf是标准库函数,在51单片机上会占用大量程序空间(ROM)。如果ROM不够用,建议改成手动拼接字符串。不过STC89C52有8KB Flash,这点代码还好。


七、扩展与改进方向

这个项目还有很大的扩展空间,你可以试试:

改进方向 实现思路
增加Wi-Fi模块 用ESP8266取代蓝牙,接入阿里云/OneNET
增加手机APP 用MIT App Inventor或微信小程序做界面
数据存储 外接AT24C02 EEPROM,存储历史数据
增加按键控制 代码里已有key.c,可接入按键手动控制
增加报警功能 温度超限时蜂鸣器报警(P1.6已有BEEP定义)
改进协议 定义帧格式:AA 命令 数据 校验和 BB

八、常见问题排查

现象 可能原因 解决方法
OLED不显示 I²C地址不对/连接错误 检查0x78地址,确认SDA/SCL接线
DHT11读失败 时序不对/上拉电阻 加4.7kΩ上拉电阻,检查Delay30us精度
蓝牙收不到数据 波特率不匹配 HC-05默认9600,确保和代码一致
风扇不转 驱动模块供电不足 电机要用外部5V供电,不能从单片机取电
串口乱码 晶振频率不匹配 确认用的是11.0592MHz,不是12MHz

九、总结

这个项目麻雀虽小五脏俱全,涵盖了:

  • GPIO控制(LED、电机)

  • 定时器中断(PWM、定时任务)

  • 串口中断(蓝牙通信)

  • 单总线协议(DHT11)

  • I²C协议(OLED)

  • 模数混合控制(光敏传感器)

对于51单片机学习者来说,这是一个非常好的综合练习项目。当你把这些模块全部调通的那一刻,你对嵌入式系统的理解会上一个台阶。

如果你在实操中遇到问题,欢迎在评论区留言,我会尽力解答。


项目代码已在文中完整给出,需要工程文件的可以直接复制整理。硬件连接以文中表格为准。祝你调试顺利!

相关推荐
集芯微电科技有限公司1 小时前
替代LM4890音频功率放大器低电磁干扰辐射
人工智能·单片机·嵌入式硬件·生成对抗网络·计算机外设
ACP广源盛139246256732 小时前
国产算力互联IX8024@ACP#Kimi K3 开源后的端侧部署硬件架构分析
大数据·人工智能·分布式·单片机·嵌入式硬件
智信软件3 小时前
MCU中的ADC应用
单片机·嵌入式硬件
xxwxx__3 小时前
51单片机电机控制全攻略:直流电机、舵机与步进电机
c语言·单片机·嵌入式硬件·51单片机
兆龙电子单片机设计14 小时前
【STM32项目开源】STM32单片机语音分类垃圾桶-蓝牙APP
stm32·单片机·嵌入式硬件·物联网·开源·自动化·毕业设计
阿龙的工作记录16 小时前
深入理解Linux设备驱动模型
linux·嵌入式硬件·工作记录
ye1501277745516 小时前
220V转5V1A隔离芯片WT5602
单片机·嵌入式硬件·其他·硬件工程
武汉万象奥科16 小时前
AGV导航控制器方案 | 基于HD-RK3576系列核心板的算控融合一体化平台
arm开发·嵌入式硬件·arm核心板·单板机
ye1501277745517 小时前
DC24V-100V转3.3V500mA隔离芯片WT5602
单片机·嵌入式硬件·其他·硬件工程