feat(boards): add 6 STM32 boards (F4 Discovery, Olimex H405, Netduino 2/+2, Pill variants)

Adds stm32-f4-discovery, stm32-olimex-h405, stm32-netduino-plus2, stm32-netduino2, stm32-blackpill-f401 and stm32-bluepill-f103cb, mapped to existing qemu-lcgamboa machines (netduinoplus2, olimex-stm32-h405, netduino2, stm32vldiscovery). A generic inline board renderer (no SVG) draws the Discovery/Olimex/Netduino boards from a header pin layout; the Pill variants reuse the Blue/Black Pill SVGs. Per-board onboard-LED pin and polarity via STM32_LED. One blink+serial example per board.

tsc --noEmit clean; all new FQBN pnum variants present in STM32 core 2.12.0; worker smoke tests pass for the new machines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Montero Crespo 2026-05-31 00:59:04 -03:00
parent d9f98709d2
commit 6813891f91
11 changed files with 574 additions and 47 deletions

View File

@ -54,13 +54,21 @@ EventCallback = Callable[[str, dict], Awaitable[None]]
# the bridge today (F100 / F405) get full GPIO; F2 (netduino2) is serial-only
# until its GPIO is wired.
_MACHINE: dict[str, str] = {
'stm32-bluepill': 'stm32vldiscovery', # F103 approximated by F100 SoC
'stm32-blackpill': 'netduinoplus2', # F411 approximated by F405 SoC (Cortex-M4)
'stm32-vldiscovery': 'stm32vldiscovery',
'stm32f4-discovery': 'netduinoplus2', # F407 approximated by F405 SoC
'netduinoplus2': 'netduinoplus2',
'olimex-stm32-h405': 'olimex-stm32-h405',
'netduino2': 'netduino2',
# Canonical BoardKind strings (sent verbatim by the frontend Stm32Bridge).
'stm32-bluepill': 'stm32vldiscovery', # F103 approximated by F100 SoC
'stm32-blackpill': 'netduinoplus2', # F411 approximated by F405 SoC (Cortex-M4)
'stm32-bluepill-f103cb': 'stm32vldiscovery', # F103CB approximated by F100 SoC
'stm32-blackpill-f401': 'netduinoplus2', # F401 approximated by F405 SoC
'stm32-f4-discovery': 'netduinoplus2', # F407 approximated by F405 SoC
'stm32-olimex-h405': 'olimex-stm32-h405', # F405 (native machine)
'stm32-netduino-plus2': 'netduinoplus2', # F405 (native machine)
'stm32-netduino2': 'netduino2', # F205 (serial-only until GPIO wired)
# Legacy / alias keys (kept so older saved projects still resolve).
'stm32-vldiscovery': 'stm32vldiscovery',
'stm32f4-discovery': 'netduinoplus2',
'netduinoplus2': 'netduinoplus2',
'olimex-stm32-h405': 'olimex-stm32-h405',
'netduino2': 'netduino2',
}

View File

