Repository navigation
feat(validation): cover every hal-st peripheral driver on NUCLEO-WB55RG/WBA55CG and fix the drivers it exercises - #96
Conversation
The hardware-in-the-loop validation app builds on EMIL's services.hil and services.hil.commands, added after the previously pinned commit. The range only adds those libraries, Terminal command hooks, DutyCycle::FromRatio and an ISB after disabling an NVIC interrupt; no interface hal-st implements changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
The destructor always called DisableClockUart(uartIndex), so destroying LPUART1 switched off USART1's clock instead (and left LPUART1 clocked). On the Nucleo-WB55/WBA55 USART1 is the ST-LINK virtual COM port. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
…UCLEO-WBA55CG Mirror hal-ti's validation app: a firmware that exposes the hal-st drivers through EMIL's services/hil terminal so a host suite and an Analog Discovery 3 can exercise them on real hardware. The command set is in validation/PROTOCOL.md. - target hal_st.validation_firmware, built for stm32wb55 and stm32wba55 with HALST_BUILD_EXAMPLES (on in every preset) - terminal on USART1 (ST-LINK VCP) with UartStmDuplexDma at 921600 8N1 - board profiles with alias tables, reserved pins, DMA request lines and clocks; STM32 pin factory over the generated pinout tables, guarding unbonded pins and shared EXTI lines - factories for GPIO, UART (UartStm, UartStmDma, UartStmDuplexDma, SynchronousUartStm, LPUART), SPI (SpiMasterStm, SpiMasterStmDma, SynchronousSpiMasterStm, GPIO chip select), ADC (one-shot and timer-triggered AdcDmaMultiChannelStm), PWM (PwmStm, SynchronousPwmStm), encoder (timer and LPTIM, plus qei.index) and the WWDG - comparator, CAN, EEPROM and Ethernet commands answer ERR unsupported - devcontainer maps host.docker.internal for the bridge setup, and a stm32wba55cg debug configuration is added Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
…r review - SPI: close waits up to 10 ms for an in-flight transfer before destroying the driver; SpiMasterStm schedules its completion with no stale-event guard, and a WB55 DMA channel left running ignores the next open - UART: USART1 answers busy only after the argument checks, so argument errors keep coming first - ADC: constant-time trigger timing instead of a prescaler search that blocked the event loop; interrupts stay masked through a triggered stop; rate without timer is a usage error before range checks - encoder: lp=1 rejects cap/offset/invb before parsing their values Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
…uite Port hal-ti's validation host to hal-st: a Python package that drives the validation firmware terminal and a Digilent Analog Discovery 3 through ad3-waveforms-bench (pinned to the same commit as hal-ti). - board files for NUCLEO-WB55RG and NUCLEO-WBA55CG: alias tables, clocks, AD3 wiring sets (bundle1, plus bundle2 for the WB55 LPTIM encoder) and every test parameter matrix - typed firmware API, STM32 expectation math (PWM quantisation and range, SPI prescaler, UART baud-rate register limits, WWDG period, ADC codes), pairwise/full parameter depth, and a fake firmware for --fake runs - HIL tests for system, GPIO, PWM, UART, SPI, ADC, encoder, watchdog and the unsupported groups; unit tests for the harness itself - known_gaps in the board files: driver bugs found while writing the firmware skip (when they abort or hang the board) or xfail the tests they affect; --run-known-gaps runs them anyway - validation/README.md: setup, wiring tables, running, bridge mode and the known driver gaps; AGENTS.md and CLAUDE.md point to the new app Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
- ConfigureTimeBase initialised the timer with Period = 0, which fails IS_TIM_PERIOD under USE_FULL_ASSERT and aborted every construction. It now initialises with Period = 1 and loads the real period in Start. - The counter direction is written only after CMS and SMS are cleared, as CR1.DIR ignores writes while centre-aligned or in encoder mode. - Centre-aligned: ARR is ticks / 2 (period 2 x ARR) instead of ticks / 2 - 1, and duty is scaled to ARR; edge-aligned down-counting compensates the extra active tick. - TRGO (MMS/MMS2), BDTR and the BKIN source are rewritten on every construction, so dead time, break, idle and trigger settings of an earlier user no longer survive. BKINP stays non-inverted, so an active-low break is no longer inverted twice. - An update event on the first Start loads the preloaded ARR/CCR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
On the WBA ADC4 (CHSELR sequencer, common sampling times): - AdcDmaMultiChannelStm passed ADC_SAMPLETIME_x where HAL_ADC_ConfigChannel takes ADC_SAMPLINGTIME_COMMON_1/2 (assert, abort). Per-channel sampling times are now mapped onto the two common groups; a sequence can use at most two distinct sampling times, and MaxChannels is 8 on ADC4. - Scan mode and NbrOfConversion are set for the sequence, and the sequencer length is written with the LL encoding instead of the rank count. - AdcStm::SelectSingleConversion restores single-channel scan and SMP1 for the single-pin, temperature and timer-triggered DMA users of the same AdcStm; the temperature path uses the common sampling time. - The ADC triggers from TIM1 TRGO2 there, so AdcTimerTriggeredBase now configures TRGO2 = update for TIM1 (new TimerBaseStm::Trigger:: TriggerOutput2). - The trigger table lacked a TIM10 slot, which shifted the triggers of TIM11 and later timers by one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
CSTART was only set when a byte was written to TXDR, so a receive-only first transfer on H5/WBA never started (SpiMasterStm timed out, SynchronousSpiMasterStm spun forever). CSTART is now set once per transfer after the interrupts are enabled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
- Stream-based DMA (F4/F7) sets TCIF once clearing EN takes effect, which reported an aborted transfer as complete. StopTransfer now masks TC/HT, waits for EN to clear, clears the flags and restores the enables, as HAL_DMA_Abort does. - GPDMA: SUSPF is cleared after the channel reset, so a stale flag no longer ends the next suspend wait before the channel is suspended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
- SWAP is always written, so a swapped instance is no longer swapped for later users (UartStm, SynchronousUartStm). - Destructors clear TXEIE/TCIE/RXNEIE (and RTOEN for the duplex DMA variant), and ~UartStmDma stops its transmit DMA, so a close during a stalled send no longer leaves an interrupt storm for the next user. - CR1 enable bits are set and cleared atomically: a TXEIE clear from the interrupt during ReceiveData's read-modify-write was undone. - A receive overrun is cleared instead of asserting (UartStm) or being left set (SynchronousUartStm), which looped on the ORE interrupt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
EnableInterrupt wrote the compacted hal::Port value into EXTICR, which selects the wrong port for ports after a gap (port H on WB/WBA). It now writes GPIO_GET_INDEX of the port, with the field width taken from the CMSIS EXTI/SYSCFG EXTICR positions (8 bits on G0, H5 and WBA). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
HAL_LPTIM_Init ORs the clock and trigger filters into CFGR without clearing them, so a previous user's filter persisted. The LPTIM is now reset through RCC before it is initialised. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
- Remove the fixed known gaps from both board files; only the SynchronousUartStm CTS-held send (WBA55) remains. - PWM: range check and host expectations follow the fixed ARR (ticks - 1 edge, ticks / 2 centre); the centre-period xfail is gone. - GPIO: port H interrupts are no longer refused. - README and PROTOCOL list the remaining gaps and the PWM range. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
…der positions PB3 (TIM2 CH2, SWO) is on no header of a stock NUCLEO-WB55RG: CN10-30 carries PB13 unless SB12/SB13 are reworked (UM2435 Rev 2, Fig. 8, Table 11). TIM2 CH2 and the default encoder B move to PA1 (A2, CN7-32 via SB14), the only other TIM2 CH2 pin. Every WB55 wiring row now names its morpho pin and Arduino socket from UM2435 Rev 2 (MB1355C), plus the ground pins, the solder bridges on the path, the D10 bridge note (PA4 by default, confirmed) and the terminal pins to keep free. The loopback note says what the SPI tests do with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
AGENTS.md asks for HAL_FOO_DeInit and a clock disable in every driver destructor. UartStm gated the clock without HAL_UART_DeInit, and SynchronousUartStm/SynchronousUartStmSendOnly did neither. The explicit interrupt-enable clear stays: the F4 HAL_UART_DeInit leaves CR1 as is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
The sampling time (SMPRx, or SMPSEL on ADC4) belongs to the channel, not to the rank, so configuring a repeated channel with another sampling time silently changed the earlier rank too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
Formatted with markdown-table-formatter, as MegaLinter suggests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
STM32WB55RGVx.xml names the QUADSPI IP "_QUADSPI" (InstanceName "QUADSPI"). GeneratePeripheralTableStructure.xsl matched items on @name = $ip-name only, so PeripheralTableWbxx.xml's QUADSPI item was dropped and the WB55 table said #undef HAS_PERIPHERAL_QUADSPI, leaving QuadSpiStm, QuadSpiStmDma and SingleSpeedQuadSpiStmDma compiled empty. Also accept @name = concat('_', $ip-name), and derive the instance position from $ip-name instead of @name. Both are identical for every existing match; _QUADSPI is the only underscore IP in mcu/*.xml. Regenerating all 13 presets changes only stm32wb55 PeripheralTable.hpp/.cpp (HAS_PERIPHERAL_QUADSPI, peripheralQuadSpi*, Enable/DisableClockQuadSpi); PinoutTableDefault is unchanged everywhere. CRLF line endings kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
STM32WBA52CGUx.xml lists LPTIM1/LPTIM2 and the WBA pinout table already carries the LPTIM pins, but PeripheralTableWbaxx.xml had no LpTimer entry, so LpTimerStm, LpTimerPwmStm and SynchronousQuadratureEncoderLpTimStm compiled empty on WBA. Add the same LpTimer entry as PeripheralTableWbxx.xml. Regenerating changes only stm32wba52/55/65 PeripheralTable.hpp/.cpp (HAS_PERIPHERAL_LPTIMER, peripheralLpTimer*, Enable/DisableClockLpTimer). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
… STM32WB/WBA All behaviour changes are guarded with STM32WB || STM32WBA inside the I2C v2 paths; F2/F4 (v1) and F0/F3/F7/G0/G4/H5 keep today's code. a. NACK: clear NACKF (I2C_FLAG_AF), flush TXDR, end the session (the IP sends STOP) and return before the TXIS/TCR/TC checks, so the next transfer no longer sees a stale NACK. b. Address NACK completes the armed onSent/onReceived after DeviceNotFound(), so the next transfer no longer trips the "callback still armed" assert. c. Data NACK reports the acknowledged bytes: a byte still in TXDR is not counted, and a NACK of the last byte reports onSent instead of calling an empty onReceived. d. TCR with both buffers empty completes onReceived for a receive with continueSession instead of an empty onSent. e. BERR/ARLO: disable the interrupts, flush TXDR, end the session, reset the IP (PE low for >= 3 APB cycles), call the hook, then complete an active transfer with Result::busError. f. Default Config::timing is 0x70b03d3d on WB/WBA: Standard mode within spec for every kernel clock up to 100 MHz (0x00304d4d gives a 62 ns SCLDEL at 64 MHz, below Fast mode's 100 ns tSU;DAT plus rise time). g. i2cHandle is value-initialised; the destructor calls HAL_I2C_DeInit before gating the clock. h. SendData/ReceiveData reset both counters; every scheduled completion calls a callback only while it is still armed. i. Own address 1 is disabled after HAL_I2C_Init, so an idle master does not ACK address 0x00 and hold SCL. Behaviour change: WB/WBA users of the default Config drop from ~340 kHz to 63.8 kHz at a 64 MHz kernel clock (99.2 kHz at 100 MHz); pass Config::timing for Fast mode. The two hpp includes are swapped into clang-format order (no code change). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
…nsfers SPE was only set under STM32H5 and HAL_SPI_Init does not set it, so on STM32WB/WBA SpiSlaveStmDma never moved a byte. DisableSpi() also set SPE instead of clearing it, so the slave kept receiving between transfers and after its destructor. WB and WBA now take the H5 sequence: clear SPE before arming the DMA streams, set it once both are armed, clear it when the transfer is done (RX DMA completion means the last frame was clocked, so no BSY wait). DisableSpi() clears SPE with LL_SPI_Disable on WB/WBA. On WB (SPI v2) the RX FIFO keeps its content while SPE=0, so frames a master clocks past the armed length would lead the next transfer: drain the RX FIFO and clear OVR before arming. H5 keeps its DisableSpi(); F4/F7/G4/H5 compile to identical code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
SetDataSize asserted dataSizeInBits >= 4 || dataSizeInBits <= 32 on SPI v3 (CFG1.DSIZE), which holds for every value. Use && like the SPI v2 branch. Debug builds only; release code is unchanged on every family. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
FlashInternalStmBase::EraseSectors passed the region-relative sector index to HAL_FLASHEx_Erase as the absolute page number, and the sector count as the page count. Unless the region started at FLASH_BASE and every sector was one page, it erased pages outside the region (the running image at the start of flash) and left the region intact. On STM32WB/WBA the page now is (region offset from FLASH_BASE + AddressOfSector(beginIndex)) / FLASH_PAGE_SIZE and the count is the byte size of [beginIndex, endIndex) / FLASH_PAGE_SIZE, so multi-page sectors of FlashInternalStm are erased completely too. This also fixes FlashCoordinatedWithWirelessStack, which erases through this driver. G0/G4 keep the old arithmetic (not validated on hardware); F4/F7/G0/G4/H5 compile to identical code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
…B/WBA SynchronousFlashInternalStmBase::EraseSectors had the same defect as FlashInternalStmBase: the region-relative sector range was erased as an absolute page range. On STM32WB/WBA without FLASH_DBANK_SUPPORT the erase now starts at the absolute page of the first sector and covers the pages of [beginIndex, endIndex), as in the asynchronous driver. The dual-bank path (G4, WBA65) and the other families are unchanged and compile to identical code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
EccErrorHandler passed FLASH_ECCR.ADDR_ECC to the region-relative SectorOfAddress and erased the resulting sector index as an absolute page. On STM32WB55 ADDR_ECC is a double-word address (stm32wb55xx.h:4150-4152: "double-word address ECC fail", 17 bits x 8 bytes = 1 MB), so the page is (ADDR_ECC * 8) / FLASH_PAGE_SIZE. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
…as already on Hsi48Enabler enabled HSI48 and switched it off again on every read when semaphore 5 is not held. On STM32WB the default clock keeps HSI48 on as the RNG kernel clock (CLK48 = HSI48), so one synchronized read left SynchronousRandomDataGeneratorStm and RandomDataGeneratorStm without a clock. Only enable HSI48, and only switch it off at the end of the read, when it was not running before. The locked branch is unchanged. Until now IsLockedByCurrentCore() always returned true, which hid this; the next commit fixes that query. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
IsLockedByCurrentCore() compared RLR == HSEM_R_LOCK | COREID, which parses as (RLR == HSEM_R_LOCK) | COREID and is always true, so WaitLock() never waited for a semaphore held by the other core. Reading RLR is also a 1-step lock attempt, so using it as a query took the semaphore and never released it. WaitLock() now loops on HAL_HSEM_FastTake() (1-step lock, process 0, which Release() frees). IsLockedByCurrentCore() reads R through LL_HSEM_IsSemaphoreLocked()/LL_HSEM_GetCoreId(), which has no side effect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
GPDMA (STM32WBA): SetPeripheralDataSize/SetMemoryDataSize masked the size with 0x03 (4 bytes became 0), shifted size >> 1 (not a log2 for 4) and ORed the field into CTR1 without clearing it, so a 16-bit width stuck after a later 8-bit setting and 32 bit was impossible. They now write log2(size) into SDW_LOG2/DDW_LOG2 with MODIFY_REG, with the same direction-based side selection. DMA v1 (STM32WB, channel based): 4 bytes were encoded as the reserved PSIZE/MSIZE value 0b11; DataSize() then returned 8 and SetTransferSize halved CNDTR. 2 bytes now encode as PSIZE_0/MSIZE_0 and 4 bytes as PSIZE_1/MSIZE_1. Other families keep their code: H5 (GPDMA), F4/F7 (stream based) and G4 (channel based) compile to identical objects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
…p reserved CR bits clear on STM32WB An indirect write completed on the DMA transfer-complete, i.e. when the last byte entered the QUADSPI FIFO, not when it left the pins. onDone (and SingleSpeedQuadSpiStmDma's ChipSelectConfigurator::EndSession) ran while the FIFO still drained, so a configurator-driven chip select could end the session early and the next command's CCR write could meet BUSY=1. On STM32WB the DMA completion of a write (CCR.FMODE = 0) now clears DMAEN and enables TCIE without clearing TCF; the existing TCF interrupt handler completes the write, as ST's HAL does. Reads keep their path. The constructor wrote (32 << 8) into CR. FTHRES is 4 bits on STM32WB (CR[11:8]), so the term only set reserved bit 13 and left the threshold at 1 byte. It is dropped on STM32WB; the threshold is unchanged. Other families keep today's code (F746/F767 compile to identical code). Proved on NUCLEO-WB55RG by test_qspi.py::test_write_completes_after_last_byte[dma,spi] (FIFO level 0 in the completion callback at prescaler 255 with >= 32 bytes) and test_qspi.py::test_back_to_back_writes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
…r the new groups Firmware infrastructure shared by the new HIL command groups: - Owners.hpp: owner ids 16..30 for the new groups (17 stays the WWDG warning pin), shared by the pin pool, TimerAllocation and ResourceAllocation. - ResourceAllocation: one holder per I2C/SPI/ADC/LPTIM instance, per shared DMA channel and per HSEM semaphore across groups (i2c, i2cs and eeprom; spi and spis; adc and ain; qei, lptim and lptpwm; flash coord and hsem.lock). A second claim answers ERR busy. - TimerAllocation::Owner() so adc.open trgo= can check that pwm holds the trigger timer. - Payload: firmware-generated payloads (inc, const, xorshift32 prbs), CRC-32 and out=hex|crc, so commands stay within 255 characters. - ChannelPins: contiguous GpioPinStm/DummyPinStm storage, as TimerPwmWithChannels indexes one MemoryRange<GpioPinStm> by channel. - Stopwatch: microseconds from the DWT cycle counter, usable in ISRs. - HsemMaster (WB55): one HSEM master that is never destroyed, since its destructor gates the HSEM clock. - PeripheralClocks: I2C/SPI/LPTIM existence, SPI limited instance, I2C and LPTIM kernel clocks. - Board profiles: DMA allocation, scaffold timer, flash scratch region, low-power pins, I2C and QSPI pins, and the new pin aliases. - WBA55: PA3, PB10, PB11 and PB13 are SMPS/VDD11 pads on the UFQFPN48 and leave the bonded mask; the debug LED moves from PB8 (red LD3) to PA9 (green LD2), freeing the only SPI3 MOSI and TIM16 CH1N pin. LD2 is not connected on a stock board (SB28 open), so the heartbeat stays dark unless SB28 is closed. Board files describe optional wiring (--with) as mappings: jumpered pins per tag, loaded pins, pull-ups and mutually exclusive options. --with tags are checked against the selected wiring sets at collection (a usage error otherwise), and need skips a test that resolves a pin an enabled option loads unless the test is marked uses_option (requires_option implies it); pins an option jumpers to a channel resolve only for those tests. --board-extra (or HAL_ST_BOARD_EXTRA) deep-merges YAML over the board file, with key! to replace a value. conflicts_option skips a test while an option is enabled; the AD3 pulls and the I2C/SPI protocol engines are reset after each AD3 test. tests/hil/test_wiring.py checks the wiring with GPIO commands only: continuity of each enabled option's jumpers, external pull-ups, the jumpers of offered options that are not enabled, and pins nothing else may drive (tests.wiring.undriven). Command group wrappers in groups/*.py (GROUPS) are attached to Firmware, and fake groups in fakes/*.py (FakeGroup) answer <prefix>.<verb> in the fake firmware, sharing pin claims, timer owners and a model of ResourceAllocation. patterns.py is the host twin of the firmware payload generator and CRC. The fake firmware follows the firmware changes: E.4 aliases, the WBA55 bonding mask (PA3, PA4, PB10, PB11, PB13 unbonded) and debug LED PA9, the pin functions of the generated tables (I2C, SPI NSS, LPTIM channels and inputs, QUADSPI), per-MCU unsupported names (eeprom.* served; WBA55 without hsem, qspi, flash.stack, clock.mco/hsi48; WB55 without lptpwm), spi bits/lsb/nss, pwm alignment modes, preload, brkfilter and trgo, adc trgo, uart sendonly and the WBA55 LPTIM encoder with cap=rise|fall. The board files' tests.unsupported.commands follow the per-MCU names. The board files take the E.4 aliases, the WBA55 debug LED PA9, the unbonded PA3/PB10/PB11/PB13 and the GPIO limit list without led1 here, so they match the board profiles of this commit (the rest of the docs-bench wiring text follows in its own commit). test_system.py treats the debug LED like the terminal pins when it opens every alias: on the WBA55 led1 is the debug LED now. With LPTIM in the WBA55 table (gen) the encoder opens with lp=1 there, so qei.open 1 lp=1 leaves the WBA55 unsupported instances, and test_qei.py::test_lptim_errors expects cap=a (not cap=ab, which the LPTIM encoder takes) to be unsupported. validation/README.md documents --board-extra, --with, the load gate, the option keys, test_wiring.py and the new host and firmware modules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
The --with options of the board files become mappings: the pins each option ties together with a jumper (the key is the end the wiring self-check drives), the pins it loads and pulls up, and the options it excludes. The NUCLEO-WB55RG bundle1 gains i2c (I2C1 PB8/PB9 to I2C3 PC0/PC1 with 4.7 kOhm pull-ups and a 24Cxx EEPROM on a breadboard fed from CN6-4/CN6-6) and spiloop (SPI1 to SPI2, PB12 through a breadboard row); the loopback jumper declares PA6/PA7 and excludes spiloop. The NUCLEO-WBA55CG wiring names a source for every header position: Zephyr's Arduino labels, the STM32CubeWBA example readmes for the morpho and Arduino positions, modm-data's transcription of UM3301 Table 8 and the STM32-Sidewalk-SDK README for 3V3/GND (the comment named UM3390, which is not this board's manual). AD3 leads move to the morpho pins so the Arduino sockets stay free for jumpers. A new bundle2 moves DIO8, DIO9, DIO11 and DIO14 to PA7, PA6, PB8 and PA0 for SPI3, TIM2 CH3/CH4, TIM16 CH1N and the LPTIM1 encoder, and offers i2c (I2C1 PB2/PB1 to I2C3 PA6/PA7, scopes in the bus rows for the rise time) and spiloop (SPI1 to SPI3). The board notes list the solder bridges that can tie ST-LINK lines to PA0, PB9, PA10, PB5 and PB15 (tests.wiring.undriven), the LD3 load on PB8 and the green LD2 on PA9, the debug LED, dark unless SB28 is closed; the E.4 aliases, PA3/PB10/PB11/PB13 as unbonded pins and the GPIO limit list follow the firmware. validation/README.md gets the physical rules, the parts list, the bench bring-up (pre-power check, then tests/hil/test_wiring.py), per-option wiring tables with sources, which tests skip and how, and the known gaps that need a decision. PROTOCOL.md gets the new aliases, instance numbers, the WBA55 debug LED, the timer and peripheral sharing rules, the instance limits, the line-length rule with the generated payload convention and the per-MCU unsupported command names; EEPROM is no longer unsupported. test_config.py follows the WBA55 bundle2 and checks the options each set offers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
…external EEPROM Add the i2c group (hal::I2cStm through I2cStmForHil, whose DeviceNotFound, BusError and ArbitrationLost hooks print EVT i2c), the i2cs group (an LL I2C target on the other instance: register file or sink, per-byte ACK control with slave byte control, clock stretching and a misplaced STOP on request) and eeprom.attach/eeprom.detach, which serve EMIL's eeprom group through I2cEepromStm, a hal::Eeprom over hal::I2cMaster with page writes, ACK polling and random reads. I2cTiming computes TIMINGR from the kernel clock and the bus frequency, never faster than nominal; the host twin expected_timing reproduces it bit for bit (PROTOCOL.md table). The i2c, i2cs and eeprom groups exclude each other per instance through ResourceAllocation; eeprom.write/read/erase leave the unsupported list. test_i2c.py runs the standalone cases with the MCU pull-ups (default and computed timing, address NACK and recovery, zero-length probe, arbitration loss held and triggered) and, with --with i2c, the loop against the target (Fast mode, rise time, data NACK, NBYTES/RELOAD lengths, repeated START, continued sessions, stretching, bus error, general call, held bus, both directions). test_eeprom.py covers EMIL's eeprom group on a 24Cxx and the 1-byte word address against the target. The fake firmware models the three groups on one bus with the target and a 24LC256. test_unsupported.py and its README bullet no longer name EEPROM. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
…ond SPI instance Add the spis command group (PROTOCOL.md "SPI slave") and extend spi.open: - spis.open builds SpiSlaveStmDma on the board's slave DMA channels (WB55 DMA2 1/2, WBA55 GPDMA1 8/7, the latter shared with the ADC and dma.wave) and holds the SPI instance in ResourceAllocation, so spi and spis never share an instance. spis.arm hands one transfer to the driver (hex or a generated payload, full duplex, send or receive only); spis.result answers when it is done, or done=0 after its own wait timer (HilPendingOperation would answer ERR timeout); spis.cancel and spis.close stop the DMA with CancelTransmission before the driver goes, one event-loop turn later. - spi.open takes bits=4..16 (dma=1: a SpiDataSizeConfiguratorStm on SpiMasterStmDma for the whole open, removed before the driver; the WBA55 SPI3 limited instance takes 8 and 16 only), lsb=1 (Config::msbFirst) and nss= (the drivers' slaveSelect pin, exclusive with cs), and holds the SPI instance in ResourceAllocation. Host: spiwords.py decodes SPI words of any width (the bench decoder masks to 8 bits) and models the frames of SpiMasterStmDma: one frame per byte up to 8 bits, one per little-endian byte pair above. groups/spis.py and fakes/spis.py (with the SPI loop of the fake spi.xfer) follow the firmware. test_spi_slave.py runs the slave against the AD3 master (directions, lengths up to 1024, clock rates, unarmed clocks, over- and under-length transfers, result waiting and busy, cancel, re-open) and, with --with spiloop, against the board's own master on the other instance. test_spi_ext.py decodes lsb and every frame size per instance, the SPI3 size limit, an 8-bit transfer after a 16-bit one, and the hardware NSS, a known gap (the masters initialise SPI_NSS_SOFT). The board files add SPI2 (WB55, spiloop) and SPI3 (WBA55, bundle2) to the SPI master tests. test_parameters_reference_wired_pins accepts SPI instances wired in any set, through the jumpers of their option (WB55 SPI2 with spiloop, WBA55 SPI3 in bundle2). test_known_gaps.py checks the gap-free case with known_gaps!: [] now that the WB55 board file has a gap. The README's known-gap list cites SynchronousSpiMasterStm.cpp:23 for SPI_NSS_SOFT and describes the 16-bit frame packing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
…nd the WBA55 LPTIM encoder Add the tim, tpwm, lptim and lptpwm command groups (PROTOCOL.md "Timer", "Timer PWM", "Low-power timer", "LPTIM PWM"): - tim builds FreeRunningTimerStm (irq=none, mode=up|down) or TimerWithInterruptStm (immediate or dispatched), toggles a marker pin per update and reports the counter register and the update count. - tpwm builds TimerPwmWithChannels<1..4> over ChannelPins, with DummyPinStm for an unused `-` channel; duty, pulse and start/stop of one channel or all. - lptim builds FreeRunningLowPowerTimerStm or LowPowerTimerWithInterruptStm with every prescaler and, on the WBA LPTIM, the repetition counter. - lptpwm builds LpTimerPwmWithChannels<1|2> (STM32WBA55; LpTimerPwmStm is not built for STM32WB, where the names answer ERR unsupported). Each group is a factory (argument checks in the protocol order, driver state) plus a HilSingleInstanceGroup. tim and tpwm hold their timer in TimerAllocation; lptim, lptpwm and the LPTIM encoder hold their LPTIM in ResourceAllocation. Groups whose driver schedules dispatched callbacks destroy it one event-loop turn after stopping it, and the PWM groups track which channels run because HAL_TIM_PWM_Start and HAL_LPTIM_PWM_Start refuse a running channel. The encoder takes lp=1 on both LPTIMs of the WBA55 once the LPTIM is in its peripheral table, and cap=rise|fall select the x2 LPTIM decode modes; cap=a|b stay unsupported with lp=1. Host: groups/timers.py (fw.tim, fw.tpwm, fw.lptim, fw.lptpwm and the expected update rates and duties), fakes/timers.py, test_timer.py, test_timer_pwm.py, test_lptim.py, test_lptim_pwm.py, the LPTIM capture modes and the WBA55 LPTIM encoders in test_qei.py, and the board YAML for them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
…and the circular DMA channel pwm.open takes every PwmStm alignment (mode=edge|edgedown|center|centerup|centerboth), preload=0|1, brkfilter=0-15 (needs brk) and trgo=<MMS source>; modes other than edge and any trgo need a master timer with a counter mode select, so TIM16/TIM17 answer ERR unsupported instead of reaching the driver's assert or reserved CR2 bits. adc.open trgo=<timer> runs the DMA circular sequence on the trigger output of a timer the pwm group drives, without taking the timer: ERR busy when another group holds it, ERR unsupported when nobody does or the ADC cannot trigger from its TRGO (STM32WBA55 ADC4 reaches TIM1 through TRGO2 only). The adc group now holds the ADC and its DMA channel in ResourceAllocation. New stateless groups: - ain.read converts a pin (AnalogToDigitalPinImplStm) or the temperature sensor (AnalogToDigitalInternalTemperatureStm, __LL_ADC_CALC_TEMPERATURE at 3300 mV); - ain.burst builds one AdcTriggeredByTimerWithDma over a 256-sample buffer and calls Measure(n) once or twice, paced by TIM2, reporting the samples or their statistics and the duration of each measurement; - dma.wave drives a pin from a CircularTransmitDmaChannel writing 32-bit BSRR words on TIM2 update requests (DMA2 channel 4 on STM32WB55, GPDMA1 channel 8 on STM32WBA55), stopped before it is destroyed. Each claims the ADC, TIM2, DMA channels and pins it uses for the length of the command. Host: groups/analog.py (fw.ain, fw.dma and the TIM2 pacing expectations), fakes/analog.py with deferred replies, the fake's pwm/adc trgo rules, test_pwm_ext.py, test_ain.py (B.12: n samples in n/rate, twice on one driver), test_dma.py (32-bit transfers on both MCUs, B.4), and the STM32WBA55 bundle2 PWM outputs (TIM2 CH3/CH4, TIM16 CH1N) in the board file. test_parameters_reference_wired_pins accepts PWM timers wired in any set (the WBA55 bundle2 timers). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
…nd the UID uart.open takes sendonly=1 and builds hal::SynchronousUartStmSendOnly: the USART constructors, or on STM32WB the SyncLpUart LPUART constructors (lp=1; ERR unsupported on STM32WBA, which has none). rx becomes optional, flow is none or rts, parity and swap are refused, and sendonly excludes dma, duplex and sync. Add the sgpio group for the synchronous GPIO classes: sgpio.out/latch build a SynchronousOutputPinStm per pin, sgpio.af a SmallPeripheralPinStm on a timer channel's alternate function from the pinout table or on a raw one, and sgpio.multi a MultiGpioPinStm with a MultiPeripheralPinStm over up to four pins. SyncGpioDriver.cpp is the only translation unit that includes SynchronousGpioStm.hpp, which redefines hal::Port and friends of GpioStm.hpp, and it offers plain functions to the group. Pins are held in the pin pool while a driver object owns them. Add the clock group: clock.info reports the bus clocks, the oscillator ready flags and the RNG kernel clock; on STM32WB55 clock.mco selects the MCO source and divider and clock.hsi48 switches HSI48, as scaffolding for the clock and RNG tests (both names answer ERR unsupported on STM32WBA55). Host: groups/io.py (fw.sgpio, fw.clock, a least-squares edge-time frequency fit, the UID layout), fakes/io.py, test_uart_sendonly.py, test_sgpio.py, test_clock.py (bus clocks and flags; on the WB55 SYSCLK, HSI, HSE, LSE and HSI48 measured on the MCO pin), test_uid.py and the board YAML for them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
Add the rng group: rng.read returns 1-128 random bytes from hal::SynchronousRandomDataGeneratorStm, hal::RandomDataGeneratorStm (variant=async, completion through HilPendingOperation) or, on STM32WB, hal::SynchronousSynchronizedRandomDataGeneratorStm (variant=hsem, through the shared HSEM master) and then reports the HSI48 ready flag. lock5=1 holds HSEM semaphore 5 around a synchronized read so the driver's Hsi48Enabler takes its locked branch. rng.stats generates up to 64 KiB in 256-byte chunks and reports the set bits, the bit runs, the byte chi-square, the CRC-32 and the duration. Add the aes group: aes.enc and aes.dec run hal::SynchronousAes128EcbStm on one to five blocks with each data swapping mode. Add the pka group: pka.mul, pka.check and pka.cmp run hal::PkaStm on secp256r1, with operands left-padded to 32 bytes by the firmware; the PkaStm is built on first use and kept. Host: crypto_ref.py (pure-Python AES-128 with a model of the STM32 data swapping, affine P-256 arithmetic, the FIPS-197, SP 800-38A and CAVP ECC CDH vectors), rngstats.py (the rng.stats counts with monobit, chi-square and runs bounds), groups/crypto.py, fakes/crypto.py, test_rng.py (lengths, distinct reads, statistics per variant, and on the STM32WB55 HSI48 kept on, switched off again and the locked branch, with HSEM semaphores 0 and 5 free afterwards), test_aes.py, test_pka.py, their unit tests and the board YAML for them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
Add the flash group for the internal flash drivers over a scratch region of absolute pages 64-127: variant=sync builds SynchronousFlash(Homogeneous)InternalStm, variant=async Flash(Homogeneous)InternalStm and, on STM32WB55, variant=coord FlashCoordinatedWithWirelessStack over the async driver; layout=homogeneous has one sector per page, layout=table a sector-size table of single pages followed by the page pattern 1, 1, 2, 4 twice. Erases and writes are accepted from the image end page on, so even an erase that takes the sector index for the absolute page cannot reach the running image, and a write over a flash word that is not erased answers ERR failed instead of tripping the driver's assertion. Reads stream any length as hex or CRC. flash.stack starting keeps a coordinated driver whose steps wait until stopped/fus (FirmwareUpgradeServicesReady); a step completing after its ERR timeout prints EVT flash. The coordinated driver borrows the WWDG from the watchdog factory: the started one, or an unstarted one that only has its clock and interrupt; wdt.start answers ERR busy while an unstarted one is borrowed. It holds HSEM 0 of the resource allocation against hsem.lock, as both rely on the HSEM interrupt. Add the hsem group (STM32WB55): two-step take/release per process with an optional EMIL hold timer, the lock state read from R (never RLR), hsem.lock over SynchronousHardwareSemaphoreStm with the scaffold timer TIM17 releasing a process-1 lock from its interrupt after hold us, and hsem.mine over IsLockedByCurrentCore. Add the bkp group over one BackupRamStm used as hal::BackupRam<volatile uint32_t> (info, write, read, fill, check), and the lpm group: LowPowerModeStm entered with PRIMASK set and every NVIC interrupt but the wake line's EXTI and TIM17 disabled and the SysTick tick off; a marker pin is low while the core sleeps, TIM17 counts the time asleep in 1 us ticks (CYCCNT stops in Sleep) and ends the window after timeout ms. Host: groups/system_ext.py (fw.flash, fw.hsem, fw.bkp, fw.lpm, the table layout rule, flash words, the bkp fill), fakes/system_ext.py (flash memory with page erase, HSEM state, backup words that survive reset, lpm waking at once, and wdt.start refused while the coordinated flash borrows the watchdog), test_flash.py (geometry, the region page of every erase per variant and layout, write shapes on distinct words, the coordinated driver against HSEM 7, the stack hold and the shared watchdog), test_hsem.py, test_backup_ram.py, test_low_power.py and the board YAML for them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
Add the qspi command group (PROTOCOL.md "QUADSPI"), STM32WB55 only: - qspi.open builds QuadSpiStm (variant=poll), QuadSpiStmDma on DMA2 channel 3 (variant=dma) or QuadSpiStmDma with SingleSpeedQuadSpiStmDma on top (variant=spi), over the six pins of the board profile, and reports the bus clock (HCLK4 / (prescaler+1)). - qspi.cmd issues one SendData/ReceiveData with every phase optional (instruction, address, alternate bytes, dummy cycles, data on 1 or 4 lines); writes answer flevel, the FIFO level read in the completion callback, and repeat= issues the next write from that callback. - qspi.poll runs PollStatus; qspi.xfer runs SingleSpeedQuadSpiStmDma as a half-duplex SpiMaster. The group is a factory plus a HilSingleInstanceGroup. Commands answer through a HilPendingOperation (ERR timeout after 2000 ms; the group stays busy until qspi.close). Close cancels the operation and destroys the driver one event-loop turn later, after stopping its DMA channel, which HAL_DMA_Init would otherwise keep enabled for the next open. The DMA variants hold DMA2 channel 3 in ResourceAllocation. STM32WBA55 answers the qspi names with ERR unsupported. Host: groups/qspi.py (fw.qspi and the decoding of QUADSPI captures), fakes/qspi.py, test_qspi.py and the board YAML. The AD3 never drives a line the QUADSPI drives: writes are decoded on the logic analyzer, and reads take static AD3 levels only on lines the QUADSPI receives on (IO1 in single-line mode; IO0-IO3 in a 4-line read without instruction, address or alternate bytes, with two dummy cycles against the QUADSPI data-phase-only read erratum, and the weakest AD3 drive). Every case passes the load gate, so --with loopback, spiloop or i2c skip them. QuadSpiStm's endless PollStatus (B.14) is a hanging known gap of test_poll_timeout[poll]. test_fake_groups.py loses its stand-in qspi fake group, which now clashes with fakes/qspi.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
Including PeripheralTable.hpp made RandomDataGeneratorStm build on every MCU with an RNG, but the IRQ was picked per family: STM32F410/F412/F413/ F423 declare only RNG_IRQn and STM32G041/G061/G081/G0C1 only AES_RNG_IRQn, so those builds failed. HASH_RNG_IRQn is now used only on the F4 parts that declare nothing else. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
QuadSpiStm passed FifoThreshold = 32, which HAL_QSPI_Init rejects on parts with a 16-byte FIFO (STM32WB, STM32G4) and so aborted under USE_FULL_ASSERT. The threshold is now the field maximum plus one: 16 on WB/G4, still 32 on F7. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
The WB/WBA page erase left FLASH_EraseInitTypeDef.Banks uninitialised and passed an absolute page across both banks, so on STM32WBA62-65 it erased a page of a random bank. The synchronous driver took the G0 dual-bank path on WBA6x and treated the region-relative sector as a physical page. Both drivers now share detail::ErasePages, which splits the range per bank, honours SWAP_BANK and passes bank-relative pages; single-bank WB/WBA keep their behaviour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
The NMI handler erased the page of an ECC error with FLASH_CR locked, so the erase did nothing. It now reads ECCR before clearing the flag, skips the erase while a flash operation is in progress, and unlocks and relocks the flash around the erase when it was locked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
WaitLock enabled C1IER for the semaphore it spins on, so the other core's release raised an HSEM interrupt that nothing clears for that semaphore (FlashCoordinatedWithWirelessStack owns the HSEM IRQ) and the CPU looped in it. WaitLock now only polls, and Release uses HAL_HSEM_Release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
CR1.DIR ignores writes while the counter is centre aligned or in encoder mode, which an earlier user (an encoder or a centre-aligned PWM) may leave, so TimerBaseStm could count the wrong way. It now initialises up-counting first, selects the internal clock, then initialises again with the requested mode. Start also clears a pending update flag left by the initialisation's update event, which fired a spurious first callback. Affects every family; the result on a timer in reset state is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
Neither the HAL de-init nor gating the clock resets BDTR, so a break input left enabled by an earlier user (PwmStm) kept MOE cleared and the outputs off. TimerPwmBaseStm now writes a cleared BDTR on timers with a break function; on a timer in reset state this changes nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
Firmware: - i2cs: the target no longer keeps slave byte control on for reads, where the NBYTES left by HAL init or the last write capped the bytes sent. - spis.arm and qspi.cmd/xfer check busy before touching the armed or shared DMA buffer; qspi.close resets a QUADSPI left busy by a timed-out command so close/reopen recovers. - tim/lptim refuse update-interrupt rates that would starve the event loop. - rng variant=hsem claims HSEM 0 in ResourceAllocation (busy against the coordinated flash driver and hsem.lock), answers busy when this core already holds semaphore 0 and failed when semaphore 5 is held with HSI48 off; sync/async reads answer failed while the RNG kernel clock is stopped, and clock.hsi48 0 answers busy while an RNG driver exists. - lpm.enter reports how many times the core slept. Tests: - test_i2c.py captures the whole repeated-start transaction and measures the target's clock stretch instead of the host round trip. - test_spi_ext.py no longer claims to prove the 16->8 bit DMA width reset it cannot reach; test_spi_slave.py checks a rejected arm keeps the armed payload; test_qspi.py checks close/reopen after a timed-out read. - test_timer*.py check the counting direction after the encoder and a break left by pwm; test_rng.py, test_sgpio.py (open drain releases the line), test_uart_sendonly.py (RTS only) and test_low_power.py (sleeps counted) now detect the defects they target. Fakes and PROTOCOL.md follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
✅
|
| Descriptor | Linter | Files | Fixed | Errors | Max errors | Warnings | Elapsed time |
|---|---|---|---|---|---|---|---|
| ✅ ACTION | actionlint | 5 | 0 | 0 | 0.06s | ||
| ✅ CPP | clang-format | 100 | 6 | 0 | 0 | 1.88s | |
| ✅ CPP | cppcheck | 100 | 0 | 0 | 3.29s | ||
| ✅ DOCKERFILE | hadolint | 1 | 0 | 0 | 0.04s | ||
| ✅ JSON | jsonlint | 9 | 0 | 0 | 0.11s | ||
| ✅ JSON | prettier | 9 | 3 | 0 | 0 | 1.12s | |
| markdownlint | 18 | 1 | 20 | 0 | 2.52s | ||
| ✅ MARKDOWN | markdown-table-formatter | 18 | 1 | 0 | 0 | 1.08s | |
| ✅ REPOSITORY | betterleaks | yes | no | no | 1.04s | ||
| ✅ REPOSITORY | checkov | yes | no | no | 20.45s | ||
| ✅ REPOSITORY | git_diff | yes | no | no | 0.86s | ||
| ✅ REPOSITORY | grype | yes | no | no | 96.06s | ||
| ✅ REPOSITORY | ls-lint | yes | no | no | 0.01s | ||
| osv-scanner | yes | no | 1 | 0.85s | |||
| ✅ REPOSITORY | secretlint | yes | no | no | 1.88s | ||
| ✅ REPOSITORY | syft | yes | no | no | 2.19s | ||
| ✅ REPOSITORY | trivy | yes | no | no | 12.76s | ||
| ✅ REPOSITORY | trivy-sbom | yes | no | no | 0.5s | ||
| ✅ REPOSITORY | trufflehog | yes | no | no | 5.84s | ||
| lychee | 50 | 6 | 0 | 16.47s | |||
| ✅ YAML | prettier | 8 | 1 | 0 | 0 | 1.13s | |
| ✅ YAML | v8r | 8 | 0 | 0 | 5.14s | ||
| ✅ YAML | yamllint | 8 | 0 | 0 | 0.54s |
Detailed Issues
⚠️ SPELL / lychee - 6 errors
📝 Summary
---------------------
🔍 Total..........289
🔗 Unique.........263
✅ Successful.....278
⏳ Timeouts.........0
🔀 Redirected.....111
👻 Excluded.........5
❓ Unknown..........0
🚫 Errors...........6
⛔ Unsupported......6
Errors in .github/agents/orchestrator.agent.md
[ERROR] file://.github/copilot-instructions.md (at 52:23) | File not found. Check if file exists and path is correct
[ERROR] file://hal_st/cortex/InterruptCortex.hpp (at 56:22) | File not found. Check if file exists and path is correct
Errors in .github/ISSUE_TEMPLATE/add-or-update-hal-driver.md
[ERROR] http://mcd.rou.st.com/modules.php?name=mcu (at 28:63) | Connection failed. Check network connectivity and firewall settings
Errors in README.md
[404] https://api.securityscorecards.dev/projects/github.com/embedded-pro/hal-st (at 3:199) | Rejected status code: 404 Not Found
[ERROR] https://st.com/ (at 5:93) | HTTP/2 protocol error. Server may not support HTTP/2 properly | Followed 1 redirect. Redirects: https://st.com/ --[301]--> https://www.st.com/
Errors in validation/README.md
[403] https://digilent.com/reference/software/waveforms/waveforms-3/start (at 190:12) | Rejected status code: 403 Forbidden
Hint: Followed 111 redirects. You might want to consider replacing redirecting URLs with the resolved URLs. Use verbose mode (`-v`/`-vv`) to see redirection details.
Hint: You can configure accepted/rejected response codes with `-a` or `--accept`
⚠️ MARKDOWN / markdownlint - 20 errors
.github/agents/executor.agent.md:11 error MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "You are the executor agent for..."]
.github/agents/orchestrator.agent.md:18:401 error MD013/line-length Line length [Expected: 400; Actual: 420]
.github/agents/orchestrator.agent.md:18 error MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "You are the orchestrator agent..."]
.github/agents/planner.agent.md:11 error MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "You are the planner agent for ..."]
.github/agents/planner.agent.md:39 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
.github/agents/reviewer.agent.md:14 error MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "You are the reviewer agent for..."]
.github/agents/reviewer.agent.md:122 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
.github/instructions/hal-st-cpp.instructions.md:18 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
.github/ISSUE_TEMPLATE/add-or-update-hal-driver.md:10 error MD025/single-title/single-h1 Multiple top-level headings in the same document [Context: "How to import a HAL driver"]
.github/prompts/orchestrate.prompt.md:8 error MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "Start a new development workfl..."]
CHANGELOG.md:83 error MD024/no-duplicate-heading Multiple headings with the same content [Context: "⚠ BREAKING CHANGES"]
CHANGELOG.md:87 error MD024/no-duplicate-heading Multiple headings with the same content [Context: "Features"]
CHANGELOG.md:94 error MD024/no-duplicate-heading Multiple headings with the same content [Context: "Bug Fixes"]
CHANGELOG.md:103 error MD024/no-duplicate-heading Multiple headings with the same content [Context: "Features"]
CHANGELOG.md:122 error MD024/no-duplicate-heading Multiple headings with the same content [Context: "Bug Fixes"]
CHANGELOG.md:140 error MD024/no-duplicate-heading Multiple headings with the same content [Context: "⚠ BREAKING CHANGES"]
CHANGELOG.md:144 error MD024/no-duplicate-heading Multiple headings with the same content [Context: "Features"]
CHANGELOG.md:151 error MD024/no-duplicate-heading Multiple headings with the same content [Context: "Features"]
CHANGELOG.md:162 error MD024/no-duplicate-heading Multiple headings with the same content [Context: "Bug Fixes"]
CLAUDE.md:1 error MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "hal-st — Claude Instructions"]
⚠️ REPOSITORY / osv-scanner - 1 warning
Scanning dir .
Starting filesystem walk for root: /
End status: 64 dirs visited, 639 inodes visited, 0 Extract calls, 14.970719ms elapsed, 14.971009ms wall time
No package sources found, --help for usage information.
[REPOSITORY_OSV_SCANNER_ERROR_NO_PACKAGE_SOURCES] osv-scanner found no lockfiles, manifests, or SBOMs to scan in the repository.
This is a configuration/scope issue, not a vulnerability finding.
Resolutions:
- Verify the repository actually contains a supported lockfile (package-lock.json, go.sum, Gemfile.lock, Pipfile.lock, etc.) at the scanned path.
- If this is expected for some sub-projects, disable osv-scanner for the affected paths or mark it non-blocking via `DISABLE_ERRORS_LINTERS`.
Notices
REPOSITORY_GITLEAKS, REPOSITORY_KICS. See Removed linters to find their replacements.
See detailed reports in MegaLinter artifacts
Your project could benefit from a custom flavor, which would allow you to run only the linters you need, and thus improve runtime performances. (Skip this info by defining FLAVOR_SUGGESTIONS: false)
- Documentation: Custom Flavors
- Command:
npx mega-linter-runner@10.1.0 --custom-flavor-setup --custom-flavor-linters ACTION_ACTIONLINT,CPP_CPPCHECK,CPP_CLANG_FORMAT,DOCKERFILE_HADOLINT,JSON_JSONLINT,JSON_PRETTIER,MARKDOWN_MARKDOWNLINT,MARKDOWN_MARKDOWN_TABLE_FORMATTER,REPOSITORY_CHECKOV,REPOSITORY_GIT_DIFF,REPOSITORY_BETTERLEAKS,REPOSITORY_GRYPE,REPOSITORY_LS_LINT,REPOSITORY_OSV_SCANNER,REPOSITORY_SECRETLINT,REPOSITORY_SYFT,REPOSITORY_TRIVY,REPOSITORY_TRIVY_SBOM,REPOSITORY_TRUFFLEHOG,SPELL_LYCHEE,YAML_PRETTIER,YAML_YAMLLINT,YAML_V8R

