一、项目开发背景
随着城市交通的不断发展和环保意识的提升,电动车作为一种便捷、经济的出行工具,在全球范围内迅速普及,尤其在中国等发展中国家,已成为日常通勤的重要选择。然而,电动车的广泛使用也带来了日益严重的盗窃问题,传统机械锁或简单电子防盗装置往往难以有效防范,车主时常面临财产损失和安全风险,这促使了对更智能、更可靠的防盗系统的需求。
现有的电动车防盗系统大多局限于本地报警功能,例如声音或灯光提示,但无法实现远程监控和及时通知,导致车主在车辆被盗时难以第一时间响应。此外,这些系统缺乏集成化的远程管理能力,无法提供实时位置追踪或远程控制功能,限制了其在实际应用中的效果。随着物联网技术的快速发展,基于云端的远程管理系统成为可能,但传统方案依赖WiFi或蓝牙连接,在户外环境中覆盖有限,难以满足电动车移动性强、无固定网络接入点的特点。
近年来,4G通信技术的成熟和物联网平台的普及,为智能电动车管理提供了新的解决方案。4G网络覆盖广泛、连接稳定,使得设备可以随时随地与云端服务器通信,实现数据上传和指令下发。结合加速度传感器、GPS模块和微控制器如STM32,可以构建一个低功耗、高可靠性的系统,不仅能够检测异常移动并触发本地报警,还能通过短信和APP通知车主,增强防盗能力。同时,远程开锁、状态监控和寻车功能进一步提升了用户体验,符合现代智能出行的发展趋势。
本项目旨在设计一个基于STM32与4G通信的智能电动车防盗与远程管理系统,通过集成防盗检测、数据上云、OLED显示、声光寻车和密码开锁等功能,解决传统系统的不足。利用华为云IoT服务器和MQTT协议,确保数据安全传输,并支持Android APP和Windows上位机进行远程管理,为车主提供全面的防护和便利。最终,该系统有望降低电动车盗窃率,提升用户满意度,并推动智能交通设备的创新应用。
下面是系统框架图:
云端服务电动车端
USART
USART
GPIO
GPIO
GPIO
MQTT协议
SMS
STM32F103RCT6主控0.96寸OLED显示屏Air780e 4G模块ATGM336H GPS模块ADXL345加速度计继电器锁控制有源蜂鸣器白色LED指示灯4x机械按键华为云IoT平台Android APPWindows上位机用户手机电源管理14500锂电池太阳能扩展接口数据存储报警推送
图例说明:
1. 硬件层(紫色节点)
-
-
- • STM32F103RCT6作为核心控制器• 通过多种接口协议连接外设(SPI/USART/I2C/GPIO)
-
2. 通信层
-
-
- • 4G模块双向通信(MQTT上行+短信下行)• 华为云IoT平台作为数据中转枢纽
-
3. 应用层(绿色节点)
-
-
- • 双平台客户端(Android+Windows)• 支持远程控制与状态监控
-
4. 供电系统
-
- • 主电源+扩展电源设计• 独立电源管理模块
系统总体设计框图,包含硬件拓扑、数据流向、控制逻辑和云平台交互:
用户终端电动车终端
SPI
USART1
USART2
I2C
GPIO
GPIO
GPIO
GPIO
MQTT上行
MQTT下行
SMS
HTTP/WebSocket
MQTT
1.采集传感器数据
2.姿态异常检测
3.触发本地报警
4.加密传输
5.云平台存储
6.实时推送
7.远程控制指令
8.下发执行
9.密码验证
10.锁状态切换
a.防盗触发条件
b.通信失败处理
c.低功耗策略
STM32F103RCT6OLED显示屏Air780e 4G模块GPS模块ADXL345加速度计继电器锁蜂鸣器LED指示灯机械按键华为云IoT平台车主手机Android APPWindows上位机控制逻辑
二、设计实现的功能
(1)支持防盗功能。当电动车在锁车状态下被移动时,本地蜂鸣器会发出声音进行报警,同时向指定的联系人发送短信提醒,告知电动车被移动,同时APP上也会弹窗提醒,电动车被移动。
(2)支持数据上云:整个设备会通过4G模块连接华为云IOT物联网服务器,电动车主要是在户外环境,没有WIFI可以连接,选择4G联网更加方便;4G模块通过MQTT协议上传数据到华为云IOT物联网服务器,再分别设计Android手机APP和Windows上位机实现远程显示设备上传的数据,包括:锁的开关状态,GPS定位地图等等。还可以远程开关锁。
(3)本地有一个OLED显示屏,可以显示GPS定位状态、锁的开关状态信息。
(4)支持声光寻车,在手机APP上按下按键,本地的设备收到指令后会利用蜂鸣器滴滴响一声,LED灯闪烁一下。
(5)支持输入密码开锁。有4个独立的机械按键,按下输入密码可以开锁,显示屏会显示输入的密码状态。
三、项目硬件模块组成
(1)STM32F103RCT6 主控芯片
(2)0.96寸SPI协议OLED显示屏
(3)Air780e 4G通信模块
(4)14500锂电池供电模块(支持太阳能扩展接口)
(5)ATGM336H-5N-GPS模块
(6)高电平触发的有源蜂鸣器
(7)ADXL345加速度传感器
(8)继电器模块(用于模拟电动车锁控制)
(9)白色LED灯模块(用于寻车提示)
(10)4个独立机械按键(用于密码输入)
模块的例子代码可以去网盘下载:https://ccnr8sukk85n.feishu.cn/wiki/QjY8weDYHibqRYkFP2qcA9aGnvb?from=from_copylink
四、设计意义
本设计基于STM32与4G通信的智能电动车防盗与远程管理系统,旨在提升电动车的安全性和管理便捷性。通过集成防盗报警、远程监控和数据传输功能,该系统有效降低了电动车被盗的风险,并为用户提供了实时的车辆状态信息。防盗功能在检测到异常移动时触发本地报警和远程通知,增强了车辆的物理安全防护,同时通过短信和APP提醒确保用户及时响应潜在威胁。
该系统支持数据上云功能,利用4G模块和MQTT协议将车辆数据实时上传至华为云IOT物联网服务器,实现了远程监控和管理。用户可以通过Android APP或Windows上位机查看锁状态、GPS定位等信息,并远程控制开关锁,这大大提升了使用的便利性和灵活性,特别适用于户外环境 where WiFi不可用,4G联网提供了稳定可靠的连接。
本地OLED显示屏和密码开锁功能增强了用户交互的直观性和安全性。显示屏实时显示GPS状态和锁状态,使用户在本地也能快速获取关键信息;而密码开锁机制通过机械按键输入,提供了额外的安全层,防止未授权访问。声光寻车功能则通过APP触发本地蜂鸣器和LED提示,帮助用户在复杂环境中快速定位车辆,提升了实用性和用户体验。
整体上,该设计体现了现代物联网技术在电动车管理中的应用,通过硬件与软件的紧密结合,实现了低成本、高可靠性的解决方案。它不仅满足了日常防盗需求,还扩展了远程管理能力,具有较高的实际应用价值,适用于个人用户或共享电动车场景,为推动智能交通设备的发展提供了参考。
五、设计思路
整个系统以STM32F103RCT6微控制器为核心,负责协调各个模块的工作。系统通过4G模块Air780e实现与华为云IOT服务器的MQTT通信,确保数据可靠上传和远程指令接收。硬件采用洞洞板搭建,使用焊接和杜邦线连接各组件,供电由14500锂电池提供,并预留太阳能扩展接口。
防盗功能通过ADXL345加速度传感器检测电动车的姿态变化,当车辆在锁车状态下被移动时,传感器触发中断,STM32控制蜂鸣器发出报警声,同时通过4G模块向预设联系人发送短信提醒,并经由MQTT协议将警报信息上传到云端,使得Android APP和Windows上位机能够实时弹窗显示移动事件。
数据上云部分依托4G模块的无线连接能力,STM32定期采集GPS模块ATGM336H-5N的位置信息、锁状态(通过继电器模拟)以及其他传感器数据,通过MQTT协议发布到华为云IOT服务器。Android APP使用QT5和C++开发,订阅云端主题以实现远程数据显示,包括地图定位和锁状态,并支持用户发送开关锁指令,这些指令通过MQTT下发到STM32执行。
本地OLED显示屏采用SPI协议与STM32通信,实时显示关键信息,如GPS定位状态(是否锁定信号)、锁的开关状态以及密码输入过程中的反馈。这为用户提供了直观的本地交互界面,增强系统的可用性。
声光寻车功能由手机APP发起,用户按下APP上的按钮后,指令通过MQTT发送到云端并转发到STM32,微控制器接收到指令后驱动蜂鸣器发出滴滴声和LED灯闪烁一次,帮助用户在嘈杂环境中快速定位车辆。
密码开锁功能通过4个独立机械按键实现,用户输入预设密码时,STM32处理按键输入并在OLED上显示输入状态(如星号表示已输入),密码验证正确后控制继电器吸合以模拟开锁,错误则保持锁闭状态,确保安全性。整个系统设计注重实际应用,兼顾远程与本地控制,提升电动车的防盗和管理效率。
六、框架图
+-----------------------------+
| STM32F103RCT6 |
| (Main Controller) |
+-----------------------------+
| | | | | | |
| | | | | | +-------------------+
| | | | | +-------------------+ |
| | | | +-------------------+ | |
| | | +-------------------+ | | |
| | +-------------------+ | | | |
| +-------------------+ | | | | |
v v v v v v v v v v
+-----+ +-----+ +-----+ +-----+ +-----+ +-----+ +-----+ +-----+
| OLED | | GPS | |ADXL345| |Buzzer| | LED | |Relay| | Keys |
|Display| |Module| |Sensor | |(GPIO)| |(GPIO)| |(GPIO)| |(GPIO)|
| (SPI) | (UART) | (I2C) | | | | |
+-----+ +-----+ +-----+ +-----+ +-----+ +-----+ +-----+ +-----+
| | | | | | | |
| | | | | | | |
+-------------------------------------------------------+
|
v
+---------------------+
| 4G Module (Air780e) |
| (UART Interface) |
+---------------------+
|
v
+-----------------------------+
| 华为云IOT服务器 |
| (MQTT Protocol) |
+-----------------------------+
|
v
+-----------------------------+
| Android APP (QT5/C++) |
| Windows上位机 |
+-----------------------------+
七、系统总体设计
本系统设计基于STM32微控制器为核心,旨在实现电动车的智能防盗与远程管理功能。系统通过集成多种传感器和通信模块,提供全面的监控和控制能力。硬件平台采用STM32F103RCT6作为主控芯片,负责处理所有输入输出操作和数据通信。
硬件设计方面,系统包括一个0.96寸SPI协议的OLED显示屏用于本地状态显示,如GPS定位信息和锁状态。4G通信模块选用Air780e,用于连接华为云IOT物联网服务器并通过MQTT协议上传数据,同时支持短信发送功能。GPS定位模块采用ATGM336H-5N,提供位置信息。防盗检测通过ADXL345加速度传感器实现,监测车辆移动。输出设备包括高电平触发的有源蜂鸣器用于报警,白色LED灯用于寻车提示,以及继电器模拟电动车锁的开关。用户输入通过四个独立机械按键实现密码输入。供电系统由14500锂电池提供,并预留接口支持太阳能扩展。所有硬件组件通过洞洞板焊接和杜邦线连接完成构建。
软件和通信部分,STM32端代码使用C语言在Keil5环境下开发,采用寄存器编程方式以提高效率。4G模块通过AT指令配置,建立与华为云IOT服务器的MQTT连接,实现数据上传和命令接收。Android手机APP采用QT5框架和C++语言开发,用于远程显示设备数据如锁状态和GPS地图,并支持远程开关锁和声光寻车指令下发。数据上云过程确保实时性,MQTT协议处理订阅和发布消息,实现双向通信。
功能实现上,防盗功能通过在锁车状态下监测ADXL345传感器的姿态变化来触发本地蜂鸣器报警,同时4G模块发送短信提醒指定联系人和APP弹窗。数据上云包括定期上传GPS位置、锁状态等信息到云端,供APP和Windows上位机显示。本地OLED显示屏实时更新GPS状态和锁状态。声光寻车功能由APP发送指令,STM32接收后控制蜂鸣器发声和LED闪烁。密码开锁通过机械按键输入,STM32验证密码后控制继电器开关锁,OLED显示输入过程。系统整体设计注重可靠性和实用性,适应户外环境。
八、系统功能总结
| 功能名称 | 功能描述 |
|---|---|
| 防盗功能 | 当电动车在锁车状态下被移动时,本地蜂鸣器报警,同时发送短信提醒指定联系人,APP弹窗提醒。 |
| 数据上云功能 | 通过4G模块连接华为云IOT服务器,使用MQTT协议上传锁状态、GPS定位等数据,Android APP和Windows上位机远程显示数据并支持远程开关锁。 |
| 本地显示功能 | OLED显示屏实时显示GPS定位状态和锁的开关状态信息。 |
| 声光寻车功能 | 通过手机APP发送指令,触发本地蜂鸣器发出声音和LED灯闪烁,用于寻车。 |
| 密码开锁功能 | 使用4个独立机械按键输入密码进行开锁,OLED显示屏显示输入状态。 |
九、设计的各个功能模块描述
防盗功能模块通过ADXL345加速度传感器检测电动车的姿态变化,当车辆在锁车状态下被移动时,传感器触发STM32主控芯片,控制本地蜂鸣器发出报警声音,同时4G模块Air780e向预设联系人发送短信提醒,告知车辆被移动,并通过MQTT协议将事件数据上传至华为云IOT服务器,使得Android APP和Windows上位机能够实时弹窗提醒用户。
数据上云模块依托4G模块Air780e实现无线连接,该模块通过MQTT协议与华为云IOT物联网服务器通信,实时上传电动车的锁状态、GPS定位数据以及其他传感器信息。STM32主控芯片处理数据并驱动4G模块,确保在户外无WIFI环境下稳定联网。Android手机APP和Windows上位机基于QT5设计,使用C++语言开发,远程显示上传的数据,并支持用户通过APP或上位机发送指令远程控制锁的开关状态。
本地显示模块采用0.96寸SPI协议OLED显示屏,由STM32主控芯片驱动,实时显示GPS定位状态(如经度、纬度信息)和锁的开关状态信息。显示屏内容根据传感器输入和用户操作更新,提供直观的本地反馈,增强用户体验。
声光寻车模块响应手机APP的远程指令,当用户在APP上按下特定按键时,指令通过华为云IOT服务器和4G模块下发至STM32主控芯片,芯片控制蜂鸣器发出滴滴声提示,同时白色LED灯闪烁一下,实现声光结合寻车功能,帮助用户快速定位车辆。
密码开锁模块通过4个独立机械按键实现用户输入,STM32主控芯片处理按键信号并在OLED显示屏上显示输入状态(如密码位数或错误提示)。正确输入预设密码后,芯片控制继电器模拟锁的开关,继电器吸合表示开锁,断开表示关锁,提供一种本地安全开锁方式。
十、部署华为云物联网平台
华为云官网: https://www.huaweicloud.com/
打开官网,搜索物联网,就能快速找到 设备接入IoTDA。
10.1 物联网平台介绍
华为云物联网平台(IoT 设备接入云服务)提供海量设备的接入和管理能力,将物理设备联接到云,支撑设备数据采集上云和云端下发命令给设备进行远程控制,配合华为云其他产品,帮助我们快速构筑物联网解决方案。
使用物联网平台构建一个完整的物联网解决方案主要包括3部分:物联网平台、业务应用和设备。
物联网平台作为连接业务应用和设备的中间层,屏蔽了各种复杂的设备接口,实现设备的快速接入;同时提供强大的开放能力,支撑行业用户构建各种物联网解决方案。
设备可以通过固网、2G/3G/4G/5G、NB-IoT、Wifi等多种网络接入物联网平台,并使用LWM2M/CoAP、MQTT、HTTPS协议将业务数据上报到平台,平台也可以将控制命令下发给设备。
业务应用通过调用物联网平台提供的API,实现设备数据采集、命令下发、设备管理等业务场景。
10.2 开通物联网服务
地址: https://www.huaweicloud.com/product/iothub.html
开通免费单元。
点击立即创建。
正在创建标准版实例,需要等待片刻。
创建完成之后,点击详情。 可以看到标准版实例的设备接入端口和地址。
下面框起来的就是端口号和域名
点击实例名称,可以查看当前免费单元的配置情况。
开通之后,点击接入信息,也能查看接入信息。 我们当前设备准备采用MQTT协议接入华为云平台,这里可以看到MQTT协议的地址和端口号等信息。
总结:
端口号: MQTT (1883)| MQTTS (8883)
接入地址: dab1a1f2c6.st1.iotda-device.cn-north-4.myhuaweicloud.com
根据域名地址得到IP地址信息:
打开Windows电脑的命令行控制台终端,使用ping 命令。ping一下即可。
Microsoft Windows [版本 10.0.19045.5011]
(c) Microsoft Corporation。保留所有权利。
C:UsersLenovo>ping dab1a1f2c6.st1.iotda-device.cn-north-4.myhuaweicloud.com
正在 Ping dab1a1f2c6.st1.iotda-device.cn-north-4.myhuaweicloud.com [117.78.5.125] 具有 32 字节的数据:
来自 117.78.5.125 的回复: 字节=32 时间=37ms TTL=44
来自 117.78.5.125 的回复: 字节=32 时间=37ms TTL=44
来自 117.78.5.125 的回复: 字节=32 时间=37ms TTL=44
来自 117.78.5.125 的回复: 字节=32 时间=37ms TTL=44
117.78.5.125 的 Ping 统计信息:
数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
最短 = 37ms,最长 = 37ms,平均 = 37ms
C:UsersLenovo>
MQTT协议接入端口号有两个,1883是非加密端口,8883是证书加密端口,单片机无法加载证书,所以使用1883端口合适。
10.3 创建产品
链接:https://console.huaweicloud.com/iotdm/?region=cn-north-4#/dm-dev/all-product?instanceId=03c5c68c-e588-458c-90c3-9e4c640be7af
(1)创建产品
(2)填写产品信息
根据自己产品名字填写,下面的设备类型选择自定义类型。
(3)产品创建成功
创建完成之后点击查看详情。
(4)添加自定义模型
产品创建完成之后,点击进入产品详情页面,翻到最下面可以看到模型定义。
模型简单来说: 就是存放设备上传到云平台的数据。
你可以根据自己的产品进行创建。
比如:
烟雾可以叫 MQ2
温度可以叫 Temperature
湿度可以叫 humidity
火焰可以叫 flame
其他的传感器自己用单词简写命名即可。 这就是你的单片机设备端上传到服务器的数据名字。
先点击自定义模型。
再创建一个服务ID。
接着点击新增属性。
10.4 添加设备
产品是属于上层的抽象模型,接下来在产品模型下添加实际的设备。添加的设备最终需要与真实的设备关联在一起,完成数据交互。
(1)注册设备
(2)根据自己的设备填写
(3)保存设备信息
创建完毕之后,点击保存并关闭,得到创建的设备密匙信息。该信息在后续生成MQTT三元组的时候需要使用。
(4)设备创建完成
(5)设备详情
10.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
业务流程:
(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
对于设备而言,一般会订阅平台下发消息给设备 这个主题。
设备想接收平台下发的消息,就需要订阅平台下发消息给设备 的主题,订阅后,平台下发消息给设备,设备就会收到消息。
如果设备想要知道平台下发的消息,需要订阅上面图片里标注的主题。
以当前设备为例,最终订阅主题的格式如下:
$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
根据帮助文档的介绍, 当前设备发布主题,上报属性的格式总结如下:
发布的主题格式:
$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":{"你的字段名字1":30,"你的字段名字2":10,"你的字段名字3":1,"你的字段名字4":0}}]}
10.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
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
(2)生成MQTT三元组
华为云提供了一个在线工具,用来生成MQTT鉴权三元组: https://iot-tool.obs-website.cn-north-4.myhuaweicloud.com/
打开这个工具,填入设备的信息(也就是刚才创建完设备之后保存的信息),点击生成,就可以得到MQTT的登录信息了。
下面是打开的页面:
填入设备的信息: (上面两行就是设备创建完成之后保存得到的)
直接得到三元组信息。
得到三元组之后,设备端通过MQTT协议登录鉴权的时候,填入参数即可。
ClientId 663cb18871d845632a0912e7_dev1_0_0_2024050911
Username 663cb18871d845632a0912e7_dev1
Password 71b82deae83e80f04c4269b5bbce3b2fc7c13f610948fe210ce18650909ac237
10.7 模拟设备登录测试
经过上面的步骤介绍,已经创建了产品,设备,数据模型,得到MQTT登录信息。 接下来就用MQTT客户端软件模拟真实的设备来登录平台。测试与服务器通信是否正常。
MQTT软件下载地址【免费】: https://download.csdn.net/download/xiaolong1126626497/89928772
(1)填入登录信息
打开MQTT客户端软件,对号填入相关信息(就是上面的文本介绍)。然后,点击登录,订阅主题,发布主题。
(2)打开网页查看
完成上面的操作之后,打开华为云网页后台,可以看到设备已经在线了。
点击详情页面,可以看到上传的数据:
到此,云平台的部署已经完成,设备已经可以正常上传数据了。
(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":{"你的字段名字1":30,"你的字段名字2":10,"你的字段名字3":1,"你的字段名字4":0}}]}
10.8 创建IAM账户
创建一个IAM账户,因为接下来开发上位机,需要使用云平台的API接口,这些接口都需要token进行鉴权。简单来说,就是身份的认证。 调用接口获取Token时,就需要填写IAM账号信息。所以,接下来演示一下过程。
地址: https://console.huaweicloud.com/iam/?region=cn-north-4#/iam/users
**【1】获取项目凭证 ** 点击左上角用户名,选择下拉菜单里的我的凭证
项目凭证:
28add376c01e4a61ac8b621c714bf459
【2】创建IAM用户
鼠标放在左上角头像上,在下拉菜单里选择统一身份认证。
点击左上角创建用户。
创建成功:
【3】创建完成
用户信息如下:
主用户名 l19504562721
IAM用户 ds_abc
密码 DS12345678
10.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
在线调试接口,可以请求影子接口,了解请求,与返回的数据格式。
调试完成看右下角的响应体,就是返回的影子数据。
设备影子接口返回的数据如下:
{
"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写代码访问此链接,获取影子数据,完成上位机开发。
链接如下:
https://ad635970a1.st1.iotda-app.cn-north-4.myhuaweicloud.com:443/v5/iot/28add376c01e4a61ac8b621c714bf459/devices/663cb18871d845632a0912e7_dev1/shadow
10.10 上位机代码设计
当前项目使用的相关软件工具、模块源码已经上传到网盘:https://ccnr8sukk85n.feishu.cn/wiki/QjY8weDYHibqRYkFP2qcA9aGnvb?from=from_copylink
#include <QApplication>
#include <QMainWindow>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QLineEdit>
#include <QTextEdit>
#include <QWebEngineView>
#include <QtMqtt/QtMqtt>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
class MainWindow : public QMainWindow {
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr) : QMainWindow(parent) {
setupUI();
setupMQTT();
}
~MainWindow() {
if (m_client) {
m_client->disconnectFromHost();
}
}
private slots:
void onConnectButtonClicked() {
QString host = hostEdit->text();
quint16 port = portEdit->text().toInt();
QString username = usernameEdit->text();
QString password = passwordEdit->text();
m_client->setHostname(host);
m_client->setPort(port);
m_client->setUsername(username);
m_client->setPassword(password);
m_client->connectToHost();
}
void onLockButtonClicked() {
if (m_client && m_client->state() == QMqttClient::Connected) {
QJsonObject json;
json["command"] = "lock";
json["value"] = lockButton->text() == "Lock" ? "lock" : "unlock";
QJsonDocument doc(json);
m_client->publish(controlTopic, doc.toJson());
}
}
void onMQTTConnected() {
statusLabel->setText("Connected to MQTT");
m_client->subscribe(statusTopic);
}
void onMQTTDisconnected() {
statusLabel->setText("Disconnected from MQTT");
}
void onMessageReceived(const QByteArray &message, const QMqttTopicName &topic) {
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(message, &error);
if (error.error != QJsonParseError::NoError) {
logEdit->append("JSON Parse Error: " + error.errorString());
return;
}
QJsonObject json = doc.object();
if (topic.name() == statusTopic) {
if (json.contains("lock_status")) {
QString lockStatus = json["lock_status"].toString();
lockStatusLabel->setText("Lock Status: " + lockStatus);
lockButton->setText(lockStatus == "locked" ? "Unlock" : "Lock");
}
if (json.contains("gps")) {
QJsonObject gps = json["gps"].toObject();
double lat = gps["lat"].toDouble();
double lon = gps["lon"].toDouble();
gpsLabel->setText(QString("GPS: Lat %1, Lon %2").arg(lat).arg(lon));
updateMap(lat, lon);
}
}
}
void updateMap(double lat, double lon) {
QString js = QString("updateMap(%1, %2);").arg(lat).arg(lon);
webView->page()->runJavaScript(js);
}
private:
void setupUI() {
QWidget *centralWidget = new QWidget(this);
QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget);
// Connection settings
QHBoxLayout *connLayout = new QHBoxLayout();
hostEdit = new QLineEdit("your-mqtt-host");
portEdit = new QLineEdit("1883");
usernameEdit = new QLineEdit("your-username");
passwordEdit = new QLineEdit("your-password");
QPushButton *connectButton = new QPushButton("Connect");
connLayout->addWidget(new QLabel("Host:"));
connLayout->addWidget(hostEdit);
connLayout->addWidget(new QLabel("Port:"));
connLayout->addWidget(portEdit);
connLayout->addWidget(new QLabel("Username:"));
connLayout->addWidget(usernameEdit);
connLayout->addWidget(new QLabel("Password:"));
connLayout->addWidget(passwordEdit);
connLayout->addWidget(connectButton);
mainLayout->addLayout(connLayout);
// Status labels
statusLabel = new QLabel("Disconnected");
lockStatusLabel = new QLabel("Lock Status: Unknown");
gpsLabel = new QLabel("GPS: No data");
mainLayout->addWidget(statusLabel);
mainLayout->addWidget(lockStatusLabel);
mainLayout->addWidget(gpsLabel);
// Map view
webView = new QWebEngineView(this);
webView->setUrl(QUrl("qrc:/map.html")); // Load map from resource
mainLayout->addWidget(webView);
// Control button
lockButton = new QPushButton("Lock");
mainLayout->addWidget(lockButton);
// Log
logEdit = new QTextEdit();
logEdit->setReadOnly(true);
mainLayout->addWidget(new QLabel("Log:"));
mainLayout->addWidget(logEdit);
setCentralWidget(centralWidget);
connect(connectButton, &QPushButton::clicked, this, &MainWindow::onConnectButtonClicked);
connect(lockButton, &QPushButton::clicked, this, &MainWindow::onLockButtonClicked);
}
void setupMQTT() {
m_client = new QMqttClient(this);
connect(m_client, &QMqttClient::connected, this, &MainWindow::onMQTTConnected);
connect(m_client, &QMqttClient::disconnected, this, &MainWindow::onMQTTDisconnected);
connect(m_client, &QMqttClient::messageReceived, this, &MainWindow::onMessageReceived);
}
QMqttClient *m_client = nullptr;
QLineEdit *hostEdit, *portEdit, *usernameEdit, *passwordEdit;
QLabel *statusLabel, *lockStatusLabel, *gpsLabel;
QWebEngineView *webView;
QPushButton *lockButton;
QTextEdit *logEdit;
const QString statusTopic = "device/status";
const QString controlTopic = "device/control";
};
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
MainWindow window;
window.show();
return app.exec();
}
#include "main.moc"
对于地图部分,需要创建一个HTML文件(map.html)并添加到Qt资源中。以下是map.html的示例内容:
<!DOCTYPE html>
<html>
<head>
<title>GPS Map</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
<style>
#map { height: 400px; }
</style>
</head>
<body>
<div id="map"></div>
<script>
var map = L.map('map').setView([0, 0], 2);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '? OpenStreetMap contributors'
}).addTo(map);
var marker = L.marker([0, 0]).addTo(map);
function updateMap(lat, lon) {
map.setView([lat, lon], 15);
marker.setLatLng([lat, lon]);
}
</script>
</body>
</html>
在Qt项目中,创建一个资源文件(如resources.qrc)来包含map.html。然后,在.pro文件中添加:
QT += mqtt webenginewidgets
RESOURCES += resources.qrc
此代码提供了基本功能:连接MQTT服务器、显示状态、更新地图和控制锁。用户需要根据华为云IOT的具体配置调整MQTT主机、端口、用户名、密码和主题。地图使用OpenStreetMap通过LeafletJS显示。
十一、上位机开发
11.1 Qt开发环境安装
Qt的中文官网: https://www.qt.io/zh-cn/
QT5.12.6的下载地址:https://download.qt.io/archive/qt/5.12/5.12.6
打开下载链接后选择下面的版本进行下载:
如果下载不了,可以在网盘里找到安装包下载: 飞书文档记录的网盘地址:https://ccnr8sukk85n.feishu.cn/wiki/QjY8weDYHibqRYkFP2qcA9aGnvb?from=from_copylink
软件安装时断网安装,否则会提示输入账户。
安装的时候,第一个复选框里的编译器可以全选,直接点击下一步继续安装。
选择编译器: (一定要看清楚了)
11.2 新建上位机工程
前面2讲解了需要用的API接口,接下来就使用Qt设计上位机,设计界面,完成整体上位机的逻辑设计。
【1】新建工程
【2】设置项目的名称。
【3】选择编译系统
【4】选择默认继承的类
【5】选择编译器
【6】点击完成
【7】工程创建完成
11.3 切换编译器
在左下角是可以切换编译器的。 可以选择用什么样的编译器编译程序。
目前新建工程的时候选择了2种编译器。 一种是mingw32这个编译Windows下运行的程序。 一种是Android编译器,可以生成Android手机APP。
不过要注意:Android的编译器需要配置一些环境才可以正常使用,这个大家可以网上找找教程配置一下就行了。
windows的编译器就没有这么麻烦,安装好Qt就可以编译使用。
下面我这里就选择的 mingw32这个编译器,编译Windows下运行的程序。
11.4 编译测试功能
创建完毕之后,编译测试一下功能是否OK。
点击左下角的绿色三角形按钮。
正常运行就可以看到弹出一个白色的框框。这就表示工程环境没有问题了。 接下来就可以放心的设计界面了。
11.5 设计UI界面与工程配置
【1】打开UI文件
打开默认的界面如下:
【2】开始设计界面
根据自己需求设计界面。
11.5 编译Windows上位机
点击软件左下角的绿色三角形按钮进行编译运行。
11.6 配置Android环境
如果想编译Android手机APP,必须要先自己配置好自己的Android环境。(搭建环境的过程可以自行百度搜索学习)
然后才可以进行下面的步骤。
【1】选择Android编译器
选择编译器。
切换编译器。
【2】创建Android配置文件
创建完成。
【3】配置Android图标与名称
根据自己的需求配置 Android图标与名称。
【3】编译Android上位机
Qt本身是跨平台的,直接选择Android的编译器,就可以将程序编译到Android平台。
然后点击构建。
成功之后,在目录下可以看到生成的apk文件,也就是Android手机的安装包,电脑端使用QQ发送给手机QQ,手机登录QQ接收,就能直接安装。
生成的apk的目录在哪里呢? 编译完成之后,在控制台会输出APK文件的路径。
知道目录在哪里之后,在Windows的文件资源管理器里,找到路径,具体看下图,找到生成的apk文件。
File: D:/QtProject/build-333_QtProject-Android_for_arm64_v8a_Clang_Qt_5_12_6_for_Android_ARM64_v8a-Release/android-build//build/outputs/apk/debug/android-build-debug.apk
11.7 设备仿真调试
通过MQTT客户端模拟设备登录华为云服务器。进行设备联调,实现数据上传和下发测试。
十二、STM32程序设计
当前项目使用的相关软件工具、模块源码已经上传到网盘:https://ccnr8sukk85n.feishu.cn/wiki/QjY8weDYHibqRYkFP2qcA9aGnvb?from=from_copylink
12.1 模块代码设计
#include "stm32f10x.h"
// 定义硬件引脚
#define OLED_CS_PIN GPIO_Pin_4 // PA4 as CS for OLED
#define OLED_DC_PIN GPIO_Pin_5 // PA5 as DC for OLED
#define OLED_RST_PIN GPIO_Pin_6 // PA6 as RST for OLED
#define OLED_SCK_PIN GPIO_Pin_7 // PA7 as SCK for OLED (SPI1)
#define OLED_MOSI_PIN GPIO_Pin_8 // PA8 as MOSI for OLED (SPI1)
#define GSM_TX_PIN GPIO_Pin_2 // PA2 as TX for GSM (USART2)
#define GSM_RX_PIN GPIO_Pin_3 // PA3 as RX for GSM (USART2)
#define GPS_TX_PIN GPIO_Pin_10 // PC10 as TX for GPS (USART3)
#define GPS_RX_PIN GPIO_Pin_11 // PC11 as RX for GPS (USART3)
#define ADXL345_SDA_PIN GPIO_Pin_7 // PB7 as SDA for ADXL345 (I2C1)
#define ADXL345_SCL_PIN GPIO_Pin_6 // PB6 as SCL for ADXL345 (I2C1)
#define BUZZER_PIN GPIO_Pin_0 // PB0 as Buzzer control
#define RELAY_PIN GPIO_Pin_1 // PB1 as Relay control
#define LED_PIN GPIO_Pin_5 // PB5 as LED for find vehicle
#define KEY1_PIN GPIO_Pin_0 // PC0 as Key1
#define KEY2_PIN GPIO_Pin_1 // PC1 as Key2
#define KEY3_PIN GPIO_Pin_2 // PC2 as Key3
#define KEY4_PIN GPIO_Pin_3 // PC3 as Key4
// 全局变量
volatile uint8_t lock_status = 0; // 0: locked, 1: unlocked
volatile uint8_t alarm_triggered = 0;
volatile uint8_t password_input[4] = {0};
volatile uint8_t input_index = 0;
volatile uint8_t correct_password[4] = {1, 2, 3, 4}; // 默认密码1234
// OLED显示函数声明
void OLED_Init(void);
void OLED_WriteCommand(uint8_t cmd);
void OLED_WriteData(uint8_t data);
void OLED_DisplayString(uint8_t x, uint8_t y, char *str);
void OLED_Clear(void);
// GPS函数声明
void GPS_Init(void);
void GPS_Process(void);
extern char gps_data[100]; // 存储GPS数据
// ADXL345函数声明
void ADXL345_Init(void);
void ADXL345_ReadValues(int16_t *x, int16_t *y, int16_t *z);
// GSM函数声明
void GSM_Init(void);
void GSM_SendSMS(char *number, char *message);
void GSM_MQTT_Publish(char *topic, char *data);
// 蜂鸣器、继电器、LED控制
void Buzzer_Beep(uint16_t duration);
void Relay_Set(uint8_t state);
void LED_Blink(uint16_t duration);
// 按键处理
void Keys_Init(void);
uint8_t Read_Key(uint8_t key_num);
// 系统初始化
void System_Init(void) {
// 配置系统时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_AFIOEN;
RCC->APB1ENR |= RCC_APB1ENR_USART2EN | RCC_APB1ENR_USART3EN | RCC_APB1ENR_I2C1EN;
RCC->APB2ENR |= RCC_APB2ENR_USART1EN | RCC_APB2ENR_SPI1EN;
// 初始化OLED
OLED_Init();
// 初始化GPS
GPS_Init();
// 初始化ADXL345
ADXL345_Init();
// 初始化GSM
GSM_Init();
// 初始化按键
Keys_Init();
// 初始化蜂鸣器、继电器、LED
GPIOB->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0); // PB0 as output push-pull for buzzer
GPIOB->CRL |= GPIO_CRL_MODE0_0;
GPIOB->CRL &= ~(GPIO_CRL_MODE1 | GPIO_CRL_CNF1); // PB1 as output push-pull for relay
GPIOB->CRL |= GPIO_CRL_MODE1_0;
GPIOB->CRL &= ~(GPIO_CRL_MODE5 | GPIO_CRL_CNF5); // PB5 as output push-pull for LED
GPIOB->CRL |= GPIO_CRL_MODE5_0;
// 默认状态
Relay_Set(0); // 锁关闭
GPIOB->BRR = BUZZER_PIN; // 蜂鸣器关闭
GPIOB->BRR = LED_PIN; // LED关闭
}
int main(void) {
System_Init();
OLED_DisplayString(0, 0, "System Started");
Delay_ms(1000);
OLED_Clear();
while (1) {
// 处理GPS数据
GPS_Process();
// 检测ADXL345移动
int16_t x, y, z;
ADXL345_ReadValues(&x, &y, &z);
if (lock_status == 0 && (abs(x) > 100 || abs(y) > 100 || abs(z) > 100)) { // 阈值检测
alarm_triggered = 1;
Buzzer_Beep(1000); // 蜂鸣器报警
GSM_SendSMS("+1234567890", "Electric vehicle moved!"); // 发送短信
GSM_MQTT_Publish("alarm", "moved"); // MQTT发布
OLED_DisplayString(0, 0, "Alarm! Moved");
}
// 处理按键输入
if (Read_Key(1)) {
password_input[input_index++] = 1;
OLED_DisplayString(0, 2, "*");
}
if (Read_Key(2)) {
password_input[input_index++] = 2;
OLED_DisplayString(1, 2, "*");
}
if (Read_Key(3)) {
password_input[input_index++] = 3;
OLED_DisplayString(2, 2, "*");
}
if (Read_Key(4)) {
password_input[input_index++] = 4;
OLED_DisplayString(3, 2, "*");
}
if (input_index == 4) {
if (memcmp(password_input, correct_password, 4) == 0) {
lock_status = 1;
Relay_Set(1); // 开锁
OLED_DisplayString(0, 3, "Unlocked");
} else {
OLED_DisplayString(0, 3, "Wrong Password");
}
input_index = 0;
memset(password_input, 0, 4);
}
// 显示状态
char status_str[20];
sprintf(status_str, "Lock: %s", lock_status ? "Open" : "Close");
OLED_DisplayString(0, 1, status_str);
// 处理云命令(通过GSM接收)
// 简化为轮询,实际可用中断
if (USART2->SR & USART_SR_RXNE) {
char cmd = USART2->DR;
if (cmd == 'F') { // 寻车命令
Buzzer_Beep(500);
LED_Blink(500);
} else if (cmd == 'L') { // 远程开锁
lock_status = 1;
Relay_Set(1);
} else if (cmd == 'C') { // 远程关锁
lock_status = 0;
Relay_Set(0);
}
}
Delay_ms(100);
}
}
// OLED驱动代码
void OLED_Init(void) {
// 初始化SPI1和GPIO
GPIOA->CRL &= ~(GPIO_CRL_MODE4 | GPIO_CRL_CNF4); // PA4 as output push-pull for CS
GPIOA->CRL |= GPIO_CRL_MODE4_0;
GPIOA->CRL &= ~(GPIO_CRL_MODE5 | GPIO_CRL_CNF5); // PA5 as output push-pull for DC
GPIOA->CRL |= GPIO_CRL_MODE5_0;
GPIOA->CRL &= ~(GPIO_CRL_MODE6 | GPIO_CRL_CNF6); // PA6 as output push-pull for RST
GPIOA->CRL |= GPIO_CRL_MODE6_0;
GPIOA->CRH &= ~(GPIO_CRH_MODE7 | GPIO_CRH_CNF7); // PA7 as alternate function push-pull for SCK
GPIOA->CRH |= GPIO_CRH_MODE7_0 | GPIO_CRH_CNF7_1;
GPIOA->CRH &= ~(GPIO_CRH_MODE8 | GPIO_CRH_CNF8); // PA8 as alternate function push-pull for MOSI
GPIOA->CRH |= GPIO_CRH_MODE8_0 | GPIO_CRH_CNF8_1;
SPI1->CR1 = SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_MSTR | SPI_CR1_SPE | SPI_CR1_BR_0; // SPI master, software CS, prescaler 2
SPI1->CR2 = 0;
// 复位OLED
GPIOA->BRR = OLED_RST_PIN;
Delay_ms(100);
GPIOA->BSRR = OLED_RST_PIN;
Delay_ms(100);
OLED_WriteCommand(0xAE); // display off
OLED_WriteCommand(0xD5); // set display clock divide ratio
OLED_WriteCommand(0x80);
OLED_WriteCommand(0xA8); // set multiplex
OLED_WriteCommand(0x3F);
OLED_WriteCommand(0xD3); // set display offset
OLED_WriteCommand(0x00);
OLED_WriteCommand(0x40); // set start line
OLED_WriteCommand(0x8D); // charge pump
OLED_WriteCommand(0x14);
OLED_WriteCommand(0x20); // memory mode
OLED_WriteCommand(0x00);
OLED_WriteCommand(0xA1); // segment remap
OLED_WriteCommand(0xC8); // com scan dec
OLED_WriteCommand(0xDA); // set com pins
OLED_WriteCommand(0x12);
OLED_WriteCommand(0x81); // set contrast
OLED_WriteCommand(0xCF);
OLED_WriteCommand(0xD9); // set precharge
OLED_WriteCommand(0xF1);
OLED_WriteCommand(0xDB); // set vcom detect
OLED_WriteCommand(0x40);
OLED_WriteCommand(0xA4); // display all on resume
OLED_WriteCommand(0xA6); // normal display
OLED_WriteCommand(0xAF); // display on
OLED_Clear();
}
void OLED_WriteCommand(uint8_t cmd) {
GPIOA->BRR = OLED_DC_PIN; // DC low for command
GPIOA->BRR = OLED_CS_PIN; // CS low
while (!(SPI1->SR & SPI_SR_TXE));
SPI1->DR = cmd;
while (SPI1->SR & SPI_SR_BSY);
GPIOA->BSRR = OLED_CS_PIN; // CS high
}
void OLED_WriteData(uint8_t data) {
GPIOA->BSRR = OLED_DC_PIN; // DC high for data
GPIOA->BRR = OLED_CS_PIN; // CS low
while (!(SPI1->SR & SPI_SR_TXE));
SPI1->DR = data;
while (SPI1->SR & SPI_SR_BSY);
GPIOA->BSRR = OLED_CS_PIN; // CS high
}
void OLED_Clear(void) {
for (uint8_t page = 0; page < 8; page++) {
OLED_WriteCommand(0xB0 + page);
OLED_WriteCommand(0x00);
OLED_WriteCommand(0x10);
for (uint8_t col = 0; col < 128; col++) {
OLED_WriteData(0x00);
}
}
}
void OLED_DisplayString(uint8_t x, uint8_t y, char *str) {
// 简单实现,显示字符串在指定位置
OLED_WriteCommand(0xB0 + y);
OLED_WriteCommand(0x00 + (x & 0x0F));
OLED_WriteCommand(0x10 + ((x >> 4) & 0x0F));
while (*str) {
OLED_WriteData(*str++);
}
}
// GPS驱动代码
void GPS_Init(void) {
// 初始化USART3 for GPS
GPIOC->CRH &= ~(GPIO_CRH_CNF10 | GPIO_CRH_MODE10); // PC10 as alternate function push-pull for TX
GPIOC->CRH |= GPIO_CRH_CNF10_1 | GPIO_CRH_MODE10_0;
GPIOC->CRH &= ~(GPIO_CRH_CNF11 | GPIO_CRH_MODE11); // PC11 as input floating for RX
GPIOC->CRH |= GPIO_CRH_CNF11_0;
USART3->BRR = 0x1D4C; // 9600 baud @ 72MHz
USART3->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;
}
char gps_data[100];
uint8_t gps_index = 0;
void GPS_Process(void) {
if (USART3->SR & USART_SR_RXNE) {
char c = USART3->DR;
if (c == 'n') {
gps_data[gps_index] = '';
// 解析GPS数据,这里简单显示
OLED_DisplayString(0, 4, gps_data);
gps_index = 0;
} else {
gps_data[gps_index++] = c;
if (gps_index >= 99) gps_index = 0;
}
}
}
// ADXL345驱动代码(假设使用I2C)
void ADXL345_Init(void) {
// 初始化I2C1
GPIOB->CRL &= ~(GPIO_CRL_CNF6 | GPIO_CRL_MODE6); // PB6 as alternate function open-drain for SCL
GPIOB->CRL |= GPIO_CRL_CNF6_1 | GPIO_CRL_MODE6_0;
GPIOB->CRL &= ~(GPIO_CRL_CNF7 | GPIO_CRL_MODE7); // PB7 as alternate function open-drain for SDA
GPIOB->CRL |= GPIO_CRL_CNF7_1 | GPIO_CRL_MODE7_0;
I2C1->CR2 = 0x24; // 36MHz
I2C1->CCR = 0x1E0; // 100kHz
I2C1->TRISE = 0x25;
I2C1->CR1 = I2C_CR1_PE;
// 配置ADXL345
I2C_Write(0x53, 0x2D, 0x08); // power on
I2C_Write(0x53, 0x31, 0x08); // full resolution, +/-2g
}
void I2C_Write(uint8_t addr, uint8_t reg, uint8_t data) {
while (I2C1->SR2 & I2C_SR2_BUSY);
I2C1->CR1 |= I2C_CR1_START;
while (!(I2C1->SR1 & I2C_SR1_SB));
I2C1->DR = addr << 1;
while (!(I2C1->SR1 & I2C_SR1_ADDR));
(void)I2C1->SR2;
I2C1->DR = reg;
while (!(I2C1->SR1 & I2C_SR1_TXE));
I2C1->DR = data;
while (!(I2C1->SR1 & I2C_SR1_BTF));
I2C1->CR1 |= I2C_CR1_STOP;
}
void ADXL345_ReadValues(int16_t *x, int16_t *y, int16_t *z) {
uint8_t data[6];
I2C_Read(0x53, 0x32, data, 6);
*x = (data[1] << 8) | data[0];
*y = (data[3] << 8) | data[2];
*z = (data[5] << 8) | data[4];
}
void I2C_Read(uint8_t addr, uint8_t reg, uint8_t *data, uint8_t len) {
while (I2C1->SR2 & I2C_SR2_BUSY);
I2C1->CR1 |= I2C_CR1_START;
while (!(I2C1->SR1 & I2C_SR1_SB));
I2C1->DR = addr << 1;
while (!(I2C1->SR1 & I2C_SR1_ADDR));
(void)I2C1->SR2;
I2C1->DR = reg;
while (!(I2C1->SR1 & I2C_SR1_TXE));
I2C1->CR1 |= I2C_CR1_START;
while (!(I2C1->SR1 & I2C_SR1_SB));
I2C1->DR = (addr << 1) | 1;
while (!(I2C1->SR1 & I2C_SR1_ADDR));
(void)I2C1->SR2;
for (uint8_t i = 0; i < len; i++) {
if (i == len - 1) I2C1->CR1 &= ~I2C_CR1_ACK;
while (!(I2C1->SR1 & I2C_SR1_RXNE));
data[i] = I2C1->DR;
}
I2C1->CR1 |= I2C_CR1_STOP;
}
// GSM驱动代码
void GSM_Init(void) {
// 初始化USART2 for GSM
GPIOA->CRL &= ~(GPIO_CRL_CNF2 | GPIO_CRL_MODE2); // PA2 as alternate function push-pull for TX
GPIOA->CRL |= GPIO_CRL_CNF2_1 | GPIO_CRL_MODE2_0;
GPIOA->CRL &= ~(GPIO_CRL_CNF3 | GPIO_CRL_MODE3); // PA3 as input floating for RX
GPIOA->CRL |= GPIO_CRL_CNF3_0;
USART2->BRR = 0x1D4C; // 9600 baud @ 72MHz
USART2->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;
// 初始化GSM模块
GSM_SendAT("AT+CMGF=1"); // set SMS text mode
GSM_SendAT("AT+CNMI=1,2,0,0,0"); // set SMS notification
}
void GSM_SendAT(char *cmd) {
while (*cmd) {
while (!(USART2->SR & USART_SR_TXE));
USART2->DR = *cmd++;
}
while (!(USART2->SR & USART_SR_TXE));
USART2->DR = 'r';
while (!(USART2->SR & USART_SR_TXE));
USART2->DR = 'n';
Delay_ms(100);
}
void GSM_SendSMS(char *number, char *message) {
char cmd[50];
sprintf(cmd, "AT+CMGS="%s"", number);
GSM_SendAT(cmd);
Delay_ms(100);
GSM_SendAT(message);
USART2->DR = 0x1A; // send CTRL+Z
Delay_ms(1000);
}
void GSM_MQTT_Publish(char *topic, char *data) {
char msg[100];
sprintf(msg, "AT+MQTTPUB="%s","%s"", topic, data);
GSM_SendAT(msg);
}
// 蜂鸣器控制
void Buzzer_Beep(uint16_t duration) {
GPIOB->BSRR = BUZZER_PIN; // turn on
Delay_ms(duration);
GPIOB->BRR = BUZZER_PIN; // turn off
}
// 继电器控制
void Relay_Set(uint8_t state) {
if (state) {
GPIOB->BSRR = RELAY_PIN;
} else {
GPIOB->BRR = RELAY_PIN;
}
}
// LED控制
void LED_Blink(uint16_t duration) {
GPIOB->BSRR = LED_PIN;
Delay_ms(duration);
GPIOB->BRR = LED_PIN;
}
// 按键初始化
void Keys_Init(void) {
GPIOC->CRL &= ~(GPIO_CRL_CNF0 | GPIO_CRL_MODE0); // PC0 as input pull-up
GPIOC->CRL |= GPIO_CRL_CNF0_1;
GPIOC->BSRR = KEY1_PIN;
// 类似配置其他按键
GPIOC->CRL &= ~(GPIO_CRL_CNF1 | GPIO_CRL_MODE1);
GPIOC->CRL |= GPIO_CRL_CNF1_1;
GPIOC->BSRR = KEY2_PIN;
GPIOC->CRL &= ~(GPIO_CRL_CNF2 | GPIO_CRL_MODE2);
GPIOC->CRL |= GPIO_CRL_CNF2_1;
GPIOC->BSRR = KEY3_PIN;
GPIOC->CRL &= ~(GPIO_CRL_CNF3 | GPIO_CRL_MODE3);
GPIOC->CRL |= GPIO_CRL_CNF3_1;
GPIOC->BSRR = KEY4_PIN;
}
uint8_t Read_Key(uint8_t key_num) {
uint32_t pin;
switch (key_num) {
case 1: pin = KEY1_PIN; break;
case 2: pin = KEY2_PIN; break;
case 3: pin = KEY3_PIN; break;
case 4: pin = KEY4_PIN; break;
default: return 0;
}
if ((GPIOC->IDR & pin) == 0) { // 按键按下(低电平)
Delay_ms(20); // 消抖
if ((GPIOC->IDR & pin) == 0) {
while ((GPIOC->IDR & pin) == 0); // 等待释放
return 1;
}
}
return 0;
}
// 简单延时函数
void Delay_ms(uint16_t ms) {
for (uint16_t i = 0; i < ms; i++) {
for (uint16_t j = 0; j < 7200; j++); // 近似1ms at 72MHz
}
}
12.2 STM32项目核心代码
#include "stm32f10x.h"
#include "oled.h"
#include "gps.h"
#include "mqtt.h"
#include "key.h"
#include "accel.h"
#include "beep.h"
#include "relay.h"
#include "led.h"
#include "string.h"
// 状态变量定义
volatile uint8_t lock_status = 0; // 0: 开锁, 1: 关锁
volatile uint8_t movement_detected = 0;
char gps_data[100] = {0};
char password[5] = {0}; // 存储密码输入
uint8_t password_index = 0;
const char correct_password[5] = "1234"; // 默认密码
void SystemInit(void) {
// 配置系统时钟为72MHz
RCC->CR |= RCC_CR_HSEON; // 开启HSE
while(!(RCC->CR & RCC_CR_HSERDY)); // 等待HSE就绪
RCC->CFGR |= RCC_CFGR_PLLSRC_HSE; // PLL源为HSE
RCC->CFGR |= RCC_CFGR_PLLMULL9; // PLL倍频9倍
RCC->CR |= RCC_CR_PLLON; // 开启PLL
while(!(RCC->CR & RCC_CR_PLLRDY)); // 等待PLL就绪
RCC->CFGR |= RCC_CFGR_SW_PLL; // 系统时钟切换为PLL
while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL); // 等待切换完成
}
void GPIO_Init(void) {
// 使能GPIO时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN;
// 初始化蜂鸣器引脚(PA0,推挽输出)
GPIOA->CRL &= ~(0xF << 0);
GPIOA->CRL |= (0x3 << 0);
// 初始化LED引脚(PA1,推挽输出)
GPIOA->CRL &= ~(0xF << 4);
GPIOA->CRL |= (0x3 << 4);
// 初始化继电器引脚(PA2,推挽输出)
GPIOA->CRL &= ~(0xF << 8);
GPIOA->CRL |= (0x3 << 8);
// 初始化按键引脚(PA3-PA6,上拉输入)
GPIOA->CRL &= ~(0xFFF << 12);
GPIOA->CRL |= (0x8 << 12) | (0x8 << 16) | (0x8 << 20) | (0x8 << 24);
GPIOA->ODR |= (1 << 3) | (1 << 4) | (1 << 5) | (1 << 6); // 上拉
}
void UART_Init(void) {
// 使能UART1和UART2时钟
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
// UART1 for 4G module (PA9 TX, PA10 RX)
GPIOA->CRH &= ~(0xFF << 4);
GPIOA->CRH |= (0xB << 4) | (0x4 << 8); // PA9: AF Push-pull, PA10: Input floating
USART1->BRR = 72000000 / 115200; // 波特率115200
USART1->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE; // 使能UART, TX, RX
// UART2 for GPS module (PA2 TX, PA3 RX) - 注意PA2被继电器使用,需调整或复用
// 假设GPS使用UART2,但PA2冲突,这里使用PC10和PC11作为示例调整
// 实际硬件连接需对应
GPIOC->CRH &= ~(0xFF << 8);
GPIOC->CRH |= (0xB << 8) | (0x4 << 12); // PC10: AF Push-pull, PC11: Input floating
USART2->BRR = 72000000 / 9600; // 波特率9600 for GPS
USART2->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
}
void SPI_Init(void) {
// 使能SPI1时钟
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
// SPI1 for OLED (PA5 SCK, PA6 MISO, PA7 MOSI)
GPIOA->CRL &= ~(0xFFF << 20);
GPIOA->CRL |= (0xB << 20) | (0x4 << 24) | (0xB << 28); // PA5: AF Push-pull, PA6: Input floating, PA7: AF Push-pull
SPI1->CR1 = SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_MSTR | SPI_CR1_SPE; // 主模式, 软件SS, 使能SPI
SPI1->CR1 |= SPI_CR1_BR_0; // 时钟分频
}
void I2C_Init(void) {
// 使能I2C1时钟
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
// I2C1 for accelerometer (PB6 SCL, PB7 SDA)
GPIOB->CRL &= ~(0xFF << 24);
GPIOB->CRL |= (0xB << 24) | (0xB << 28); // PB6 and PB7: AF Open-drain
I2C1->CR1 = I2C_CR1_SWRST; // 复位I2C
I2C1->CR1 = 0;
I2C1->CR2 = 0x08; // 频率设置
I2C1->CCR = 0x50; // 时钟控制
I2C1->TRISE = 0x09; // 上升时间
I2C1->CR1 |= I2C_CR1_PE; // 使能I2C
}
int main(void) {
SystemInit();
GPIO_Init();
UART_Init();
SPI_Init();
I2C_Init();
// 初始化模块
oled_init();
gps_init();
mqtt_init();
accel_init();
key_init();
beep_init();
relay_init();
led_init();
// 初始显示
oled_show_string(0, 0, "System Ready");
delay_ms(1000);
while(1) {
// 处理按键输入
uint8_t key = key_scan();
if (key != 0) {
if (password_index < 4) {
password[password_index] = key + '0'; // 假设按键返回数字0-3
password_index++;
oled_show_string(0, 2, "Input: ");
oled_show_string(40, 2, password);
}
if (password_index == 4) {
if (strcmp(password, correct_password) == 0) {
relay_on(); // 开锁
lock_status = 0;
oled_show_string(0, 3, "Unlocked");
beep_beep(); // 提示音
} else {
oled_show_string(0, 3, "Wrong Password");
beep_on();
delay_ms(500);
beep_off();
}
password_index = 0;
memset(password, 0, sizeof(password));
}
}
// 读取加速度数据并检查移动
accel_data_t accel_data = accel_get_data();
if (lock_status == 1 && accel_is_moved(accel_data)) {
movement_detected = 1;
beep_on(); // 蜂鸣器报警
mqtt_send_alert("Movement detected"); // 发送MQTT警报
// 发送短信 via 4G module
uart_send_string(USART1, "AT+CMGS="+1234567890"r"); // 假设电话号码
delay_ms(100);
uart_send_string(USART1, "Electric vehicle moved!r");
uart_send_byte(USART1, 0x1A); // Ctrl+Z to send
} else {
movement_detected = 0;
}
// 更新GPS数据
gps_update();
gps_get_string(gps_data);
// 显示状态到OLED
oled_clear();
oled_show_string(0, 0, lock_status ? "Locked" : "Unlocked");
oled_show_string(0, 1, gps_data);
// 处理MQTT消息
if (mqtt_receive()) {
char msg[50];
mqtt_get_message(msg);
if (strcmp(msg, "unlock") == 0) {
relay_on();
lock_status = 0;
oled_show_string(0, 3, "Remote Unlock");
} else if (strcmp(msg, "lock") == 0) {
relay_off();
lock_status = 1;
oled_show_string(0, 3, "Remote Lock");
} else if (strcmp(msg, "find_car") == 0) {
beep_beep();
led_blink();
oled_show_string(0, 3, "Find Car Activated");
}
}
// 延迟以防止频繁操作
delay_ms(100);
}
}
十三、总结
本项目成功设计并实现了一个基于STM32与4G通信的智能电动车防盗与远程管理系统,集成了防盗报警、数据上云、远程控制等核心功能,有效提升了电动车的安全性和管理效率。系统通过本地传感器检测车辆状态,触发报警并通过4G模块发送提醒,同时支持手机APP和上位机进行远程监控与操作,为用户提供了便捷、实时的管理体验。
在技术实现上,系统采用STM32F103RCT6主控芯片,通过寄存器编程方式开发硬件端代码,确保高效性和稳定性;4G模块连接华为云IoT服务器,使用MQTT协议实现数据上传与命令下发;Android APP基于QT5和C++设计,提供了友好的用户界面和功能交互。硬件方面,通过洞洞板制作整合了OLED显示屏、GPS模块、加速度传感器等组件,支持密码开锁和声光寻车功能,体现了模块化与可扩展性的设计理念。
总体而言,该项目结合了物联网技术、嵌入式系统和移动应用开发,为电动车管理提供了创新且实用的解决方案,适用于户外环境,并具备良好的扩展潜力,如支持太阳能供电,未来可进一步优化和商业化应用。
3675