Skip to content

WIP - New mcu work and refactor - #189

Merged
vintagepc merged 73 commits into
MINI404from
merge-new-MCU-work
Sep 8, 2026
Merged

vintagepc merged 73 commits into
MINI404from
merge-new-MCU-work

Conversation

@vintagepc

Copy link
Copy Markdown
Owner

Wholesale merge of a feature/development tree for the C1 and new MCUs involved in the XBE (H503, C092).

Largely just a sync, the work is by no means complete and still rough around the edges.

- Stubbed out PWR
- Various RCC functionality
- Added python script(s) for auto-generating register data headers.
- UART print example now works.
- Update CRC module to use generated headers
- add more headers for other used chips
- Change DMA printfs to debug
- Fix bitbanding masking ADC peripheral space
- fix MPU region count
- Add extension board machine
- Add bridge variant for c1
- add example launch script
- initial 2d dashboard for extension
- Misc implementations for thermistors and fan on extension
vintagepc and others added 24 commits May 17, 2026 15:59
- Comment timer trace that's missing
- Add attempt at xdirect movement
- Fix incorrect pin assignments for PWM/step
- Add CXY motor handling
- Support Timer input capture
- Fix USART IRQ bugs
- Fix default timer ARR on reset
- First working XBE boot!
- XBE fan test passes
- Fix missing display invalidations if no motors exist
#5)

* prusa-mk4: set OTP BOM ID so the FW boots

buddy::hw::Configuration::check_bom_compatible() accepts board_bom_id >= 37
(plus legacy 12/14, and >= 1 on iX). The OTP_v4 initializer left .bomID at 0,
so every xBuddy machine -- MK4, MK3.x, CORE One, iX -- hit
bsod("BOM ID not compatible") on current firmware.

The BSOD fires before usb_device_init(), so nothing reaches the USB CDC
syslog and the failure presents as "CDC logging is broken" rather than
"the firmware stopped": the GUI still renders, cdc.log and itm.log stay
empty, and the USB DCTL register is never written.

MINI404 has carried this fix for a while; MINI418 never got it.

38 is a shipped customer board. Note bomID is a dedicated OTP byte -- the
27/34 digits in the datamatrix are the board revision, a different field.

* c1-bridge: route ANFC (0xdd) to the extension-board socket

CORE One polls an ANFC reader at modbus unit 221 every ~170 ms. The bridge
had the case scaffolded but the forwarding call commented out, so the frames
went nowhere: the master logged

    Failed to read input register 221:0x8000@63
    Puppies: Communication error, going to recovery puppies

on every poll -- 1146 times in a 100 s boot. Not fatal in the sim (the FW still
reaches the marlin loop) but on real hardware those retries saturate Modbus and
take MMU communication down with them.

The commented-out target resolves to C1_DEV_EXT, i.e. the same socket as the
xBE, so fake_puppies/xbe_dummy.py answers for both units. It can be told not to
(--no-anfc), which restores the unanswered-poll behaviour for reproducing the
saturation bug.

* prusa-mk4: name the thermistor and heater instances

Every qdev_new("thermistor") in the tree was anonymous, and on the xBuddy
machines the heaters were too, so there was no QOM path to address one with.
That blocks qtests (which is why there is no thermistor test) and makes
per-instance configuration impossible: -global applies to every instance of a
type, and a machine has five thermistors.

Names the five thermistors by sensor role and the two heaters as heater-E /
heater-B, matching what prusa-mini.c already does for its heaters, pinda and
ir-sensor.

    /machine/peripheral/thermistor-{noz,bed,brk,brd,case}
    /machine/peripheral/heater-{E,B}

* prusa-mk4: add ht-hotend machine property (PT1000 nozzle)

The HT hotend swaps the nozzle NTC for a PT1000, which the firmware detects at
boot by reading the raw nozzle ADC (hotend_detect.hpp). Simulating it needs the
nozzle thermistor -- and only that one -- to follow the PT1000 curve.

    -machine prusa-core-one,ht-hotend=on

