Compare commits

...

2 commits

Author SHA1 Message Date
Stephan I. Böttcher
8179bdb629 dorn.py: thhor ifc 2026-03-26 22:07:32 +01:00
Stephan I. Böttcher
f94145f233 dorn.py: original copy from irena-arm 2026-03-26 09:53:48 +01:00
2 changed files with 643 additions and 6 deletions

611
src/dorn.py Normal file
View file

@ -0,0 +1,611 @@
import math, time, sys
def _connect(ifc):
global _ifc, acmd, rfifo
_ifc = ifc
acmd = ifc.acmd
rfifo = ifc.rfifo
def degCβ(a, R1=10e3, R25=10e3, B25=3940, res=0x1000):
if not a:
a = 1
R = R1 * a / (res - (a & (res-1)))
T = B25/(math.log(R/R25) + B25/298) - 273
return T
def degC(a, idx=0):
return degCβ(a, **CONFIG.NTC[idx])
def HK_fmt(s, i, n, d):
return f"{i}. {n}: {repr(d)}\n"
def HK3_fmt(s, i, n, d):
return f"""{i}. {n}
T = {d[5]:6.2f} °C, GND = {d[0]:6.3f} V,
Vff = {d[1]:6.3f} V, Vnn = {d[2]:6.3f} V, Vpp = {d[3]:6.3f} V, Vdig = {d[4]:6.3f} V,
Vcc = {d[6]:6.3f} V, Vss = {d[7]:6.3f} V.
"""
def HK4_AHBGO_fmt(s, i, n, d):
return f"""{i}. {n}
Tbgo = {d[0]:6.2f} °C, {d[1]:6.2f} °C, {d[3]:6.2f} °C,
Vbias = {d[2]:6.3f} V, Ibias = {d[4]:6.1f} nA, {d[5]:6.1f} nA,
Vcc = {d[7]:6.3f} V, Vss = {d[6]:6.3f} V.
"""
def HK4_SETH_fmt(s, i, n, d):
return f"""{i}. {n}
Tpa = {d[4]:6.2f} °C, Tbgo = {d[3]:6.2f} °C, {d[5]:6.2f} °C,
Vbias = {d[2]:6.3f} V, Ibias = {d[1]:6.1f} nA, {d[0]:6.1f} nA,
Vcc = {d[7]:6.3f} V, Vss = {d[6]:6.3f} V.
"""
def HK7_SETH_fmt(s, i, n, d):
if s==0:
return f"""{i}. {n}
na = {repr(d[:4])}
Tpwr = {d[6]:.1f} °C,
Vprim = {d[5]:.1f} V, Iprim = {d[4]:.1f} mA,
Ibias = {d[7]:.1f} nA.
"""
return f"""{i}. {n}
na = {repr(d[:4])}
Text = {d[6]:.1f} °C,
Ibias = {d[4]:.1f} nA,
Ibias = {d[7]:.1f} nA, Vbias = {d[5]:.1f} V.
"""
def HK3_LEIA_fmt(s, i, n, d):
return f"""{i}. {n}
Tadc = {d[0]:6.2f} °C,
Vadc = {d[1]:6.3f} V,
VbiasD = {d[7]:6.2f} V,
Vbias2 = {d[6]:6.1f} V,
Vpp = {d[3]:6.3f} V, Vnn = {d[2]:6.3f} V,
Vcc = {d[5]:6.3f} V, Vss = {d[4]:6.3f} V.
"""
def HK4_LEIA_fmt(s, i, n, d):
return f"""{i}. {n}
Ibias1 = {d[3]:.1f} nA, Ibias2 = {d[2]:.1f} nA, IbiasD = {d[0]:.1f} nA,
VbiasG = {d[1]:.1f} V, Vbias1 = {d[4]:.1f} V,
Tpa0 = {d[5]:6.2f} °C, Tpa1= {d[6]:6.2f} °C, Tpa2 = {d[7]:6.2f} °C.
"""
class DORN_CONFIG:
# stis_ana_core 2×24 ch
dorn_addr = 0x2000
verbose = True
slices = (0,1)
n_channels = 24
l2 = ( # set6
(0, -1203, 0),
(1, -1203, 0),
(2, -1203, 0),
(3, -1186, 0),
(4, -39, 2000),
(5, 1709, 970),
(6, 2000, -1082),
(7, 1125, -1888),
)
HK3 = [[
("GND",),
("Vff", 2.0),
("Vnn", 2.5, ("Vpp", -1.5)),
("Vpp", 2.0),
("Vdig", 2.0),
("Tadc", (degC, {})),
("Vcc", 2.0),
("Vss", 2.5, ("Vcc", -1.5)),
]]
HK = [
("INP₀", None, HK_fmt),
("INP₁", None, HK_fmt),
("INP₂", None, HK_fmt),
("HK ADC", HK3, HK3_fmt),
("HK PA", None, HK_fmt),
("VrefL", None, HK_fmt),
("VrefH", None, HK_fmt),
("HK_PWR", None, HK_fmt),
]
def n_adc(self):
return self.n_channels//3
def Vref(self, slice=None):
try:
return self.VREF[slice]
except:
pass
return 3.3
def n_trigs(self):
if self.n_channels > 16:
return self.n_adc()
return self.n_channels
def m_trigs(self):
return (1 << self.n_trigs()) - 1
NTC = [dict(R1=10e3, R25=10e3, B25=3940, res=0x1000)]
HK4_SETH = [
[
("Ibias₂", 10470/470 * 51/1051 * 100, -97),
("Ibias₁", 10470/470 * 51/1051 * 100, -135),
("Vbias", -1/0.047),
("Tbgo₁", (degC, {})),
("Tpa", (degC, {})),
("Tbgo₂", (degC, {})),
("Vss", 2.5, ("Vcc", -1.5)),
("Vcc", 2.0),
],
[
("Ibias₂", 10470/470 * 51/1051 * 100, -98),
("Ibias₁", 10470/470 * 51/1051 * 100, -112),
("Vbias", -1/0.047, 2.0),
("Tbgo₁", (degC, {})),
("Tpa", (degC, {})),
("Tbgo₂", (degC, {})),
("Vss", 2.5, ("Vcc", -1.5)),
("Vcc", 2.0),
],
]
HK7_SETH = [
[
("na₁",), ("na₂",), ("na₃",), ("na₄",),
("Iprim", 2000.0),
("Vprim", 16.3),
("Tpwr", (degC, {})),
("Ibias⁺", 100.0),
],
[
("na₁",), ("na₂",), ("na₃",), ("na₄",),
("Ibias", 100.0),
("Vbias⁺", 100.0),
("Text", (degC, {})),
("Ibias⁺", 100.0),
],
]
def seth(self):
# ! change the class attribute .HK
self.HK[4] = ("HK PA", self.HK4_SETH, HK4_SETH_fmt)
self.HK[7] = ("HK_PWR", self.HK7_SETH, HK7_SETH_fmt)
self.BGO = [(0,3), (0,12), (0,20), (1,3), (1,12), (1,20)]
HK4_AHBGO = [[
("Tbgo₁", (degC, {})),
("Tbgo₂", (degC, {})),
("Vbias1", -1/0.047),
("Tbgo₃", (degC, {})),
("Ibias₁", 10470/470 * 51/1051 * 100),
("Ibias₂", 10470/470 * 51/1051 * 100),
("Vss", 2.5, ("Vcc", -1.5)),
("Vcc", 2.0),
]]
def ahbgo(self):
# ! change the class attribute .HK
self.HK[4] = ("HK PA", self.HK4_AHBGO, HK4_AHBGO_fmt)
# calib 2025-11-25
Fluke_cal = 1/0.862
# SN2 dac, 14*HK3H, VbiasD
VbiasD_SN2 = [
[ 64, 0.51, 0.5 * Fluke_cal ],
[ 960, 1.85, 1.6 * Fluke_cal ],
[ 1984, 3.36, 2.9 * Fluke_cal ],
[ 4992, 7.85, 6.7 * Fluke_cal ],
[ 9984, 15.27, 13.1 * Fluke_cal ],
[ 14976, 22.56, 19.3 * Fluke_cal ],
[ 19968, 29.91, 25.6 * Fluke_cal ],
[ 20992, 31.42, 26.9 * Fluke_cal ],
[ 21440, 32.06, 27.5 * Fluke_cal ],
[ 21952, 32.83, 28.0 * Fluke_cal ],
[ 24960, 37.36, 28.6 * Fluke_cal ],
]
VbiasD_a = 0.033
VbiasD_b = 0.9918
HK3_LEIA = [[
("Tadc", (degC, {})),
("Vadc", 2.0),
("Vnn", 2.5, ("Vpp", -1.5)),
("Vpp", 2.0),
("Vss", 2.5, ("Vcc", -1.5)),
("Vcc", 2.0),
("Vbias2", -1/0.022 * 66.6/75.2),
("VbiasD", -14 * VbiasD_b - VbiasD_a),
]]
HK4_LEIA = [[
("IbiasD", 100., -1.4),
("VbiasG", 46.3, ("Vref", -45.3)),
("Ibias2", 10470/470 * 51/1051 * 100, -71.0),
("Ibias1", 10470/470 * 51/1051 * 100, -52.0),
("Vbias1", -1/0.047 * 44.7/48.5),
("Tpa0", (degC, {})),
("Tpa1", (degC, {})),
("Tpa2", (degC, {})),
]]
def leia(self):
self.VREF = (3.342,)
self.slices=(0,)
self.HK[3] = ("HK_AD", self.HK3_LEIA, HK3_LEIA_fmt)
self.HK[4] = ("HK_PA", self.HK4_LEIA, HK4_LEIA_fmt)
def thhor(self):
self.slices=(0,)
self.n_channels = 12
self.NTC = [dict(R1=22e3, R25=10e3, B25=3940, res=0x1000)]
CONFIG = DORN_CONFIG()
def hk(sl=0, what="print", data=None):
if not data:
_ifc.menable()
fifo_enable(en=True, sl=sl, hk=True)
fifo_reset(sl=sl, hk=True)
strobe(sl=sl, hk=True)
time.sleep(0.1)
fifo_read(sl=sl, hk=True)
data=rfifo()
if data[0] != 0x5710 + sl:
raise ValueError("Read HK packet error", data)
if isinstance(data, str):
# HDORN … data line
if "x" in data:
return sl, None
data = data.split()
if data[0] != "HDORN":
return sl, None
data = list(map(int, data[1:]))
if sl is None:
sl = data[0] & 3;
if data[0] & 3 != sl:
return sl, None
if what=="data":
return sl, data
ND = CONFIG.n_adc()
NV = ND
data = [d & 0xfff for d in data]
data = [data[ND*i+1:ND*i+1+NV] for i in range(8)]
if what=="raw":
return sl, data
Vref = CONFIG.Vref(sl)/4096
ddata = []
VV = {"Vref": CONFIG.Vref(sl)}
for a in range(8):
V = {}
ddata.append(V)
HK = CONFIG.HK[a][1]
# cookbook:
# None
# (name, factor, offset)
# (name, factor, (offset_name, factor))
# (name, (func, {args}))
if not HK:
V.update({c:data[a][c] for c in range(8)})
continue
HK = HK[sl % len(HK)]
for c in range(8):
H = HK[c]
if len(H) > 1:
if isinstance(H[1], tuple):
data[a][c] = H[1][0](data[a][c], **H[1][1])
else:
data[a][c] *= Vref*H[1]
V[H[0]] = data[a][c]
VV.update(V)
for c in range(8):
H = HK[c]
if len(H) > 2:
if isinstance(H[2], tuple):
data[a][c] += VV[H[2][0]] * H[2][1]
else:
data[a][c] += H[2]
V[H[0]] = data[a][c]
if what=="cooked":
return sl, data
if what=="dict":
return sl, ddata
r = []
for a in range(8):
H=CONFIG.HK[a]
# (name, [cookbooks], fmt_function)
r.append(H[2](sl, a, H[0], data[a]))
if what=="print":
sys.stderr.write("".join(r))
return sl, r
def dorn_config(a, v=None, mes=None, slice=0):
verb = CONFIG.verbose
a |= CONFIG.dorn_addr | (slice<<11)
if v is None:
if not mes:
mes = f"dorn[0x{a:03x}]"
return acmd(a, task=mes, verb=verb)
if not mes:
mes = f"dorn[0x{a:03x}] = 0x{v:04x}"
if v is False:
acmd(a, task=mes, verb=verb)
else:
acmd(a, v, task=mes, verb=verb)
def thres(sl, ch, v):
dorn_config(0x040 | ch, v, slice=sl)
def write_l2(sl, ch, i, a, b):
dorn_config(0x400 | (ch<<4) | (i<<1) | 0, a & 0xffff, slice=sl)
dorn_config(0x400 | (ch<<4) | (i<<1) | 1, b & 0xffff, slice=sl)
def calib_l2(cal, coeff):
if cal is None:
return coeff
from numpy import array, floor, add, any
c = array(coeff)[:,1:] * cal
i = floor(c+0.5)
e = add.reduce(i)
v = e>0
w = e<0
while any(v) or any(w):
d = i - c
vv = d.argmax(axis=0)
ww = d.argmin(axis=0)
for x,f in enumerate(v):
if f:
i[vv[x],x] -= 1
for x,f in enumerate(w):
if f:
i[ww[x],x] += 1
e = add.reduce(i)
v = e>0
w = e<0
return [(x, int(ii[0]), int(ii[1])) for x, ii in enumerate(i)]
def l2filter(coeff=None, cal=None):
if coeff==None:
coeff = CONFIG.l2
for sl in CONFIG.slices:
for ch in range(CONFIG.n_channels):
c = coeff
if cal and cal[sl] and cal[sl][ch]:
c = calib_l2(cal[sl][ch], c)
for cc in c:
write_l2(sl, ch, *cc)
def write_l3(sl, ch, *p):
scales = (14, 14, 15, 15)
for i,pp in enumerate(p):
if isinstance(pp, float):
pp = int(pp * 2**scales[i])
dorn_config(0x100 | (ch<<2) | i, pp & 0xffff, slice=sl)
def l3banana(
# 2025-02-20-seth-pa1-4
p0 = -3839,
p2 = 13891,
p3 = 3579,
p4 = 22853 ):
for sl in CONFIG.slices:
for ch in range(CONFIG.n_channels):
write_l3(sl, ch, p0, p2, p3, p4)
def c16log2(A):
return int(math.ceil(16*math.log(1024*A)/math.log(2)))
def enable_trigger(sl, triggers, det=False, sa=False):
i = 2 if sa else 0 if det else 1
dorn_config(0x008 | i, triggers, slice=sl)
def nsamples(sl, n=None, m=None):
if n is None:
return dorn_config(0x00b, slice=sl)
if m is None:
m = 0xffff if n else 0
enable_trigger(sl, m, sa=True)
dorn_config(0x00b, n, slice=sl)
def triggers(sl, atrig=0xffff, dtrig=0, sam=0, nsa=10):
enable_trigger(sl, dtrig, det=True)
enable_trigger(sl, atrig)
nsamples(sl, nsa, sam)
def atriggers(sl, atrig=0, gtrig=0, csa=False, gsa=False, nsa=10):
dorn_config(0x008, slice=sl, v=atrig & 0xffff, mes="atrig[15:0]")
dorn_config(0x009, slice=sl, v=(atrig>>16)|((gtrig&0xff)<<8), mes="gtrig[7:0],atrig[23:8]")
dorn_config(0x00a, slice=sl, v=gtrig>>8, mes="gtrig[23:8]")
dorn_config(0x00b, slice=sl, v=(gsa<<9)|(csa<<8)|nsa, mes="nsamples")
def hist_bins(sl, bin0=4.0, res=1, cwin=6, xtalk=6):
"""configure dorn_l4 histogramming
bin0: float: A-value of bin 0, int: 16LOG2(A)
res: bin = 16×log₂(A) >> res
cwin: anti-coincidence window [µs]
xtalk: x-talk ratio cut: reject if S > (S>>xtalk)
"""
if isinstance(bin0, float):
bin0=c16log2(bin0)
dorn_config(0x00c, (xtalk<<8) | cwin, slice=sl)
dorn_config(0x00d, (res<<9) | bin0, slice=sl)
def base_address(hists=None, counters=None, hmask=None, cmask=None):
if hists is not None:
if counters is None:
counters = hists
r = dorn_config(0x088, (counters<<3) | hists)
else:
if hmask is not None:
r = dorn_config(0x088, 0x4000 | hmask)
if cmask is not None:
r = dorn_config(0x088, 0x8000 | cmask)
if cmask is None and hmask is None:
r = dorn_config(0x088, 0x8000)
return r & 7, r >> 3
def fifo_status(i=None):
e = dorn_config(0x084)
p = dorn_config(0x085)
f = dorn_config(0x086)
if i is None:
return e,p,f
return (e>>i)&1, (p>>i)&1, (f>>i)&1
def fifo_select(hi=False, sl=None, hk=False, ev=False, sa=False, i=None):
if i is not None:
f = 1<<i
else:
f = (sa<<2) | (ev<<1) | hk
if sl is not None:
f <<= 3*sl+1
else:
f *= 0b0010010010010
f |= hi
return f
def fifo_enable(en=True, rfifo=True, men=True, sl=None, **aa):
f = fifo_select(sl=sl, **aa)
if sl is None and en is True:
en = 0xf
elif sl is not None:
en <<= sl
dorn_config(0x087, (f<<6) | (rfifo<<5) | (men<<4) | en)
STROBES = {
"read_fifos": 0,
"incr": 1,
"resync": 2,
"clock_reset": 3,
"hk": 4,
"dorn_reset": 8,
"mem_reset": 12,
"rb_reset": 13,
"fifo_reset": 14,
}
def strobe(sl=None, **aa):
s = 0
for k in aa:
if not k in STROBES:
raise KeyError(f"No STROBE: {k}")
if aa[k]:
b = 1
if k=="hk":
if sl is None:
b = 15;
else:
b <<= sl
s |= b << STROBES[k]
dorn_config(0x08b, s)
def fifo_reset(**aa):
f = fifo_select(**aa)
if not f:
strobe(fifo_reset=True)
else:
dorn_config(0x084, f)
def fifo_read(**aa):
f = fifo_select(**aa)
if not f:
acmd(0x8800, verb=_verbose)
else:
dorn_config(0x085, f)
def default_config(thr=0x7ff):
for sl in CONFIG.slices:
for ch in range(CONFIG.n_channels):
thres(sl, ch, thr)
l2filter()
l3banana()
for sl in CONFIG.slices:
atriggers(sl)
def print_trigger_config():
tri = [[dorn_config(0x008 + i, slice=sl)
for i in range(4)]
for sl in CONFIG.slices]
ff = [dorn_config(0x084+i) for i in range(4)]
tri.append(ff)
e = ff[3]
df = [_ifc.Areg(0x11+i) for i in range(3)]
df.append(_ifc.Areg(5))
tri.append(df)
ct0 = tri[0][0] | (tri[0][1]<<16) & 0xff0000
ct1 = tri[1][0] | (tri[1][1]<<16) & 0xff0000
gt0 = (tri[0][2]<<8) | (tri[0][1]>>8)
gt1 = (tri[1][2]<<8) | (tri[1][1]>>8)
ns0 = tri[0][3] & 0xff
ns1 = tri[1][3] & 0xff
se0 = tri[0][3] >> 8
se1 = tri[1][3] >> 8
print(f"""narena trigger config:
trigger detector masks:
ctriggers: 0x{ct0:06x} 0x{ct1:06x}
gtriggers: 0x{gt0:06x} 0x{gt1:06x}
nsamples: {ns0:-6d} {ns1}
samples enable: {se0:-6d} {se1}
enable: {e&1:-6d} {(e>>1)&1}
fifos enable SA/EV/HK HI {(e>>7)&7:03b} {(e>>10)&7:03b} {(e>>6)&1}
empty {(ff[0]>>1)&7:03b} {(ff[0]>>4)&7:03b} {ff[0]&1} {(df[3]>>9)&7:03b}
packet {(ff[1]>>1)&7:03b} {(ff[1]>>4)&7:03b} {ff[1]&1} {(df[3]>>13)&7:03b}
full {(ff[2]>>1)&7:03b} {(ff[2]>>4)&7:03b} {ff[2]&1} {(df[3]>>5)&7:03b}
dig fifo status {df[2]&0x3ff} {df[1]&0x3ff} {df[0]&0x3ff}
""", file=sys.stderr)
return tri
def read_event(sl=None, ev=None):
if ev is None:
fifo_read(sl=sl, ev=True)
ev = _ifc._read_fifo(2)
if (ev[0] & 0xfffc) != 0x5718:
raise ValueError(f"EV packet magic mismatch {ev[0]:04x}")
Ba = ev[5]
Ph = ev[6]
A = ev[7]<<8 | ev[9]>>8
B = ev[8]<<8 | ev[9]&0xff
E = ev[10] | ev[11]<<16
if Ba & 0x8000:
Ba -= 0x10000
if Ph & 0x8000:
Ph -= 0x10000
if A & 0x800000:
A -= 0x1000000
if B & 0x800000:
B -= 0x1000000
if E & 0x80000000:
E -= 0x100000000
print(f"""PHA Event slice {ev[0]&3} ch {ev[4]&15}
time: {ev[1] | ev[2]<<16}
Δtime: {ev[3]}
lost: {ev[4]>>4}
A: {A/1024:.2f}
B: {B/1024:.2f}
banana: {Ba/0x4000:.4f}
φ: {Ph/0x4000:.4f}
E: {E/0x20000:.2f}
""", file=sys.stderr)
return ev
def read_samples(sl=0, n=None, sa=None):
if not sa:
if not n:
n = nsamples(sl)
sa = []
while n:
ff = dorn_config(0x085)
if not (ff & (8 << (3*sl))):
break;
fifo_read(sl=sl, sa=True)
sa.append(rfifo(3))
n -= 1
for s in sa:
if (s[0] & 0xfffc) != 0x5714:
raise ValueError(f"SA magic mismatch {s[0]:04x}")
t = s[1] | s[2]<<16
print(f"{t} ", " ".join([f"{ss&0xfff:5d}" for ss in s[3:]]), file=sys.stderr)
return sa

