envproduction·api/v1·backendcommongnd.org·checking…build
[ §1 · control ]

The defaults are somebody else's coil

DRSSTC

A programmable driver arrives with every threshold already set. Somebody typed those numbers against the coil in front of them, and the documentation and the firmware no longer agree.

A programmable driver arrives with every threshold already set. Somebody typed those numbers once, against the coil that was in front of them, and the compiler has carried them forward ever since. UD3 is open source, so you can read exactly which numbers they are, and reading them is how you discover that three of the current limits its own documentation describes are not in the firmware at all.

What it is and how it works

A configurable instrument keeps its settings in a table. Each row carries a name, the address of the variable it writes into, a permitted range, a scale for turning what the operator types into what the machine stores, and a function to call afterwards. The terminal walks that table to answer a request for a value, and walks it again to find the row a new value belongs to.

The table says nothing about what any value ought to be. That lives somewhere else, in an initialisation routine that assigns a constant to every variable the table points at and runs once, before anything else does. Those constants are the defaults. They are not recommendations and they are not safe values. They are what was on the author's bench.

Then there is a store that survives a power cut. At start-up the initialisation runs first and the store is read over the top of it, so a board that has ever been saved comes up on its own settings and a board that has not comes up on the constants. Two routes to the same variable, and only one of them is visible in the source.

The documentation is a fourth thing. A different file, usually in a different repository, written by different people at different times, and nothing in the build compares it against either the table or the constants. A feature can be deleted from the firmware and the page describing it stays exactly as convincing as it was the day before. There is no compiler for prose.

What it does in a coil

UD3 is Jens Kerrinnes' programmable driver, the end of the UD line and the revision that gives up the phase lead inductor in favour of a number in nanoseconds. What it is and where it sits in that line is the UD boards. Its initialisation routine is init_config() in common/ud3core/cli_common.c, a flat block of assignments, and every reading below is from commit 52a2e54.

The coil the defaults describe

start_freq, the frequency the bridge runs at before feedback takes over, ships at 63.0 kHz. The autotune sweep ships to run from just over 40 kHz to 100 kHz. That is a large, slow machine with a lot of primary inductance, and it is a band a great many amateur coils sit entirely outside. Load the defaults onto a primary that resonates above 100 kHz and the sweep never reaches it. The command still reports a peak, because it reports whichever of its 128 samples drew the most current and one of them always did, and that peak means nothing.

That is the whole argument in one parameter. The rest of this section is how far the same problem reaches.

The amps are not amps

The current limits are the numbers people copy out of forum threads, and they are the numbers that mean least on their own. The firmware never compares a current against a current. It converts, in ZCDtoPWM.c:

DAC steps = (max_tr_current / ct1_ratio) * ct1_burden / (0.016 * 10)

0.016 is the volts per step of the comparator's reference DAC. The ten is there because the burden is stored in tenths of an ohm. Everything the driver knows about amperes arrives through ct1_ratio and ct1_burden, and the two are not the same kind of quantity. ct1_burden ships at 3.3 ohm, which is R41 on the UD3 board itself and is therefore right until somebody changes the resistor. It is not the 51 ohm that overcurrent is not a setting quotes: that is the analogue board's feedback burden, and the UD boards files it under UD2.7. ct1_ratio ships at 600, which is a transformer somebody wound.

So setting a current limit and leaving the ratio alone does not set a current. It sets a voltage on a comparator, and what current that voltage corresponds to is your transformer's business. The error is multiplicative and silent: run a 1:1000 current transformer against the shipped 600 and a dial reading 400 A opens the bridge at 669. Most of that gap is the ratio of the ratios, 1000 over 600; the rest is the comparator's own rounding, which the next paragraph gets to. Nothing in the terminal says so. The number you typed is still the number that comes back.

The same conversion sets a ceiling you cannot type past. The DAC spans 255 steps of 16 mV, so the highest threshold it can express at the shipped transformer figures is 741 A, and the firmware prints a one-line warning and then writes that back over anything larger. The row in the parameter table goes on declaring a maximum of 8000, because the table holds a range and a range is a constant, while what the hardware can actually reach depends on two other rows. The strength of the feedback path is what fixes that ceiling: 3.3 ohm across a 1:600 ratio is 5.5 mV per ampere, and 4.08 V of DAC divided by 5.5 mV/A is 741.8, which the firmware's own integer arithmetic truncates to the 741 it writes back. A weaker burden buys range and costs resolution, and neither side of that trade is described anywhere on the parameter page.