@ -57,6 +57,12 @@ const BOARD_DESCRIPTIONS: Record<BoardKind, string> = {
'aitewinrobot-esp32c3-supermini': 'ESP32-C3 SuperMini (QEMU)',
'stm32-bluepill': 'STM32F103C8 Cortex-M3, 64KB flash, 37 GPIO (QEMU)',
'stm32-blackpill': 'STM32F411CE Cortex-M4, 512KB flash, 50 GPIO (QEMU)',
'stm32-bluepill-f103cb': 'STM32F103CB Cortex-M3, 128KB flash, 37 GPIO (QEMU)',
'stm32-blackpill-f401': 'STM32F401CE Cortex-M4, 512KB flash, 50 GPIO (QEMU)',
'stm32-f4-discovery': 'STM32F407VG Cortex-M4, 1MB flash, 4 onboard LEDs (QEMU)',
'stm32-olimex-h405': 'Olimex STM32-H405, F405RG Cortex-M4, 1MB flash (QEMU)',
'stm32-netduino-plus2': 'Netduino Plus 2, STM32F405 Cortex-M4 (QEMU)',
'stm32-netduino2': 'Netduino 2, STM32F205 Cortex-M3 (QEMU, serial)',
attiny85: '8-bit AVR, 8KB flash, 6 GPIO (browser)',
};
@ -81,6 +87,12 @@ const ALL_BOARDS: BoardKind[] = [
'aitewinrobot-esp32c3-supermini',
'stm32-bluepill',
'stm32-blackpill',
'stm32-bluepill-f103cb',
'stm32-blackpill-f401',
'stm32-f4-discovery',
'stm32-olimex-h405',
'stm32-netduino-plus2',
'stm32-netduino2',
'attiny85',
];
@ -424,6 +436,12 @@ const BOARD_TAG: Partial<Record<BoardKind, string>> = {
'aitewinrobot-esp32c3-supermini': 'velxio-esp32',
'stm32-bluepill': 'velxio-stm32-bluepill',
'stm32-blackpill': 'velxio-stm32-blackpill',
'stm32-bluepill-f103cb': 'velxio-stm32-bluepill-f103cb',
'stm32-blackpill-f401': 'velxio-stm32-blackpill-f401',
'stm32-f4-discovery': 'velxio-stm32-f4-discovery',
'stm32-olimex-h405': 'velxio-stm32-olimex-h405',
'stm32-netduino-plus2': 'velxio-stm32-netduino-plus2',
'stm32-netduino2': 'velxio-stm32-netduino2',
};
interface BoardCardProps {

View File

@ -68,6 +68,12 @@ const BOARD_PILL_ICON: Record<BoardKind, string> = {
'esp32-c3': '⬡',
'stm32-bluepill': '◈',
'stm32-blackpill': '◈',
'stm32-bluepill-f103cb': '◈',
'stm32-blackpill-f401': '◈',
'stm32-f4-discovery': '◈',
'stm32-olimex-h405': '◈',
'stm32-netduino-plus2': '◈',
'stm32-netduino2': '◈',
};
const BOARD_PILL_COLOR: Record<BoardKind, string> = {
@ -83,6 +89,12 @@ const BOARD_PILL_COLOR: Record<BoardKind, string> = {
'esp32-c3': '#a5d6a7',
'stm32-bluepill': '#80cbc4',
'stm32-blackpill': '#b0bec5',
'stm32-bluepill-f103cb': '#80cbc4',
'stm32-blackpill-f401': '#b0bec5',
'stm32-f4-discovery': '#90caf9',
'stm32-olimex-h405': '#a5d6a7',
'stm32-netduino-plus2': '#ce93d8',
'stm32-netduino2': '#ce93d8',
};
export const EditorToolbar = ({

View File

@ -141,6 +141,12 @@ const BOARD_ICON: Record<BoardKind, string> = {
'esp32-c3': '⬡',
'stm32-bluepill': '◈',
'stm32-blackpill': '◈',
'stm32-bluepill-f103cb': '◈',
'stm32-blackpill-f401': '◈',
'stm32-f4-discovery': '◈',
'stm32-olimex-h405': '◈',
'stm32-netduino-plus2': '◈',
'stm32-netduino2': '◈',
};
// Color accent per board family
@ -155,6 +161,12 @@ const BOARD_COLOR: Record<BoardKind, string> = {
'esp32-c3': '#a5d6a7',
'stm32-bluepill': '#80cbc4',
'stm32-blackpill': '#b0bec5',
'stm32-bluepill-f103cb': '#80cbc4',
'stm32-blackpill-f401': '#b0bec5',
'stm32-f4-discovery': '#90caf9',
'stm32-olimex-h405': '#a5d6a7',
'stm32-netduino-plus2': '#ce93d8',
'stm32-netduino2': '#ce93d8',
};
function FileIcon({ name }: { name: string }) {

View File

@ -13,7 +13,16 @@ import { RaspberryPi5 } from '../velxio-components/RaspberryPi5';
import { Esp32 } from '../velxio-components/Esp32';
import { Attiny85 } from '../velxio-components/Attiny85';
import { PiPicoW } from '../velxio-components/PiPicoW';
import { Stm32BluePill, Stm32BlackPill } from '../velxio-components/Stm32BluePill';
import {
Stm32BluePill,
Stm32BlackPill,
Stm32BluePillF103CB,
Stm32BlackPillF401,
Stm32F4Discovery,
Stm32OlimexH405,
Stm32NetduinoPlus2,
Stm32Netduino2,
} from '../velxio-components/Stm32BluePill';
import { PinOverlay } from './PinOverlay';
// Board visual dimensions (width × height) for the drag-overlay sizing.
@ -47,6 +56,14 @@ const BOARD_SIZE: Record<string, { w: number; h: number }> = {
'aitewinrobot-esp32c3-supermini': { w: 90, h: 123 },
'stm32-bluepill': { w: 114, h: 271 }, // 22.855 × 54.193 mm (wokwi-boards SVG)
'stm32-blackpill': { w: 103, h: 266 }, // 20.695 × 53.125 mm (wokwi-boards SVG)
'stm32-bluepill-f103cb': { w: 114, h: 271 }, // reuses Blue Pill SVG
'stm32-blackpill-f401': { w: 103, h: 266 }, // reuses Black Pill SVG
// Inline-rendered boards — sizes match Stm32BoardElement.inlineConfig()
// (INLINE_W=158; h = 24 + rows*13 + 16).
'stm32-f4-discovery': { w: 158, h: 235 }, // 15 rows per side
'stm32-olimex-h405': { w: 158, h: 196 }, // 12 rows per side
'stm32-netduino-plus2': { w: 158, h: 196 }, // 12 rows per side
'stm32-netduino2': { w: 158, h: 196 }, // 12 rows per side
attiny85: { w: 160, h: 132 },
};
@ -123,6 +140,18 @@ export const BoardOnCanvas = ({
return <Stm32BluePill id={id} x={x} y={y} />;
case 'stm32-blackpill':
return <Stm32BlackPill id={id} x={x} y={y} />;
case 'stm32-bluepill-f103cb':
return <Stm32BluePillF103CB id={id} x={x} y={y} />;
case 'stm32-blackpill-f401':
return <Stm32BlackPillF401 id={id} x={x} y={y} />;
case 'stm32-f4-discovery':
return <Stm32F4Discovery id={id} x={x} y={y} />;
case 'stm32-olimex-h405':
return <Stm32OlimexH405 id={id} x={x} y={y} />;
case 'stm32-netduino-plus2':
return <Stm32NetduinoPlus2 id={id} x={x} y={y} />;
case 'stm32-netduino2':
return <Stm32Netduino2 id={id} x={x} y={y} />;
case 'attiny85':
return <Attiny85 id={id} x={x} y={y} led1={led13} />;
}

View File

@ -1,17 +1,19 @@
import './Stm32BluePillElement';
type StmIntrinsic = React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace JSX {
interface IntrinsicElements {
'velxio-stm32-bluepill': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement>,
HTMLElement
>;
'velxio-stm32-blackpill': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement>,
HTMLElement
>;
'velxio-stm32-bluepill': StmIntrinsic;
'velxio-stm32-blackpill': StmIntrinsic;
'velxio-stm32-bluepill-f103cb': StmIntrinsic;
'velxio-stm32-blackpill-f401': StmIntrinsic;
'velxio-stm32-f4-discovery': StmIntrinsic;
'velxio-stm32-olimex-h405': StmIntrinsic;
'velxio-stm32-netduino-plus2': StmIntrinsic;
'velxio-stm32-netduino2': StmIntrinsic;
}
}
}
@ -30,3 +32,27 @@ export const Stm32BluePill = ({ id, x, y }: Props) => (
export const Stm32BlackPill = ({ id, x, y }: Props) => (
<velxio-stm32-blackpill id={id} style={{ position: 'absolute', left: x, top: y }} />
);
export const Stm32BluePillF103CB = ({ id, x, y }: Props) => (
<velxio-stm32-bluepill-f103cb id={id} style={{ position: 'absolute', left: x, top: y }} />
);
export const Stm32BlackPillF401 = ({ id, x, y }: Props) => (
<velxio-stm32-blackpill-f401 id={id} style={{ position: 'absolute', left: x, top: y }} />
);
export const Stm32F4Discovery = ({ id, x, y }: Props) => (
<velxio-stm32-f4-discovery id={id} style={{ position: 'absolute', left: x, top: y }} />
);
export const Stm32OlimexH405 = ({ id, x, y }: Props) => (
<velxio-stm32-olimex-h405 id={id} style={{ position: 'absolute', left: x, top: y }} />
);
export const Stm32NetduinoPlus2 = ({ id, x, y }: Props) => (
<velxio-stm32-netduino-plus2 id={id} style={{ position: 'absolute', left: x, top: y }} />
);
export const Stm32Netduino2 = ({ id, x, y }: Props) => (
<velxio-stm32-netduino2 id={id} style={{ position: 'absolute', left: x, top: y }} />
);

View File

@ -1,17 +1,23 @@
/**
* STM32 board Web Components (Blue Pill, Black Pill).
* STM32 board Web Components.
*
* Per CLAUDE.md rule 6a, boards that take wire connections MUST be real Web
* Components exposing a `pinInfo` getter. Renders the official wokwi-boards
* SVG with the pin coordinates from that board.json (mm x 5 px/mm), plus an
* overlay for the onboard PC13 LED (active-LOW: lit when PC13 is LOW).
* Components exposing a `pinInfo` getter. Two render modes:
*
* Pin `name`s are the real port labels ('PA9', 'PC13', ...) so examples wire
* to them; Stm32Bridge.stm32PinNameToLinear() maps them to the backend's
* linear pin index (port*16 + pin).
* - 'svg': renders the official wokwi-boards SVG with hand-placed pin
* coordinates (Blue Pill / Black Pill, and the pin-compatible
* F103CB / F401 variants reuse those exact SVGs).
* - 'inline': draws the board in-component (no SVG file) a green PCB with an
* MCU square, labeled header pads computed from `headers`, and an
* onboard LED dot. Used by the Discovery / Olimex / Netduino boards
* for which wokwi-boards ships no SVG.
*
* Registers two custom elements: <velxio-stm32-bluepill> and
* <velxio-stm32-blackpill>, selected by config.
* Pin `name`s are the real port labels ('PA9', 'PC13', ...) so examples wire to
* them; Stm32Bridge.stm32PinNameToLinear() maps them to the backend's linear
* pin index (port*16 + pin).
*
* Registers one custom element per board (one tag, one subclass), all driven by
* the shared Stm32BoardElement class and the CONFIGS table.
*/
interface PinDef {
@ -21,14 +27,33 @@ interface PinDef {
}
interface BoardConfig {
svgUrl: string;
render: 'svg' | 'inline';
w: number;
h: number;
pins: PinDef[];
/** Onboard PC13 LED center (px). */
/** Onboard LED center (px). */
led: { x: number; y: number };
/** Active-LOW boards (Blue/Black Pill PC13) are lit when the pin is LOW. */
ledActiveLow: boolean;
// 'svg' mode only:
svgUrl?: string;
// 'inline' mode only:
label?: string;
}
/** Which port pin carries the onboard LED, and its polarity, per board kind.
* Consumed by useSimulatorStore to light the correct LED dot. */
export const STM32_LED: Record<string, { pin: string; activeLow: boolean }> = {
'stm32-bluepill': { pin: 'PC13', activeLow: true },
'stm32-blackpill': { pin: 'PC13', activeLow: true },
'stm32-bluepill-f103cb': { pin: 'PC13', activeLow: true },
'stm32-blackpill-f401': { pin: 'PC13', activeLow: true },
'stm32-f4-discovery': { pin: 'PD12', activeLow: false },
'stm32-olimex-h405': { pin: 'PC12', activeLow: false },
'stm32-netduino-plus2': { pin: 'PA10', activeLow: false },
'stm32-netduino2': { pin: 'PA10', activeLow: false },
};
// ── STM32 Blue Pill (F103C8) — 22.855 x 54.193 mm -> 114 x 271 px ───────────
const BLUEPILL_PINS: PinDef[] = [
{ name: 'PB12', x: 18, y: 14 }, { name: 'PB13', x: 18, y: 27 },
@ -77,15 +102,109 @@ const BLACKPILL_PINS: PinDef[] = [
{ name: 'PB13', x: 89, y: 243 }, { name: 'PB12', x: 89, y: 256 },
];
// ── Inline-rendered boards (no SVG) — generic PCB drawn from a header layout ──
// Geometry constants shared by every inline board.
const PAD_PITCH = 13;
const MARGIN_TOP = 24;
const MARGIN_BOTTOM = 16;
const INLINE_W = 158;
const TIP_L = 9; // left-column pin tip x (wire connection point)
const TIP_R = INLINE_W - 9; // right-column pin tip x
/** Build a flat BoardConfig for an inline board from its header pin lists.
* Computes pad/tip coordinates so `pinInfo` returns correct wire endpoints
* without per-pin hand placement. */
function inlineConfig(opts: {
label: string;
left: string[];
right: string[];
ledPin: string;
ledActiveLow: boolean;
}): BoardConfig {
const rows = Math.max(opts.left.length, opts.right.length);
const h = MARGIN_TOP + rows * PAD_PITCH + MARGIN_BOTTOM;
const pins: PinDef[] = [];
opts.left.forEach((name, i) =>
pins.push({ name, x: TIP_L, y: MARGIN_TOP + i * PAD_PITCH + PAD_PITCH / 2 }),
);
opts.right.forEach((name, i) =>
pins.push({ name, x: TIP_R, y: MARGIN_TOP + i * PAD_PITCH + PAD_PITCH / 2 }),
);
// LED dot sits in the free center column, just under the board title.
return {
render: 'inline',
label: opts.label,
w: INLINE_W,
h,
pins,
led: { x: INLINE_W / 2, y: MARGIN_TOP + 2 },
ledActiveLow: opts.ledActiveLow,
};
}
const CONFIGS: Record<string, BoardConfig> = {
'stm32-bluepill': {
svgUrl: '/boards/stm32-bluepill.svg', w: 114, h: 271,
pins: BLUEPILL_PINS, led: { x: 37, y: 228 },
render: 'svg', svgUrl: '/boards/stm32-bluepill.svg', w: 114, h: 271,
pins: BLUEPILL_PINS, led: { x: 37, y: 228 }, ledActiveLow: true,
},
'stm32-blackpill': {
svgUrl: '/boards/stm32-blackpill.svg', w: 103, h: 266,
pins: BLACKPILL_PINS, led: { x: 27, y: 55 },
render: 'svg', svgUrl: '/boards/stm32-blackpill.svg', w: 103, h: 266,
pins: BLACKPILL_PINS, led: { x: 27, y: 55 }, ledActiveLow: true,
},
// Pin-compatible variants — reuse the Pill SVGs and pin coordinates.
'stm32-bluepill-f103cb': {
render: 'svg', svgUrl: '/boards/stm32-bluepill.svg', w: 114, h: 271,
pins: BLUEPILL_PINS, led: { x: 37, y: 228 }, ledActiveLow: true,
},
'stm32-blackpill-f401': {
render: 'svg', svgUrl: '/boards/stm32-blackpill.svg', w: 103, h: 266,
pins: BLACKPILL_PINS, led: { x: 27, y: 55 }, ledActiveLow: true,
},
// Inline boards (F405 / F205 SoCs) — drawn generically.
'stm32-f4-discovery': inlineConfig({
label: 'STM32F4 Discovery',
left: ['3V3', 'GND', 'PA0', 'PA1', 'PA2', 'PA3', 'PA4', 'PA5', 'PA6', 'PA7',
'PB0', 'PB1', 'PB2', 'PE0', 'PE1'],
right: ['5V', 'GND', 'PD12', 'PD13', 'PD14', 'PD15', 'PC0', 'PC1', 'PC2',
'PC3', 'PB6', 'PB7', 'PB8', 'PB9', 'PE2'],
ledPin: 'PD12', ledActiveLow: false,
}),
'stm32-olimex-h405': inlineConfig({
label: 'Olimex STM32-H405',
left: ['3V3', 'GND', 'PA0', 'PA1', 'PA2', 'PA3', 'PB6', 'PB7', 'PB8', 'PB9',
'PC0', 'PC1'],
right: ['5V', 'GND', 'PC12', 'PC10', 'PC11', 'PA4', 'PA5', 'PA6', 'PA7',
'PB0', 'PB1', 'PB10'],
ledPin: 'PC12', ledActiveLow: false,
}),
'stm32-netduino-plus2': inlineConfig({
label: 'Netduino Plus 2',
left: ['3V3', 'GND', 'PA0', 'PA1', 'PA2', 'PA3', 'PA4', 'PA5', 'PB6', 'PB7',
'PB8', 'PB9'],
right: ['5V', 'GND', 'PA10', 'PB11', 'PC0', 'PC1', 'PC2', 'PC3', 'PA6',
'PA7', 'PB0', 'PB1'],
ledPin: 'PA10', ledActiveLow: false,
}),
'stm32-netduino2': inlineConfig({
label: 'Netduino 2',
left: ['3V3', 'GND', 'PA0', 'PA1', 'PA2', 'PA3', 'PA4', 'PA5', 'PB6', 'PB7',
'PB8', 'PB9'],
right: ['5V', 'GND', 'PA10', 'PB11', 'PC0', 'PC1', 'PC2', 'PC3', 'PA6',
'PA7', 'PB0', 'PB1'],
ledPin: 'PA10', ledActiveLow: false,
}),
};
/** Tag name -> CONFIGS key. One custom element per board kind. */
const TAG_TO_KIND: Record<string, string> = {
'velxio-stm32-bluepill': 'stm32-bluepill',
'velxio-stm32-blackpill': 'stm32-blackpill',
'velxio-stm32-bluepill-f103cb': 'stm32-bluepill-f103cb',
'velxio-stm32-blackpill-f401': 'stm32-blackpill-f401',
'velxio-stm32-f4-discovery': 'stm32-f4-discovery',
'velxio-stm32-olimex-h405': 'stm32-olimex-h405',
'velxio-stm32-netduino-plus2': 'stm32-netduino-plus2',
'velxio-stm32-netduino2': 'stm32-netduino2',
};
class Stm32BoardElement extends HTMLElement {
@ -112,9 +231,7 @@ class Stm32BoardElement extends HTMLElement {
}
private _defaultKind(): string {
return this.tagName.toLowerCase() === 'velxio-stm32-blackpill'
? 'stm32-blackpill'
: 'stm32-bluepill';
return TAG_TO_KIND[this.tagName.toLowerCase()] ?? 'stm32-bluepill';
}
/** Pin tip coordinates in CSS px relative to element top-left (rule 6a). */
@ -122,7 +239,7 @@ class Stm32BoardElement extends HTMLElement {
return this.config.pins;
}
/** Onboard PC13 LED (visual lit state; store passes polarity-corrected value). */
/** Onboard LED (visual lit state; store passes polarity-corrected value). */
set led(on: boolean) {
if (this._ledOn === on) return;
this._ledOn = on;
@ -135,7 +252,16 @@ class Stm32BoardElement extends HTMLElement {
private render() {
if (!this.shadowRoot) return;
const { svgUrl, w, h, led } = this.config;
if (this.config.render === 'inline') {
this.renderInline(this.config);
} else {
this.renderSvg(this.config);
}
}
private renderSvg(cfg: BoardConfig) {
if (!this.shadowRoot) return;
const { svgUrl, w, h, led } = cfg;
this.shadowRoot.innerHTML = `
<style>
:host { display: inline-block; line-height: 0; position: relative; }
@ -151,16 +277,72 @@ class Stm32BoardElement extends HTMLElement {
<div id="led"></div>
`;
}
private renderInline(cfg: BoardConfig) {
if (!this.shadowRoot) return;
const { w, h, pins, led, label } = cfg;
const mcu = 50;
const padHtml = pins
.map((p) => {
const onLeft = p.x <= w / 2;
const padX = onLeft ? 4 : w - 14; // 10px-wide pad, center aligns to tip
const labelStyle = onLeft
? `left:${padX + 13}px; text-align:left;`
: `right:${w - padX + 3}px; text-align:right;`;
return (
`<div class="pad" style="left:${padX}px; top:${p.y - 4}px;"></div>` +
`<div class="plabel" style="top:${p.y - 5}px; ${labelStyle}">${p.name}</div>`
);
})
.join('');
this.shadowRoot.innerHTML = `
<style>
:host { display: inline-block; line-height: 0; position: relative; }
.pcb {
position: relative; box-sizing: border-box;
background: #0b6b3a; border-radius: 8px;
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.10);
font-family: ui-sans-serif, system-ui, sans-serif;
}
.title {
position: absolute; top: 4px; left: 0; right: 0; text-align: center;
font-size: 8px; color: #d6efe0; letter-spacing: 0.3px; line-height: 1;
}
.mcu {
position: absolute; background: #161616; border-radius: 3px;
box-shadow: inset 0 0 0 1px #333, 0 1px 2px rgba(0,0,0,0.4);
}
.pad {
position: absolute; width: 10px; height: 8px;
background: #d4af37; border-radius: 2px;
}
.plabel {
position: absolute; font-size: 7px; color: #eaf5ee;
line-height: 1; white-space: nowrap;
}
#led {
position: absolute; left: ${led.x - 5}px; top: ${led.y - 5}px;
width: 10px; height: 10px; border-radius: 50%;
background: #00e676; box-shadow: 0 0 6px 2px #00e676;
opacity: 0; transition: opacity 40ms linear; pointer-events: none;
}
</style>
<div class="pcb" style="width:${w}px; height:${h}px;">
<div class="title">${label ?? 'STM32'}</div>
<div class="mcu" style="left:${w / 2 - mcu / 2}px; top:${h * 0.42}px; width:${mcu}px; height:${mcu}px;"></div>
${padHtml}
<div id="led"></div>
</div>
`;
}
}
export { Stm32BoardElement as Stm32BluePillElement };
if (!customElements.get('velxio-stm32-bluepill')) {
customElements.define('velxio-stm32-bluepill', Stm32BoardElement);
}
if (!customElements.get('velxio-stm32-blackpill')) {
// Separate class instance per tag name (customElements requires a unique
// constructor per name); subclass so both register cleanly.
class Stm32BlackPillElement extends Stm32BoardElement {}
customElements.define('velxio-stm32-blackpill', Stm32BlackPillElement);
// One unique constructor per tag — customElements.define() requires it.
for (const tag of Object.keys(TAG_TO_KIND)) {
if (!customElements.get(tag)) {
const cls = class extends Stm32BoardElement {};
customElements.define(tag, cls);
}
}

View File

@ -157,6 +157,216 @@ void loop() {
wires: [],
tags: ['stm32', 'blue pill', 'serial', 'usart', 'qemu'],
},
{
id: 'stm32-f4-discovery-blink',
title: 'STM32F4 Discovery LED Blink',
description:
'Blink the onboard PD12 green LED and print to Serial on an STM32F4 Discovery (STM32F407VG, Cortex-M4) under QEMU.',
category: 'basics',
difficulty: 'beginner',
boardFilter: 'stm32-f4-discovery',
boards: [
{
boardKind: 'stm32-f4-discovery',
x: 100,
y: 100,
code: `// STM32F4 Discovery (STM32F407VG) — onboard LEDs PD12..PD15 (active HIGH).
// PD12 green, PD13 orange, PD14 red, PD15 blue. Runs under QEMU (libqemu-arm).
void setup() {
pinMode(PD12, OUTPUT);
Serial.begin(115200);
Serial.println("velxio stm32f4 discovery");
}
void loop() {
digitalWrite(PD12, HIGH);
delay(200);
digitalWrite(PD12, LOW);
delay(200);
}`,
},
],
code: '',
components: [],
wires: [],
tags: ['stm32', 'discovery', 'f407', 'cortex-m4', 'qemu', 'blink'],
},
{
id: 'stm32-olimex-h405-blink',
title: 'Olimex STM32-H405 Blink',
description:
'Blink the onboard PC12 status LED and print to Serial on an Olimex STM32-H405 (STM32F405RG, Cortex-M4) under QEMU.',
category: 'basics',
difficulty: 'beginner',
boardFilter: 'stm32-olimex-h405',
boards: [
{
boardKind: 'stm32-olimex-h405',
x: 100,
y: 100,
code: `// Olimex STM32-H405 (STM32F405RG) — onboard status LED on PC12 (active HIGH).
void setup() {
pinMode(PC12, OUTPUT);
Serial.begin(115200);
Serial.println("velxio olimex h405");
}
void loop() {
digitalWrite(PC12, HIGH);
delay(250);
digitalWrite(PC12, LOW);
delay(250);
}`,
},
],
code: '',
components: [],
wires: [],
tags: ['stm32', 'olimex', 'h405', 'f405', 'cortex-m4', 'qemu'],
},
{
id: 'stm32-netduino-plus2-blink',
title: 'Netduino Plus 2 Blink',
description:
'Blink the onboard PA10 LED and print to Serial on a Netduino Plus 2 (STM32F405, Cortex-M4) under QEMU.',
category: 'basics',
difficulty: 'beginner',
boardFilter: 'stm32-netduino-plus2',
boards: [
{
boardKind: 'stm32-netduino-plus2',
x: 100,
y: 100,
code: `// Netduino Plus 2 (STM32F405) — onboard LED on PA10 (active HIGH).
void setup() {
pinMode(PA10, OUTPUT);
Serial.begin(115200);
Serial.println("velxio netduino plus 2");
}
void loop() {
digitalWrite(PA10, HIGH);
delay(300);
digitalWrite(PA10, LOW);
delay(300);
}`,
},
],
code: '',
components: [],
wires: [],
tags: ['stm32', 'netduino', 'f405', 'cortex-m4', 'qemu', 'blink'],
},
{
id: 'stm32-netduino2-serial',
title: 'Netduino 2 Serial Counter',
description:
'Netduino 2 (STM32F205, Cortex-M3) prints an incrementing counter over Serial. GPIO/LED support arrives once the F205 SoC is wired; serial works today under QEMU.',
category: 'communication',
difficulty: 'beginner',
boardFilter: 'stm32-netduino2',
boards: [
{
boardKind: 'stm32-netduino2',
x: 100,
y: 100,
code: `// Netduino 2 (STM32F205) — serial counter at 115200 baud.
// Onboard LED is on PA10; GPIO becomes visual once the F205 SoC is wired.
unsigned long n = 0;
void setup() {
Serial.begin(115200);
pinMode(PA10, OUTPUT);
Serial.println("Netduino 2 ready");
}
void loop() {
n++;
Serial.print("count=");
Serial.print(n);
Serial.print(" uptime_ms=");
Serial.println(millis());
digitalWrite(PA10, n % 2 ? HIGH : LOW);
delay(1000);
}`,
},
],
code: '',
components: [],
wires: [],
tags: ['stm32', 'netduino', 'f205', 'serial', 'qemu'],
},
{
id: 'stm32-blackpill-f401-blink',
title: 'STM32 Black Pill (F401) Blink',
description:
'Blink the onboard PC13 LED and print to Serial on an STM32F401CE Black Pill (Cortex-M4) under QEMU.',
category: 'basics',
difficulty: 'beginner',
boardFilter: 'stm32-blackpill-f401',
boards: [
{
boardKind: 'stm32-blackpill-f401',
x: 100,
y: 100,
code: `// STM32 Black Pill (STM32F401CE) — onboard LED on PC13 (active LOW).
void setup() {
pinMode(PC13, OUTPUT);
Serial.begin(115200);
Serial.println("velxio stm32 f401 blackpill");
}
void loop() {
digitalWrite(PC13, LOW); // LED on
delay(200);
digitalWrite(PC13, HIGH); // LED off
delay(200);
}`,
},
],
code: '',
components: [],
wires: [],
tags: ['stm32', 'black pill', 'f401', 'cortex-m4', 'qemu', 'blink'],
},
{
id: 'stm32-bluepill-f103cb-blink',
title: 'STM32 Blue Pill (F103CB) Blink',
description:
'Blink the onboard PC13 LED and print to Serial on an STM32F103CB Blue Pill (Cortex-M3, 128KB flash) under QEMU.',
category: 'basics',
difficulty: 'beginner',
boardFilter: 'stm32-bluepill-f103cb',
boards: [
{
boardKind: 'stm32-bluepill-f103cb',
x: 100,
y: 100,
code: `// STM32 Blue Pill (STM32F103CB, 128KB flash) — onboard LED on PC13 (active LOW).
void setup() {
pinMode(PC13, OUTPUT);
Serial.begin(115200);
Serial.println("velxio stm32 f103cb blue pill");
}
void loop() {
digitalWrite(PC13, LOW); // LED on
delay(200);
digitalWrite(PC13, HIGH); // LED off
delay(200);
}`,
},
],
code: '',
components: [],
wires: [],
tags: ['stm32', 'blue pill', 'f103cb', 'cortex-m3', 'qemu', 'blink'],
},
{
id: 'stm32-uno-gpio-mirror',
title: '[STM32 + Arduino] GPIO Mirror',

View File

@ -25,6 +25,7 @@ import { useOscilloscopeStore } from './useOscilloscopeStore';
import { RaspberryPi3Bridge } from '../simulation/RaspberryPi3Bridge';
import { Esp32Bridge } from '../simulation/Esp32Bridge';
import { Stm32Bridge, stm32PinNameToLinear } from '../simulation/Stm32Bridge';
import { STM32_LED } from '../components/velxio-components/Stm32BluePillElement';
import { useEditorStore } from './useEditorStore';
import { useVfsStore } from './useVfsStore';
import { boardPinToNumber, isBoardComponent } from '../utils/boardPinMapping';
@ -1097,14 +1098,18 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
simulatorMap.set(id, shim);
} else if (isStm32BoardKind(boardKind)) {
const bridge = new Stm32Bridge(id, boardKind);
// Onboard-LED pin + polarity per board kind. Blue/Black Pill drive PC13
// active-LOW; the F4 Discovery / Olimex / Netduino boards drive their LED
// active-HIGH on a different port pin (see STM32_LED).
const ledCfg = STM32_LED[boardKind] ?? { pin: 'PC13', activeLow: true };
const ledLinear = stm32PinNameToLinear(ledCfg.pin);
bridge.onSerialData = serialCallback;
bridge.onPinChange = (gpioPin, state) => {
const boardPm = pinManagerMap.get(id);
if (boardPm) boardPm.triggerPinChange(gpioPin, state, 'mcu');
// Onboard LED on PC13 (linear pin 45) is active-LOW: lit when LOW.
if (gpioPin === stm32PinNameToLinear('PC13')) {
if (gpioPin === ledLinear) {
const dom = document.getElementById(id) as (HTMLElement & { led?: boolean }) | null;
if (dom && 'led' in dom) dom.led = !state;
if (dom && 'led' in dom) dom.led = ledCfg.activeLow ? !state : !!state;
}
};
bridge.onPinChangeWithTime = getOscilloscopeCallback(id);

View File

@ -22,6 +22,12 @@ export type BoardKind =
| 'aitewinrobot-esp32c3-supermini' // ESP32-C3 SuperMini, QEMU backend
| 'stm32-bluepill' // STM32F103C8 (Cortex-M3), QEMU backend (libqemu-arm)
| 'stm32-blackpill' // STM32F411CE (Cortex-M4), QEMU backend (libqemu-arm)
| 'stm32-bluepill-f103cb' // STM32F103CB (Cortex-M3, 128KB), QEMU (F100 SoC)
| 'stm32-blackpill-f401' // STM32F401CE (Cortex-M4), QEMU (F405 SoC)
| 'stm32-f4-discovery' // STM32F407VG Discovery (Cortex-M4), QEMU (F405 SoC)
| 'stm32-olimex-h405' // Olimex STM32-H405 (F405RG, Cortex-M4), QEMU
| 'stm32-netduino-plus2' // Netduino Plus 2 (F405, Cortex-M4), QEMU
| 'stm32-netduino2' // Netduino 2 (F205, Cortex-M3), QEMU (serial until F205 GPIO wired)
| 'attiny85'; // AVR ATtiny85, browser emulation (avr8js)
export type LanguageMode = 'arduino' | 'micropython';
@ -114,6 +120,12 @@ export const BOARD_KIND_LABELS: Record<BoardKind, string> = {
'aitewinrobot-esp32c3-supermini': 'ESP32-C3 SuperMini',
'stm32-bluepill': 'STM32 Blue Pill',
'stm32-blackpill': 'STM32 Black Pill',
'stm32-bluepill-f103cb': 'STM32 Blue Pill (F103CB)',
'stm32-blackpill-f401': 'STM32 Black Pill (F401)',
'stm32-f4-discovery': 'STM32F4 Discovery',
'stm32-olimex-h405': 'Olimex STM32-H405',
'stm32-netduino-plus2': 'Netduino Plus 2',
'stm32-netduino2': 'Netduino 2',
attiny85: 'ATtiny85',
};
@ -141,5 +153,11 @@ export const BOARD_KIND_FQBN: Record<BoardKind, string | null> = {
'aitewinrobot-esp32c3-supermini': 'esp32:esp32:esp32c3',
'stm32-bluepill': 'STMicroelectronics:stm32:GenF1:pnum=BLUEPILL_F103C8',
'stm32-blackpill': 'STMicroelectronics:stm32:GenF4:pnum=BLACKPILL_F411CE',
'stm32-bluepill-f103cb': 'STMicroelectronics:stm32:GenF1:pnum=BLUEPILL_F103CB',
'stm32-blackpill-f401': 'STMicroelectronics:stm32:GenF4:pnum=BLACKPILL_F401CE',
'stm32-f4-discovery': 'STMicroelectronics:stm32:Disco:pnum=DISCO_F407VG',
'stm32-olimex-h405': 'STMicroelectronics:stm32:GenF4:pnum=GENERIC_F405RGTX',
'stm32-netduino-plus2': 'STMicroelectronics:stm32:GenF4:pnum=GENERIC_F405RGTX',
'stm32-netduino2': 'STMicroelectronics:stm32:GenF2:pnum=GENERIC_F205RGTX',
attiny85: 'ATTinyCore:avr:attinyx5:chip=85,clock=16pll',
};

View File

@ -228,6 +228,13 @@ export const BOARD_COMPONENT_IDS = [
'xiao-esp32-c3',
'aitewinrobot-esp32c3-supermini',
'stm32-bluepill',
'stm32-blackpill',
'stm32-bluepill-f103cb',
'stm32-blackpill-f401',
'stm32-f4-discovery',
'stm32-olimex-h405',
'stm32-netduino-plus2',
'stm32-netduino2',
'attiny85',
];