基于STM32设计的智能盲人辅助导航系统设计

一、项目开发背景

1.1 前言

随着社会对特殊群体关怀意识的不断提升,盲人群体的出行安全问题日益受到重视。传统盲杖虽能提供基础触觉反馈,但存在探测范围有限、无法预警空中障碍物、缺乏实时数据记录等局限性。近年来嵌入式技术和物联网的快速发展,为开发智能化辅助设备提供了技术基础。

基于STM32的智能导航系统结合超声波测距、多模态反馈和云平台技术,能够有效扩展盲人的环境感知维度。通过实时采集障碍物距离数据并进行分级预警,系统既可通过振动马达提供隐蔽的触觉提示,又能通过语音模块提供明确的方向指引,形成互补的双重保障机制。

华为云平台的接入实现了行走数据的云端存储与分析,使监护人可通过QT上位机远程查看行动轨迹和遇障统计,为后续出行路线优化提供数据支持。这种硬件与软件结合、本地与云端协同的设计方案,不仅提升了设备的实用性,更构建了完整的安全监护生态体系,体现了科技赋能人文关怀的创新理念。

下面是硬件调试阶段的硬件模块:

1.2 设计实现的功能

(1) 实时采集前方障碍物距离信息并进行危险等级判断

(2) 通过振动马达和语音模块提供多模式导航提示

(3) 行走轨迹和遇障数据上传至华为云平台

(4) QT上位机显示行走路径地图和遇障统计信息

(5) 服务器采用华为云物联网服务器

1.3 项目硬件模块组成

(1)STM32F103C8T6最小系统核心板作为主控制器

(2)HC-SR04超声波传感器检测前方障碍物

(3)JL-03语音模块提供语音导航提示

(4)振动马达模块提供触觉导航反馈

(5)ESP8266-01S Wi-Fi模块实现华为云平台通信

(6)洞洞板焊接传感器接口电路,杜邦线连接各模块

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

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

2.1 物联网平台介绍

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

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

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

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

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

2.2 开通物联网服务

开通免费单元。

点击立即创建

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

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

下面框起来的就是端口号域名

点击实例名称,可以查看当前免费单元的配置情况。

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

总结:

scss 复制代码
端口号:   MQTT (1883)| MQTTS (8883)    
接入地址: dab1a1f2c6.st1.iotda-device.cn-north-4.myhuaweicloud.com

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

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

ini 复制代码
Microsoft Windows [版本 10.0.19045.5011]
(c) Microsoft Corporation。保留所有权利。

正在 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:\Users\Lenovo>

MQTT协议接入端口号有两个,1883是非加密端口,8883是证书加密端口,单片机无法加载证书,所以使用1883端口合适

2.3 创建产品

(1)创建产品

(2)填写产品信息

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

(3)产品创建成功

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

(4)添加自定义模型

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

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

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

比如:

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

先点击自定义模型。

再创建一个服务ID。

接着点击新增属性。

2.4 添加设备

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

(1)注册设备

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

(3)保存设备信息

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

(4)设备创建完成

(5)设备详情

2.5 MQTT协议主题订阅与发布

(1)MQTT协议介绍

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

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

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

业务流程:

(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)主题订阅格式

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

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

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

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

(4)主题发布格式

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

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

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

cpp 复制代码
发布的主题格式:
$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}}]}

2.6 MQTT三元组

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

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

(1)MQTT服务器地址

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

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

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

cpp 复制代码
华为云的MQTT服务器地址:117.78.5.125
华为云的MQTT端口号:1883

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

cpp 复制代码
ping  ad635970a1.st1.iotda-device.cn-north-4.myhuaweicloud.com

(2)生成MQTT三元组

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

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

下面是打开的页面:

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

直接得到三元组信息。

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

cpp 复制代码
ClientId  663cb18871d845632a0912e7_dev1_0_0_2024050911
Username  663cb18871d845632a0912e7_dev1
Password  71b82deae83e80f04c4269b5bbce3b2fc7c13f610948fe210ce18650909ac237

2.7 模拟设备登录测试

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

(1)填入登录信息

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

(2)打开网页查看

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

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

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

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

cpp 复制代码
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}}]}

2.8 创建IAM账户

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

地址: console.huaweicloud.com/iam/?region...

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

项目凭证:

cpp 复制代码
28add376c01e4a61ac8b621c714bf459

【2】创建IAM用户

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

点击左上角创建用户

创建成功:

【3】创建完成

用户信息如下:

cpp 复制代码
主用户名  l19504562721
IAM用户  ds_abc
密码     DS12345678

2.9 获取影子数据

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

设备影子介绍:

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

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

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

如果对接口不熟悉,可以先进行在线调试:apiexplorer.developer.huaweicloud.com/apiexplorer...

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

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

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

