去年冬天在宿舍闲着无聊,正好赶上嵌入式系统课程设计,我就琢磨着做个既实用又有趣的项目。作为一个常年赖床的懒人,智能窗帘这个点子突然蹦进脑子——既能自动调节室内光线,又能手动控制,完美符合"懒人科技"的定位。整个项目从构思到完成用了大概两周,期间踩了不少坑,但也积累了不少实战经验。
这个智能窗帘控制系统的核心功能其实很直观:通过光敏电阻检测环境亮度,根据预设阈值自动控制窗帘开合,同时也保留手动操作模式。硬件方面选用了STM32F103C8T6最小系统板作为主控,搭配常见的28BYJ-48步进电机、OLED显示屏和几个按键。最棒的是,用Proteus仿真就能验证全部功能,不用真买硬件,对学生党特别友好。
选用STM32F103C8T6这颗芯片有几个重要考量:
特别值得一提的是它的ADC功能,12位精度完全够用,采样速率最快1MHz,对于光照检测这种低速应用绰绰有余。我用的PA0引脚正好对应ADC1_IN0通道,省去了通道重映射的麻烦。
光敏电阻选用了最常见的GL5528,价格不到1块钱。它的阻值范围在黑暗环境下约1MΩ,强光下约10kΩ,响应曲线比较平滑。为了简化电路,直接采用分压方式连接:
code复制VCC ---[光敏]--- PA0 ---[10kΩ电阻]--- GND
这种接法有个小技巧:电阻值选择要与光敏电阻的典型工作阻值接近(我选10kΩ),这样分压点电压变化范围大,ADC采样值更灵敏。
步进电机选用28BYJ-48,主要考虑:
不过要注意,这种电机单步角度5.625°,加上减速箱后实际步距角约0.088°,所以转动速度较慢,但正好适合窗帘这种不需要快速动作的场景。
ADC配置有几个关键点需要注意:
实际调试中发现,如果不做校准,采样值会漂移严重。我做了组对比测试:
| 校准状态 | 台灯照射下采样值波动范围 |
|---|---|
| 未校准 | ±85 |
| 已校准 | ±12 |
校准后的稳定性提升明显。另外,采样值到实际照度的转换可以更精确:
c复制// 更精确的照度计算(单位:lux)
float GetIlluminance(u16 adc_val)
{
float voltage = adc_val * 3.3 / 4095.0; // 转换为电压
float Rldr = 10000 * (3.3 - voltage) / voltage; // 计算光敏电阻阻值
return 500.0 / (Rldr/1000); // 近似换算为照度
}
SSD1306驱动的OLED屏有几个使用技巧:
我优化后的显示函数增加了更多状态信息:
c复制void OLED_ShowSysStatus(u16 light, u16 threshold, u8 mode, u8 curtain)
{
char buf[32];
OLED_Clear();
// 第一行:亮度值和进度条
sprintf(buf, "L:%04d", light);
OLED_ShowString(0,0,(u8*)buf,16);
DrawProgressBar(40, 2, 80, 10, light*100/4095);
// 第二行:阈值和模式
sprintf(buf, "T:%04d %s", threshold, mode?"MANU":"AUTO");
OLED_ShowString(0,20,(u8*)buf,16);
// 第三行:窗帘状态和提示
sprintf(buf, "Curtain: %s", curtain?"CLOSE":"OPEN ");
OLED_ShowString(0,40,(u8*)buf,16);
OLED_Update();
}
28BYJ-48电机控制可以进一步优化:
改进后的电机驱动函数:
c复制#define MOTOR_STEPS 512 // 电机转一圈的总步数
u32 current_step = 0; // 记录当前步数
void Motor_Run(u8 dir, u16 steps)
{
static u8 motor_seq[8] = {0x01,0x03,0x02,0x06,0x04,0x0c,0x08,0x09};
u16 i,j;
u8 delay = 5; // 初始延迟
for(j=0; j<steps; j++) {
// 逐渐加速
if(j < 20) delay = 5 - j/5;
else if(j > steps-20) delay = 1 + (steps-j)/5;
else delay = 1;
if(dir) {
current_step--;
if(current_step < 0) current_step = MOTOR_STEPS-1;
GPIO_Write(GPIOC, motor_seq[current_step%8]);
} else {
current_step++;
if(current_step >= MOTOR_STEPS) current_step = 0;
GPIO_Write(GPIOC, motor_seq[current_step%8]);
}
delay_ms(delay);
}
}
主循环中加入了更多状态检测和异常处理:
c复制while(1) {
u16 light_val = ADC_GetConversionValue(ADC1);
static u32 last_motor_time = 0;
// 防抖处理:连续采样5次取平均
static u16 light_buf[5] = {0};
static u8 buf_idx = 0;
light_buf[buf_idx++] = light_val;
if(buf_idx >= 5) buf_idx = 0;
u32 light_avg = (light_buf[0]+light_buf[1]+light_buf[2]+light_buf[3]+light_buf[4])/5;
OLED_ShowSysStatus(light_avg, threshold, work_mode, curtain_status);
Key_Scan();
// 电机保护:两次动作至少间隔2秒
if(HAL_GetTick() - last_motor_time < 2000) continue;
if(work_mode == 0) { // 自动模式
if(light_avg > threshold && curtain_status == 0) {
Motor_Run(1, 100); // 关窗帘100步
curtain_status = 1;
last_motor_time = HAL_GetTick();
}
else if(light_avg < threshold - HYSTERESIS && curtain_status == 1) {
Motor_Run(0, 100); // 开窗帘100步
curtain_status = 0;
last_motor_time = HAL_GetTick();
}
}
delay_ms(50);
}
改进了按键扫描函数,支持长按加速和组合键功能:
c复制typedef struct {
u8 cnt;
u8 state;
u8 long_press;
} Key_Type;
Key_Type keys[4]; // 对应4个按键
void Key_Scan_Enhanced(void)
{
u8 i;
static u8 key_pins[4] = {GPIO_Pin_0, GPIO_Pin_1, GPIO_Pin_2, GPIO_Pin_3};
for(i=0; i<4; i++) {
if(GPIO_ReadInputDataBit(GPIOB, key_pins[i]) == 0) {
if(keys[i].cnt < 255) keys[i].cnt++;
// 按下超过30次循环视为长按
if(keys[i].cnt > 30) {
keys[i].long_press = 1;
// 长按加速:每5次循环触发一次
if(keys[i].cnt % 5 == 0) {
keys[i].state = 1;
}
}
} else {
if(keys[i].cnt > 3 && keys[i].cnt <= 30) {
keys[i].state = 1; // 短按
}
keys[i].cnt = 0;
keys[i].long_press = 0;
}
}
// 处理阈值+键
if(keys[0].state) {
threshold += keys[0].long_press ? 100 : 10;
if(threshold > 4095) threshold = 4095;
keys[0].state = 0;
}
// 其他按键处理类似...
}
在Proteus中搭建电路时要注意:
推荐元件列表:
仿真时常见问题及解决方法:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| OLED不显示 | I2C地址错误 | 检查0x3C或0x3D地址 |
| 电机不转 | 相位顺序错误 | 调整ULN2003输出顺序 |
| ADC值不变 | 光敏电阻未连接 | 检查LDR分压电路 |
这个基础框架可以进一步扩展:
蓝牙控制示例代码框架:
c复制void USART2_IRQHandler(void) // 蓝牙串口中断
{
if(USART_GetITStatus(USART2, USART_IT_RXNE)) {
u8 cmd = USART_ReceiveData(USART2);
switch(cmd) {
case 'O': // Open
Motor_Run(0, 100);
curtain_status = 0;
break;
case 'C': // Close
Motor_Run(1, 100);
curtain_status = 1;
break;
case 'A': // Auto
work_mode = 0;
break;
case 'M': // Manual
work_mode = 1;
break;
}
}
}
在实际开发中遇到的典型问题:
电机抖动严重
ADC采样不稳定
按键响应迟钝
OLED显示花屏
调试小技巧:可以在GPIO上接LED作为状态指示,比如: