0
mirror of https://github.com/torvalds/GuitarPedal.git synced 2026-08-14 04:43:53 +00:00
Files
torvalds-GuitarPedal/Validation/exp-probe.py
Linus Torvalds 0ee77abcc9 Find out what the expression jack actually reads
The usb-stomp boards have a TRS jack on two ADC pins that has never had
anything plugged into it, and two quite different things are meant to go
there: a double footswitch shorting tip and ring to sleeve, and an
expression pedal, which is a potentiometer.  Before either can be built
on, somebody has to find out what the jack reads.

So this is a probe and not a feature.  SysEx 0x0e sweeps every pin
configuration worth trying - both hi-Z, both pulled up, and each pin
driven high while the other is read - and reports the raw counts.
Nothing polls it and nothing acts on it.

The wiring decides most of the design:

	J103.T --- R107 1k --- GPIO27/ADC1 --- C108 22nF --- GND
	J103.R --- R106 1k --- GPIO26/ADC0 --- C105 22nF --- GND
	J103.S, J103.TN, J103.RN --- GND

The 1k in series with each contact is what makes driving a pin safe.
Anything plugged in here shorts one of them to sleeve sooner or later,
and 3.3mA is not a fault.  A BAT54S on each clamps to the rails.

Both normalling contacts are grounded, so an empty jack reads as two
pins at zero - the same as a footswitch held down.  That ambiguity is
real and cannot be resolved from the jack; only "plugged in and not
pressed" is distinctive.

And expression pedals do not agree on which contact is the wiper, so
both polarities are probed rather than picking the Roland convention and
hoping.

The 22nF is why the first version reported an open circuit as a pedal
held at maximum, which is the one wrong answer that would have been
believed.  With nothing connected past the jack there is nothing to
discharge it, so the driven measurement returned whatever the previous
step left on the capacitor - and the previous step is the pull-up, which
charges it to the rail.  An open-ended cable came back 4041 and 4047 out
of 4095.  The pin is now held low as an output for a millisecond before
the read, rather than pulled down, because 50 ohms empties it at once
where a pull-down would cost another time constant.  The same open cable
now reads 57 and 60.  The float pair has the same disease, is kept, and
is relabelled as residual rather than measured.

Then the part worth having probed both ways for.  Measured against a
real pedal - a Sonicake VEXPRESS, Roland convention, nominal 10k:

	drive RING -> read TIP      3 .. 3677    span 3674
	drive TIP  -> read RING      6 .. 3676    span 3670

Both sweep essentially the full range, so a test that only asks whether
the reading moves passes either one.  The shape is what differs, and it
falls out of which part of the pot the 1k series resistor is loaded by.
Driving the supply and reading the wiper puts the whole pot across it,
so the answer is linear in position.  Driving the wiper leaves only the
lower section loading it, and that section shrinks as the reading rises:
the response saturates, and at half travel it is already at 92% of its
range, which would pack most of the resolution into the first third of
the treadle.

So the useful configuration is the opposite of the obvious one - drive
the ring, read the tip - and the endpoints alone cannot tell you that.
It is written down in exp.h next to the probe, because the numbers that
show it only exist while a pedal is plugged in.

The temperature sensor goes out with the sweep, because the expected
reading on this jack is zero and a board that reads zero everywhere
should be able to say whether its ADC is converting at all.

Measured with nothing plugged in: the grounded pins read 3 counts, the
pulled-up pins 75, which back-solves the internal pull-up to 54k against
the 1k - so the divider is doing exactly what the schematic says.  Noise
over twenty sweeps is a standard deviation under half a count and a
peak-to-peak of one, against the 34 counts a step would need for the 120
levels this is wanted for.

Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2026-08-06 09:47:39 -07:00

158 lines
5.1 KiB
Python
Executable File