View file

@ -1,4 +1,4 @@
#! /usr/bin/ipython3 --profile=turbo_dose
#! /usr/bin/ipython3 --profile=thhor
import sys, time, getopt, fileinput, struct
import uart
@ -452,9 +452,11 @@ class dose_cmd(uart.uart):
"HKSZ": 0x0018,
"HKMA": 0x0019,
"HKHV": 0x001a,
"BATE": 0x037f,
}
def icmd(self, c, p=None, r=2, flgs=0):
def icmd(self, c, p=None, r=2, flgs=0, mes=None):
if c in self.AADDR:
c = self.AADDR[c]+flgs
w = [c | 0x8000]
@ -465,10 +467,23 @@ class dose_cmd(uart.uart):
w.extend([0x8001]*(r-1))
w.append(0)
d = struct.pack(f">{len(w)}H", *w)
print(f"icmd → {[f"{ww:04x}" for ww in w]}")
if self._verbose:
if mes:
mes = f"[{mes}]"
else:
mes = ""
print(f"icmd{mes}{[f"{ww:04x}" for ww in w]}")
d = self.fpga_cmd(d)
r = struct.unpack(f">{len(w)+1}H", d[:2*len(w)+2])
print(f"{[f"{rr:04x}" for rr in r]}", file=sys.stderr)
if self._verbose:
print(f" {mes}{[f"{rr:04x}" for rr in r]}", file=sys.stderr)
return r
def acmd(self, c, v=None, task=None, verb=True):
_v = self._verbose
self._verbose = _v
r = self.icmd(c, v, mes=task)[-1]
self._verbose = _v
return r
def rfifo(self, fifo=1, n=None):
@ -493,9 +508,9 @@ class dose_cmd(uart.uart):
print(f"FIFO[{fifo}] ← {[f"{rr:04x}" for rr in r]}", file=sys.stderr)
return r
def pressure(self, data=None, cmd=0x037f, fifo=1):
def pressure(self, data=None, cmd="BATE", fifo=1):
if data is None:
self.icmd(cmd)
self.icmd(cmd, mes="bate")
time.sleep(0.2)
data = self.rfifo(fifo)
import pressure
@ -505,6 +520,13 @@ class dose_cmd(uart.uart):
print(f"bate reading {p=:.2f} mbar, {T=:.2f} °C", file=sys.stderr)
return p, T, data
def menable(self, on=True, what=0x0001):
if on:
self.icmd("MCONF_SET", what)
else:
self.icmd("MCONF_CLR", what)
def flags2int(FLAGS, flags):
r = 0
if isinstance(flags, int):
@ -529,6 +551,10 @@ if tty:
tty = dose_cmd(tty, baud)
tty._export(globals())
tty._verbose = False
import dorn
from dorn import *
dorn._connect(tty)
dorn.CONFIG.thhor()
uart.set_prompt("GRETEL")