来自Peter Hinch在Github上的项目分享 它是一个轻量级的、可移植的MicroPython GUI库,用于显示,并从framebuf子类化了驱动程序。它是用Python编写的,可以在标准的MicroPython固件构建下运行。输入资料的选项包括:

  1. 根据应用程序,通过2到5个按钮。
  2. 通过一个开关导航操纵杆。
  3. 通过两个按钮和一个像这样的旋转编码器。

由于对输入的支持,它比nano-gui更大、更复杂。它可以在屏幕之间切换和启动模态窗口。除了nano-gui小部件外,它还支持列表框、下拉列表、各种输入或显示浮点值的方法以及其他小部件。 它与nano-gui的所有显示驱动程序兼容,因此可移植到各种显示器上。它还可以在主机之间移植。

# Arbitray waveform generator (AWG) with GUI - in short AWG -
#
# 
# updated:
version_date = '03-Oct-2021'
# RP2040 based arbitrary wave form generator (AWG) with GUI
#
# GUI based on micro-gui released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2021 Peter Hinch
#
# AWG based on "Arbitrary waveform generator for Rasberry Pi Pico"
# Rolf Oldeman, 13/2/2021. CC BY-NC-SA 4.0 licence
 
# AWG Hardware modified for 10bit R2R ladder network DAC
# AWG sofware modified to fit 10bit and AWG UI use model
 
# micro python package consists of:
# ui.py             -this file- user interface and generator controls
# wave_gen.py       calculates wave form and starts PIO (generator)
# hardware_setup.py display driver and ui initialisation
# main.py           standard micropython file starts imports ui.py
 
 
# Minimize memory fragmentation to avoid running out of memory:
# Hardware_setup must be imported first before other modules because of RAM use.
# Next buffers for AWG are created, then AWG functions are imported, 
# Finally all gui functions are imported
# garbage collector gc is called at specific points during imports and run time
 
# 
import hardware_setup  # Create a display instance
import gc
 
gc.collect() # precaution to free up unused RAM
 
# make buffers for the waveform.
# large buffers give better results but are slower to fill
# buffer and sample size used are calculated dynamically based on frequency of AWG
# in function setup_wave in wage_gen module
wavbuf={}
maxnsamp= 512   # must be a multiple of 4
wavbuf[0]=bytearray(maxnsamp*2)
#wavbuf[1]=bytearray(maxnsamp*2)
#ibuf=0
#remark: ibuf and wavbuf[1] are for later implementation
 
# AWG_status flag:
# status:   Meaning:
# -------   --------
# stopped     generator output stopped, new set up trigger is allowed, initialization status
# calc wave   generetor set up is trigger, wave form is calculated, no further trigger is allowed
# running     calculation finished, generator output active, new set up trigger is allowed
# -- init --  intitialization, generator not yet started using start button
 
#import AWG functions
from wave_gen import *
#define wave with init defaults
wave = {'func' : sine,
        'frequency' : 40000,
        'amplitude' : 0.5,
        'offset' : 0.5,
        'phase' : 0,
        'replicate' : 1,
        'pars' : [0.2, 0.4, 0.2],
        'frequency_value' : 40000,
        'freq_range' : 1,
        'AWG_status' : '- init -',
        'nsamp' : 0,
        'F_out' : 0,}
 
# amplitude max default values to avoid overdriving AD8055 input stage
max_ampl = {'sine' : 0.48,
            'pulse' : 0.89,
            'gauss' : 0.55,
            'sinc' : 0.5,
            'expo' : 0.5,
            'noise' : 1,
            }
gc.collect() # precaution to free up unused RAM
 
# import gui functions
from gui.core.ugui import Screen, Window, ssd
 
from gui.widgets.label import Label
from gui.widgets.buttons import Button, CloseButton
from gui.widgets.dropdown import Dropdown
from gui.widgets.sliders import HorizSlider
from gui.widgets.scale_log import ScaleLog
from gui.core.writer import CWriter
 
# set font for CWriter
import gui.fonts.font6 as font      # FreeSans 14 pix
 
from gui.core.colors import *
import sys
from machine import Pin, freq
import utime
import utime
 
# set GP23 to high to switch Pico power supply from PFM to PWM to reduce PWS noise
GP23 = Pin(23, Pin.OUT)
GP23.value(1)
 
gc.collect()    # precaution to free up unused RAM
 
 
#======= define UI screen =======
 
