基于物联网技术的电气火灾报警控制系统的设计【玩转华为云】

举报
DS小龙哥 发表于 2024/06/20 10:59:26 2024/06/20
【摘要】 能够采集本地环境的温度、湿度、烟雾浓度,火光信息,在OLED显示屏上显示。 如果检测到烟雾、温度、火光超过阀值会触发蜂鸣器报警。 同时通过WIFI将本都设备采集的信息上传到华为云物联网云平台。 通过Qt(C++)开发Android手机APP,从华为云物联网云平台获取设备最新的数据进行显示,完成信息展示。

一、前言

视频教程: 手把手讲解华为云物联网云平台的使用以及应用侧的开发(2024最新版)

https://www.bilibili.com/video/BV1mr421c75S


1.1 项目介绍

【1】项目开发背景

随着现代城市化进程的加速,建筑电气系统的复杂性与日俱增,电气火灾作为城市火灾的主要类型之一,给人们的生命财产安全带来了严重威胁。据统计,电气故障是引发火灾的重要原因之一,其突发性强、蔓延速度快,往往造成难以估量的损失。因此,构建高效可靠的电气火灾预警系统显得尤为重要。

近年来,物联网(IoT)技术的飞速发展为消防安全管理提供了新的解决方案。物联网技术通过智能感知、无线通信等手段,实现对物理世界数据的实时采集、传输与处理,为火灾预防、早期预警及快速响应提供了强有力的技术支持。结合云计算平台的数据处理与分析能力,可以实现远程监控、智能分析和预警推送等功能,极大地提高了火灾防控的效率与准确性。

在此背景下,本项目设计并实现一套基于物联网技术的电气火灾报警控制系统。该系统充分利用先进的传感器技术和无线通信技术,针对电气火灾的主要诱因——异常温升、烟雾浓度增加以及火光出现,进行实时监测与预警。通过集成STM32F103RCT6微控制器作为核心控制单元,搭配高精度的SHT30温湿度传感器、MQ2烟雾传感器、火光检测传感器,以及ESP8266 WiFi模组,构建了一个集数据采集、处理、报警与远程监控于一体的智能化系统。

系统不仅能在现场通过OLED显示屏直观显示环境状态,还能在检测到火灾隐患时立即启动蜂鸣器报警,同时借助华为云物联网平台实现数据的云端存储与分析,用户可以通过专门开发的Qt(C++) Android手机APP远程查看设备实时监测数据,及时掌握电气安全状况,实现预防性维护与管理,大大降低了电气火灾的发生概率,保障了公共与私人空间的安全。

综上所述,本项目的开发是顺应时代需求,利用现代科技手段解决实际安全问题的一次重要尝试,对于提升社会消防安全管理水平,构建智慧城市具有重要意义。

image-20240612102258041

image-20240612101311495

【2】设计实现的功能

(1)环境参数实时监测

  • 温湿度监测:集成SHT30温湿度传感器,连续采集并显示当前环境的温度和湿度值。
  • 烟雾浓度监测:利用MQ2气体传感器,监测环境中烟雾(及可燃气体)的浓度,预防火灾发生。
  • 火光检测:通过专用的火光检测传感器,识别环境中是否存在异常火光或高温辐射源,增加火灾预警的准确性。

(2)本地数据显示

  • 采用0.96寸OLED显示屏,通过SPI接口与STM32F103RCT6主控芯片连接,实时显示温湿度、烟雾浓度及系统工作状态,便于现场直观了解环境状况。

(3)智能预警系统

  • 设定合理的温湿度、烟雾浓度及火光强度阈值,一旦监测数据超过预设安全范围,立即激活高电平触发的有源蜂鸣器报警,及时发出声光警示。

(4)云端数据上传与管理

  • 配置ESP8266 WiFi模块,实现与华为云物联网平台的稳定连接。自动上传监测到的环境数据,包括温度、湿度、烟雾浓度和火光检测状态,支持远程监控和数据分析。

(5)远程监控APP

  • 利用Qt(C++)框架开发的Android手机应用程序,用户可以远程查看每个设备的实时数据和历史记录,接收报警通知,便于远程管理和快速响应。

(6)系统自检与维护

  • 设备内置自检机制,定期检查各传感器、WiFi模块及主控芯片的工作状态,确保系统正常运行,异常时向云端发送故障报告。

(7)电源管理

  • 采用外部稳定的5V 2A电源供电,保证系统长时间稳定运行,避免因电源问题引起的误报或系统中断。

(8)安全加密通信

  • 数据传输过程采用加密技术,确保从设备到华为云物联网平台之间的通信安全,保护用户数据隐私。

【3】项目硬件模块组成

(1)主控单元

  • STM32F103RCT6微控制器:作为系统的核心控制器件,负责处理传感器输入数据,执行逻辑判断,驱动显示及报警设备,并通过WiFi模块与云端通信。该芯片具有高性能、低功耗的特点,支持SPI、I2C等多种通信接口。

(2)环境监测模块

  • SHT30温湿度传感器:通过I2C接口与STM32连接,高精度测量环境温度和湿度,为火灾预警提供基础数据。
  • MQ2烟雾传感器:通过模拟输入方式连接到STM32,检测空气中的烟雾浓度,对火灾初期的烟雾进行预警。
  • 火光检测传感器:使用光电原理或红外技术,监测环境中火光或高温辐射的存在,增加火灾预警的灵敏度。

(3)显示与报警模块

  • 0.96寸OLED显示屏:采用SPI通信协议与主控芯片相连,用于实时显示温湿度、烟雾浓度及系统状态信息,提供直观的现场监控界面。
  • 有源蜂鸣器:由STM32直接驱动(高电平触发),当检测到火灾风险时立即发声报警,快速引起注意。

(4)无线通信模块

  • ESP8266 WiFi模块:通过UART接口与STM32F103RCT6连接,实现设备与华为云物联网云平台的无线数据传输,支持TCP/IP协议栈,确保数据实时上传至云端。

(5)电源模块

  • 外部5V 2A电源适配器:为整个系统提供稳定电源供应,保证所有组件正常运作,且留有足够的功率余量应对峰值电流需求。

【4】摘要

本项目设计并实现一种基于物联网技术的电气火灾报警控制系统,以提升电气火灾的预警能力和应急响应速度。系统核心采用高性能STM32F103RCT6微控制器,集成SHT30温湿度传感器、MQ2烟雾传感器、火光检测传感器进行多维度环境监测,通过OLED显示屏实时展示现场数据,并在检测到危险阈值时,利用有源蜂鸣器现场报警。此外,系统利用ESP8266 WiFi模块将监测数据实时上传至华为云物联网平台,结合Qt(C++)开发的Android手机APP,用户可以远程监控设备状态,获取实时环境信息及历史记录,实现电气安全的远程管理与智能预警。本设计通过综合运用现代传感技术、无线通信技术及云计算能力,构建了一个高度集成、反应迅速、操作便捷的电气火灾预防解决方案,有效增强了公共场所及家庭的消防安全保障能力。

1.2 设计思路

【1】需求分析与目标设定

  • 明确需求:首先,明确项目旨在通过实时监测关键环境参数(如温度、湿度、烟雾浓度、火光),预防电气火灾的发生。
  • 目标设定:设计一个既能现场预警又能远程监控的智能系统,确保及时发现火灾隐患,减少损失。