Show us your support by starring ⭐ the repository
betterleaks flags the FIPS-197 C.1 and SP 800-38A F.1.1 keys in the board files' AES known-answer vectors as generic API keys. They are public test vectors, so the scan config allowlists those two values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
main's #95 squash commit has the same tree as the #95 head this branch is built on, so every add/add conflict resolves to this branch's version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
|
The The error comes from MegaLinter's The same error fails There is no code fix, because this is the upstream database registry. I'll re-run the failed job once, after giving the registry time to recover. Generated by Claude Code |
Summary
Follows #95 (merged). This PR extends the hardware-in-the-loop validation app so it exercises every hal-st peripheral driver built for the NUCLEO-WB55RG and NUCLEO-WBA55CG. USB, the radio stacks,
services/st_utiland RTC/tamper are out of scope; RTC/tamper drivers exist only for H5.It also fixes the driver bugs found along the way, and enables three drivers that compiled to nothing on these MCUs.
New coverage
EMIL's HIL terminal has no command group for most of these peripherals, so the validation firmware adds its own groups (specified in
validation/PROTOCOL.md). Each has a host test file and a fake-firmware model.i2c,i2cs,eeprom.*I2cStmon I2C1/I2C3 against an LL I2C target scaffold on the other instance (ACK/NACK at byte k, clock stretching, bus errors), plus EMIL'seeprom.*over a newI2cEepromStmadapter on an external 24LC256spis,spi bits=/lsb=/nss=SpiSlaveStmDma(AD3 master and an SPI1↔SPI2/SPI3 loop),SpiDataSizeConfiguratorStm, the second SPI instancetim,tpwm,lptim,lptpwm,qei lp=1TimerStm,TimerPwmStm,LpTimerStm,LpTimerPwmStm, the WBA55 LPTIM encoderpwmoptions,adc trgo=,ain,dma.waveAdcTriggeredByTimerWithDma;CircularTransmitDmaChanneluart sendonly=1,sgpio,clock, UIDSynchronousUartStmSendOnly, the synchronous GPIO classes, default clocks and MCO,UniqueDeviceIdrng,aes,pkaSynchronousAes128EcbStm,PkaStm(secp256r1), checked against pure-Python referencesflash,hsem,bkp,lpmFlashCoordinatedWithWirelessStack; HSEM;BackupRamStm;LowPowerModeStmqspi(WB55)QuadSpiStm,QuadSpiStmDma,SingleSpeedQuadSpiStmDmaNew infrastructure:
ResourceAllocationand owner ids keep groups that share I2C, SPI, ADC, LPTIM, DMA channels or HSEM off each other.len=/pattern=), with CRC replies, so transfers can exceed the 255-character line.Bench
All extra wiring is in
validation/README.md. Every position is cited from UM2435 for WB55, and from ST readmes and Zephyr for WBA55.--with i2c: PB8–PC0 and PB9–PC1 jumpers, 4.7 kΩ pull-ups and a 24LC256 on a breadboard.--with spiloop: SPI1↔SPI2 jumpers.bundle2(W1/W2 unplugged), with--with i2c(PB2/PB1↔PA6/PA7) and--with spiloop(SPI1↔SPI3).test_wiring.pychecks continuity and pull-ups before a run.Driver fixes (one commit each, guarded to WB/WBA unless noted)
I2cStmrecovers from NACK, bus error and arbitration loss, and clears NACKF.SpiSlaveStmDmaactually enables the SPI on WB/WBA and clears SPE between transfers.SpiMasterStmDma's data-size check can fire.RandomDataGeneratorStmbuilds, with the IRQ name each device declares.hal::BackupRam, with TAMP access on WBA.AdcTriggeredByTimerWithDmatakes the requested sample count and stops conversions afterwards.QuadSpiStmDmacompletes writes on TCF.QuadSpiStmsizes its FIFO threshold from FTHRES (still 32 on F7).TimerStmwrites the counter direction only after CMS/SMS are cleared, and drops a stale update flag before Start.TimerPwmStmresets BDTR.Needs a decision (documented, tests assert current behaviour or mark a known gap)
SPI_NSS_SOFT).QuadSpiStm:PollStatuswaits withHAL_MAX_DELAY. This is a hanging known gap.numberOfSamples.Testing
-Werror. The validation firmware is about 241 KB (WB55) and 218 KB (WBA55); WB55 stays below the flash scratch region at 256 KB.--withoption.🤖 Generated with Claude Code
https://claude.ai/code/session_01PQVqtLNig9iboP9KR4Snno
Generated by Claude Code