## 基于MicroPython的Micro-GUI 来自[[https://github.com/peterhinch/micropython-micro-gui|Peter Hinch在Github上的项目分享]] 它是一个轻量级的、可移植的MicroPython GUI库,用于显示,并从framebuf子类化了驱动程序。它是用Python编写的,可以在标准的MicroPython固件构建下运行。输入资料的选项包括: - 根据应用程序,通过2到5个按钮。 - 通过一个开关导航操纵杆。 - 通过两个按钮和一个像这样的旋转编码器。 由于对输入的支持,它比nano-gui更大、更复杂。它可以在屏幕之间切换和启动模态窗口。除了nano-gui小部件外,它还支持列表框、下拉列表、各种输入或显示浮点值的方法以及其他小部件。 它与nano-gui的所有显示驱动程序兼容,因此可移植到各种显示器上。它还可以在主机之间移植。 ### UI # 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 # 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