【2】技术选型与模块设计

  • 主控芯片:选择STM32F103RCT6,因其高性能、低功耗、丰富的外设接口,适合物联网应用。
  • 传感器集成:整合SHT30(温湿度)、MQ2(烟雾)、火光传感器,确保全方位环境监测。
  • 无线通信:ESP8266 WiFi模块,因其成本低、集成度高,便于与华为云物联网平台对接。
  • 人机交互:采用OLED显示屏和蜂鸣器,实现本地直观显示和报警提示。
  • 远程监控平台:依托华为云物联网平台,实现数据远程管理与分析,以及通过Qt(C++)开发的Android APP进行移动监控。

1.3 系统功能总结

功能模块 功能描述
数据采集与处理 - 实时采集环境温湿度、烟雾浓度、火光信息
- 数据预处理与阈值判断
显示屏显示 - OLED显示屏实时展示温湿度、烟雾浓度及系统状态
- 友好直观的界面设计
现场报警 - 超过预设阈值时,触发有源蜂鸣器报警
- 高效警示现场人员
无线传输 - ESP8266 WiFi模块实现与华为云物联网平台的数据传输
- 稳定的无线连接
云端数据管理 - 实时上传数据至华为云,实现远程监控与分析
- 数据存储与备份
移动端监控APP - Qt(C++)开发的Android APP远程查看数据
- 实时更新与历史记录查询
自动化控制与报警 - 系统自检功能,监测硬件状态
- 故障自动报警至云端管理平台
安全与加密 - 数据传输全程加密,保障信息安全
- 支持用户权限管理
电源管理 - 外部5V 2A稳定供电,确保系统长期可靠运行

1.4 开发工具的选择

【1】设备端开发

STM32的编程语言选择C语言,C语言执行效率高,大学里主学的C语言,C语言编译出来的可执行文件最接近于机器码,汇编语言执行效率最高,但是汇编的移植性比较差,目前在一些操作系统内核里还有一些低配的单片机使用的较多,平常的单片机编程还是以C语言为主。C语言的执行效率仅次于汇编,语法理解简单、代码通用性强,也支持跨平台,在嵌入式底层、单片机编程里用的非常多,当前的设计就是采用C语言开发。

开发工具选择Keil,keil是一家世界领先的嵌入式微控制器软件开发商,在2015年,keil被ARM公司收购。因为当前芯片选择的是STM32F103系列,STMF103是属于ARM公司的芯片构架、Cortex-M3内核系列的芯片,所以使用Kile来开发STM32是有先天优势的,而keil在各大高校使用的也非常多,很多教科书里都是以keil来教学,开发51单片机、STM32单片机等等。目前作为MCU芯片开发的软件也不只是keil一家独大,IAR在MCU微处理器开发领域里也使用的非常多,IAR扩展性更强,也支持STM32开发,也支持其他芯片,比如:CC2530,51单片机的开发。从软件的使用上来讲,IAR比keil更加简洁,功能相对少一些。如果之前使用过keil,而且使用频率较多,已经习惯再使用IAR是有点不适应界面的。

image-20221210225339928

【2】上位机开发

上位机的开发选择Qt框架,编程语言采用C++;Qt是一个1991年由Qt Company开发的跨平台C++图形用户界面应用程序开发框架。它既可以开发GUI程序,也可用于开发非GUI程序,比如控制台工具和服务器。Qt是面向对象的框架,使用特殊的代码生成扩展(称为元对象编译器(Meta Object Compiler, moc))以及一些宏,Qt很容易扩展,并且允许真正地组件编程。Qt能轻松创建具有原生C++性能的连接设备、用户界面(UI)和应用程序。它功能强大且结构紧凑,拥有直观的工具和库。

image-20230218001243591

image-20230218001219105

二、部署华为云物联网平台

华为云官网: https://www.huaweicloud.com/

打开官网,搜索物联网,就能快速找到 设备接入IoTDA

image-20221204193824815

2.1 物联网平台介绍

华为云物联网平台(IoT 设备接入云服务)提供海量设备的接入和管理能力,将物理设备联接到云,支撑设备数据采集上云和云端下发命令给设备进行远程控制,配合华为云其他产品,帮助我们快速构筑物联网解决方案。

使用物联网平台构建一个完整的物联网解决方案主要包括3部分:物联网平台、业务应用和设备。

物联网平台作为连接业务应用和设备的中间层,屏蔽了各种复杂的设备接口,实现设备的快速接入;同时提供强大的开放能力,支撑行业用户构建各种物联网解决方案。

设备可以通过固网、2G/3G/4G/5G、NB-IoT、Wifi等多种网络接入物联网平台,并使用LWM2M/CoAP、MQTT、HTTPS协议将业务数据上报到平台,平台也可以将控制命令下发给设备。

业务应用通过调用物联网平台提供的API,实现设备数据采集、命令下发、设备管理等业务场景。

img

2.2 开通物联网服务

地址: https://www.huaweicloud.com/product/iothub.html

image-20221204194233414

点击立即创建

image-20240117134653452

正在创建标准版实例,需要等待片刻。

image-20240117134729401

创建完成之后,点击实例名称。 可以看到标准版实例的设备接入端口和地址。

image-20240425180759670

在上面也能看到 免费单元的限制。

image-20240425180817704

开通之后,点击总览,也能查看接入信息。 我们当前设备准备采用MQTT协议接入华为云平台,这里可以看到MQTT协议的地址和端口号等信息。

image-20240425180845461

总结:

端口号:   MQTT (1883)| MQTTS (8883)	
接入地址:ad635970a1.st1.iotda-device.cn-north-4.myhuaweicloud.com

根据域名地址得到IP地址信息: 

打开Windows电脑的命令行控制台终端,使用ping 命令。ping一下即可。

Microsoft Windows [版本 10.0.19045.4170]
(c) Microsoft Corporation。保留所有权利。

C:\Users\11266>ping ad635970a1.st1.iotda-device.cn-north-4.myhuaweicloud.com

正在 Ping ad635970a1.st1.iotda-device.cn-north-4.myhuaweicloud.com [117.78.5.125] 具有 32 字节的数据:
来自 117.78.5.125 的回复: 字节=32 时间=35ms TTL=93
来自 117.78.5.125 的回复: 字节=32 时间=36ms TTL=93
来自 117.78.5.125 的回复: 字节=32 时间=36ms TTL=93
来自 117.78.5.125 的回复: 字节=32 时间=39ms TTL=93

117.78.5.125 的 Ping 统计信息:
    数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
    最短 = 35ms,最长 = 39ms,平均 = 36ms

C:\Users\11266>

MQTT协议接入端口号有两个,1883是非加密端口,8883是证书加密端口,单片机无法加载证书,所以使用1883端口比较合适。 接下来的ESP8266就采用1883端口连接华为云物联网平台。

2.3 创建产品

(1)创建产品

image-20230109164412041

(2)填写产品信息

根据自己产品名字填写,下面的设备类型选择自定义类型。

image-20240612094809689

(3)产品创建成功

image-20240612095148945

创建完成之后点击查看详情。

image-20240612095134263

(4)添加自定义模型

产品创建完成之后,点击进入产品详情页面,翻到最下面可以看到模型定义。

模型简单来说: 就是存放设备上传到云平台的数据。

你可以根据自己的产品进行创建。

比如:

烟雾可以叫  MQ2
温度可以叫  Temperature
湿度可以叫  humidity
火焰可以叫  flame
其他的传感器自己用单词简写命名即可。 这就是你的单片机设备端上传到服务器的数据名字。

先点击自定义模型。

image-20240612095517900

再创建一个服务ID。

image-20240612095542749

接着点击新增属性。

image-20240612095648815

image-20240612095711898

2.4 添加设备

产品是属于上层的抽象模型,接下来在产品模型下添加实际的设备。添加的设备最终需要与真实的设备关联在一起,完成数据交互。

(1)注册设备

image-20240425181935561

(2)根据自己的设备填写

image-20240612100115167

(3)保存设备信息

创建完毕之后,点击保存并关闭,得到创建的设备密匙信息。该信息在后续生成MQTT三元组的时候需要使用。

image-20240612100128061

(4)设备创建完成

image-20240612100147232

(5)设备详情

image-20240612100202960

image-20240612100217236

2.5 MQTT协议主题订阅与发布

(1)MQTT协议介绍

当前的设备是采用MQTT协议与华为云平台进行通信。

MQTT是一个物联网传输协议,它被设计用于轻量级的发布/订阅式消息传输,旨在为低带宽和不稳定的网络环境中的物联网设备提供可靠的网络服务。MQTT是专门针对物联网开发的轻量级传输协议。MQTT协议针对低带宽网络,低计算能力的设备,做了特殊的优化,使得其能适应各种物联网应用场景。目前MQTT拥有各种平台和设备上的客户端,已经形成了初步的生态系统。

MQTT是一种消息队列协议,使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合,相对于其他协议,开发更简单;MQTT协议是工作在TCP/IP协议上;由TCP/IP协议提供稳定的网络连接;所以,只要具备TCP协议栈的网络设备都可以使用MQTT协议。 本次设备采用的ESP8266就具备TCP协议栈,能够建立TCP连接,所以,配合STM32代码里封装的MQTT协议,就可以与华为云平台完成通信。

华为云的MQTT协议接入帮助文档在这里: https://support.huaweicloud.com/devg-iothub/iot_02_2200.html

img

业务流程:

img

(2)华为云平台MQTT协议使用限制

描述 限制
支持的MQTT协议版本 3.1.1
与标准MQTT协议的区别 支持Qos 0和Qos 1支持Topic自定义不支持QoS2不支持will、retain msg
MQTTS支持的安全等级 采用TCP通道基础 + TLS协议(最高TLSv1.3版本)
单帐号每秒最大MQTT连接请求数 无限制
单个设备每分钟支持的最大MQTT连接数 1
单个MQTT连接每秒的吞吐量,即带宽,包含直连设备和网关 3KB/s
MQTT单个发布消息最大长度,超过此大小的发布请求将被直接拒绝 1MB
MQTT连接心跳时间建议值 心跳时间限定为30至1200秒,推荐设置为120秒
产品是否支持自定义Topic 支持
消息发布与订阅 设备只能对自己的Topic进行消息发布与订阅
每个订阅请求的最大订阅数 无限制

(3)主题订阅格式

帮助文档地址:https://support.huaweicloud.com/devg-iothub/iot_02_2200.html

image-20221207153310037

对于设备而言,一般会订阅平台下发消息给设备 这个主题。

设备想接收平台下发的消息,就需要订阅平台下发消息给设备 的主题,订阅后,平台下发消息给设备,设备就会收到消息。

如果设备想要知道平台下发的消息,需要订阅上面图片里标注的主题。

以当前设备为例,最终订阅主题的格式如下:
$oc/devices/{device_id}/sys/messages/down
    
最终的格式:
$oc/devices/663cb18871d845632a0912e7_dev1/sys/messages/down

(4)主题发布格式

对于设备来说,主题发布表示向云平台上传数据,将最新的传感器数据,设备状态上传到云平台。

这个操作称为:属性上报。

帮助文档地址:https://support.huaweicloud.com/usermanual-iothub/iot_06_v5_3010.html

image-20221207153637391

根据帮助文档的介绍, 当前设备发布主题,上报属性的格式总结如下:

发布的主题格式:
$oc/devices/{device_id}/sys/properties/report
 
最终的格式:
$oc/devices/663cb18871d845632a0912e7_dev1/sys/properties/report
发布主题时,需要上传数据,这个数据格式是JSON格式。

上传的JSON数据格式如下:

{
  "services": [
    {
      "service_id": <填服务ID>,
      "properties": {
        "<填属性名称1>": <填属性值>,
        "<填属性名称2>": <填属性值>,
        ..........
      }
    }
  ]
}
根据JSON格式,一次可以上传多个属性字段。 这个JSON格式里的,服务ID,属性字段名称,属性值类型,在前面创建产品的时候就已经介绍了,不记得可以翻到前面去查看。

根据这个格式,组合一次上传的属性数据:
{"services": [{"service_id": "stm32","properties":{"DHT11_T":30,"DHT11_H":10,"BH1750":1,"MQ135":0}}]}

2.6 MQTT三元组

MQTT协议登录需要填用户ID,设备ID,设备密码等信息,就像我们平时登录QQ,微信一样要输入账号密码才能登录。MQTT协议登录的这3个参数,一般称为MQTT三元组。

接下来介绍,华为云平台的MQTT三元组参数如何得到。

(1)MQTT服务器地址

要登录MQTT服务器,首先记得先知道服务器的地址是多少,端口是多少。

帮助文档地址:https://console.huaweicloud.com/iotdm/?region=cn-north-4#/dm-portal/home

image-20240509193207359

MQTT协议的端口支持1883和8883,它们的区别是:8883 是加密端口更加安全。但是单片机上使用比较困难,所以当前的设备是采用1883端口进连接的。

根据上面的域名和端口号,得到下面的IP地址和端口号信息: 如果设备支持填写域名可以直接填域名,不支持就直接填写IP地址。 (IP地址就是域名解析得到的)

华为云的MQTT服务器地址:117.78.5.125
华为云的MQTT端口号:1883

如何得到IP地址?如何域名转IP? 打开Windows的命令行输入以下命令。

ping  ad635970a1.st1.iotda-device.cn-north-4.myhuaweicloud.com

image-20240425182610048

(2)生成MQTT三元组

华为云提供了一个在线工具,用来生成MQTT鉴权三元组: https://iot-tool.obs-website.cn-north-4.myhuaweicloud.com/

打开这个工具,填入设备的信息(也就是刚才创建完设备之后保存的信息),点击生成,就可以得到MQTT的登录信息了。

下面是打开的页面:

image-20240425183025893

填入设备的信息: (上面两行就是设备创建完成之后保存得到的)

直接得到三元组信息。

image-20240509193310020

得到三元组之后,设备端通过MQTT协议登录鉴权的时候,填入参数即可。

ClientId  663cb18871d845632a0912e7_dev1_0_0_2024050911
Username  663cb18871d845632a0912e7_dev1
Password  71b82deae83e80f04c4269b5bbce3b2fc7c13f610948fe210ce18650909ac237

2.7 模拟设备登录测试

经过上面的步骤介绍,已经创建了产品,设备,数据模型,得到MQTT登录信息。 接下来就用MQTT客户端软件模拟真实的设备来登录平台。测试与服务器通信是否正常。

(1)填入登录信息

打开MQTT客户端软件,对号填入相关信息(就是上面的文本介绍)。然后,点击登录,订阅主题,发布主题。

image-20240509193457358

(2)打开网页查看

完成上面的操作之后,打开华为云网页后台,可以看到设备已经在线了。

image-20240612100508790

点击详情页面,可以看到上传的数据:

image-20240612100529581