cpp 复制代码
{
 "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写代码访问此链接,获取影子数据,完成上位机开发。

链接如下:

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

四、设计意义

该智能盲人辅助导航系统设计旨在显著提升视障人士的独立出行能力和安全性,通过集成多种传感器和反馈机制,有效减少盲人在行走过程中遇到障碍物的风险。系统实时采集前方障碍物距离信息,并进行危险等级判断,从而为用户提供及时预警,帮助其规避潜在危险,增强户外活动的信心和自主性。

通过振动马达和语音模块提供多模式导航提示,系统适应了不同环境和使用者偏好,振动反馈在嘈杂环境中尤为有效,而语音提示则提供直观的指导,这种双模式设计确保了导航信息的可靠传递,提升了用户体验的舒适度和实用性。

系统将行走轨迹和遇障数据上传至华为云平台,并通过QT上位机显示行走路径地图和遇障统计信息,实现了数据的远程监控和分析,这不仅便于 caregivers 或研究人员实时跟踪用户状态,还为长期优化导航算法和提供个性化服务提供了数据支持,从而推动盲人辅助技术的持续改进。

硬件组成基于STM32F103C8T6最小系统核心板和常见模块如HC-SR04超声波传感器、ESP8266-01S Wi-Fi模块,这种设计注重成本效益和可靠性,使得系统易于部署和维护,同时通过洞洞板焊接和杜邦线连接,确保了硬件的灵活性和可扩展性,为实际应用提供了坚实的基础。

五、设计思路

该系统设计以STM32F103C8T6最小系统核心板作为主控制器,负责协调各模块工作,实现智能盲人辅助导航功能。系统通过HC-SR04超声波传感器实时采集前方障碍物距离信息,STM32对采集到的数据进行处理,根据预设阈值进行危险等级判断,例如将距离分为安全、警告和危险等级别,以触发相应的反馈机制。

对于导航提示,系统集成振动马达模块和JL-03语音模块,提供多模式反馈。当检测到障碍物时,STM32根据危险等级控制振动马达产生不同强度的振动,同时语音模块播放预录的提示音或语音消息,帮助用户感知环境并做出避障决策,确保导航过程直观有效。

数据上传部分通过ESP8266-01S Wi-Fi模块实现,STM32将处理后的行走轨迹和遇障数据封装成JSON格式,通过MQTT协议发送至华为云物联网平台。华为云服务器负责存储和管理数据,支持后续分析和远程监控。

QT上位机软件设计用于显示用户行走路径地图和遇障统计信息,它从华为云平台获取数据,解析后以图形化界面展示路径轨迹、障碍物位置和频率统计,方便 caregivers 或管理员进行可视化分析和历史回顾。

硬件方面,所有模块通过洞洞板焊接接口电路,使用杜邦线连接至STM32核心板,确保连接可靠且易于调试。整个系统注重低功耗和实时性,以实际应用场景为基础,避免不必要的功能扩展。

七、系统总体设计

系统以STM32F103C8T6最小系统核心板作为主控制器,负责协调整个系统的运行和数据处理。该系统设计用于为盲人提供辅助导航,通过集成多种传感器和模块实现实时障碍物检测、多模式反馈以及远程数据监控。

HC-SR04超声波传感器用于实时采集前方障碍物的距离信息,STM32控制器接收这些数据并进行处理,根据预设阈值判断危险等级,例如将距离分为安全、警告和危险等级别,以支持后续的导航决策。

导航提示通过振动马达模块和JL-03语音模块实现,STM32根据危险等级控制振动马达产生不同强度的触觉反馈,同时驱动语音模块播放相应的语音警告或指引,为用户提供触觉和听觉的双重导航辅助。

ESP8266-01S Wi-Fi模块负责与华为云物联网平台建立连接,STM32将通过Wi-Fi模块上传行走轨迹数据和遇障事件信息到云服务器,实现数据的远程存储和监控,为后续分析提供基础。

QT上位机软件从华为云平台获取数据,并显示行走路径的地图可视化以及遇障统计信息,帮助用户或管理员查看历史行程和障碍情况,提升系统的实用性和可管理性。

硬件方面,所有模块通过洞洞板焊接的接口电路和杜邦线连接,确保各组件之间的可靠通信和系统整体稳定性。

八、系统功能总结

功能需求 实现方式
实时采集前方障碍物距离信息并进行危险等级判断 HC-SR04超声波传感器检测距离,STM32F103C8T6处理器进行判断
通过振动马达和语音模块提供多模式导航提示 振动马达模块提供触觉反馈,JL-03语音模块提供语音提示
行走轨迹和遇障数据上传至华为云平台 ESP8266-01S Wi-Fi模块实现数据上传
QT上位机显示行走路径地图和遇障统计信息 数据通过Wi-Fi传输,QT软件解析并显示
服务器采用华为云物联网服务器 使用华为云物联网平台,通过Wi-Fi模块连接

九、设计的各个功能模块描述

STM32F103C8T6最小系统核心板作为主控制器,负责整个系统的协调与控制。它通过GPIO、UART等接口与各模块通信,实时处理传感器数据,执行危险等级判断算法,并根据结果控制振动马达和语音模块提供相应的导航提示。主控制器还管理数据采集、处理和上传流程,确保系统稳定运行。

HC-SR04超声波传感器模块用于实时采集前方障碍物的距离信息。该传感器通过发射和接收超声波来测量距离,并将数据通过数字信号传送给STM32主控制器。主控制器根据距离值进行危险等级判断,例如近距离障碍物会触发高级别警告。

JL-03语音模块通过串口与STM32主控制器连接,用于提供语音导航提示。当主控制器判断出危险情况时,它会发送指令给语音模块,播放预录的语音警告,如"前方有障碍物",帮助用户感知环境并做出避障反应。

振动马达模块由STM32主控制器通过PWM或数字输出控制,提供触觉导航反馈。根据障碍物的距离和危险等级,主控制器调节马达的振动强度和模式,例如短促振动表示轻度警告,持续强振动表示紧急危险,使用户能通过触觉直观感知障碍物信息。

ESP8266-01S Wi-Fi模块通过UART与STM32主控制器通信,负责实现与华为云物联网平台的连接。该模块配置为STA模式,连接到无线网络后,使用MQTT协议将系统采集的行走轨迹和遇障数据上传到云平台,实现远程数据存储和监控。

硬件连接方面,使用洞洞板焊接传感器接口电路,包括电源管理、信号调理和模块接口,确保各模块稳定供电和信号传输。杜邦线用于临时连接各模块,便于系统调试和原型开发,提高灵活性和可靠性。

十、上位机代码设计

10.1 Qt开发环境安装

Qt的中文官网: <www.qt.io/zh-cn/

>

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

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

安装的时候,第一个复选框里的编译器可以全选,直接点击下一步继续安装。

选择编译器: (一定要看清楚了)

10.2 新建上位机工程

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

【1】新建工程

【2】设置项目的名称。

【3】选择编译系统

【4】选择默认继承的类

【5】选择编译器

【6】点击完成

【7】工程创建完成

10.3 切换编译器

在左下角是可以切换编译器的。 可以选择用什么样的编译器编译程序。

目前新建工程的时候选择了2种编译器。 一种是mingw32这个编译Windows下运行的程序。 一种是Android编译器,可以生成Android手机APP。

不过要注意:Android的编译器需要配置一些环境才可以正常使用,这个大家可以网上找找教程配置一下就行了。

windows的编译器就没有这么麻烦,安装好Qt就可以编译使用。

下面我这里就选择的 mingw32这个编译器,编译Windows下运行的程序。

10.4 编译测试功能

创建完毕之后,编译测试一下功能是否OK。

点击左下角的绿色三角形按钮

正常运行就可以看到弹出一个白色的框框。这就表示工程环境没有问题了。 接下来就可以放心的设计界面了。

10.5 设计UI界面与工程配置

【1】打开UI文件

打开默认的界面如下:

【2】开始设计界面

根据自己需求设计界面。

10.6 设计代码

【1】获取token

调用华为云的API都需要填token参数,先看帮助文章,了解如何获取token。

帮助文档:support.huaweicloud.com/api-iam/iam...

根据帮助文档,写完成下面代码编写:

这段代码的功能是通过华为云IAM服务获取Token,以便后续调用华为云API时使用。以下是代码的详细功能解释:


1. 设置功能标识
cpp 复制代码
function_select = 3;
  • function_select是一个标识变量,用于区分当前请求的功能类型。这里设置为3,表示当前请求是获取Token。

2. 构造请求URL
cpp 复制代码
QString requestUrl;
QNetworkRequest request;

// 设置请求地址
QUrl url;

// 获取token请求地址
requestUrl = QString("https://iam.%1.myhuaweicloud.com/v3/auth/tokens")
             .arg(SERVER_ID);
  • 构造获取Token的请求URL,URL格式为:https://iam.{SERVER_ID}.myhuaweicloud.com/v3/auth/tokens
  • SERVER_ID是华为云服务器的区域ID(如cn-north-1),通过QStringarg方法动态替换到URL中。

3. 设置请求头
cpp 复制代码
// 设置数据提交格式
request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json;charset=UTF-8"));
  • 设置HTTP请求头,指定请求体的数据格式为application/json;charset=UTF-8,表示发送的数据是JSON格式。

4. 设置请求URL
cpp 复制代码
// 构造请求
url.setUrl(requestUrl);
request.setUrl(url);
  • 将构造好的URL设置到QUrl对象中,并将其绑定到QNetworkRequest对象。

5. 构造请求体
cpp 复制代码
QString text = QString("{\"auth\":{\"identity\":{\"methods\":[\"password\"],\"password\":"
"{\"user\":{\"domain\": {"
"\"name\":\"%1\"},\"name\": \"%2\",\"password\": \"%3\"}}},"
"\"scope\":{\"project\":{\"name\":\"%4\"}}}}")
        .arg(MAIN_USER)
        .arg(IAM_USER)
        .arg(IAM_PASSWORD)
        .arg(SERVER_ID);
  • 构造JSON格式的请求体,用于向华为云IAM服务请求Token。请求体包含以下字段:
    • auth:认证信息。
      • identity:身份信息。
        • methods:认证方法,这里使用密码认证(password)。
        • password:密码认证的具体信息。
          • user:用户信息。
            • domain:用户所属的域名。
              • name:域名名称(MAIN_USER)。
            • name:用户名(IAM_USER)。
            • password:用户密码(IAM_PASSWORD)。
      • scope:请求的范围。
        • project:项目信息。
          • name:项目名称(SERVER_ID)。
  • 使用QStringarg方法动态替换请求体中的变量(如MAIN_USERIAM_USER等)。

6. 发送HTTP POST请求
cpp 复制代码
// 发送请求
manager->post(request, text.toUtf8());
  • 使用QNetworkAccessManagerpost方法发送HTTP POST请求。
  • request是构造好的请求对象,text.toUtf8()是将请求体转换为UTF-8编码的字节数组。

7. 总结

这段代码的核心功能是:

  1. 构造获取Token的HTTP请求:包括请求URL、请求头和请求体。
  2. 发送请求 :通过QNetworkAccessManager发送POST请求,向华为云IAM服务请求Token。
  3. Token的作用:获取到的Token将用于后续调用华为云API时的身份验证。

通过这段代码,QT上位机能够获取华为云的Token,为后续的设备数据查询、控制等操作提供身份验证支持。

【2】获取影子数据

前面章节介绍了影子数据获取接口。下面是对应编写的代码:

这段代码的功能是向华为云IoT平台查询设备的属性信息(设备状态)。以下是对代码的详细功能含义解释:

代码功能含义解释:

(1)function_select = 0;

  • 这行代码设置function_select为0,表示当前操作是查询设备属性。这个变量用于标识不同的操作,可以帮助后续根据不同的操作类型执行不同的处理逻辑。

(2)QString requestUrl; QNetworkRequest request;

  • requestUrl:用于存储请求的URL地址,后续将构造一个用于查询设备属性的URL。
  • request:用来封装HTTP请求的对象,包含请求的所有信息,包括请求头、URL等。

(3)QUrl url;

  • url:用于存储并处理请求的URL对象,确保请求使用正确的地址。

(4)构造请求URL:

cpp 复制代码
requestUrl = QString("https://%1:443/v5/iot/%2/devices/%3/shadow")
             .arg(IP_ADDR)
             .arg(PROJECT_ID)
             .arg(device_id);

这行代码构建了一个URL,用于查询设备的状态(属性)。URL包括了:

IP_ADDR:华为云IoT平台的IP地址或域名。

PROJECT_ID:项目的ID,用于区分不同的项目。

device_id:设备的唯一标识符,用于查询指定设备的属性。

:443指定使用HTTPS协议(端口443)进行安全通信。

最终构造出的URL形如:https://<IP_ADDR>:443/v5/iot/<PROJECT_ID>/devices/<device_id>/shadow,这是查询设备状态的API接口。

(1)request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json"));

  • 设置请求头的内容类型为application/json,表明请求体中的数据格式是JSON。

(2)request.setRawHeader("X-Auth-Token", Token);

  • 设置请求头中的X-Auth-Token字段,传递身份验证令牌(Token)。这个令牌用于验证请求的合法性,确保只有授权的用户可以查询设备的状态。

(3)url.setUrl(requestUrl);

  • 将前面构建好的请求URL赋值给url对象,确保后续的请求使用正确的URL。

(4)request.setUrl(url);

  • url对象设置到request对象中,准备发送请求。

(5)manager->get(request);

  • 使用QNetworkAccessManagerget方法发送GET请求,查询设备的属性。request中包含了URL、请求头以及Token等信息,服务器接收到请求后将返回设备的属性信息(如设备状态、属性值等)。

代码整体功能:

该代码实现了通过华为云IoT平台的API查询设备的属性信息。具体步骤包括:

  1. 构造查询设备属性的API请求URL。
  2. 设置请求头,指定数据格式为JSON,并传递Token进行身份验证。
  3. 使用QNetworkAccessManager发送GET请求,向服务器请求设备的状态数据。
  4. 服务器将返回设备的属性数据,供后续处理。

总结:

这段代码的功能是向华为云IoT平台查询指定设备的属性信息,并通过GET请求将设备的状态返回给客户端。通过Token进行身份验证,确保请求的合法性。

【3】解析数据更新界面

根据接口返回的数据更新界面。

【4】判断设备是否离线

这段代码用于判断设备是否离线。通过获取设备上传到服务器数据的时间与本地的系统时间差进行对比。

这段代码的核心功能是通过比较设备上传数据的时间和本地系统时间来判断设备是否处于离线状态,以下是其详细解释:


(1)功能分析

显示最新更新时间

cpp 复制代码
ui->label_update_time->setText("最新时间:" + update_time);

将设备上传的最新时间 update_time 显示在界面上的 label_update_time 控件中,格式为 最新时间:yyyy-MM-dd HH:mm:ss

方便用户了解设备数据的最近更新时间。

获取本地当前时间

cpp 复制代码
QDateTime currentDateTime = QDateTime::currentDateTime();

使用 QDateTime::currentDateTime() 获取系统当前时间,作为对比基准。

计算时间差

cpp 复制代码
qint64 secondsDiff = currentDateTime.secsTo(dateTime);

secsTo: 计算 currentDateTime 和设备上传时间 dateTime 之间的时间差(单位:秒)。

dateTime 是通过解析 JSON 数据提取到的设备数据上传时间,并已转换为本地时间格式。

判断设备状态

cpp 复制代码
if (qAbs(secondsDiff) >= 5 * 60)

使用 qAbs 获取时间差的绝对值。

如果时间差超过 5 分钟(300秒),表示设备长时间未上传数据,判定为"离线"。


(2)离线处理

更新状态显示

cpp 复制代码
ui->label_dev_state->setText("设备状态:离线");

在界面 label_dev_state 控件中显示设备当前状态为"离线"。


(3)在线处理

状态更新ui->label_dev_state->setText("设备状态:在线");如果时间差小于 5 分钟,显示"设备状态:在线"。

【5】获取设备最新数据上传时间

这是解析华为云API接口返回的数据,解析出来里面设备数据的时间,进行显示。

这段代码的主要作用是解析华为云 API 返回的 JSON 数据中的设备数据时间字段,转换为本地时间格式,并最终以用户友好的标准格式输出到界面。


(1)详细代码解析

(1)提取时间字段

cpp 复制代码
QString event_time = obj3.take("event_time").toString();
qDebug() << "event_time:" << event_time;

obj3.take("event_time"):从 JSON 数据中的 reported 对象提取 event_time 字段,值为一个字符串,表示设备上传数据的时间。

toString():将提取的字段值转换为 QString 类型,便于后续操作。

调试输出:使用 qDebug() 输出提取的时间值,例如:20231121T120530Z

2. 转换为 QDateTime 对象

cpp 复制代码
QDateTime dateTime = QDateTime::fromString(event_time, "yyyyMMddTHHmmssZ");

QDateTime::fromString:

使用指定格式解析 event_time 字符串为 QDateTime 对象。

格式说明:

  • yyyyMMdd: 年、月、日(如 20231121)。

  • T: 时间部分的分隔符(固定为 T)。

  • HHmmss: 时、分、秒(如 120530)。

  • Z: 表示时间是 UTC 时间。

  • 如果时间字符串格式不匹配,会返回一个无效的 QDateTime 对象。

3. 转换时区到本地时间

cpp 复制代码
dateTime.setTimeSpec(Qt::UTC);
dateTime = dateTime.toLocalTime();

setTimeSpec(Qt::UTC):

  • 明确告知 dateTime 对象,当前时间是 UTC 时间。
  • 确保时间转换准确,避免因为默认时区不明确导致的误差。

toLocalTime():

  • 将时间从 UTC 转换为本地时区时间,例如中国标准时间(CST, UTC+8)。

4. 格式化输出为标准时间字符串

cpp 复制代码
QString update_time = dateTime.toString("yyyy-MM-dd HH:mm:ss");

toString():将 QDateTime 转换为指定格式的字符串。

格式说明:

  • yyyy-MM-dd: 年-月-日。
  • HH:mm:ss: 小时:分钟:秒。

示例结果:2023-11-21 20:05:30

用户显示友好性:转换后的格式易读,符合国际通用的日期时间表示规范。


(2)代码运行效果

假设 API 返回的时间字段值为 20231121T120530Z

转换流程:

  1. 解析为 QDateTime 对象:2023-11-21 12:05:30 (UTC);
  2. 转换为本地时间:2023-11-21 20:05:30 (CST)
  3. 格式化输出:"2023-11-21 20:05:30"

输出到界面时,显示为:

makefile 复制代码
最新时间: 2023-11-21 20:05:30

10.5 编译Windows上位机

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

10.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文件。

cpp 复制代码
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

10.7 上位机

cpp 复制代码
#include <QtWidgets/QApplication>
#include <QtWidgets/QMainWindow>
#include <QtCharts/QChartView>
#include <QtCharts/QLineSeries>
#include <QtCharts/QScatterSeries>
#include <QtNetwork/QTcpSocket>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonArray>
#include <QtCore/QTimer>
#include <QtWidgets/QLabel>
#include <QtWidgets/QVBoxLayout>
#include <QtWidgets/QHBoxLayout>
#include <QtWidgets/QTableWidget>

QT_CHARTS_USE_NAMESPACE

class ObstacleMap : public QChartView {
    Q_OBJECT
public:
    ObstacleMap(QWidget* parent = nullptr) : QChartView(parent) {
        series = new QLineSeries();
        obstacles = new QScatterSeries();
        obstacles->setMarkerSize(10);
        obstacles->setColor(Qt::red);
        
        chart = new QChart();
        chart->addSeries(series);
        chart->addSeries(obstacles);
        chart->createDefaultAxes();
        chart->setTitle("行走路径与障碍物分布");
        setChart(chart);
    }
    
    void addPoint(double x, double y, bool isObstacle) {
        if (isObstacle) {
            obstacles->append(x, y);
        } else {
            series->append(x, y);
        }
        chart->axes(Qt::Horizontal).first()->setRange(0, x + 1);
        chart->axes(Qt::Vertical).first()->setRange(0, y + 1);
    }

private:
    QChart* chart;
    QLineSeries* series;
    QScatterSeries* obstacles;
};

class MainWindow : public QMainWindow {
    Q_OBJECT
public:
    MainWindow(QWidget* parent = nullptr) : QMainWindow(parent) {
        setupUI();
        setupNetwork();
        setupTimer();
    }

private slots:
    void onDataReceived() {
        while (socket->canReadLine()) {
            QByteArray data = socket->readLine();
            QJsonDocument doc = QJsonDocument::fromJson(data);
            if (!doc.isNull()) {
                processData(doc.object());
            }
        }
    }
    
    void reconnect() {
        if (socket->state() != QTcpSocket::ConnectedState) {
            socket->connectToHost("华为云IP地址", 1883); // 替换实际IP
        }
    }

private:
    void setupUI() {
        QWidget* central = new QWidget(this);
        QHBoxLayout* layout = new QHBoxLayout(central);
        
        // 地图显示
        map = new ObstacleMap();
        layout->addWidget(map, 2);
        
        // 右侧统计面板
        QWidget* rightPanel = new QWidget();
        QVBoxLayout* rightLayout = new QVBoxLayout(rightPanel);
        
        statsTable = new QTableWidget();
        statsTable->setColumnCount(3);
        statsTable->setHorizontalHeaderLabels({"时间", "位置", "距离(cm)"});
        rightLayout->addWidget(new QLabel("障碍物统计"));
        rightLayout->addWidget(statsTable);
        
        statusLabel = new QLabel("未连接");
        rightLayout->addWidget(statusLabel);
        
        layout->addWidget(rightPanel, 1);
        setCentralWidget(central);
        resize(1200, 600);
    }

    void setupNetwork() {
        socket = new QTcpSocket(this);
        connect(socket, &QTcpSocket::readyRead, this, &MainWindow::onDataReceived);
        connect(socket, &QTcpSocket::stateChanged, [this]() {
            statusLabel->setText(socket->state() == QTcpSocket::ConnectedState ? 
                                "已连接" : "断开连接");
        });
        reconnect();
    }

    void setupTimer() {
        QTimer* timer = new QTimer(this);
        connect(timer, &QTimer::timeout, this, &MainWindow::reconnect);
        timer->start(5000);
    }

    void processData(const QJsonObject& obj) {
        double x = obj["x"].toDouble();
        double y = obj["y"].toDouble();
        bool obstacle = obj["obstacle"].toBool();
        double distance = obj["distance"].toDouble();
        
        map->addPoint(x, y, obstacle);
        
        if (obstacle) {
            int row = statsTable->rowCount();
            statsTable->insertRow(row);
            statsTable->setItem(row, 0, new QTableWidgetItem(QDateTime::currentDateTime().toString()));
            statsTable->setItem(row, 1, new QTableWidgetItem(QString("(%1,%2)").arg(x).arg(y)));
            statsTable->setItem(row, 2, new QTableWidgetItem(QString::number(distance)));
        }
    }

    QTcpSocket* socket;
    ObstacleMap* map;
    QTableWidget* statsTable;
    QLabel* statusLabel;
};

int main(int argc, char* argv[]) {
    QApplication app(argc, argv);
    MainWindow window;
    window.show();
    return app.exec();
}

#include "main.moc"

注意:此代码需要以下配置:

  1. 在.pro文件中添加:
makefile 复制代码
QT += charts network
  1. 需要替换实际的华为云服务器IP地址 3. 假设设备上传的数据格式为JSON:
json 复制代码
{"x": 1.5, "y": 2.3, "obstacle": true, "distance": 25.4}
  1. 实际部署时需要添加错误处理和身份验证机制
  2. 需要根据实际通信协议调整数据解析逻辑

十一、模块代码设计

c 复制代码
#include "stm32f10x.h"

// 定义引脚和端口
#define TRIG_PIN GPIO_Pin_0   // PA0 as Trig
#define ECHO_PIN GPIO_Pin_1   // PA1 as Echo
#define VIBRATE_PIN GPIO_Pin_2 // PA2 as Vibrate Motor control
#define ESP8266_TX_PIN GPIO_Pin_9  // PA9 as USART1 TX
#define ESP8266_RX_PIN GPIO_Pin_10 // PA10 as USART1 RX
#define VOICE_TX_PIN GPIO_Pin_2    // PA2 for Voice module TX? Wait, conflict with VIBRATE_PIN? Let's reassign.

// Reassign pins to avoid conflict: Use PA0 for Trig, PA1 for Echo, PA2 for Vibrate, PA9 for USART1 TX (ESP8266), PA10 for USART1 RX (ESP8266)
// For Voice module, let's use USART2: PA2 as TX? But PA2 is used for Vibrate. So, use another pin. Alternatively, use software UART for voice if needed.
// Since JL-03语音模块可能通过UART, we need to assign a UART. For simplicity, let's use USART2 for voice: PA2 as TX (voice), PA3 as RX (voice). But PA2 is already Vibrate? Conflict.

// Revised pin assignment:
// - HC-SR04: Trig PA0, Echo PA1
// - Vibrate Motor: PA2
// - ESP8266: USART1, PA9 (TX), PA10 (RX)
// - Voice module: USART2, PA2 (TX) -- but PA2 is used for Vibrate! So, we cannot use PA2 for both. Let's change Vibrate to PA3 and Voice TX to PA2.
// Actually, for voice module, we only need to send commands, so TX only. We can use PA2 for Voice TX, and move Vibrate to another pin, say PA3.

// Final pin assignment:
// - Trig: PA0
// - Echo: PA1
// - Vibrate: PA3
// - ESP8266 TX: PA9 (USART1)
// - ESP8266 RX: PA10 (USART1)
// - Voice TX: PA2 (USART2) -- we'll configure USART2 for voice commands.

// Define ports
#define TRIG_PORT GPIOA
#define ECHO_PORT GPIOA
#define VIBRATE_PORT GPIOA
#define ESP8266_TX_PORT GPIOA
#define ESP8266_RX_PORT GPIOA
#define VOICE_TX_PORT GPIOA

// USART definitions
#define USART1_DR_BASE (0x40013804) // USART1 Data register address
#define USART2_DR_BASE (0x40004404) // USART2 Data register address

// Timer for ultrasound
#define TIM2_CNT_BASE (0x40000024) // TIM2 counter register address

// Variables
volatile uint32_t distance_cm = 0;
volatile uint8_t obstacle_level = 0; // 0: safe, 1: warning, 2: danger

// Function prototypes
void SystemInit(void);
void GPIO_Init(void);
void TIM2_Init(void);
void USART1_Init(void);
void USART2_Init(void);
void HC_SR04_Start(void);
uint32_t HC_SR04_GetDistance(void);
void Vibrate_Motor_Control(uint8_t state);
void Voice_Play_Warning(uint8_t level);
void ESP8266_SendData(uint32_t dist, uint8_t level);
void Delay_ms(uint32_t nTime);

int main(void) {
    SystemInit();
    GPIO_Init();
    TIM2_Init();
    USART1_Init(); // For ESP8266
    USART2_Init(); // For voice module

    while (1) {
        // Start measurement
        HC_SR04_Start();
        distance_cm = HC_SR04_GetDistance();

        // Determine obstacle level
        if (distance_cm > 100) {
            obstacle_level = 0; // safe
        } else if (distance_cm > 50) {
            obstacle_level = 1; // warning
        } else {
            obstacle_level = 2; // danger
        }

        // Provide feedback based on level
        if (obstacle_level == 1) {
            Vibrate_Motor_Control(1); // Vibrate on
            Voice_Play_Warning(1);    // Play warning voice
            Delay_ms(500);
            Vibrate_Motor_Control(0);
        } else if (obstacle_level == 2) {
            Vibrate_Motor_Control(1); // Continuous vibrate for danger
            Voice_Play_Warning(2);
            // Keep vibrating until next cycle
        } else {
            Vibrate_Motor_Control(0); // Off
        }

        // Send data to Huawei Cloud via ESP8266
        ESP8266_SendData(distance_cm, obstacle_level);

        Delay_ms(1000); // Wait for 1 second before next measurement
    }
}

void SystemInit(void) {
    // Enable clocks for GPIOA, USART1, USART2, TIM2
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN | RCC_APB2ENR_AFIOEN;
    RCC->APB1ENR |= RCC_APB1ENR_USART2EN | RCC_APB1ENR_TIM2EN;

    // Set system clock to 72MHz (assumed for delay functions)
    // Clock configuration is complex; assuming default HSI for simplicity
}

void GPIO_Init(void) {
    // PA0 as output for Trig
    TRIG_PORT->CRL &= ~(0xF << (0 * 4)); // Clear CNF0 and MODE0
    TRIG_PORT->CRL |= (0x3 << (0 * 4));  // Output push-pull, max speed 50MHz

    // PA1 as input for Echo
    ECHO_PORT->CRL &= ~(0xF << (1 * 4));
    ECHO_PORT->CRL |= (0x4 << (1 * 4));  // Input with pull-up/pull-down? Actually, for floating input, but often set to input without pull
    // Alternatively, set to input floating: CNF=01, MODE=00 -> 0x4 for pin1? Let's calculate: for PA1, bit4-7: CNF1[1:0] and MODE1[1:0]
    // We want input floating: CNF=01, MODE=00 -> binary 0100 = 0x4
    // So correct.

    // PA3 as output for Vibrate Motor
    VIBRATE_PORT->CRL &= ~(0xF << (3 * 4));
    VIBRATE_PORT->CRL |= (0x3 << (3 * 4)); // Output push-pull, max speed

    // PA9 as USART1 TX (alternate function push-pull)
    ESP8266_TX_PORT->CRH &= ~(0xF << ( (9-8) * 4 )); // PA9 is in CRH, bit4-7 for pin9? Pin9: bits 4-7 in CRH? Actually, for pin9, it's bits 4-7: (9-8)=1, so index 1 in CRH, which is bits 4-7.
    ESP8266_TX_PORT->CRH |= (0xB << (4 * 1)); // CNF9=10 (alternate output push-pull), MODE9=11 (max speed) -> 1011 for bits 4-7? Wait, for pin9: CRH bits 4-7: CNF9[1:0] and MODE9[1:0]. So MODE9=11 (0x3), CNF9=10 (0x2) -> combined 0xB for bits 4-7? Actually, 0xB is 1011, which is CNF9[1]=1, CNF9[0]=0, MODE9[1]=1, MODE9[0]=1 -> correct for alternate output push-pull with max speed.

    // PA10 as USART1 RX (input floating)
    ESP8266_RX_PORT->CRH &= ~(0xF << ( (10-8) * 4 )); // Pin10: bits 8-11 in CRH? (10-8)=2, so index 2, bits 8-11.
    ESP8266_RX_PORT->CRH |= (0x4 << (4 * 2)); // Input floating: CNF10=01, MODE10=00 -> 0100 for bits 8-11? 0x4 shifted left by 8 bits? Wait, for pin10, it's bits 8-11: we set to 0x4 meaning CNF10[1:0]=01, MODE10[1:0]=00.

    // PA2 as USART2 TX (alternate function push-pull)
    VOICE_TX_PORT->CRL &= ~(0xF << (2 * 4)); // PA2 is in CRL, bits 8-11
    VOICE_TX_PORT->CRL |= (0xB << (2 * 4)); // Similar to above: alternate output push-pull, max speed
}

void TIM2_Init(void) {
    // TIM2 for ultrasound measurement
    TIM2->PSC = 71; // Prescaler: 72MHz / (71+1) = 1MHz -> 1us per count
    TIM2->ARR = 0xFFFF; // Auto-reload to max
    TIM2->CR1 = 0; // Disable initially
}

void USART1_Init(void) {
    // USART1 for ESP8266: 115200 baud, 8 data bits, no parity, 1 stop bit
    USART1->BRR = 72000000 / 115200; // Calculate BRR: for 72MHz, 115200 baud -> 72000000/115200 = 625 -> 0x271
    USART1->CR1 |= USART_CR1_TE | USART_CR1_RE | USART_CR1_UE; // Enable TX, RX, and USART
}

void USART2_Init(void) {
    // USART2 for voice module: typically 9600 baud
    USART2->BRR = 72000000 / 9600; // 72MHz / 9600 = 7500 -> 0x1D4C
    USART2->CR1 |= USART_CR1_TE | USART_CR1_UE; // Enable TX and USART
}

void HC_SR04_Start(void) {
    // Send Trig pulse for 10us
    TRIG_PORT->BSRR = TRIG_PIN; // Set Trig high
    Delay_ms(0.01); // Delay 10us -- but Delay_ms is for ms. We need us delay.
    // Instead, use a loop for precise 10us delay.
    for (volatile int i = 0; i < 72; i++); // Roughly 10us at 72MHz
    TRIG_PORT->BRR = TRIG_PIN; // Set Trig low
}

uint32_t HC_SR04_GetDistance(void) {
    while (ECHO_PORT->IDR & ECHO_PIN); // Wait for Echo to be low initially
    while (!(ECHO_PORT->IDR & ECHO_PIN)); // Wait for Echo to go high

    TIM2->CNT = 0; // Reset timer counter
    TIM2->CR1 |= TIM_CR1_CEN; // Start timer

    while (ECHO_PORT->IDR & ECHO_PIN); // Wait for Echo to go low
    TIM2->CR1 &= ~TIM_CR1_CEN; // Stop timer

    uint32_t time_us = TIM2->CNT; // Get time in us
    uint32_t distance = time_us / 58; // Convert to cm (time in us / 58)

    return distance;
}

void Vibrate_Motor_Control(uint8_t state) {
    if (state) {
        VIBRATE_PORT->BSRR = VIBRATE_PIN; // Set high
    } else {
        VIBRATE_PORT->BRR = VIBRATE_PIN; // Set low
    }
}

void Voice_Play_Warning(uint8_t level) {
    // Send command to voice module via USART2
    // Assume voice module has pre-recorded messages for level 1 and 2
    // Example command: for level 1, send "PLAY01" or similar; adjust based on module protocol
    char *command;
    if (level == 1) {
        command = "PLAY01\r\n"; // Example command for warning
    } else if (level == 2) {
        command = "PLAY02\r\n"; // Example command for danger
    } else {
        return;
    }

    while (*command) {
        while (!(USART2->SR & USART_SR_TXE)); // Wait for TX buffer empty
        USART2->DR = *command++;
    }
}

void ESP8266_SendData(uint32_t dist, uint8_t level) {
    // Send data to ESP8266 via USART1 using AT commands
    // Example: connect to Huawei云 and send data. This is simplified.
    // First, ensure ESP8266 is connected. We assume it's pre-configured or we send AT commands.
    // For simplicity, we'll send a raw string with data.
    char buffer[50];
    sprintf(buffer, "AT+发送数据=%lu,%d\r\n", dist, level); // This is placeholder; adjust based on actual protocol

    char *p = buffer;
    while (*p) {
        while (!(USART1->SR & USART_SR_TXE));
        USART1->DR = *p++;
    }
}

void Delay_ms(uint32_t nTime) {
    // Simple delay function using SysTick or loop; for simplicity, use loop
    for (volatile uint32_t i = 0; i < nTime * 10000; i++);
}

此代码基于STM32F103C8T6使用寄存器方式开发,实现了超声波测距、振动马达控制、语音提示和Wi-Fi通信功能。请注意,实际部署时需要根据硬件连接和华为云物联网服务器的具体协议调整AT命令和数据格式。代码中的延迟函数和通信协议仅为示例,可能需要优化。

十二、项目核心代码

c 复制代码
#include "stm32f10x.h"

// 假设其他模块已经编写好,以下是函数原型
extern void Ultrasonic_Init(void);
extern uint32_t Ultrasonic_GetDistance(void);
extern void Motor_Control(uint8_t state);
extern void Voice_Play(uint8_t message);
extern void WiFi_Init(void);
extern void WiFi_SendData(float distance, uint8_t danger_level);

// 定义危险等级
#define DANGER_LEVEL_SAFE     0
#define DANGER_LEVEL_WARNING  1
#define DANGER_LEVEL_DANGER   2

// 定义引脚
#define ULTRASONIC_TRIG_PIN   GPIO_Pin_0   // PA0
#define ULTRASONIC_ECHO_PIN   GPIO_Pin_1   // PA1
#define MOTOR_PIN             GPIO_Pin_13  // PC13

// 系统时钟初始化函数
void RCC_Configuration(void)
{
    // 启用GPIOA、GPIOC、USART1、USART2、AFIO时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_USART1EN | RCC_APB2ENR_AFIOEN;
    RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
    
    // 设置系统时钟为72MHz(假设使用8MHz HSE)
    // 注意:这里简化了时钟配置,实际可能需要更详细的设置
    RCC->CR |= RCC_CR_HSEON; // 开启HSE
    while(!(RCC->CR & RCC_CR_HSERDY)); // 等待HSE就绪
    RCC->CFGR |= RCC_CFGR_PLLSRC; // PLL源为HSE
    RCC->CFGR |= RCC_CFGR_PLLMULL9; // PLL倍频为9倍,8MHz * 9 = 72MHz
    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); // 等待切换完成
}

