【项目开源】基于STM32的智能路灯控制系统设计
本文介绍了一个基于STM32的智能路灯控制系统设计方案。系统通过BH1750光照传感器检测环境亮度,结合HC-SR501人体红外传感器判断人员活动情况,实现按需照明功能。当光照低于阈值且检测到有人时自动开灯,光照充足或无人时自动关灯。系统采用ESP-01S WiFi模块通过MQTT协议上报路灯状态、光照值和人体检测数据至服务器,实现远程监控。文章详细说明了硬件连接方式、ESP01S固件配置要求以及
项目说明
使用STM32为主要控制器,检测到周围光照强度高于一定值或者无人情况下自动关闭路灯,光照强度小于一定值且有人时自动开灯。
功能如下:
1、使用 BH1750 获得光照强度。
2、人体检测传感器判断是否有人。
3、根据检测情况自动开关路灯。
4、使用 OLED 屏幕显示检测状态。
5、使用 ESP8266 连接 WIFI 通过 MQTT 实时上报检测数据。
6、可使用 MQTT 软件检测上报数据。
7、可使用 APP 查看软件检测上报数据。
项目开源链接
本项目资料完全开源。资料包获取方式:
github : https://github.com/snqx-lqh/ProjectReleasePage
gitee(国内镜像) :https://gitee.com/snqx-lqh/ProjectOpenSourceReleasePage
项目属于 32 的编号 B001 ,在发布页中,找到对应项目获取方式。
硬件设计
硬件设计如图所示。

