# 打砖块、频谱图--简易示波器项目的软件功能扩展
## 0 简介
本工程实现了示波器、打砖块、频谱图等功能。硬件部分是#嘉立创训练营##简易示波器#的复刻项目(GD32版本)。其中包含了模拟信号采集、TFT屏幕、旋转编码器等有趣外设,因此希望充分利用硬件资源提高可玩性。只在外围硬件和软件层面进行扩展,硬件部分没有改动,复刻该项目同学可以直接烧录快速试玩。其中:示波器项目训练营项目自带功能;打砖块使用旋转编码器控制,无需其他外设;音频频谱图通过模拟信号采集一个外置麦克放大模块或耳机插头信号,再通过FFT计算实现。
## 1 功能展示
![主界面](//image.lceda.cn/pullimage/v9FGzluzfUca0DsUXK2hkcHLYX7TLHUqrKQrntUg.jpeg)
![示波器](//image.lceda.cn/pullimage/ThkjNFQ8v5ZAuDDqAtHfxJ6ITjsvVGRE6Zn8mHO6.jpeg)
![打砖块(旋转编码器损毁者)](//image.lceda.cn/pullimage/GILJZeG2t3plRDD38yNLjwnYuQJKsH6yBTirzNMu.jpeg)
![音频频谱](//image.lceda.cn/pullimage/FJquqUYpZMRkfKK6RzrmcTMeFzc2qPeRE6iN8xSn.jpeg)
### 1.1 软件源码及hex文件下载地址
* 复刻官方GD32项目可直接使用这里的源码
[示波器、频谱图、打砖块 源码](https://gitee.com/snowcube/oscilloscope-base-on-gd32e230)
* 编译好的hex文件在文章末尾的附件中,直接烧写即可使用
## 2 简易示波器
(官方内置、略)
## 3 频谱图
在示波器示例中已经能够采集并显示自身输出的PWM波形信号,但作为业余的蜗居的硬件爱好者来说,能让示波器发挥功能的地方太少了。怎样才能更好的让硬件示“波”呢?很容易联想到无处不在的声波以及能跟随声波律动的频谱。
检测并呈现频谱图的核心流程如下:
![核心流程](//image.lceda.cn/pullimage/4KkJ7Tr7yCXjHwQKx4x4bYGxXSDVdG8L9y5DiZwZ.jpeg)
### 3.1 信号接口-耳机插头 or MAX4466麦克风前置放大模块
硬件前端用示波器探头接到耳机插头上,耳机插头接入手机等设备就能得到声音信号了。但是这样的话又会依赖播放设备,最不好的一点是一般播放设备插入耳机插头后就不能外放声音了,听声音、看频谱只能二选一。因此更好的选择是使用麦克风模块,这里选择了MAX4466麦克风前置放大模块(某宝7.5元).
![耳机插头](//image.lceda.cn/pullimage/w2For8p9M1VuxJULSJbXPoJBCpe924CD9PneyQE6.jpeg) ![MAX4466模块](//image.lceda.cn/pullimage/hnOuxXVvAR6jSHJwJXL8mWNv6I4qgxSJTffn8QbA.jpeg)
### 3.2 FFT参数-2K 64点
根据硬件条件:GD32E230、72MHz频率、8k的RAM存储空间、无FPU;被采样信信号特点:人耳感知频率范围大致20~ 2KHz、低频更为敏感,最终确定采样64点,采样频率2K。如果采样点为256,会报超出RAM限制错误,编译不过,这里没有继续探索优化方法,直接使用了64采样点。这导致最高检测频率是1KHz,频率分辨率也不高,效果一般。使用大RAM存储空间单片机的同学可以用256或更高采样点数来提升效果。
音频信号的采集过程,与示波器信号采集过程相似。区别需要通过配置时钟产生2KHz PWM来控制ADC采样,ADC采样数据输入DMA,DMA数量设置为64。这样在DMA中断中,就能取到采集好的信号啦。
### 3.3 FFT计算
FFT计算过程应该是最有技术含量的地方,随便列出一些推导FFT所需前置的概念吧:自然底数e、虚数、复数、欧拉公式、傅里叶级数、傅里叶变换、三角函数正交性、离散傅里叶变换DFT、蝶形变换、旋转因子。。。 别念啦!别念啦。。。
好在如此重要的算法,大家研究的很透彻。我们只需学习原理,程序部分stm32有库函数可以直接调用,网络上也有极致优化的代码可以参考使用,这是我参考的文章:
[C语言实现的FFT与IFFT源代码,不依赖特定平台](https://blog.csdn.net/weixin_44457994/article/details/121259018)
基本原理是利用旋转因子特性和蝶形变换简化DFT计算量,复杂度从O(n^2) 降至 O(nlogn) ,高斯、库利-图基 太会玩了!其中二进制逆序、蝶形变换体现了分治的思想。
实际过程中为了防止截断频率溢出对输入FFT的数据加窗处理,本工程采用了汉明窗;而对于输出结果需要进行历史融合滤波让频谱跳跃更连贯不闪烁;为了更符合人耳频谱感知的特点还应加入梅尔变换、以及对应的梅尔刻度显示;频谱幅值差距太大需要使用对数坐标调整显示幅值 20*log10( x )。
算法部分流程小结:
![流程小结](//image.lceda.cn/pullimage/USPCTryqoTXVwvqUT1lFnnhIKeGoC7AvIV0rudtL.jpeg)
### 3.4 输出至屏幕
FFT后第一个数据是0Hz的直流分量显示时去除,本工程中使用 ST7735 160x128 屏幕,调用 TFT_FillRect 绘制频谱。
## 4 打砖块
主界面中通过旋钮选择 BRICKBREAKER ,按下旋钮进入游戏;
玩法:通过旋钮移动平台,按下旋钮发射。KEY1键调速 (只在运行、暂停时生效)
### 4.1 基本流程
游戏运行基本逻辑
![基本逻辑](//image.lceda.cn/pullimage/ZCM3cIwUsM7oCzFiowtF0WZEvazfEnIZPdwER4mB.jpeg)
其中较为复杂的是计算场景内变化,对于打砖块来说主要就是:平台的运动,小球的运动,小球与墙、平台、砖块的碰撞。
如果没有碰撞,通过
P' = P0 + V * deltaT
就能根据,当前位置P0 ,速度向量 V , 时间 deltaT 算出新位置 P'。
如果有碰撞如何计算deltaT时间后状态呢?
![发生碰撞](//image.lceda.cn/pullimage/41IeS6Xx7QtXq79rx01xYeUVeL8GQDx1CSscuAnq.jpeg)
其实我们可以将小球deltaT内运动的轨迹视为线段,判断改线段是否与最近的方块相交。不相交则无碰撞计算完毕;如果相交,计算出交点,交点占线段比例,进而求碰撞耗时,改变小球速度向量(只需改变运动方向)。
接下来,重复上面的步骤进一步计算直到,deltaT时间内都计算完毕。
简单的说,就是将**deltaT时间内**的运动,用碰撞时刻作为**分隔点**
1. 分隔点改变小球运动状态,记录被撞砖块
2. 分隔点之间是无碰撞时间按公式,用距离计算耗时
![碰撞耗时](//image.lceda.cn/pullimage/WTh1KW7JdSWeu991P7e7NiGiocgSiPvoJwlDHp6I.jpeg)
实际程序计算过程中是利用循环步进的计算是否碰撞及其耗时的。
### 4.2 碰撞简化计算
判断碰撞比较容易就是判断线段是否相交。打砖块游戏中更加简单,因为只有小球的运动轨迹是任意的,砖块、平台、墙壁的边线都是与横平竖直的(与坐标轴对齐)。
比如判断向上运动的小球是否与砖块碰撞
1. 只需判断小球运动线段的起、终点是否分别位于砖块下边线的上下两侧。否,则无碰撞;是,则进一步判断
2. 针对上一步为是的情况,在线段中求得分隔点。如果分隔点在砖块下边线外部则无碰撞;在内部,则有碰撞,同时根据分割点在小球运动线段的占比,可知碰撞时间在deltaT中的占比。
![Snipaste_2024-03-28_14-24-53.jpg](//image.lceda.cn/pullimage/HCK5PZtYjD9atQPCPhsPSqK1624FTzeWCFzXtlYK.jpeg)
### 4.3 砖块地图数据
由于示波器、频谱占用了不少内存、以及屏幕的容量,砖块地图数据采用 const uint8_t[3][10],存储了三张 8 列 10行的位置。
一个 uint8_t 数据包含8个bit,每一bit表示该位置是否有砖块。
### 4.4 EC11 抖动问题及解决
**问题1:**
手头的EC11编码器在旋转时抖动的厉害,基本上是不论顺时针还是逆时针转动结果都是roll 骰子,导致无法正常使用,即使使用官方带的延时再次检测也无济于事。
**解决1:**
1. 编码器A、B两个引脚与GND间分别增加用104 (10uF)瓷片电容连接。
2. 通过上下沿分别触发检测。GD32 配置 A 引脚触发中断。初始时配置为下降沿触发。此时触发记录B引脚转态为B1,同时配置中断为上升沿触发。再次触发时,检测B引脚状态B2,同时配置中断为下降触发,进入下一次轮检测。如果B1、B2两次结果不同则有效,再根据B2确定到底是顺时针还是逆时针即可。总之,**A引脚上下沿触发都记录B的状态,两次结果相异则有效**。
**问题2:**
对于不同的界面,编码器触发后的作用是不一样的。例如:主界面中,编码器用来选择功能;打砖块中,编码器用来移动平台发射小球。如何动态配置编码器触发后的作用呢?
**解决2:**
**函数指针**, 使用一个全局函数指针变量,指向不同界面下的回调函数。在每个界面初始化时修改这个函数指针。在编码器中端回调函数中,如果这个指针不为空则调用。
```C
// 定义回调函数指针为一个类型
typedef void (*EC11RotateHandle) (int);
// 声明一个回调函数指针的全局变量
EC11RotateHandle ec11RotateHandle = NULL;
// EC11 中断函数
EXTI4_15_IRQHandler(void)
{
// ...
// 没有回调不处理
if (ec11RotateHandle != NULL)
{
// 1 表示顺时针 -1 表示逆时针
ec11RotateHandle(-1);
}
// ...
}
```
## 5 其他扩展
旋转编码器适合一轴控制的游戏:比如,FlipBird、泡泡龙、是男人就下100层等游戏。
## 6 总结
感谢#立创EDA#团队组织本次训练营,感谢热情的群友同学,通过本次工程,学习了PCB设计的基本流程、EDA软件的简单使用、FFT算法原理、嵌入式软件开发流程(C99),对于爱好者来说收获颇丰。本工程中,并没有实现梅尔变换,打砖块中也存在计算上的BUG,后续有时间再逐步完善,同时期待与大家交流合作。
## 参考
1. [C语言实现的FFT与IFFT源代码,不依赖特定平台](https://blog.csdn.net/weixin_44457994/article/details/121259018)
2
3
收藏到专辑