From 18c70e3e04732193ea46ce36232d045ea83266db Mon Sep 17 00:00:00 2001 From: Helmut Lord Date: Fri, 6 Feb 2026 08:02:13 -0500 Subject: [PATCH 1/8] Modularize script.js, add CI/CD, console UART, devkit loading, responsive UI Split monolithic 3849-line script.js into 14 ES modules in js/ directory. Add serial console UART selector with warnings, fix FLPR/NS console UART bugs (previously hardcoded uart30 or first-found). Add devkit config loading with overlay export mode. Add responsive CSS breakpoints, toast notification system, and accessibility focus-visible styles. Add GitHub Actions CI workflows for formatting, schema validation, smoke tests, and Zephyr build verification. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ci.yml | 22 + .github/workflows/zephyr-build.yml | 54 + .gitignore | 5 +- CLAUDE.md | 147 +- ISSUES.md | 22 + ci/extract-devkit-configs.js | 451 ++++ ci/generate-test-boards.js | 1080 ++++++++ ci/smoke-test.js | 328 +++ ci/validate-mcu-schemas.js | 186 ++ devkits/nrf54l15dk.json | 49 + devkits/nrf54lm20dk.json | 30 + index.html | 63 +- js/console-config.js | 72 + js/devicetree.js | 1104 ++++++++ js/devkit-loader.js | 148 ++ js/export.js | 342 +++ js/main.js | 204 ++ js/mcu-loader.js | 164 ++ js/peripherals.js | 601 +++++ js/pin-layout.js | 219 ++ js/state.js | 202 ++ js/ui/import-export.js | 229 ++ js/ui/modals.js | 739 ++++++ js/ui/notifications.js | 50 + js/ui/selected-list.js | 110 + js/utils.js | 52 + package-lock.json | 84 + package.json | 19 + script.js | 3849 ---------------------------- style.css | 197 +- 30 files changed, 6918 insertions(+), 3904 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/zephyr-build.yml create mode 100644 ISSUES.md create mode 100644 ci/extract-devkit-configs.js create mode 100644 ci/generate-test-boards.js create mode 100644 ci/smoke-test.js create mode 100644 ci/validate-mcu-schemas.js create mode 100644 devkits/nrf54l15dk.json create mode 100644 devkits/nrf54lm20dk.json create mode 100644 js/console-config.js create mode 100644 js/devicetree.js create mode 100644 js/devkit-loader.js create mode 100644 js/export.js create mode 100644 js/main.js create mode 100644 js/mcu-loader.js create mode 100644 js/peripherals.js create mode 100644 js/pin-layout.js create mode 100644 js/state.js create mode 100644 js/ui/import-export.js create mode 100644 js/ui/modals.js create mode 100644 js/ui/notifications.js create mode 100644 js/ui/selected-list.js create mode 100644 js/utils.js create mode 100644 package-lock.json create mode 100644 package.json delete mode 100644 script.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3667818 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: CI +on: + push: + branches: [main, dev] + pull_request: + branches: [main, dev] + +jobs: + lint-and-validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npm ci + - name: Check formatting + run: npx prettier --check . + - name: Validate MCU JSON schemas + run: node ci/validate-mcu-schemas.js + - name: Smoke test + run: node ci/smoke-test.js diff --git a/.github/workflows/zephyr-build.yml b/.github/workflows/zephyr-build.yml new file mode 100644 index 0000000..10a0f86 --- /dev/null +++ b/.github/workflows/zephyr-build.yml @@ -0,0 +1,54 @@ +name: Zephyr Build Test +on: + push: + branches: [main] + paths: ["js/devicetree.js", "js/export.js", "mcus/**"] + pull_request: + paths: ["js/devicetree.js", "js/export.js", "mcus/**"] + workflow_dispatch: + +jobs: + generate-boards: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: "20" } + - run: npm ci + - name: Generate test board definitions + run: node ci/generate-test-boards.js + - uses: actions/upload-artifact@v4 + with: + name: generated-boards + path: ci/output/boards/ + + build-test: + needs: generate-boards + runs-on: ubuntu-latest + container: + image: ghcr.io/nrfconnect/sdk-nrf:v2.9.0 + strategy: + fail-fast: false + matrix: + include: + - mcu: nrf54l15 + target: cpuapp + - mcu: nrf54l15 + target: cpuapp/ns + - mcu: nrf54l15 + target: cpuflpr + - mcu: nrf54l10 + target: cpuapp + - mcu: nrf54l05 + target: cpuapp + steps: + - uses: actions/download-artifact@v4 + with: + name: generated-boards + path: boards/ + - name: Install boards + run: cp -r boards/test_board_* $ZEPHYR_BASE/boards/custom/ || true + - name: Build hello_world + run: | + west build -b test_board_${{ matrix.mcu }}/${{ matrix.mcu }}/${{ matrix.target }} \ + $ZEPHYR_BASE/samples/hello_world --pristine always diff --git a/.gitignore b/.gitignore index 735293f..f2a4b98 100644 --- a/.gitignore +++ b/.gitignore @@ -140,7 +140,8 @@ vite.config.js.timestamp-* vite.config.ts.timestamp-* .vite/ -/package-lock.json -/package.json /bun.lock + +# CI output +ci/output/ diff --git a/CLAUDE.md b/CLAUDE.md index 7a83846..b0a53d3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,10 +10,31 @@ Nordic Pin Planner is an **unofficial** web-based tool for visualizing and plann ## Development Commands +### Install dependencies + +```bash +npm install +``` + ### Formatting ```bash -npx prettier --write . +npm run format # Auto-fix formatting +npm run format:check # Check formatting (CI) +``` + +### Validation & Testing + +```bash +npm run validate:schemas # Validate MCU JSON against mcuSchema.json +npm run smoke-test # Smoke test MCU data integrity +npm test # Run all checks (format + schema + smoke) +``` + +### Devkit Extraction (requires local Zephyr checkout) + +```bash +npm run extract-devkits -- --zephyr-path=/path/to/zephyr ``` ### Running the Application @@ -28,19 +49,42 @@ npx http-server ## Architecture -### Core Files Structure - -- **index.html**: Main application UI with modals for pin selection, oscillator config, and board info -- **script.js** (2573 lines): All application logic including state management, UI rendering, and export -- **style.css**: Complete styling including dark mode support -- **mcus/**: MCU package definitions and templates +### Module Structure (`js/`) + +The application uses native ES modules (` +
+ diff --git a/js/console-config.js b/js/console-config.js new file mode 100644 index 0000000..7c91a3c --- /dev/null +++ b/js/console-config.js @@ -0,0 +1,72 @@ +// --- SERIAL CONSOLE UART SELECTION AND WARNINGS --- + +import state from "./state.js"; +import { saveStateToLocalStorage } from "./state.js"; + +export function updateConsoleConfig() { + const section = document.getElementById("consoleConfigSection"); + if (!section) return; + + const banner = document.getElementById("consoleStatusBanner"); + const selectorDiv = document.getElementById("consoleSelector"); + const select = document.getElementById("consoleUartSelect"); + + if (!state.deviceTreeTemplates) { + section.style.display = "none"; + return; + } + + section.style.display = ""; + + // Find all selected UARTs + const selectedUarts = state.selectedPeripherals.filter((p) => { + const template = state.deviceTreeTemplates[p.id]; + return template && template.type === "UART"; + }); + + if (selectedUarts.length === 0) { + // No UARTs selected - show warning + banner.className = "console-banner console-warning"; + banner.innerHTML = + "No UART selected. RTT will be used for logging."; + banner.style.display = ""; + selectorDiv.style.display = "none"; + state.consoleUart = null; + } else if (selectedUarts.length === 1) { + // Exactly one UART - auto-select + const uart = selectedUarts[0]; + const template = state.deviceTreeTemplates[uart.id]; + state.consoleUart = uart.id; + banner.className = "console-banner console-info"; + banner.innerHTML = `UART console enabled on &${template.dtNodeName}`; + banner.style.display = ""; + selectorDiv.style.display = "none"; + } else { + // Multiple UARTs - show selector + banner.style.display = "none"; + selectorDiv.style.display = ""; + + // Preserve current selection if still valid + const currentValid = selectedUarts.some((u) => u.id === state.consoleUart); + if (!currentValid) { + state.consoleUart = selectedUarts[0].id; + } + + select.innerHTML = ""; + selectedUarts.forEach((uart) => { + const template = state.deviceTreeTemplates[uart.id]; + const option = document.createElement("option"); + option.value = uart.id; + option.textContent = `${uart.id} (&${template.dtNodeName})`; + if (uart.id === state.consoleUart) { + option.selected = true; + } + select.appendChild(option); + }); + } +} + +export function handleConsoleUartChange(event) { + state.consoleUart = event.target.value; + saveStateToLocalStorage(); +} diff --git a/js/devicetree.js b/js/devicetree.js new file mode 100644 index 0000000..1b02a1b --- /dev/null +++ b/js/devicetree.js @@ -0,0 +1,1104 @@ +// --- DEVICETREE GENERATION --- + +import state from "./state.js"; +import { parsePinName } from "./utils.js"; + +export function getMcuSupportsNonSecure(mcuId) { + const mcuInfo = state.mcuManifest.mcus.find((m) => m.id === mcuId); + return mcuInfo ? mcuInfo.supportsNonSecure === true : false; +} + +export function getMcuSupportsFLPR(mcuId) { + const mcuInfo = state.mcuManifest.mcus.find((m) => m.id === mcuId); + return mcuInfo ? mcuInfo.supportsFLPR === true : false; +} + +// Helper: get the DT node name for the selected console UART +function getConsoleUartNodeName() { + if (!state.consoleUart || !state.deviceTreeTemplates) return null; + const template = state.deviceTreeTemplates[state.consoleUart]; + return template ? template.dtNodeName : null; +} + +export function generatePinctrlFile() { + let content = `/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * SPDX-License-Identifier: Apache-2.0 + */ + +&pinctrl { +`; + + state.selectedPeripherals.forEach((p) => { + const template = state.deviceTreeTemplates[p.id]; + if (!template) { + return; + } + content += generatePinctrlForPeripheral(p, template); + }); + + content += "};\n"; + return content; +} + +export function generateCommonDtsi(mcu) { + let content = `/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "${state.boardInfo.name}_${mcu}-pinctrl.dtsi" + +`; + + state.selectedPeripherals.forEach((p) => { + if (p.config && p.config.loadCapacitors) return; + if (p.type === "GPIO") return; + + const template = state.deviceTreeTemplates[p.id]; + if (!template) return; + content += generatePeripheralNode(p, template); + }); + + const gpioPins = state.selectedPeripherals.filter((p) => p.type === "GPIO"); + if (gpioPins.length > 0) { + content += generateGpioNodes(gpioPins); + } + + return content; +} + +function generateGpioNodes(gpioPins) { + let content = "\n/ {\n"; + + gpioPins.forEach((gpio) => { + if (!gpio.pin) { + return; + } + + const pinInfo = parsePinName(gpio.pin); + if (!pinInfo) { + return; + } + + const activeFlag = + gpio.activeState === "active-low" + ? "GPIO_ACTIVE_LOW" + : "GPIO_ACTIVE_HIGH"; + + content += `\t${gpio.label}: ${gpio.label} {\n`; + content += `\t\tgpios = <&gpio${pinInfo.port} ${pinInfo.pin} ${activeFlag}>;\n`; + content += `\t};\n`; + }); + + content += "};\n"; + return content; +} + +export function generateCpuappCommonDtsi(mcu) { + let content = `/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* This file is common to the secure and non-secure domain */ + +#include "${state.boardInfo.name}_common.dtsi" + +/ { +\tchosen { +`; + + // Use state.consoleUart instead of first-found UART + const consoleNodeName = getConsoleUartNodeName(); + if (consoleNodeName) { + content += `\t\tzephyr,console = &${consoleNodeName};\n`; + content += `\t\tzephyr,shell-uart = &${consoleNodeName};\n`; + content += `\t\tzephyr,uart-mcumgr = &${consoleNodeName};\n`; + content += `\t\tzephyr,bt-mon-uart = &${consoleNodeName};\n`; + content += `\t\tzephyr,bt-c2h-uart = &${consoleNodeName};\n`; + } + + content += `\t\tzephyr,flash-controller = &rram_controller; +\t\tzephyr,flash = &cpuapp_rram; +\t\tzephyr,ieee802154 = &ieee802154; +\t\tzephyr,boot-mode = &boot_mode0; +\t}; +}; + +&cpuapp_sram { +\tstatus = "okay"; +}; +`; + + // Add LFXO configuration if enabled + const lfxo = state.selectedPeripherals.find((p) => p.id === "LFXO"); + if (lfxo && lfxo.config) { + content += ` +&lfxo { +\tload-capacitors = "${lfxo.config.loadCapacitors}";`; + if ( + lfxo.config.loadCapacitors === "internal" && + lfxo.config.loadCapacitanceFemtofarad + ) { + content += ` +\tload-capacitance-femtofarad = <${lfxo.config.loadCapacitanceFemtofarad}>;`; + } + content += ` +}; +`; + } + + // HFXO (always present) + const hfxo = state.selectedPeripherals.find((p) => p.id === "HFXO"); + const hfxoConfig = + hfxo && hfxo.config + ? hfxo.config + : { loadCapacitors: "internal", loadCapacitanceFemtofarad: 15000 }; + content += ` +&hfxo { +\tload-capacitors = "${hfxoConfig.loadCapacitors}";`; + if ( + hfxoConfig.loadCapacitors === "internal" && + hfxoConfig.loadCapacitanceFemtofarad + ) { + content += ` +\tload-capacitance-femtofarad = <${hfxoConfig.loadCapacitanceFemtofarad}>;`; + } + content += ` +}; +`; + + content += ` +®ulators { +\tstatus = "okay"; +}; + +&vregmain { +\tstatus = "okay"; +\tregulator-initial-mode = ; +}; + +&grtc { +\towned-channels = <0 1 2 3 4 5 6 7 8 9 10 11>; +\t/* Channels 7-11 reserved for Zero Latency IRQs, 3-4 for FLPR */ +\tchild-owned-channels = <3 4 7 8 9 10 11>; +\tstatus = "okay"; +}; + +&gpio0 { +\tstatus = "okay"; +}; + +&gpio1 { +\tstatus = "okay"; +}; + +&gpio2 { +\tstatus = "okay"; +}; + +&gpiote20 { +\tstatus = "okay"; +}; + +&gpiote30 { +\tstatus = "okay"; +}; + +&radio { +\tstatus = "okay"; +}; + +&ieee802154 { +\tstatus = "okay"; +}; + +&temp { +\tstatus = "okay"; +}; + +&clock { +\tstatus = "okay"; +}; + +&gpregret1 { +\tstatus = "okay"; + +\tboot_mode0: boot_mode@0 { +\t\tcompatible = "zephyr,retention"; +\t\tstatus = "okay"; +\t\treg = <0x0 0x1>; +\t}; +}; +`; + + // Check NFC usage + let nfcUsed = false; + state.selectedPeripherals.forEach((p) => { + const template = state.deviceTreeTemplates[p.id]; + if (template && template.type === "NFCT") { + nfcUsed = true; + } + }); + + if (!nfcUsed) { + content += ` +&uicr { +\tnfct-pins-as-gpios; +}; +`; + } + + return content; +} + +export function generateMainDts(mcu) { + const mcuUpper = mcu.toUpperCase().replace("NRF", "nRF"); + return `/dts-v1/; + +#include +#include "${mcu}_cpuapp_common.dtsi" + +/ { +\tcompatible = "${state.boardInfo.vendor},${state.boardInfo.name}-${mcu}-cpuapp"; +\tmodel = "${state.boardInfo.fullName} ${mcuUpper} Application MCU"; + +\tchosen { +\t\tzephyr,code-partition = &slot0_partition; +\t\tzephyr,sram = &cpuapp_sram; +\t}; +}; + +/* Include default memory partition configuration file */ +#include +`; +} + +export function generateYamlCapabilities(mcu, isNonSecure) { + const supportedFeatures = new Set(); + + state.selectedPeripherals.forEach((p) => { + const template = state.deviceTreeTemplates[p.id]; + if (template) { + switch (template.type) { + case "UART": + supportedFeatures.add("uart"); + break; + case "SPI": + supportedFeatures.add("spi"); + break; + case "I2C": + supportedFeatures.add("i2c"); + break; + case "PWM": + supportedFeatures.add("pwm"); + break; + case "ADC": + supportedFeatures.add("adc"); + break; + case "NFCT": + supportedFeatures.add("nfc"); + break; + } + } + }); + + supportedFeatures.add("gpio"); + supportedFeatures.add("watchdog"); + + const featuresArray = Array.from(supportedFeatures).sort(); + + const identifier = isNonSecure + ? `${state.boardInfo.name}/${mcu}/cpuapp/ns` + : `${state.boardInfo.name}/${mcu}/cpuapp`; + const name = isNonSecure + ? `${state.boardInfo.fullName}-Non-Secure` + : state.boardInfo.fullName; + const ram = isNonSecure ? 256 : 188; + const flash = isNonSecure ? 1524 : 1428; + + return `# Copyright (c) 2025 Generated by nRF54L Pin Planner +# SPDX-License-Identifier: Apache-2.0 + +identifier: ${identifier} +name: ${name} +type: mcu +arch: arm +toolchain: + - gnuarmemb + - zephyr +sysbuild: true +ram: ${ram} +flash: ${flash} +supported: +${featuresArray.map((f) => ` - ${f}`).join("\n")} +vendor: ${state.boardInfo.vendor} +`; +} + +export function generateDefconfig(isNonSecure) { + let config = `# Copyright (c) 2025 Generated by nRF54L Pin Planner +# SPDX-License-Identifier: Apache-2.0 + +`; + + if (isNonSecure) { + config += `CONFIG_ARM_MPU=y +CONFIG_HW_STACK_PROTECTION=y +CONFIG_NULL_POINTER_EXCEPTION_DETECTION_NONE=y +CONFIG_ARM_TRUSTZONE_M=y + +# This Board implies building Non-Secure firmware +CONFIG_TRUSTED_EXECUTION_NONSECURE=y + +# Don't enable the cache in the non-secure image as it is a +# secure-only peripheral on 54l +CONFIG_CACHE_MANAGEMENT=n +CONFIG_EXTERNAL_CACHE=n + +CONFIG_UART_CONSOLE=y +CONFIG_CONSOLE=y +CONFIG_SERIAL=y +CONFIG_GPIO=y + +# Start SYSCOUNTER on driver init +CONFIG_NRF_GRTC_START_SYSCOUNTER=y + +# Disable TFM BL2 since it is not supported +CONFIG_TFM_BL2=n + +# Support for silence logging is not supported at the moment +CONFIG_TFM_LOG_LEVEL_SILENCE=n + +# The oscillators are configured as secure and cannot be configured +# from the non secure application directly. This needs to be set +# otherwise nrfx will try to configure them, resulting in a bus +# fault. +CONFIG_SOC_NRF54LX_SKIP_CLOCK_CONFIG=y +`; + } else { + // Use state.consoleUart to check if UART console is enabled + const hasConsoleUart = state.consoleUart !== null; + + if (hasConsoleUart) { + config += `# Enable UART driver +CONFIG_SERIAL=y + +# Enable console +CONFIG_CONSOLE=y +CONFIG_UART_CONSOLE=y + +`; + } + + config += `# Enable GPIO +CONFIG_GPIO=y + +# Enable MPU +CONFIG_ARM_MPU=y + +# Enable hardware stack protection +CONFIG_HW_STACK_PROTECTION=y +`; + + const lfxoEnabled = state.selectedPeripherals.some((p) => p.id === "LFXO"); + if (!lfxoEnabled) { + config += ` +# Use RC oscillator for low-frequency clock +CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC=y +`; + } + } + + return config; +} + +export function generateNSDts(mcu) { + const mcuUpper = mcu.toUpperCase().replace("NRF", "nRF"); + + // Use state.consoleUart instead of first-found UART + const uartNodeName = getConsoleUartNodeName(); + + let uartDisableSection = ""; + if (uartNodeName) { + uartDisableSection = ` +&${uartNodeName} { +\t/* Disable so that TF-M can use this UART */ +\tstatus = "disabled"; + +\tcurrent-speed = <115200>; +\tpinctrl-0 = <&${uartNodeName.replace(/uart/, "uart")}_default>; +\tpinctrl-1 = <&${uartNodeName.replace(/uart/, "uart")}_sleep>; +\tpinctrl-names = "default", "sleep"; +}; + +`; + } + + return `/dts-v1/; + +#define USE_NON_SECURE_ADDRESS_MAP 1 + +#include +#include "${mcu}_cpuapp_common.dtsi" + +/ { +\tcompatible = "${state.boardInfo.vendor},${state.boardInfo.name}-${mcu}-cpuapp"; +\tmodel = "${state.boardInfo.fullName} ${mcuUpper} Application MCU"; + +\tchosen { +\t\tzephyr,code-partition = &slot0_ns_partition; +\t\tzephyr,sram = &sram0_ns; +\t\tzephyr,entropy = &psa_rng; +\t}; + +\t/delete-node/ rng; + +\tpsa_rng: psa-rng { +\t\tstatus = "okay"; +\t}; +}; + +/ { +\t/* +\t * Default SRAM planning when building for ${mcuUpper} with ARM TrustZone-M support +\t * - Lowest 80 kB SRAM allocated to Secure image (sram0_s). +\t * - Upper 80 kB SRAM allocated to Non-Secure image (sram0_ns). +\t * +\t * ${mcuUpper} has 256 kB of volatile memory (SRAM) but the last 96kB are reserved for +\t * the FLPR MCU. +\t * This static layout needs to be the same with the upstream TF-M layout in the +\t * header flash_layout.h of the relevant platform. Any updates in the layout +\t * needs to happen both in the flash_layout.h and in this file at the same time. +\t */ +\treserved-memory { +\t\t#address-cells = <1>; +\t\t#size-cells = <1>; +\t\tranges; + +\t\tsram0_s: image_s@20000000 { +\t\t\t/* Secure image memory */ +\t\t\treg = <0x20000000 DT_SIZE_K(80)>; +\t\t}; + +\t\tsram0_ns: image_ns@20014000 { +\t\t\t/* Non-Secure image memory */ +\t\t\treg = <0x20014000 DT_SIZE_K(80)>; +\t\t}; +\t}; +}; + +${uartDisableSection}/* Include default memory partition configuration file */ +#include +`; +} + +export function generateFLPRDts(mcu) { + const mcuUpper = mcu.toUpperCase().replace("NRF", "nRF"); + + // Use selected console UART instead of hardcoded uart30 + const consoleNodeName = getConsoleUartNodeName(); + let chosenUartLines = ""; + let uartStatusSection = ""; + + if (consoleNodeName) { + chosenUartLines = `\t\tzephyr,console = &${consoleNodeName};\n\t\tzephyr,shell-uart = &${consoleNodeName};\n`; + uartStatusSection = `\n&${consoleNodeName} {\n\tstatus = "okay";\n};\n`; + } + + return `/dts-v1/; +#include +#include "${state.boardInfo.name}_common.dtsi" + +/ { +\tmodel = "${state.boardInfo.fullName} ${mcuUpper} FLPR MCU"; +\tcompatible = "${state.boardInfo.vendor},${state.boardInfo.name}-${mcu}-cpuflpr"; + +\tchosen { +${chosenUartLines}\t\tzephyr,code-partition = &cpuflpr_code_partition; +\t\tzephyr,flash = &cpuflpr_rram; +\t\tzephyr,sram = &cpuflpr_sram; +\t}; +}; + +&cpuflpr_sram { +\tstatus = "okay"; +\t/* size must be increased due to booting from SRAM */ +\treg = <0x20028000 DT_SIZE_K(96)>; +\tranges = <0x0 0x20028000 0x18000>; +}; + +&cpuflpr_rram { +\tpartitions { +\t\tcompatible = "fixed-partitions"; +\t\t#address-cells = <1>; +\t\t#size-cells = <1>; + +\t\tcpuflpr_code_partition: partition@0 { +\t\t\tlabel = "image-0"; +\t\t\treg = <0x0 DT_SIZE_K(96)>; +\t\t}; +\t}; +}; + +&grtc { +\towned-channels = <3 4>; +\tstatus = "okay"; +}; +${uartStatusSection} +&gpio0 { +\tstatus = "okay"; +}; + +&gpio1 { +\tstatus = "okay"; +}; + +&gpio2 { +\tstatus = "okay"; +}; + +&gpiote20 { +\tstatus = "okay"; +}; + +&gpiote30 { +\tstatus = "okay"; +}; +`; +} + +export function generateFLPRXIPDts(mcu) { + return `/* + * Copyright (c) 2025 Generated by nRF54L Pin Planner + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "${state.boardInfo.name}_${mcu}_cpuflpr.dts" + +&cpuflpr_sram { +\treg = <0x2002f000 DT_SIZE_K(68)>; +\tranges = <0x0 0x2002f000 0x11000>; +}; +`; +} + +export function generateFLPRYaml(mcu, isXIP) { + const identifier = isXIP + ? `${state.boardInfo.name}/${mcu}/cpuflpr/xip` + : `${state.boardInfo.name}/${mcu}/cpuflpr`; + const name = isXIP + ? `${state.boardInfo.fullName}-Fast-Lightweight-Peripheral-Processor (RRAM XIP)` + : `${state.boardInfo.fullName}-Fast-Lightweight-Peripheral-Processor`; + const ram = isXIP ? 68 : 96; + + return `# Copyright (c) 2025 Generated by nRF54L Pin Planner +# SPDX-License-Identifier: Apache-2.0 + +identifier: ${identifier} +name: ${name} +type: mcu +arch: riscv +toolchain: + - zephyr +sysbuild: true +ram: ${ram} +flash: 96 +supported: + - counter + - gpio + - i2c + - spi + - watchdog +`; +} + +export function generateFLPRDefconfig(isXIP) { + // Only enable UART configs if a console UART is selected + const hasConsoleUart = state.consoleUart !== null; + + let config = `# Copyright (c) 2025 Generated by nRF54L Pin Planner +# SPDX-License-Identifier: Apache-2.0 + +`; + + if (hasConsoleUart) { + config += `# Enable UART driver +CONFIG_SERIAL=y + +# Enable console +CONFIG_CONSOLE=y +CONFIG_UART_CONSOLE=y + +`; + } + + config += `# Enable GPIO +CONFIG_GPIO=y + +${isXIP ? "# Execute from RRAM\nCONFIG_XIP=y" : "# Execute from SRAM\nCONFIG_USE_DT_CODE_PARTITION=y\nCONFIG_XIP=n"} +`; + + return config; +} + +export function generateBoardYml(mcu, supportsNS, supportsFLPR) { + let socSection = ` socs: + - name: ${mcu}`; + + if (supportsNS || supportsFLPR) { + socSection += ` + variants:`; + if (supportsFLPR) { + socSection += ` + - name: xip + cpucluster: cpuflpr`; + } + if (supportsNS) { + socSection += ` + - name: ns + cpucluster: cpuapp`; + } + } + + let boardsList = `${state.boardInfo.name}/${mcu}/cpuapp`; + if (supportsNS) { + boardsList += ` + - ${state.boardInfo.name}/${mcu}/cpuapp/ns`; + } + if (supportsFLPR) { + boardsList += ` + - ${state.boardInfo.name}/${mcu}/cpuflpr + - ${state.boardInfo.name}/${mcu}/cpuflpr/xip`; + } + + return `board: + name: ${state.boardInfo.name} + full_name: ${state.boardInfo.fullName} + vendor: ${state.boardInfo.vendor} +${socSection} +runners: + run_once: + '--recover': + - runners: + - nrfjprog + - nrfutil + run: first + groups: + - boards: + - ${boardsList} + '--erase': + - runners: + - nrfjprog + - jlink + - nrfutil + run: first + groups: + - boards: + - ${boardsList} + '--reset': + - runners: + - nrfjprog + - jlink + - nrfutil + run: last + groups: + - boards: + - ${boardsList} +`; +} + +export function generateBoardCmake(mcu, supportsNS, supportsFLPR) { + const mcuUpper = mcu.toUpperCase(); + const boardNameUpper = state.boardInfo.name.toUpperCase(); + + let content = `# Copyright (c) 2024 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +if(CONFIG_SOC_${mcuUpper}_CPUAPP) +\tboard_runner_args(jlink "--device=nRF${mcuUpper.substring(3)}_M33" "--speed=4000") +`; + + if (supportsFLPR) { + if (mcu === "nrf54l15") { + content += `elseif(CONFIG_SOC_${mcuUpper}_CPUFLPR) +\tboard_runner_args(jlink "--device=nRF${mcuUpper.substring(3)}_RV32") +`; + } else { + content += `elseif(CONFIG_SOC_${mcuUpper}_CPUFLPR) +\tset(JLINKSCRIPTFILE \${CMAKE_CURRENT_LIST_DIR}/support/${mcu}_cpuflpr.JLinkScript) +\tboard_runner_args(jlink "--device=RISC-V" "--speed=4000" "-if SW" "--tool-opt=-jlinkscriptfile \${JLINKSCRIPTFILE}") +`; + } + } + + content += `endif() + +`; + + if (supportsNS) { + content += `if(CONFIG_BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS) +\tset(TFM_PUBLIC_KEY_FORMAT "full") +endif() + +if(CONFIG_TFM_FLASH_MERGED_BINARY) +\tset_property(TARGET runners_yaml_props_target PROPERTY hex_file tfm_merged.hex) +endif() + +`; + } + + content += `include(\${ZEPHYR_BASE}/boards/common/nrfutil.board.cmake) +include(\${ZEPHYR_BASE}/boards/common/jlink.board.cmake) +`; + + return content; +} + +export function generateKconfigTrustZone(mcu) { + const boardNameUpper = state.boardInfo.name.toUpperCase(); + const mcuUpper = mcu.toUpperCase(); + return `# Copyright (c) 2025 Generated by nRF54L Pin Planner +# SPDX-License-Identifier: Apache-2.0 + +# ${state.boardInfo.fullName} board configuration + +if BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS + +DT_NRF_MPC := $(dt_nodelabel_path,nrf_mpc) + +config NRF_TRUSTZONE_FLASH_REGION_SIZE +\thex +\tdefault $(dt_node_int_prop_hex,$(DT_NRF_MPC),override-granularity) +\thelp +\t This defines the flash region size from the TrustZone perspective. +\t It is used when configuring the TrustZone and when setting alignments +\t requirements for the partitions. +\t This abstraction allows us to configure TrustZone without depending +\t on peripheral-specific symbols. + +config NRF_TRUSTZONE_RAM_REGION_SIZE +\thex +\tdefault $(dt_node_int_prop_hex,$(DT_NRF_MPC),override-granularity) +\thelp +\t This defines the RAM region size from the TrustZone perspective. +\t It is used when configuring the TrustZone and when setting alignments +\t requirements for the partitions. +\t This abstraction allows us to configure TrustZone without depending +\t on peripheral specific symbols. + +endif # BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS +`; +} + +export function generateKconfigDefconfig(mcu, supportsNS) { + const boardNameUpper = state.boardInfo.name.toUpperCase(); + const mcuUpper = mcu.toUpperCase(); + + let content = `# Copyright (c) 2024 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +config HW_STACK_PROTECTION +\tdefault ARCH_HAS_STACK_PROTECTION + +if BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP + +config ROM_START_OFFSET +\tdefault 0 if PARTITION_MANAGER_ENABLED +\tdefault 0x800 if BOOTLOADER_MCUBOOT + +endif # BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP +`; + + if (supportsNS) { + content += ` +if BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS + +config BOARD_${boardNameUpper} +\tselect USE_DT_CODE_PARTITION if BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS + +config BT_CTLR +\tdefault BT + +# By default, if we build for a Non-Secure version of the board, +# enable building with TF-M as the Secure Execution Environment. +config BUILD_WITH_TFM +\tdefault y + +endif # BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS +`; + } + + return content; +} + +export function generateKconfigBoard(mcu, supportsNS) { + const boardNameUpper = state.boardInfo.name.toUpperCase(); + const mcuUpper = mcu.toUpperCase(); + + let selectCondition = `BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP`; + if (supportsNS) { + selectCondition += ` || BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS`; + } + + return `# Copyright (c) 2025 Generated by nRF54L Pin Planner +# SPDX-License-Identifier: Apache-2.0 + +config BOARD_${boardNameUpper} +\tselect SOC_${mcuUpper}_CPUAPP if ${selectCondition} +`; +} + +export function generateReadme(mcu, pkg, supportsNS, supportsFLPR) { + let readme = `# ${state.boardInfo.fullName} + +**Generated by:** nRF54L Pin Planner +**MCU:** ${mcu.toUpperCase()} +**Package:** ${pkg} +${state.boardInfo.revision ? `**Revision:** ${state.boardInfo.revision}\n` : ""}${state.boardInfo.description ? `\n${state.boardInfo.description}\n` : ""} + +## Usage + +1. Copy this directory to your Zephyr boards directory: + \`\`\`bash + cp -r ${state.boardInfo.name} $ZEPHYR_BASE/boards/${state.boardInfo.vendor}/ + \`\`\` + +2. Build your application for this board: + \`\`\`bash + west build -b ${state.boardInfo.name}/${mcu}/cpuapp samples/hello_world + \`\`\` +`; + + if (supportsNS) { + readme += ` + Or build for Non-Secure target with TF-M: + \`\`\`bash + west build -b ${state.boardInfo.name}/${mcu}/cpuapp/ns samples/hello_world + \`\`\` +`; + } + + if (supportsFLPR) { + readme += ` + Or build for FLPR (Fast Lightweight Processor): + \`\`\`bash + west build -b ${state.boardInfo.name}/${mcu}/cpuflpr samples/hello_world + \`\`\` + + Or build for FLPR with XIP (Execute In Place from RRAM): + \`\`\`bash + west build -b ${state.boardInfo.name}/${mcu}/cpuflpr/xip samples/hello_world + \`\`\` +`; + } + + readme += ` +3. Flash to your device: + \`\`\`bash + west flash + \`\`\` + +## Selected Peripherals + +${state.selectedPeripherals + .map((p) => { + if (p.config) { + const capLabel = + p.config.loadCapacitors === "internal" ? "Internal" : "External"; + const oscData = state.mcuData.socPeripherals.find((sp) => sp.id === p.id); + let info = `${capLabel} capacitors`; + if ( + p.config.loadCapacitors === "internal" && + p.config.loadCapacitanceFemtofarad + ) { + info += `, ${(p.config.loadCapacitanceFemtofarad / 1000).toFixed(p.id === "HFXO" ? 2 : 1)} pF`; + } + if (oscData && oscData.signals && oscData.signals.length > 0) { + const pins = oscData.signals + .filter((s) => s.allowedGpio && s.allowedGpio.length > 0) + .map((s) => s.allowedGpio[0]) + .join(", "); + if (pins) { + info += ` (${pins})`; + } + } + return `- **${p.id}**: ${info}`; + } else if (p.pinFunctions) { + const pins = Object.entries(p.pinFunctions) + .map(([pin, func]) => `${pin}: ${func}`) + .join(", "); + return `- **${p.id}**: ${pins}`; + } else { + return `- **${p.id}**`; + } + }) + .join("\n")} + +## Pin Configuration + +See \`${state.boardInfo.name}_${mcu}-pinctrl.dtsi\` for complete pin mapping. + +## Notes + +- This is a generated board definition. Verify pin assignments match your hardware. +- Modify \`${state.boardInfo.name}_common.dtsi\` to add additional peripherals or features. +- Consult the [nRF Connect SDK documentation](https://docs.nordicsemi.com/) for more information. +`; + + return readme; +} + +export function generatePinctrlForPeripheral(peripheral, template) { + if (template.noPinctrl) { + return ""; + } + + const pinctrlName = template.pinctrlBaseName; + let content = `\n\t/omit-if-no-ref/ ${pinctrlName}_default: ${pinctrlName}_default {\n`; + + const outputSignals = []; + const inputSignals = []; + + for (const [pinName, signalName] of Object.entries(peripheral.pinFunctions)) { + const pinInfo = parsePinName(pinName); + if (!pinInfo) continue; + + const dtSignalName = template.signalMappings[signalName]; + if (!dtSignalName) { + continue; + } + + const signal = peripheral.peripheral.signals.find( + (s) => s.name === signalName, + ); + if (signal && signal.direction === "input") { + inputSignals.push({ pinInfo, dtSignalName }); + } else { + outputSignals.push({ pinInfo, dtSignalName }); + } + } + + const allSignals = [...outputSignals, ...inputSignals]; + + if (allSignals.length === 0) { + return ""; + } + + if (outputSignals.length > 0) { + content += `\t\tgroup1 {\n\t\t\tpsels = `; + content += outputSignals + .map( + (s) => + ``, + ) + .join(",\n\t\t\t\t"); + content += `;\n\t\t};\n`; + } + + if (inputSignals.length > 0) { + content += `\n\t\tgroup2 {\n\t\t\tpsels = `; + content += inputSignals + .map( + (s) => + ``, + ) + .join(",\n\t\t\t\t"); + content += `;\n\t\t\tbias-pull-up;\n\t\t};\n`; + } + + content += `\t};\n`; + + content += `\n\t/omit-if-no-ref/ ${pinctrlName}_sleep: ${pinctrlName}_sleep {\n`; + content += `\t\tgroup1 {\n\t\t\tpsels = `; + + content += allSignals + .map( + (s) => + ``, + ) + .join(",\n\t\t\t\t"); + content += `;\n\t\t\tlow-power-enable;\n\t\t};\n`; + content += `\t};\n`; + + return content; +} + +export function generatePeripheralNode(peripheral, template) { + const nodeName = template.dtNodeName; + const pinctrlName = template.pinctrlBaseName; + + let content = `\n&${nodeName} {\n`; + content += `\tstatus = "okay";\n`; + + if (!template.noPinctrl && pinctrlName) { + content += `\tpinctrl-0 = <&${pinctrlName}_default>;\n`; + content += `\tpinctrl-1 = <&${pinctrlName}_sleep>;\n`; + content += `\tpinctrl-names = "default", "sleep";\n`; + } + + switch (template.type) { + case "UART": + content += `\tcurrent-speed = <115200>;\n`; + if (peripheral.config && peripheral.config.disableRx) { + content += `\tdisable-rx;\n`; + } + break; + case "SPI": + if (template.outOfBandSignals) { + template.outOfBandSignals.forEach((signal) => { + const pin = Object.keys(peripheral.pinFunctions).find( + (p) => peripheral.pinFunctions[p] === signal, + ); + if (pin) { + const pinInfo = parsePinName(pin); + if (pinInfo) { + content += `\t/* ${signal} pin: P${pinInfo.port}.${pinInfo.pin} */\n`; + } + } + }); + } + + const csGpioEntries = []; + + const csPin = Object.keys(peripheral.pinFunctions).find( + (pin) => peripheral.pinFunctions[pin] === "CS", + ); + if (csPin) { + const csPinInfo = parsePinName(csPin); + if (csPinInfo) { + csGpioEntries.push( + `<&gpio${csPinInfo.port} ${csPinInfo.pin} GPIO_ACTIVE_LOW>`, + ); + } + } + + if (peripheral.config && peripheral.config.extraCsGpios) { + peripheral.config.extraCsGpios.forEach((gpio) => { + const pinInfo = parsePinName(gpio); + if (pinInfo) { + csGpioEntries.push( + `<&gpio${pinInfo.port} ${pinInfo.pin} GPIO_ACTIVE_LOW>`, + ); + } + }); + } + + if (csGpioEntries.length > 0) { + if (csGpioEntries.length === 1) { + content += `\tcs-gpios = ${csGpioEntries[0]};\n`; + } else { + content += `\tcs-gpios = ${csGpioEntries.join(",\n\t\t ")};\n`; + } + } + break; + case "I2C": + content += `\tclock-frequency = ;\n`; + break; + } + + content += `};\n`; + return content; +} diff --git a/js/devkit-loader.js b/js/devkit-loader.js new file mode 100644 index 0000000..ad86267 --- /dev/null +++ b/js/devkit-loader.js @@ -0,0 +1,148 @@ +// --- DEVKIT CONFIGURATION LOADING --- + +import state from "./state.js"; +import { applyConfig, saveStateToLocalStorage, resetState } from "./state.js"; +import { reinitializeView, handleMcuChange } from "./mcu-loader.js"; +import { organizePeripherals } from "./peripherals.js"; +import { updatePinDisplay } from "./pin-layout.js"; +import { updateSelectedPeripheralsList } from "./ui/selected-list.js"; +import { updateConsoleConfig } from "./console-config.js"; +import { showToast } from "./ui/notifications.js"; + +export async function loadDevkitConfig(boardName) { + if (!boardName) { + // Reset to custom board mode + state.devkitConfig = null; + const notice = document.querySelector(".devkit-eval-notice"); + if (notice) notice.style.display = "none"; + const versionEl = document.getElementById("devkitZephyrVersion"); + if (versionEl) versionEl.style.display = "none"; + return; + } + + try { + const response = await fetch(`devkits/${boardName}.json`); + if (!response.ok) { + throw new Error(`Devkit config not found: ${boardName}`); + } + const devkitData = await response.json(); + state.devkitConfig = devkitData; + + // Show evaluation notice and version + const notice = document.querySelector(".devkit-eval-notice"); + if (notice) notice.style.display = ""; + const versionEl = document.getElementById("devkitZephyrVersion"); + if (versionEl) { + versionEl.textContent = `Based on Zephyr v${devkitData.zephyrVersion}`; + versionEl.style.display = ""; + } + + // Auto-select matching MCU/package if needed + if (devkitData.supportedMcus && devkitData.supportedMcus.length > 0) { + const mcuSelector = document.getElementById("mcuSelector"); + const currentMcu = mcuSelector.value; + if (!devkitData.supportedMcus.includes(currentMcu)) { + mcuSelector.value = devkitData.supportedMcus[0]; + await handleMcuChange(); + } + } + + if (devkitData.package) { + const packageSelector = document.getElementById("packageSelector"); + const pkgOption = Array.from(packageSelector.options).find( + (opt) => opt.value === devkitData.package, + ); + if (pkgOption && packageSelector.value !== devkitData.package) { + packageSelector.value = devkitData.package; + // Trigger reload + const { loadCurrentMcuData } = await import("./mcu-loader.js"); + await loadCurrentMcuData(); + } + } + + // Apply devkit peripherals + applyDevkitConfig(devkitData); + + showToast(`Loaded ${devkitData.description} configuration`, "info"); + } catch (error) { + console.error("Failed to load devkit config:", error); + showToast(`Failed to load devkit config: ${error.message}`, "warning"); + state.devkitConfig = null; + } +} + +function applyDevkitConfig(devkitData) { + // Build a config object compatible with applyConfig + const peripheralConfigs = []; + + if (devkitData.peripherals) { + devkitData.peripherals.forEach((p) => { + const pinFunctions = {}; + if (p.signals) { + Object.entries(p.signals).forEach(([signalName, pinName]) => { + pinFunctions[pinName] = signalName; + }); + } + peripheralConfigs.push({ + id: p.id, + pinFunctions, + }); + + // Set console UART + if (p.isConsole) { + state.consoleUart = p.id; + } + }); + } + + // Apply oscillator configs + if (devkitData.oscillators) { + if (devkitData.oscillators.hfxo) { + peripheralConfigs.push({ + id: "HFXO", + config: devkitData.oscillators.hfxo, + }); + } + if (devkitData.oscillators.lfxo) { + peripheralConfigs.push({ + id: "LFXO", + config: devkitData.oscillators.lfxo, + }); + } + } + + applyConfig({ selectedPeripherals: peripheralConfigs }); + + // Mark devkit GPIO pins + if (devkitData.gpios) { + devkitData.gpios.forEach((gpio) => { + state.selectedPeripherals.push({ + id: `GPIO_${gpio.label.toUpperCase()}`, + type: "GPIO", + label: gpio.label, + pin: gpio.pin, + activeState: gpio.activeState, + }); + state.usedPins[gpio.pin] = { + peripheral: `GPIO_${gpio.label.toUpperCase()}`, + function: "GPIO", + required: true, + isDevkit: true, + }; + }); + } + + organizePeripherals(); + updateSelectedPeripheralsList(); + updatePinDisplay(); + updateConsoleConfig(); + saveStateToLocalStorage(); +} + +export function isDevkitMode() { + return state.devkitConfig !== null; +} + +export function getDevkitConfig() { + return state.devkitConfig; +} diff --git a/js/export.js b/js/export.js new file mode 100644 index 0000000..f169765 --- /dev/null +++ b/js/export.js @@ -0,0 +1,342 @@ +// --- BOARD DEFINITION EXPORT --- + +import state from "./state.js"; +import { isDevkitMode, getDevkitConfig } from "./devkit-loader.js"; +import { + getMcuSupportsNonSecure, + getMcuSupportsFLPR, + generateBoardYml, + generateBoardCmake, + generateKconfigDefconfig, + generateKconfigBoard, + generateCommonDtsi, + generateCpuappCommonDtsi, + generatePinctrlFile, + generateMainDts, + generateYamlCapabilities, + generateDefconfig, + generateReadme, + generateKconfigTrustZone, + generateNSDts, + generateFLPRDts, + generateFLPRYaml, + generateFLPRDefconfig, + generateFLPRXIPDts, + generatePinctrlForPeripheral, + generatePeripheralNode, +} from "./devicetree.js"; +import { parsePinName } from "./utils.js"; +import { showToast } from "./ui/notifications.js"; + +export function openBoardInfoModal() { + if (state.selectedPeripherals.length === 0) { + alert("No peripherals selected. Please select peripherals first."); + return; + } + + // In devkit mode, generate overlay instead + if (isDevkitMode()) { + exportOverlay(); + return; + } + + setupBoardNameValidation(); + document.getElementById("boardInfoModal").style.display = "block"; + document.getElementById("boardInfoError").style.display = "none"; +} + +function setupBoardNameValidation() { + const boardNameInput = document.getElementById("boardNameInput"); + const boardVendorInput = document.getElementById("boardVendorInput"); + + let boardNameError = document.getElementById("boardNameInputError"); + if (!boardNameError) { + boardNameError = document.createElement("small"); + boardNameError.id = "boardNameInputError"; + boardNameError.style.color = "var(--error-color)"; + boardNameError.style.display = "none"; + boardNameError.style.marginTop = "4px"; + boardNameInput.parentElement.appendChild(boardNameError); + } + + let vendorError = document.getElementById("boardVendorInputError"); + if (!vendorError) { + vendorError = document.createElement("small"); + vendorError.id = "boardVendorInputError"; + vendorError.style.color = "var(--error-color)"; + vendorError.style.display = "none"; + vendorError.style.marginTop = "4px"; + boardVendorInput.parentElement.appendChild(vendorError); + } + + const validateInput = (input, errorElement) => { + const pattern = /^[a-z0-9_]+$/; + const value = input.value.trim(); + + if (value && !pattern.test(value)) { + errorElement.textContent = + "Only lowercase letters, numbers, and underscores allowed"; + errorElement.style.display = "block"; + input.style.borderColor = "var(--error-color)"; + return false; + } else { + errorElement.style.display = "none"; + input.style.borderColor = "var(--border-color)"; + return true; + } + }; + + boardNameInput.addEventListener("input", () => + validateInput(boardNameInput, boardNameError), + ); + boardVendorInput.addEventListener("input", () => + validateInput(boardVendorInput, vendorError), + ); +} + +export function closeBoardInfoModal() { + document.getElementById("boardInfoModal").style.display = "none"; +} + +function validateBoardName(name) { + return /^[a-z0-9_]+$/.test(name); +} + +export async function confirmBoardInfoAndGenerate() { + const boardName = document.getElementById("boardNameInput").value.trim(); + const fullName = document.getElementById("boardFullNameInput").value.trim(); + const vendor = + document.getElementById("boardVendorInput").value.trim() || "custom"; + const revision = document.getElementById("boardRevisionInput").value.trim(); + const description = document + .getElementById("boardDescriptionInput") + .value.trim(); + + const errorElement = document.getElementById("boardInfoError"); + + if (!boardName) { + errorElement.textContent = "Board name is required."; + errorElement.style.display = "block"; + return; + } + + if (!validateBoardName(boardName)) { + errorElement.textContent = + "Board name must contain only lowercase letters, numbers, and underscores."; + errorElement.style.display = "block"; + return; + } + + if (!fullName) { + errorElement.textContent = "Full board name is required."; + errorElement.style.display = "block"; + return; + } + + if (vendor && !validateBoardName(vendor)) { + errorElement.textContent = + "Vendor name must contain only lowercase letters, numbers, and underscores."; + errorElement.style.display = "block"; + return; + } + + state.boardInfo = { + name: boardName, + fullName: fullName, + vendor: vendor, + revision: revision, + description: description, + }; + + closeBoardInfoModal(); + await exportBoardDefinition(); +} + +async function exportBoardDefinition() { + const mcu = document.getElementById("mcuSelector").value; + const pkg = document.getElementById("packageSelector").value; + + if (!state.deviceTreeTemplates) { + const { loadDeviceTreeTemplates } = await import("./mcu-loader.js"); + state.deviceTreeTemplates = await loadDeviceTreeTemplates(mcu); + if (!state.deviceTreeTemplates) { + alert("DeviceTree templates not available for this MCU yet."); + return; + } + } + + try { + const files = await generateBoardFiles(mcu, pkg); + await downloadBoardAsZip(files, state.boardInfo.name); + showToast("Board definition exported successfully!", "info"); + } catch (error) { + console.error("Board definition generation failed:", error); + alert(`Failed to generate board definition: ${error.message}`); + } +} + +async function generateBoardFiles(mcu, pkg) { + const supportsNS = getMcuSupportsNonSecure(mcu); + const supportsFLPR = getMcuSupportsFLPR(mcu); + const files = {}; + + files["board.yml"] = generateBoardYml(mcu, supportsNS, supportsFLPR); + files["board.cmake"] = generateBoardCmake(mcu, supportsNS, supportsFLPR); + files["Kconfig.defconfig"] = generateKconfigDefconfig(mcu, supportsNS); + files[`Kconfig.${state.boardInfo.name}`] = generateKconfigBoard( + mcu, + supportsNS, + ); + files[`${state.boardInfo.name}_common.dtsi`] = generateCommonDtsi(mcu); + files[`${mcu}_cpuapp_common.dtsi`] = generateCpuappCommonDtsi(mcu); + files[`${state.boardInfo.name}_${mcu}-pinctrl.dtsi`] = generatePinctrlFile(); + files[`${state.boardInfo.name}_${mcu}_cpuapp.dts`] = generateMainDts(mcu); + files[`${state.boardInfo.name}_${mcu}_cpuapp.yaml`] = + generateYamlCapabilities(mcu, false); + files[`${state.boardInfo.name}_${mcu}_cpuapp_defconfig`] = + generateDefconfig(false); + files["README.md"] = generateReadme(mcu, pkg, supportsNS, supportsFLPR); + + if (supportsNS) { + files["Kconfig"] = generateKconfigTrustZone(mcu); + files[`${state.boardInfo.name}_${mcu}_cpuapp_ns.dts`] = generateNSDts(mcu); + files[`${state.boardInfo.name}_${mcu}_cpuapp_ns.yaml`] = + generateYamlCapabilities(mcu, true); + files[`${state.boardInfo.name}_${mcu}_cpuapp_ns_defconfig`] = + generateDefconfig(true); + } + + if (supportsFLPR) { + files[`${state.boardInfo.name}_${mcu}_cpuflpr.dts`] = generateFLPRDts(mcu); + files[`${state.boardInfo.name}_${mcu}_cpuflpr.yaml`] = generateFLPRYaml( + mcu, + false, + ); + files[`${state.boardInfo.name}_${mcu}_cpuflpr_defconfig`] = + generateFLPRDefconfig(false); + files[`${state.boardInfo.name}_${mcu}_cpuflpr_xip.dts`] = + generateFLPRXIPDts(mcu); + files[`${state.boardInfo.name}_${mcu}_cpuflpr_xip.yaml`] = generateFLPRYaml( + mcu, + true, + ); + files[`${state.boardInfo.name}_${mcu}_cpuflpr_xip_defconfig`] = + generateFLPRDefconfig(true); + } + + return files; +} + +async function downloadBoardAsZip(files, boardName) { + if (typeof JSZip === "undefined") { + const script = document.createElement("script"); + script.src = + "https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"; + await new Promise((resolve) => { + script.onload = resolve; + document.head.appendChild(script); + }); + } + + const zip = new JSZip(); + const boardFolder = zip.folder(boardName); + + const stableDate = new Date(2024, 0, 1, 12, 0, 0); + + for (const [filename, content] of Object.entries(files)) { + boardFolder.file(filename, content, { date: stableDate }); + } + + const blob = await zip.generateAsync({ + type: "blob", + compression: "DEFLATE", + compressionOptions: { level: 9 }, + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${boardName}.zip`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +// --- OVERLAY EXPORT MODE --- + +function exportOverlay() { + const devkit = getDevkitConfig(); + if (!devkit) return; + + const mcu = document.getElementById("mcuSelector").value; + + let overlayContent = `/* + * EVALUATION ONLY - Generated by nRF54L Pin Planner + * Base board: ${devkit.board} (Zephyr v${devkit.zephyrVersion}) + * + * This overlay is for evaluation and prototyping purposes. + * Using overlays for pin reassignment is not recommended for + * production use. For production, create a proper custom board + * definition based on your hardware design. + */ + +`; + + // Generate overlay nodes for peripherals that differ from devkit base + const devkitPeripheralIds = new Set( + (devkit.peripherals || []).map((p) => p.id), + ); + + state.selectedPeripherals.forEach((p) => { + if (p.type === "GPIO") return; + if (p.config && p.config.loadCapacitors) return; + + // Only emit nodes that are new (not in devkit base) + if (!devkitPeripheralIds.has(p.id)) { + const template = state.deviceTreeTemplates[p.id]; + if (template) { + overlayContent += generatePeripheralNode(p, template); + } + } + }); + + // Check if we need a pinctrl overlay + let pinctrlContent = ""; + state.selectedPeripherals.forEach((p) => { + if (p.type === "GPIO") return; + if (p.config && p.config.loadCapacitors) return; + if (!devkitPeripheralIds.has(p.id)) { + const template = state.deviceTreeTemplates[p.id]; + if (template) { + pinctrlContent += generatePinctrlForPeripheral(p, template); + } + } + }); + + // Download overlay file + const blob = new Blob([overlayContent], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${devkit.board}.overlay`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + // If there's pinctrl content, also download pinctrl file + if (pinctrlContent.trim()) { + const pinctrlFull = `&pinctrl {\n${pinctrlContent}};\n`; + const pinctrlBlob = new Blob([pinctrlFull], { type: "text/plain" }); + const pinctrlUrl = URL.createObjectURL(pinctrlBlob); + const pinctrlA = document.createElement("a"); + pinctrlA.href = pinctrlUrl; + pinctrlA.download = `${devkit.board}-pinctrl.dtsi`; + document.body.appendChild(pinctrlA); + pinctrlA.click(); + document.body.removeChild(pinctrlA); + URL.revokeObjectURL(pinctrlUrl); + } + + showToast("Overlay exported for evaluation", "info"); +} diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..df7af3e --- /dev/null +++ b/js/main.js @@ -0,0 +1,204 @@ +// --- ENTRY POINT --- + +import { + initializeApp, + handleMcuChange, + handlePackageChange, +} from "./mcu-loader.js"; +import { + filterPeripherals, + closeOscillatorConfig, + confirmOscillatorConfig, +} from "./peripherals.js"; +import { + closePinSelectionModal, + confirmPinSelection, + closeGpioModal, + confirmGpioModal, + addGpioTableRowPublic, + addSpiCsGpio, +} from "./ui/modals.js"; +import { + openExportConfigModal, + openImportConfigModal, + handleImportConfigFile, + closeImportExportModal, + confirmImportExport, +} from "./ui/import-export.js"; +import { + openBoardInfoModal, + closeBoardInfoModal, + confirmBoardInfoAndGenerate, +} from "./export.js"; +import { handleConsoleUartChange } from "./console-config.js"; +import { loadDevkitConfig } from "./devkit-loader.js"; +import { enableScrollWheelSelection } from "./utils.js"; +import state from "./state.js"; +import { resetState, saveStateToLocalStorage } from "./state.js"; +import { organizePeripherals } from "./peripherals.js"; +import { updatePinDisplay } from "./pin-layout.js"; +import { updateSelectedPeripheralsList } from "./ui/selected-list.js"; +import { updateConsoleConfig } from "./console-config.js"; + +document.addEventListener("DOMContentLoaded", function () { + // Set up event listeners + document + .getElementById("mcuSelector") + .addEventListener("change", handleMcuChange); + document + .getElementById("packageSelector") + .addEventListener("change", handlePackageChange); + document + .getElementById("clearAllBtn") + .addEventListener("click", clearAllPeripherals); + document + .getElementById("exportDeviceTreeBtn") + .addEventListener("click", openBoardInfoModal); + document + .getElementById("searchPeripherals") + .addEventListener("input", filterPeripherals); + document + .querySelector("#pinSelectionModal .close") + .addEventListener("click", closePinSelectionModal); + document + .getElementById("cancelPinSelection") + .addEventListener("click", closePinSelectionModal); + document + .getElementById("confirmPinSelection") + .addEventListener("click", confirmPinSelection); + document + .getElementById("closeBoardInfoModal") + .addEventListener("click", closeBoardInfoModal); + document + .getElementById("cancelBoardInfo") + .addEventListener("click", closeBoardInfoModal); + document + .getElementById("confirmBoardInfo") + .addEventListener("click", confirmBoardInfoAndGenerate); + document + .getElementById("closeOscillatorModal") + .addEventListener("click", closeOscillatorConfig); + document + .getElementById("cancelOscillatorConfig") + .addEventListener("click", closeOscillatorConfig); + document + .getElementById("confirmOscillatorConfig") + .addEventListener("click", confirmOscillatorConfig); + + // Import/Export config listeners + document + .getElementById("exportConfigBtn") + .addEventListener("click", openExportConfigModal); + document + .getElementById("importConfigBtn") + .addEventListener("click", openImportConfigModal); + document + .getElementById("importConfigFile") + .addEventListener("change", handleImportConfigFile); + document + .getElementById("closeImportExportModal") + .addEventListener("click", closeImportExportModal); + document + .getElementById("cancelImportExport") + .addEventListener("click", closeImportExportModal); + document + .getElementById("confirmImportExport") + .addEventListener("click", confirmImportExport); + + // GPIO modal listeners + document + .getElementById("closeGpioModal") + .addEventListener("click", closeGpioModal); + document + .getElementById("cancelGpioModal") + .addEventListener("click", closeGpioModal); + document + .getElementById("confirmGpioModal") + .addEventListener("click", confirmGpioModal); + document + .getElementById("addGpioRow") + .addEventListener("click", addGpioTableRowPublic); + document.getElementById("gpioModal").addEventListener("click", (e) => { + if (e.target === document.getElementById("gpioModal")) { + closeGpioModal(); + } + }); + + // SPI CS GPIO button + document + .getElementById("addSpiCsGpioBtn") + .addEventListener("click", addSpiCsGpio); + + // Console UART selector + const consoleUartSelect = document.getElementById("consoleUartSelect"); + if (consoleUartSelect) { + consoleUartSelect.addEventListener("change", handleConsoleUartChange); + } + + // Devkit selector + const devkitSelector = document.getElementById("devkitSelector"); + if (devkitSelector) { + devkitSelector.addEventListener("change", (e) => { + loadDevkitConfig(e.target.value); + }); + } + + // --- THEME SWITCHER LOGIC --- + const themeToggle = document.getElementById("theme-toggle"); + const body = document.body; + + const setTheme = (isDark) => { + if (isDark) { + body.classList.add("dark-mode"); + themeToggle.checked = true; + localStorage.setItem("theme", "dark"); + } else { + body.classList.remove("dark-mode"); + themeToggle.checked = false; + localStorage.setItem("theme", "light"); + } + }; + + const savedTheme = localStorage.getItem("theme"); + const prefersDark = + window.matchMedia && + window.matchMedia("(prefers-color-scheme: dark)").matches; + + if (savedTheme) { + setTheme(savedTheme === "dark"); + } else { + setTheme(prefersDark); + } + + themeToggle.addEventListener("change", () => { + setTheme(themeToggle.checked); + }); + + window + .matchMedia("(prefers-color-scheme: dark)") + .addEventListener("change", (e) => { + const currentTheme = localStorage.getItem("theme"); + if (!currentTheme) { + setTheme(e.matches); + } + }); + + // Scroll-wheel selection on dropdowns + enableScrollWheelSelection("mcuSelector"); + enableScrollWheelSelection("packageSelector"); + + // Initial data load + initializeApp(); +}); + +function clearAllPeripherals() { + if (!confirm("Are you sure you want to clear all peripherals?")) { + return; + } + resetState(); + organizePeripherals(); + updateSelectedPeripheralsList(); + updatePinDisplay(); + updateConsoleConfig(); + saveStateToLocalStorage(); +} diff --git a/js/mcu-loader.js b/js/mcu-loader.js new file mode 100644 index 0000000..a0f79e0 --- /dev/null +++ b/js/mcu-loader.js @@ -0,0 +1,164 @@ +// --- MCU/PACKAGE LOADING AND INITIALIZATION --- + +import state from "./state.js"; +import { + resetState, + loadStateFromLocalStorage, + saveStateToLocalStorage, +} from "./state.js"; +import { + organizePeripherals, + addOscillatorsToPeripherals, + autoSelectHFXO, +} from "./peripherals.js"; +import { createPinLayout, updatePinDisplay } from "./pin-layout.js"; +import { updateSelectedPeripheralsList } from "./ui/selected-list.js"; +import { updateConsoleConfig } from "./console-config.js"; + +export async function initializeApp() { + try { + const response = await fetch("mcus/manifest.json"); + if (!response.ok) throw new Error("Manifest file not found."); + state.mcuManifest = await response.json(); + populateMcuSelector(); + } catch (error) { + console.error("Failed to initialize application:", error); + alert( + "Could not load MCU manifest. The application may not function correctly.", + ); + } +} + +export function populateMcuSelector() { + const mcuSelector = document.getElementById("mcuSelector"); + mcuSelector.innerHTML = ""; + state.mcuManifest.mcus.forEach((mcu) => { + const option = document.createElement("option"); + option.value = mcu.id; + option.textContent = mcu.name; + option.dataset.packages = JSON.stringify(mcu.packages); + mcuSelector.appendChild(option); + }); + handleMcuChange(); +} + +export async function handleMcuChange() { + const mcuSelector = document.getElementById("mcuSelector"); + const packageSelector = document.getElementById("packageSelector"); + const selectedMcuOption = mcuSelector.options[mcuSelector.selectedIndex]; + + if (!selectedMcuOption) return; + + const packages = JSON.parse(selectedMcuOption.dataset.packages || "[]"); + packageSelector.innerHTML = ""; + + if (packages.length > 0) { + packages.forEach((pkg) => { + const option = document.createElement("option"); + option.value = pkg.file; + option.textContent = pkg.name; + packageSelector.appendChild(option); + }); + await loadCurrentMcuData(); + } else { + reinitializeView(true); + } +} + +export async function handlePackageChange() { + await loadCurrentMcuData(); +} + +export async function loadCurrentMcuData() { + const mcu = document.getElementById("mcuSelector").value; + const pkg = document.getElementById("packageSelector").value; + if (mcu && pkg) { + await loadMCUData(mcu, pkg); + } +} + +export async function loadMCUData(mcu, pkg) { + const path = `mcus/${mcu}/${pkg}.json`; + try { + const response = await fetch(path); + if (!response.ok) { + throw new Error(`File not found or invalid: ${path}`); + } + state.mcuData = await response.json(); + + state.deviceTreeTemplates = await loadDeviceTreeTemplates(mcu); + + reinitializeView(); + } catch (error) { + console.error("Error loading MCU data:", error); + alert(`Could not load data for ${mcu} - ${pkg}.\n${error.message}`); + reinitializeView(true); + } +} + +export async function loadDeviceTreeTemplates(mcuId) { + try { + const response = await fetch(`mcus/${mcuId}/devicetree-templates.json`); + if (!response.ok) { + console.warn(`No DeviceTree templates found for ${mcuId}`); + return null; + } + const data = await response.json(); + return data.templates; + } catch (error) { + console.error("Failed to load DeviceTree templates:", error); + return null; + } +} + +export function reinitializeView(clearOnly = false) { + resetState(); + + if (clearOnly || !state.mcuData.partInfo) { + document.getElementById("chipTitleDisplay").textContent = "No MCU Loaded"; + organizePeripherals(); + createPinLayout(); + updateSelectedPeripheralsList(); + updatePinDisplay(); + updateConsoleConfig(); + return; + } + + addOscillatorsToPeripherals(); + autoSelectHFXO(); + + document.getElementById("chipTitleDisplay").textContent = + `${state.mcuData.partInfo.packageType} Pin Layout`; + organizePeripherals(); + createPinLayout(); + + loadStateFromLocalStorage(); + + // Ensure HFXO is always selected after loading state (and remove duplicates) + const hfxoCount = state.selectedPeripherals.filter( + (p) => p.id === "HFXO", + ).length; + if (hfxoCount === 0) { + const hfxo = state.mcuData.socPeripherals.find((p) => p.id === "HFXO"); + if (hfxo) { + state.selectedPeripherals.push({ + id: "HFXO", + description: hfxo.description, + config: { ...hfxo.config }, + }); + } + } else if (hfxoCount > 1) { + const firstHfxo = state.selectedPeripherals.find((p) => p.id === "HFXO"); + for (let i = state.selectedPeripherals.length - 1; i >= 0; i--) { + if (state.selectedPeripherals[i].id === "HFXO") { + state.selectedPeripherals.splice(i, 1); + } + } + state.selectedPeripherals.push(firstHfxo); + } + + organizePeripherals(); + updateSelectedPeripheralsList(); + updatePinDisplay(); + updateConsoleConfig(); +} diff --git a/js/peripherals.js b/js/peripherals.js new file mode 100644 index 0000000..3b9f151 --- /dev/null +++ b/js/peripherals.js @@ -0,0 +1,601 @@ +// --- PERIPHERAL ORGANIZATION AND DISPLAY --- + +import state from "./state.js"; +import { hasAddressConflict, saveStateToLocalStorage } from "./state.js"; +import { updatePinDisplay } from "./pin-layout.js"; +import { updateSelectedPeripheralsList } from "./ui/selected-list.js"; +import { openPinSelectionModal } from "./ui/modals.js"; +import { updateConsoleConfig } from "./console-config.js"; +import { enableScrollWheelSelectionForElement } from "./utils.js"; + +export function addOscillatorsToPeripherals() { + if (!state.mcuData.socPeripherals) { + state.mcuData.socPeripherals = []; + } + + const lfxoIndex = state.mcuData.socPeripherals.findIndex( + (p) => p.id === "LFXO", + ); + + if (lfxoIndex !== -1) { + const lfxo = state.mcuData.socPeripherals[lfxoIndex]; + lfxo.uiHint = "oscillator"; + lfxo.optional = true; + if (!lfxo.config) { + lfxo.config = { + loadCapacitors: "internal", + loadCapacitanceFemtofarad: 15000, + }; + } + } else { + state.mcuData.socPeripherals.push({ + id: "LFXO", + description: "Low Frequency Crystal Oscillator", + uiHint: "oscillator", + optional: true, + signals: [], + config: { + loadCapacitors: "internal", + loadCapacitanceFemtofarad: 15000, + }, + }); + } + + const hfxoIndex = state.mcuData.socPeripherals.findIndex( + (p) => p.id === "HFXO", + ); + + if (hfxoIndex !== -1) { + const hfxo = state.mcuData.socPeripherals[hfxoIndex]; + hfxo.uiHint = "oscillator"; + hfxo.optional = false; + hfxo.alwaysPresent = true; + if (!hfxo.config) { + hfxo.config = { + loadCapacitors: "internal", + loadCapacitanceFemtofarad: 15000, + }; + } + } else { + state.mcuData.socPeripherals.push({ + id: "HFXO", + description: "High Frequency Crystal Oscillator", + uiHint: "oscillator", + optional: false, + alwaysPresent: true, + signals: [], + config: { + loadCapacitors: "internal", + loadCapacitanceFemtofarad: 15000, + }, + }); + } +} + +export function autoSelectHFXO() { + const existingIndex = state.selectedPeripherals.findIndex( + (p) => p.id === "HFXO", + ); + if (existingIndex !== -1) { + state.selectedPeripherals.splice(existingIndex, 1); + } + + const hfxo = state.mcuData.socPeripherals.find((p) => p.id === "HFXO"); + if (hfxo) { + state.selectedPeripherals.push({ + id: "HFXO", + description: hfxo.description, + config: { ...hfxo.config }, + }); + updateSelectedPeripheralsList(); + } +} + +export function organizePeripherals() { + const peripheralsListContainer = document.getElementById("peripherals-list"); + if (!peripheralsListContainer) return; + peripheralsListContainer.innerHTML = ""; + + if (!state.mcuData.socPeripherals) return; + + const checkboxPeripherals = []; + const oscillators = []; + const singleInstancePeripherals = []; + const multiInstanceGroups = {}; + + state.mcuData.socPeripherals.forEach((p) => { + if (p.uiHint === "oscillator") { + oscillators.push(p); + } else if (p.uiHint === "checkbox") { + checkboxPeripherals.push(p); + } else { + const baseName = p.id.replace(/\d+$/, ""); + if (!multiInstanceGroups[baseName]) { + multiInstanceGroups[baseName] = []; + } + multiInstanceGroups[baseName].push(p); + } + }); + + for (const baseName in multiInstanceGroups) { + if (multiInstanceGroups[baseName].length === 1) { + singleInstancePeripherals.push(multiInstanceGroups[baseName][0]); + delete multiInstanceGroups[baseName]; + } + } + + oscillators.sort((a, b) => a.id.localeCompare(b.id)); + checkboxPeripherals.sort((a, b) => a.id.localeCompare(b.id)); + singleInstancePeripherals.sort((a, b) => a.id.localeCompare(b.id)); + const sortedMultiInstanceKeys = Object.keys(multiInstanceGroups).sort(); + + // Render oscillators + oscillators.forEach((p) => { + const oscGroup = document.createElement("div"); + oscGroup.className = "oscillator-group"; + oscGroup.style.marginBottom = "10px"; + + const btn = document.createElement("button"); + btn.className = "single-peripheral-btn"; + btn.dataset.id = p.id; + btn.style.width = "100%"; + + const isSelected = state.selectedPeripherals.some((sp) => sp.id === p.id); + if (isSelected) { + btn.classList.add("selected"); + } + + if (p.id === "HFXO") { + btn.textContent = `${p.description} (Configure)`; + btn.addEventListener("click", () => openOscillatorConfig(p)); + } else { + btn.textContent = isSelected + ? `${p.description} (Configure)` + : `${p.description} (Add)`; + btn.addEventListener("click", () => openOscillatorConfig(p)); + } + + oscGroup.appendChild(btn); + peripheralsListContainer.appendChild(oscGroup); + }); + + // Render checkbox peripherals + checkboxPeripherals.forEach((p) => { + const checkboxGroup = document.createElement("div"); + checkboxGroup.className = "checkbox-group"; + + const label = document.createElement("label"); + label.className = "checkbox-label"; + + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.id = `${p.id.toLowerCase()}-checkbox`; + checkbox.dataset.peripheralId = p.id; + checkbox.addEventListener("change", toggleSimplePeripheral); + + const span = document.createElement("span"); + span.textContent = p.description; + + label.appendChild(checkbox); + label.appendChild(span); + + const description = document.createElement("div"); + description.className = "checkbox-description"; + description.textContent = `Uses ${p.signals.map((s) => s.allowedGpio.join("/")).join(", ")}`; + + checkboxGroup.appendChild(label); + checkboxGroup.appendChild(description); + peripheralsListContainer.appendChild(checkboxGroup); + }); + + // Render single-instance peripherals + singleInstancePeripherals.forEach((p) => { + const btn = document.createElement("button"); + btn.className = "single-peripheral-btn"; + btn.dataset.id = p.id; + btn.textContent = `${p.id} (${p.type})`; + btn.addEventListener("click", () => handlePeripheralClick(p)); + peripheralsListContainer.appendChild(btn); + }); + + // Render multi-instance peripherals + if (sortedMultiInstanceKeys.length > 0) { + const accordionContainer = document.createElement("div"); + accordionContainer.className = "accordion"; + + sortedMultiInstanceKeys.forEach((baseName) => { + const peripherals = multiInstanceGroups[baseName]; + const accordionItem = document.createElement("div"); + accordionItem.className = "accordion-item"; + const header = document.createElement("div"); + header.className = "accordion-header"; + header.innerHTML = `${baseName}`; + const content = document.createElement("div"); + content.className = "accordion-content"; + + peripherals + .sort((a, b) => a.id.localeCompare(b.id)) + .forEach((p) => { + const item = document.createElement("div"); + item.className = "peripheral-item"; + item.dataset.id = p.id; + item.innerHTML = `${p.id}`; + item.addEventListener("click", () => handlePeripheralClick(p)); + content.appendChild(item); + }); + + header.addEventListener("click", () => { + const isActive = header.classList.toggle("active"); + content.style.display = isActive ? "block" : "none"; + }); + + accordionItem.appendChild(header); + accordionItem.appendChild(content); + accordionContainer.appendChild(accordionItem); + }); + peripheralsListContainer.appendChild(accordionContainer); + } + + // Add GPIO allocation section + const gpioSection = document.createElement("div"); + gpioSection.className = "gpio-section"; + gpioSection.style.marginTop = "20px"; + gpioSection.style.paddingTop = "20px"; + gpioSection.style.paddingBottom = "10px"; + gpioSection.style.borderTop = "1px solid var(--border-color)"; + + const gpioHeader = document.createElement("h4"); + gpioHeader.textContent = "GPIO Pins"; + gpioHeader.style.marginBottom = "10px"; + gpioSection.appendChild(gpioHeader); + + const addGpioBtn = document.createElement("button"); + addGpioBtn.className = "single-peripheral-btn"; + addGpioBtn.textContent = "+ Add GPIO Pin"; + addGpioBtn.style.width = "100%"; + // Import on demand to avoid circular dependency + addGpioBtn.addEventListener("click", () => { + import("./ui/modals.js").then((m) => m.openGpioModal()); + }); + gpioSection.appendChild(addGpioBtn); + + peripheralsListContainer.appendChild(gpioSection); +} + +export function handlePeripheralClick(peripheral) { + const isSelected = state.selectedPeripherals.some( + (p) => p.id === peripheral.id, + ); + if (isSelected) { + editPeripheral(peripheral.id); + } else if (!hasAddressConflict(peripheral)) { + openPinSelectionModal(peripheral); + } else { + alert( + `Cannot select ${peripheral.id} because it shares the same address space (${peripheral.baseAddress}) with another selected peripheral.`, + ); + } +} + +export function toggleSimplePeripheral(event) { + const checkbox = event.target; + const peripheralId = checkbox.dataset.peripheralId; + const peripheral = state.mcuData.socPeripherals.find( + (p) => p.id === peripheralId, + ); + + if (!peripheral) { + console.error( + `Peripheral with ID '${peripheralId}' not found in socPeripherals.`, + ); + return; + } + + const pinNames = peripheral.signals.map((s) => s.allowedGpio[0]); + + if (checkbox.checked) { + if (pinNames.some((pin) => state.usedPins[pin])) { + alert( + `One or more pins for ${peripheral.description} are already in use.`, + ); + checkbox.checked = false; + return; + } + const pinFunctions = {}; + peripheral.signals.forEach((s) => { + const pinName = s.allowedGpio[0]; + state.usedPins[pinName] = { + peripheral: peripheral.id, + function: s.name, + required: true, + }; + pinFunctions[pinName] = s.name; + }); + state.selectedPeripherals.push({ + id: peripheral.id, + peripheral, + pinFunctions, + }); + } else { + pinNames.forEach((pin) => delete state.usedPins[pin]); + const index = state.selectedPeripherals.findIndex( + (p) => p.id === peripheral.id, + ); + if (index !== -1) state.selectedPeripherals.splice(index, 1); + } + updateSelectedPeripheralsList(); + updatePinDisplay(); + updateConsoleConfig(); + saveStateToLocalStorage(); +} + +export function removePeripheral(id) { + const index = state.selectedPeripherals.findIndex((p) => p.id === id); + if (index === -1) return; + + const peripheral = state.selectedPeripherals[index]; + const peripheralData = state.mcuData.socPeripherals.find((p) => p.id === id); + + const checkbox = document.getElementById(`${id.toLowerCase()}-checkbox`); + if (checkbox) { + checkbox.checked = false; + } + + if (peripheral.type === "GPIO") { + if (peripheral.pin && state.usedPins[peripheral.pin]) { + delete state.usedPins[peripheral.pin]; + } + } else if (peripheralData && peripheralData.uiHint === "oscillator") { + if (peripheralData.signals && peripheralData.signals.length > 0) { + peripheralData.signals.forEach((s) => { + if (s.allowedGpio && s.allowedGpio.length > 0) { + const pinName = s.allowedGpio[0]; + if ( + state.usedPins[pinName] && + state.usedPins[pinName].peripheral === id + ) { + delete state.usedPins[pinName]; + } + } + }); + } + } else { + for (const pinName in peripheral.pinFunctions) { + delete state.usedPins[pinName]; + } + } + + if (peripheral.peripheral && peripheral.peripheral.baseAddress) { + delete state.usedAddresses[peripheral.peripheral.baseAddress]; + } + state.selectedPeripherals.splice(index, 1); + + updateSelectedPeripheralsList(); + organizePeripherals(); + updatePinDisplay(); + updateConsoleConfig(); + saveStateToLocalStorage(); +} + +export function editPeripheral(id) { + const gpioPeripheral = state.selectedPeripherals.find( + (p) => p.id === id && p.type === "GPIO", + ); + if (gpioPeripheral) { + import("./ui/modals.js").then((m) => m.openGpioModal()); + return; + } + + const peripheralData = state.mcuData.socPeripherals.find((p) => p.id === id); + if (!peripheralData) return; + + if (peripheralData.uiHint === "oscillator") { + openOscillatorConfig(peripheralData); + return; + } + + if (peripheralData.uiHint === "checkbox") { + return; + } + + const selected = state.selectedPeripherals.find((p) => p.id === id); + if (!selected) return; + openPinSelectionModal( + selected.peripheral, + selected.pinFunctions, + selected.config || {}, + ); +} + +export function filterPeripherals() { + const searchTerm = document + .getElementById("searchPeripherals") + .value.toLowerCase(); + const peripheralsList = document.getElementById("peripherals-list"); + + const items = peripheralsList.querySelectorAll( + ".single-peripheral-btn, .accordion-item, .checkbox-group", + ); + + items.forEach((item) => { + const text = item.textContent.toLowerCase(); + + let tags = []; + if (state.mcuData.socPeripherals) { + if (item.matches(".single-peripheral-btn")) { + const p = state.mcuData.socPeripherals.find( + (p) => p.id === item.dataset.id, + ); + if (p && p.tags) tags = p.tags; + } else if (item.matches(".checkbox-group")) { + const id = item.querySelector("[data-peripheral-id]").dataset + .peripheralId; + const p = state.mcuData.socPeripherals.find((p) => p.id === id); + if (p && p.tags) tags = p.tags; + } else if (item.matches(".accordion-item")) { + item.querySelectorAll(".peripheral-item").forEach((pItem) => { + const p = state.mcuData.socPeripherals.find( + (p) => p.id === pItem.dataset.id, + ); + if (p && p.tags) tags = tags.concat(p.tags); + }); + } + } + const tagsText = tags.join(" ").toLowerCase(); + + if (text.includes(searchTerm) || tagsText.includes(searchTerm)) { + item.style.display = ""; + } else { + item.style.display = "none"; + } + }); +} + +// --- OSCILLATOR CONFIGURATION --- + +let currentOscillator = null; + +export function openOscillatorConfig(oscillator) { + currentOscillator = oscillator; + + document.getElementById("oscillatorModalTitle").textContent = + `Configure ${oscillator.description}`; + + const existingConfig = state.selectedPeripherals.find( + (p) => p.id === oscillator.id, + ); + const config = existingConfig ? existingConfig.config : oscillator.config; + + const internalRadio = document.getElementById("oscillatorCapInternal"); + const externalRadio = document.getElementById("oscillatorCapExternal"); + + internalRadio.checked = config.loadCapacitors === "internal"; + externalRadio.checked = config.loadCapacitors === "external"; + + const loadCapSelect = document.getElementById("oscillatorLoadCapacitance"); + loadCapSelect.innerHTML = ""; + + const template = state.deviceTreeTemplates + ? state.deviceTreeTemplates[oscillator.id] + : null; + let min, max, step; + + if (template && template.loadCapacitanceRange) { + min = template.loadCapacitanceRange.min; + max = template.loadCapacitanceRange.max; + step = template.loadCapacitanceRange.step; + } else { + if (oscillator.id === "LFXO") { + min = 4000; + max = 18000; + step = 500; + } else { + min = 4000; + max = 17000; + step = 250; + } + } + + for (let i = min; i <= max; i += step) { + const option = document.createElement("option"); + option.value = i; + option.textContent = `${(i / 1000).toFixed(step === 250 ? 2 : 1)} pF (${i} fF)`; + if ( + config.loadCapacitanceFemtofarad && + i === config.loadCapacitanceFemtofarad + ) { + option.selected = true; + } + loadCapSelect.appendChild(option); + } + + enableScrollWheelSelectionForElement(loadCapSelect); + + const toggleLoadCapacitance = () => { + const isInternal = internalRadio.checked; + loadCapSelect.disabled = !isInternal; + }; + + internalRadio.onchange = toggleLoadCapacitance; + externalRadio.onchange = toggleLoadCapacitance; + + toggleLoadCapacitance(); + + document.getElementById("oscillatorConfigModal").style.display = "block"; +} + +export function closeOscillatorConfig() { + document.getElementById("oscillatorConfigModal").style.display = "none"; + currentOscillator = null; +} + +export function confirmOscillatorConfig() { + if (!currentOscillator) return; + + const loadCapacitors = document.querySelector( + 'input[name="oscillatorCapacitors"]:checked', + ).value; + + const config = { + loadCapacitors, + }; + + if (loadCapacitors === "internal") { + config.loadCapacitanceFemtofarad = parseInt( + document.getElementById("oscillatorLoadCapacitance").value, + ); + } + + let removed = false; + do { + const existingIndex = state.selectedPeripherals.findIndex( + (p) => p.id === currentOscillator.id, + ); + if (existingIndex !== -1) { + state.selectedPeripherals.splice(existingIndex, 1); + removed = true; + } else { + removed = false; + } + } while (removed); + + if (currentOscillator.signals && currentOscillator.signals.length > 0) { + currentOscillator.signals.forEach((s) => { + if (s.allowedGpio && s.allowedGpio.length > 0) { + const pinName = s.allowedGpio[0]; + if ( + state.usedPins[pinName] && + state.usedPins[pinName].peripheral === currentOscillator.id + ) { + delete state.usedPins[pinName]; + } + } + }); + } + + state.selectedPeripherals.push({ + id: currentOscillator.id, + description: currentOscillator.description, + config, + }); + + if (currentOscillator.signals && currentOscillator.signals.length > 0) { + currentOscillator.signals.forEach((s) => { + if (s.allowedGpio && s.allowedGpio.length > 0) { + const pinName = s.allowedGpio[0]; + state.usedPins[pinName] = { + peripheral: currentOscillator.id, + function: s.name, + required: s.isMandatory || true, + }; + } + }); + } + + updateSelectedPeripheralsList(); + organizePeripherals(); + updatePinDisplay(); + updateConsoleConfig(); + closeOscillatorConfig(); + saveStateToLocalStorage(); +} diff --git a/js/pin-layout.js b/js/pin-layout.js new file mode 100644 index 0000000..c5eed2f --- /dev/null +++ b/js/pin-layout.js @@ -0,0 +1,219 @@ +// --- PIN LAYOUT AND DETAILS --- + +import state from "./state.js"; + +function createPinElement(pinInfo) { + const pinElement = document.createElement("div"); + pinElement.className = "pin"; + pinElement.dataset.number = pinInfo.packagePinId; + pinElement.dataset.name = pinInfo.name; + pinElement.textContent = pinInfo.packagePinId; + + if (pinInfo.isClockCapable) pinElement.classList.add("clock"); + const specialTypes = [ + "power_positive", + "power_ground", + "debug", + "crystal_hf", + "crystal_lf", + "rf_antenna", + ]; + if (specialTypes.includes(pinInfo.defaultType)) { + pinElement.classList.add(pinInfo.defaultType.replace("_", "-")); + } + + pinElement.addEventListener("click", () => showPinDetails(pinInfo)); + return pinElement; +} + +export function createPinLayout() { + const chipContainer = document.querySelector(".chip-container"); + chipContainer.innerHTML = ""; + if (!state.mcuData.renderConfig || !state.mcuData.pins) return; + + const chipBody = document.createElement("div"); + chipBody.className = "chip-body"; + chipContainer.appendChild(chipBody); + + const strategy = state.mcuData.renderConfig.layoutStrategy; + const padding = state.mcuData.renderConfig.canvasDefaults?.padding || 20; + // Read actual container width for responsive layout + const containerSize = + Math.min(chipContainer.clientWidth, chipContainer.clientHeight) || 400; + + if (strategy.layoutType === "quadPerimeter") { + const pinsBySide = { + left: state.mcuData.pins + .filter((p) => p.side === "left") + .sort((a, b) => parseInt(a.packagePinId) - parseInt(b.packagePinId)), + bottom: state.mcuData.pins + .filter((p) => p.side === "bottom") + .sort((a, b) => parseInt(a.packagePinId) - parseInt(b.packagePinId)), + right: state.mcuData.pins + .filter((p) => p.side === "right") + .sort((a, b) => parseInt(a.packagePinId) - parseInt(b.packagePinId)), + top: state.mcuData.pins + .filter((p) => p.side === "top") + .sort((a, b) => parseInt(a.packagePinId) - parseInt(b.packagePinId)), + }; + + const activeArea = containerSize - 2 * padding; + + const placePins = (side, pins) => { + const len = pins.length; + if (len === 0) return; + const spacing = activeArea / (len + 1); + + pins.forEach((pinInfo, index) => { + const pinElement = createPinElement(pinInfo); + const pos = padding + (index + 1) * spacing; + + switch (side) { + case "left": + pinElement.style.left = "0px"; + pinElement.style.top = pos + "px"; + pinElement.style.transform = "translate(-50%, -50%)"; + break; + case "bottom": + pinElement.style.bottom = "0px"; + pinElement.style.left = pos + "px"; + pinElement.style.transform = "translate(-50%, 50%)"; + break; + case "right": + pinElement.style.right = "0px"; + pinElement.style.top = containerSize - pos + "px"; + pinElement.style.transform = "translate(50%, -50%)"; + break; + case "top": + pinElement.style.top = "0px"; + pinElement.style.left = containerSize - pos + "px"; + pinElement.style.transform = "translate(-50%, -50%)"; + break; + } + chipContainer.appendChild(pinElement); + }); + }; + + placePins("left", pinsBySide.left); + placePins("bottom", pinsBySide.bottom); + placePins("right", pinsBySide.right); + placePins("top", pinsBySide.top); + } else if (strategy.layoutType === "gridMatrix") { + const { rowLabels, columnLabels } = strategy; + const activeArea = containerSize - 2 * padding; + + const cellWidth = + columnLabels.length > 1 + ? activeArea / (columnLabels.length - 1) + : activeArea; + const cellHeight = + rowLabels.length > 1 ? activeArea / (rowLabels.length - 1) : activeArea; + + const pinMap = new Map( + state.mcuData.pins.map((p) => [p.gridCoordinates, p]), + ); + + for (let r = 0; r < rowLabels.length; r++) { + for (let c = 0; c < columnLabels.length; c++) { + const coord = `${rowLabels[r]}${columnLabels[c]}`; + + if (pinMap.has(coord)) { + const pinInfo = pinMap.get(coord); + const pinElement = createPinElement(pinInfo); + + pinElement.style.position = "absolute"; + const leftPos = + columnLabels.length > 1 + ? c * cellWidth + padding + : containerSize / 2; + const topPos = + rowLabels.length > 1 ? r * cellHeight + padding : containerSize / 2; + + pinElement.style.top = `${topPos}px`; + pinElement.style.left = `${leftPos}px`; + pinElement.style.transform = "translate(-50%, -50%)"; + + chipContainer.appendChild(pinElement); + } + } + } + } +} + +export function showPinDetails(pinInfo) { + const detailsElement = document.getElementById("pinDetails"); + + let usedByHtml = ""; + if (state.usedPins[pinInfo.name]) { + const usage = state.usedPins[pinInfo.name]; + usedByHtml = ` + + Used by + ${usage.peripheral} (${usage.function}) + + `; + } + + const functions = pinInfo.functions || []; + const functionsHtml = + functions.length > 0 + ? ` + Functions + ${functions.join("
")} + ` + : ""; + + detailsElement.innerHTML = ` +

${pinInfo.name} (Pin ${pinInfo.packagePinId})

+ + + + + + + ${pinInfo.isClockCapable ? "" : ""} + ${usedByHtml} + ${functionsHtml} + +
Type${pinInfo.defaultType}
AttributeClock capable
+ `; +} + +export function updatePinDisplay() { + document.querySelectorAll(".pin").forEach((pinElement) => { + const pinName = pinElement.dataset.name; + pinElement.classList.remove( + "used", + "required", + "system", + "devkit-occupied", + ); + if (state.usedPins[pinName]) { + pinElement.classList.add("used"); + if (state.usedPins[pinName].required) + pinElement.classList.add("required"); + if (state.usedPins[pinName].isSystem) pinElement.classList.add("system"); + if (state.usedPins[pinName].isDevkit) + pinElement.classList.add("devkit-occupied"); + } + }); + updatePeripheralConflictUI(); +} + +function updatePeripheralConflictUI() { + document.querySelectorAll("[data-id]").forEach((el) => { + const id = el.dataset.id; + if (!state.mcuData.socPeripherals) return; + const p = state.mcuData.socPeripherals.find((p) => p.id === id); + if ( + p && + state.usedAddresses[p.baseAddress] && + p.baseAddress && + !state.selectedPeripherals.some((sp) => sp.id === id) + ) { + el.classList.add("disabled"); + } else { + el.classList.remove("disabled"); + } + }); +} diff --git a/js/state.js b/js/state.js new file mode 100644 index 0000000..b7e1de4 --- /dev/null +++ b/js/state.js @@ -0,0 +1,202 @@ +// --- GLOBAL STATE --- + +const state = { + mcuManifest: {}, + mcuData: {}, + selectedPeripherals: [], + usedPins: {}, + usedAddresses: {}, + currentPeripheral: null, + tempSelectedPins: {}, + deviceTreeTemplates: null, + boardInfo: null, + consoleUart: null, // Peripheral ID (e.g., "UARTE20") of selected console UART, or null for RTT + devkitConfig: null, // Loaded devkit config, or null for custom board +}; + +export default state; + +// --- PERSISTENCE --- + +export function getPersistenceKey() { + const mcu = document.getElementById("mcuSelector").value; + const pkg = document.getElementById("packageSelector").value; + if (!mcu || !pkg) return null; + return `pinPlannerConfig-${mcu}-${pkg}`; +} + +export function serializePeripheral(peripheral) { + if (peripheral.type === "GPIO") { + return { + id: peripheral.id, + type: peripheral.type, + label: peripheral.label, + pin: peripheral.pin, + activeState: peripheral.activeState, + }; + } + + return { + id: peripheral.id, + pinFunctions: peripheral.pinFunctions, + config: peripheral.config, + }; +} + +export function saveStateToLocalStorage() { + const key = getPersistenceKey(); + if (!key) return; + + const config = { + selectedPeripherals: state.selectedPeripherals.map(serializePeripheral), + consoleUart: state.consoleUart, + }; + localStorage.setItem(key, JSON.stringify(config)); +} + +export function applyConfig(config) { + if (!config || !config.selectedPeripherals) return; + + for (const p_config of config.selectedPeripherals) { + if (p_config.type === "GPIO") { + state.selectedPeripherals.push({ + id: p_config.id, + type: p_config.type, + label: p_config.label, + pin: p_config.pin, + activeState: p_config.activeState, + }); + state.usedPins[p_config.pin] = { + peripheral: p_config.id, + function: "GPIO", + required: true, + }; + continue; + } + + const p_data = state.mcuData.socPeripherals.find( + (p) => p.id === p_config.id, + ); + if (p_data) { + if (p_data.uiHint === "oscillator") { + state.selectedPeripherals.push({ + id: p_data.id, + description: p_data.description, + config: p_config.config || p_data.config, + }); + if (p_data.signals && p_data.signals.length > 0) { + p_data.signals.forEach((s) => { + if (s.allowedGpio && s.allowedGpio.length > 0) { + const pinName = s.allowedGpio[0]; + state.usedPins[pinName] = { + peripheral: p_data.id, + function: s.name, + required: s.isMandatory || true, + }; + } + }); + } + } else if (p_data.uiHint === "checkbox") { + const checkbox = document.getElementById( + `${p_data.id.toLowerCase()}-checkbox`, + ); + if (checkbox) checkbox.checked = true; + + const pinFunctions = {}; + p_data.signals.forEach((s) => { + const pinName = s.allowedGpio[0]; + state.usedPins[pinName] = { + peripheral: p_data.id, + function: s.name, + required: true, + }; + pinFunctions[pinName] = s.name; + }); + state.selectedPeripherals.push({ + id: p_data.id, + peripheral: p_data, + pinFunctions, + }); + } else { + state.selectedPeripherals.push({ + id: p_data.id, + peripheral: p_data, + pinFunctions: p_config.pinFunctions, + }); + for (const pinName in p_config.pinFunctions) { + const signal = p_data.signals.find( + (s) => s.name === p_config.pinFunctions[pinName], + ); + state.usedPins[pinName] = { + peripheral: p_data.id, + function: p_config.pinFunctions[pinName], + required: signal ? signal.isMandatory : false, + }; + } + if (p_data.baseAddress) { + state.usedAddresses[p_data.baseAddress] = p_data.id; + } + } + } + } +} + +export function loadStateFromLocalStorage() { + const key = getPersistenceKey(); + if (!key) return; + + const savedState = localStorage.getItem(key); + if (!savedState) { + return; + } + + try { + const config = JSON.parse(savedState); + applyConfig(config); + if (config.consoleUart) { + state.consoleUart = config.consoleUart; + } + } catch (error) { + console.error("Failed to load or parse saved state:", error); + localStorage.removeItem(key); + } +} + +export function resetState() { + state.selectedPeripherals = []; + state.usedPins = {}; + state.usedAddresses = {}; + state.consoleUart = null; + state.devkitConfig = null; + document + .querySelectorAll('input[type="checkbox"][data-peripheral-id]') + .forEach((cb) => { + cb.checked = false; + }); + if (state.mcuData.pins) { + setHFXtalAsSystemRequirement(); + } +} + +export function setHFXtalAsSystemRequirement() { + if (!state.mcuData.pins) return; + const hfxtalPins = state.mcuData.pins.filter( + (p) => p.defaultType === "crystal_hf", + ); + if (hfxtalPins.length === 2) { + state.usedPins[hfxtalPins[0].name] = { + peripheral: "32MHz Crystal", + function: "XC1", + isSystem: true, + }; + state.usedPins[hfxtalPins[1].name] = { + peripheral: "32MHz Crystal", + function: "XC2", + isSystem: true, + }; + } +} + +export function hasAddressConflict(peripheral) { + return peripheral.baseAddress && state.usedAddresses[peripheral.baseAddress]; +} diff --git a/js/ui/import-export.js b/js/ui/import-export.js new file mode 100644 index 0000000..f2ca0dc --- /dev/null +++ b/js/ui/import-export.js @@ -0,0 +1,229 @@ +// --- IMPORT/EXPORT CONFIGURATION --- + +import state from "../state.js"; +import { + serializePeripheral, + saveStateToLocalStorage, + applyConfig, + resetState, +} from "../state.js"; +import { handleMcuChange, loadCurrentMcuData } from "../mcu-loader.js"; +import { organizePeripherals } from "../peripherals.js"; +import { updatePinDisplay } from "../pin-layout.js"; +import { updateSelectedPeripheralsList } from "./selected-list.js"; +import { updateConsoleConfig } from "../console-config.js"; + +let pendingImportConfig = null; +let isExportMode = true; + +export function openExportConfigModal() { + isExportMode = true; + const modal = document.getElementById("importExportInfoModal"); + const title = document.getElementById("importExportModalTitle"); + const text = document.getElementById("importExportModalText"); + const bullet1 = document.getElementById("importExportBullet1"); + const bullet2 = document.getElementById("importExportBullet2"); + const bullet3 = document.getElementById("importExportBullet3"); + const warning = document.getElementById("importExportWarning"); + const warningText = document.getElementById("importExportWarningText"); + const confirmBtn = document.getElementById("confirmImportExport"); + + title.textContent = "Export Configuration"; + text.textContent = + "This will export your current pin configuration for the selected MCU/package to a JSON file. You can use this file to:"; + bullet1.textContent = "Share configurations with team members"; + bullet2.textContent = "Back up your pin assignments"; + bullet3.textContent = + "Restore configurations on a different browser or computer"; + warning.style.backgroundColor = "#fff3cd"; + warning.style.color = "#856404"; + warning.style.borderColor = "#ffeeba"; + warningText.textContent = + "The exported file is specific to the currently selected MCU and package."; + confirmBtn.textContent = "Export"; + + modal.style.display = "block"; +} + +export function openImportConfigModal() { + document.getElementById("importConfigFile").click(); +} + +export function handleImportConfigFile(event) { + const file = event.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = function (e) { + try { + const config = JSON.parse(e.target.result); + validateAndShowImportModal(config); + } catch (error) { + alert("Invalid JSON file: " + error.message); + } + event.target.value = ""; + }; + reader.readAsText(file); +} + +function validateAndShowImportModal(config) { + if (!config.mcu || !config.package || !config.selectedPeripherals) { + alert( + "Invalid configuration file. Missing required fields (mcu, package, or selectedPeripherals).", + ); + return; + } + + pendingImportConfig = config; + isExportMode = false; + + const modal = document.getElementById("importExportInfoModal"); + const title = document.getElementById("importExportModalTitle"); + const text = document.getElementById("importExportModalText"); + const bullet1 = document.getElementById("importExportBullet1"); + const bullet2 = document.getElementById("importExportBullet2"); + const bullet3 = document.getElementById("importExportBullet3"); + const warning = document.getElementById("importExportWarning"); + const warningText = document.getElementById("importExportWarningText"); + const confirmBtn = document.getElementById("confirmImportExport"); + + const currentMcu = document.getElementById("mcuSelector").value; + const currentPkg = document.getElementById("packageSelector").value; + + const isDifferentPart = + config.mcu !== currentMcu || config.package !== currentPkg; + + title.textContent = "Import Configuration"; + text.textContent = `This will import a pin configuration from the file. The configuration is for:`; + bullet1.innerHTML = `MCU: ${config.mcu}`; + bullet2.innerHTML = `Package: ${config.package}`; + bullet3.innerHTML = `Peripherals: ${config.selectedPeripherals.length} configured`; + + if (isDifferentPart) { + warning.style.backgroundColor = "#fff3cd"; + warning.style.color = "#856404"; + warning.style.borderColor = "#ffeeba"; + warningText.innerHTML = `Note: This configuration is for a different MCU/package than currently selected. Importing will switch to ${config.mcu} with package ${config.package}.`; + } else { + warning.style.backgroundColor = "#d4edda"; + warning.style.color = "#155724"; + warning.style.borderColor = "#c3e6cb"; + warningText.innerHTML = `This configuration matches your currently selected MCU and package.`; + } + + confirmBtn.textContent = "Import"; + + const existingWarning = document.getElementById("importOverwriteWarning"); + if (!existingWarning) { + const overwriteWarning = document.createElement("div"); + overwriteWarning.id = "importOverwriteWarning"; + overwriteWarning.style.cssText = + "background-color: #f8d7da; color: #721c24; padding: 10px; border: 1px solid #f5c6cb; border-radius: 5px; margin-top: 10px;"; + overwriteWarning.innerHTML = + "Warning: Importing will replace your current configuration and overwrite any saved data for this MCU/package."; + warning.parentNode.insertBefore(overwriteWarning, warning.nextSibling); + } + + modal.style.display = "block"; +} + +export function closeImportExportModal() { + const modal = document.getElementById("importExportInfoModal"); + modal.style.display = "none"; + pendingImportConfig = null; + + const overwriteWarning = document.getElementById("importOverwriteWarning"); + if (overwriteWarning) { + overwriteWarning.remove(); + } +} + +export function confirmImportExport() { + if (isExportMode) { + exportConfig(); + } else { + importConfig(); + } + closeImportExportModal(); +} + +function exportConfig() { + const mcu = document.getElementById("mcuSelector").value; + const pkg = document.getElementById("packageSelector").value; + + if (!mcu || !pkg) { + alert("Please select an MCU and package first."); + return; + } + + const config = { + version: 1, + exportDate: new Date().toISOString(), + mcu: mcu, + package: pkg, + selectedPeripherals: state.selectedPeripherals.map(serializePeripheral), + }; + + const json = JSON.stringify(config, null, 2); + const blob = new Blob([json], { type: "application/json" }); + const url = URL.createObjectURL(blob); + + const a = document.createElement("a"); + a.href = url; + a.download = `pinplanner-${mcu}-${pkg}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +async function importConfig() { + if (!pendingImportConfig) return; + + const config = pendingImportConfig; + const currentMcu = document.getElementById("mcuSelector").value; + const currentPkg = document.getElementById("packageSelector").value; + + if (config.mcu !== currentMcu || config.package !== currentPkg) { + const mcuSelector = document.getElementById("mcuSelector"); + const mcuOption = Array.from(mcuSelector.options).find( + (opt) => opt.value === config.mcu, + ); + + if (!mcuOption) { + alert(`MCU "${config.mcu}" not found in available options.`); + return; + } + + mcuSelector.value = config.mcu; + await handleMcuChange(); + + const packageSelector = document.getElementById("packageSelector"); + const pkgOption = Array.from(packageSelector.options).find( + (opt) => opt.value === config.package, + ); + + if (!pkgOption) { + alert(`Package "${config.package}" not found for MCU "${config.mcu}".`); + return; + } + + packageSelector.value = config.package; + await loadCurrentMcuData(); + } + + // Clear - but skip confirm dialog + resetState(); + state.selectedPeripherals = []; + + applyConfig({ + selectedPeripherals: config.selectedPeripherals, + }); + + saveStateToLocalStorage(); + + organizePeripherals(); + updateSelectedPeripheralsList(); + updatePinDisplay(); + updateConsoleConfig(); +} diff --git a/js/ui/modals.js b/js/ui/modals.js new file mode 100644 index 0000000..296009c --- /dev/null +++ b/js/ui/modals.js @@ -0,0 +1,739 @@ +// --- PIN SELECTION MODAL, GPIO MODAL --- + +import state from "../state.js"; +import { saveStateToLocalStorage } from "../state.js"; +import { updatePinDisplay } from "../pin-layout.js"; +import { updateSelectedPeripheralsList } from "./selected-list.js"; +import { + organizePeripherals, + removePeripheral, + editPeripheral, +} from "../peripherals.js"; +import { updateConsoleConfig } from "../console-config.js"; +import { enableScrollWheelSelectionForElement } from "../utils.js"; + +// --- PIN SELECTION MODAL --- + +let tempSpiCsGpios = []; + +export function openPinSelectionModal( + peripheral, + existingPins = {}, + existingConfig = {}, +) { + state.currentPeripheral = peripheral; + state.tempSelectedPins = { ...existingPins }; + + document.getElementById("modalTitle").textContent = + `Select Pins for ${peripheral.id}`; + populatePinSelectionTable(peripheral); + + const uartConfigSection = document.getElementById("uartConfigSection"); + const uartDisableRxCheckbox = document.getElementById("uartDisableRx"); + if (peripheral.type === "UART") { + uartConfigSection.style.display = "block"; + uartDisableRxCheckbox.checked = existingConfig.disableRx || false; + uartDisableRxCheckbox.onchange = updateRxdRequiredStatus; + updateRxdRequiredStatus(); + } else { + uartConfigSection.style.display = "none"; + uartDisableRxCheckbox.checked = false; + uartDisableRxCheckbox.onchange = null; + } + + const spiConfigSection = document.getElementById("spiConfigSection"); + if (peripheral.type === "SPI") { + spiConfigSection.style.display = "block"; + initSpiCsGpioList(existingConfig.extraCsGpios || []); + } else { + spiConfigSection.style.display = "none"; + } + + const noteSection = document.getElementById("peripheralNoteSection"); + const noteInput = document.getElementById("peripheralNote"); + if (["SPI", "I2C", "UART"].includes(peripheral.type)) { + noteSection.style.display = "block"; + noteInput.value = existingConfig.note || ""; + } else { + noteSection.style.display = "none"; + noteInput.value = ""; + } + + document.getElementById("pinSelectionModal").style.display = "block"; +} + +export function closePinSelectionModal() { + document.getElementById("pinSelectionModal").style.display = "none"; + state.currentPeripheral = null; + state.tempSelectedPins = {}; +} + +function updateRxdRequiredStatus() { + const disableRxChecked = document.getElementById("uartDisableRx").checked; + const tableBody = document.getElementById("pinSelectionTableBody"); + const rows = tableBody.querySelectorAll("tr"); + + rows.forEach((row) => { + const functionCell = row.querySelector("td:first-child"); + const requiredCell = row.querySelector("td:nth-child(2)"); + if (functionCell && requiredCell && functionCell.textContent === "RXD") { + requiredCell.textContent = disableRxChecked ? "No" : "Yes"; + } + }); +} + +// --- SPI CS GPIO --- + +function initSpiCsGpioList(existingCsGpios) { + tempSpiCsGpios = [...existingCsGpios]; + renderSpiCsGpioList(); +} + +function renderSpiCsGpioList() { + const container = document.getElementById("spiCsGpioList"); + container.innerHTML = ""; + + tempSpiCsGpios.forEach((gpio, index) => { + const otherCsGpios = tempSpiCsGpios.filter((g, i) => i !== index && g); + + const row = document.createElement("div"); + row.style.cssText = + "display: flex; align-items: center; gap: 10px; margin-bottom: 8px;"; + row.innerHTML = ` + + + `; + container.appendChild(row); + }); + + container.querySelectorAll("select").forEach((select) => { + select.addEventListener("change", (e) => { + const idx = parseInt(e.target.dataset.csIndex); + tempSpiCsGpios[idx] = e.target.value; + renderSpiCsGpioList(); + }); + enableScrollWheelSelectionForElement(select); + }); + + container.querySelectorAll(".remove-cs-btn").forEach((btn) => { + btn.addEventListener("click", (e) => { + const idx = parseInt(e.target.dataset.csIndex); + tempSpiCsGpios.splice(idx, 1); + renderSpiCsGpioList(); + }); + }); +} + +function getGpioPinOptions(selectedPin, filterUsed = false, excludePins = []) { + if (!state.mcuData.pins) return ''; + + const gpioPins = state.mcuData.pins + .filter( + (pin) => + Array.isArray(pin.functions) && pin.functions.includes("Digital I/O"), + ) + .sort((a, b) => { + const aMatch = a.name.match(/P(\d+)\.(\d+)/); + const bMatch = b.name.match(/P(\d+)\.(\d+)/); + if (!aMatch || !bMatch) return a.name.localeCompare(b.name); + const aPort = parseInt(aMatch[1]); + const bPort = parseInt(bMatch[1]); + const aPin = parseInt(aMatch[2]); + const bPin = parseInt(bMatch[2]); + if (aPort !== bPort) return aPort - bPort; + return aPin - bPin; + }); + + let options = ''; + gpioPins.forEach((pin) => { + const isSelected = pin.name === selectedPin; + + let isDisabled = false; + if (filterUsed && !isSelected) { + if (state.usedPins[pin.name]) { + isDisabled = true; + } + if (state.tempSelectedPins[pin.name]) { + isDisabled = true; + } + if (excludePins.includes(pin.name)) { + isDisabled = true; + } + } + + options += ``; + }); + return options; +} + +export function addSpiCsGpio() { + tempSpiCsGpios.push(""); + renderSpiCsGpioList(); +} + +// --- PIN SELECTION TABLE --- + +function populatePinSelectionTable(peripheral) { + const tableBody = document.getElementById("pinSelectionTableBody"); + tableBody.innerHTML = ""; + + peripheral.signals.forEach((signal) => { + const row = document.createElement("tr"); + const allPossiblePins = getPinsForSignal(signal); + + let selectionHtml; + if (allPossiblePins.length === 1 && signal.allowedGpio.length === 1) { + const pin = allPossiblePins[0]; + selectionHtml = ``; + } else { + let optionsHtml = ''; + allPossiblePins.forEach((pin) => { + const isSelected = state.tempSelectedPins[pin.name] === signal.name; + optionsHtml += ``; + }); + selectionHtml = ``; + } + + row.innerHTML = ` + ${signal.name} + ${signal.isMandatory ? "Yes" : "No"} + ${selectionHtml} + ${signal.description || ""} + `; + tableBody.appendChild(row); + }); + + tableBody + .querySelectorAll('select, input[type="checkbox"]') + .forEach((input) => { + input.addEventListener("change", handlePinSelectionChange); + }); + + tableBody.querySelectorAll("select").forEach((select) => { + enableScrollWheelSelectionForElement(select); + }); + + updateModalPinAvailability(); +} + +function handlePinSelectionChange(event) { + const input = event.target; + const signalName = input.dataset.signal; + + Object.keys(state.tempSelectedPins).forEach((pin) => { + if (state.tempSelectedPins[pin] === signalName) { + delete state.tempSelectedPins[pin]; + } + }); + + if (input.type === "checkbox") { + if (input.checked) { + state.tempSelectedPins[input.dataset.pin] = signalName; + } + } else { + if (input.value) { + state.tempSelectedPins[input.value] = signalName; + } + } + + updateModalPinAvailability(); +} + +function updateModalPinAvailability() { + const selects = document.querySelectorAll("#pinSelectionTableBody select"); + const checkboxes = document.querySelectorAll( + '#pinSelectionTableBody input[type="checkbox"]', + ); + + const pinsSelectedInModal = Object.keys(state.tempSelectedPins); + + selects.forEach((select) => { + const signalForThisSelect = select.dataset.signal; + for (const option of select.options) { + const pinName = option.value; + if (!pinName) continue; + + const isUsedByOtherPeripheral = + state.usedPins[pinName] && + state.usedPins[pinName].peripheral !== state.currentPeripheral.id; + const isUsedInThisModal = + pinsSelectedInModal.includes(pinName) && + state.tempSelectedPins[pinName] !== signalForThisSelect; + + option.disabled = isUsedByOtherPeripheral || isUsedInThisModal; + } + }); + + checkboxes.forEach((checkbox) => { + const pinName = checkbox.dataset.pin; + const signalForThisCheckbox = checkbox.dataset.signal; + + const isUsedByOtherPeripheral = + state.usedPins[pinName] && + state.usedPins[pinName].peripheral !== state.currentPeripheral.id; + const isUsedInThisModal = + pinsSelectedInModal.includes(pinName) && + state.tempSelectedPins[pinName] !== signalForThisCheckbox; + + checkbox.disabled = isUsedByOtherPeripheral || isUsedInThisModal; + }); +} + +function getPinsForSignal(signal) { + if (!state.mcuData.pins) return []; + const pins = state.mcuData.pins.filter((pin) => { + if (!Array.isArray(pin.functions) || !pin.functions.includes("Digital I/O")) + return false; + if (signal.requiresClockCapablePin && !pin.isClockCapable) return false; + return signal.allowedGpio.some((allowed) => + allowed.endsWith("*") + ? pin.port === allowed.slice(0, -1) + : pin.name === allowed, + ); + }); + + return pins.sort((a, b) => { + const aMatch = a.name.match(/P(\d+)\.(\d+)/); + const bMatch = b.name.match(/P(\d+)\.(\d+)/); + + if (!aMatch || !bMatch) return a.name.localeCompare(b.name); + + const aPort = parseInt(aMatch[1]); + const bPort = parseInt(bMatch[1]); + const aPin = parseInt(aMatch[2]); + const bPin = parseInt(bMatch[2]); + + if (aPort !== bPort) return aPort - bPort; + return aPin - bPin; + }); +} + +export function confirmPinSelection() { + const disableRxChecked = + state.currentPeripheral.type === "UART" && + document.getElementById("uartDisableRx").checked; + + const missingSignals = state.currentPeripheral.signals.filter((s) => { + if (disableRxChecked && s.name === "RXD") return false; + return ( + s.isMandatory && !Object.values(state.tempSelectedPins).includes(s.name) + ); + }); + + if (missingSignals.length > 0) { + alert( + `Please select pins for mandatory functions: ${missingSignals.map((s) => s.name).join(", ")}`, + ); + return; + } + + for (const pinName in state.tempSelectedPins) { + if ( + state.usedPins[pinName] && + state.usedPins[pinName].peripheral !== state.currentPeripheral.id + ) { + alert( + `Pin ${pinName} is already used by ${state.usedPins[pinName].peripheral}.`, + ); + return; + } + } + + const existingIndex = state.selectedPeripherals.findIndex( + (p) => p.id === state.currentPeripheral.id, + ); + if (existingIndex !== -1) { + const oldPeripheral = state.selectedPeripherals[existingIndex]; + for (const pinName in oldPeripheral.pinFunctions) { + delete state.usedPins[pinName]; + } + state.selectedPeripherals.splice(existingIndex, 1); + } + + const peripheralEntry = { + id: state.currentPeripheral.id, + peripheral: state.currentPeripheral, + pinFunctions: { ...state.tempSelectedPins }, + }; + + if (state.currentPeripheral.type === "UART") { + const disableRx = document.getElementById("uartDisableRx").checked; + if (disableRx) { + peripheralEntry.config = { disableRx: true }; + } + } + + if (state.currentPeripheral.type === "SPI") { + const validCsGpios = tempSpiCsGpios.filter( + (gpio) => gpio && gpio.trim() !== "", + ); + if (validCsGpios.length > 0) { + peripheralEntry.config = peripheralEntry.config || {}; + peripheralEntry.config.extraCsGpios = validCsGpios; + } + } + + if (["SPI", "I2C", "UART"].includes(state.currentPeripheral.type)) { + const note = document.getElementById("peripheralNote").value.trim(); + if (note) { + peripheralEntry.config = peripheralEntry.config || {}; + peripheralEntry.config.note = note; + } + } + + state.selectedPeripherals.push(peripheralEntry); + + for (const pinName in state.tempSelectedPins) { + state.usedPins[pinName] = { + peripheral: state.currentPeripheral.id, + function: state.tempSelectedPins[pinName], + required: state.currentPeripheral.signals.find( + (s) => s.name === state.tempSelectedPins[pinName], + ).isMandatory, + }; + } + if (state.currentPeripheral.baseAddress) { + state.usedAddresses[state.currentPeripheral.baseAddress] = + state.currentPeripheral.id; + } + + updateSelectedPeripheralsList(); + updatePinDisplay(); + updateConsoleConfig(); + closePinSelectionModal(); + saveStateToLocalStorage(); +} + +// --- GPIO PIN ALLOCATION MODAL --- + +let gpioTableRows = []; +let nextGpioRowId = 1; + +export function openGpioModal() { + initializeGpioTable(); + + const errorEl = document.getElementById("gpioError"); + errorEl.style.display = "none"; + + document.getElementById("gpioModal").style.display = "block"; +} + +function initializeGpioTable() { + gpioTableRows = []; + nextGpioRowId = 1; + + state.selectedPeripherals + .filter((p) => p.type === "GPIO") + .forEach((gpio) => { + addGpioTableRow(gpio.label, gpio.pin, gpio.activeState, gpio.id); + }); + + if (gpioTableRows.length === 0) { + addGpioTableRow(); + } + + renderGpioTable(); +} + +function addGpioTableRow( + label = "", + pin = "", + activeState = "active-high", + existingId = null, +) { + const rowId = `gpio_row_${nextGpioRowId++}`; + gpioTableRows.push({ + id: rowId, + label: label, + pin: pin, + activeState: activeState, + existingId: existingId, + isValid: true, + }); + renderGpioTable(); +} + +// Expose for onclick handler +window._removeGpioTableRow = function (rowId) { + const index = gpioTableRows.findIndex((row) => row.id === rowId); + if (index !== -1) { + gpioTableRows.splice(index, 1); + renderGpioTable(); + } +}; + +function renderGpioTable() { + const tableBody = document.getElementById("gpioTableBody"); + + if (gpioTableRows.length === 0) { + tableBody.innerHTML = ` + + No GPIO pins configured. Click "Add GPIO Pin" to add one. + + `; + return; + } + + tableBody.innerHTML = ""; + + gpioTableRows.forEach((row) => { + const tr = document.createElement("tr"); + tr.innerHTML = ` + + + + + + + + + + + + + `; + tableBody.appendChild(tr); + }); + + attachGpioTableEventListeners(); +} + +function attachGpioTableEventListeners() { + document + .querySelectorAll("#gpioTable input, #gpioTable select") + .forEach((element) => { + element.addEventListener("input", handleGpioTableInputChange); + element.addEventListener("change", handleGpioTableInputChange); + + if ( + element.type === "text" && + element.getAttribute("data-field") === "label" + ) { + element.addEventListener("blur", (event) => { + const value = event.target.value.toLowerCase(); + if (event.target.value !== value) { + event.target.value = value; + handleGpioTableInputChange(event); + } + }); + } + }); +} + +function handleGpioTableInputChange(event) { + const rowId = event.target.getAttribute("data-row-id"); + const field = event.target.getAttribute("data-field"); + const value = event.target.value; + + const row = gpioTableRows.find((r) => r.id === rowId); + if (row) { + row[field] = value; + validateGpioRow(row); + + if (field === "pin") { + renderGpioTable(); + } else { + updateInputValidation(event.target, row); + } + } +} + +function updateInputValidation(inputElement, row) { + if (row.isValid) { + inputElement.classList.remove("validation-error"); + } else { + inputElement.classList.add("validation-error"); + } +} + +function validateGpioRow(row) { + row.isValid = true; + + if (!row.label || !/^[a-z0-9_]+$/.test(row.label)) { + row.isValid = false; + return; + } + + const duplicateLabel = gpioTableRows.find( + (r) => r.id !== row.id && r.label === row.label && r.label !== "", + ); + if (duplicateLabel) { + row.isValid = false; + return; + } + + if (!row.pin) { + row.isValid = false; + return; + } + + const duplicatePin = gpioTableRows.find( + (r) => r.id !== row.id && r.pin === row.pin && r.pin !== "", + ); + if (duplicatePin) { + row.isValid = false; + return; + } +} + +function getGpioPinOptionsForTable(selectedPin, existingGpioId) { + let options = ""; + + if (!state.mcuData.pins) return options; + + const gpioPins = state.mcuData.pins.filter( + (pin) => pin.functions && pin.functions.includes("Digital I/O"), + ); + + gpioPins.sort((a, b) => { + const aMatch = a.name.match(/P(\d+)\.(\d+)/); + const bMatch = b.name.match(/P(\d+)\.(\d+)/); + + if (aMatch && bMatch) { + const aPort = parseInt(aMatch[1]); + const bPort = parseInt(bMatch[1]); + const aPin = parseInt(aMatch[2]); + const bPin = parseInt(bMatch[2]); + + if (aPort !== bPort) { + return aPort - bPort; + } + return aPin - bPin; + } + + return a.name.localeCompare(b.name); + }); + + gpioPins.forEach((pin) => { + const isSelected = pin.name === selectedPin; + let isDisabled = false; + + if ( + state.usedPins[pin.name] && + !state.usedPins[pin.name].peripheral.startsWith("GPIO_") + ) { + isDisabled = true; + } + + const gpioUsingPin = state.selectedPeripherals.find( + (p) => p.type === "GPIO" && p.pin === pin.name && p.id !== existingGpioId, + ); + if (gpioUsingPin && !isSelected) { + isDisabled = true; + } + + options += ``; + }); + + return options; +} + +export function closeGpioModal() { + document.getElementById("gpioModal").style.display = "none"; + gpioTableRows = []; +} + +export function confirmGpioModal() { + const errorEl = document.getElementById("gpioError"); + + let hasErrors = false; + const validRows = []; + const errorMessages = []; + + gpioTableRows.forEach((row) => { + validateGpioRow(row); + if (!row.isValid && row.label && row.pin) { + hasErrors = true; + if (!/^[a-z0-9_]+$/.test(row.label)) { + errorMessages.push( + `"${row.label}": Label must be lowercase letters, numbers, and underscores only`, + ); + } else if ( + gpioTableRows.find( + (r) => r.id !== row.id && r.label === row.label && r.label !== "", + ) + ) { + errorMessages.push(`"${row.label}": Duplicate label`); + } else if ( + gpioTableRows.find( + (r) => r.id !== row.id && r.pin === row.pin && r.pin !== "", + ) + ) { + errorMessages.push(`${row.pin}: Pin used multiple times`); + } + } else if (row.label && row.pin && row.isValid) { + validRows.push(row); + } + }); + + if (hasErrors) { + const uniqueMessages = [...new Set(errorMessages)]; + errorEl.textContent = "Validation errors: " + uniqueMessages.join("; "); + errorEl.style.display = "block"; + renderGpioTable(); + return; + } + + if (validRows.length === 0) { + errorEl.textContent = "Please add at least one GPIO pin or cancel"; + errorEl.style.display = "block"; + return; + } + + state.selectedPeripherals + .filter((p) => p.type === "GPIO") + .forEach((gpio) => { + delete state.usedPins[gpio.pin]; + }); + state.selectedPeripherals = state.selectedPeripherals.filter( + (p) => p.type !== "GPIO", + ); + + validRows.forEach((row) => { + const gpioId = `GPIO_${row.label.toUpperCase()}`; + + state.selectedPeripherals.push({ + id: gpioId, + type: "GPIO", + label: row.label, + pin: row.pin, + activeState: row.activeState, + }); + + state.usedPins[row.pin] = { + peripheral: gpioId, + function: "GPIO", + required: true, + }; + }); + + updateSelectedPeripheralsList(); + updatePinDisplay(); + closeGpioModal(); + saveStateToLocalStorage(); +} + +// Expose addGpioTableRow for the button +export function addGpioTableRowPublic() { + addGpioTableRow(); +} diff --git a/js/ui/notifications.js b/js/ui/notifications.js new file mode 100644 index 0000000..e67c035 --- /dev/null +++ b/js/ui/notifications.js @@ -0,0 +1,50 @@ +// --- TOAST NOTIFICATION SYSTEM --- + +let toastContainer = null; + +function getToastContainer() { + if (!toastContainer) { + toastContainer = document.getElementById("toastContainer"); + if (!toastContainer) { + toastContainer = document.createElement("div"); + toastContainer.id = "toastContainer"; + document.body.appendChild(toastContainer); + } + } + return toastContainer; +} + +export function showToast(message, type = "info", duration = 5000) { + const container = getToastContainer(); + + const toast = document.createElement("div"); + toast.className = `toast toast-${type}`; + toast.textContent = message; + + // Close button + const closeBtn = document.createElement("button"); + closeBtn.className = "toast-close"; + closeBtn.textContent = "\u00d7"; + closeBtn.addEventListener("click", () => { + toast.classList.add("toast-exit"); + setTimeout(() => toast.remove(), 300); + }); + toast.appendChild(closeBtn); + + container.appendChild(toast); + + // Trigger enter animation + requestAnimationFrame(() => { + toast.classList.add("toast-enter"); + }); + + // Auto-remove + if (duration > 0) { + setTimeout(() => { + toast.classList.add("toast-exit"); + setTimeout(() => toast.remove(), 300); + }, duration); + } + + return toast; +} diff --git a/js/ui/selected-list.js b/js/ui/selected-list.js new file mode 100644 index 0000000..e6bb360 --- /dev/null +++ b/js/ui/selected-list.js @@ -0,0 +1,110 @@ +// --- SELECTED PERIPHERALS LIST --- + +import state from "../state.js"; +import { removePeripheral, editPeripheral } from "../peripherals.js"; + +export function updateSelectedPeripheralsList() { + const selectedList = document.getElementById("selectedList"); + selectedList.innerHTML = ""; + + if (state.selectedPeripherals.length === 0) { + selectedList.innerHTML = + '
  • No peripherals selected yet.
  • '; + return; + } + + // Remove duplicates (safety check) + const uniquePeripherals = []; + const seenIds = new Set(); + state.selectedPeripherals.forEach((p) => { + if (!seenIds.has(p.id)) { + seenIds.add(p.id); + uniquePeripherals.push(p); + } + }); + + if (uniquePeripherals.length !== state.selectedPeripherals.length) { + state.selectedPeripherals.length = 0; + uniquePeripherals.forEach((p) => state.selectedPeripherals.push(p)); + } + + state.selectedPeripherals.forEach((p) => { + const item = document.createElement("li"); + item.className = "selected-item"; + + let details; + if (p.type === "GPIO") { + const activeLabel = + p.activeState === "active-low" ? "active-low" : "active-high"; + details = `${p.pin} (${activeLabel})`; + } else if (p.config && p.config.loadCapacitors) { + const capLabel = + p.config.loadCapacitors === "internal" ? "Internal" : "External"; + if (p.config.loadCapacitors === "external") { + details = `${capLabel} caps`; + } else { + const capValue = (p.config.loadCapacitanceFemtofarad / 1000).toFixed( + p.id === "HFXO" ? 2 : 1, + ); + details = `${capLabel} caps, ${capValue} pF`; + } + + const oscData = state.mcuData.socPeripherals.find((sp) => sp.id === p.id); + if (oscData && oscData.signals && oscData.signals.length > 0) { + const pins = oscData.signals + .filter((s) => s.allowedGpio && s.allowedGpio.length > 0) + .map((s) => s.allowedGpio[0]) + .join(", "); + if (pins) { + details += ` (${pins})`; + } + } + } else { + details = + Object.entries(p.pinFunctions || {}) + .map(([pin, func]) => `${pin}: ${func}`) + .join(", ") || "Auto-assigned"; + + if (p.config && p.config.disableRx) { + details += " [RX disabled]"; + } + + if ( + p.config && + p.config.extraCsGpios && + p.config.extraCsGpios.length > 0 + ) { + details += ` [+${p.config.extraCsGpios.length} CS: ${p.config.extraCsGpios.join(", ")}]`; + } + } + + const removeBtn = + p.id === "HFXO" + ? "" + : ``; + + let displayName = p.type === "GPIO" ? `GPIO: ${p.label}` : p.id; + if ( + p.config && + p.config.note && + ["SPI", "I2C", "UART"].includes(p.peripheral?.type) + ) { + displayName += `: ${p.config.note}`; + } + + item.innerHTML = ` +
    ${displayName}
    ${details}
    + ${removeBtn} + `; + + if (p.id !== "HFXO") { + item.querySelector(".remove-btn").addEventListener("click", (e) => { + e.stopPropagation(); + removePeripheral(p.id); + }); + } + + item.addEventListener("click", () => editPeripheral(p.id)); + selectedList.appendChild(item); + }); +} diff --git a/js/utils.js b/js/utils.js new file mode 100644 index 0000000..8e5e7ec --- /dev/null +++ b/js/utils.js @@ -0,0 +1,52 @@ +// --- UTILITY FUNCTIONS --- + +export function enableScrollWheelSelection(selectorId) { + const selector = document.getElementById(selectorId); + if (!selector) return; + enableScrollWheelSelectionForElement(selector); +} + +export function enableScrollWheelSelectionForElement(selector) { + if (!selector) return; + + selector.addEventListener( + "wheel", + function (event) { + event.preventDefault(); + + const direction = Math.sign(event.deltaY); + const currentIndex = selector.selectedIndex; + const numOptions = selector.options.length; + + if (numOptions === 0) return; + + let nextIndex = currentIndex + direction; + + while (nextIndex >= 0 && nextIndex < numOptions) { + const option = selector.options[nextIndex]; + if (!option.disabled && option.value !== "") { + break; + } + nextIndex += direction; + } + + nextIndex = Math.max(0, Math.min(nextIndex, numOptions - 1)); + + if (nextIndex !== currentIndex && !selector.options[nextIndex].disabled) { + selector.selectedIndex = nextIndex; + selector.dispatchEvent(new Event("change", { bubbles: true })); + } + }, + { passive: false }, + ); +} + +export function parsePinName(pinName) { + const match = pinName.match(/P(\d+)\.(\d+)/); + if (!match) return null; + return { + port: parseInt(match[1]), + pin: parseInt(match[2]), + name: pinName, + }; +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..834f678 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,84 @@ +{ + "name": "nrf54l-pin-planner", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nrf54l-pin-planner", + "version": "1.0.0", + "devDependencies": { + "ajv": "^8.12.0", + "prettier": "^3.2.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..97efcab --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "nrf54l-pin-planner", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "format": "prettier --write .", + "format:check": "prettier --check .", + "validate:schemas": "node ci/validate-mcu-schemas.js", + "smoke-test": "node ci/smoke-test.js", + "generate-test-boards": "node ci/generate-test-boards.js", + "extract-devkits": "node ci/extract-devkit-configs.js", + "test": "npm run format:check && npm run validate:schemas && npm run smoke-test" + }, + "devDependencies": { + "prettier": "^3.2.0", + "ajv": "^8.12.0" + } +} diff --git a/script.js b/script.js deleted file mode 100644 index aae5387..0000000 --- a/script.js +++ /dev/null @@ -1,3849 +0,0 @@ -// --- GLOBAL STATE --- -let mcuManifest = {}; // Holds the content of manifest.json -let mcuData = {}; // Holds data for the currently selected MCU package -let selectedPeripherals = []; -let usedPins = {}; -let usedAddresses = {}; // Track used address spaces -let currentPeripheral = null; -let tempSelectedPins = {}; // Used for storing pin selections temporarily during modal dialog - -// --- INITIALIZATION --- -document.addEventListener("DOMContentLoaded", function () { - console.log("Initializing nRF54L Pin Planner..."); - - // Set up event listeners - document - .getElementById("mcuSelector") - .addEventListener("change", handleMcuChange); - document - .getElementById("packageSelector") - .addEventListener("change", handlePackageChange); - document - .getElementById("clearAllBtn") - .addEventListener("click", clearAllPeripherals); - document - .getElementById("exportDeviceTreeBtn") - .addEventListener("click", openBoardInfoModal); - document - .getElementById("searchPeripherals") - .addEventListener("input", filterPeripherals); - document - .querySelector("#pinSelectionModal .close") - .addEventListener("click", closePinSelectionModal); - document - .getElementById("cancelPinSelection") - .addEventListener("click", closePinSelectionModal); - document - .getElementById("confirmPinSelection") - .addEventListener("click", confirmPinSelection); - document - .getElementById("closeBoardInfoModal") - .addEventListener("click", closeBoardInfoModal); - document - .getElementById("cancelBoardInfo") - .addEventListener("click", closeBoardInfoModal); - document - .getElementById("confirmBoardInfo") - .addEventListener("click", confirmBoardInfoAndGenerate); - document - .getElementById("closeOscillatorModal") - .addEventListener("click", closeOscillatorConfig); - document - .getElementById("cancelOscillatorConfig") - .addEventListener("click", closeOscillatorConfig); - document - .getElementById("confirmOscillatorConfig") - .addEventListener("click", confirmOscillatorConfig); - - // Import/Export config listeners - document - .getElementById("exportConfigBtn") - .addEventListener("click", openExportConfigModal); - document - .getElementById("importConfigBtn") - .addEventListener("click", openImportConfigModal); - document - .getElementById("importConfigFile") - .addEventListener("change", handleImportConfigFile); - document - .getElementById("closeImportExportModal") - .addEventListener("click", closeImportExportModal); - document - .getElementById("cancelImportExport") - .addEventListener("click", closeImportExportModal); - document - .getElementById("confirmImportExport") - .addEventListener("click", confirmImportExport); - - // --- THEME SWITCHER LOGIC --- - const themeToggle = document.getElementById("theme-toggle"); - const body = document.body; - - const setTheme = (isDark) => { - if (isDark) { - body.classList.add("dark-mode"); - themeToggle.checked = true; - localStorage.setItem("theme", "dark"); - } else { - body.classList.remove("dark-mode"); - themeToggle.checked = false; - localStorage.setItem("theme", "light"); - } - }; - - const savedTheme = localStorage.getItem("theme"); - const prefersDark = - window.matchMedia && - window.matchMedia("(prefers-color-scheme: dark)").matches; - - if (savedTheme) { - setTheme(savedTheme === "dark"); - } else { - setTheme(prefersDark); - } - - themeToggle.addEventListener("change", () => { - setTheme(themeToggle.checked); - }); - - window - .matchMedia("(prefers-color-scheme: dark)") - .addEventListener("change", (e) => { - const currentTheme = localStorage.getItem("theme"); - if (!currentTheme) { - setTheme(e.matches); - } - }); - - // Add scroll-wheel selection to dropdowns - enableScrollWheelSelection("mcuSelector"); - enableScrollWheelSelection("packageSelector"); - - // Initial data load and package population - initializeApp(); -}); - -function enableScrollWheelSelection(selectorId) { - const selector = document.getElementById(selectorId); - if (!selector) return; - enableScrollWheelSelectionForElement(selector); -} - -function enableScrollWheelSelectionForElement(selector) { - if (!selector) return; - - selector.addEventListener( - "wheel", - function (event) { - // Prevent the page from scrolling - event.preventDefault(); - - const direction = Math.sign(event.deltaY); - const currentIndex = selector.selectedIndex; - const numOptions = selector.options.length; - - if (numOptions === 0) return; - - // Find the next enabled option in the scroll direction - let nextIndex = currentIndex + direction; - - // Keep moving in the direction until we find an enabled option or reach the end - while (nextIndex >= 0 && nextIndex < numOptions) { - const option = selector.options[nextIndex]; - // Skip disabled options and empty value options (like "-- Select Pin --") - if (!option.disabled && option.value !== "") { - break; - } - nextIndex += direction; - } - - // Clamp to valid range and only change if we found a valid option - nextIndex = Math.max(0, Math.min(nextIndex, numOptions - 1)); - - if (nextIndex !== currentIndex && !selector.options[nextIndex].disabled) { - selector.selectedIndex = nextIndex; - // Dispatch a change event to trigger the application's logic - selector.dispatchEvent(new Event("change", { bubbles: true })); - } - }, - { passive: false }, - ); // passive: false is required to use preventDefault -} - -async function initializeApp() { - try { - const response = await fetch("mcus/manifest.json"); - if (!response.ok) throw new Error("Manifest file not found."); - mcuManifest = await response.json(); - populateMcuSelector(); - } catch (error) { - console.error("Failed to initialize application:", error); - alert( - "Could not load MCU manifest. The application may not function correctly.", - ); - } -} - -function populateMcuSelector() { - const mcuSelector = document.getElementById("mcuSelector"); - mcuSelector.innerHTML = ""; - mcuManifest.mcus.forEach((mcu) => { - const option = document.createElement("option"); - option.value = mcu.id; - option.textContent = mcu.name; - option.dataset.packages = JSON.stringify(mcu.packages); - mcuSelector.appendChild(option); - }); - handleMcuChange(); -} - -// --- DATA LOADING AND UI REFRESH --- - -async function handleMcuChange() { - const mcuSelector = document.getElementById("mcuSelector"); - const packageSelector = document.getElementById("packageSelector"); - const selectedMcuOption = mcuSelector.options[mcuSelector.selectedIndex]; - - if (!selectedMcuOption) return; - - const packages = JSON.parse(selectedMcuOption.dataset.packages || "[]"); - packageSelector.innerHTML = ""; - - if (packages.length > 0) { - packages.forEach((pkg) => { - const option = document.createElement("option"); - option.value = pkg.file; - option.textContent = pkg.name; - packageSelector.appendChild(option); - }); - await loadCurrentMcuData(); - } else { - reinitializeView(true); // No packages, clear view - } -} - -async function handlePackageChange() { - await loadCurrentMcuData(); -} - -async function loadCurrentMcuData() { - const mcu = document.getElementById("mcuSelector").value; - const pkg = document.getElementById("packageSelector").value; - if (mcu && pkg) { - await loadMCUData(mcu, pkg); - } -} - -async function loadMCUData(mcu, pkg) { - const path = `mcus/${mcu}/${pkg}.json`; - try { - const response = await fetch(path); - if (!response.ok) { - throw new Error(`File not found or invalid: ${path}`); - } - mcuData = await response.json(); - console.log(`Loaded data for ${mcuData.partInfo.partNumber}`); - - // Load devicetree templates for this MCU - deviceTreeTemplates = await loadDeviceTreeTemplates(mcu); - - reinitializeView(); - } catch (error) { - console.error("Error loading MCU data:", error); - alert(`Could not load data for ${mcu} - ${pkg}.\n${error.message}`); - reinitializeView(true); // Clear the view on error - } -} - -function reinitializeView(clearOnly = false) { - resetState(); - - if (clearOnly || !mcuData.partInfo) { - document.getElementById("chipTitleDisplay").textContent = "No MCU Loaded"; - organizePeripherals(); - createPinLayout(); - updateSelectedPeripheralsList(); - updatePinDisplay(); - return; - } - - // Add oscillators to peripheral list - addOscillatorsToPeripherals(); - - // Auto-select HFXO with default configuration - autoSelectHFXO(); - - document.getElementById("chipTitleDisplay").textContent = - `${mcuData.partInfo.packageType} Pin Layout`; - organizePeripherals(); - createPinLayout(); - - loadStateFromLocalStorage(); - - // Ensure HFXO is always selected after loading state (and remove duplicates) - const hfxoCount = selectedPeripherals.filter((p) => p.id === "HFXO").length; - if (hfxoCount === 0) { - // No HFXO found, add it - const hfxo = mcuData.socPeripherals.find((p) => p.id === "HFXO"); - if (hfxo) { - selectedPeripherals.push({ - id: "HFXO", - description: hfxo.description, - config: { ...hfxo.config }, - }); - } - } else if (hfxoCount > 1) { - // Multiple HFXO found, keep only the first one - const firstHfxo = selectedPeripherals.find((p) => p.id === "HFXO"); - // Remove all HFXO instances - for (let i = selectedPeripherals.length - 1; i >= 0; i--) { - if (selectedPeripherals[i].id === "HFXO") { - selectedPeripherals.splice(i, 1); - } - } - // Add back just one - selectedPeripherals.push(firstHfxo); - } - - organizePeripherals(); // Re-render to show HFXO as selected - updateSelectedPeripheralsList(); - updatePinDisplay(); - console.log( - "Initialization complete. Peripherals loaded:", - mcuData.socPeripherals.length, - ); -} - -// --- PERIPHERAL ORGANIZATION AND DISPLAY --- - -function addOscillatorsToPeripherals() { - if (!mcuData.socPeripherals) { - mcuData.socPeripherals = []; - } - - // Find existing LFXO (might be defined with checkbox uiHint) - const lfxoIndex = mcuData.socPeripherals.findIndex((p) => p.id === "LFXO"); - - if (lfxoIndex !== -1) { - // LFXO exists - convert it to oscillator type - const lfxo = mcuData.socPeripherals[lfxoIndex]; - lfxo.uiHint = "oscillator"; - lfxo.optional = true; - if (!lfxo.config) { - lfxo.config = { - loadCapacitors: "internal", - loadCapacitanceFemtofarad: 15000, - }; - } - } else { - // Add LFXO as optional oscillator - mcuData.socPeripherals.push({ - id: "LFXO", - description: "Low Frequency Crystal Oscillator", - uiHint: "oscillator", - optional: true, - signals: [], - config: { - loadCapacitors: "internal", - loadCapacitanceFemtofarad: 15000, - }, - }); - } - - // Find or add HFXO - const hfxoIndex = mcuData.socPeripherals.findIndex((p) => p.id === "HFXO"); - - if (hfxoIndex !== -1) { - // HFXO exists - convert it to oscillator type - const hfxo = mcuData.socPeripherals[hfxoIndex]; - hfxo.uiHint = "oscillator"; - hfxo.optional = false; - hfxo.alwaysPresent = true; - if (!hfxo.config) { - hfxo.config = { - loadCapacitors: "internal", - loadCapacitanceFemtofarad: 15000, - }; - } - } else { - // Add HFXO as required oscillator - mcuData.socPeripherals.push({ - id: "HFXO", - description: "High Frequency Crystal Oscillator", - uiHint: "oscillator", - optional: false, - alwaysPresent: true, - signals: [], - config: { - loadCapacitors: "internal", - loadCapacitanceFemtofarad: 15000, - }, - }); - } -} - -function autoSelectHFXO() { - // Remove any existing HFXO first (prevent duplicates) - const existingIndex = selectedPeripherals.findIndex((p) => p.id === "HFXO"); - if (existingIndex !== -1) { - selectedPeripherals.splice(existingIndex, 1); - } - - const hfxo = mcuData.socPeripherals.find((p) => p.id === "HFXO"); - if (hfxo) { - selectedPeripherals.push({ - id: "HFXO", - description: hfxo.description, - config: { ...hfxo.config }, - }); - updateSelectedPeripheralsList(); - } -} - -function organizePeripherals() { - const peripheralsListContainer = document.getElementById("peripherals-list"); - if (!peripheralsListContainer) return; - peripheralsListContainer.innerHTML = ""; - - if (!mcuData.socPeripherals) return; - - const checkboxPeripherals = []; - const oscillators = []; - const singleInstancePeripherals = []; - const multiInstanceGroups = {}; - - // First, separate out checkbox peripherals, oscillators, and group the rest - mcuData.socPeripherals.forEach((p) => { - if (p.uiHint === "oscillator") { - oscillators.push(p); - } else if (p.uiHint === "checkbox") { - checkboxPeripherals.push(p); - } else { - const baseName = p.id.replace(/\d+$/, ""); - if (!multiInstanceGroups[baseName]) { - multiInstanceGroups[baseName] = []; - } - multiInstanceGroups[baseName].push(p); - } - }); - - // Now separate single from multi-instance from the groups - for (const baseName in multiInstanceGroups) { - if (multiInstanceGroups[baseName].length === 1) { - singleInstancePeripherals.push(multiInstanceGroups[baseName][0]); - delete multiInstanceGroups[baseName]; - } - } - - // Sort the lists alphabetically - oscillators.sort((a, b) => a.id.localeCompare(b.id)); - checkboxPeripherals.sort((a, b) => a.id.localeCompare(b.id)); - singleInstancePeripherals.sort((a, b) => a.id.localeCompare(b.id)); - const sortedMultiInstanceKeys = Object.keys(multiInstanceGroups).sort(); - - // Render oscillators - oscillators.forEach((p) => { - const oscGroup = document.createElement("div"); - oscGroup.className = "oscillator-group"; - oscGroup.style.marginBottom = "10px"; - - const btn = document.createElement("button"); - btn.className = "single-peripheral-btn"; - btn.dataset.id = p.id; - btn.style.width = "100%"; - - // Check if oscillator is selected - const isSelected = selectedPeripherals.some((sp) => sp.id === p.id); - if (isSelected) { - btn.classList.add("selected"); - } - - // HFXO is always present, show as configured - if (p.id === "HFXO") { - btn.textContent = `${p.description} (Configure)`; - btn.addEventListener("click", () => openOscillatorConfig(p)); - } else { - // LFXO is optional - always opens config modal - btn.textContent = isSelected - ? `${p.description} (Configure)` - : `${p.description} (Add)`; - btn.addEventListener("click", () => openOscillatorConfig(p)); - } - - oscGroup.appendChild(btn); - peripheralsListContainer.appendChild(oscGroup); - }); - - // Render checkbox peripherals - checkboxPeripherals.forEach((p) => { - const checkboxGroup = document.createElement("div"); - checkboxGroup.className = "checkbox-group"; - - const label = document.createElement("label"); - label.className = "checkbox-label"; - - const checkbox = document.createElement("input"); - checkbox.type = "checkbox"; - checkbox.id = `${p.id.toLowerCase()}-checkbox`; - checkbox.dataset.peripheralId = p.id; - checkbox.addEventListener("change", toggleSimplePeripheral); - - const span = document.createElement("span"); - span.textContent = p.description; - - label.appendChild(checkbox); - label.appendChild(span); - - const description = document.createElement("div"); - description.className = "checkbox-description"; - description.textContent = `Uses ${p.signals.map((s) => s.allowedGpio.join("/")).join(", ")}`; - - checkboxGroup.appendChild(label); - checkboxGroup.appendChild(description); - peripheralsListContainer.appendChild(checkboxGroup); - }); - - // Render single-instance peripherals - singleInstancePeripherals.forEach((p) => { - const btn = document.createElement("button"); - btn.className = "single-peripheral-btn"; - btn.dataset.id = p.id; - btn.textContent = `${p.id} (${p.type})`; - btn.addEventListener("click", () => handlePeripheralClick(p)); - peripheralsListContainer.appendChild(btn); - }); - - // Render multi-instance peripherals - if (sortedMultiInstanceKeys.length > 0) { - const accordionContainer = document.createElement("div"); - accordionContainer.className = "accordion"; - - sortedMultiInstanceKeys.forEach((baseName) => { - const peripherals = multiInstanceGroups[baseName]; - const accordionItem = document.createElement("div"); - accordionItem.className = "accordion-item"; - const header = document.createElement("div"); - header.className = "accordion-header"; - header.innerHTML = `${baseName}`; - const content = document.createElement("div"); - content.className = "accordion-content"; - - peripherals - .sort((a, b) => a.id.localeCompare(b.id)) - .forEach((p) => { - const item = document.createElement("div"); - item.className = "peripheral-item"; - item.dataset.id = p.id; - item.innerHTML = `${p.id}`; - item.addEventListener("click", () => handlePeripheralClick(p)); - content.appendChild(item); - }); - - header.addEventListener("click", () => { - const isActive = header.classList.toggle("active"); - content.style.display = isActive ? "block" : "none"; - }); - - accordionItem.appendChild(header); - accordionItem.appendChild(content); - accordionContainer.appendChild(accordionItem); - }); - peripheralsListContainer.appendChild(accordionContainer); - } - - // Add GPIO allocation section - const gpioSection = document.createElement("div"); - gpioSection.className = "gpio-section"; - gpioSection.style.marginTop = "20px"; - gpioSection.style.paddingTop = "20px"; - gpioSection.style.paddingBottom = "10px"; - gpioSection.style.borderTop = "1px solid var(--border-color)"; - - const gpioHeader = document.createElement("h4"); - gpioHeader.textContent = "GPIO Pins"; - gpioHeader.style.marginBottom = "10px"; - gpioSection.appendChild(gpioHeader); - - const addGpioBtn = document.createElement("button"); - addGpioBtn.className = "single-peripheral-btn"; - addGpioBtn.textContent = "+ Add GPIO Pin"; - addGpioBtn.style.width = "100%"; - addGpioBtn.addEventListener("click", openGpioModal); - gpioSection.appendChild(addGpioBtn); - - peripheralsListContainer.appendChild(gpioSection); -} - -function handlePeripheralClick(peripheral) { - const isSelected = selectedPeripherals.some((p) => p.id === peripheral.id); - if (isSelected) { - editPeripheral(peripheral.id); - } else if (!hasAddressConflict(peripheral)) { - openPinSelectionModal(peripheral); - } else { - alert( - `Cannot select ${peripheral.id} because it shares the same address space (${peripheral.baseAddress}) with another selected peripheral.`, - ); - } -} - -// --- PIN LAYOUT AND DETAILS --- - -function createPinElement(pinInfo) { - const pinElement = document.createElement("div"); - pinElement.className = "pin"; - pinElement.dataset.number = pinInfo.packagePinId; - pinElement.dataset.name = pinInfo.name; - pinElement.textContent = pinInfo.packagePinId; - - if (pinInfo.isClockCapable) pinElement.classList.add("clock"); - const specialTypes = [ - "power_positive", - "power_ground", - "debug", - "crystal_hf", - "crystal_lf", - "rf_antenna", - ]; - if (specialTypes.includes(pinInfo.defaultType)) { - pinElement.classList.add(pinInfo.defaultType.replace("_", "-")); - } - - pinElement.addEventListener("click", () => showPinDetails(pinInfo)); - return pinElement; -} - -function createPinLayout() { - const chipContainer = document.querySelector(".chip-container"); - chipContainer.innerHTML = ""; - if (!mcuData.renderConfig || !mcuData.pins) return; - - const chipBody = document.createElement("div"); - chipBody.className = "chip-body"; - chipContainer.appendChild(chipBody); - - const strategy = mcuData.renderConfig.layoutStrategy; - const padding = mcuData.renderConfig.canvasDefaults?.padding || 20; - const containerSize = 400; - - if (strategy.layoutType === "quadPerimeter") { - const pinsBySide = { - left: mcuData.pins - .filter((p) => p.side === "left") - .sort((a, b) => parseInt(a.packagePinId) - parseInt(b.packagePinId)), - bottom: mcuData.pins - .filter((p) => p.side === "bottom") - .sort((a, b) => parseInt(a.packagePinId) - parseInt(b.packagePinId)), - right: mcuData.pins - .filter((p) => p.side === "right") - .sort((a, b) => parseInt(a.packagePinId) - parseInt(b.packagePinId)), - top: mcuData.pins - .filter((p) => p.side === "top") - .sort((a, b) => parseInt(a.packagePinId) - parseInt(b.packagePinId)), - }; - - const activeArea = containerSize - 2 * padding; - - const placePins = (side, pins) => { - const len = pins.length; - if (len === 0) return; - const spacing = activeArea / (len + 1); - - pins.forEach((pinInfo, index) => { - const pinElement = createPinElement(pinInfo); - const pos = padding + (index + 1) * spacing; - - switch (side) { - case "left": - pinElement.style.left = "0px"; - pinElement.style.top = pos + "px"; - pinElement.style.transform = "translate(-50%, -50%)"; - break; - case "bottom": - pinElement.style.bottom = "0px"; - pinElement.style.left = pos + "px"; - pinElement.style.transform = "translate(-50%, 50%)"; - break; - case "right": - pinElement.style.right = "0px"; - pinElement.style.top = containerSize - pos + "px"; - pinElement.style.transform = "translate(50%, -50%)"; - break; - case "top": - pinElement.style.top = "0px"; - pinElement.style.left = containerSize - pos + "px"; - pinElement.style.transform = "translate(-50%, -50%)"; - break; - } - chipContainer.appendChild(pinElement); - }); - }; - - placePins("left", pinsBySide.left); - placePins("bottom", pinsBySide.bottom); - placePins("right", pinsBySide.right); - placePins("top", pinsBySide.top); - } else if (strategy.layoutType === "gridMatrix") { - const { rowLabels, columnLabels } = strategy; - const activeArea = containerSize - 2 * padding; - - // Handle cases with a single row or column to avoid division by zero - const cellWidth = - columnLabels.length > 1 - ? activeArea / (columnLabels.length - 1) - : activeArea; - const cellHeight = - rowLabels.length > 1 ? activeArea / (rowLabels.length - 1) : activeArea; - - const pinMap = new Map(mcuData.pins.map((p) => [p.gridCoordinates, p])); - - for (let r = 0; r < rowLabels.length; r++) { - for (let c = 0; c < columnLabels.length; c++) { - const coord = `${rowLabels[r]}${columnLabels[c]}`; - - if (pinMap.has(coord)) { - const pinInfo = pinMap.get(coord); - const pinElement = createPinElement(pinInfo); - - pinElement.style.position = "absolute"; - // For a single item, center it. Otherwise, distribute along the axis. - const leftPos = - columnLabels.length > 1 - ? c * cellWidth + padding - : containerSize / 2; - const topPos = - rowLabels.length > 1 ? r * cellHeight + padding : containerSize / 2; - - pinElement.style.top = `${topPos}px`; - pinElement.style.left = `${leftPos}px`; - pinElement.style.transform = "translate(-50%, -50%)"; - - chipContainer.appendChild(pinElement); - } - } - } - } -} - -function showPinDetails(pinInfo) { - const detailsElement = document.getElementById("pinDetails"); - - let usedByHtml = ""; - if (usedPins[pinInfo.name]) { - const usage = usedPins[pinInfo.name]; - usedByHtml = ` - - Used by - ${usage.peripheral} (${usage.function}) - - `; - } - - const functions = pinInfo.functions || []; - const functionsHtml = - functions.length > 0 - ? ` - Functions - ${functions.join("
    ")} - ` - : ""; - - detailsElement.innerHTML = ` -

    ${pinInfo.name} (Pin ${pinInfo.packagePinId})

    - - - - - - - ${pinInfo.isClockCapable ? "" : ""} - ${usedByHtml} - ${functionsHtml} - -
    Type${pinInfo.defaultType}
    AttributeClock capable
    - `; -} - -// --- STATE MANAGEMENT --- - -function resetState() { - selectedPeripherals = []; - usedPins = {}; - usedAddresses = {}; - document - .querySelectorAll('input[type="checkbox"][data-peripheral-id]') - .forEach((cb) => { - cb.checked = false; - }); - if (mcuData.pins) { - setHFXtalAsSystemRequirement(); - } -} - -function clearAllPeripherals() { - if (!confirm("Are you sure you want to clear all peripherals?")) { - return; - } - resetState(); - updateSelectedPeripheralsList(); - updatePinDisplay(); - saveStateToLocalStorage(); -} - -// --- PERSISTENCE --- - -function getPersistenceKey() { - const mcu = document.getElementById("mcuSelector").value; - const pkg = document.getElementById("packageSelector").value; - if (!mcu || !pkg) return null; - return `pinPlannerConfig-${mcu}-${pkg}`; -} - -// Helper function to serialize peripheral data for export/persistence -function serializePeripheral(peripheral) { - if (peripheral.type === "GPIO") { - return { - id: peripheral.id, - type: peripheral.type, - label: peripheral.label, - pin: peripheral.pin, - activeState: peripheral.activeState, - }; - } - - // Regular peripherals (oscillators, UART, SPI, etc.) - return { - id: peripheral.id, - pinFunctions: peripheral.pinFunctions, - config: peripheral.config, - }; -} - -function saveStateToLocalStorage() { - const key = getPersistenceKey(); - if (!key) return; - - const config = { - selectedPeripherals: selectedPeripherals.map(serializePeripheral), - }; - localStorage.setItem(key, JSON.stringify(config)); - console.log(`State saved for ${key}`); -} - -function applyConfig(config) { - if (!config || !config.selectedPeripherals) return; - - for (const p_config of config.selectedPeripherals) { - // Handle GPIO pins first (they're not in socPeripherals) - if (p_config.type === "GPIO") { - selectedPeripherals.push({ - id: p_config.id, - type: p_config.type, - label: p_config.label, - pin: p_config.pin, - activeState: p_config.activeState, - }); - // Mark pin as used - usedPins[p_config.pin] = { - peripheral: p_config.id, - function: "GPIO", - required: true, - }; - continue; - } - - const p_data = mcuData.socPeripherals.find((p) => p.id === p_config.id); - if (p_data) { - // Handle oscillators - if (p_data.uiHint === "oscillator") { - selectedPeripherals.push({ - id: p_data.id, - description: p_data.description, - config: p_config.config || p_data.config, - }); - // Mark oscillator pins as used if they have signals - if (p_data.signals && p_data.signals.length > 0) { - p_data.signals.forEach((s) => { - if (s.allowedGpio && s.allowedGpio.length > 0) { - const pinName = s.allowedGpio[0]; - usedPins[pinName] = { - peripheral: p_data.id, - function: s.name, - required: s.isMandatory || true, - }; - } - }); - } - } - // Handle simple checkbox peripherals - else if (p_data.uiHint === "checkbox") { - const checkbox = document.getElementById( - `${p_data.id.toLowerCase()}-checkbox`, - ); - if (checkbox) checkbox.checked = true; - - const pinFunctions = {}; - p_data.signals.forEach((s) => { - const pinName = s.allowedGpio[0]; - usedPins[pinName] = { - peripheral: p_data.id, - function: s.name, - required: true, - }; - pinFunctions[pinName] = s.name; - }); - selectedPeripherals.push({ - id: p_data.id, - peripheral: p_data, - pinFunctions, - }); - } else { - // Handle modal-based peripherals - selectedPeripherals.push({ - id: p_data.id, - peripheral: p_data, - pinFunctions: p_config.pinFunctions, - }); - for (const pinName in p_config.pinFunctions) { - const signal = p_data.signals.find( - (s) => s.name === p_config.pinFunctions[pinName], - ); - usedPins[pinName] = { - peripheral: p_data.id, - function: p_config.pinFunctions[pinName], - required: signal ? signal.isMandatory : false, - }; - } - if (p_data.baseAddress) { - usedAddresses[p_data.baseAddress] = p_data.id; - } - } - } - } -} - -function loadStateFromLocalStorage() { - const key = getPersistenceKey(); - if (!key) return; - - const savedState = localStorage.getItem(key); - if (!savedState) { - console.log(`No saved state found for ${key}`); - return; - } - - try { - const config = JSON.parse(savedState); - applyConfig(config); - console.log(`State loaded for ${key}`); - } catch (error) { - console.error("Failed to load or parse saved state:", error); - localStorage.removeItem(key); - } -} - -function setHFXtalAsSystemRequirement() { - if (!mcuData.pins) return; - const hfxtalPins = mcuData.pins.filter((p) => p.defaultType === "crystal_hf"); - if (hfxtalPins.length === 2) { - usedPins[hfxtalPins[0].name] = { - peripheral: "32MHz Crystal", - function: "XC1", - isSystem: true, - }; - usedPins[hfxtalPins[1].name] = { - peripheral: "32MHz Crystal", - function: "XC2", - isSystem: true, - }; - } -} - -function toggleSimplePeripheral(event) { - const checkbox = event.target; - const peripheralId = checkbox.dataset.peripheralId; - const peripheral = mcuData.socPeripherals.find((p) => p.id === peripheralId); - - if (!peripheral) { - console.error( - `Peripheral with ID '${peripheralId}' not found in socPeripherals.`, - ); - return; - } - - const pinNames = peripheral.signals.map((s) => s.allowedGpio[0]); - - if (checkbox.checked) { - if (pinNames.some((pin) => usedPins[pin])) { - alert( - `One or more pins for ${peripheral.description} are already in use.`, - ); - checkbox.checked = false; - return; - } - const pinFunctions = {}; - peripheral.signals.forEach((s) => { - const pinName = s.allowedGpio[0]; - usedPins[pinName] = { - peripheral: peripheral.id, - function: s.name, - required: true, - }; - pinFunctions[pinName] = s.name; - }); - selectedPeripherals.push({ id: peripheral.id, peripheral, pinFunctions }); - } else { - pinNames.forEach((pin) => delete usedPins[pin]); - const index = selectedPeripherals.findIndex((p) => p.id === peripheral.id); - if (index !== -1) selectedPeripherals.splice(index, 1); - } - updateSelectedPeripheralsList(); - updatePinDisplay(); - saveStateToLocalStorage(); -} - -// --- PIN SELECTION MODAL --- - -function openPinSelectionModal( - peripheral, - existingPins = {}, - existingConfig = {}, -) { - currentPeripheral = peripheral; - tempSelectedPins = { ...existingPins }; // Pre-populate if editing - - document.getElementById("modalTitle").textContent = - `Select Pins for ${peripheral.id}`; - populatePinSelectionTable(peripheral); - - // Show/hide UART config section based on peripheral type - const uartConfigSection = document.getElementById("uartConfigSection"); - const uartDisableRxCheckbox = document.getElementById("uartDisableRx"); - if (peripheral.type === "UART") { - uartConfigSection.style.display = "block"; - uartDisableRxCheckbox.checked = existingConfig.disableRx || false; - // Add event listener to update RXD required status - uartDisableRxCheckbox.onchange = updateRxdRequiredStatus; - // Update initial state - updateRxdRequiredStatus(); - } else { - uartConfigSection.style.display = "none"; - uartDisableRxCheckbox.checked = false; - uartDisableRxCheckbox.onchange = null; - } - - // Show/hide SPI config section based on peripheral type - const spiConfigSection = document.getElementById("spiConfigSection"); - if (peripheral.type === "SPI") { - spiConfigSection.style.display = "block"; - initSpiCsGpioList(existingConfig.extraCsGpios || []); - } else { - spiConfigSection.style.display = "none"; - } - - // Show/hide note section for SPI, I2C, and UART peripherals - const noteSection = document.getElementById("peripheralNoteSection"); - const noteInput = document.getElementById("peripheralNote"); - if (["SPI", "I2C", "UART"].includes(peripheral.type)) { - noteSection.style.display = "block"; - noteInput.value = existingConfig.note || ""; - } else { - noteSection.style.display = "none"; - noteInput.value = ""; - } - - document.getElementById("pinSelectionModal").style.display = "block"; -} - -function closePinSelectionModal() { - document.getElementById("pinSelectionModal").style.display = "none"; - currentPeripheral = null; - tempSelectedPins = {}; -} - -function updateRxdRequiredStatus() { - const disableRxChecked = document.getElementById("uartDisableRx").checked; - const tableBody = document.getElementById("pinSelectionTableBody"); - const rows = tableBody.querySelectorAll("tr"); - - rows.forEach((row) => { - const functionCell = row.querySelector("td:first-child"); - const requiredCell = row.querySelector("td:nth-child(2)"); - if (functionCell && requiredCell && functionCell.textContent === "RXD") { - requiredCell.textContent = disableRxChecked ? "No" : "Yes"; - } - }); -} - -// --- SPI CS GPIO MANAGEMENT --- - -let tempSpiCsGpios = []; - -function initSpiCsGpioList(existingCsGpios) { - tempSpiCsGpios = [...existingCsGpios]; - renderSpiCsGpioList(); -} - -function renderSpiCsGpioList() { - const container = document.getElementById("spiCsGpioList"); - container.innerHTML = ""; - - tempSpiCsGpios.forEach((gpio, index) => { - // Get list of other CS GPIOs (exclude current one from the exclude list) - const otherCsGpios = tempSpiCsGpios.filter((g, i) => i !== index && g); - - const row = document.createElement("div"); - row.style.cssText = - "display: flex; align-items: center; gap: 10px; margin-bottom: 8px;"; - row.innerHTML = ` - - - `; - container.appendChild(row); - }); - - // Attach event listeners - container.querySelectorAll("select").forEach((select) => { - select.addEventListener("change", (e) => { - const index = parseInt(e.target.dataset.csIndex); - tempSpiCsGpios[index] = e.target.value; - // Re-render to update disabled states - renderSpiCsGpioList(); - }); - enableScrollWheelSelectionForElement(select); - }); - - container.querySelectorAll(".remove-cs-btn").forEach((btn) => { - btn.addEventListener("click", (e) => { - const index = parseInt(e.target.dataset.csIndex); - tempSpiCsGpios.splice(index, 1); - renderSpiCsGpioList(); - }); - }); -} - -function getGpioPinOptions(selectedPin, filterUsed = false, excludePins = []) { - if (!mcuData.pins) return ''; - - const gpioPins = mcuData.pins - .filter( - (pin) => - Array.isArray(pin.functions) && pin.functions.includes("Digital I/O"), - ) - .sort((a, b) => { - const aMatch = a.name.match(/P(\d+)\.(\d+)/); - const bMatch = b.name.match(/P(\d+)\.(\d+)/); - if (!aMatch || !bMatch) return a.name.localeCompare(b.name); - const aPort = parseInt(aMatch[1]); - const bPort = parseInt(bMatch[1]); - const aPin = parseInt(aMatch[2]); - const bPin = parseInt(bMatch[2]); - if (aPort !== bPort) return aPort - bPort; - return aPin - bPin; - }); - - let options = ''; - gpioPins.forEach((pin) => { - const isSelected = pin.name === selectedPin; - - // Check if pin should be disabled - let isDisabled = false; - if (filterUsed && !isSelected) { - // Check if used by other peripherals - if (usedPins[pin.name]) { - isDisabled = true; - } - // Check if used by pins selected in the current modal - if (tempSelectedPins[pin.name]) { - isDisabled = true; - } - // Check if in the exclude list (other CS pins in the same list) - if (excludePins.includes(pin.name)) { - isDisabled = true; - } - } - - options += ``; - }); - return options; -} - -function addSpiCsGpio() { - tempSpiCsGpios.push(""); - renderSpiCsGpioList(); -} - -// Set up SPI CS GPIO add button listener -document - .getElementById("addSpiCsGpioBtn") - .addEventListener("click", addSpiCsGpio); - -// --- GPIO PIN ALLOCATION --- - -let gpioTableRows = []; // Track GPIO table rows -let nextGpioRowId = 1; // Unique ID counter for GPIO rows - -function openGpioModal() { - // Initialize table with existing GPIO pins - initializeGpioTable(); - - const errorEl = document.getElementById("gpioError"); - errorEl.style.display = "none"; - - document.getElementById("gpioModal").style.display = "block"; -} - -function initializeGpioTable() { - gpioTableRows = []; - nextGpioRowId = 1; - - // Add existing GPIO pins to the table - selectedPeripherals.filter(p => p.type === "GPIO").forEach(gpio => { - addGpioTableRow(gpio.label, gpio.pin, gpio.activeState, gpio.id); - }); - - // Add one empty row if no GPIO pins exist - if (gpioTableRows.length === 0) { - addGpioTableRow(); - } - - renderGpioTable(); -} - -function addGpioTableRow(label = "", pin = "", activeState = "active-high", existingId = null) { - const rowId = `gpio_row_${nextGpioRowId++}`; - gpioTableRows.push({ - id: rowId, - label: label, - pin: pin, - activeState: activeState, - existingId: existingId, // If editing existing GPIO - isValid: true - }); - renderGpioTable(); -} - -function removeGpioTableRow(rowId) { - const index = gpioTableRows.findIndex(row => row.id === rowId); - if (index !== -1) { - gpioTableRows.splice(index, 1); - renderGpioTable(); - } -} - -function renderGpioTable() { - const tableBody = document.getElementById("gpioTableBody"); - - if (gpioTableRows.length === 0) { - tableBody.innerHTML = ` - - No GPIO pins configured. Click "Add GPIO Pin" to add one. - - `; - return; - } - - tableBody.innerHTML = ""; - - gpioTableRows.forEach((row, index) => { - const tr = document.createElement("tr"); - tr.innerHTML = ` - - - - - - - - - - - - - `; - tableBody.appendChild(tr); - }); - - // Add event listeners for input changes - attachGpioTableEventListeners(); -} - -function attachGpioTableEventListeners() { - // Handle input changes - document.querySelectorAll('#gpioTable input, #gpioTable select').forEach(element => { - element.addEventListener('input', handleGpioTableInputChange); - element.addEventListener('change', handleGpioTableInputChange); - - // Add blur event for label fields to convert to lowercase when done typing - if (element.type === 'text' && element.getAttribute('data-field') === 'label') { - element.addEventListener('blur', (event) => { - const value = event.target.value.toLowerCase(); - if (event.target.value !== value) { - event.target.value = value; - handleGpioTableInputChange(event); - } - }); - } - }); -} - -function handleGpioTableInputChange(event) { - const rowId = event.target.getAttribute('data-row-id'); - const field = event.target.getAttribute('data-field'); - const value = event.target.value; - - // Update the row data - const row = gpioTableRows.find(r => r.id === rowId); - if (row) { - row[field] = value; - - // Validate the row - validateGpioRow(row); - - // Re-render if pin selection changed (to update other dropdowns) - if (field === 'pin') { - renderGpioTable(); - } else { - // Just update validation styling for this input - updateInputValidation(event.target, row); - } - } -} - -function updateInputValidation(inputElement, row) { - if (row.isValid) { - inputElement.classList.remove('validation-error'); - } else { - inputElement.classList.add('validation-error'); - } -} - -function validateGpioRow(row) { - row.isValid = true; - - // Validate label - if (!row.label || !/^[a-z0-9_]+$/.test(row.label)) { - row.isValid = false; - return; - } - - // Check for duplicate labels - const duplicateLabel = gpioTableRows.find(r => - r.id !== row.id && - r.label === row.label && - r.label !== "" - ); - if (duplicateLabel) { - row.isValid = false; - return; - } - - // Validate pin selection - if (!row.pin) { - row.isValid = false; - return; - } - - // Check for duplicate pin usage - const duplicatePin = gpioTableRows.find(r => - r.id !== row.id && - r.pin === row.pin && - r.pin !== "" - ); - if (duplicatePin) { - row.isValid = false; - return; - } -} - -function getGpioPinOptionsForTable(selectedPin, existingGpioId) { - let options = ""; - - if (!mcuData.pins) return options; - - const gpioPins = mcuData.pins.filter(pin => - pin.functions && pin.functions.includes("Digital I/O") - ); - - // Sort pins by port and pin number (P0.00, P0.01, ..., P1.00, P1.01, ...) - gpioPins.sort((a, b) => { - const aMatch = a.name.match(/P(\d+)\.(\d+)/); - const bMatch = b.name.match(/P(\d+)\.(\d+)/); - - if (aMatch && bMatch) { - const aPort = parseInt(aMatch[1]); - const bPort = parseInt(bMatch[1]); - const aPin = parseInt(aMatch[2]); - const bPin = parseInt(bMatch[2]); - - // Sort by port first, then by pin number - if (aPort !== bPort) { - return aPort - bPort; - } - return aPin - bPin; - } - - // Fallback to string comparison - return a.name.localeCompare(b.name); - }); - - gpioPins.forEach(pin => { - const isSelected = pin.name === selectedPin; - let isDisabled = false; - - // Check if pin is used by other peripherals (not GPIO) - if (usedPins[pin.name] && !usedPins[pin.name].peripheral.startsWith("GPIO_")) { - isDisabled = true; - } - - // Check if pin is used by other GPIO entries in selected peripherals - const gpioUsingPin = selectedPeripherals.find(p => - p.type === "GPIO" && - p.pin === pin.name && - p.id !== existingGpioId - ); - if (gpioUsingPin && !isSelected) { - isDisabled = true; - } - - options += ``; - }); - - return options; -} - -function closeGpioModal() { - document.getElementById("gpioModal").style.display = "none"; - gpioTableRows = []; -} - -function confirmGpioModal() { - const errorEl = document.getElementById("gpioError"); - - // Validate all rows - let hasErrors = false; - const validRows = []; - const errorMessages = []; - - gpioTableRows.forEach(row => { - validateGpioRow(row); - if (!row.isValid && row.label && row.pin) { - hasErrors = true; - // Identify specific error - if (!/^[a-z0-9_]+$/.test(row.label)) { - errorMessages.push(`"${row.label}": Label must be lowercase letters, numbers, and underscores only`); - } else if (gpioTableRows.find(r => r.id !== row.id && r.label === row.label && r.label !== "")) { - errorMessages.push(`"${row.label}": Duplicate label`); - } else if (gpioTableRows.find(r => r.id !== row.id && r.pin === row.pin && r.pin !== "")) { - errorMessages.push(`${row.pin}: Pin used multiple times`); - } - } else if (row.label && row.pin && row.isValid) { - validRows.push(row); - } - }); - - if (hasErrors) { - const uniqueMessages = [...new Set(errorMessages)]; - errorEl.textContent = "Validation errors: " + uniqueMessages.join("; "); - errorEl.style.display = "block"; - renderGpioTable(); // Re-render to show validation errors - return; - } - - if (validRows.length === 0) { - errorEl.textContent = "Please add at least one GPIO pin or cancel"; - errorEl.style.display = "block"; - return; - } - - // Remove existing GPIO pins - selectedPeripherals.filter(p => p.type === "GPIO").forEach(gpio => { - delete usedPins[gpio.pin]; - }); - selectedPeripherals = selectedPeripherals.filter(p => p.type !== "GPIO"); - - // Add new/updated GPIO pins - validRows.forEach(row => { - const gpioId = `GPIO_${row.label.toUpperCase()}`; - - selectedPeripherals.push({ - id: gpioId, - type: "GPIO", - label: row.label, - pin: row.pin, - activeState: row.activeState, - }); - - usedPins[row.pin] = { - peripheral: gpioId, - function: "GPIO", - required: true, - }; - }); - - updateSelectedPeripheralsList(); - updatePinDisplay(); - closeGpioModal(); - saveStateToLocalStorage(); -} - -// Set up GPIO modal event listeners -document - .getElementById("closeGpioModal") - .addEventListener("click", closeGpioModal); -document - .getElementById("cancelGpioModal") - .addEventListener("click", closeGpioModal); -document - .getElementById("confirmGpioModal") - .addEventListener("click", confirmGpioModal); -document - .getElementById("addGpioRow") - .addEventListener("click", () => addGpioTableRow()); -document.getElementById("gpioModal").addEventListener("click", (e) => { - if (e.target === document.getElementById("gpioModal")) { - closeGpioModal(); - } -}); - -function populatePinSelectionTable(peripheral) { - const tableBody = document.getElementById("pinSelectionTableBody"); - tableBody.innerHTML = ""; - - peripheral.signals.forEach((signal) => { - const row = document.createElement("tr"); - const allPossiblePins = getPinsForSignal(signal); - - let selectionHtml; - if (allPossiblePins.length === 1 && signal.allowedGpio.length === 1) { - const pin = allPossiblePins[0]; - selectionHtml = ``; - } else { - let optionsHtml = ''; - allPossiblePins.forEach((pin) => { - // Show all pins - updateModalPinAvailability() will handle disabling used ones - const isSelected = tempSelectedPins[pin.name] === signal.name; - optionsHtml += ``; - }); - selectionHtml = ``; - } - - row.innerHTML = ` - ${signal.name} - ${signal.isMandatory ? "Yes" : "No"} - ${selectionHtml} - ${signal.description || ""} - `; - tableBody.appendChild(row); - }); - - tableBody - .querySelectorAll('select, input[type="checkbox"]') - .forEach((input) => { - input.addEventListener("change", handlePinSelectionChange); - }); - - // Add scroll-wheel selection to dropdowns in the modal - tableBody.querySelectorAll("select").forEach((select) => { - enableScrollWheelSelectionForElement(select); - }); - - updateModalPinAvailability(); // Set initial disabled states -} - -function handlePinSelectionChange(event) { - const input = event.target; - const signalName = input.dataset.signal; - - // Clear the old pin for this signal, if any - Object.keys(tempSelectedPins).forEach((pin) => { - if (tempSelectedPins[pin] === signalName) { - delete tempSelectedPins[pin]; - } - }); - - // Set the new pin - if (input.type === "checkbox") { - if (input.checked) { - tempSelectedPins[input.dataset.pin] = signalName; - } - } else { - // Dropdown - if (input.value) { - tempSelectedPins[input.value] = signalName; - } - } - - updateModalPinAvailability(); -} - -function updateModalPinAvailability() { - const selects = document.querySelectorAll("#pinSelectionTableBody select"); - const checkboxes = document.querySelectorAll( - '#pinSelectionTableBody input[type="checkbox"]', - ); - - const pinsSelectedInModal = Object.keys(tempSelectedPins); - - // Update dropdowns - selects.forEach((select) => { - const signalForThisSelect = select.dataset.signal; - for (const option of select.options) { - const pinName = option.value; - if (!pinName) continue; - - const isUsedByOtherPeripheral = - usedPins[pinName] && - usedPins[pinName].peripheral !== currentPeripheral.id; - const isUsedInThisModal = - pinsSelectedInModal.includes(pinName) && - tempSelectedPins[pinName] !== signalForThisSelect; - - option.disabled = isUsedByOtherPeripheral || isUsedInThisModal; - } - }); - - // Update checkboxes - checkboxes.forEach((checkbox) => { - const pinName = checkbox.dataset.pin; - const signalForThisCheckbox = checkbox.dataset.signal; - - const isUsedByOtherPeripheral = - usedPins[pinName] && - usedPins[pinName].peripheral !== currentPeripheral.id; - const isUsedInThisModal = - pinsSelectedInModal.includes(pinName) && - tempSelectedPins[pinName] !== signalForThisCheckbox; - - checkbox.disabled = isUsedByOtherPeripheral || isUsedInThisModal; - }); -} - -function getPinsForSignal(signal) { - if (!mcuData.pins) return []; - const pins = mcuData.pins.filter((pin) => { - if (!Array.isArray(pin.functions) || !pin.functions.includes("Digital I/O")) - return false; - if (signal.requiresClockCapablePin && !pin.isClockCapable) return false; - return signal.allowedGpio.some((allowed) => - allowed.endsWith("*") - ? pin.port === allowed.slice(0, -1) - : pin.name === allowed, - ); - }); - - // Sort pins in ascending order: P0.00, P0.01, ..., P1.00, P1.01, ..., P2.00, etc. - return pins.sort((a, b) => { - const aMatch = a.name.match(/P(\d+)\.(\d+)/); - const bMatch = b.name.match(/P(\d+)\.(\d+)/); - - if (!aMatch || !bMatch) return a.name.localeCompare(b.name); - - const aPort = parseInt(aMatch[1]); - const bPort = parseInt(bMatch[1]); - const aPin = parseInt(aMatch[2]); - const bPin = parseInt(bMatch[2]); - - // First sort by port, then by pin number - if (aPort !== bPort) return aPort - bPort; - return aPin - bPin; - }); -} - -function confirmPinSelection() { - // Check if Disable RX is checked for UART - if so, RXD is not mandatory - const disableRxChecked = - currentPeripheral.type === "UART" && - document.getElementById("uartDisableRx").checked; - - const missingSignals = currentPeripheral.signals.filter((s) => { - // Skip RXD requirement if Disable RX is checked - if (disableRxChecked && s.name === "RXD") return false; - return s.isMandatory && !Object.values(tempSelectedPins).includes(s.name); - }); - - if (missingSignals.length > 0) { - alert( - `Please select pins for mandatory functions: ${missingSignals.map((s) => s.name).join(", ")}`, - ); - return; - } - - for (const pinName in tempSelectedPins) { - if ( - usedPins[pinName] && - usedPins[pinName].peripheral !== currentPeripheral.id - ) { - alert( - `Pin ${pinName} is already used by ${usedPins[pinName].peripheral}.`, - ); - return; - } - } - - const existingIndex = selectedPeripherals.findIndex( - (p) => p.id === currentPeripheral.id, - ); - if (existingIndex !== -1) { - const oldPeripheral = selectedPeripherals[existingIndex]; - for (const pinName in oldPeripheral.pinFunctions) { - delete usedPins[pinName]; - } - selectedPeripherals.splice(existingIndex, 1); - } - - // Build peripheral entry with optional config - const peripheralEntry = { - id: currentPeripheral.id, - peripheral: currentPeripheral, - pinFunctions: { ...tempSelectedPins }, - }; - - // Add UART-specific config if applicable - if (currentPeripheral.type === "UART") { - const disableRx = document.getElementById("uartDisableRx").checked; - if (disableRx) { - peripheralEntry.config = { disableRx: true }; - } - } - - // Add SPI-specific config if applicable - if (currentPeripheral.type === "SPI") { - const validCsGpios = tempSpiCsGpios.filter( - (gpio) => gpio && gpio.trim() !== "", - ); - if (validCsGpios.length > 0) { - peripheralEntry.config = peripheralEntry.config || {}; - peripheralEntry.config.extraCsGpios = validCsGpios; - } - } - - // Add note for SPI, I2C, and UART peripherals - if (["SPI", "I2C", "UART"].includes(currentPeripheral.type)) { - const note = document.getElementById("peripheralNote").value.trim(); - if (note) { - peripheralEntry.config = peripheralEntry.config || {}; - peripheralEntry.config.note = note; - } - } - - selectedPeripherals.push(peripheralEntry); - - for (const pinName in tempSelectedPins) { - usedPins[pinName] = { - peripheral: currentPeripheral.id, - function: tempSelectedPins[pinName], - required: currentPeripheral.signals.find( - (s) => s.name === tempSelectedPins[pinName], - ).isMandatory, - }; - } - if (currentPeripheral.baseAddress) { - usedAddresses[currentPeripheral.baseAddress] = currentPeripheral.id; - } - - updateSelectedPeripheralsList(); - updatePinDisplay(); - closePinSelectionModal(); - saveStateToLocalStorage(); -} - -// --- OSCILLATOR CONFIGURATION --- - -let currentOscillator = null; - -function openOscillatorConfig(oscillator) { - currentOscillator = oscillator; - - document.getElementById("oscillatorModalTitle").textContent = - `Configure ${oscillator.description}`; - - // Get current config if oscillator is already selected - const existingConfig = selectedPeripherals.find( - (p) => p.id === oscillator.id, - ); - const config = existingConfig ? existingConfig.config : oscillator.config; - - // Set radio buttons - const internalRadio = document.getElementById("oscillatorCapInternal"); - const externalRadio = document.getElementById("oscillatorCapExternal"); - - internalRadio.checked = config.loadCapacitors === "internal"; - externalRadio.checked = config.loadCapacitors === "external"; - - // Populate load capacitance dropdown based on oscillator type - const loadCapSelect = document.getElementById("oscillatorLoadCapacitance"); - loadCapSelect.innerHTML = ""; - - const template = deviceTreeTemplates - ? deviceTreeTemplates[oscillator.id] - : null; - let min, max, step; - - if (template && template.loadCapacitanceRange) { - min = template.loadCapacitanceRange.min; - max = template.loadCapacitanceRange.max; - step = template.loadCapacitanceRange.step; - } else { - // Default ranges - if (oscillator.id === "LFXO") { - min = 4000; - max = 18000; - step = 500; - } else { - // HFXO - min = 4000; - max = 17000; - step = 250; - } - } - - for (let i = min; i <= max; i += step) { - const option = document.createElement("option"); - option.value = i; - option.textContent = `${(i / 1000).toFixed(step === 250 ? 2 : 1)} pF (${i} fF)`; - if ( - config.loadCapacitanceFemtofarad && - i === config.loadCapacitanceFemtofarad - ) { - option.selected = true; - } - loadCapSelect.appendChild(option); - } - - // Enable scroll wheel selection on the dropdown - enableScrollWheelSelectionForElement(loadCapSelect); - - // Set up event listeners for capacitor radio buttons - const toggleLoadCapacitance = () => { - const isInternal = internalRadio.checked; - loadCapSelect.disabled = !isInternal; - }; - - internalRadio.onchange = toggleLoadCapacitance; - externalRadio.onchange = toggleLoadCapacitance; - - // Initial state - toggleLoadCapacitance(); - - document.getElementById("oscillatorConfigModal").style.display = "block"; -} - -function closeOscillatorConfig() { - document.getElementById("oscillatorConfigModal").style.display = "none"; - currentOscillator = null; -} - -function confirmOscillatorConfig() { - if (!currentOscillator) return; - - const loadCapacitors = document.querySelector( - 'input[name="oscillatorCapacitors"]:checked', - ).value; - - const config = { - loadCapacitors, - }; - - // Only include load capacitance if internal - if (loadCapacitors === "internal") { - config.loadCapacitanceFemtofarad = parseInt( - document.getElementById("oscillatorLoadCapacitance").value, - ); - } - - // Remove ALL existing instances of this oscillator (prevent duplicates) - let removed = false; - do { - const existingIndex = selectedPeripherals.findIndex( - (p) => p.id === currentOscillator.id, - ); - if (existingIndex !== -1) { - selectedPeripherals.splice(existingIndex, 1); - removed = true; - } else { - removed = false; - } - } while (removed); - - // Clear pins used by this oscillator - if (currentOscillator.signals && currentOscillator.signals.length > 0) { - currentOscillator.signals.forEach((s) => { - if (s.allowedGpio && s.allowedGpio.length > 0) { - const pinName = s.allowedGpio[0]; - if ( - usedPins[pinName] && - usedPins[pinName].peripheral === currentOscillator.id - ) { - delete usedPins[pinName]; - } - } - }); - } - - // Add oscillator with configuration - selectedPeripherals.push({ - id: currentOscillator.id, - description: currentOscillator.description, - config, - }); - - // Mark oscillator pins as used - if (currentOscillator.signals && currentOscillator.signals.length > 0) { - currentOscillator.signals.forEach((s) => { - if (s.allowedGpio && s.allowedGpio.length > 0) { - const pinName = s.allowedGpio[0]; - usedPins[pinName] = { - peripheral: currentOscillator.id, - function: s.name, - required: s.isMandatory || true, - }; - } - }); - } - - updateSelectedPeripheralsList(); - organizePeripherals(); // Refresh to update button text - updatePinDisplay(); // Update pin display to show pins as used - closeOscillatorConfig(); - saveStateToLocalStorage(); -} - -// --- UI UPDATES --- - -function updateSelectedPeripheralsList() { - const selectedList = document.getElementById("selectedList"); - selectedList.innerHTML = ""; - - if (selectedPeripherals.length === 0) { - selectedList.innerHTML = - '
  • No peripherals selected yet.
  • '; - return; - } - - // Remove duplicates (safety check) - const uniquePeripherals = []; - const seenIds = new Set(); - selectedPeripherals.forEach((p) => { - if (!seenIds.has(p.id)) { - seenIds.add(p.id); - uniquePeripherals.push(p); - } - }); - - // Update the actual array if we found duplicates - if (uniquePeripherals.length !== selectedPeripherals.length) { - selectedPeripherals.length = 0; - uniquePeripherals.forEach((p) => selectedPeripherals.push(p)); - } - - selectedPeripherals.forEach((p) => { - const item = document.createElement("li"); - item.className = "selected-item"; - - let details; - if (p.type === "GPIO") { - // GPIO pin - show pin and active state - const activeLabel = - p.activeState === "active-low" ? "active-low" : "active-high"; - details = `${p.pin} (${activeLabel})`; - } else if (p.config && p.config.loadCapacitors) { - // Oscillator - show configuration - const capLabel = - p.config.loadCapacitors === "internal" ? "Internal" : "External"; - if (p.config.loadCapacitors === "external") { - details = `${capLabel} caps`; - } else { - const capValue = (p.config.loadCapacitanceFemtofarad / 1000).toFixed( - p.id === "HFXO" ? 2 : 1, - ); - details = `${capLabel} caps, ${capValue} pF`; - } - - // Add pin info for oscillators with signals (like LFXO) - const oscData = mcuData.socPeripherals.find((sp) => sp.id === p.id); - if (oscData && oscData.signals && oscData.signals.length > 0) { - const pins = oscData.signals - .filter((s) => s.allowedGpio && s.allowedGpio.length > 0) - .map((s) => s.allowedGpio[0]) - .join(", "); - if (pins) { - details += ` (${pins})`; - } - } - } else { - // Regular peripheral - show pin assignments - details = - Object.entries(p.pinFunctions || {}) - .map(([pin, func]) => `${pin}: ${func}`) - .join(", ") || "Auto-assigned"; - - // Add UART config info if applicable - if (p.config && p.config.disableRx) { - details += " [RX disabled]"; - } - - // Add SPI extra CS GPIOs info if applicable - if ( - p.config && - p.config.extraCsGpios && - p.config.extraCsGpios.length > 0 - ) { - details += ` [+${p.config.extraCsGpios.length} CS: ${p.config.extraCsGpios.join(", ")}]`; - } - } - - const removeBtn = - p.id === "HFXO" - ? "" - : ``; - - // Use label for GPIO pins, id for everything else - // Include note for SPI/I2C/UART if present - let displayName = p.type === "GPIO" ? `GPIO: ${p.label}` : p.id; - if ( - p.config && - p.config.note && - ["SPI", "I2C", "UART"].includes(p.peripheral?.type) - ) { - displayName += `: ${p.config.note}`; - } - - item.innerHTML = ` -
    ${displayName}
    ${details}
    - ${removeBtn} - `; - - if (p.id !== "HFXO") { - item.querySelector(".remove-btn").addEventListener("click", (e) => { - e.stopPropagation(); - removePeripheral(p.id); - }); - } - - item.addEventListener("click", () => editPeripheral(p.id)); - selectedList.appendChild(item); - }); -} - -function updatePinDisplay() { - document.querySelectorAll(".pin").forEach((pinElement) => { - const pinName = pinElement.dataset.name; - pinElement.classList.remove("used", "required", "system"); - if (usedPins[pinName]) { - pinElement.classList.add("used"); - if (usedPins[pinName].required) pinElement.classList.add("required"); - if (usedPins[pinName].isSystem) pinElement.classList.add("system"); - } - }); - updatePeripheralConflictUI(); -} - -function updatePeripheralConflictUI() { - document.querySelectorAll("[data-id]").forEach((el) => { - const id = el.dataset.id; - if (!mcuData.socPeripherals) return; - const p = mcuData.socPeripherals.find((p) => p.id === id); - if ( - p && - hasAddressConflict(p) && - !selectedPeripherals.some((sp) => sp.id === id) - ) { - el.classList.add("disabled"); - } else { - el.classList.remove("disabled"); - } - }); -} - -function hasAddressConflict(peripheral) { - return peripheral.baseAddress && usedAddresses[peripheral.baseAddress]; -} - -function removePeripheral(id) { - const index = selectedPeripherals.findIndex((p) => p.id === id); - if (index === -1) return; - - const peripheral = selectedPeripherals[index]; - const peripheralData = mcuData.socPeripherals.find((p) => p.id === id); - - // For checkbox-based (simple) peripherals, uncheck the box - const checkbox = document.getElementById(`${id.toLowerCase()}-checkbox`); - if (checkbox) { - checkbox.checked = false; - } - - // Handle GPIO pins - if (peripheral.type === "GPIO") { - if (peripheral.pin && usedPins[peripheral.pin]) { - delete usedPins[peripheral.pin]; - } - } - // Handle oscillators - clear their signal pins - else if (peripheralData && peripheralData.uiHint === "oscillator") { - if (peripheralData.signals && peripheralData.signals.length > 0) { - peripheralData.signals.forEach((s) => { - if (s.allowedGpio && s.allowedGpio.length > 0) { - const pinName = s.allowedGpio[0]; - if (usedPins[pinName] && usedPins[pinName].peripheral === id) { - delete usedPins[pinName]; - } - } - }); - } - } else { - // Handle regular peripherals with pinFunctions - for (const pinName in peripheral.pinFunctions) { - delete usedPins[pinName]; - } - } - - if (peripheral.peripheral && peripheral.peripheral.baseAddress) { - delete usedAddresses[peripheral.peripheral.baseAddress]; - } - selectedPeripherals.splice(index, 1); - - updateSelectedPeripheralsList(); - organizePeripherals(); // Re-render to update button states - updatePinDisplay(); - saveStateToLocalStorage(); -} - -function editPeripheral(id) { - // Handle GPIO pins - open the table modal with all GPIO pins - const gpioPeripheral = selectedPeripherals.find( - (p) => p.id === id && p.type === "GPIO", - ); - if (gpioPeripheral) { - openGpioModal(); // Open modal with all GPIO pins - return; - } - - const peripheralData = mcuData.socPeripherals.find((p) => p.id === id); - if (!peripheralData) return; - - // Handle oscillators - if (peripheralData.uiHint === "oscillator") { - openOscillatorConfig(peripheralData); - return; - } - - // Checkbox peripherals are not editable via modal - if (peripheralData.uiHint === "checkbox") { - return; - } - - const selected = selectedPeripherals.find((p) => p.id === id); - if (!selected) return; - openPinSelectionModal( - selected.peripheral, - selected.pinFunctions, - selected.config || {}, - ); -} - -// --- BOARD DEFINITION EXPORT --- - -let deviceTreeTemplates = null; // Will be loaded per-MCU -let boardInfo = null; // Stores board metadata from user input - -async function loadDeviceTreeTemplates(mcuId) { - try { - const response = await fetch(`mcus/${mcuId}/devicetree-templates.json`); - if (!response.ok) { - console.warn(`No DeviceTree templates found for ${mcuId}`); - return null; - } - const data = await response.json(); - return data.templates; - } catch (error) { - console.error("Failed to load DeviceTree templates:", error); - return null; - } -} - -// Helper function to parse pin names like "P1.05" into {port: 1, pin: 5} -function parsePinName(pinName) { - const match = pinName.match(/P(\d+)\.(\d+)/); - if (!match) return null; - return { - port: parseInt(match[1]), - pin: parseInt(match[2]), - name: pinName, - }; -} - -function openBoardInfoModal() { - if (selectedPeripherals.length === 0) { - alert("No peripherals selected. Please select peripherals first."); - return; - } - - // Set up inline validation for board name fields - setupBoardNameValidation(); - - document.getElementById("boardInfoModal").style.display = "block"; - document.getElementById("boardInfoError").style.display = "none"; -} - -function setupBoardNameValidation() { - const boardNameInput = document.getElementById("boardNameInput"); - const boardVendorInput = document.getElementById("boardVendorInput"); - - // Create or get validation error elements - let boardNameError = document.getElementById("boardNameInputError"); - if (!boardNameError) { - boardNameError = document.createElement("small"); - boardNameError.id = "boardNameInputError"; - boardNameError.style.color = "var(--error-color)"; - boardNameError.style.display = "none"; - boardNameError.style.marginTop = "4px"; - boardNameInput.parentElement.appendChild(boardNameError); - } - - let vendorError = document.getElementById("boardVendorInputError"); - if (!vendorError) { - vendorError = document.createElement("small"); - vendorError.id = "boardVendorInputError"; - vendorError.style.color = "var(--error-color)"; - vendorError.style.display = "none"; - vendorError.style.marginTop = "4px"; - boardVendorInput.parentElement.appendChild(vendorError); - } - - // Validation function - const validateInput = (input, errorElement) => { - const pattern = /^[a-z0-9_]+$/; - const value = input.value.trim(); - - if (value && !pattern.test(value)) { - errorElement.textContent = - "Only lowercase letters, numbers, and underscores allowed"; - errorElement.style.display = "block"; - input.style.borderColor = "var(--error-color)"; - return false; - } else { - errorElement.style.display = "none"; - input.style.borderColor = "var(--border-color)"; - return true; - } - }; - - // Add event listeners - boardNameInput.addEventListener("input", () => - validateInput(boardNameInput, boardNameError), - ); - boardVendorInput.addEventListener("input", () => - validateInput(boardVendorInput, vendorError), - ); -} - -function closeBoardInfoModal() { - document.getElementById("boardInfoModal").style.display = "none"; -} - -function validateBoardName(name) { - return /^[a-z0-9_]+$/.test(name); -} - -async function confirmBoardInfoAndGenerate() { - const boardName = document.getElementById("boardNameInput").value.trim(); - const fullName = document.getElementById("boardFullNameInput").value.trim(); - const vendor = - document.getElementById("boardVendorInput").value.trim() || "custom"; - const revision = document.getElementById("boardRevisionInput").value.trim(); - const description = document - .getElementById("boardDescriptionInput") - .value.trim(); - - const errorElement = document.getElementById("boardInfoError"); - - // Validation - if (!boardName) { - errorElement.textContent = "Board name is required."; - errorElement.style.display = "block"; - return; - } - - if (!validateBoardName(boardName)) { - errorElement.textContent = - "Board name must contain only lowercase letters, numbers, and underscores."; - errorElement.style.display = "block"; - return; - } - - if (!fullName) { - errorElement.textContent = "Full board name is required."; - errorElement.style.display = "block"; - return; - } - - if (vendor && !validateBoardName(vendor)) { - errorElement.textContent = - "Vendor name must contain only lowercase letters, numbers, and underscores."; - errorElement.style.display = "block"; - return; - } - - // Store board info - boardInfo = { - name: boardName, - fullName: fullName, - vendor: vendor, - revision: revision, - description: description, - }; - - closeBoardInfoModal(); - await exportBoardDefinition(); -} - -async function exportBoardDefinition() { - const mcu = document.getElementById("mcuSelector").value; - const pkg = document.getElementById("packageSelector").value; - - // Load templates if not already loaded - if (!deviceTreeTemplates) { - deviceTreeTemplates = await loadDeviceTreeTemplates(mcu); - if (!deviceTreeTemplates) { - alert("DeviceTree templates not available for this MCU yet."); - return; - } - } - - try { - // Generate all board files - const files = await generateBoardFiles(mcu, pkg); - await downloadBoardAsZip(files, boardInfo.name); - } catch (error) { - console.error("Board definition generation failed:", error); - alert(`Failed to generate board definition: ${error.message}`); - } -} - -function getMcuSupportsNonSecure(mcuId) { - const mcuInfo = mcuManifest.mcus.find((m) => m.id === mcuId); - return mcuInfo ? mcuInfo.supportsNonSecure === true : false; -} - -function getMcuSupportsFLPR(mcuId) { - const mcuInfo = mcuManifest.mcus.find((m) => m.id === mcuId); - return mcuInfo ? mcuInfo.supportsFLPR === true : false; -} - -async function generateBoardFiles(mcu, pkg) { - const supportsNS = getMcuSupportsNonSecure(mcu); - const supportsFLPR = getMcuSupportsFLPR(mcu); - const files = {}; - - files["board.yml"] = generateBoardYml(mcu, supportsNS, supportsFLPR); - files["board.cmake"] = generateBoardCmake(mcu, supportsNS, supportsFLPR); - files["Kconfig.defconfig"] = generateKconfigDefconfig(mcu, supportsNS); - files[`Kconfig.${boardInfo.name}`] = generateKconfigBoard(mcu, supportsNS); - files[`${boardInfo.name}_common.dtsi`] = generateCommonDtsi(mcu); - files[`${mcu}_cpuapp_common.dtsi`] = generateCpuappCommonDtsi(mcu); - files[`${boardInfo.name}_${mcu}-pinctrl.dtsi`] = generatePinctrlFile(); - files[`${boardInfo.name}_${mcu}_cpuapp.dts`] = generateMainDts(mcu); - files[`${boardInfo.name}_${mcu}_cpuapp.yaml`] = generateYamlCapabilities( - mcu, - false, - ); - files[`${boardInfo.name}_${mcu}_cpuapp_defconfig`] = generateDefconfig(false); - files["README.md"] = generateReadme(mcu, pkg, supportsNS, supportsFLPR); - - // Generate NS-specific files if MCU supports TrustZone-M - if (supportsNS) { - files["Kconfig"] = generateKconfigTrustZone(mcu); - files[`${boardInfo.name}_${mcu}_cpuapp_ns.dts`] = generateNSDts(mcu); - files[`${boardInfo.name}_${mcu}_cpuapp_ns.yaml`] = generateYamlCapabilities( - mcu, - true, - ); - files[`${boardInfo.name}_${mcu}_cpuapp_ns_defconfig`] = - generateDefconfig(true); - } - - // Generate FLPR-specific files if MCU supports FLPR - if (supportsFLPR) { - files[`${boardInfo.name}_${mcu}_cpuflpr.dts`] = generateFLPRDts(mcu); - files[`${boardInfo.name}_${mcu}_cpuflpr.yaml`] = generateFLPRYaml( - mcu, - false, - ); - files[`${boardInfo.name}_${mcu}_cpuflpr_defconfig`] = - generateFLPRDefconfig(false); - files[`${boardInfo.name}_${mcu}_cpuflpr_xip.dts`] = generateFLPRXIPDts(mcu); - files[`${boardInfo.name}_${mcu}_cpuflpr_xip.yaml`] = generateFLPRYaml( - mcu, - true, - ); - files[`${boardInfo.name}_${mcu}_cpuflpr_xip_defconfig`] = - generateFLPRDefconfig(true); - } - - return files; -} - -function generateBoardYml(mcu, supportsNS, supportsFLPR) { - const socName = mcu.replace("nrf", ""); - - let socSection = ` socs: - - name: ${mcu}`; - - // Add variants if needed - if (supportsNS || supportsFLPR) { - socSection += ` - variants:`; - if (supportsFLPR) { - socSection += ` - - name: xip - cpucluster: cpuflpr`; - } - if (supportsNS) { - socSection += ` - - name: ns - cpucluster: cpuapp`; - } - } - - let boardsList = `${boardInfo.name}/${mcu}/cpuapp`; - if (supportsNS) { - boardsList += ` - - ${boardInfo.name}/${mcu}/cpuapp/ns`; - } - if (supportsFLPR) { - boardsList += ` - - ${boardInfo.name}/${mcu}/cpuflpr - - ${boardInfo.name}/${mcu}/cpuflpr/xip`; - } - - return `board: - name: ${boardInfo.name} - full_name: ${boardInfo.fullName} - vendor: ${boardInfo.vendor} -${socSection} -runners: - run_once: - '--recover': - - runners: - - nrfjprog - - nrfutil - run: first - groups: - - boards: - - ${boardsList} - '--erase': - - runners: - - nrfjprog - - jlink - - nrfutil - run: first - groups: - - boards: - - ${boardsList} - '--reset': - - runners: - - nrfjprog - - jlink - - nrfutil - run: last - groups: - - boards: - - ${boardsList} -`; -} - -function generateBoardCmake(mcu, supportsNS, supportsFLPR) { - const mcuUpper = mcu.toUpperCase(); - const boardNameUpper = boardInfo.name.toUpperCase(); - - let content = `# Copyright (c) 2024 Nordic Semiconductor ASA -# SPDX-License-Identifier: Apache-2.0 - -if(CONFIG_SOC_${mcuUpper}_CPUAPP) -\tboard_runner_args(jlink "--device=nRF${mcuUpper.substring(3)}_M33" "--speed=4000") -`; - - if (supportsFLPR) { - if (mcu === "nrf54l15") { - content += `elseif(CONFIG_SOC_${mcuUpper}_CPUFLPR) -\tboard_runner_args(jlink "--device=nRF${mcuUpper.substring(3)}_RV32") -`; - } else { - // L05 and L10 need a JLink script - content += `elseif(CONFIG_SOC_${mcuUpper}_CPUFLPR) -\tset(JLINKSCRIPTFILE \${CMAKE_CURRENT_LIST_DIR}/support/${mcu}_cpuflpr.JLinkScript) -\tboard_runner_args(jlink "--device=RISC-V" "--speed=4000" "-if SW" "--tool-opt=-jlinkscriptfile \${JLINKSCRIPTFILE}") -`; - } - } - - content += `endif() - -`; - - if (supportsNS) { - content += `if(CONFIG_BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS) -\tset(TFM_PUBLIC_KEY_FORMAT "full") -endif() - -if(CONFIG_TFM_FLASH_MERGED_BINARY) -\tset_property(TARGET runners_yaml_props_target PROPERTY hex_file tfm_merged.hex) -endif() - -`; - } - - content += `include(\${ZEPHYR_BASE}/boards/common/nrfutil.board.cmake) -include(\${ZEPHYR_BASE}/boards/common/jlink.board.cmake) -`; - - return content; -} - -function generateKconfigTrustZone(mcu) { - const boardNameUpper = boardInfo.name.toUpperCase(); - const mcuUpper = mcu.toUpperCase(); - return `# Copyright (c) 2025 Generated by nRF54L Pin Planner -# SPDX-License-Identifier: Apache-2.0 - -# ${boardInfo.fullName} board configuration - -if BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS - -DT_NRF_MPC := $(dt_nodelabel_path,nrf_mpc) - -config NRF_TRUSTZONE_FLASH_REGION_SIZE -\thex -\tdefault $(dt_node_int_prop_hex,$(DT_NRF_MPC),override-granularity) -\thelp -\t This defines the flash region size from the TrustZone perspective. -\t It is used when configuring the TrustZone and when setting alignments -\t requirements for the partitions. -\t This abstraction allows us to configure TrustZone without depending -\t on peripheral-specific symbols. - -config NRF_TRUSTZONE_RAM_REGION_SIZE -\thex -\tdefault $(dt_node_int_prop_hex,$(DT_NRF_MPC),override-granularity) -\thelp -\t This defines the RAM region size from the TrustZone perspective. -\t It is used when configuring the TrustZone and when setting alignments -\t requirements for the partitions. -\t This abstraction allows us to configure TrustZone without depending -\t on peripheral specific symbols. - -endif # BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS -`; -} - -function generateKconfigDefconfig(mcu, supportsNS) { - const boardNameUpper = boardInfo.name.toUpperCase(); - const mcuUpper = mcu.toUpperCase(); - - let content = `# Copyright (c) 2024 Nordic Semiconductor ASA -# SPDX-License-Identifier: Apache-2.0 - -config HW_STACK_PROTECTION -\tdefault ARCH_HAS_STACK_PROTECTION - -if BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP - -config ROM_START_OFFSET -\tdefault 0 if PARTITION_MANAGER_ENABLED -\tdefault 0x800 if BOOTLOADER_MCUBOOT - -endif # BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP -`; - - if (supportsNS) { - content += ` -if BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS - -config BOARD_${boardNameUpper} -\tselect USE_DT_CODE_PARTITION if BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS - -config BT_CTLR -\tdefault BT - -# By default, if we build for a Non-Secure version of the board, -# enable building with TF-M as the Secure Execution Environment. -config BUILD_WITH_TFM -\tdefault y - -endif # BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS -`; - } - - return content; -} - -function generateKconfigBoard(mcu, supportsNS) { - const boardNameUpper = boardInfo.name.toUpperCase(); - const mcuUpper = mcu.toUpperCase(); - - let selectCondition = `BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP`; - if (supportsNS) { - selectCondition += ` || BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS`; - } - - return `# Copyright (c) 2025 Generated by nRF54L Pin Planner -# SPDX-License-Identifier: Apache-2.0 - -config BOARD_${boardNameUpper} -\tselect SOC_${mcuUpper}_CPUAPP if ${selectCondition} -`; -} - -function generatePinctrlFile() { - let content = `/* - * Copyright (c) 2024 Nordic Semiconductor ASA - * SPDX-License-Identifier: Apache-2.0 - */ - -&pinctrl { -`; - - selectedPeripherals.forEach((p) => { - const template = deviceTreeTemplates[p.id]; - if (!template) { - console.warn(`No template found for ${p.id}`); - return; - } - - // Generate pinctrl configurations - content += generatePinctrlForPeripheral(p, template); - }); - - content += "};\n"; - return content; -} - -function generateCommonDtsi(mcu) { - let content = `/* - * Copyright (c) 2024 Nordic Semiconductor ASA - * SPDX-License-Identifier: Apache-2.0 - */ - -#include "${boardInfo.name}_${mcu}-pinctrl.dtsi" - -`; - - // Generate peripheral node configurations with pinctrl and status - // Skip oscillators - they go in cpuapp_common.dtsi instead - // Skip GPIO pins - they are handled separately - selectedPeripherals.forEach((p) => { - // Skip oscillators (LFXO, HFXO) and GPIO pins - if (p.config && p.config.loadCapacitors) return; - if (p.type === "GPIO") return; - - const template = deviceTreeTemplates[p.id]; - if (!template) return; - content += generatePeripheralNode(p, template); - }); - - // Generate GPIO pin nodes - const gpioPins = selectedPeripherals.filter((p) => p.type === "GPIO"); - if (gpioPins.length > 0) { - content += generateGpioNodes(gpioPins); - } - - return content; -} - -function generateGpioNodes(gpioPins) { - let content = "\n/ {\n"; - - gpioPins.forEach((gpio) => { - console.log("Processing GPIO:", gpio); // Debug log - if (!gpio.pin) { - console.warn("GPIO missing pin property:", gpio); - return; - } - - const pinInfo = parsePinName(gpio.pin); - if (!pinInfo) { - console.warn("Failed to parse pin name:", gpio.pin); - return; - } - - const activeFlag = - gpio.activeState === "active-low" - ? "GPIO_ACTIVE_LOW" - : "GPIO_ACTIVE_HIGH"; - - content += `\t${gpio.label}: ${gpio.label} {\n`; - content += `\t\tgpios = <&gpio${pinInfo.port} ${pinInfo.pin} ${activeFlag}>;\n`; - content += `\t};\n`; - }); - - content += "};\n"; - return content; -} - -function generateCpuappCommonDtsi(mcu) { - let content = `/* - * Copyright (c) 2024 Nordic Semiconductor ASA - * - * SPDX-License-Identifier: Apache-2.0 - */ - -/* This file is common to the secure and non-secure domain */ - -#include "${boardInfo.name}_common.dtsi" - -/ { -\tchosen { -`; - - // Add console/uart aliases in chosen section if UART is selected - let hasUart = false; - selectedPeripherals.forEach((p) => { - const template = deviceTreeTemplates[p.id]; - if ( - template && - template.dtNodeName && - template.type === "UART" && - !hasUart - ) { - content += `\t\tzephyr,console = &${template.dtNodeName};\n`; - content += `\t\tzephyr,shell-uart = &${template.dtNodeName};\n`; - content += `\t\tzephyr,uart-mcumgr = &${template.dtNodeName};\n`; - content += `\t\tzephyr,bt-mon-uart = &${template.dtNodeName};\n`; - content += `\t\tzephyr,bt-c2h-uart = &${template.dtNodeName};\n`; - hasUart = true; - } - }); - - content += `\t\tzephyr,flash-controller = &rram_controller; -\t\tzephyr,flash = &cpuapp_rram; -\t\tzephyr,ieee802154 = &ieee802154; -\t\tzephyr,boot-mode = &boot_mode0; -\t}; -}; - -&cpuapp_sram { -\tstatus = "okay"; -}; -`; - - // Add LFXO configuration if enabled - const lfxo = selectedPeripherals.find((p) => p.id === "LFXO"); - if (lfxo && lfxo.config) { - content += ` -&lfxo { -\tload-capacitors = "${lfxo.config.loadCapacitors}";`; - if ( - lfxo.config.loadCapacitors === "internal" && - lfxo.config.loadCapacitanceFemtofarad - ) { - content += ` -\tload-capacitance-femtofarad = <${lfxo.config.loadCapacitanceFemtofarad}>;`; - } - content += ` -}; -`; - } - - // Add HFXO configuration (always present) - const hfxo = selectedPeripherals.find((p) => p.id === "HFXO"); - const hfxoConfig = - hfxo && hfxo.config - ? hfxo.config - : { loadCapacitors: "internal", loadCapacitanceFemtofarad: 15000 }; - content += ` -&hfxo { -\tload-capacitors = "${hfxoConfig.loadCapacitors}";`; - if ( - hfxoConfig.loadCapacitors === "internal" && - hfxoConfig.loadCapacitanceFemtofarad - ) { - content += ` -\tload-capacitance-femtofarad = <${hfxoConfig.loadCapacitanceFemtofarad}>;`; - } - content += ` -}; -`; - - content += ` -®ulators { -\tstatus = "okay"; -}; - -&vregmain { -\tstatus = "okay"; -\tregulator-initial-mode = ; -}; - -&grtc { -\towned-channels = <0 1 2 3 4 5 6 7 8 9 10 11>; -\t/* Channels 7-11 reserved for Zero Latency IRQs, 3-4 for FLPR */ -\tchild-owned-channels = <3 4 7 8 9 10 11>; -\tstatus = "okay"; -}; - -&gpio0 { -\tstatus = "okay"; -}; - -&gpio1 { -\tstatus = "okay"; -}; - -&gpio2 { -\tstatus = "okay"; -}; - -&gpiote20 { -\tstatus = "okay"; -}; - -&gpiote30 { -\tstatus = "okay"; -}; - -&radio { -\tstatus = "okay"; -}; - -&ieee802154 { -\tstatus = "okay"; -}; - -&temp { -\tstatus = "okay"; -}; - -&clock { -\tstatus = "okay"; -}; - -&gpregret1 { -\tstatus = "okay"; - -\tboot_mode0: boot_mode@0 { -\t\tcompatible = "zephyr,retention"; -\t\tstatus = "okay"; -\t\treg = <0x0 0x1>; -\t}; -}; -`; - - // Check if NFC pins are not being used as NFC - let nfcUsed = false; - selectedPeripherals.forEach((p) => { - const template = deviceTreeTemplates[p.id]; - if (template && template.type === "NFCT") { - nfcUsed = true; - } - }); - - // If NFCT is not enabled, configure UICR to use NFC pins as GPIO - if (!nfcUsed) { - content += ` -&uicr { -\tnfct-pins-as-gpios; -}; -`; - } - - return content; -} - -function generateMainDts(mcu) { - const mcuUpper = mcu.toUpperCase().replace("NRF", "nRF"); - return `/dts-v1/; - -#include -#include "${mcu}_cpuapp_common.dtsi" - -/ { -\tcompatible = "${boardInfo.vendor},${boardInfo.name}-${mcu}-cpuapp"; -\tmodel = "${boardInfo.fullName} ${mcuUpper} Application MCU"; - -\tchosen { -\t\tzephyr,code-partition = &slot0_partition; -\t\tzephyr,sram = &cpuapp_sram; -\t}; -}; - -/* Include default memory partition configuration file */ -#include -`; -} - -function generateYamlCapabilities(mcu, isNonSecure) { - const supportedFeatures = new Set(); - - selectedPeripherals.forEach((p) => { - const template = deviceTreeTemplates[p.id]; - if (template) { - switch (template.type) { - case "UART": - supportedFeatures.add("uart"); - break; - case "SPI": - supportedFeatures.add("spi"); - break; - case "I2C": - supportedFeatures.add("i2c"); - break; - case "PWM": - supportedFeatures.add("pwm"); - break; - case "ADC": - supportedFeatures.add("adc"); - break; - case "NFCT": - supportedFeatures.add("nfc"); - break; - } - } - }); - - // Always add these - supportedFeatures.add("gpio"); - supportedFeatures.add("watchdog"); - - const featuresArray = Array.from(supportedFeatures).sort(); - - const identifier = isNonSecure - ? `${boardInfo.name}/${mcu}/cpuapp/ns` - : `${boardInfo.name}/${mcu}/cpuapp`; - const name = isNonSecure - ? `${boardInfo.fullName}-Non-Secure` - : boardInfo.fullName; - const ram = isNonSecure ? 256 : 188; - const flash = isNonSecure ? 1524 : 1428; - - return `# Copyright (c) 2025 Generated by nRF54L Pin Planner -# SPDX-License-Identifier: Apache-2.0 - -identifier: ${identifier} -name: ${name} -type: mcu -arch: arm -toolchain: - - gnuarmemb - - zephyr -sysbuild: true -ram: ${ram} -flash: ${flash} -supported: -${featuresArray.map((f) => ` - ${f}`).join("\n")} -vendor: ${boardInfo.vendor} -`; -} - -function generateDefconfig(isNonSecure) { - let config = `# Copyright (c) 2025 Generated by nRF54L Pin Planner -# SPDX-License-Identifier: Apache-2.0 - -`; - - if (isNonSecure) { - // NS-specific configuration - config += `CONFIG_ARM_MPU=y -CONFIG_HW_STACK_PROTECTION=y -CONFIG_NULL_POINTER_EXCEPTION_DETECTION_NONE=y -CONFIG_ARM_TRUSTZONE_M=y - -# This Board implies building Non-Secure firmware -CONFIG_TRUSTED_EXECUTION_NONSECURE=y - -# Don't enable the cache in the non-secure image as it is a -# secure-only peripheral on 54l -CONFIG_CACHE_MANAGEMENT=n -CONFIG_EXTERNAL_CACHE=n - -CONFIG_UART_CONSOLE=y -CONFIG_CONSOLE=y -CONFIG_SERIAL=y -CONFIG_GPIO=y - -# Start SYSCOUNTER on driver init -CONFIG_NRF_GRTC_START_SYSCOUNTER=y - -# Disable TFM BL2 since it is not supported -CONFIG_TFM_BL2=n - -# Support for silence logging is not supported at the moment -CONFIG_TFM_LOG_LEVEL_SILENCE=n - -# The oscillators are configured as secure and cannot be configured -# from the non secure application directly. This needs to be set -# otherwise nrfx will try to configure them, resulting in a bus -# fault. -CONFIG_SOC_NRF54LX_SKIP_CLOCK_CONFIG=y -`; - } else { - // Regular secure build configuration - const hasUart = selectedPeripherals.some((p) => { - const template = deviceTreeTemplates[p.id]; - return template && template.type === "UART"; - }); - - if (hasUart) { - config += `# Enable UART driver -CONFIG_SERIAL=y - -# Enable console -CONFIG_CONSOLE=y -CONFIG_UART_CONSOLE=y - -`; - } - - config += `# Enable GPIO -CONFIG_GPIO=y - -# Enable MPU -CONFIG_ARM_MPU=y - -# Enable hardware stack protection -CONFIG_HW_STACK_PROTECTION=y -`; - - // Only add RC oscillator config if LFXO is not enabled - const lfxoEnabled = selectedPeripherals.some((p) => p.id === "LFXO"); - if (!lfxoEnabled) { - config += ` -# Use RC oscillator for low-frequency clock -CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC=y -`; - } - } - - return config; -} - -function generateNSDts(mcu) { - const mcuUpper = mcu.toUpperCase().replace("NRF", "nRF"); - - // Find the UART used for console (if any) - TF-M will use it - let uartNodeName = null; - selectedPeripherals.forEach((p) => { - const template = deviceTreeTemplates[p.id]; - if ( - template && - template.dtNodeName && - template.type === "UART" && - !uartNodeName - ) { - uartNodeName = template.dtNodeName; - } - }); - - let uartDisableSection = ""; - if (uartNodeName) { - uartDisableSection = ` -&${uartNodeName} { -\t/* Disable so that TF-M can use this UART */ -\tstatus = "disabled"; - -\tcurrent-speed = <115200>; -\tpinctrl-0 = <&${uartNodeName.replace(/uart/, "uart")}_default>; -\tpinctrl-1 = <&${uartNodeName.replace(/uart/, "uart")}_sleep>; -\tpinctrl-names = "default", "sleep"; -}; - -`; - } - - return `/dts-v1/; - -#define USE_NON_SECURE_ADDRESS_MAP 1 - -#include -#include "${mcu}_cpuapp_common.dtsi" - -/ { -\tcompatible = "${boardInfo.vendor},${boardInfo.name}-${mcu}-cpuapp"; -\tmodel = "${boardInfo.fullName} ${mcuUpper} Application MCU"; - -\tchosen { -\t\tzephyr,code-partition = &slot0_ns_partition; -\t\tzephyr,sram = &sram0_ns; -\t\tzephyr,entropy = &psa_rng; -\t}; - -\t/delete-node/ rng; - -\tpsa_rng: psa-rng { -\t\tstatus = "okay"; -\t}; -}; - -/ { -\t/* -\t * Default SRAM planning when building for ${mcuUpper} with ARM TrustZone-M support -\t * - Lowest 80 kB SRAM allocated to Secure image (sram0_s). -\t * - Upper 80 kB SRAM allocated to Non-Secure image (sram0_ns). -\t * -\t * ${mcuUpper} has 256 kB of volatile memory (SRAM) but the last 96kB are reserved for -\t * the FLPR MCU. -\t * This static layout needs to be the same with the upstream TF-M layout in the -\t * header flash_layout.h of the relevant platform. Any updates in the layout -\t * needs to happen both in the flash_layout.h and in this file at the same time. -\t */ -\treserved-memory { -\t\t#address-cells = <1>; -\t\t#size-cells = <1>; -\t\tranges; - -\t\tsram0_s: image_s@20000000 { -\t\t\t/* Secure image memory */ -\t\t\treg = <0x20000000 DT_SIZE_K(80)>; -\t\t}; - -\t\tsram0_ns: image_ns@20014000 { -\t\t\t/* Non-Secure image memory */ -\t\t\treg = <0x20014000 DT_SIZE_K(80)>; -\t\t}; -\t}; -}; - -${uartDisableSection}/* Include default memory partition configuration file */ -#include -`; -} - -function generateFLPRDts(mcu) { - const mcuUpper = mcu.toUpperCase().replace("NRF", "nRF"); - return `/dts-v1/; -#include -#include "${boardInfo.name}_common.dtsi" - -/ { -\tmodel = "${boardInfo.fullName} ${mcuUpper} FLPR MCU"; -\tcompatible = "${boardInfo.vendor},${boardInfo.name}-${mcu}-cpuflpr"; - -\tchosen { -\t\tzephyr,console = &uart30; -\t\tzephyr,shell-uart = &uart30; -\t\tzephyr,code-partition = &cpuflpr_code_partition; -\t\tzephyr,flash = &cpuflpr_rram; -\t\tzephyr,sram = &cpuflpr_sram; -\t}; -}; - -&cpuflpr_sram { -\tstatus = "okay"; -\t/* size must be increased due to booting from SRAM */ -\treg = <0x20028000 DT_SIZE_K(96)>; -\tranges = <0x0 0x20028000 0x18000>; -}; - -&cpuflpr_rram { -\tpartitions { -\t\tcompatible = "fixed-partitions"; -\t\t#address-cells = <1>; -\t\t#size-cells = <1>; - -\t\tcpuflpr_code_partition: partition@0 { -\t\t\tlabel = "image-0"; -\t\t\treg = <0x0 DT_SIZE_K(96)>; -\t\t}; -\t}; -}; - -&grtc { -\towned-channels = <3 4>; -\tstatus = "okay"; -}; - -&uart30 { -\tstatus = "okay"; -}; - -&gpio0 { -\tstatus = "okay"; -}; - -&gpio1 { -\tstatus = "okay"; -}; - -&gpio2 { -\tstatus = "okay"; -}; - -&gpiote20 { -\tstatus = "okay"; -}; - -&gpiote30 { -\tstatus = "okay"; -}; -`; -} - -function generateFLPRXIPDts(mcu) { - return `/* - * Copyright (c) 2025 Generated by nRF54L Pin Planner - * SPDX-License-Identifier: Apache-2.0 - */ - -#include "${boardInfo.name}_${mcu}_cpuflpr.dts" - -&cpuflpr_sram { -\treg = <0x2002f000 DT_SIZE_K(68)>; -\tranges = <0x0 0x2002f000 0x11000>; -}; -`; -} - -function generateFLPRYaml(mcu, isXIP) { - const mcuUpper = mcu.toUpperCase().replace("NRF", "nRF"); - const identifier = isXIP - ? `${boardInfo.name}/${mcu}/cpuflpr/xip` - : `${boardInfo.name}/${mcu}/cpuflpr`; - const name = isXIP - ? `${boardInfo.fullName}-Fast-Lightweight-Peripheral-Processor (RRAM XIP)` - : `${boardInfo.fullName}-Fast-Lightweight-Peripheral-Processor`; - const ram = isXIP ? 68 : 96; - - return `# Copyright (c) 2025 Generated by nRF54L Pin Planner -# SPDX-License-Identifier: Apache-2.0 - -identifier: ${identifier} -name: ${name} -type: mcu -arch: riscv -toolchain: - - zephyr -sysbuild: true -ram: ${ram} -flash: 96 -supported: - - counter - - gpio - - i2c - - spi - - watchdog -`; -} - -function generateFLPRDefconfig(isXIP) { - return `# Copyright (c) 2025 Generated by nRF54L Pin Planner -# SPDX-License-Identifier: Apache-2.0 - -# Enable UART driver -CONFIG_SERIAL=y - -# Enable console -CONFIG_CONSOLE=y -CONFIG_UART_CONSOLE=y - -# Enable GPIO -CONFIG_GPIO=y - -${isXIP ? "# Execute from RRAM\nCONFIG_XIP=y" : "# Execute from SRAM\nCONFIG_USE_DT_CODE_PARTITION=y\nCONFIG_XIP=n"} -`; -} - -function generateReadme(mcu, pkg, supportsNS, supportsFLPR) { - let readme = `# ${boardInfo.fullName} - -**Generated by:** nRF54L Pin Planner -**MCU:** ${mcu.toUpperCase()} -**Package:** ${pkg} -${boardInfo.revision ? `**Revision:** ${boardInfo.revision}\n` : ""}${boardInfo.description ? `\n${boardInfo.description}\n` : ""} - -## Usage - -1. Copy this directory to your Zephyr boards directory: - \`\`\`bash - cp -r ${boardInfo.name} $ZEPHYR_BASE/boards/${boardInfo.vendor}/ - \`\`\` - -2. Build your application for this board: - \`\`\`bash - west build -b ${boardInfo.name}/${mcu}/cpuapp samples/hello_world - \`\`\` -`; - - if (supportsNS) { - readme += ` - Or build for Non-Secure target with TF-M: - \`\`\`bash - west build -b ${boardInfo.name}/${mcu}/cpuapp/ns samples/hello_world - \`\`\` -`; - } - - if (supportsFLPR) { - readme += ` - Or build for FLPR (Fast Lightweight Processor): - \`\`\`bash - west build -b ${boardInfo.name}/${mcu}/cpuflpr samples/hello_world - \`\`\` - - Or build for FLPR with XIP (Execute In Place from RRAM): - \`\`\`bash - west build -b ${boardInfo.name}/${mcu}/cpuflpr/xip samples/hello_world - \`\`\` -`; - } - - readme += ` -3. Flash to your device: - \`\`\`bash - west flash - \`\`\` - -## Selected Peripherals - -${selectedPeripherals - .map((p) => { - if (p.config) { - // Oscillator - show configuration - const capLabel = - p.config.loadCapacitors === "internal" ? "Internal" : "External"; - const oscData = mcuData.socPeripherals.find((sp) => sp.id === p.id); - let info = `${capLabel} capacitors`; - if ( - p.config.loadCapacitors === "internal" && - p.config.loadCapacitanceFemtofarad - ) { - info += `, ${(p.config.loadCapacitanceFemtofarad / 1000).toFixed(p.id === "HFXO" ? 2 : 1)} pF`; - } - if (oscData && oscData.signals && oscData.signals.length > 0) { - const pins = oscData.signals - .filter((s) => s.allowedGpio && s.allowedGpio.length > 0) - .map((s) => s.allowedGpio[0]) - .join(", "); - if (pins) { - info += ` (${pins})`; - } - } - return `- **${p.id}**: ${info}`; - } else if (p.pinFunctions) { - // Regular peripheral - show pin assignments - const pins = Object.entries(p.pinFunctions) - .map(([pin, func]) => `${pin}: ${func}`) - .join(", "); - return `- **${p.id}**: ${pins}`; - } else { - // No pin info available - return `- **${p.id}**`; - } - }) - .join("\n")} - -## Pin Configuration - -See \`${boardInfo.name}_${mcu}-pinctrl.dtsi\` for complete pin mapping. - -## Notes - -- This is a generated board definition. Verify pin assignments match your hardware. -- Modify \`${boardInfo.name}_common.dtsi\` to add additional peripherals or features. -- Consult the [nRF Connect SDK documentation](https://docs.nordicsemi.com/) for more information. -`; -} - -function generatePinctrlForPeripheral(peripheral, template) { - // Skip pinctrl generation for peripherals that don't need it - if (template.noPinctrl) { - return ""; - } - - const pinctrlName = template.pinctrlBaseName; - let content = `\n\t/omit-if-no-ref/ ${pinctrlName}_default: ${pinctrlName}_default {\n`; - - // Group pins by their characteristics (outputs vs inputs with pull-ups) - const outputSignals = []; - const inputSignals = []; - - for (const [pinName, signalName] of Object.entries(peripheral.pinFunctions)) { - const pinInfo = parsePinName(pinName); - if (!pinInfo) continue; - - const dtSignalName = template.signalMappings[signalName]; - if (!dtSignalName) { - console.warn( - `No DT mapping for signal ${signalName} in ${peripheral.id}`, - ); - continue; - } - - const signal = peripheral.peripheral.signals.find( - (s) => s.name === signalName, - ); - if (signal && signal.direction === "input") { - inputSignals.push({ pinInfo, dtSignalName }); - } else { - outputSignals.push({ pinInfo, dtSignalName }); - } - } - - const allSignals = [...outputSignals, ...inputSignals]; - - // Don't generate empty pinctrl blocks - if (allSignals.length === 0) { - console.warn( - `No pins configured for ${peripheral.id}, skipping pinctrl generation`, - ); - return ""; - } - - // Generate group1 (outputs and bidirectional) - if (outputSignals.length > 0) { - content += `\t\tgroup1 {\n\t\t\tpsels = `; - content += outputSignals - .map( - (s) => - ``, - ) - .join(",\n\t\t\t\t"); - content += `;\n\t\t};\n`; - } - - // Generate group2 (inputs with pull-up) - if (inputSignals.length > 0) { - content += `\n\t\tgroup2 {\n\t\t\tpsels = `; - content += inputSignals - .map( - (s) => - ``, - ) - .join(",\n\t\t\t\t"); - content += `;\n\t\t\tbias-pull-up;\n\t\t};\n`; - } - - content += `\t};\n`; - - // Generate sleep state - content += `\n\t/omit-if-no-ref/ ${pinctrlName}_sleep: ${pinctrlName}_sleep {\n`; - content += `\t\tgroup1 {\n\t\t\tpsels = `; - - content += allSignals - .map( - (s) => - ``, - ) - .join(",\n\t\t\t\t"); - content += `;\n\t\t\tlow-power-enable;\n\t\t};\n`; - content += `\t};\n`; - - return content; -} - -function generatePeripheralNode(peripheral, template) { - const nodeName = template.dtNodeName; - const pinctrlName = template.pinctrlBaseName; - - let content = `\n&${nodeName} {\n`; - content += `\tstatus = "okay";\n`; - - // Only add pinctrl if the peripheral needs it - if (!template.noPinctrl && pinctrlName) { - content += `\tpinctrl-0 = <&${pinctrlName}_default>;\n`; - content += `\tpinctrl-1 = <&${pinctrlName}_sleep>;\n`; - content += `\tpinctrl-names = "default", "sleep";\n`; - } - - // Add type-specific properties - switch (template.type) { - case "UART": - content += `\tcurrent-speed = <115200>;\n`; - // Check for disable-rx config - if (peripheral.config && peripheral.config.disableRx) { - content += `\tdisable-rx;\n`; - } - break; - case "SPI": - // Check for out-of-band signals (CS, DCX) and add as comments - if (template.outOfBandSignals) { - template.outOfBandSignals.forEach((signal) => { - const pin = Object.keys(peripheral.pinFunctions).find( - (p) => peripheral.pinFunctions[p] === signal, - ); - if (pin) { - const pinInfo = parsePinName(pin); - if (pinInfo) { - content += `\t/* ${signal} pin: P${pinInfo.port}.${pinInfo.pin} */\n`; - } - } - }); - } - - // Build cs-gpios array including primary CS and extra CS GPIOs - const csGpioEntries = []; - - // Check if primary CS pin is selected - const csPin = Object.keys(peripheral.pinFunctions).find( - (pin) => peripheral.pinFunctions[pin] === "CS", - ); - if (csPin) { - const csPinInfo = parsePinName(csPin); - if (csPinInfo) { - csGpioEntries.push( - `<&gpio${csPinInfo.port} ${csPinInfo.pin} GPIO_ACTIVE_LOW>`, - ); - } - } - - // Add extra CS GPIOs from config - if (peripheral.config && peripheral.config.extraCsGpios) { - peripheral.config.extraCsGpios.forEach((gpio) => { - const pinInfo = parsePinName(gpio); - if (pinInfo) { - csGpioEntries.push( - `<&gpio${pinInfo.port} ${pinInfo.pin} GPIO_ACTIVE_LOW>`, - ); - } - }); - } - - // Output cs-gpios if any - if (csGpioEntries.length > 0) { - if (csGpioEntries.length === 1) { - content += `\tcs-gpios = ${csGpioEntries[0]};\n`; - } else { - content += `\tcs-gpios = ${csGpioEntries.join(",\n\t\t ")};\n`; - } - } - break; - case "I2C": - content += `\tclock-frequency = ;\n`; - break; - } - - content += `};\n`; - return content; -} - -async function downloadBoardAsZip(files, boardName) { - // Load JSZip library dynamically if not already loaded - if (typeof JSZip === "undefined") { - const script = document.createElement("script"); - script.src = - "https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"; - await new Promise((resolve) => { - script.onload = resolve; - document.head.appendChild(script); - }); - } - - const zip = new JSZip(); - const boardFolder = zip.folder(boardName); - - // Use current date/time for all files to avoid CMake reconfiguration loops - // Set to a stable past date to prevent future timestamps - const stableDate = new Date(2024, 0, 1, 12, 0, 0); // Jan 1, 2024 12:00:00 - - // Add all generated files to the board directory with stable timestamp - for (const [filename, content] of Object.entries(files)) { - boardFolder.file(filename, content, { date: stableDate }); - } - - // Generate and download the ZIP with proper options - const blob = await zip.generateAsync({ - type: "blob", - compression: "DEFLATE", - compressionOptions: { level: 9 }, - }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `${boardName}.zip`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); -} - -function filterPeripherals() { - const searchTerm = document - .getElementById("searchPeripherals") - .value.toLowerCase(); - const peripheralsList = document.getElementById("peripherals-list"); - - // Query for all top-level peripheral display elements - const items = peripheralsList.querySelectorAll( - ".single-peripheral-btn, .accordion-item, .checkbox-group", - ); - - items.forEach((item) => { - const text = item.textContent.toLowerCase(); - - let tags = []; - if (mcuData.socPeripherals) { - if (item.matches(".single-peripheral-btn")) { - const p = mcuData.socPeripherals.find((p) => p.id === item.dataset.id); - if (p && p.tags) tags = p.tags; - } else if (item.matches(".checkbox-group")) { - const id = item.querySelector("[data-peripheral-id]").dataset - .peripheralId; - const p = mcuData.socPeripherals.find((p) => p.id === id); - if (p && p.tags) tags = p.tags; - } else if (item.matches(".accordion-item")) { - item.querySelectorAll(".peripheral-item").forEach((pItem) => { - const p = mcuData.socPeripherals.find( - (p) => p.id === pItem.dataset.id, - ); - if (p && p.tags) tags = tags.concat(p.tags); - }); - } - } - const tagsText = tags.join(" ").toLowerCase(); - - if (text.includes(searchTerm) || tagsText.includes(searchTerm)) { - item.style.display = ""; // Show the item if it matches - } else { - item.style.display = "none"; // Hide the item if it doesn't match - } - }); -} - -// --- IMPORT/EXPORT CONFIGURATION --- - -let pendingImportConfig = null; -let isExportMode = true; - -function openExportConfigModal() { - isExportMode = true; - const modal = document.getElementById("importExportInfoModal"); - const title = document.getElementById("importExportModalTitle"); - const text = document.getElementById("importExportModalText"); - const bullet1 = document.getElementById("importExportBullet1"); - const bullet2 = document.getElementById("importExportBullet2"); - const bullet3 = document.getElementById("importExportBullet3"); - const warning = document.getElementById("importExportWarning"); - const warningText = document.getElementById("importExportWarningText"); - const confirmBtn = document.getElementById("confirmImportExport"); - - title.textContent = "Export Configuration"; - text.textContent = - "This will export your current pin configuration for the selected MCU/package to a JSON file. You can use this file to:"; - bullet1.textContent = "Share configurations with team members"; - bullet2.textContent = "Back up your pin assignments"; - bullet3.textContent = - "Restore configurations on a different browser or computer"; - warning.style.backgroundColor = "#fff3cd"; - warning.style.color = "#856404"; - warning.style.borderColor = "#ffeeba"; - warningText.textContent = - "The exported file is specific to the currently selected MCU and package."; - confirmBtn.textContent = "Export"; - - modal.style.display = "block"; -} - -function openImportConfigModal() { - // Trigger file selection - document.getElementById("importConfigFile").click(); -} - -function handleImportConfigFile(event) { - const file = event.target.files[0]; - if (!file) return; - - const reader = new FileReader(); - reader.onload = function (e) { - try { - const config = JSON.parse(e.target.result); - validateAndShowImportModal(config); - } catch (error) { - alert("Invalid JSON file: " + error.message); - } - // Reset the file input so the same file can be selected again - event.target.value = ""; - }; - reader.readAsText(file); -} - -function validateAndShowImportModal(config) { - // Validate required fields - if (!config.mcu || !config.package || !config.selectedPeripherals) { - alert( - "Invalid configuration file. Missing required fields (mcu, package, or selectedPeripherals).", - ); - return; - } - - pendingImportConfig = config; - isExportMode = false; - - const modal = document.getElementById("importExportInfoModal"); - const title = document.getElementById("importExportModalTitle"); - const text = document.getElementById("importExportModalText"); - const bullet1 = document.getElementById("importExportBullet1"); - const bullet2 = document.getElementById("importExportBullet2"); - const bullet3 = document.getElementById("importExportBullet3"); - const warning = document.getElementById("importExportWarning"); - const warningText = document.getElementById("importExportWarningText"); - const confirmBtn = document.getElementById("confirmImportExport"); - - const currentMcu = document.getElementById("mcuSelector").value; - const currentPkg = document.getElementById("packageSelector").value; - - const isDifferentPart = - config.mcu !== currentMcu || config.package !== currentPkg; - - title.textContent = "Import Configuration"; - text.textContent = `This will import a pin configuration from the file. The configuration is for:`; - bullet1.innerHTML = `MCU: ${config.mcu}`; - bullet2.innerHTML = `Package: ${config.package}`; - bullet3.innerHTML = `Peripherals: ${config.selectedPeripherals.length} configured`; - - if (isDifferentPart) { - warning.style.backgroundColor = "#fff3cd"; - warning.style.color = "#856404"; - warning.style.borderColor = "#ffeeba"; - warningText.innerHTML = `Note: This configuration is for a different MCU/package than currently selected. Importing will switch to ${config.mcu} with package ${config.package}.`; - } else { - warning.style.backgroundColor = "#d4edda"; - warning.style.color = "#155724"; - warning.style.borderColor = "#c3e6cb"; - warningText.innerHTML = `This configuration matches your currently selected MCU and package.`; - } - - confirmBtn.textContent = "Import"; - - // Add additional warning about overwriting - const existingWarning = document.getElementById("importOverwriteWarning"); - if (!existingWarning) { - const overwriteWarning = document.createElement("div"); - overwriteWarning.id = "importOverwriteWarning"; - overwriteWarning.style.cssText = - "background-color: #f8d7da; color: #721c24; padding: 10px; border: 1px solid #f5c6cb; border-radius: 5px; margin-top: 10px;"; - overwriteWarning.innerHTML = - "Warning: Importing will replace your current configuration and overwrite any saved data for this MCU/package."; - warning.parentNode.insertBefore(overwriteWarning, warning.nextSibling); - } - - modal.style.display = "block"; -} - -function closeImportExportModal() { - const modal = document.getElementById("importExportInfoModal"); - modal.style.display = "none"; - pendingImportConfig = null; - - // Remove the overwrite warning if it exists - const overwriteWarning = document.getElementById("importOverwriteWarning"); - if (overwriteWarning) { - overwriteWarning.remove(); - } -} - -function confirmImportExport() { - if (isExportMode) { - exportConfig(); - } else { - importConfig(); - } - closeImportExportModal(); -} - -function exportConfig() { - const mcu = document.getElementById("mcuSelector").value; - const pkg = document.getElementById("packageSelector").value; - - if (!mcu || !pkg) { - alert("Please select an MCU and package first."); - return; - } - - const config = { - version: 1, - exportDate: new Date().toISOString(), - mcu: mcu, - package: pkg, - selectedPeripherals: selectedPeripherals.map(serializePeripheral), - }; - - const json = JSON.stringify(config, null, 2); - const blob = new Blob([json], { type: "application/json" }); - const url = URL.createObjectURL(blob); - - const a = document.createElement("a"); - a.href = url; - a.download = `pinplanner-${mcu}-${pkg}.json`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); -} - -async function importConfig() { - if (!pendingImportConfig) return; - - const config = pendingImportConfig; - const currentMcu = document.getElementById("mcuSelector").value; - const currentPkg = document.getElementById("packageSelector").value; - - // Check if we need to switch MCU/package - if (config.mcu !== currentMcu || config.package !== currentPkg) { - // Switch to the target MCU first - const mcuSelector = document.getElementById("mcuSelector"); - const mcuOption = Array.from(mcuSelector.options).find( - (opt) => opt.value === config.mcu, - ); - - if (!mcuOption) { - alert(`MCU "${config.mcu}" not found in available options.`); - return; - } - - mcuSelector.value = config.mcu; - await handleMcuChange(); - - // Then switch to the target package - const packageSelector = document.getElementById("packageSelector"); - const pkgOption = Array.from(packageSelector.options).find( - (opt) => opt.value === config.package, - ); - - if (!pkgOption) { - alert(`Package "${config.package}" not found for MCU "${config.mcu}".`); - return; - } - - packageSelector.value = config.package; - await loadCurrentMcuData(); - } - - // Clear current state and apply imported config - clearAllPeripherals(); - - // Apply the imported configuration - applyConfig({ - selectedPeripherals: config.selectedPeripherals, - }); - - // Save to localStorage - saveStateToLocalStorage(); - - // Update UI - need to re-render peripherals list to show selected state - organizePeripherals(); - updateSelectedPeripheralsList(); - updatePinDisplay(); - - console.log( - `Configuration imported for ${config.mcu}/${config.package} with ${config.selectedPeripherals.length} peripherals`, - ); -} diff --git a/style.css b/style.css index cddf3b5..d1b3bb7 100644 --- a/style.css +++ b/style.css @@ -446,8 +446,9 @@ button.secondary-btn:hover { } .chip-container { position: relative; - width: 400px; - height: 400px; + width: 100%; + max-width: 500px; + aspect-ratio: 1; margin: 1rem auto; background-color: var(--secondary-color); border-radius: 10px; @@ -828,3 +829,195 @@ option:disabled { border-color: var(--error-color) !important; box-shadow: 0 0 0 2px rgba(217, 83, 79, 0.2) !important; } + +/* --- RESPONSIVE DESIGN --- */ + +/* Desktop (default): 3-column grid - already defined as 380px 1fr 380px */ + +/* Tablet (<1200px): 2-column, selected panel below */ +@media (max-width: 1199px) { + .content-grid { + grid-template-columns: 300px 1fr; + } + .selected-view { + grid-column: 1 / -1; + } +} + +/* Small tablet (<900px): single column */ +@media (max-width: 899px) { + .content-grid { + grid-template-columns: 1fr; + } +} + +/* Mobile (<600px): compressed layout */ +@media (max-width: 599px) { + .container { + padding: 0.5rem; + } + .header { + flex-direction: column; + gap: 0.5rem; + } + .footer { + grid-template-columns: 1fr; + text-align: center; + } + .footer > p { + justify-self: center; + } + .footer .github-link { + justify-self: center; + } +} + +/* --- CONSOLE CONFIG --- */ +.console-config-section { + margin-bottom: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--border-color); +} + +.console-config-section h3 { + font-size: 0.95rem; + margin-bottom: 0.5rem; +} + +.console-banner { + padding: 0.5rem 0.75rem; + border-radius: 6px; + font-size: 0.85rem; + margin-bottom: 0.5rem; +} + +.console-warning { + background-color: #fff3cd; + color: #856404; + border: 1px solid #ffeeba; +} + +.console-info { + background-color: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +body.dark-mode .console-warning { + background-color: rgba(255, 183, 77, 0.15); + color: #ffb74d; + border-color: rgba(255, 183, 77, 0.3); +} + +body.dark-mode .console-info { + background-color: rgba(45, 212, 191, 0.15); + color: #2dd4bf; + border-color: rgba(45, 212, 191, 0.3); +} + +/* --- TOAST NOTIFICATIONS --- */ +#toastContainer { + position: fixed; + top: 1rem; + right: 1rem; + z-index: 2000; + display: flex; + flex-direction: column; + gap: 0.5rem; + pointer-events: none; +} + +.toast { + pointer-events: auto; + padding: 0.75rem 1.25rem; + border-radius: 6px; + font-size: 0.9rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + display: flex; + align-items: center; + gap: 0.5rem; + opacity: 0; + transform: translateX(100%); + transition: + opacity 0.3s, + transform 0.3s; + max-width: 400px; +} + +.toast-enter { + opacity: 1; + transform: translateX(0); +} + +.toast-exit { + opacity: 0; + transform: translateX(100%); +} + +.toast-info { + background-color: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.toast-warning { + background-color: #fff3cd; + color: #856404; + border: 1px solid #ffeeba; +} + +.toast-error { + background-color: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +body.dark-mode .toast-info { + background-color: rgba(45, 212, 191, 0.2); + color: #2dd4bf; + border-color: rgba(45, 212, 191, 0.3); +} + +body.dark-mode .toast-warning { + background-color: rgba(255, 183, 77, 0.2); + color: #ffb74d; + border-color: rgba(255, 183, 77, 0.3); +} + +body.dark-mode .toast-error { + background-color: rgba(229, 115, 115, 0.2); + color: #e57373; + border-color: rgba(229, 115, 115, 0.3); +} + +.toast-close { + background: none; + border: none; + font-size: 1.2rem; + cursor: pointer; + color: inherit; + padding: 0 0.25rem; + line-height: 1; + opacity: 0.7; +} + +.toast-close:hover { + opacity: 1; + background: transparent; +} + +/* --- DEVKIT PIN STYLES --- */ +.pin.devkit-occupied { + background-color: var(--warning-color); + color: white; + border-color: var(--warning-color); +} + +/* --- ACCESSIBILITY --- */ +button:focus-visible, +select:focus-visible, +input:focus-visible, +a:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} From bac3b63625db68c604ea5b51316bcc3276d27d4b Mon Sep 17 00:00:00 2001 From: Helmut Lord Date: Fri, 6 Feb 2026 13:07:19 -0500 Subject: [PATCH 2/8] Fix Zephyr build compilation for generated board definitions Validated generated boards against Zephyr west build. Key fixes: - Fix BT_CTLR -> HAS_BT_CTLR in Kconfig.defconfig for NS builds - Add CPUFLPR SOC select to Kconfig.board for FLPR builds - Fix DTS include paths: use vendor/nordic/ for _cpuapp_partition, nordic/ for nrf54l05 simple partition - Handle nrf54lm20a _enga_ DTSI suffix via getMcuDtsiBaseName() - Remove &uicr block from cpuapp_common (undefined in NS context) - Remove reserved-memory section from NS DTS (defined by partition DTSI) - Add uart30 pinctrl to test boards for FLPR console support - Skip nrf54lv10a in CI (no Zephyr DTSI support yet) Verified: nrf54l15, nrf54l10, nrf54l05 cpuapp targets build hello_world successfully with Zephyr 4.3.99. Co-Authored-By: Claude Opus 4.6 --- ci/generate-test-boards.js | 160 +++++++++++++++++++++++++++---------- js/devicetree.js | 104 ++++++++++-------------- js/export.js | 6 +- 3 files changed, 168 insertions(+), 102 deletions(-) diff --git a/ci/generate-test-boards.js b/ci/generate-test-boards.js index 4a68ac2..023ae92 100644 --- a/ci/generate-test-boards.js +++ b/ci/generate-test-boards.js @@ -29,6 +29,17 @@ let exitCode = 0; // Helpers // ----------------------------------------------------------------------- +/** + * Map MCU names to their Zephyr DTSI base names. + * Some MCUs have engineering sample suffixes in their DTSI filenames. + */ +function getMcuDtsiBaseName(mcu) { + const dtsiNameMap = { + 'nrf54lm20a': 'nrf54lm20a_enga', + }; + return dtsiNameMap[mcu] || mcu; +} + function parsePinName(pinName) { const match = pinName.match(/P(\d+)\.(\d+)/); if (!match) return null; @@ -117,6 +128,51 @@ function buildUartState(packageData, templates) { }; } +/** + * Build synthetic peripheral state for UARTE30 (FLPR console). + * Takes a set of already-used pins to avoid conflicts. + * Returns { id, type, peripheral, pinFunctions, config } or null if unavailable. + */ +function buildUart30State(packageData, templates, existingUsedPins) { + const uart = packageData.socPeripherals.find((p) => p.id === "UARTE30"); + if (!uart) return null; + + const template = templates["UARTE30"]; + if (!template) return null; + + const usedPins = new Set(existingUsedPins); + const pinFunctions = {}; + + for (const signal of uart.signals) { + if (!signal.allowedGpio || signal.allowedGpio.length === 0) continue; + + const pin = resolveGpioToPin( + signal.allowedGpio, + packageData.pins, + usedPins, + ); + if (pin) { + pinFunctions[pin] = signal.name; + usedPins.add(pin); + } else if (signal.isMandatory) { + console.warn( + ` WARNING: Could not find available pin for mandatory signal ${signal.name} on UARTE30`, + ); + } + } + + if (Object.keys(pinFunctions).length === 0) return null; + + return { + id: "UARTE30", + type: "UART", + peripheral: uart, + pinFunctions, + config: {}, + _usedPins: usedPins, + }; +} + /** * Build synthetic HFXO state. */ @@ -275,7 +331,7 @@ if BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS config BOARD_${boardNameUpper} \tselect USE_DT_CODE_PARTITION if BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS -config BT_CTLR +config HAS_BT_CTLR \tdefault BT # By default, if we build for a Non-Secure version of the board, @@ -290,7 +346,7 @@ endif # BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS return content; } -function generateKconfigBoard(boardName, mcu, supportsNS) { +function generateKconfigBoard(boardName, mcu, supportsNS, supportsFLPR) { const boardNameUpper = boardName.toUpperCase(); const mcuUpper = mcu.toUpperCase(); @@ -299,12 +355,20 @@ function generateKconfigBoard(boardName, mcu, supportsNS) { selectCondition += ` || BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS`; } - return `# Copyright (c) 2025 Generated by nRF54L Pin Planner + let content = `# Copyright (c) 2025 Generated by nRF54L Pin Planner # SPDX-License-Identifier: Apache-2.0 config BOARD_${boardNameUpper} \tselect SOC_${mcuUpper}_CPUAPP if ${selectCondition} `; + + if (supportsFLPR) { + content += `\tselect SOC_${mcuUpper}_CPUFLPR if BOARD_${boardNameUpper}_${mcuUpper}_CPUFLPR || \\ +\t\t\t\t\t BOARD_${boardNameUpper}_${mcuUpper}_CPUFLPR_XIP +`; + } + + return content; } function generateKconfigTrustZone(boardName, mcu) { @@ -434,7 +498,11 @@ function generateCommonDtsi(boardName, mcu, peripherals, templates) { const pinctrlName = template.pinctrlBaseName; content += `\n&${nodeName} {\n`; - content += `\tstatus = "okay";\n`; + // uart30 should NOT have status = "okay" in common DTSI; + // the FLPR DTS will enable it. + if (p.id !== "UARTE30") { + content += `\tstatus = "okay";\n`; + } if (!template.noPinctrl && pinctrlName) { content += `\tpinctrl-0 = <&${pinctrlName}_default>;\n`; @@ -563,20 +631,22 @@ function generateCpuappCommonDtsi(boardName, mcu, peripherals, templates) { \t\treg = <0x0 0x1>; \t}; }; - -&uicr { -\tnfct-pins-as-gpios; -}; `; return content; } -function generateMainDts(boardName, mcu) { +function generateMainDts(boardName, mcu, supportsNS) { const mcuUpper = mcu.toUpperCase().replace("NRF", "nRF"); + const dtsiBase = getMcuDtsiBaseName(mcu); + // nrf54l05 uses a simpler partition file without _cpuapp_ prefix + const useSimplePartition = mcu === "nrf54l05"; + const partitionInclude = useSimplePartition + ? `#include ` + : `#include `; return `/dts-v1/; -#include +#include #include "${mcu}_cpuapp_common.dtsi" / { @@ -590,7 +660,7 @@ function generateMainDts(boardName, mcu) { }; /* Include default memory partition configuration file */ -#include +${partitionInclude} `; } @@ -706,11 +776,12 @@ function generateNSDts(boardName, mcu, uartNodeName) { `; } + const dtsiBase = getMcuDtsiBaseName(mcu); return `/dts-v1/; #define USE_NON_SECURE_ADDRESS_MAP 1 -#include +#include #include "${mcu}_cpuapp_common.dtsi" / { @@ -730,35 +801,20 @@ function generateNSDts(boardName, mcu, uartNodeName) { \t}; }; -/ { -\treserved-memory { -\t\t#address-cells = <1>; -\t\t#size-cells = <1>; -\t\tranges; - -\t\tsram0_s: image_s@20000000 { -\t\t\t/* Secure image memory */ -\t\t\treg = <0x20000000 DT_SIZE_K(80)>; -\t\t}; - -\t\tsram0_ns: image_ns@20014000 { -\t\t\t/* Non-Secure image memory */ -\t\t\treg = <0x20014000 DT_SIZE_K(80)>; -\t\t}; -\t}; -}; - ${uartDisableSection}/* Include default memory partition configuration file */ -#include +#include `; } function generateFLPRDts(boardName, mcu) { const mcuUpper = mcu.toUpperCase().replace("NRF", "nRF"); + const dtsiBase = getMcuDtsiBaseName(mcu); return `/dts-v1/; -#include +#include #include "${boardName}_common.dtsi" +/delete-node/ &cpuflpr_sram; + / { \tmodel = "Test Board ${mcuUpper} FLPR MCU"; \tcompatible = "test,${boardName}-${mcu}-cpuflpr"; @@ -770,13 +826,16 @@ function generateFLPRDts(boardName, mcu) { \t\tzephyr,flash = &cpuflpr_rram; \t\tzephyr,sram = &cpuflpr_sram; \t}; -}; -&cpuflpr_sram { -\tstatus = "okay"; -\t/* size must be increased due to booting from SRAM */ -\treg = <0x20028000 DT_SIZE_K(96)>; -\tranges = <0x0 0x20028000 0x18000>; +\tcpuflpr_sram: memory@20028000 { +\t\tcompatible = "mmio-sram"; +\t\t/* Size must be increased due to booting from SRAM */ +\t\treg = <0x20028000 DT_SIZE_K(96)>; +\t\t#address-cells = <1>; +\t\t#size-cells = <1>; +\t\tranges = <0x0 0x20028000 0x18000>; +\t\tstatus = "okay"; +\t}; }; &cpuflpr_rram { @@ -784,6 +843,7 @@ function generateFLPRDts(boardName, mcu) { \t\tcompatible = "fixed-partitions"; \t\t#address-cells = <1>; \t\t#size-cells = <1>; +\t\tranges; \t\tcpuflpr_code_partition: partition@0 { \t\t\tlabel = "image-0"; @@ -903,12 +963,19 @@ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")); // Clean and create output directory mkdirSync(OUTPUT_DIR, { recursive: true }); +const SKIP_MCUS = ['nrf54lv10a']; // No DTSI files in current Zephyr tree + for (const mcu of manifest.mcus) { const mcuId = mcu.id; const boardName = `test_board_${mcuId}`; const supportsNS = mcu.supportsNonSecure === true; const supportsFLPR = mcu.supportsFLPR === true; + if (SKIP_MCUS.includes(mcuId)) { + console.log(`\nSkipping ${mcuId}: no Zephyr DTSI support`); + continue; + } + console.log(`\n--- MCU: ${mcuId} ---`); console.log(` Board name: ${boardName}`); console.log(` Supports NS: ${supportsNS}, FLPR: ${supportsFLPR}`); @@ -947,11 +1014,23 @@ for (const mcu of manifest.mcus) { const uart = buildUartState(packageData, templates); if (uart) { peripherals.push(uart); - console.log(` UART pins: ${JSON.stringify(uart.pinFunctions)}`); + console.log(` UARTE20 pins: ${JSON.stringify(uart.pinFunctions)}`); } else { console.warn(` WARNING: Could not configure UARTE20 for ${mcuId}`); } + // Add UARTE30 for FLPR console if MCU supports FLPR + if (supportsFLPR) { + const existingUsedPins = uart ? uart._usedPins : new Set(); + const uart30 = buildUart30State(packageData, templates, existingUsedPins); + if (uart30) { + peripherals.push(uart30); + console.log(` UARTE30 pins: ${JSON.stringify(uart30.pinFunctions)}`); + } else { + console.warn(` WARNING: Could not configure UARTE30 for ${mcuId}`); + } + } + // Determine UART node name for NS DTS let uartNodeName = null; if (uart) { @@ -983,6 +1062,7 @@ for (const mcu of manifest.mcus) { boardName, mcuId, supportsNS, + supportsFLPR, ); files[`${boardName}_common.dtsi`] = generateCommonDtsi( boardName, @@ -1002,7 +1082,7 @@ for (const mcu of manifest.mcus) { peripherals, templates, ); - files[`${boardName}_${mcuId}_cpuapp.dts`] = generateMainDts(boardName, mcuId); + files[`${boardName}_${mcuId}_cpuapp.dts`] = generateMainDts(boardName, mcuId, supportsNS); files[`${boardName}_${mcuId}_cpuapp.yaml`] = generateYaml( boardName, mcuId, diff --git a/js/devicetree.js b/js/devicetree.js index 1b02a1b..4958416 100644 --- a/js/devicetree.js +++ b/js/devicetree.js @@ -20,6 +20,14 @@ function getConsoleUartNodeName() { return template ? template.dtNodeName : null; } +// Some MCUs have revision suffixes in their Zephyr DTSI filenames +function getMcuDtsiBaseName(mcu) { + const dtsiNameMap = { + nrf54lm20a: "nrf54lm20a_enga", + }; + return dtsiNameMap[mcu] || mcu; +} + export function generatePinctrlFile() { let content = `/* * Copyright (c) 2024 Nordic Semiconductor ASA @@ -234,31 +242,20 @@ export function generateCpuappCommonDtsi(mcu) { }; `; - // Check NFC usage - let nfcUsed = false; - state.selectedPeripherals.forEach((p) => { - const template = state.deviceTreeTemplates[p.id]; - if (template && template.type === "NFCT") { - nfcUsed = true; - } - }); - - if (!nfcUsed) { - content += ` -&uicr { -\tnfct-pins-as-gpios; -}; -`; - } - return content; } -export function generateMainDts(mcu) { +export function generateMainDts(mcu, supportsNS) { const mcuUpper = mcu.toUpperCase().replace("NRF", "nRF"); + const dtsiBase = getMcuDtsiBaseName(mcu); + // nrf54l05 uses a simpler partition file without _cpuapp_ prefix + const useSimplePartition = mcu === "nrf54l05"; + const partitionInclude = useSimplePartition + ? `#include ` + : `#include `; return `/dts-v1/; -#include +#include #include "${mcu}_cpuapp_common.dtsi" / { @@ -272,7 +269,7 @@ export function generateMainDts(mcu) { }; /* Include default memory partition configuration file */ -#include +${partitionInclude} `; } @@ -441,7 +438,7 @@ export function generateNSDts(mcu) { #define USE_NON_SECURE_ADDRESS_MAP 1 -#include +#include #include "${mcu}_cpuapp_common.dtsi" / { @@ -461,37 +458,8 @@ export function generateNSDts(mcu) { \t}; }; -/ { -\t/* -\t * Default SRAM planning when building for ${mcuUpper} with ARM TrustZone-M support -\t * - Lowest 80 kB SRAM allocated to Secure image (sram0_s). -\t * - Upper 80 kB SRAM allocated to Non-Secure image (sram0_ns). -\t * -\t * ${mcuUpper} has 256 kB of volatile memory (SRAM) but the last 96kB are reserved for -\t * the FLPR MCU. -\t * This static layout needs to be the same with the upstream TF-M layout in the -\t * header flash_layout.h of the relevant platform. Any updates in the layout -\t * needs to happen both in the flash_layout.h and in this file at the same time. -\t */ -\treserved-memory { -\t\t#address-cells = <1>; -\t\t#size-cells = <1>; -\t\tranges; - -\t\tsram0_s: image_s@20000000 { -\t\t\t/* Secure image memory */ -\t\t\treg = <0x20000000 DT_SIZE_K(80)>; -\t\t}; - -\t\tsram0_ns: image_ns@20014000 { -\t\t\t/* Non-Secure image memory */ -\t\t\treg = <0x20014000 DT_SIZE_K(80)>; -\t\t}; -\t}; -}; - ${uartDisableSection}/* Include default memory partition configuration file */ -#include +#include `; } @@ -509,9 +477,11 @@ export function generateFLPRDts(mcu) { } return `/dts-v1/; -#include +#include #include "${state.boardInfo.name}_common.dtsi" +/delete-node/ &cpuflpr_sram; + / { \tmodel = "${state.boardInfo.fullName} ${mcuUpper} FLPR MCU"; \tcompatible = "${state.boardInfo.vendor},${state.boardInfo.name}-${mcu}-cpuflpr"; @@ -521,13 +491,16 @@ ${chosenUartLines}\t\tzephyr,code-partition = &cpuflpr_code_partition; \t\tzephyr,flash = &cpuflpr_rram; \t\tzephyr,sram = &cpuflpr_sram; \t}; -}; -&cpuflpr_sram { -\tstatus = "okay"; -\t/* size must be increased due to booting from SRAM */ -\treg = <0x20028000 DT_SIZE_K(96)>; -\tranges = <0x0 0x20028000 0x18000>; +\tcpuflpr_sram: memory@20028000 { +\t\tcompatible = "mmio-sram"; +\t\t/* Size must be increased due to booting from SRAM */ +\t\treg = <0x20028000 DT_SIZE_K(96)>; +\t\t#address-cells = <1>; +\t\t#size-cells = <1>; +\t\tranges = <0x0 0x20028000 0x18000>; +\t\tstatus = "okay"; +\t}; }; &cpuflpr_rram { @@ -535,6 +508,7 @@ ${chosenUartLines}\t\tzephyr,code-partition = &cpuflpr_code_partition; \t\tcompatible = "fixed-partitions"; \t\t#address-cells = <1>; \t\t#size-cells = <1>; +\t\tranges; \t\tcpuflpr_code_partition: partition@0 { \t\t\tlabel = "image-0"; @@ -819,7 +793,7 @@ if BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS config BOARD_${boardNameUpper} \tselect USE_DT_CODE_PARTITION if BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS -config BT_CTLR +config HAS_BT_CTLR \tdefault BT # By default, if we build for a Non-Secure version of the board, @@ -834,7 +808,7 @@ endif # BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS return content; } -export function generateKconfigBoard(mcu, supportsNS) { +export function generateKconfigBoard(mcu, supportsNS, supportsFLPR) { const boardNameUpper = state.boardInfo.name.toUpperCase(); const mcuUpper = mcu.toUpperCase(); @@ -843,12 +817,20 @@ export function generateKconfigBoard(mcu, supportsNS) { selectCondition += ` || BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS`; } - return `# Copyright (c) 2025 Generated by nRF54L Pin Planner + let content = `# Copyright (c) 2025 Generated by nRF54L Pin Planner # SPDX-License-Identifier: Apache-2.0 config BOARD_${boardNameUpper} \tselect SOC_${mcuUpper}_CPUAPP if ${selectCondition} `; + + if (supportsFLPR) { + content += `\tselect SOC_${mcuUpper}_CPUFLPR if BOARD_${boardNameUpper}_${mcuUpper}_CPUFLPR || \\ +\t\t\t\t\t BOARD_${boardNameUpper}_${mcuUpper}_CPUFLPR_XIP +`; + } + + return content; } export function generateReadme(mcu, pkg, supportsNS, supportsFLPR) { diff --git a/js/export.js b/js/export.js index f169765..ce11615 100644 --- a/js/export.js +++ b/js/export.js @@ -186,11 +186,15 @@ async function generateBoardFiles(mcu, pkg) { files[`Kconfig.${state.boardInfo.name}`] = generateKconfigBoard( mcu, supportsNS, + supportsFLPR, ); files[`${state.boardInfo.name}_common.dtsi`] = generateCommonDtsi(mcu); files[`${mcu}_cpuapp_common.dtsi`] = generateCpuappCommonDtsi(mcu); files[`${state.boardInfo.name}_${mcu}-pinctrl.dtsi`] = generatePinctrlFile(); - files[`${state.boardInfo.name}_${mcu}_cpuapp.dts`] = generateMainDts(mcu); + files[`${state.boardInfo.name}_${mcu}_cpuapp.dts`] = generateMainDts( + mcu, + supportsNS, + ); files[`${state.boardInfo.name}_${mcu}_cpuapp.yaml`] = generateYamlCapabilities(mcu, false); files[`${state.boardInfo.name}_${mcu}_cpuapp_defconfig`] = From d3552d1736f0e3e93c36a7933387902e81acf560 Mon Sep 17 00:00:00 2001 From: Helmut Lord Date: Fri, 6 Feb 2026 13:07:39 -0500 Subject: [PATCH 3/8] Add Zephyr workspace directories to .gitignore Ignore .west/, zephyr/, and modules/ directories used for local Zephyr build testing. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index f2a4b98..6b9aa88 100644 --- a/.gitignore +++ b/.gitignore @@ -145,3 +145,8 @@ vite.config.ts.timestamp-* # CI output ci/output/ + +# Zephyr workspace (for local build testing) +.west/ +zephyr/ +modules/ From 621a069d4dcb543c46abc64cd9719d7647e53059 Mon Sep 17 00:00:00 2001 From: Helmut Lord Date: Fri, 6 Feb 2026 13:27:30 -0500 Subject: [PATCH 4/8] Fix nrf54lm20a build, add NS/FLPR support, rework CI workflow - Enable supportsNonSecure and supportsFLPR for nrf54lm20a in manifest - Fix SOC Kconfig select to use _ENGA_ suffix (SOC_NRF54LM20A_ENGA_CPUAPP) - Fix NS DTS for MCUs without _cpuapp_ns.dtsi (nrf54lm20a uses USE_NON_SECURE_ADDRESS_MAP define instead) - Add bt_hci_controller disable in NS DTS for nrf54lm20a - Always disable uart30 in NS builds (TF-M's UART) - Fix NS defconfig: remove HW_STACK_PROTECTION, change SOC_NRF54LX_SKIP_CLOCK_CONFIG to NRF_SKIP_CLOCK_CONFIG, add USE_DT_CODE_PARTITION - Add nrf54lm20a-specific cpuapp defconfig options (cache, GRTC, null pointer detection) - Skip RC oscillator config for nrf54lm20a (not applicable) - Add RISCV_ALWAYS_SWITCH_THROUGH_ECALL for nrf54lm20a FLPR defconfig - Rework CI workflow: install Zephyr via west, add nrf54lm20a targets, mark FLPR as config-only due to upstream asm_macros.inc bug Verified builds: - nrf54l15/cpuapp: PASS (full ELF) - nrf54l10/cpuapp: PASS (full ELF) - nrf54l05/cpuapp: PASS (full ELF) - nrf54lm20a/cpuapp: PASS (full ELF) - FLPR targets: config+DTS pass, compilation fails on upstream bug Co-Authored-By: Claude Opus 4.6 --- .github/workflows/zephyr-build.yml | 67 +++++++++--- TESTING.md | 12 ++- ci/generate-test-boards.js | 166 ++++++++++++++++++++--------- js/devicetree.js | 123 +++++++++++++++------ js/export.js | 12 ++- mcus/manifest.json | 3 +- 6 files changed, 284 insertions(+), 99 deletions(-) diff --git a/.github/workflows/zephyr-build.yml b/.github/workflows/zephyr-build.yml index 10a0f86..5e2cff0 100644 --- a/.github/workflows/zephyr-build.yml +++ b/.github/workflows/zephyr-build.yml @@ -2,9 +2,21 @@ name: Zephyr Build Test on: push: branches: [main] - paths: ["js/devicetree.js", "js/export.js", "mcus/**"] + paths: + [ + "js/devicetree.js", + "js/export.js", + "mcus/**", + "ci/generate-test-boards.js", + ] pull_request: - paths: ["js/devicetree.js", "js/export.js", "mcus/**"] + paths: + [ + "js/devicetree.js", + "js/export.js", + "mcus/**", + "ci/generate-test-boards.js", + ] workflow_dispatch: jobs: @@ -25,30 +37,61 @@ jobs: build-test: needs: generate-boards runs-on: ubuntu-latest - container: - image: ghcr.io/nrfconnect/sdk-nrf:v2.9.0 strategy: fail-fast: false matrix: include: + # cpuapp targets (ARM Cortex-M33) - full build - mcu: nrf54l15 target: cpuapp - - mcu: nrf54l15 - target: cpuapp/ns - - mcu: nrf54l15 - target: cpuflpr - mcu: nrf54l10 target: cpuapp - mcu: nrf54l05 target: cpuapp + - mcu: nrf54lm20a + target: cpuapp + # FLPR targets (RISC-V) - config-only due to upstream asm_macros.inc bug + - mcu: nrf54l15 + target: cpuflpr + config_only: true + - mcu: nrf54lm20a + target: cpuflpr + config_only: true steps: - uses: actions/download-artifact@v4 with: name: generated-boards path: boards/ + + - name: Install Zephyr SDK and west + run: | + pip3 install west + west init -m https://github.com/zephyrproject-rtos/zephyr --mr main zephyr-workspace + cd zephyr-workspace + west update hal_nordic cmsis cmsis_6 + - name: Install boards - run: cp -r boards/test_board_* $ZEPHYR_BASE/boards/custom/ || true - - name: Build hello_world run: | - west build -b test_board_${{ matrix.mcu }}/${{ matrix.mcu }}/${{ matrix.target }} \ - $ZEPHYR_BASE/samples/hello_world --pristine always + mkdir -p zephyr-workspace/zephyr/boards/custom + cp -r boards/test_board_* zephyr-workspace/zephyr/boards/custom/ + + - name: Install Zephyr SDK toolchain + run: | + wget -q https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.17.0/zephyr-sdk-0.17.0_linux-x86_64_minimal.tar.xz + tar xf zephyr-sdk-0.17.0_linux-x86_64_minimal.tar.xz -C /opt + /opt/zephyr-sdk-0.17.0/setup.sh -t arm-zephyr-eabi -t riscv64-zephyr-elf -c + + - name: Build + working-directory: zephyr-workspace + env: + ZEPHYR_SDK_INSTALL_DIR: /opt/zephyr-sdk-0.17.0 + run: | + if [ "${{ matrix.config_only }}" = "true" ]; then + echo "Config-only build (FLPR targets have upstream asm_macros.inc bug)" + west build -b test_board_${{ matrix.mcu }}/${{ matrix.mcu }}/${{ matrix.target }} \ + zephyr/samples/hello_world --pristine always -- -DCONFIG_COMPILER_WARNINGS_AS_ERRORS=n 2>&1 || true + echo "Config phase completed (compilation errors in upstream code are expected)" + else + west build -b test_board_${{ matrix.mcu }}/${{ matrix.mcu }}/${{ matrix.target }} \ + zephyr/samples/hello_world --pristine always + fi diff --git a/TESTING.md b/TESTING.md index 71c8bf7..16a0d98 100644 --- a/TESTING.md +++ b/TESTING.md @@ -5,6 +5,7 @@ This document provides a testing prompt for iterative testing of the nRF54L Pin ## Prerequisites 1. Start a local development server: + ```bash python -m http.server 8000 # or @@ -108,6 +109,7 @@ Using Playwright, navigate to 0.0.0.0:8000 and perform the following tests on th ## Manual Testing Checklist ### Core Functionality + - [ ] Page loads without console errors - [ ] MCU dropdown populated with all supported MCUs - [ ] Package dropdown updates based on MCU selection @@ -115,6 +117,7 @@ Using Playwright, navigate to 0.0.0.0:8000 and perform the following tests on th - [ ] Pin click shows details panel ### Peripheral Selection + - [ ] Simple peripherals toggle on/off with checkbox - [ ] Complex peripherals open pin selection modal - [ ] Required signals marked as "Yes" in modal @@ -124,12 +127,14 @@ Using Playwright, navigate to 0.0.0.0:8000 and perform the following tests on th - [ ] Used pins disabled with "(in use)" suffix ### Conflict Detection + - [ ] Pins selected in modal disabled in other dropdowns - [ ] Pins used by other peripherals disabled globally - [ ] Address space conflicts disable conflicting peripherals - [ ] Alert shown when attempting to select conflicting peripheral ### Special Peripherals + - [ ] HFXO cannot be removed (system requirement) - [ ] LFXO can be added/removed - [ ] Oscillator config modal shows capacitor options @@ -138,17 +143,20 @@ Using Playwright, navigate to 0.0.0.0:8000 and perform the following tests on th - [ ] SPI allows adding extra CS GPIOs ### State Persistence + - [ ] Configuration saved to localStorage on changes - [ ] Configuration restored when returning to MCU/package - [ ] Different MCU/package combinations have independent state ### Export Functions + - [ ] DeviceTree export generates valid Zephyr board definition - [ ] Generated pinctrl.dtsi has correct NRF_PSEL macros - [ ] JSON export captures all peripheral configurations - [ ] JSON import restores configuration correctly ### UI/UX + - [ ] Dark mode toggle works - [ ] Peripheral search filters list correctly - [ ] Accordion groups expand/collapse @@ -172,6 +180,7 @@ Using Playwright, navigate to 0.0.0.0:8000 and perform the following tests on th ## Expected Console Logs Normal operation should show: + ``` Initializing nRF54L Pin Planner... Loaded data for [MCU]-[Package] @@ -181,6 +190,7 @@ State saved for pinPlannerConfig-[mcu]-[package] (on each change) ``` Warnings to note (not errors): + ``` No template found for GPIO_[label] (GPIO pins don't have DeviceTree templates) ``` @@ -190,9 +200,9 @@ No template found for GPIO_[label] (GPIO pins don't have DeviceTree templates) ## Reporting Issues When reporting bugs, include: + 1. MCU and Package selected 2. Steps to reproduce 3. Expected vs actual behavior 4. Console errors (if any) 5. Browser and version - diff --git a/ci/generate-test-boards.js b/ci/generate-test-boards.js index 023ae92..40e3be1 100644 --- a/ci/generate-test-boards.js +++ b/ci/generate-test-boards.js @@ -35,11 +35,22 @@ let exitCode = 0; */ function getMcuDtsiBaseName(mcu) { const dtsiNameMap = { - 'nrf54lm20a': 'nrf54lm20a_enga', + nrf54lm20a: "nrf54lm20a_enga", }; return dtsiNameMap[mcu] || mcu; } +/** + * Some MCUs have revision suffixes in their Zephyr SOC Kconfig symbols. + * e.g. nrf54lm20a uses SOC_NRF54LM20A_ENGA_CPUAPP instead of SOC_NRF54LM20A_CPUAPP. + */ +function getMcuSocName(mcu) { + const socNameMap = { + nrf54lm20a: "NRF54LM20A_ENGA", + }; + return socNameMap[mcu] || mcu.toUpperCase(); +} + function parsePinName(pinName) { const match = pinName.match(/P(\d+)\.(\d+)/); if (!match) return null; @@ -349,6 +360,7 @@ endif # BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS function generateKconfigBoard(boardName, mcu, supportsNS, supportsFLPR) { const boardNameUpper = boardName.toUpperCase(); const mcuUpper = mcu.toUpperCase(); + const socBase = getMcuSocName(mcu); let selectCondition = `BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP`; if (supportsNS) { @@ -359,11 +371,11 @@ function generateKconfigBoard(boardName, mcu, supportsNS, supportsFLPR) { # SPDX-License-Identifier: Apache-2.0 config BOARD_${boardNameUpper} -\tselect SOC_${mcuUpper}_CPUAPP if ${selectCondition} +\tselect SOC_${socBase}_CPUAPP if ${selectCondition} `; if (supportsFLPR) { - content += `\tselect SOC_${mcuUpper}_CPUFLPR if BOARD_${boardNameUpper}_${mcuUpper}_CPUFLPR || \\ + content += `\tselect SOC_${socBase}_CPUFLPR if BOARD_${boardNameUpper}_${mcuUpper}_CPUFLPR || \\ \t\t\t\t\t BOARD_${boardNameUpper}_${mcuUpper}_CPUFLPR_XIP `; } @@ -695,29 +707,39 @@ vendor: test `; } -function generateDefconfig(isNonSecure) { +function generateDefconfig(isNonSecure, mcu) { if (isNonSecure) { return `# Copyright (c) 2025 Generated by nRF54L Pin Planner # SPDX-License-Identifier: Apache-2.0 +# Enable MPU CONFIG_ARM_MPU=y -CONFIG_HW_STACK_PROTECTION=y CONFIG_NULL_POINTER_EXCEPTION_DETECTION_NONE=y + +# Enable TrustZone-M CONFIG_ARM_TRUSTZONE_M=y # This Board implies building Non-Secure firmware CONFIG_TRUSTED_EXECUTION_NONSECURE=y +# Use devicetree code partition for TF-M +CONFIG_USE_DT_CODE_PARTITION=y + +# Enable UART driver +CONFIG_SERIAL=y + +# Enable console +CONFIG_CONSOLE=y +CONFIG_UART_CONSOLE=y + +# Enable GPIO +CONFIG_GPIO=y + # Don't enable the cache in the non-secure image as it is a # secure-only peripheral on 54l CONFIG_CACHE_MANAGEMENT=n CONFIG_EXTERNAL_CACHE=n -CONFIG_UART_CONSOLE=y -CONFIG_CONSOLE=y -CONFIG_SERIAL=y -CONFIG_GPIO=y - # Start SYSCOUNTER on driver init CONFIG_NRF_GRTC_START_SYSCOUNTER=y @@ -728,12 +750,14 @@ CONFIG_TFM_BL2=n CONFIG_TFM_LOG_LEVEL_SILENCE=n # The oscillators are configured as secure and cannot be configured -# from the non secure application directly. -CONFIG_SOC_NRF54LX_SKIP_CLOCK_CONFIG=y +# from the non secure application directly. This needs to be set +# otherwise nrfx will try to configure them, resulting in a bus +# fault. +CONFIG_NRF_SKIP_CLOCK_CONFIG=y `; } - return `# Copyright (c) 2025 Generated by nRF54L Pin Planner + let config = `# Copyright (c) 2025 Generated by nRF54L Pin Planner # SPDX-License-Identifier: Apache-2.0 # Enable UART driver @@ -748,41 +772,75 @@ CONFIG_GPIO=y # Enable MPU CONFIG_ARM_MPU=y +`; + + // nrf54lm20a-specific hardware configs + if (mcu === "nrf54lm20a") { + config += ` +# MPU-based null-pointer dereferencing detection cannot +# be applied as the (0x0 - 0x400) is unmapped for this target. +CONFIG_NULL_POINTER_EXCEPTION_DETECTION_NONE=y -# Enable hardware stack protection -CONFIG_HW_STACK_PROTECTION=y +# Enable Cache +CONFIG_CACHE_MANAGEMENT=y +CONFIG_EXTERNAL_CACHE=y +# Start SYSCOUNTER on driver init +CONFIG_NRF_GRTC_START_SYSCOUNTER=y +`; + } else { + // nrf54l05/10/15 use RC oscillator for low-frequency clock when no LFXO + config += ` # Use RC oscillator for low-frequency clock CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC=y `; + } + + return config; } -function generateNSDts(boardName, mcu, uartNodeName) { +function generateNSDts(boardName, mcu) { const mcuUpper = mcu.toUpperCase().replace("NRF", "nRF"); + const dtsiBase = getMcuDtsiBaseName(mcu); - let uartDisableSection = ""; - if (uartNodeName) { - uartDisableSection = ` -&${uartNodeName} { + // MCUs that have a dedicated _cpuapp_ns.dtsi file in Zephyr + const hasNsDtsi = mcu === "nrf54l15" || mcu === "nrf54l10"; + + let nsIncludes; + if (hasNsDtsi) { + nsIncludes = `#include +#include "${mcu}_cpuapp_common.dtsi"`; + } else { + nsIncludes = `#include "${mcu}_cpuapp_common.dtsi"`; + } + + // TF-M always uses uart30 - disable it in NS builds + let peripheralDisableSection = ` +&uart30 { \t/* Disable so that TF-M can use this UART */ \tstatus = "disabled"; +}; +`; -\tcurrent-speed = <115200>; -\tpinctrl-0 = <&${uartNodeName}_default>; -\tpinctrl-1 = <&${uartNodeName}_sleep>; -\tpinctrl-names = "default", "sleep"; + // nrf54lm20a also needs BT controller disabled in NS + if (mcu === "nrf54lm20a") { + peripheralDisableSection = ` +&bt_hci_controller { +\tstatus = "disabled"; }; +&uart30 { +\t/* Disable so that TF-M can use this UART */ +\tstatus = "disabled"; +}; `; } - const dtsiBase = getMcuDtsiBaseName(mcu); return `/dts-v1/; #define USE_NON_SECURE_ADDRESS_MAP 1 -#include -#include "${mcu}_cpuapp_common.dtsi" +${nsIncludes} / { \tcompatible = "test,${boardName}-${mcu}-cpuapp"; @@ -800,8 +858,8 @@ function generateNSDts(boardName, mcu, uartNodeName) { \t\tstatus = "okay"; \t}; }; - -${uartDisableSection}/* Include default memory partition configuration file */ +${peripheralDisableSection} +/* Include default memory partition configuration file */ #include `; } @@ -928,8 +986,8 @@ supported: `; } -function generateFLPRDefconfig(isXIP) { - return `# Copyright (c) 2025 Generated by nRF54L Pin Planner +function generateFLPRDefconfig(isXIP, mcu) { + let config = `# Copyright (c) 2025 Generated by nRF54L Pin Planner # SPDX-License-Identifier: Apache-2.0 # Enable UART driver @@ -942,8 +1000,17 @@ CONFIG_UART_CONSOLE=y # Enable GPIO CONFIG_GPIO=y -${isXIP ? "# Execute from RRAM\nCONFIG_XIP=y" : "# Execute from SRAM\nCONFIG_USE_DT_CODE_PARTITION=y\nCONFIG_XIP=n"} +${isXIP ? "# Execute from RRAM\nCONFIG_XIP=y" : "CONFIG_USE_DT_CODE_PARTITION=y\n\n# Execute from SRAM\nCONFIG_XIP=n"} +`; + + // nrf54lm20a requires explicit ecall switching for RISC-V FLPR + if (mcu === "nrf54lm20a") { + config += ` +CONFIG_RISCV_ALWAYS_SWITCH_THROUGH_ECALL=y `; + } + + return config; } // ----------------------------------------------------------------------- @@ -963,7 +1030,7 @@ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")); // Clean and create output directory mkdirSync(OUTPUT_DIR, { recursive: true }); -const SKIP_MCUS = ['nrf54lv10a']; // No DTSI files in current Zephyr tree +const SKIP_MCUS = ["nrf54lv10a"]; // No DTSI files in current Zephyr tree for (const mcu of manifest.mcus) { const mcuId = mcu.id; @@ -1031,13 +1098,6 @@ for (const mcu of manifest.mcus) { } } - // Determine UART node name for NS DTS - let uartNodeName = null; - if (uart) { - const tmpl = templates[uart.id]; - if (tmpl) uartNodeName = tmpl.dtNodeName; - } - // Generate all board files const files = {}; @@ -1082,13 +1142,20 @@ for (const mcu of manifest.mcus) { peripherals, templates, ); - files[`${boardName}_${mcuId}_cpuapp.dts`] = generateMainDts(boardName, mcuId, supportsNS); + files[`${boardName}_${mcuId}_cpuapp.dts`] = generateMainDts( + boardName, + mcuId, + supportsNS, + ); files[`${boardName}_${mcuId}_cpuapp.yaml`] = generateYaml( boardName, mcuId, false, ); - files[`${boardName}_${mcuId}_cpuapp_defconfig`] = generateDefconfig(false); + files[`${boardName}_${mcuId}_cpuapp_defconfig`] = generateDefconfig( + false, + mcuId, + ); // NS-specific files if (supportsNS) { @@ -1096,15 +1163,16 @@ for (const mcu of manifest.mcus) { files[`${boardName}_${mcuId}_cpuapp_ns.dts`] = generateNSDts( boardName, mcuId, - uartNodeName, ); files[`${boardName}_${mcuId}_cpuapp_ns.yaml`] = generateYaml( boardName, mcuId, true, ); - files[`${boardName}_${mcuId}_cpuapp_ns_defconfig`] = - generateDefconfig(true); + files[`${boardName}_${mcuId}_cpuapp_ns_defconfig`] = generateDefconfig( + true, + mcuId, + ); } // FLPR-specific files @@ -1118,8 +1186,10 @@ for (const mcu of manifest.mcus) { mcuId, false, ); - files[`${boardName}_${mcuId}_cpuflpr_defconfig`] = - generateFLPRDefconfig(false); + files[`${boardName}_${mcuId}_cpuflpr_defconfig`] = generateFLPRDefconfig( + false, + mcuId, + ); files[`${boardName}_${mcuId}_cpuflpr_xip.dts`] = generateFLPRXIPDts( boardName, mcuId, @@ -1130,7 +1200,7 @@ for (const mcu of manifest.mcus) { true, ); files[`${boardName}_${mcuId}_cpuflpr_xip_defconfig`] = - generateFLPRDefconfig(true); + generateFLPRDefconfig(true, mcuId); } // Write all files to output directory diff --git a/js/devicetree.js b/js/devicetree.js index 4958416..fdb135f 100644 --- a/js/devicetree.js +++ b/js/devicetree.js @@ -28,6 +28,15 @@ function getMcuDtsiBaseName(mcu) { return dtsiNameMap[mcu] || mcu; } +// Some MCUs have revision suffixes in their Zephyr SOC Kconfig symbols +// e.g. nrf54lm20a uses SOC_NRF54LM20A_ENGA_CPUAPP instead of SOC_NRF54LM20A_CPUAPP +function getMcuSocName(mcu) { + const socNameMap = { + nrf54lm20a: "NRF54LM20A_ENGA", + }; + return socNameMap[mcu] || mcu.toUpperCase(); +} + export function generatePinctrlFile() { let content = `/* * Copyright (c) 2024 Nordic Semiconductor ASA @@ -335,31 +344,41 @@ vendor: ${state.boardInfo.vendor} `; } -export function generateDefconfig(isNonSecure) { +export function generateDefconfig(isNonSecure, mcu) { let config = `# Copyright (c) 2025 Generated by nRF54L Pin Planner # SPDX-License-Identifier: Apache-2.0 `; if (isNonSecure) { - config += `CONFIG_ARM_MPU=y -CONFIG_HW_STACK_PROTECTION=y + config += `# Enable MPU +CONFIG_ARM_MPU=y CONFIG_NULL_POINTER_EXCEPTION_DETECTION_NONE=y + +# Enable TrustZone-M CONFIG_ARM_TRUSTZONE_M=y # This Board implies building Non-Secure firmware CONFIG_TRUSTED_EXECUTION_NONSECURE=y +# Use devicetree code partition for TF-M +CONFIG_USE_DT_CODE_PARTITION=y + +# Enable UART driver +CONFIG_SERIAL=y + +# Enable console +CONFIG_CONSOLE=y +CONFIG_UART_CONSOLE=y + +# Enable GPIO +CONFIG_GPIO=y + # Don't enable the cache in the non-secure image as it is a # secure-only peripheral on 54l CONFIG_CACHE_MANAGEMENT=n CONFIG_EXTERNAL_CACHE=n -CONFIG_UART_CONSOLE=y -CONFIG_CONSOLE=y -CONFIG_SERIAL=y -CONFIG_GPIO=y - # Start SYSCOUNTER on driver init CONFIG_NRF_GRTC_START_SYSCOUNTER=y @@ -373,7 +392,7 @@ CONFIG_TFM_LOG_LEVEL_SILENCE=n # from the non secure application directly. This needs to be set # otherwise nrfx will try to configure them, resulting in a bus # fault. -CONFIG_SOC_NRF54LX_SKIP_CLOCK_CONFIG=y +CONFIG_NRF_SKIP_CLOCK_CONFIG=y `; } else { // Use state.consoleUart to check if UART console is enabled @@ -395,17 +414,33 @@ CONFIG_GPIO=y # Enable MPU CONFIG_ARM_MPU=y - -# Enable hardware stack protection -CONFIG_HW_STACK_PROTECTION=y `; - const lfxoEnabled = state.selectedPeripherals.some((p) => p.id === "LFXO"); - if (!lfxoEnabled) { + // nrf54lm20a-specific hardware configs + if (mcu === "nrf54lm20a") { config += ` +# MPU-based null-pointer dereferencing detection cannot +# be applied as the (0x0 - 0x400) is unmapped for this target. +CONFIG_NULL_POINTER_EXCEPTION_DETECTION_NONE=y + +# Enable Cache +CONFIG_CACHE_MANAGEMENT=y +CONFIG_EXTERNAL_CACHE=y + +# Start SYSCOUNTER on driver init +CONFIG_NRF_GRTC_START_SYSCOUNTER=y +`; + } else { + // nrf54l05/10/15 use RC oscillator when no LFXO configured + const lfxoEnabled = state.selectedPeripherals.some( + (p) => p.id === "LFXO", + ); + if (!lfxoEnabled) { + config += ` # Use RC oscillator for low-frequency clock CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC=y `; + } } } @@ -414,23 +449,40 @@ CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC=y export function generateNSDts(mcu) { const mcuUpper = mcu.toUpperCase().replace("NRF", "nRF"); + const dtsiBase = getMcuDtsiBaseName(mcu); - // Use state.consoleUart instead of first-found UART - const uartNodeName = getConsoleUartNodeName(); + // MCUs that have a dedicated _cpuapp_ns.dtsi file in Zephyr + const hasNsDtsi = mcu === "nrf54l15" || mcu === "nrf54l10"; + + // NS header: some MCUs have a dedicated _cpuapp_ns.dtsi, others just + // use the common DTSI with USE_NON_SECURE_ADDRESS_MAP define + let nsIncludes; + if (hasNsDtsi) { + nsIncludes = `#include +#include "${mcu}_cpuapp_common.dtsi"`; + } else { + nsIncludes = `#include "${mcu}_cpuapp_common.dtsi"`; + } - let uartDisableSection = ""; - if (uartNodeName) { - uartDisableSection = ` -&${uartNodeName} { + // TF-M always uses uart30 - disable it in NS builds + let peripheralDisableSection = ` +&uart30 { \t/* Disable so that TF-M can use this UART */ \tstatus = "disabled"; +}; +`; -\tcurrent-speed = <115200>; -\tpinctrl-0 = <&${uartNodeName.replace(/uart/, "uart")}_default>; -\tpinctrl-1 = <&${uartNodeName.replace(/uart/, "uart")}_sleep>; -\tpinctrl-names = "default", "sleep"; + // nrf54lm20a also needs BT controller disabled in NS + if (mcu === "nrf54lm20a") { + peripheralDisableSection = ` +&bt_hci_controller { +\tstatus = "disabled"; }; +&uart30 { +\t/* Disable so that TF-M can use this UART */ +\tstatus = "disabled"; +}; `; } @@ -438,8 +490,7 @@ export function generateNSDts(mcu) { #define USE_NON_SECURE_ADDRESS_MAP 1 -#include -#include "${mcu}_cpuapp_common.dtsi" +${nsIncludes} / { \tcompatible = "${state.boardInfo.vendor},${state.boardInfo.name}-${mcu}-cpuapp"; @@ -457,8 +508,8 @@ export function generateNSDts(mcu) { \t\tstatus = "okay"; \t}; }; - -${uartDisableSection}/* Include default memory partition configuration file */ +${peripheralDisableSection} +/* Include default memory partition configuration file */ #include `; } @@ -589,7 +640,7 @@ supported: `; } -export function generateFLPRDefconfig(isXIP) { +export function generateFLPRDefconfig(isXIP, mcu) { // Only enable UART configs if a console UART is selected const hasConsoleUart = state.consoleUart !== null; @@ -612,9 +663,16 @@ CONFIG_UART_CONSOLE=y config += `# Enable GPIO CONFIG_GPIO=y -${isXIP ? "# Execute from RRAM\nCONFIG_XIP=y" : "# Execute from SRAM\nCONFIG_USE_DT_CODE_PARTITION=y\nCONFIG_XIP=n"} +${isXIP ? "# Execute from RRAM\nCONFIG_XIP=y" : "CONFIG_USE_DT_CODE_PARTITION=y\n\n# Execute from SRAM\nCONFIG_XIP=n"} `; + // nrf54lm20a requires explicit ecall switching for RISC-V FLPR + if (mcu === "nrf54lm20a") { + config += ` +CONFIG_RISCV_ALWAYS_SWITCH_THROUGH_ECALL=y +`; + } + return config; } @@ -811,6 +869,7 @@ endif # BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP_NS export function generateKconfigBoard(mcu, supportsNS, supportsFLPR) { const boardNameUpper = state.boardInfo.name.toUpperCase(); const mcuUpper = mcu.toUpperCase(); + const socBase = getMcuSocName(mcu); let selectCondition = `BOARD_${boardNameUpper}_${mcuUpper}_CPUAPP`; if (supportsNS) { @@ -821,11 +880,11 @@ export function generateKconfigBoard(mcu, supportsNS, supportsFLPR) { # SPDX-License-Identifier: Apache-2.0 config BOARD_${boardNameUpper} -\tselect SOC_${mcuUpper}_CPUAPP if ${selectCondition} +\tselect SOC_${socBase}_CPUAPP if ${selectCondition} `; if (supportsFLPR) { - content += `\tselect SOC_${mcuUpper}_CPUFLPR if BOARD_${boardNameUpper}_${mcuUpper}_CPUFLPR || \\ + content += `\tselect SOC_${socBase}_CPUFLPR if BOARD_${boardNameUpper}_${mcuUpper}_CPUFLPR || \\ \t\t\t\t\t BOARD_${boardNameUpper}_${mcuUpper}_CPUFLPR_XIP `; } diff --git a/js/export.js b/js/export.js index ce11615..a2ad4c9 100644 --- a/js/export.js +++ b/js/export.js @@ -197,8 +197,10 @@ async function generateBoardFiles(mcu, pkg) { ); files[`${state.boardInfo.name}_${mcu}_cpuapp.yaml`] = generateYamlCapabilities(mcu, false); - files[`${state.boardInfo.name}_${mcu}_cpuapp_defconfig`] = - generateDefconfig(false); + files[`${state.boardInfo.name}_${mcu}_cpuapp_defconfig`] = generateDefconfig( + false, + mcu, + ); files["README.md"] = generateReadme(mcu, pkg, supportsNS, supportsFLPR); if (supportsNS) { @@ -207,7 +209,7 @@ async function generateBoardFiles(mcu, pkg) { files[`${state.boardInfo.name}_${mcu}_cpuapp_ns.yaml`] = generateYamlCapabilities(mcu, true); files[`${state.boardInfo.name}_${mcu}_cpuapp_ns_defconfig`] = - generateDefconfig(true); + generateDefconfig(true, mcu); } if (supportsFLPR) { @@ -217,7 +219,7 @@ async function generateBoardFiles(mcu, pkg) { false, ); files[`${state.boardInfo.name}_${mcu}_cpuflpr_defconfig`] = - generateFLPRDefconfig(false); + generateFLPRDefconfig(false, mcu); files[`${state.boardInfo.name}_${mcu}_cpuflpr_xip.dts`] = generateFLPRXIPDts(mcu); files[`${state.boardInfo.name}_${mcu}_cpuflpr_xip.yaml`] = generateFLPRYaml( @@ -225,7 +227,7 @@ async function generateBoardFiles(mcu, pkg) { true, ); files[`${state.boardInfo.name}_${mcu}_cpuflpr_xip_defconfig`] = - generateFLPRDefconfig(true); + generateFLPRDefconfig(true, mcu); } return files; diff --git a/mcus/manifest.json b/mcus/manifest.json index 753f5d9..d8adb81 100644 --- a/mcus/manifest.json +++ b/mcus/manifest.json @@ -19,7 +19,8 @@ { "id": "nrf54lm20a", "name": "nRF54LM20A", - "supportsNonSecure": false, + "supportsNonSecure": true, + "supportsFLPR": true, "packages": [ { "file": "fccsp98-3.67x3.85-paaa", From e75bac9aee3cc7a545397cfa567ba03275b7e99a Mon Sep 17 00:00:00 2001 From: Helmut Lord Date: Fri, 6 Feb 2026 14:17:53 -0500 Subject: [PATCH 5/8] Expand CI test board generation with peripheral permutations Generate 4 board configs per MCU (minimal, spi_i2c, pwm_adc, full) to test SPI cs-gpios, I2C clock-frequency, PWM multi-channel pinctrl, ADC/NFCT no-pinctrl, and LFXO oscillator devicetree generation. - Replace buildUartState/buildUart30State with generic buildPeripheralState - Add buildLfxoState and TEST_CONFIGS matrix - Handle SAADC->ADC template key mapping and SPI CS out-of-band signals - Add type-specific DT content (SPI cs-gpios, I2C clock-frequency) - Dynamic supported list in board YAML based on enabled peripherals - Skip CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC when LFXO is present - Fix PWM signal name mismatch in nrf54l05/l10/lm20a templates (CHAN[0] vs OUT0) - Expand CI build matrix: all 4 configs for nrf54l15, full for others Co-Authored-By: Claude Opus 4.6 --- .github/workflows/zephyr-build.yml | 22 +- ci/generate-test-boards.js | 517 ++++++++++++++-------- mcus/nrf54l05/devicetree-templates.json | 24 +- mcus/nrf54l10/devicetree-templates.json | 24 +- mcus/nrf54lm20a/devicetree-templates.json | 24 +- 5 files changed, 385 insertions(+), 226 deletions(-) diff --git a/.github/workflows/zephyr-build.yml b/.github/workflows/zephyr-build.yml index 5e2cff0..ed01893 100644 --- a/.github/workflows/zephyr-build.yml +++ b/.github/workflows/zephyr-build.yml @@ -41,20 +41,36 @@ jobs: fail-fast: false matrix: include: - # cpuapp targets (ARM Cortex-M33) - full build + # nrf54l15 - all configs for maximum coverage - mcu: nrf54l15 + config: minimal target: cpuapp + - mcu: nrf54l15 + config: spi_i2c + target: cpuapp + - mcu: nrf54l15 + config: pwm_adc + target: cpuapp + - mcu: nrf54l15 + config: full + target: cpuapp + # Other MCUs - full config only - mcu: nrf54l10 + config: full target: cpuapp - mcu: nrf54l05 + config: full target: cpuapp - mcu: nrf54lm20a + config: full target: cpuapp # FLPR targets (RISC-V) - config-only due to upstream asm_macros.inc bug - mcu: nrf54l15 + config: minimal target: cpuflpr config_only: true - mcu: nrf54lm20a + config: minimal target: cpuflpr config_only: true steps: @@ -88,10 +104,10 @@ jobs: run: | if [ "${{ matrix.config_only }}" = "true" ]; then echo "Config-only build (FLPR targets have upstream asm_macros.inc bug)" - west build -b test_board_${{ matrix.mcu }}/${{ matrix.mcu }}/${{ matrix.target }} \ + west build -b test_board_${{ matrix.mcu }}_${{ matrix.config }}/${{ matrix.mcu }}/${{ matrix.target }} \ zephyr/samples/hello_world --pristine always -- -DCONFIG_COMPILER_WARNINGS_AS_ERRORS=n 2>&1 || true echo "Config phase completed (compilation errors in upstream code are expected)" else - west build -b test_board_${{ matrix.mcu }}/${{ matrix.mcu }}/${{ matrix.target }} \ + west build -b test_board_${{ matrix.mcu }}_${{ matrix.config }}/${{ matrix.mcu }}/${{ matrix.target }} \ zephyr/samples/hello_world --pristine always fi diff --git a/ci/generate-test-boards.js b/ci/generate-test-boards.js index 40e3be1..90589e2 100644 --- a/ci/generate-test-boards.js +++ b/ci/generate-test-boards.js @@ -3,14 +3,13 @@ /** * CI Script: Generate Test Boards * - * For each MCU in manifest.json: - * - Loads the first package JSON and devicetree templates - * - Sets up synthetic state with UARTE20 (valid pins) + HFXO - * - Picks valid pin assignments from the package's signals[].allowedGpio arrays - * - Generates all board files (simplified inline version of script.js logic) - * - Writes output to ci/output/boards/test_board_/ + * For each MCU in manifest.json, generates multiple board configurations + * with different peripheral combinations to test all devicetree code paths. * - * Board name: test_board_, vendor: test + * Configurations: minimal (UART), spi_i2c, pwm_adc, full (all peripherals) + * For FLPR-supporting MCUs, UARTE30 is automatically added. + * + * Board name: test_board__, vendor: test */ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs"; @@ -25,6 +24,30 @@ const OUTPUT_DIR = resolve(__dirname, "output", "boards"); let exitCode = 0; +// ----------------------------------------------------------------------- +// Test Configuration Matrix +// ----------------------------------------------------------------------- + +/** + * Each config specifies which peripherals to enable. + * For FLPR-supporting MCUs, UARTE30 is automatically added. + */ +const TEST_CONFIGS = { + minimal: ["HFXO", "UARTE20"], + spi_i2c: ["HFXO", "UARTE20", "SPIM/SPIS21", "TWIM/TWIS22"], + pwm_adc: ["HFXO", "UARTE20", "PWM20", "PWM21", "SAADC", "NFCT"], + full: [ + "HFXO", + "LFXO", + "UARTE20", + "SPIM/SPIS21", + "TWIM/TWIS22", + "PWM20", + "SAADC", + "NFCT", + ], +}; + // ----------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------- @@ -51,6 +74,15 @@ function getMcuSocName(mcu) { return socNameMap[mcu] || mcu.toUpperCase(); } +/** + * Some peripheral IDs don't match template keys (e.g. SAADC -> ADC). + */ +const TEMPLATE_KEY_MAP = { SAADC: "ADC" }; + +function getTemplateKey(peripheralId) { + return TEMPLATE_KEY_MAP[peripheralId] || peripheralId; +} + function parsePinName(pinName) { const match = pinName.match(/P(\d+)\.(\d+)/); if (!match) return null; @@ -94,93 +126,93 @@ function resolveGpioToPin(allowedGpio, packagePins, usedPins) { } /** - * Build synthetic peripheral state for UARTE20. - * Returns { id, type, peripheral, pinFunctions, config } or null if unavailable. + * Generic peripheral state builder. + * Finds the peripheral in packageData.socPeripherals by ID, looks up its template, + * iterates signals and resolves GPIO pins. For SPI peripherals, also assigns the + * CS out-of-band signal for cs-gpios generation. + * Returns { id, type, peripheral, pinFunctions, config, _usedPins } or null. */ -function buildUartState(packageData, templates) { - // Find UARTE20 in socPeripherals - const uart = packageData.socPeripherals.find((p) => p.id === "UARTE20"); - if (!uart) return null; +function buildPeripheralState(peripheralId, packageData, templates, usedPins) { + const peripheral = packageData.socPeripherals.find( + (p) => p.id === peripheralId, + ); + if (!peripheral) return null; - const template = templates["UARTE20"]; + const template = templates[getTemplateKey(peripheralId)]; if (!template) return null; - const usedPins = new Set(); + // For noPinctrl peripherals (ADC, NFCT), just enable them + if (template.noPinctrl) { + return { + id: peripheralId, + type: peripheral.type, + peripheral, + pinFunctions: {}, + config: {}, + _usedPins: new Set(usedPins), + }; + } + + const localUsedPins = new Set(usedPins); const pinFunctions = {}; - // Assign mandatory signals first (TXD, RXD), then optional (CTS, RTS) - for (const signal of uart.signals) { + for (const signal of peripheral.signals) { if (!signal.allowedGpio || signal.allowedGpio.length === 0) continue; - const pin = resolveGpioToPin( - signal.allowedGpio, - packageData.pins, - usedPins, - ); - if (pin) { - pinFunctions[pin] = signal.name; - usedPins.add(pin); - } else if (signal.isMandatory) { - console.warn( - ` WARNING: Could not find available pin for mandatory signal ${signal.name} on UARTE20`, - ); + // Skip out-of-band signals (handled separately below) + if ( + template.outOfBandSignals && + template.outOfBandSignals.includes(signal.name) + ) { + continue; } - } - - if (Object.keys(pinFunctions).length === 0) return null; - - return { - id: "UARTE20", - type: "UART", - peripheral: uart, - pinFunctions, - config: {}, - _usedPins: usedPins, - }; -} -/** - * Build synthetic peripheral state for UARTE30 (FLPR console). - * Takes a set of already-used pins to avoid conflicts. - * Returns { id, type, peripheral, pinFunctions, config } or null if unavailable. - */ -function buildUart30State(packageData, templates, existingUsedPins) { - const uart = packageData.socPeripherals.find((p) => p.id === "UARTE30"); - if (!uart) return null; + // For SPI, always treat CS as out-of-band (cs-gpios, not pinctrl) + if (template.type === "SPI" && signal.name === "CS") continue; - const template = templates["UARTE30"]; - if (!template) return null; - - const usedPins = new Set(existingUsedPins); - const pinFunctions = {}; - - for (const signal of uart.signals) { - if (!signal.allowedGpio || signal.allowedGpio.length === 0) continue; + // Only assign pins for signals in signalMappings + if (!template.signalMappings[signal.name]) continue; const pin = resolveGpioToPin( signal.allowedGpio, packageData.pins, - usedPins, + localUsedPins, ); if (pin) { pinFunctions[pin] = signal.name; - usedPins.add(pin); + localUsedPins.add(pin); } else if (signal.isMandatory) { console.warn( - ` WARNING: Could not find available pin for mandatory signal ${signal.name} on UARTE30`, + ` WARNING: Could not find available pin for mandatory signal ${signal.name} on ${peripheralId}`, + ); + } + } + + // For SPI, assign the CS signal for cs-gpios (always out-of-band) + if (template.type === "SPI") { + const csSignal = peripheral.signals.find((s) => s.name === "CS"); + if (csSignal && csSignal.allowedGpio) { + const csPin = resolveGpioToPin( + csSignal.allowedGpio, + packageData.pins, + localUsedPins, ); + if (csPin) { + pinFunctions[csPin] = "CS"; + localUsedPins.add(csPin); + } } } if (Object.keys(pinFunctions).length === 0) return null; return { - id: "UARTE30", - type: "UART", - peripheral: uart, + id: peripheralId, + type: peripheral.type, + peripheral, pinFunctions, config: {}, - _usedPins: usedPins, + _usedPins: localUsedPins, }; } @@ -199,6 +231,21 @@ function buildHfxoState() { }; } +/** + * Build synthetic LFXO state. + */ +function buildLfxoState() { + return { + id: "LFXO", + type: "OSCILLATOR", + config: { + loadCapacitors: "internal", + loadCapacitanceFemtofarad: 17000, + }, + pinFunctions: {}, + }; +} + // ----------------------------------------------------------------------- // Board file generation functions (simplified from script.js) // ----------------------------------------------------------------------- @@ -421,7 +468,7 @@ function generatePinctrlFile(boardName, mcu, peripherals, templates) { `; for (const p of peripherals) { - const template = templates[p.id]; + const template = templates[getTemplateKey(p.id)]; if (!template || template.noPinctrl) continue; const pinctrlName = template.pinctrlBaseName; @@ -503,7 +550,7 @@ function generateCommonDtsi(boardName, mcu, peripherals, templates) { if (p.config && p.config.loadCapacitors) continue; if (p.type === "GPIO") continue; - const template = templates[p.id]; + const template = templates[getTemplateKey(p.id)]; if (!template) continue; const nodeName = template.dtNodeName; @@ -526,6 +573,22 @@ function generateCommonDtsi(boardName, mcu, peripherals, templates) { content += `\tcurrent-speed = <115200>;\n`; } + if (template.type === "SPI") { + const csEntry = Object.entries(p.pinFunctions).find( + ([, sig]) => sig === "CS", + ); + if (csEntry) { + const csPinInfo = parsePinName(csEntry[0]); + if (csPinInfo) { + content += `\tcs-gpios = <&gpio${csPinInfo.port} ${csPinInfo.pin} GPIO_ACTIVE_LOW>;\n`; + } + } + } + + if (template.type === "I2C") { + content += `\tclock-frequency = ;\n`; + } + content += `};\n`; } @@ -550,7 +613,7 @@ function generateCpuappCommonDtsi(boardName, mcu, peripherals, templates) { // Add UART console if available let hasUart = false; for (const p of peripherals) { - const template = templates[p.id]; + const template = templates[getTemplateKey(p.id)]; if ( template && template.dtNodeName && @@ -581,7 +644,22 @@ function generateCpuappCommonDtsi(boardName, mcu, peripherals, templates) { \tload-capacitors = "internal"; \tload-capacitance-femtofarad = <15000>; }; +`; + + // Add LFXO node if present in peripherals + const lfxo = peripherals.find((p) => p.id === "LFXO"); + if (lfxo) { + const lfxoCap = lfxo.config.loadCapacitanceFemtofarad || 17000; + const lfxoCapType = lfxo.config.loadCapacitors || "internal"; + content += ` +&lfxo { +\tload-capacitors = "${lfxoCapType}"; +\tload-capacitance-femtofarad = <${lfxoCap}>; +}; +`; + } + content += ` ®ulators { \tstatus = "okay"; }; @@ -676,7 +754,7 @@ ${partitionInclude} `; } -function generateYaml(boardName, mcu, isNonSecure) { +function generateYaml(boardName, mcu, isNonSecure, peripherals) { const identifier = isNonSecure ? `${boardName}/${mcu}/cpuapp/ns` : `${boardName}/${mcu}/cpuapp`; @@ -686,6 +764,31 @@ function generateYaml(boardName, mcu, isNonSecure) { const ram = isNonSecure ? 256 : 188; const flash = isNonSecure ? 1524 : 1428; + // Dynamically build supported list from peripherals + const supported = new Set(["gpio", "watchdog"]); + if (peripherals) { + for (const p of peripherals) { + if (p.type === "UART" || (p.id && p.id.startsWith("UARTE"))) + supported.add("uart"); + if (p.type === "SPI" || (p.id && p.id.startsWith("SPIM"))) + supported.add("spi"); + if ( + p.type === "TWI" || + p.type === "I2C" || + (p.id && p.id.startsWith("TWIM")) + ) + supported.add("i2c"); + if (p.type === "PWM" || (p.id && p.id.startsWith("PWM"))) + supported.add("pwm"); + if (p.type === "SAADC" || p.type === "ADC") supported.add("adc"); + if (p.type === "NFCT") supported.add("nfct"); + } + } + const supportedList = [...supported] + .sort() + .map((s) => ` - ${s}`) + .join("\n"); + return `# Copyright (c) 2025 Generated by nRF54L Pin Planner # SPDX-License-Identifier: Apache-2.0 @@ -700,14 +803,12 @@ sysbuild: true ram: ${ram} flash: ${flash} supported: - - gpio - - uart - - watchdog +${supportedList} vendor: test `; } -function generateDefconfig(isNonSecure, mcu) { +function generateDefconfig(isNonSecure, mcu, hasLfxo) { if (isNonSecure) { return `# Copyright (c) 2025 Generated by nRF54L Pin Planner # SPDX-License-Identifier: Apache-2.0 @@ -788,7 +889,7 @@ CONFIG_EXTERNAL_CACHE=y # Start SYSCOUNTER on driver init CONFIG_NRF_GRTC_START_SYSCOUNTER=y `; - } else { + } else if (!hasLfxo) { // nrf54l05/10/15 use RC oscillator for low-frequency clock when no LFXO config += ` # Use RC oscillator for low-frequency clock @@ -1031,10 +1132,10 @@ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")); mkdirSync(OUTPUT_DIR, { recursive: true }); const SKIP_MCUS = ["nrf54lv10a"]; // No DTSI files in current Zephyr tree +let totalBoards = 0; for (const mcu of manifest.mcus) { const mcuId = mcu.id; - const boardName = `test_board_${mcuId}`; const supportsNS = mcu.supportsNonSecure === true; const supportsFLPR = mcu.supportsFLPR === true; @@ -1044,7 +1145,6 @@ for (const mcu of manifest.mcus) { } console.log(`\n--- MCU: ${mcuId} ---`); - console.log(` Board name: ${boardName}`); console.log(` Supports NS: ${supportsNS}, FLPR: ${supportsFLPR}`); // Use first package for this MCU @@ -1073,153 +1173,196 @@ for (const mcu of manifest.mcus) { const dtData = JSON.parse(readFileSync(dtPath, "utf-8")); const templates = dtData.templates; - // Build synthetic state: UARTE20 + HFXO - const peripherals = []; - const hfxo = buildHfxoState(); - peripherals.push(hfxo); + // Generate boards for each test configuration + for (const [configName, peripheralIds] of Object.entries(TEST_CONFIGS)) { + const boardName = `test_board_${mcuId}_${configName}`; + const usedPins = new Set(); + const peripherals = []; + let skipConfig = false; + const hasLfxo = peripheralIds.includes("LFXO"); - const uart = buildUartState(packageData, templates); - if (uart) { - peripherals.push(uart); - console.log(` UARTE20 pins: ${JSON.stringify(uart.pinFunctions)}`); - } else { - console.warn(` WARNING: Could not configure UARTE20 for ${mcuId}`); - } + console.log(`\n Config: ${configName} -> ${boardName}`); - // Add UARTE30 for FLPR console if MCU supports FLPR - if (supportsFLPR) { - const existingUsedPins = uart ? uart._usedPins : new Set(); - const uart30 = buildUart30State(packageData, templates, existingUsedPins); - if (uart30) { - peripherals.push(uart30); - console.log(` UARTE30 pins: ${JSON.stringify(uart30.pinFunctions)}`); - } else { - console.warn(` WARNING: Could not configure UARTE30 for ${mcuId}`); + for (const pId of peripheralIds) { + if (pId === "HFXO") { + peripherals.push(buildHfxoState()); + continue; + } + if (pId === "LFXO") { + peripherals.push(buildLfxoState()); + continue; + } + + const state = buildPeripheralState(pId, packageData, templates, usedPins); + if (!state) { + console.log( + ` Skipping config ${configName}: ${pId} unavailable for ${mcuId}`, + ); + skipConfig = true; + break; + } + peripherals.push(state); + // Accumulate used pins + for (const pin of state._usedPins) usedPins.add(pin); + + const pinCount = Object.keys(state.pinFunctions).length; + if (pinCount > 0) { + console.log(` ${pId} pins: ${JSON.stringify(state.pinFunctions)}`); + } else { + console.log(` ${pId}: enabled (no pinctrl)`); + } } - } - // Generate all board files - const files = {}; + if (skipConfig) continue; - files["board.yml"] = generateBoardYml( - boardName, - mcuId, - supportsNS, - supportsFLPR, - ); - files["board.cmake"] = generateBoardCmake( - boardName, - mcuId, - supportsNS, - supportsFLPR, - ); - files["Kconfig.defconfig"] = generateKconfigDefconfig( - boardName, - mcuId, - supportsNS, - ); - files[`Kconfig.${boardName}`] = generateKconfigBoard( - boardName, - mcuId, - supportsNS, - supportsFLPR, - ); - files[`${boardName}_common.dtsi`] = generateCommonDtsi( - boardName, - mcuId, - peripherals, - templates, - ); - files[`${mcuId}_cpuapp_common.dtsi`] = generateCpuappCommonDtsi( - boardName, - mcuId, - peripherals, - templates, - ); - files[`${boardName}_${mcuId}-pinctrl.dtsi`] = generatePinctrlFile( - boardName, - mcuId, - peripherals, - templates, - ); - files[`${boardName}_${mcuId}_cpuapp.dts`] = generateMainDts( - boardName, - mcuId, - supportsNS, - ); - files[`${boardName}_${mcuId}_cpuapp.yaml`] = generateYaml( - boardName, - mcuId, - false, - ); - files[`${boardName}_${mcuId}_cpuapp_defconfig`] = generateDefconfig( - false, - mcuId, - ); + // Add UARTE30 for FLPR console if MCU supports FLPR + if (supportsFLPR) { + const uart30 = buildPeripheralState( + "UARTE30", + packageData, + templates, + usedPins, + ); + if (uart30) { + peripherals.push(uart30); + for (const pin of uart30._usedPins) usedPins.add(pin); + console.log( + ` UARTE30 (FLPR) pins: ${JSON.stringify(uart30.pinFunctions)}`, + ); + } else { + console.warn(` WARNING: Could not configure UARTE30 for ${mcuId}`); + } + } - // NS-specific files - if (supportsNS) { - files["Kconfig"] = generateKconfigTrustZone(boardName, mcuId); - files[`${boardName}_${mcuId}_cpuapp_ns.dts`] = generateNSDts( + // Generate all board files + const files = {}; + + files["board.yml"] = generateBoardYml( boardName, mcuId, + supportsNS, + supportsFLPR, ); - files[`${boardName}_${mcuId}_cpuapp_ns.yaml`] = generateYaml( + files["board.cmake"] = generateBoardCmake( boardName, mcuId, - true, + supportsNS, + supportsFLPR, ); - files[`${boardName}_${mcuId}_cpuapp_ns_defconfig`] = generateDefconfig( - true, + files["Kconfig.defconfig"] = generateKconfigDefconfig( + boardName, mcuId, + supportsNS, ); - } - - // FLPR-specific files - if (supportsFLPR) { - files[`${boardName}_${mcuId}_cpuflpr.dts`] = generateFLPRDts( + files[`Kconfig.${boardName}`] = generateKconfigBoard( boardName, mcuId, + supportsNS, + supportsFLPR, ); - files[`${boardName}_${mcuId}_cpuflpr.yaml`] = generateFLPRYaml( + files[`${boardName}_common.dtsi`] = generateCommonDtsi( boardName, mcuId, - false, + peripherals, + templates, ); - files[`${boardName}_${mcuId}_cpuflpr_defconfig`] = generateFLPRDefconfig( - false, + files[`${mcuId}_cpuapp_common.dtsi`] = generateCpuappCommonDtsi( + boardName, mcuId, + peripherals, + templates, ); - files[`${boardName}_${mcuId}_cpuflpr_xip.dts`] = generateFLPRXIPDts( + files[`${boardName}_${mcuId}-pinctrl.dtsi`] = generatePinctrlFile( boardName, mcuId, + peripherals, + templates, ); - files[`${boardName}_${mcuId}_cpuflpr_xip.yaml`] = generateFLPRYaml( + files[`${boardName}_${mcuId}_cpuapp.dts`] = generateMainDts( boardName, mcuId, - true, + supportsNS, + ); + files[`${boardName}_${mcuId}_cpuapp.yaml`] = generateYaml( + boardName, + mcuId, + false, + peripherals, + ); + files[`${boardName}_${mcuId}_cpuapp_defconfig`] = generateDefconfig( + false, + mcuId, + hasLfxo, ); - files[`${boardName}_${mcuId}_cpuflpr_xip_defconfig`] = - generateFLPRDefconfig(true, mcuId); - } - // Write all files to output directory - const boardDir = resolve(OUTPUT_DIR, boardName); - mkdirSync(boardDir, { recursive: true }); + // NS-specific files + if (supportsNS) { + files["Kconfig"] = generateKconfigTrustZone(boardName, mcuId); + files[`${boardName}_${mcuId}_cpuapp_ns.dts`] = generateNSDts( + boardName, + mcuId, + ); + files[`${boardName}_${mcuId}_cpuapp_ns.yaml`] = generateYaml( + boardName, + mcuId, + true, + peripherals, + ); + files[`${boardName}_${mcuId}_cpuapp_ns_defconfig`] = generateDefconfig( + true, + mcuId, + hasLfxo, + ); + } - let fileCount = 0; - for (const [filename, content] of Object.entries(files)) { - const filePath = resolve(boardDir, filename); - writeFileSync(filePath, content, "utf-8"); - fileCount++; - } + // FLPR-specific files + if (supportsFLPR) { + files[`${boardName}_${mcuId}_cpuflpr.dts`] = generateFLPRDts( + boardName, + mcuId, + ); + files[`${boardName}_${mcuId}_cpuflpr.yaml`] = generateFLPRYaml( + boardName, + mcuId, + false, + ); + files[`${boardName}_${mcuId}_cpuflpr_defconfig`] = generateFLPRDefconfig( + false, + mcuId, + ); + files[`${boardName}_${mcuId}_cpuflpr_xip.dts`] = generateFLPRXIPDts( + boardName, + mcuId, + ); + files[`${boardName}_${mcuId}_cpuflpr_xip.yaml`] = generateFLPRYaml( + boardName, + mcuId, + true, + ); + files[`${boardName}_${mcuId}_cpuflpr_xip_defconfig`] = + generateFLPRDefconfig(true, mcuId); + } - console.log(` Generated ${fileCount} files in ${boardDir}`); + // Write all files to output directory + const boardDir = resolve(OUTPUT_DIR, boardName); + mkdirSync(boardDir, { recursive: true }); + + let fileCount = 0; + for (const [filename, content] of Object.entries(files)) { + const filePath = resolve(boardDir, filename); + writeFileSync(filePath, content, "utf-8"); + fileCount++; + } + + console.log(` Generated ${fileCount} files in ${boardDir}`); + totalBoards++; + } } // Summary console.log("\n=== Summary ==="); console.log(`MCUs processed: ${manifest.mcus.length}`); +console.log(`Board configs generated: ${totalBoards}`); if (exitCode !== 0) { console.log("\nGeneration completed with ERRORS."); diff --git a/mcus/nrf54l05/devicetree-templates.json b/mcus/nrf54l05/devicetree-templates.json index 49bfd75..c7337d8 100644 --- a/mcus/nrf54l05/devicetree-templates.json +++ b/mcus/nrf54l05/devicetree-templates.json @@ -127,10 +127,10 @@ "pinctrlBaseName": "pwm20", "type": "PWM", "signalMappings": { - "OUT0": "PWM_OUT0", - "OUT1": "PWM_OUT1", - "OUT2": "PWM_OUT2", - "OUT3": "PWM_OUT3" + "CHAN[0]": "PWM_OUT0", + "CHAN[1]": "PWM_OUT1", + "CHAN[2]": "PWM_OUT2", + "CHAN[3]": "PWM_OUT3" } }, "PWM21": { @@ -138,10 +138,10 @@ "pinctrlBaseName": "pwm21", "type": "PWM", "signalMappings": { - "OUT0": "PWM_OUT0", - "OUT1": "PWM_OUT1", - "OUT2": "PWM_OUT2", - "OUT3": "PWM_OUT3" + "CHAN[0]": "PWM_OUT0", + "CHAN[1]": "PWM_OUT1", + "CHAN[2]": "PWM_OUT2", + "CHAN[3]": "PWM_OUT3" } }, "PWM22": { @@ -149,10 +149,10 @@ "pinctrlBaseName": "pwm22", "type": "PWM", "signalMappings": { - "OUT0": "PWM_OUT0", - "OUT1": "PWM_OUT1", - "OUT2": "PWM_OUT2", - "OUT3": "PWM_OUT3" + "CHAN[0]": "PWM_OUT0", + "CHAN[1]": "PWM_OUT1", + "CHAN[2]": "PWM_OUT2", + "CHAN[3]": "PWM_OUT3" } }, "RADIO": { diff --git a/mcus/nrf54l10/devicetree-templates.json b/mcus/nrf54l10/devicetree-templates.json index 49bfd75..c7337d8 100644 --- a/mcus/nrf54l10/devicetree-templates.json +++ b/mcus/nrf54l10/devicetree-templates.json @@ -127,10 +127,10 @@ "pinctrlBaseName": "pwm20", "type": "PWM", "signalMappings": { - "OUT0": "PWM_OUT0", - "OUT1": "PWM_OUT1", - "OUT2": "PWM_OUT2", - "OUT3": "PWM_OUT3" + "CHAN[0]": "PWM_OUT0", + "CHAN[1]": "PWM_OUT1", + "CHAN[2]": "PWM_OUT2", + "CHAN[3]": "PWM_OUT3" } }, "PWM21": { @@ -138,10 +138,10 @@ "pinctrlBaseName": "pwm21", "type": "PWM", "signalMappings": { - "OUT0": "PWM_OUT0", - "OUT1": "PWM_OUT1", - "OUT2": "PWM_OUT2", - "OUT3": "PWM_OUT3" + "CHAN[0]": "PWM_OUT0", + "CHAN[1]": "PWM_OUT1", + "CHAN[2]": "PWM_OUT2", + "CHAN[3]": "PWM_OUT3" } }, "PWM22": { @@ -149,10 +149,10 @@ "pinctrlBaseName": "pwm22", "type": "PWM", "signalMappings": { - "OUT0": "PWM_OUT0", - "OUT1": "PWM_OUT1", - "OUT2": "PWM_OUT2", - "OUT3": "PWM_OUT3" + "CHAN[0]": "PWM_OUT0", + "CHAN[1]": "PWM_OUT1", + "CHAN[2]": "PWM_OUT2", + "CHAN[3]": "PWM_OUT3" } }, "RADIO": { diff --git a/mcus/nrf54lm20a/devicetree-templates.json b/mcus/nrf54lm20a/devicetree-templates.json index 49bfd75..c7337d8 100644 --- a/mcus/nrf54lm20a/devicetree-templates.json +++ b/mcus/nrf54lm20a/devicetree-templates.json @@ -127,10 +127,10 @@ "pinctrlBaseName": "pwm20", "type": "PWM", "signalMappings": { - "OUT0": "PWM_OUT0", - "OUT1": "PWM_OUT1", - "OUT2": "PWM_OUT2", - "OUT3": "PWM_OUT3" + "CHAN[0]": "PWM_OUT0", + "CHAN[1]": "PWM_OUT1", + "CHAN[2]": "PWM_OUT2", + "CHAN[3]": "PWM_OUT3" } }, "PWM21": { @@ -138,10 +138,10 @@ "pinctrlBaseName": "pwm21", "type": "PWM", "signalMappings": { - "OUT0": "PWM_OUT0", - "OUT1": "PWM_OUT1", - "OUT2": "PWM_OUT2", - "OUT3": "PWM_OUT3" + "CHAN[0]": "PWM_OUT0", + "CHAN[1]": "PWM_OUT1", + "CHAN[2]": "PWM_OUT2", + "CHAN[3]": "PWM_OUT3" } }, "PWM22": { @@ -149,10 +149,10 @@ "pinctrlBaseName": "pwm22", "type": "PWM", "signalMappings": { - "OUT0": "PWM_OUT0", - "OUT1": "PWM_OUT1", - "OUT2": "PWM_OUT2", - "OUT3": "PWM_OUT3" + "CHAN[0]": "PWM_OUT0", + "CHAN[1]": "PWM_OUT1", + "CHAN[2]": "PWM_OUT2", + "CHAN[3]": "PWM_OUT3" } }, "RADIO": { From a6d1e1205ef0caa4e8b193b53928bed43ab4ef2d Mon Sep 17 00:00:00 2001 From: Helmut Lord Date: Fri, 6 Feb 2026 14:20:09 -0500 Subject: [PATCH 6/8] Add console UART selection comment to generated board files The generated cpuapp common DTSI now includes a comment explaining that in Pin Planner, users can choose which UART serves as the serial console, or omit it entirely to use Segger RTT instead. Co-Authored-By: Claude Opus 4.6 --- ci/generate-test-boards.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ci/generate-test-boards.js b/ci/generate-test-boards.js index 90589e2..17e6089 100644 --- a/ci/generate-test-boards.js +++ b/ci/generate-test-boards.js @@ -610,7 +610,10 @@ function generateCpuappCommonDtsi(boardName, mcu, peripherals, templates) { \tchosen { `; - // Add UART console if available + // Add UART console if available. + // Note: In the Pin Planner UI, the user can choose which UART is used + // for the serial console/shell, or omit it entirely to use RTT instead. + // For CI test boards, the first available UART is always assigned. let hasUart = false; for (const p of peripherals) { const template = templates[getTemplateKey(p.id)]; @@ -620,6 +623,11 @@ function generateCpuappCommonDtsi(boardName, mcu, peripherals, templates) { template.type === "UART" && !hasUart ) { + content += `\t\t/*\n`; + content += `\t\t * Console UART - in Pin Planner you can select which\n`; + content += `\t\t * UART (if any) serves as the serial console. If no\n`; + content += `\t\t * UART is chosen, Segger RTT is used instead.\n`; + content += `\t\t */\n`; content += `\t\tzephyr,console = &${template.dtNodeName};\n`; content += `\t\tzephyr,shell-uart = &${template.dtNodeName};\n`; content += `\t\tzephyr,uart-mcumgr = &${template.dtNodeName};\n`; From 5e30e8c76a5758703060fd1912443f157e01964d Mon Sep 17 00:00:00 2001 From: Helmut Lord Date: Fri, 6 Feb 2026 14:25:48 -0500 Subject: [PATCH 7/8] Add description text to Serial Console UI section Explains that users can select which UART is used for the Zephyr shell and serial console, or use Segger RTT if no UART is selected. Co-Authored-By: Claude Opus 4.6 --- index.html | 5 +++++ style.css | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index c106ec4..587e39e 100644 --- a/index.html +++ b/index.html @@ -192,6 +192,11 @@

    Selected

    style="display: none" >

    Serial Console

    +

    + Select which UART is used for the Zephyr shell and serial + console. If no UART is selected, Segger RTT will be used + instead. +

    Date: Fri, 6 Feb 2026 14:32:36 -0500 Subject: [PATCH 8/8] Add console UART opt-out: dropdown with None (Segger RTT) option Previously, selecting a single UART would auto-assign it as the serial console with no way to opt out. Now the console selector always shows a dropdown with a "None (Segger RTT)" option when any UART is selected, letting users choose RTT instead. Co-Authored-By: Claude Opus 4.6 --- index.html | 12 ++---------- js/console-config.js | 34 ++++++++++++++++++---------------- style.css | 8 +------- 3 files changed, 21 insertions(+), 33 deletions(-) diff --git a/index.html b/index.html index 587e39e..d431b0d 100644 --- a/index.html +++ b/index.html @@ -192,16 +192,8 @@

    Selected

    style="display: none" >

    Serial Console

    -

    - Select which UART is used for the Zephyr shell and serial - console. If no UART is selected, Segger RTT will be used - instead. -

    -
    - No UART selected. RTT will be used for logging. +
    + No UART selected — Segger RTT will be used.