Adds temptable_1010_ht to thermistortables.h, generated from the firmware's
thermistor_1010.h and reordered descending-by-temperature like the other tables
here. PT1000 is the reversed case (ADC rises with temperature); the existing
lookup in thermistor.c handles that unchanged, since it keys on temperature
rather than ADC. Verified against the firmware table: exact at every knot, worst
round-trip error 0.71 C, which is ADC quantisation (3.13 C/count at 400 C).

Named _ht because thermistortables.h already carries an inactive,
PtLine-generated upstream Marlin temptable_1010 guarded by THERMISTORHEATER_*.

Refuses ht-hotend on non-CORE-One machines (no HAS_HT_HOTEND firmware support)
and alongside the machine-level has_sock (the HT hotend takes no sock). Note the
latter guard does not see -global heater.has_sock=on, since -global is applied
at realize; that combination still produces a PT1000 nozzle with sock-reduced
thermal mass.

Property name uses a dash: -machine normalises `_` to `-`, so the neighbouring
has_sock machine property is in fact unreachable from the command line and only
settable via QMP qom-set. Left as-is.

* prusa-mk4: register the has_sock machine property as has-sock

-machine rewrites underscores to dashes in option keys before the QOM lookup
(keyval_dashify(), system/vl.c), so a machine property registered with an
underscore can never be set from the command line:

    $ qemu-system-buddy -machine prusa-mk4-027c,has_sock=on
    qemu-system-buddy: Property 'prusa-mk4-027c-machine.has-sock' not found

prusa-mini.c already registers "has-sock", and this file's own
"qtest-force-mmu" uses dashes; the xBuddy has_sock is the odd one out. It was
only ever reachable via QMP qom-set.

Not a breaking change for -machine users: the spelling that fails today starts
working, since both has_sock= and has-sock= now resolve to the same property.
QMP callers using the exact old name would need updating.

Device properties are unaffected -- -global heater.has_sock=on goes through a
different path and keeps its underscore.

* prusa-mk4: settable ambient and power-on nozzle temperature

The simulated hotend always started at a hardcoded 18 C and could only cool.
Boot-time hotend detection is about what the nozzle ADC does in the first few
seconds after power-on, so the bench needs to choose that starting point -- a
warm restart, a cold room, or a cold-soaked printer in hot air.

    -machine prusa-core-one,ambient-temp-c=45,nozzle-start-adc10=512

ambient-temp-c (default 25) is what the heaters cool towards; on CORE One that
is the chamber, which is effectively constant over a detection window (it takes
20+ min to move, against 3-8 s of observation).

nozzle-start-adc10 is the reading at power-on, in the 10-bit units every
firmware threshold is expressed in. It is resolved to a temperature through
whichever sensor curve the nozzle is configured with, so the same raw value
means 150 C on the NTC and 95.8 C on the PT1000 -- it describes the electrical
reality rather than assuming a sensor. Unset means start at ambient, which is
the only default that is correct for both curves.

Resolving it to a temperature (rather than pinning the ADC output) is what makes
the reading *evolve*: the model then cools or warms from there, and the slope is
the signal detection strategies actually read.

heater.c: adds ambient_temp_x10 / start_temp_x10, and makes the tick condition
two-sided. It was `currentTemp > ambientTemp + 0.3`, so a hotend below ambient
took the else branch on the very first tick and teleported to ambient with no
warming transient -- which made the cold-soak case unrepresentable.

thermistor.c: adds thermistor_temp_for_adc10(), the reverse of the sensor curve.
thermistor_select_table() is split out side-effect free so the lookup can probe
a table without driving IRQs.

* prusa-mk4: don't disturb machines that opt out, and drop the table probe

Two review findings.

ambient-temp-c defaulted to 25 and was applied unconditionally, so every
xBuddy machine's nozzle start temperature moved from its own 18 C baseline to
25 C whether or not anything used the feature. It now defaults to "unset" and
falls back to the machine's own value; the baseline only moves when
ambient-temp-c or nozzle-start-adc10 is actually given. Verified: MK4 and
CORE One both still report 18 C untouched, 45 C when asked for 45,
150 C for nozzle-start-adc10=626.