到此,云平台的部署已经完成,设备已经可以正常上传数据了。

(3)MQTT登录测试参数总结

MQTT服务器:  117.78.5.125
MQTT端口号:  183

//物联网服务器的设备信息
#define MQTT_ClientID "663cb18871d845632a0912e7_dev1_0_0_2024050911"
#define MQTT_UserName "663cb18871d845632a0912e7_dev1"
#define MQTT_PassWord "71b82deae83e80f04c4269b5bbce3b2fc7c13f610948fe210ce18650909ac237"

//订阅与发布的主题
#define SET_TOPIC  "$oc/devices/663cb18871d845632a0912e7_dev1/sys/messages/down"  //订阅
#define POST_TOPIC "$oc/devices/663cb18871d845632a0912e7_dev1/sys/properties/report"  //发布


发布的数据:
{"services": [{"service_id": "stm32","properties":{"DHT11_T":30,"DHT11_H":10,"BH1750":1,"MQ135":0}}]}

2.8 创建IAM账户

创建一个IAM账户,因为接下来开发上位机,需要使用云平台的API接口,这些接口都需要token进行鉴权。简单来说,就是身份的认证。 调用接口获取Token时,就需要填写IAM账号信息。所以,接下来演示一下过程。

地址: https://console.huaweicloud.com/iam/?region=cn-north-4#/iam/users

**【1】获取项目凭证 ** 点击左上角用户名,选择下拉菜单里的我的凭证

image-20240509193646253

image-20240509193701262

项目凭证:

28add376c01e4a61ac8b621c714bf459

【2】创建IAM用户

鼠标放在左上角头像上,在下拉菜单里选择统一身份认证

image-20240509193729078

点击左上角创建用户

image-20240509193744287

image-20240314153208692

image-20240314153228359

image-20240314153258229

创建成功:

image-20240314153315444

【3】创建完成

image-20240509193828289

用户信息如下:

主用户名  l19504562721
IAM用户  ds_abc
密码     DS12345678

2.9 获取影子数据

帮助文档:https://support.huaweicloud.com/api-iothub/iot_06_v5_0079.html

设备影子介绍:

设备影子是一个用于存储和检索设备当前状态信息的JSON文档。
每个设备有且只有一个设备影子,由设备ID唯一标识
设备影子仅保存最近一次设备的上报数据和预期数据
无论该设备是否在线,都可以通过该影子获取和设置设备的属性

简单来说:设备影子就是保存,设备最新上传的一次数据。

我们设计的软件里,如果想要获取设备的最新状态信息,就采用设备影子接口。

如果对接口不熟悉,可以先进行在线调试:https://apiexplorer.developer.huaweicloud.com/apiexplorer/doc?product=IoTDA&api=ShowDeviceShadow

在线调试接口,可以请求影子接口,了解请求,与返回的数据格式。

调试完成看右下角的响应体,就是返回的影子数据。

image-20240509194152229

设备影子接口返回的数据如下:

{
 "device_id": "663cb18871d845632a0912e7_dev1",
 "shadow": [
  {
   "service_id": "stm32",
   "desired": {
    "properties": null,
    "event_time": null
   },
   "reported": {
    "properties": {
     "DHT11_T": 18,
     "DHT11_H": 90,
     "BH1750": 38,
     "MQ135": 70
    },
    "event_time": "20240509T113448Z"
   },
   "version": 3
  }
 ]
}

调试成功之后,可以得到访问影子数据的真实链接,接下来的代码开发中,就采用Qt写代码访问此链接,获取影子数据,完成上位机开发。

image-20240509194214716

链接如下:

https://ad635970a1.st1.iotda-app.cn-north-4.myhuaweicloud.com:443/v5/iot/28add376c01e4a61ac8b621c714bf459/devices/663cb18871d845632a0912e7_dev1/shadow

三、STM32设备端代码设计

3.1 RTC时钟配置代码

#include "delay.h"
#include "usart.h"
#include "rtc.h" 	
#include "calendar.h"	    
   
