352 lines
11 KiB
Python
352 lines
11 KiB
Python
# (c) Copyright 2023 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
|
#
|
|
# gpu.py - GPU co-processor access and support.
|
|
#
|
|
# - see notes in misc/gpu/README.md
|
|
# - bl = Bootloader, provided by ST Micro in ROM of chip
|
|
# - errors are suppressed so we can boot w/o GPU loaded (factory)
|
|
#
|
|
import utime, struct
|
|
import uasyncio as asyncio
|
|
from machine import Pin
|
|
from ustruct import pack
|
|
|
|
# boot loader ROM response to this I2C address
|
|
BL_ADDR = const(0x64)
|
|
# my GPU micro code responses to this I2C address
|
|
GPU_ADDR = const(0x65)
|
|
|
|
BL_ACK = b'y' # 0x79
|
|
BL_NACK = b'\x1f'
|
|
BL_BUSY = b'v' # 0x76
|
|
|
|
FLASH_START = const(0x0800_0000)
|
|
|
|
def add_xor_check(lst):
|
|
# byte-wise xor over list of bytes (used as a very weak checksum in BL)
|
|
rv = 0x0
|
|
for b in lst:
|
|
rv ^= b
|
|
return bytes(lst + bytes([rv]))
|
|
|
|
class GPUAccess:
|
|
def __init__(self):
|
|
# much sharing/overlap in these pins!
|
|
# - pins are already setup in bootloader, no need to change here
|
|
self.g_reset = Pin('G_RESET') #, mode=Pin.OPEN_DRAIN, pull=Pin.PULL_UP)
|
|
self.g_ctrl = Pin('G_CTRL') #, mode=Pin.OUT_PP, value=1)
|
|
self.mosi_pin = Pin('LCD_MOSI')
|
|
self.sclk_pin = Pin('LCD_SCLK')
|
|
self.g_busy = Pin('G_BUSY', Pin.IN, pull=Pin.PULL_DOWN)
|
|
|
|
from machine import I2C
|
|
self.i2c = I2C(1, freq=400000) # same bus & speed as nfc.py
|
|
|
|
# let the GPU run, but we have SPI for now
|
|
self.g_ctrl(1)
|
|
self.g_reset(1)
|
|
|
|
def bl_cmd_read(self, cmd, expect_len, addr=None, arg2=None, no_final=False):
|
|
# send a one-byte command to bootloader ROM and get response
|
|
# - need len to expect, because limitations of hard i2c on this setup
|
|
i2c = self.i2c
|
|
|
|
self._send_cmd(cmd)
|
|
|
|
if addr is not None:
|
|
if isinstance(addr, int):
|
|
# write 4 bytes of address
|
|
qq = add_xor_check(struct.pack('>I', addr))
|
|
else:
|
|
qq = bytes(addr)
|
|
|
|
i2c.writeto(BL_ADDR, qq)
|
|
|
|
resp = i2c.readfrom(BL_ADDR, 1)
|
|
if resp != BL_ACK:
|
|
raise ValueError('bad addr')
|
|
|
|
if arg2 is not None:
|
|
# write second argument, might be a length or date to be written
|
|
if isinstance(arg2, int):
|
|
i2c.writeto(BL_ADDR, bytes([arg2, 0xff ^ arg2]))
|
|
else:
|
|
i2c.writeto(BL_ADDR, add_xor_check(arg2))
|
|
|
|
resp = i2c.readfrom(BL_ADDR, 1)
|
|
if resp != BL_ACK:
|
|
raise ValueError('bad arg2')
|
|
|
|
if expect_len == 0:
|
|
return
|
|
|
|
# for some commands, first byte of response is length and it can vary
|
|
# - however, they are inconsistent on how they count that and not
|
|
# all commands use it, etc.
|
|
# - tried and failed to check/handle the length here; now caller's problem
|
|
rv = i2c.readfrom(BL_ADDR, expect_len)
|
|
|
|
if not no_final:
|
|
# final ack/nack
|
|
resp = i2c.readfrom(BL_ADDR, 1)
|
|
if resp != BL_ACK:
|
|
raise ValueError(resp)
|
|
|
|
return rv
|
|
|
|
def _wait_done(self):
|
|
for retry in range(100):
|
|
try:
|
|
resp = self.i2c.readfrom(BL_ADDR, 1)
|
|
except OSError: # ENODEV
|
|
#print('recover')
|
|
utime.sleep_ms(50)
|
|
continue
|
|
|
|
if resp != BL_BUSY:
|
|
break
|
|
|
|
#print('busy')
|
|
utime.sleep_ms(20)
|
|
|
|
return resp
|
|
|
|
def _send_cmd(self, cmd):
|
|
# do just the cmd + ack part
|
|
self.i2c.writeto(BL_ADDR, bytes([cmd, 0xff ^ cmd]))
|
|
resp = self.i2c.readfrom(BL_ADDR, 1)
|
|
if resp != BL_ACK:
|
|
raise ValueError('unknown command')
|
|
|
|
def bl_doit(self, cmd, arg):
|
|
# send a one-byte command and an argument, wait until done
|
|
self._send_cmd(cmd)
|
|
|
|
self.i2c.writeto(BL_ADDR, add_xor_check(arg))
|
|
|
|
return self._wait_done()
|
|
|
|
def bl_double_ack(self, cmd):
|
|
# some commands need two acks because they do stuff during that time?
|
|
self._send_cmd(cmd)
|
|
resp = self._wait_done()
|
|
if resp == BL_ACK:
|
|
return self._wait_done()
|
|
return resp
|
|
|
|
def reset(self):
|
|
# Pulse reset and let it run
|
|
self.g_reset(0)
|
|
self.g_reset(1)
|
|
|
|
def enter_bl(self):
|
|
# Get it into bootloader. Reliable. Still allows SWD to work.
|
|
# XXX doesn't seem to work anymore?
|
|
self.g_reset(0)
|
|
g_boot0 = Pin('G_BUSY', mode=Pin.OUT_PP, value=1)
|
|
self.g_reset(1)
|
|
g_boot0.init(mode=Pin.IN, pull=Pin.PULL_DOWN) # restore self.g_busy operation
|
|
|
|
def bl_version(self):
|
|
# assume already in bootloader
|
|
return self.bl_cmd_read(0x0, 20)
|
|
|
|
def bulk_erase(self):
|
|
# "No-Stretch Erase Memory" with 0xFFFF arg = "global mass erase"
|
|
return self.bl_doit(0x45, b'\xff\xff') == BL_ACK
|
|
|
|
def readout_unprotect(self):
|
|
# "No-Stretch Readout Unprotect" -- may wipe chip in process?
|
|
return self.bl_double_ack(0x93)
|
|
|
|
def readout_protect(self):
|
|
# "No-Stretch Readout Protect"
|
|
return self.bl_double_ack(0x83)
|
|
|
|
def read_at(self, addr=FLASH_START+0x100, ln=16):
|
|
# read memory, but address must be "correct" and mapped, which is undocumented
|
|
# - need not be aligned, up to 256
|
|
# - 0x1fff0cd0 also fun: BL code; 0x20001000 => RAM (but wont allow any lower?)
|
|
assert ln <= 256
|
|
return self.bl_cmd_read(0x11, ln, addr=addr, arg2=ln-1, no_final=True)
|
|
|
|
def write_at(self, addr=FLASH_START+0x100, data=b'1234'):
|
|
# "No-Stretch Write Memory command"
|
|
# - flash must be erased beforehand, or does nothing (no error)
|
|
ln = len(data)
|
|
assert ln <= 256
|
|
assert ln % 4 == 0
|
|
assert addr % 4 == 0
|
|
|
|
arg = add_xor_check(bytes([ln-1]) + data)
|
|
|
|
# send cmd, addr
|
|
self.bl_cmd_read(0x32, 0, addr=addr, arg2=None)
|
|
|
|
# then second arg, and wait til done
|
|
self.i2c.writeto(BL_ADDR, arg)
|
|
|
|
return self._wait_done() == BL_ACK
|
|
|
|
def run_at(self, addr=FLASH_START):
|
|
# "Go command" - starts code, but wants a reset vector really (stack+PC values)
|
|
self.bl_cmd_read(0x21, 0, addr=addr)
|
|
|
|
def cmd_resp(self, cmd_args, expect_len=0):
|
|
# send a command and read response back from our code running on GPU
|
|
# - will fail w/ OSError: ENODEV if i2c device (GPU) doesn't respond
|
|
self.i2c.writeto(GPU_ADDR, cmd_args)
|
|
return self.i2c.readfrom(GPU_ADDR, expect_len)
|
|
|
|
def goto_bootloader(self):
|
|
# switch working GPU code into bootloader mode
|
|
resp = self.cmd_resp(b'b', 2)
|
|
assert resp == b'OK'
|
|
self.reset()
|
|
utime.sleep_ms(100)
|
|
|
|
def get_version(self):
|
|
# see if running, and what version
|
|
try:
|
|
for retry in range(3):
|
|
resp = self.cmd_resp(b'v', 20)
|
|
if resp[0] != 0xff: break # bugfix for intermittent issue
|
|
utime.sleep_ms(10)
|
|
except OSError:
|
|
try:
|
|
# check bootloader is running
|
|
self.bl_version()
|
|
except:
|
|
return 'FAIL'
|
|
return 'BL' # ready to load via BL
|
|
return resp[0:resp.index(b'\0')].decode()
|
|
|
|
def take_spi(self):
|
|
# change the MOSI/SCLK lines to be input so we don't interfere
|
|
# with the GPU.. other lines are OD
|
|
# - signal by G_CTRL that CPU will take over
|
|
# - but first, wait until GPU is done if it's doing something (G_BUSY)
|
|
# - return T if GPU had control before
|
|
if self.g_ctrl() == 1:
|
|
# we already have control
|
|
return False
|
|
|
|
# say we will take control
|
|
self.g_ctrl(1)
|
|
|
|
while self.g_busy() == 1:
|
|
# let GPU finish
|
|
pass
|
|
|
|
self.mosi_pin.init(mode=Pin.ALT, pull=Pin.PULL_DOWN, af=Pin.AF5_SPI1)
|
|
self.sclk_pin.init(mode=Pin.ALT, pull=Pin.PULL_DOWN, af=Pin.AF5_SPI1)
|
|
|
|
return True
|
|
|
|
def give_spi(self):
|
|
# give up SPI and let GPU control things
|
|
self.mosi_pin.init(mode=Pin.IN)
|
|
self.sclk_pin.init(mode=Pin.IN)
|
|
self.g_ctrl(0)
|
|
|
|
def have_spi(self):
|
|
# do we control the display?
|
|
return self.g_ctrl() == 1
|
|
|
|
def busy_bar(self, enable):
|
|
if enable:
|
|
# start the bar
|
|
try:
|
|
self.cmd_resp(b'a')
|
|
except: pass
|
|
self.give_spi()
|
|
else:
|
|
# stop showing it
|
|
self.take_spi()
|
|
|
|
def cursor_off(self):
|
|
# stop showing the cursor
|
|
self.take_spi()
|
|
try:
|
|
self.cmd_resp(b'a')
|
|
except: pass
|
|
|
|
def cursor_at(self, x, y, cur_type):
|
|
# enable a cursor at indicated position. few different styles
|
|
cmd = b'c' + bytes([x, y, cur_type])
|
|
try:
|
|
self.cmd_resp(cmd)
|
|
except: pass
|
|
self.give_spi()
|
|
|
|
def show_test_pattern(self):
|
|
# show a barcode used to validate that GPU has access to LCD
|
|
self.cmd_resp(b't')
|
|
self.give_spi()
|
|
|
|
def upgrade(self):
|
|
# do in-circuit programming of GPU chip
|
|
import gpu_binary, zlib
|
|
|
|
# get into bootloader
|
|
if self.get_version() != 'BL':
|
|
self.goto_bootloader()
|
|
assert self.get_version() == 'BL'
|
|
|
|
# wipe old program
|
|
ok = self.bulk_erase()
|
|
assert ok, 'bulk erase fail'
|
|
|
|
tmp = zlib.decompress(gpu_binary.BINARY)
|
|
|
|
# write block by block, but skip first part, so we can handle powerfail w/o brick
|
|
for pos in range(256, gpu_binary.LENGTH, 256):
|
|
ok = self.write_at(FLASH_START+pos, tmp[pos:pos+256])
|
|
assert ok
|
|
|
|
# finally, the first part, which commits us to running this code on reset
|
|
self.write_at(FLASH_START, tmp[0:256])
|
|
|
|
self.run_at(FLASH_START)
|
|
utime.sleep_ms(50)
|
|
|
|
v = self.get_version()
|
|
assert v == gpu_binary.VERSION
|
|
|
|
return v
|
|
|
|
def upgrade_if_needed(self):
|
|
# called at boot time
|
|
from gpu_binary import VERSION
|
|
v = self.get_version()
|
|
if v == VERSION:
|
|
# correct version in place and running -- do nothing.
|
|
return
|
|
self.upgrade()
|
|
|
|
async def reflash_gpu_ux(self):
|
|
# Available from Advanced > Danger Zone > Reflash GPU
|
|
from ux import ux_show_story
|
|
from gpu_binary import VERSION
|
|
from utils import problem_file_line
|
|
from glob import dis
|
|
|
|
b4 = self.get_version()
|
|
ch = await ux_show_story('''This action reloads the firmware on the GPU co-processor. \
|
|
Should not be needed in normal use.\n
|
|
Current GPU version is: %s
|
|
We have version: %s\n\nContinue?''' % (b4, VERSION))
|
|
|
|
if ch != 'y': return
|
|
|
|
dis.fullscreen('Reflashing...')
|
|
|
|
try:
|
|
aft = self.upgrade()
|
|
await ux_show_story('Upgraded/reflashed.\n\nNew version is: %s' % aft)
|
|
except BaseException as exc:
|
|
await ux_show_story('GPU Flash Failed!\n\n%s' % problem_file_line(exc))
|
|
|
|
|
|
# EOF
|