thermistor_table_for() resolved a table number by constructing a throwaway
ThermistorState on the stack and running the selection switch over it. That was
safe only while thermistor_select_table() stayed side-effect free -- one added
line driving an IRQ would have dereferenced GPIO pointers on a never-realized
device. Turned the switch into a plain (table_no) -> (table, len) lookup that
holds no device state, with thermistor_select_table() as a thin wrapper, so the
hazard is gone structurally rather than by comment.

* heater: use the measured cool-down constant on CORE One

The cooling branch decayed towards ambient with a hardcoded 0.005/s, i.e. a
200 s time constant, identically on every machine. Boot-time hotend detection
reads the *slope* of the nozzle ADC in the seconds after power-on, so that
constant is the signal, not a detail.

Measured on a CORE One over Grafana/Influx (temp_noz + ttemp_noz at 1 Hz, five
heat-and-cool runs; see sim/docs/HotendThermalMeasurement.md): 210 s standard and
230 s HT in still air. Wired per machine, defaulting to the previous 200 s so
unmeasured machines behave exactly as before. Also replaces pow(2.7183, ...) with
expf().

The sim now reproduces the measurement to about 2%: -0.565 vs -0.553 degC/s
(standard) and -0.516 vs -0.502 (HT) at 145 degC, the middle of the ambiguous
detection band. That is 3.5 ADC counts/s against 3.4 measured.

Deliberately NOT changed: the heating gain. Applying the measured constants to
heat-up drops the MK4 nozzle selftest from 227 to 190 degC, outside the
firmware's [195,245]. That is not a bad measurement -- the captures ramp from
cold, while the selftest starts from a *stabilised* 80 degC, and a single-node
lumped model has no notion of the block's internal gradient, so it cannot fit
both. Cool-down has no such ambiguity: it was measured directly, and it is the
half detection depends on. As a cross-check the measured HT constants put the
CORE One HT selftest at 238.8 degC, comfortably inside its [170,260] window.

Fan-dependent cooling (~160 s heatbreak fan, ~136 s print fan) is measured and
recorded in the doc but not wired: both fans are off at boot, which is the state
detection observes. cool_tau_s is a device property, so a scenario that needs a
fan-running rate can set it without new plumbing.

* thermistor: allow pinning the raw ADC reading

The sensor curve can only express readings a working probe could produce, so a
scenario could not until now describe a short, an open circuit, or a DMA buffer
that has not been filled yet -- all of which the firmware's boot hotend detection
classifies, and all of which sit outside every table.

    -machine prusa-core-one,nozzle-raw-adc10=0      # short / not-yet-valid
    -machine prusa-core-one,nozzle-raw-adc10=1023   # open circuit

Pins the nozzle channel for the whole run, bypassing the curve and the thermal
model. Note this is the opposite of nozzle-start-adc10, which is resolved
*through* the curve into a starting temperature and then evolves.

Also adds thermistor::HoldRaw(adc10) / ::ReleaseRaw script actions, so a probe
can fail during a run rather than only at boot -- an intermittent connection is
a different failure from a permanently open one.

This makes the two silent-demotion hypotheses testable. Both confirmed on a
CORE One with an HT hotend fitted:

    ADC 0     -> clear-NTC -> selected standard
    ADC 1023  -> clear-NTC -> selected standard

i.e. one bad reading makes the firmware classify an HT printer as standard with
no dialog; with the stored type already high_temp that silently rewrites EEPROM
and invalidates the nozzle selftest, and the dialog then appears on the *next*
boot. That is a strong candidate for the reported "dialog fires too often".

It also unlocks the MINTEMP/MAXTEMP RSOD scenarios: the same rail means opposite
things on the two curves, so the errors are expected to look swapped between a
standard and an HT hotend. See docs/HTHotendSimPlan.md 3c.

* heater: model fan-dependent cooling, sensor lag and transport delay