//实时时钟配置
//初始化RTC时钟,同时检测时钟是否工作正常
//BKP->DR1用于保存是否第一次配置的设置
//返回0:正常
//其他:错误代码
u8 RTC_Init(void)
{
	//检查是不是第一次配置时钟
	u8 temp=0;
	if(BKP->DR1!=0X5050)//第一次配置
	{	 
	  	RCC->APB1ENR|=1<<28;     //使能电源时钟	    
		RCC->APB1ENR|=1<<27;     //使能备份时钟	    
		PWR->CR|=1<<8;           //取消备份区写保护
		RCC->BDCR|=1<<16;        //备份区域软复位	   
		RCC->BDCR&=~(1<<16);     //备份区域软复位结束	  	 
	    RCC->BDCR|=1<<0;         //开启外部低速振荡器 
	    while((!(RCC->BDCR&0X02))&&temp<250)//等待外部时钟就绪	 
		{
			temp++;
			delay_ms(10);
		};
		if(temp>=250)return 1;//初始化时钟失败,晶振有问题	   
		RCC->BDCR|=1<<8; //LSI作为RTC时钟 	    
		RCC->BDCR|=1<<15;//RTC时钟使能	  
	  	while(!(RTC->CRL&(1<<5)));//等待RTC寄存器操作完成	 
    	while(!(RTC->CRL&(1<<3)));//等待RTC寄存器同步  
    	RTC->CRH|=0X01;  		  //允许秒中断
    	RTC->CRH|=0X02;  		  //允许闹钟中断
    	while(!(RTC->CRL&(1<<5)));//等待RTC寄存器操作完成	 
		RTC->CRL|=1<<4;           //允许配置	 
		RTC->PRLH=0X0000;
		RTC->PRLL=32767;          //时钟周期设置(有待观察,看是否跑慢了?)理论值:32767	
		RTC_Set(2015,1,14,17,42,55); //设置时间	  
		RTC->CRL&=~(1<<4);           //配置更新
		while(!(RTC->CRL&(1<<5)));   //等待RTC寄存器操作完成		 									  
		BKP->DR1=0X5050;  
	 	printf("FIRST TIME\n");
	}else//系统继续计时
	{
    	while(!(RTC->CRL&(1<<3)));//等待RTC寄存器同步  
    	RTC->CRH|=0X01;  		  //允许秒中断
    	while(!(RTC->CRL&(1<<5)));//等待RTC寄存器操作完成
		printf("OK\n");
	}		    				  
	MY_NVIC_Init(0,0,RTC_IRQn,2);//优先级设置    
	RTC_Get();//更新时间 
	return 0; //ok
}		 				    
//RTC时钟中断
//每秒触发一次  	 
void RTC_IRQHandler(void)
{	
	OSIntEnter();    			 
	if(RTC->CRL&0x0001)			//秒钟中断
	{							
		RTC_Get();				//更新时间   
		//printf("sec:%d\r\n",calendar.sec);
 	}
	if(RTC->CRL&0x0002)			//闹钟中断
	{
		RTC->CRL&=~(0x0002);	//清闹钟中断	  
		RTC_Get();				//更新时间   
  		printf("Alarm Time:%d-%d-%d %d:%d:%d\n",calendar.w_year,calendar.w_month,calendar.w_date,calendar.hour,calendar.min,calendar.sec);//输出闹铃时间	   
		alarm.ringsta|=1<<7;	//开启闹铃
	} 				  								 
    RTC->CRL&=0X0FFA;         	//清除溢出,秒钟中断标志
	while(!(RTC->CRL&(1<<5)));	//等待RTC寄存器操作完成	  							 
	OSIntExit();  	    						 	   	 
}
//判断是否是闰年函数
//月份   1  2  3  4  5  6  7  8  9  10 11 12
//闰年   31 29 31 30 31 30 31 31 30 31 30 31
//非闰年 31 28 31 30 31 30 31 31 30 31 30 31
//year:年份
//返回值:该年份是不是闰年.1,是.0,不是
u8 Is_Leap_Year(u16 year)
{			  
	if(year%4==0) //必须能被4整除
	{ 
		if(year%100==0) 
		{ 
			if(year%400==0)return 1;//如果以00结尾,还要能被400整除 	   
			else return 0;   
		}else return 1;   
	}else return 0;	
}	 			   
//设置时钟
//把输入的时钟转换为秒钟
//以1970年1月1日为基准
//1970~2099年为合法年份
//返回值:0,成功;其他:错误代码.
//月份数据表											 
u8 const table_week[12]={0,3,3,6,1,4,6,2,5,0,3,5}; //月修正数据表	  
//平年的月份日期表
const u8 mon_table[12]={31,28,31,30,31,30,31,31,30,31,30,31};
//syear,smon,sday,hour,min,sec:年月日时分秒
//返回值:设置结果。0,成功;1,失败。
u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec)
{
	u16 t;
	u32 seccount=0;
	if(syear<1970||syear>2099)return 1;	   
	for(t=1970;t<syear;t++)	//把所有年份的秒钟相加
	{
		if(Is_Leap_Year(t))seccount+=31622400;//闰年的秒钟数
		else seccount+=31536000;			  //平年的秒钟数
	}
	smon-=1;
	for(t=0;t<smon;t++)	   //把前面月份的秒钟数相加
	{
		seccount+=(u32)mon_table[t]*86400;//月份秒钟数相加
		if(Is_Leap_Year(syear)&&t==1)seccount+=86400;//闰年2月份增加一天的秒钟数	   
	}
	seccount+=(u32)(sday-1)*86400;//把前面日期的秒钟数相加 
	seccount+=(u32)hour*3600;//小时秒钟数
    seccount+=(u32)min*60;	 //分钟秒钟数
	seccount+=sec;//最后的秒钟加上去
													    
	//设置时钟
    RCC->APB1ENR|=1<<28;//使能电源时钟
    RCC->APB1ENR|=1<<27;//使能备份时钟
	PWR->CR|=1<<8;    //取消备份区写保护
	//上面三步是必须的!
	RTC->CRL|=1<<4;   //允许配置 
	RTC->CNTL=seccount&0xffff;
	RTC->CNTH=seccount>>16;
	RTC->CRL&=~(1<<4);//配置更新
	while(!(RTC->CRL&(1<<5)));//等待RTC寄存器操作完成 
	RTC_Get();//设置完之后更新一下数据 	
	return 0;	    
}
//初始化闹钟		  
//以1970年1月1日为基准
//1970~2099年为合法年份
//syear,smon,sday,hour,min,sec:闹钟的年月日时分秒   
//返回值:0,成功;其他:错误代码.
u8 RTC_Alarm_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec)
{
	u16 t;
	u32 seccount=0;
	if(syear<1970||syear>2099)return 1;	   
	for(t=1970;t<syear;t++)	//把所有年份的秒钟相加
	{
		if(Is_Leap_Year(t))seccount+=31622400;//闰年的秒钟数
		else seccount+=31536000;			  //平年的秒钟数
	}
	smon-=1;
	for(t=0;t<smon;t++)	   //把前面月份的秒钟数相加
	{
		seccount+=(u32)mon_table[t]*86400;//月份秒钟数相加
		if(Is_Leap_Year(syear)&&t==1)seccount+=86400;//闰年2月份增加一天的秒钟数	   
	}
	seccount+=(u32)(sday-1)*86400;//把前面日期的秒钟数相加 
	seccount+=(u32)hour*3600;//小时秒钟数
    seccount+=(u32)min*60;	 //分钟秒钟数
	seccount+=sec;//最后的秒钟加上去 			    
	//设置时钟
    RCC->APB1ENR|=1<<28;//使能电源时钟
    RCC->APB1ENR|=1<<27;//使能备份时钟
	PWR->CR|=1<<8;    //取消备份区写保护
	//上面三步是必须的!
	RTC->CRL|=1<<4;   //允许配置 
	RTC->ALRL=seccount&0xffff;
	RTC->ALRH=seccount>>16;
	RTC->CRL&=~(1<<4);//配置更新
	while(!(RTC->CRL&(1<<5)));//等待RTC寄存器操作完成  
	return 0;	    
}
//得到当前的时间,结果保存在calendar结构体里面
//返回值:0,成功;其他:错误代码.
u8 RTC_Get(void)
{
	static u16 daycnt=0;
	u32 timecount=0; 
	u32 temp=0;
	u16 temp1=0;	  
 	timecount=RTC->CNTH;//得到计数器中的值(秒钟数)
	timecount<<=16;
	timecount+=RTC->CNTL;			 

 	temp=timecount/86400;   //得到天数(秒钟数对应的)
	if(daycnt!=temp)//超过一天了
	{	  
		daycnt=temp;
		temp1=1970;	//从1970年开始
		while(temp>=365)
		{				 
			if(Is_Leap_Year(temp1))//是闰年
			{
				if(temp>=366)temp-=366;//闰年的秒钟数
				else break;  
			}
			else temp-=365;	  //平年 
			temp1++;  
		}   
		calendar.w_year=temp1;//得到年份
		temp1=0;
		while(temp>=28)//超过了一个月
		{
			if(Is_Leap_Year(calendar.w_year)&&temp1==1)//当年是不是闰年/2月份
			{
				if(temp>=29)temp-=29;//闰年的秒钟数
				else break; 
			}
			else 
			{
				if(temp>=mon_table[temp1])temp-=mon_table[temp1];//平年
				else break;
			}
			temp1++;  
		}
		calendar.w_month=temp1+1;	//得到月份
		calendar.w_date=temp+1;  	//得到日期 
	}
	temp=timecount%86400;     		//得到秒钟数   	   
	calendar.hour=temp/3600;     	//小时
	calendar.min=(temp%3600)/60; 	//分钟	
	calendar.sec=(temp%3600)%60; 	//秒钟
	calendar.week=RTC_Get_Week(calendar.w_year,calendar.w_month,calendar.w_date);//获取星期   
	return 0;
}	 
//获得现在是星期几
//功能描述:输入公历日期得到星期(只允许1901-2099年)
//year,month,day:公历年月日 
//返回值:星期号																						 
u8 RTC_Get_Week(u16 year,u8 month,u8 day)
{	
	u16 temp2;
	u8 yearH,yearL;
	
	yearH=year/100;	yearL=year%100; 
	// 如果为21世纪,年份数加100  
	if (yearH>19)yearL+=100;
	// 所过闰年数只算1900年之后的  
	temp2=yearL+yearL/4;
	temp2=temp2%7; 
	temp2=temp2+day+table_week[month-1];
	if (yearL%4==0&&month<3)temp2--;
	return(temp2%7);
}			  

3.2 SPI协议封装

#include "spi.h"