The rest of the case for not trusting a threshold you have not measured is overcurrent is not a setting.

The ramp is 400 steps and it ships empty

The QCW ramp is an array of 400 unsigned bytes in qcw.h, played out one entry per interrupt at 8 kHz, so one step every 125 microseconds and 50 ms of buffer. The parameter that shapes it is a slope, not a duration:

ramp_increment = qcw_ramp / 100.0        (qcw.c)

At the shipped qcw_ramp of 200 that is two counts per step. Run the loop from zero and the first sample carrying the full 255 is the 130th, 16.25 ms in, not the 128th: the increment is skipped on the first pass and the last one is trimmed by the ceiling. The shipped max_qcw_pw clips the pulse at 10 ms, which is 80 samples, and the last one written is 156 of 255. The default slope and the default ceiling do not fit each other: on the shipped values the modulator tops out at 61 per cent and the ramp is cut off climbing.

None of which happens, because the shipped qcw_pw is zero. The buffer length is computed from it, so it comes out at zero, the stop index is zero, and the first thing the ramp handler does is find the index already past the end and switch the modulator off. A UD3 loaded with defaults produces no ramp at all. What the ramp is doing to the coil while it plays is the shape of the ramp; this page only owns what ships.

What the documentation has that the firmware does not

The parameter reference on the UD3 wiki reached its present form in the first week of March 2022, in three commits by Dan Smith, and has been edited once since, by malte0811 in August 2022. It is careful, it is thorough, and it documents twelve parameters that this firmware does not have.

Five of them belong to one absent subsystem, and the firmware built that subsystem as two mechanisms rather than one. max_const_i and max_fault_i set a software fuse: an i-squared-t integrator fed from the bus current, which the UD3 reads from a Hall effect sensor on its IBUS connector, and which on reaching its trip point raised a fault and killed the interrupter outright. max_dc_curr is a separate bus current setpoint, and pid_curr_p and pid_curr_i tune the loop that held the duty cycle down below its configured ceiling to keep the bus current at that setpoint. The wiki's fuse_reset entry runs the two together, saying the UD3 tries to limit the duty cycle to keep the fuse under 60 per cent of the trip point. In the code the duty loop was gated on max_dc_curr and regulated to max_dc_curr, and the fuse's own 60 per cent was a warning state and nothing more. Both mechanisms are gone.

The wiki describes the whole thing, gives each parameter a default, and, on the commands page rather than the parameter page, adds the hedge "Note: This feature is still experimental and may not function correctly." It does not say that it is gone. Grep the firmware for any of the five names and nothing comes back. The fuse went in on 17 March 2019 in a commit called "electronic fuse" and came out on 14 May 2024 in a commit called "Cleanup". The names survived another year in a hand-written tab-completion list and were swept up in July 2025 by a commit about autocompletion.

One piece of it is still in the firmware. tsk_overlay.c registers a telemetry channel named Fuse, in per cent, minimum zero and maximum 100, which is set to zero at start-up and never updated afterwards by anything in the firmware. It is bound to no gauge and no chart, so it is not on your screen unless you put it there. The command telemetry ls prints its name in the list of channels, and telemetry gauge will accept it. The channel is real, the reading is not.

The same "Cleanup" commit took the other half of the claim. The wiki documents the tuning command as tune [prim/sec], "Autotunes the primary or secondary coil", and hedges honestly that the secondary option needs custom wiring and is being phased out. It adds, as a separate sentence and without joining the two, that the signal is mapped to pin 49 of the processor and pin 49 is not routed anywhere on the UD3 board. In the firmware the branch is simply not there. CMD_tune in autotune.c reads its argument only to test it against -?, its own usage line is Usage: tune, and it makes one call to one sweep of the primary. There is no secondary autotune. What is still there is the advertisement: the command is registered with the help string "Autotune [prim/sec]" and a completion list containing prim and sec. Type tune sec and you get the primary sweep, with no complaint.

Three more absences, briefly. ena_display and the display command drove a WS2812 light strip, removed in April 2025, though the strip's hardware component is still sitting in the PSoC project. ct3_burden and ct3_ratio configure the secondary current transformer on that same unrouted pin 49, and the wiki flags them itself. pwp, min_enable, synth_filter and vol_mod complete the twelve.