Three things the previous model could not express, all measured on a CORE One
(sim/docs/HotendThermalMeasurement.md 4c-4e):

Fan-dependent cool-down. A new "cooling-fan-in" GPIO array carries the print and
heatbreak fan duties, and the cool-down constant follows them. The two fans do
not add: the print fan blows directly under the nozzle tip while the heatbreak
fan only cools the heatsink above the thermal choke, so once the print fan runs
the heatbreak path stops mattering. Summing conductances over-predicts cooling by
~25% against measurement; the dominant-fan form reproduces all four measured
states to within 8% (18% in the one state whose measurement is itself uncertain).
Airflow enters as duty^0.6, which is the Nusselt-vs-Reynolds form and is what the
heatbreak fan's two measured duty points give.

Block -> thermistor lag and heater -> block dead time. The thermistor sits beside
the cartridge in the block rather than in it, so after PWM is applied the reading
does not move for ~1-2 s and needs several more to reach its asymptotic rate. A
one-node model is 6.7-21x worse than this on every cold ramp measured, on two
machines and both hotends. This matters because boot-time hotend detection is
being evaluated on pulses of exactly that length; without the lag the sim answers
such a question optimistically.

All four new properties default to "not characterised", which reproduces the
previous behaviour exactly, so no existing machine changes.

* prusa-mk4: apply the measured CORE One hotend thermals

Wires both cooling fans to the hotend heater and fills in the constants measured
on a CORE One (sim/docs/HotendThermalMeasurement.md 4d/4e). Machines without
measurements pass zeroes and are unaffected; the wiring is harmless for them.

The still-air cool-down was previously 210 s (standard) / 230 s (HT), taken from
fits whose captures had the heatbreak fan running at 51/255 throughout. With both
fans genuinely off it is 324 s / ~510 s. That is the state boot-time hotend
detection observes, because the firmware does not start the heatbreak fan until
45 degC, so the figure in use was wrong in exactly the regime the detection bench
runs in.

thermal_mass_x10 is left alone for the stock hotend: with the sock fitted -- the
shipping configuration, and the one that was captured -- 45 * 0.85 gives
3.83 degC/s against a measured 3.41, so touching it would only risk the selftest
window. The HT hotend cannot inherit that value, as it is heavier and takes no
sock; 33 is what its measured step response gives.

Checked against the captures with harness/heater_model_check.py: the step
response now tracks the real printer to within a few percent from 5 s onward for
both hotends, where before it was 1.4-2.5x too fast.

* prusa-mk4: drive the hotend heater from the timer's duty ratio

TIM3 emits a duty ratio 0..255 (f2xx_tim_calc_pwm_ratio), but every xBuddy
machine except iX fed it into the heater's "pwm_in" port -- the soft-PWM *edge*
input, which reads any nonzero value as "on" and then latches full power through
its 500 ms timeout. A proportional heater was therefore behaving as a bang-bang
one, and a sustained command was reaching the block half a second late and
leaving a spurious residual duty (tOn & 0xFF) behind on release.

iX was moved to "raw-pwm-in" when it was added; MK4, MK3.5 and CORE One were left
on the old port. This routes them all the same way and drops the branch.

Found while checking whether an 8 s open-loop heat pulse would be faithfully
reproduced for the boot-time hotend detection work; it is not, on the old path.
MK4 CDC smoke test still passes.

* fan: separate commanded duty from actual airflow, and split the fault modes

"pwm-out" carries what the firmware commanded, and dashboards consume it as such.
Nothing carried what the fan is actually moving, so the heater's cooling model had
to use the commanded duty -- meaning a stalled fan still cooled the hotend at full
rate. New "airflow-out" carries the real thing, and the hotend uses that.

The fault modes were also welded together. They are physically distinct and each
stages a different failure:

  is_stalled    seized rotor: no tach pulses and no airflow
  tach_failed   broken tach wire: the fan spins and cools, but reports nothing
  airflow_scale obstructed duct: cools less than commanded, and the firmware
                cannot see it at all