// GPIO初始化函数
void GPIO_Configuration(void)
{
    // 配置超声波触发引脚(PA0)为推挽输出
    GPIOA->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0);
    GPIOA->CRL |= GPIO_CRL_MODE0_0; // 输出模式,最大速度10MHz
    
    // 配置超声波回波引脚(PA1)为浮空输入
    GPIOA->CRL &= ~(GPIO_CRL_MODE1 | GPIO_CRL_CNF1);
    GPIOA->CRL |= GPIO_CRL_CNF1_0; // 浮空输入
    
    // 配置振动马达引脚(PC13)为推挽输出
    GPIOC->CRH &= ~(GPIO_CRH_MODE13 | GPIO_CRH_CNF13);
    GPIOC->CRH |= GPIO_CRH_MODE13_0; // 输出模式,最大速度10MHz
    
    // 配置USART1引脚(PA9为TX, PA10为RX)
    GPIOA->CRH &= ~(GPIO_CRH_CNF9 | GPIO_CRH_CNF10);
    GPIOA->CRH |= GPIO_CRH_CNF9_1 | GPIO_CRH_CNF10_0; // PA9: 复用推挽输出, PA10: 浮空输入
    GPIOA->CRH |= GPIO_CRH_MODE9_0 | GPIO_CRH_MODE9_1; // PA9: 输出模式,最大速度50MHz
    
    // 配置USART2引脚(PA2为TX, PA3为RX)
    GPIOA->CRL &= ~(GPIO_CRL_CNF2 | GPIO_CRL_CNF3);
    GPIOA->CRL |= GPIO_CRL_CNF2_1 | GPIO_CRL_CNF3_0; // PA2: 复用推挽输出, PA3: 浮空输入
    GPIOA->CRL |= GPIO_CRL_MODE2_0 | GPIO_CRL_MODE2_1; // PA2: 输出模式,最大速度50MHz
}

