《基于stm32的智慧家居基础项目》
智慧家居
开发文档:
OneNet开发文档
系统架构设计
首先我想先通过一个图片,来说明项目整体框架
硬件部分
1.按键与LED实现开关功能
LED驱动代码
#include "led.h"
uint8_t led_sta=0;
void led_init(void)
{GPIO_InitTypeDef GPIO_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Pin=GPIO_Pin_13;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOC,&GPIO_InitStructure);GPIO_SetBits(GPIOC,GPIO_Pin_13);
}
void led_turn(void)
{if(GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_13)==1){GPIO_ResetBits(GPIOC,GPIO_Pin_13);led_sta=1; }else{GPIO_SetBits(GPIOC,GPIO_Pin_13);led_sta=0;}
}
void led_off(void)
{GPIO_SetBits(GPIOC,GPIO_Pin_13);led_sta=0;
}void led_on(void)
{GPIO_ResetBits(GPIOC,GPIO_Pin_13);led_sta=1;
}
Key驱动代码
疑点解析:
- 我这里用的是外部中断实现的
- 上拉输入就是默认是高电平1,比如我有个按键,按下就是0,如果是下拉输入,默认是0,按下是1
EXTI9_5_IRQHandler(void)
是触发中断要执行的功能
#include "key.h"void key_init(void)
{GPIO_InitTypeDef my_key_init;EXTI_InitTypeDef my_exti_init;NVIC_InitTypeDef my_nvic_init;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开启gpio口时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//开启外部中断AFIO时钟//gpio初始化my_key_init.GPIO_Mode=GPIO_Mode_IPU;//上拉输入my_key_init.GPIO_Speed=GPIO_Speed_50MHz;my_key_init.GPIO_Pin=GPIO_Pin_6;GPIO_Init(GPIOA,&my_key_init);//AFIO初始化GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource6);//EXTI初始化my_exti_init.EXTI_Line = EXTI_Line6; // 选择中断线 6,对应 GPIO 的第 6 号引脚my_exti_init.EXTI_LineCmd = ENABLE; // 使能这条中断线my_exti_init.EXTI_Mode = EXTI_Mode_Interrupt; // 中断模式(不是事件)my_exti_init.EXTI_Trigger = EXTI_Trigger_Falling;// 触发方式:下降沿触发EXTI_Init(&my_exti_init); // 调用库函数初始化//NVIC初始化//设置优先级my_nvic_init.NVIC_IRQChannel=EXTI9_5_IRQn;my_nvic_init.NVIC_IRQChannelCmd=ENABLE;my_nvic_init.NVIC_IRQChannelPreemptionPriority=2;my_nvic_init.NVIC_IRQChannelSubPriority=2;NVIC_Init(&my_nvic_init);}void EXTI9_5_IRQHandler(void)
{if (EXTI_GetITStatus(EXTI_Line6) != RESET) // 判断是否真的是 EXTI6 触发的中断{if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6) == 0){DelayMs(20); // 简单消抖while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6) == 0);DelayMs(20);led_turn(); // 执行你的功能函数}EXTI_ClearITPendingBit(EXTI_Line6); // 清除中断标志,避免重复进入中断}
}
OLED驱动我采用的是江科大的驱动代码,这里不写出了
2.温度与光照传感器模块
这里主要是
adc
初始化、adc
转化温度光照值
#include "waishe.h"
#include <math.h>#define R1 10000.0f // NTC分压电阻阻值 10kΩ
#define B 3950.0f // NTC B值
#define R0 10000.0f // NTC 25℃时阻值
#define T0 298.15f // 25℃开尔文温度
#define R2 10000.0f // LDR分压电阻阻值 10kΩ
#define ADC_REF 3.3f // ADC参考电压
#define ADC_MAX 4095.0f // 12位ADC最大值void waishe_init(void)
{GPIO_InitTypeDef gpio;ADC_InitTypeDef adc;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);gpio.GPIO_Mode = GPIO_Mode_AIN;gpio.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;GPIO_Init(GPIOA, &gpio);//adc初始化ADC_DeInit(ADC1);adc.ADC_Mode = ADC_Mode_Independent;adc.ADC_ScanConvMode = DISABLE;adc.ADC_ContinuousConvMode = DISABLE;adc.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;adc.ADC_DataAlign = ADC_DataAlign_Right;adc.ADC_NbrOfChannel = 1;ADC_Init(ADC1, &adc);ADC_Cmd(ADC1, ENABLE);ADC_ResetCalibration(ADC1);while (ADC_GetResetCalibrationStatus(ADC1));ADC_StartCalibration(ADC1);while (ADC_GetCalibrationStatus(ADC1));
}uint16_t Read_ADC_Channel(uint8_t channel)
{ADC_RegularChannelConfig(ADC1, channel, 1, ADC_SampleTime_239Cycles5);ADC_SoftwareStartConvCmd(ADC1, ENABLE);while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC));return ADC_GetConversionValue(ADC1);
}// 计算NTC温度,返回摄氏度
float calculate_ntc_temperature(uint16_t adc_val)
{float voltage;float resistance;float temp_k;voltage = adc_val * ADC_REF / ADC_MAX;if (voltage == 0) return -273.15f; // 防止除零错误resistance = (voltage * R1) / (ADC_REF - voltage);temp_k = 1.0f / ( (1.0f / T0) + (1.0f / B) * log(resistance / R0) );return temp_k - 273.15f; // 转摄氏度
}float calculate_ldr_light_level(uint16_t adc_val)
{float voltage;float resistance;float light_level;voltage = adc_val * ADC_REF / ADC_MAX;if (voltage == 0) return 0.0f; // 防止除零resistance = (ADC_REF - voltage) * R2 / voltage;light_level = 100000.0f / resistance;if (light_level > 100.0f) light_level = 100.0f;if (light_level < 0.0f) light_level = 0.0f;return light_level;
}void show_info(float *temperature, float *light)
{uint16_t temp_val = Read_ADC_Channel(ADC_Channel_0); // PA0: NTCuint16_t light_val = Read_ADC_Channel(ADC_Channel_1); // PA1: LDR*temperature = calculate_ntc_temperature(temp_val);*light = calculate_ldr_light_level(light_val);OLED_ShowString(1, 1, "Tem:");OLED_ShowNum(1, 5, (int)(*temperature), 2);OLED_ShowString(2, 1, "Light:");OLED_ShowNum(2, 7, (100-(int)(*light)%100), 2);OLED_ShowString(2, 9, "%");if (GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_13) == 0){OLED_ShowString(3, 1, "Led_status:On "); // 注意后面空格}else{OLED_ShowString(3, 1, "Led_status:Off ");}
}
3.usart通讯模块
Usart1
用于ch340通讯
/*
************************************************************
* 函数名称: Usart1_Init
*
* 函数功能: 串口1初始化
*
* 入口参数: baud:设定的波特率
*
* 返回参数: 无
*
* 说明: TX-PA9 RX-PA10
************************************************************
*/
void Usart1_Init(unsigned int baud)
{GPIO_InitTypeDef gpio_initstruct;USART_InitTypeDef usart_initstruct;NVIC_InitTypeDef nvic_initstruct;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);//PA9 TXDgpio_initstruct.GPIO_Mode = GPIO_Mode_AF_PP;gpio_initstruct.GPIO_Pin = GPIO_Pin_9;gpio_initstruct.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &gpio_initstruct);//PA10 RXDgpio_initstruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;gpio_initstruct.GPIO_Pin = GPIO_Pin_10;gpio_initstruct.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &gpio_initstruct);usart_initstruct.USART_BaudRate = baud;usart_initstruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //无硬件流控usart_initstruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //接收和发送usart_initstruct.USART_Parity = USART_Parity_No; //无校验usart_initstruct.USART_StopBits = USART_StopBits_1; //1位停止位usart_initstruct.USART_WordLength = USART_WordLength_8b; //8位数据位USART_Init(USART1, &usart_initstruct);USART_Cmd(USART1, ENABLE); //使能串口USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //使能接收中断nvic_initstruct.NVIC_IRQChannel = USART1_IRQn;nvic_initstruct.NVIC_IRQChannelCmd = ENABLE;nvic_initstruct.NVIC_IRQChannelPreemptionPriority = 0;nvic_initstruct.NVIC_IRQChannelSubPriority = 2;NVIC_Init(&nvic_initstruct);}
Usart2
用于esp
和服务器通讯
/*
************************************************************
* 函数名称: Usart2_Init
*
* 函数功能: 串口2初始化
*
* 入口参数: baud:设定的波特率
*
* 返回参数: 无
*
* 说明: TX-PA2 RX-PA3
************************************************************
*/
void Usart2_Init(unsigned int baud)
{GPIO_InitTypeDef gpio_initstruct;USART_InitTypeDef usart_initstruct;NVIC_InitTypeDef nvic_initstruct;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);//PA2 TXDgpio_initstruct.GPIO_Mode = GPIO_Mode_AF_PP;gpio_initstruct.GPIO_Pin = GPIO_Pin_2;gpio_initstruct.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &gpio_initstruct);//PA3 RXDgpio_initstruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;gpio_initstruct.GPIO_Pin = GPIO_Pin_3;gpio_initstruct.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &gpio_initstruct);usart_initstruct.USART_BaudRate = baud;usart_initstruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //无硬件流控usart_initstruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //接收和发送usart_initstruct.USART_Parity = USART_Parity_No; //无校验usart_initstruct.USART_StopBits = USART_StopBits_1; //1位停止位usart_initstruct.USART_WordLength = USART_WordLength_8b; //8位数据位USART_Init(USART2, &usart_initstruct);USART_Cmd(USART2, ENABLE); //使能串口USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); //使能接收中断nvic_initstruct.NVIC_IRQChannel = USART2_IRQn;nvic_initstruct.NVIC_IRQChannelCmd = ENABLE;nvic_initstruct.NVIC_IRQChannelPreemptionPriority = 0;nvic_initstruct.NVIC_IRQChannelSubPriority = 0;NVIC_Init(&nvic_initstruct);}
串口函数
/*
************************************************************
* 函数名称: Usart_SendString
*
* 函数功能: 串口数据发送
*
* 入口参数: USARTx:串口组
* str:要发送的数据
* len:数据长度
*
* 返回参数: 无
*
* 说明:
************************************************************
*/
void Usart_SendString(USART_TypeDef *USARTx, unsigned char *str, unsigned short len)
{unsigned short count = 0;for(; count < len; count++){USART_SendData(USARTx, *str++); //发送数据while(USART_GetFlagStatus(USARTx, USART_FLAG_TC) == RESET); //等待发送完成}}/*
************************************************************
* 函数名称: UsartPrintf
*
* 函数功能: 格式化打印
*
* 入口参数: USARTx:串口组
* fmt:不定长参
*
* 返回参数: 无
*
* 说明:
************************************************************
*/
void UsartPrintf(USART_TypeDef *USARTx, char *fmt,...)
{unsigned char UsartPrintfBuf[296];va_list ap;unsigned char *pStr = UsartPrintfBuf;va_start(ap, fmt);vsnprintf((char *)UsartPrintfBuf, sizeof(UsartPrintfBuf), fmt, ap); //格式化va_end(ap);while(*pStr != 0){USART_SendData(USARTx, *pStr++);while(USART_GetFlagStatus(USARTx, USART_FLAG_TC) == RESET);}}/*
************************************************************
* 函数名称: USART1_IRQHandler
*
* 函数功能: 串口1收发中断
*
* 入口参数: 无
*
* 返回参数: 无
*
* 说明:
************************************************************
*/
void USART1_IRQHandler(void)
{if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断{USART_ClearFlag(USART1, USART_FLAG_RXNE);}}
4.esp8266驱动
为了便于后续了解外设各个功能,我这里拆解一下esp的驱动代码
#define REV_OK 0 //接收完成标志 #define REV_WAIT 1 //接收未完成标志
1.初始化
void ESP8266_Init(void)
{ESP8266_Clear();UsartPrintf(USART_DEBUG, "1. AT\r\n"); //u1负责打印到串口助手while(ESP8266_SendCmd("AT\r\n", "OK"))DelayXms(500);UsartPrintf(USART_DEBUG, "2. CWMODE\r\n");while(ESP8266_SendCmd("AT+CWMODE=1\r\n", "OK"))DelayXms(500);UsartPrintf(USART_DEBUG, "3. AT+CWDHCP\r\n");while(ESP8266_SendCmd("AT+CWDHCP=1,1\r\n", "OK"))DelayXms(500);UsartPrintf(USART_DEBUG, "4. CWJAP\r\n");while(ESP8266_SendCmd(ESP8266_WIFI_INFO, "GOT IP"))DelayXms(500);UsartPrintf(USART_DEBUG, "5. ESP8266 Init OK\r\n");}
2.发送esp
执行命令
Bool ESP8266_SendCmd(char *cmd, char *res)
{unsigned char timeOut = 200;Usart_SendString(USART2, (unsigned char *)cmd, strlen((const char *)cmd));while(timeOut--){if(ESP8266_WaitRecive() == REV_OK) //如果收到数据{if(strstr((const char *)esp8266_buf, res) != NULL) //如果检索到关键词{ESP8266_Clear(); //清空缓存return 0;}} DelayXms(10);}return 1;
}
3.清空缓存
void ESP8266_Clear(void)
{memset(esp8266_buf, 0, sizeof(esp8266_buf));esp8266_cnt = 0;
}
4.等待响应
Bool ESP8266_WaitRecive(void)
{if(esp8266_cnt == 0) //如果接收计数为0 则说明没有处于接收数据中,所以直接跳出,结束函数return REV_WAIT;if(esp8266_cnt == esp8266_cntPre) //如果上一次的值和这次相同,则说明接收完毕{esp8266_cnt = 0; //清0接收计数return REV_OK; //返回接收完成标志}esp8266_cntPre = esp8266_cnt; //置为相同return REV_WAIT; //返回接收未完成标志}
5.esp发送数据给服务器
AT+CIPSEND=%d\r\n
这是发送数据给服务器的指令,所以下面串口发送的数据不是给esp而是服务器
void ESP8266_SendData(unsigned char *data, unsigned short len)
{char cmdBuf[32];ESP8266_Clear(); //清空接收缓存sprintf(cmdBuf, "AT+CIPSEND=%d\r\n", len); //发送命令if(!ESP8266_SendCmd(cmdBuf, ">")) //收到‘>’时可以发送数据{Usart_SendString(USART2, data, len); //发送设备连接请求数据}
}
6.获取平台返回数据
unsigned char *ESP8266_GetIPD(unsigned short timeOut)
{char *ptrIPD = NULL;do{if(ESP8266_WaitRecive() == REV_OK) //如果接收完成{ptrIPD = strstr((char *)esp8266_buf, "IPD,"); //搜索“IPD”头if(ptrIPD == NULL) //如果没找到,可能是IPD头的延迟,还是需要等待一会,但不会超过设定的时间{//UsartPrintf(USART_DEBUG, "\"IPD\" not found\r\n");}else{ptrIPD = strchr(ptrIPD, ':'); //找到':'if(ptrIPD != NULL){ptrIPD++;return (unsigned char *)(ptrIPD);}elsereturn NULL; }}DelayXms(5); //延时等待} while(timeOut--); return NULL; //超时还未找到,返回空指针
}
通信部分
1.mqtt
这里我们要用到MQTT.fx
这一个客户端去进行测试,客户端就是在模拟我们设备(这个项目),去和服务器通讯
你作为客户端程序:
- 想要连服务器 → 调
MQTT_PacketConnect()
组一个 CONNECT 报文,通过 TCP 发送。 - 想要订阅主题 → 调
MQTT_PacketSubscribe()
组一个 SUBSCRIBE 报文,发给服务器。 - 想要发消息 → 调
MQTT_PacketPublish()
组一个 PUBLISH 报文,发给服务器。
服务器返回的数据(二进制 MQTT 报文):
- 收到后先用
MQTT_UnPacketRecv()
或者具体的解包函数,比如MQTT_UnPacketConnectAck()
、MQTT_UnPacketPublish()
来解析。 - 解析出来的结果就是你能在代码里直接用的内容(比如连接是否成功、主题是什么、消息内容是什么)。
小程序部分
先介绍一下
JavaScript 和 JSON 是两个不同的东西:
JavaScript
-
是一种编程语言,用来写逻辑、操作网页、调用 API、控制流程等。
-
例如:
let name = "Tom"; function sayHello() {console.log("Hello, " + name); } sayHello();
👉 这是 JavaScript 代码,能执行。
JSON (JavaScript Object Notation)
-
是一种 数据格式,长得很像 JavaScript 的对象语法,但只能用来存数据,不能执行逻辑。
-
常用在前后端通信、配置文件里。
-
例如:
{"name": "Tom","age": 18,"isStudent": true }
👉 这是 JSON,只能存数据,不能像 JavaScript 那样写函数或逻辑。
关系
-
JSON 的语法最早就是从 JavaScript 对象语法演变出来的。
-
但 JSON 独立于语言,Python、Java、C、Go 等都能读写 JSON。
-
JavaScript 可以很方便地处理 JSON:
let obj = JSON.parse('{"name":"Tom"}'); // JSON → JS 对象 console.log(obj.name);let str = JSON.stringify({name: "Tom"}); // JS 对象 → JSON console.log(str);
👉 所以:
- JavaScript = 一门编程语言
- JSON = 一种数据交换格式
Vue 组件的 <script>
,功能是:
👉 定时从 OneNET 平台获取设备数据(温度、光照、LED 状态),并能控制 LED 开关。
1. 导入部分
const { createCommonToken } = require('@/key.js')
- 从
key.js
文件里引入了createCommonToken
函数。 - 这个函数用来生成 鉴权 token,请求 OneNET 平台 API 时要带上。
2. data 数据
data() {return {temp: '', // 温度light: '', // 光照led: true, // led状态(布尔值)token: '', // 鉴权 token}
}
这些变量绑定到页面 <template>
里,用来显示和交互。
3. 生命周期钩子
onLoad() {const params = {author_key: 'xxx',version: '2022-05-01',user_id: '460751',}this.token = createCommonToken(params);
},
- 页面加载时执行。
- 传入密钥、版本号、用户 ID,生成 token,保存到
this.token
。
onShow() {this.fetchDevData();setInterval(() => {this.fetchDevData();}, 3000)
},
- 页面显示时执行。
- 先调用一次
fetchDevData()
获取设备数据。 - 然后每隔 3 秒刷新一次数据,实现 实时监控。
4. methods 方法
(1) 获取设备数据
fetchDevData() {uni.request({url: 'https://iot-api.heclouds.com/thingmodel/query-device-property',method: 'GET',data: {product_id: 'ECHECArR1s',device_name: 't1'},header: {'authorization': this.token // 自定义请求头信息},success: (res) => {console.log(res.data);this.led = res.data.data[0].value === 'true';this.light = res.data.data[1].value;this.temp = res.data.data[2].value;}})
}
- 调用 OneNET 设备属性查询接口,获取
t1
设备的数据。 - 带上
authorization
头部(token)。 - 成功后解析返回数据,更新页面的 LED、光照、温度。
this.led = res.data.data[0].value === 'true';
→ 解析出 LED 状态并转成布尔值。this.light = res.data.data[1].value;
→ 光照数值。this.temp = res.data.data[2].value;
→ 温度数值。
(2) 控制 LED 开关
on_led(event) {let value = event.detail.value;uni.request({url: 'https://iot-api.heclouds.com/thingmodel/set-device-property',method: 'POST',data: {product_id: 'ECHECArR1s',device_name: 't1',params: {"led": value}},header: {'authorization': this.token},success: () => {console.log();}})
}
- 当用户在页面点击
<switch>
控件时触发。 - 获取开关状态
event.detail.value
(true / false)。 - 调用 OneNET 设置设备属性接口,修改 LED 的状态。
- 请求体里的
params: {"led": value}
告诉服务器要开还是关。
5. 总体流程
- 生成 token → 用于鉴权。
- 页面加载 → 保存 token。
- 页面显示 → 每 3 秒调用一次
OneNET
查询接口,获取设备属性(温度、光照、LED)。 - 用户切换开关 → 调用
OneNET
设置接口,控制设备的 LED 灯。
内容还需更新改进,感谢Onenet平台提供的开发文档以及demo,后续可以在此基础添加新的功能