diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9753d590..33c874f4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -86,12 +86,16 @@ jobs:
if [ -n "$4" ]; then git -C "$RUNNER_TEMP/st/$1" sparse-checkout set $4; fi
git -C "$RUNNER_TEMP/st/$1" checkout -q FETCH_HEAD
}
- fetch cubef4 https://github.com/STMicroelectronics/STM32CubeF4.git 60c8abe0ae88a23bbd1c925358330031e36dd20f "Projects/STM32446E-Nucleo Projects/STM32F446ZE-Nucleo"
+ fetch cubef4 https://github.com/STMicroelectronics/STM32CubeF4.git 60c8abe0ae88a23bbd1c925358330031e36dd20f "Projects/STM32446E-Nucleo Projects/STM32F446ZE-Nucleo Drivers/BSP/Adafruit_Shield Utilities/Log Utilities/Fonts"
fetch stm32f4xx_hal_driver https://github.com/STMicroelectronics/stm32f4xx_hal_driver.git 1f6451c3e07728b4c830744de380e56bf5bc0026
fetch cmsis_device_f4 https://github.com/STMicroelectronics/cmsis_device_f4.git 9192c7b9df75a142f2027ab266601fe061fc00b3
fetch stm32f4xx-nucleo-bsp https://github.com/STMicroelectronics/stm32f4xx-nucleo-bsp.git 84b99c762908ef56f711acf970ab13d4ec278df6
fetch stm32f4xx-nucleo-144-bsp https://github.com/STMicroelectronics/stm32f4xx-nucleo-144-bsp.git 1daff230cd72b9860df011c25d815e399af6d117
fetch stm32-mw-usb-device https://github.com/STMicroelectronics/stm32-mw-usb-device.git 2a0a3521ac4d84e6e494d37bed615e2d36c373f5
+ fetch stm32-mw-usb-host https://github.com/STMicroelectronics/stm32-mw-usb-host.git 9d46e44c858a77bf126920b7bcc258d59afa89b1
+ fetch stm32-mw-fatfs https://github.com/STMicroelectronics/stm32-mw-fatfs.git fc7f8edc3ac2e263c2f231026053e15d3587b34b
+ fetch stm32-st7735 https://github.com/STMicroelectronics/stm32-st7735.git 2b7db825b3946fd19a158947f9be685e761bed3d
+ fetch stm32-bsp-common https://github.com/STMicroelectronics/stm32-bsp-common.git 1e18c5afdf1f5971a35c8e2f88b6a21e5568ed92
python3 tests/vendor/stm32cubef4/run.py --st "$RUNNER_TEMP/st" --embsim build/${{ matrix.preset }}/bin/embsim
python3 tests/vendor/stm32cubef4/run.py --st "$RUNNER_TEMP/st" --board nucleo_f446ze --embsim build/${{ matrix.preset }}/bin/embsim
- name: Pico SDK applications on the unmodified SDK
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fa6d2738..74626eaa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,58 @@ versions follow semantic versioning. There is no release yet.
## Unreleased
+### USB host mode: the NUCLEO-F446ZE drives a mouse and a flash drive, with Adafruit's TFT shield
+
+- The STM32F446's OTG FS core as a host (`stm32-otg-fs`, FHMOD): the port
+ (PPWR, connection and disconnection, PRST's reset, the port enabled at
+ the device's full or low speed, PSUSP and PRES, PLSTS), an SOF or a
+ low-speed keep-alive every HFIR frame, and twelve channels with SETUP,
+ OUT and IN transactions at the bus's bit time, their data toggles, ACK,
+ NAK, STALL, time-outs, babble and toggle errors, the CHDIS/CHENA halt
+ and its CHH, one IN token per enable, interrupt channels once a frame on
+ ODDFRM's parity, the transmit FIFOs' space and the receive FIFO's host
+ statuses, as ST's HCD driver expects them. The MCU is both ends of its
+ cable: a device's pull-up reaches its host side (`mach.h`'s usbh hooks).
+- USB device parts share a device core (`core/model/usb_dev.c`: chapter 9's
+ standard requests, EP0 in bMaxPacketSize0 packets, toggles, halts,
+ VBUS above 4.4 V); `emblink/usb_mouse`, a low-speed HID boot mouse
+ (046D:C06C) whose inputs move it and press its buttons. The PC's host
+ enumerates low-speed devices at 1.5 Mbit/s.
+- `emblink/stmps2151str`, ST's 500 mA USB power switch with an active-high
+ enable, its values from ST's datasheet (90 mOhm, VIL 0.8 V, VIH 2.0 V);
+ the NUCLEO-F446ZE's U12 powers CN13's VBUS from 5V while PG6 is high.
+- `emblink/st7735_tft18`, a 1.8" 128 x 160 TFT on a Sitronix ST7735R as a C
+ model: its 4-wire SPI, the 18-bit frame memory, CASET/RASET (each address
+ as its bytes come), RAMWR, MADCTL's order, COLMOD's 12, 16 and 18 bits,
+ sleep, display, inversion, idle and partial modes, the resets; its image
+ (as the module is held), a checksum and a pixel probe are outputs.
+- `emblink/adafruit_tft_shield18`, Adafruit's 1.8" TFT shield (product
+ 802) on the Arduino header: the TFT and the five-way joystick's ladder
+ on A3, its resistors from Adafruit's schematic.
+- `emblink/usb_flash_drive`, a USB flash drive: a full-speed mass storage
+ device (Bulk-Only Transport with its CBW, CSW, residues, STALLs and reset
+ recovery; SCSI's INQUIRY, TEST UNIT READY, REQUEST SENSE, MODE SENSE,
+ READ FORMAT CAPACITIES, READ CAPACITY(10), READ(10), WRITE(10) and the
+ rest a host sends), its medium formatted as from the shop (an MBR, one
+ FAT16 partition); its outputs list its files as a computer would.
+- STM32CubeF4's USB_Host/MSC_Standalone runs unmodified on the
+ NUCLEO-F446ZE with the shield and the drive on CN13: it mounts the drive
+ with FatFs, writes 'USBHost.txt', reads it back ('FatFs data compare
+ SUCCESS') and lists it; the drive then holds USBHOST.TXT, 40 bytes
+ (14/14).
+- STM32CubeF4's USB_Host/HID_Standalone runs unmodified on the NUCLEO-F446ZE
+ with the shield and a mouse on CN13: it switches VBUS on through U12,
+ enumerates the mouse at low speed, logs it on the LCD, starts its mouse
+ demo on the joystick's SEL and draws the mouse's moves (13/13).
+- The engine: an analog island chopped by another part's events under a
+ microsecond apart (a core's SPI clock beside a settling supply) lags up
+ to 10 us behind instead of a step to each, and catches up before a part
+ changes its sources, an MCU or a probe reads it, and at an advance's end;
+ the STM32's alternate-function table is indexed and its peripherals'
+ names cached, and an MCU no longer re-reads its ports on its own
+ outputs' echoes. The HID host scenario runs 7 times faster than before
+ these (18 min to 2.5 min for 3.3 s).
+
### A logic analyzer: the Saleae Logic 8
- `emblink/saleae_logic8`: Saleae's 8-channel USB logic analyzer as a part
diff --git a/catalog/boards/nucleo_f446ze/board.json b/catalog/boards/nucleo_f446ze/board.json
index 840b465c..7db3c5f1 100644
--- a/catalog/boards/nucleo_f446ze/board.json
+++ b/catalog/boards/nucleo_f446ze/board.json
@@ -266,6 +266,11 @@
},
"doc": "R35: CN13's VBUS to PA9 (OTG_FS_VBUS), through SB127 (closed)"
},
+ {
+ "id": "U12",
+ "component": "emblink/stmps2151str",
+ "doc": "U12 (STMPS2151STR): +5V to CN13's VBUS while PG6 (USB_PowerSwitchOn) is high, for a device plugged into CN13 when the OTG core is a host; its FAULT to USB_OVCR (PG7 through JP4, LD7)"
+ },
{
"id": "R72",
"component": "emblink/resistor",
@@ -431,7 +436,8 @@
"U6.IN",
"R29.1",
"CN8.9",
- "CN11.18"
+ "CN11.18",
+ "U12.IN"
],
"LD6_A": [
"R29.2",
@@ -515,7 +521,8 @@
"CN12.63",
"CN12.71",
"CN12.72",
- "CN13.5"
+ "CN13.5",
+ "U12.GND"
],
"NRST": [
"U8.NRST",
@@ -596,7 +603,8 @@
"USB_VBUS": [
"R35.1",
"R68.1",
- "CN13.1"
+ "CN13.1",
+ "U12.OUT"
],
"PA9": [
"U8.PA9",
@@ -606,7 +614,8 @@
"PG6": [
"U8.PG6",
"R72.1",
- "CN12.70"
+ "CN12.70",
+ "U12.EN"
],
"PG7": [
"U8.PG7",
@@ -616,7 +625,8 @@
"USB_OVCR": [
"JP4.2",
"R71.2",
- "LD7.K"
+ "LD7.K",
+ "U12.FAULT"
],
"LD7_A": [
"R61.2",
@@ -3716,12 +3726,11 @@
},
"fidelity": {
"functional": {
- "claims": "Nets, ports, LD1 to LD3 and LD6 to LD8, B1, B2 and the jumpers as parts, as MB1137 B-01's schematic draws them; the STM32F446ZE runs its firmware (see its description), its HSE an 8 MHz clock on PH0 (the ST-LINK's MCO through SB149: HSEBYP needed, as on the real board) and its LSE the X2 crystal. The ST Zio connectors CN7 to CN10 carry the NUCLEO-F446ZE's assignment (D0 to D72, A0 to A8, the solder bridges in their factory state) and the morpho headers CN11 and CN12 (2 x 36) every one of the part's 114 I/Os. CN13's VBUS reaches PA9 through R35 and lights LD8; its D-, D+ and ID are PA11, PA12 and PA10, and a USB host on them (emblink/usb_host) enumerates the OTG FS controller in device mode (STM32CubeF4's HID_Standalone runs on it unmodified); PG6 and PG7 carry U12's enable and fault lines (R72's pull-down, R71's pull-up through JP4). The ST-LINK/V2-1's virtual COM port is the session's console on USART3 (PD8/PD9, through SB5 and SB6) and a serial port on the host.",
+ "claims": "Nets, ports, LD1 to LD3 and LD6 to LD8, B1, B2 and the jumpers as parts, as MB1137 B-01's schematic draws them; the STM32F446ZE runs its firmware (see its description), its HSE an 8 MHz clock on PH0 (the ST-LINK's MCO through SB149: HSEBYP needed, as on the real board) and its LSE the X2 crystal. The ST Zio connectors CN7 to CN10 carry the NUCLEO-F446ZE's assignment (D0 to D72, A0 to A8, the solder bridges in their factory state) and the morpho headers CN11 and CN12 (2 x 36) every one of the part's 114 I/Os. CN13's VBUS reaches PA9 through R35 and lights LD8; its D-, D+ and ID are PA11, PA12 and PA10, and a USB host on them (emblink/usb_host) enumerates the OTG FS controller in device mode (STM32CubeF4's USB_Device HID_Standalone runs on it unmodified), and as a host the controller enumerates a device on CN13, U12 powering its VBUS (USB_Host HID_Standalone, with Adafruit's 1.8\" TFT shield, runs on it unmodified with a USB mouse, MSC_Standalone with a USB flash drive); U12 (STMPS2151STR) powers CN13's VBUS from 5V while PG6 is high (R72 pulls it down), for a device on CN13 when the OTG core is a host; its fault line reaches PG7 through JP4 (R71's pull-up) and never asserts. The ST-LINK/V2-1's virtual COM port is the session's console on USART3 (PD8/PD9, through SB5 and SB6) and a serial port on the host.",
"omissions": [
"the ST-LINK/V2-1 itself: its USB enumeration, the mass-storage drag-and-drop programming, SWD and its NRST drive (the session programs and debugs the board, ADR-031)",
"the VCP's bytes reach USART3 directly, not as frames on PD9",
"LD4 (COM), the ST-LINK's tricolour LED",
- "U12 (STMPS2151STR), the switch that would power VBUS from +5V for a USB host (the OTG core's host mode is not modelled either): VBUS comes only from what is plugged into CN13, and U12's fault output never asserts (LD7 stays dark)",
"the Ethernet PHY and RJ45 of sheet 5, which the NUCLEO-F446ZE does not carry (the STM32F446 has no Ethernet MAC)",
"the solder bridges are fixed in their factory state (SB120 on PB0, SB121 on PA7, SB144/SB145/SB148 open, SB163 closed, SB143/SB138 open, the F303ZE, F412ZG and L476ZG alternatives open)",
"one JP3 shunt sits on one position only; the three switches here can all be closed",
diff --git a/catalog/boards/nucleo_f446ze/tests/usb-host-vbus.json b/catalog/boards/nucleo_f446ze/tests/usb-host-vbus.json
new file mode 100644
index 00000000..dbf899fe
--- /dev/null
+++ b/catalog/boards/nucleo_f446ze/tests/usb-host-vbus.json
@@ -0,0 +1,108 @@
+{
+ "schema_version": 1,
+ "kind": "component_test",
+ "name": "usb-host-vbus",
+ "description": "Powered from the ST-LINK's USB, PG6 (USB_PowerSwitchOn) driven high as the OTG core's host firmware does (USBH_LL_DriverVBUS): U12 (STMPS2151) joins 5V to CN13's VBUS, which feeds a 100 mA device (a 50 ohm load), lights LD8 and reaches PA9; U12's fault line stays high (LD7 dark).",
+ "policy": "electrical",
+ "until": "10ms",
+ "circuit": {
+ "boards": [
+ {
+ "id": "B1",
+ "board": "emblink/nucleo_f446ze"
+ }
+ ],
+ "components": [
+ {
+ "id": "HOST",
+ "component": "emblink/vsource",
+ "params": {
+ "kind": "dc",
+ "dc": 5.0
+ }
+ },
+ {
+ "id": "CABLE",
+ "component": "emblink/resistor",
+ "params": {
+ "resistance": 0.25
+ }
+ },
+ {
+ "id": "G",
+ "component": "emblink/gnd"
+ },
+ {
+ "id": "EN",
+ "component": "emblink/vsource",
+ "params": {
+ "kind": "dc",
+ "dc": 3.3
+ }
+ },
+ {
+ "id": "DEV",
+ "component": "emblink/resistor",
+ "params": {
+ "resistance": 50.0
+ }
+ }
+ ],
+ "nets": {
+ "HOSTV": [
+ "HOST.+",
+ "CABLE.1"
+ ],
+ "VBUS": [
+ "CABLE.2",
+ "B1.U5V"
+ ],
+ "GND": [
+ "HOST.-",
+ "G.GND",
+ "B1.GND",
+ "EN.-",
+ "DEV.2"
+ ],
+ "PA9": [
+ "B1.CN12_21"
+ ],
+ "CN13VBUS": [
+ "B1.USB_VBUS",
+ "DEV.1"
+ ],
+ "PG6": [
+ "EN.+",
+ "B1.CN12_70"
+ ],
+ "PG7": [
+ "B1.CN12_67"
+ ]
+ }
+ },
+ "inputs": [],
+ "expect": {
+ "net:CN13VBUS": {
+ "at": "9ms",
+ "min_v": 4.6,
+ "max_v": 5.05
+ },
+ "net:PA9": {
+ "at": "9ms",
+ "min_v": 4.6,
+ "max_v": 5.05
+ },
+ "output:B1.LD8.luminance": {
+ "at": "9ms",
+ "min": 0.05
+ },
+ "output:B1.LD7.luminance": {
+ "at": "9ms",
+ "max": 0.001
+ },
+ "net:PG7": {
+ "at": "9ms",
+ "min_v": 3.0
+ }
+ }
+}
diff --git a/catalog/components/adafruit_tft_shield18/component.json b/catalog/components/adafruit_tft_shield18/component.json
new file mode 100644
index 00000000..a79e234d
--- /dev/null
+++ b/catalog/components/adafruit_tft_shield18/component.json
@@ -0,0 +1,370 @@
+{
+ "schema_version": 1,
+ "kind": "component",
+ "id": "emblink/adafruit_tft_shield18",
+ "version": "0.1.0",
+ "name": "Adafruit 1.8\" TFT shield",
+ "category": "displays",
+ "description": "Adafruit's 1.8\" TFT shield (v1, product 802) on an Arduino Uno V3 header: a 128 x 160 ST7735R TFT on SPI (SCK D13, MOSI D11, TFT_CS D10, TFT_DC D8, its reset on the header's RESET), a five-way joystick read as one analog voltage on A3 (a resistor ladder), and a microSD socket (SD_CS D4). ST's NUCLEO examples use it (STM32CubeF4's USB host and FatFs demos): they look for it by reading A3 high with a pull-down.",
+ "keywords": [
+ "shield",
+ "adafruit",
+ "tft",
+ "st7735",
+ "joystick",
+ "arduino",
+ "nucleo",
+ "1.8 inch"
+ ],
+ "pins": [
+ {
+ "name": "5V",
+ "number": 1,
+ "model": {
+ "kind": "analog"
+ },
+ "doc": "The Arduino header's 5 V: the TFT's supply"
+ },
+ {
+ "name": "3V3",
+ "number": 2,
+ "model": {
+ "kind": "analog"
+ },
+ "doc": "The header's 3.3 V: the joystick ladder's pull-up",
+ "required": false
+ },
+ {
+ "name": "GND",
+ "number": 3,
+ "model": {
+ "kind": "analog"
+ },
+ "doc": "Ground"
+ },
+ {
+ "name": "RESET",
+ "number": 4,
+ "model": {
+ "kind": "analog"
+ },
+ "doc": "The header's RESET: the TFT's RESX (the board's reset resets the display)",
+ "required": false
+ },
+ {
+ "name": "D13",
+ "number": 5,
+ "model": {
+ "kind": "analog"
+ },
+ "doc": "SCK, to the TFT and the microSD socket"
+ },
+ {
+ "name": "D12",
+ "number": 6,
+ "model": {
+ "kind": "passive"
+ },
+ "doc": "MISO, from the microSD socket only: the socket is empty here (a 1 GOhm leakage)",
+ "required": false
+ },
+ {
+ "name": "D11",
+ "number": 7,
+ "model": {
+ "kind": "analog"
+ },
+ "doc": "MOSI, to the TFT's SDA and the microSD socket"
+ },
+ {
+ "name": "D10",
+ "number": 8,
+ "model": {
+ "kind": "analog"
+ },
+ "doc": "TFT_CS, the TFT's chip select"
+ },
+ {
+ "name": "D8",
+ "number": 9,
+ "model": {
+ "kind": "analog"
+ },
+ "doc": "TFT_DC, the TFT's data/command line"
+ },
+ {
+ "name": "D4",
+ "number": 10,
+ "model": {
+ "kind": "passive"
+ },
+ "doc": "SD_CS, the microSD socket's chip select: the socket is empty here (a 1 GOhm leakage)",
+ "required": false
+ },
+ {
+ "name": "A3",
+ "number": 11,
+ "model": {
+ "kind": "analog"
+ },
+ "doc": "The joystick: a resistor ladder, a different voltage for each direction"
+ }
+ ],
+ "params": {},
+ "model": {
+ "composite": {
+ "components": [
+ {
+ "id": "TFT",
+ "component": "emblink/st7735_tft18"
+ },
+ {
+ "id": "RPU",
+ "component": "emblink/resistor",
+ "params": {
+ "resistance": "1k"
+ }
+ },
+ {
+ "id": "LEFT",
+ "component": "emblink/push_button"
+ },
+ {
+ "id": "RLEFT",
+ "component": "emblink/resistor",
+ "params": {
+ "resistance": "10k"
+ }
+ },
+ {
+ "id": "UP",
+ "component": "emblink/push_button"
+ },
+ {
+ "id": "RUP",
+ "component": "emblink/resistor",
+ "params": {
+ "resistance": "1k"
+ }
+ },
+ {
+ "id": "SEL",
+ "component": "emblink/push_button"
+ },
+ {
+ "id": "RSEL",
+ "component": "emblink/resistor",
+ "params": {
+ "resistance": "470"
+ }
+ },
+ {
+ "id": "RIGHT",
+ "component": "emblink/push_button"
+ },
+ {
+ "id": "RRIGHT",
+ "component": "emblink/resistor",
+ "params": {
+ "resistance": "220"
+ }
+ },
+ {
+ "id": "DOWN",
+ "component": "emblink/push_button"
+ },
+ {
+ "id": "RDOWN",
+ "component": "emblink/resistor",
+ "params": {
+ "resistance": "22"
+ }
+ },
+ {
+ "id": "RMISO",
+ "component": "emblink/resistor",
+ "params": {
+ "resistance": "1G"
+ }
+ },
+ {
+ "id": "RSDCS",
+ "component": "emblink/resistor",
+ "params": {
+ "resistance": "1G"
+ }
+ }
+ ],
+ "nets": {
+ "JOY": [
+ "RPU.2",
+ "LEFT.1",
+ "UP.1",
+ "SEL.1",
+ "RIGHT.1",
+ "DOWN.1"
+ ],
+ "GND": [
+ "RLEFT.2",
+ "RUP.2",
+ "RSEL.2",
+ "RRIGHT.2",
+ "RDOWN.2",
+ "TFT.GND",
+ "RMISO.2",
+ "RSDCS.2"
+ ],
+ "V5": [
+ "TFT.VCC"
+ ],
+ "NLEFT": [
+ "LEFT.2",
+ "RLEFT.1"
+ ],
+ "NUP": [
+ "UP.2",
+ "RUP.1"
+ ],
+ "NSEL": [
+ "SEL.2",
+ "RSEL.1"
+ ],
+ "NRIGHT": [
+ "RIGHT.2",
+ "RRIGHT.1"
+ ],
+ "NDOWN": [
+ "DOWN.2",
+ "RDOWN.1"
+ ]
+ },
+ "ports": {
+ "5V": "TFT.VCC",
+ "3V3": "RPU.1",
+ "GND": "TFT.GND",
+ "RESET": "TFT.RST",
+ "D13": "TFT.SCK",
+ "D11": "TFT.SDA",
+ "D10": "TFT.CS",
+ "D8": "TFT.DC",
+ "A3": "RPU.2",
+ "D12": "RMISO.1",
+ "D4": "RSDCS.1"
+ }
+ }
+ },
+ "symbol": {
+ "svg": "symbol.svg",
+ "width": 80,
+ "height": 120,
+ "pins": {
+ "5V": {
+ "x": 0,
+ "y": 10,
+ "side": "left"
+ },
+ "3V3": {
+ "x": 0,
+ "y": 20,
+ "side": "left"
+ },
+ "GND": {
+ "x": 0,
+ "y": 30,
+ "side": "left"
+ },
+ "RESET": {
+ "x": 0,
+ "y": 40,
+ "side": "left"
+ },
+ "D13": {
+ "x": 0,
+ "y": 50,
+ "side": "left"
+ },
+ "D12": {
+ "x": 0,
+ "y": 60,
+ "side": "left"
+ },
+ "D11": {
+ "x": 0,
+ "y": 70,
+ "side": "left"
+ },
+ "D10": {
+ "x": 0,
+ "y": 80,
+ "side": "left"
+ },
+ "D8": {
+ "x": 0,
+ "y": 90,
+ "side": "left"
+ },
+ "D4": {
+ "x": 0,
+ "y": 100,
+ "side": "left"
+ },
+ "A3": {
+ "x": 0,
+ "y": 110,
+ "side": "left"
+ }
+ }
+ },
+ "fidelity": {
+ "functional": {
+ "claims": "Its TFT as emblink/st7735_tft18 says, wired as the shield wires it; the joystick's five switches (LEFT, UP, SEL, RIGHT, DOWN, BTN1's five contacts, each a push button: its pressed input) as a ladder on A3.",
+ "omissions": [
+ "the joystick's voltages: logic levels only (electrical gives them)"
+ ],
+ "validation": [
+ "tests/*.json"
+ ]
+ },
+ "timed": {
+ "claims": "As functional.",
+ "omissions": []
+ },
+ "electrical": {
+ "claims": "A3 (the schematic's NAV) pulled up to 3V3 through R8 (1 kOhm), a pressed direction of the five-way switch BTN1 pulling it to ground through its resistor: LEFT R6 (10 kOhm, 3.0 V), UP R3 (1.0 kOhm, 1.65 V), SEL R4 (470 Ohm, 1.05 V), RIGHT R5 (220 Ohm, 0.60 V), DOWN R7 (22 Ohm, 71 mV); 3.3 V with nothing pressed, so a pull-down of tens of kOhm (ST's shield detection) still reads high. Each lands in the window ST's BSP decodes (BSP_JOY_GetState).",
+ "omissions": [
+ "the microSD socket (D4, D12): empty, each line a 1 GOhm leakage",
+ "the RT9193 3.3 V regulator (IC2) and the 5 V to 3.3 V level shifting (IC1): the TFT is fed from 5V and driven directly",
+ "the backlight's NPN (Q1, R1, R2, R9 on BACKLITE): always on",
+ "the shield's own reset button and the 0.1 uF and 10 uF decoupling",
+ "two directions pressed at once read as their parallel resistance, as on the shield"
+ ],
+ "validation": [
+ "tests/*.json"
+ ]
+ }
+ },
+ "provenance": {
+ "author": "EmbSIM",
+ "license": "Apache-2.0",
+ "sources": [
+ {
+ "title": "Adafruit 1.8\" TFT shield Eagle schematic (Adafruit 1.8 TFTshield.sch, the original revision): BTN1, R3 to R8 on A3, Q1, IC2 RT9193-33",
+ "url": "https://github.com/adafruit/Adafruit-1.8-TFT-Shield-PCB",
+ "date": "2026-10-31"
+ },
+ {
+ "title": "Adafruit 1.8\" TFT shield (product 802) tutorial: its pins, the joystick on A3",
+ "url": "https://learn.adafruit.com/1-8-tft-display",
+ "date": "2026-10-31"
+ },
+ {
+ "title": "STMicroelectronics stm32f4xx-nucleo-144-bsp: BSP_JOY_GetState's windows on ADC3 channel 9 (PF3) and the LCD and SD pins",
+ "url": "https://github.com/STMicroelectronics/stm32f4xx-nucleo-144-bsp",
+ "date": "2026-10-31"
+ }
+ ]
+ },
+ "tests": [
+ "tests/*.json"
+ ]
+}
diff --git a/catalog/components/adafruit_tft_shield18/symbol.svg b/catalog/components/adafruit_tft_shield18/symbol.svg
new file mode 100644
index 00000000..24598c93
--- /dev/null
+++ b/catalog/components/adafruit_tft_shield18/symbol.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/catalog/components/adafruit_tft_shield18/tests/joy-down.json b/catalog/components/adafruit_tft_shield18/tests/joy-down.json
new file mode 100644
index 00000000..954c7c69
--- /dev/null
+++ b/catalog/components/adafruit_tft_shield18/tests/joy-down.json
@@ -0,0 +1,70 @@
+{
+ "schema_version": 1,
+ "kind": "component_test",
+ "name": "joy-down",
+ "description": "DOWN pressed: A3 inside the window ST's BSP reads as JOY_DOWN (ADC codes 50 to 130 at 3.3 V).",
+ "policy": "electrical",
+ "until": "4ms",
+ "circuit": {
+ "components": [
+ {
+ "id": "V33",
+ "component": "emblink/vsource",
+ "params": {
+ "kind": "dc",
+ "dc": 3.3
+ }
+ },
+ {
+ "id": "V5",
+ "component": "emblink/vsource",
+ "params": {
+ "kind": "dc",
+ "dc": 5.0
+ }
+ },
+ {
+ "id": "DUT",
+ "component": "emblink/adafruit_tft_shield18"
+ },
+ {
+ "id": "G",
+ "component": "emblink/gnd"
+ }
+ ],
+ "nets": {
+ "P33": [
+ "V33.+",
+ "DUT.3V3"
+ ],
+ "P5": [
+ "V5.+",
+ "DUT.5V"
+ ],
+ "GND": [
+ "V33.-",
+ "V5.-",
+ "DUT.GND",
+ "G.GND"
+ ],
+ "A3": [
+ "DUT.A3"
+ ]
+ }
+ },
+ "expect": {
+ "net:A3": {
+ "at": "3ms",
+ "min_v": 0.040293040293040296,
+ "max_v": 0.10476190476190476
+ }
+ },
+ "inputs": [
+ {
+ "at": "1ms",
+ "target": "DUT.DOWN",
+ "input": "pressed",
+ "value": true
+ }
+ ]
+}
diff --git a/catalog/components/adafruit_tft_shield18/tests/joy-left.json b/catalog/components/adafruit_tft_shield18/tests/joy-left.json
new file mode 100644
index 00000000..0878ca77
--- /dev/null
+++ b/catalog/components/adafruit_tft_shield18/tests/joy-left.json
@@ -0,0 +1,70 @@
+{
+ "schema_version": 1,
+ "kind": "component_test",
+ "name": "joy-left",
+ "description": "LEFT pressed: A3 inside the window ST's BSP reads as JOY_LEFT (ADC codes 3680 to 3760 at 3.3 V).",
+ "policy": "electrical",
+ "until": "4ms",
+ "circuit": {
+ "components": [
+ {
+ "id": "V33",
+ "component": "emblink/vsource",
+ "params": {
+ "kind": "dc",
+ "dc": 3.3
+ }
+ },
+ {
+ "id": "V5",
+ "component": "emblink/vsource",
+ "params": {
+ "kind": "dc",
+ "dc": 5.0
+ }
+ },
+ {
+ "id": "DUT",
+ "component": "emblink/adafruit_tft_shield18"
+ },
+ {
+ "id": "G",
+ "component": "emblink/gnd"
+ }
+ ],
+ "nets": {
+ "P33": [
+ "V33.+",
+ "DUT.3V3"
+ ],
+ "P5": [
+ "V5.+",
+ "DUT.5V"
+ ],
+ "GND": [
+ "V33.-",
+ "V5.-",
+ "DUT.GND",
+ "G.GND"
+ ],
+ "A3": [
+ "DUT.A3"
+ ]
+ }
+ },
+ "expect": {
+ "net:A3": {
+ "at": "3ms",
+ "min_v": 2.9655677655677657,
+ "max_v": 3.03003663003663
+ }
+ },
+ "inputs": [
+ {
+ "at": "1ms",
+ "target": "DUT.LEFT",
+ "input": "pressed",
+ "value": true
+ }
+ ]
+}
diff --git a/catalog/components/adafruit_tft_shield18/tests/joy-right.json b/catalog/components/adafruit_tft_shield18/tests/joy-right.json
new file mode 100644
index 00000000..93c49e86
--- /dev/null
+++ b/catalog/components/adafruit_tft_shield18/tests/joy-right.json
@@ -0,0 +1,70 @@
+{
+ "schema_version": 1,
+ "kind": "component_test",
+ "name": "joy-right",
+ "description": "RIGHT pressed: A3 inside the window ST's BSP reads as JOY_RIGHT (ADC codes 680 to 780 at 3.3 V).",
+ "policy": "electrical",
+ "until": "4ms",
+ "circuit": {
+ "components": [
+ {
+ "id": "V33",
+ "component": "emblink/vsource",
+ "params": {
+ "kind": "dc",
+ "dc": 3.3
+ }
+ },
+ {
+ "id": "V5",
+ "component": "emblink/vsource",
+ "params": {
+ "kind": "dc",
+ "dc": 5.0
+ }
+ },
+ {
+ "id": "DUT",
+ "component": "emblink/adafruit_tft_shield18"
+ },
+ {
+ "id": "G",
+ "component": "emblink/gnd"
+ }
+ ],
+ "nets": {
+ "P33": [
+ "V33.+",
+ "DUT.3V3"
+ ],
+ "P5": [
+ "V5.+",
+ "DUT.5V"
+ ],
+ "GND": [
+ "V33.-",
+ "V5.-",
+ "DUT.GND",
+ "G.GND"
+ ],
+ "A3": [
+ "DUT.A3"
+ ]
+ }
+ },
+ "expect": {
+ "net:A3": {
+ "at": "3ms",
+ "min_v": 0.547985347985348,
+ "max_v": 0.6285714285714286
+ }
+ },
+ "inputs": [
+ {
+ "at": "1ms",
+ "target": "DUT.RIGHT",
+ "input": "pressed",
+ "value": true
+ }
+ ]
+}
diff --git a/catalog/components/adafruit_tft_shield18/tests/joy-sel.json b/catalog/components/adafruit_tft_shield18/tests/joy-sel.json
new file mode 100644
index 00000000..715f87a9
--- /dev/null
+++ b/catalog/components/adafruit_tft_shield18/tests/joy-sel.json
@@ -0,0 +1,70 @@
+{
+ "schema_version": 1,
+ "kind": "component_test",
+ "name": "joy-sel",
+ "description": "SEL pressed: A3 inside the window ST's BSP reads as JOY_SEL (ADC codes 1270 to 1350 at 3.3 V).",
+ "policy": "electrical",
+ "until": "4ms",
+ "circuit": {
+ "components": [
+ {
+ "id": "V33",
+ "component": "emblink/vsource",
+ "params": {
+ "kind": "dc",
+ "dc": 3.3
+ }
+ },
+ {
+ "id": "V5",
+ "component": "emblink/vsource",
+ "params": {
+ "kind": "dc",
+ "dc": 5.0
+ }
+ },
+ {
+ "id": "DUT",
+ "component": "emblink/adafruit_tft_shield18"
+ },
+ {
+ "id": "G",
+ "component": "emblink/gnd"
+ }
+ ],
+ "nets": {
+ "P33": [
+ "V33.+",
+ "DUT.3V3"
+ ],
+ "P5": [
+ "V5.+",
+ "DUT.5V"
+ ],
+ "GND": [
+ "V33.-",
+ "V5.-",
+ "DUT.GND",
+ "G.GND"
+ ],
+ "A3": [
+ "DUT.A3"
+ ]
+ }
+ },
+ "expect": {
+ "net:A3": {
+ "at": "3ms",
+ "min_v": 1.0234432234432234,
+ "max_v": 1.0879120879120878
+ }
+ },
+ "inputs": [
+ {
+ "at": "1ms",
+ "target": "DUT.SEL",
+ "input": "pressed",
+ "value": true
+ }
+ ]
+}
diff --git a/catalog/components/adafruit_tft_shield18/tests/joy-up.json b/catalog/components/adafruit_tft_shield18/tests/joy-up.json
new file mode 100644
index 00000000..98f3925c
--- /dev/null
+++ b/catalog/components/adafruit_tft_shield18/tests/joy-up.json
@@ -0,0 +1,70 @@
+{
+ "schema_version": 1,
+ "kind": "component_test",
+ "name": "joy-up",
+ "description": "UP pressed: A3 inside the window ST's BSP reads as JOY_UP (ADC codes 2010 to 2090 at 3.3 V).",
+ "policy": "electrical",
+ "until": "4ms",
+ "circuit": {
+ "components": [
+ {
+ "id": "V33",
+ "component": "emblink/vsource",
+ "params": {
+ "kind": "dc",
+ "dc": 3.3
+ }
+ },
+ {
+ "id": "V5",
+ "component": "emblink/vsource",
+ "params": {
+ "kind": "dc",
+ "dc": 5.0
+ }
+ },
+ {
+ "id": "DUT",
+ "component": "emblink/adafruit_tft_shield18"
+ },
+ {
+ "id": "G",
+ "component": "emblink/gnd"
+ }
+ ],
+ "nets": {
+ "P33": [
+ "V33.+",
+ "DUT.3V3"
+ ],
+ "P5": [
+ "V5.+",
+ "DUT.5V"
+ ],
+ "GND": [
+ "V33.-",
+ "V5.-",
+ "DUT.GND",
+ "G.GND"
+ ],
+ "A3": [
+ "DUT.A3"
+ ]
+ }
+ },
+ "expect": {
+ "net:A3": {
+ "at": "3ms",
+ "min_v": 1.6197802197802198,
+ "max_v": 1.6842490842490843
+ }
+ },
+ "inputs": [
+ {
+ "at": "1ms",
+ "target": "DUT.UP",
+ "input": "pressed",
+ "value": true
+ }
+ ]
+}
diff --git a/catalog/components/adafruit_tft_shield18/tests/released.json b/catalog/components/adafruit_tft_shield18/tests/released.json
new file mode 100644
index 00000000..edc86e21
--- /dev/null
+++ b/catalog/components/adafruit_tft_shield18/tests/released.json
@@ -0,0 +1,62 @@
+{
+ "schema_version": 1,
+ "kind": "component_test",
+ "name": "released",
+ "description": "Nothing pressed: A3 at 3V3 through the 10 kOhm pull-up (ST's BSP reads JOY_NONE, and its examples find the shield).",
+ "policy": "electrical",
+ "until": "4ms",
+ "circuit": {
+ "components": [
+ {
+ "id": "V33",
+ "component": "emblink/vsource",
+ "params": {
+ "kind": "dc",
+ "dc": 3.3
+ }
+ },
+ {
+ "id": "V5",
+ "component": "emblink/vsource",
+ "params": {
+ "kind": "dc",
+ "dc": 5.0
+ }
+ },
+ {
+ "id": "DUT",
+ "component": "emblink/adafruit_tft_shield18"
+ },
+ {
+ "id": "G",
+ "component": "emblink/gnd"
+ }
+ ],
+ "nets": {
+ "P33": [
+ "V33.+",
+ "DUT.3V3"
+ ],
+ "P5": [
+ "V5.+",
+ "DUT.5V"
+ ],
+ "GND": [
+ "V33.-",
+ "V5.-",
+ "DUT.GND",
+ "G.GND"
+ ],
+ "A3": [
+ "DUT.A3"
+ ]
+ }
+ },
+ "expect": {
+ "net:A3": {
+ "at": "3ms",
+ "min_v": 3.25,
+ "max_v": 3.31
+ }
+ }
+}
diff --git a/catalog/components/st7735_tft18/component.json b/catalog/components/st7735_tft18/component.json
new file mode 100644
index 00000000..df1a1449
--- /dev/null
+++ b/catalog/components/st7735_tft18/component.json
@@ -0,0 +1,262 @@
+{
+ "schema_version": 1,
+ "kind": "component",
+ "id": "emblink/st7735_tft18",
+ "version": "0.1.0",
+ "name": "1.8\" 128 x 160 TFT (ST7735R)",
+ "category": "displays",
+ "description": "A 1.8 inch 128 x 160 colour TFT on a Sitronix ST7735R controller, driven over its 4-wire SPI interface (SCK, SDA, CS, D/C, RESET), with its backlight: the display of Adafruit's 1.8\" TFT breakout and shield and of many 1.8\" modules.",
+ "keywords": [
+ "tft",
+ "lcd",
+ "display",
+ "st7735",
+ "st7735r",
+ "spi",
+ "128x160",
+ "1.8 inch",
+ "adafruit"
+ ],
+ "pins": [
+ {
+ "name": "VCC",
+ "number": 1,
+ "model": {
+ "kind": "digital_in"
+ },
+ "doc": "Supply (3.3 V logic on the module): the controller resets as it rises past about 2.3 V",
+ "required": false
+ },
+ {
+ "name": "GND",
+ "number": 2,
+ "model": {
+ "kind": "power",
+ "role": "ground"
+ },
+ "doc": "Ground"
+ },
+ {
+ "name": "SCK",
+ "number": 3,
+ "model": {
+ "kind": "digital_in"
+ },
+ "doc": "SCL: the serial clock, each bit taken on its rising edge"
+ },
+ {
+ "name": "SDA",
+ "number": 4,
+ "model": {
+ "kind": "digital_in"
+ },
+ "doc": "SDA: serial data in (MOSI), MSB first"
+ },
+ {
+ "name": "CS",
+ "number": 5,
+ "model": {
+ "kind": "digital_in"
+ },
+ "doc": "CSX: chip select, active low",
+ "required": false
+ },
+ {
+ "name": "DC",
+ "number": 6,
+ "model": {
+ "kind": "digital_in"
+ },
+ "doc": "D/CX (A0): low for a command, high for data, read with each byte's last bit"
+ },
+ {
+ "name": "RST",
+ "number": 7,
+ "model": {
+ "kind": "digital_in"
+ },
+ "doc": "RESX: reset, active low",
+ "required": false
+ },
+ {
+ "name": "LED",
+ "number": 8,
+ "model": {
+ "kind": "digital_in"
+ },
+ "doc": "The backlight's enable (LITE): high or open lights it",
+ "required": false
+ }
+ ],
+ "params": {},
+ "inputs": [
+ {
+ "name": "probe_x",
+ "type": "integer",
+ "default": 0,
+ "doc": "The probed pixel's column"
+ },
+ {
+ "name": "probe_y",
+ "type": "integer",
+ "default": 0,
+ "doc": "The probed pixel's row"
+ }
+ ],
+ "outputs": [
+ {
+ "name": "on",
+ "type": "boolean",
+ "doc": "Awake and its display on"
+ },
+ {
+ "name": "sleeping",
+ "type": "boolean",
+ "doc": "In sleep mode (SLPIN, after a reset)"
+ },
+ {
+ "name": "inverted",
+ "type": "boolean",
+ "doc": "INVON"
+ },
+ {
+ "name": "colmod",
+ "type": "integer",
+ "doc": "COLMOD's interface format: 3 (12 bits a pixel), 5 (16), 6 (18)"
+ },
+ {
+ "name": "madctl",
+ "type": "integer",
+ "doc": "MADCTL: MY, MX, MV, ML, RGB order"
+ },
+ {
+ "name": "ram_writes",
+ "type": "integer",
+ "doc": "Pixels written to the frame memory"
+ },
+ {
+ "name": "commands",
+ "type": "integer",
+ "doc": "Commands taken"
+ },
+ {
+ "name": "white_pixels",
+ "type": "integer",
+ "doc": "Pixels the panel shows white"
+ },
+ {
+ "name": "probe",
+ "type": "integer",
+ "doc": "The probed pixel's colour as shown, RGB565 ((0, 0) the top left as the module is held)"
+ },
+ {
+ "name": "checksum",
+ "type": "string",
+ "doc": "FNV-1a of the image shown (RGB565, big-endian, row by row)"
+ },
+ {
+ "name": "frame",
+ "type": "string",
+ "doc": "The image shown, as the module is held (its pins at the top): 128 x 160 RGB565 pixels, big-endian, row by row from the top left, in base64"
+ }
+ ],
+ "model": {
+ "primitive": "st7735"
+ },
+ "functional": {
+ "primitive": "st7735"
+ },
+ "symbol": {
+ "svg": "symbol.svg",
+ "width": 80,
+ "height": 100,
+ "pins": {
+ "VCC": {
+ "x": 0,
+ "y": 10,
+ "side": "left"
+ },
+ "GND": {
+ "x": 0,
+ "y": 20,
+ "side": "left"
+ },
+ "SCK": {
+ "x": 0,
+ "y": 30,
+ "side": "left"
+ },
+ "SDA": {
+ "x": 0,
+ "y": 40,
+ "side": "left"
+ },
+ "CS": {
+ "x": 0,
+ "y": 50,
+ "side": "left"
+ },
+ "DC": {
+ "x": 0,
+ "y": 60,
+ "side": "left"
+ },
+ "RST": {
+ "x": 0,
+ "y": 70,
+ "side": "left"
+ },
+ "LED": {
+ "x": 0,
+ "y": 80,
+ "side": "left"
+ }
+ }
+ },
+ "fidelity": {
+ "functional": {
+ "claims": "The ST7735R's 4-wire serial interface (bits on SCL's rising edge, D/CX with the last bit, CSX resetting the bit counter only) and its frame memory of 128 x 160 pixels at 18 bits: CASET, RASET (each address taken as its two bytes come, so one cut short after its start keeps the end, as ST's driver relies on) and RAMWR in the window, MADCTL's MV, MX, MY and RGB, COLMOD's 12, 16 and 18 bits a pixel, SLPIN/SLPOUT, DISPON/DISPOFF, INVON/INVOFF, IDMON/IDMOFF, PTLON with PTLAR, NORON, SWRESET and RESX, a byte within their 5 ms reset cancel time (the datasheet's maximum) taken and warned of once; after a reset it sleeps with its display off. The panel shows white while blank and nothing without supply or backlight; the memory holds noise until written. The panel as the module is held (its pins at the top): frame memory (0, 0) at its bottom right, so MADCTL's MX and MY (as Adafruit's and ST's drivers set them) give an upright image; the outputs show it that way.",
+ "omissions": [
+ "the 3-wire (9-bit) and parallel interfaces",
+ "the read commands (RDDID, RDDST, RAMRD): SDA is never driven",
+ "vertical scrolling (SCRLAR, VSCSAD) and the tearing effect output",
+ "the frame rate, the panel's response time and the gamma curves' effect on colours (B1 to C5, E0, E1 and GAMSET are taken without effect)",
+ "the 120 ms an SLPOUT needs before an SLPIN, and the supply current",
+ "a part slower than typical: a byte within the reset cancel time is always taken"
+ ],
+ "validation": [
+ "tests/*.json"
+ ]
+ },
+ "timed": {
+ "claims": "As functional.",
+ "omissions": []
+ },
+ "electrical": {
+ "claims": "As functional, its supply a threshold on VCC.",
+ "omissions": [],
+ "validation": [
+ "tests/*.json"
+ ]
+ }
+ },
+ "provenance": {
+ "author": "EmbSIM",
+ "license": "Apache-2.0",
+ "sources": [
+ {
+ "title": "Sitronix ST7735R datasheet V2.1: the serial interface (9.4), the command set (10.1, 10.2), the reset timing (9.15) and the frame memory (9.7)",
+ "url": "https://www.displayfuture.com/Display/datasheet/controller/ST7735.pdf",
+ "date": "2026-10-31"
+ },
+ {
+ "title": "STMicroelectronics stm32-st7735 driver (st7735.c), the sequence ST's BSP sends",
+ "url": "https://github.com/STMicroelectronics/stm32-st7735",
+ "date": "2026-10-31"
+ }
+ ]
+ },
+ "tests": [
+ "tests/*.json"
+ ]
+}
diff --git a/catalog/components/st7735_tft18/symbol.svg b/catalog/components/st7735_tft18/symbol.svg
new file mode 100644
index 00000000..56398812
--- /dev/null
+++ b/catalog/components/st7735_tft18/symbol.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/catalog/components/st7735_tft18/tests/asleep.json b/catalog/components/st7735_tft18/tests/asleep.json
new file mode 100644
index 00000000..9cf8237f
--- /dev/null
+++ b/catalog/components/st7735_tft18/tests/asleep.json
@@ -0,0 +1,58 @@
+{
+ "schema_version": 1,
+ "kind": "component_test",
+ "name": "asleep",
+ "description": "After its reset the controller sleeps with its display off: the panel is blank (white) until SLPOUT and DISPON, whatever the memory holds.",
+ "policy": "functional",
+ "until": "20ms",
+ "circuit": {
+ "components": [
+ {
+ "id": "PG",
+ "component": "emblink/pattern_generator",
+ "params": {
+ "pattern": "0,1,0,1,2,3,2,3,2,3,0,1,2,3,0,1,9,4,5,4,5,4,5,4,5,4,5,6,7,4,5,6,7,d",
+ "period": 1e-07,
+ "start": 0.006,
+ "idle": 9
+ }
+ },
+ {
+ "id": "DUT",
+ "component": "emblink/st7735_tft18"
+ }
+ ],
+ "nets": {
+ "SCK": [
+ "PG.OUT0",
+ "DUT.SCK"
+ ],
+ "SDA": [
+ "PG.OUT1",
+ "DUT.SDA"
+ ],
+ "DC": [
+ "PG.OUT2",
+ "DUT.DC"
+ ],
+ "CS": [
+ "PG.OUT3",
+ "DUT.CS"
+ ]
+ }
+ },
+ "expect": {
+ "output:DUT.white_pixels": {
+ "at": "19ms",
+ "equals": 20480
+ },
+ "output:DUT.sleeping": {
+ "at": "19ms",
+ "equals": true
+ },
+ "output:DUT.colmod": {
+ "at": "19ms",
+ "equals": 5
+ }
+ }
+}
diff --git a/catalog/components/st7735_tft18/tests/bgr-inverted.json b/catalog/components/st7735_tft18/tests/bgr-inverted.json
new file mode 100644
index 00000000..3247c7b5
--- /dev/null
+++ b/catalog/components/st7735_tft18/tests/bgr-inverted.json
@@ -0,0 +1,68 @@
+{
+ "schema_version": 1,
+ "kind": "component_test",
+ "name": "bgr-inverted",
+ "description": "MADCTL's BGR bit swaps red and blue on the panel, and INVON inverts: a pure red pixel (F800) at frame memory (0, 0), the panel's bottom right, shows as the inverse of blue, FFE0.",
+ "policy": "functional",
+ "until": "20ms",
+ "circuit": {
+ "components": [
+ {
+ "id": "PG",
+ "component": "emblink/pattern_generator",
+ "params": {
+ "pattern": "0,1,0,1,0,1,2,3,0,1,0,1,0,1,2,3,9,0,1,0,1,2,3,0,1,2,3,0,1,0,1,2,3,9,0,1,0,1,2,3,2,3,2,3,0,1,2,3,0,1,9,4,5,4,5,4,5,4,5,4,5,6,7,4,5,6,7,d,0,1,0,1,2,3,2,3,0,1,2,3,2,3,0,1,9,4,5,4,5,4,5,4,5,6,7,4,5,4,5,4,5,d,0,1,0,1,2,3,0,1,0,1,0,1,0,1,2,3,9,0,1,0,1,2,3,0,1,2,3,0,1,2,3,0,1,9,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,d,0,1,0,1,2,3,0,1,2,3,0,1,2,3,2,3,9,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,d,0,1,0,1,2,3,0,1,2,3,2,3,0,1,0,1,9,6,7,6,7,6,7,6,7,6,7,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,d",
+ "period": 1e-07,
+ "start": 0.006,
+ "idle": 9
+ }
+ },
+ {
+ "id": "DUT",
+ "component": "emblink/st7735_tft18"
+ }
+ ],
+ "nets": {
+ "SCK": [
+ "PG.OUT0",
+ "DUT.SCK"
+ ],
+ "SDA": [
+ "PG.OUT1",
+ "DUT.SDA"
+ ],
+ "DC": [
+ "PG.OUT2",
+ "DUT.DC"
+ ],
+ "CS": [
+ "PG.OUT3",
+ "DUT.CS"
+ ]
+ }
+ },
+ "expect": {
+ "output:DUT.probe": {
+ "at": "19ms",
+ "equals": 65504
+ },
+ "output:DUT.inverted": {
+ "at": "19ms",
+ "equals": true
+ }
+ },
+ "inputs": [
+ {
+ "at": "18ms",
+ "target": "DUT",
+ "input": "probe_x",
+ "value": 127
+ },
+ {
+ "at": "18ms",
+ "target": "DUT",
+ "input": "probe_y",
+ "value": 159
+ }
+ ]
+}
diff --git a/catalog/components/st7735_tft18/tests/mirrored.json b/catalog/components/st7735_tft18/tests/mirrored.json
new file mode 100644
index 00000000..76d1840c
--- /dev/null
+++ b/catalog/components/st7735_tft18/tests/mirrored.json
@@ -0,0 +1,64 @@
+{
+ "schema_version": 1,
+ "kind": "component_test",
+ "name": "mirrored",
+ "description": "MADCTL 0xC0 (MY and MX, as ST's st7735 driver sets it): the window's first pixel, address (0, 0), lands in frame memory (127, 159), the panel's top left as the module is held: the image is upright.",
+ "policy": "functional",
+ "until": "20ms",
+ "circuit": {
+ "components": [
+ {
+ "id": "PG",
+ "component": "emblink/pattern_generator",
+ "params": {
+ "pattern": "0,1,0,1,0,1,2,3,0,1,0,1,0,1,2,3,9,0,1,0,1,2,3,0,1,2,3,0,1,0,1,2,3,9,0,1,0,1,2,3,2,3,2,3,0,1,2,3,0,1,9,4,5,4,5,4,5,4,5,4,5,6,7,4,5,6,7,d,0,1,0,1,2,3,2,3,0,1,2,3,2,3,0,1,9,6,7,6,7,4,5,4,5,4,5,4,5,4,5,4,5,d,0,1,0,1,2,3,0,1,2,3,0,1,2,3,0,1,9,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,d,0,1,0,1,2,3,0,1,2,3,0,1,2,3,2,3,9,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,d,0,1,0,1,2,3,0,1,2,3,2,3,0,1,0,1,9,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,6,7,6,7,6,7,6,7,6,7,d",
+ "period": 1e-07,
+ "start": 0.006,
+ "idle": 9
+ }
+ },
+ {
+ "id": "DUT",
+ "component": "emblink/st7735_tft18"
+ }
+ ],
+ "nets": {
+ "SCK": [
+ "PG.OUT0",
+ "DUT.SCK"
+ ],
+ "SDA": [
+ "PG.OUT1",
+ "DUT.SDA"
+ ],
+ "DC": [
+ "PG.OUT2",
+ "DUT.DC"
+ ],
+ "CS": [
+ "PG.OUT3",
+ "DUT.CS"
+ ]
+ }
+ },
+ "expect": {
+ "output:DUT.probe": {
+ "at": "19ms",
+ "equals": 31
+ }
+ },
+ "inputs": [
+ {
+ "at": "18ms",
+ "target": "DUT",
+ "input": "probe_x",
+ "value": 0
+ },
+ {
+ "at": "18ms",
+ "target": "DUT",
+ "input": "probe_y",
+ "value": 0
+ }
+ ]
+}
diff --git a/catalog/components/st7735_tft18/tests/pixels.json b/catalog/components/st7735_tft18/tests/pixels.json
new file mode 100644
index 00000000..88e53697
--- /dev/null
+++ b/catalog/components/st7735_tft18/tests/pixels.json
@@ -0,0 +1,72 @@
+{
+ "schema_version": 1,
+ "kind": "component_test",
+ "name": "pixels",
+ "description": "Awake, the display on, 16 bits a pixel, MADCTL 0: a window of columns 2 and 3 on row 5 written red then green; frame memory (3, 5) is green, which the panel (its scan from its bottom right) shows at (124, 154).",
+ "policy": "functional",
+ "until": "20ms",
+ "circuit": {
+ "components": [
+ {
+ "id": "PG",
+ "component": "emblink/pattern_generator",
+ "params": {
+ "pattern": "0,1,0,1,0,1,2,3,0,1,0,1,0,1,2,3,9,0,1,0,1,2,3,0,1,2,3,0,1,0,1,2,3,9,0,1,0,1,2,3,2,3,2,3,0,1,2,3,0,1,9,4,5,4,5,4,5,4,5,4,5,6,7,4,5,6,7,d,0,1,0,1,2,3,0,1,2,3,0,1,2,3,0,1,9,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,6,7,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,6,7,6,7,d,0,1,0,1,2,3,0,1,2,3,0,1,2,3,2,3,9,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,6,7,4,5,6,7,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,6,7,4,5,6,7,d,0,1,0,1,2,3,0,1,2,3,2,3,0,1,0,1,9,6,7,6,7,6,7,6,7,6,7,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,6,7,6,7,6,7,6,7,6,7,6,7,4,5,4,5,4,5,4,5,4,5,d",
+ "period": 1e-07,
+ "start": 0.006,
+ "idle": 9
+ }
+ },
+ {
+ "id": "DUT",
+ "component": "emblink/st7735_tft18"
+ }
+ ],
+ "nets": {
+ "SCK": [
+ "PG.OUT0",
+ "DUT.SCK"
+ ],
+ "SDA": [
+ "PG.OUT1",
+ "DUT.SDA"
+ ],
+ "DC": [
+ "PG.OUT2",
+ "DUT.DC"
+ ],
+ "CS": [
+ "PG.OUT3",
+ "DUT.CS"
+ ]
+ }
+ },
+ "expect": {
+ "output:DUT.probe": {
+ "at": "19ms",
+ "equals": 2016
+ },
+ "output:DUT.on": {
+ "at": "19ms",
+ "equals": true
+ },
+ "output:DUT.ram_writes": {
+ "at": "19ms",
+ "equals": 2
+ }
+ },
+ "inputs": [
+ {
+ "at": "18ms",
+ "target": "DUT",
+ "input": "probe_x",
+ "value": 124
+ },
+ {
+ "at": "18ms",
+ "target": "DUT",
+ "input": "probe_y",
+ "value": 154
+ }
+ ]
+}
diff --git a/catalog/components/st7735_tft18/tests/reset-wait.json b/catalog/components/st7735_tft18/tests/reset-wait.json
new file mode 100644
index 00000000..c376e25f
--- /dev/null
+++ b/catalog/components/st7735_tft18/tests/reset-wait.json
@@ -0,0 +1,54 @@
+{
+ "schema_version": 1,
+ "kind": "component_test",
+ "name": "reset-wait",
+ "description": "SLPOUT and DISPON 1 ms after power-up, inside the reset cancel time (at most 5 ms): taken as a typical part takes them, with a warning; the display is on.",
+ "policy": "functional",
+ "until": "20ms",
+ "circuit": {
+ "components": [
+ {
+ "id": "PG",
+ "component": "emblink/pattern_generator",
+ "params": {
+ "pattern": "0,1,0,1,0,1,2,3,0,1,0,1,0,1,2,3,9,0,1,0,1,2,3,0,1,2,3,0,1,0,1,2,3,9",
+ "period": 1e-07,
+ "start": 0.001,
+ "idle": 9
+ }
+ },
+ {
+ "id": "DUT",
+ "component": "emblink/st7735_tft18"
+ }
+ ],
+ "nets": {
+ "SCK": [
+ "PG.OUT0",
+ "DUT.SCK"
+ ],
+ "SDA": [
+ "PG.OUT1",
+ "DUT.SDA"
+ ],
+ "DC": [
+ "PG.OUT2",
+ "DUT.DC"
+ ],
+ "CS": [
+ "PG.OUT3",
+ "DUT.CS"
+ ]
+ }
+ },
+ "expect": {
+ "output:DUT.sleeping": {
+ "at": "19ms",
+ "equals": false
+ },
+ "output:DUT.on": {
+ "at": "19ms",
+ "equals": true
+ }
+ }
+}
diff --git a/catalog/components/st7735_tft18/tests/set-cursor.json b/catalog/components/st7735_tft18/tests/set-cursor.json
new file mode 100644
index 00000000..61ddd700
--- /dev/null
+++ b/catalog/components/st7735_tft18/tests/set-cursor.json
@@ -0,0 +1,68 @@
+{
+ "schema_version": 1,
+ "kind": "component_test",
+ "name": "set-cursor",
+ "description": "As ST's st7735_SetCursor and st7735_DrawHLine do: after a full-screen window, CASET and RASET with only their start (column 10, row 20), then RAMWR: the start moves, the end stays, and two green pixels land at (10, 20) and (11, 20) of the upright image.",
+ "policy": "functional",
+ "until": "20ms",
+ "circuit": {
+ "components": [
+ {
+ "id": "PG",
+ "component": "emblink/pattern_generator",
+ "params": {
+ "pattern": "0,1,0,1,0,1,2,3,0,1,0,1,0,1,2,3,9,0,1,0,1,2,3,0,1,2,3,0,1,0,1,2,3,9,0,1,0,1,2,3,2,3,2,3,0,1,2,3,0,1,9,4,5,4,5,4,5,4,5,4,5,6,7,4,5,6,7,d,0,1,0,1,2,3,2,3,0,1,2,3,2,3,0,1,9,6,7,6,7,4,5,4,5,4,5,4,5,4,5,4,5,d,0,1,0,1,2,3,0,1,2,3,0,1,2,3,0,1,9,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,6,7,6,7,6,7,6,7,6,7,6,7,6,7,d,0,1,0,1,2,3,0,1,2,3,0,1,2,3,2,3,9,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,6,7,4,5,4,5,6,7,6,7,6,7,6,7,6,7,d,0,1,0,1,2,3,0,1,2,3,0,1,2,3,0,1,9,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,6,7,4,5,6,7,4,5,d,0,1,0,1,2,3,0,1,2,3,0,1,2,3,2,3,9,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,6,7,4,5,6,7,4,5,4,5,d,0,1,0,1,2,3,0,1,2,3,2,3,0,1,0,1,9,4,5,4,5,4,5,4,5,4,5,6,7,6,7,6,7,6,7,6,7,6,7,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,4,5,6,7,6,7,6,7,6,7,6,7,6,7,4,5,4,5,4,5,4,5,4,5,d",
+ "period": 1e-07,
+ "start": 0.006,
+ "idle": 9
+ }
+ },
+ {
+ "id": "DUT",
+ "component": "emblink/st7735_tft18"
+ }
+ ],
+ "nets": {
+ "SCK": [
+ "PG.OUT0",
+ "DUT.SCK"
+ ],
+ "SDA": [
+ "PG.OUT1",
+ "DUT.SDA"
+ ],
+ "DC": [
+ "PG.OUT2",
+ "DUT.DC"
+ ],
+ "CS": [
+ "PG.OUT3",
+ "DUT.CS"
+ ]
+ }
+ },
+ "expect": {
+ "output:DUT.probe": {
+ "at": "19ms",
+ "equals": 2016
+ },
+ "output:DUT.ram_writes": {
+ "at": "19ms",
+ "equals": 2
+ }
+ },
+ "inputs": [
+ {
+ "at": "18ms",
+ "target": "DUT",
+ "input": "probe_x",
+ "value": 11
+ },
+ {
+ "at": "18ms",
+ "target": "DUT",
+ "input": "probe_y",
+ "value": 20
+ }
+ ]
+}
diff --git a/catalog/components/stmps2151str/component.json b/catalog/components/stmps2151str/component.json
new file mode 100644
index 00000000..eb16a6de
--- /dev/null
+++ b/catalog/components/stmps2151str/component.json
@@ -0,0 +1,230 @@
+{
+ "schema_version": 1,
+ "kind": "component",
+ "id": "emblink/stmps2151str",
+ "version": "1.0.0",
+ "name": "STMPS2151STR power distribution switch",
+ "category": "power",
+ "description": "STMicroelectronics STMPS2151STR, a 500 mA P-channel high-side power distribution switch for USB with an active-high enable and an open-drain fault output, in SOT23-5: the NUCLEO-144's (MB1137) U12, which powers CN13's VBUS from +5V when the MCU drives PG6 (USB_PowerSwitchOn) high, for the OTG core's host mode.",
+ "keywords": [
+ "power switch",
+ "load switch",
+ "high-side",
+ "usb",
+ "vbus",
+ "stmps2151",
+ "stmps2151str",
+ "stmicroelectronics",
+ "sot23-5",
+ "nucleo"
+ ],
+ "pins": [
+ {
+ "name": "OUT",
+ "number": 1,
+ "model": {
+ "kind": "analog"
+ },
+ "doc": "Output of the power switch (SOT23-5L pin 1): VBUS"
+ },
+ {
+ "name": "GND",
+ "number": 2,
+ "model": {
+ "kind": "analog"
+ },
+ "doc": "Ground (SOT23-5L pin 2)"
+ },
+ {
+ "name": "FAULT",
+ "number": 3,
+ "model": {
+ "kind": "passive"
+ },
+ "doc": "Open-drain FAULT indicator, active low (SOT23-5L pin 3): never asserted here; its off current a 1 GOhm path to GND (the datasheet's IOFF is at most 1 uA)",
+ "required": false
+ },
+ {
+ "name": "EN",
+ "number": 4,
+ "model": {
+ "kind": "analog"
+ },
+ "doc": "Enable for the power switch, active high on the STMPS2151 (SOT23-5L pin 4): VIH 2.0 V, VIL 0.8 V with 4.5 to 5.5 V on IN"
+ },
+ {
+ "name": "IN",
+ "number": 5,
+ "model": {
+ "kind": "analog"
+ },
+ "doc": "Input, 2.7 to 5.5 V (SOT23-5L pin 5)"
+ }
+ ],
+ "params": {},
+ "model": {
+ "composite": {
+ "components": [
+ {
+ "id": "M",
+ "component": "emblink/pmos",
+ "params": {
+ "part": "generic",
+ "vto": -1.0,
+ "kp": 2.8,
+ "lambda": 0.005
+ }
+ },
+ {
+ "id": "Q",
+ "component": "emblink/nmos",
+ "params": {
+ "part": "generic",
+ "vto": 1.4,
+ "kp": "0.05",
+ "lambda": 0.01
+ }
+ },
+ {
+ "id": "RG",
+ "component": "emblink/resistor",
+ "params": {
+ "resistance": "100k"
+ }
+ },
+ {
+ "id": "RF",
+ "component": "emblink/resistor",
+ "params": {
+ "resistance": "1G"
+ }
+ }
+ ],
+ "nets": {
+ "GATE": [
+ "M.G",
+ "Q.D",
+ "RG.2"
+ ],
+ "IN": [
+ "M.S",
+ "RG.1"
+ ],
+ "GND": [
+ "Q.S",
+ "RF.2"
+ ]
+ },
+ "ports": {
+ "IN": "M.S",
+ "OUT": "M.D",
+ "EN": "Q.G",
+ "GND": "Q.S",
+ "FAULT": "RF.1"
+ }
+ }
+ },
+ "functional": {
+ "composite": {
+ "components": [],
+ "nets": {},
+ "ports": {}
+ }
+ },
+ "symbol": {
+ "svg": "symbol.svg",
+ "width": 80,
+ "height": 60,
+ "pins": {
+ "IN": {
+ "x": 0,
+ "y": 20,
+ "side": "left"
+ },
+ "EN": {
+ "x": 0,
+ "y": 40,
+ "side": "left"
+ },
+ "GND": {
+ "x": 40,
+ "y": 60,
+ "side": "bottom"
+ },
+ "OUT": {
+ "x": 80,
+ "y": 20,
+ "side": "right"
+ },
+ "FAULT": {
+ "x": 80,
+ "y": 40,
+ "side": "right"
+ }
+ }
+ },
+ "fidelity": {
+ "functional": {
+ "claims": "Pins only: the functional policy has no supply voltages.",
+ "omissions": [
+ "switching"
+ ],
+ "validation": [
+ "tests/*.json"
+ ]
+ },
+ "timed": {
+ "claims": "As electrical.",
+ "omissions": []
+ },
+ "electrical": {
+ "claims": "The switch between IN and OUT, on while EN is high and off while EN is low, its threshold at 1.4 V between the datasheet's VIL (0.8 V) and VIH (2.0 V); on, about 90 mOhm with 5 V on IN (the datasheet's 90 mOhm typical, 110 mOhm maximum, SOT23-5L at 25 C); off, high impedance both ways (section 3.4's reverse-current blocking); EN draws no current (at most 0.5 uA). Inside: a P-channel stand-in for the part's N-channel switch and its charge pump, with the same resistance and the same outside.",
+ "omissions": [
+ "the current limit (0.6 to 1.0 A, 0.8 A typical, about 120 % of the rated 500 mA) and the short-circuit current (0.9 A): the switch passes what the load draws",
+ "FAULT: the over-current flag after its 4 to 15 ms blanking, the immediate flag on over-temperature and under-voltage; it never asserts",
+ "the thermal shutdown (145 C, recovery at 120 C) and the on-resistance's rise with temperature (to 135 mOhm at 125 C)",
+ "the under-voltage lock-out (2.0 to 2.5 V, 75 mV hysteresis): the switch's resistance only grows as IN falls",
+ "the output's rise time (0.05 to 2 ms), the turn-on and turn-off times (up to 5 and 10 ms) and the supply current (40 uA on, 6 uA off)",
+ "damage"
+ ],
+ "range": {
+ "voltage_v": [
+ -0.3,
+ 6
+ ],
+ "current_a": [
+ -1,
+ 1
+ ]
+ },
+ "validation": [
+ "tests/*.json"
+ ]
+ }
+ },
+ "provenance": {
+ "author": "EmbSIM",
+ "license": "Apache-2.0",
+ "sources": [
+ {
+ "title": "STMicroelectronics STMPS2141, STMPS2151, STMPS2161, STMPS2171 datasheet, Rev 3 (January 2008): pin connections (figure 2, table 2), functional description (section 3), electrical characteristics (tables 9, 11 to 16)",
+ "url": "https://www.st.com/resource/en/datasheet/stmps2141.pdf",
+ "date": "2026-10-31"
+ },
+ {
+ "title": "MB1137 B-01 electrical schematics (NUCLEO-XXXXZX), USB OTG sheet: U12 STMPS2151STR from +5V to USB_VBUS, EN on PG6 (USB_PowerSwitchOn) with R72's pull-down, FAULT to PG7 (USB_OverCurrent) through JP4 with R71's pull-up and LD7",
+ "url": "https://www.st.com/en/evaluation-tools/nucleo-f446ze.html#cad-resources",
+ "date": "2026-10-31"
+ }
+ ]
+ },
+ "tests": [
+ "tests/*.json"
+ ],
+ "part": {
+ "mpn": "STMPS2151STR",
+ "manufacturer": "STMicroelectronics",
+ "package": "SOT23-5",
+ "datasheet": "https://www.st.com/resource/en/datasheet/stmps2141.pdf"
+ }
+}
diff --git a/catalog/components/stmps2151str/symbol.svg b/catalog/components/stmps2151str/symbol.svg
new file mode 100644
index 00000000..e08df9e9
--- /dev/null
+++ b/catalog/components/stmps2151str/symbol.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/catalog/components/stmps2151str/tests/en-vih.json b/catalog/components/stmps2151str/tests/en-vih.json
new file mode 100644
index 00000000..598122a5
--- /dev/null
+++ b/catalog/components/stmps2151str/tests/en-vih.json
@@ -0,0 +1,68 @@
+{
+ "schema_version": 1,
+ "kind": "component_test",
+ "name": "en-vih",
+ "description": "2.0 V on EN (VIH): on, the 10 ohm load near IN.",
+ "policy": "electrical",
+ "until": "2ms",
+ "circuit": {
+ "components": [
+ {
+ "id": "VS",
+ "component": "emblink/vsource",
+ "params": {
+ "dc": 5.0
+ }
+ },
+ {
+ "id": "VE",
+ "component": "emblink/vsource",
+ "params": {
+ "dc": 2.0
+ }
+ },
+ {
+ "id": "DUT",
+ "component": "emblink/stmps2151str"
+ },
+ {
+ "id": "RL",
+ "component": "emblink/resistor",
+ "params": {
+ "resistance": 10.0
+ }
+ },
+ {
+ "id": "G0",
+ "component": "emblink/gnd"
+ }
+ ],
+ "nets": {
+ "IN": [
+ "VS.+",
+ "DUT.IN"
+ ],
+ "EN": [
+ "VE.+",
+ "DUT.EN"
+ ],
+ "OUT": [
+ "DUT.OUT",
+ "RL.1"
+ ],
+ "GND": [
+ "VS.-",
+ "VE.-",
+ "RL.2",
+ "DUT.GND",
+ "G0.GND"
+ ]
+ }
+ },
+ "expect": {
+ "net:OUT": {
+ "at": "1ms",
+ "min_v": 4.9
+ }
+ }
+}
diff --git a/catalog/components/stmps2151str/tests/en-vil.json b/catalog/components/stmps2151str/tests/en-vil.json
new file mode 100644
index 00000000..fc779154
--- /dev/null
+++ b/catalog/components/stmps2151str/tests/en-vil.json
@@ -0,0 +1,68 @@
+{
+ "schema_version": 1,
+ "kind": "component_test",
+ "name": "en-vil",
+ "description": "0.8 V on EN (the datasheet's VIL with 5 V on IN): off, the 10 ohm load sees nothing.",
+ "policy": "electrical",
+ "until": "2ms",
+ "circuit": {
+ "components": [
+ {
+ "id": "VS",
+ "component": "emblink/vsource",
+ "params": {
+ "dc": 5.0
+ }
+ },
+ {
+ "id": "VE",
+ "component": "emblink/vsource",
+ "params": {
+ "dc": 0.8
+ }
+ },
+ {
+ "id": "DUT",
+ "component": "emblink/stmps2151str"
+ },
+ {
+ "id": "RL",
+ "component": "emblink/resistor",
+ "params": {
+ "resistance": 10.0
+ }
+ },
+ {
+ "id": "G0",
+ "component": "emblink/gnd"
+ }
+ ],
+ "nets": {
+ "IN": [
+ "VS.+",
+ "DUT.IN"
+ ],
+ "EN": [
+ "VE.+",
+ "DUT.EN"
+ ],
+ "OUT": [
+ "DUT.OUT",
+ "RL.1"
+ ],
+ "GND": [
+ "VS.-",
+ "VE.-",
+ "RL.2",
+ "DUT.GND",
+ "G0.GND"
+ ]
+ }
+ },
+ "expect": {
+ "net:OUT": {
+ "at": "1ms",
+ "max_v": 0.01
+ }
+ }
+}
diff --git a/catalog/components/stmps2151str/tests/off.json b/catalog/components/stmps2151str/tests/off.json
new file mode 100644
index 00000000..de7ef5a6
--- /dev/null
+++ b/catalog/components/stmps2151str/tests/off.json
@@ -0,0 +1,68 @@
+{
+ "schema_version": 1,
+ "kind": "component_test",
+ "name": "off",
+ "description": "EN at 0 V: the switch is off and the 10 ohm load sees nothing.",
+ "policy": "electrical",
+ "until": "2ms",
+ "circuit": {
+ "components": [
+ {
+ "id": "VS",
+ "component": "emblink/vsource",
+ "params": {
+ "dc": 5.0
+ }
+ },
+ {
+ "id": "VE",
+ "component": "emblink/vsource",
+ "params": {
+ "dc": 0.0
+ }
+ },
+ {
+ "id": "DUT",
+ "component": "emblink/stmps2151str"
+ },
+ {
+ "id": "RL",
+ "component": "emblink/resistor",
+ "params": {
+ "resistance": 10.0
+ }
+ },
+ {
+ "id": "G0",
+ "component": "emblink/gnd"
+ }
+ ],
+ "nets": {
+ "IN": [
+ "VS.+",
+ "DUT.IN"
+ ],
+ "EN": [
+ "VE.+",
+ "DUT.EN"
+ ],
+ "OUT": [
+ "DUT.OUT",
+ "RL.1"
+ ],
+ "GND": [
+ "VS.-",
+ "VE.-",
+ "RL.2",
+ "DUT.GND",
+ "G0.GND"
+ ]
+ }
+ },
+ "expect": {
+ "net:OUT": {
+ "at": "1ms",
+ "max_v": 0.01
+ }
+ }
+}
diff --git a/catalog/components/stmps2151str/tests/on.json b/catalog/components/stmps2151str/tests/on.json
new file mode 100644
index 00000000..b0cd3dd3
--- /dev/null
+++ b/catalog/components/stmps2151str/tests/on.json
@@ -0,0 +1,69 @@
+{
+ "schema_version": 1,
+ "kind": "component_test",
+ "name": "on",
+ "description": "EN at 3.3 V (an MCU's high), 5 V on IN and a 10 ohm load (0.5 A): OUT about 45 mV below IN (90 mOhm typical, 110 mOhm maximum: at least 4.945 V).",
+ "policy": "electrical",
+ "until": "2ms",
+ "circuit": {
+ "components": [
+ {
+ "id": "VS",
+ "component": "emblink/vsource",
+ "params": {
+ "dc": 5.0
+ }
+ },
+ {
+ "id": "VE",
+ "component": "emblink/vsource",
+ "params": {
+ "dc": 3.3
+ }
+ },
+ {
+ "id": "DUT",
+ "component": "emblink/stmps2151str"
+ },
+ {
+ "id": "RL",
+ "component": "emblink/resistor",
+ "params": {
+ "resistance": 10.0
+ }
+ },
+ {
+ "id": "G0",
+ "component": "emblink/gnd"
+ }
+ ],
+ "nets": {
+ "IN": [
+ "VS.+",
+ "DUT.IN"
+ ],
+ "EN": [
+ "VE.+",
+ "DUT.EN"
+ ],
+ "OUT": [
+ "DUT.OUT",
+ "RL.1"
+ ],
+ "GND": [
+ "VS.-",
+ "VE.-",
+ "RL.2",
+ "DUT.GND",
+ "G0.GND"
+ ]
+ }
+ },
+ "expect": {
+ "net:OUT": {
+ "at": "1ms",
+ "min_v": 4.945,
+ "max_v": 4.99
+ }
+ }
+}
diff --git a/catalog/components/usb_flash_drive/component.json b/catalog/components/usb_flash_drive/component.json
new file mode 100644
index 00000000..1d58bcb0
--- /dev/null
+++ b/catalog/components/usb_flash_drive/component.json
@@ -0,0 +1,209 @@
+{
+ "schema_version": 1,
+ "kind": "component",
+ "id": "emblink/usb_flash_drive",
+ "version": "0.1.0",
+ "name": "USB flash drive (mass storage)",
+ "category": "misc",
+ "description": "A USB flash drive on its cable: a full-speed USB Mass Storage device (Bulk-Only Transport, SCSI transparent command set), bus powered, its medium formatted as one comes from the shop (an MBR with one FAT16 partition, the label EMBSIM). A host (a computer's port, a microcontroller's USB host with FatFs) mounts it, reads and writes its files; its outputs list them as a computer would.",
+ "keywords": [
+ "usb",
+ "flash drive",
+ "usb stick",
+ "mass storage",
+ "msc",
+ "bulk-only",
+ "scsi",
+ "fat16",
+ "host"
+ ],
+ "pins": [
+ {
+ "name": "VBUS",
+ "number": 1,
+ "model": {
+ "kind": "digital_in"
+ },
+ "doc": "VBUS from the host: it attaches while VBUS is above 4.4 V"
+ },
+ {
+ "name": "DM",
+ "number": 2,
+ "model": {
+ "kind": "digital_in"
+ },
+ "aliases": [
+ "D-"
+ ],
+ "doc": "D-: its pull-up (low speed)"
+ },
+ {
+ "name": "DP",
+ "number": 3,
+ "model": {
+ "kind": "digital_in"
+ },
+ "aliases": [
+ "D+"
+ ],
+ "doc": "D+: the host on this net is the one it talks to"
+ },
+ {
+ "name": "GND",
+ "number": 4,
+ "model": {
+ "kind": "power",
+ "role": "ground"
+ },
+ "doc": "Ground"
+ }
+ ],
+ "params": {
+ "capacity_mb": {
+ "type": "number",
+ "unit": "MiB",
+ "default": 8,
+ "min": 2,
+ "max": 64,
+ "doc": "Its capacity (512-byte sectors), held in memory"
+ },
+ "write_protect": {
+ "type": "boolean",
+ "default": false,
+ "doc": "Its write-protect switch: WRITE(10) fails with DATA PROTECT and MODE SENSE reports it"
+ }
+ },
+ "inputs": [
+ {
+ "name": "plugged",
+ "type": "boolean",
+ "default": true,
+ "doc": "Its cable is in"
+ }
+ ],
+ "outputs": [
+ {
+ "name": "state",
+ "type": "string",
+ "doc": "detached, default, address or configured"
+ },
+ {
+ "name": "address",
+ "type": "integer",
+ "doc": "Its USB address"
+ },
+ {
+ "name": "files",
+ "type": "string",
+ "doc": "The root directory's files as a computer lists them: NAME.EXT (size in bytes), directories with a slash"
+ },
+ {
+ "name": "commands",
+ "type": "integer",
+ "doc": "SCSI commands taken"
+ },
+ {
+ "name": "last_command",
+ "type": "string",
+ "doc": "The last SCSI command"
+ },
+ {
+ "name": "sectors_read",
+ "type": "integer",
+ "doc": "Sectors read by the host"
+ },
+ {
+ "name": "sectors_written",
+ "type": "integer",
+ "doc": "Sectors written by the host"
+ },
+ {
+ "name": "capacity_sectors",
+ "type": "integer",
+ "doc": "Its capacity in 512-byte sectors"
+ }
+ ],
+ "model": {
+ "primitive": "usb_flash_drive"
+ },
+ "functional": {
+ "primitive": "usb_flash_drive"
+ },
+ "symbol": {
+ "svg": "symbol.svg",
+ "width": 60,
+ "height": 60,
+ "pins": {
+ "VBUS": {
+ "x": 0,
+ "y": 10,
+ "side": "left"
+ },
+ "DM": {
+ "x": 0,
+ "y": 20,
+ "side": "left"
+ },
+ "DP": {
+ "x": 0,
+ "y": 30,
+ "side": "left"
+ },
+ "GND": {
+ "x": 0,
+ "y": 50,
+ "side": "left"
+ }
+ }
+ },
+ "fidelity": {
+ "functional": {
+ "claims": "A full-speed USB 2.0 device (D+ pulled up) with chapter 9's standard requests, attached while VBUS is above 4.4 V; USB MSC Bulk-Only Transport 1.0: Get Max LUN (one LUN), the class reset, CBWs checked (length, signature, LUN, command length; an invalid one halts both endpoints until reset recovery), the data phase and the CSW with tag, residue and status, a short data phase ended by a short packet or the IN endpoint's STALL, a failed command's STALL and CHECK CONDITION; SCSI's TEST UNIT READY, REQUEST SENSE (fixed format), INQUIRY (removable), MODE SENSE(6) and (10) with write protection, START STOP UNIT, PREVENT ALLOW MEDIUM REMOVAL, READ FORMAT CAPACITIES, READ CAPACITY(10), READ(10), WRITE(10), VERIFY(10), SYNCHRONIZE CACHE(10); ILLEGAL REQUEST for anything else, LBA OUT OF RANGE, DATA PROTECT. The medium: 512-byte sectors, an MBR with one FAT16 partition at sector 2048, as Windows formats a small stick.",
+ "omissions": [
+ "the NAND's page program and block erase times (a real stick NAKs while busy; here data moves at once)",
+ "high speed and UAS",
+ "more than one LUN",
+ "the medium kept between runs or loaded from an image file: each power-up starts it empty",
+ "its activity LED and its supply current"
+ ],
+ "validation": [
+ "tests/*.json"
+ ]
+ },
+ "timed": {
+ "claims": "As functional.",
+ "omissions": []
+ },
+ "electrical": {
+ "claims": "As functional.",
+ "omissions": [],
+ "validation": [
+ "tests/*.json"
+ ]
+ }
+ },
+ "provenance": {
+ "author": "EmbSIM",
+ "license": "Apache-2.0",
+ "sources": [
+ {
+ "title": "USB Mass Storage Class Bulk-Only Transport, revision 1.0 (CBW, CSW, the thirteen cases, reset recovery)",
+ "url": "https://www.usb.org/document-library/mass-storage-bulk-only-10",
+ "date": "2026-10-31"
+ },
+ {
+ "title": "Microsoft FAT32 File System Specification 1.03 (the BPB, FAT16's geometry, the directory entries)",
+ "url": "https://download.microsoft.com/download/1/6/1/161ba512-40e2-4cc9-843a-923143f3456c/fatgen103.doc",
+ "date": "2026-10-31"
+ },
+ {
+ "title": "STMicroelectronics stm32-mw-usb-host's MSC class (usbh_msc_bot.c, usbh_msc_scsi.c): the commands a host sends",
+ "url": "https://github.com/STMicroelectronics/stm32-mw-usb-host",
+ "date": "2026-10-31"
+ }
+ ]
+ },
+ "tests": [
+ "tests/*.json"
+ ]
+}
diff --git a/catalog/components/usb_flash_drive/symbol.svg b/catalog/components/usb_flash_drive/symbol.svg
new file mode 100644
index 00000000..c2baa887
--- /dev/null
+++ b/catalog/components/usb_flash_drive/symbol.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/catalog/components/usb_flash_drive/tests/on-a-computer.json b/catalog/components/usb_flash_drive/tests/on-a-computer.json
new file mode 100644
index 00000000..27c85b5a
--- /dev/null
+++ b/catalog/components/usb_flash_drive/tests/on-a-computer.json
@@ -0,0 +1,65 @@
+{
+ "schema_version": 1,
+ "kind": "component_test",
+ "name": "on-a-computer",
+ "description": "Plugged into a computer's port: enumerated at full speed, configured as a mass storage device (its serial number read); its medium out of the box lists no files.",
+ "policy": "electrical",
+ "until": "500ms",
+ "circuit": {
+ "components": [
+ {
+ "id": "DUT",
+ "component": "emblink/usb_flash_drive"
+ },
+ {
+ "id": "PC",
+ "component": "emblink/usb_host"
+ },
+ {
+ "id": "G0",
+ "component": "emblink/gnd"
+ }
+ ],
+ "nets": {
+ "VBUS": [
+ "PC.VBUS",
+ "DUT.VBUS"
+ ],
+ "DM": [
+ "PC.DM",
+ "DUT.DM"
+ ],
+ "DP": [
+ "PC.DP",
+ "DUT.DP"
+ ],
+ "GND": [
+ "PC.GND",
+ "DUT.GND",
+ "G0.GND"
+ ]
+ }
+ },
+ "expect": {
+ "output:DUT.state": {
+ "at": "390ms",
+ "equals": "configured"
+ },
+ "output:PC.HOST.product": {
+ "at": "390ms",
+ "equals": "USB Flash Drive"
+ },
+ "output:PC.HOST.log": {
+ "at": "390ms",
+ "contains": "new full-speed USB device"
+ },
+ "output:DUT.files": {
+ "at": "390ms",
+ "equals": ""
+ },
+ "output:DUT.capacity_sectors": {
+ "at": "390ms",
+ "equals": 16384
+ }
+ }
+}
diff --git a/catalog/components/usb_mouse/component.json b/catalog/components/usb_mouse/component.json
new file mode 100644
index 00000000..d88aa514
--- /dev/null
+++ b/catalog/components/usb_mouse/component.json
@@ -0,0 +1,204 @@
+{
+ "schema_version": 1,
+ "kind": "component",
+ "id": "emblink/usb_mouse",
+ "version": "0.1.0",
+ "name": "USB mouse (low-speed HID boot mouse)",
+ "category": "inputs",
+ "description": "A wired USB mouse on its cable: a low-speed HID device of the boot mouse protocol (three buttons, X, Y and a wheel), bus powered. Its inputs move it and press its buttons; a host (a computer's port, a microcontroller's USB host) enumerates it and polls its reports.",
+ "keywords": [
+ "usb",
+ "mouse",
+ "hid",
+ "boot protocol",
+ "low speed",
+ "pointer",
+ "host"
+ ],
+ "pins": [
+ {
+ "name": "VBUS",
+ "number": 1,
+ "model": {
+ "kind": "digital_in"
+ },
+ "doc": "VBUS from the host: it attaches while VBUS is above 4.4 V"
+ },
+ {
+ "name": "DM",
+ "number": 2,
+ "model": {
+ "kind": "digital_in"
+ },
+ "aliases": [
+ "D-"
+ ],
+ "doc": "D-: its pull-up (low speed)"
+ },
+ {
+ "name": "DP",
+ "number": 3,
+ "model": {
+ "kind": "digital_in"
+ },
+ "aliases": [
+ "D+"
+ ],
+ "doc": "D+: the host on this net is the one it talks to"
+ },
+ {
+ "name": "GND",
+ "number": 4,
+ "model": {
+ "kind": "power",
+ "role": "ground"
+ },
+ "doc": "Ground"
+ }
+ ],
+ "params": {},
+ "inputs": [
+ {
+ "name": "move_x",
+ "type": "number",
+ "default": 0,
+ "doc": "Counts to move along X (right positive), added to what is still to send"
+ },
+ {
+ "name": "move_y",
+ "type": "number",
+ "default": 0,
+ "doc": "Counts to move along Y (down positive)"
+ },
+ {
+ "name": "wheel",
+ "type": "number",
+ "default": 0,
+ "doc": "Wheel detents (away from the user positive)"
+ },
+ {
+ "name": "buttons",
+ "type": "integer",
+ "default": 0,
+ "doc": "The buttons held: bit 0 left, 1 right, 2 middle"
+ },
+ {
+ "name": "plugged",
+ "type": "boolean",
+ "default": true,
+ "doc": "Its cable is in"
+ }
+ ],
+ "outputs": [
+ {
+ "name": "state",
+ "type": "string",
+ "doc": "detached, default, address or configured"
+ },
+ {
+ "name": "address",
+ "type": "integer",
+ "doc": "Its USB address"
+ },
+ {
+ "name": "reports",
+ "type": "integer",
+ "doc": "Reports sent"
+ },
+ {
+ "name": "sent_x",
+ "type": "integer",
+ "doc": "X counts sent so far"
+ },
+ {
+ "name": "sent_y",
+ "type": "integer",
+ "doc": "Y counts sent so far"
+ },
+ {
+ "name": "protocol",
+ "type": "integer",
+ "doc": "0 boot, 1 report"
+ },
+ {
+ "name": "idle_ms",
+ "type": "integer",
+ "doc": "The idle rate SET_IDLE set (0: reports only on change)"
+ }
+ ],
+ "model": {
+ "primitive": "usb_mouse"
+ },
+ "functional": {
+ "primitive": "usb_mouse"
+ },
+ "symbol": {
+ "svg": "symbol.svg",
+ "width": 60,
+ "height": 60,
+ "pins": {
+ "VBUS": {
+ "x": 0,
+ "y": 10,
+ "side": "left"
+ },
+ "DM": {
+ "x": 0,
+ "y": 20,
+ "side": "left"
+ },
+ "DP": {
+ "x": 0,
+ "y": 30,
+ "side": "left"
+ },
+ "GND": {
+ "x": 0,
+ "y": 50,
+ "side": "left"
+ }
+ }
+ },
+ "fidelity": {
+ "functional": {
+ "claims": "A low-speed USB 2.0 device (D- pulled up) with chapter 9's standard requests (GET_DESCRIPTOR, SET_ADDRESS in effect after its status stage, SET/GET_CONFIGURATION, GET_STATUS, SET/CLEAR_FEATURE for remote wakeup and endpoint halt, GET/SET_INTERFACE), EP0 in 8-byte packets, data toggles per endpoint, attached while VBUS is above 4.4 V; HID 1.11's boot mouse: its HID and report descriptors, GET_REPORT, GET/SET_IDLE (an idle report every duration), GET/SET_PROTOCOL, an interrupt IN endpoint of 4 bytes polled every 10 ms that answers with a report when the buttons or the movement changed (at most 127 counts each way a report), NAK otherwise.",
+ "omissions": [
+ "its supply current from VBUS (a mouse's 20 to 100 mA)",
+ "remote wakeup signalling (a button press on a suspended bus)",
+ "an optical sensor's resolution and acceleration: the movement is the inputs' counts"
+ ]
+ },
+ "timed": {
+ "claims": "As functional.",
+ "omissions": []
+ },
+ "electrical": {
+ "claims": "As functional.",
+ "omissions": [],
+ "validation": [
+ "tests/*.json"
+ ]
+ }
+ },
+ "provenance": {
+ "author": "EmbSIM",
+ "license": "Apache-2.0",
+ "sources": [
+ {
+ "title": "Device Class Definition for Human Interface Devices (HID), Version 1.11 (USB-IF, 2001)",
+ "url": "https://www.usb.org/document-library/device-class-definition-hid-111",
+ "date": "2026-10-31",
+ "notes": "The boot mouse (appendix B.2), its report descriptor (appendix E.10), the class requests (7.2)."
+ },
+ {
+ "title": "Universal Serial Bus Specification, Revision 2.0 (USB-IF, 2000): chapter 9",
+ "url": "https://www.usb.org/document-library/usb-20-specification",
+ "date": "2026-10-31",
+ "notes": "The standard requests and descriptors, low-speed EP0's 8-byte packets."
+ }
+ ]
+ },
+ "tests": [
+ "tests/*.json"
+ ]
+}
diff --git a/catalog/components/usb_mouse/symbol.svg b/catalog/components/usb_mouse/symbol.svg
new file mode 100644
index 00000000..a05314d7
--- /dev/null
+++ b/catalog/components/usb_mouse/symbol.svg
@@ -0,0 +1,8 @@
+
diff --git a/catalog/components/usb_mouse/tests/on-a-computer.json b/catalog/components/usb_mouse/tests/on-a-computer.json
new file mode 100644
index 00000000..42f67d97
--- /dev/null
+++ b/catalog/components/usb_mouse/tests/on-a-computer.json
@@ -0,0 +1,83 @@
+{
+ "schema_version": 1,
+ "kind": "component_test",
+ "name": "on-a-computer",
+ "description": "Plugged into a computer's port: enumerated at low speed (8-byte EP0), configured, its HID interface ready; a move of 300 counts right goes in reports of 127, 127 and 46, and the button's press is seen.",
+ "policy": "electrical",
+ "until": "500ms",
+ "circuit": {
+ "components": [
+ {
+ "id": "DUT",
+ "component": "emblink/usb_mouse"
+ },
+ {
+ "id": "PC",
+ "component": "emblink/usb_host"
+ },
+ {
+ "id": "G0",
+ "component": "emblink/gnd"
+ }
+ ],
+ "nets": {
+ "VBUS": [
+ "PC.VBUS",
+ "DUT.VBUS"
+ ],
+ "DM": [
+ "PC.DM",
+ "DUT.DM"
+ ],
+ "DP": [
+ "PC.DP",
+ "DUT.DP"
+ ],
+ "GND": [
+ "PC.GND",
+ "DUT.GND",
+ "G0.GND"
+ ]
+ }
+ },
+ "inputs": [
+ {
+ "at": "400ms",
+ "target": "DUT",
+ "input": "move_x",
+ "value": 300
+ },
+ {
+ "at": "400ms",
+ "target": "DUT",
+ "input": "buttons",
+ "value": 1
+ }
+ ],
+ "expect": {
+ "output:DUT.state": {
+ "at": "390ms",
+ "equals": "configured"
+ },
+ "output:PC.HOST.log": {
+ "at": "390ms",
+ "contains": "low-speed device attached"
+ },
+ "output:PC.HOST.product": {
+ "at": "390ms",
+ "equals": "USB Optical Mouse"
+ },
+ "output:PC.HOST.cursor_x": {
+ "at": "480ms",
+ "equals": 300
+ },
+ "output:PC.HOST.buttons": {
+ "at": "480ms",
+ "equals": 1
+ },
+ "output:DUT.reports": {
+ "at": "480ms",
+ "equals": 3
+ }
+ }
+}
diff --git a/catalog/mcus/st/stm32f446re/mcu.json b/catalog/mcus/st/stm32f446re/mcu.json
index 7cff62d8..425bd4c1 100644
--- a/catalog/mcus/st/stm32f446re/mcu.json
+++ b/catalog/mcus/st/stm32f446re/mcu.json
@@ -1714,7 +1714,7 @@
},
"fidelity": {
"functional": {
- "claims": "Cortex-M4F instruction execution (Thumb-2 with ARMv7E-M's DSP instructions, FPv4-SP, NVIC with 4 priority bits, SCB, SysTick, DWT, the MPU with eight regions, lazy floating-point context stacking, semihosting); every register of ST's STM32F446 SVD at its address with its reset value and access type, corrected where RM0390 differs (the corrections are listed in registers.json). RCC: HSI 16 MHz, HSE ready only as the board drives OSC_IN (a crystal without HSEBYP, a clock with it), LSE and LSI, each source and PLL ready its DS10693 start-up time after it is enabled (HSI 2.2 us, an HSE crystal 2 ms, an external clock six of its periods, LSI 15 us, LSE 2 s, the PLLs' lock time 75 to 100 us by their VCO), LSE running on through every reset but a power-on one, CIR's ready flags and their RCC interrupt with RDYIE, cleared by RDYC; the main PLL (PLLM, PLLN, PLLP, PLLR from HSI or HSE), SYSCLK from HSI, HSE, PLL_P or PLL_R switched once ready (SWS), HPRE, PPRE1, PPRE2 and the timers' clock with TIMPRE, the PLLI2S and PLLSAI ready bits; what the system clock uses cannot be turned off, HSEBYP is kept while HSE is on, BDCR is written only while PWR's DBP is set; a VCO, SYSCLK, PCLK1 or PCLK2 outside the datasheet's limits and too few flash wait states for HCLK are reported (mcu.clock_out_of_spec, mcu.flash_wait_states). PWR: VOSRDY with the PLL, ODRDY and ODSWRDY as RM0390's over-drive sequence sets them, BRR with BRE. The low-power modes (RM0390 5.3): Sleep (WFI or WFE, SLEEPONEXIT); Stop (SLEEPDEEP, PDDS clear): the 1.2 V domain's clocks stop (the core, SysTick and every peripheral but the RTC and IWDG), HSE and the PLLs turn off, any EXTI line's interrupt or event wakes the core on HSI with the prescalers kept, under-drive's UDRDY with UDEN and LPUDS or MRUDS; Standby (PDDS set): RAM lost, the backup SRAM kept with BRE, the pins let go but the enabled WKUP pins (PA0, PC13: inputs with a pull-down, EWUP's in every mode) and RTC_OUT, no interrupt wakes it; a WKUP pin's rising edge, an RTC alarm, wake-up, tamper or timestamp event, NRST or the IWDG ends it with a reset that is a power reset but for the backup domain, CSR keeping WUF, EWUP and BRE with SBF set; WUF set at entry ends it at once, a WKUP pin enabled while high is a wake-up event; CWUF, CSBF; the option bytes' nRST_STOP and nRST_STDBY making entry a reset (LPWRRSTF). SCR's SEVONPEND. FLASH: an accepted program or erase keeps SR's BSY (and STRT) set for DS10693's typical time (16 us a program; 250 ms to 2 s a sector by its size and PSIZE; 8 to 16 s the mass erase), EOP at its end, CR not written meanwhile, and a program running from the flash stalls on its bus until it ends (no instruction, no interrupt taken); ACR keeps LATENCY, the ART accelerator's and the prefetch bits; KEYR's keys unlock CR (a wrong key a bus error, locked until reset); with PG a store programs the main memory or the OTP area (bits only from 1 to 0) at PSIZE's width, PGPERR, PGAERR across a 128-bit row, PGSERR without PG, WRPERR on a protected sector, a locked OTP block left; SER erases sector SNB, MER the main memory; EOP with EOPIE, OPERR with ERRIE, the FLASH interrupt; OPTKEYR unlocks OPTCR, OPTSTRT writes RDP, USER, nWRP and SPRMOD to the option bytes, loaded into OPTCR at each reset, their write protection acting. GPIO: MODER's input, output, alternate-function and analog modes, OTYPER's push-pull and open-drain, OSPEEDR's speed setting the pin's output resistance and edge rate, PUPDR's pulls in every mode but analog, BSRR (a set winning, halfword lanes), IDR from the pins, LCKR's key sequence freezing the locked pins until reset, AFRL and AFRH selecting among the alternate functions io.alternate lists (from ST's open pin data); an alternate function whose peripheral has no model lets its pin go. SYSCFG: EXTICR's port for each EXTI line, MEMRMP's memory at 0 (flash, system memory, SRAM1), CMPCR's READY. EXTI: lines 0 to 22, edges, SWIER, PR, the shared interrupts and the internal lines' (PVD, RTC alarm, tamper and wake-up, USB wake-ups), EMR's events ending a WFE. RCC's *RSTR bits reset their peripherals. Timers (TIM1-TIM14): up, down and center-aligned counting, the repetition counter, preloads, update events, output compare and PWM modes on the pins, input capture with its edge, prescaler and filter, TI1S, complementary outputs with dead time, MOE, OSSR/OSSI and idle levels, break, LOCK, COM events, the slave modes (reset, gated, trigger, external clock 1, encoder 1-3) on ITR0-3, TI1F_ED, TI1FP1, TI2FP2 or ETRF, external clock mode 2, TRGO for the timers that listen, at the clock RCC gives the timers. TIMx_OR's remaps: TIM5's TI4 on LSI or LSE (HAL's measurement of LSI), TIM11's TI1 on HSE_RTC, TIM2's ITR1 from TIM8's TRGO. SysTick's external reference is HCLK/8 (CLKSOURCE 0). The CRC unit: the CRC-32 (0x04C11DB7, MSB first, from 0xFFFFFFFF) of the words written to DR, CR's RESET, IDR. USART1-3, UART4-5 and USART6: 8 or 9 data bits with parity, 0.5 to 2 stop bits, OVER8, TXE/TC with the held frame, TE's idle frame, breaks (LIN's 13 bits), the receiver from the RX pin's waveform or the session with RXNE, ORE (frames lost), PE, FE, IDLE and LBD and their read-SR-then-DR clearing, mute mode by idle line or address mark, nCTS and nRTS flow control, half-duplex, the synchronous master clock on CK with RX sampled at its edges, every interrupt source. DMA1 and DMA2: eight streams each, CHSEL's channel taking the requests dma_requests maps (RM0390 tables 28 and 29), a request waiting for its stream; peripheral-to-memory, memory-to-peripheral and DMA2's memory-to-memory through the bus as the core's accesses go; direct mode and the FIFO's packing, threshold, peripheral bursts and FS; NDTR with HTIF and TCIF, circular mode, double buffer with CT, transfer errors, the FIFO configuration error; LIFCR/HIFCR and the streams' interrupts. The USARTs' DMAT/DMAR, the SPIs' TXDMAEN/RXDMAEN and the timers' UDE, CCxDE (CCDS), TDE, COMDE and DCR/DMAR burst make their requests. SPI1-4 on their alternate-function pins as masters or slaves: modes 0 to 3, 8 and 16 bits, MSB or LSB first, the baud prescaler, TXE, RXNE, OVR and its clearing, BSY, the receive-only and bidirectional modes with the master clocking on its own, NSS by SSM/SSI, SSOE or the pin, MODF and its write protection, a slave's selection with its bit count kept across NSS, the TI mode's frame pulse and FRE, CRC with CRCNEXT, CRCERR and the CRC frame after a DMA stream's last item, the interrupts. I2C1-3 as masters and slaves on their open-drain pins: START, repeated START (once the bus is free), STOP, 7-bit and 10-bit addressing (ADD10), ACK and POS, the SR1 and SR2 flags with their clearing sequences, BTF clock holding, CCR's standard and fast timing with a slave's clock stretching honoured, arbitration lost (ARLO), BUSY and BERR from the bus as the pins show it; the slave's OAR1 (7 or 10 bits), OAR2's dual address, the general call, the SMBus default, host and alert response addresses, ADDR, BTF and TXE stretching or NOSTRETCH's OVR, STOPF (not after the master's NACK), the master's NACK (AF), a byte received over what the program wrote to DR; PEC with CR1.PEC and PECERR; SMBus's TIMEOUT and SMBALERT; SWRST; the event and error interrupts; DMAEN's requests and LAST's NACK after the DMA's EOT-1, the PEC after a DMA transfer. ADC1-3: the regular and injected groups on SWSTART, JSWSTART, the timers' and EXTI's triggers with their edges or JAUTO; scan, continuous and discontinuous modes; SMPRx's sampling times and RES's 12, 10, 8 or 6 bits at the ADC clock CCR's ADCPRE gives; the pins' voltages (io.analog), VREFINT, the temperature sensor at the world's temperature and VBAT/4; EOC (EOCS), JEOC, STRT, JSTRT, OVR stopping the group, the analog watchdog, JOFRx's offsets and the signed alignments, DMA with DDS, the interrupts; the common CSR, CCR and the dual and triple modes (regular and injected simultaneous, interleaved with DELAY, alternate trigger) with CDR's DMA modes 1-3. The DAC's two channels on PA4 and PA5: the holding registers' views, DOR one APB1 clock after a write or three after a trigger (TIM2, 4-8 TRGO, EXTI9, SWTRIG), the noise and triangle waves, DMA with its underrun, the output buffer's range or 15 kOhm without it. The device signature's UID, flash size and the factory calibration values (TS_CAL1, TS_CAL2, VREFINT_CAL) in system memory. IWDG: the keys, PR and RLR with PVU and RVU, LSI forced on, RLR+1 counts to the reset (IWDGRSTF), the option bytes' hardware watchdog loaded at every reset. WWDG: the free-running counter at PCLK1/4096/2^WDGTB, WDGA, the window, T6's reset (WWDGRSTF), EWI and EWIF. RCC_CSR's reset flags (PORRSTF, PINRSTF, SFTRSTF, IWDGRSTF, WWDGRSTF, LPWRRSTF) kept until RMVF; RAM kept through every reset but a power-on one and Standby's. RTC: the BCD calendar with its leap years and 12/24-hour modes from LSE, LSI or HSE/RTCPRE through PREDIV_A and PREDIV_S, INIT and INITF, WPR's keys and DBP's protection, the shadow registers with RSF, their locking and BYPSHAD, alarms A and B with their masks and subseconds through EXTI17, the wakeup timer through EXTI22, the timestamp and tamper on PC13 through EXTI21 (the tamper erasing the backup registers), RTC_OUT on PC13, the smooth and coarse calibrations, SHIFTR, ADD1H/SUB1H, the twenty backup registers; the backup domain (BDCR, the RTC) keeps going through every reset but a power-on one, and BDRST resets it. The factory option bytes (RDP level 0, the software watchdog). USB OTG FS (the DWC2 core, RM0390 30) in device mode, on its own register block (OTGFS, as RM0390 30.15 lays it out: six endpoints each way) behind AHB2ENR's OTGFSEN and AHB2RSTR's OTGFSRST, with a host on its cable carried as transactions (ADR-031): the 1.25 KiB FIFO RAM with the receive FIFO's status words (GRXSTSR, GRXSTSP; SETUP data, OUT data, the SETUP stage done and OUT transfer done statuses raising STUP and XFRC when taken) and each IN endpoint's FIFO (DIEPTXFn, DTXFSTS, TXFE by TXFELVL, DIEPEMPMSK); SETUPs always taken (STUPCNT, B2BSTUP), EP0's STALL cleared and NAKs set by them; OUT packets NAKed unless the endpoint is enabled with room and packets left, a repeated data toggle dropped, a short packet ending the transfer; IN packets sent once wholly in the FIFO (ITTXFE, XFRC, EPENA cleared); STALL, SNAK/CNAK, SD0PID/SD1PID, EPDIS with EPDISD, DCTL's global NAKs; DCFG's address taking effect after its SET_ADDRESS's status stage; the D+ pull-up from GCCFG's PWRDWN, DCTL's SDIS, the B-session (VBUS on PA9 above 4 V with VBDEN, SEDET when it goes, or GOTGCTL's overrides) and PA12 on AF10; USBRST, ENUMDNE at full speed, SOF and DSTS's frame number, ESUSP, USBSUSP and SUSPSTS after 3 ms of an idle bus, WKUINT; CSRST completing only while the 48 MHz clock runs (PLL_Q, or PLLSAI_P by DCKCFGR2's CK48MSEL), RXFFLSH and TXFFLSH; FDMOD and FHMOD changing CMOD after 25 ms; the OTG_FS interrupt as a level (GINT, GINTMSK, DAINTMSK and the endpoint masks), the NVIC pending it again when its handler returns with it still asserted.",
+ "claims": "Cortex-M4F instruction execution (Thumb-2 with ARMv7E-M's DSP instructions, FPv4-SP, NVIC with 4 priority bits, SCB, SysTick, DWT, the MPU with eight regions, lazy floating-point context stacking, semihosting); every register of ST's STM32F446 SVD at its address with its reset value and access type, corrected where RM0390 differs (the corrections are listed in registers.json). RCC: HSI 16 MHz, HSE ready only as the board drives OSC_IN (a crystal without HSEBYP, a clock with it), LSE and LSI, each source and PLL ready its DS10693 start-up time after it is enabled (HSI 2.2 us, an HSE crystal 2 ms, an external clock six of its periods, LSI 15 us, LSE 2 s, the PLLs' lock time 75 to 100 us by their VCO), LSE running on through every reset but a power-on one, CIR's ready flags and their RCC interrupt with RDYIE, cleared by RDYC; the main PLL (PLLM, PLLN, PLLP, PLLR from HSI or HSE), SYSCLK from HSI, HSE, PLL_P or PLL_R switched once ready (SWS), HPRE, PPRE1, PPRE2 and the timers' clock with TIMPRE, the PLLI2S and PLLSAI ready bits; what the system clock uses cannot be turned off, HSEBYP is kept while HSE is on, BDCR is written only while PWR's DBP is set; a VCO, SYSCLK, PCLK1 or PCLK2 outside the datasheet's limits and too few flash wait states for HCLK are reported (mcu.clock_out_of_spec, mcu.flash_wait_states). PWR: VOSRDY with the PLL, ODRDY and ODSWRDY as RM0390's over-drive sequence sets them, BRR with BRE. The low-power modes (RM0390 5.3): Sleep (WFI or WFE, SLEEPONEXIT); Stop (SLEEPDEEP, PDDS clear): the 1.2 V domain's clocks stop (the core, SysTick and every peripheral but the RTC and IWDG), HSE and the PLLs turn off, any EXTI line's interrupt or event wakes the core on HSI with the prescalers kept, under-drive's UDRDY with UDEN and LPUDS or MRUDS; Standby (PDDS set): RAM lost, the backup SRAM kept with BRE, the pins let go but the enabled WKUP pins (PA0, PC13: inputs with a pull-down, EWUP's in every mode) and RTC_OUT, no interrupt wakes it; a WKUP pin's rising edge, an RTC alarm, wake-up, tamper or timestamp event, NRST or the IWDG ends it with a reset that is a power reset but for the backup domain, CSR keeping WUF, EWUP and BRE with SBF set; WUF set at entry ends it at once, a WKUP pin enabled while high is a wake-up event; CWUF, CSBF; the option bytes' nRST_STOP and nRST_STDBY making entry a reset (LPWRRSTF). SCR's SEVONPEND. FLASH: an accepted program or erase keeps SR's BSY (and STRT) set for DS10693's typical time (16 us a program; 250 ms to 2 s a sector by its size and PSIZE; 8 to 16 s the mass erase), EOP at its end, CR not written meanwhile, and a program running from the flash stalls on its bus until it ends (no instruction, no interrupt taken); ACR keeps LATENCY, the ART accelerator's and the prefetch bits; KEYR's keys unlock CR (a wrong key a bus error, locked until reset); with PG a store programs the main memory or the OTP area (bits only from 1 to 0) at PSIZE's width, PGPERR, PGAERR across a 128-bit row, PGSERR without PG, WRPERR on a protected sector, a locked OTP block left; SER erases sector SNB, MER the main memory; EOP with EOPIE, OPERR with ERRIE, the FLASH interrupt; OPTKEYR unlocks OPTCR, OPTSTRT writes RDP, USER, nWRP and SPRMOD to the option bytes, loaded into OPTCR at each reset, their write protection acting. GPIO: MODER's input, output, alternate-function and analog modes, OTYPER's push-pull and open-drain, OSPEEDR's speed setting the pin's output resistance and edge rate, PUPDR's pulls in every mode but analog, BSRR (a set winning, halfword lanes), IDR from the pins, LCKR's key sequence freezing the locked pins until reset, AFRL and AFRH selecting among the alternate functions io.alternate lists (from ST's open pin data); an alternate function whose peripheral has no model lets its pin go. SYSCFG: EXTICR's port for each EXTI line, MEMRMP's memory at 0 (flash, system memory, SRAM1), CMPCR's READY. EXTI: lines 0 to 22, edges, SWIER, PR, the shared interrupts and the internal lines' (PVD, RTC alarm, tamper and wake-up, USB wake-ups), EMR's events ending a WFE. RCC's *RSTR bits reset their peripherals. Timers (TIM1-TIM14): up, down and center-aligned counting, the repetition counter, preloads, update events, output compare and PWM modes on the pins, input capture with its edge, prescaler and filter, TI1S, complementary outputs with dead time, MOE, OSSR/OSSI and idle levels, break, LOCK, COM events, the slave modes (reset, gated, trigger, external clock 1, encoder 1-3) on ITR0-3, TI1F_ED, TI1FP1, TI2FP2 or ETRF, external clock mode 2, TRGO for the timers that listen, at the clock RCC gives the timers. TIMx_OR's remaps: TIM5's TI4 on LSI or LSE (HAL's measurement of LSI), TIM11's TI1 on HSE_RTC, TIM2's ITR1 from TIM8's TRGO. SysTick's external reference is HCLK/8 (CLKSOURCE 0). The CRC unit: the CRC-32 (0x04C11DB7, MSB first, from 0xFFFFFFFF) of the words written to DR, CR's RESET, IDR. USART1-3, UART4-5 and USART6: 8 or 9 data bits with parity, 0.5 to 2 stop bits, OVER8, TXE/TC with the held frame, TE's idle frame, breaks (LIN's 13 bits), the receiver from the RX pin's waveform or the session with RXNE, ORE (frames lost), PE, FE, IDLE and LBD and their read-SR-then-DR clearing, mute mode by idle line or address mark, nCTS and nRTS flow control, half-duplex, the synchronous master clock on CK with RX sampled at its edges, every interrupt source. DMA1 and DMA2: eight streams each, CHSEL's channel taking the requests dma_requests maps (RM0390 tables 28 and 29), a request waiting for its stream; peripheral-to-memory, memory-to-peripheral and DMA2's memory-to-memory through the bus as the core's accesses go; direct mode and the FIFO's packing, threshold, peripheral bursts and FS; NDTR with HTIF and TCIF, circular mode, double buffer with CT, transfer errors, the FIFO configuration error; LIFCR/HIFCR and the streams' interrupts. The USARTs' DMAT/DMAR, the SPIs' TXDMAEN/RXDMAEN and the timers' UDE, CCxDE (CCDS), TDE, COMDE and DCR/DMAR burst make their requests. SPI1-4 on their alternate-function pins as masters or slaves: modes 0 to 3, 8 and 16 bits, MSB or LSB first, the baud prescaler, TXE, RXNE, OVR and its clearing, BSY, the receive-only and bidirectional modes with the master clocking on its own, NSS by SSM/SSI, SSOE or the pin, MODF and its write protection, a slave's selection with its bit count kept across NSS, the TI mode's frame pulse and FRE, CRC with CRCNEXT, CRCERR and the CRC frame after a DMA stream's last item, the interrupts. I2C1-3 as masters and slaves on their open-drain pins: START, repeated START (once the bus is free), STOP, 7-bit and 10-bit addressing (ADD10), ACK and POS, the SR1 and SR2 flags with their clearing sequences, BTF clock holding, CCR's standard and fast timing with a slave's clock stretching honoured, arbitration lost (ARLO), BUSY and BERR from the bus as the pins show it; the slave's OAR1 (7 or 10 bits), OAR2's dual address, the general call, the SMBus default, host and alert response addresses, ADDR, BTF and TXE stretching or NOSTRETCH's OVR, STOPF (not after the master's NACK), the master's NACK (AF), a byte received over what the program wrote to DR; PEC with CR1.PEC and PECERR; SMBus's TIMEOUT and SMBALERT; SWRST; the event and error interrupts; DMAEN's requests and LAST's NACK after the DMA's EOT-1, the PEC after a DMA transfer. ADC1-3: the regular and injected groups on SWSTART, JSWSTART, the timers' and EXTI's triggers with their edges or JAUTO; scan, continuous and discontinuous modes; SMPRx's sampling times and RES's 12, 10, 8 or 6 bits at the ADC clock CCR's ADCPRE gives; the pins' voltages (io.analog), VREFINT, the temperature sensor at the world's temperature and VBAT/4; EOC (EOCS), JEOC, STRT, JSTRT, OVR stopping the group, the analog watchdog, JOFRx's offsets and the signed alignments, DMA with DDS, the interrupts; the common CSR, CCR and the dual and triple modes (regular and injected simultaneous, interleaved with DELAY, alternate trigger) with CDR's DMA modes 1-3. The DAC's two channels on PA4 and PA5: the holding registers' views, DOR one APB1 clock after a write or three after a trigger (TIM2, 4-8 TRGO, EXTI9, SWTRIG), the noise and triangle waves, DMA with its underrun, the output buffer's range or 15 kOhm without it. The device signature's UID, flash size and the factory calibration values (TS_CAL1, TS_CAL2, VREFINT_CAL) in system memory. IWDG: the keys, PR and RLR with PVU and RVU, LSI forced on, RLR+1 counts to the reset (IWDGRSTF), the option bytes' hardware watchdog loaded at every reset. WWDG: the free-running counter at PCLK1/4096/2^WDGTB, WDGA, the window, T6's reset (WWDGRSTF), EWI and EWIF. RCC_CSR's reset flags (PORRSTF, PINRSTF, SFTRSTF, IWDGRSTF, WWDGRSTF, LPWRRSTF) kept until RMVF; RAM kept through every reset but a power-on one and Standby's. RTC: the BCD calendar with its leap years and 12/24-hour modes from LSE, LSI or HSE/RTCPRE through PREDIV_A and PREDIV_S, INIT and INITF, WPR's keys and DBP's protection, the shadow registers with RSF, their locking and BYPSHAD, alarms A and B with their masks and subseconds through EXTI17, the wakeup timer through EXTI22, the timestamp and tamper on PC13 through EXTI21 (the tamper erasing the backup registers), RTC_OUT on PC13, the smooth and coarse calibrations, SHIFTR, ADD1H/SUB1H, the twenty backup registers; the backup domain (BDCR, the RTC) keeps going through every reset but a power-on one, and BDRST resets it. The factory option bytes (RDP level 0, the software watchdog). USB OTG FS (the DWC2 core, RM0390 30) in device and host mode, on its own register block (OTGFS, as RM0390 30.15 lays it out: six endpoints each way) behind AHB2ENR's OTGFSEN and AHB2RSTR's OTGFSRST, with a host on its cable carried as transactions (ADR-031): the 1.25 KiB FIFO RAM with the receive FIFO's status words (GRXSTSR, GRXSTSP; SETUP data, OUT data, the SETUP stage done and OUT transfer done statuses raising STUP and XFRC when taken) and each IN endpoint's FIFO (DIEPTXFn, DTXFSTS, TXFE by TXFELVL, DIEPEMPMSK); SETUPs always taken (STUPCNT, B2BSTUP), EP0's STALL cleared and NAKs set by them; OUT packets NAKed unless the endpoint is enabled with room and packets left, a repeated data toggle dropped, a short packet ending the transfer; IN packets sent once wholly in the FIFO (ITTXFE, XFRC, EPENA cleared); STALL, SNAK/CNAK, SD0PID/SD1PID, EPDIS with EPDISD, DCTL's global NAKs; DCFG's address taking effect after its SET_ADDRESS's status stage; the D+ pull-up from GCCFG's PWRDWN, DCTL's SDIS, the B-session (VBUS on PA9 above 4 V with VBDEN, SEDET when it goes, or GOTGCTL's overrides) and PA12 on AF10; USBRST, ENUMDNE at full speed, SOF and DSTS's frame number, ESUSP, USBSUSP and SUSPSTS after 3 ms of an idle bus, WKUINT; CSRST completing only while the 48 MHz clock runs (PLL_Q, or PLLSAI_P by DCKCFGR2's CK48MSEL), RXFFLSH and TXFFLSH; FDMOD and FHMOD changing CMOD after 25 ms; the OTG_FS interrupt as a level (GINT, GINTMSK, DAINTMSK and the endpoint masks), the NVIC pending it again when its handler returns with it still asserted. As a host (FHMOD): the port (PPWR; the device's pull-up a connection, PCSTS, PCDET, its end DISCINT; PRST's reset for as long as it is set, its end enabling the port at the device's speed, PSPD full or low; PENA written 1 disabling it; PSUSP and PRES; PLSTS), an SOF or low-speed keep-alive every frame of HFIR at HCFG's PHY clock (HFNUM's number and time left), and twelve channels: SETUP, OUT and IN transactions at the bus's bit time with their data toggles, OUT once its packet is in the non-periodic or periodic transmit FIFO (HNPTXSTS, HPTXSTS), one IN token per CHENA with room in the receive FIFO, ACK, NAK, STALL, the time-out (TXERR), babble (BBERR) and toggle errors (DTERR), the halt by CHDIS with CHENA (CHH), the IN data and transfer-done statuses (XFRC when taken), interrupt channels once a frame in the frames ODDFRM asks; HAINT, HCINT, HPRTINT, NPTXFE and PTXFE.",
"omissions": [
"watchdogs: LSI's spread (32 kHz here), the debug freeze bits (a debugger's halt stops all of the machine's time); the WWDG's counter while WDGA is clear counts from the last access made while its clock is on",
"RTC: the reference clock detection (REFCKON, RTC_REFIN), RTC_AF2 (PI8, not bonded), the tamper input's precharge and pull-up, the 8 and 16 s calibration windows (the rate is applied evenly), RECALPF's wait",
@@ -1731,7 +1731,7 @@
"RCC: the clock security system's detection (CSSON is kept; the board's HSE never fails), a peripheral held in reset while its *RSTR bit stays set (its reset happens on the write), the brownout reset (BORRSTF only at power-on), the start-up times' spread between parts and with temperature and LSEMOD's drive level (the typical times always), MCO1 and MCO2 on their pins, and the I2S, SAI, SPDIF-RX, CEC and FMPI2C clocks from PLLI2S and PLLSAI (DCKCFGR, DCKCFGR2) beyond TIMPRE and the 48 MHz clock's CK48MSEL (PLL_Q or PLLSAI_P, which the USB OTG FS core needs)",
"PWR: the regulator's settling times and the low-power modes' wake-up times (DS10693's low-power mode wake-up timings: the core runs at the wake-up event), the regulator's settings (LPDS, FPDS, LPLVDS, MRLVDS: the supply current is not modelled), the programmable voltage detector, DBGMCU_CR's DBG_STOP and DBG_STANDBY (a debugger's halt stops all of the machine's time), the tamper and timestamp pin in Standby beyond RTC_OUT; DWT's CYCCNT counts through Stop",
"CAN, SDIO, QUADSPI, SAI, SPDIF-RX, HDMI-CEC, DCMI, FMC, FMPI2C and the USB OTG HS controller",
- "USB OTG FS: host mode (its channel registers are storage), the ID pin, SRP and HNP, isochronous endpoints' frame parity and incomplete-transfer interrupts, LPM, GCCFG's battery-charging detection, remote wakeup signalling (RWUSIG), the OTG_FS_WKUP interrupt through EXTI line 18, the SOF pulse on PA8; the device's turnaround inside a transaction; GSNPSID reads 0x4F54320A (core 3.20a), the value TinyUSB's dwc2_info table measures on the F412's and F76x's OTG_FS, the same generation of the core (ST's HAL groups the F446 with the F412, F413 and F469); no ST document gives it and it was not read from an F446",
+ "USB OTG FS: the ID pin, SRP and HNP, split transactions through a hub, isochronous endpoints' frame parity and incomplete-transfer interrupts and a host's isochronous channels, FRMOR, LPM, GCCFG's battery-charging detection, the OTG_FS_WKUP interrupt through EXTI line 18, the SOF pulse on PA8; the device's turnaround inside a transaction (a transaction is whole), a host channel's OUT data kept per channel rather than in the shared FIFO's order (the words free are the same); GSNPSID reads 0x4F54320A (core 3.20a), the value TinyUSB's dwc2_info table measures on the F412's and F76x's OTG_FS, the same generation of the core (ST's HAL groups the F446 with the F412, F413 and F469); no ST document gives it and it was not read from an F446",
"flash: the times' spread and their maximum values (the typical ones always), the ART accelerator's cache hits during an operation (a program in the flash stalls whatever its cache holds), a program in SRAM whose interrupt handler is in the flash (it does not stall), the option bytes' programming time (OPTSTRT is done at once), a reset during an operation leaving the memory half-programmed, HCLK's 1 MHz minimum, the read protection's levels (RDP stored, no mass erase on a regression), PCROP's read refusal (RDERR), the ART accelerator's caches (DCRST, ICRST)",
"CRC: the bus stall of a DR access while a word is computed (four AHB clocks), and a byte or halfword write to DR, which RM0390 does not allow, feeding its value zero-extended"
],
diff --git a/catalog/mcus/st/stm32f446ze/mcu.json b/catalog/mcus/st/stm32f446ze/mcu.json
index c970539e..5f928890 100644
--- a/catalog/mcus/st/stm32f446ze/mcu.json
+++ b/catalog/mcus/st/stm32f446ze/mcu.json
@@ -2766,7 +2766,7 @@
},
"fidelity": {
"functional": {
- "claims": "Cortex-M4F instruction execution (Thumb-2 with ARMv7E-M's DSP instructions, FPv4-SP, NVIC with 4 priority bits, SCB, SysTick, DWT, the MPU with eight regions, lazy floating-point context stacking, semihosting); every register of ST's STM32F446 SVD at its address with its reset value and access type, corrected where RM0390 differs (the corrections are listed in registers.json). RCC: HSI 16 MHz, HSE ready only as the board drives OSC_IN (a crystal without HSEBYP, a clock with it), LSE and LSI, each source and PLL ready its DS10693 start-up time after it is enabled (HSI 2.2 us, an HSE crystal 2 ms, an external clock six of its periods, LSI 15 us, LSE 2 s, the PLLs' lock time 75 to 100 us by their VCO), LSE running on through every reset but a power-on one, CIR's ready flags and their RCC interrupt with RDYIE, cleared by RDYC; the main PLL (PLLM, PLLN, PLLP, PLLR from HSI or HSE), SYSCLK from HSI, HSE, PLL_P or PLL_R switched once ready (SWS), HPRE, PPRE1, PPRE2 and the timers' clock with TIMPRE, the PLLI2S and PLLSAI ready bits; what the system clock uses cannot be turned off, HSEBYP is kept while HSE is on, BDCR is written only while PWR's DBP is set; a VCO, SYSCLK, PCLK1 or PCLK2 outside the datasheet's limits and too few flash wait states for HCLK are reported (mcu.clock_out_of_spec, mcu.flash_wait_states). PWR: VOSRDY with the PLL, ODRDY and ODSWRDY as RM0390's over-drive sequence sets them, BRR with BRE. The low-power modes (RM0390 5.3): Sleep (WFI or WFE, SLEEPONEXIT); Stop (SLEEPDEEP, PDDS clear): the 1.2 V domain's clocks stop (the core, SysTick and every peripheral but the RTC and IWDG), HSE and the PLLs turn off, any EXTI line's interrupt or event wakes the core on HSI with the prescalers kept, under-drive's UDRDY with UDEN and LPUDS or MRUDS; Standby (PDDS set): RAM lost, the backup SRAM kept with BRE, the pins let go but the enabled WKUP pins (PA0, PC13: inputs with a pull-down, EWUP's in every mode) and RTC_OUT, no interrupt wakes it; a WKUP pin's rising edge, an RTC alarm, wake-up, tamper or timestamp event, NRST or the IWDG ends it with a reset that is a power reset but for the backup domain, CSR keeping WUF, EWUP and BRE with SBF set; WUF set at entry ends it at once, a WKUP pin enabled while high is a wake-up event; CWUF, CSBF; the option bytes' nRST_STOP and nRST_STDBY making entry a reset (LPWRRSTF). SCR's SEVONPEND. FLASH: an accepted program or erase keeps SR's BSY (and STRT) set for DS10693's typical time (16 us a program; 250 ms to 2 s a sector by its size and PSIZE; 8 to 16 s the mass erase), EOP at its end, CR not written meanwhile, and a program running from the flash stalls on its bus until it ends (no instruction, no interrupt taken); ACR keeps LATENCY, the ART accelerator's and the prefetch bits; KEYR's keys unlock CR (a wrong key a bus error, locked until reset); with PG a store programs the main memory or the OTP area (bits only from 1 to 0) at PSIZE's width, PGPERR, PGAERR across a 128-bit row, PGSERR without PG, WRPERR on a protected sector, a locked OTP block left; SER erases sector SNB, MER the main memory; EOP with EOPIE, OPERR with ERRIE, the FLASH interrupt; OPTKEYR unlocks OPTCR, OPTSTRT writes RDP, USER, nWRP and SPRMOD to the option bytes, loaded into OPTCR at each reset, their write protection acting. GPIO: MODER's input, output, alternate-function and analog modes, OTYPER's push-pull and open-drain, OSPEEDR's speed setting the pin's output resistance and edge rate, PUPDR's pulls in every mode but analog, BSRR (a set winning, halfword lanes), IDR from the pins, LCKR's key sequence freezing the locked pins until reset, AFRL and AFRH selecting among the alternate functions io.alternate lists (from ST's open pin data); an alternate function whose peripheral has no model lets its pin go. SYSCFG: EXTICR's port for each EXTI line, MEMRMP's memory at 0 (flash, system memory, SRAM1), CMPCR's READY. EXTI: lines 0 to 22, edges, SWIER, PR, the shared interrupts and the internal lines' (PVD, RTC alarm, tamper and wake-up, USB wake-ups), EMR's events ending a WFE. RCC's *RSTR bits reset their peripherals. Timers (TIM1-TIM14): up, down and center-aligned counting, the repetition counter, preloads, update events, output compare and PWM modes on the pins, input capture with its edge, prescaler and filter, TI1S, complementary outputs with dead time, MOE, OSSR/OSSI and idle levels, break, LOCK, COM events, the slave modes (reset, gated, trigger, external clock 1, encoder 1-3) on ITR0-3, TI1F_ED, TI1FP1, TI2FP2 or ETRF, external clock mode 2, TRGO for the timers that listen, at the clock RCC gives the timers. TIMx_OR's remaps: TIM5's TI4 on LSI or LSE (HAL's measurement of LSI), TIM11's TI1 on HSE_RTC, TIM2's ITR1 from TIM8's TRGO. SysTick's external reference is HCLK/8 (CLKSOURCE 0). The CRC unit: the CRC-32 (0x04C11DB7, MSB first, from 0xFFFFFFFF) of the words written to DR, CR's RESET, IDR. USART1-3, UART4-5 and USART6: 8 or 9 data bits with parity, 0.5 to 2 stop bits, OVER8, TXE/TC with the held frame, TE's idle frame, breaks (LIN's 13 bits), the receiver from the RX pin's waveform or the session with RXNE, ORE (frames lost), PE, FE, IDLE and LBD and their read-SR-then-DR clearing, mute mode by idle line or address mark, nCTS and nRTS flow control, half-duplex, the synchronous master clock on CK with RX sampled at its edges, every interrupt source. DMA1 and DMA2: eight streams each, CHSEL's channel taking the requests dma_requests maps (RM0390 tables 28 and 29), a request waiting for its stream; peripheral-to-memory, memory-to-peripheral and DMA2's memory-to-memory through the bus as the core's accesses go; direct mode and the FIFO's packing, threshold, peripheral bursts and FS; NDTR with HTIF and TCIF, circular mode, double buffer with CT, transfer errors, the FIFO configuration error; LIFCR/HIFCR and the streams' interrupts. The USARTs' DMAT/DMAR, the SPIs' TXDMAEN/RXDMAEN and the timers' UDE, CCxDE (CCDS), TDE, COMDE and DCR/DMAR burst make their requests. SPI1-4 on their alternate-function pins as masters or slaves: modes 0 to 3, 8 and 16 bits, MSB or LSB first, the baud prescaler, TXE, RXNE, OVR and its clearing, BSY, the receive-only and bidirectional modes with the master clocking on its own, NSS by SSM/SSI, SSOE or the pin, MODF and its write protection, a slave's selection with its bit count kept across NSS, the TI mode's frame pulse and FRE, CRC with CRCNEXT, CRCERR and the CRC frame after a DMA stream's last item, the interrupts. I2C1-3 as masters and slaves on their open-drain pins: START, repeated START (once the bus is free), STOP, 7-bit and 10-bit addressing (ADD10), ACK and POS, the SR1 and SR2 flags with their clearing sequences, BTF clock holding, CCR's standard and fast timing with a slave's clock stretching honoured, arbitration lost (ARLO), BUSY and BERR from the bus as the pins show it; the slave's OAR1 (7 or 10 bits), OAR2's dual address, the general call, the SMBus default, host and alert response addresses, ADDR, BTF and TXE stretching or NOSTRETCH's OVR, STOPF (not after the master's NACK), the master's NACK (AF), a byte received over what the program wrote to DR; PEC with CR1.PEC and PECERR; SMBus's TIMEOUT and SMBALERT; SWRST; the event and error interrupts; DMAEN's requests and LAST's NACK after the DMA's EOT-1, the PEC after a DMA transfer. ADC1-3: the regular and injected groups on SWSTART, JSWSTART, the timers' and EXTI's triggers with their edges or JAUTO; scan, continuous and discontinuous modes; SMPRx's sampling times and RES's 12, 10, 8 or 6 bits at the ADC clock CCR's ADCPRE gives; the pins' voltages (io.analog), VREFINT, the temperature sensor at the world's temperature and VBAT/4; EOC (EOCS), JEOC, STRT, JSTRT, OVR stopping the group, the analog watchdog, JOFRx's offsets and the signed alignments, DMA with DDS, the interrupts; the common CSR, CCR and the dual and triple modes (regular and injected simultaneous, interleaved with DELAY, alternate trigger) with CDR's DMA modes 1-3. The DAC's two channels on PA4 and PA5: the holding registers' views, DOR one APB1 clock after a write or three after a trigger (TIM2, 4-8 TRGO, EXTI9, SWTRIG), the noise and triangle waves, DMA with its underrun, the output buffer's range or 15 kOhm without it. The device signature's UID, flash size and the factory calibration values (TS_CAL1, TS_CAL2, VREFINT_CAL) in system memory. IWDG: the keys, PR and RLR with PVU and RVU, LSI forced on, RLR+1 counts to the reset (IWDGRSTF), the option bytes' hardware watchdog loaded at every reset. WWDG: the free-running counter at PCLK1/4096/2^WDGTB, WDGA, the window, T6's reset (WWDGRSTF), EWI and EWIF. RCC_CSR's reset flags (PORRSTF, PINRSTF, SFTRSTF, IWDGRSTF, WWDGRSTF, LPWRRSTF) kept until RMVF; RAM kept through every reset but a power-on one and Standby's. RTC: the BCD calendar with its leap years and 12/24-hour modes from LSE, LSI or HSE/RTCPRE through PREDIV_A and PREDIV_S, INIT and INITF, WPR's keys and DBP's protection, the shadow registers with RSF, their locking and BYPSHAD, alarms A and B with their masks and subseconds through EXTI17, the wakeup timer through EXTI22, the timestamp and tamper on PC13 through EXTI21 (the tamper erasing the backup registers), RTC_OUT on PC13, the smooth and coarse calibrations, SHIFTR, ADD1H/SUB1H, the twenty backup registers; the backup domain (BDCR, the RTC) keeps going through every reset but a power-on one, and BDRST resets it. The factory option bytes (RDP level 0, the software watchdog). USB OTG FS (the DWC2 core, RM0390 30) in device mode, on its own register block (OTGFS, as RM0390 30.15 lays it out: six endpoints each way) behind AHB2ENR's OTGFSEN and AHB2RSTR's OTGFSRST, with a host on its cable carried as transactions (ADR-031): the 1.25 KiB FIFO RAM with the receive FIFO's status words (GRXSTSR, GRXSTSP; SETUP data, OUT data, the SETUP stage done and OUT transfer done statuses raising STUP and XFRC when taken) and each IN endpoint's FIFO (DIEPTXFn, DTXFSTS, TXFE by TXFELVL, DIEPEMPMSK); SETUPs always taken (STUPCNT, B2BSTUP), EP0's STALL cleared and NAKs set by them; OUT packets NAKed unless the endpoint is enabled with room and packets left, a repeated data toggle dropped, a short packet ending the transfer; IN packets sent once wholly in the FIFO (ITTXFE, XFRC, EPENA cleared); STALL, SNAK/CNAK, SD0PID/SD1PID, EPDIS with EPDISD, DCTL's global NAKs; DCFG's address taking effect after its SET_ADDRESS's status stage; the D+ pull-up from GCCFG's PWRDWN, DCTL's SDIS, the B-session (VBUS on PA9 above 4 V with VBDEN, SEDET when it goes, or GOTGCTL's overrides) and PA12 on AF10; USBRST, ENUMDNE at full speed, SOF and DSTS's frame number, ESUSP, USBSUSP and SUSPSTS after 3 ms of an idle bus, WKUINT; CSRST completing only while the 48 MHz clock runs (PLL_Q, or PLLSAI_P by DCKCFGR2's CK48MSEL), RXFFLSH and TXFFLSH; FDMOD and FHMOD changing CMOD after 25 ms; the OTG_FS interrupt as a level (GINT, GINTMSK, DAINTMSK and the endpoint masks), the NVIC pending it again when its handler returns with it still asserted.",
+ "claims": "Cortex-M4F instruction execution (Thumb-2 with ARMv7E-M's DSP instructions, FPv4-SP, NVIC with 4 priority bits, SCB, SysTick, DWT, the MPU with eight regions, lazy floating-point context stacking, semihosting); every register of ST's STM32F446 SVD at its address with its reset value and access type, corrected where RM0390 differs (the corrections are listed in registers.json). RCC: HSI 16 MHz, HSE ready only as the board drives OSC_IN (a crystal without HSEBYP, a clock with it), LSE and LSI, each source and PLL ready its DS10693 start-up time after it is enabled (HSI 2.2 us, an HSE crystal 2 ms, an external clock six of its periods, LSI 15 us, LSE 2 s, the PLLs' lock time 75 to 100 us by their VCO), LSE running on through every reset but a power-on one, CIR's ready flags and their RCC interrupt with RDYIE, cleared by RDYC; the main PLL (PLLM, PLLN, PLLP, PLLR from HSI or HSE), SYSCLK from HSI, HSE, PLL_P or PLL_R switched once ready (SWS), HPRE, PPRE1, PPRE2 and the timers' clock with TIMPRE, the PLLI2S and PLLSAI ready bits; what the system clock uses cannot be turned off, HSEBYP is kept while HSE is on, BDCR is written only while PWR's DBP is set; a VCO, SYSCLK, PCLK1 or PCLK2 outside the datasheet's limits and too few flash wait states for HCLK are reported (mcu.clock_out_of_spec, mcu.flash_wait_states). PWR: VOSRDY with the PLL, ODRDY and ODSWRDY as RM0390's over-drive sequence sets them, BRR with BRE. The low-power modes (RM0390 5.3): Sleep (WFI or WFE, SLEEPONEXIT); Stop (SLEEPDEEP, PDDS clear): the 1.2 V domain's clocks stop (the core, SysTick and every peripheral but the RTC and IWDG), HSE and the PLLs turn off, any EXTI line's interrupt or event wakes the core on HSI with the prescalers kept, under-drive's UDRDY with UDEN and LPUDS or MRUDS; Standby (PDDS set): RAM lost, the backup SRAM kept with BRE, the pins let go but the enabled WKUP pins (PA0, PC13: inputs with a pull-down, EWUP's in every mode) and RTC_OUT, no interrupt wakes it; a WKUP pin's rising edge, an RTC alarm, wake-up, tamper or timestamp event, NRST or the IWDG ends it with a reset that is a power reset but for the backup domain, CSR keeping WUF, EWUP and BRE with SBF set; WUF set at entry ends it at once, a WKUP pin enabled while high is a wake-up event; CWUF, CSBF; the option bytes' nRST_STOP and nRST_STDBY making entry a reset (LPWRRSTF). SCR's SEVONPEND. FLASH: an accepted program or erase keeps SR's BSY (and STRT) set for DS10693's typical time (16 us a program; 250 ms to 2 s a sector by its size and PSIZE; 8 to 16 s the mass erase), EOP at its end, CR not written meanwhile, and a program running from the flash stalls on its bus until it ends (no instruction, no interrupt taken); ACR keeps LATENCY, the ART accelerator's and the prefetch bits; KEYR's keys unlock CR (a wrong key a bus error, locked until reset); with PG a store programs the main memory or the OTP area (bits only from 1 to 0) at PSIZE's width, PGPERR, PGAERR across a 128-bit row, PGSERR without PG, WRPERR on a protected sector, a locked OTP block left; SER erases sector SNB, MER the main memory; EOP with EOPIE, OPERR with ERRIE, the FLASH interrupt; OPTKEYR unlocks OPTCR, OPTSTRT writes RDP, USER, nWRP and SPRMOD to the option bytes, loaded into OPTCR at each reset, their write protection acting. GPIO: MODER's input, output, alternate-function and analog modes, OTYPER's push-pull and open-drain, OSPEEDR's speed setting the pin's output resistance and edge rate, PUPDR's pulls in every mode but analog, BSRR (a set winning, halfword lanes), IDR from the pins, LCKR's key sequence freezing the locked pins until reset, AFRL and AFRH selecting among the alternate functions io.alternate lists (from ST's open pin data); an alternate function whose peripheral has no model lets its pin go. SYSCFG: EXTICR's port for each EXTI line, MEMRMP's memory at 0 (flash, system memory, SRAM1), CMPCR's READY. EXTI: lines 0 to 22, edges, SWIER, PR, the shared interrupts and the internal lines' (PVD, RTC alarm, tamper and wake-up, USB wake-ups), EMR's events ending a WFE. RCC's *RSTR bits reset their peripherals. Timers (TIM1-TIM14): up, down and center-aligned counting, the repetition counter, preloads, update events, output compare and PWM modes on the pins, input capture with its edge, prescaler and filter, TI1S, complementary outputs with dead time, MOE, OSSR/OSSI and idle levels, break, LOCK, COM events, the slave modes (reset, gated, trigger, external clock 1, encoder 1-3) on ITR0-3, TI1F_ED, TI1FP1, TI2FP2 or ETRF, external clock mode 2, TRGO for the timers that listen, at the clock RCC gives the timers. TIMx_OR's remaps: TIM5's TI4 on LSI or LSE (HAL's measurement of LSI), TIM11's TI1 on HSE_RTC, TIM2's ITR1 from TIM8's TRGO. SysTick's external reference is HCLK/8 (CLKSOURCE 0). The CRC unit: the CRC-32 (0x04C11DB7, MSB first, from 0xFFFFFFFF) of the words written to DR, CR's RESET, IDR. USART1-3, UART4-5 and USART6: 8 or 9 data bits with parity, 0.5 to 2 stop bits, OVER8, TXE/TC with the held frame, TE's idle frame, breaks (LIN's 13 bits), the receiver from the RX pin's waveform or the session with RXNE, ORE (frames lost), PE, FE, IDLE and LBD and their read-SR-then-DR clearing, mute mode by idle line or address mark, nCTS and nRTS flow control, half-duplex, the synchronous master clock on CK with RX sampled at its edges, every interrupt source. DMA1 and DMA2: eight streams each, CHSEL's channel taking the requests dma_requests maps (RM0390 tables 28 and 29), a request waiting for its stream; peripheral-to-memory, memory-to-peripheral and DMA2's memory-to-memory through the bus as the core's accesses go; direct mode and the FIFO's packing, threshold, peripheral bursts and FS; NDTR with HTIF and TCIF, circular mode, double buffer with CT, transfer errors, the FIFO configuration error; LIFCR/HIFCR and the streams' interrupts. The USARTs' DMAT/DMAR, the SPIs' TXDMAEN/RXDMAEN and the timers' UDE, CCxDE (CCDS), TDE, COMDE and DCR/DMAR burst make their requests. SPI1-4 on their alternate-function pins as masters or slaves: modes 0 to 3, 8 and 16 bits, MSB or LSB first, the baud prescaler, TXE, RXNE, OVR and its clearing, BSY, the receive-only and bidirectional modes with the master clocking on its own, NSS by SSM/SSI, SSOE or the pin, MODF and its write protection, a slave's selection with its bit count kept across NSS, the TI mode's frame pulse and FRE, CRC with CRCNEXT, CRCERR and the CRC frame after a DMA stream's last item, the interrupts. I2C1-3 as masters and slaves on their open-drain pins: START, repeated START (once the bus is free), STOP, 7-bit and 10-bit addressing (ADD10), ACK and POS, the SR1 and SR2 flags with their clearing sequences, BTF clock holding, CCR's standard and fast timing with a slave's clock stretching honoured, arbitration lost (ARLO), BUSY and BERR from the bus as the pins show it; the slave's OAR1 (7 or 10 bits), OAR2's dual address, the general call, the SMBus default, host and alert response addresses, ADDR, BTF and TXE stretching or NOSTRETCH's OVR, STOPF (not after the master's NACK), the master's NACK (AF), a byte received over what the program wrote to DR; PEC with CR1.PEC and PECERR; SMBus's TIMEOUT and SMBALERT; SWRST; the event and error interrupts; DMAEN's requests and LAST's NACK after the DMA's EOT-1, the PEC after a DMA transfer. ADC1-3: the regular and injected groups on SWSTART, JSWSTART, the timers' and EXTI's triggers with their edges or JAUTO; scan, continuous and discontinuous modes; SMPRx's sampling times and RES's 12, 10, 8 or 6 bits at the ADC clock CCR's ADCPRE gives; the pins' voltages (io.analog), VREFINT, the temperature sensor at the world's temperature and VBAT/4; EOC (EOCS), JEOC, STRT, JSTRT, OVR stopping the group, the analog watchdog, JOFRx's offsets and the signed alignments, DMA with DDS, the interrupts; the common CSR, CCR and the dual and triple modes (regular and injected simultaneous, interleaved with DELAY, alternate trigger) with CDR's DMA modes 1-3. The DAC's two channels on PA4 and PA5: the holding registers' views, DOR one APB1 clock after a write or three after a trigger (TIM2, 4-8 TRGO, EXTI9, SWTRIG), the noise and triangle waves, DMA with its underrun, the output buffer's range or 15 kOhm without it. The device signature's UID, flash size and the factory calibration values (TS_CAL1, TS_CAL2, VREFINT_CAL) in system memory. IWDG: the keys, PR and RLR with PVU and RVU, LSI forced on, RLR+1 counts to the reset (IWDGRSTF), the option bytes' hardware watchdog loaded at every reset. WWDG: the free-running counter at PCLK1/4096/2^WDGTB, WDGA, the window, T6's reset (WWDGRSTF), EWI and EWIF. RCC_CSR's reset flags (PORRSTF, PINRSTF, SFTRSTF, IWDGRSTF, WWDGRSTF, LPWRRSTF) kept until RMVF; RAM kept through every reset but a power-on one and Standby's. RTC: the BCD calendar with its leap years and 12/24-hour modes from LSE, LSI or HSE/RTCPRE through PREDIV_A and PREDIV_S, INIT and INITF, WPR's keys and DBP's protection, the shadow registers with RSF, their locking and BYPSHAD, alarms A and B with their masks and subseconds through EXTI17, the wakeup timer through EXTI22, the timestamp and tamper on PC13 through EXTI21 (the tamper erasing the backup registers), RTC_OUT on PC13, the smooth and coarse calibrations, SHIFTR, ADD1H/SUB1H, the twenty backup registers; the backup domain (BDCR, the RTC) keeps going through every reset but a power-on one, and BDRST resets it. The factory option bytes (RDP level 0, the software watchdog). USB OTG FS (the DWC2 core, RM0390 30) in device and host mode, on its own register block (OTGFS, as RM0390 30.15 lays it out: six endpoints each way) behind AHB2ENR's OTGFSEN and AHB2RSTR's OTGFSRST, with a host on its cable carried as transactions (ADR-031): the 1.25 KiB FIFO RAM with the receive FIFO's status words (GRXSTSR, GRXSTSP; SETUP data, OUT data, the SETUP stage done and OUT transfer done statuses raising STUP and XFRC when taken) and each IN endpoint's FIFO (DIEPTXFn, DTXFSTS, TXFE by TXFELVL, DIEPEMPMSK); SETUPs always taken (STUPCNT, B2BSTUP), EP0's STALL cleared and NAKs set by them; OUT packets NAKed unless the endpoint is enabled with room and packets left, a repeated data toggle dropped, a short packet ending the transfer; IN packets sent once wholly in the FIFO (ITTXFE, XFRC, EPENA cleared); STALL, SNAK/CNAK, SD0PID/SD1PID, EPDIS with EPDISD, DCTL's global NAKs; DCFG's address taking effect after its SET_ADDRESS's status stage; the D+ pull-up from GCCFG's PWRDWN, DCTL's SDIS, the B-session (VBUS on PA9 above 4 V with VBDEN, SEDET when it goes, or GOTGCTL's overrides) and PA12 on AF10; USBRST, ENUMDNE at full speed, SOF and DSTS's frame number, ESUSP, USBSUSP and SUSPSTS after 3 ms of an idle bus, WKUINT; CSRST completing only while the 48 MHz clock runs (PLL_Q, or PLLSAI_P by DCKCFGR2's CK48MSEL), RXFFLSH and TXFFLSH; FDMOD and FHMOD changing CMOD after 25 ms; the OTG_FS interrupt as a level (GINT, GINTMSK, DAINTMSK and the endpoint masks), the NVIC pending it again when its handler returns with it still asserted. As a host (FHMOD): the port (PPWR; the device's pull-up a connection, PCSTS, PCDET, its end DISCINT; PRST's reset for as long as it is set, its end enabling the port at the device's speed, PSPD full or low; PENA written 1 disabling it; PSUSP and PRES; PLSTS), an SOF or low-speed keep-alive every frame of HFIR at HCFG's PHY clock (HFNUM's number and time left), and twelve channels: SETUP, OUT and IN transactions at the bus's bit time with their data toggles, OUT once its packet is in the non-periodic or periodic transmit FIFO (HNPTXSTS, HPTXSTS), one IN token per CHENA with room in the receive FIFO, ACK, NAK, STALL, the time-out (TXERR), babble (BBERR) and toggle errors (DTERR), the halt by CHDIS with CHENA (CHH), the IN data and transfer-done statuses (XFRC when taken), interrupt channels once a frame in the frames ODDFRM asks; HAINT, HCINT, HPRTINT, NPTXFE and PTXFE.",
"omissions": [
"watchdogs: LSI's spread (32 kHz here), the debug freeze bits (a debugger's halt stops all of the machine's time); the WWDG's counter while WDGA is clear counts from the last access made while its clock is on",
"RTC: the reference clock detection (REFCKON, RTC_REFIN), RTC_AF2 (PI8, not bonded), the tamper input's precharge and pull-up, the 8 and 16 s calibration windows (the rate is applied evenly), RECALPF's wait",
@@ -2783,7 +2783,7 @@
"RCC: the clock security system's detection (CSSON is kept; the board's HSE never fails), a peripheral held in reset while its *RSTR bit stays set (its reset happens on the write), the brownout reset (BORRSTF only at power-on), the start-up times' spread between parts and with temperature and LSEMOD's drive level (the typical times always), MCO1 and MCO2 on their pins, and the I2S, SAI, SPDIF-RX, CEC and FMPI2C clocks from PLLI2S and PLLSAI (DCKCFGR, DCKCFGR2) beyond TIMPRE and the 48 MHz clock's CK48MSEL (PLL_Q or PLLSAI_P, which the USB OTG FS core needs)",
"PWR: the regulator's settling times and the low-power modes' wake-up times (DS10693's low-power mode wake-up timings: the core runs at the wake-up event), the regulator's settings (LPDS, FPDS, LPLVDS, MRLVDS: the supply current is not modelled), the programmable voltage detector, DBGMCU_CR's DBG_STOP and DBG_STANDBY (a debugger's halt stops all of the machine's time), the tamper and timestamp pin in Standby beyond RTC_OUT; DWT's CYCCNT counts through Stop",
"CAN, SDIO, QUADSPI, SAI, SPDIF-RX, HDMI-CEC, DCMI, FMC, FMPI2C and the USB OTG HS controller",
- "USB OTG FS: host mode (its channel registers are storage), the ID pin, SRP and HNP, isochronous endpoints' frame parity and incomplete-transfer interrupts, LPM, GCCFG's battery-charging detection, remote wakeup signalling (RWUSIG), the OTG_FS_WKUP interrupt through EXTI line 18, the SOF pulse on PA8; the device's turnaround inside a transaction; GSNPSID reads 0x4F54320A (core 3.20a), the value TinyUSB's dwc2_info table measures on the F412's and F76x's OTG_FS, the same generation of the core (ST's HAL groups the F446 with the F412, F413 and F469); no ST document gives it and it was not read from an F446",
+ "USB OTG FS: the ID pin, SRP and HNP, split transactions through a hub, isochronous endpoints' frame parity and incomplete-transfer interrupts and a host's isochronous channels, FRMOR, LPM, GCCFG's battery-charging detection, the OTG_FS_WKUP interrupt through EXTI line 18, the SOF pulse on PA8; the device's turnaround inside a transaction (a transaction is whole), a host channel's OUT data kept per channel rather than in the shared FIFO's order (the words free are the same); GSNPSID reads 0x4F54320A (core 3.20a), the value TinyUSB's dwc2_info table measures on the F412's and F76x's OTG_FS, the same generation of the core (ST's HAL groups the F446 with the F412, F413 and F469); no ST document gives it and it was not read from an F446",
"flash: the times' spread and their maximum values (the typical ones always), the ART accelerator's cache hits during an operation (a program in the flash stalls whatever its cache holds), a program in SRAM whose interrupt handler is in the flash (it does not stall), the option bytes' programming time (OPTSTRT is done at once), a reset during an operation leaving the memory half-programmed, HCLK's 1 MHz minimum, the read protection's levels (RDP stored, no mass erase on a regression), PCROP's read refusal (RDERR), the ART accelerator's caches (DCRST, ICRST)",
"CRC: the bus stall of a DR access while a word is computed (four AHB clocks), and a byte or halfword write to DR, which RM0390 does not allow, feeding its value zero-extended"
],
diff --git a/core/analog/analog.h b/core/analog/analog.h
index fda657d7..3da40a80 100644
--- a/core/analog/analog.h
+++ b/core/analog/analog.h
@@ -70,6 +70,7 @@ struct esk_island {
double *x; /* accepted solution */
es_time_t t_accepted; /* time of the accepted solution */
es_time_t h_next_ps; /* proposed next step */
+ es_time_t active_at; /* its last source change or crossing */
esk_event_handle step_event;
uint64_t steps, rejections, newton_iters;
uint64_t quiet_steps; /* steps a steady island took without a solve */
@@ -104,6 +105,12 @@ es_status esk_island_reset(esk_island *isl);
es_status esk_island_step(esk_island *isl, es_time_t until, es_time_t *reached);
/* a source changed (a bridge pin toggled): the island must re-evaluate from now */
void esk_island_touch(esk_island *isl, es_time_t now);
+/* integrate a lagging island up to `now` with its sources as they are, so
+ * that its voltages are now's (a quiet one is already) */
+void esk_island_sync(esk_island *isl, es_time_t now);
+/* a watched pin may cross its threshold within `ahead` of the island's
+ * accepted time, on its last two solutions' slope */
+int esk_island_may_cross(esk_island *isl, es_time_t ahead);
/* a component wake for its analog device (a device with a clock of its own,
* as a PTC fuse's temperatures) */
void esk_adev_on_time(esk_adev *d, es_time_t now);
diff --git a/core/analog/mna.c b/core/analog/mna.c
index e991ec96..67c2d4aa 100644
--- a/core/analog/mna.c
+++ b/core/analog/mna.c
@@ -48,6 +48,8 @@ typedef struct mna {
int nextra; /* branch currents and internal nodes */
int n;
double *J, *f, *x_iter, *x_old, *dx;
+ double *x_prev; /* the solution accepted before isl->x, for the slope */
+ es_time_t t_prev;
int *piv;
int allocated;
int started, running, in_step;
@@ -419,6 +421,10 @@ static void publish(esk_island *isl, es_time_t t)
{
mna *p = (mna *)isl->priv;
size_t i;
+ if (p->x_prev && t > isl->t_accepted) {
+ memcpy(p->x_prev, isl->x, (size_t)p->n * sizeof *p->x_prev);
+ p->t_prev = isl->t_accepted;
+ }
memcpy(isl->x, p->x_iter, (size_t)p->n * sizeof *isl->x);
isl->t_accepted = t;
for (i = 1; i < isl->nets.len; i++) {
@@ -533,10 +539,14 @@ static void deliver_crossings(esk_island *isl, es_time_t t)
for (i = 0; i < isl->devs.len; i++) {
esk_adev *d = isl->devs.data[i];
esk_logic nv;
- if (bridge_input_role(d) && crossing(d, isl->x, &nv))
+ if (bridge_input_role(d) && crossing(d, isl->x, &nv)) {
esk_engine_bridge_crossed(isl->e, d->pin, nv, t);
- if (watch_crossing(d, isl->x, &nv))
+ isl->active_at = t;
+ }
+ if (watch_crossing(d, isl->x, &nv)) {
esk_engine_watch_crossed(isl->e, d->pin, nv, t);
+ isl->active_at = t;
+ }
}
}
@@ -638,10 +648,11 @@ static void free_matrices(esk_island *isl)
esk_free(isl->a, p->f, n * sizeof *p->f);
esk_free(isl->a, p->x_iter, n * sizeof *p->x_iter);
esk_free(isl->a, p->x_old, n * sizeof *p->x_old);
+ esk_free(isl->a, p->x_prev, n * sizeof *p->x_prev);
esk_free(isl->a, p->dx, n * sizeof *p->dx);
esk_free(isl->a, p->piv, n * sizeof *p->piv);
esk_free(isl->a, isl->x, n * sizeof *isl->x);
- p->J = p->f = p->x_iter = p->x_old = p->dx = NULL;
+ p->J = p->f = p->x_iter = p->x_old = p->x_prev = p->dx = NULL;
p->piv = NULL;
isl->x = NULL;
p->allocated = 0;
@@ -841,21 +852,23 @@ static es_status allocate(esk_island *isl)
p->f = (double *)esk_calloc(isl->a, n, sizeof *p->f);
p->x_iter = (double *)esk_calloc(isl->a, n, sizeof *p->x_iter);
p->x_old = (double *)esk_calloc(isl->a, n, sizeof *p->x_old);
+ p->x_prev = (double *)esk_calloc(isl->a, n, sizeof *p->x_prev);
p->dx = (double *)esk_calloc(isl->a, n, sizeof *p->dx);
p->piv = (int *)esk_calloc(isl->a, n, sizeof *p->piv);
isl->x = (double *)esk_calloc(isl->a, n, sizeof *isl->x);
p->allocated = 1;
- if (!p->J || !p->f || !p->x_iter || !p->x_old || !p->dx || !p->piv || !isl->x) {
+ if (!p->J || !p->f || !p->x_iter || !p->x_old || !p->x_prev || !p->dx || !p->piv || !isl->x) {
/* sizes were computed from n: free with the same n */
size_t nn = n;
esk_free(isl->a, p->J, nn * nn * sizeof *p->J);
esk_free(isl->a, p->f, nn * sizeof *p->f);
esk_free(isl->a, p->x_iter, nn * sizeof *p->x_iter);
esk_free(isl->a, p->x_old, nn * sizeof *p->x_old);
+ esk_free(isl->a, p->x_prev, nn * sizeof *p->x_prev);
esk_free(isl->a, p->dx, nn * sizeof *p->dx);
esk_free(isl->a, p->piv, nn * sizeof *p->piv);
esk_free(isl->a, isl->x, nn * sizeof *isl->x);
- p->J = p->f = p->x_iter = p->x_old = p->dx = NULL;
+ p->J = p->f = p->x_iter = p->x_old = p->x_prev = p->dx = NULL;
p->piv = NULL;
isl->x = NULL;
p->allocated = 0;
@@ -1388,6 +1401,69 @@ int esk_island_quiet(esk_island *isl, es_time_t *next)
return 1;
}
+/* an island that lags behind (engine.c's island_step_cb puts its steps
+ * off while another part's events come far faster) integrates up to `now`,
+ * its sources unchanged: a reading is then of now */
+/* might a watched pin's node cross its threshold within `ahead` of the
+ * accepted time, its last two solutions' slope kept (with a margin of
+ * two)? Then the island does not put its steps off: a crossing found late
+ * would come late */
+int esk_island_may_cross(esk_island *isl, es_time_t ahead)
+{
+ mna *p = (mna *)isl->priv;
+ double dt, k;
+ int i;
+ if (!p->x_prev || !p->x_iter || isl->t_accepted <= p->t_prev)
+ return 1;
+ if (next_breakpoint(isl, isl->t_accepted) <= isl->t_accepted + ahead)
+ return 1; /* a source's ramp or edge within the lag: step to it */
+ dt = (double)(isl->t_accepted - p->t_prev);
+ k = 2.0 * (double)ahead / dt;
+ for (i = 0; i < p->n; i++)
+ p->x_old[i] = isl->x[i] + (isl->x[i] - p->x_prev[i]) * k;
+ if (any_crossing(isl, p->x_old))
+ return 1;
+ {
+ /* any digital pin's thresholds, whatever its role: a part reads
+ * back the pad it drives (an I2C master its released lines) */
+ size_t j;
+ for (j = 0; j < isl->devs.len; j++) {
+ const esk_adev *d = isl->devs.data[j];
+ double up, down, v, w;
+ int node;
+ if (!d->pin || (d->ops != &esk_bridge_in_ops && d->ops != &esk_bridge_out_ops) ||
+ d->pin->model.kind == ESK_PIN_POWER)
+ continue;
+ node = d->node[0];
+ if (node <= 0)
+ continue;
+ thresholds(d->pin, &up, &down);
+ v = isl->x[node - 1];
+ w = p->x_old[node - 1];
+ if ((v < up) != (w < up) || (v > down) != (w > down))
+ return 1;
+ }
+ }
+ return 0;
+}
+
+void esk_island_sync(esk_island *isl, es_time_t now)
+{
+ mna *p = (mna *)isl->priv;
+ es_time_t reached = 0;
+ uint32_t guard = 0;
+ if (!p->running || isl->failed || p->in_step || p->quiet || isl->t_accepted >= now)
+ return;
+ p->exact_until = 1;
+ while (isl->t_accepted < now && guard++ < MNA_MAX_CATCHUP) {
+ if (esk_island_step(isl, now, &reached) != ES_OK) {
+ isl->failed = 1;
+ break;
+ }
+ }
+ p->exact_until = 0;
+}
+
void esk_island_touch(esk_island *isl, es_time_t now)
{
mna *p = (mna *)isl->priv;
@@ -1409,6 +1485,8 @@ void esk_island_touch(esk_island *isl, es_time_t now)
}
}
p->exact_until = 0;
+ if (now > isl->active_at)
+ isl->active_at = now;
p->restart = 1;
p->quiet = 0;
p->still = 0;
diff --git a/core/engine/engine.c b/core/engine/engine.c
index 0b1e6d77..1d674a99 100644
--- a/core/engine/engine.c
+++ b/core/engine/engine.c
@@ -762,8 +762,13 @@ void esk_comp_set_supply(esk_comp *c, double amps, es_time_t at)
for (j = 0; j < k && !seen; j++)
seen = c->pins[j].model.kind == ESK_PIN_POWER && c->pins[j].bridge && c->pins[j].net &&
c->pins[j].net->island == isl;
- if (!seen)
+ if (!seen) {
+ /* a part's load changed: integrated exactly, but no bus activity
+ * that would keep the island from lagging behind */
+ es_time_t active = isl->active_at;
esk_island_touch(isl, at);
+ isl->active_at = active;
+ }
}
c->supply_a = amps;
}
@@ -946,6 +951,10 @@ es_status esk_engine_set_input(esk_engine *e, esk_comp *c, const char *input, co
copy = esk_json_clone(e->a, value);
if (!copy)
return ES_ENOMEM;
+ /* an analog part's island, if it lagged, reaches now with the input as
+ * it was: the change is from now on */
+ if (c->adev && c->adev->isl)
+ esk_island_sync(c->adev->isl, e->sched.now);
if (esk_json_object_set(e->a, c->inputs, input, copy)) {
esk_json_free(e->a, copy);
return ES_ENOMEM;
@@ -1092,6 +1101,15 @@ void esk_pin_refresh(esk_pin *p)
}
static void island_step_cb(void *ctx, void *arg, es_time_t now);
+/* a step clipped to less than 1/ISLAND_CHOP of the island's own proposal
+ * and shorter than ISLAND_DENSE_PS (another part's events tens of
+ * nanoseconds apart: a core's SPI clock) is put off, up to ISLAND_LAG_PS */
+#define ISLAND_CHOP 16u
+#define ISLAND_DENSE_PS ES_PS_PER_US
+#define ISLAND_LAG_PS (10u * ES_PS_PER_US)
+/* ... and only an island calm for ISLAND_CALM_PS (no source changed, no
+ * crossing): a bus's lines (I2C on its pull-ups) step as they always did */
+#define ISLAND_CALM_PS ES_PS_PER_MS
static es_status schedule_island(esk_engine *e, esk_island *isl, es_time_t at)
{
@@ -1110,16 +1128,41 @@ static void island_step_cb(void *ctx, void *arg, es_time_t now)
* events cannot (crossings arrive as SIGNAL events), so they are ignored */
es_time_t next = esk_sched_next_time_excluding(&e->sched, ESK_EV_ANALOG), until, reached = now;
es_status st;
+ es_time_t h = isl->h_next_ps ? isl->h_next_ps : isl->p.h_max_ps;
isl->step_event = 0;
if (isl->failed)
return;
- until = now;
- if (esk_time_add(now, isl->h_next_ps ? isl->h_next_ps : isl->p.h_max_ps, &until))
- until = ES_TIME_MAX - 1;
- if (next < until)
- until = next;
- if (until <= now)
- until = now + 1;
+ if (isl->t_accepted < now) {
+ /* lagging (below): integrate up to now, which no event can undo */
+ until = now;
+ } else {
+ until = now;
+ if (esk_time_add(now, h, &until))
+ until = ES_TIME_MAX - 1;
+ if (next < until)
+ until = next;
+ if (until <= now)
+ until = now + 1;
+ if (next > now && until - now < ISLAND_DENSE_PS && (until - now) * ISLAND_CHOP < h &&
+ now - isl->active_at > ISLAND_CALM_PS) {
+ /* another part's events come far faster than this island needs
+ * steps (a core's SPI clock beside a supply settling), under a
+ * microsecond apart: rather
+ * than a step to each, it lags behind and catches up in steps
+ * of its own, at most ISLAND_LAG_PS later; a part that changes
+ * its sources meanwhile integrates it up to that moment first
+ * (esk_island_touch), so only its readings and crossings may
+ * come that late */
+ es_time_t lag = h < ISLAND_LAG_PS ? h : ISLAND_LAG_PS, at;
+ if (esk_island_may_cross(isl, lag))
+ goto step; /* a crossing may come: it must come on time */
+ if (esk_time_add(now, lag, &at))
+ at = ES_TIME_MAX - 1;
+ schedule_island(e, isl, at);
+ return;
+ }
+ }
+step:
st = esk_island_step(isl, until, &reached);
if (st != ES_OK) {
isl->failed = 1;
@@ -1395,7 +1438,21 @@ es_status esk_engine_reset(esk_engine *e)
static void sync_cb(void *ctx, void *arg, es_time_t now)
{
- ES_UNUSED(ctx); ES_UNUSED(arg); ES_UNUSED(now);
+ esk_engine *e = (esk_engine *)ctx;
+ size_t i;
+ ES_UNUSED(arg);
+ /* the advance ends here: an island that lagged catches up */
+ for (i = 0; i < e->islands.len; i++)
+ esk_island_sync(e->islands.data[i], now);
+}
+
+double esk_net_voltage_now(esk_net *n)
+{
+ if (n && n->island) {
+ esk_island_sync(n->island, n->island->e->sched.now);
+ return n->voltage;
+ }
+ return 0.0;
}
es_status esk_engine_advance(esk_engine *e, es_time_t until)
diff --git a/core/engine/engine.h b/core/engine/engine.h
index a56aaa5b..ee0152cd 100644
--- a/core/engine/engine.h
+++ b/core/engine/engine.h
@@ -221,6 +221,9 @@ void esk_pin_drive(esk_pin *p, esk_logic value, esk_strength strength, es_time_t
void esk_pin_release(esk_pin *p);
esk_logic esk_pin_read(esk_pin *p); /* samples the net with floating/unknown diagnostics */
esk_logic esk_net_value(const esk_net *n);
+/* an analog net's voltage at the engine's time (its island, if it lagged
+ * behind, catches up first); 0 for a digital net */
+double esk_net_voltage_now(esk_net *n);
/* Current opposing known digital drives. Analog islands compare active digital
* output intents; digital nets compare known values at the strongest strength.
* X by itself is not evidence of contention. */
diff --git a/core/engine/usb.c b/core/engine/usb.c
index 3e1351f6..c658f280 100644
--- a/core/engine/usb.c
+++ b/core/engine/usb.c
@@ -59,9 +59,9 @@ esk_usb_dev *esk_usb_device_of(esk_engine *e, const esk_usb_host *h)
size_t i;
if (!h->dp || !h->dp->net)
return NULL;
- for (i = 0; i < e->usb.devs.len; i++) {
+ for (i = 0; i < e->usb.devs.len; i++) { /* not its own device side (an OTG core's) */
esk_usb_dev *d = e->usb.devs.data[i];
- if (d->dp && d->dp->net == h->dp->net)
+ if (d->dp && d->dp->net == h->dp->net && d->comp != h->comp)
return d;
}
return NULL;
@@ -74,7 +74,7 @@ void esk_usb_changed(esk_engine *e, const esk_usb_dev *d, es_time_t at)
return;
for (i = 0; i < e->usb.hosts.len; i++) {
esk_usb_host *h = e->usb.hosts.data[i];
- if (h->dp && h->dp->net == d->dp->net && h->changed)
+ if (h->dp && h->dp->net == d->dp->net && h->comp != d->comp && h->changed)
h->changed(h->ctx, at);
}
}
@@ -86,7 +86,7 @@ void esk_usb_remote_wakeup(esk_engine *e, const esk_usb_dev *d, es_time_t at)
return;
for (i = 0; i < e->usb.hosts.len; i++) {
esk_usb_host *h = e->usb.hosts.data[i];
- if (h->dp && h->dp->net == d->dp->net && h->wakeup)
+ if (h->dp && h->dp->net == d->dp->net && h->comp != d->comp && h->wakeup)
h->wakeup(h->ctx, at);
}
}
@@ -104,3 +104,5 @@ es_time_t esk_usb_fs_time(int len, int handshake)
bits += 8 + 8 + 3 + 8;
return (es_time_t)(bits * FS_BIT_PS_X3 / 3);
}
+
+es_time_t esk_usb_ls_time(int len, int handshake) { return esk_usb_fs_time(len, handshake) * 8u; }
diff --git a/core/engine/usb.h b/core/engine/usb.h
index 4a9b7aaf..526b6f29 100644
--- a/core/engine/usb.h
+++ b/core/engine/usb.h
@@ -78,7 +78,8 @@ es_status esk_usb_attach_dev(struct esk_engine *e, esk_usb_dev *d);
void esk_usb_detach_dev(struct esk_engine *e, esk_usb_dev *d);
es_status esk_usb_attach_host(struct esk_engine *e, esk_usb_host *h);
void esk_usb_detach_host(struct esk_engine *e, esk_usb_host *h);
-/* the device on the host's cable (its D+ on the host's D+ net), or NULL */
+/* the device on the host's cable (its D+ on the host's D+ net, another
+ * part than the host: an OTG core is both), or NULL */
esk_usb_dev *esk_usb_device_of(struct esk_engine *e, const esk_usb_host *h);
/* a device's pull-up changed: the host on its cable hears it */
void esk_usb_changed(struct esk_engine *e, const esk_usb_dev *d, es_time_t at);
@@ -88,5 +89,7 @@ void esk_usb_remote_wakeup(struct esk_engine *e, const esk_usb_dev *d, es_time_t
* of `len` bytes (none for -1) and the handshake (when `handshake`), each
* with its SYNC, PID, CRC and EOP, and the inter-packet gaps */
es_time_t esk_usb_fs_time(int len, int handshake);
+/* the same at low speed (1.5 Mbit/s, a low-speed device on a root port) */
+es_time_t esk_usb_ls_time(int len, int handshake);
#endif
diff --git a/core/ext/lua_model.c b/core/ext/lua_model.c
index ae086f55..46c980a5 100644
--- a/core/ext/lua_model.c
+++ b/core/ext/lua_model.c
@@ -403,7 +403,7 @@ static int pin_field(lua_State *L)
const esk_net *n = p->net;
double volts = 0.0;
if (n && n->island)
- volts = n->voltage;
+ volts = esk_net_voltage_now(p->net); /* its island, if it lagged, caught up */
else if (n && esk_net_value(n) == ESK_L1)
volts = p->comp->e->default_supply_v;
lua_pushnumber(L, (lua_Number)volts);
@@ -420,7 +420,7 @@ static int analog_volts(const esk_pin *p, double *v)
{
if (!p->net || !p->net->island)
return 0;
- *v = p->net->voltage;
+ *v = esk_net_voltage_now(p->net);
return 1;
}
diff --git a/core/mcu/mach.h b/core/mcu/mach.h
index c9eeb0db..5b994bbc 100644
--- a/core/mcu/mach.h
+++ b/core/mcu/mach.h
@@ -318,6 +318,15 @@ typedef struct esk_mach_host {
void (*usb_changed)(void *ctx);
/* it signals remote wakeup (K on the suspended bus) */
void (*usb_remote_wakeup)(void *ctx);
+ /* the chip's USB controller as a host (an OTG core in host mode): the
+ * pull-up of the device on its cable (engine/usb.h's ESK_USB_DETACHED,
+ * _FULL_SPEED, _LOW_SPEED), the bus states it drives and its
+ * transactions, answered as engine/usb.h says */
+ int (*usbh_pullup)(void *ctx);
+ void (*usbh_bus)(void *ctx, int ev, u32 frame);
+ int (*usbh_setup)(void *ctx, int addr, int ep, const u8 data[8]);
+ int (*usbh_out)(void *ctx, int addr, int ep, int data1, const u8 *data, u32 len);
+ int (*usbh_in)(void *ctx, int addr, int ep, u8 *data, u32 cap, u32 *len, int *data1);
} esk_mach_host;
enum { ESK_RADIO_WIFI = 1, ESK_RADIO_BLE = 2 };
@@ -346,6 +355,10 @@ struct esk_usb_port {
int (*setup)(void *ctx, int addr, int ep, const u8 data[8]);
int (*out)(void *ctx, int addr, int ep, int data1, const u8 *data, u32 len);
int (*in)(void *ctx, int addr, int ep, u8 *data, u32 cap, u32 *len, int *data1);
+ /* as a host: the device on its cable changed its pull-up, or signals
+ * remote wakeup on the suspended bus */
+ void (*host_changed)(void *ctx);
+ void (*host_wakeup)(void *ctx);
};
/* ---- the machine ---------------------------------------------------------- */
@@ -500,6 +513,10 @@ struct esk_mach {
/* the GPIO model's: the pins' outside levels changed; where a UART's
* TX or RX pin is (0, or -1 when the part has no such UART) */
void (*pins_changed)(struct esk_mach *m);
+ /* the GPIO model reads an output pin's level from its own drive (an
+ * STM32F4's IDR): the echo of its own output tells it nothing, and the
+ * runtime need not call pins_changed for it */
+ int outputs_read_back;
int (*uart_pin)(struct esk_mach *m, const char *unit, int tx, int *port, int *bit);
/* esk_mach_run returns as soon as the core sleeps, for the runtime to
* schedule the wake-up itself */
diff --git a/core/mcu/mcu.c b/core/mcu/mcu.c
index 9a23633e..746cd1b9 100644
--- a/core/mcu/mcu.c
+++ b/core/mcu/mcu.c
@@ -140,6 +140,7 @@ typedef struct mcu_state {
int on_air;
esk_usb_dev usb; /* its USB device controller's end of a cable */
int on_usb;
+ esk_usb_host usbh; /* the same controller as a host (an OTG core) */
mcu_supply sup;
/* each exception's entries in the current window, for mcu.irq_storm */
es_time_t storm_t[MCU_STORM_N];
@@ -755,7 +756,7 @@ static int h_pin_volts(void *ctx, int port, int bit, double *v)
return 0;
n = p->net;
if (n->island)
- *v = n->voltage;
+ *v = esk_net_voltage_now(p->net); /* an ADC samples it now */
else
*v = esk_net_value(n) == ESK_L1 ? st->m.vdd : 0.0;
return 1;
@@ -1403,6 +1404,67 @@ static void h_usb_changed(void *ctx)
esk_usb_changed(st->c->e, &st->usb, local_now(st));
}
+/* as a host: the device on its cable, at the machine's time */
+static esk_usb_dev *usbh_dev(mcu_state *st) { return st->on_usb ? esk_usb_device_of(st->c->e, &st->usbh) : NULL; }
+
+static int h_usbh_pullup(void *ctx)
+{
+ esk_usb_dev *d = usbh_dev((mcu_state *)ctx);
+ return d ? d->pullup(d->ctx) : ESK_USB_DETACHED;
+}
+
+static void h_usbh_bus(void *ctx, int ev, u32 frame)
+{
+ mcu_state *st = (mcu_state *)ctx;
+ esk_usb_dev *d = usbh_dev(st);
+ if (d)
+ d->bus(d->ctx, ev, frame, local_now(st));
+}
+
+static int h_usbh_setup(void *ctx, int addr, int ep, const u8 data[8])
+{
+ mcu_state *st = (mcu_state *)ctx;
+ esk_usb_dev *d = usbh_dev(st);
+ return d ? d->setup(d->ctx, addr, ep, data, local_now(st)) : ESK_USB_NONE;
+}
+
+static int h_usbh_out(void *ctx, int addr, int ep, int data1, const u8 *data, u32 len)
+{
+ mcu_state *st = (mcu_state *)ctx;
+ esk_usb_dev *d = usbh_dev(st);
+ return d ? d->out(d->ctx, addr, ep, data1, data, len, local_now(st)) : ESK_USB_NONE;
+}
+
+static int h_usbh_in(void *ctx, int addr, int ep, u8 *data, u32 cap, u32 *len, int *data1)
+{
+ mcu_state *st = (mcu_state *)ctx;
+ esk_usb_dev *d = usbh_dev(st);
+ return d ? d->in(d->ctx, addr, ep, data, cap, len, data1, local_now(st)) : ESK_USB_NONE;
+}
+
+/* the device on its cable pulled up or let go */
+static void usbh_changed(void *ctx, es_time_t at)
+{
+ mcu_state *st = (mcu_state *)ctx;
+ if (!st->m.usb.host_changed)
+ return;
+ catch_up(st, at);
+ esk_mach_outside(&st->m);
+ st->m.usb.host_changed(st->m.usb.ctx);
+ wake(st);
+}
+
+static void usbh_wakeup(void *ctx, es_time_t at)
+{
+ mcu_state *st = (mcu_state *)ctx;
+ if (!st->m.usb.host_wakeup)
+ return;
+ catch_up(st, at);
+ esk_mach_outside(&st->m);
+ st->m.usb.host_wakeup(st->m.usb.ctx);
+ wake(st);
+}
+
static void h_usb_remote_wakeup(void *ctx)
{
mcu_state *st = (mcu_state *)ctx;
@@ -1428,8 +1490,10 @@ static void mcu_destroy(esk_comp *c)
if (st->on_air)
esk_air_detach(e, &st->air);
st->on_air = 0;
- if (st->on_usb)
+ if (st->on_usb) {
esk_usb_detach_dev(e, &st->usb);
+ esk_usb_detach_host(e, &st->usbh);
+ }
st->on_usb = 0;
esk_an_free(st->an);
st->an = NULL;
@@ -1526,6 +1590,11 @@ static void mcu_on_input(esk_comp *c, esk_pin *p)
}
if (st->held)
return;
+ /* the echo of its own output, which the net agrees with, where the GPIO
+ * model reads an output's own drive: it has it already */
+ if (st->m.outputs_read_back && p->is_output && p->drive_strength == ESK_STR_STRONG && p->drive_value != ESK_LZ && p->net &&
+ !p->net->island && esk_net_value(p->net) == p->drive_value)
+ return;
if (st->m.pins_changed) {
es_time_t now = e->sched.now;
st->m.input_lead = st->t > now ? (u32)ps_to_cycles(st->t - now, esk_mach_core_hz(&st->m)) : 0;
@@ -1836,6 +1905,11 @@ es_status esk_mcu_attach(esk_comp *c, const esk_mcu_setup *su, char *why, size_t
st->m.host.radio_tx = h_radio_tx;
st->m.host.usb_changed = h_usb_changed;
st->m.host.usb_remote_wakeup = h_usb_remote_wakeup;
+ st->m.host.usbh_pullup = h_usbh_pullup;
+ st->m.host.usbh_bus = h_usbh_bus;
+ st->m.host.usbh_setup = h_usbh_setup;
+ st->m.host.usbh_out = h_usbh_out;
+ st->m.host.usbh_in = h_usbh_in;
st->quantum_ps = su->quantum_ps ? su->quantum_ps : MCU_DEFAULT_QUANTUM_PS;
supply_setup(st, su->mcu);
speed_table(esk_json_get(esk_json_get(esk_json_get(su->mcu, "io"), "electrical"), "rout"), st->rout);
@@ -1921,6 +1995,14 @@ es_status esk_mcu_attach(esk_comp *c, const esk_mcu_setup *su, char *why, size_t
if (esk_usb_attach_dev(e, &st->usb) != ES_OK)
return ES_ENOMEM;
st->on_usb = 1;
+ /* and the other end, for its host mode */
+ st->usbh.ctx = st;
+ st->usbh.comp = c;
+ st->usbh.dp = st->usb.dp;
+ st->usbh.changed = usbh_changed;
+ st->usbh.wakeup = usbh_wakeup;
+ if (esk_usb_attach_host(e, &st->usbh) != ES_OK)
+ return ES_ENOMEM;
}
return ES_OK;
}
diff --git a/core/mcu/rf.c b/core/mcu/rf.c
index 46382bb7..d942ce61 100644
--- a/core/mcu/rf.c
+++ b/core/mcu/rf.c
@@ -358,6 +358,54 @@ int esk_rf_pin(const struct esk_rf *rf, const char *name)
return -1;
}
+#define AF_PINS (11 * 32) /* PA0 to PK15, as port * 32 + bit */
+
+struct af_key {
+ const char *sig;
+ int i;
+};
+
+static int af_key_cmp(const void *a, const void *b)
+{
+ const struct af_key *x = (const struct af_key *)a, *y = (const struct af_key *)b;
+ int c = strcmp(x->sig, y->sig);
+ return c ? c : (x->i > y->i) - (x->i < y->i);
+}
+
+/* the alternate functions' indexes, by pin and by signal: the same answers
+ * as a scan of the table, in its order */
+static es_status af_index(struct esk_mach *m, struct esk_rf *rf)
+{
+ int i, n = rf->naf, *count;
+ struct af_key *keys;
+ rf->af_by_pin = (int *)esk_mach_alloc(m, (size_t)(n ? n : 1) * sizeof *rf->af_by_pin);
+ rf->af_pin_first = (int *)esk_mach_alloc(m, (AF_PINS + 1) * sizeof *rf->af_pin_first);
+ rf->af_by_sig = (int *)esk_mach_alloc(m, (size_t)(n ? n : 1) * sizeof *rf->af_by_sig);
+ count = (int *)esk_mach_alloc(m, (AF_PINS + 1) * sizeof *count);
+ keys = (struct af_key *)esk_mach_alloc(m, (size_t)(n ? n : 1) * sizeof *keys);
+ if (!rf->af_by_pin || !rf->af_pin_first || !rf->af_by_sig || !count || !keys)
+ return ES_ENOMEM;
+ for (i = 0; i < n; i++)
+ if (rf->af_at[i] >= 0 && rf->af_at[i] < AF_PINS)
+ rf->af_pin_first[rf->af_at[i] + 1]++;
+ for (i = 0; i < AF_PINS; i++)
+ rf->af_pin_first[i + 1] += rf->af_pin_first[i];
+ for (i = 0; i < n; i++) {
+ int at = rf->af_at[i];
+ if (at >= 0 && at < AF_PINS)
+ rf->af_by_pin[rf->af_pin_first[at] + count[at]++] = i;
+ }
+ for (i = 0; i < n; i++) {
+ keys[i].sig = rf->af_sig[i];
+ keys[i].i = i;
+ }
+ if (n)
+ qsort(keys, (size_t)n, sizeof *keys, af_key_cmp);
+ for (i = 0; i < n; i++)
+ rf->af_by_sig[i] = keys[i].i;
+ return ES_OK;
+}
+
es_status esk_rf_alternates(struct esk_mach *m, const esk_json *alt, char *why, size_t cap)
{
struct esk_rf *rf = m->rf;
@@ -409,7 +457,7 @@ es_status esk_rf_alternates(struct esk_mach *m, const esk_json *alt, char *why,
}
}
rf->naf = at;
- return ES_OK;
+ return af_index(m, rf);
}
es_status esk_rf_dma_requests(struct esk_mach *m, const esk_json *map, char *why, size_t cap)
@@ -522,22 +570,37 @@ int esk_rf_dma_maps(const struct esk_rf *rf, const char *ctl, int stream, int ch
const char *esk_rf_af_signal(const struct esk_rf *rf, int port, int bit, int af, int k)
{
int i, at = port * 32 + bit;
- for (i = 0; rf && i < rf->naf; i++)
- if (rf->af_at[i] == at && rf->af_num[i] == af && k-- == 0)
- return rf->af_sig[i];
+ if (!rf || !rf->naf || !rf->af_pin_first || at < 0 || at >= AF_PINS)
+ return 0;
+ for (i = rf->af_pin_first[at]; i < rf->af_pin_first[at + 1]; i++) {
+ int e = rf->af_by_pin[i];
+ if (rf->af_num[e] == af && k-- == 0)
+ return rf->af_sig[e];
+ }
return 0;
}
int esk_rf_af_pin(const struct esk_rf *rf, const char *sig, int k, int *af)
{
- int i;
- for (i = 0; rf && sig && i < rf->naf; i++)
- if (!strcmp(rf->af_sig[i], sig) && k-- == 0) {
- if (af)
- *af = rf->af_num[i];
- return rf->af_at[i];
- }
- return -1;
+ int lo = 0, hi, e;
+ if (!rf || !sig || !rf->naf || !rf->af_by_sig || k < 0)
+ return -1;
+ hi = rf->naf;
+ while (lo < hi) { /* the first entry of `sig` */
+ int mid = lo + (hi - lo) / 2;
+ if (strcmp(rf->af_sig[rf->af_by_sig[mid]], sig) < 0)
+ lo = mid + 1;
+ else
+ hi = mid;
+ }
+ if (lo + k >= rf->naf)
+ return -1;
+ e = rf->af_by_sig[lo + k];
+ if (strcmp(rf->af_sig[e], sig) != 0)
+ return -1;
+ if (af)
+ *af = rf->af_num[e];
+ return rf->af_at[e];
}
static void sort_regs(struct esk_rf_reg **r, int n)
@@ -729,9 +792,21 @@ static struct esk_rf_reg *reg_at(struct esk_rf *rf, u32 a, int n)
struct esk_rf_periph *esk_rf_periph(struct esk_rf *rf, const char *name)
{
int i;
- for (i = 0; rf && i < rf->np; i++)
- if (!strcmp(rf->p[i].name, name))
+ unsigned slot;
+ if (!rf || !name)
+ return 0;
+ /* the models ask by the same few names at every edge: the last answer
+ * for this address, if it still names that peripheral */
+ slot = (unsigned)(((uintptr_t)name >> 3) % ES_COUNTOF(rf->pcache_p));
+ if (rf->pcache_name[slot] == name && rf->pcache_p[slot] >= rf->p && rf->pcache_p[slot] < rf->p + rf->np &&
+ !strcmp(rf->pcache_p[slot]->name, name))
+ return rf->pcache_p[slot];
+ for (i = 0; i < rf->np; i++)
+ if (!strcmp(rf->p[i].name, name)) {
+ rf->pcache_name[slot] = name;
+ rf->pcache_p[slot] = &rf->p[i];
return &rf->p[i];
+ }
return 0;
}
diff --git a/core/mcu/rf.h b/core/mcu/rf.h
index b59ed1c6..02533752 100644
--- a/core/mcu/rf.h
+++ b/core/mcu/rf.h
@@ -172,6 +172,14 @@ struct esk_rf {
int *af_at, *af_num;
const char **af_sig;
int naf;
+ /* their indexes (esk_rf_alternates builds them): the entries of pin
+ * `at` are af_by_pin[af_pin_first[at]] to [af_pin_first[at + 1] - 1],
+ * in the table's order; af_by_sig holds them all by signal name, a
+ * name's entries in the table's order */
+ int *af_by_pin, *af_pin_first, *af_by_sig;
+ /* esk_rf_periph's last answers, by the name's address */
+ struct esk_rf_periph *pcache_p[16];
+ const char *pcache_name[16];
/* the MCU description's DMA request mapping (dma_requests): the
* controller, stream, channel and request of each entry */
const char **dma_ctl, **dma_sig;
diff --git a/core/mcu/stm32/gpio_f4.c b/core/mcu/stm32/gpio_f4.c
index 4407c54d..57ee105f 100644
--- a/core/mcu/stm32/gpio_f4.c
+++ b/core/mcu/stm32/gpio_f4.c
@@ -109,16 +109,17 @@ static void update_once(struct f4gpio *g)
struct esk_mach *s = g->s;
u32 idr = 0, odr = (u32)g->odr->value, old = (u32)g->idr->value & 0xffffu;
int i, standby = esk_stm32f4_pwr_standby(s);
+ u32 wkup = esk_stm32f4_pwr_wkup_mask(s, g->port);
for (i = 0; i < 16; i++) {
u32 mode = field(g->moder, i, 2), pupd = field(g->pupdr, i, 2);
int od = (int)field(g->otyper, i, 1), lvl, ext = 0;
esk_mach_pin o;
memset(&o, 0, sizeof o);
o.drive = -1;
- if (standby || esk_stm32f4_pwr_wkup_pin(s, g->port, i)) {
+ if (standby || (wkup >> i & 1u)) {
/* Standby lets the pins go; an enabled WKUP pin is an input
* with a pull-down (pwr_f4.c) */
- o.pull = esk_stm32f4_pwr_wkup_pin(s, g->port, i) ? 2 : 0;
+ o.pull = (wkup >> i & 1u) ? 2 : 0;
} else if (mode == 3) {
o.analog = 1;
} else {
@@ -380,6 +381,7 @@ void *esk_stm32f4_gpio_create(struct esk_mach *s, struct esk_rf_periph *p)
for (i = 0; i < 16; i++)
g.af_in[i] = -1;
s->pins_changed = esk_stm32f4_pins_changed;
+ s->outputs_read_back = 1; /* update_once: an output reads its own drive */
c = (struct f4gpio *)esk_mach_alloc(s, sizeof *c);
if (c)
*c = g;
diff --git a/core/mcu/stm32/otg_fs.c b/core/mcu/stm32/otg_fs.c
index d40bd04c..4dbb292b 100644
--- a/core/mcu/stm32/otg_fs.c
+++ b/core/mcu/stm32/otg_fs.c
@@ -1,8 +1,8 @@
/* SPDX-License-Identifier: Apache-2.0 */
/* Copyright 2026 Junior Deogracias */
/* otg_fs.c -- the STM32F4's USB on-the-go full-speed controller (RM0390
- * 30, the Synopsys DWC2 core) in device mode, over the register file's
- * OTGFS block (tools/importers/dwc2_regs.py), with the host on its cable
+ * 30, the Synopsys DWC2 core) in device and host mode, over the register
+ * file's OTGFS block (tools/importers/dwc2_regs.py), with the host on its cable
* (engine/usb.h, ADR-031: the bus as transactions).
*
* As the core does it:
@@ -52,12 +52,48 @@
* - the OTG_FS interrupt is a level: GAHBCFG's GINT and any GINTSTS bit
* GINTMSK lets through (DAINT, DAINTMSK and the endpoints' masks
* behind IEPINT and OEPINT).
+ *
+ * As a host (CMOD 1: GUSBCFG's FHMOD), as RM0390 30.5 and the HAL's HCD
+ * driver have it:
+ * - the port: with PPWR set, the pull-up of the device on the cable
+ * (engine/usb.h) is a connection (PCSTS, PCDET and HPRTINT), its end a
+ * disconnection (DISCINT, the port disabled, PENCHNG); PRST drives the
+ * reset's SE0 for as long as it is set, and its end enables the port
+ * (PENA, PENCHNG) at the device's speed (PSPD 1 full, 2 low); PENA
+ * written 1 disables it; PSUSP stops the frames, PRES drives resume
+ * (K) until it is cleared; PLSTS the line's state;
+ * - the frames: an SOF (a low-speed device's keep-alive) every HFIR's
+ * FRIVL clocks of HCFG's PHY clock (48 MHz full speed, 6 MHz low
+ * speed), GINTSTS's SOF and HFNUM's frame number and time left;
+ * - twelve channels: SETUP (DPID 3), OUT and IN transactions at the bus's
+ * bit time, OUT's packet once all its words are in the channel's
+ * transmit FIFO (the non-periodic one, or the periodic one for an
+ * interrupt or isochronous channel; HNPTXSTS and HPTXSTS their words
+ * free), one IN token for each CHENA (the HAL re-enables after each
+ * packet) with room for its packet in the receive FIFO; the device's
+ * ACK (the data toggle, PKTCNT and XFRSIZ counted down, XFRC and CHENA
+ * cleared after the last OUT packet), NAK, STALL, no answer (TXERR),
+ * more than MPSIZ (BBERR), a toggle not expected (DTERR); the channel
+ * stops after any answer but ACK until the program halts it or
+ * enables it again; an IN packet's data in the receive FIFO with
+ * status 2 and, after the last or a short one, status 3, whose taking
+ * is XFRC; CHDIS with CHENA halts the channel (CHH) once the bus is
+ * done with it, flushing what it had queued; an interrupt channel
+ * goes once a frame, in the frames of the parity ODDFRM asks, before
+ * the non-periodic ones; a transaction starts only if it fits in what
+ * is left of the frame;
+ * - HAINT, HAINTMSK, GINTSTS's HCINT, HPRTINT, NPTXFE and PTXFE (TXFELVL
+ * and PTXFELVL), the interrupt level as in device mode.
* Inside: the transactions come whole at the time the host part sends
- * them (engine/usb.h); the device's turnaround is not seen. Not yet: host
- * mode (the host channel registers are storage), the ID pin and OTG's SRP
- * and HNP, isochronous endpoints' frame parity and incomplete-transfer
- * interrupts, LPM, the battery-charging detection of GCCFG, the
- * OTG_FS_WKUP interrupt through
+ * them, or this core sends them as a host (engine/usb.h); the device's
+ * turnaround is not seen; a host channel's OUT words are kept per channel
+ * (the core keeps them in its shared FIFO in order: the words free are
+ * the same). Not yet: the ID pin and OTG's SRP and HNP, split
+ * transactions through a hub (one device on a cable), isochronous
+ * endpoints' frame parity and incomplete-transfer interrupts and a host's
+ * isochronous channels' timing, FRMOR, LPM, the battery-charging
+ * detection of GCCFG, the over-current input (the F446 has none: a
+ * board's switch reports it on a GPIO), the OTG_FS_WKUP interrupt through
* EXTI line 18, the SOF pulse on PA8, DMA (the F446's OTG_FS has none). */
#include
@@ -105,6 +141,19 @@
#define DOEPINT(n) (0xB08u + 0x20u * (u32)(n))
#define DOEPTSIZ(n) (0xB10u + 0x20u * (u32)(n))
#define PCGCCTL 0xE00u
+#define HNPTXSTS 0x02Cu
+#define HPTXFSIZ 0x100u
+#define HCFG 0x400u
+#define HFIR 0x404u
+#define HFNUM 0x408u
+#define HPTXSTS 0x410u
+#define HAINT 0x414u
+#define HAINTMSK 0x418u
+#define HPRT 0x440u
+#define HCCHAR(n) (0x500u + 0x20u * (u32)(n))
+#define HCINT(n) (0x508u + 0x20u * (u32)(n))
+#define HCINTMSK(n) (0x50Cu + 0x20u * (u32)(n))
+#define HCTSIZ(n) (0x510u + 0x20u * (u32)(n))
/* bits */
#define BVALOEN (1u << 6)
@@ -164,6 +213,45 @@
#define PKT_OUT_DATA 2u
#define PKT_OUT_DONE 3u
#define PKT_SETUP_DATA 6u
+/* host mode */
+#define NCH 12
+#define I_NPTXFE (1u << 5)
+#define I_HPRTINT (1u << 24)
+#define I_HCINT (1u << 25)
+#define I_PTXFE (1u << 26)
+#define I_DISCINT (1u << 29)
+#define I_RO_HOST (I_CMOD | I_OTGINT | I_RXFLVL | I_NPTXFE | I_HPRTINT | I_HCINT | I_PTXFE)
+#define PTXFELVL (1u << 8)
+#define FSLSS (1u << 2)
+#define PCSTS (1u << 0)
+#define PCDET (1u << 1)
+#define PENA (1u << 2)
+#define PENCHNG (1u << 3)
+#define POCCHNG (1u << 5)
+#define PRES (1u << 6)
+#define PSUSP (1u << 7)
+#define PRST (1u << 8)
+#define PPWR (1u << 12)
+#define CH_EPDIR (1u << 15)
+#define CH_ODDFRM (1u << 29)
+#define CH_CHDIS (1u << 30)
+#define CH_CHENA (1u << 31)
+#define HC_XFRC (1u << 0)
+#define HC_CHH (1u << 1)
+#define HC_STALL (1u << 3)
+#define HC_NAK (1u << 4)
+#define HC_ACK (1u << 5)
+#define HC_TXERR (1u << 7)
+#define HC_BBERR (1u << 8)
+#define HC_DTERR (1u << 10)
+#define PKT_IN_DATA 2u
+#define PKT_IN_DONE 3u
+#define EPTYP_CTRL 0u
+#define EPTYP_ISOC 1u
+#define EPTYP_INTR 3u
+#define DPID_DATA0 0u
+#define DPID_DATA1 2u
+#define DPID_SETUP 3u
#define STPPCLK (1u << 0)
#define GATEHCLK (1u << 1)
@@ -171,6 +259,27 @@ struct txq {
u32 rd, used; /* words, within the FIFO's place in the RAM */
};
+/* a host channel: its OUT data as written to its FIFO window (the core
+ * keeps it in the shared FIFO, in order: the program's view, the words
+ * free, is the same) */
+struct hch {
+ u32 w[FIFO_WORDS];
+ u32 rd, used;
+ int req; /* IN: a token asked for (each CHENA, one packet) */
+ int stopped; /* NAK, STALL, an error: no tokens until re-enabled */
+ int halting; /* CHDIS with CHENA: CHH once the bus is done with it */
+ u32 sent; /* the frame a periodic channel last went in */
+ int sent_ok;
+};
+
+/* the transaction on the bus: its answer, posted when its bits end */
+struct hxact {
+ int ch, kind, ans; /* kind: 0 SETUP, 1 OUT, 2 IN */
+ u32 len, words;
+ int data1;
+ u8 data[1024];
+};
+
struct otg {
struct esk_mach *s;
struct esk_rf_periph *p;
@@ -195,6 +304,17 @@ struct otg {
u32 mode_want; /* CMOD once mode_ps has passed */
int mode_wait;
int warned_fifo, warned_rx;
+ /* host mode: the port and the bus it drives */
+ struct hch ch[NCH];
+ struct hxact x;
+ int h_on; /* host mode as last seen */
+ int h_conn, h_speed; /* a device's pull-up seen (PCSTS), its speed */
+ int h_ena, h_rst, h_susp, h_res;
+ u32 h_frame;
+ u64 h_frame_ps; /* into the frame */
+ u64 h_busy_ps; /* the transaction on the bus: its bits still to go */
+ int h_busy, h_rr;
+ int h_kick; /* the program changed something: the bus looks again */
};
/* ---- registers --------------------------------------------------------- */
@@ -386,6 +506,10 @@ static u32 out_mps(const struct otg *o, int ep)
return ep == 0 ? ep0[rv(o, DIEPCTL(0)) & 3u] : rv(o, DOEPCTL(ep)) & 0x7ffu;
}
+static void host_refresh(struct otg *o);
+static void port_check(struct otg *o);
+static int host_clock(const struct otg *o);
+
/* the read-only bits that follow the core's state, and the interrupt */
static void refresh(struct otg *o)
{
@@ -395,6 +519,10 @@ static void refresh(struct otg *o)
rbits(o, GOTGINT, SEDET, 0); /* the session ended */
o->session = session;
rset(o, GOTGCTL, (rv(o, GOTGCTL) & ~(CIDSTS | BSVLD | (1u << 18) | (1u << 17))) | CIDSTS | (session ? BSVLD : 0u));
+ if (!device_mode(o)) {
+ host_refresh(o);
+ return;
+ }
rbits(o, DOEPCTL(0), rv(o, DIEPCTL(0)) & 3u, 3u);
for (ep = 0; ep < NEP; ep++) {
u32 d, s0, free = tx_free(o, ep), emp, iin;
@@ -444,7 +572,7 @@ static void refresh(struct otg *o)
* bus's idle time towards a suspend */
static void clock_check(struct otg *o)
{
- int on = o->mode_wait || (o->pulled && !o->suspended);
+ int on = o->mode_wait || (o->pulled && !o->suspended) || host_clock(o);
if (on && !o->running)
o->since = o->s->cycles;
esk_rf_clock(o->p, &o->running, on);
@@ -452,12 +580,481 @@ static void clock_check(struct otg *o)
static void after(struct otg *o)
{
+ port_check(o);
refresh(o);
tell_host(o);
refresh(o);
clock_check(o);
}
+/* ---- host mode: the port ---------------------------------------------------- */
+
+static int host_mode(const struct otg *o) { return (rv(o, GINTSTS) & I_CMOD) != 0; }
+
+static int host_live(const struct otg *o) { return o->h_ena && !o->h_susp && !o->h_rst; }
+
+static int ch_periodic(const struct otg *o, int n)
+{
+ u32 t = (rv(o, HCCHAR(n)) >> 18) & 3u;
+ return t == EPTYP_INTR || t == EPTYP_ISOC;
+}
+
+static int host_clock(const struct otg *o)
+{
+ int n;
+ if (!host_mode(o))
+ return 0;
+ if (host_live(o) || o->h_busy || o->h_kick)
+ return 1;
+ for (n = 0; n < NCH; n++)
+ if (o->ch[n].halting)
+ return 1;
+ return 0;
+}
+
+/* the bus state the port drives, to the device on the cable */
+static void h_bus(struct otg *o, int ev)
+{
+ if (o->s->host.usbh_bus)
+ o->s->host.usbh_bus(o->s->host.ctx, ev, o->h_frame & 0x7ffu);
+}
+
+/* the frame: HFIR's FRIVL clocks of the PHY's (HCFG's FSLSPCS: 48 MHz
+ * for full speed, 6 MHz for low speed; 0, the 30/60 MHz of an external
+ * PHY's, is taken as 60) */
+static u32 phy_mhz(const struct otg *o)
+{
+ u32 cs = rv(o, HCFG) & 3u;
+ return cs == 1u ? 48u : cs == 2u ? 6u : 60u;
+}
+
+static u64 frame_len(const struct otg *o)
+{
+ u64 f = rv(o, HFIR) & 0xffffu;
+ return (f ? f : 1u) * 1000000ull / phy_mhz(o);
+}
+
+static void new_frame(struct otg *o)
+{
+ o->h_frame_ps = 0;
+ o->h_frame = (o->h_frame + 1u) & 0x3fffu;
+ rbits(o, GINTSTS, I_SOF, 0);
+ h_bus(o, ESK_USB_SOF); /* an SOF at full speed, a keep-alive at low */
+}
+
+static void host_drop(struct otg *o)
+{
+ o->h_conn = o->h_ena = o->h_rst = o->h_susp = o->h_res = 0;
+ o->h_busy = 0;
+}
+
+/* the device's pull-up while the port is powered: a connection (PCDET)
+ * or its end (DISCINT; the port is disabled) */
+static void port_check(struct otg *o)
+{
+ int p = ESK_USB_DETACHED;
+ if (!host_mode(o)) {
+ if (o->h_on)
+ host_drop(o);
+ o->h_on = 0;
+ return;
+ }
+ o->h_on = 1;
+ if ((rv(o, HPRT) & PPWR) && !esk_rf_gated(o->p) && o->s->host.usbh_pullup)
+ p = o->s->host.usbh_pullup(o->s->host.ctx);
+ if (p != ESK_USB_DETACHED && !o->h_conn) {
+ o->h_conn = 1;
+ o->h_speed = p;
+ rbits(o, HPRT, PCDET, 0);
+ } else if (p == ESK_USB_DETACHED && o->h_conn) {
+ if (o->h_ena)
+ rbits(o, HPRT, PENCHNG, 0);
+ host_drop(o);
+ rbits(o, GINTSTS, I_DISCINT, 0);
+ }
+}
+
+static void hprt_write(struct otg *o, u32 v, u32 old)
+{
+ u32 r = rv(o, HPRT);
+ if ((v & PENA) && o->h_ena) {
+ /* PENA written 1: the program disables the port */
+ o->h_ena = o->h_susp = 0;
+ rbits(o, HPRT, PENCHNG, 0);
+ }
+ port_check(o); /* PPWR */
+ if ((r & PRST) && !o->h_rst && o->h_conn) {
+ /* the reset's SE0: the port is disabled while it lasts */
+ o->h_rst = 1;
+ o->h_ena = o->h_susp = o->h_res = 0;
+ h_bus(o, ESK_USB_RESET);
+ } else if (!(r & PRST) && o->h_rst) {
+ int p = o->s->host.usbh_pullup ? o->s->host.usbh_pullup(o->s->host.ctx) : ESK_USB_DETACHED;
+ o->h_rst = 0;
+ h_bus(o, ESK_USB_RESET_END);
+ if (p != ESK_USB_DETACHED) {
+ /* enabled at the speed the device's pull-up says */
+ o->h_speed = p;
+ o->h_ena = 1;
+ rbits(o, HPRT, PENCHNG, 0);
+ new_frame(o);
+ }
+ }
+ if ((v & PSUSP) && o->h_ena && !o->h_susp)
+ o->h_susp = 1; /* no more SOFs: the device suspends after 3 ms */
+ if ((r & PRES) && !(old & PRES) && o->h_conn && !o->h_res) {
+ o->h_res = 1;
+ h_bus(o, ESK_USB_RESUME);
+ } else if (!(r & PRES) && o->h_res) {
+ o->h_res = 0;
+ h_bus(o, ESK_USB_RESUME_END);
+ if (o->h_susp) {
+ o->h_susp = 0;
+ new_frame(o);
+ }
+ }
+ rbits(o, HPRT, 0, PSUSP);
+}
+
+/* ---- host mode: the channels ------------------------------------------------ */
+
+/* the non-periodic (0) or periodic (1) transmit FIFO: its depth, the words
+ * in it */
+static u32 htx_depth(const struct otg *o, int per)
+{
+ u32 d = (per ? rv(o, HPTXFSIZ) : rv(o, DIEPTXF0)) >> 16;
+ return d < 16u ? 16u : d;
+}
+
+static u32 htx_used(const struct otg *o, int per)
+{
+ u32 n = 0;
+ int c;
+ for (c = 0; c < NCH; c++)
+ if (ch_periodic(o, c) == per)
+ n += o->ch[c].used;
+ return n;
+}
+
+static void hch_push(struct otg *o, int n, u32 w)
+{
+ struct hch *h = &o->ch[n];
+ int per = ch_periodic(o, n);
+ if (htx_used(o, per) >= htx_depth(o, per) || h->used >= FIFO_WORDS)
+ return; /* the FIFO is full: the word is lost, as on the part */
+ h->w[(h->rd + h->used) % FIFO_WORDS] = w;
+ h->used++;
+}
+
+static void hch_drop(struct otg *o, int n)
+{
+ o->ch[n].rd = o->ch[n].used = 0;
+}
+
+static void hch_halted(struct otg *o, int n)
+{
+ struct hch *h = &o->ch[n];
+ h->halting = h->req = h->stopped = 0;
+ hch_drop(o, n); /* its requests and the data still queued are flushed */
+ rbits(o, HCCHAR(n), 0, CH_CHENA | CH_CHDIS);
+ rbits(o, HCINT(n), HC_CHH, 0);
+}
+
+static void hcchar_write(struct otg *o, int n, u32 old)
+{
+ u32 c = rv(o, HCCHAR(n));
+ struct hch *h = &o->ch[n];
+ if ((c & CH_CHDIS) && (c & CH_CHENA)) {
+ h->halting = 1; /* CHENA stays set until the halt is done */
+ } else if ((c & CH_CHENA) && !h->halting) {
+ h->stopped = 0;
+ if (c & CH_EPDIR)
+ h->req = 1; /* one IN token per enable (the HAL's re-enable) */
+ if (!(old & CH_CHENA))
+ h->sent_ok = 0;
+ }
+}
+
+static es_time_t hx_time(const struct otg *o, int len, int hs)
+{
+ return o->h_speed == ESK_USB_LOW_SPEED ? esk_usb_ls_time(len, hs) : esk_usb_fs_time(len, hs);
+}
+
+/* the channel's next transaction, if it can go: its kind and length */
+static int ch_ready(const struct otg *o, int n, int *kind, u32 *len)
+{
+ u32 c = rv(o, HCCHAR(n)), t = rv(o, HCTSIZ(n)), mps = c & 0x7ffu;
+ u32 pkt = (t >> 19) & 0x3ffu, xfr = t & 0x7ffffu;
+ const struct hch *h = &o->ch[n];
+ if (!(c & CH_CHENA) || (c & CH_CHDIS) || h->halting || h->stopped || !pkt)
+ return 0;
+ if (ch_periodic(o, n)) {
+ /* once a frame, in the frames of the parity ODDFRM asks */
+ if (((o->h_frame & 1u) != 0) != ((c & CH_ODDFRM) != 0))
+ return 0;
+ if (h->sent_ok && h->sent == o->h_frame)
+ return 0;
+ }
+ if (c & CH_EPDIR) {
+ if (!h->req || o->rx_used + 2u + (mps + 3u) / 4u > rx_depth(o))
+ return 0; /* no token without room for the packet */
+ *kind = 2;
+ *len = mps;
+ } else if (((t >> 29) & 3u) == DPID_SETUP) {
+ *kind = 0;
+ *len = 8;
+ if (h->used < 2u)
+ return 0;
+ } else {
+ *kind = 1;
+ *len = xfr < mps ? xfr : mps;
+ if (h->used < (*len + 3u) / 4u)
+ return 0; /* the packet is not all in the FIFO yet */
+ }
+ return 1;
+}
+
+static void hx_start(struct otg *o, int n, int kind, u32 len)
+{
+ struct hxact *x = &o->x;
+ struct hch *h = &o->ch[n];
+ u32 c = rv(o, HCCHAR(n)), t = rv(o, HCTSIZ(n)), i;
+ int addr = (int)((c >> 22) & 0x7fu), ep = (int)((c >> 11) & 0xfu), dlen;
+ struct esk_mach_host *hs = &o->s->host;
+ x->ch = n;
+ x->kind = kind;
+ x->len = len;
+ x->data1 = 0;
+ x->ans = ESK_USB_NONE;
+ if (kind != 2) {
+ for (i = 0; i < len; i++)
+ x->data[i] = (u8)(h->w[(h->rd + i / 4u) % FIFO_WORDS] >> (8u * (i & 3u)));
+ if (kind == 0 && hs->usbh_setup)
+ x->ans = hs->usbh_setup(hs->ctx, addr, ep, x->data);
+ else if (kind == 1 && hs->usbh_out)
+ x->ans = hs->usbh_out(hs->ctx, addr, ep, ((t >> 29) & 3u) == DPID_DATA1, x->data, len);
+ dlen = (int)len;
+ } else {
+ u32 got = 0;
+ int d1 = 0;
+ if (hs->usbh_in)
+ x->ans = hs->usbh_in(hs->ctx, addr, ep, x->data, sizeof x->data, &got, &d1);
+ x->len = got;
+ x->data1 = d1;
+ dlen = x->ans == ESK_USB_ACK ? (int)got : -1;
+ }
+ if (ch_periodic(o, n)) {
+ h->sent = o->h_frame;
+ h->sent_ok = 1;
+ }
+ o->h_busy = 1;
+ o->h_busy_ps = hx_time(o, dlen, x->ans != ESK_USB_NONE);
+ if (!o->h_busy_ps)
+ o->h_busy_ps = 1;
+}
+
+/* the transaction's bits are done: its answer on the channel */
+static void hx_end(struct otg *o)
+{
+ struct hxact *x = &o->x;
+ int n = x->ch;
+ struct hch *h = &o->ch[n];
+ u32 c = rv(o, HCCHAR(n)), t = rv(o, HCTSIZ(n)), mps = c & 0x7ffu;
+ u32 pkt = (t >> 19) & 0x3ffu, xfr = t & 0x7ffffu, dpid = (t >> 29) & 3u, words;
+ o->h_busy = 0;
+ if (x->ans != ESK_USB_ACK) {
+ h->stopped = 1;
+ h->req = 0;
+ rbits(o, HCINT(n), x->ans == ESK_USB_NAK ? HC_NAK : x->ans == ESK_USB_STALL ? HC_STALL : HC_TXERR, 0);
+ return;
+ }
+ if (x->kind == 2) {
+ h->req = 0;
+ if (x->len > mps) {
+ h->stopped = 1; /* babble: more than MPSIZ */
+ rbits(o, HCINT(n), HC_BBERR, 0);
+ return;
+ }
+ if ((x->data1 != 0) != (dpid == DPID_DATA1)) {
+ h->stopped = 1; /* the PID the channel did not expect */
+ rbits(o, HCINT(n), HC_DTERR, 0);
+ return;
+ }
+ (void)rx_push(o, (u32)n | x->len << 4 | dpid << 15 | PKT_IN_DATA << 17, x->data, x->len);
+ dpid = dpid == DPID_DATA1 ? DPID_DATA0 : DPID_DATA1;
+ pkt--;
+ xfr = xfr > x->len ? xfr - x->len : 0;
+ rbits(o, HCINT(n), HC_ACK, 0);
+ if (!pkt || x->len < mps) /* XFRC when this status is taken */
+ (void)rx_push(o, (u32)n | dpid << 15 | PKT_IN_DONE << 17, 0, 0);
+ } else {
+ words = (x->len + 3u) / 4u;
+ h->rd = (h->rd + words) % FIFO_WORDS;
+ h->used = h->used > words ? h->used - words : 0;
+ rbits(o, HCINT(n), HC_ACK, 0);
+ if (x->kind == 0)
+ dpid = DPID_DATA1; /* the data stage starts at DATA1 */
+ else
+ dpid = dpid == DPID_DATA1 ? DPID_DATA0 : DPID_DATA1;
+ pkt--;
+ xfr = xfr > x->len ? xfr - x->len : 0;
+ if (!pkt) {
+ rbits(o, HCINT(n), HC_XFRC, 0);
+ rbits(o, HCCHAR(n), 0, CH_CHENA);
+ }
+ }
+ rset(o, HCTSIZ(n), (t & ~(0x3ffu << 19) & ~0x7ffffu & ~(3u << 29)) | pkt << 19 | xfr | dpid << 29);
+}
+
+/* the bus is free: the halts asked for, then the next transaction, the
+ * periodic channels first */
+static void host_pick(struct otg *o)
+{
+ int n, k, per, kind;
+ u32 len;
+ for (n = 0; n < NCH; n++)
+ if (o->ch[n].halting && !(o->h_busy && o->x.ch == n))
+ hch_halted(o, n);
+ if (o->h_busy || !host_live(o))
+ return;
+ for (per = 1; per >= 0; per--)
+ for (k = 0; k < NCH; k++) {
+ n = (o->h_rr + k) % NCH;
+ if (ch_periodic(o, n) != per || !ch_ready(o, n, &kind, &len))
+ continue;
+ if (o->h_frame_ps + (u64)hx_time(o, (int)len, 1) > frame_len(o))
+ continue; /* not in what is left of the frame */
+ o->h_rr = (n + 1) % NCH;
+ hx_start(o, n, kind, len);
+ return;
+ }
+}
+
+/* the host's time: transactions' ends and frames */
+static int host_run(struct otg *o, u64 ps)
+{
+ int ev = 0, guard;
+ if (o->h_kick) {
+ o->h_kick = 0;
+ host_pick(o);
+ ev = 1;
+ }
+ for (guard = 0; guard < 100000; guard++) {
+ int live = host_live(o);
+ u64 fl = frame_len(o), step = ~0ull;
+ if (live)
+ step = fl > o->h_frame_ps ? fl - o->h_frame_ps : 0;
+ if (o->h_busy && o->h_busy_ps < step)
+ step = o->h_busy_ps;
+ if (step == ~0ull)
+ break;
+ if (step > ps) {
+ if (live)
+ o->h_frame_ps += ps;
+ if (o->h_busy)
+ o->h_busy_ps -= ps;
+ break;
+ }
+ ps -= step;
+ if (live)
+ o->h_frame_ps += step;
+ if (o->h_busy) {
+ o->h_busy_ps -= step;
+ if (!o->h_busy_ps)
+ hx_end(o);
+ }
+ if (live && o->h_frame_ps >= fl)
+ new_frame(o);
+ host_pick(o);
+ ev = 1;
+ }
+ return ev;
+}
+
+static u64 host_next(const struct otg *o)
+{
+ u64 left = ~0ull, fl;
+ if (!host_mode(o))
+ return left;
+ if (o->h_kick)
+ return 0;
+ if (host_live(o)) {
+ fl = frame_len(o);
+ left = fl > o->h_frame_ps ? fl - o->h_frame_ps : 0;
+ }
+ if (o->h_busy && o->h_busy_ps < left)
+ left = o->h_busy_ps;
+ return left;
+}
+
+static void host_refresh(struct otg *o)
+{
+ u32 haint = 0, st, hprt, lvl, used, d, ft;
+ int n, np_empty, p_empty;
+ u64 fl = frame_len(o);
+ for (n = 0; n < NCH; n++)
+ if (rv(o, HCINT(n)) & rv(o, HCINTMSK(n)))
+ haint |= 1u << n;
+ rset(o, HAINT, haint);
+ /* the transmit FIFOs' words free; the request queues never fill (the
+ * HAL's halt reads their space) */
+ used = htx_used(o, 0);
+ d = htx_depth(o, 0);
+ rset(o, HNPTXSTS, (used >= d ? 0u : d - used) | 8u << 16);
+ np_empty = (rv(o, GAHBCFG) & TXFELVL) ? used == 0 : used <= d / 2u;
+ used = htx_used(o, 1);
+ d = htx_depth(o, 1);
+ rset(o, HPTXSTS, (used >= d ? 0u : d - used) | 8u << 16);
+ p_empty = (rv(o, GAHBCFG) & PTXFELVL) ? used == 0 : used <= d / 2u;
+ rbits(o, HCFG, FSLSS, 0); /* FS and LS only */
+ ft = host_live(o) && fl > o->h_frame_ps ? (u32)((fl - o->h_frame_ps) * phy_mhz(o) / 1000000ull) : 0u;
+ rset(o, HFNUM, (o->h_frame & 0xffffu) | (ft & 0xffffu) << 16);
+ hprt = rv(o, HPRT) & ~(PCSTS | PENA | PSUSP | (3u << 10) | (3u << 17));
+ if (o->h_conn) {
+ u32 j = o->h_speed == ESK_USB_LOW_SPEED ? 2u : 1u; /* J: D- high at low speed, D+ at full */
+ hprt |= PCSTS | (o->h_speed == ESK_USB_LOW_SPEED ? 2u : 1u) << 17;
+ if (!o->h_rst)
+ hprt |= (o->h_res ? 3u - j : j) << 10;
+ }
+ if (o->h_ena)
+ hprt |= PENA;
+ if (o->h_susp)
+ hprt |= PSUSP;
+ rset(o, HPRT, hprt);
+ st = rv(o, GINTSTS) & ~I_RO_HOST;
+ st |= I_CMOD;
+ if (rv(o, GOTGINT))
+ st |= I_OTGINT;
+ if (o->rx_statuses && !o->rx_left)
+ st |= I_RXFLVL;
+ if (np_empty)
+ st |= I_NPTXFE;
+ if (p_empty)
+ st |= I_PTXFE;
+ if (hprt & (PCDET | PENCHNG | POCCHNG))
+ st |= I_HPRTINT;
+ if (haint & rv(o, HAINTMSK))
+ st |= I_HCINT;
+ rset(o, GINTSTS, st);
+ lvl = (rv(o, GAHBCFG) & GINT) && (st & rv(o, GINTMSK));
+ esk_rf_irq_level(o->p, o->irq, (int)lvl, &o->level);
+}
+
+static void host_changed(void *ctx)
+{
+ struct otg *o = (struct otg *)ctx;
+ o->h_kick = host_mode(o);
+ after(o);
+}
+
+static void host_wakeup(void *ctx)
+{
+ struct otg *o = (struct otg *)ctx;
+ if (host_mode(o) && o->h_susp)
+ rbits(o, GINTSTS, I_WKUINT, 0); /* the device's remote wakeup (K) */
+ after(o);
+}
+
/* ---- the core's resets --------------------------------------------------- */
static void core_soft_reset(struct otg *o)
@@ -497,6 +1094,17 @@ static void core_soft_reset(struct otg *o)
o->setup_done = 0;
o->addr = 0;
o->addr_pending = 0;
+ /* the host channels; the port follows its line */
+ for (ep = 0; ep < NCH; ep++) {
+ rset(o, HCCHAR(ep), 0);
+ rset(o, HCINT(ep), 0);
+ rset(o, HCINTMSK(ep), 0);
+ rset(o, HCTSIZ(ep), 0);
+ memset(&o->ch[ep], 0, sizeof o->ch[ep]);
+ }
+ rset(o, HAINT, 0);
+ rset(o, HAINTMSK, 0);
+ o->h_busy = 0;
}
/* CSRST completes once the 48 MHz clock runs: the PHY's domain resets
@@ -516,6 +1124,16 @@ static void take_status(struct otg *o, u32 st)
int ep = (int)(st & 0xfu);
u32 pkt = (st >> 17) & 0xfu, bcnt = (st >> 4) & 0x7ffu;
o->rx_statuses--;
+ if (host_mode(o)) {
+ /* a channel's IN data (2), its transfer done (3): XFRC */
+ o->rx_left = pkt == PKT_IN_DATA ? (bcnt + 3u) / 4u : 0;
+ if (pkt == PKT_IN_DONE && ep < NCH) {
+ rbits(o, HCINT(ep), HC_XFRC, 0);
+ rbits(o, HCCHAR(ep), 0, CH_CHENA);
+ }
+ o->h_kick = 1; /* room in the receive FIFO */
+ return;
+ }
o->rx_left = pkt == PKT_OUT_DATA || pkt == PKT_SETUP_DATA ? (bcnt + 3u) / 4u : 0;
if (ep >= NEP)
return;
@@ -634,7 +1252,11 @@ static void otg_write(void *ctx, u32 off, int n, u32 v)
int ep;
if (off >= WIN_BASE && off < WIN_END) {
ep = (int)((off - WIN_BASE) >> 12);
- if (ep < NEP)
+ if (host_mode(o)) {
+ if (ep < NCH)
+ hch_push(o, ep, v);
+ o->h_kick = 1;
+ } else if (ep < NEP)
tx_push(o, ep, v);
after(o);
return;
@@ -704,7 +1326,14 @@ static void otg_write(void *ctx, u32 off, int n, u32 v)
if (((rv(o, DCFG) ^ old) >> 4) & 0x7fu)
o->addr_pending = 1;
break;
+ case HPRT:
+ if (host_mode(o))
+ hprt_write(o, v, old);
+ break;
default:
+ for (ep = 0; ep < NCH; ep++)
+ if (a == HCCHAR(ep) && host_mode(o))
+ hcchar_write(o, ep, old);
for (ep = 0; ep < NEP; ep++) {
if (a == DIEPCTL(ep))
ctl_in(o, ep, v, old);
@@ -713,6 +1342,8 @@ static void otg_write(void *ctx, u32 off, int n, u32 v)
}
break;
}
+ if (host_mode(o))
+ o->h_kick = 1;
after(o);
}
@@ -951,6 +1582,8 @@ static void otg_tick(void *ctx, u32 cycles)
changed = 1;
}
}
+ if (host_mode(o) && host_run(o, ps))
+ changed = 1;
if (o->pulled && !o->suspended && !o->reset_on && !o->resume_on) {
o->idle_ps += ps;
if (o->idle_ps >= SUSPEND_PS) {
@@ -976,9 +1609,11 @@ static int otg_next(void *ctx, u32 *cycles)
left = MODE_PS - o->mode_ps;
if (o->pulled && !o->suspended && !o->reset_on && !o->resume_on && SUSPEND_PS - o->idle_ps < left)
left = SUSPEND_PS - o->idle_ps;
+ if (host_next(o) < left)
+ left = host_next(o);
if (left == ~0ull)
return 0;
- c = left / 1000000ull * (core_hz(o) / 1000000u) + 1u;
+ c = left / 1000000ull * (core_hz(o) / 1000000u) + (left % 1000000ull) * (core_hz(o) / 1000000u) / 1000000ull + 1u;
*cycles = c > 0xffffffffu ? 0xffffffffu : (u32)c;
return 1;
}
@@ -1004,6 +1639,12 @@ static void otg_reset(void *ctx)
o->level = 0;
o->warned_fifo = o->warned_rx = 0;
o->pulled = 0;
+ memset(o->ch, 0, sizeof o->ch);
+ host_drop(o);
+ o->h_on = o->h_kick = 0;
+ o->h_frame = 0;
+ o->h_frame_ps = o->h_busy_ps = 0;
+ o->h_rr = 0;
if (o->s->host.pin_watch)
(void)o->s->host.pin_watch(o->s->host.ctx, 0, 9, 0, VBUS_VALID_V);
if (was && o->s->host.usb_changed)
@@ -1047,5 +1688,7 @@ void *esk_stm32_otg_fs_create(struct esk_mach *m, struct esk_rf_periph *p)
m->usb.setup = usb_setup;
m->usb.out = usb_out;
m->usb.in = usb_in;
+ m->usb.host_changed = host_changed;
+ m->usb.host_wakeup = host_wakeup;
return o;
}
diff --git a/core/mcu/stm32/pwr_f4.c b/core/mcu/stm32/pwr_f4.c
index 57c17b15..0b0882e4 100644
--- a/core/mcu/stm32/pwr_f4.c
+++ b/core/mcu/stm32/pwr_f4.c
@@ -162,6 +162,19 @@ int esk_stm32f4_pwr_wkup_pin(struct esk_mach *m, int port, int pin)
return w && b && (w->csr->value & b) != 0;
}
+u32 esk_stm32f4_pwr_wkup_mask(struct esk_mach *m, int port)
+{
+ struct pwr *w = f4_pwr(m);
+ u32 mask = 0;
+ int pin;
+ if (!w || (port != 0 && port != 2))
+ return 0;
+ for (pin = 0; pin < 16; pin++)
+ if (wkup_bit(port, pin) && (w->csr->value & wkup_bit(port, pin)))
+ mask |= 1u << pin;
+ return mask;
+}
+
void esk_stm32f4_pwr_pin_edge(struct esk_mach *m, int port, int pin, int level)
{
if (level && esk_stm32f4_pwr_wkup_pin(m, port, pin))
diff --git a/core/mcu/stm32/stm32.h b/core/mcu/stm32/stm32.h
index 508bbd83..78de4bd3 100644
--- a/core/mcu/stm32/stm32.h
+++ b/core/mcu/stm32/stm32.h
@@ -69,6 +69,8 @@ void esk_stm32f4_rcc_stop(struct esk_mach *m);
* Standby (its pins let go) */
void esk_stm32f4_pwr_wakeup(struct esk_mach *m);
int esk_stm32f4_pwr_wkup_pin(struct esk_mach *m, int port, int pin);
+/* the port's enabled WKUP pins, a bit each */
+u32 esk_stm32f4_pwr_wkup_mask(struct esk_mach *m, int port);
void esk_stm32f4_pwr_pin_edge(struct esk_mach *m, int port, int pin, int level);
void esk_stm32f4_pwr_pins_settled(struct esk_mach *m);
int esk_stm32f4_pwr_standby(struct esk_mach *m);
diff --git a/core/model/digital.c b/core/model/digital.c
index 00bde436..d3a66048 100644
--- a/core/model/digital.c
+++ b/core/model/digital.c
@@ -749,7 +749,7 @@ static const esk_model_ops vt_ops = { "virtual_terminal", sizeof(vt_state), vt_c
static const esk_model_ops *const digital_table[] = {
&wire_ops, &none_ops, &buffer_ops, &inverter_ops, &and_ops, &nand_ops, &or_ops, &nor_ops, &xor_ops, &xnor_ops,
&tristate_ops, &dff_ops, &clock_ops, &pulse_ops, &level_ops, &pull_ops, &button_ops, &switch_ops,
- &indicator_ops, &rgb_indicator_ops, &probe_ops, &vt_ops, &hc_ops, &esk_wifi_ap_ops, &esk_ble_scanner_ops, &esk_web_server_ops, &esk_wifi_scanner_ops, &esk_air_sniffer_ops, &esk_usb_host_ops,
+ &indicator_ops, &rgb_indicator_ops, &probe_ops, &vt_ops, &hc_ops, &esk_wifi_ap_ops, &esk_ble_scanner_ops, &esk_web_server_ops, &esk_wifi_scanner_ops, &esk_air_sniffer_ops, &esk_usb_host_ops, &esk_usb_mouse_ops, &esk_st7735_ops, &esk_usb_flash_drive_ops,
&esk_ble_central_ops, &esk_logic_analyzer_ops
};
diff --git a/core/model/registry.h b/core/model/registry.h
index 72873ae0..bb06c00a 100644
--- a/core/model/registry.h
+++ b/core/model/registry.h
@@ -31,6 +31,12 @@ extern const esk_model_ops esk_wifi_scanner_ops;
extern const esk_model_ops esk_air_sniffer_ops;
/* a computer's USB port: the host on a USB cable (usb_host.c) */
extern const esk_model_ops esk_usb_host_ops;
+/* a USB mouse, a low-speed HID boot mouse (usb_mouse.c) */
+extern const esk_model_ops esk_usb_mouse_ops;
+/* a Sitronix ST7735R TFT controller and its 128 x 160 panel (st7735.c) */
+extern const esk_model_ops esk_st7735_ops;
+/* a USB flash drive: Bulk-Only mass storage, SCSI, a FAT16 medium (usb_flash_drive.c) */
+extern const esk_model_ops esk_usb_flash_drive_ops;
/* a web server on the internet beyond the routers (web_server.c) */
extern const esk_model_ops esk_web_server_ops;
/* an 8-channel logic analyzer, the Saleae Logic 8 (logic_analyzer.c) */
diff --git a/core/model/st7735.c b/core/model/st7735.c
new file mode 100644
index 00000000..51a68807
--- /dev/null
+++ b/core/model/st7735.c
@@ -0,0 +1,480 @@
+/* SPDX-License-Identifier: Apache-2.0 */
+/* Copyright 2026 Junior Deogracias */
+/* st7735.c -- a Sitronix ST7735R TFT controller with its 1.8" 128 x 160
+ * panel (the Adafruit 1.8" TFT's), on its 4-wire serial interface.
+ *
+ * As the controller does it (ST7735R datasheet, sections 9 and 10):
+ * - the 4-wire serial interface: CSX low selects it, each SDA bit is
+ * taken on SCL's rising edge, MSB first; D/CX, read with the eighth
+ * bit, says command (low) or data (high); CSX high resets the bit
+ * counter but not the command, so a RAMWR's data may come in several
+ * selections (as ST's BSP sends it);
+ * - the frame memory, 128 x 160 at 18 bits a pixel (GM 128 x 160), its
+ * content at power-up whatever the cells hold (noise here, the same
+ * each time);
+ * - CASET and RASET set the window, each address as its two bytes come
+ * (one cut short after its start keeps the end: ST's st7735 driver's
+ * st7735_SetCursor relies on it), RAMWR writes it from its start,
+ * column by column then row by row as MADCTL's MV, MX and MY order
+ * them, wrapping at its end; COLMOD's 12, 16 and 18 bits a pixel;
+ * MADCTL's RGB bit swaps red and blue on the panel;
+ * - SLPIN, SLPOUT, DISPOFF, DISPON, INVOFF, INVON, IDMOFF, IDMON (eight
+ * colours: each component's top bit), NORON, PTLON with PTLAR (the
+ * rows outside the partial area blank), SWRESET and the RESX pin;
+ * a byte within 5 ms of a reset (the pin, SWRESET, power-up: the
+ * datasheet's reset cancel time in sleep in mode, a maximum) is
+ * taken, as a typical part takes it, and warned of once;
+ * - after a reset: sleep in, display off, normal mode, no inversion,
+ * COLMOD 18 bits, MADCTL 0, the window the whole memory;
+ * - the panel: blank (white, a normally-white panel) while sleeping or
+ * the display is off, dark without power or backlight (LED low); its
+ * scan starts at its bottom right as the module is held, frame memory
+ * (0, 0) there (MADCTL's MX and MY set: an upright image).
+ * The parameters of the panel commands (frame rate B1 to B3, inversion
+ * B4, power C0 to C5, gamma E0, E1 and GAMSET) are taken and kept, with
+ * no effect on the image.
+ * Not modelled: the 3-wire (9-bit) interface and the parallel ones, the
+ * read commands (RDDID, RDDST, RAMRD: SDA is never driven), vertical
+ * scrolling (SCRLAR, VSCSAD), the tearing effect output, the frame rate
+ * and the panel's response time, the gamma curves' effect on colours. */
+#include
+#include
+
+#include "engine/engine.h"
+#include "model/registry.h"
+
+#define W 128
+#define H 160
+#define RESET_PS 5000000000ull /* 5 ms */
+
+typedef struct st7735_state {
+ esk_pin *vcc, *scl, *sda, *cs, *dc, *rst, *led;
+ uint32_t mem[W * H]; /* RGB666: r << 12 | g << 6 | b */
+ int bits;
+ uint8_t shift;
+ uint8_t cmd;
+ int nparam;
+ uint8_t param[16];
+ int ramwr;
+ uint8_t px[3];
+ int npx;
+ int xs, xe, ys, ye, x, y;
+ int sleeping, display_on, inverted, idle, partial;
+ int psr, per; /* the partial area's rows */
+ uint8_t colmod, madctl;
+ int last_scl, powered, in_reset;
+ es_time_t busy_until;
+ int warned_busy;
+ uint64_t ram_writes, commands;
+ int probe_x, probe_y;
+ /* the image as last rendered, until it changes */
+ int dirty;
+ uint32_t white;
+ char hex[12];
+ char b64[(W * H * 2 + 2) / 3 * 4 + 1];
+} st7735_state;
+
+static int lv(esk_pin *p, int dflt)
+{
+ esk_logic l;
+ if (!p || !p->net)
+ return dflt;
+ l = esk_pin_read(p);
+ return l == ESK_L1 ? 1 : l == ESK_L0 ? 0 : dflt;
+}
+
+static int powered(st7735_state *s)
+{
+ if (!s->vcc || !s->vcc->net)
+ return 1;
+ if (s->vcc->watch_on)
+ return s->vcc->watch_state == ESK_L1;
+ return esk_net_value(s->vcc->net) == ESK_L1;
+}
+
+/* what the cells hold at power-up: the same noise each time */
+static void noise(st7735_state *s)
+{
+ uint32_t x = 0x2545F491u, i;
+ for (i = 0; i < W * H; i++) {
+ x ^= x << 13;
+ x ^= x >> 17;
+ x ^= x << 5;
+ s->mem[i] = x & 0x3FFFFu;
+ }
+}
+
+static void soft_reset(st7735_state *s, esk_comp *c)
+{
+ s->bits = 0;
+ s->cmd = 0;
+ s->nparam = 0;
+ s->ramwr = 0;
+ s->npx = 0;
+ s->xs = 0;
+ s->xe = W - 1;
+ s->ys = 0;
+ s->ye = H - 1;
+ s->x = s->y = 0;
+ s->sleeping = 1;
+ s->display_on = 0;
+ s->inverted = s->idle = s->partial = 0;
+ s->psr = 0;
+ s->per = H - 1;
+ s->colmod = 6;
+ s->madctl = 0;
+ s->dirty = 1;
+ s->busy_until = esk_comp_now(c) + RESET_PS;
+}
+
+/* the window's next address, as MADCTL orders it */
+static void store(st7735_state *s, uint32_t rgb)
+{
+ int mv = (s->madctl >> 5) & 1, mx = (s->madctl >> 6) & 1, my = (s->madctl >> 7) & 1;
+ int maxc = mv ? H - 1 : W - 1, maxr = mv ? W - 1 : H - 1;
+ int c = mx ? maxc - s->x : s->x, r = my ? maxr - s->y : s->y;
+ int px = mv ? r : c, py = mv ? c : r;
+ if (px >= 0 && px < W && py >= 0 && py < H) {
+ s->mem[py * W + px] = rgb;
+ s->dirty = 1;
+ }
+ s->ram_writes++;
+ if (++s->x > s->xe) {
+ s->x = s->xs;
+ if (++s->y > s->ye)
+ s->y = s->ys;
+ }
+}
+
+static uint32_t c5to6(uint32_t v) { return v << 1 | v >> 4; }
+static uint32_t c4to6(uint32_t v) { return v << 2 | v >> 2; }
+
+static void pixel_byte(st7735_state *s, uint8_t b)
+{
+ s->px[s->npx++] = b;
+ if (s->colmod == 5 && s->npx == 2) {
+ uint32_t v = (uint32_t)s->px[0] << 8 | s->px[1];
+ store(s, c5to6(v >> 11) << 12 | ((v >> 5) & 0x3Fu) << 6 | c5to6(v & 0x1Fu));
+ s->npx = 0;
+ } else if (s->colmod == 3 && s->npx == 3) {
+ /* 12 bits a pixel: two pixels in three bytes */
+ uint32_t a = (uint32_t)s->px[0] << 4 | s->px[1] >> 4, b2 = ((uint32_t)s->px[1] & 0xFu) << 8 | s->px[2];
+ store(s, c4to6(a >> 8) << 12 | c4to6((a >> 4) & 0xFu) << 6 | c4to6(a & 0xFu));
+ store(s, c4to6(b2 >> 8) << 12 | c4to6((b2 >> 4) & 0xFu) << 6 | c4to6(b2 & 0xFu));
+ s->npx = 0;
+ } else if (s->colmod != 5 && s->colmod != 3 && s->npx == 3) {
+ store(s, (uint32_t)(s->px[0] >> 2) << 12 | (uint32_t)(s->px[1] >> 2) << 6 | (uint32_t)(s->px[2] >> 2));
+ s->npx = 0;
+ }
+}
+
+static void command(st7735_state *s, esk_comp *c, uint8_t b)
+{
+ s->cmd = b;
+ s->dirty = 1;
+ s->nparam = 0;
+ s->ramwr = 0;
+ s->npx = 0;
+ s->commands++;
+ switch (b) {
+ case 0x01: /* SWRESET */
+ soft_reset(s, c);
+ break;
+ case 0x10: /* SLPIN */
+ s->sleeping = 1;
+ break;
+ case 0x11: /* SLPOUT */
+ s->sleeping = 0;
+ break;
+ case 0x12: /* PTLON */
+ s->partial = 1;
+ break;
+ case 0x13: /* NORON */
+ s->partial = 0;
+ break;
+ case 0x20: /* INVOFF */
+ s->inverted = 0;
+ break;
+ case 0x21: /* INVON */
+ s->inverted = 1;
+ break;
+ case 0x28: /* DISPOFF */
+ s->display_on = 0;
+ break;
+ case 0x29: /* DISPON */
+ s->display_on = 1;
+ break;
+ case 0x2C: /* RAMWR: from the window's start */
+ s->ramwr = 1;
+ s->x = s->xs;
+ s->y = s->ys;
+ break;
+ case 0x38: /* IDMOFF */
+ s->idle = 0;
+ break;
+ case 0x39: /* IDMON */
+ s->idle = 1;
+ break;
+ default:
+ break;
+ }
+}
+
+static void data(st7735_state *s, uint8_t b)
+{
+ int n = s->nparam;
+ if (s->ramwr) {
+ pixel_byte(s, b);
+ return;
+ }
+ if (n < (int)sizeof s->param)
+ s->param[n] = b;
+ s->nparam++;
+ switch (s->cmd) {
+ case 0x2A: /* CASET: XS, then XE (16 bits each), each taken as it
+ * comes: a CASET cut short after XS keeps XE (ST's
+ * st7735_SetCursor sends only XS) */
+ case 0x2B: /* RASET: YS, YE */
+ if (n == 1 || n == 3) {
+ int v = s->param[n - 1] << 8 | s->param[n];
+ if (s->cmd == 0x2A) {
+ if (n == 1)
+ s->xs = v;
+ else
+ s->xe = v;
+ } else {
+ if (n == 1)
+ s->ys = v;
+ else
+ s->ye = v;
+ }
+ }
+ break;
+ case 0x30: /* PTLAR: PSL, PEL, as they come */
+ s->dirty = 1;
+ if (n == 1)
+ s->psr = s->param[0] << 8 | s->param[1];
+ else if (n == 3)
+ s->per = s->param[2] << 8 | s->param[3];
+ break;
+ case 0x36: /* MADCTL */
+ if (n == 0)
+ s->madctl = b;
+ s->dirty = 1;
+ break;
+ case 0x3A: /* COLMOD: IFPF */
+ if (n == 0)
+ s->colmod = b & 7u;
+ break;
+ default:
+ break; /* the panel's settings: kept, no effect on the image */
+ }
+}
+
+static void byte_in(st7735_state *s, esk_comp *c, uint8_t b, int is_data)
+{
+ if (esk_comp_now(c) < s->busy_until && !s->warned_busy) {
+ /* the datasheet's reset cancel time is a maximum: a part may take
+ * it, or not; the program is told it relies on a typical part */
+ s->warned_busy = 1;
+ esk_comp_diag(c, ES_SEV_WARNING, "st7735.reset_wait",
+ "a byte came within 5 ms of the controller's reset (RESX, SWRESET or power-up): the "
+ "ST7735R's reset cancel time is up to 5 ms, so a part may miss it");
+ }
+ if (is_data)
+ data(s, b);
+ else
+ command(s, c, b);
+}
+
+static void st_input(esk_comp *c, esk_pin *p)
+{
+ st7735_state *s = (st7735_state *)c->state;
+ int scl, now_power = powered(s), rst;
+ if (p == s->led)
+ s->dirty = 1;
+ if (now_power != s->powered) {
+ s->powered = now_power;
+ s->dirty = 1;
+ if (now_power) {
+ noise(s);
+ soft_reset(s, c);
+ }
+ }
+ if (!s->powered)
+ return;
+ rst = lv(s->rst, 1);
+ if (!rst) {
+ if (!s->in_reset)
+ s->in_reset = 1;
+ return;
+ }
+ if (s->in_reset) {
+ s->in_reset = 0;
+ soft_reset(s, c); /* the reset ends: 5 ms before the first command */
+ }
+ if (p == s->cs && lv(s->cs, 1)) {
+ s->bits = 0; /* deselected: the serial interface starts again */
+ return;
+ }
+ if (p != s->scl)
+ return;
+ scl = lv(s->scl, 0);
+ if (scl && !s->last_scl && !lv(s->cs, 1)) {
+ s->shift = (uint8_t)((unsigned)s->shift << 1 | (lv(s->sda, 0) ? 1u : 0u));
+ if (++s->bits == 8) {
+ s->bits = 0;
+ byte_in(s, c, s->shift, lv(s->dc, 1));
+ }
+ }
+ s->last_scl = scl;
+}
+
+static es_status st_create(esk_comp *c)
+{
+ st7735_state *s = (st7735_state *)c->state;
+ uint32_t i;
+ s->vcc = esk_comp_pin(c, "VCC");
+ s->scl = esk_comp_pin(c, "SCK");
+ s->sda = esk_comp_pin(c, "SDA");
+ s->cs = esk_comp_pin(c, "CS");
+ s->dc = esk_comp_pin(c, "DC");
+ s->rst = esk_comp_pin(c, "RST");
+ s->led = esk_comp_pin(c, "LED");
+ if (!s->scl || !s->sda)
+ return ES_ESCHEMA;
+ for (i = 0; i < c->npins; i++)
+ if (c->pins[i].model.kind == ESK_PIN_DIGITAL_IN || c->pins[i].model.kind == ESK_PIN_DIGITAL_IO)
+ esk_pin_watch(&c->pins[i], 1);
+ return ES_OK;
+}
+
+static void st_reset(esk_comp *c)
+{
+ st7735_state *s = (st7735_state *)c->state;
+ if (s->vcc && s->vcc->net) {
+ (void)esk_pin_compare(s->vcc, 2.3); /* its supply's power-on reset */
+ esk_pin_watch(s->vcc, 1);
+ }
+ s->powered = powered(s);
+ noise(s);
+ soft_reset(s, c);
+ s->last_scl = lv(s->scl, 0);
+ s->in_reset = !lv(s->rst, 1);
+ s->warned_busy = 0;
+ s->ram_writes = s->commands = 0;
+ s->probe_x = s->probe_y = 0;
+}
+
+static es_status st_set(esk_comp *c, const char *input, const esk_json *value)
+{
+ st7735_state *s = (st7735_state *)c->state;
+ int v;
+ if (value->type != ESK_JSON_NUMBER)
+ return ES_EINVAL;
+ v = (int)value->u.number.value;
+ if (!strcmp(input, "probe_x") && v >= 0 && v < W)
+ s->probe_x = v;
+ else if (!strcmp(input, "probe_y") && v >= 0 && v < H)
+ s->probe_y = v;
+ else
+ return ES_EINVAL;
+ return ES_OK;
+}
+
+/* the panel as it shows, (0, 0) its top left as the module is held (its
+ * pins at the top): RGB565. The 1.8" panel's scan starts at its bottom
+ * right, so frame memory (0, 0) is there, and the drivers (Adafruit's,
+ * ST's) set MADCTL's MX and MY for an upright image */
+static uint16_t shown(const st7735_state *s, int px, int py)
+{
+ uint32_t v, r, g, b;
+ px = W - 1 - px;
+ py = H - 1 - py;
+ if (!s->powered || !lv(s->led, 1))
+ return 0x0000; /* no backlight: dark */
+ if (s->sleeping || !s->display_on || (s->partial && (py < s->psr || py > s->per)))
+ return 0xFFFF; /* blank: a normally-white panel */
+ v = s->mem[py * W + px];
+ r = v >> 12 & 0x3Fu;
+ g = v >> 6 & 0x3Fu;
+ b = v & 0x3Fu;
+ if (s->madctl & 8u) { /* BGR */
+ uint32_t t = r;
+ r = b;
+ b = t;
+ }
+ if (s->idle) {
+ r = (r & 0x20u) ? 0x3Fu : 0;
+ g = (g & 0x20u) ? 0x3Fu : 0;
+ b = (b & 0x20u) ? 0x3Fu : 0;
+ }
+ if (s->inverted) {
+ r ^= 0x3Fu;
+ g ^= 0x3Fu;
+ b ^= 0x3Fu;
+ }
+ return (uint16_t)((r >> 1) << 11 | g << 5 | (b >> 1));
+}
+
+static const char B64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+/* the image shown, rendered again only when something changed it */
+static void render(st7735_state *s)
+{
+ static uint8_t raw[W * H * 2];
+ uint32_t h = 2166136261u, i, o = 0;
+ int x, y;
+ s->white = 0;
+ for (y = 0; y < H; y++)
+ for (x = 0; x < W; x++) {
+ uint16_t v = shown(s, x, y);
+ raw[2 * (y * W + x)] = (uint8_t)(v >> 8);
+ raw[2 * (y * W + x) + 1] = (uint8_t)v;
+ if (v == 0xFFFF)
+ s->white++;
+ }
+ for (i = 0; i < sizeof raw; i++) {
+ h ^= raw[i];
+ h *= 16777619u;
+ }
+ for (i = 0; i + 2 < sizeof raw; i += 3) {
+ uint32_t v = (uint32_t)raw[i] << 16 | (uint32_t)raw[i + 1] << 8 | raw[i + 2];
+ s->b64[o++] = B64[v >> 18];
+ s->b64[o++] = B64[(v >> 12) & 63u];
+ s->b64[o++] = B64[(v >> 6) & 63u];
+ s->b64[o++] = B64[v & 63u];
+ }
+ if (i < sizeof raw) { /* the last one or two bytes */
+ uint32_t v = (uint32_t)raw[i] << 16 | (i + 1 < sizeof raw ? (uint32_t)raw[i + 1] << 8 : 0u);
+ s->b64[o++] = B64[v >> 18];
+ s->b64[o++] = B64[(v >> 12) & 63u];
+ s->b64[o++] = i + 1 < sizeof raw ? B64[(v >> 6) & 63u] : '=';
+ s->b64[o++] = '=';
+ }
+ s->b64[o] = 0;
+ (void)snprintf(s->hex, sizeof s->hex, "%08lx", (unsigned long)h);
+ s->dirty = 0;
+}
+
+static void st_observe(esk_comp *c, esk_json *out)
+{
+ st7735_state *s = (st7735_state *)c->state;
+ esk_alloc *a = c->e->a;
+ if (s->dirty)
+ render(s);
+ esk_json_set_bool(a, out, "on", s->powered && !s->sleeping && s->display_on);
+ esk_json_set_bool(a, out, "sleeping", s->sleeping);
+ esk_json_set_bool(a, out, "inverted", s->inverted);
+ esk_json_set_integer(a, out, "colmod", s->colmod);
+ esk_json_set_integer(a, out, "madctl", s->madctl);
+ esk_json_set_integer(a, out, "ram_writes", (int64_t)s->ram_writes);
+ esk_json_set_integer(a, out, "commands", (int64_t)s->commands);
+ esk_json_set_integer(a, out, "white_pixels", s->white);
+ esk_json_set_integer(a, out, "probe", shown(s, s->probe_x, s->probe_y));
+ esk_json_set_string(a, out, "checksum", s->hex);
+ esk_json_set_string(a, out, "frame", s->b64);
+}
+
+const esk_model_ops esk_st7735_ops = { "st7735", sizeof(st7735_state), st_create, NULL, st_reset, st_input,
+ NULL, st_set, st_observe, NULL, 0 };
diff --git a/core/model/usb_dev.c b/core/model/usb_dev.c
new file mode 100644
index 00000000..4cc0886e
--- /dev/null
+++ b/core/model/usb_dev.c
@@ -0,0 +1,378 @@
+/* SPDX-License-Identifier: Apache-2.0 */
+/* Copyright 2026 Junior Deogracias */
+/* usb_dev.c -- a USB part's device side (usb_dev.h says what it does). */
+#include
+
+#include "model/usb_dev.h"
+
+#define VBUS_VALID_V 4.4 /* a bus-powered device's VBUS (USB 2.0 7.2.1: 4.40 V at a device) */
+
+uint32_t usbd_mps0(const usbd *u) { return u->dev_desc ? u->dev_desc[7] : 8u; }
+
+static int powered(const usbd *u)
+{
+ const esk_pin *p = u->vbus;
+ if (!u->plugged)
+ return 0;
+ if (!p || !p->net)
+ return 1;
+ if (p->watch_on)
+ return p->watch_state == ESK_L1;
+ return esk_net_value(p->net) == ESK_L1;
+}
+
+static int d_pullup(void *ctx)
+{
+ usbd *u = (usbd *)ctx;
+ if (!powered(u))
+ return ESK_USB_DETACHED;
+ return u->low_speed ? ESK_USB_LOW_SPEED : ESK_USB_FULL_SPEED;
+}
+
+static void bus_reset(usbd *u)
+{
+ int i;
+ u->addr = u->new_addr = u->addr_pending = 0;
+ u->config = 0;
+ u->alt = 0;
+ u->halted_in = u->halted_out = 0;
+ u->remote_wakeup = 0;
+ for (i = 0; i < USBD_EPS; i++)
+ u->pid_in[i] = u->pid_out[i] = 0;
+ u->ep0_len = u->ep0_pos = 0;
+ u->ep0_stalled = 0;
+ if (u->cls && u->cls->configured)
+ u->cls->configured(u, 0);
+}
+
+static void d_bus(void *ctx, int ev, uint32_t frame, es_time_t at)
+{
+ usbd *u = (usbd *)ctx;
+ (void)frame;
+ (void)at;
+ u->suspended = 0;
+ if (ev == ESK_USB_RESET)
+ bus_reset(u);
+ else if (ev == ESK_USB_SOF)
+ u->frames++;
+}
+
+static int for_me(usbd *u, int addr)
+{
+ if (!powered(u))
+ return 0;
+ if (addr == u->addr)
+ return 1;
+ if (u->addr_pending && addr == u->new_addr) { /* the host moved on: in effect */
+ u->addr = u->new_addr;
+ u->addr_pending = 0;
+ return 1;
+ }
+ return 0;
+}
+
+/* a string descriptor from ASCII */
+static uint32_t string_desc(const char *s, uint8_t *out, uint32_t cap)
+{
+ uint32_t n = 2, i;
+ for (i = 0; s[i] && n + 2u <= cap && n + 2u <= 254u; i++) {
+ out[n++] = (uint8_t)s[i];
+ out[n++] = 0;
+ }
+ out[0] = (uint8_t)n;
+ out[1] = 3;
+ return n;
+}
+
+/* EP0's standard request: the IN data to ep0[], or STALL */
+static int standard(usbd *u, const uint8_t *s)
+{
+ unsigned type = s[0], req = s[1], value = (unsigned)s[2] | (unsigned)s[3] << 8;
+ unsigned index = (unsigned)s[4] | (unsigned)s[5] << 8;
+ int ep = (int)(index & 0x0fu), in = (index & 0x80u) != 0;
+ switch (req) {
+ case 6: /* GET_DESCRIPTOR */
+ switch (value >> 8) {
+ case 1: memcpy(u->ep0, u->dev_desc, 18); u->ep0_len = 18; return ESK_USB_ACK;
+ case 2:
+ memcpy(u->ep0, u->cfg_desc, u->cfg_len);
+ u->ep0_len = u->cfg_len;
+ return ESK_USB_ACK;
+ case 3:
+ if ((value & 0xffu) == 0) {
+ static const uint8_t lang[4] = { 4, 3, 0x09, 0x04 };
+ memcpy(u->ep0, lang, 4);
+ u->ep0_len = 4;
+ return ESK_USB_ACK;
+ }
+ if ((value & 0xffu) <= 7u && u->strings[(value & 0xffu) - 1u]) {
+ u->ep0_len = string_desc(u->strings[(value & 0xffu) - 1u], u->ep0, sizeof u->ep0);
+ return ESK_USB_ACK;
+ }
+ return ESK_USB_STALL;
+ default:
+ return ESK_USB_STALL; /* a class descriptor: the class's */
+ }
+ case 5: /* SET_ADDRESS: in effect after the status stage */
+ u->new_addr = (int)(value & 0x7fu);
+ u->addr_pending = 1;
+ return ESK_USB_ACK;
+ case 9: /* SET_CONFIGURATION */
+ if (value > 1u)
+ return ESK_USB_STALL;
+ u->config = (int)value;
+ memset(u->pid_in, 0, sizeof u->pid_in);
+ memset(u->pid_out, 0, sizeof u->pid_out);
+ u->halted_in = u->halted_out = 0;
+ if (u->cls && u->cls->configured)
+ u->cls->configured(u, u->config);
+ return ESK_USB_ACK;
+ case 8: /* GET_CONFIGURATION */
+ u->ep0[0] = (uint8_t)u->config;
+ u->ep0_len = 1;
+ return ESK_USB_ACK;
+ case 10: /* GET_INTERFACE */
+ u->ep0[0] = (uint8_t)u->alt;
+ u->ep0_len = 1;
+ return ESK_USB_ACK;
+ case 11: /* SET_INTERFACE: one alternate setting each */
+ return value == 0 ? ESK_USB_ACK : ESK_USB_STALL;
+ case 0: /* GET_STATUS */
+ u->ep0[0] = u->ep0[1] = 0;
+ if ((type & 3u) == 0)
+ u->ep0[0] = (uint8_t)((u->cfg_desc[7] & 0x40u ? 1u : 0u) | (u->remote_wakeup ? 2u : 0u));
+ else if ((type & 3u) == 2)
+ u->ep0[0] = (uint8_t)(((in ? u->halted_in : u->halted_out) >> ep) & 1u);
+ u->ep0_len = 2;
+ return ESK_USB_ACK;
+ case 1: /* CLEAR_FEATURE */
+ case 3: /* SET_FEATURE */
+ if ((type & 3u) == 0 && value == 1) { /* DEVICE_REMOTE_WAKEUP */
+ u->remote_wakeup = req == 3;
+ return ESK_USB_ACK;
+ }
+ if ((type & 3u) == 2 && value == 0 && ep) { /* ENDPOINT_HALT */
+ uint32_t *h = in ? &u->halted_in : &u->halted_out;
+ if (req == 3)
+ *h |= 1u << ep;
+ else {
+ *h &= ~(1u << ep);
+ if (in)
+ u->pid_in[ep] = 0;
+ else
+ u->pid_out[ep] = 0;
+ }
+ return ESK_USB_ACK;
+ }
+ return ESK_USB_STALL;
+ default:
+ return ESK_USB_STALL;
+ }
+}
+
+static int run_setup(usbd *u, const uint8_t *data)
+{
+ uint32_t len = 0;
+ int r;
+ u->ep0_len = 0;
+ if ((u->setup[0] & 0x60u) == 0) {
+ r = standard(u, u->setup);
+ if (r == ESK_USB_ACK || !u->cls || !u->cls->setup)
+ return r;
+ /* a standard request for the class's part: its descriptors on an
+ * interface (a HID's report descriptor) */
+ }
+ r = u->cls && u->cls->setup ? u->cls->setup(u, u->setup, data, u->ep0, sizeof u->ep0, &len) : ESK_USB_STALL;
+ u->ep0_len = len;
+ return r;
+}
+
+static int d_setup(void *ctx, int addr, int ep, const uint8_t data[8], es_time_t at)
+{
+ usbd *u = (usbd *)ctx;
+ unsigned wlen;
+ (void)at;
+ if (!for_me(u, addr) || ep != 0)
+ return ESK_USB_NONE;
+ u->setups++;
+ memcpy(u->setup, data, 8);
+ wlen = (unsigned)data[6] | (unsigned)data[7] << 8;
+ u->ep0_dir_in = (data[0] & 0x80u) != 0;
+ u->ep0_pos = 0;
+ u->ep0_stalled = 0;
+ u->ep0_status_zlp_sent = 0;
+ u->pid_in[0] = u->pid_out[0] = 1;
+ if (!u->ep0_dir_in && wlen) { /* the OUT data comes first */
+ u->ep0_out_want = wlen > sizeof u->ep0 ? (uint32_t)sizeof u->ep0 : wlen;
+ u->ep0_len = 0;
+ return ESK_USB_ACK; /* a SETUP is always ACKed */
+ }
+ u->ep0_out_want = 0;
+ if (run_setup(u, NULL) != ESK_USB_ACK)
+ u->ep0_stalled = 1;
+ else if (u->ep0_dir_in && u->ep0_len > wlen)
+ u->ep0_len = wlen;
+ return ESK_USB_ACK;
+}
+
+static int d_out(void *ctx, int addr, int ep, int data1, const uint8_t *data, uint32_t len, es_time_t at)
+{
+ usbd *u = (usbd *)ctx;
+ int r;
+ (void)at;
+ if (!for_me(u, addr) || ep < 0 || ep >= USBD_EPS)
+ return ESK_USB_NONE;
+ if (ep == 0) {
+ if (u->ep0_stalled)
+ return ESK_USB_STALL;
+ if (u->ep0_out_want) { /* the data stage of a control write */
+ if ((data1 != 0) != (u->pid_out[0] != 0))
+ return ESK_USB_ACK;
+ if (u->ep0_len + len > u->ep0_out_want)
+ len = u->ep0_out_want - u->ep0_len;
+ memcpy(u->ep0 + u->ep0_len, data, len);
+ u->ep0_len += len;
+ u->pid_out[0] ^= 1;
+ if (u->ep0_len >= u->ep0_out_want) {
+ uint8_t buf[1024];
+ memcpy(buf, u->ep0, u->ep0_len);
+ u->ep0_out_want = 0;
+ if (run_setup(u, buf) != ESK_USB_ACK)
+ u->ep0_stalled = 1;
+ u->ep0_len = 0;
+ }
+ return ESK_USB_ACK;
+ }
+ return ESK_USB_ACK; /* the status stage of a control read */
+ }
+ if (!u->config)
+ return ESK_USB_NONE;
+ if ((u->halted_out >> ep) & 1u)
+ return ESK_USB_STALL;
+ if ((data1 != 0) != (u->pid_out[ep] != 0))
+ return ESK_USB_ACK; /* a repeat: dropped */
+ r = u->cls && u->cls->out ? u->cls->out(u, ep, data, len) : ESK_USB_STALL;
+ if (r == ESK_USB_ACK)
+ u->pid_out[ep] ^= 1;
+ return r;
+}
+
+static int d_in(void *ctx, int addr, int ep, uint8_t *data, uint32_t cap, uint32_t *len, int *data1, es_time_t at)
+{
+ usbd *u = (usbd *)ctx;
+ int r;
+ (void)at;
+ if (!for_me(u, addr) || ep < 0 || ep >= USBD_EPS)
+ return ESK_USB_NONE;
+ if (ep == 0) {
+ uint32_t n, mps = usbd_mps0(u);
+ if (u->ep0_stalled)
+ return ESK_USB_STALL;
+ if (u->ep0_out_want) /* the data stage is not done */
+ return ESK_USB_NAK;
+ if (!u->ep0_dir_in || u->ep0_pos >= u->ep0_len) {
+ /* the status stage of a control write, or a read's end */
+ if (!u->ep0_dir_in || !u->ep0_last_full || u->ep0_status_zlp_sent) {
+ *len = 0;
+ *data1 = 1;
+ if (!u->ep0_dir_in && u->addr_pending) { /* SET_ADDRESS done */
+ u->addr = u->new_addr;
+ u->addr_pending = 0;
+ }
+ return ESK_USB_ACK;
+ }
+ }
+ n = u->ep0_len - u->ep0_pos;
+ if (n > mps)
+ n = mps;
+ if (n > cap)
+ n = cap;
+ memcpy(data, u->ep0 + u->ep0_pos, n);
+ u->ep0_pos += n;
+ u->ep0_last_full = n == mps;
+ if (n == 0)
+ u->ep0_status_zlp_sent = 1;
+ *len = n;
+ *data1 = u->pid_in[0];
+ u->pid_in[0] ^= 1;
+ return ESK_USB_ACK;
+ }
+ if (!u->config)
+ return ESK_USB_NONE;
+ if ((u->halted_in >> ep) & 1u)
+ return ESK_USB_STALL;
+ r = u->cls && u->cls->in ? u->cls->in(u, ep, data, cap, len) : ESK_USB_STALL;
+ if (r == ESK_USB_ACK) {
+ *data1 = u->pid_in[ep];
+ u->pid_in[ep] ^= 1;
+ }
+ return r;
+}
+
+es_status usbd_init(usbd *u, esk_comp *c, int low_speed, const uint8_t *dev_desc, const uint8_t *cfg_desc,
+ uint32_t cfg_len, const char *const *strings, int nstrings, const usbd_class *cls,
+ void *cls_ctx)
+{
+ int i;
+ memset(u, 0, sizeof *u);
+ u->c = c;
+ u->low_speed = low_speed;
+ u->dev_desc = dev_desc;
+ u->cfg_desc = cfg_desc;
+ u->cfg_len = cfg_len;
+ for (i = 0; i < nstrings && i < 7; i++)
+ u->strings[i] = strings[i];
+ u->cls = cls;
+ u->cls_ctx = cls_ctx;
+ u->plugged = 1;
+ u->vbus = esk_comp_pin(c, "VBUS");
+ u->link.ctx = u;
+ u->link.comp = c;
+ u->link.dp = esk_comp_pin(c, "DP");
+ u->link.pullup = d_pullup;
+ u->link.bus = d_bus;
+ u->link.setup = d_setup;
+ u->link.out = d_out;
+ u->link.in = d_in;
+ if (esk_usb_attach_dev(c->e, &u->link) != ES_OK)
+ return ES_ENOMEM;
+ u->linked = 1;
+ return ES_OK;
+}
+
+void usbd_destroy(usbd *u)
+{
+ if (u->linked)
+ esk_usb_detach_dev(u->c->e, &u->link);
+ u->linked = 0;
+}
+
+void usbd_reset(usbd *u)
+{
+ bus_reset(u);
+ u->suspended = 0;
+ u->frames = u->setups = 0;
+ u->plugged = esk_comp_input_bool(u->c, "plugged", 1);
+ if (u->vbus && u->vbus->net) {
+ (void)esk_pin_compare(u->vbus, VBUS_VALID_V);
+ esk_pin_watch(u->vbus, 1);
+ }
+}
+
+void usbd_power(usbd *u)
+{
+ if (!powered(u))
+ bus_reset(u);
+ esk_usb_changed(u->c->e, &u->link, esk_comp_now(u->c));
+}
+
+const char *usbd_state(const usbd *u)
+{
+ if (!powered(u))
+ return "detached";
+ if (u->config)
+ return "configured";
+ if (u->addr || u->addr_pending)
+ return "address";
+ return "default";
+}
diff --git a/core/model/usb_dev.h b/core/model/usb_dev.h
new file mode 100644
index 00000000..79c0cdc2
--- /dev/null
+++ b/core/model/usb_dev.h
@@ -0,0 +1,83 @@
+/* SPDX-License-Identifier: Apache-2.0 */
+/* Copyright 2026 Junior Deogracias */
+/* usb_dev.h -- the device side of a USB part (a mouse, a flash drive) on a
+ * cable of the bench (engine/usb.h): what every USB device's controller and
+ * chapter 9 firmware do, for the class code of the part.
+ *
+ * - attached (its pull-up on D+ at full speed, on D- at low speed) while
+ * VBUS at its VBUS pin is above 4.4 V (a bus-powered device; unwired:
+ * always), and its `plugged` input is true;
+ * - the bus's reset puts it in the default state at address 0; 3 ms
+ * without frames suspends it, any activity resumes it;
+ * - EP0 answers the standard requests (USB 2.0 9.4): GET_DESCRIPTOR
+ * (device, configuration, strings), SET_ADDRESS (in effect after its
+ * status stage), SET_CONFIGURATION, GET_CONFIGURATION, GET_STATUS,
+ * SET/CLEAR_FEATURE (ENDPOINT_HALT, DEVICE_REMOTE_WAKEUP), GET/SET_
+ * INTERFACE; the class's requests go to the part; anything else
+ * STALLs; IN data in packets of bMaxPacketSize0, cut to wLength;
+ * - other endpoints: the part's in() and out(); the data toggles are
+ * kept per endpoint and direction, a repeated OUT packet is ACKed and
+ * dropped, a halted endpoint STALLs.
+ * Inside: no bit-level signalling, no isochronous endpoints. */
+#ifndef ESK_MODEL_USB_DEV_H
+#define ESK_MODEL_USB_DEV_H
+
+#include "engine/engine.h"
+
+#define USBD_EPS 16
+
+typedef struct usbd usbd;
+
+typedef struct usbd_class {
+ /* a class or vendor request (bmRequestType's type not standard): the
+ * IN data to *out (up to cap bytes, *len set), or the OUT data in
+ * `data`; ESK_USB_ACK, ESK_USB_STALL */
+ int (*setup)(usbd *u, const uint8_t setup[8], const uint8_t *data, uint8_t *out, uint32_t cap, uint32_t *len);
+ /* an IN token on endpoint `ep` (1-15): ACK with a packet, NAK, STALL */
+ int (*in)(usbd *u, int ep, uint8_t *data, uint32_t cap, uint32_t *len);
+ /* an OUT packet on endpoint `ep`: ACK (taken), NAK (no room), STALL */
+ int (*out)(usbd *u, int ep, const uint8_t *data, uint32_t len);
+ /* the configuration was set (cfg 0: unconfigured) or the bus reset */
+ void (*configured)(usbd *u, int cfg);
+} usbd_class;
+
+struct usbd {
+ esk_comp *c;
+ esk_usb_dev link;
+ int linked;
+ esk_pin *vbus;
+ int low_speed;
+ int plugged;
+ const uint8_t *dev_desc, *cfg_desc;
+ uint32_t cfg_len;
+ const char *strings[8]; /* string descriptors 1..7 (ASCII) */
+ const usbd_class *cls;
+ void *cls_ctx;
+ /* chapter 9 state */
+ int addr, new_addr, addr_pending, config, suspended, remote_wakeup, alt;
+ uint32_t halted_in, halted_out; /* endpoint bits */
+ int pid_in[USBD_EPS], pid_out[USBD_EPS];
+ /* EP0's control transfer */
+ uint8_t setup[8];
+ uint8_t ep0[1024];
+ uint32_t ep0_len, ep0_pos, ep0_out_want;
+ int ep0_dir_in, ep0_stalled, ep0_status_zlp_sent;
+ int ep0_last_full; /* a full last packet: a ZLP may still be asked */
+ uint64_t frames, setups;
+};
+
+/* set up the device: its pins VBUS (optional), DP, DM; descriptors kept by
+ * the caller; strings[i] is string descriptor i + 1 */
+es_status usbd_init(usbd *u, esk_comp *c, int low_speed, const uint8_t *dev_desc, const uint8_t *cfg_desc,
+ uint32_t cfg_len, const char *const *strings, int nstrings, const usbd_class *cls,
+ void *cls_ctx);
+void usbd_destroy(usbd *u);
+void usbd_reset(usbd *u);
+/* the VBUS pin changed or `plugged`: the host is told */
+void usbd_power(usbd *u);
+/* a part's EP0 for its standard parts: wMaxPacketSize0 */
+uint32_t usbd_mps0(const usbd *u);
+/* the device's own state, for outputs */
+const char *usbd_state(const usbd *u);
+
+#endif
diff --git a/core/model/usb_flash_drive.c b/core/model/usb_flash_drive.c
new file mode 100644
index 00000000..26832b7b
--- /dev/null
+++ b/core/model/usb_flash_drive.c
@@ -0,0 +1,564 @@
+/* SPDX-License-Identifier: Apache-2.0 */
+/* Copyright 2026 Junior Deogracias */
+/* usb_flash_drive.c -- a USB flash drive: a full-speed USB Mass Storage
+ * device (USB MSC Bulk-Only Transport 1.0 with the SCSI transparent command
+ * set) on a cable of the bench (usb_dev.h), its medium formatted as a stick
+ * comes from the shop: an MBR with one FAT16 partition from sector 2048,
+ * 512-byte sectors, the volume label EMBSIM.
+ *
+ * - its descriptors: a device of class 0, one configuration (bus powered,
+ * 200 mA), one interface of class 8 (mass storage), subclass 6 (SCSI
+ * transparent), protocol 0x50 (Bulk-Only), a bulk IN endpoint 0x81 and
+ * a bulk OUT endpoint 0x02 of 64 bytes; a serial number, as BOT asks;
+ * - its class requests: Get Max LUN (one LUN: 0) and Bulk-Only Mass
+ * Storage Reset;
+ * - Bulk-Only Transport: a CBW of 31 bytes with its signature on the OUT
+ * endpoint, its data phase, then the CSW of 13 bytes on the IN endpoint
+ * with the CBW's tag, the residue and its status; a CBW that is not
+ * valid (its length, its signature) halts both endpoints until the
+ * host's reset recovery (the class reset, then CLEAR_FEATURE on each);
+ * a data phase shorter than the host asked ends with a short packet,
+ * or with a STALL of the IN endpoint when the last packet was full; a
+ * command that fails answers CHECK CONDITION (status 1) and keeps its
+ * sense for REQUEST SENSE;
+ * - SCSI: TEST UNIT READY, REQUEST SENSE, INQUIRY (removable, 'EmbSIM'
+ * 'USB Flash Drive' '1.00'), MODE SENSE(6) and (10) (the write
+ * protection), START STOP UNIT, PREVENT ALLOW MEDIUM REMOVAL, READ
+ * FORMAT CAPACITIES, READ CAPACITY(10), READ(10), WRITE(10), VERIFY(10),
+ * SYNCHRONIZE CACHE(10); anything else ILLEGAL REQUEST, INVALID COMMAND
+ * OPERATION CODE; a block past the end LOGICAL BLOCK ADDRESS OUT OF
+ * RANGE; a write while write_protect is set DATA PROTECT, WRITE
+ * PROTECTED;
+ * - its outputs: what a computer shows of it: the files in the root
+ * directory with their sizes, and what it was asked to do.
+ * Inside: the medium is in memory, as large as its capacity parameter, and
+ * starts formatted at each power-up of the simulation (no image kept
+ * between runs); a command's data moves at once, with no NAND page times
+ * (a real stick NAKs while it programs). Not modelled: high speed (the
+ * drive is a full-speed device, as on a full-speed host anyway), UAS,
+ * more than one LUN, the drive's activity LED. */
+#include
+#include
+
+#include "engine/engine.h"
+#include "model/registry.h"
+#include "model/usb_dev.h"
+
+#define SECTOR 512u
+#define PART_START 2048u
+#define MPS 64u
+#define CBW_SIG 0x43425355u
+#define CSW_SIG 0x53425355u
+
+static const uint8_t dev_desc[18] = {
+ 18, 1, 0x00, 0x02, 0, 0, 0, 64, /* USB 2.0, class in the interface, EP0 64 bytes */
+ 0x09, 0x12, 0x01, 0x00, /* 1209:0001, pid.codes' test PID: no vendor's own */
+ 0x00, 0x01, 1, 2, 3, 1, /* bcdDevice 1.00, strings 1, 2 and the serial, one configuration */
+};
+
+static const uint8_t cfg_desc[32] = {
+ 9, 2, 32, 0, 1, 1, 0, 0x80, 100, /* bus powered, 200 mA */
+ 9, 4, 0, 0, 2, 8, 6, 0x50, 0, /* mass storage, SCSI, Bulk-Only */
+ 7, 5, 0x81, 2, MPS, 0, 0, /* bulk IN */
+ 7, 5, 0x02, 2, MPS, 0, 0, /* bulk OUT */
+};
+
+static const char *const strings[3] = { "EmbSIM", "USB Flash Drive", "EMBSIM0000000001" };
+
+enum { IDLE, DATA_IN, DATA_OUT, STATUS };
+
+typedef struct drive_state {
+ usbd u;
+ uint8_t *disk;
+ uint32_t sectors;
+ int write_protect;
+ /* the command under way */
+ int phase;
+ uint32_t tag, want, done; /* dCBWDataTransferLength, bytes moved */
+ uint8_t status;
+ uint8_t buf[4096];
+ uint32_t buf_len; /* an IN command's answer (not READ) */
+ uint32_t lba, blocks; /* READ(10), WRITE(10) */
+ int reading, writing;
+ int stall_after; /* the data phase ends with the IN endpoint's STALL */
+ uint8_t sense_key, asc, ascq;
+ /* outputs */
+ uint64_t commands, sectors_read, sectors_written;
+ char last[24];
+ char files[1024];
+} drive_state;
+
+static void put32be(uint8_t *p, uint32_t v)
+{
+ p[0] = (uint8_t)(v >> 24);
+ p[1] = (uint8_t)(v >> 16);
+ p[2] = (uint8_t)(v >> 8);
+ p[3] = (uint8_t)v;
+}
+
+static void put16le(uint8_t *p, uint32_t v)
+{
+ p[0] = (uint8_t)v;
+ p[1] = (uint8_t)(v >> 8);
+}
+
+static void put32le(uint8_t *p, uint32_t v)
+{
+ put16le(p, v & 0xFFFFu);
+ put16le(p + 2, v >> 16);
+}
+
+static uint32_t get32be(const uint8_t *p)
+{
+ return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | p[3];
+}
+
+static uint32_t get16le(const uint8_t *p) { return (uint32_t)p[0] | (uint32_t)p[1] << 8; }
+
+static uint32_t get32le(const uint8_t *p) { return get16le(p) | get16le(p + 2) << 16; }
+
+/* the FAT16 geometry of the partition: one sector a cluster up to 16 MiB,
+ * more beyond, as FORMAT would choose */
+static void geometry(uint32_t part, uint32_t *spc, uint32_t *fat_sz)
+{
+ uint32_t c = 1, f;
+ while (part / c > 65000u && c < 64u)
+ c *= 2u;
+ f = ((part / c + 2u) * 2u + SECTOR - 1u) / SECTOR;
+ *spc = c;
+ *fat_sz = f;
+}
+
+/* an empty stick: the MBR, the partition's boot sector, its two FATs and
+ * the root directory with the volume label */
+static void format(drive_state *d)
+{
+ uint8_t *m = d->disk, *b = d->disk + (size_t)PART_START * SECTOR;
+ uint32_t part = d->sectors - PART_START, spc, fsz, k;
+ memset(d->disk, 0, (size_t)d->sectors * SECTOR);
+ geometry(part, &spc, &fsz);
+ /* the MBR: one partition, FAT16 (0x06 from 32 MiB, else 0x04), LBA */
+ m[446] = 0x00;
+ m[446 + 4] = part >= 65536u ? 0x06 : 0x04;
+ put32le(m + 446 + 8, PART_START);
+ put32le(m + 446 + 12, part);
+ m[446 + 1] = 0xFE; /* CHS fields out of range: the LBA fields count */
+ m[446 + 2] = 0xFF;
+ m[446 + 3] = 0xFF;
+ m[446 + 5] = 0xFE;
+ m[446 + 6] = 0xFF;
+ m[446 + 7] = 0xFF;
+ put32le(m + 440, 0x454D4253u); /* the disk signature */
+ m[510] = 0x55;
+ m[511] = 0xAA;
+ /* the boot sector's BPB */
+ b[0] = 0xEB;
+ b[1] = 0x3C;
+ b[2] = 0x90;
+ memcpy(b + 3, "MSDOS5.0", 8);
+ put16le(b + 11, SECTOR);
+ b[13] = (uint8_t)spc;
+ put16le(b + 14, 1); /* reserved sectors */
+ b[16] = 2; /* FATs */
+ put16le(b + 17, 512); /* root directory entries */
+ if (part < 65536u)
+ put16le(b + 19, part);
+ else
+ put32le(b + 32, part);
+ b[21] = 0xF8;
+ put16le(b + 22, fsz);
+ put16le(b + 24, 63);
+ put16le(b + 26, 255);
+ put32le(b + 28, PART_START);
+ b[36] = 0x80;
+ b[38] = 0x29;
+ put32le(b + 39, 0x20261031u);
+ memcpy(b + 43, "EMBSIM ", 11);
+ memcpy(b + 54, "FAT16 ", 8);
+ b[510] = 0x55;
+ b[511] = 0xAA;
+ /* the FATs' first two entries: the media byte, end of chain */
+ for (k = 0; k < 2u; k++) {
+ uint8_t *f = b + (size_t)(1u + k * fsz) * SECTOR;
+ put16le(f, 0xFFF8u);
+ put16le(f + 2, 0xFFFFu);
+ }
+ /* the root directory: the volume label */
+ memcpy(b + (size_t)(1u + 2u * fsz) * SECTOR, "EMBSIM ", 11);
+ b[(size_t)(1u + 2u * fsz) * SECTOR + 11] = 0x08;
+}
+
+/* the root directory's files as a computer lists them: NAME.EXT (size) */
+static void list_files(drive_state *d)
+{
+ const uint8_t *m = d->disk, *b, *e;
+ uint32_t start, rsv, nfat, fsz, roots, i;
+ size_t o = 0;
+ d->files[0] = 0;
+ start = get32le(m + 446 + 8);
+ if (m[510] != 0x55 || m[511] != 0xAA || !start || start >= d->sectors)
+ start = 0; /* no partition table: a superfloppy */
+ b = m + (size_t)start * SECTOR;
+ if (get16le(b + 11) != SECTOR)
+ return;
+ rsv = get16le(b + 14);
+ nfat = b[16];
+ roots = get16le(b + 17);
+ fsz = get16le(b + 22);
+ if (!roots || (size_t)(start + rsv + nfat * fsz) * SECTOR + (size_t)roots * 32u > (size_t)d->sectors * SECTOR)
+ return;
+ e = b + (size_t)(rsv + nfat * fsz) * SECTOR;
+ for (i = 0; i < roots; i++, e += 32) {
+ char name[13];
+ int n = 0, k;
+ if (e[0] == 0)
+ break;
+ if (e[0] == 0xE5 || (e[11] & 0x0Fu) == 0x0Fu || (e[11] & 0x08u))
+ continue; /* deleted, a long name's part, the volume label */
+ for (k = 0; k < 8 && e[k] != ' '; k++)
+ name[n++] = (char)e[k];
+ if (e[8] != ' ') {
+ name[n++] = '.';
+ for (k = 8; k < 11 && e[k] != ' '; k++)
+ name[n++] = (char)e[k];
+ }
+ name[n] = 0;
+ o += (size_t)snprintf(d->files + o, sizeof d->files - o, "%s%s%s (%lu)", o ? ", " : "", name,
+ (e[11] & 0x10u) ? "/" : "", (unsigned long)get32le(e + 28));
+ if (o >= sizeof d->files - 1)
+ break;
+ }
+}
+
+static void sense(drive_state *d, uint8_t key, uint8_t asc, uint8_t ascq)
+{
+ d->sense_key = key;
+ d->asc = asc;
+ d->ascq = ascq;
+}
+
+/* a CBW's command: its answer or its blocks; the status if it fails */
+static void command(drive_state *d, const uint8_t *cb, int dir_in)
+{
+ uint8_t op = cb[0];
+ uint8_t *r = d->buf;
+ d->buf_len = 0;
+ d->reading = d->writing = 0;
+ d->status = 0;
+ d->commands++;
+ switch (op) {
+ case 0x00: /* TEST UNIT READY */
+ snprintf(d->last, sizeof d->last, "TEST UNIT READY");
+ break;
+ case 0x03: /* REQUEST SENSE: fixed format */
+ snprintf(d->last, sizeof d->last, "REQUEST SENSE");
+ memset(r, 0, 18);
+ r[0] = 0x70;
+ r[2] = d->sense_key;
+ r[7] = 10;
+ r[12] = d->asc;
+ r[13] = d->ascq;
+ d->buf_len = cb[4] < 18 ? cb[4] : 18;
+ sense(d, 0, 0, 0);
+ break;
+ case 0x12: /* INQUIRY: standard data */
+ snprintf(d->last, sizeof d->last, "INQUIRY");
+ memset(r, 0, 36);
+ r[1] = 0x80; /* removable */
+ r[2] = 0x04; /* SPC-2 */
+ r[3] = 0x02;
+ r[4] = 31;
+ memcpy(r + 8, "EmbSIM ", 8);
+ memcpy(r + 16, "USB Flash Drive ", 16);
+ memcpy(r + 32, "1.00", 4);
+ d->buf_len = cb[4] < 36 ? cb[4] : 36;
+ break;
+ case 0x1A: /* MODE SENSE(6): the header, the write protection */
+ case 0x5A: /* MODE SENSE(10) */
+ snprintf(d->last, sizeof d->last, "MODE SENSE");
+ if (op == 0x1A) {
+ r[0] = 3;
+ r[1] = 0;
+ r[2] = d->write_protect ? 0x80 : 0;
+ r[3] = 0;
+ d->buf_len = cb[4] < 4 ? cb[4] : 4;
+ } else {
+ memset(r, 0, 8);
+ r[1] = 6;
+ r[3] = d->write_protect ? 0x80 : 0;
+ d->buf_len = 8;
+ }
+ break;
+ case 0x1B: /* START STOP UNIT */
+ snprintf(d->last, sizeof d->last, "START STOP UNIT");
+ break;
+ case 0x1E: /* PREVENT ALLOW MEDIUM REMOVAL */
+ snprintf(d->last, sizeof d->last, "PREVENT ALLOW REMOVAL");
+ break;
+ case 0x23: /* READ FORMAT CAPACITIES: the formatted capacity */
+ snprintf(d->last, sizeof d->last, "READ FORMAT CAPACITIES");
+ memset(r, 0, 12);
+ r[3] = 8;
+ put32be(r + 4, d->sectors);
+ r[8] = 0x02;
+ r[10] = SECTOR >> 8;
+ d->buf_len = 12;
+ break;
+ case 0x25: /* READ CAPACITY(10): the last block, its length */
+ snprintf(d->last, sizeof d->last, "READ CAPACITY(10)");
+ put32be(r, d->sectors - 1u);
+ put32be(r + 4, SECTOR);
+ d->buf_len = 8;
+ break;
+ case 0x28: /* READ(10) */
+ case 0x2A: /* WRITE(10) */
+ case 0x2F: /* VERIFY(10) */
+ d->lba = get32be(cb + 2);
+ d->blocks = (uint32_t)cb[7] << 8 | cb[8];
+ snprintf(d->last, sizeof d->last, op == 0x28 ? "READ(10)" : op == 0x2A ? "WRITE(10)" : "VERIFY(10)");
+ if (d->lba > d->sectors || d->blocks > d->sectors - d->lba) {
+ sense(d, 0x05, 0x21, 0x00); /* LOGICAL BLOCK ADDRESS OUT OF RANGE */
+ d->status = 1;
+ break;
+ }
+ if (op == 0x2A && d->write_protect) {
+ sense(d, 0x07, 0x27, 0x00); /* DATA PROTECT, WRITE PROTECTED */
+ d->status = 1;
+ break;
+ }
+ d->reading = op == 0x28 && dir_in;
+ d->writing = op == 0x2A && !dir_in;
+ if ((op == 0x28 && !dir_in && d->want) || (op == 0x2A && dir_in && d->want))
+ d->status = 2; /* the direction the host gave is not the command's: a phase error */
+ break;
+ case 0x35: /* SYNCHRONIZE CACHE(10) */
+ snprintf(d->last, sizeof d->last, "SYNCHRONIZE CACHE");
+ break;
+ default:
+ snprintf(d->last, sizeof d->last, "0x%02X", op);
+ sense(d, 0x05, 0x20, 0x00); /* ILLEGAL REQUEST, INVALID COMMAND OPERATION CODE */
+ d->status = 1;
+ break;
+ }
+}
+
+static void start_status(drive_state *d)
+{
+ d->phase = STATUS;
+}
+
+static int msc_out(usbd *u, int ep, const uint8_t *data, uint32_t len)
+{
+ drive_state *d = (drive_state *)u->cls_ctx;
+ if (ep != 2)
+ return ESK_USB_STALL;
+ if (d->phase == IDLE) {
+ int dir_in;
+ if (len != 31 || get32le(data) != CBW_SIG || (data[13] & 0x0Fu) != 0 || data[14] < 1 || data[14] > 16) {
+ /* not a valid CBW: both endpoints halt until reset recovery
+ * (BOT 6.6.1) */
+ u->halted_in |= 1u << 1;
+ u->halted_out |= 1u << 2;
+ return ESK_USB_STALL;
+ }
+ d->tag = get32le(data + 4);
+ d->want = get32le(data + 8);
+ d->done = 0;
+ d->stall_after = 0;
+ dir_in = (data[12] & 0x80u) != 0;
+ command(d, data + 15, dir_in);
+ if (!d->want || d->status)
+ start_status(d);
+ else
+ d->phase = dir_in ? DATA_IN : DATA_OUT;
+ if (d->status && d->want && dir_in) {
+ /* nothing to send: the IN endpoint stalls, the host clears it
+ * and takes the CSW (BOT's case 4) */
+ u->halted_in |= 1u << 1;
+ } else if (d->status && d->want && !dir_in) {
+ u->halted_out |= 1u << 2; /* case 9 */
+ }
+ return ESK_USB_ACK;
+ }
+ if (d->phase == DATA_OUT) {
+ if (d->writing) {
+ uint32_t at = d->lba * SECTOR + d->done, end = (d->lba + d->blocks) * SECTOR;
+ uint32_t n = len;
+ if (at + n > end)
+ n = at < end ? end - at : 0;
+ memcpy(d->disk + at, data, n);
+ if ((at % SECTOR) + len >= SECTOR)
+ d->sectors_written += ((at % SECTOR) + len) / SECTOR;
+ }
+ d->done += len;
+ if (d->done >= d->want || len < MPS) {
+ if (d->writing)
+ list_files(d);
+ start_status(d);
+ }
+ return ESK_USB_ACK;
+ }
+ return ESK_USB_NAK; /* the CSW is still to be taken */
+}
+
+static int msc_in(usbd *u, int ep, uint8_t *data, uint32_t cap, uint32_t *len)
+{
+ drive_state *d = (drive_state *)u->cls_ctx;
+ uint32_t n;
+ if (ep != 1)
+ return ESK_USB_STALL;
+ if (d->phase == DATA_IN) {
+ uint32_t have = d->reading ? d->blocks * SECTOR : d->buf_len;
+ if (d->done >= have) {
+ /* less than the host asked, the last packet full: STALL ends
+ * the data phase (BOT 6.7.2) */
+ u->halted_in |= 1u << 1;
+ start_status(d);
+ return ESK_USB_STALL;
+ }
+ n = have - d->done;
+ if (n > d->want - d->done)
+ n = d->want - d->done;
+ if (n > MPS)
+ n = MPS;
+ if (n > cap)
+ n = cap;
+ if (d->reading) {
+ uint32_t at = d->lba * SECTOR + d->done;
+ memcpy(data, d->disk + at, n);
+ if ((at % SECTOR) + n >= SECTOR)
+ d->sectors_read++;
+ } else {
+ memcpy(data, d->buf + d->done, n);
+ }
+ d->done += n;
+ *len = n;
+ if (d->done >= d->want || (d->done >= have && n < MPS))
+ start_status(d); /* all of it, or a short packet's end */
+ else if (d->done >= have)
+ d->stall_after = 1;
+ return ESK_USB_ACK;
+ }
+ if (d->phase == STATUS) {
+ uint32_t residue = d->want > d->done ? d->want - d->done : 0;
+ put32le(data, CSW_SIG);
+ put32le(data + 4, d->tag);
+ put32le(data + 8, residue);
+ data[12] = d->status;
+ *len = 13;
+ d->phase = IDLE;
+ return ESK_USB_ACK;
+ }
+ return ESK_USB_NAK;
+}
+
+static int msc_setup(usbd *u, const uint8_t s[8], const uint8_t *data, uint8_t *out, uint32_t cap, uint32_t *len)
+{
+ drive_state *d = (drive_state *)u->cls_ctx;
+ (void)data;
+ (void)cap;
+ if ((s[0] & 0x60u) != 0x20u)
+ return ESK_USB_STALL;
+ if (s[1] == 0xFE) { /* Get Max LUN */
+ out[0] = 0;
+ *len = 1;
+ return ESK_USB_ACK;
+ }
+ if (s[1] == 0xFF) { /* Bulk-Only Mass Storage Reset: ready for a CBW */
+ d->phase = IDLE;
+ d->done = d->want = 0;
+ return ESK_USB_ACK;
+ }
+ return ESK_USB_STALL;
+}
+
+static void msc_configured(usbd *u, int cfg)
+{
+ drive_state *d = (drive_state *)u->cls_ctx;
+ (void)cfg;
+ d->phase = IDLE;
+}
+
+static const usbd_class msc_class = { msc_setup, msc_in, msc_out, msc_configured };
+
+static es_status f_create(esk_comp *c)
+{
+ drive_state *d = (drive_state *)c->state;
+ double mb = 8.0;
+ (void)esk_comp_param_double(c, "capacity_mb", &mb);
+ if (mb < 2.0)
+ mb = 2.0;
+ if (mb > 64.0)
+ mb = 64.0;
+ d->sectors = (uint32_t)(mb * 2048.0);
+ d->disk = (uint8_t *)esk_calloc(c->e->a, d->sectors, SECTOR);
+ if (!d->disk)
+ return ES_ENOMEM;
+ return usbd_init(&d->u, c, 0, dev_desc, cfg_desc, sizeof cfg_desc, strings, 3, &msc_class, d);
+}
+
+static void f_destroy(esk_comp *c)
+{
+ drive_state *d = (drive_state *)c->state;
+ if (!d)
+ return;
+ usbd_destroy(&d->u);
+ if (d->disk)
+ esk_free(c->e->a, d->disk, (size_t)d->sectors * SECTOR);
+ d->disk = NULL;
+}
+
+static void f_reset(esk_comp *c)
+{
+ drive_state *d = (drive_state *)c->state;
+ int wp = 0;
+ (void)esk_comp_param_bool(c, "write_protect", &wp);
+ d->write_protect = wp;
+ format(d);
+ list_files(d);
+ d->phase = IDLE;
+ d->want = d->done = 0;
+ sense(d, 0, 0, 0);
+ d->commands = d->sectors_read = d->sectors_written = 0;
+ d->last[0] = 0;
+ usbd_reset(&d->u);
+}
+
+static void f_input(esk_comp *c, esk_pin *pin)
+{
+ drive_state *d = (drive_state *)c->state;
+ (void)pin;
+ usbd_power(&d->u);
+}
+
+static es_status f_set(esk_comp *c, const char *input, const esk_json *value)
+{
+ drive_state *d = (drive_state *)c->state;
+ if (!strcmp(input, "plugged") && value->type == ESK_JSON_BOOL) {
+ d->u.plugged = value->u.boolean != 0;
+ usbd_power(&d->u);
+ return ES_OK;
+ }
+ if (!strcmp(input, "write_protect") && value->type == ESK_JSON_BOOL) {
+ d->write_protect = value->u.boolean != 0;
+ return ES_OK;
+ }
+ return ES_EINVAL;
+}
+
+static void f_observe(esk_comp *c, esk_json *out)
+{
+ drive_state *d = (drive_state *)c->state;
+ esk_alloc *a = c->e->a;
+ esk_json_set_string(a, out, "state", usbd_state(&d->u));
+ esk_json_set_integer(a, out, "address", d->u.addr);
+ esk_json_set_string(a, out, "files", d->files);
+ esk_json_set_integer(a, out, "commands", (int64_t)d->commands);
+ esk_json_set_string(a, out, "last_command", d->last);
+ esk_json_set_integer(a, out, "sectors_read", (int64_t)d->sectors_read);
+ esk_json_set_integer(a, out, "sectors_written", (int64_t)d->sectors_written);
+ esk_json_set_integer(a, out, "capacity_sectors", d->sectors);
+}
+
+const esk_model_ops esk_usb_flash_drive_ops = { "usb_flash_drive", sizeof(drive_state), f_create, f_destroy, f_reset,
+ f_input, NULL, f_set, f_observe, NULL, 0 };
diff --git a/core/model/usb_host.c b/core/model/usb_host.c
index f3abdea9..931e67cb 100644
--- a/core/model/usb_host.c
+++ b/core/model/usb_host.c
@@ -109,6 +109,7 @@ typedef struct uh_state {
uint32_t frame;
uint64_t frames;
int after_reset; /* the step to go on with when the reset ends */
+ int low_speed; /* the device pulled D- up */
/* suspend and resume */
esk_pin *vbus; /* VBUS at the device end of the cable, or NULL */
int allow_wakeup; /* the host arms remote wakeup before a suspend */
@@ -215,6 +216,12 @@ static void kick(uh_state *s, es_time_t t)
(void)esk_comp_wake_at(s->c, t, T_RUN);
}
+/* a transaction's bus time at the device's speed */
+static es_time_t xtime(const uh_state *s, int len, int handshake)
+{
+ return s->low_speed ? esk_usb_ls_time(len, handshake) : esk_usb_fs_time(len, handshake);
+}
+
static esk_usb_dev *device(uh_state *s) { return esk_usb_device_of(s->c->e, &s->link); }
/* the cable is in: VBUS at its device end (no VBUS pin wired: always) */
@@ -497,7 +504,7 @@ static void done(uh_state *s)
s->i_manu = b[14];
s->i_prod = b[15];
s->i_ser = b[16];
- say(s, "new full-speed USB device: idVendor %04x, idProduct %04x, bcdDevice %x.%02x", u16le(b + 8),
+ say(s, "new %s-speed USB device: idVendor %04x, idProduct %04x, bcdDevice %x.%02x", s->low_speed ? "low" : "full", u16le(b + 8),
u16le(b + 10), (unsigned)b[13], (unsigned)b[12]);
break;
case E_CFG9:
@@ -611,14 +618,14 @@ static es_time_t control_step(uh_state *s, esk_usb_dev *d, es_time_t now)
} else if (++s->ct.errs >= ERR_RETRIES) {
fail(s, "no handshake to SETUP");
}
- return esk_usb_fs_time(8, r != ESK_USB_NONE) + (r == ESK_USB_NONE ? TIMEOUT_EXTRA_PS : 0);
+ return xtime(s, 8, r != ESK_USB_NONE) + (r == ESK_USB_NONE ? TIMEOUT_EXTRA_PS : 0);
case C_DATA_IN: {
uint32_t room = s->ct.want - s->ct.got;
r = d->in(d->ctx, s->addr, 0, pkt, sizeof pkt, &len, &pid, now);
if (r == ESK_USB_ACK) {
if (len > (uint32_t)s->mps0) {
fail(s, "a packet longer than bMaxPacketSize0 (babble)");
- return esk_usb_fs_time((int)len, 1);
+ return xtime(s, (int)len, 1);
}
if ((pid != 0) == (s->ct.pid != 0)) {
memcpy(s->ct.buf + s->ct.got, pkt, len < room ? len : room);
@@ -628,13 +635,13 @@ static es_time_t control_step(uh_state *s, esk_usb_dev *d, es_time_t now)
s->ct.stage = C_STATUS_OUT;
} /* else a repeat: the host ACKs it and keeps what it has */
s->ct.errs = 0;
- return esk_usb_fs_time((int)len, 1);
+ return xtime(s, (int)len, 1);
}
if (r == ESK_USB_STALL)
stalled(s);
else if (r == ESK_USB_NONE && ++s->ct.errs >= ERR_RETRIES)
fail(s, "no answer to IN");
- return esk_usb_fs_time(-1, r != ESK_USB_NONE) + (r == ESK_USB_NONE ? TIMEOUT_EXTRA_PS : 0);
+ return xtime(s, -1, r != ESK_USB_NONE) + (r == ESK_USB_NONE ? TIMEOUT_EXTRA_PS : 0);
}
case C_DATA_OUT: {
uint32_t n = s->ct.want - s->ct.got;
@@ -652,7 +659,7 @@ static es_time_t control_step(uh_state *s, esk_usb_dev *d, es_time_t now)
} else if (r == ESK_USB_NONE && ++s->ct.errs >= ERR_RETRIES) {
fail(s, "no answer to OUT");
}
- return esk_usb_fs_time((int)n, r != ESK_USB_NONE) + (r == ESK_USB_NONE ? TIMEOUT_EXTRA_PS : 0);
+ return xtime(s, (int)n, r != ESK_USB_NONE) + (r == ESK_USB_NONE ? TIMEOUT_EXTRA_PS : 0);
}
case C_STATUS_OUT:
r = d->out(d->ctx, s->addr, 0, 1, NULL, 0, now);
@@ -662,7 +669,7 @@ static es_time_t control_step(uh_state *s, esk_usb_dev *d, es_time_t now)
stalled(s);
else if (r == ESK_USB_NONE && ++s->ct.errs >= ERR_RETRIES)
fail(s, "no answer to the status stage");
- return esk_usb_fs_time(0, r != ESK_USB_NONE) + (r == ESK_USB_NONE ? TIMEOUT_EXTRA_PS : 0);
+ return xtime(s, 0, r != ESK_USB_NONE) + (r == ESK_USB_NONE ? TIMEOUT_EXTRA_PS : 0);
default: /* C_STATUS_IN */
r = d->in(d->ctx, s->addr, 0, pkt, sizeof pkt, &len, &pid, now);
if (r == ESK_USB_ACK) {
@@ -670,13 +677,13 @@ static es_time_t control_step(uh_state *s, esk_usb_dev *d, es_time_t now)
done(s);
else
fail(s, "data in the status stage");
- return esk_usb_fs_time((int)len, 1);
+ return xtime(s, (int)len, 1);
}
if (r == ESK_USB_STALL)
stalled(s);
else if (r == ESK_USB_NONE && ++s->ct.errs >= ERR_RETRIES)
fail(s, "no answer to the status stage");
- return esk_usb_fs_time(-1, r != ESK_USB_NONE) + (r == ESK_USB_NONE ? TIMEOUT_EXTRA_PS : 0);
+ return xtime(s, -1, r != ESK_USB_NONE) + (r == ESK_USB_NONE ? TIMEOUT_EXTRA_PS : 0);
}
}
@@ -699,11 +706,11 @@ static es_time_t poll_hid(uh_state *s, esk_usb_dev *d, uh_hid *h, es_time_t now)
s->y += (int8_t)pkt[2];
}
}
- return esk_usb_fs_time((int)len, 1);
+ return xtime(s, (int)len, 1);
}
if (r == ESK_USB_STALL)
say(s, "HID endpoint %d stalls", h->ep);
- return esk_usb_fs_time(-1, r != ESK_USB_NONE) + (r == ESK_USB_NONE ? TIMEOUT_EXTRA_PS : 0);
+ return xtime(s, -1, r != ESK_USB_NONE) + (r == ESK_USB_NONE ? TIMEOUT_EXTRA_PS : 0);
}
@@ -1072,12 +1079,13 @@ static void run(uh_state *s)
switch (s->phase) {
case P_DETACHED:
/* until the device descriptor says, EP0 takes 64-byte packets at
- * full speed (as Linux and Windows assume) */
- s->mps0 = 64;
+ * full speed (as Linux and Windows assume), 8 at low speed */
+ s->low_speed = d->pullup(d->ctx) == ESK_USB_LOW_SPEED;
+ s->mps0 = s->low_speed ? 8 : 64;
s->phase = P_DEBOUNCE;
s->state = S_ATTACHED;
s->phase_end = now + s->debounce_ps;
- say(s, "full-speed device attached");
+ say(s, s->low_speed ? "low-speed device attached" : "full-speed device attached");
kick(s, s->phase_end);
return;
case P_DEBOUNCE:
@@ -1139,7 +1147,7 @@ static void run(uh_state *s)
s->frames++;
d->bus(d->ctx, ESK_USB_SOF, s->frame, now);
s->next_sof += FRAME_PS;
- s->busy_until = now + esk_usb_fs_time(-1, 0);
+ s->busy_until = now + xtime(s, -1, 0);
for (i = 0; i < s->nhid && s->state == S_CONFIGURED; i++) {
uh_hid *h = &s->hid[i];
if (s->hid_i > i && ((s->frame - h->next_frame) & 0x7ffu) < 0x400u) {
@@ -1176,7 +1184,7 @@ static void run(uh_state *s)
dfu_start(s);
}
if (!d_t && s->ct.on) {
- if (now + esk_usb_fs_time(64, 1) + EOF_PS > s->next_sof) {
+ if (now + xtime(s, 64, 1) + EOF_PS > s->next_sof) {
kick(s, s->next_sof); /* not across the frame's start */
return;
}
diff --git a/core/model/usb_mouse.c b/core/model/usb_mouse.c
new file mode 100644
index 00000000..05792044
--- /dev/null
+++ b/core/model/usb_mouse.c
@@ -0,0 +1,232 @@
+/* SPDX-License-Identifier: Apache-2.0 */
+/* Copyright 2026 Junior Deogracias */
+/* usb_mouse.c -- a USB mouse: a low-speed HID boot mouse (HID 1.11,
+ * appendix B.2 and E.10) on a cable of the bench (usb_dev.h).
+ *
+ * - its descriptors: a device of class 0, one configuration (bus
+ * powered, remote wakeup, 100 mA), one interface of class 3, subclass
+ * 1 (boot), protocol 2 (mouse), its HID descriptor and an interrupt IN
+ * endpoint 0x81 of 4 bytes polled every 10 ms; EP0's 8-byte packets, as
+ * a low-speed device has them;
+ * - its report: buttons (3 bits), X, Y and the wheel, each a signed
+ * byte, the report protocol's report being the boot protocol's (its
+ * report descriptor says so);
+ * - the inputs move it (move_x, move_y, wheel: counts added) and press
+ * its buttons (buttons, bit 0 left): a report goes at the next poll
+ * when something changed, at most 127 counts each way in one, the rest
+ * in the next; nothing changed, the endpoint NAKs, or repeats the last
+ * report every SET_IDLE duration (HID 7.2.4; 0: never, the default
+ * after SET_IDLE(0));
+ * - GET_REPORT, GET_IDLE, SET_IDLE, GET_PROTOCOL, SET_PROTOCOL; the HID
+ * and report descriptors by GET_DESCRIPTOR on the interface;
+ * - its outputs: what it has sent and the host's view of it.
+ * Inside: no optical sensor (the movement is the inputs'), no remote
+ * wakeup signalling yet. */
+#include
+
+#include "engine/engine.h"
+#include "model/registry.h"
+#include "model/usb_dev.h"
+
+static const uint8_t dev_desc[18] = {
+ 18, 1, 0x00, 0x02, 0, 0, 0, 8, /* USB 2.0, class in the interface, EP0 8 bytes */
+ 0x6D, 0x04, 0x6C, 0xC0, /* idVendor 0x046D, idProduct 0xC06C: a generic optical mouse's */
+ 0x00, 0x01, 1, 2, 0, 1, /* bcdDevice 1.00, strings 1, 2, no serial, one configuration */
+};
+
+static const uint8_t report_desc[50] = {
+ 0x05, 0x01, 0x09, 0x02, 0xA1, 0x01, 0x09, 0x01, 0xA1, 0x00, 0x05, 0x09, 0x19, 0x01, 0x29, 0x03,
+ 0x15, 0x00, 0x25, 0x01, 0x95, 0x03, 0x75, 0x01, 0x81, 0x02, 0x95, 0x01, 0x75, 0x05, 0x81, 0x01,
+ 0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x38, 0x15, 0x81, 0x25, 0x7F, 0x75, 0x08, 0x95, 0x03,
+ 0x81, 0x06, /* X, Y, wheel: relative */
+};
+#define REPORT_LEN 4u
+
+static const uint8_t cfg_desc[34] = {
+ 9, 2, 34, 0, 1, 1, 0, 0xA0, 50, /* bus powered, remote wakeup, 100 mA */
+ 9, 4, 0, 0, 1, 3, 1, 2, 0, /* HID, boot, mouse */
+ 9, 0x21, 0x11, 0x01, 0, 1, 0x22, sizeof report_desc, 0,
+ 7, 5, 0x81, 3, REPORT_LEN, 0, 10, /* interrupt IN, 4 bytes, 10 ms */
+};
+
+static const char *const strings[2] = { "EmbSIM", "USB Optical Mouse" };
+
+typedef struct mouse_state {
+ usbd u;
+ int64_t dx, dy, dw; /* movement still to send */
+ int buttons, sent_buttons;
+ int idle_4ms, protocol;
+ uint8_t last[REPORT_LEN];
+ uint64_t reports;
+ int64_t sent_x, sent_y;
+ uint64_t idle_frame;
+} mouse_state;
+
+static int clamp(int64_t *v)
+{
+ int64_t n = *v > 127 ? 127 : *v < -127 ? -127 : *v;
+ *v -= n;
+ return (int)n;
+}
+
+static void make_report(mouse_state *m, uint8_t *r)
+{
+ int x = clamp(&m->dx), y = clamp(&m->dy), w = clamp(&m->dw);
+ r[0] = (uint8_t)(m->buttons & 7);
+ r[1] = (uint8_t)(int8_t)x;
+ r[2] = (uint8_t)(int8_t)y;
+ r[3] = (uint8_t)(int8_t)w;
+ m->sent_x += x;
+ m->sent_y += y;
+}
+
+static int m_setup(usbd *u, const uint8_t s[8], const uint8_t *data, uint8_t *out, uint32_t cap, uint32_t *len)
+{
+ mouse_state *m = (mouse_state *)u->cls_ctx;
+ (void)data;
+ (void)cap;
+ if ((s[0] & 0x60u) != 0x20u) /* class */
+ return ESK_USB_STALL;
+ switch (s[1]) {
+ case 0x01: /* GET_REPORT */
+ memcpy(out, m->last, REPORT_LEN);
+ *len = REPORT_LEN;
+ return ESK_USB_ACK;
+ case 0x02: /* GET_IDLE */
+ out[0] = (uint8_t)m->idle_4ms;
+ *len = 1;
+ return ESK_USB_ACK;
+ case 0x03: /* GET_PROTOCOL */
+ out[0] = (uint8_t)m->protocol;
+ *len = 1;
+ return ESK_USB_ACK;
+ case 0x0A: /* SET_IDLE: the duration in 4 ms units */
+ m->idle_4ms = s[3];
+ m->idle_frame = u->frames;
+ return ESK_USB_ACK;
+ case 0x0B: /* SET_PROTOCOL */
+ m->protocol = s[2] & 1;
+ return ESK_USB_ACK;
+ default:
+ return ESK_USB_STALL;
+ }
+}
+
+static int m_in(usbd *u, int ep, uint8_t *data, uint32_t cap, uint32_t *len)
+{
+ mouse_state *m = (mouse_state *)u->cls_ctx;
+ int changed = m->dx || m->dy || m->dw || m->buttons != m->sent_buttons;
+ int idle_due = m->idle_4ms && u->frames - m->idle_frame >= (uint64_t)m->idle_4ms * 4u;
+ if (ep != 1 || cap < REPORT_LEN)
+ return ESK_USB_STALL;
+ if (!changed && !idle_due)
+ return ESK_USB_NAK;
+ if (changed)
+ make_report(m, m->last);
+ m->sent_buttons = m->buttons;
+ m->idle_frame = u->frames;
+ memcpy(data, m->last, REPORT_LEN);
+ *len = REPORT_LEN;
+ m->reports++;
+ return ESK_USB_ACK;
+}
+
+static void m_configured(usbd *u, int cfg)
+{
+ mouse_state *m = (mouse_state *)u->cls_ctx;
+ (void)cfg;
+ m->idle_4ms = 0;
+ m->protocol = 1; /* report protocol after a reset (HID 7.2.6) */
+}
+
+/* the class's descriptors: GET_DESCRIPTOR on the interface (HID 7.1) */
+static int m_setup_std(usbd *u, const uint8_t s[8], const uint8_t *data, uint8_t *out, uint32_t cap, uint32_t *len)
+{
+ if ((s[0] & 0x60u) == 0 && s[1] == 6 && (s[0] & 3u) == 1) {
+ if (s[3] == 0x22) {
+ memcpy(out, report_desc, sizeof report_desc);
+ *len = sizeof report_desc;
+ return ESK_USB_ACK;
+ }
+ if (s[3] == 0x21) {
+ memcpy(out, cfg_desc + 18, 9);
+ *len = 9;
+ return ESK_USB_ACK;
+ }
+ }
+ return m_setup(u, s, data, out, cap, len);
+}
+
+static const usbd_class mouse_class = { m_setup_std, m_in, NULL, m_configured };
+
+static es_status m_create(esk_comp *c)
+{
+ mouse_state *m = (mouse_state *)c->state;
+ return usbd_init(&m->u, c, 1, dev_desc, cfg_desc, sizeof cfg_desc, strings, 2, &mouse_class, m);
+}
+
+static void m_destroy(esk_comp *c)
+{
+ mouse_state *m = (mouse_state *)c->state;
+ if (m)
+ usbd_destroy(&m->u);
+}
+
+static void m_reset(esk_comp *c)
+{
+ mouse_state *m = (mouse_state *)c->state;
+ m->dx = m->dy = m->dw = 0;
+ m->buttons = m->sent_buttons = 0;
+ m->idle_4ms = 0;
+ m->protocol = 1;
+ memset(m->last, 0, sizeof m->last);
+ m->reports = 0;
+ m->sent_x = m->sent_y = 0;
+ usbd_reset(&m->u);
+}
+
+static void m_input(esk_comp *c, esk_pin *pin)
+{
+ mouse_state *m = (mouse_state *)c->state;
+ (void)pin;
+ usbd_power(&m->u);
+}
+
+static es_status m_set(esk_comp *c, const char *input, const esk_json *value)
+{
+ mouse_state *m = (mouse_state *)c->state;
+ if (!strcmp(input, "plugged") && value->type == ESK_JSON_BOOL) {
+ m->u.plugged = value->u.boolean != 0;
+ usbd_power(&m->u);
+ return ES_OK;
+ }
+ if (value->type != ESK_JSON_NUMBER)
+ return ES_EINVAL;
+ if (!strcmp(input, "move_x"))
+ m->dx += (int64_t)value->u.number.value;
+ else if (!strcmp(input, "move_y"))
+ m->dy += (int64_t)value->u.number.value;
+ else if (!strcmp(input, "wheel"))
+ m->dw += (int64_t)value->u.number.value;
+ else if (!strcmp(input, "buttons"))
+ m->buttons = (int)value->u.number.value & 7;
+ else
+ return ES_EINVAL;
+ return ES_OK;
+}
+
+static void m_observe(esk_comp *c, esk_json *out)
+{
+ mouse_state *m = (mouse_state *)c->state;
+ esk_alloc *a = c->e->a;
+ esk_json_set_string(a, out, "state", usbd_state(&m->u));
+ esk_json_set_integer(a, out, "address", m->u.addr);
+ esk_json_set_integer(a, out, "reports", (int64_t)m->reports);
+ esk_json_set_integer(a, out, "sent_x", m->sent_x);
+ esk_json_set_integer(a, out, "sent_y", m->sent_y);
+ esk_json_set_integer(a, out, "protocol", m->protocol);
+ esk_json_set_integer(a, out, "idle_ms", m->idle_4ms * 4);
+}
+
+const esk_model_ops esk_usb_mouse_ops = { "usb_mouse", sizeof(mouse_state), m_create, m_destroy, m_reset, m_input,
+ NULL, m_set, m_observe, NULL, 0 };
diff --git a/core/session/session.c b/core/session/session.c
index e7b7cd7a..97ba8161 100644
--- a/core/session/session.c
+++ b/core/session/session.c
@@ -347,7 +347,7 @@ static double probe_value(es_session *s, esk_probe *p)
case ESK_PROBE_DIGITAL:
if (p->net && p->net->island) {
/* threshold with the default family's levels and hysteresis */
- double volts = p->net->voltage;
+ double volts = esk_net_voltage_now(p->net);
if (!p->has_last)
return volts >= 1.4 ? 1.0 : 0.0;
if (p->last >= 1.0)
@@ -357,7 +357,8 @@ static double probe_value(es_session *s, esk_probe *p)
return p->net ? (double)esk_net_value(p->net) : 2.0;
case ESK_PROBE_ANALOG:
if (p->net)
- return p->net->island ? p->net->voltage : (esk_net_value(p->net) == ESK_L1 ? s->eng.default_supply_v : 0.0);
+ return p->net->island ? esk_net_voltage_now(p->net)
+ : (esk_net_value(p->net) == ESK_L1 ? s->eng.default_supply_v : 0.0);
/* a device quantity: same as a value probe */
/* fall through */
case ESK_PROBE_VALUE:
diff --git a/docs/STATUS.md b/docs/STATUS.md
index b739e2e6..4719bb32 100644
--- a/docs/STATUS.md
+++ b/docs/STATUS.md
@@ -5,6 +5,45 @@ maintained by hand until evidence tooling exists. It is not a generated
support matrix. Specifications define target contracts; their acceptance
does not mean the described behaviour exists.
+## USB host mode, a USB mouse, a flash drive, and Adafruit's TFT shield (2026-10-31)
+
+The STM32F446's OTG FS core works as a host too (`stm32-otg-fs` with
+FHMOD: the port, frames and twelve channels as ST's HCD driver uses them),
+and the MCU is both ends of its cable. USB parts share a device core
+(`core/model/usb_dev.c`); `emblink/usb_mouse` is a low-speed HID boot mouse,
+`emblink/usb_flash_drive` a full-speed mass storage device (Bulk-Only,
+SCSI) whose medium is FAT16 as from the shop.
+The NUCLEO-F446ZE's U12 (`emblink/stmps2151str`, values from ST's
+datasheet) powers CN13's VBUS from PG6. `emblink/st7735_tft18` (a C model of
+the ST7735R and its 1.8" panel) and `emblink/adafruit_tft_shield18` (its
+joystick ladder from Adafruit's schematic) give ST's host examples their
+display. Evidence: `test_stm32f4`'s `test_otg_host` (connection with PPWR,
+PRST's reset and the port enabled at full and low speed, SOFs every 1 ms,
+a SETUP waiting for its FIFO words and taking 14 us, IN data with its
+statuses and XFRC on the taking, NAK stopping the channel, TXERR, the halt
+and CHH, an OUT's toggle, an interrupt channel once a frame, disconnection,
+a low-speed SETUP's 113 us, PSUSP and PRES); the mouse's fixture on the
+PC's host; U12's four fixtures (on at 0.5 A within the datasheet's
+resistance, EN at VIL and VIH) and the board's `usb-host-vbus`; the TFT's
+six fixtures (16 bits a pixel, MADCTL's mirroring, BGR and inversion,
+sleep, the reset cancel time, ST's cursor-only CASET); the shield's six
+(each direction in BSP_JOY_GetState's window); CubeF4's USB_Host/
+HID_Standalone, unmodified, 13/13: VBUS on through U12 at 150 ms, the
+mouse enumerated at low speed (address 1, report protocol), the log on the
+LCD, SEL starting the mouse demo once the log is quiet (while it scrolls
+the main loop draws and does not read the joystick, as on the board), its
+window drawn, reports as the mouse moves and clicks; the drive's fixture on
+the PC's port; CubeF4's USB_Host/MSC_Standalone, unmodified, 14/14: the
+drive enumerated at full speed, its LUN taken, FatFs mounting it, B1
+writing 'USBHost.txt' and reading it back, the drive then holding
+USBHOST.TXT (40 bytes). The analog islands
+lag up to 10 us behind an MCU's SPI clock instead of stepping at each edge
+(caught up before any reading), which brought this run from 18 min to
+2.5 min. Not yet: the ID pin, SRP and HNP, the drive's NAND busy times
+and an image file kept between runs, hubs and split transactions, the over-current flag's
+blanking and current limit in U12, the shield's microSD socket, a
+workbench view of the LCD and the USB host's state.
+
## Bench instruments: the Saleae Logic 8 (2026-10-31)
The first of three real bench instruments (`docs/engineering/bench-instruments.md`):
@@ -49,10 +88,10 @@ GPIOToggle .dfu as dfu-util would (with its static allocator's size
corrected: as shipped it overruns into the PCD handle and never answers
SET_CONFIGURATION, which its own scenario asserts), resetting into it with
the flash kept, 10/10 and 5/5; `emblink/usb_host`'s and
-`emblink/usb_host_controller`'s fixtures. Not yet: host mode and U12,
-class drivers beyond HID's polling and DFU's download (CDC and MSC are
+`emblink/usb_host_controller`'s fixtures. Not yet (then): host mode and U12
+(now above), class drivers beyond HID's polling and DFU's download (CDC and MSC are
configured, then idle), the USB/IP bridge to the computer's own USB, LPM, low-speed devices
-and hubs, the other MCUs' USB controllers (RP2040, nRF52840, PIC32,
+(now above) and hubs, the other MCUs' USB controllers (RP2040, nRF52840, PIC32,
ESP32-C3).
## The STM32F446 completed further, and the NUCLEO-F446ZE (2026-10-31)
diff --git a/docs/coverage.md b/docs/coverage.md
index 3e0ca04b..941e3c69 100644
--- a/docs/coverage.md
+++ b/docs/coverage.md
@@ -442,7 +442,7 @@ Boards: NUCLEO-F446RE (STM32F446RE).
| QUADSPI | — | **missing** |
| DBG | — | **missing** |
-Declared omissions: watchdogs: LSI's spread (32 kHz here), the debug freeze bits (a debugger's halt stops all of the machine's time); the WWDG's counter while WDGA is clear counts from the last access made while its clock is on; RTC: the reference clock detection (REFCKON, RTC_REFIN), RTC_AF2 (PI8, not bonded), the tamper input's precharge and pull-up, the 8 and 16 s calibration windows (the rate is applied evenly), RECALPF's wait; ADC: the start-up time after ADON (conversions are exact at once), the input's source impedance against the sampling time, the converter's offset, gain, linearity errors and noise (the code is the ideal one), the die's self-heating; DAC: the buffer's offset and gain errors and load regulation, the conversion's non-linearity; I2C: FLTR's analog and digital noise filters (the circuit's edges are taken as they come), the SMBus's cumulative clock low extensions (TLOW:SEXT, TLOW:MEXT), clock synchronization between two masters, TRISE's rise time compensation, CR2.FREQ (times come from the real PCLK1); FMPI2C1 (another IP) keeps its registers; SPI: I2S (I2SCFGR's I2SMOD keeps its value and the SPI stays an SPI; no catalog part speaks I2S), the TI slave's MISO release half a clock after the last bit (let go at the frame's end); DMA: the arbitration's delays between streams (a request is served at once), the direct mode and FIFO overrun/underrun errors (DMEIF never sets, FEIF only for the configuration error), the peripheral flow controller (PFCTRL), the bus matrix's timing (memory-to-memory moves an item every two cycles); dma_requests omits SAI2's and SPI4's requests; USART: noise detection (one sample a bit: NF never sets, ONEBIT changes nothing), smartcard mode (SCEN, NACK, GTPR) and IrDA (IREN, IRLP), which no catalog part speaks; timers: TIM5's TI4 on the RTC's wake-up interrupt (IT4_RMP 11) and TIM2's ITR1 on the OTG controllers' SOF, MSM's delay, the idle levels' dead time after a break, OCxFE; TIMx_CCy as an ADC trigger is OCyREF for an output channel and a pulse at a capture or a frozen channel's match; SysTick: STK_CALIB's TENMS (RM0390's value not taken: it reads 0 with SKEW set); GPIO: EVENTOUT (AF15, SEV's pulse), the I/O compensation cell's effect on the edges; SYSCFG: the FMC memories MEMRMP can map at 0 (none is on the bus), PMC's ADC DC2 bits' effect; EXTI: the internal lines' sources but the RTC's (the PVD, the USB OTG controllers' wake-ups) until their models land; SWIER triggers them; RCC: the clock security system's detection (CSSON is kept; the board's HSE never fails), a peripheral held in reset while its *RSTR bit stays set (its reset happens on the write), the brownout reset (BORRSTF only at power-on), the start-up times' spread between parts and with temperature and LSEMOD's drive level (the typical times always), MCO1 and MCO2 on their pins, and the I2S, SAI, SPDIF-RX, CEC and FMPI2C clocks from PLLI2S and PLLSAI (DCKCFGR, DCKCFGR2) beyond TIMPRE and the 48 MHz clock's CK48MSEL (PLL_Q or PLLSAI_P, which the USB OTG FS core needs); PWR: the regulator's settling times and the low-power modes' wake-up times (DS10693's low-power mode wake-up timings: the core runs at the wake-up event), the regulator's settings (LPDS, FPDS, LPLVDS, MRLVDS: the supply current is not modelled), the programmable voltage detector, DBGMCU_CR's DBG_STOP and DBG_STANDBY (a debugger's halt stops all of the machine's time), the tamper and timestamp pin in Standby beyond RTC_OUT; DWT's CYCCNT counts through Stop; CAN, SDIO, QUADSPI, SAI, SPDIF-RX, HDMI-CEC, DCMI, FMC, FMPI2C and the USB OTG HS controller; USB OTG FS: host mode (its channel registers are storage), the ID pin, SRP and HNP, isochronous endpoints' frame parity and incomplete-transfer interrupts, LPM, GCCFG's battery-charging detection, remote wakeup signalling (RWUSIG), the OTG_FS_WKUP interrupt through EXTI line 18, the SOF pulse on PA8; the device's turnaround inside a transaction; GSNPSID reads 0x4F54320A (core 3.20a), the value TinyUSB's dwc2_info table measures on the F412's and F76x's OTG_FS, the same generation of the core (ST's HAL groups the F446 with the F412, F413 and F469); no ST document gives it and it was not read from an F446; flash: the times' spread and their maximum values (the typical ones always), the ART accelerator's cache hits during an operation (a program in the flash stalls whatever its cache holds), a program in SRAM whose interrupt handler is in the flash (it does not stall), the option bytes' programming time (OPTSTRT is done at once), a reset during an operation leaving the memory half-programmed, HCLK's 1 MHz minimum, the read protection's levels (RDP stored, no mass erase on a regression), PCROP's read refusal (RDERR), the ART accelerator's caches (DCRST, ICRST); CRC: the bus stall of a DR access while a word is computed (four AHB clocks), and a byte or halfword write to DR, which RM0390 does not allow, feeding its value zero-extended.
+Declared omissions: watchdogs: LSI's spread (32 kHz here), the debug freeze bits (a debugger's halt stops all of the machine's time); the WWDG's counter while WDGA is clear counts from the last access made while its clock is on; RTC: the reference clock detection (REFCKON, RTC_REFIN), RTC_AF2 (PI8, not bonded), the tamper input's precharge and pull-up, the 8 and 16 s calibration windows (the rate is applied evenly), RECALPF's wait; ADC: the start-up time after ADON (conversions are exact at once), the input's source impedance against the sampling time, the converter's offset, gain, linearity errors and noise (the code is the ideal one), the die's self-heating; DAC: the buffer's offset and gain errors and load regulation, the conversion's non-linearity; I2C: FLTR's analog and digital noise filters (the circuit's edges are taken as they come), the SMBus's cumulative clock low extensions (TLOW:SEXT, TLOW:MEXT), clock synchronization between two masters, TRISE's rise time compensation, CR2.FREQ (times come from the real PCLK1); FMPI2C1 (another IP) keeps its registers; SPI: I2S (I2SCFGR's I2SMOD keeps its value and the SPI stays an SPI; no catalog part speaks I2S), the TI slave's MISO release half a clock after the last bit (let go at the frame's end); DMA: the arbitration's delays between streams (a request is served at once), the direct mode and FIFO overrun/underrun errors (DMEIF never sets, FEIF only for the configuration error), the peripheral flow controller (PFCTRL), the bus matrix's timing (memory-to-memory moves an item every two cycles); dma_requests omits SAI2's and SPI4's requests; USART: noise detection (one sample a bit: NF never sets, ONEBIT changes nothing), smartcard mode (SCEN, NACK, GTPR) and IrDA (IREN, IRLP), which no catalog part speaks; timers: TIM5's TI4 on the RTC's wake-up interrupt (IT4_RMP 11) and TIM2's ITR1 on the OTG controllers' SOF, MSM's delay, the idle levels' dead time after a break, OCxFE; TIMx_CCy as an ADC trigger is OCyREF for an output channel and a pulse at a capture or a frozen channel's match; SysTick: STK_CALIB's TENMS (RM0390's value not taken: it reads 0 with SKEW set); GPIO: EVENTOUT (AF15, SEV's pulse), the I/O compensation cell's effect on the edges; SYSCFG: the FMC memories MEMRMP can map at 0 (none is on the bus), PMC's ADC DC2 bits' effect; EXTI: the internal lines' sources but the RTC's (the PVD, the USB OTG controllers' wake-ups) until their models land; SWIER triggers them; RCC: the clock security system's detection (CSSON is kept; the board's HSE never fails), a peripheral held in reset while its *RSTR bit stays set (its reset happens on the write), the brownout reset (BORRSTF only at power-on), the start-up times' spread between parts and with temperature and LSEMOD's drive level (the typical times always), MCO1 and MCO2 on their pins, and the I2S, SAI, SPDIF-RX, CEC and FMPI2C clocks from PLLI2S and PLLSAI (DCKCFGR, DCKCFGR2) beyond TIMPRE and the 48 MHz clock's CK48MSEL (PLL_Q or PLLSAI_P, which the USB OTG FS core needs); PWR: the regulator's settling times and the low-power modes' wake-up times (DS10693's low-power mode wake-up timings: the core runs at the wake-up event), the regulator's settings (LPDS, FPDS, LPLVDS, MRLVDS: the supply current is not modelled), the programmable voltage detector, DBGMCU_CR's DBG_STOP and DBG_STANDBY (a debugger's halt stops all of the machine's time), the tamper and timestamp pin in Standby beyond RTC_OUT; DWT's CYCCNT counts through Stop; CAN, SDIO, QUADSPI, SAI, SPDIF-RX, HDMI-CEC, DCMI, FMC, FMPI2C and the USB OTG HS controller; USB OTG FS: the ID pin, SRP and HNP, split transactions through a hub, isochronous endpoints' frame parity and incomplete-transfer interrupts and a host's isochronous channels, FRMOR, LPM, GCCFG's battery-charging detection, the OTG_FS_WKUP interrupt through EXTI line 18, the SOF pulse on PA8; the device's turnaround inside a transaction (a transaction is whole), a host channel's OUT data kept per channel rather than in the shared FIFO's order (the words free are the same); GSNPSID reads 0x4F54320A (core 3.20a), the value TinyUSB's dwc2_info table measures on the F412's and F76x's OTG_FS, the same generation of the core (ST's HAL groups the F446 with the F412, F413 and F469); no ST document gives it and it was not read from an F446; flash: the times' spread and their maximum values (the typical ones always), the ART accelerator's cache hits during an operation (a program in the flash stalls whatever its cache holds), a program in SRAM whose interrupt handler is in the flash (it does not stall), the option bytes' programming time (OPTSTRT is done at once), a reset during an operation leaving the memory half-programmed, HCLK's 1 MHz minimum, the read protection's levels (RDP stored, no mass erase on a regression), PCROP's read refusal (RDERR), the ART accelerator's caches (DCRST, ICRST); CRC: the bus stall of a DR access while a word is computed (four AHB clocks), and a byte or halfword write to DR, which RM0390 does not allow, feeding its value zero-extended.
## STM32F446ZE (`st/stm32f446ze`)
@@ -520,4 +520,4 @@ Boards: NUCLEO-F446ZE (STM32F446ZE).
| QUADSPI | — | **missing** |
| DBG | — | **missing** |
-Declared omissions: watchdogs: LSI's spread (32 kHz here), the debug freeze bits (a debugger's halt stops all of the machine's time); the WWDG's counter while WDGA is clear counts from the last access made while its clock is on; RTC: the reference clock detection (REFCKON, RTC_REFIN), RTC_AF2 (PI8, not bonded), the tamper input's precharge and pull-up, the 8 and 16 s calibration windows (the rate is applied evenly), RECALPF's wait; ADC: the start-up time after ADON (conversions are exact at once), the input's source impedance against the sampling time, the converter's offset, gain, linearity errors and noise (the code is the ideal one), the die's self-heating; DAC: the buffer's offset and gain errors and load regulation, the conversion's non-linearity; I2C: FLTR's analog and digital noise filters (the circuit's edges are taken as they come), the SMBus's cumulative clock low extensions (TLOW:SEXT, TLOW:MEXT), clock synchronization between two masters, TRISE's rise time compensation, CR2.FREQ (times come from the real PCLK1); FMPI2C1 (another IP) keeps its registers; SPI: I2S (I2SCFGR's I2SMOD keeps its value and the SPI stays an SPI; no catalog part speaks I2S), the TI slave's MISO release half a clock after the last bit (let go at the frame's end); DMA: the arbitration's delays between streams (a request is served at once), the direct mode and FIFO overrun/underrun errors (DMEIF never sets, FEIF only for the configuration error), the peripheral flow controller (PFCTRL), the bus matrix's timing (memory-to-memory moves an item every two cycles); dma_requests omits SAI2's and SPI4's requests; USART: noise detection (one sample a bit: NF never sets, ONEBIT changes nothing), smartcard mode (SCEN, NACK, GTPR) and IrDA (IREN, IRLP), which no catalog part speaks; timers: TIM5's TI4 on the RTC's wake-up interrupt (IT4_RMP 11) and TIM2's ITR1 on the OTG controllers' SOF, MSM's delay, the idle levels' dead time after a break, OCxFE; TIMx_CCy as an ADC trigger is OCyREF for an output channel and a pulse at a capture or a frozen channel's match; SysTick: STK_CALIB's TENMS (RM0390's value not taken: it reads 0 with SKEW set); GPIO: EVENTOUT (AF15, SEV's pulse), the I/O compensation cell's effect on the edges; SYSCFG: the FMC memories MEMRMP can map at 0 (none is on the bus), PMC's ADC DC2 bits' effect; EXTI: the internal lines' sources but the RTC's (the PVD, the USB OTG controllers' wake-ups) until their models land; SWIER triggers them; RCC: the clock security system's detection (CSSON is kept; the board's HSE never fails), a peripheral held in reset while its *RSTR bit stays set (its reset happens on the write), the brownout reset (BORRSTF only at power-on), the start-up times' spread between parts and with temperature and LSEMOD's drive level (the typical times always), MCO1 and MCO2 on their pins, and the I2S, SAI, SPDIF-RX, CEC and FMPI2C clocks from PLLI2S and PLLSAI (DCKCFGR, DCKCFGR2) beyond TIMPRE and the 48 MHz clock's CK48MSEL (PLL_Q or PLLSAI_P, which the USB OTG FS core needs); PWR: the regulator's settling times and the low-power modes' wake-up times (DS10693's low-power mode wake-up timings: the core runs at the wake-up event), the regulator's settings (LPDS, FPDS, LPLVDS, MRLVDS: the supply current is not modelled), the programmable voltage detector, DBGMCU_CR's DBG_STOP and DBG_STANDBY (a debugger's halt stops all of the machine's time), the tamper and timestamp pin in Standby beyond RTC_OUT; DWT's CYCCNT counts through Stop; CAN, SDIO, QUADSPI, SAI, SPDIF-RX, HDMI-CEC, DCMI, FMC, FMPI2C and the USB OTG HS controller; USB OTG FS: host mode (its channel registers are storage), the ID pin, SRP and HNP, isochronous endpoints' frame parity and incomplete-transfer interrupts, LPM, GCCFG's battery-charging detection, remote wakeup signalling (RWUSIG), the OTG_FS_WKUP interrupt through EXTI line 18, the SOF pulse on PA8; the device's turnaround inside a transaction; GSNPSID reads 0x4F54320A (core 3.20a), the value TinyUSB's dwc2_info table measures on the F412's and F76x's OTG_FS, the same generation of the core (ST's HAL groups the F446 with the F412, F413 and F469); no ST document gives it and it was not read from an F446; flash: the times' spread and their maximum values (the typical ones always), the ART accelerator's cache hits during an operation (a program in the flash stalls whatever its cache holds), a program in SRAM whose interrupt handler is in the flash (it does not stall), the option bytes' programming time (OPTSTRT is done at once), a reset during an operation leaving the memory half-programmed, HCLK's 1 MHz minimum, the read protection's levels (RDP stored, no mass erase on a regression), PCROP's read refusal (RDERR), the ART accelerator's caches (DCRST, ICRST); CRC: the bus stall of a DR access while a word is computed (four AHB clocks), and a byte or halfword write to DR, which RM0390 does not allow, feeding its value zero-extended.
+Declared omissions: watchdogs: LSI's spread (32 kHz here), the debug freeze bits (a debugger's halt stops all of the machine's time); the WWDG's counter while WDGA is clear counts from the last access made while its clock is on; RTC: the reference clock detection (REFCKON, RTC_REFIN), RTC_AF2 (PI8, not bonded), the tamper input's precharge and pull-up, the 8 and 16 s calibration windows (the rate is applied evenly), RECALPF's wait; ADC: the start-up time after ADON (conversions are exact at once), the input's source impedance against the sampling time, the converter's offset, gain, linearity errors and noise (the code is the ideal one), the die's self-heating; DAC: the buffer's offset and gain errors and load regulation, the conversion's non-linearity; I2C: FLTR's analog and digital noise filters (the circuit's edges are taken as they come), the SMBus's cumulative clock low extensions (TLOW:SEXT, TLOW:MEXT), clock synchronization between two masters, TRISE's rise time compensation, CR2.FREQ (times come from the real PCLK1); FMPI2C1 (another IP) keeps its registers; SPI: I2S (I2SCFGR's I2SMOD keeps its value and the SPI stays an SPI; no catalog part speaks I2S), the TI slave's MISO release half a clock after the last bit (let go at the frame's end); DMA: the arbitration's delays between streams (a request is served at once), the direct mode and FIFO overrun/underrun errors (DMEIF never sets, FEIF only for the configuration error), the peripheral flow controller (PFCTRL), the bus matrix's timing (memory-to-memory moves an item every two cycles); dma_requests omits SAI2's and SPI4's requests; USART: noise detection (one sample a bit: NF never sets, ONEBIT changes nothing), smartcard mode (SCEN, NACK, GTPR) and IrDA (IREN, IRLP), which no catalog part speaks; timers: TIM5's TI4 on the RTC's wake-up interrupt (IT4_RMP 11) and TIM2's ITR1 on the OTG controllers' SOF, MSM's delay, the idle levels' dead time after a break, OCxFE; TIMx_CCy as an ADC trigger is OCyREF for an output channel and a pulse at a capture or a frozen channel's match; SysTick: STK_CALIB's TENMS (RM0390's value not taken: it reads 0 with SKEW set); GPIO: EVENTOUT (AF15, SEV's pulse), the I/O compensation cell's effect on the edges; SYSCFG: the FMC memories MEMRMP can map at 0 (none is on the bus), PMC's ADC DC2 bits' effect; EXTI: the internal lines' sources but the RTC's (the PVD, the USB OTG controllers' wake-ups) until their models land; SWIER triggers them; RCC: the clock security system's detection (CSSON is kept; the board's HSE never fails), a peripheral held in reset while its *RSTR bit stays set (its reset happens on the write), the brownout reset (BORRSTF only at power-on), the start-up times' spread between parts and with temperature and LSEMOD's drive level (the typical times always), MCO1 and MCO2 on their pins, and the I2S, SAI, SPDIF-RX, CEC and FMPI2C clocks from PLLI2S and PLLSAI (DCKCFGR, DCKCFGR2) beyond TIMPRE and the 48 MHz clock's CK48MSEL (PLL_Q or PLLSAI_P, which the USB OTG FS core needs); PWR: the regulator's settling times and the low-power modes' wake-up times (DS10693's low-power mode wake-up timings: the core runs at the wake-up event), the regulator's settings (LPDS, FPDS, LPLVDS, MRLVDS: the supply current is not modelled), the programmable voltage detector, DBGMCU_CR's DBG_STOP and DBG_STANDBY (a debugger's halt stops all of the machine's time), the tamper and timestamp pin in Standby beyond RTC_OUT; DWT's CYCCNT counts through Stop; CAN, SDIO, QUADSPI, SAI, SPDIF-RX, HDMI-CEC, DCMI, FMC, FMPI2C and the USB OTG HS controller; USB OTG FS: the ID pin, SRP and HNP, split transactions through a hub, isochronous endpoints' frame parity and incomplete-transfer interrupts and a host's isochronous channels, FRMOR, LPM, GCCFG's battery-charging detection, the OTG_FS_WKUP interrupt through EXTI line 18, the SOF pulse on PA8; the device's turnaround inside a transaction (a transaction is whole), a host channel's OUT data kept per channel rather than in the shared FIFO's order (the words free are the same); GSNPSID reads 0x4F54320A (core 3.20a), the value TinyUSB's dwc2_info table measures on the F412's and F76x's OTG_FS, the same generation of the core (ST's HAL groups the F446 with the F412, F413 and F469); no ST document gives it and it was not read from an F446; flash: the times' spread and their maximum values (the typical ones always), the ART accelerator's cache hits during an operation (a program in the flash stalls whatever its cache holds), a program in SRAM whose interrupt handler is in the flash (it does not stall), the option bytes' programming time (OPTSTRT is done at once), a reset during an operation leaving the memory half-programmed, HCLK's 1 MHz minimum, the read protection's levels (RDP stored, no mass erase on a regression), PCROP's read refusal (RDERR), the ART accelerator's caches (DCRST, ICRST); CRC: the bus stall of a DR access while a word is computed (four AHB clocks), and a byte or halfword write to DR, which RM0390 does not allow, feeding its value zero-extended.
diff --git a/docs/gallery/README.md b/docs/gallery/README.md
index 31d18da1..e76296e5 100644
--- a/docs/gallery/README.md
+++ b/docs/gallery/README.md
@@ -5,6 +5,29 @@ evolves. Newer milestones come first. The milestone sets from M1 on are captured
`ui/apps/workbench/scripts/screenshots.mjs` (the same views each time:
`pnpm build`, then `node scripts/screenshots.mjs --out ../../../docs/gallery/`).
+## The NUCLEO-F446ZE as a USB host (2026-10-31)
+
+STM32CubeF4's USB_Host/HID_Standalone, unmodified, on the NUCLEO-F446ZE with
+Adafruit's 1.8" TFT shield (`emblink/adafruit_tft_shield18`) and a USB mouse
+(`emblink/usb_mouse`) on CN13. The firmware switches VBUS on through U12
+(PG6), its OTG core enumerates the mouse at low speed, and ST's LCD log
+shows it; SEL on the shield's joystick starts the mouse demo, which draws
+the pointer as the mouse moves. These are the ST7735's own frames (its
+`frame` output, 128 x 160 at 3x), as the panel shows them: the uninitialised
+memory's noise is gone once ST's BSP clears the screen.
+
+| Enumerated (1.95 s) | The mouse demo (3.25 s) |
+|---|---|
+|  |  |
+
+USB_Host/MSC_Standalone, the same way, with a USB flash drive
+(`emblink/usb_flash_drive`) instead of the mouse: B1 writes 'USBHost.txt' on
+it and reads it back, then lists the disk.
+
+| Written and read back (3.9 s) | The disk's content (4.95 s) |
+|---|---|
+|  |  |
+
## A logic analyzer: the Saleae Logic 8 (2026-10-31)
A Saleae Logic 8 wired to a terminal's TX (D0) and a 1 kHz clock (D1),
diff --git a/docs/gallery/nucleo-f446ze/host-hid-enumerated.png b/docs/gallery/nucleo-f446ze/host-hid-enumerated.png
new file mode 100644
index 00000000..48729e86
Binary files /dev/null and b/docs/gallery/nucleo-f446ze/host-hid-enumerated.png differ
diff --git a/docs/gallery/nucleo-f446ze/host-hid-mouse-demo.png b/docs/gallery/nucleo-f446ze/host-hid-mouse-demo.png
new file mode 100644
index 00000000..4f8f590f
Binary files /dev/null and b/docs/gallery/nucleo-f446ze/host-hid-mouse-demo.png differ
diff --git a/docs/gallery/nucleo-f446ze/host-msc-disk-content.png b/docs/gallery/nucleo-f446ze/host-msc-disk-content.png
new file mode 100644
index 00000000..0c775309
Binary files /dev/null and b/docs/gallery/nucleo-f446ze/host-msc-disk-content.png differ
diff --git a/docs/gallery/nucleo-f446ze/host-msc-file-written.png b/docs/gallery/nucleo-f446ze/host-msc-file-written.png
new file mode 100644
index 00000000..8297896e
Binary files /dev/null and b/docs/gallery/nucleo-f446ze/host-msc-file-written.png differ
diff --git a/tests/unit/test_stm32f4.c b/tests/unit/test_stm32f4.c
index 3106de69..36106775 100644
--- a/tests/unit/test_stm32f4.c
+++ b/tests/unit/test_stm32f4.c
@@ -3681,6 +3681,340 @@ static void test_tim_widths(void)
f4_close(&f);
}
+/* ---- the OTG core as a host (otg_fs.c), driven as the HAL's HCD driver
+ * does, with a scripted device on its cable ---- */
+
+#define H_HNPTXSTS (OTG + 0x02Cu)
+#define H_HPTXFSIZ (OTG + 0x100u)
+#define H_HCFG (OTG + 0x400u)
+#define H_HFIR (OTG + 0x404u)
+#define H_HFNUM (OTG + 0x408u)
+#define H_HAINT (OTG + 0x414u)
+#define H_HAINTMSK (OTG + 0x418u)
+#define H_HPRT (OTG + 0x440u)
+#define H_HCCHAR(n) (OTG + 0x500u + 0x20u * (n))
+#define H_HCINT(n) (OTG + 0x508u + 0x20u * (n))
+#define H_HCINTMSK(n) (OTG + 0x50Cu + 0x20u * (n))
+#define H_HCTSIZ(n) (OTG + 0x510u + 0x20u * (n))
+#define HPRT_W1C (1u << 1 | 1u << 2 | 1u << 3 | 1u << 5)
+
+static struct {
+ int pull, resets, reset_ends, sofs, setups, outs, ins, naks;
+ u32 last_frame;
+ u8 setup[8];
+ u8 out[64];
+ u32 out_len;
+ int out_d1;
+ const u8 *in;
+ u32 in_len;
+ int in_d1, answer;
+} sd;
+
+static int sd_pullup(void *ctx)
+{
+ (void)ctx;
+ return sd.pull;
+}
+
+static void sd_bus(void *ctx, int ev, u32 frame)
+{
+ (void)ctx;
+ if (ev == ESK_USB_RESET)
+ sd.resets++;
+ else if (ev == ESK_USB_RESET_END)
+ sd.reset_ends++;
+ else if (ev == ESK_USB_SOF) {
+ sd.sofs++;
+ sd.last_frame = frame;
+ }
+}
+
+static int sd_setup(void *ctx, int addr, int ep, const u8 data[8])
+{
+ (void)ctx;
+ if (addr != 0 || ep != 0)
+ return ESK_USB_NONE;
+ memcpy(sd.setup, data, 8);
+ sd.setups++;
+ return ESK_USB_ACK;
+}
+
+static int sd_out(void *ctx, int addr, int ep, int data1, const u8 *data, u32 len)
+{
+ (void)ctx;
+ (void)addr;
+ (void)ep;
+ sd.outs++;
+ if (sd.answer != ESK_USB_ACK)
+ return sd.answer;
+ memcpy(sd.out, data, len < 64u ? len : 64u);
+ sd.out_len = len;
+ sd.out_d1 = data1;
+ return ESK_USB_ACK;
+}
+
+static int sd_in(void *ctx, int addr, int ep, u8 *data, u32 cap, u32 *len, int *data1)
+{
+ (void)ctx;
+ (void)addr;
+ (void)ep;
+ sd.ins++;
+ if (sd.answer != ESK_USB_ACK) {
+ if (sd.answer == ESK_USB_NAK)
+ sd.naks++;
+ return sd.answer;
+ }
+ *len = sd.in_len < cap ? sd.in_len : cap;
+ memcpy(data, sd.in, *len);
+ *data1 = sd.in_d1;
+ return ESK_USB_ACK;
+}
+
+/* HPRT written as the HAL does: its write-1-to-clear bits masked out */
+static void host_hprt(f4 *f, u32 set, u32 clear)
+{
+ u32 v = rd(f, H_HPRT) & ~HPRT_W1C;
+ wr(f, H_HPRT, (v | set) & ~clear);
+}
+
+/* USB_HC_Halt for a control or bulk channel, then CHH taken */
+static void host_halt(f4 *f, u32 n)
+{
+ wr(f, H_HCCHAR(n), rd(f, H_HCCHAR(n)) | 1u << 30);
+ wr(f, H_HCCHAR(n), rd(f, H_HCCHAR(n)) | 1u << 31);
+ CHECK(wait_set(f, H_HCINT(n), 1u << 1, 10) >= 0);
+ CHECK_EQ(rd(f, H_HCCHAR(n)) >> 30, 0u);
+ wr(f, H_HCINT(n), 0xFFFFFFFFu);
+}
+
+static void host_enable(f4 *f, u32 n) { wr(f, H_HCCHAR(n), (rd(f, H_HCCHAR(n)) & ~(1u << 30)) | 1u << 31); }
+
+static void test_otg_host(void)
+{
+ f4 f;
+ static const u8 get_dev[8] = { 0x80, 6, 0, 1, 0, 0, 64, 0 };
+ static const u8 desc[18] = { 18, 1, 0, 2, 0, 0, 0, 64, 0x83, 0x04, 0x40, 0x57, 0, 2, 1, 2, 3, 1 };
+ u32 st, w[5], i, f0;
+ long t;
+ if (!f4_open(&f, 1))
+ return;
+ memset(&sd, 0, sizeof sd);
+ sd.answer = ESK_USB_ACK;
+ f.m.host.usbh_pullup = sd_pullup;
+ f.m.host.usbh_bus = sd_bus;
+ f.m.host.usbh_setup = sd_setup;
+ f.m.host.usbh_out = sd_out;
+ f.m.host.usbh_in = sd_in;
+ CHECK(f.m.usb.host_changed != NULL);
+ otg_msp(&f);
+ pll_from_hse(&f, PLLCFGR_168);
+ wr(&f, G_GRSTCTL, 1u);
+ CHECK(wait_clr(&f, G_GRSTCTL, 1u, 10) >= 0);
+ wr(&f, G_GUSBCFG, rd(&f, G_GUSBCFG) | 1u << 29); /* FHMOD */
+ CHECK(wait_set(&f, G_GINTSTS, 1u, 30000) >= 20000);
+
+ /* USB_HostInit, as the HAL does it */
+ wr(&f, G_GCCFG, 1u << 16); /* PWRDWN, no VBUS sensing */
+ CHECK(rd(&f, H_HCFG) & 1u << 2); /* FSLSS: FS and LS only */
+ wr(&f, H_HCFG, 1u); /* FSLSPCS 48 MHz */
+ wr(&f, H_HFIR, 48000u);
+ wr(&f, G_GRXFSIZ, 0x80u);
+ wr(&f, G_DIEPTXF0, 0x60u << 16 | 0x80u); /* HNPTXFSIZ */
+ wr(&f, H_HPTXFSIZ, 0x40u << 16 | 0xE0u);
+ wr(&f, G_GINTSTS, 0xFFFFFFFFu);
+ wr(&f, G_GINTMSK, 1u << 4 | 1u << 24 | 1u << 25 | 1u << 3 | 1u << 29);
+ wr(&f, G_GAHBCFG, 1u);
+ CHECK_EQ(rd(&f, H_HNPTXSTS), 8u << 16 | 0x60u); /* the FIFO's words free, the queue */
+
+ /* a device pulled up while the port is off: nothing; PPWR: connected */
+ sd.pull = ESK_USB_FULL_SPEED;
+ f.m.usb.host_changed(f.m.usb.ctx);
+ CHECK_EQ(rd(&f, H_HPRT) & 3u, 0u);
+ host_hprt(&f, 1u << 12, 0); /* PPWR (USB_DriveVbus) */
+ CHECK_EQ(rd(&f, H_HPRT) & 3u, 3u); /* PCSTS, PCDET */
+ CHECK(rd(&f, G_GINTSTS) & 1u << 24); /* HPRTINT */
+ CHECK_EQ((rd(&f, H_HPRT) >> 10) & 3u, 1u); /* the line at J: D+ high */
+ CHECK(rd(&f, NVIC_ISPR2) & 1u << 3);
+ wr(&f, H_HPRT, (rd(&f, H_HPRT) & ~HPRT_W1C) | 1u << 1);
+ CHECK_EQ(rd(&f, G_GINTSTS) & 1u << 24, 0u);
+
+ /* USB_ResetPort: SE0 while PRST is set, then the port enabled at the
+ * device's speed, and SOFs every 1 ms */
+ host_hprt(&f, 1u << 8, 0);
+ CHECK_EQ(sd.resets, 1);
+ CHECK_EQ((rd(&f, H_HPRT) >> 10) & 3u, 0u); /* SE0 */
+ run_us(&f, 10000);
+ CHECK_EQ(sd.sofs, 0);
+ host_hprt(&f, 0, 1u << 8);
+ CHECK_EQ(sd.reset_ends, 1);
+ CHECK(rd(&f, H_HPRT) & 1u << 2); /* PENA */
+ CHECK(rd(&f, H_HPRT) & 1u << 3); /* PENCHNG */
+ CHECK_EQ((rd(&f, H_HPRT) >> 17) & 3u, 1u); /* PSPD full speed */
+ wr(&f, H_HPRT, (rd(&f, H_HPRT) & ~HPRT_W1C) | 1u << 3);
+ CHECK(rd(&f, H_HPRT) & 1u << 2); /* PENA stays */
+ wr(&f, G_GINTSTS, 1u << 3);
+ f0 = rd(&f, H_HFNUM) & 0xFFFFu;
+ t = wait_set(&f, G_GINTSTS, 1u << 3, 2000);
+ CHECK(t > 0 && t <= 1001);
+ CHECK_EQ(rd(&f, H_HFNUM) & 0xFFFFu, f0 + 1u);
+ CHECK(rd(&f, H_HFNUM) >> 16 > 40000u); /* FTREM: most of the frame left */
+ CHECK_EQ(sd.last_frame, f0 + 1u);
+ sd.sofs = 0;
+ run_us(&f, 10000);
+ CHECK(sd.sofs >= 9 && sd.sofs <= 11);
+
+ /* a SETUP on channel 0 (USB_HC_Init, USB_HC_StartXfer): it goes once
+ * its 8 bytes are in the non-periodic FIFO; ACK, XFRC, CHENA cleared,
+ * the data stage's DATA1 next */
+ wr(&f, H_HCINT(0), 0xFFFFFFFFu);
+ wr(&f, H_HCINTMSK(0), 1u | 1u << 1 | 1u << 3 | 1u << 4 | 1u << 7 | 1u << 10);
+ wr(&f, H_HAINTMSK, 7u);
+ wr(&f, H_HCCHAR(0), 64u); /* MPSIZ 64, EP0 OUT, control, address 0 */
+ wr(&f, H_HCTSIZ(0), 8u | 1u << 19 | 3u << 29); /* SETUP */
+ host_enable(&f, 0);
+ run_us(&f, 100);
+ CHECK_EQ(sd.setups, 0); /* no data yet */
+ wr(&f, FIFO(0), 0x01000680u);
+ CHECK_EQ(rd(&f, H_HNPTXSTS) & 0xFFFFu, 0x5Fu);
+ wr(&f, FIFO(0), 0x00400000u);
+ t = wait_set(&f, H_HCINT(0), 1u, 100);
+ CHECK(t >= 13 && t <= 16); /* 169 bits at 12 Mbit/s: 14 us */
+ CHECK_EQ(sd.setups, 1);
+ CHECK(memcmp(sd.setup, get_dev, 8) == 0);
+ CHECK(rd(&f, H_HCINT(0)) & 1u << 5); /* ACK */
+ CHECK_EQ(rd(&f, H_HCCHAR(0)) >> 31, 0u);
+ CHECK_EQ(rd(&f, H_HCTSIZ(0)), 2u << 29); /* DATA1, nothing left */
+ CHECK_EQ(rd(&f, H_HNPTXSTS) & 0xFFFFu, 0x60u);
+ CHECK(rd(&f, H_HAINT) & 1u);
+ CHECK(rd(&f, G_GINTSTS) & 1u << 25); /* HCINT */
+ wr(&f, H_HCINT(0), 0xFFFFFFFFu);
+ host_halt(&f, 0);
+
+ /* the data stage on channel 1, IN: the device's 18 bytes in the
+ * receive FIFO with their status, then "transfer done", whose taking
+ * is XFRC */
+ sd.in = desc;
+ sd.in_len = 18;
+ sd.in_d1 = 1;
+ wr(&f, H_HCINT(1), 0xFFFFFFFFu);
+ wr(&f, H_HCINTMSK(1), 1u | 1u << 1 | 1u << 3 | 1u << 4 | 1u << 7 | 1u << 8 | 1u << 10);
+ wr(&f, H_HCCHAR(1), 64u | 1u << 15);
+ wr(&f, H_HCTSIZ(1), 64u | 1u << 19 | 2u << 29);
+ host_enable(&f, 1);
+ CHECK(wait_set(&f, G_GINTSTS, 1u << 4, 100) >= 0); /* RXFLVL */
+ CHECK_EQ(sd.ins, 1);
+ st = rd(&f, G_GRXSTSP);
+ CHECK_EQ(st, 1u | 18u << 4 | 2u << 15 | 2u << 17);
+ for (i = 0; i < 5u; i++)
+ w[i] = rd(&f, FIFO(1));
+ CHECK_EQ(w[0], 0x02000112u);
+ CHECK_EQ(w[4] & 0xFFFFu, 0x0103u);
+ CHECK_EQ(rd(&f, H_HCINT(1)) & 1u, 0u);
+ CHECK_EQ(rd(&f, G_GRXSTSP), 1u | 0u << 15 | 3u << 17); /* done, DATA0 next */
+ CHECK(rd(&f, H_HCINT(1)) & 1u); /* XFRC */
+ CHECK_EQ(rd(&f, H_HCTSIZ(1)), 46u); /* 64 - 18, DATA0, no packets */
+ CHECK_EQ(rd(&f, G_GINTSTS) & 1u << 4, 0u);
+ wr(&f, H_HCINT(1), 0xFFFFFFFFu);
+ host_halt(&f, 1);
+
+ /* a NAK: the channel stops until the program halts and re-enables it */
+ sd.answer = ESK_USB_NAK;
+ wr(&f, H_HCTSIZ(1), 64u | 1u << 19 | 2u << 29);
+ host_enable(&f, 1);
+ CHECK(wait_set(&f, H_HCINT(1), 1u << 4, 100) >= 0);
+ run_us(&f, 200);
+ CHECK_EQ(sd.naks, 1);
+ wr(&f, H_HCINT(1), 0xFFFFFFFFu);
+ host_halt(&f, 1);
+ host_enable(&f, 1);
+ CHECK(wait_set(&f, H_HCINT(1), 1u << 4, 100) >= 0);
+ CHECK_EQ(sd.naks, 2);
+ host_halt(&f, 1);
+ /* no answer at all (an address nobody has): TXERR */
+ sd.answer = ESK_USB_NONE;
+ host_enable(&f, 1);
+ CHECK(wait_set(&f, H_HCINT(1), 1u << 7, 100) >= 0);
+ host_halt(&f, 1);
+ sd.answer = ESK_USB_ACK;
+
+ /* an OUT with its data toggle: DATA1, 4 bytes */
+ wr(&f, H_HCCHAR(0), 64u);
+ wr(&f, H_HCTSIZ(0), 4u | 1u << 19 | 2u << 29);
+ host_enable(&f, 0);
+ wr(&f, FIFO(0), 0x44332211u);
+ CHECK(wait_set(&f, H_HCINT(0), 1u, 100) >= 0);
+ CHECK_EQ(sd.out_len, 4u);
+ CHECK_EQ(sd.out_d1, 1);
+ CHECK_EQ(sd.out[3], 0x44u);
+ CHECK_EQ(rd(&f, H_HCTSIZ(0)) >> 29, 0u); /* DATA0 next */
+ wr(&f, H_HCINT(0), 0xFFFFFFFFu);
+ host_halt(&f, 0);
+
+ /* an interrupt IN channel goes once a frame, in the frames ODDFRM
+ * says: a NAK stops it */
+ sd.answer = ESK_USB_NAK;
+ sd.naks = 0;
+ wr(&f, H_HCINTMSK(2), 1u | 1u << 1 | 1u << 3 | 1u << 4 | 1u << 7);
+ wr(&f, H_HCCHAR(2), 8u | 1u << 11 | 1u << 15 | 3u << 18 | ((rd(&f, H_HFNUM) & 1u) ? 0u : 1u << 29));
+ wr(&f, H_HCTSIZ(2), 8u | 1u << 19);
+ host_enable(&f, 2);
+ run_us(&f, 3000);
+ CHECK_EQ(sd.naks, 1);
+ host_halt(&f, 2);
+ sd.answer = ESK_USB_ACK;
+
+ /* the device goes: DISCINT, the port disabled, no more frames */
+ sd.pull = ESK_USB_DETACHED;
+ f.m.usb.host_changed(f.m.usb.ctx);
+ CHECK(rd(&f, G_GINTSTS) & 1u << 29);
+ CHECK_EQ(rd(&f, H_HPRT) & (1u | 1u << 2), 0u);
+ CHECK(rd(&f, H_HPRT) & 1u << 3); /* PENCHNG */
+ wr(&f, G_GINTSTS, 1u << 29);
+ wr(&f, H_HPRT, (rd(&f, H_HPRT) & ~HPRT_W1C) | HPRT_W1C);
+ sd.sofs = 0;
+ run_us(&f, 3000);
+ CHECK_EQ(sd.sofs, 0);
+
+ /* a low-speed device: D- at J, PSPD 2, transactions at 1.5 Mbit/s */
+ sd.pull = ESK_USB_LOW_SPEED;
+ f.m.usb.host_changed(f.m.usb.ctx);
+ CHECK_EQ(rd(&f, H_HPRT) & 3u, 3u);
+ CHECK_EQ((rd(&f, H_HPRT) >> 10) & 3u, 2u);
+ host_hprt(&f, 1u << 8, 0);
+ run_us(&f, 10000);
+ host_hprt(&f, 0, 1u << 8);
+ CHECK_EQ((rd(&f, H_HPRT) >> 17) & 3u, 2u);
+ wr(&f, H_HCFG, 2u); /* 6 MHz, as the HAL then sets */
+ wr(&f, H_HFIR, 6000u);
+ wr(&f, H_HCINT(0), 0xFFFFFFFFu);
+ wr(&f, H_HCCHAR(0), 8u);
+ wr(&f, H_HCTSIZ(0), 8u | 1u << 19 | 3u << 29);
+ host_enable(&f, 0);
+ wr(&f, FIFO(0), 0x01000680u);
+ wr(&f, FIFO(0), 0x00080000u);
+ t = wait_set(&f, H_HCINT(0), 1u, 1000);
+ CHECK(t >= 100 && t <= 130); /* 169 bits at 1.5 Mbit/s: 113 us */
+ host_halt(&f, 0);
+ sd.sofs = 0;
+ run_us(&f, 5000);
+ CHECK(sd.sofs >= 4 && sd.sofs <= 6); /* keep-alives each 1 ms */
+
+ /* PSUSP: no more frames; PRES, then its end: frames again */
+ host_hprt(&f, 1u << 7, 0);
+ CHECK(rd(&f, H_HPRT) & 1u << 7);
+ sd.sofs = 0;
+ run_us(&f, 5000);
+ CHECK_EQ(sd.sofs, 0);
+ host_hprt(&f, 1u << 6, 0);
+ CHECK_EQ((rd(&f, H_HPRT) >> 10) & 3u, 1u); /* K at low speed: D+ high */
+ run_us(&f, 20000);
+ host_hprt(&f, 0, 1u << 6);
+ CHECK_EQ(rd(&f, H_HPRT) & 1u << 7, 0u);
+ run_us(&f, 3000);
+ CHECK(sd.sofs >= 2);
+ f4_close(&f);
+}
+
TEST_MAIN_BEGIN
RUN(test_reset);
RUN(test_180mhz);
@@ -3738,4 +4072,5 @@ RUN(test_crc);
RUN(test_tim5_lsi);
RUN(test_tim_widths);
RUN(test_otg_fs);
+RUN(test_otg_host);
TEST_MAIN_END
diff --git a/tests/vendor/README.md b/tests/vendor/README.md
index b0ff22b0..cb03f366 100644
--- a/tests/vendor/README.md
+++ b/tests/vendor/README.md
@@ -10,7 +10,7 @@ toolchains.
| --- | --- | --- |
| `arduino/` | ArduinoCore-avr (Blink, a tour of the core) | `emblink/arduino_uno` |
| `stm32cube/` | ST's STM32F1 HAL and CMSIS device files | `emblink/bluepill` |
-| `stm32cubef4/` | STM32CubeF4's own examples for the NUCLEO-F446RE (GPIO, HAL time bases, RCC, RTC, timers, UART, PWR's low-power modes, I2C between two boards, EEPROM emulation in flash) and, with `--board nucleo_f446ze`, for the NUCLEO-F446ZE (ADC, CRC, Cortex, DMA, flash erase and program, GPIO, the watchdogs, RTC, seven timer examples, UART, the USB device HID mouse on a computer's port; its middleware, stm32-mw-usb-device v2.11.4, fetched like the rest) | `emblink/nucleo_f446re`, `emblink/nucleo_f446ze` |
+| `stm32cubef4/` | STM32CubeF4's own examples for the NUCLEO-F446RE (GPIO, HAL time bases, RCC, RTC, timers, UART, PWR's low-power modes, I2C between two boards, EEPROM emulation in flash) and, with `--board nucleo_f446ze`, for the NUCLEO-F446ZE (ADC, CRC, Cortex, DMA, flash erase and program, GPIO, the watchdogs, RTC, seven timer examples, UART, the USB device HID mouse on a computer's port, the USB host HID and MSC demos with Adafruit's 1.8" TFT shield, a mouse and a flash drive; their middleware and components, stm32-mw-usb-device v2.11.4, stm32-mw-usb-host, stm32-mw-fatfs, stm32-st7735 and stm32-bsp-common at the commits CubeF4 pins, fetched like the rest and copied into CubeF4's tree, with its Adafruit_Shield BSP and Utilities' LCD log and fonts) | `emblink/nucleo_f446re`, `emblink/nucleo_f446ze` |
| `pico-sdk/` | The Raspberry Pi Pico SDK | `emblink/pico` |
| `freedom-metal/` | SiFive's freedom-metal and freedom-e-sdk's examples | `emblink/hifive1_revb` |
| `nrfx/` | Nordic's nrfx drivers and MDK | `emblink/nrf52840dk` |
diff --git a/tests/vendor/stm32cubef4/nucleo_f446ze/usb_host_hid_standalone/project.json b/tests/vendor/stm32cubef4/nucleo_f446ze/usb_host_hid_standalone/project.json
new file mode 100644
index 00000000..b94f3502
--- /dev/null
+++ b/tests/vendor/stm32cubef4/nucleo_f446ze/usb_host_hid_standalone/project.json
@@ -0,0 +1,211 @@
+{
+ "schema_version": 1,
+ "kind": "project",
+ "name": "stm32cubef4-nucleo-f446ze-usb-host-hid",
+ "description": "A NUCLEO-F446ZE running STM32CubeF4's USB_Host/HID_Standalone, built from ST's unmodified sources (tests/vendor/stm32cubef4/run.py --board nucleo_f446ze puts the image beside this file as sketch.elf), on its ST-LINK's USB power, Adafruit's 1.8\" TFT shield on the Arduino connectors (its LCD on SPI1, its joystick on A3) and a USB mouse on CN13, powered by U12 when the firmware drives PG6 high.",
+ "policy": "electrical",
+ "boards": [
+ {
+ "id": "B1",
+ "board": "emblink/nucleo_f446ze",
+ "layout": {
+ "x": 360,
+ "y": 120
+ },
+ "firmware": {
+ "image": "sketch.elf",
+ "format": "elf"
+ }
+ }
+ ],
+ "components": [
+ {
+ "id": "USB",
+ "component": "emblink/vsource",
+ "params": {
+ "kind": "dc",
+ "dc": 5
+ },
+ "layout": {
+ "x": 120,
+ "y": 160
+ }
+ },
+ {
+ "id": "GND1",
+ "component": "emblink/gnd",
+ "layout": {
+ "x": 120,
+ "y": 320
+ }
+ },
+ {
+ "id": "SHIELD",
+ "component": "emblink/adafruit_tft_shield18",
+ "layout": {
+ "x": 620,
+ "y": 80
+ }
+ },
+ {
+ "id": "MOUSE",
+ "component": "emblink/usb_mouse",
+ "layout": {
+ "x": 120,
+ "y": 480
+ }
+ }
+ ],
+ "nets": {
+ "USB5V": [
+ "USB.+",
+ "B1.U5V"
+ ],
+ "GND": [
+ "USB.-",
+ "GND1.GND",
+ "B1.GND",
+ "SHIELD.GND",
+ "MOUSE.GND"
+ ],
+ "OTG_VBUS": [
+ "MOUSE.VBUS",
+ "B1.USB_VBUS"
+ ],
+ "OTG_DM": [
+ "MOUSE.DM",
+ "B1.USB_DM"
+ ],
+ "OTG_DP": [
+ "MOUSE.DP",
+ "B1.USB_DP"
+ ],
+ "SH_5V": [
+ "SHIELD.5V",
+ "B1.5V"
+ ],
+ "SH_3V3": [
+ "SHIELD.3V3",
+ "B1.3V3"
+ ],
+ "SH_RESET": [
+ "SHIELD.RESET",
+ "B1.RESET"
+ ],
+ "SH_SCK": [
+ "SHIELD.D13",
+ "B1.D13"
+ ],
+ "SH_MISO": [
+ "SHIELD.D12",
+ "B1.D12"
+ ],
+ "SH_MOSI": [
+ "SHIELD.D11",
+ "B1.D11"
+ ],
+ "SH_CS": [
+ "SHIELD.D10",
+ "B1.D10"
+ ],
+ "SH_DC": [
+ "SHIELD.D8",
+ "B1.D8"
+ ],
+ "SH_SDCS": [
+ "SHIELD.D4",
+ "B1.D4"
+ ],
+ "SH_JOY": [
+ "SHIELD.A3",
+ "B1.A3"
+ ]
+ },
+ "probes": [
+ {
+ "id": "console",
+ "target": "serial:B1",
+ "kind": "serial"
+ },
+ {
+ "id": "led",
+ "target": "net:B1.PB0",
+ "kind": "digital"
+ },
+ {
+ "id": "led2",
+ "target": "net:B1.PB7",
+ "kind": "digital"
+ },
+ {
+ "id": "led3",
+ "target": "net:B1.PB14",
+ "kind": "digital"
+ },
+ {
+ "id": "button",
+ "target": "net:B1.PC13",
+ "kind": "digital"
+ },
+ {
+ "id": "vbus_sense",
+ "target": "net:B1.PA9",
+ "kind": "analog"
+ },
+ {
+ "id": "ld8",
+ "target": "net:B1.USB_VBUS",
+ "kind": "analog"
+ },
+ {
+ "id": "mouse_state",
+ "target": "output:MOUSE.state",
+ "kind": "value"
+ },
+ {
+ "id": "mouse_address",
+ "target": "output:MOUSE.address",
+ "kind": "value"
+ },
+ {
+ "id": "mouse_reports",
+ "target": "output:MOUSE.reports",
+ "kind": "value"
+ },
+ {
+ "id": "mouse_protocol",
+ "target": "output:MOUSE.protocol",
+ "kind": "value"
+ },
+ {
+ "id": "lcd_on",
+ "target": "output:SHIELD.TFT.on",
+ "kind": "value"
+ },
+ {
+ "id": "lcd_writes",
+ "target": "output:SHIELD.TFT.ram_writes",
+ "kind": "value"
+ },
+ {
+ "id": "vbus",
+ "target": "net:B1.USB_VBUS",
+ "kind": "analog"
+ },
+ {
+ "id": "pg6",
+ "target": "net:B1.PG6",
+ "kind": "digital"
+ },
+ {
+ "id": "joy",
+ "target": "net:B1.PF3",
+ "kind": "analog"
+ },
+ {
+ "id": "lcd_pixel",
+ "target": "output:SHIELD.TFT.probe",
+ "kind": "value"
+ }
+ ]
+}
diff --git a/tests/vendor/stm32cubef4/nucleo_f446ze/usb_host_hid_standalone/scenario.json b/tests/vendor/stm32cubef4/nucleo_f446ze/usb_host_hid_standalone/scenario.json
new file mode 100644
index 00000000..15a37291
--- /dev/null
+++ b/tests/vendor/stm32cubef4/nucleo_f446ze/usb_host_hid_standalone/scenario.json
@@ -0,0 +1,170 @@
+{
+ "schema_version": 1,
+ "kind": "scenario",
+ "name": "usb_host_hid_standalone",
+ "until": "3300ms",
+ "description": "USB_Host/HID_Standalone, as it ships, on a NUCLEO-F446ZE with Adafruit's 1.8\" TFT shield and a USB mouse (low speed) on CN13. It finds the shield (A3 high against its pull-down), starts the LCD (ST's st7735 driver: 'OTG FS HID Host' and its log), powers VBUS through U12 by driving PG6 high (USBH_LL_DriverVBUS), and its OTG core, as a host, sees the mouse's pull-up on D-, resets the port and enumerates it at low speed: address 1, its strings, its configuration, the HID class (SET_IDLE, the report descriptor), as the log on the LCD says. While the log scrolls the main loop is busy drawing and does not read the joystick; once it is quiet, SEL (the shield's joystick, read on ADC3) starts the mouse demo, which clears the log and draws its window (a white frame at x 7, y 52) and buttons; the mouse moved and clicked sends reports on its interrupt endpoint, which the host polls every 10 ms and the demo draws.",
+ "inputs": [
+ {
+ "at": "0ms",
+ "target": "SHIELD.TFT",
+ "input": "probe_x",
+ "value": 7
+ },
+ {
+ "at": "0ms",
+ "target": "SHIELD.TFT",
+ "input": "probe_y",
+ "value": 70
+ },
+ {
+ "at": "2000ms",
+ "target": "SHIELD.SEL",
+ "input": "pressed",
+ "value": true
+ },
+ {
+ "at": "2150ms",
+ "target": "SHIELD.SEL",
+ "input": "pressed",
+ "value": false
+ },
+ {
+ "at": "2500ms",
+ "target": "MOUSE",
+ "input": "move_x",
+ "value": 40
+ },
+ {
+ "at": "2500ms",
+ "target": "MOUSE",
+ "input": "move_y",
+ "value": 20
+ },
+ {
+ "at": "2800ms",
+ "target": "MOUSE",
+ "input": "buttons",
+ "value": 1
+ },
+ {
+ "at": "3000ms",
+ "target": "MOUSE",
+ "input": "buttons",
+ "value": 0
+ },
+ {
+ "at": "3100ms",
+ "target": "MOUSE",
+ "input": "move_x",
+ "value": -200
+ }
+ ],
+ "assertions": [
+ {
+ "id": "vbus-off-at-start",
+ "at": "50ms",
+ "probe": "pg6",
+ "expect": {
+ "logic": "0"
+ }
+ },
+ {
+ "id": "mouse-unpowered",
+ "at": "50ms",
+ "probe": "mouse_state",
+ "expect": {
+ "equals": "detached"
+ }
+ },
+ {
+ "id": "vbus-switched-on",
+ "at": "200ms",
+ "probe": "pg6",
+ "expect": {
+ "logic": "1"
+ }
+ },
+ {
+ "id": "vbus-5v",
+ "at": "200ms",
+ "probe": "vbus",
+ "expect": {
+ "min_v": 4.9,
+ "max_v": 5.05
+ }
+ },
+ {
+ "id": "port-reset",
+ "at": "250ms",
+ "probe": "mouse_state",
+ "expect": {
+ "equals": "default"
+ }
+ },
+ {
+ "id": "configured",
+ "by": "1200ms",
+ "probe": "mouse_state",
+ "expect": {
+ "equals": "configured"
+ }
+ },
+ {
+ "id": "address-1",
+ "at": "1500ms",
+ "probe": "mouse_address",
+ "expect": {
+ "equals": 1
+ }
+ },
+ {
+ "id": "report-protocol",
+ "at": "1500ms",
+ "probe": "mouse_protocol",
+ "expect": {
+ "equals": 1
+ }
+ },
+ {
+ "id": "lcd-on",
+ "at": "1000ms",
+ "probe": "lcd_on",
+ "expect": {
+ "equals": true
+ }
+ },
+ {
+ "id": "no-report-while-still",
+ "at": "2400ms",
+ "probe": "mouse_reports",
+ "expect": {
+ "equals": 0
+ }
+ },
+ {
+ "id": "mouse-window-drawn",
+ "at": "2400ms",
+ "probe": "lcd_pixel",
+ "expect": {
+ "equals": 65535
+ }
+ },
+ {
+ "id": "first-report",
+ "at": "2600ms",
+ "probe": "mouse_reports",
+ "expect": {
+ "min": 1
+ }
+ },
+ {
+ "id": "reports",
+ "at": "3250ms",
+ "probe": "mouse_reports",
+ "expect": {
+ "min": 4
+ }
+ }
+ ]
+}
diff --git a/tests/vendor/stm32cubef4/nucleo_f446ze/usb_host_msc_standalone/project.json b/tests/vendor/stm32cubef4/nucleo_f446ze/usb_host_msc_standalone/project.json
new file mode 100644
index 00000000..d6a83a44
--- /dev/null
+++ b/tests/vendor/stm32cubef4/nucleo_f446ze/usb_host_msc_standalone/project.json
@@ -0,0 +1,221 @@
+{
+ "schema_version": 1,
+ "kind": "project",
+ "name": "stm32cubef4-nucleo-f446ze-usb-host-msc",
+ "description": "A NUCLEO-F446ZE running STM32CubeF4's USB_Host/MSC_Standalone, built from ST's unmodified sources (tests/vendor/stm32cubef4/run.py --board nucleo_f446ze puts the image beside this file as sketch.elf), on its ST-LINK's USB power, Adafruit's 1.8\" TFT shield on the Arduino connectors (its LCD on SPI1) and a USB flash drive on CN13, powered by U12 when the firmware drives PG6 high.",
+ "policy": "electrical",
+ "boards": [
+ {
+ "id": "B1",
+ "board": "emblink/nucleo_f446ze",
+ "layout": {
+ "x": 360,
+ "y": 120
+ },
+ "firmware": {
+ "image": "sketch.elf",
+ "format": "elf"
+ }
+ }
+ ],
+ "components": [
+ {
+ "id": "USB",
+ "component": "emblink/vsource",
+ "params": {
+ "kind": "dc",
+ "dc": 5
+ },
+ "layout": {
+ "x": 120,
+ "y": 160
+ }
+ },
+ {
+ "id": "GND1",
+ "component": "emblink/gnd",
+ "layout": {
+ "x": 120,
+ "y": 320
+ }
+ },
+ {
+ "id": "SHIELD",
+ "component": "emblink/adafruit_tft_shield18",
+ "layout": {
+ "x": 620,
+ "y": 80
+ }
+ },
+ {
+ "id": "DRIVE",
+ "component": "emblink/usb_flash_drive",
+ "layout": {
+ "x": 120,
+ "y": 480
+ }
+ }
+ ],
+ "nets": {
+ "USB5V": [
+ "USB.+",
+ "B1.U5V"
+ ],
+ "GND": [
+ "USB.-",
+ "GND1.GND",
+ "B1.GND",
+ "SHIELD.GND",
+ "DRIVE.GND"
+ ],
+ "OTG_VBUS": [
+ "DRIVE.VBUS",
+ "B1.USB_VBUS"
+ ],
+ "OTG_DM": [
+ "DRIVE.DM",
+ "B1.USB_DM"
+ ],
+ "OTG_DP": [
+ "DRIVE.DP",
+ "B1.USB_DP"
+ ],
+ "SH_5V": [
+ "SHIELD.5V",
+ "B1.5V"
+ ],
+ "SH_3V3": [
+ "SHIELD.3V3",
+ "B1.3V3"
+ ],
+ "SH_RESET": [
+ "SHIELD.RESET",
+ "B1.RESET"
+ ],
+ "SH_SCK": [
+ "SHIELD.D13",
+ "B1.D13"
+ ],
+ "SH_MISO": [
+ "SHIELD.D12",
+ "B1.D12"
+ ],
+ "SH_MOSI": [
+ "SHIELD.D11",
+ "B1.D11"
+ ],
+ "SH_CS": [
+ "SHIELD.D10",
+ "B1.D10"
+ ],
+ "SH_DC": [
+ "SHIELD.D8",
+ "B1.D8"
+ ],
+ "SH_SDCS": [
+ "SHIELD.D4",
+ "B1.D4"
+ ],
+ "SH_JOY": [
+ "SHIELD.A3",
+ "B1.A3"
+ ]
+ },
+ "probes": [
+ {
+ "id": "console",
+ "target": "serial:B1",
+ "kind": "serial"
+ },
+ {
+ "id": "led",
+ "target": "net:B1.PB0",
+ "kind": "digital"
+ },
+ {
+ "id": "led2",
+ "target": "net:B1.PB7",
+ "kind": "digital"
+ },
+ {
+ "id": "led3",
+ "target": "net:B1.PB14",
+ "kind": "digital"
+ },
+ {
+ "id": "button",
+ "target": "net:B1.PC13",
+ "kind": "digital"
+ },
+ {
+ "id": "vbus_sense",
+ "target": "net:B1.PA9",
+ "kind": "analog"
+ },
+ {
+ "id": "ld8",
+ "target": "net:B1.USB_VBUS",
+ "kind": "analog"
+ },
+ {
+ "id": "lcd_on",
+ "target": "output:SHIELD.TFT.on",
+ "kind": "value"
+ },
+ {
+ "id": "lcd_writes",
+ "target": "output:SHIELD.TFT.ram_writes",
+ "kind": "value"
+ },
+ {
+ "id": "vbus",
+ "target": "net:B1.USB_VBUS",
+ "kind": "analog"
+ },
+ {
+ "id": "pg6",
+ "target": "net:B1.PG6",
+ "kind": "digital"
+ },
+ {
+ "id": "joy",
+ "target": "net:B1.PF3",
+ "kind": "analog"
+ },
+ {
+ "id": "drive_state",
+ "target": "output:DRIVE.state",
+ "kind": "value"
+ },
+ {
+ "id": "drive_address",
+ "target": "output:DRIVE.address",
+ "kind": "value"
+ },
+ {
+ "id": "drive_files",
+ "target": "output:DRIVE.files",
+ "kind": "value"
+ },
+ {
+ "id": "drive_commands",
+ "target": "output:DRIVE.commands",
+ "kind": "value"
+ },
+ {
+ "id": "drive_last",
+ "target": "output:DRIVE.last_command",
+ "kind": "value"
+ },
+ {
+ "id": "drive_read",
+ "target": "output:DRIVE.sectors_read",
+ "kind": "value"
+ },
+ {
+ "id": "drive_written",
+ "target": "output:DRIVE.sectors_written",
+ "kind": "value"
+ }
+ ]
+}
diff --git a/tests/vendor/stm32cubef4/nucleo_f446ze/usb_host_msc_standalone/scenario.json b/tests/vendor/stm32cubef4/nucleo_f446ze/usb_host_msc_standalone/scenario.json
new file mode 100644
index 00000000..44ee9095
--- /dev/null
+++ b/tests/vendor/stm32cubef4/nucleo_f446ze/usb_host_msc_standalone/scenario.json
@@ -0,0 +1,148 @@
+{
+ "schema_version": 1,
+ "kind": "scenario",
+ "name": "usb_host_msc_standalone",
+ "until": "4300ms",
+ "description": "USB_Host/MSC_Standalone, as it ships, on a NUCLEO-F446ZE with Adafruit's 1.8\" TFT shield (its LCD log) and a USB flash drive on CN13, fresh from the shop (FAT16, empty). The firmware switches VBUS on through U12, its OTG core enumerates the drive at full speed, ST's MSC class takes its LUN (Get Max LUN, INQUIRY, TEST UNIT READY, READ CAPACITY) and FatFs mounts it. The first press of B1 writes 'USBHost.txt' (the 40 bytes of 'USB Host Library : Mass Storage Example' with its NUL), reads it back and compares; the second lists the disk. The drive's outputs show the file a computer would then see.",
+ "inputs": [
+ {
+ "at": "2500ms",
+ "target": "B1.B1",
+ "input": "pressed",
+ "value": true
+ },
+ {
+ "at": "2700ms",
+ "target": "B1.B1",
+ "input": "pressed",
+ "value": false
+ },
+ {
+ "at": "3600ms",
+ "target": "B1.B1",
+ "input": "pressed",
+ "value": true
+ },
+ {
+ "at": "3800ms",
+ "target": "B1.B1",
+ "input": "pressed",
+ "value": false
+ }
+ ],
+ "assertions": [
+ {
+ "id": "vbus-off-at-start",
+ "at": "50ms",
+ "probe": "pg6",
+ "expect": {
+ "logic": "0"
+ }
+ },
+ {
+ "id": "drive-unpowered",
+ "at": "50ms",
+ "probe": "drive_state",
+ "expect": {
+ "equals": "detached"
+ }
+ },
+ {
+ "id": "vbus-switched-on",
+ "at": "200ms",
+ "probe": "pg6",
+ "expect": {
+ "logic": "1"
+ }
+ },
+ {
+ "id": "vbus-5v",
+ "at": "200ms",
+ "probe": "vbus",
+ "expect": {
+ "min_v": 4.9,
+ "max_v": 5.05
+ }
+ },
+ {
+ "id": "configured",
+ "by": "1600ms",
+ "probe": "drive_state",
+ "expect": {
+ "equals": "configured"
+ }
+ },
+ {
+ "id": "address-1",
+ "at": "2000ms",
+ "probe": "drive_address",
+ "expect": {
+ "equals": 1
+ }
+ },
+ {
+ "id": "lun-taken",
+ "at": "2400ms",
+ "probe": "drive_last",
+ "expect": {
+ "equals": "READ CAPACITY(10)"
+ }
+ },
+ {
+ "id": "empty-before-b1",
+ "at": "2400ms",
+ "probe": "drive_files",
+ "expect": {
+ "equals": ""
+ }
+ },
+ {
+ "id": "nothing-written-before-b1",
+ "at": "2400ms",
+ "probe": "drive_written",
+ "expect": {
+ "equals": 0
+ }
+ },
+ {
+ "id": "file-written",
+ "by": "3500ms",
+ "probe": "drive_files",
+ "expect": {
+ "equals": "USBHOST.TXT (40)"
+ }
+ },
+ {
+ "id": "sectors-written",
+ "at": "3500ms",
+ "probe": "drive_written",
+ "expect": {
+ "min": 3
+ }
+ },
+ {
+ "id": "read-back",
+ "at": "3500ms",
+ "probe": "drive_read",
+ "expect": {
+ "min": 1
+ }
+ },
+ {
+ "id": "lcd-on",
+ "at": "1000ms",
+ "probe": "lcd_on",
+ "expect": {
+ "equals": true
+ }
+ },
+ {
+ "id": "file-kept",
+ "at": "4250ms",
+ "probe": "drive_files",
+ "expect": {
+ "equals": "USBHOST.TXT (40)"
+ }
+ }
+ ]
+}
diff --git a/tests/vendor/stm32cubef4/run.py b/tests/vendor/stm32cubef4/run.py
index 3927ff44..cc73e3c9 100644
--- a/tests/vendor/stm32cubef4/run.py
+++ b/tests/vendor/stm32cubef4/run.py
@@ -103,6 +103,9 @@
"usb_device_hid_standalone": ZE + "Applications/USB_Device/HID_Standalone",
# the same image, suspended, woken by B1, unplugged and plugged back
"usb_device_hid_suspend": ZE + "Applications/USB_Device/HID_Standalone",
+ # the host side, the Adafruit 1.8" TFT shield on the Arduino connectors
+ "usb_host_hid_standalone": ZE + "Applications/USB_Host/HID_Standalone",
+ "usb_host_msc_standalone": ZE + "Applications/USB_Host/MSC_Standalone",
# ST's DFU loader as it ships: its static allocator (262 words, sized for
# MSC) is smaller than the DFU class's handle (1056 bytes with the
# middleware CubeF4 pins), so USBD_DFU_Init overruns it into hpcd
@@ -141,9 +144,30 @@ def checkouts(st: Path) -> dict:
"Drivers/BSP/STM32F4xx_Nucleo_144": st / "stm32f4xx-nucleo-144-bsp",
"Drivers/CMSIS/Include": st / "CMSIS_5" / "CMSIS" / "Core" / "Include",
"Middlewares/ST/STM32_USB_Device_Library": st / "stm32-mw-usb-device",
+ "Middlewares/ST/STM32_USB_Host_Library": st / "stm32-mw-usb-host",
+ "Middlewares/Third_Party/FatFs": st / "stm32-mw-fatfs",
+ "Drivers/BSP/Components/st7735": st / "stm32-st7735",
+ "Drivers/BSP/Components/Common": st / "stm32-bsp-common",
}
+def link_checkouts(st: Path) -> None:
+ """each submodule's checkout also where CubeF4 has it: its sources
+ include one another by paths relative to CubeF4's tree (the Adafruit
+ shield's BSP: "../Components/st7735/st7735.h")"""
+ for prefix, there in checkouts(st).items():
+ here = st / "cubef4" / prefix
+ if not there.exists() or (here.exists() and any(here.iterdir())):
+ continue
+ if here.is_symlink():
+ here.unlink()
+ elif here.exists():
+ here.rmdir() # the submodule's empty place
+ # a copy, not a link: "../" in their includes is resolved from the
+ # file's real directory
+ shutil.copytree(there, here, ignore=shutil.ignore_patterns(".git"))
+
+
def resolve(st: Path, path: Path) -> Path:
"""a path inside the CubeF4 checkout, with its submodules' parts mapped"""
cube = (st / "cubef4").resolve()
@@ -153,8 +177,10 @@ def resolve(st: Path, path: Path) -> Path:
except ValueError:
return p
for prefix, there in checkouts(st).items():
- if rel == prefix or rel.startswith(prefix + "/"):
+ if (rel == prefix or rel.startswith(prefix + "/")) and not (cube / prefix).is_dir():
return there / rel[len(prefix):].lstrip("/")
+ if rel == prefix or rel.startswith(prefix + "/"):
+ return p # its copy in CubeF4's tree (link_checkouts)
return p
@@ -285,6 +311,7 @@ def main() -> int:
if not (st / need).exists():
print(f"error: {st} has no {need}")
return 2
+ link_checkouts(st)
names = args.examples or sorted(d.name for d in BOARD["dir"].iterdir() if d.is_dir())
with tempfile.TemporaryDirectory() as tmp:
work = Path(args.keep).resolve() if args.keep else Path(tmp)