diff --git a/proxyclient/experiments/speaker_amp.py b/proxyclient/experiments/speaker_amp.py new file mode 100644 index 00000000..5f184be3 --- /dev/null +++ b/proxyclient/experiments/speaker_amp.py @@ -0,0 +1,236 @@ +# speaker_amp.py -- play audio through the embedded speaker on Mac mini +# +# sample usage with sox: +# +# sox INPUT_FILE -t raw -r 48000 -c 1 -e signed-int -b 32 -L - gain -63 | python3 ./speaker_amp.py +# +# (expects mono, 24-bit signed samples padded to 32 bits on the msb side) + +import argparse +import os.path +import code +import sys + +from m1n1.setup import * +from m1n1.hw.dart import DART, DARTRegs +from m1n1.hw.admac import ADMAC, ADMACRegs +from m1n1.hw.i2c import I2C + +# this here is an embedded console so that one can poke while +# the descriptors keep being filled-in +class PollingConsole(code.InteractiveConsole): + def __init__(self, locals=None, filename=""): + global patch_stdout, PromptSession, FileHistory + global Thread, Queue, Empty + from prompt_toolkit import PromptSession + from prompt_toolkit.history import FileHistory + from prompt_toolkit.patch_stdout import patch_stdout + from threading import Thread + from queue import Queue, Empty + + super().__init__(locals, filename) + + self._qu_input = Queue() + self._qu_result = Queue() + self._should_exit = False + + self.session = PromptSession(history=FileHistory(os.path.expanduser("~/.m1n1-history"))) + self._other_thread = Thread(target=self._other_thread_main, daemon=False) + self._other_thread.start() + + def __enter__(self): + self._patch = patch_stdout() + self._patch.__enter__() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self._patch.__exit__(exc_type, exc_val, exc_tb) + + def _other_thread_main(self): + first = True + + while True: + if first: + more_input = False + first = False + else: + more_input = self._qu_result.get() + + try: + self._qu_input.put(self.session.prompt("(♫♫) " if not more_input else "... ")) + except EOFError: + self._qu_input.put(None) + return + + def poll(self): + if self._should_exit: + return False + + try: + line = self._qu_input.get(timeout=0.01) + except Empty: + return True + if line is None: + self._should_exit = True + return False + self._qu_result.put(self.push(line)) + return True + + +class NoConsole: + def poll(self): + time.sleep(0.01) + return True + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + +argparser = argparse.ArgumentParser() +argparser.add_argument("--console", action='store_true') +argparser.add_argument("-f", "--file", "--input", "--samples", + type=str, default=None, + help='input filename to take samples from ' \ + '(default: standard input)') +argparser.add_argument("-b", "--bufsize", type=int, default=1024*32, + help='size of buffers to keep submitting to DMA') +args = argparser.parse_args() + +if args.console and args.file is None: + print("Specify file with samples (option -f) if using console") + sys.exit(1) +inputf = open(args.file, "rb") if args.file is not None else sys.stdin.buffer + + +p.pmgr_adt_clocks_enable("/arm-io/i2c1") +p.pmgr_adt_clocks_enable("/arm-io/admac-sio") +p.pmgr_adt_clocks_enable("/arm-io/dart-sio") +p.pmgr_adt_clocks_enable("/arm-io/mca-switch") + + +i2c1 = I2C(u, "/arm-io/i2c1") + +dart_base, _ = u.adt["/arm-io/dart-sio"].get_reg(0) # stream index 2 +dart = DART(iface, DARTRegs(u, dart_base), util=u) +dart.initialize() + +admac = ADMAC(u, "/arm-io/admac-sio", dart, debug=True) +tx_chan = admac.tx[2] + +tx_chan.disable() +tx_chan.reset() + +#admac.regs.UNK_CONTROL.val = 1 +#admac.regs.UNK_CONTROL.val = 0 + +tx_chan.poll() # read stale reports + + +def pmgr_reset(): + # pmgr-related, unknown meaning, + # needs to be written for the speaker-amp IC to respond over I2C + p.write32(0x23d10c000, 0) + p.write32(0x23d10c004, 3) + p.write32(0x23d10c008, 0) + p.write32(0x23d10c00c, 3) +pmgr_reset() + + +mca_switch_base = 0x2_3840_0000 + +for off in [0x0, 0x100, 0x300, 0x4000, 0x4100, 0x4300]: + p.write32(mca_switch_base + off, 0x0) + p.write32(mca_switch_base + off, 0x2) + + +p.write32(0x238208840, 0x22) +p.write32(0x238208854, 0xc00060) +p.write32(0x238208854, 0xc00060) + +p.write32(mca_switch_base + 0x4004, 0x100) +p.write32(mca_switch_base + 0x4104, 0x200) +p.write32(mca_switch_base + 0x4108, 0x0) +p.write32(mca_switch_base + 0x410c, 0xfe) +p.write32(mca_switch_base + 0x8004, 0x100) +p.write32(mca_switch_base + 0xc004, 0x100) + +p.write32(0x238308000, 0x102048) +# bits 0x0000e0 influence clock +# 0x00000f influence sample serialization + +p.write32(0x23b0400d8, 0x06000000) # 48 ksps, zero out for ~96 ksps + +p.write32(mca_switch_base + 0x0600, 0xe) # 0x8 or have zeroed samples, 0x6 or have no clock +p.write32(mca_switch_base + 0x0604, 0x200) # sensitive in mask 0xf00, any other value disables clock +p.write32(mca_switch_base + 0x0608, 0x4) # 0x4 or zeroed samples + +# toggle the GPIO line driving the speaker-amp IC reset +p.write32(0x23c1002d4, 0x76a02) # invoke reset +p.write32(0x23c1002d4, 0x76a03) # take out of reset + + +tx_chan.submit(inputf.read(args.bufsize)) +tx_chan.enable() + + +# accesses to 0x100-sized blocks in the +0x4000 region require +# the associated enable bit cleared, or they cause SErrors +def mca_switch_unk_disable(): + for off in [0x4000, 0x4100, 0x4300]: + p.write32(mca_switch_base + off, 0x0) + +def mca_switch_unk_enable(): + for off in [0x4000, 0x4100, 0x4300]: + p.write32(mca_switch_base + off, 0x1) + +p.write32(mca_switch_base + 0x4104, 0x202) +p.write32(mca_switch_base + 0x4208, 0x3107) +mca_switch_unk_enable() + + +# by ADT and leaked schematic, i2c1 contains TAS5770L, +# which is not a public part. but there's e.g. TAS2110 +# with similar registers +# +# https://www.ti.com/product/TAS2110 +# +# if the speaker-amp IC loses clock on the serial sample input, +# it automatically switches to software shutdown. +# + +i2c1.write_reg(0x31, 0x08, [0x40]) +i2c1.write_reg(0x31, 0x0a, [0x06, 0x00, 0x1a]) +i2c1.write_reg(0x31, 0x1b, [0x01, 0x82, 0x06]) +i2c1.write_reg(0x31, 0x16, [0x50, 0x04]) +i2c1.write_reg(0x31, 0x0d, [0x00]) +#i2c1.write_reg(0x31, 0x03, [0x14]) + +# amplifier gain, presumably this is the lowest setting +i2c1.write_reg(0x31, 0x03, [0x0]) + +# take the IC out of software shutdown +i2c1.write_reg(0x31, 0x02, [0x0c]) + + +with (PollingConsole(locals()) if args.console else NoConsole()) as cons: + try: + while cons.poll(): + while (not tx_chan.can_submit()) and cons.poll(): + tx_chan.poll() + pass + + tx_chan.submit(inputf.read(args.bufsize)) + except KeyboardInterrupt: + pass + + +# mute +i2c1.write_reg(0x31, 0x02, [0x0d]) + +# software shutdown +i2c1.write_reg(0x31, 0x02, [0x0e]) + +tx_chan.disable() diff --git a/proxyclient/m1n1/hw/admac.py b/proxyclient/m1n1/hw/admac.py new file mode 100644 index 00000000..1bad9533 --- /dev/null +++ b/proxyclient/m1n1/hw/admac.py @@ -0,0 +1,240 @@ +from ..utils import * + +__all__ = ["ADMACRegs", "ADMAC"] + + +class R_RING(Register32): + # overflow/underflow counter + OF_UF = 31, 16 + + # goes through 0, 1, 2, 3 as the pieces of a report/descriptor + # are being read/written through REPORT_READ/DESC_WRITE + READOUT_PROGRESS = 13, 12 + + # when READ_SLOT==WRITE_SLOT one of the two is set + EMPTY = 8 + FULL = 9 + + ERR = 10 + + # next slot to read + READ_SLOT = 5, 4 + + # next slot to be written to + WRITE_SLOT = 1, 0 + +class R_CHAN_STATUS(Register32): + # only raised if the descriptor had NOTIFY set + DESC_DONE = 1 + + DESC_RING_EMPTY = 4 + REPORT_RING_FULL = 5 + + # cleared by writing ERR=1 either to TX_DESC_RING or TX_REPORT_RING + ERR = 7 + + UNK3 = 8 + UNK4 = 9 + UNK5 = 10 + +class R_CHAN_CONTROL(Register32): + RESET_RINGS = 0 + CLEAR_OF_UF_COUNTERS = 1 + UNK1 = 3 + +class ADMACRegs(RegMap): + TX_EN = 0x0, Register32 # one bit per channel + TX_EN_CLR = 0x4, Register32 + + RX_EN = 0x8, Register32 + RX_EN_CLR = 0xc, Register32 + + UNK_CTL = 0x10, Register32 + + # each of the four registers represents an internal interrupt line, + # bits represent DMA channels which at the moment raise that particular line + # + # the irq-destination-index prop in ADT maybe selects the line which + # is actually wired out + # + TX_INTSTATE = irange(0x30, 4, 0x4), Register32 + + # a 24 MHz always-running counter, top bit is always set + COUNTER = 0x70, Register64 + + # -- per-channel registers -- + + TX_CTL = (irange(0x8000, 16, 0x400)), R_CHAN_CONTROL + + TX_DESC_RING = irange(0x8070, 16, 0x400), R_RING + TX_REPORT_RING = irange(0x8074, 16, 0x400), R_RING + + TX_DESC_WRITE = irange(0x10000, 16, 4), Register32 + TX_REPORT_READ = irange(0x10100, 16, 4), Register32 + + # per-channel, per-internal-line + TX_STATUS = (irange(0x8010, 16, 0x400), irange(0x0, 4, 0x4)), R_CHAN_STATUS + TX_INTMASK = (irange(0x8010, 16, 0x400), irange(0x0, 4, 0x4)), R_CHAN_STATUS + + # missing: RX variety of registers shifted by +0x200 + + +class ADMACDescriptorFlags(Register32): + # whether to raise DESC_DONE in TX_STATUS + NOTIFY = 16 + + # arbitrary ID propagated into reports + DESC_ID = 7, 0 + +class ADMACDescriptor(Reloadable): + def __init__(self, addr, length, **flags): + self.addr = addr + self.length = length + self.flags = ADMACDescriptorFlags(**flags) + + def __repr__(self): + return f"" + + def ser(self): + return [ + self.addr & (1<<32)-1, + self.addr>>32 & (1<<32)-1, + self.length & (1<<32)-1, + int(self.flags) + ] + + @classmethod + def deser(self, seq): + if not len(seq) == 4: + raise ValueError + return ADMACDescriptor( + seq[0] | seq[1] << 32, # addr + seq[2], # length (in bytes) + seq[3] # flags + ) + + +class ADMACReportFlags(Register32): + UNK1 = 24 + UNK2 = 25 + UNK3 = 27 + DESC_ID = 7, 0 + +class ADMACReport(Reloadable): + def __init__(self, countval, unk1, flags): + self.countval, self.unk1, self.flags = countval, unk1, ADMACReportFlags(flags) + + def __repr__(self): + return f"" + + def ser(self): + return [ + self.countval & (1<<32)-1, + self.countval>>32 & (1<<32)-1, + self.unk1 & (1<<32)-1, + int(self.flags) + ] + + @classmethod + def deser(self, seq): + if not len(seq) == 4: + raise ValueError + return ADMACReport( + seq[0] | seq[1] << 32, # countval + seq[2], # unk1 + seq[3] # flags + ) + + +class ADMACTXChannel(Reloadable): + def __init__(self, parent, channo): + self.p = parent + self.iface = parent.p.iface + self.dart = parent.dart + self.regs = parent.regs + self.ch = channo + self.desc_id = 0 + + def reset(self): + self.regs.TX_CTL[self.ch].set(RESET_RINGS=1) + self.regs.TX_CTL[self.ch].set(RESET_RINGS=0) + + def enable(self): + self.regs.TX_EN.val = 1 << self.ch + + def disable(self): + self.regs.TX_EN_CLR.val = 1 << self.ch + + def can_submit(self): + return not self.regs.TX_DESC_RING[self.ch].reg.FULL + + def submit_desc(self, desc): + if self.regs.TX_DESC_RING[self.ch].reg.FULL: + raise Exception(f"ch{self.ch} descriptor ring full") + + if self.p.debug: + print(f"admac: submitting (ch{self.ch}): {desc}") + + for piece in desc.ser(): + self.regs.TX_DESC_WRITE[self.ch].val = piece + + def submit(self, data): + assert self.dart is not None + + self.poll() + + buf, iova = self.p._get_buffer(len(data)) + self.iface.writemem(buf, data) + self.submit_desc(ADMACDescriptor( + iova, len(data), DESC_ID=self.desc_id, NOTIFY=1, + )) + self.desc_id += 1 + + def poll(self): + if self.regs.TX_STATUS[self.ch, 1].reg.ERR: + if self.p.debug: + print(f"TX_STATUS={self.regs.TX_STATUS[self.ch,1].reg} " + \ + f"REPORT_RING={self.regs.TX_DESC_RING[self.ch]} " + \ + f"DESC_RING={self.regs.TX_REPORT_RING[self.ch]}") + self.regs.TX_DESC_RING[self.ch].set(ERR=1) + self.regs.TX_REPORT_RING[self.ch].set(ERR=1) + + while not self.regs.TX_REPORT_RING[self.ch].reg.EMPTY: + pieces = [] + for _ in range(4): + pieces.append(self.regs.TX_REPORT_READ[self.ch].val) + report = ADMACReport.deser(pieces) + + if self.p.debug: + print(f"admac: picked up (ch{self.ch}): {report}") + + +class ADMAC(Reloadable): + def __init__(self, u, devpath, dart=None, dart_stream=2, nchans=12, + reserved_size=4*1024*1024, debug=False): + self.u = u + self.p = u.proxy + self.debug = debug + + self.base, _ = u.adt[devpath].get_reg(0) + self.regs = ADMACRegs(u, self.base) + self.dart = dart + + if dart is not None: + self.resmem_base = u.heap.memalign(128*1024, reserved_size) + self.resmem_size = reserved_size + self.resmem_pos = self.resmem_base + self.iova_base = self.dart.iomap(dart_stream, self.resmem_base, self.resmem_size) + self.dart.invalidate_streams(1 << dart_stream) + + self.tx = [ADMACTXChannel(self, no) for no in range(nchans)] + + def _get_buffer(self, size): + assert size < self.resmem_size + + if self.resmem_pos + size > self.resmem_base + self.resmem_size: + self.resmem_pos = self.resmem_base + + bufptr = self.resmem_pos + self.resmem_pos += size + return bufptr, bufptr - self.resmem_base + self.iova_base