//以下是SPI模块的初始化代码,配置成主机模式,访问W25Q128/NRF24L01
//SPI口初始化
//这里针是对SPI2的初始化
void SPI2_Init(void)
{	 
	RCC->APB2ENR|=1<<3;  	//PORTB时钟使能 	 
	RCC->APB1ENR|=1<<14;   	//SPI2时钟使能 
	//这里只针对SPI口初始化
	GPIOB->CRH&=0X000FFFFF; 
	GPIOB->CRH|=0XBBB00000;	//PB13/14/15复用 	    
	GPIOB->ODR|=0X7<<13;   	//PB13/14/15上拉
	SPI2->CR1|=0<<10;		//全双工模式	
	SPI2->CR1|=1<<9; 		//软件nss管理
	SPI2->CR1|=1<<8;  

	SPI2->CR1|=1<<2; 		//SPI主机
	SPI2->CR1|=0<<11;		//8bit数据格式	
	SPI2->CR1|=1<<1; 		//空闲模式下SCK为1 CPOL=1
	SPI2->CR1|=1<<0; 		//数据采样从第二个时间边沿开始,CPHA=1  
	//对SPI2属于APB1的外设.时钟频率最大为36M.
	SPI2->CR1|=3<<3; 		//Fsck=Fpclk1/256
	SPI2->CR1|=0<<7; 		//MSBfirst   
	SPI2->CR1|=1<<6; 		//SPI设备使能
	SPI2_ReadWriteByte(0xff);//启动传输		 
}  
//SPI2速度设置函数
//SpeedSet:0~7
//SPI速度=fAPB1/2^(SpeedSet+1)
//APB1时钟一般为36Mhz
void SPI2_SetSpeed(u8 SpeedSet)
{
	SpeedSet&=0X07;			//限制范围
	SPI2->CR1&=0XFFC7; 
	SPI2->CR1|=SpeedSet<<3;	//设置SPI2速度  
	SPI2->CR1|=1<<6; 		//SPI设备使能	  
} 
//SPI2 读写一个字节
//TxData:要写入的字节
//返回值:读取到的字节
u8 SPI2_ReadWriteByte(u8 TxData)
{		
	u16 retry=0;				 
	while((SPI2->SR&1<<1)==0)		//等待发送区空	
	{
		retry++;
		if(retry>=0XFFFE)return 0; 	//超时退出
	}			  
	SPI2->DR=TxData;	 	  		//发送一个byte 
	retry=0;
	while((SPI2->SR&1<<0)==0) 		//等待接收完一个byte  
	{
		retry++;
		if(retry>=0XFFFE)return 0;	//超时退出
	}	  						    
	return SPI2->DR;          		//返回收到的数据				    
}
//SPI1初始化
void SPI1_Init(void)
{	    
	RCC->APB2ENR|=1<<2;     //PORTA时钟使能 	 
	RCC->APB2ENR|=1<<12;    //SPI1时钟使能 
		   
	//这里只针对SPI口初始化
	GPIOA->CRL&=0X000FFFFF; 
	GPIOA->CRL|=0XBBB00000;	//PA5.6.7复用 	    
	GPIOA->ODR|=0X7<<5;    	//PA5.6.7上拉
		
	SPI1->CR1|=0<<10;//全双工模式	
	SPI1->CR1|=1<<9; //软件nss管理
	SPI1->CR1|=1<<8;  

	SPI1->CR1|=1<<2; //SPI主机
	SPI1->CR1|=0<<11;//8bit数据格式	
	SPI1->CR1|=1<<1; //CPOL=0时空闲模式下SCK为1  CPOL=1
	SPI1->CR1|=1<<0; //数据采样从第二个时间边沿开始,CPHA=1  
	  
	SPI1->CR1|=7<<3; //Fsck=Fcpu/256
	SPI1->CR1|=0<<7; //MSBfirst   
	SPI1->CR1|=1<<6; //SPI设备使能
 	SPI1_ReadWriteByte(0xff);//启动传输	 
} 
//SPI1 速度设置函数
//SpeedSet:0~7
//SPI速度=fAPB2/2^(SpeedSet+1)
//APB2时钟一般为72Mhz
void SPI1_SetSpeed(u8 SpeedSet)
{
	SpeedSet&=0X07;			//限制范围
	SPI1->CR1&=0XFFC7; 
	SPI1->CR1|=SpeedSet<<3;	//设置SPI1速度  
	SPI1->CR1|=1<<6; 		//SPI设备使能	   
} 
//SPIx 读写一个字节
//TxData:要写入的字节
//返回值:读取到的字节
u8 SPI1_ReadWriteByte(u8 TxData)
{				   			 
	while((SPI1->SR&1<<1)==0);//等待发送区空  
	SPI1->DR=TxData;	 	  //发送一个byte 	   
	while((SPI1->SR&1<<0)==0); //等待接收完一个byte   			    
	return SPI1->DR;          //返回收到的数据				    
}

3.3 IIC协议封装

#include "myiic.h"
#include "delay.h"

//初始化IIC
void IIC_Init(void)
{					     
 	RCC->APB2ENR|=1<<3;		//先使能外设IO PORTB时钟 							 
	GPIOB->CRL&=0X00FFFFFF;	//PB6/7 推挽输出
	GPIOB->CRL|=0X33000000;	   
	GPIOB->ODR|=3<<6;     	//PB6,7 输出高
}
//产生IIC起始信号
void IIC_Start(void)
{
	SDA_OUT();     //sda线输出
	IIC_SDA=1;	  	  
	IIC_SCL=1;
	delay_us(4);
 	IIC_SDA=0;//START:when CLK is high,DATA change form high to low 
	delay_us(4);
	IIC_SCL=0;//钳住I2C总线,准备发送或接收数据 
}	  
//产生IIC停止信号
void IIC_Stop(void)
{
	SDA_OUT();//sda线输出
	IIC_SCL=0;
	IIC_SDA=0;//STOP:when CLK is high DATA change form low to high
 	delay_us(4);
	IIC_SCL=1; 
	IIC_SDA=1;//发送I2C总线结束信号
	delay_us(4);							   	
}
//等待应答信号到来
//返回值:1,接收应答失败
//        0,接收应答成功
u8 IIC_Wait_Ack(void)
{
	u8 ucErrTime=0;
	SDA_IN();      //SDA设置为输入  
	IIC_SDA=1;delay_us(1);	   
	IIC_SCL=1;delay_us(1);	 
	while(READ_SDA)
	{
		ucErrTime++;
		if(ucErrTime>250)
		{
			IIC_Stop();
			return 1;
		}
	}
	IIC_SCL=0;//时钟输出0 	   
	return 0;  
} 
//产生ACK应答
void IIC_Ack(void)
{
	IIC_SCL=0;
	SDA_OUT();
	IIC_SDA=0;
	delay_us(2);
	IIC_SCL=1;
	delay_us(2);
	IIC_SCL=0;
}
//不产生ACK应答		    
void IIC_NAck(void)
{
	IIC_SCL=0;
	SDA_OUT();
	IIC_SDA=1;
	delay_us(2);
	IIC_SCL=1;
	delay_us(2);
	IIC_SCL=0;
}					 				     
//IIC发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答			  
void IIC_Send_Byte(u8 txd)
{                        
    u8 t;   
	SDA_OUT(); 	    
    IIC_SCL=0;//拉低时钟开始数据传输
    for(t=0;t<8;t++)
    {              
        IIC_SDA=(txd&0x80)>>7;
        txd<<=1; 	  
		delay_us(2);   //对TEA5767这三个延时都是必须的
		IIC_SCL=1;
		delay_us(2); 
		IIC_SCL=0;	
		delay_us(2);
    }	 
} 	    
//读1个字节,ack=1时,发送ACK,ack=0,发送nACK   
u8 IIC_Read_Byte(unsigned char ack)
{
	unsigned char i,receive=0;
	SDA_IN();//SDA设置为输入
    for(i=0;i<8;i++ )
	{
        IIC_SCL=0; 
        delay_us(2);
		IIC_SCL=1;
        receive<<=1;
        if(READ_SDA)receive++;   
		delay_us(1); 
    }					 
    if (!ack)
        IIC_NAck();//发送nACK
    else
        IIC_Ack(); //发送ACK   
    return receive;
}