#!/usr/bin/env python3
#
# What is plugged into the expression jack, read straight off the ADC.
#
# ./exp-probe.py one reading
# ./exp-probe.py --watch keep reading until interrupted
#
# Bringup only. The firmware side is exp.h, reached by SysEx 0x0e, and
# neither end is wired into anything the pedal does - this exists to find
# out whether the jack is worth building on, on a board where it has
# never been used.
#
# WHAT TO EXPECT. Both normalling contacts are grounded, so an empty
# jack reads near zero everywhere and that is correct rather than broken.
# The interesting rows are the ones that need something plugged in:
#
# nothing all near 0, because the jack grounds itself
# TRS, nothing pressed pull-up rows near 4095
# TRS, tip to sleeve pull-up tip near 0, ring still high
# expression pedal one of the driven rows sweeping with the treadle
#
# The temperature row is not about the jack at all. It is the one input
# whose answer is known in advance, so a board reading zero everywhere
# can be told from a board whose ADC is not converting.
#
import argparse
import re
import subprocess
import sys
import time
import pedal
# Must match enum in exp.h - order is the wire format.
NAMES = [
("float ring", "hi-Z, no pull"),
("float tip", "hi-Z, no pull"),
("pullup ring", "footswitch: low = shorted to sleeve"),
("pullup tip", "footswitch: low = shorted to sleeve"),
("drive tip -> read ring", "expression, tip = supply"),
("drive ring -> read tip", "expression, ring = supply"),
("temperature", "ADC self-check, not the jack"),
]
FULL_SCALE = 4095
VREF = 3.3
def volts(raw):
return raw * VREF / FULL_SCALE
def temp_c(raw):
# RP2350 datasheet: T = 27 - (V - 0.706) / 0.001721
return 27.0 - (volts(raw) - 0.706) / 0.001721
def probe(port, wait=1.5):
"""Ask for one sweep and decode the reply, or None."""
dump = subprocess.Popen(["aseqdump", "-p", port],
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
text=True)
try:
time.sleep(0.4)
pedal.send(port, 0x0E)
time.sleep(wait)
finally:
dump.terminate()
text = dump.stdout.read()
dump.wait()
blob = bytes.fromhex("".join(
re.findall(r"System exclusive\s+((?:[0-9A-Fa-f]{2} ?)+)", text)
).replace(" ", ""))
i = blob.find(bytes([0xF0, 0x7D, 0x0E]))
if i < 0:
return None
body = blob[i + 3:]
end = body.find(0xF7)
if end < 0:
return None
body = body[:end]
if not body or body[0] != 1:
return None
pairs = body[1:]
return [(pairs[j] << 7) | pairs[j + 1] for j in range(0, len(pairs) - 1, 2)]
def show(vals):
for (name, note), raw in zip(NAMES, vals):
extra = f" ({temp_c(raw):.1f} C)" if name == "temperature" else ""
bar = "#" * round(24 * raw / FULL_SCALE)
print(f" {name:24} {raw:5} {volts(raw):5.3f} V {bar:<24}{extra} {note}")
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--watch", action="store_true",
help="keep probing; move the treadle and watch it track")
ap.add_argument("--interval", type=float, default=0.0,
help="extra seconds between sweeps in --watch")
args = ap.parse_args()
found = pedal.discover()
if not found:
print("exp-probe: SKIPPED - no pedal found")
return 0
d = found[0]
print(f"exp-probe: {d['label']} (midi {d['port']})")
if not args.watch:
vals = probe(d["port"])
if vals is None:
print(" no reply - is this firmware built with the probe in it?")
return 1
print()
show(vals)
return 0
#
# Only the two driven rows move with a treadle, so the watch view is
# those plus whichever pull-up row is doing something. Printed as a
# line each time rather than redrawn, because what matters when you
# sweep a pedal is the range and the steps, and a scrolling log keeps
# both where you can see them.
#
print(" drive-tip->ring drive-ring->tip pullup ring/tip (^C to stop)")
lo = [FULL_SCALE] * 2
hi = [0] * 2
try:
while True:
vals = probe(d["port"], wait=0.35)
if vals is None:
print(" (no reply)")
continue
a, b = vals[4], vals[5]
lo = [min(lo[0], a), min(lo[1], b)]
hi = [max(hi[0], a), max(hi[1], b)]
print(f" {a:5} ({volts(a):5.3f}V) {b:5} ({volts(b):5.3f}V)"
f" {vals[2]:5} {vals[3]:5}"
f" span {hi[0]-lo[0]:5} {hi[1]-lo[1]:5}")
if args.interval:
time.sleep(args.interval)
except KeyboardInterrupt:
print()
for i, (name, _) in enumerate(NAMES[4:6]):
span = hi[i] - lo[i]
levels = span / 32.0 # 4096/128, one pot step
print(f" {name:24} {lo[i]:5} .. {hi[i]:5} span {span:5}"
f" = {levels:5.1f} pot steps of 128")
return 0
if __name__ == "__main__":
sys.exit(main())