2021暑期一起练 用 树莓派PICO 完成项目一 音频测量
该作品是“暑期一起练”树莓派PICO活动的项目一的参赛作品。 用树莓派PICO去采集麦克风的音频信号,并进行波形,频谱显示。同时可以进行波形的上下左右移动,以及缩放。
标签
嵌入式系统
MicroPython
树莓派PICO
音频测量
2021暑期一起练
唐承乾
更新2021-09-07
6837

任务要求

  1. 通过PICO内部的ADC采集板上麦克风的音频信号
  2. 通过按键或光电旋转编码器能够左右、上下移动波形;左右、上下缩放波形,按键或旋转编码器的功能可以自己定义
  3. 通过ADC采集音频信号的电压峰峰值,并能够将音频信号的电压峰峰值显示在LCD屏幕上
  4. 对采集到的波形进行FFT变换,得到被测信号的频谱并在LCD上显示出来,并对单频信号显示其频率值

一:环境配置

   1、thonny:

         作为官方推荐的开发软件,thonny页面简洁,基础功能齐全,简单易上手,非常适合初学者(比如我)。安装的教程网上比较多,这里推荐一个硬禾的教学视频https://class.eetree.cn/live_pc/l_60fe7f4fe4b0a27d0e360f74

   2、硬禾学堂树莓派pico平台:

         硬禾学堂为“暑期一起练”制作了一个平台,这平台正是我视频中演示用到的板子他的原理图如下,具体可以参考https://www.eetree.cn/project/detail/103Fg6gesESvY0_ip3V2CN9ZDl4avY7

 

二:程序实现:

程序均使用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并列存放

Fo2gMUA5WU7Sh5SDks_4rXEZkBWJ

使用方式如下:

      初始化配置

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、整体实现

      这一部分我将用注释的方式来介绍代码的实现。

FtmC78m6CivmKGMUGKB3IS8tUs7G

"""
整体布局:
    可以参考上图
    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/ (这个教程是在插件的“细节”页面找到的)

 

Fh14mXVCk-CuXtpKlwkKzj5yotLf

 

 

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   ——韦神的教程,可以学习到基础的外设使用

我学习过程中搜集了一些电子书教程,已经打包好放在了附录里。

 

附件下载
pico学习笔记.zip
里面的很多程序,是我在学习的过程中搜集到,不全是本人写的
搜集到的电子书教程.zip
PICO的数据手册太大,就不上传了,大家可以去官网下载
项目代码.zip
项目的完整代码
团队介绍
南京邮电大学 19级 电子科学与技术专业 爱好DIY。
团队成员
唐承乾
南京邮电大学 电子科学与技术专业 19级
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号