Going the other way, twenty-six parameters exist in the firmware and appear nowhere on the parameter page, including the entire temperature loop. That count is ours, from matching the eighty-one parameter rows in cli_common.c against the sixty-seven bold headings on the wiki page. Twenty-four of the twenty-six are declared visible in the terminal; the other two, d_factor and hwGauge_cfg, are flagged not to print.

There is a controller on temperature and it is worth being exact about it, because the name oversells it in three directions at once. It is called PID, its derivative term is passed in as zero and its own help text calls it a PI. It runs at 10 Hz. It does not touch the coil's power: it writes a PWM compare register, and that register is one of the two general purpose relay outputs, the same two the relay and pwm commands drive. The fan is a different output altogether, switched on and off by a plain comparison against temp1_setpoint. And pid_temp_mode ships at zero, which is off.

Two defaults that are not defaults

init_config() assigns every field of the configuration structure except one. is_qcw, which the terminal calls qcw_coil and which decides whether the QCW commands do anything at all, is never touched. On a virgin board it is zero because it is a static variable, and load_default leaves whatever was there. It is the difference between a value chosen and a value inherited, and the terminal prints them identically.

The same is true of pwd, the pulse period, which is the parameter that sets the note. The wiki gives it a default of 50000 microseconds. The firmware never assigns it, so it is zero on a fresh board, and the transient path in the interrupter has an explicit guard that returns without doing anything when it finds a zero there. A guard like that is written by somebody who has seen the case happen. It is also further away than it looks, because that path is only entered when synth is set to transient mode, and synth ships off.

And load_default does not reach the hardware

CMD_load_defaults calls init_config() and returns. init_config() regenerates the ramp buffer and writes the routing register that inverts the UART lines and the status LED, and that is everything it pushes anywhere. The boot path is different: it calls the initialisation, reads the stored configuration over it, and then calls configure_ZCD_to_PWM(), which is what recomputes the comparator thresholds, the lead time in bus cycles and the start-up period. So does eeprom load. CMD_load_defaults does none of it.

So after typing load_default the terminal reports the shipped values and the hardware is still holding the previous ones. A reset fixes it. So does setting a parameter, but only one whose row carries a callback that reconfigures the hardware, and most rows do not. Of the sixty-two configuration rows, twenty-nine reach configure_ZCD_to_PWM() and nineteen carry no callback at all, so setting one of those prints OK and reaches nothing. Those two counts are ours, from the callback column of the table itself. lead_time and start_freq are on the right side of that line. qcw_coil, temp2_max, autostart and max_fb_errors are not. The window between is quiet and it is the window in which people take a first shot.

The numbers

All from commit 52a2e54 unless the line says otherwise. Where the arithmetic is ours it says so.

Ramp buffer            400 samples, 8 bit, one per 125 us (8 kHz)
                       = 50 ms end to end, our arithmetic
Ramp slope             ramp_increment = qcw_ramp / 100.0
  at the default 200   2.0 counts per step; running the loop in qcw.c,
                       the first sample at the full 255 is the 130th,
                       so 16.25 ms, our arithmetic
Shipped pulse          qcw_pw = 0, so the ramp has zero length
Shipped ceiling        max_qcw_pw = 10.00 ms = 80 samples, last sample
                       156 of 255, 61 per cent, our arithmetic

Feedback CT            ct1_ratio 600, ct1_burden 3.3 ohm (R41)
Feedback strength      5.5 mV/A, our arithmetic on those two
Current limits         max_tr_current 400 A, max_qcw_current 300 A,
                       min_tr_current 100 A, min_fb_current 25 A
Comparator DAC         255 steps of 16 mV, span 4.08 V
  400 A lands on       137.5 steps, rounded to 138 = 2.208 V,
                       our arithmetic
  those 138 steps      against a 1:1000 CT and the same burden are
                       669 A, our arithmetic
  one step is worth    2.9 A at the shipped CT figures, our arithmetic
  OCD ceiling          741 A, the firmware's own integer expression;
                       4.08 V / 5.5 mV/A is 741.8, our arithmetic
  peak reading ceiling 909 A, the firmware's own integer expression