实际接线如下:
BH1750:
- SCL->PB6
- SDA->PB7
- ADDR->GND
- VCC->3V3
- GND->GND
OLED: - SCK->PB8
- SDA->PB9
- VCC->3V3
- GND->GND
HC-SR501: - VCC->5V
- OUT->PA0
- GND->GND
- 模块上的功能跳帽接到 L
ESP8266: - 3V3->3V3
- GND->GND
- TX->PB11
- RX->PB10
USB转TTL: - TX->PA10
- RX->PA9
- GND->GND
软件设计
软件设计包含驱动设计和具体功能设计。
驱动设计
BH1750驱动
关于 BH1750 ,我们只需要知道怎么使用 IIC 读取值和写入值就可以了。我感觉它和常规IIC器件不同,因为一般都是先写设备地址,然后找寄存器,然后再读写数,他好像没有寄存器地址这一说法,写数据就先写地址再写数据就行,读也是先写地址,然后紧接着读两个数。
更完善的内容可以看我之前写的博客文章。
STM32驱动BH1750:https://blog.csdn.net/wan1234512/article/details/157580681?spm=1011.2124.3001.6209
HC-SR501驱动
这个设备,由于它是检测到人后产生电平变化,所以使用了引脚中断来检测他的变化。检测到人的话,模块电平输出会由低到高,我们只需要抓这个上升沿即可,再使用一个全局变量状态标志位,标志这个中断已被触发。
void EXTI0_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line0)!= RESET)
{
hc_sr50_state = 1;
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
ESP01S驱动
ESP01S使用的固件是 (1471)ESP8266-AT-1M.bin。这个在我的开源文件中包含。
关于此设备驱动,最重要的是怎么处理它返回值的不定长以及不是及时响应,一个命令可能会隔段时间才会回应,还不连续。所以,我们使用一个环形缓冲区把接收到的内容先暂存,然后再处理。
环形缓冲区这里使用了 RT-Thread 中的环形缓冲区思想,但是只保留了一些常用函数。环形缓冲区可以当作被两个索引管理的数组。我们只谈使用。在串口接收中断中,我们将接收到的数值放到环形缓冲区中,在主任务中进行提取处理。
主要的接收处理如下:
void USART3_IRQHandler(void)
{
if (USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)
{
uint8_t data = USART_ReceiveData(USART3);
ringbuffer_putchar(&esp01s_device.rx_rb, data); // 将数据放入环形缓冲区 esp01s_device.rx_rb 中
USART_ClearITPendingBit(USART3, USART_IT_RXNE);
}
}
esp01s_device 是一个管理 ESP01S 变量的结构体变量,rx_rb 就是里面定义的环形缓冲结构体。
然后在 主任务中,我的发送指令函数如下:
int esp_at_cmd(struct esp01s *esp, const char *cmd, const char *expect, uint16_t timeout_ms)
{
// 清空缓存(很重要)
int ret = 0;
ringbuffer_reset(&esp->rx_rb);
esp_send(cmd);
esp_send("\r\n");
ret = esp_wait_response(esp, expect, timeout_ms);
esp->resp_len = 0;
return ret;
}
int esp_wait_response(struct esp01s *dev, const char *expect, uint32_t timeout)
{
uint16_t delay_ms_counter = 0;
dev->resp_len = 0;
while (delay_ms_counter < timeout)
{
uint8_t ch;
if (ringbuffer_getchar(&dev->rx_rb, &ch) == 1)
{
// 保存数据
if (dev->resp_len < ESP_RESP_BUF_SIZE - 1)
{
dev->resp_buf[dev->resp_len++] = ch;
dev->resp_buf[dev->resp_len] = '\0';
}
// 判断成功
if (strstr((char *)dev->resp_buf, expect))
{
return 0;
}
// 判断错误
if (strstr((char *)dev->resp_buf, "ERROR"))
{
return -1;
}
} else {
// 没有数据,等待一段时间
delay_ms_counter++;
esp_delay_ms(1);
}
}
return -2; // 超时
}
发送数据完成后,就等待响应,响应就是把串口中断中环形缓冲区得到的值,一个个取出来存到数组中,然后再进行判断,比如判断是否接收到响应字符,是否接收到错误字符。
OLED驱动
OLED 就是使用的中景园电子的,只不过使用的我自己的 IIC 函数。
应用设计
应用设计首先是初始化。然后是while任务轮询。
初始化
在初始化中,值得注意的就是 ESP01S 的初始化,其他驱动初始化比较一般。
我们的 ESP 01s 使用了 MQTT ,所以我们需要连接一些 MQTT 的配置。
首先是 ESP 复位,没什么好说的,只是需要切换到station模式,也就是使用 "AT+CWMODE=1" 步骤如下:
ret = esp_at_cmd(&esp01s_device, "AT+RST", "OK", 2000);
printf("AT+RESET resp: %s\n", esp01s_device.resp_buf);
if(ret != 0) while(1);
delay_ms(500);
ret = esp_at_cmd(&esp01s_device, "AT", "OK", 2000);
printf("AT resp: %s\n", esp01s_device.resp_buf);
if(ret != 0) while(1);
ret = esp_at_cmd(&esp01s_device, "AT+CWMODE=1", "OK", 2000);
printf("AT+CWMODE resp: %s\n", esp01s_device.resp_buf);
if(ret != 0) while(1);
然后是连接 WIFI。这是我的 WIFI 名 和 密码 。你需要替换成自己的。
ret = esp_at_cmd(&esp01s_device, "AT+CWJAP=\"CMCC-XJmL\",\"sR62HiPv\"", "OK", 5000);
printf("AT+CWJAP resp: %s\n", esp01s_device.resp_buf);
if(ret != 0) while(1);
然后是 MQTT 的处理,下面是常用指令:
AT+MQTTUSERCFG=0,1,"用户ID","账号","密码",0,0,""
# 设置MQTT连接所需要的的参数,包括用户ID(不为空)、
# 账号(admin)以及密码(public)
AT+MQTTCONN=0,"broker.emqx.io",1883,0
AT+MQTTPUB=0,"ESP8266/online","1",0,0
#发布一条topic为“ESP8266/online”,message为“1”的数据, #QOS设置为0
AT+MQTTSUB=0,"ESP8266/EMQX",0
#订阅一条topic为“ESP8266/EMQX”,QOS为0的数据
这里需要注意,需要先配置用户 ID ,不要使用 test 这种 ID 最好,因为我使用的公共测试服务器 broker.emqx.io,用 test 这个名字很可能连不上。
ret = esp_at_cmd(&esp01s_device, "AT+MQTTUSERCFG=0,1,\"user\",\"user\",\"123\",0,0,\"\"", "OK", 2000);
printf("AT+MQTTUSERCFG resp: %s\n", esp01s_device.resp_buf);
if(ret != 0) while(1);
ret = esp_at_cmd(&esp01s_device, "AT+MQTTCONN=0,\"broker.emqx.io\",1883,0", "OK", 5000);
printf("AT+MQTTCONN resp: %s\n", esp01s_device.resp_buf);
if(ret != 0) while(1);
我的代码还订阅了 /user/mqtttest/led 做调试。
ret = esp_at_cmd(&esp01s_device, "AT+MQTTSUB=0,\"/user/mqtttest/led\",0", "OK", 2000);
printf("AT+MQTTSUB resp: %s\n", esp01s_device.resp_buf);
if(ret != 0) while(1);
while 循环
在任务循环中,做了一个简单的计数器,每隔10ms加1,以此计数器达到计时作用,这个其实并不准确,需要准确最好使用定时器来做这个计数。但是我们这个项目不需要太高的实时性,这样也可以用。
首先是是否有人经过的检测,前面我们说过,有人的时候,中断中hc_sr50_state会置1,我们得到这个标识后,可以把有人的状态保持久一点,有人,我就做一个计数器加加,在加到200这个时间内,都是表示有人状态 hc_sr50x_delay_state 为 1 。因为一个while循环延时10ms,所以循环200次,也就是2S,但是这个2S其实没那么准,因为你一轮WHILE循环不使用定时器也不是一定的10ms。
// 如果有人 hc_sr50_state 这个状态在中断中置 1
if(hc_sr50_state == 1)
{
// 有人就处于状态延迟状态
hc_sr50x_delay_state = 1;
hc_sr50x_state_delay_count = 0;
hc_sr50_state = 0;
printf("Someone has entered the area\r\n");
}
// 如果处于状态延迟状态
if(hc_sr50x_delay_state == 1)
{
hc_sr50x_state_delay_count++;
if(hc_sr50x_state_delay_count > 200) // 将每次的有人进入状态至少保持 2S
{
// 2S后清空这个 状态延迟状态
hc_sr50x_state_delay_count = 0;
hc_sr50x_delay_state = 0;
}
}
然后是 1S 的时候进行光照强度读取以及数据上报,这个就比较简单了,就是一个读取后比较。
if(count % 100 == 0) // 1S 读取一次光照强度
{
if (bh1750_read_lux(BH1750_ADDR_L, BH1750_CONT_H_RES, &lux) == 0)
{
printf("continuous Light = %.2f lux hc_sr50x_delay_state:%d\r\n", lux, hc_sr50x_delay_state);
}
// 光照小于一定值,且有人处于状态延迟状态时,打开灯
if(lux < 500 && hc_sr50x_delay_state == 1){
led_on();
led_state = 1;
}else{
// 其他情况关灯
led_off();
led_state = 0;
}
// 上报状态信息
sprintf(mqtt_put_message,"AT+MQTTPUB=0,\"/user/mqtttest/led_state\",\"%d\",0,0",led_state);
esp_at_cmd(&esp01s_device, mqtt_put_message , "OK", 2000);
printf("AT+MQTTSUB resp: %s\n", esp01s_device.resp_buf);
sprintf(mqtt_put_message,"AT+MQTTPUB=0,\"/user/mqtttest/lux\",\"%.2f\",0,0",lux);
esp_at_cmd(&esp01s_device, mqtt_put_message , "OK", 2000);
printf("AT+MQTTSUB resp: %s\n", esp01s_device.resp_buf);
sprintf(mqtt_put_message,"AT+MQTTPUB=0,\"/user/mqtttest/hc_sr50x_delay_state\",\"%d\",0,0",hc_sr50x_delay_state);
esp_at_cmd(&esp01s_device, mqtt_put_message , "OK", 2000);
printf("AT+MQTTSUB resp: %s\n", esp01s_device.resp_buf);
// 更新一次界面显示
OLED_Clear_Buffer(); //清除之前的缓存
sprintf((char*)oled_show_str,"Smart LED");
OLED_ShowString(24,0,oled_show_str,16,1);
sprintf((char*)oled_show_str,"LED: %s",led_state==1?"ON":"OFF");
OLED_ShowString(0,16,oled_show_str,16,1);
sprintf((char*)oled_show_str,"LUX: %.2f",lux);
OLED_ShowString(0,32,oled_show_str,16,1);
sprintf((char*)oled_show_str,"PERSON: %s",hc_sr50x_delay_state == 1?"Y":"N");
OLED_ShowString(0,48,oled_show_str,16,1);
OLED_Refresh(); //更新
}
最后还做了一个 WIFI 接收信息解析,因为我们前面不是订阅了一个消息吗。当我们读取到一行 WIFI 发送的数据时,对其进行解析。
// 读取 ESP 接收
int ret = esp_read_line(&esp01s_device);
if(ret == 0)
{
printf("Received line: %s", esp01s_device.resp_buf);
esp_dispatch_line(&esp01s_device, (char *)esp01s_device.resp_buf);
} else {
// 没有完整行数据,继续等待
}
mqttfx 连接测试
我们上报的信息,想要快速查看是否真的上报成功,可以使用 mqttfx 工具进行查看。工具在我的开源文件中包含。
1、点击设置,准备创建一个连接。

2、点击此处新建一个连接。

我把这个连接创建为 NewConnect 并且配置访问服务器。

主页面点击连接即可。

然后我们订阅一个主题,主题名自己输入,需要和代码中发布的主题一致。

便可以看到发布的消息了。

APP
本项目还使用 Uni-APP 搭建了一个简易的手机 APP 。
开发过程中遇到的主要问题是 mqtt 库 的选取,在下载mqtt库的时候,一定要下载3.0.0,其他的4.1.0和更高版本,我用着总是有莫名其妙的问题。
安装流程如下,前提是你的UNI-APP开发环境已经搭建完成。
安装
首先安装mqtt.js,建议使用较为稳定的3.0.0版本,在项目工程下打开终端。
npm install mqtt@3.0.0
请注意一定要在main.js中增加如下代码。

// #ifndef MP
// 处理 wx.connectSocket promisify 兼容问题,强制返回 SocketTask
uni.connectSocket = (function(connectSocket) {
return function(options) {
console.log(options)
options.success = options.success || function() {}
return connectSocket.call(this, options)
}
})(uni.connectSocket)
// #endif
软件编写
直接放代码了,由于是示例程序,代码结构比较简单。
<template>
<!-- 页面主容器:负责整体页面的留白和布局容器 -->
<view class="page-wrapper">
<!-- 卡片容器:使用 flex 布局,并让子元素自动换行 -->
<view class="card-grid">
<!-- 卡片1:路灯亮度 -->
<view class="card">
<!-- 卡片图标:显示灯的图标或 logo -->
<image class="card-icon" src="/static/light.png" mode="aspectFit"></image>
<!-- 卡片内容区域:标题和亮度信息的容器 -->
<view class="card-content">
<!-- 卡片标题:显示项目名称 -->
<text class="card-title">路灯亮度</text>
<!-- 卡片信息行:显示 lux 单位 -->
<view class="card-info">
<!-- 显示路灯亮度,单位为 lux -->
<text class="info-text">{{ streetLightLux }} lux</text>
</view>
</view>
</view>
<!-- 卡片2:是否有人 -->
<view class="card">
<!-- 卡片图标 -->
<image class="card-icon" src="/static/person.png" mode="aspectFit"></image>
<!-- 卡片内容区域 -->
<view class="card-content">
<!-- 卡片标题 -->
<text class="card-title">是否有人</text>
<!-- 卡片信息行 -->
<view class="card-info">
<!-- 1 代表有人,0 代表无人 -->
<text class="info-text">{{ hasPerson ? '1' : '0' }}</text>
</view>
</view>
</view>
<!-- 卡片3:LED 状态 -->
<view class="card">
<!-- 卡片图标 -->
<image class="card-icon" src="/static/led.png" mode="aspectFit"></image>
<!-- 卡片内容区域 -->
<view class="card-content">
<!-- 卡片标题 -->
<text class="card-title">LED 状态</text>
<!-- 卡片信息行 -->
<view class="card-info">
<!-- 显示 LED 是否点亮 -->
<text class="info-text">{{ ledOn ? '已点亮' : '未点亮' }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
const MQTT_TOPICS = {
streetLightLux: '/user/mqtttest/lux',
hasPerson: '/user/mqtttest/hc_sr50x_delay_state',
ledOn: '/user/mqtttest/led_state'
}
import mqtt from 'mqtt/dist/mqtt.js'
export default {
data() {
return {
streetLightLux: 0,
hasPerson: false,
ledOn: false,
mqttStatus: '未连接',
client: null
}
},
onLoad() {
this.initMqttClient()
},
onUnload() {
this.disconnectMqttClient()
},
methods: {
// 初始化 MQTT 客户端
initMqttClient() {
const MQTT_GATWAY = 'broker.emqx.io:8083/mqtt'
const MQTT_USERNAME = 'username'
const MQTT_PASSWORD = '123456'
const MQTT_OPTIONS = {
connectTimeout: 5000,
clientId: 'uniapp_' + Math.random().toString(36).substr(2, 9),
username: MQTT_USERNAME,
password: MQTT_PASSWORD,
clean: true
}
console.log('starting init mqtt')
let client = null
// #ifdef H5
client = mqtt.connect('ws://' + MQTT_GATWAY, MQTT_OPTIONS)
// #endif
// #ifdef APP-PLUS
console.log('applus')
client = mqtt.connect('wx://' + MQTT_GATWAY, MQTT_OPTIONS)
// #endif
if (!client) {
console.warn('当前平台不支持 MQTT')
this.mqttStatus = '当前平台不支持'
return
}
this.client = client
client.on('connect', () => {
console.log('连接成功!')
this.mqttStatus = '已连接'
client.subscribe(
Object.values(MQTT_TOPICS),
{ qos: 0 },
(err) => {
if (!err) {
console.log('订阅成功!')
} else {
console.error('订阅失败:', err)
}
}
)
})
client.on('reconnect', () => {
console.log('正在重连...')
this.mqttStatus = '正在重连'
})
client.on('end', () => {
console.log('连接断开!')
this.mqttStatus = '已断开'
})
client.on('error', (err) => {
console.error('MQTT 错误:', err)
this.mqttStatus = '连接异常'
})
client.on('message', (topic, message) => {
const msg = message.toString()
console.log('Received Message:', msg, 'On topic:', topic)
this.handleMessage(topic, msg)
})
},
// 处理收到的消息,更新页面数据
handleMessage(topic, message) {
switch (topic) {
case MQTT_TOPICS.streetLightLux:
this.streetLightLux = Number(message) || 0
break
case MQTT_TOPICS.hasPerson:
this.hasPerson = message === '1' || message === 'true'
break
case MQTT_TOPICS.ledOn:
this.ledOn = message === '1' || message === 'true'
break
default:
console.log('未知主题:', topic)
}
},
// 发布消息(保留方法,方便后续调用)
mqttPublish(topic, data) {
if (this.client && this.client.connected) {
this.client.publish(
topic,
JSON.stringify(data),
{ qos: 1 }
)
} else {
console.warn('MQTT 未连接,无法发布')
}
},
// 断开连接
disconnectMqttClient() {
if (this.client) {
this.client.end()
this.client = null
}
}
}
}
</script>
<style scoped>
.page-wrapper {
padding: 20rpx;
box-sizing: border-box;
}
.card-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.card {
background-color: #fff;
border-radius: 10rpx;
padding: 20rpx;
box-sizing: border-box;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
}
/* 卡片图标样式:固定尺寸并与文字保持间距 */
.card-icon {
width: 80rpx;
height: 80rpx;
margin-right: 20rpx;
}
/* 卡片内容区域样式:让文字区域占据剩余水平空间 */
.card-content {
flex: 1;
}
/* 卡片标题样式:独占一行、大号字体、加粗 */
.card-title {
display: block;
font-size: 32rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
/* 卡片信息行样式:横向排列信息内容,居中对齐 */
.card-info {
display: flex;
align-items: center;
}
/* 信息文字样式:设置字号和右侧间距 */
.info-text {
font-size: 28rpx;
margin-right: 20rpx;
}
</style>
然后就可以正常使用了,建议安卓模拟器使用 MuMu模拟器,这样就不用去下载安卓开发软件了。最后云打包即可。


打包完成后,下载应用,就可以看到

但是我感觉 uniapp 的响应总是很慢,不知道为什么。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)