// USART初始化函数
void USART_Configuration(void)
{
    // USART1初始化 for 语音模块
    USART1->BRR = 72000000 / 9600; // 设置波特率为9600 (72MHz / 9600)
    USART1->CR1 |= USART_CR1_UE | USART_CR1_TE | USART_CR1_RE; // 启用USART, 发送和接收
    
    // USART2初始化 for Wi-Fi模块
    USART2->BRR = 72000000 / 115200; // 设置波特率为115200
    USART2->CR1 |= USART_CR1_UE | USART_CR1_TE | USART_CR1_RE; // 启用USART, 发送和接收
}

// 定时器初始化 for 超声波测量(假设使用定时器2)
void TIM_Configuration(void)
{
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // 启用TIM2时钟
    TIM2->PSC = 71; // 预分频器,72MHz / 72 = 1MHz
    TIM2->ARR = 0xFFFF; // 自动重载值
    TIM2->CR1 |= TIM_CR1_CEN; // 启用定时器
}

// 主函数
int main(void)
{
    // 初始化系统
    RCC_Configuration();
    GPIO_Configuration();
    USART_Configuration();
    TIM_Configuration();
    Ultrasonic_Init();
    WiFi_Init();
    
    // 变量定义
    uint32_t distance = 0;
    uint8_t danger_level = DANGER_LEVEL_SAFE;
    
    while(1)
    {
        // 获取超声波距离
        distance = Ultrasonic_GetDistance();
        
        // 判断危险等级
        if(distance > 100) // 距离大于100cm,安全
        {
            danger_level = DANGER_LEVEL_SAFE;
            Motor_Control(0); // 关闭振动
            Voice_Play(0);    // 播放安全语音
        }
        else if(distance > 50) // 距离50-100cm,警告
        {
            danger_level = DANGER_LEVEL_WARNING;
            Motor_Control(1); // 开启振动(慢速)
            Voice_Play(1);    // 播放警告语音
        }
        else // 距离小于50cm,危险
        {
            danger_level = DANGER_LEVEL_DANGER;
            Motor_Control(2); // 开启振动(快速)
            Voice_Play(2);    // 播放危险语音
        }
        
        // 上传数据到华为云
        WiFi_SendData((float)distance, danger_level);
        
        // 简单延时,实际可能需要使用定时器或中断
        for(volatile int i = 0; i < 1000000; i++);
    }
}