Stall/Resume keep their previous both-at-once meaning, so existing scripts are
unaffected; TachFail/TachOK and SetAirflow are new. The tach-only fault is the
common field failure and could not be staged before without also changing the
thermals and confounding whatever was being tested.

Requested for staging a fan selftest failure on a thermally healthy printer.

* thermistor: optional deterministic ADC noise

The model's ADC reading is exact, so any detection threshold tuned against the
sim looks more comfortable than it is. -machine nozzle-noise-adc10=N adds a
bounded, seeded perturbation to the nozzle reading so a classifier's *margin* can
be exercised. It cannot say what the real noise floor is; that still has to be
measured on hardware.

Off by default, and never applied to a pinned raw_adc10 reading -- a hard short
reads a steady zero, not a noisy one. xorshift32 seeded from a property, so a
scenario replays identically.

The output is otherwise refreshed only when the heater pushes a new temperature,
and the heater stops ticking once settled -- which is exactly the state
boot-time detection observes -- so a 10 ms timer re-emits while noise is enabled.
Without it the "noise" would be a single frozen offset. Note this means the
jitter updates at 100 Hz rather than per ADC conversion, so firmware-side
oversampling will not attenuate it the way it would real noise; the knob is
pessimistic, which is the safe direction for a margin test.

* stm32f4xx_rcc: stop returning uninitialised stack data from RCC->CSR

The CSR read handler built a fresh local, set only LSION/LSIRDY on it and
returned that, so bits 24..31 -- the reset-cause flags -- were whatever happened
to be on the stack. Any firmware branching on the reset cause was reading
nondeterministic data, and the stored reset value (0x0E000000: BOR, PAD, POR) was
never visible.

Read the stored register and overlay only the synthesised LSI status. Writes now
honour RMVF, which clears the flags as on hardware; previously it was ignored.

Note what this does NOT do: nothing sets IWDGRSTF, so an IWDG reset is still
indistinguishable from a power-on. The watchdog itself is modelled and does
request a real system reset (stm32_iwdg.c), so a reboot loop reproduces --
only the cause is unreadable. Wiring the watchdog to latch a cause across the
reset needs a link from the IWDG to the RCC that no machine currently has, and is
left for whoever needs it.

* prusa-mk4: stop inverting fan PWM the firmware does not invert

The firmware inverts fan PWM only when Configuration::has_inverted_fans() is
true, which is `!iX && board bom_id < 37`. Every machine in this file shares the
one OTP_v4 initializer in mk4_init(), and that has carried .bomID = 38 since
674710b0b5 -- so the firmware stopped inverting, while five of the six configs
here kept f_inverted = true. iX was wrong independently: the firmware never
inverts it.

The effect is silent and total: a commanded 100% arrives as 0% and a commanded
0% arrives as 100%. Confirmed by instrumenting the heater's fan input with guest
timestamps -- during an 8 s window in which the firmware held the print fan at
full, the model saw duty 0, and the fan only went to 255 once the firmware turned
it off.

Caught while validating the BFW-9282 hotend probe, where the fan phase cooled as
though no fan were running. With this fixed the same scenario's ADC delta goes
from 72 to 142 counts.

Note what this means for anything fan-related done in this sim since
674710b0b5: fan behaviour has been inverted on prusa-mk4-*, prusa-mk3v9,
prusa-core-one* and prusa-iX-*. mk3v5 was already correct.

* prusa-mk4: let ambient-temp-c reach the chamber limit

The setter capped ambient at 60 degC. A CORE One's own chamber limit is 65, which
is also where boot-time hotend detection puts its warm-nozzle interlock -- so the
one scenario aimed squarely at that interlock could not be expressed, and failed
with a zero-byte log rather than anything that pointed at the cause.

Widened to 0..100: generous for a test knob, still rejects a typo'd 1000.

* heater: reject out-of-range dead_time_ms and a zero cool_tau_s at realize

