blob: 42127b75ec5c84412b768edbbe4c0685569eed2a [file] [log] [blame]
#!/usr/bin/env python
# Copyright 2023 The ChromiumOS Authors
# SPDX-License-Identifier: Apache-2.0
import ctypes
import mmap
import os
import re
import struct
import sys
import time
from glob import glob
# MT8195 audio firmware load/debug gadget
# Note that the hardware handling here is only partial: in practice
# the audio DSP depends on clock and power well devices whose drivers
# live elsewhere in the kernel. Those aren't duplicated here. Make
# sure the DSP has been started by a working kernel driver first.
#
# See gen_img.py for docs on the image format itself. The way this
# script works is to map the device memory regions and registers via
# /dev/mem and copy the two segments while resetting the DSP.
#
# In the kernel driver, the address/size values come from devicetree.
# But currently the MediaTek architecture is one kernel driver per SOC
# (i.e. the devicetree values in the kenrel source are tied to the
# specific SOC anyway), so it really doesn't matter and we hard-code
# the addresses for simplicity.
#
# (For future reference: in /proc/device-tree on current ChromeOS
# kernels, the host registers are a "cfg" platform resource on the
# "adsp@10803000" node. The sram is likewise the "sram" resource on
# that device node, and the two dram areas are "memory-region"
# phandles pointing to "adsp_mem_region" and "adsp_dma_mem_region"
# nodes under "/reserved-memory").
FILE_MAGIC = 0xE463BE95
# Runtime mmap objects for each MAPPINGS entry
maps = {}
# Returns a string (e.g. "mt8195", "mt8186", "mt8188") if a supported
# adsp is detected, or None if not
def detect():
compat = readfile(glob("/proc/device-tree/**/adsp@*/compatible", recursive=True)[0], "r")
m = re.match(r'.*(mt\d{4})-dsp', compat)
if m:
return m.group(1)
# Parse devicetree to find the MMIO mappings: there is an "adsp" node
# (in various locations) with an array of named "reg" mappings. It
# also refers by reference to reserved-memory regions of system
# DRAM. Those don't have names, call them dram0/1 (dram1 is the main
# region to which code is linked, dram0 is presumably a dma pool but
# unused by current firmware). Returns a dict mapping name to a
# (addr, size) tuple.
def mappings():
path = glob("/proc/device-tree/**/adsp@*/", recursive=True)[0]
rnames = readfile(path + "reg-names", "r").split('\0')[:-1]
regs = struct.unpack(f">{2 * len(rnames)}Q", readfile(path + "reg"))
maps = {n: (regs[2 * i], regs[2 * i + 1]) for i, n in enumerate(rnames)}
for i, ph in enumerate(struct.unpack(">II", readfile(path + "memory-region"))):
for rmem in glob("/proc/device-tree/reserved-memory/*/"):
phf = rmem + "phandle"
if os.path.exists(phf) and struct.unpack(">I", readfile(phf))[0] == ph:
(addr, sz) = struct.unpack(">QQ", readfile(rmem + "reg"))
maps[f"dram{i}"] = (addr, sz)
break
return maps
# Register API for 8195
class MT8195:
def __init__(self, maps):
# Create a Regs object for the registers
r = Regs(ctypes.addressof(ctypes.c_int.from_buffer(maps["cfg"])))
r.ALTRESETVEC = 0x0004 # Xtensa boot address
r.RESET_SW = 0x0024 # Xtensa halt/reset/boot control
r.PDEBUGBUS0 = 0x000C # Unclear, enabled by host, unused by SOF?
r.SRAM_POOL_CON = 0x0930 # SRAM power control: low 4 bits (banks?) enable
r.EMI_MAP_ADDR = 0x981C # == host SRAM mapping - 0x40000000 (controls MMIO map?)
r.freeze()
self.cfg = r
def logrange(self):
return range(0x700000, 0x800000)
def stop(self):
self.cfg.RESET_SW |= 8 # Set RUNSTALL: halt CPU
self.cfg.RESET_SW |= 3 # Set low two bits: "BRESET|DRESET"
def start(self, boot_vector):
self.stop()
self.cfg.RESET_SW |= 0x10 # Enable "alternate reset" boot vector
self.cfg.ALTRESETVEC = boot_vector
self.cfg.RESET_SW &= ~3 # Release reset bits
self.cfg.RESET_SW &= ~8 # Clear RUNSTALL: go!
# Register API for 8186/8188
class MT818x:
def __init__(self, maps):
# These have registers spread across two blocks
cfg_base = ctypes.addressof(ctypes.c_int.from_buffer(maps["cfg"]))
sec_base = ctypes.addressof(ctypes.c_int.from_buffer(maps["sec"]))
self.cfg = Regs(cfg_base)
self.cfg.SW_RSTN = 0x00
self.cfg.IO_CONFIG = 0x0C
self.cfg.freeze()
self.sec = Regs(sec_base)
self.sec.ALTVEC_C0 = 0x04
self.sec.ALTVECSEL = 0x0C
self.sec.freeze()
def logrange(self):
return range(0x700000, 0x800000)
def stop(self):
self.cfg.IO_CONFIG |= 1 << 31 # Set RUNSTALL to stop core
time.sleep(0.1)
self.cfg.SW_RSTN |= 0x11 # Assert reset: SW_RSTN_C0|SW_DBG_RSTN_C0
# Note: 8186 and 8188 use different bits in ALTVECSEC, but
# it's safe to write both to enable the alternate boot vector
def start(self, boot_vector):
self.cfg.IO_CONFIG |= 1 << 31 # Set RUNSTALL
self.sec.ALTVEC_C0 = boot_vector
self.sec.ALTVECSEL = 0x03 # Enable alternate vector
self.cfg.SW_RSTN |= 0x00000011 # Assert reset
self.cfg.SW_RSTN &= 0xFFFFFFEE # Release reset
self.cfg.IO_CONFIG &= 0x7FFFFFFF # Clear RUNSTALL
class MT8196:
def __init__(self, maps):
cfg_base = ctypes.addressof(ctypes.c_int.from_buffer(maps["cfg"]))
sec_base = ctypes.addressof(ctypes.c_int.from_buffer(maps["sec"]))
self.cfg = Regs(cfg_base)
self.cfg.CFGREG_SW_RSTN = 0x0000
self.cfg.MBOX_IRQ_EN = 0x009C
self.cfg.HIFI_RUNSTALL = 0x0108
self.cfg.freeze()
self.sec = Regs(sec_base)
self.sec.ALTVEC_C0 = 0x04
self.sec.ALTVECSEL = 0x0C
self.sec.freeze()
def logrange(self):
return range(0x580000, 0x600000)
def stop(self):
self.cfg.HIFI_RUNSTALL |= 0x1000
self.cfg.CFGREG_SW_RSTN |= 0x11
def start(self, boot_vector):
self.sec.ALTVEC_C0 = 0
self.sec.ALTVECSEL = 0
self.sec.ALTVEC_C0 = boot_vector
self.sec.ALTVECSEL = 1
self.cfg.HIFI_RUNSTALL |= 0x1000
self.cfg.MBOX_IRQ_EN |= 3
self.cfg.CFGREG_SW_RSTN |= 0x11
time.sleep(0.1)
self.cfg.CFGREG_SW_RSTN &= ~0x11
self.cfg.HIFI_RUNSTALL &= ~0x1000
# Temporary logging protocol: watch the 1M null-terminated log
# stream at 0x60700000 -- the top of the linkable region of
# existing SOF firmware, before the heap. Nothing uses this
# currently. Will be replaced by winstream very soon.
def old_log(dev):
msg = b''
dram = maps["dram1"]
for i in dev.logrange():
x = dram[i]
if x == 0:
sys.stdout.buffer.write(msg)
sys.stdout.buffer.flush()
msg = b''
while x == 0:
time.sleep(0.1)
x = dram[i]
msg += x.to_bytes(1, "little")
sys.stdout.buffer.write(msg)
sys.stdout.buffer.flush()
# (Cribbed from cavstool.py)
class Regs:
def __init__(self, base_addr):
vars(self)["base_addr"] = base_addr
vars(self)["ptrs"] = {}
vars(self)["frozen"] = False
def freeze(self):
vars(self)["frozen"] = True
def __setattr__(self, name, val):
if not self.frozen and name not in self.ptrs:
addr = self.base_addr + val
self.ptrs[name] = ctypes.c_uint32.from_address(addr)
else:
self.ptrs[name].value = val
def __getattr__(self, name):
return self.ptrs[name].value
def readfile(f, mode="rb"):
return open(f, mode).read()
def le4(bstr):
assert len(bstr) == 4
return struct.unpack("<I", bstr)[0]
# Wrapper class for winstream logging. Instantiate with a single
# integer argument representing a local/in-process address for the
# shared winstream memory. The memory mapped access is encapsulated
# with a Regs object for the fields and a ctypes array for the data
# area. The lockless algorithm in read() matches the C version in
# upstream Zephyr, don't modify in isolation. Note that on some
# platforms word access to the data array (by e.g. copying a slice
# into a bytes object or by calling memmove) produces bus errors
# (plausibly an alignment requirement on the fabric with the DSP
# memory, where arm64 python is happy doing unaligned loads?). Access
# to the data bytes is done bytewise for safety.
class Winstream:
def __init__(self, addr):
r = Regs(addr)
r.WLEN = 0x00
r.START = 0x04
r.END = 0x08
r.SEQ = 0x0C
r.freeze()
# Sanity-check, the 32M size limit isn't a rule, but seems reasonable
if r.WLEN > 0x2000000 or (r.START >= r.WLEN) or (r.END >= r.WLEN):
raise RuntimeError("Invalid winstream")
self.regs = r
self.data = (ctypes.c_char * r.WLEN).from_address(addr + 16)
self.msg = bytearray(r.WLEN)
self.seq = 0
def read(self):
ws, msg, data = self.regs, self.msg, self.data
last_seq = self.seq
wlen = ws.WLEN
while True:
start, end, seq = ws.START, ws.END, ws.SEQ
self.seq = seq
if seq == last_seq or start == end:
return ""
behind = seq - last_seq
if behind > ((end - start) % wlen):
return ""
copy = (end - behind) % wlen
suffix = min(behind, wlen - copy)
for i in range(suffix):
msg[i] = data[copy + i][0]
msglen = suffix
l2 = behind - suffix
if l2 > 0:
for i in range(l2):
msg[msglen + i] = data[i][0]
msglen += l2
if start == ws.START and seq == ws.SEQ:
return msg[0:msglen].decode("utf-8", "replace")
# Locates a winstream descriptor in the firmware via its 96-bit magic
# number and returns the address and size fields it finds there.
def find_winstream(maps):
magic = b'\x74\x5f\x6a\xd0\x79\xe2\x4f\x00\xcd\xb8\xbd\xf9'
for m in maps:
if "dram" in m:
# Some python versions produce bus errors (!) on the
# hardware when finding a 12 byte substring (maybe a SIMD
# load that the hardware doesn't like?). Do it in two
# chunks.
magoff = maps[m].find(magic[0:8])
if magoff >= 0:
magoff = maps[m].find(magic[8:], magoff) - 8
if magoff >= 0:
addr = le4(maps[m][magoff + 12 : magoff + 16])
return addr
raise RuntimeError("Cannot find winstream descriptor in firmware runtime")
def winstream_localaddr(globaddr, mmio, maps):
for m in mmio:
off = globaddr - mmio[m][0]
if 0 <= off < mmio[m][1]:
return ctypes.addressof(ctypes.c_int.from_buffer(maps[m])) + off
raise RuntimeError("Winstream address not inside DSP memory")
def winstream_log(mmio, maps):
physaddr = find_winstream(maps)
regsbase = winstream_localaddr(physaddr, mmio, maps)
ws = Winstream(regsbase)
while True:
msg = ws.read()
if msg:
sys.stdout.write(msg)
sys.stdout.flush()
else:
time.sleep(0.1)
def main():
dsp = detect()
assert dsp
# Probe devicetree for mappable memory regions
mmio = mappings()
# Open device and establish mappings
with open("/dev/mem", "wb+") as devmem_fd:
for mp in mmio:
paddr = mmio[mp][0]
mapsz = mmio[mp][1]
mapsz = int((mapsz + 4095) / 4096) * 4096
maps[mp] = mmap.mmap(
devmem_fd.fileno(),
mapsz,
offset=paddr,
flags=mmap.MAP_SHARED,
prot=mmap.PROT_WRITE | mmap.PROT_READ,
)
if dsp == "mt8195":
dev = MT8195(maps)
elif dsp in ("mt8186", "mt8188"):
dev = MT818x(maps)
elif dsp == "mt8196":
dev = MT8196(maps)
if sys.argv[1] == "load":
dat = None
with open(sys.argv[2], "rb") as f:
dat = f.read()
assert le4(dat[0:4]) == FILE_MAGIC
sram_len = le4(dat[4:8])
boot_vector = le4(dat[8:12])
sram = dat[12 : 12 + sram_len]
dram = dat[12 + sram_len :]
assert len(sram) <= mmio["sram"][1]
assert len(dram) <= mmio["dram1"][1]
# Stop the device and write the regions. Note that we don't
# zero-fill SRAM, as that's been observed to reboot the host
# (!!) on mt8186 when the writes near the end of the 512k
# region.
# pylint: disable=consider-using-enumerate
for i in range(sram_len):
maps["sram"][i] = sram[i]
# for i in range(sram_len, mmio["sram"][1]):
# maps["sram"][i] = 0
for i in range(len(dram)):
maps["dram1"][i] = dram[i]
for i in range(len(dram), mmio["dram1"][1]):
maps["dram1"][i] = 0
dev.start(boot_vector)
winstream_log(mmio, maps)
elif sys.argv[1] == "log":
winstream_log(mmio, maps)
elif sys.argv[1] == "oldlog":
old_log(dev)
elif sys.argv[1] == "mem":
print("Memory Regions:")
for m in mmio:
print(f" {m}: {mmio[m][1]} @ 0x{mmio[m][0]:08x}")
elif sys.argv[1] == "dump":
sz = mmio[sys.argv[2]][1]
mm = maps[sys.argv[2]]
sys.stdout.buffer.write(mm[0:sz])
else:
print(f"Usage: {sys.argv[0]} log | load <file>")
if __name__ == "__main__":
main()