任务要求
-
通过PICO内部的ADC采集板上麦克风的音频信号
-
通过按键或光电旋转编码器能够左右、上下移动波形;左右、上下缩放波形,按键或旋转编码器的功能可以自己定义
-
通过ADC采集音频信号的电压峰峰值,并能够将音频信号的电压峰峰值显示在LCD屏幕上
-
对采集到的波形进行FFT变换,得到被测信号的频谱并在LCD上显示出来,并对单频信号显示其频率值
一:环境配置
1、thonny:
作为官方推荐的开发软件,thonny页面简洁,基础功能齐全,简单易上手,非常适合初学者(比如我)。安装的教程网上比较多,这里推荐一个硬禾的教学视频https://class.eetree.cn/live_pc/l_60fe7f4fe4b0a27d0e360f74
2、硬禾学堂树莓派pico平台:
硬禾学堂为“暑期一起练”制作了一个平台,这平台正是我视频中演示用到的板子他的原理图如下,具体可以参考https://www.eetree.cn/project/detail/103
二:程序实现:
程序均使用micropython编写,我是第一次使用micropython,之前一直是使用C语言写stm32h7的。如果有代码写的不是特别简洁干练,那,我也不想改<_<。
1、模块介绍:
1、显示屏的使用:
首先需要下载st7789的库,可以从github下载最新版的,也可以用电子森林上,别人开源项目中使用的旧版本,介于我初学时跟着开源项目学习的,这里就分享下我使用的旧版本的使用。代码文件可以在附件里面下载到。
先在pico里面新建一个文件夹,命名为fonts,将vga1_16*32.py和vga2_8*8分别包含进去。16*32和8*8是指字体大小,比如8*8的字体,一个字符长8个像素,宽8个像素。包含完字库后,再包含st7789.py进pico中,他和fonts并列存放
使用方式如下:
初始化配置
import machine
from time import sleep
import st7789
from fonts import vga2_8x8 as font1
from fonts import vga1_16x32 as font2
""" 硬件初始化
这部分代码相对固定,基本上可以当做模板使用。
首先是要初始化SPI,复位引脚RESET,片选引脚CS,用来与屏幕通信。
然后调用st7789的初始化函数。注意啦,我用的st7789是240*240分辨率
"""
spi = machine.SPI(0, baudrate=4000000, phase=0, polarity=1,
sck=machine.Pin(2),
mosi=machine.Pin(3))
reset = machine.Pin(0, machine.Pin.OUT)
dc = machine.Pin(1, machine.Pin.OUT)
oled = st7789.ST7789(spi, 240, 240, reset, dc)
"""
下面是常用函数的使用,我会逐一解释
"""
oled.fill(st7789.BLACK)
屏幕填充
oled.fill(st7789.BLACK)
"""
定义:fill(self, color):
参数:
color:颜色,具体可以查看st7789.py文件,就在文件的开头,这里举几个 例子:BLACK,BLUE,RED。。。
注意:
很多时候我都用它来刷屏,比如更改设置的时候,要改太多,暴躁老哥直接刷屏。重启解决99%的问题 ^_^
"""
画点
pixel(100, 100, st7789.RED)
"""
定义:pixel(self, x, y, color):
参数:
x:横坐标
y:纵坐标
color:点的颜色
注意:
无。没有为啥写?看起来优雅。
"""
画线
oled.line(0, 0, 100, 100, st7789.RED)
"""
定义:line(self, x0, y0, x1, y1, color):
参数:
x0:起点横坐标
y0:起点纵坐标
x1:终点横坐标
y1:终点纵坐标
color:线的颜色
注意:
这个函数默认画点是连续的,不会跨着像素点画,这样画的线效果好,但是不可避免的有画的慢的问题,特是在
micropython这样运行速度慢的程序里表现的非常明显。这时候就要去st7789里面,把函数的内容更改。就在
这个函数的最后一行,我发的文件里的448行:x0 += 1。改成+5。这个开源项目里面就涉及到这个问题。
"""
写字
oled.text(font1, "VPP:", 120, 120)
"""
定义:text(self, font, text, x0, y0, color=WHITE, background=BLACK):
参数:
font:使用的字体
text:需要打印的字符串
x0:起点的横坐标
y0:起点的纵坐标
color:字体颜色,默认白色
BLACK:背景颜色,默认黑色
注意:
无。没有为啥写?看起来优雅。
"""
2、machine库
下面介绍的是项目中使用到的外设,由于比较基础,这里就只把他们的使用代码示例贴出来
ADC
potentiometer = machine.ADC(26)#初始化ADC
potentiometer.read_u16()#读取ADC的值,返回16位数
定时器
tim1 = machine.Timer()#初始化定时器
tim1.init(freq=1.5, mode=machine.Timer.PERIODIC, callback=irq_init)#设置频率和是否周期,还有对应的中断函数,这个函数需要自己def
按键(PIN+外部中断
key = machine.Pin(6, machine.Pin.IN)#初始化PIN引脚
key.irq(trigger=machine.Pin.IRQ_RISING, handler=origin)#设置触发方式,中断处理函数
3、FFT的实现
一开始想选用scipy的fft包来做FFT的,但是折腾了一翻过后,发现micropython不支持scipy,这里作为前车之鉴。micropython作为简化的python,有很多库是不支持的。最终选择使用pyhon根据网上的教程,写一个FFT函数。
class FFT_pack:
"""FFT算法
这里进行FFT算法,通过对输入的list列表进行复数FFT
返回list列表型变量。
"""
def __init__(self, _list=[], N=0): # _list 是传入的待计算的离散序列,N是序列采样点数,对于本方法,点数必须是2^n才可以得到正确结果
self.list = _list # 初始化数据
self.N = N
self.total_m = 0 # 序列的总层数
self._reverse_list = [] # 位倒序列表
self.output = [] # 计算结果存储列表
self._W = [] # 系数因子列表
for _ in range(len(self.list)):
self._reverse_list.append(self.list[self._reverse_pos(_)])
self.output = self._reverse_list.copy()
for _ in range(self.N):
# 提前计算W值,降低算法复杂度
self._W.append((cos(2 * pi / N) - sin(2 * pi / N) * 1j) ** _)
def _reverse_pos(self, num) -> int: # 得到位倒序后的索引
out = 0
bits = 0
_i = self.N
data = num
while (_i != 0):
_i = _i // 2
bits += 1
for i in range(bits - 1):
out = out << 1
out |= (data >> i) & 1
self.total_m = bits - 1
return out
def FFT(self, _list, N) -> list: # 计算给定序列的傅里叶变换结果,返回一个列表,结果是没有经过归一化处理的
"""参数abs=True表示输出结果是否取得绝对值"""
self.__init__(_list, N)
for m in range(self.total_m):
_split = self.N // 2 ** (m + 1)
num_each = self.N // _split
for _ in range(_split):
for __ in range(num_each // 2):
temp = self.output[_ * num_each + __]
temp2 = self.output[_ * num_each + __ + num_each //
2] * self._W[__ * 2 ** (self.total_m - m - 1)]
self.output[_ * num_each + __] = (temp + temp2)
self.output[_ * num_each + __ +
num_each // 2] = (temp - temp2)
return self.output
2、整体实现
这一部分我将用注释的方式来介绍代码的实现。
"""
整体布局:
可以参考上图
0-120上半部分的屏幕用来显示波形
120-240下半部分屏幕用来显示频谱,为了显示明显,有时会存在跑到上半部分屏幕的情况
功能实现:
通过ADC去采集麦克风上面的音频信号,将信号用波形和频谱的方式打印出来。
波形支持在动态刷新的情况下上下移动和缩放波形。
波形可以暂停,暂停的情况下可以左右移动。
测量波形的峰峰值和最大能量的频率分量的频率值
"""
import machine
from time import sleep
import st7789
from fonts import vga2_8x8 as font1
from fonts import vga1_16x32 as font2
from cmath import sin, cos, pi # 复数库,用于FFT
""" 硬件初始化
OLED+按键+连接麦克风的ADC
注意:
无
"""
spi = machine.SPI(0, baudrate=4000000, phase=0, polarity=1,
sck=machine.Pin(2),
mosi=machine.Pin(3))
reset = machine.Pin(0, machine.Pin.OUT)
dc = machine.Pin(1, machine.Pin.OUT)
oled = st7789.ST7789(spi, 240, 240, reset, dc)
oled.fill(st7789.BLACK)
key = machine.Pin(6, machine.Pin.IN)
keyleft = machine.Pin(4, machine.Pin.IN)
keyright = machine.Pin(5, machine.Pin.IN)
key1 = machine.Pin(8, machine.Pin.IN)
key2 = machine.Pin(7, machine.Pin.IN)
key = machine.Pin(6, machine.Pin.IN)
potentiometer = machine.ADC(26) # 这个ADC连接着mi头
oled.text(font2, "3", 1, 1) # 显示波的放大级别,越大越明显
mo = 3 # 控制波形放大倍数,数字越大,放大倍数越大
dif = 60 # 用于控制音频波形的中心位置
k = 0 # 输出到屏幕的横坐标
# 采样率
# 因为用于演示噪声的频率普遍低,采样率设置为1k
# 如果测量人说话的声音,这里要设置成10k左右
fs = 1000
stop = 0 # stop为1时暂停波形,为0时动态刷新波形
star_index = 20 # 用于左右移动波形,默认是从wave[20]-wave[99]
star_index_last = 20 # 上一次的波形移动位置,用于擦除上次的波形
adcbuff = [0 for i in range(0, 120, 1)] # ADC采集到的16位电压直接存放在这里
wave = [0 for i in range(0, 120, 1)] # 存储最新的波形
wave_last = [0 for i in range(0, 120, 1)] # 存储上次的波形,用于擦除
############################fft部分变量和函数#######################
fftin = [0 for i in range(0, 64, 1)] # 用于计算频谱的数据,这里做64个数的FFT
fftbuff = [0 for i in range(0, 64, 1)] # 用于显示频谱
fftbuff_last = [0 for i in range(0, 64, 1)] # 用于擦除上次的频谱
class FFT_pack:
"""FFT算法
这里进行FFT算法,通过对输入的list列表进行复数FFT
返回list列表型变量。
"""
def __init__(self, _list=[], N=0): # _list 是传入的待计算的离散序列,N是序列采样点数,对于本方法,点数必须是2^n才可以得到正确结果
self.list = _list # 初始化数据
self.N = N
self.total_m = 0 # 序列的总层数
self._reverse_list = [] # 位倒序列表
self.output = [] # 计算结果存储列表
self._W = [] # 系数因子列表
for _ in range(len(self.list)):
self._reverse_list.append(self.list[self._reverse_pos(_)])
self.output = self._reverse_list.copy()
for _ in range(self.N):
# 提前计算W值,降低算法复杂度
self._W.append((cos(2 * pi / N) - sin(2 * pi / N) * 1j) ** _)
def _reverse_pos(self, num) -> int: # 得到位倒序后的索引
out = 0
bits = 0
_i = self.N
data = num
while (_i != 0):
_i = _i // 2
bits += 1
for i in range(bits - 1):
out = out << 1
out |= (data >> i) & 1
self.total_m = bits - 1
return out
def FFT(self, _list, N) -> list: # 计算给定序列的傅里叶变换结果,返回一个列表,结果是没有经过归一化处理的
"""参数abs=True表示输出结果是否取得绝对值"""
self.__init__(_list, N)
for m in range(self.total_m):
_split = self.N // 2 ** (m + 1)
num_each = self.N // _split
for _ in range(_split):
for __ in range(num_each // 2):
temp = self.output[_ * num_each + __]
temp2 = self.output[_ * num_each + __ + num_each //
2] * self._W[__ * 2 ** (self.total_m - m - 1)]
self.output[_ * num_each + __] = (temp + temp2)
self.output[_ * num_each + __ +
num_each // 2] = (temp - temp2)
return self.output
###################################################################
tim1 = machine.Timer() # 用于初始化按键中断
tim2 = machine.Timer() # 用于ADC采样
spin_ready = 1
down_ready = 1
stop_ready = 1
up_ready = 1
"""每个按键对应的的服务函数
ADC_SAMPLE:adc的采样中断,由定时器触发
UP:波形向上移动 KEY1
DOWN:波形向下移动 KEY2
ORIGINL:暂停/恢复屏幕
SPIN_HANDLER:处理旋转按钮,来调节分辨等级
IRQ_INIT:重新使能中断
"""
def adc_sample(Pin):
global adcbuff
global k
if k < 120: # 采集120个数为一组
adcbuff[k] = potentiometer.read_u16()
k = k+1
def up(Pin):
global dif
global up_ready
global stop
global star_index
global exc
if up_ready == 1:
up_ready = 0
dif += 10
if dif > 120:
dif = 120
print("下移")
def down(Pin):
global dif
global down_ready
if down_ready == 1:
down_ready = 0
dif -= 10
if dif < 0:
dif = 0
print("上移")
def origin(Pin):
global stop
global stop_ready
if stop_ready == 1:
if stop == 0:
stop = 1
else:
stop = 0
stop_ready = 0
def spin_handler(Pin): # 分辨率调节F
global spin_ready
global mo
global dif
global star_index
global star_index_last
global i
global oled
if spin_ready == 1:
spin_ready = 0
if stop == 0: # 如果在正常运行
if keyleft.value() == 0:
if keyright.value() == 1:
print("顺时针")
if mo <= 5 and mo > 1:
mo = mo-1
elif keyright.value() == 0:
print("逆时针")
if mo >= 1 and mo < 5:
mo = mo+1
elif keyleft.value() == 1:
if keyright.value() == 0:
print("顺时针")
if mo <= 5 and mo > 1:
mo = mo-1
elif keyright.value() == 1:
print("逆时针")
if mo >= 1 and mo < 5:
mo = mo+1
oled.fill(st7789.BLACK)
if mo == 5:
oled.text(font2, "5", 1, 1)
if mo == 4:
oled.text(font2, "4", 1, 1)
if mo == 3:
oled.text(font2, "3", 1, 1)
if mo == 2:
oled.text(font2, "2", 1, 1)
if mo == 1:
oled.text(font2, "1", 1, 1)
else: # 如果处于暂停状态
if keyleft.value() == 0:
if keyright.value() == 1:
print("顺时针")
star_index_last = star_index
star_index = star_index-5
if star_index < 0:
star_index = 0
for i in range(0, 80, 1):
# 清除上次画的点,可以把他注释掉试试,会发现产生了白色冲击波
oled.pixel(
i*3, wave_last[i+star_index_last], st7789.BLACK)
# micropython运行速度慢,所以要跳着显示,少显示一部分点
oled.pixel(i*3, wave[i+star_index], st7789.WHITE)
elif keyright.value() == 0:
print("逆时针")
star_index_last = star_index
star_index = star_index+5
if star_index > 40:
star_index = 40
for i in range(0, 80, 1):
# 清除上次画的点,可以把他注释掉试试,会发现产生了白色冲击波
oled.pixel(
i*3, wave_last[i+star_index_last], st7789.BLACK)
# micropython运行速度慢,所以要跳着显示,少显示一部分点
oled.pixel(i*3, wave[i+star_index], st7789.WHITE)
elif keyleft.value() == 1:
if keyright.value() == 0:
print("顺时针")
star_index_last = star_index
star_index = star_index-5
if star_index < 0:
star_index = 0
for i in range(0, 80, 1):
# 清除上次画的点,可以把他注释掉试试,会发现产生了白色冲击波
oled.pixel(
i*3, wave_last[i+star_index_last], st7789.BLACK)
# micropython运行速度慢,所以要跳着显示,少显示一部分点
oled.pixel(i*3, wave[i+star_index], st7789.WHITE)
elif keyright.value() == 1:
print("逆时针")
star_index_last = star_index
star_index = star_index+5
if star_index > 40:
star_index = 40
for i in range(0, 80, 1):
# 清除上次画的点,可以把他注释掉试试,会发现产生了白色冲击波
oled.pixel(
i*3, wave_last[i+star_index_last], st7789.BLACK)
# micropython运行速度慢,所以要跳着显示,少显示一部分点
oled.pixel(i*3, wave[i+star_index], st7789.WHITE)
def irq_init(Time):
global spin_ready
global up_ready
global down_ready
global stop_ready
spin_ready = 1
down_ready = 1
stop_ready = 1
up_ready = 1
"""
如果想消除旋转过快带来的影响,就必须要加延时,可是总不能在中断里延时吧?
先不说耽误主程序运行,光考虑有概率卡死就不建议使用。
因此将旋转编码器设置成定时器中断开启,频率为1.5。
实测频率为2有小概率出现判断出错,1基本不会出错,1但是太慢了。
"""
tim1.init(freq=1.5, mode=machine.Timer.PERIODIC, callback=irq_init)
"""
用定时器中断去触发ADC,中断频率是FS,也就是采样率是FS。不知道可不可以像stm32一样改成硬件触发,DMA传输。
"""
tim2.init(freq=fs, mode=machine.Timer.PERIODIC, callback=adc_sample)
"""
按键初始化
"""
keyleft.irq(trigger=machine.Pin.IRQ_FALLING |
machine.Pin.IRQ_RISING, handler=spin_handler)
key1.irq(trigger=machine.Pin.IRQ_RISING, handler=up)
key2.irq(trigger=machine.Pin.IRQ_RISING, handler=down)
key.irq(trigger=machine.Pin.IRQ_RISING, handler=origin)
while True:
if k >= 120:
while(stop): # 如果波形暂停了,主程序就暂停在这里
pass
for i in range(0, 64, 1):
fftin[i] = int((adcbuff[i]-33500)/300)#这里的除以300是为了方便波形处理,没有实际含义
for i in range(0, 120, 1):
wave[i] = int((adcbuff[i]-33500) / 600 / (6-mo))+dif#这里的除以600是为了方便波形显示,没有实际含义
##########防止溢出##########
if wave[i] > 120:
wave[i] = 120
if wave[i] < 1:
wave[i] = 1
#####################
for i in range(0, 80, 1):
# 清除上次画的点,可以把他注释掉试试,会发现产生了白色冲击波
oled.pixel(i*3, wave_last[i+star_index], st7789.BLACK)
# micropython运行速度慢,所以要跳着显示,少显示一部分点
oled.pixel(i*3, wave[i+star_index], st7789.WHITE)
for i in range(0, 120, 1):
wave_last[i] = wave[i]
fftbuff = FFT_pack().FFT(fftin, 64)
index = 0
for i in range(0, 32, 1):
fftbuff[i] = (int)(abs(fftbuff[i])/20) # 整数化,方便画线
if fftbuff[index] < fftbuff[i]: # 寻找最大值的地址
index = i
fq = round(index*fs/64, 3) # 获取频率,保留3位小数
##########求出峰峰值##########
vpp_val = (max(adcbuff)-min(adcbuff))*3.3/65536
oled.text(font1, "VPP:", 120, 120)
oled.text(font1, str(round(vpp_val, 3)), 160, 120)
oled.text(font1, "FQ:", 120, 135)
oled.text(font1, str(fq), 160, 135)
#####################
for i in range(0, 32, 1):
"""line函数存在修改
因为micropython运行慢,我将st7789.py文件中,line函数进行了修改
改为每隔5个像素点画一个点,从实线变成了虚线。
少画点,运行快
"""
oled.line(i*7, 240, i*7, 240-fftbuff_last[i], st7789.BLACK)
oled.line(i*7, 240, i*7, 240-fftbuff[i], st7789.WHITE)
fftbuff_last = fftbuff
k = 0 # 再次开启采集
三:后记
除了选择用thonny开发外,还可以选择使用VSCODE开发。官方为VSCODE写好了一个扩展插件Pico-Go。下载完插件后,需要配置环境,我最终配置下来,下载可以下载进去,但是程序运行始终异常,最终选择了在VSCODE里面写好代码,复制到Thonny里下载调试。附上插件的配置教程:pico-go.net/docs/start/quick/ (这个教程是在插件的“细节”页面找到的)
python的开发难度比C/C++低不少,易于上手,但是速度上不及C/C++。通过这次项目的开发,我算是掌握了基本的micropython开发,但是感觉一时用不到,因为很多地方对速度敏感,比如电赛仪表题吧,因为对指标的极致追求,即便我学会了micropython开发stm32,仍然会选择用C开发。不可否认,在开发时间敏感,速度不敏感的场合,micropython有独当一面的优势。
我把我学习过程中收集的学习资料列一下,方便后来者学习使用:(2021-8月,我相信随着时间的推移会有更好的,更加成熟的教程出现。)
https://docs.micropython.org/en/latest/rp2/quickref.html#delay-and-timing ——官方的使用教程
https://class.eetree.cn/live_pc/l_60fe7f4fe4b0a27d0e360f74 ——硬禾学堂的直播录像,适合第一次使用pico
https://www.eetree.cn/project/detail/72 ——硬禾学堂的开源项目,主要看他的案例。
https://www.eetree.cn/project/detail/103 ——我参加的活动地址,这里也有很多案例。
https://www.raspberrypi.org/documentation/rp2040/getting-started/ ——官网地址,这里有许多电子资料
https://www.bilibili.com/video/BV1ZK411c7yf ——韦神的教程,可以学习到基础的外设使用
我学习过程中搜集了一些电子书教程,已经打包好放在了附录里。