Start-up               start_freq 63.0 kHz, start_cycles 3
Autotune               tune_start 400, tune_end 1000, in units of
                       100 Hz; 128 points, one sample each,
                       tune_pw 50, tune_delay 50 ms
  the band it walks    first sample 40.4 kHz, last 100.0 kHz, our
                       arithmetic on the index expression in autotune.c
  spacing              about 470 Hz per point, our arithmetic, and it
                       lands on whole units of 100 Hz, so steps
                       alternate 400 and 500 Hz
Phase lead             lead_time 200 ns, truncated to whole 64 MHz bus
                       cycles of 15.625 ns, so 12 cycles and 187.5 ns,
                       our arithmetic on the firmware's expression
Temperature            temp1_max and temp2_max 40 C, fault after five
                       consecutive readings above
Temperature PI         off, setpoint 45 C, P 0.2, I 0.2, 10 Hz, output
                       on relay 3 or relay 4
Watchdog               on, 1000 ms
Duty ceilings          max_tr_duty 10.0 per cent, max_qcw_duty 35.0

The wiki's own arithmetic on the lead is worth carrying across, because it goes further than the firmware does. It states a propagation delay of six bus cycles, 94 ns, inside the hardware, and gives a worked example: ask for 300 ns and you get 203.125. Apply the same correction to the shipped 200 and the actual lead is 93.75 ns. That figure is the wiki's method on the firmware's default, not anybody's measurement.

That number is this article's argument in its sharpest form, and it needs no deleted feature to make it. The wiki's own lead_time entry says reasonable values are around 500 for small bricks and 1000 for large bricks. The UD boards puts it at 1000 to 2000 nanoseconds on large slow bricks. Those two do not agree with each other, and neither of them agrees with the 200 the firmware ships. We have not adjudicated between them here. The point is narrower and it holds either way: the shipped value is not a recommendation from anybody, including the page that documents it.

What will get you

What goes wrong

  • You set max_tr_current to 2000 and get returns 741. The comparator DAC cannot express more at your ct1_ratio and ct1_burden. The firmware prints a warning and clamps. Change the transformer or the burden, not the limit.
  • Overcurrent trips far from where the dial says. The ratio, the burden, or both. Work out the voltage the firmware is computing and measure it at the comparator, because that is the only quantity in the chain that is not an assumption.
  • tune reports a confident peak in the wrong place. The sweep ran over the shipped band, just over 40 kHz to 100 kHz, and picked the largest of 128 samples in a band your coil is not in. It always returns something.
  • tune sec behaves exactly like tune. There is no secondary sweep. The help string and the tab completion are older than the code.
  • You assign the Fuse channel to a gauge and it reads zero for ever. Nothing writes to it. It is the surviving front panel of a feature removed in 2024, and it is not on a gauge until you put it on one.
  • qcw start says this is not a QCW coil. qcw_coil is the one setting load_default does not set, so it is whatever it was, and on a fresh board it is zero.
  • qcw start is accepted and nothing happens. qcw_pw is zero, so the ramp is zero samples long and the modulator is switched off on the first interrupt.
  • The ramp climbs and stops short of full. qcw_ramp and max_qcw_pw are set independently and the shipped pair do not meet. Slope times available steps has to reach qcw_max, or the bang ends mid climb.
  • The interrupter makes no sound on a board nobody has configured. pwd is not among the parameters the initialisation assigns, so on a fresh board it is zero and the transient update returns on its guard. load_default will not put it back to zero either, for the same reason: it never assigns it. And at the shipped synth of off, the pulse width callback takes no branch at all, so nothing reaches the interrupter until synth is set.
  • Changing lead_time by small amounts does nothing. It is truncated to whole 64 MHz bus cycles of 15.625 ns, so anything under one cycle of change is invisible, and the wiki reports a further six cycles of fixed delay below which nothing takes effect at all.
  • The coil faults on temperature with a cold heatsink. temp1_max ships at 40 C, and the thermistor constants ship for one particular part. A different NTC reads a different temperature at the same resistance, so the trip is at neither 40 nor anywhere you chose.

Where next


Source: the UD3 firmware at commit 52a2e54, read as source code, and its parameter and command wiki pages, read at wiki commit 686fbec. The firmware is Jens Kerrinnes' work, on Steve Ward's design; the parameter reference is Dan Smith's, with a later correction by malte0811. Where a figure is our arithmetic rather than theirs, the line says so.

more in DRSSTC