Add Alientek T90 (Nations N32L40x) BSP with GC9 colour display#2219
Add Alientek T90 (Nations N32L40x) BSP with GC9 colour display#2219vshuraeff wants to merge 23 commits into
Conversation
Adds a new IronOS build target for the Alientek T90B soldering iron: - Nations N32L40x (Cortex-M4, soft-float) vendor SDK + startup/linker (app base 0x08005000) - BSP integration layer (Setup/BSP/IRQ/Power/ThermoModel/flash) on the N32 std-periph driver - GC9-family 160x40 RGB565 SPI display via a GC9Display shim bound through OLED.hpp, rendering the mono 128x32 UI centered + transposed into the color panel - CH224Q USB-PD over bit-bang I2C; T245 tip thermo model (placeholder calibration) - Makefile family block (model=T90) + host-only .atk packaging rule Builds clean: FLASH 29.8% (106K), RAM 82.4% (32K). Analog calibration values are placeholders pending on-hardware measurement.
Safety / correctness fixes from the 5-corner (Codex + Antigravity) review: - heater ADC blanking: TIM4 update now forces the heater off for the injected tip sample and the duty is re-applied in ADC_IRQHandler after conversion; TIM2 OC preload disabled so the blanking takes effect immediately (cross-engine BLOCKER) - preStartChecks slow-PWM now represcales TIM2 (carrier), not TIM4 (schedule/safety) - flash: settings and boot logo split into two separate 2K pages so a settings save no longer erases the logo; clamp save length to one page; app region 106K -> 104K - cold-junction NTC reading scaled <<3 to match the 15-bit NTCHandleLookup (was stuck 45C) - drop CUSTOM_MAX_TEMP_C so isTipDisconnected uses the ADC-rail max, not the 400C cap (avoids a false 'no tip' at high setpoints) - CH224Q: do not report negotiated voltage until power_check confirms a live contract, so preStartChecks actually waits for PD to settle before heating - setTipPWM writes duty before re-arming the safety enable, with a barrier - fault handlers force the heater off before spinning - getInputVoltageX10 guards divisor==0; GC9 SPI waits are bounded; panel index guarded Builds clean: FLASH 30.4% (104K), RAM 82.4% (32K).
Lower-severity items from the concurrency/quality corners (Codex+Antigravity): - bound the ADC ENDC/RDY/calibration poll loops with a timeout (no unbounded spin) - flash.c uses the N32 FLASH_STS_CLRFLAG (incl. PVERR/EVERR), not the STM32F1 subset - clarify the NVIC priority comment (numeric>=5 = FreeRTOS-safe zone; do not lower) - fix two inherited comment typos Build still clean: FLASH 30.4% (104K), RAM 82.4% (32K).
- add T90 to the CI build matrix (push.yml) + build.sh AVAILABLE_MODELS - vendor a stdlib-only .atk packer (source/atk-pack.py) and wire .atk into firmware-% for T90 so the release build produces a flashable image in CI (replaces the host-only uvx path; ATK_NAME overridable) - archive *.atk alongside *.hex/*.bin/*.dfu - README: add T90 to Supported Hardware + downloads, with an experimental-port note
Two showstoppers found by validating against real T90 firmware images before flashing:
1. .atk format was wrong (T80P-style 32-byte ASCII header). The real T90 bootloader
format is a 13-byte header (magic 08 02 0A 01 A0 20 + uint32 payload size + 3-byte
build date) followed by the payload XOR 0xFF, with no payload checksum. Rewrote
atk-pack.py to this format; verified by byte-exact round-trip against the official
factory t90.atk and PanKleszcz's t90_msdmem.atk, and our T90_EN.atk now XOR-decodes
to a valid vector table (SP 0x20006000, reset in the 0x08005000 app region).
2. SRAM is 24K, not 32K (confirmed: factory/msdmem stacks at SP=0x20006000). The build
used 27K RAM (>24K) -> would hardfault on boot. Set the linker to 24K /
_estack=0x20006000 and rewrote GC9Display to STREAM the RGB565 conversion on the fly
instead of a 12.8K full-frame buffer. RAM now 14.2K/24K (57.7%), FLASH 30.3%/104K.
Result: T90_EN.{bin,atk} are now correctly sized and packaged for the real hardware.
Cross-checked against the N32L40x reference (PanKleszcz HS-02 BSP - note its panel is ST7735, so MCU-level only) and the N32L43 datasheet AF table; our GC9 init was already validated against the decompiled T90 stock firmware. - SPI1 AF was a uniform AF5 on PB3+PB5 (wrong -> blank panel). N32L40x AF is per-pin: SCK PB3 = AF1, MOSI PB5 = AF0 (confirmed by T90 firmware register writes AND N32L43 user-manual Table 5-23). Split the GPIO AF config in Setup.cpp + GC9Display.cpp. - ADC: enable the 1 MHz calibration/bandgap clock (RCC_ConfigAdc1mClk) and the internal reference (ADC_EnableTempSensorVrefint); without them calibration/conversions feeding the PID are untrustworthy. - GC9: add a ~120 ms power-on settle before the 0xFE/0xEF unlock (matches stock). - preRTOS: __enable_irq() in case the bootloader hands off with interrupts masked. Build clean: FLASH 30.4%/104K, RAM 57.7%/24K.
A small dependency-light C toolkit to work with the Alientek T90 firmware image format and (experimentally) flash over USB, so the iron can be packed/flashed without the official Windows updater. - pack/unpack/info: pure C, no deps. Implements the reverse-engineered T90 .atk format (13-byte header magic 08 02 0A 01 A0 20 + uint32 size + 3-byte date; payload XOR 0xFF). Verified by byte-exact round-trip against the real factory t90.atk and t90_msdmem.atk. - probe/flash: hidapi (auto-detected), implementing the Alientek HID bootloader protocol from the T80P reference (dozed-dev/atk-fw-util). T90 update-mode VID/PID + chunking are not fully confirmed, so flash is EXPERIMENTAL and gated behind --force, with --vid/--pid overrides and a recovery-first workflow documented in the README.
…, buttons) The iron now boots into the IronOS UI. Fixes that make it run: - preRTOS FPU frame: the update-mode bootloader hands control off with CONTROL.FPCA set (the factory firmware is hard-float), but this is a soft-float build on the FreeRTOS ARM_CM3 port, whose PendSV never saves an FP exception frame. With FPCA set the core stacked extended (FP) frames and armed lazy S0-S15 stacking into stack slots the port does not account for, clobbering a preempted task's saved return address -> deterministic UsageFault INVSTATE (EXC_RETURN 0xFFFFFFED). Clear FPCA, disable FPCCR ASPEN/LSPEN and leave CP10/CP11 denied so every exception uses the 8-word basic frame the port expects. - preRTOS bring-up order: stop the leftover bootloader SysTick (+ clear the pending tick) so xPortSysTickHandler cannot fire before the scheduler, clear all NVIC enables/pendings (leftover bootloader IRQs), and defer __enable_irq() to the end of bring-up. - linker: cap SRAM at the proven-real 20K (_estack 0x20005000); 0x20006000 (24K) faults on this part. Reserve the HID-bootloader app-validity gate (8 bytes 00 00 00 00 00 FF FF FF at 0x08005800), the only thing the bootloader checks before jumping to the app. - display: drop the unneeded AFIO clock; SPI1 alternate-function on the N32L40x is configured per-pin via AFL/AFH, not through AFIO remap. - buttons: the panel is mounted rotated 180 deg from the reference (the comfortable right-hand grip), so map logical A/B to the opposite keys so the +/- buttons match their on-screen meaning.
…nsweep - The post-END commit must use SetBoot (0x15), which finalizes the freshly flashed image (sets the app-valid flag) and reboots. SetStartAPP (0x16) only jumps to an already-valid resident app without finalizing, so a fresh flash committed with 0x16 never boots. Confirmed live on the device. Default the commit code to 0x15; add --commit <code> and --no-commit overrides. - Add an fnsweep subcommand to probe the bootloader function-code space against addr 0xAF, plus a --dexor option, to support protocol reverse-engineering.
The T90 has a QST QMA6100P motion sensor on the same bit-bang I2C bus as the
CH224Q (PB6=SCL / PB7=SDA, 7-bit address 0x12, chip-id 0x90); the earlier
bring-up wrongly assumed there was no accelerometer. Wire it into the movement
framework so the iron sleeps when idle and wakes on motion.
- new Core/Drivers/QMA6100P.{hpp,cpp} + QMA6100P_defines.h: static-class driver
in the SC7A20 mould. detect() = probe + chip-id 0x90; initalize() runs the QST
init sequence (soft reset, wake/MCLK, +/-8g range, default ODR); getAxisReadings
reads the 6 output bytes as 14-bit left-justified signed values; getOrientation
derives hand/flat in software (mapping flagged for on-device tuning).
- MOVThread: add the ACCEL_QMA detect/read branches; add AccelType::QMA.
- Translations: add the "QMA" accelerometer name.
- T90 configuration.h: drop NO_ACCEL; enable ACCEL_QMA + ACCEL_I2CBB2 +
I2C_SOFT_BUS_2 (the accelerometer talks over the I2CBB2 soft bus).
- Software_I2C.h: expose the PB6/PB7 soft-I2C macros for I2C_SOFT_BUS_2 too, not
just CH224_SOFT_I2C (the two devices share the bus).
- postRTOS: call I2CBB2::init() after the scheduler starts (it creates a FreeRTOS
static mutex).
- I2CBB2::init(): skip the STM32-HAL GPIO setup block on the N32L40X, where the
SCL2/SDA2 pins are already configured open-drain in Setup.cpp via the std-periph
driver and the STM32 HAL API does not exist; no change for other targets.
Verified on hardware: the debug menu reports ACC = QMA, the display sleeps when
the iron is set down and wakes when it is picked up.
Left/right-hand orientation mapping for display auto-rotate still needs on-device
tuning.
On the N32L40x, ADC channel N reads PA(N-1) (ADC_CH_2_PA1, ADC_CH_3_PA2, ...). The earlier pinmap applied a ch_n=PA_n convention and shifted every analog signal up by one pad: "Vin" actually sampled the NTC node, "tip" sampled the current shunt (a bogus temperature that drove full-power runaway) and the real Vin pin (PA1) was never configured. Corrected to Vin=ch2/PA1, NTC=ch3/PA2, tip=ch4/PA3 (injected group), current=ch5/PA4 (reserved). All five pads are now driven analog. Also harden the shared single ADC: - serialise the regular group with a static FreeRTOS mutex so the PID thread (Vin) and GUI thread (NTC) can no longer race the regular-rank reconfiguration; - two-pass "settle" read that discards the first conversion (the rank is re-pointed every call, so the sample-and-hold still carries the previous channel's charge); - phase the injected tip sample a settling holdoff after the heater blanks (TIM4 OC2REF -> TRGO) so the high-impedance thermocouple front-end has recovered; - drop the redundant injected-length re-write from the regular read path: it lives in a separate register set once in MX_ADC_Init, and re-asserting it on every read could abort an in-flight TIM4-triggered tip conversion; - remove the dead current-sense reader (tip presence now uses an idle-temperature test, not the floating current node).
Reverse-engineer the T245 (JBC C245) thermal front-end from the Alientek T90B stock firmware instead of carrying Sequre S60 placeholders: - uV -> degC curve (ThermoModel.cpp) taken from the factory op-amp-output lookup, expressed in the IronOS delta model and passed through InterpolateLookupTable; - OP_AMP_GAIN_STAGE 250 and VOLTAGE_DIV 370 from the factory front-end (536/467 were stale placeholders; the 536 gain halved the computed tip uV, so a hot tip read ~half temperature and the PID drove full power); - getHandleTemperature() returns a fixed 38 degC. The factory LUT is ABSOLUTE and anchored at a ~38 degC cold junction; our delta table stores those breakpoints minus 38 and getTipInC() adds the handle term back, so a fixed 38 reproduces the factory absolute reading exactly. The on-board NTC runs through an uncharacterised divider, so a live reading just injected tens of degrees of error (the original cause of the overheating). TODO: restore a live cold junction by reversing the factory NTC curve once a reference thermometer is available. Tip presence (isTipDisconnected) now uses an idle-temperature test: an open handle reads the op-amp open-circuit output (~360 degC, steady) while idle, whereas a fitted idle tip sits near ambient. The classic rail-high test is useless here (the T90 open thermocouple does not rail) and the floating current node is unreliable. configuration.h carries the full T90 model block: auto-rotate orientation default (QMA6100P fitted), MAX_TEMP for the T245, the calibration constants above, the flash-layout addresses, and the color-display / small-font / framebuffer-rotation feature flags consumed by the UI commits that follow.
Settings never survived a power cycle at the old 0x0801F000 page: the top of the 128K die (~0x0801C954..0x08020000) is owned by the Alientek HID bootloader and its metadata, so writes there read back in-session but are wiped on the next power-up. Relocate the settings and logo pages just ABOVE the IronOS app, inside the region the bootloader treats as application flash (proven to retain across power cycles - the factory firmware's own code lives there): settings page: 0x08018000..0x08018800 (2K) logo page: 0x08018800..0x08019000 (2K) The linker FLASH region is shortened to 0x08005000..0x08018000 (76K) so app code can never reach them. flash_save_buffer now acts on the N32 erase/program status: it bails and re-locks on a write-protected or failed page erase, and stops on the first failed word program, instead of programming into an un-erased or faulted page.
…n, small-font menus The T90 has a GC9-family 160x40 RGB565 color LCD where IronOS expects a 1bpp mono panel. The GC9 shim expands the mono framebuffer to color on the fly (no 12.8 KB full-frame buffer fits the 20 KB SRAM). - FRAMEBUFFER_ROTATION: the shim rotates the WHOLE framebuffer in software, so the UI must draw a single un-rotated layout. OLED::getRotation() is forced false under this flag while OLED::getRawRotation() exposes the true left/right-hand state for the shim and for rotation-aware reads; otherwise the per-screen getRotation() branches (mirrored icons, swapped positions) double-apply on top of the frame rotation and the home-screen icons scatter. draw_temperature_change keeps the +/- symbols on the side of the physical key per the true orientation. - Native RGB565 "hero" soldering screen (ColorUI/ColorTheme): a heat-mapped big temperature, voltage/wattage/setpoint columns and a live power bar streamed straight to the panel. Other modes/screens fall back to IronOS's mono pipeline, so the no-tip icon, warnings, menus and boot logo render normally. renderActiveScreen reads the active mode via a new getCurrentOperatingMode() C accessor and only takes over for soldering with a tip fitted. - The full-frame GC9 Transmit is serialised with a display mutex: OLED::setRotation() issues a full-frame Transmit from the higher-priority motion task and could preempt the GUI task mid-frame, interleaving the SPI stream and rebuilding the ColorUI render state under a reader. - MENU_DESCRIPTION_SMALL_FONT renders scrolling help text small: the wide/short panel turns the default large font into an unreadable smear for long descriptions.
Upstream replaces the whole settings screen with the description scrolling sideways in the large font after a few seconds of inactivity. On a wide/short panel (e.g. the T90 colour LCD) that hides the setting name and value and leaves a two-button user without context (which key changes, exits, or moves on), and the large font is barely readable there. Under MENU_DESCRIPTION_SMALL_FONT the description is shown in the small font in the otherwise-empty area under the setting value instead: the name, value and scrollbar stay visible and the buttons keep their normal actions (no help-mode takeover). Long descriptions page vertically so nothing scrolls sideways. The descriptions were previously only ever encoded for the large-font symbol table, so rendering them in the small font produced garbage (the indices point at different glyphs). make_translation.py now, when MENU_DESCRIPTION_SMALL_FONT is defined, word-wraps the descriptions and encodes them with the small-font table (and emits the small glyphs for their characters). `\n` always encodes to 0x01, so the firmware splits the wrapped lines back out at render time. Non-T90 builds are byte-identical (the flag gates every change).
…EADME Add a BSP README for the Alientek T90 (hardware/pin/ADC map, the reverse-engineering approach, and the key engineering decisions: corrected ADC channel map, factory-reversed tip calibration, fixed cold junction, idle-temperature tip detection, retained-flash settings, the colour display + software framebuffer rotation, and the compact menu help layout). Add a flashing guide (atktool over the HID update-mode bootloader, the B+USB+power update-mode entry, and recovery via the factory image). Refresh the main README and docs index: the port is now validated on real hardware (boots, reads tip temperature, heats, persists settings, auto-rotates, native colour screen) with calibration reversed from the stock firmware, and link the new flashing guide.
Copy each wrapped help line a whole glyph at a time into the render buffer so a 0xF1..0xFF two-byte glyph can never be split at the buffer boundary (the previous fixed-byte clamp could leave a dangling lead byte), and document that the build-time wrap width counts display glyphs. Harmless in practice for the Latin small font (lines stay well under the buffer) but robust for wider wraps / multi-byte glyphs. Addresses review feedback on the compact small-font descriptions.
… glyph indices Two fallouts of rendering menu descriptions in the small font: - CJK languages (JA_JP, ZH_*, ...) failed to build: a description's CJK characters were pulled into the small-font symbol set, but CJK has no small glyph (large font only), so make_translation raised a missing-glyph KeyError. small_font_renderable() now keeps any description containing a character without a small glyph in the large font, as before; only fully small-renderable descriptions are wrapped and small-encoded. - The native colour soldering screen showed the wrong unit letters (e.g. "9.2M"/"9.9k" instead of "9.2V"/"9.9W"): ColorUI hardcoded the small-font glyph indices for V / W / '.', which shifted once the descriptions were added to the small font. They are now derived at runtime from the IronOS small-symbol constants (encoded byte == font-table position + 2), so they track any font reshuffle.
`make check-style` (clang-format) covers the new T90 BSP and its bundled Nations std-periph driver, the same as the other boards' vendor code. Apply clang-format so the check passes. No functional change.
The Alientek bootloader leaves SysTick + its interrupt, leftover NVIC IRQs and possibly a DMA channel running on a normal power-up (but not on a direct post-flash boot). settingsConstants[] lives in .data because one of its bounds is runtime-initialised (TemperatureInF.max via HasFahrenheit), so that stray activity zeroed QCIdealVoltage's min/max right after the Reset_Handler .data copy. The resulting [0,0] bounds with an intact default of 90 made sanitise reset the value on every boot -> loadSettings() returned dirty -> the "Certain settings changed" warning on every power-on. - SystemInit: stop SysTick, disable+clear all NVIC IRQs and DMA channels, then re-initialise .data (copy from flash) and .bss (re-zero) before the C++ constructors run, so the corruption cannot survive into the app. - Relocate the settings page to 0x0801B000, verified on hardware to retain all 120 bytes across power-off (sentinel write + power-cycle read-back). - flash_save_buffer: reset the iCache after programming so the saveSettings verify and the next-boot load read the freshly written cells, not stale lines.
The 2K logo flash page cannot hold a 160x40 colour frame, so the splash is compiled in (BootLogoData.cpp, RGB565). showBootLogo streams it through the GC9 colour path honouring the LOGOTime setting (skip / N seconds / until a button), and the GUI state machine shows it before the startup warnings.
…age move Update the BSP README's settings-persistence note to the relocated 0x0801B000 page and add the bootloader-leftover (SysTick/NVIC/DMA) .data/.bss corruption finding and the SystemInit fix.
There was a problem hiding this comment.
Please do not include files that are not required for the project to keep bloat down 🙏🏼
| #include <stdint.h> | ||
|
|
||
| // clang-format off | ||
| extern const uint16_t t90BootLogoRGB565[6400] = { |
There was a problem hiding this comment.
Rather than one time generating this, its preffered if this is generated in the Makefile so we only commit the source image data; and can derive this at compile time
| // | ||
| // Re-implements the BSP.h + BSP_Power.h contract against the N32 std-periph driver, following | ||
| // the Sequre S60 algorithms (the only other std-periph / Cortex-M4 IronOS target) but with no | ||
| // ST HAL: every HAL_* call is replaced with the equivalent Nations driver call or register poke. |
There was a problem hiding this comment.
Please do NOT do this sorts of comparative comments, I know the LLM likes to make them but they do not really help. Its best to just document what is here rather than what isn't
| #define ADC_MAX_READING (4096 * 8) // Maximum reading of the adc | ||
| #define ADC_VDD_MV 3300 // ADC max reading millivolts | ||
|
|
||
| // Deriving the Voltage div: |
|
Hia, Thank you so much for the PR. A few questions / small ask.
You ignored the PR template and instead raised the PR with a very LLM-like message, as well as lots of the code comments read like an LLM. If an LLM is used please disclose this.
Is it brand new, or is it from another project (So I know for the software licence)?
For the flashing doc, thank you for writing one 🙇🏼 -> Where does I dont mind if it does or doesn't work on Windows, just want to make it very clear so we dont get issues raised
Please do not do colourisation or theme's in the BSP folder. All drawing logic needs to live in the As you are adding a colour screen, create a new folder This way you can link to your display driver instead of the oled file,and call functions directly to draw how you want.
Traditionally, IronOS does not burn in a boot logo by default and that is up to the user to flash later on. It would be preferable to keep matching this setup. This way the boot logo can be submitted to IronOS-meta repo, and users can flash their own. Some thought will need to be given to how to encode and store boot logos vs flash size for a colour display.
For the first two code changes; these should be raised in their own PR's for review.
This PR is quite large, I've some a skim review of things that show up as soon as I open the file, but its hard to read each line when its this big. Would you be willing to break this up into PR's that implement logical blocks / changes incrementally so that its not a 60k line delta to sift through.
When updating the main readme, you dont need to add the LLM-speak long explanation, just mark it as experimental. I hope that all makes sense 🙇🏼 |
|
Also the atktool should probably do firmware flashing only, and be separated from this repo. This aligns better to what other platforms do (e.g. Pinecil). |
Summary
Adds a new BSP target for the Alientek T90 soldering iron — a Nations N32L40x (Cortex-M4F) iron
with a JBC T245 / C245 cartridge, a GC9-family 160x40 RGB565 colour SPI LCD, a CH224Q USB-PD
sink and a QST QMA6100P accelerometer. Build with
make model=T90 firmware-EN.The T90 has no exposed SWD header and ships a closed HID "update-mode" bootloader, so the port was
brought up almost entirely by reverse-engineering the stock firmware. A small
atktoolutility(
Tools/atktool/) packs the.atkimage and flashes over the HID bootloader; see the flashing guide.Status — validated on real hardware
renders through IronOS's normal mono pipeline.
Calibration caveat: the tip / Vin scale is reverse-engineered from the stock firmware and tracks the
factory closely, but a final fit against a reference thermometer is still pending — treat absolute
readings as approximate (room temperature currently reads at the fixed ~38 °C cold-junction anchor).
Reverse-engineering notes
UsageFault/INVSTATEwas an inherited FPU exceptionframe: the hard-float factory bootloader branches into the app with
CONTROL.FPCAstill set, whilethis is a soft-float build on the FreeRTOS ARM_CM3 port that never saves FP context. Fixed in
preRTOS.cpp(clear FPCA, disable FPCCR auto/lazy stacking, deny CP10/11 before the scheduler).tip=ch4/PA3 (injected group), current=ch5/PA4.
lut_interp(opamp_input)with no live cold-junction term at soldering temperatures; reproduced as auV→°C LUT in the IronOS delta model plus a fixed 38 °C cold junction (the factory's baked-in anchor).
relocated into the retained application-flash region (linker shortened to 76K).
.atkformat and HID update protocol were recovered by byte-exact round-tripagainst factory images; the commit opcode is
SetBoot(0x15).More detail in
source/Core/BSP/Alientek-T90/README.md.Shared-core changes (for discussion)
Most of the change is confined to the new BSP, but a few small, flag-gated hooks touch shared code —
non-T90 builds are byte-identical:
FRAMEBUFFER_ROTATION(OLED.hpp) — when the BSP rotates the whole framebuffer in software the UIdraws a single un-rotated layout:
getRotation()is forced false andgetRawRotation()exposes thetrue left/right-hand state (otherwise the per-screen rotation branches double-apply).
MENU_DESCRIPTION_SMALL_FONT— word-wraps menu descriptions and renders them in the small font underthe setting value instead of the full-screen scrolling takeover (build-side support in
make_translation.py).getCurrentOperatingMode()(GUIThread.cpp) — a C accessor so the BSP colour renderer can read theactive operating mode without depending on the deep UI enum header.
Testing
make model=T90 firmware-ENbuilds clean (FLASH ~49 %, RAM ~70 % of the N32L403 budget); the resultingimage was flashed and exercised on a physical T90 — boot, heating, settings persistence, auto-rotation,
the colour soldering screen and the menus.
#2195