3.4 ADC代码封装

#include "adc.h"
#include "delay.h"					   
	
//初始化ADC1
//这里我们仅以规则通道为例
//我们默认仅开启通道1																	   
void  Adc_Init(void)
{    
	//先初始化IO口
 	RCC->APB2ENR|=1<<2;    //使能PORTA口时钟 
	GPIOA->CRL&=0XFFFFFF0F;//PA1 anolog输入 
	RCC->APB2ENR|=1<<9;    //ADC1时钟使能	  
	RCC->APB2RSTR|=1<<9;   //ADC1复位
	RCC->APB2RSTR&=~(1<<9);//复位结束	    
	RCC->CFGR&=~(3<<14);   //分频因子清零	
	//SYSCLK/DIV2=12M ADC时钟设置为12M,ADC最大时钟不能超过14M!
	//否则将导致ADC准确度下降! 
	RCC->CFGR|=2<<14;      	 
	ADC1->CR1&=0XF0FFFF;   //工作模式清零
	ADC1->CR1|=0<<16;      //独立工作模式  
	ADC1->CR1&=~(1<<8);    //非扫描模式	  
	ADC1->CR2&=~(1<<1);    //单次转换模式
	ADC1->CR2&=~(7<<17);	   
	ADC1->CR2|=7<<17;	   //软件控制转换  
	ADC1->CR2|=1<<20;      //使用用外部触发(SWSTART)!!!	必须使用一个事件来触发
	ADC1->CR2&=~(1<<11);   //右对齐	 
	
	ADC1->CR2|=1<<23;      //使能温度传感器

	ADC1->SQR1&=~(0XF<<20);
	ADC1->SQR1|=0<<20;     //1个转换在规则序列中 也就是只转换规则序列1 			   
	//设置通道1的采样时间
	ADC1->SMPR2&=~(3*1);   //通道1采样时间清空	  
 	ADC1->SMPR2|=7<<(3*1); //通道1  239.5周期,提高采样时间可以提高精确度	 

	ADC1->SMPR1&=~(7<<3*6);//清除通道16原来的设置	 
	ADC1->SMPR1|=7<<(3*6); //通道16  239.5周期,提高采样时间可以提高精确度	 

	ADC1->CR2|=1<<0;	   //开启AD转换器	 
	ADC1->CR2|=1<<3;       //使能复位校准  
	while(ADC1->CR2&1<<3); //等待校准结束 			 
    //该位由软件设置并由硬件清除。在校准寄存器被初始化后该位将被清除。 		 
	ADC1->CR2|=1<<2;        //开启AD校准	   
	while(ADC1->CR2&1<<2);  //等待校准结束
	//该位由软件设置以开始校准,并在校准结束时由硬件清除  
}				  
//获得ADC1某个通道的值
//ch:通道值 0~16
//返回值:转换结果
u16 Get_Adc(u8 ch)   
{
	//设置转换序列	  		 
	ADC1->SQR3&=0XFFFFFFE0;//规则序列1 通道ch
	ADC1->SQR3|=ch;		  			    
	ADC1->CR2|=1<<22;       //启动规则转换通道 
	while(!(ADC1->SR&1<<1));//等待转换结束	 	   
	return ADC1->DR;		//返回adc值	
}
//获取通道ch的转换值,取times次,然后平均 
//ch:通道编号
//times:获取次数
//返回值:通道ch的times次转换结果平均值
u16 Get_Adc_Average(u8 ch,u8 times)
{
	u32 temp_val=0;
	u8 t;
	for(t=0;t<times;t++)
	{
		temp_val+=Get_Adc(ch);
		delay_ms(5);
	}
	return temp_val/times;
} 
//得到温度值
//返回值:温度值(扩大了100倍,单位:℃.)
short Get_Temprate(void)
{
	u32 adcx;
	short result;
 	double temperate;
	adcx=Get_Adc_Average(ADC_CH_TEMP,20);	//读取通道16,20次取平均
	temperate=(float)adcx*(3.3/4096);		//电压值 
	temperate=(1.43-temperate)/0.0043+25;	//转换为温度值 	 
	result=temperate*=100;					//扩大100倍.
	return result;
} 
//初始化ADC3
//这里我们仅以规则通道为例
//我们默认仅开启通道6																	   
void  Adc3_Init(void)
{      
	RCC->APB2ENR|=1<<15;   //ADC3时钟使能	  
	RCC->APB2RSTR|=1<<15;   //ADC复位
	RCC->APB2RSTR&=~(1<<15);//复位结束	    
	RCC->CFGR&=~(3<<14);   //分频因子清零	 
	//SYSCLK/DIV2=12M ADC时钟设置为12M,ADC最大时钟不能超过14M!
	//否则将导致ADC准确度下降! 
	RCC->CFGR|=2<<14;      	 
	ADC3->CR1&=0XF0FFFF;   //工作模式清零
	ADC3->CR1|=0<<16;      //独立工作模式  
	ADC3->CR1&=~(1<<8);    //非扫描模式	  
	ADC3->CR2&=~(1<<1);    //单次转换模式
	ADC3->CR2&=~(7<<17);	   
	ADC3->CR2|=7<<17;	   //软件控制转换  
	ADC3->CR2|=1<<20;      //使用用外部触发(SWSTART)!!!	必须使用一个事件来触发
	ADC3->CR2&=~(1<<11);   //右对齐	   
	ADC3->SQR1&=~(0XF<<20);
	ADC3->SQR1|=0<<20;     //1个转换在规则序列中 也就是只转换规则序列1 			   
	//设置通道1的采样时间
	ADC3->SMPR2&=~(7<<(3*6));//通道6采样时间清空	  
 	ADC3->SMPR2|=7<<(3*6); //通道6  239.5个周期,提高采样时间可以提高精确度	

	ADC3->CR2|=1<<0;	   //开启AD转换器	 
	ADC3->CR2|=1<<3;       //使能复位校准  
	while(ADC1->CR2&1<<3); //等待校准结束 			 
    //该位由软件设置并由硬件清除。在校准寄存器被初始化后该位将被清除。 		 
	ADC3->CR2|=1<<2;        //开启AD校准	   
	while(ADC3->CR2&1<<2);  //等待校准结束
	//该位由软件设置以开始校准,并在校准结束时由硬件清除  
}		 
//获得ADC3某个通道的值
//ch:通道值 0~16
//返回值:转换结果
u16 Get_Adc3(u8 ch)   
{
	//设置转换序列	  		 
	ADC3->SQR3&=0XFFFFFFE0;//规则序列1 通道ch
	ADC3->SQR3|=ch;		  			    
	ADC3->CR2|=1<<22;       //启动规则转换通道 
	while(!(ADC3->SR&1<<1));//等待转换结束	 	   
	return ADC3->DR;		//返回adc值	
} 