head_line = ' Arbirtaty wave form generator       ws'
version = version_date
 
 
class BaseScreen(Screen):
 
    def __init__(self):
 
 
        startstop_buttons = []
 
        table_startstop_buttons = (
            {'text' : 'setup', 'args' : ('setup',), 'bgcolor' : LIGHTGREEN, 'bdcolor' : False, 'litcolor' : GREEN},
            {'text' : 'stop', 'args' : ('stop',), 'bgcolor' : LIGHTRED, 'bdcolor' : False, 'litcolor' : RED},
        )
 
        # definition of call backs
        # call backs need to be defined first as they are called further down when gui input functions are defined
        def startstop_cb(button, val):
            gc.collect()
            #print('0: mem at startstop callback', gc.mem_free())
 
 
            if val == 'setup':
 
                wave['AWG_status']='calc wave'
                update_status(wave['AWG_status'])
                wave['frequency'] = wave['frequency_value'] * wave['freq_range']
 
                setupwave(wavbuf[0],wave)
 
                update_status(wave['AWG_status'])
                nsamp_lbl.value(str(wave['nsamp']))
 
                # due to digital wave synthesis, AWG frequency can deviate from frequency entered
                # actual frequency is calculated while wave is generated, displayed to the user here
                f_out = wave['F_out']
                if f_out > 999999:
                    fout_lbl.value('{:7.3f}'.format(f_out/1000000) + ' MHz')
                elif f_out > 999:
                    fout_lbl.value('{:7.3f}'.format(f_out/1000) + ' kHz')
                else:
                    fout_lbl.value('{:7.3f}'.format(f_out) + ' Hz')
 
 
            elif val == 'stop':
                wave['AWG_status']='stopped'
                update_status(wave['AWG_status'])
                wave['nsamp'] = 0
                nsamp_lbl.value(str(wave['nsamp']))
                fout_lbl.value('0' + ' Hz')
                stopDMA()
 
            else:
                print('wrong button received')
 
 
 
        def function_cb(dd):
            fun = dd.textvalue()
            # for each function 
            # enable/disable function parameter controls as required by function
            if fun == 'sine':
                rise_Slider.greyed_out(val=1)
                up_Slider.greyed_out(val=1)
                fall_Slider.greyed_out(val=1)
                width_Slider.greyed_out(val=1)
                noise_Slider.greyed_out(val=1)
                expo_Slider.greyed_out(val=1)
                wave['func'] = sine
                wave['replicate'] = 1
                Amplitude.value(max_ampl['sine'])
                Offset.value(0.5)
                wave['amplitude'] = max_ampl['sine']
                wave['offset'] = 0.5
 
            elif fun == 'pulse':
                width_Slider.greyed_out(val=1)
                rise_Slider.greyed_out(val=0)
                up_Slider.greyed_out(val=0)
                fall_Slider.greyed_out(val=0)
                noise_Slider.greyed_out(val=1)
                expo_Slider.greyed_out(val=1)
                wave['func'] = pulse
                wave['replicate'] = 1
                Amplitude.value(max_ampl['pulse'])
                Offset.value(0)
                rise_Slider.value(0.05)
                wave['amplitude'] = max_ampl['pulse']
                wave['offset'] = 0
                wave['pars'][0] = 0.05
 
 
            elif fun == 'gauss':
                width_Slider.greyed_out(val=0)
                rise_Slider.greyed_out(val=1)
                up_Slider.greyed_out(val=1)
                fall_Slider.greyed_out(val=1)
                noise_Slider.greyed_out(val=1)
                expo_Slider.greyed_out(val=1)
                wave['func'] = gaussian
                wave['replicate'] = 1
                Amplitude.value(max_ampl['gauss'])
                Offset.value(0)
                width_Slider.value(0.1)
                wave['amplitude'] = max_ampl['gauss']
                wave['offset'] = 0
                wave['pars'][0] = 0.023
 
 
            elif fun == 'noise':
                width_Slider.greyed_out(val=1)
                rise_Slider.greyed_out(val=1)
                up_Slider.greyed_out(val=1)
                fall_Slider.greyed_out(val=1)
                noise_Slider.greyed_out(val=0)
                expo_Slider.greyed_out(val=1)
                wave['func'] = noise
                wave['replicate'] = 1
                Offset.value(0)
                noise_Slider.value(0.5)
                wave['pars'][0] = 4
 
            elif fun == 'sinc':
                width_Slider.greyed_out(val=0)
                rise_Slider.greyed_out(val=1)
                up_Slider.greyed_out(val=1)
                fall_Slider.greyed_out(val=1)
                noise_Slider.greyed_out(val=1)
                expo_Slider.greyed_out(val=1)
                wave['func'] = sinc
                wave['replicate'] = 1
                Amplitude.value(max_ampl['sinc'])
                Offset.value(0.5)
                width_Slider.value(0.1)
                wave['amplitude'] = 0.5
                wave['offset'] = 0.5
                wave['pars'][0] = 0.03
 
            elif fun == 'expo':
                width_Slider.greyed_out(val=0)
                rise_Slider.greyed_out(val=1)
                up_Slider.greyed_out(val=1)
                fall_Slider.greyed_out(val=1)
                noise_Slider.greyed_out(val=1)
                expo_Slider.greyed_out(val=0)
                wave['func'] = exponential
                wave['replicate'] = -1
                Amplitude.value(max_ampl['expo'])
                Offset.value(0)
                width_Slider.value(0.1)
                expo_Slider.value(0.49)
                wave['amplitude'] = max_ampl['expo']
                wave['offset'] = 0
                wave['pars'][0] = 0.1
 
            else:
                print('no valid function selected')
 
 
        # define call backs for each UI element changable by buttons and encoder
        def amplitude_cb(s):
            v = s.value()
            wave['amplitude'] = v
 
        def offset_cb(s):
            v = s.value()
            wave['offset'] = v
 
        def freqlog_cb(f):
            v = f.value()
            if v < 80:
                freq_lbl.value('{:3.1f}'.format(2*v))    
            else:
                freq_lbl.value('{:4.0f}'.format(2*v))
            wave['frequency_value'] = int(2*v)
 
        def freq_range_cb(dd):
            f = dd.textvalue()
            if f == 'Hz':
                wave['freq_range'] = 1
            if f == 'kHz':
                wave['freq_range'] = 1000
 
        def rise_cb(s):
            v = s.value()
            rise_lbl.value('{:0.3f}'.format(v))
            wave['pars'][0] = v
 
        def up_cb(s):
            v = s.value()
            up_lbl.value('{:0.3f}'.format(v))
            wave['pars'][1] = v
 
        def fall_cb(s):
            v = s.value()
            fall_lbl.value('{:0.3f}'.format(v))
            wave['pars'][2] = v
 
        def width_cb(s):
            v = s.value()
            width_lbl.value('{:0.3f}'.format(v*0.2+0.003))
            wave['pars'][0] = v*0.2+0.003
 
        def expo_cb(s):
            v = s.value()
            if v < 0.5:
                repli = -1
            else:
                repli = 1
            #print('v= ', v, 'repli= ', repli)
            expo_lbl.value('{:1.0f}'.format(repli))
            wave['replicate'] = repli
 
        def noise_cb(s):
            v = s.value()
            noise_lbl.value('{:1.0f}'.format(int(v*8)))
            wave['pars'][0] = int(v*8)
 
        def update_status(s):
            if s == 'stopped':
                status_lbl.value(text = s, bdcolor = None, bgcolor = LIGHTRED, fgcolor = WHITE)
            elif s == 'calc wave':
                status_lbl.value(text = s, bdcolor = None, bgcolor = BLACK, fgcolor = ORANGE)
            elif s == 'running':
                status_lbl.value(text = s, bdcolor = None, bgcolor = LIGHTGREEN, fgcolor = WHITE)
            elif s == '- init -':
                status_lbl.value(text = s, bdcolor = None, bgcolor = DARKBLUE, fgcolor = ORANGE)
            else:
                status_lbl.value(text = 'no stat' , bdcolor = RED, bgcolor = WHITE, fgcolor = RED)
 
        # legend_cb shows only k(Hz) in the scale. This is consistent (my view)
        #   example 1 MHZ will show as 1000 kHz
 
        def legend_cb(f):
            if f < 1999:
                return '{:<1.0f}'.format(2*f)
            return '{:<1.0f}K'.format(2*f/1000)
 
        # ======== instantiate screen and writer =======
        super().__init__()
        wri = CWriter(ssd, font, GREEN, BLACK, verbose=False)
 
        # headline and version
        col = 2
        row = 2
        Label(wri, row, col, head_line + version, fgcolor = BLUE, bgcolor = LIGHTGREY)
 
 
        # create function labels
        row = 25
        Label(wri, row, col, 'Function:')
        row -=2
        Label(wri, row, col+215, 'AWG:', fgcolor = BLUE)
 
        status_lbl=Label(wri, row, col+260, 65, bgcolor = ORANGE, fgcolor = BLACK)
        update_status(wave['AWG_status'])
 
        row += 45
        Label(wri, row, col, 'Frequency:', fgcolor = CYAN)
        row += 45
        Label(wri, row, col, 'Amplitude:', fgcolor = LIGHTGREY)
        col += 170
        Label(wri, row, col, 'Offset:', fgcolor = LIGHTGREY)
 
        row = 40
        col = 215
        Label(wri, row, col, 'samples:', fgcolor = BLUE)
        nsamp_lbl = Label(wri, row, col+60, '000', bgcolor = BLUE, fgcolor = WHITE)
 
        # ======= create AWG controls =======
 
        # dropdown for function
        col = 80
        row = 22
        Dropdown(wri, row, col, callback=function_cb,
                elements = ('sine', 'pulse', 'gauss', 'sinc', 'expo', 'noise'),
                bdcolor = GREEN, bgcolor = DARKGREEN)
 
 
        # FREQUENCY: slider and frequency range dropdown
        # Instantiate Label first, because Slider callback will run now.
 
        row +=35
        freq_lbl = Label(wri, row+11, col+120, 50, bdcolor=CYAN, fgcolor=YELLOW)
 
        ScaleLog(wri, row-5, col, width = 110, legendcb = legend_cb,
                pointercolor=RED, fontcolor=YELLOW, bdcolor=CYAN,
                callback=freqlog_cb, value=1000, decades = 4, active=True)
 
        Dropdown(wri, row+10, col+180, callback=freq_range_cb, elements = ('Hz', 'kHz'),
                bdcolor = CYAN, fgcolor = YELLOW, bgcolor = DARKGREEN)
 
        # Amplitude and offset sliders
        row +=60
        Amplitude = HorizSlider(wri, row, col, callback=amplitude_cb,
               divisions = 10, width = 70, height = 12, fgcolor = LIGHTGREY, bdcolor=ORANGE, slotcolor=BLUE,
               legends=('0', '0.5', '1'), value=0.5, active=True)
 
        Offset = HorizSlider(wri, row, col+150, callback=offset_cb,
               divisions = 10, width = 70, height = 12, fgcolor = LIGHTGREY, bdcolor=ORANGE, slotcolor=BLUE,
               legends=('0', '0.5', '1'), value=0.5, active=True)
 
        # Parameters for pulse will fill pars[0], pars[1] and pars[2].
        # implemented as "hidden sliders" and value label
        # as micro-gui doesn not have a active numerical label(yet), sliders with minimal width are used for numerical input
        # number is displayed by a label next to slider
        row +=35
        col = 2
        Label(wri, row, col, 'rise', fgcolor = BLUE)
        rise_lbl = Label(wri, row, col+50, 40, bdcolor=False, fgcolor=LIGHTGREY)
        rise_Slider = HorizSlider(wri, row, col+30, callback=rise_cb, fgcolor=BLUE, bdcolor=False, slotcolor=BLUE, width=16,
                legends=None, value=0.05, active=True)
        rise_Slider.greyed_out(1)
 
        col = 110
        Label(wri, row, col, 'up', fgcolor = BLUE)
        up_lbl = Label(wri, row, col+40, 40, bdcolor=False, fgcolor=LIGHTGREY)
        up_Slider = HorizSlider(wri, row, col+20, callback=up_cb, fgcolor=BLUE, bdcolor=False, slotcolor=BLUE, width=16,
                legends=None, value=0.5, active=True)
        up_Slider.greyed_out(1)
 
        col = 220
        Label(wri, row, col, 'fall', fgcolor = BLUE)
        fall_lbl = Label(wri, row, col+50, 40, bdcolor=False, fgcolor=LIGHTGREY)
        fall_Slider = HorizSlider(wri, row, col+25, callback=fall_cb, fgcolor=BLUE, bdcolor=False, slotcolor=BLUE, width=16,
                legends=None, value=0.05, active=True)
        fall_Slider.greyed_out(1)
 
        #Parameter "width" for Gauss and Sinc, Paremeters "expo" for Expo and "noiseq" for Noise
        # all will fill parameter pars[0] but are implemented separately as they have have different ranges
        row +=30
        col = 2
        Label(wri, row, col, 'width', fgcolor = BLUE)
        width_lbl = Label(wri, row, col+60, 40, bdcolor=False, fgcolor=LIGHTGREY)
        width_Slider = HorizSlider(wri, row, col+40, callback=width_cb, fgcolor=BLUE, bdcolor=False, slotcolor=BLUE, width=16,
                legends=None, value=0.5, active=True)
        width_Slider.greyed_out(1)
 
        col = 110
        Label(wri, row, col, 'expo', fgcolor = BLUE)
        expo_lbl = Label(wri, row, col+60, 30, bdcolor=False, fgcolor=LIGHTGREY)
        expo_Slider = HorizSlider(wri, row, col+40, callback=expo_cb, fgcolor=BLUE, bdcolor=False, slotcolor=BLUE, width=16,
                legends=None, value=0.49, active=True)
        expo_Slider.greyed_out(1)
 
        col = 200
        Label(wri, row, col, 'noiseq', fgcolor = BLUE)
        noise_lbl = Label(wri, row, col+80, 50, bdcolor=False, fgcolor=LIGHTGREY)
        noise_Slider = HorizSlider(wri, row, col+40, callback=noise_cb, fgcolor=BLUE, bdcolor=False, slotcolor=BLUE, width=16,
                legends=None, value=0.5, active=True)
        noise_Slider.greyed_out(1)
 
        # "Setup" button to start output of the AWG, "stop" button to stop output of the AWG
        col = 80
        row = 210
        for t in table_startstop_buttons:
            startstop_buttons.append(Button(wri, row, col, textcolor = WHITE, fgcolor = BLUE,
                callback=startstop_cb, shape=CLIPPED_RECT, height = 25, **t))
            col +=80 # second button distance to the right
 
 
        col = 220
 
        Label(wri, row-2, col+10, 'Frequency out:', fgcolor = ORANGE)
        fout_lbl = Label(wri, row+14, col+10, 90,fgcolor = ORANGE)
        fout_lbl.value('0' + 'Hz')
 
        # close button to stop the UI disabled, power off switch used insted
        # CloseButton(wri)  # Quit the application
 
 
# start of main program
try:
    gc.collect() # precaution to free up unused RAM after all is initialised
    print('0: starting ui, mem: ', gc.mem_free())
    #run the ui
 
    Screen.change(BaseScreen)
 
except KeyboardInterrupt:
    # DMA must be stopped, when program is interrupted, otherwise RP2040 needs a reset to restat DMA
    print('0: Got ctrl-c')
    stopDMA()
 
except Exception as e:
    print('0: mainloop crashed: ', e)
 
finally:
        print('0 finally: cleaning up')
        stopDMA()
        #sys.exit()
# hardware_setup.py customised for KMRTM28028-SPI 8-Jul-21 ws
 
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2021 Peter Hinch
 
# As written, supports:
# ili9341 240x320 displays on Pi Pico
# Initialisation procedure designed to minimise risk of memory fail
# when instantiating the frame buffer. The aim is to do this as early as
# possible before importing other modules.
 
# WIRING
# Pico      Display
# GPIO Pin
# 5V   na	VCC
# 3v3  36   LED
# IO21 27   RESET 
# IO20 26   D/C
# IO19 25   SD   (AKA MOSI)
# IO18 24   CLK  Hardware SPI0
# GND  23 	GND
# IO17 22   CS
# IO16 21   SDO	 (AKA MISO)
#           Miso is assigned, because SPI0 default pin is used otherwise, not used here
 