十三、总结

本系统基于STM32微控制器设计,旨在为盲人提供智能导航辅助,通过实时采集前方障碍物距离信息并进行危险等级判断,确保用户安全出行。系统集成多模式反馈机制,包括振动和语音提示,以增强用户体验和导航效果。

硬件组成以STM32F103C8T6核心板为主控制器,配合HC-SR04超声波传感器实现障碍物检测,JL-03语音模块和振动马达模块提供导航反馈,ESP8266-01S Wi-Fi模块负责与华为云平台通信,所有模块通过洞洞板焊接和杜邦线连接,确保系统稳定性和灵活性。

系统还将行走轨迹和遇障数据实时上传至华为云物联网服务器,并通过QT上位机显示行走路径地图和遇障统计信息,实现了数据的远程监控和分析,提升了系统的智能化和实用性。

总体而言,该设计不仅有效提升了盲人出行的安全性和独立性,还体现了物联网技术在辅助设备中的创新应用,具有显著的社会价值和推广潜力。

相关推荐
豌豆花下猫5 小时前
Python 潮流周刊#118:Python 异步为何不够流行?(摘要)
后端·python·ai
秋难降6 小时前
SQL 索引突然 “罢工”?快来看看为什么
数据库·后端·sql
Access开发易登软件7 小时前
Access开发导出PDF的N种姿势,你get了吗?
后端·低代码·pdf·excel·vba·access·access开发
中国胖子风清扬8 小时前
Rust 序列化技术全解析:从基础到实战
开发语言·c++·spring boot·vscode·后端·中间件·rust
bobz9658 小时前
分析 docker.service 和 docker.socket 这两个服务各自的作用
后端
野犬寒鸦8 小时前
力扣hot100:旋转图像(48)(详细图解以及核心思路剖析)
java·数据结构·后端·算法·leetcode
phiilo9 小时前
golang 设置进程退出时kill所有子进程
后端
花花无缺9 小时前
python自动化-pytest-用例发现规则和要求
后端·python
程序员小假9 小时前
我们来说一说 Cglib 与 JDK 动态代理
后端
摆烂工程师11 小时前
教你如何认证 Gemini 教育优惠的二次验证,薅个 1年的 Gemini Pro 会员
后端·程序员·gemini