mirror of
https://github.com/AsahiLinux/m1n1
synced 2024-11-26 16:30:17 +00:00
Add m1n1.hw.admac, experiments/speaker_amp.py
Add initial code for driving the ADMAC hw blocks, also add a script which shows it in action by streaming audio to the Mac mini's embedded speaker. Signed-off-by: Martin Povišer <povik@protonmail.com>
This commit is contained in:
parent
d25581ddb3
commit
18bc2c7db1
2 changed files with 476 additions and 0 deletions
236
proxyclient/experiments/speaker_amp.py
Normal file
236
proxyclient/experiments/speaker_amp.py
Normal file
|
@ -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="<console>"):
|
||||||
|
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()
|
240
proxyclient/m1n1/hw/admac.py
Normal file
240
proxyclient/m1n1/hw/admac.py
Normal file
|
@ -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"<descriptor: addr=0x{self.addr:x} len=0x{self.length:x} flags={self.flags}>"
|
||||||
|
|
||||||
|
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"<report: countval=0x{self.countval:x} unk1=0x{self.unk1:x} flags={self.flags}>"
|
||||||
|
|
||||||
|
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
|
Loading…
Reference in a new issue