# Pushbuttons are wired between the pin and Gnd 
# remark: using hardware debounce results eliminates (rare) hang ups of RP2040
# Pico pin  Function
# IO11 15   Select next control
# IO12 16   Select previous control
# IO13 17   Select / operate current control
# IO14 19   Increase value of current control
# IO15 20   Decrease value of current control
# n/a  18   Gnd
 
from machine import Pin, SPI, freq
import gc
 
from drivers.ili93xx.ili9341 import ILI9341 as SSD
freq(250_000_000)  # RP2 overclock
# Create and export an SSD instance
pdc = Pin(20, Pin.OUT, value=0)  # Arbitrary pins
prst = Pin(21, Pin.OUT, value=1)
pcs = Pin(17, Pin.OUT, value=1)
spi = SPI(0, baudrate=30_000_000, mosi=Pin(19), miso=Pin(16), sck=Pin(18))
gc.collect()  # Precaution before instantiating framebuf
ssd = SSD(spi, pcs, pdc, prst, usd=False)
 
from gui.core.ugui import Display
# Create and export a Display instance
# Define control buttons
nxt = Pin(11, Pin.IN)  # Move to next control
prev = Pin(12, Pin.IN)  # Move to previous control
sel = Pin(13, Pin.IN)  # Operate current control
increase = Pin(15, Pin.IN)  # Increase control's value
decrease = Pin(14, Pin.IN)  # Decrease control's value
display = Display(ssd, nxt, sel, prev, increase, decrease, encoder=4) # with encoder
#display = Display(ssd, nxt, sel, prev, increase, decrease) # with buttons