Two limits that were implicit. dead_time_ms is a uint16 but the PWM history holds
only 31 slots of 250 ms, so anything above 7750 ms was silently truncated to it.
cool_tau_s is a divisor and a zero would have produced an infinity.

Both now fail at realize with a message naming the actual bound. A property whose
range is really set by a constant somewhere else, with nothing linking the two, is
how a knob ends up quietly doing something other than what it says -- the same
shape as the two bugs this branch already fixes.

* fixup! thermistor: allow pinning the raw ADC reading

* fixup! thermistor: optional deterministic ADC noise

* fixup! heater: model fan-dependent cooling, sensor lag and transport delay

* fixup! prusa-mk4: add ht-hotend machine property (PT1000 nozzle)

* fixup! heater: model fan-dependent cooling, sensor lag and transport delay

* fixup! prusa-mk4: apply the measured CORE One hotend thermals

* fixup! prusa-mk4: drive the hotend heater from the timer's duty ratio

* Update fan.h

Clean Claude drivel

* Update prusa-mk4.c

Delete claude drivel that's self evident or not relevant to the current state of the code

* - Delete claude drivel/novellas/tangents
- Collapse pointless single-use functions
- Simplify  inefficient constructs/patterns

* heater: restore the vmstate version bump for the sensor-node fields

The cleanup set .version_id back to 1 but left four fields tagged _V(...,2):
sensor_x100, fan_duty, pwm_hist and pwm_hist_idx. vmstate_field_exists()
skips any field whose version_id exceeds the description's, so all four are
silently dropped from the stream -- a restored heater loses the sensor node,
the per-fan duty the cooling model reads, and the dead-time ring buffer.
post_load then read sensor_x100 unguarded, so sensorTemp came back from a
value that was never loaded.

Either the fields or the bump had to go; keeping the bump preserves loading
of a v1 stream, which minimum_version_id = 1 still promises.

---------

Co-authored-by: D.R.racer <drracer@drracer.eu>
Co-authored-by: vintagepc <53943260+vintagepc@users.noreply.github.com>
…1.0-MINI404

Closes #4, Closes #8 (via #186, #188)

Prep for merge to public
@vintagepc
vintagepc requested a review from DRracer September 5, 2026 16:36
@codecov

codecov Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 63.51430% with 625 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (MINI404@1a669bc). Learn more about missing BASE report.

Files with missing lines Patch % Lines
hw/arm/prusa/parts/c1_bridge.c 10.71% 150 Missing ⚠️
hw/arm/prusa/prusa-mk4.c 51.44% 73 Missing and 11 partials ⚠️
hw/arm/prusa/stm32_common/stm32_adc.c 43.47% 58 Missing and 20 partials ⚠️
hw/arm/prusa/stm32_common/stm32_fdcan.c 67.29% 55 Missing and 14 partials ⚠️
hw/arm/prusa/parts/thermistor.c 51.93% 54 Missing and 8 partials ⚠️
hw/arm/prusa/parts/heater.c 57.89% 15 Missing and 9 partials ⚠️
hw/arm/prusa/parts/tmc2130.c 4.00% 23 Missing and 1 partial ⚠️
hw/arm/prusa/stm32_common/stm32_usart.c 45.16% 12 Missing and 5 partials ⚠️
hw/arm/prusa/prusa-xbuddy-extension.c 79.45% 13 Missing and 2 partials ⚠️
hw/arm/prusa/parts/fan.c 68.42% 10 Missing and 2 partials ⚠️
... and 18 more
Additional details and impacted files
@@            Coverage Diff             @@
##             MINI404     #189   +/-   ##
==========================================
  Coverage           ?   53.01%           
==========================================
  Files              ?      139           
  Lines              ?    14195           
  Branches           ?     2655           
==========================================
  Hits               ?     7526           
  Misses             ?     6090           
  Partials           ?      579           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@vintagepc
vintagepc merged commit 077e406 into MINI404 Sep 8, 2026
8 checks passed
@vintagepc
vintagepc deleted the merge-new-MCU-work branch September 8, 2026 17:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants