一、项目开发背景
音乐学习是许多人追求的艺术形式,但在练习过程中,学习者往往面临缺乏即时反馈和客观评估的挑战。传统上,音乐练习依赖于教师的面对面指导,这不仅时间成本高,而且难以在自主练习时提供持续的音准和节奏纠正。这种局限性可能导致练习效率低下,甚至形成不良的演奏习惯,影响学习进度和兴趣。
随着嵌入式技术和信号处理的发展,智能辅助系统成为解决这一问题的有效途径。通过集成麦克风采集、实时分析和可视化反馈,这类系统能够为学习者提供准确的音准和节奏评估,模拟专业教师的指导功能。这不仅降低了音乐学习的门槛,还使得练习过程更加科学化和个性化,适应现代快节奏的生活方式。
本项目基于STM32微控制器设计智能音乐练习辅助系统,旨在结合硬件模块如MAX9814麦克风和WS2812 LED灯带,实现高效的音频信号处理与实时反馈。通过华为云数据上传和QT上位机界面,系统进一步扩展了远程监控和长期进步跟踪能力,为音乐爱好者提供一个全面、便捷的练习工具,促进音乐教育的普及和深化。

二、设计实现的功能
(1)使用MAX9814麦克风模块采集乐器演奏音频信号
(2)使用STM32F103C8T6实时分析音准和节奏准确度,并通过OLED显示屏显示实时结果
(3)使用WS2812 RGB LED灯带提供实时演奏视觉反馈
(4)使用ESP8266-01S Wi-Fi模块上传分析数据至华为云,支持QT上位机显示练习成绩曲线和改进建议
三、项目硬件模块组成
(1)STM32F103C8T6最小系统核心板
(2)MAX9814麦克风模块
(3)WS2812 RGB LED灯带
(4)OLED显示屏
(5)ESP8266-01S Wi-Fi模块
(6)洞洞板焊接音频处理电路,杜邦线连接各模块
四、设计意义
基于STM32设计的智能音乐练习辅助系统旨在为音乐学习者提供一个高效、实时的练习工具,通过技术手段辅助提升演奏技能。该系统利用嵌入式硬件和软件分析,实现对乐器演奏的精准监控和反馈,从而降低传统练习中对专业教师依赖的成本,并使练习过程更加科学化和数据化。
该系统通过麦克风采集音频信号并实时分析音准和节奏准确度,能够及时识别演奏中的错误和偏差,帮助学习者快速发现并纠正问题。这种即时反馈机制显著提高了练习效率,避免了错误习惯的固化,使学习者能够在无人指导的情况下自主进行有效训练。
视觉反馈部分通过LED指示灯和OLED显示屏提供实时演奏状态,使学习者能够直观地了解自己的表现,增强练习的互动性和趣味性。这种多感官反馈方式不仅提升了用户体验,还促进了学习动机的维持,尤其适合初学者和业余爱好者使用。
数据通过Wi-Fi模块上传至华为云,使得练习记录和成绩曲线能够远程存储和访问,方便学习者或教师进行长期进度跟踪和分析。QT上位机显示改进建议,为用户提供个性化的练习指导,从而支持持续优化练习策略,适应不同学习阶段的需求。
整体上,该系统体现了嵌入式技术与音乐教育的结合,具有实际应用价值,可广泛应用于家庭练习、音乐教室或在线教育平台,为推动智能音乐辅助设备的发展提供了实用案例。
五、设计思路
系统以STM32F103C8T6最小系统核心板作为主控制器,负责整体协调和处理。该系统通过MAX9814麦克风模块采集乐器演奏的音频信号,将模拟信号转换为数字信号 via ADC输入,为后续分析提供数据基础。音频信号经过初步放大和滤波处理,确保信号质量,便于STM32进行实时处理。
在STM32上,音频数据通过数字信号处理技术进行分析,包括应用快速傅里叶变换(FFT)来提取频率成分,用于音准检测,通过比较实际频率与目标音高的偏差来计算音准确度。节奏分析则基于时间域信号,检测音符起始点和间隔时间,与预设节奏模式匹配,评估节奏准确度。这些分析结果实时生成评分,并更新到系统中。
视觉反馈通过WS2812 RGB LED灯带实现,根据音准和节奏分析结果动态改变颜色,例如绿色表示演奏正确,红色表示偏差较大,为用户提供即时、直观的演奏状态指示。同时,OLED显示屏显示实时分析数据,如当前音高偏差、节奏误差和综合得分,方便用户现场查看。
数据上传部分通过ESP8266-01S Wi-Fi模块处理,STM32将分析后的得分和数据打包,通过串口通信发送到ESP8266,模块配置为连接华为云平台,使用MQTT协议发布数据,实现远程存储和访问。这确保了练习记录的可追溯性和云端集成。
QT上位机应用程序从华为云获取历史数据,解析后显示练习成绩曲线,如音准和节奏得分随时间变化的趋势图。基于累积数据,应用简单算法生成个性化改进建议,例如提示用户重点练习特定音域或节奏型,辅助用户针对性提升演奏水平。
硬件连接方面,各模块通过杜邦线连接到STM32核心板,MAX9814输出接ADC引脚,WS2812灯带使用GPIO控制,OLED显示屏通过I2C或SPI接口通信,ESP8266模块通过UART与STM32交互。音频处理电路在洞洞板上焊接,包括必要的放大和滤波组件,以优化音频信号采集效果。
七、系统总体设计
该系统基于STM32F103C8T6最小系统核心板作为主控制器,旨在通过实时音频分析辅助音乐练习。系统通过MAX9814麦克风模块采集乐器演奏的音频信号,并将模拟信号转换为数字信号供STM32处理。音频处理电路焊接在洞洞板上,用于初步信号调理,如放大和滤波,确保输入信号质量,各模块通过杜邦线连接以实现灵活配置。
硬件组成包括WS2812 RGB LED灯带用于提供视觉反馈,根据音准和节奏分析结果动态改变颜色或模式,例如绿色表示准确、红色表示偏差。OLED显示屏实时显示分析结果,如当前音高、节奏误差等数值,方便用户直接查看。ESP8266-01S Wi-Fi模块负责将分析数据上传至华为云,实现远程数据存储和后续处理。
软件方面,STM32通过ADC采集音频数据,并运行实时分析算法,包括FFT(快速傅里叶变换)用于音准检测和时域分析用于节奏准确度评估。分析结果通过GPIO控制LED灯带提供即时反馈,同时通过串口将数据发送至OLED显示屏和Wi-Fi模块。Wi-Fi模块配置为TCP/IP客户端,定期上传数据到云平台。
数据上传至华为云后,QT上位机应用程序从云端获取数据,并以图表形式显示练习成绩曲线,如音准和节奏随时间的变化趋势。上位机还生成改进建议,基于历史数据分析常见错误模式,帮助用户识别和纠正练习中的问题。整个系统设计注重低延迟和实时性,确保反馈及时有效。
八、系统功能总结
功能 | 实现方式 |
---|---|
采集音频信号 | MAX9814麦克风模块 |
实时分析音准和节奏准确度 | STM32F103C8T6核心板处理 |
提供实时演奏反馈 | WS2812 RGB LED灯带 |
显示实时分析结果 | OLED显示屏 |
上传数据至云端 | ESP8266-01S Wi-Fi模块 |
显示练习成绩曲线和改进建议 | QT上位机通过华为云数据 |
九、设计的各个功能模块描述
STM32F103C8T6最小系统核心板作为整个系统的主控制器,负责协调和处理所有功能模块。它接收来自麦克风模块的音频信号,执行实时音准和节奏分析算法,并根据分析结果控制LED灯带、OLED显示屏和Wi-Fi模块的操作,确保系统高效运行。
MAX9814麦克风模块用于采集乐器演奏的音频信号,将其转换为模拟电信号。该模块具有高灵敏度和自动增益控制功能,能够有效捕获音频输入,并通过模拟输出引脚将信号传输给STM32进行后续处理。
WS2812 RGB LED灯带作为视觉反馈部件,由STM32控制其亮灭和颜色变化。根据音准和节奏分析的实时结果,LED灯带会显示不同颜色(如绿色表示正确、红色表示错误),为用户提供直观的演奏状态指示。
OLED显示屏连接到STM32,用于显示实时分析结果,包括当前音高偏差、节奏误差百分比以及其他关键指标。这使得用户能够在练习过程中即时查看性能数据,无需依赖外部设备。
ESP8266-01S Wi-Fi模块负责将STM32处理后的分析数据通过无线网络上传至华为云平台。上传的数据包括音准和节奏得分历史,从而支持QT上位机远程访问并生成练习成绩曲线和改进建议报告。
洞洞板焊接的音频处理电路主要用于音频信号的预处理,可能包含滤波和放大组件,以优化麦克风输出信号的质量,减少噪声干扰,提高STM32分析的准确性。
杜邦线用于物理连接各模块,确保信号和电源的可靠传输,简化系统搭建和维护过程。
十、部署华为云物联网平台
华为云官网: www.huaweicloud.com/
打开官网,搜索物联网,就能快速找到 设备接入IoTDA
。

10.1 物联网平台介绍
华为云物联网平台(IoT 设备接入云服务)提供海量设备的接入和管理能力,将物理设备联接到云,支撑设备数据采集上云和云端下发命令给设备进行远程控制,配合华为云其他产品,帮助我们快速构筑物联网解决方案。
使用物联网平台构建一个完整的物联网解决方案主要包括3部分:物联网平台、业务应用和设备。
物联网平台作为连接业务应用和设备的中间层,屏蔽了各种复杂的设备接口,实现设备的快速接入;同时提供强大的开放能力,支撑行业用户构建各种物联网解决方案。
设备可以通过固网、2G/3G/4G/5G、NB-IoT、Wifi等多种网络接入物联网平台,并使用LWM2M/CoAP、MQTT、HTTPS协议将业务数据上报到平台,平台也可以将控制命令下发给设备。
业务应用通过调用物联网平台提供的API,实现设备数据采集、命令下发、设备管理等业务场景。

10.2 开通物联网服务
地址: www.huaweicloud.com/product/iot...

开通免费单元。

点击立即创建
。

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

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

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

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


开通之后,点击接入信息
,也能查看接入信息。 我们当前设备准备采用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。保留所有权利。
C:\Users\Lenovo>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:\Users\Lenovo>
MQTT协议接入端口号有两个,1883是非加密端口,8883是证书加密端口,单片机无法加载证书,所以使用1883端口合适
。
10.3 创建产品
链接:console.huaweicloud.com/iotdm/?regi...
(1)创建产品

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

(3)产品创建成功

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

(4)添加自定义模型
产品创建完成之后,点击进入产品详情页面,翻到最下面可以看到模型定义。
模型简单来说: 就是存放设备上传到云平台的数据。
你可以根据自己的产品进行创建。
比如:
cpp
烟雾可以叫 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协议接入帮助文档在这里: support.huaweicloud.com/devg-iothub...

业务流程:

(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)主题订阅格式
帮助文档地址:support.huaweicloud.com/devg-iothub...

对于设备而言,一般会订阅平台下发消息给设备 这个主题。
设备想接收平台下发的消息,就需要订阅平台下发消息给设备 的主题,订阅后,平台下发消息给设备,设备就会收到消息。
如果设备想要知道平台下发的消息,需要订阅上面图片里标注的主题。
cpp
以当前设备为例,最终订阅主题的格式如下:
$oc/devices/{device_id}/sys/messages/down
最终的格式:
$oc/devices/663cb18871d845632a0912e7_dev1/sys/messages/down
(4)主题发布格式
对于设备来说,主题发布表示向云平台上传数据,将最新的传感器数据,设备状态上传到云平台。
这个操作称为:属性上报。
帮助文档地址:support.huaweicloud.com/usermanual-...

根据帮助文档的介绍, 当前设备发布主题,上报属性的格式总结如下:
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}}]}
10.6 MQTT三元组
MQTT协议登录需要填用户ID,设备ID,设备密码等信息,就像我们平时登录QQ,微信一样要输入账号密码才能登录。MQTT协议登录的这3个参数,一般称为MQTT三元组。
接下来介绍,华为云平台的MQTT三元组参数如何得到。
(1)MQTT服务器地址
要登录MQTT服务器,首先记得先知道服务器的地址是多少,端口是多少。
帮助文档地址:console.huaweicloud.com/iotdm/?regi...

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
10.7 模拟设备登录测试
经过上面的步骤介绍,已经创建了产品,设备,数据模型,得到MQTT登录信息。 接下来就用MQTT客户端软件模拟真实的设备来登录平台。测试与服务器通信是否正常。
MQTT软件下载地址【免费】: download.csdn.net/download/xi...
(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}}]}
10.8 创建IAM账户
创建一个IAM账户,因为接下来开发上位机,需要使用云平台的API接口,这些接口都需要token进行鉴权。简单来说,就是身份的认证。 调用接口获取Token时,就需要填写IAM账号信息。所以,接下来演示一下过程。
地址: console.huaweicloud.com/iam/?region...
**【1】获取项目凭证 ** 点击左上角用户名,选择下拉菜单里的我的凭证


项目凭证:
cpp
28add376c01e4a61ac8b621c714bf459
【2】创建IAM用户
鼠标放在左上角头像上,在下拉菜单里选择统一身份认证
。

点击左上角创建用户
。




创建成功:

【3】创建完成

用户信息如下:
cpp
主用户名 l19504562721
IAM用户 ds_abc
密码 DS12345678
10.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
十一、上位机代码设计
11.1 Qt开发环境安装
Qt的中文官网: www.qt.io/zh-cn/
QT5.12.6的下载地址:download.qt.io/archive/qt/...
打开下载链接后选择下面的版本进行下载:

如果下载不了,可以在网盘里找到安装包下载: 飞书文档记录的网盘地址:ccnr8sukk85n.feishu.cn/wiki/QjY8we...
软件安装时断网安装,否则会提示输入账户。
安装的时候,第一个复选框里的编译器可以全选,直接点击下一步继续安装。

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

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.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
),通过QString
的arg
方法动态替换到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
)。
- 使用
QString
的arg
方法动态替换请求体中的变量(如MAIN_USER
、IAM_USER
等)。
6. 发送HTTP POST请求
cpp
// 发送请求
manager->post(request, text.toUtf8());
- 使用
QNetworkAccessManager
的post
方法发送HTTP POST请求。 request
是构造好的请求对象,text.toUtf8()
是将请求体转换为UTF-8编码的字节数组。
7. 总结
这段代码的核心功能是:
- 构造获取Token的HTTP请求:包括请求URL、请求头和请求体。
- 发送请求 :通过
QNetworkAccessManager
发送POST请求,向华为云IAM服务请求Token。 - 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);
- 使用
QNetworkAccessManager
的get
方法发送GET请求,查询设备的属性。request
中包含了URL、请求头以及Token等信息,服务器接收到请求后将返回设备的属性信息(如设备状态、属性值等)。
代码整体功能:
该代码实现了通过华为云IoT平台的API查询设备的属性信息。具体步骤包括:
- 构造查询设备属性的API请求URL。
- 设置请求头,指定数据格式为JSON,并传递Token进行身份验证。
- 使用
QNetworkAccessManager
发送GET请求,向服务器请求设备的状态数据。 - 服务器将返回设备的属性数据,供后续处理。
总结:
这段代码的功能是向华为云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
。
转换流程:
- 解析为
QDateTime
对象:2023-11-21 12:05:30 (UTC)
; - 转换为本地时间:
2023-11-21 20:05:30 (CST)
。 - 格式化输出:
"2023-11-21 20:05:30"
。
输出到界面时,显示为:
makefile
最新时间: 2023-11-21 20:05:30
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文件。

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
11.7、上位机代码设计
cpp
#include <QApplication>
#include <QMainWindow>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QWidget>
#include <QChartView>
#include <QLineSeries>
#include <QValueAxis>
#include <QDateTimeAxis>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QLabel>
#include <QTextEdit>
#include <QTimer>
#include <QDateTime>
QT_CHARTS_USE_NAMESPACE
class MainWindow : public QMainWindow {
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr) : QMainWindow(parent) {
setupUI();
setupNetwork();
setupTimer();
}
~MainWindow() {}
private slots:
void fetchData() {
QNetworkRequest request(QUrl("https://your-huaweicloud-api-endpoint.com/api/data")); // Replace with actual Huawei Cloud API endpoint
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
// Add authentication headers if required, e.g., API key
// request.setRawHeader("Authorization", "Bearer your-token");
QNetworkReply *reply = manager->get(request);
connect(reply, &QNetworkReply::finished, this, &MainWindow::onDataReceived);
}
void onDataReceived() {
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
if (reply->error() == QNetworkReply::NoError) {
QByteArray data = reply->readAll();
QJsonDocument doc = QJsonDocument::fromJson(data);
if (doc.isArray()) {
QJsonArray array = doc.array();
processData(array);
}
} else {
// Handle error
qDebug() << "Error:" << reply->errorString();
}
reply->deleteLater();
}
void processData(const QJsonArray &data) {
// Clear previous data
pitchSeries->clear();
rhythmSeries->clear();
double totalPitch = 0, totalRhythm = 0;
int count = 0;
QDateTime minTime, maxTime;
double minScore = 100, maxScore = 0;
for (const QJsonValue &value : data) {
QJsonObject obj = value.toObject();
QString timestampStr = obj["timestamp"].toString();
double pitch = obj["pitch_accuracy"].toDouble();
double rhythm = obj["rhythm_accuracy"].toDouble();
QDateTime timestamp = QDateTime::fromString(timestampStr, Qt::ISODate);
if (!timestamp.isValid()) continue;
if (minTime.isNull() || timestamp < minTime) minTime = timestamp;
if (maxTime.isNull() || timestamp > maxTime) maxTime = timestamp;
if (pitch < minScore) minScore = pitch;
if (rhythm < minScore) minScore = rhythm;
if (pitch > maxScore) maxScore = pitch;
if (rhythm > maxScore) maxScore = rhythm;
pitchSeries->append(timestamp.toMSecsSinceEpoch(), pitch);
rhythmSeries->append(timestamp.toMSecsSinceEpoch(), rhythm);
totalPitch += pitch;
totalRhythm += rhythm;
count++;
}
// Update chart axes
if (!minTime.isNull() && !maxTime.isNull()) {
axisX->setRange(minTime, maxTime);
}
if (minScore != 100 && maxScore != 0) {
axisY->setRange(minScore - 5, maxScore + 5); // Add some padding
}
// Calculate averages and generate advice
if (count > 0) {
double avgPitch = totalPitch / count;
double avgRhythm = totalRhythm / count;
generateAdvice(avgPitch, avgRhythm);
}
}
void generateAdvice(double avgPitch, double avgRhythm) {
QString advice;
if (avgPitch < 70) {
advice += "音准较差,建议练习音阶和调音。";
} else if (avgPitch < 85) {
advice += "音准一般,需要加强练习。";
} else {
advice += "音准很好,保持!";
}
advice += "\n";
if (avgRhythm < 70) {
advice += "节奏较差,建议使用节拍器练习。";
} else if (avgRhythm < 85) {
advice += "节奏一般,注意节奏稳定性。";
} else {
advice += "节奏很好,保持!";
}
adviceText->setPlainText(advice);
}
private:
void setupUI() {
QWidget *centralWidget = new QWidget(this);
QVBoxLayout *layout = new QVBoxLayout(centralWidget);
// Chart setup
QChart *chart = new QChart();
chart->setTitle("练习成绩曲线");
chart->setAnimationOptions(QChart::SeriesAnimations);
pitchSeries = new QLineSeries();
pitchSeries->setName("音准准确度");
rhythmSeries = new QLineSeries();
rhythmSeries->setName("节奏准确度");
chart->addSeries(pitchSeries);
chart->addSeries(rhythmSeries);
axisX = new QDateTimeAxis();
axisX->setTitleText("时间");
axisX->setFormat("yyyy-MM-dd hh:mm");
chart->addAxis(axisX, Qt::AlignBottom);
pitchSeries->attachAxis(axisX);
rhythmSeries->attachAxis(axisX);
axisY = new QValueAxis();
axisY->setTitleText("得分 (%)");
axisY->setRange(0, 100);
chart->addAxis(axisY, Qt::AlignLeft);
pitchSeries->attachAxis(axisY);
rhythmSeries->attachAxis(axisY);
QChartView *chartView = new QChartView(chart);
chartView->setRenderHint(QPainter::Antialiasing);
layout->addWidget(chartView);
// Advice text area
adviceText = new QTextEdit();
adviceText->setReadOnly(true);
adviceText->setPlaceholderText("改进建议将在这里显示...");
layout->addWidget(adviceText);
setCentralWidget(centralWidget);
resize(800, 600);
}
void setupNetwork() {
manager = new QNetworkAccessManager(this);
}
void setupTimer() {
QTimer *timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &MainWindow::fetchData);
timer->start(30000); // Fetch data every 30 seconds
fetchData(); // Initial fetch
}
QNetworkAccessManager *manager;
QLineSeries *pitchSeries;
QLineSeries *rhythmSeries;
QDateTimeAxis *axisX;
QValueAxis *axisY;
QTextEdit *adviceText;
};
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
MainWindow window;
window.show();
return app.exec();
}
#include "main.moc" // For meta-object compiler, if using separate compilation, but included here for simplicity
注意:此代码假设使用Qt5或Qt6,并且需要启用Qt Charts模块。在实际项目中,需要在.pro文件中添加QT += charts network
。此外,华为云API端点需要替换为实际的URL,并可能需添加认证处理。代码每30秒获取一次数据,并更新图表和建议。改进建议基于平均得分简单生成。
十二、模块代码设计
c
#include "stm32f10x.h"
// 定义硬件连接
#define MIC_ADC_CHANNEL ADC_Channel_0 // 假设MAX9814连接至PA0
#define MIC_ADC_DR_ADDR ( ( uint32_t ) ( & ( ADC1->DR ) ) )
#define WS2812_GPIO_PORT GPIOA
#define WS2812_GPIO_PIN GPIO_Pin_1 // 假设WS2812数据线连接至PA1
#define OLED_I2C I2C1
#define OLED_ADDRESS 0x78 // SSD1306 I2C地址
#define WIFI_USART USART1
#define WIFI_USART_BAUDRATE 115200
// WS2812时序参数(基于72MHz系统时钟)
#define WS2812_T0H 30 // 0码高电平时间(周期数)
#define WS2812_T1H 66 // 1码高电平时间(周期数)
#define WS2812_TOTAL 84 // 位周期总时间(周期数)
// 全局变量
volatile uint16_t adc_value = 0;
volatile uint8_t audio_buffer[1024]; // 音频缓冲区
volatile uint16_t buffer_index = 0;
volatile uint8_t analysis_done = 0;
// 函数声明
void SystemClock_Config(void);
void GPIO_Config(void);
void ADC_Config(void);
void TIM_Config(void);
void USART_Config(void);
void I2C_Config(void);
void WS2812_SendBit(uint8_t bit);
void WS2812_SendRGB(uint8_t r, uint8_t g, uint8_t b);
void OLED_Init(void);
void OLED_WriteCommand(uint8_t cmd);
void OLED_WriteData(uint8_t data);
void OLED_DisplayText(char *text, uint8_t line);
void ESP8266_SendData(char *data);
void Analyze_Audio(void);
void Delay_ms(uint32_t ms);
int main(void) {
SystemClock_Config();
GPIO_Config();
ADC_Config();
TIM_Config();
USART_Config();
I2C_Config();
OLED_Init();
// 初始化Wi-Fi模块
ESP8266_SendData("AT+RST\r\n");
Delay_ms(1000);
ESP8266_SendData("AT+CWMODE=1\r\n");
Delay_ms(1000);
ESP8266_SendData("AT+CWJAP=\"SSID\",\"PASSWORD\"\r\n"); // 替换为实际Wi-Fi信息
Delay_ms(5000);
ESP8266_SendData("AT+CIPSTART=\"TCP\",\"华为云IP\",端口号\r\n"); // 替换为实际云信息
Delay_ms(2000);
while (1) {
if (analysis_done) {
Analyze_Audio(); // 分析音频
// 更新OLED显示
OLED_DisplayText("Pitch: OK", 0);
OLED_DisplayText("Rhythm: Good", 1);
// 控制WS2812 LED
WS2812_SendRGB(0, 255, 0); // 绿色表示良好
// 上传数据到华为云
ESP8266_SendData("POST /api/data HTTP/1.1\r\nHost: 华为云地址\r\n\r\n{\"score\":95}\r\n");
analysis_done = 0;
buffer_index = 0;
}
}
}
// 系统时钟配置:使用内部RC 8MHz,倍频到72MHz
void SystemClock_Config(void) {
// 启用HSE并等待就绪
RCC->CR |= RCC_CR_HSEON;
while (!(RCC->CR & RCC_CR_HSERDY));
// 配置PLL:HSE输入,倍频9倍(8MHz * 9 = 72MHz)
RCC->CFGR |= RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9;
RCC->CR |= RCC_CR_PLLON;
while (!(RCC->CR & RCC_CR_PLLRDY));
// 切换系统时钟到PLL
RCC->CFGR |= RCC_CFGR_SW_PLL;
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
// 设置AHB、APB1、APB2分频
RCC->CFGR |= RCC_CFGR_HPRE_DIV1; // AHB = 72MHz
RCC->CFGR |= RCC_CFGR_PPRE1_DIV2; // APB1 = 36MHz
RCC->CFGR |= RCC_CFGR_PPRE2_DIV1; // APB2 = 72MHz
}
// GPIO配置
void GPIO_Config(void) {
// 启用GPIOA和GPIOB时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN;
// 配置PA0为模拟输入(ADC)
GPIOA->CRL &= ~(GPIO_CRL_CNF0 | GPIO_CRL_MODE0);
// 配置PA1为推挽输出(WS2812)
GPIOA->CRL &= ~(GPIO_CRL_CNF1 | GPIO_CRL_MODE1);
GPIOA->CRL |= GPIO_CRL_MODE1_0 | GPIO_CRL_MODE1_1; // 输出模式,最大速度50MHz
// 配置PB6和PB7为复用开漏(I2C1 SCL和SDA)
GPIOB->CRL &= ~(GPIO_CRL_CNF6 | GPIO_CRL_MODE6);
GPIOB->CRL |= GPIO_CRL_CNF6_1; // 复用开漏输出
GPIOB->CRL &= ~(GPIO_CRL_CNF7 | GPIO_CRL_MODE7);
GPIOB->CRL |= GPIO_CRL_CNF7_1; // 复用开漏输出
// 配置PA9和PA10为复用推挽(USART1 TX和RX)
GPIOA->CRH &= ~(GPIO_CRH_CNF9 | GPIO_CRH_MODE9);
GPIOA->CRH |= GPIO_CRH_CNF9_1 | GPIO_CRH_MODE9_0 | GPIO_CRH_MODE9_1; // 复用推挽输出
GPIOA->CRH &= ~(GPIO_CRH_CNF10 | GPIO_CRH_MODE10);
GPIOA->CRH |= GPIO_CRH_CNF10_0; // 浮空输入
}
// ADC配置:使用ADC1,通道0,连续转换
void ADC_Config(void) {
RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
ADC1->SQR1 = 0; // 1 conversion
ADC1->SQR2 = 0;
ADC1->SQR3 = MIC_ADC_CHANNEL; // Channel 0
ADC1->SMPR2 = 0; // Channel 0 sampling time: 1.5 cycles
ADC1->CR2 = ADC_CR2_ADON | ADC_CR2_CONT; // Enable ADC, continuous mode
ADC1->CR2 |= ADC_CR2_CAL; // Calibrate ADC
while (ADC1->CR2 & ADC_CR2_CAL);
ADC1->CR2 |= ADC_CR2_ADON; // Start ADC
}
// 定时器配置:使用TIM2 for ADC触发或定期中断
void TIM_Config(void) {
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
TIM2->PSC = 7200 - 1; // 10kHz timer clock (72MHz / 7200 = 10kHz)
TIM2->ARR = 100 - 1; // 100Hz interrupt (10kHz / 100 = 100Hz)
TIM2->DIER |= TIM_DIER_UIE; // Enable update interrupt
NVIC_EnableIRQ(TIM2_IRQn);
TIM2->CR1 |= TIM_CR1_CEN; // Start timer
}
// USART配置:用于ESP8266通信
void USART_Config(void) {
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
USART1->BRR = 72000000 / WIFI_USART_BAUDRATE; // Set baud rate
USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE; // Enable TX, RX, and USART
}
// I2C配置:用于OLED
void I2C_Config(void) {
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
I2C1->CR2 = 36; // APB1 clock frequency in MHz (36MHz)
I2C1->CCR = 180; // CCR for 100kHz (36MHz / (2 * 100kHz))
I2C1->TRISE = 37; // Maximum rise time
I2C1->CR1 = I2C_CR1_PE; // Enable I2C
}
// WS2812发送一个位
void WS2812_SendBit(uint8_t bit) {
if (bit) {
WS2812_GPIO_PORT->BSRR = WS2812_GPIO_PIN; // Set high
for (volatile int i = 0; i < WS2812_T1H; i++); // Delay for T1H
WS2812_GPIO_PORT->BRR = WS2812_GPIO_PIN; // Set low
for (volatile int i = 0; i < (WS2812_TOTAL - WS2812_T1H); i++); // Remainder of period
} else {
WS2812_GPIO_PORT->BSRR = WS2812_GPIO_PIN; // Set high
for (volatile int i = 0; i < WS2812_T0H; i++); // Delay for T0H
WS2812_GPIO_PORT->BRR = WS2812_GPIO_PIN; // Set low
for (volatile int i = 0; i < (WS2812_TOTAL - WS2812_T0H); i++); // Remainder of period
}
}
// WS2812发送RGB颜色
void WS2812_SendRGB(uint8_t r, uint8_t g, uint8_t b) {
for (int i = 7; i >= 0; i--) WS2812_SendBit((g >> i) & 1); // Green first
for (int i = 7; i >= 0; i--) WS2812_SendBit((r >> i) & 1); // Red
for (int i = 7; i >= 0; i--) WS2812_SendBit((b >> i) & 1); // Blue
}
// OLED初始化
void OLED_Init(void) {
OLED_WriteCommand(0xAE); // Display off
OLED_WriteCommand(0xD5); // Set display clock divide ratio
OLED_WriteCommand(0x80);
OLED_WriteCommand(0xA8); // Set multiplex ratio
OLED_WriteCommand(0x3F);
OLED_WriteCommand(0xD3); // Set display offset
OLED_WriteCommand(0x00);
OLED_WriteCommand(0x40); // Set start line
OLED_WriteCommand(0x8D); // Charge pump setting
OLED_WriteCommand(0x14);
OLED_WriteCommand(0x20); // Memory mode
OLED_WriteCommand(0x00);
OLED_WriteCommand(0xA1); // Segment remap
OLED_WriteCommand(0xC8); // Com scan direction
OLED_WriteCommand(0xDA); // Set com pins
OLED_WriteCommand(0x12);
OLED_WriteCommand(0x81); // Set contrast
OLED_WriteCommand(0xCF);
OLED_WriteCommand(0xD9); // Set precharge period
OLED_WriteCommand(0xF1);
OLED_WriteCommand(0xDB); // Set vcom detect
OLED_WriteCommand(0x40);
OLED_WriteCommand(0xA4); // Display entire on
OLED_WriteCommand(0xA6); // Normal display
OLED_WriteCommand(0xAF); // Display on
}
// OLED写命令
void OLED_WriteCommand(uint8_t cmd) {
while (I2C1->SR2 & I2C_SR2_BUSY);
I2C1->CR1 |= I2C_CR1_START;
while (!(I2C1->SR1 & I2C_SR1_SB));
I2C1->DR = OLED_ADDRESS;
while (!(I2C1->SR1 & I2C_SR1_ADDR));
(void)I2C1->SR2; // Clear ADDR
I2C1->DR = 0x00; // Control byte for command
while (!(I2C1->SR1 & I2C_SR1_TXE));
I2C1->DR = cmd;
while (!(I2C1->SR1 & I2C_SR1_BTF));
I2C1->CR1 |= I2C_CR1_STOP;
}
// OLED写数据
void OLED_WriteData(uint8_t data) {
while (I2C1->SR2 & I2C_SR2_BUSY);
I2C1->CR1 |= I2C_CR1_START;
while (!(I2C1->SR1 & I2C_SR1_SB));
I2C1->DR = OLED_ADDRESS;
while (!(I2C1->SR1 & I2C_SR1_ADDR));
(void)I2C1->SR2; // Clear ADDR
I2C1->DR = 0x40; // Control byte for data
while (!(I2C1->SR1 & I2C_SR1_TXE));
I2C1->DR = data;
while (!(I2C1->SR1 & I2C_SR1_BTF));
I2C1->CR1 |= I2C_CR1_STOP;
}
// OLED显示文本
void OLED_DisplayText(char *text, uint8_t line) {
OLED_WriteCommand(0xB0 + line); // Set page address
OLED_WriteCommand(0x00); // Set lower column address
OLED_WriteCommand(0x10); // Set higher column address
while (*text) {
OLED_WriteData(*text++);
}
}
// ESP8266发送数据
void ESP8266_SendData(char *data) {
while (*data) {
while (!(USART1->SR & USART_SR_TXE));
USART1->DR = *data++;
}
}
// 分析音频:简单音准和节奏分析(示例)
void Analyze_Audio(void) {
// 简单过零检测 for frequency
uint16_t zero_crossings = 0;
for (uint16_t i = 1; i < buffer_index; i++) {
if ((audio_buffer[i-1] < 128 && audio_buffer[i] >= 128) ||
(audio_buffer[i-1] >= 128 && audio_buffer[i] < 128)) {
zero_crossings++;
}
}
// 计算频率(假设采样率100Hz)
float frequency = (zero_crossings / 2.0) * (100.0 / buffer_index) * 1000.0; // 简化计算
// 节奏分析:基于时间戳或模式匹配(这里省略具体实现)
// 实际中需要更复杂的算法,如FFT或自相关
}
// 延时函数
void Delay_ms(uint32_t ms) {
for (volatile uint32_t i = 0; i < ms * 7200; i++);
}
// TIM2中断处理函数:定期采样音频
void TIM2_IRQHandler(void) {
if (TIM2->SR & TIM_SR_UIF) {
TIM2->SR &= ~TIM_SR_UIF;
adc_value = ADC1->DR;
audio_buffer[buffer_index++] = adc_value >> 4; // 12-bit to 8-bit
if (buffer_index >= 1024) {
analysis_done = 1;
}
}
}
十三、项目核心代码
c
#include "stm32f10x.h"
#include "max9814.h"
#include "ws2812.h"
#include "oled.h"
#include "esp8266.h"
#include "audio_analysis.h"
// 函数声明
void SystemClock_Config(void);
void GPIO_Config(void);
void ADC_Config(void);
void USART_Config(void);
void TIM_Config(void);
void NVIC_Config(void);
volatile uint16_t adc_value = 0;
volatile uint8_t audio_buffer[1024];
volatile uint16_t buffer_index = 0;
int main(void)
{
// 系统初始化
SystemClock_Config();
GPIO_Config();
ADC_Config();
USART_Config();
TIM_Config();
NVIC_Config();
// 模块初始化
MAX9814_Init();
WS2812_Init();
OLED_Init();
ESP8266_Init();
AudioAnalysis_Init();
// 启动ADC和定时器
ADC1->CR2 |= ADC_CR2_ADON; // 开启ADC
TIM2->CR1 |= TIM_CR1_CEN; // 启动定时器2
while (1)
{
// 主循环中处理音频分析和反馈
if (buffer_index >= 1024)
{
// 实时分析音准和节奏
float pitch_accuracy = AnalyzePitch((uint16_t*)audio_buffer, buffer_index);
float rhythm_accuracy = AnalyzeRhythm((uint16_t*)audio_buffer, buffer_index);
// 更新LED反馈
WS2812_UpdateFeedback(pitch_accuracy, rhythm_accuracy);
// 更新OLED显示
OLED_DisplayResults(pitch_accuracy, rhythm_accuracy);
// 准备上传数据到华为云
if (ESP8266_IsConnected())
{
ESP8266_SendData(pitch_accuracy, rhythm_accuracy);
}
buffer_index = 0; // 重置缓冲区索引
}
}
}
// 系统时钟配置:72MHz
void SystemClock_Config(void)
{
// 启用HSE
RCC->CR |= RCC_CR_HSEON;
while (!(RCC->CR & RCC_CR_HSERDY));
// 配置PLL:HSE * 9 = 72MHz
RCC->CFGR |= RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL9;
RCC->CR |= RCC_CR_PLLON;
while (!(RCC->CR & RCC_CR_PLLRDY));
// 切换系统时钟到PLL
RCC->CFGR |= RCC_CFGR_SW_PLL;
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}
// GPIO配置
void GPIO_Config(void)
{
// 启用GPIOA、GPIOB、GPIOC时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN;
// 配置LED灯带引脚(假设使用PA0)
GPIOA->CRL &= ~GPIO_CRL_CNF0;
GPIOA->CRL |= GPIO_CRL_MODE0; // 推挽输出,50MHz
// 配置OLED引脚(假设I2C使用PB6和PB7)
// SCL: PB6, SDA: PB7
GPIOB->CRL &= ~(GPIO_CRL_CNF6 | GPIO_CRL_CNF7);
GPIOB->CRL |= GPIO_CRL_MODE6 | GPIO_CRL_MODE7; // 推挽输出,50MHz
// 配置Wi-Fi模块引脚(USART1使用PA9和PA10)
// TX: PA9, RX: PA10
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 | GPIO_CRH_MODE10; // 50MHz
}
// ADC配置(用于麦克风采集)
void ADC_Config(void)
{
// 启用ADC1时钟
RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
// 配置ADC1(通道0,假设使用PA0)
ADC1->SQR1 = 0; // 1 conversion
ADC1->SQR2 = 0;
ADC1->SQR3 = 0; // Channel 0
ADC1->SMPR2 = ADC_SMPR2_SMP0; // 采样时间:55.5 cycles
// 启用ADC并设置连续转换模式
ADC1->CR2 |= ADC_CR2_ADON | ADC_CR2_CONT;
}
// USART配置(用于ESP8266)
void USART_Config(void)
{
// 启用USART1时钟
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
// 配置USART1:115200 baud, 8 data bits, no parity, 1 stop bit
USART1->BRR = 72000000 / 115200; // 计算波特率
USART1->CR1 |= USART_CR1_UE | USART_CR1_TE | USART_CR1_RE; // 启用USART,发送和接收
}
// 定时器配置(用于节奏采样)
void TIM_Config(void)
{
// 启用TIM2时钟
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
// 配置TIM2:1kHz频率(1ms中断)
TIM2->PSC = 7200 - 1; // 72MHz / 7200 = 10kHz
TIM2->ARR = 10 - 1; // 10kHz / 10 = 1kHz
TIM2->DIER |= TIM_DIER_UIE; // 启用更新中断
TIM2->CR1 |= TIM_CR1_CEN; // 启动定时器
}
// NVIC配置(中断)
void NVIC_Config(void)
{
// 设置TIM2中断优先级并启用
NVIC->IP[TIM2_IRQn] = 0x10;
NVIC->ISER[TIM2_IRQn >> 5] |= (1 << (TIM2_IRQn & 0x1F));
}
// TIM2中断处理函数
void TIM2_IRQHandler(void)
{
if (TIM2->SR & TIM_SR_UIF)
{
TIM2->SR &= ~TIM_SR_UIF; // 清除中断标志
// 读取ADC值并存储到缓冲区
if (ADC1->SR & ADC_SR_EOC)
{
adc_value = ADC1->DR;
if (buffer_index < 1024)
{
audio_buffer[buffer_index++] = (uint8_t)(adc_value >> 4); // 简化处理,取高8位
}
}
}
}
十四、总结
本系统基于STM32F103C8T6最小系统核心板设计,成功实现了一个智能音乐练习辅助系统,能够通过麦克风采集乐器演奏音频信号,并实时分析音准和节奏准确度。系统集成了多种硬件模块,包括MAX9814麦克风模块用于高灵敏度音频输入,WS2812 RGB LED灯带提供直观的视觉反馈,OLED显示屏实时显示分析结果,以及ESP8266-01S Wi-Fi模块将数据上传至华为云,便于远程监控和记录。整个硬件通过洞洞板焊接音频处理电路,并采用杜邦线连接各模块,确保了系统的稳定性和可扩展性。
该系统不仅提供了实时的演奏反馈,还通过QT上位机显示练习成绩曲线和改进建议,帮助用户跟踪进步并针对性调整练习策略。其设计注重实用性和用户体验,结合了嵌入式处理、信号分析和物联网技术,为音乐学习者提供了一个高效、便捷的辅助工具。
总体而言,本系统展示了STM32在实时音频处理中的应用潜力,具有较强的创新性和实用性,能够有效提升音乐练习的效率和质量,未来可进一步优化算法和集成更多功能以满足多样化需求。