3.5 串口配置代码

#include "sys.h"
#include "usart3.h"	  
#include "stdarg.h"	 	 
#include "stdio.h"	 	 
#include "string.h"
#include "timer.h"
#include "ucos_ii.h"
#include "malloc.h"


//串口接收缓存区 	
u8 USART3_RX_BUF[USART3_MAX_RECV_LEN]; 				//接收缓冲,最大USART3_MAX_RECV_LEN个字节.


//通过判断接收连续2个字符之间的时间差不大于10ms来决定是不是一次连续的数据.
//如果2个字符接收间隔超过10ms,则认为不是1次连续数据.也就是超过10ms没有接收到
//任何数据,则表示此次接收完毕.
//接收到的数据状态
//[15]:0,没有接收到数据;1,接收到了一批数据.
//[14:0]:接收到的数据长度
vu16 USART3_RX_STA=0;   	 
void USART3_IRQHandler(void)
{
	u8 res;	    
	OSIntEnter();    
	if(USART3->SR&(1<<5))//接收到数据
	{	 
		res=USART3->DR; 			 
		if((USART3_RX_STA&(1<<15))==0)//接收完的一批数据,还没有被处理,则不再接收其他数据
		{ 
			if(USART3_RX_STA<USART3_MAX_RECV_LEN)	//还可以接收数据
			{
				TIM7->CNT=0;         				//计数器清空
				if(USART3_RX_STA==0) 				//使能定时器7的中断 
				{
					TIM7->CR1|=1<<0;     			//使能定时器7
				}
				USART3_RX_BUF[USART3_RX_STA++]=res;	//记录接收到的值	 
			}else 
			{
				USART3_RX_STA|=1<<15;				//强制标记接收完成
			} 
		}
	}  											 
	OSIntExit();  											 
}   
//初始化IO 串口3
//pclk1:PCLK1时钟频率(Mhz)
//bound:波特率 
void usart3_init(u32 pclk1,u32 bound)
{  	 
	RCC->APB2ENR|=1<<3;   	//使能PORTB口时钟  
 	GPIOB->CRH&=0XFFFF00FF;	//IO状态设置
	GPIOB->CRH|=0X00008B00;	//IO状态设置 
	
	RCC->APB1ENR|=1<<18;  	//使能串口时钟 	 
	RCC->APB1RSTR|=1<<18;   //复位串口3
	RCC->APB1RSTR&=~(1<<18);//停止复位	
	//波特率设置
 	USART3->BRR=(pclk1*1000000)/(bound);// 波特率设置	 
	USART3->CR1|=0X200C;  	//1位停止,无校验位.
	//使能接收中断 
	USART3->CR1|=1<<5;    	//接收缓冲区非空中断使能	    	
	MY_NVIC_Init(0,1,USART3_IRQn,2);//组2
	TIM7_Int_Init(99,7199);	//10ms中断
	TIM7->CR1&=~(1<<0);		//关闭定时器7
	USART3_RX_STA=0;		//清零
}

//串口3,printf 函数
//确保一次发送数据不超过USART3_MAX_SEND_LEN字节
void u3_printf(char* fmt,...)  
{  
	u16 i,j;
	u8 *pbuf;
	va_list ap;
	pbuf=mymalloc(SRAMIN,USART3_MAX_SEND_LEN);	//申请内存
	if(!pbuf)									//内存申请失败
	{
		printf("u3 malloc error\r\n");
		return ;
	}
	va_start(ap,fmt);
	vsprintf((char*)pbuf,fmt,ap);
	va_end(ap);
	i=strlen((const char*)pbuf);				//此次发送数据的长度
	for(j=0;j<i;j++)							//循环发送数据
	{
		while((USART3->SR&0X40)==0);			//循环发送,直到发送完毕   
		USART3->DR=pbuf[j];  
	}
	myfree(SRAMIN,pbuf);						//释放内存
}

四、上位机开发

为了方便查看设备上传的数据,接下来利用Qt开发一款Android手机APP 和 Windows上位机。

使用华为云平台提供的API接口获取设备上传的数据,进行可视化显示,以及远程控制设备。

4.1 Qt开发环境安装

Qt的中文官网: https://www.qt.io/zh-cn/![image-20221207160550486](https://led-obs.obs.cn-north-1.myhuaweicloud.com/Blog/img/image-20221207160550486.png)

image-20221207160606892

QT5.12.6的下载地址:https://download.qt.io/archive/qt/5.12/5.12.6

打开下载链接后选择下面的版本进行下载:

qt-opensource-windows-x86-5.12.6.exe 13-Nov-2019 07:28 3.7G Details

软件安装时断网安装,否则会提示输入账户。

安装的时候,第一个复选框里勾选一个mingw 32编译器即可,其他的不管默认就行,直接点击下一步继续安装。

image-20221203151742653

选择MinGW 32-bit 编译器: (一定要看清楚了)

image-20221203151750344

说明: 我这里只是介绍PC端,也就是Windows系统下的Qt环境搭建。 Android的开发环境比较麻烦,如果想学习Android开发,想编译Android程序的APP,需要自己去搭建Android环境。

也可以看下面这篇文章,不过这个文章是在Qt开发专栏里付费的,需要订阅专栏才可以看。 如果不想付费看,也可以自行找其他教程,自己搭建好必须的环境就行了

Android环境搭建的博客链接: https://blog.csdn.net/xiaolong1126626497/article/details/117254453

4.2 新建上位机工程

前面2讲解了需要用的API接口,接下来就使用Qt设计上位机,设计界面,完成整体上位机的逻辑设计。

【1】新建工程

image-20240117144052547

【2】设置项目的名称。

image-20240509195711965

【3】选择编译系统

image-20240117144239681

【4】选择默认继承的类

image-20240117144302275

【5】选择编译器

image-20240314162137170

【6】点击完成

image-20240117144354252

【7】工程创建完成

image-20230421094133333

4.3 设计UI界面与工程配置

【1】打开UI文件

image-20230421094815236

打开默认的界面如下:

image-20240425194845233

【2】开始设计界面

根据自己需求设计界面。

image-20240612101120537

4.5 编译Windows上位机

点击软件左下角的绿色三角形按钮进行编译运行。

image-20240509202031739

编译之后的效果:

image-20240612101227544

image-20240612101246804

4.6 配置Android环境

如果想编译Android手机APP,必须要先自己配置好自己的Android环境。(搭建环境的过程可以自行百度搜索学习)

然后才可以进行下面的步骤。

【1】选择Android编译器

image-20240425232651515

image-20240509202408776

【2】创建Android配置文件

image-20240117144604025

image-20240117144635052

image-20240117144652014

创建完成。

【3】配置Android图标与名称

image-20240612100947190

【3】编译Android上位机

Qt本身是跨平台的,直接选择Android的编译器,就可以将程序编译到Android平台。

然后点击构建。

image-20240509202534407

成功之后,在目录下可以看到生成的apk文件,也就是Android手机的安装包,电脑端使用QQ发送给手机QQ,手机登录QQ接收,就能直接安装。

生成的apk的目录在哪里呢? 编译完成之后,在控制台会输出APK文件的路径。

知道目录在哪里之后,在Windows的文件资源管理器里,找到路径,具体看下图,找到生成的apk文件。

image-20240509202712295


D:/linux-share-dir/QT/build-app_Huawei_Eco_tracking-Android_for_arm64_v8a_Clang_Qt_5_12_6_for_Android_ARM64
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。