diff --git a/.gitignore b/.gitignore index 3938248..65755ee 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,5 @@ mcusdk_tagfile __pycache__/ !*boardcfg_*.bin source/sysconfig/ -source/board/pmic/pmic_lld/ \ No newline at end of file +source/board/pmic/pmic_lld/ +build_out/ diff --git a/.metadata/sysconfig/.meta/pru_blocks/application_specific/crc_block.syscfg.js b/.metadata/sysconfig/.meta/pru_blocks/application_specific/crc_block.syscfg.js index de0dd4b..5719f00 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/application_specific/crc_block.syscfg.js +++ b/.metadata/sysconfig/.meta/pru_blocks/application_specific/crc_block.syscfg.js @@ -61,6 +61,13 @@ function getLookUptable(instance) } function validate(inst, report) { + // $name is emitted as the CRC lookup-table symbol (" .byte/.ushort/.uint ..." in .initdmem); reject clpru reserved identifiers via the shared helper. + if (typeof system !== "undefined" && system.getScript) { + var reservedNames = system.getScript("/pru_blocks/common/pru_reserved_names.js"); + if (reservedNames && typeof reservedNames.validateInstanceName === "function") { + reservedNames.validateInstanceName(inst, report, "This block emits the instance name as the CRC lookup-table symbol in .initdmem."); + } + } if(inst["input1"].length == 0) { report.logWarning("crc is not intialized",inst) @@ -153,17 +160,33 @@ function getMacro(pruInstructionMacro, opCode) } if (macroName && macroName.trim().startsWith("m_calculate_crc16")) { + // LUT entries are 2 bytes: the byte offset is index*2, which does not + // fit in a .b0 operand, so the scaled offset is added into the base + // and the LBBO offset register is zeroed (LDI TEMP_REG2, 0) first. The + // LDI is required on every model: without it the .b0 offset left in + // TEMP_REG2 is added a second time on top of the scaled base. macroBody += ` LDI32 TEMP_REG1, ${lutAddress} LSR TEMP_REG2.b0, ${crcInit}, 8 XOR TEMP_REG2.b0, TEMP_REG2.b0, ${dataByte} + AND TEMP_REG2, TEMP_REG2, 0xFF + LSL TEMP_REG2, TEMP_REG2, 1 + ADD TEMP_REG1, TEMP_REG1, TEMP_REG2 + LDI TEMP_REG2, 0 LBBO &${crcResult}, TEMP_REG1, TEMP_REG2.b0, 2 LSL TEMP_REG2, ${crcInit}, 8 XOR ${crcResult}, TEMP_REG2.w0, ${crcResult}`; } if (macroName && macroName.trim().startsWith("m_calculate_crc32")) { + // LUT entries are 4 bytes: the byte offset is index*4 (up to 1020), + // which does not fit in a .b0 operand, so the scaled offset is added + // into the base and the LBBO offset register is zeroed first. macroBody += ` LDI32 TEMP_REG1, ${lutAddress} LSR TEMP_REG2.b0, ${crcInit}, 24 XOR TEMP_REG2.b0, TEMP_REG2.b0, ${dataByte} + AND TEMP_REG2, TEMP_REG2, 0xFF + LSL TEMP_REG2, TEMP_REG2, 2 + ADD TEMP_REG1, TEMP_REG1, TEMP_REG2 + LDI TEMP_REG2, 0 LBBO &${crcResult}, TEMP_REG1, TEMP_REG2.b0, 4 LSL TEMP_REG2, ${crcInit}, 8 XOR ${crcResult}, TEMP_REG2, ${crcResult}`; @@ -302,22 +325,30 @@ Calculates CRC checksums for error detection in data transmission and storage. C - LDI32 TEMP_REG1, CRC_LUT_address ; Load LUT address - LSR TEMP_REG2.b0, init, 8 ; Extract high byte - XOR TEMP_REG2.b0, TEMP_REG2.b0, dataByte ; XOR with data +- AND TEMP_REG2, TEMP_REG2, 0xFF ; Zero-extend the table index +- LSL TEMP_REG2, TEMP_REG2, 1 ; Scale: entries are 2 bytes (index*2) +- ADD TEMP_REG1, TEMP_REG1, TEMP_REG2 ; Base += scaled byte offset +- LDI TEMP_REG2, 0 ; Zero the LBBO offset register - LBBO &result, TEMP_REG1, TEMP_REG2.b0, 2 ; Lookup - LSL TEMP_REG2, init, 8 ; Shift init left -- XOR result, TEMP_REG2.w0, result ; XOR with lookup (9 cycles) +- XOR result, TEMP_REG2.w0, result ; XOR with lookup (13 cycles) **Generated Assembly** (CRC32 example): - LDI32 TEMP_REG1, CRC_LUT_address ; Load LUT address - LSR TEMP_REG2.b0, init, 24 ; Extract high byte - XOR TEMP_REG2.b0, TEMP_REG2.b0, dataByte ; XOR with data +- AND TEMP_REG2, TEMP_REG2, 0xFF ; Zero-extend the table index +- LSL TEMP_REG2, TEMP_REG2, 2 ; Scale: entries are 4 bytes (index*4) +- ADD TEMP_REG1, TEMP_REG1, TEMP_REG2 ; Base += scaled byte offset +- LDI TEMP_REG2, 0 ; Zero the LBBO offset register - LBBO &result, TEMP_REG1, TEMP_REG2.b0, 4 ; Lookup - LSL TEMP_REG2, init, 8 ; Shift init left -- XOR result, TEMP_REG2, result ; XOR with lookup (9 cycles) +- XOR result, TEMP_REG2, result ; XOR with lookup (13 cycles) **Performance**: - CRC8: 6 PRU cycles per byte -- CRC16: 9 PRU cycles per byte -- CRC32: 9 PRU cycles per byte +- CRC16: 13 PRU cycles per byte +- CRC32: 13 PRU cycles per byte **Lookup Table**: Automatically generated 256-entry table based on polynomial - CRC8: 256 bytes (256 × 1 byte) @@ -589,14 +620,14 @@ exports = { { return 6 } - if(inst["opCode"] == "m_calculate_crc16") - { - return 9 - } - if(inst["opCode"] == "m_calculate_crc32") - { - return 9 - } + if(inst["opCode"] == "m_calculate_crc16") + { + return 13 + } + if(inst["opCode"] == "m_calculate_crc32") + { + return 13 + } } } ], @@ -627,4 +658,4 @@ exports = { getMacro, getDataSize, getLookUptable -} \ No newline at end of file +} diff --git a/.metadata/sysconfig/.meta/pru_blocks/common/block_control_flow_data.js b/.metadata/sysconfig/.meta/pru_blocks/common/block_control_flow_data.js new file mode 100644 index 0000000..daaef9c --- /dev/null +++ b/.metadata/sysconfig/.meta/pru_blocks/common/block_control_flow_data.js @@ -0,0 +1,33 @@ +// Control-flow class ("sequential" or "continuous") of each block, used by the GUI pin-sharing check in +// pru_blocks_static_module: instances of one sequential module may share a two-way pad, and a GPO driven by +// two instances is flagged when either block is continuous. A block that is not listed is treated as +// continuous (the check fails closed). blockControlFlowBound names a config that makes a continuous block +// sequential when it is > 0. +exports = { + blockControlFlow: { + "access_look_up_table": "sequential", + "arithmetic_block": "sequential", + "bitwise_block": "sequential", + "conditional_block": "continuous", + "crc_block": "sequential", + "delay_block": "continuous", + "flow_control_block": "continuous", + "group_block": "continuous", + "label": "continuous", + "load_constant_block": "sequential", + "look_up_table": "continuous", + "loop_block": "continuous", + "memory_access_block": "continuous", + "memory_variable_block": "continuous", + "pru_gpi_block": "sequential", + "pru_gpo_block": "sequential", + "pru_spi_read": "sequential", + "pru_spi_transfer": "sequential", + "pru_spi_write": "sequential", + }, + blockControlFlowBound: { + } +}; +if (typeof module !== 'undefined') { + module.exports = exports; +} diff --git a/.metadata/sysconfig/.meta/pru_blocks/common/pru_blocks_static_module.syscfg.js b/.metadata/sysconfig/.meta/pru_blocks/common/pru_blocks_static_module.syscfg.js index 21a84f5..5b4a321 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/common/pru_blocks_static_module.syscfg.js +++ b/.metadata/sysconfig/.meta/pru_blocks/common/pru_blocks_static_module.syscfg.js @@ -2,6 +2,13 @@ const pruRegisterAllocator = system.getScript("/pru_blocks/common/register_alloc const pruDMEM0 = system.getScript("/pru_blocks/common/simulation/pru_dmem0.js"); const pruSMEM = system.getScript("/pru_blocks/common/simulation/pru_smem.js"); const pruSimulator = system.getScript("/pru_blocks/common/simulation/pru_core.js"); +// single-source device capability table (TRM SPRUIM2J §6.4.1.1 / SPRUJB6 §7.3.1.1) +let pruDeviceCaps = null; +try { + pruDeviceCaps = system.getScript("/pru_blocks/common/pru_device_caps.js"); +} catch (e) { + try { pruDeviceCaps = system.getScript("/pru_blocks/common/pru_device_caps"); } catch (e2) { /* capsule unavailable in test stub */ } +} const simulationData = {cycleCount : [], r30Bits : [], r31Bits : []}; // ============================================================================ @@ -553,9 +560,19 @@ function expandMacros(pruInstructionsWithMacros, pruInstructionsLabelWithMacros) macroCount++; } else { - // No macro definition, use instruction as is - pruInstructions.push(pruInstructionsWithMacros[i]); - pruInstructionsLabels.push(pruInstructionsLabelWithMacros[i]); + // No macro definition, use instruction as is (expanding multi-line instructions if present) + if (typeof pruInstructionsWithMacros[i] === "string" && pruInstructionsWithMacros[i].includes("\n")) { + let lines = pruInstructionsWithMacros[i].split("\n").map(l => l.trim()).filter(l => l && !l.startsWith(";")); + pruInstructions.push(0); + pruInstructionsLabels.push(pruInstructionsLabelWithMacros[i]); + for (let line of lines) { + pruInstructions.push(line); + pruInstructionsLabels.push(0); + } + } else { + pruInstructions.push(pruInstructionsWithMacros[i]); + pruInstructionsLabels.push(pruInstructionsLabelWithMacros[i]); + } } } @@ -574,10 +591,12 @@ function replaceTemporaryRegisters(instructions) { return instruction; } - // Replace TEMP_REG1 with R28 and TEMP_REG2 with R29 + // Replace TEMP_REG1 with R28, TEMP_REG2 with R29, TEMP_REG3 with R26, TEMP_REG4 with R27 return instruction .replace(/TEMP_REG1(\.\w+)?/g, 'R28$1') - .replace(/TEMP_REG2(\.\w+)?/g, 'R29$1'); + .replace(/TEMP_REG2(\.\w+)?/g, 'R29$1') + .replace(/TEMP_REG3(\.\w+)?/g, 'R26$1') + .replace(/TEMP_REG4(\.\w+)?/g, 'R27$1'); }); } @@ -681,7 +700,7 @@ function getDataSectionContents() */ function getUsectLinkerDirectives() { - let usectDirectives = []; + let usectDirectives = new Set(); let systemModules = system.modules; // Look for Memory Reserve module @@ -691,20 +710,20 @@ function getUsectLinkerDirectives() if (instance["labelName"] && instance["labelName"] !== "") { // Call the module's getUsectDirective function if available if (typeof memReserveModule.getUsectDirective === 'function') { - usectDirectives.push(memReserveModule.getUsectDirective(instance)); + usectDirectives.add(memReserveModule.getUsectDirective(instance)); } else { // Fallback: construct directive directly (always 4-byte aligned) const labelName = instance.labelName; const sizeInBytes = instance.sizeInBytes; const sectionName = instance.sectionName ; - usectDirectives.push(`${labelName}\t.usect "${sectionName}", ${sizeInBytes}, 1`); + usectDirectives.add(`${labelName}\t.usect "${sectionName}", ${sizeInBytes}, 1`); } } } } - return usectDirectives; + return Array.from(usectDirectives); } /** @@ -743,6 +762,126 @@ function getInitSectDirectives() { return assembly; } +// ============================================================================ +// constant-table pointer c28 (PRU shared RAM) +// ============================================================================ +// c28 is only partially fixed: TRM SPRUIM2J Table 6-65 (PRU_ICSSG) +// gives c28 = 00nn_nn00h with nnnn = CTPPR0.C28_POINTER[15:0]. CTPPR0 sits at +// offset 28h of the core's own PRU_CTRL block and resets to 0 +// (SPRUIM2J 14.3.6.1.1), so until firmware writes it every LBCO/SBCO through +// c28 hits local 0x00000000 -- the core's own data RAM -- not shared RAM. +// The design-wide fix lives here, once, rather than in each block: one write +// at every entry point into the generated code serves all c28 users and costs +// the per-call blocks that use c28 nothing. + +// PRU_CTRL base of each SysConfig core context (core-local addresses). +// SPRUIM2J Table 6-65 entry 11 and the CTPPR0 instance table; SPRUJB6 confirms only the c28 pointer formula and the local control bases; +// the AM261x CTPPR0 offset 0x28 / reset 0 is INFERRED from the shared PRU-ICSS IP, not +// documented in SPRUJB6. +const PRU_CTRL_BASE_BY_CORE = [ + [/tx_pru(\d)$/, n => 0x25000 + 0x800 * n], + [/rtu_pru(\d)$/, n => 0x23000 + 0x800 * n], + [/pru(\d)$/, n => 0x22000 + 0x2000 * n], +]; +const PRU_CTRL_CTPPR0 = 0x28; +const SHARED_RAM_LOCAL = 0x00010000; // PRU-local view of shared RAM + +function pruCtrlBase(coreName) { + for (const [re, base] of PRU_CTRL_BASE_BY_CORE) { + const m = (coreName || "").match(re); + if (m) return base(parseInt(m[1])); + } + return null; +} + +/** True when any instantiated block's macro addresses memory through c28. */ +function designUsesC28(modules) { + for (const moduleName in modules) { + const module = modules[moduleName]; + if (!module || typeof module.getMacro !== "function" || !module.$instances) continue; + for (const inst of module.$instances) { + if (!inst || !inst.opCode) continue; + if (/\bc28\b/i.test(module.getMacro("", inst.opCode))) return true; + } + } + return false; +} + +/** + * Assembly that programs CTPPR0 so c28 = shared RAM (0x00010000). Empty when + * no block in the design uses c28. Clobbers TEMP_REG1/TEMP_REG2 only. + * C29_POINTER is written 0, its reset value; nothing in the library uses c29. + */ +function getCtppr0Setup(coreName, modules) { + if (!designUsesC28(modules || system.modules)) return ""; + if (coreName === undefined) coreName = system.getScript("/common").getSelfSysCfgCoreName(); + const base = pruCtrlBase(coreName); + if (base === null) { + throw new Error(`No PRU_CTRL base known for core context '${coreName}'; ` + + "cannot program CTPPR0 for c28"); + } + const ptr = SHARED_RAM_LOCAL >> 8; + const hex = v => "0x" + v.toString(16).toUpperCase(); + return [ + ` ; c28 = CTPPR0.C28_POINTER << 8 and CTPPR0 resets to 0 (TRM SPRUIM2J`, + ` ; Table 6-65, 14.3.6.1.1). Point c28 at shared RAM ${hex(SHARED_RAM_LOCAL)} for ${coreName}.`, + ` ldi TEMP_REG1, ${hex(ptr)}`, + ` ldi32 TEMP_REG2, ${hex(base + PRU_CTRL_CTPPR0)}`, + ` sbbo &TEMP_REG1, TEMP_REG2, 0, 4`, + ].join("\n"); +} + +// the pru_spi_* blocks' controller mode asserts CS with `clr r30, r30, ` and releases it with `set`, but R30 resets to 0, so +// with nothing driving the pin high first the first transaction has no assertion edge. The prologue drives each distinct controller CS +// pin to its idle (inactive, high) level once; a peripheral block samples CS on R31 and must not drive it. +const SPI_MODULE_PATHS = [ + "/pru_blocks/pru_io_blocks/pru_spi_read", + "/pru_blocks/pru_io_blocks/pru_spi_write", + "/pru_blocks/pru_io_blocks/pru_spi_transfer", +]; + +/** Sorted distinct CS pin numbers (R30 bits) of every controller-mode pru_spi_* instance. */ +function spiControllerCsPins(modules) { + const pins = new Set(); + for (const path of SPI_MODULE_PATHS) { + const module = (modules || system.modules)[path]; + if (!module || !module.$instances) continue; + for (const inst of module.$instances) { + if (inst && inst["Device Mode"] !== "peripheral") pins.add(Number(inst["CS Signal"])); + } + } + return Array.from(pins).filter(n => Number.isInteger(n) && n >= 0).sort((a, b) => a - b); +} + +function getSpiCsIdleSetup(modules) { + const pins = spiControllerCsPins(modules); + if (pins.length === 0) return ""; + return [` ; drive each SPI controller CS pin to its idle (high) level once; R30 resets to 0, which would hide the first assertion edge.`] + .concat(pins.map(n => ` set r30, r30, ${n}`)).join("\n"); +} + +/** + * Everything emitted by pru_syscfg.asm.xdt right after sysconfig_generated_start and every group start label: the CTPPR0 + * write (only when a block uses c28) and the CS idle level of the SPI controller blocks. Empty when neither applies. + */ +function getConstantTableSetup(coreName, modules) { + return [getCtppr0Setup(coreName, modules), getSpiCsIdleSetup(modules)].filter(Boolean).join("\n"); +} + +// Instructions the CTPPR0 write costs at each entry (ldi + ldi32 (2) + sbbo). Counted +// as 4 cycles; a store to the PRU_CTRL peripheral may stall longer on silicon. +const CTPPR0_SETUP_CYCLES = 4; + +/** Cycles of the SPI CS idle part of the prologue: one `set r30` per distinct controller CS pin. */ +function getSpiCsIdleSetupCycles(modules) { + return spiControllerCsPins(modules).length; +} + +/** Cycles getConstantTableSetup() adds to every entry into generated code: CTPPR0 write (c28 users) + one `set r30` per SPI CS pin. */ +function getConstantTableSetupCycles(modules) { + return (designUsesC28(modules || system.modules) ? CTPPR0_SETUP_CYCLES : 0) + getSpiCsIdleSetupCycles(modules); +} + function prepareR30DataForPlotting(r30ValueHistory) { // Clear existing data simulationData.cycleCount = []; @@ -819,6 +958,587 @@ function collectR31History() { return {}; } +/** + * register-pool exhaustion is an error naming the block and the budget; the instruction would otherwise be missing from the asm. + * Runs BEFORE the generic "-1" gate in the validate pass so loop-counter and group-return failures (which leave "-1" in the + * instructions) are named too. Returns the number of recorded failures reported. + */ +function reportRegisterExhaustion(inst, report) { + for (const f of pruRegisterAllocator.getAllocationFailures()) { + let failedInst = null; + for (let moduleName in system.modules) { + const mod = system.modules[moduleName]; + if (mod && mod.$instances) failedInst = mod.$instances.find(i => i.$name === f.block) || failedInst; + } + const message = f.contractFailure + ? "Accumulator binding error: block '" + f.block + "' cannot honor " + f.what + "." + : "Out of registers: block '" + f.block + "' needs " + f.bytes + " byte(s) for " + f.what + + " but only " + f.freeBytes + " of the " + f.budgetBytes + " allocatable bytes (R0-R" + (f.budgetRegisters - 1) + + ", " + f.budgetRegisters + " registers; R28/R29 are reserved) are free, so its instruction cannot be generated. " + + (f.scratchBytes > 0 ? f.scratchBytes + " byte(s) are held for the macro scratch registers " + f.scratchRegisters.join("/") + " (R26/R27) that blocks in the design use. " : "") + + (f.pinnedBytes > 0 ? f.pinnedBytes + " byte(s) of the pool are pinned by registers typed into block fields (" + + f.pinnedBy.join(", ") + ") and are not available to the allocator. " : "") + + "Reduce the number of values alive at once, or store intermediate results to memory."; + report.logError(message, failedInst || inst); + } + return pruRegisterAllocator.getAllocationFailures().length; +} + +/** + * registers typed into block fields. Reports (a) a typed value that is not a usable register window (ERROR), + * (b) two fields that name overlapping registers - an ERROR when the user typed at least one of them, a WARNING when both + * still hold their default value (a saved design with two blocks left on defaults used to generate; it must not suddenly fail), + * (c) a hand-off reader that runs before its writer outside any common loop (WARNING). Returns the number of ERRORS. + */ +function reportPinnedRegisterConflicts(inst, report) { + const regName = (p) => p.token + " (" + p.bytes + " byte" + (p.bytes === 1 ? "" : "s") + ")"; + const instanceNamed = (name) => { + let target = null; + for (let moduleName in system.modules) { + const mod = system.modules[moduleName]; + if (mod && mod.$instances) target = mod.$instances.find(i => i.$name === name) || target; + } + return target || inst; + }; + let errors = 0; + for (const p of pruRegisterAllocator.getPinnedRegisterProblems()) { + report.logError("Invalid register: " + p.block + "." + p.field + " = '" + p.token + "' " + p.why + ".", instanceNamed(p.block)); + errors++; + } + for (const c of pruRegisterAllocator.getPinnedRegisterConflicts()) { + const text = "Register conflict: " + c.second.block + "." + c.second.field + " = " + regName(c.second) + + " overlaps " + c.first.block + "." + c.first.field + " = " + regName(c.first) + + ". Both blocks use these registers directly, so one result would overwrite the other. Give each block its own registers"; + if (c.harmful) { + report.logError(text + ": " + c.first.block + " runs first and " + c.second.block + " overwrites " + regName(c.first) + + " before any block reads it, so " + c.first.block + "'s value is lost (" + (c.first.atDefault && c.second.atDefault ? "both fields are still at their default value; " : "") + + "type a different register into " + c.second.block + "." + c.second.field + " or " + c.first.block + "." + c.first.field + ").", instanceNamed(c.second.block)); + errors++; + } else if (c.first.atDefault && c.second.atDefault) { + const free = pruRegisterAllocator.suggestFreeRegister(c.second.bytes); + report.logWarning(text + " (both fields are still at their default value: type a different register into " + c.second.block + "." + + c.second.field + " or " + c.first.block + "." + c.first.field + (free ? " - " + free + " is not used by any block" : "") + + " - to remove this warning).", instanceNamed(c.second.block)); + } else { + report.logError(text + ".", instanceNamed(c.second.block)); + errors++; + } + } + for (const o of pruRegisterAllocator.getPinnedHandOffOrderWarnings()) { + report.logWarning("Register hand-off order: " + o.reader.block + "." + o.reader.field + " = " + o.reader.token + " uses the register that " + + o.writer.block + "." + o.writer.field + " = " + o.writer.token + " fills, but " + o.reader.block + " runs BEFORE " + o.writer.block + + " and no loop contains both, so it reads the register before the value arrives. Connect " + o.writer.block + " first, or put both in one loop (inside one loop the reader sees the previous iteration's value: the first iteration reads the uninitialised register and the last value is never read).", + instanceNamed(o.reader.block)); + } + return errors; +} + +/** + * Validate PRU instructions for undefined symbols, arity mismatches, and unconnected ports. + * Runs right after register allocation. + */ +function validatePruInstructions(inst, report) { + let definedSymbols = new Set(); + let usects = getUsectLinkerDirectives(); + usects.forEach(u => { + let parts = u.split('\t'); + if (parts.length > 0) definedSymbols.add(parts[0].trim()); + }); + + let dataSects = getDataSectionContents(); + dataSects.forEach(d => { + if (d.symbolName) definedSymbols.add(d.symbolName.trim()); + }); + + let pruInstructionsWithMacros = pruRegisterAllocator.getPruRegisterAllocationSummary().pruInstructions; + let pruInstructionModules = pruRegisterAllocator.getPruRegisterAllocationSummary().pruInstructionModules; + let pruInstructionInstances = pruRegisterAllocator.getPruRegisterAllocationSummary().pruInstructionInstances; + + // 1. Scan all blocks for graph connection issues and symbol usages + for (let moduleName in system.modules) { + let mod = system.modules[moduleName]; + if (!mod || !mod.$instances) continue; + for (let b_inst of mod.$instances) { + // A. Undefined symbol checks + if (b_inst.symbolName && !definedSymbols.has(b_inst.symbolName)) { + report.logError("Undefined memory symbol referenced: " + b_inst.symbolName, b_inst, "symbolName"); + } + if (b_inst.variableName && !definedSymbols.has(b_inst.variableName)) { + report.logError("Undefined shared-memory variable referenced: " + b_inst.variableName, b_inst, "variableName"); + } + + // B. Required ports unconnected + if (b_inst.numOfInputPorts > 0) { + for (let i = 1; i <= b_inst.numOfInputPorts; i++) { + let portName = "input" + i; + if (!b_inst[portName] || b_inst[portName].length === 0 || !b_inst[portName][0].inst) { + report.logError("Required graph input port '" + portName + "' is unconnected", b_inst); + } + } + } + + // C. Invalid graph link targets + if (b_inst.numOfInputPorts > 0) { + for (let i = 1; i <= b_inst.numOfInputPorts; i++) { + let portName = "input" + i; + if (b_inst[portName] && b_inst[portName].length > 0 && b_inst[portName][0].inst) { + let sourceInst = b_inst[portName][0].inst; + if (sourceInst.numOfOutputPorts === 0 || sourceInst.numOfOutputPorts === undefined) { + report.logError("Invalid connection: source block '" + sourceInst.$name + "' does not produce an output for '" + portName + "'", b_inst); + } + } + } + } + } + } + + // 1b. A consumer wired to an output port that owns no register is refused; the allocator never reroutes it to output1 + for (const c of pruRegisterAllocator.getUnroutedConnections()) { + let consumerInst = null; + for (let moduleName in system.modules) { + const mod = system.modules[moduleName]; + if (mod && mod.$instances) consumerInst = mod.$instances.find(i => i.$name === c.consumer) || consumerInst; + } + report.logError("Invalid connection: block '" + c.source + "' port '" + c.sourcePort + "' has no register of its own, so '" + + c.consumer + "." + c.inputPort + "' cannot read it (the block's macro writes only output1 here)", consumerInst || inst); + } + + // 1c. Loop contents consumed outside loop/group + for (const v of pruRegisterAllocator.getLoopBoundaryViolations()) { + let consumerInst = null; + let loopInst = null; + for (let moduleName in system.modules) { + const mod = system.modules[moduleName]; + if (mod && mod.$instances) { + if (v.consumer !== "generator") { + consumerInst = consumerInst || mod.$instances.find(i => i.$name === v.consumer); + } + loopInst = loopInst || mod.$instances.find(i => i.$name === v.container); + } + } + if (v.consumer === "generator") { + report.logError("Invalid container contents: group '" + v.producer + "' cannot be placed inside container '" + + v.container + "'. Group blocks cannot be nested inside loops or other groups.", loopInst || inst); + } else { + // a loop may explicitly export a block's final-iteration output (loop_block.exportedBlocks). + // A consumer outside such a loop reading an exported block is the supported post-loop store, not a + // boundary violation. Anything not exported is still rejected below. + const isDownstream = (start, target) => { + const seen = new Set(); + const walk = (node) => { + if (!node || seen.has(node)) return false; + seen.add(node); + if (node.$name === target) return true; + return [].concat(node.next || [], node.T || [], node.F || []).some(c => walk(c && c.inst)); + }; + return walk(start); + }; + if (loopInst && Array.isArray(loopInst.exportedBlocks) && loopInst.exportedBlocks.includes(v.producer) && + isDownstream(loopInst, v.consumer)) { + continue; + } + if (loopInst && Array.isArray(loopInst.exportedBlocks) && loopInst.exportedBlocks.includes(v.producer)) { + report.logError("Invalid exported result: consumer '" + v.consumer + "' reads exported block '" + + v.producer + "' before or outside the downstream control flow of loop '" + v.container + + "'. Exported blocks may only be consumed downstream of their loop.", consumerInst || inst); + continue; + } + report.logError("Invalid connection: consumer '" + v.consumer + "' is outside container '" + v.container + + "' but attempts to read '" + v.producer + "' which is inside it (via port '" + v.port + "'). Container contents cannot be consumed from the outside.", consumerInst || inst); + if (loopInst) { + report.logError("Invalid container: contains '" + v.producer + "' which is consumed from the outside by '" + + v.consumer + "'. Container contents cannot be consumed from the outside.", loopInst); + } + } + } + + // 2. Macro arity mismatch + for (let i = 0; i < pruInstructionsWithMacros.length; i++) { + let call = pruInstructionsWithMacros[i]; + let moduleName = pruInstructionModules[i]; + let b_inst = pruInstructionInstances[i]; + + if (!call || call === 0 || call === "0" || !moduleName || !b_inst) continue; + let mod = system.modules[moduleName]; + + if (mod && typeof mod.getMacro === 'function') { + let callArgsStr = call.substring(call.indexOf(' ') + 1).trim(); + let callArgs = callArgsStr ? callArgsStr.split(',').map(s=>s.trim()).filter(s=>s!=="") : []; + let callArity = callArgs.length; + + if (callArgs.some(arg => arg === "undefined")) { + report.logError("Macro call contains 'undefined' argument. Call: " + call, b_inst); + } + + + // The arity check needs the macro DEFINITION, not the inline expansion. + // Blocks switch on an empty first argument to emit the `.macro` line + // (see uart_tx getMacroSingleShot, uart_rx getMacro), so calling with + // the call string returns a body that has no signature in it at all. + let opCode = call.trim().split(/\s+/)[0]; + let defBody = mod.getMacro("", opCode); + // Definition bodies may be preceded by guarded .set/.asg prologues + // (uart_rx prepends the ICSS_CFG block), so find the signature line + // rather than assuming it is line 1. + let defLine = defBody ? (defBody.split('\n').find(l => l.includes('.macro')) || "") : ""; + + if (defLine) { + let defArgsStr = defLine.substring(defLine.indexOf('.macro') + 6).trim(); + let defArgs = defArgsStr ? defArgsStr.split(',').map(s=>s.trim()).filter(s=>s!=="") : []; + let defArity = defArgs.length; + + if (callArity !== defArity) { + report.logError("Macro arity mismatch: generated call has " + callArity + " args but definition expects " + defArity + ". Call: " + call, b_inst); + } + } + } + } +} + +/** + * GUI pin warnings: shared GPO/GPI physical pads, duplicate GPO drivers, + * malformed getPinUsage results, and unavailable device/core pad data. + * + * A two-way (bidirectional) pad shared by blocks of DIFFERENT modules (e.g. two blocks of different + * modules reading and driving the same two-way pad) makes the generated firmware contend on that pad. + * This validator emits a report.logWarning (not logError, so code generation still succeeds) so the + * user sees the conflict in the SysConfig GUI. + * + * Rules and exemptions: + * - claims come from each block's moduleStatic pin usage query (inst); + * - exempt (no warning): + * (a) the same instance's own open-drain pair; + * (b) different instances of the SAME block module that classify SEQUENTIAL (sourced from + * the generated common/block_control_flow_data.js table). + * - only pads that are actually shared between GPO and GPI on the device count (derived from + * SysConfig device data). + * + * Warning text names both blocks (instance names) and the pad. + * Device scope: the design's own device and PRU core (system.deviceData and the core name). + */ +// SysConfig core name -> pad signal prefix: +// icss_g_[rtu_|tx_]pru -> PRG_PRU (ICSSG), icss_m_pru -> PR_PRU (ICSSM). +function padPrefixForCore(coreName) { + const m = /^icss_([gm])(\d)(?:_(?:rtu|tx))?_pru(\d)$/.exec(coreName || ''); + return m ? (m[1] === 'g' ? 'PRG' : 'PR') + m[2] + '_PRU' + m[3] : null; +} + +function getDevicePadSharing(deviceData, coreName) { + if (!deviceData) return null; + const prefix = padPrefixForCore(coreName); + if (!prefix) return null; + const sharedIndices = new Set(); + + // 1. SysConfig runtime interface pins (ICSSG devices) + const ifaceName = /^PRG(\d)_/.test(prefix) ? 'PRU_ICSSG' + prefix[3] + '_PRU' : null; + if (ifaceName && deviceData.interfaces && deviceData.interfaces[ifaceName] && deviceData.interfaces[ifaceName].interfacePins) { + const ifacePins = deviceData.interfaces[ifaceName].interfacePins; + for (let i = 0; i < 20; i++) { + const gpoName = `${prefix}_GPO${i}`; + const gpiName = `${prefix}_GPI${i}`; + const gpoPin = ifacePins[`GPO${i}`]; + if (!gpoPin || !gpoPin.pinMappings) continue; + for (const pm of gpoPin.pinMappings) { + if (pm.name !== gpoName) continue; + for (const dp of (pm.pinMappings || [])) { + if (dp && dp.mux && Array.isArray(dp.mux.muxSetting)) { + const signals = dp.mux.muxSetting.map(s => s.peripheralPin && s.peripheralPin.name); + if (signals.includes(gpoName) && signals.includes(gpiName)) { + sharedIndices.add(i); + } + } + } + } + } + return sharedIndices; + } + + // 1b. SysConfig runtime interface pins (ICSSM devices): interface PRU-ICSS holds one two-way + // PR_PRU_GPIO interface pin per pad that exists; GPO i and GPI i share it. + const icssmIface = /^PR(\d)_/.test(prefix) ? 'PRU-ICSS' + prefix[2] : null; + if (icssmIface && deviceData.interfaces && deviceData.interfaces[icssmIface] && deviceData.interfaces[icssmIface].interfacePins) { + const ifacePins = deviceData.interfaces[icssmIface].interfacePins; + for (let i = 0; i < 20; i++) { + const pin = ifacePins[`${prefix}_GPIO${i}`]; + if (pin && pin.pinMappings && pin.pinMappings.length) sharedIndices.add(i); + } + return sharedIndices; + } + + // 2. Raw JSON device data structure (ICSSM devices, stubs and test harnesses). ICSSG pads carry a + // PRGx_PRUy_GPOn + PRGx_PRUy_GPIn pair; ICSSM pads carry one two-way PRx_PRUy_GPIOn signal. + // No usable pin data from any source: report "unknown" (null), not "no pad is shared" (an empty set). + const infos = deviceData.pinCommonInfos; + if (!infos || typeof infos !== 'object' || Object.keys(infos).length === 0) return null; + for (const info of Object.values(infos)) { + if (!info || !info.pinModeInfo) continue; + const sigs = new Set(info.pinModeInfo.map(m => m.signalName)); + for (let i = 0; i < 20; i++) { + if ((sigs.has(`${prefix}_GPO${i}`) && sigs.has(`${prefix}_GPI${i}`)) || sigs.has(`${prefix}_GPIO${i}`)) { + sharedIndices.add(i); + } + } + } + return sharedIndices; +} + +let bcfData = null; +function isSequentialBlock(blockName, instCfg) { + if (!bcfData && typeof system !== 'undefined' && typeof system.getScript === 'function') { + try { + bcfData = system.getScript("/pru_blocks/common/block_control_flow_data.js"); + } catch (e) { + bcfData = null; + } + } + if (bcfData && bcfData.blockControlFlow && bcfData.blockControlFlow[blockName] !== undefined) { + if (bcfData.blockControlFlow[blockName] === 'sequential') return true; + // continuous by default, sequential once the block's bound config is > 0 (e.g. a period count). + const bound = bcfData.blockControlFlowBound && bcfData.blockControlFlowBound[blockName]; + return !!(bound && instCfg && Number.isInteger(Number(instCfg[bound])) && Number(instCfg[bound]) > 0); + } + return false; // Fail closed if undecided or unknown +} + +function validateBidiPinCollisionsGlobal(inst, report) { + try { + // GUI pin warnings: shared GPO/GPI physical pads, duplicate GPO drivers + //, malformed pin-usage results, and unavailable device/core pad data. + // The GPO/GPI shared-pad rule needs device pad data; the duplicate-GPO rule and the + // 'could not evaluate' diagnostics do not. + const padSharing = getDevicePadSharing(system.deviceData, system.getScript("/common").getSelfSysCfgCoreName()); + const sharedIndices = padSharing || new Set(); + + const claims = []; + for (const modName in system.modules) { + const mod = system.modules[modName]; + if (!mod || !mod.$instances || mod.$instances.length === 0) continue; + for (let idx = 0; idx < mod.$instances.length; idx++) { + const bi = mod.$instances[idx]; + const moduleStatic = (bi.$module && bi.$module.moduleStatic) || mod.moduleStatic; + if (!moduleStatic || typeof moduleStatic.getPinUsage !== 'function') continue; + const iname = typeof bi.$name === 'string' ? bi.$name : `#${idx}`; + let pins = []; + try { + pins = moduleStatic.getPinUsage(bi); + } catch (e) { + const reason = (e && e.message) ? e.message : String(e); + report.logWarning( + `Pin-sharing check could not evaluate block '${iname}' (${modName}): ${reason}`, + inst + ); + continue; + } + const block = modName.split('/').pop(); + if (!Array.isArray(pins)) { + // A non-array result cannot be checked; report it rather than skip it silently. + report.logWarning( + `Pin-sharing check could not evaluate block '${iname}' (${modName}): its pin-usage function did not return an array, so its pins are not checked for sharing.`, + inst + ); + continue; + } + for (const p of pins) { + if (!p || (p.dir !== "GPO" && p.dir !== "GPI")) { + report.logWarning( + `Pin-sharing check could not evaluate block '${iname}' (${modName}): its pin-usage function returned an entry without a GPO/GPI dir, so that entry is not checked for sharing.`, + inst + ); + continue; + } + const pin = parseInt(p.pin); + if (isNaN(pin)) { + report.logWarning( + `Pin-sharing check could not evaluate block '${iname}' (${modName}): unparseable ${p.dir} pin "${String(p.pin)}", so that pin is not checked for sharing.`, + inst + ); + continue; + } + claims.push({ + inst: bi, + block: block, + iname: iname, + dir: p.dir, + pin: pin, + bidi: p.bidi === true, + cfg: bi + }); + } + } + } + + if (claims.length === 0) return; + + const gpoBits = claims.filter(c => c.dir === "GPO"); + const gpiBits = claims.filter(c => c.dir === "GPI"); + const warnedPairs = new Set(); + + if (padSharing === null && gpoBits.length && gpiBits.length) { + report.logWarning( + "GPO/GPI pad sharing could not be checked for this device/core: no pad data is available, so shared GPO/GPI pads are not detected.", + inst + ); + } + + if (padSharing !== null) { + for (const o of gpoBits) { + for (const i of gpiBits) { + if (o.pin !== i.pin || !sharedIndices.has(o.pin)) continue; + if (o.inst === i.inst && o.bidi && i.bidi) continue; // (a) same instance AND both bidi + if (o.inst !== i.inst && o.bidi && i.bidi && o.block === i.block && isSequentialBlock(o.block)) continue; // (b) different instances, both bidi, same block module, module classifies sequential + + const keyA = (o.inst && o.inst.$name) || o.iname; + const keyB = (i.inst && i.inst.$name) || i.iname; + const pairKey = o.inst === i.inst + ? `self:${keyA}:${o.pin}` + : (keyA < keyB ? `${keyA}:${keyB}:${o.pin}` : `${keyB}:${keyA}:${o.pin}`); + if (warnedPairs.has(pairKey)) continue; + warnedPairs.add(pairKey); + + report.logWarning( + `Pad ${o.pin} is driven (GPO) by '${o.iname}' (${o.block}) and read (GPI) by '${i.iname}' (${i.block}); the generated firmware will contend on this pad.`, + inst + ); + } + } + } + + // two different instances drive one GPO index and at least one block is CONTINUOUS + // (fail closed: a block missing from the control-flow table counts as continuous). + for (let x = 0; x < gpoBits.length; x++) { + for (let y = x + 1; y < gpoBits.length; y++) { + const a = gpoBits[x], b = gpoBits[y]; + if (a.pin !== b.pin || a.inst === b.inst) continue; + const cont = [a, b] + .filter((c) => !isSequentialBlock(c.block, c.cfg)) + .map((c) => c.block) + .filter((blk, k, arr) => arr.indexOf(blk) === k); + if (!cont.length) continue; + report.logWarning( + `GPO ${a.pin} is driven by both '${a.iname}' (${a.block}) and '${b.iname}' (${b.block}); continuous: ${cont.join(', ')}; the drivers will overwrite each other on this pin (duplicate GPO drivers).`, + inst + ); + } + } + } catch (e) { + const reason = (e && e.message) ? e.message : String(e); + report.logWarning( + `Pin-sharing check could not evaluate the design: ${reason}`, + inst + ); + } +} + +// device-capability gating for hardware-widget blocks +// every failure path in this gate fails CLOSED or errors VISIBLY. +// No degradation (missing caps script, missing SoC, unknown SoC, capability +// lookup exception) may silently skip validation and leave the build clean. +function validateDeviceCapabilities(inst, report) { + // Only gate when at least one module actually declares widget requirements, + // so widget-free designs stay clean. + let widgetModules = []; + for (let moduleName in system.modules) { + let mod = system.modules[moduleName]; + if (!mod || !mod.requiresWidget) continue; + widgetModules.push([moduleName, mod]); + } + if (widgetModules.length === 0) return; + + // Failure path 1: capability table itself failed to load -> refuse to pass. + if (!pruDeviceCaps) { + report.logError( + "Device capability table (pru_device_caps.js) could not be loaded - cannot verify hardware-widget requirements. Failing closed.", + inst + ); + return; + } + + // Failure path 2: /common getScript failure -> visible error, not a silent return. + let socName = null; + let commonFailure = null; + try { + const common = system.getScript("/common"); + if (common && typeof common.getSocName === "function") { + socName = common.getSocName(); + } + } catch (e) { + commonFailure = String(e && e.message || e); + } + if (commonFailure) { + report.logError( + `Cannot load common module to determine SoC (${commonFailure}) - cannot verify hardware-widget requirements. Failing closed.`, + inst + ); + return; + } + + // Failure path 3: SoC missing entirely -> visible error. + if (!socName) { + report.logError( + "SoC name could not be determined - cannot verify hardware-widget requirements. Failing closed.", + inst + ); + return; + } + + let deviceName = socName; + try { + if (system.deviceData && system.deviceData.device) { + deviceName = system.deviceData.device; + } + } catch (e) { /* use socName */ } + + // Failure path 4: SoC not in the capability table -> visible error, never a + // silent default capability set. + let socKnown = false; + try { + socKnown = (typeof pruDeviceCaps.isKnownSoc === "function") + ? !!pruDeviceCaps.isKnownSoc(socName) + : Object.keys(pruDeviceCaps.DEVICE_CAPS || {}).some(function (k) { return k === String(socName).toLowerCase(); }); + } catch (e) { socKnown = false; } + if (!socKnown) { + report.logError( + `SoC '${socName}' (device '${deviceName}') is not in the device capability table - cannot verify hardware-widget requirements. Failing closed.`, + inst + ); + return; + } + + for (let [moduleName, mod] of widgetModules) { + let raw = mod.requiresWidget; + let widgets = Array.isArray(raw) ? raw : [raw]; + for (let w of widgets) { + if (!w) continue; + let available = false; + let widgetFailure = null; + try { + available = pruDeviceCaps.isWidgetAvailable(w, socName); + } catch (e) { + // Failure path 5: capability lookup threw -> treat widget as + // UNAVAILABLE and say so, instead of assuming available. + widgetFailure = String(e && e.message || e); + } + if (widgetFailure) { + let instances = (mod.$instances && mod.$instances.length) ? mod.$instances : [inst]; + for (let bInst of instances) { + report.logError( + `Widget availability check for '${w}' threw an exception (${widgetFailure}) - treating as unavailable on device '${deviceName}' (SoC '${socName}'). Failing closed.`, + bInst + ); + } + } else if (!available) { + let instances = (mod.$instances && mod.$instances.length) ? mod.$instances : [inst]; + for (let bInst of instances) { + report.logError( + `Block '${mod.displayName || moduleName}' requires widget '${w}' which is not available on device '${deviceName}' (SoC '${socName}')`, + bInst + ); + } + } + } + } +} + /** * Detects cycles in the control flow and data flow graph (prev/next/T/F and input/output ports). @@ -926,6 +1646,9 @@ function validate(inst, report) return; } + // Device-capability gate (must name both widget and device, ERROR not warning) + validateDeviceCapabilities(inst, report); + // Validate R31 simulation input configuration validateR31SimulationInput(inst, report); @@ -943,6 +1666,8 @@ function validate(inst, report) //allocating pru registers for all blocks at system level pruRegisterAllocator.allocatePruRegisters(); + // GUI warning for cross-module bidirectional pad sharing + validateBidiPinCollisionsGlobal(inst, report); //validating register allocation let pruInstructionsWithMacros = pruRegisterAllocator.getPruRegisterAllocationSummary().pruInstructions; let pruInstructionsLabelWithMacros = pruRegisterAllocator.getPruRegisterAllocationSummary().labels; @@ -951,11 +1676,18 @@ function validate(inst, report) instruction.includes("-1") ); + // named, budgeted errors first; the generic message stays as the fallback for any unrecorded -1 + const recordedExhaustion = reportRegisterExhaustion(inst, report); + // user-typed registers that overlap each other + reportPinnedRegisterConflicts(inst, report); + if (hasRegisterError) { - report.logError("Out of PRU registers, reduce number of PRU blocks added", inst); - } - /*prepare instructions and init dmem0 for simulation*/ - else { + if (recordedExhaustion === 0) report.logError("Out of PRU registers, reduce number of PRU blocks added", inst); + } else { + // --- Generate-time validation pass --- + validatePruInstructions(inst, report); + + /*prepare instructions and init dmem0 for simulation*/ // If UART blocks are present, skip simulation and return zeros if (hasUartBlock) { const cyclesToSimulate = inst["pruCyclesToSimulate"]; @@ -1245,5 +1977,12 @@ This allows you to test PRU blocks that read from input pins (like SPI Read's SD getDataSectionContents, getInitSectDirectives, getUsectLinkerDirectives, - simulationData -}; \ No newline at end of file + getConstantTableSetup, + getConstantTableSetupCycles, + getSpiCsIdleSetupCycles, + pruCtrlBase, + simulationData, + validateDeviceCapabilities, + pruDeviceCaps, + validateBidiPinCollisionsGlobal +}; diff --git a/.metadata/sysconfig/.meta/pru_blocks/common/pru_device_caps.js b/.metadata/sysconfig/.meta/pru_blocks/common/pru_device_caps.js new file mode 100644 index 0000000..0e874a4 --- /dev/null +++ b/.metadata/sysconfig/.meta/pru_blocks/common/pru_device_caps.js @@ -0,0 +1,144 @@ +"use strict"; +/* + * pru_device_caps.js — single-source device capability table for the PRU blocks. + * + * TRM references: + * - AM64x/AM243x carry PRU_ICSSG (SPRUIM2J §6.4.1.1) + * - AM261x carries PRU_ICSSM (SPRUJB6 §7.3.1.1) + * + * Widget availability per subsystem: + * widget ICSSG (AM64x/AM243x) ICSSM (AM261x/AM263Px) + * MAC (multiply/acc) yes yes + * CRC16/32 yes yes + * SPAD banks 8 3 (widget present on both, bank count differs) + * BSWAP (byte swap) yes NO + * SUM32 yes NO + * Task Manager yes NO + * Broadside Stitch FIFO yes (PRU0/PRU1 only) NO + * Spinlock (XFR2SPIN) yes (64 flags, all 6) NO + * BS RAM 2KB per core yes NO + * RTU_PRU / TX_PRU cores yes NO + * IEP instances 2 1 + * PWM module yes (4 sets of 3 pairs) NO (SPRUIM2J 6.4.10; SPRUJB6 7.3.1.1 lists no PWM) + * + * AM263Px is ICSSM-class (-motor-control-sdk docs: SOC_AM263PX || SOC_AM261X -> PRU-ICSSM). + */ + +// Canonical widget identifiers — use these in requiresWidget declarations. +const WIDGETS = { + MAC: "mac", + CRC_HW: "crc_hw", + BSWAP: "bswap", + SUM32: "sum32", + TASK_MGR: "task_manager", + STITCH_FIFO: "stitch_fifo", + BS_RAM: "bs_ram", + RTU_PRU: "rtu_pru", + TX_PRU: "tx_pru", + IEP2: "iep2", // second IEP instance (IEP0 exists on all; IEP1 only on ICSSG) + SPAD: "spad", // scratchpad banks (present on both, but kept for completeness) + XFR2PSI: "xfr2psi", // XFRDMA / PSI-L packet streaming (ICSSG only, no PSI-L on ICSSM SPRUJB6 7.3.1.1) + XFR2TR: "xfr2tr", // RTU_PRU XFR2TR ring (SPRUIM2J 6.4.6.3.3 / Table 6-112). Attached to the + // RTU_PRU cores, which exist on ICSSG only (SPRUIM2J 6.4.1.1); ICSSM has no RTU_PRU. + FDB_GP: "fdb_gp", // FDB Generic RAM + compare (SPRUIM2J 6.4.11.2.3.7.4, Tables 6-103/6-105; + // broadside IDs 30/31/35). PRU0/PRU1 only, ICSSG only. + XFR2SPIN: "xfr2spin", // ICSSG hardware spinlock (SPRUIM2J Table 6-60 "XFR2SPIN 0x90 1 copy per + // PRU_ICSSG system"; 6.4.6.2.9 / Table 6-98). 64 ownership flags reachable from all + // six ICSSG cores. ICSSM (SPRUJB6 7.3.1.1) has no spinlock unit, and there is no + // software substitute for a cross-core atomic, so this is ICSSG-only. + QUEUE_EMPTY: "queue_empty", // ICSSG QUEUE_EMPTY status query (SPRUIM2J Table 6-60 BS ID 0xF0) + ICSSG_PWM: "icssg_pwm", // PRU_ICSSG PWM module driven by IEP compares (SPRUIM2J 6.4.10). ICSSM + // (SPRUJB6 7.3.1.1) has no PWM module; the R30 software PWM blocks are the fallback. +}; + +// ICSSG: full feature set +const ICSSG_CAPS = new Set([ + WIDGETS.MAC, + WIDGETS.CRC_HW, + WIDGETS.SPAD, + WIDGETS.BSWAP, + WIDGETS.SUM32, + WIDGETS.TASK_MGR, + WIDGETS.STITCH_FIFO, + WIDGETS.BS_RAM, + WIDGETS.RTU_PRU, + WIDGETS.TX_PRU, + WIDGETS.IEP2, + WIDGETS.XFR2PSI, + WIDGETS.XFR2TR, + WIDGETS.FDB_GP, + WIDGETS.XFR2SPIN, + WIDGETS.QUEUE_EMPTY, + WIDGETS.ICSSG_PWM, +]); + +// ICSSM (AM261x, AM263Px): smaller PRU-ICSS +const ICSSM_CAPS = new Set([ + WIDGETS.MAC, + WIDGETS.CRC_HW, + WIDGETS.SPAD, + // BSWAP, SUM32, TASK_MGR, STITCH_FIFO, BS_RAM, RTU_PRU, TX_PRU, IEP2 are absent +]); + +const DEVICE_CAPS = { + am64x: ICSSG_CAPS, + am243x: ICSSG_CAPS, + am261x: ICSSM_CAPS, + am263px: ICSSM_CAPS, + am263x: ICSSM_CAPS, +}; + +// fail closed. A missing or unknown SoC must never silently inherit +// a capability set — return an EMPTY set so every requiresWidget check reports +// the widget as missing (visible error) instead of quietly passing. +function isKnownSoc(socName) { + if (!socName) return false; + return Object.prototype.hasOwnProperty.call(DEVICE_CAPS, String(socName).toLowerCase()); +} + +function getCapsForSoc(socName) { + if (!socName) return new Set(); + const key = String(socName).toLowerCase(); + return DEVICE_CAPS[key] || new Set(); +} + +function isWidgetAvailable(widget, socName) { + const caps = getCapsForSoc(socName); + return caps.has(widget); +} + +function getMissingWidgets(requiresWidget, socName) { + if (!requiresWidget) return []; + const list = Array.isArray(requiresWidget) ? requiresWidget : [requiresWidget]; + const caps = getCapsForSoc(socName); + return list.filter(function (w) { return !caps.has(w); }); +} + +// All known device ids (sorted for docs) +const ALL_DEVICES = Object.keys(DEVICE_CAPS).sort(); + +// SysConfig export (system.getScript) +if (typeof exports !== "undefined") { + exports.WIDGETS = WIDGETS; + exports.ICSSG_CAPS = ICSSG_CAPS; + exports.ICSSM_CAPS = ICSSM_CAPS; + exports.DEVICE_CAPS = DEVICE_CAPS; + exports.getCapsForSoc = getCapsForSoc; + exports.isKnownSoc = isKnownSoc; + exports.isWidgetAvailable = isWidgetAvailable; + exports.getMissingWidgets = getMissingWidgets; + exports.ALL_DEVICES = ALL_DEVICES; +} + +// Node / test harness export +if (typeof module !== "undefined" && module.exports) { + module.exports.WIDGETS = WIDGETS; + module.exports.ICSSG_CAPS = ICSSG_CAPS; + module.exports.ICSSM_CAPS = ICSSM_CAPS; + module.exports.DEVICE_CAPS = DEVICE_CAPS; + module.exports.getCapsForSoc = getCapsForSoc; + module.exports.isKnownSoc = isKnownSoc; + module.exports.isWidgetAvailable = isWidgetAvailable; + module.exports.getMissingWidgets = getMissingWidgets; + module.exports.ALL_DEVICES = ALL_DEVICES; +} diff --git a/.metadata/sysconfig/.meta/pru_blocks/common/pru_reserved_names.js b/.metadata/sysconfig/.meta/pru_blocks/common/pru_reserved_names.js new file mode 100644 index 0000000..4ba7328 --- /dev/null +++ b/.metadata/sysconfig/.meta/pru_blocks/common/pru_reserved_names.js @@ -0,0 +1,160 @@ +"use strict"; +/* + * pru_reserved_names.js — ONE shared clpru reserved-identifier list. + * + * A SysConfig instance name the assembler rejects must be a SysConfig + * validation ERROR, not a build failure. Every block that emits its instance + * name (or a name derived from it) as an assembly label or symbol calls into + * this file from its validate(); the list lives HERE ONLY — do not copy it + * into blocks. + * + * Checked against TI clpru 2.3.3: each name below, used as a data symbol + * (` .byte 1, 2, 3` in .initdmem — exactly what the CRC/LUT blocks emit) + * and as a code label (`:`), fails with [E0300] " is a reserved + * identifier". Names that assemble are not listed: SCAN, BROAD, IDLE, MULL, MUL, b4-b7, + * w0-w7, assembler directives (byte/ushort/uint/usect/text/sect/global/...), + * sp, pc, main, call, ret, TEMP_REG1 and ordinary names all assemble fine. + * + * Case (each claim checked with clpru 2.3.3, both as a data symbol and as + * a code label): register names r0-r31/c0-c31 + * are case-insensitive (R5, C0 fail) — ANY case is rejected. Mnemonics are + * rejected in exact-lowercase (add) and exact-ALL-UPPERCASE (ADD) only; a + * mixed-case form (Add, Ldi32) ASSEMBLES and must be accepted, because a + * name that assembles is not reserved. Byte lanes are + * rejected lowercase only (b0 fails; B0 ASSEMBLES and is accepted). + * + * Loading: blocks reach this file through + * system.getScript("/pru_blocks/common/pru_reserved_names.js") + * the same way pru_spi_* blocks share pru_spi_timing.js. It needs no + * `system` object itself, so harnesses can load it for real in a bare vm. + */ + +// PRU general-purpose registers R0-R31 and constant-table registers C0-C31. +var REGISTER_NAMES = []; +(function () { + var i; + for (i = 0; i < 32; i++) { REGISTER_NAMES.push("r" + i); } + for (i = 0; i < 32; i++) { REGISTER_NAMES.push("c" + i); } +})(); + +// Byte-lane selectors the assembler reserves as identifiers (w0-w7 are NOT +// reserved — checked with clpru, they assemble — so only b0-b3 are listed). +var BYTE_LANE_NAMES = ["b0", "b1", "b2", "b3"]; + +// PRU instruction mnemonics clpru reserves (each checked lowercase and +// ALL-UPPERCASE; mixed-case forms assemble and are NOT rejected — see +// isReservedAsmName. SCAN/BROAD/IDLE/MULL/MUL assemble and are NOT listed). +var MNEMONIC_NAMES = [ + "add", "adc", "sub", "suc", "rsb", "rsc", + "and", "or", "xor", "not", "min", "max", "clr", "set", + "lsl", "lsr", "lmbd", + "mov", "ldi", "ldi32", "lbbo", "sbbo", "lbco", "sbco", "zero", "fill", + "mvib", "mviw", "mvid", + "jmp", "jal", + "qbgt", "qbge", "qblt", "qble", "qbeq", "qbne", "qba", "qbbc", "qbbs", + "loop", "halt", "slp", + "wbc", "wbs", + "xin", "xout", "xchg", "sxin", "sxout", "sxchg", "nop" +]; + +var RESERVED_ASM_NAMES = REGISTER_NAMES.concat(BYTE_LANE_NAMES, MNEMONIC_NAMES); + +var REGISTER_LOOKUP = {}; +REGISTER_NAMES.forEach(function (n) { REGISTER_LOOKUP[n] = true; }); + +var BYTE_LANE_LOOKUP = {}; +BYTE_LANE_NAMES.forEach(function (n) { BYTE_LANE_LOOKUP[n] = true; }); + +var MNEMONIC_LOOKUP = {}; +var MNEMONIC_UPPER_LOOKUP = {}; +MNEMONIC_NAMES.forEach(function (n) { + MNEMONIC_LOOKUP[n] = true; + MNEMONIC_UPPER_LOOKUP[n.toUpperCase()] = true; +}); + +/** + * True when `name` is a clpru reserved identifier, using the assembler's own + * case rules (see header): registers match case-insensitively; mnemonics + * match the exact-lowercase or exact-ALL-UPPERCASE spelling only (mixed-case + * assembles, so it is accepted); byte lanes match the exact-lowercase + * spelling only (B0 assembles, so it is accepted). + */ +function isReservedAsmName(name) { + if (name === undefined || name === null) return false; + var s = String(name); + if (REGISTER_LOOKUP[s.toLowerCase()] === true) return true; + if (BYTE_LANE_LOOKUP[s] === true) return true; + if (MNEMONIC_LOOKUP[s] === true) return true; + if (MNEMONIC_UPPER_LOOKUP[s] === true) return true; + return false; +} + +function reservedErrorFor(kind, value, detail) { + return "Instance " + kind + " \"" + value + "\" is a clpru reserved " + + "identifier ([E0300]; PRU register, constant-table register or " + + "instruction mnemonic) and cannot be used as an assembly label or " + + "symbol. Rename it." + (detail ? " " + detail : ""); +} + +/** + * Reject a reserved instance ($name). For blocks that emit the bare name + * (crc, look_up_table, modbus lutName). `detail` names the emission, e.g. + * "This block emits ' .byte ...' into .initdmem." + */ +function validateInstanceName(inst, report, detail) { + if (!inst || inst.$name === undefined || inst.$name === null) return; + if (isReservedAsmName(inst.$name)) { + report.logError(reservedErrorFor("name", inst.$name, detail), inst); + } +} + +/** + * Reject a reserved value in a derived assembly symbol. Some blocks never + * emit the raw name: conditional emits `_TRUE`/`_FALSE` branch + * labels, group emits `_start`, pid emits `_pid_state`. + * Those derived symbols assemble even for a reserved base (checked with clpru: c0_TRUE, + * c0_start, c0_pid_state and add_TRUE all build), so only the DERIVED form + * is checked — a reserved base whose derived symbols assemble is accepted. + * `field` is a config field name, or "$name" for the instance name. + */ +function validateDerivedSymbol(inst, report, field, suffixes, detail) { + if (!inst) return; + var value = (field === "$name") ? inst.$name : inst[field]; + if (value === undefined || value === null) return; + if (String(value).trim() === "") return; // emptiness is reported by the block itself + suffixes.forEach(function (suffix) { + var derived = String(value) + suffix; + if (isReservedAsmName(derived)) { + report.logError("Derived name \"" + derived + "\" (from " + field + ") is a clpru reserved " + + "identifier ([E0300]; PRU register, constant-table register or " + + "instruction mnemonic) and cannot be used as an assembly label or " + + "symbol. Rename it." + (detail ? " " + detail : ""), inst, field === "$name" ? undefined : field); + } + }); +} + +/** + * Reject a reserved value in a named config field that is emitted as an + * assembly label or symbol (groupName, labelName, variableName). + */ +function validateSymbolField(inst, report, field, detail) { + if (!inst) return; + var value = inst[field]; + if (value === undefined || value === null) return; + if (String(value).trim() === "") return; // emptiness is reported by the block itself + if (isReservedAsmName(value)) { + report.logError("Reserved name \"" + value + "\" (" + field + ") is a clpru reserved " + + "identifier ([E0300]; PRU register, constant-table register or " + + "instruction mnemonic) and cannot be used as an assembly label or " + + "symbol. Rename it." + (detail ? " " + detail : ""), inst, field); + } +} + +exports = { + RESERVED_ASM_NAMES: RESERVED_ASM_NAMES, + isReservedAsmName: isReservedAsmName, + validateInstanceName: validateInstanceName, + validateDerivedSymbol: validateDerivedSymbol, + validateSymbolField: validateSymbolField +}; +if (typeof module !== "undefined" && module && module.exports) { module.exports = exports; } diff --git a/.metadata/sysconfig/.meta/pru_blocks/common/pru_spi_timing.js b/.metadata/sysconfig/.meta/pru_blocks/common/pru_spi_timing.js new file mode 100644 index 0000000..ddfd21d --- /dev/null +++ b/.metadata/sysconfig/.meta/pru_blocks/common/pru_spi_timing.js @@ -0,0 +1,153 @@ +"use strict"; +/* + * pru_spi_timing.js - shared timing checks of the pru_spi_read / pru_spi_write / pru_spi_transfer blocks: the + * "CS Setup Time" / "CS Hold Time" / "Data Setup Time" nanosecond settings as the PRU cycle counts their macros take. + * + * Facts this rests on (TI clpru 2.3.3, measured with --asm_listing): + * - `.loop N` / `nop` / `.endloop` is unrolled at assembly time into N `nop` instructions. A PRU instruction + * takes one cycle, so a macro formal of N costs exactly N cycles with no loop overhead. + * - `.loop 0` is accepted and emits nothing. + * - Every nop is a word of IRAM (4 bytes; a PRU core has 12 KB), so the count is capped (MAX_DELAY_CYCLES). + * + * The PRU core clock is the "PRU Clock Frequency" setting (pruClkFreq) of the Simulation Settings static module. + * When the static module is absent the helper falls back to that setting's default, DEFAULT_PRU_CLK_MHZ. + */ + +const DEFAULT_PRU_CLK_MHZ = 200; + +// Each delay site expands to this many nops at most (1 KB of the 12 KB PRU IRAM); a macro has up to two such sites. +const MAX_DELAY_CYCLES = 256; + +// The Simulation Settings option "333.333 MHz" is the AM64x/AM243x 1000/3 MHz core clock. +const CLK_333_OPTION_MHZ = 333.333; + +function pruClockMhz() { + let mhz = DEFAULT_PRU_CLK_MHZ; + const staticModule = system.modules["/pru_blocks/common/pru_blocks_static_module"]; + if (staticModule && staticModule.$static && staticModule.$static.pruClkFreq) { + mhz = Number(staticModule.$static.pruClkFreq); + } + return Math.abs(mhz - CLK_333_OPTION_MHZ) < 1e-3 ? 1000 / 3 : mhz; +} + +/** + * Setup and hold times are minimums, so the conversion rounds UP: ceil(ns * MHz / 1000). + * The tiny subtraction keeps an exact product (35 ns at 200 MHz = 7 cycles) from tipping to 8 on float noise. + * 0 ns (or less) is 0 cycles, which the macros turn into an empty `.loop 0`. + */ +function nsToCycles(ns, mhz) { + const t = Number(ns); + if (!(t > 0)) return 0; + return Math.ceil(t * mhz / 1000 - 1e-9); +} + +/** Cycle count for one nanosecond setting at the design's current PRU clock. */ +function csDelayCycles(ns) { + return nsToCycles(ns, pruClockMhz()); +} + +/** + * Report an error for each of CS Setup Time / CS Hold Time / Data Setup Time whose converted count exceeds + * MAX_DELAY_CYCLES. Peripheral mode does not use these settings. + */ +function validateCsTimes(inst, report) { + if (inst["Device Mode"] === "peripheral") return; + const mhz = pruClockMhz(); + ["CS Setup Time", "CS Hold Time", "Data Setup Time"].forEach(function (name) { + const cycles = nsToCycles(inst[name], mhz); + if (cycles > MAX_DELAY_CYCLES) { + report.logError(name + " of " + inst[name] + " ns is " + cycles + " PRU cycles at " + mhz.toFixed(3) + " MHz; the limit is " + + MAX_DELAY_CYCLES + " PRU cycles (each cycle is one nop in program memory). Lower the time or the PRU Clock Frequency.", + inst, name); + } + }); +} + +// program-memory budget check. Every delay cycle is one nop (4 bytes of IRAM), so one maxed block is ~2 KB and a few +// of them overflow the 12 KB a PRU core has. The linker already refuses an overflowing .text, but only after the whole design has +// been generated; this reports it at validation, naming the design total. +const IRAM_BUDGET_BYTES = 12 * 1024; +// Fixed instructions of one block's macro body, rounded UP from the clpru .text of single-block builds (controller and +// peripheral, 8-32 bit packets, MODE0-3: 0x44..0x64 bytes for the whole design incl. its data-load and CS-idle blocks). +const FIXED_BLOCK_INSTRUCTIONS = 28; +// Peripheral macros unroll `.loop CS_FILTER_CYCLES` at this many sites. +const FILTER_SITES = 2; +// CS Filter Cycles has range [1, 0xFFFFFFFF]: the smallest filter a peripheral block can be lowered to. +const MIN_CS_FILTER_CYCLES = 1; +const SPI_MODULES = [ + "/pru_blocks/pru_io_blocks/pru_spi_read", + "/pru_blocks/pru_io_blocks/pru_spi_write", + "/pru_blocks/pru_io_blocks/pru_spi_transfer" +]; + +/** + * Upper-bound estimate of the program memory one SPI block instance takes: 4 bytes per instruction. Controller mode: the fixed body + * plus every delay site (CS setup, CS hold, data setup and the two SCLK pulse compensations). Peripheral mode: the fixed body plus + * the CS glitch filter, `.loop CS_FILTER_CYCLES` at two sites of the slave macro (wait for CS high, wait for CS low), each unrolled + * into CS Filter Cycles copies of one qbbc/qbbs. Both modes' settings are hidden in the other mode. + */ +function estimateIramBytes(inst) { + let instructions = FIXED_BLOCK_INSTRUCTIONS; + if (inst["Device Mode"] !== "peripheral") { + instructions += csDelayCycles(inst["CS Setup Time"]) + csDelayCycles(inst["CS Hold Time"]) + + csDelayCycles(inst["Data Setup Time"]) + Number(inst["delay_component1"]) + Number(inst["delay_component2"]); + } else { + instructions += FILTER_SITES * Number(inst["CS Filter Cycles"]); + } + return 4 * instructions; +} + +/** + * Warn, once per design (on the first SPI instance), when the estimated IRAM of all pru_spi_* instances together exceeds the + * 12 KB of one PRU core. A warning and not an error: the estimate is an upper bound and the other blocks of the design are + * not counted, so it can neither prove a fit nor prove an overflow (the linker is the final word). The advice names the + * actual cause: when the peripheral CS Filter Cycles loops alone push the design over the budget, the warning + * says so instead of advising the controller-mode CS Setup / Hold, Data Setup and SCLK widths. "Alone" means the design would + * fit with every filter at its minimum of 1 cycle; a design that still overflows then gets the controller advice. + * Tightness (estimate / linked .text, 3 peripheral blocks): within 10% at CS Filter Cycles >= 100 (read 1.08x, write + * 1.07x, transfer 1.04x); loose at small filters because the fixed body is rounded up (read: 2.20x at 1, 2.04x at 2, 1.52x at 10, + * 1.23x at 30; write 2.05x / 1.92x / 1.47x / 1.21x; transfer 1.39x / 1.35x / 1.21x / 1.11x). Always conservative, never under. + */ +function validateIramBudget(inst, report) { + const all = []; + SPI_MODULES.forEach(function (name) { + const mod = system.modules[name]; + if (mod && mod.$instances) mod.$instances.forEach(function (i) { all.push(i); }); + }); + if (all.length === 0 || all[0] !== inst) return; + let total = 0, filterBytes = 0, minFilterBytes = 0; + all.forEach(function (i) { + total += estimateIramBytes(i); + if (i["Device Mode"] === "peripheral") { + filterBytes += 4 * FILTER_SITES * Number(i["CS Filter Cycles"]); + minFilterBytes += 4 * FILTER_SITES * MIN_CS_FILTER_CYCLES; + } + }); + if (total > IRAM_BUDGET_BYTES) { + let advice; + // Lowering the filter only helps while the design fits with every filter at its minimum (CS Filter Cycles >= 1, so a + // peripheral block never drops below 8 bytes of filter): otherwise the controller-side widths must shrink too. + if (filterBytes > 0 && total - filterBytes + minFilterBytes <= IRAM_BUDGET_BYTES) { + advice = "The CS Filter Cycles loops of the peripheral-mode block(s) push it over: lower the CS Filter Cycles"; + } else { + advice = "Shorten the CS Setup / Hold Time, Data Setup Time and SCLK pulse widths, or lower the PRU Clock Frequency"; + } + report.logWarning("The " + all.length + " SPI block(s) in this design need an estimated " + total + " bytes of program memory (IRAM), over the " + + IRAM_BUDGET_BYTES + " bytes of one PRU core (4 bytes per instruction; every delay cycle is a nop). " + advice + + "; the linker will refuse an overflowing .text." + + (filterBytes > 0 ? " The estimate is conservative: within 10% of the linked size for peripheral CS filters of 100 cycles or more, " + + "but up to about 2x over at small filters (the default filter of 2 cycles reads about 2x for read / write, 1.35x for transfer)." : ""), inst); + } +} + +exports = { + IRAM_BUDGET_BYTES: IRAM_BUDGET_BYTES, + estimateIramBytes: estimateIramBytes, + validateIramBudget: validateIramBudget, + DEFAULT_PRU_CLK_MHZ: DEFAULT_PRU_CLK_MHZ, + MAX_DELAY_CYCLES: MAX_DELAY_CYCLES, + pruClockMhz: pruClockMhz, + nsToCycles: nsToCycles, + csDelayCycles: csDelayCycles, + validateCsTimes: validateCsTimes +}; diff --git a/.metadata/sysconfig/.meta/pru_blocks/common/pru_syscfg.asm.xdt b/.metadata/sysconfig/.meta/pru_blocks/common/pru_syscfg.asm.xdt index 1f395e1..673040b 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/common/pru_syscfg.asm.xdt +++ b/.metadata/sysconfig/.meta/pru_blocks/common/pru_syscfg.asm.xdt @@ -1,6 +1,7 @@ %const pruRegisterAllocationSummary = system.getScript("/pru_blocks/common/register_allocation/pru_register_allocator.js").getPruRegisterAllocationSummary(); %const registerAllocator = system.getScript("/pru_blocks/common/register_allocation/pru_register_allocator.js"); %const systemModules = system.modules; +%const bindingModule = system.modules["/pru_blocks/common/pru_blocks_static_module"]; ; Copyright (C) 2024 Texas Instruments Incorporated - http://www.ti.com/ ; ; Redistribution and use in source and binary forms, with or without @@ -53,6 +54,12 @@ ;************************************* includes ************************************* .include "pru_syscfg.inc" +; Define temporary registers for macro usage + .asg R28, TEMP_REG1 + .asg R29, TEMP_REG2 + .asg R26, TEMP_REG3 + .asg R27, TEMP_REG4 + .retain ; Required for building .out with assembly file .retainrefs ; Required for building .out with assembly file @@ -98,10 +105,16 @@ % } % } %} +%// Program CTPPR0 (c28 -> shared RAM) and the SPI CS idle levels at every entry into generated code. +%let ctSetup = bindingModule && typeof bindingModule.getConstantTableSetup === "function" ? bindingModule.getConstantTableSetup() : ""; +%let groupStartLabelSet = new Set((groupMetadata || []).map(g => `${g.groupName}_start`)); ;************************************************************************************** ; PRU assembly code outside group block ;************************************************************************************** sysconfig_generated_start: +%if(ctSetup){ +`ctSetup` +%} %// First loop: Output ungrouped blocks (sysconfig_generated section) %for(let iterator = 0; iterator < groupStartIndex; iterator++) %{ @@ -128,6 +141,9 @@ sysconfig_generated_start: %if(typeof(labels[iterator]) == "string" && labels[iterator] !== "") %{ `labels[iterator]`: +%if(ctSetup && groupStartLabelSet.has(labels[iterator])){ +`ctSetup` +%} %} % if((!pruInstructions[iterator].includes("-1")) && (pruInstructions[iterator] != "0") && (!pruInstructions[iterator].includes("undefined"))) % { diff --git a/.metadata/sysconfig/.meta/pru_blocks/common/pru_syscfg.inc.xdt b/.metadata/sysconfig/.meta/pru_blocks/common/pru_syscfg.inc.xdt index 2da4cd3..ab90138 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/common/pru_syscfg.inc.xdt +++ b/.metadata/sysconfig/.meta/pru_blocks/common/pru_syscfg.inc.xdt @@ -72,6 +72,8 @@ SLICE`icssgNum` .set 1 ;************************************* register definitions ************************* .asg R28, TEMP_REG1 .asg R29, TEMP_REG2 + .asg R26, TEMP_REG3 + .asg R27, TEMP_REG4 % // Define RET_ADDR0 as the dynamically allocated group return address register % let groupMetadata = system.getScript("/pru_blocks/common/register_allocation/pru_register_allocator.js").getGroupMetadata(); % if (groupMetadata && groupMetadata.length > 0) { diff --git a/.metadata/sysconfig/.meta/pru_blocks/common/register_allocation/pru_register_allocator.js b/.metadata/sysconfig/.meta/pru_blocks/common/register_allocation/pru_register_allocator.js index d224604..1fd7b37 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/common/register_allocation/pru_register_allocator.js +++ b/.metadata/sysconfig/.meta/pru_blocks/common/register_allocation/pru_register_allocator.js @@ -2,7 +2,8 @@ * PRU Register Allocator * * Manages allocation of PRU registers (R0-R29) for blocks. - * R28 and R29 are reserved as temporary registers per block. + * R28 and R29 are reserved as temporary registers per block. R26 and R27 (TEMP_REG3/TEMP_REG4) are also + * held back when a block macro in the design uses them. */ // Constants for register allocation @@ -23,6 +24,288 @@ let cycleBudgetMap = new Map(); let groupMetadata; let loopCounterRegisters = {}; // Track loop counters: {instanceName: {byteOffset, numBytes}} let groupReturnAddrByteOffset = -1; // Single allocated return address register for all group blocks +let containerOf = {}; // child instance name -> name of the loop/group block that lists it in $groupContents +let deferredLoopReleases = {}; // loop instance name -> map entries to release when that loop's body has been emitted +let hoistedOutOf = {}; // child instance name -> loops that list it in preInitBlocks (it is emitted BEFORE them) +let hoistedInPlace = {}; // ALU instance name -> [{entry, container}] preInit inputs it released to accumulate in place +let accumulatorBindings = {}; // update block name -> {preInit, container} from loop_block.accumulators + +/** + * Block fields that name a PRU register typed by the user and emitted VERBATIM by the block's macro. The allocator must + * not hand such a register to another live value, or the macro would overwrite it. Every register named here is + * removed from the pool, for the whole design, before anything is allocated. + * Key: module path. Value: list of field descriptors: + * default : the field's default value in the block source; a conflict between two fields both still at default is a WARNING, not an error + * field : config name holding the register ("R4", "R5.b0", "R3.w1", or a bare register number) + * bytes : width of a bare register token (a .bN/.wN token carries its own width); a number or (inst) => number + * fullRegister : the macro does a fixed 4-byte access (`zero ®, 4`, `lbco ®, ..., 4`, `ldi32 REG`) whatever the token, so a + * sub-register token (R0.b0) still reserves max(token, 4) bytes from its first byte + * when : (inst) => bool, the field is only read/written in some modes + * role : what the macro does with the register: "w" writes a result there, "r" only reads it, "rm" reads it AND destroys + * it (shifts it out); a string or (inst) => string. Used for the overlap check only (see reservePinnedRegisters). + * Fields that exist but are never touched by the macro are deliberately absent: reserving them would move allocations for nothing. + */ +const PINNED_REGISTER_FIELDS = { + // Entries are added together with the blocks that declare such fields. +}; + +let pinnedRegisters = []; // {block, field, token, firstByte, bytes} for every user-typed register, in the pool or not +let pinnedRegisterConflicts = []; // two pins that claim the same byte, reported by validation as an ERROR +let pinnedProblems = []; // {block, field, token, why} typed registers that are not a usable register window (ERROR in validation) +let pinnedByteCount = 0; // bytes of the pool removed by pins +let scratchRegisterBytes = 0; // bytes of the pool held for macro scratch registers TEMP_REG3 (R26) / TEMP_REG4 (R27) +let scratchRegistersUsed = []; // the scratch registers ("TEMP_REG3", "TEMP_REG4") some block macro in the design uses +let poolAfterPins = []; // pruByteArray right after the architectural windows and the pins were removed (free-register suggestion) + +/** + * Parses a typed register ("R4", "R5.b0", "R3.w1", 7) into {firstByte, bytes} (windowBytes: the macro's window, not the token, sets the width of a sub-register token), or null when it is not a register token. + */ +function parsePinnedRegister(value, bareBytes, windowBytes) { + const m = /^\s*R(\d+)(?:\.([bw])([0-3]))?\s*$/i.exec(typeof value === "number" ? "R" + value : String(value)); + if (!m || (m[2] && m[2].toLowerCase() === "w" && m[3] === "3")) return null; // .w3 would straddle two registers + const reg = parseInt(m[1], 10); + if (!m[2]) return { firstByte: reg * numOfBytesPerRegister, bytes: bareBytes }; + const tokenBytes = m[2].toLowerCase() === "b" ? 1 : 2; + // a macro that moves a byte window starting at the token covers the window, not just the token + return { firstByte: reg * numOfBytesPerRegister + parseInt(m[3], 10), bytes: windowBytes ? Math.max(tokenBytes, bareBytes) : tokenBytes }; +} + +/** + * Two pins on the same bytes are fine only when one block hands a value to another through the register (one writes, + * the other only reads) or both only read. Two writers, a reader + * that destroys what another reads, and any two fields of ONE block would overwrite each other. + */ +function isIntendedHandOff(a, b) { + if (a.block === b.block) return false; + const pair = [a.role, b.role].sort().join("+"); + return pair === "r+w" || pair === "r+r" || pair === "rm+w"; +} + +/** + * TEMP_REG3 / TEMP_REG4 are the macro scratch registers R26 / R27. They sit inside the allocatable range, so a design whose + * macros use them must not also keep values there: the macro would overwrite a live value, or its own result. Removes R26 + * and/or R27 from the pool when any block's macro text names TEMP_REG3 / TEMP_REG4. Designs that use neither keep the full pool. + * Must run after the pool is created and before anything is allocated. + */ +function reserveMacroScratchRegisters() { + scratchRegisterBytes = 0; + scratchRegistersUsed = []; + const scratch = [["TEMP_REG3", 26], ["TEMP_REG4", 27]]; + const found = {}; + for (const moduleName in systemModules) { + const module = systemModules[moduleName]; + if (!module || typeof module.getMacro !== "function") continue; + for (const instance of module.$instances || []) { + if (!instance || !instance.opCode) continue; + let body = ""; + try { body = String(module.getMacro("", instance.opCode) || ""); } catch (e) { body = ""; } + for (const [name] of scratch) if (new RegExp("\\b" + name + "\\b").test(body)) found[name] = true; + } + } + for (const [name, reg] of scratch) { + if (!found[name]) continue; + scratchRegistersUsed.push(name); + for (let b = reg * numOfBytesPerRegister; b < (reg + 1) * numOfBytesPerRegister && b < totalBytes; b++) { + if (pruByteArray[b] === 0) { pruByteArray[b] = 1; scratchRegisterBytes++; } + } + } +} + +/** + * Removes the user-pinned registers (all bytes they overlap) from the pool. Must run before anything is allocated. + * Pins that fall outside R0-R27 (R28/R29 temporaries, R30/R31) take nothing from the pool. + * + * A pin whose block only READS the register (role "r") cannot damage + * anything, so it is reserved only when another block's pin WRITES those bytes (a hand-off through + * the register). A read-only pin with no writer is fed by an allocator output that happens to sit in that + * register. Reserving it would + * take the register away from the very value it is meant to publish, so it stays in the pool until the block takes a data + * input and is flagged reserved:false in getPinnedRegisters(). + */ +function reservePinnedRegisters() { + pinnedRegisters = []; + pinnedRegisterConflicts = []; + pinnedProblems = []; + pinnedByteCount = 0; + for (const moduleName in PINNED_REGISTER_FIELDS) { + const instances = systemModules[moduleName]?.$instances || []; + for (const instance of instances) { + for (const spec of PINNED_REGISTER_FIELDS[moduleName]) { + if (spec.when && !spec.when(instance)) continue; + const raw = instance[spec.field]; + if (raw === undefined || raw === null || raw === "") continue; + const bareBytes = typeof spec.bytes === "function" ? spec.bytes(instance) : (spec.bytes || numOfBytesPerRegister); + const parsed = parsePinnedRegister(raw, bareBytes, typeof spec.bytes === "function" || spec.fullRegister === true); + const token = typeof raw === "number" ? "R" + raw : String(raw); + if (!parsed) { + pinnedProblems.push({ block: instance.$name, field: spec.field, token: token, + why: "is not a PRU register (write R0-R27, optionally with .b0-.b3 or .w0-.w2, e.g. R4 or R5.b0)" }); + continue; + } + // a field marked ownWindowCheck is range-checked by its own block validator; every other field is checked here + if (!spec.ownWindowCheck && parsed.firstByte + parsed.bytes > totalBytes) { + pinnedProblems.push({ block: instance.$name, field: spec.field, token: token, + why: "makes the block touch " + parsed.bytes + " byte(s) from byte " + parsed.firstByte + ", past R27 (R28/R29 are the compiler temporaries, R30/R31 the I/O registers); choose a register whose window ends at or before R27.b3" }); + } + const role = typeof spec.role === "function" ? spec.role(instance) : spec.role; + pinnedRegisters.push({ block: instance.$name, field: spec.field, role: role, token: token, + firstByte: parsed.firstByte, bytes: parsed.bytes, reserved: true, + alwaysReserve: Boolean(spec.alwaysReserve), + atDefault: String(raw).trim().toUpperCase() === String(spec.default).toUpperCase() }); + } + } + } + const overlaps = (p, q) => p.firstByte < q.firstByte + q.bytes && q.firstByte < p.firstByte + p.bytes; + for (const pin of pinnedRegisters) { + if (pin.role === "r" && !pin.alwaysReserve) pin.reserved = pinnedRegisters.some(q => q.role === "w" && q.block !== pin.block && overlaps(pin, q)); + } + const owners = Array.from({ length: totalBytes }, () => []); + const reported = {}; + for (const pin of pinnedRegisters) { + for (let b = pin.firstByte; b < pin.firstByte + pin.bytes && b < totalBytes; b++) { + for (const other of owners[b]) { + const key = other.block + "." + other.field + "|" + pin.block + "." + pin.field; + if (!reported[key] && !isIntendedHandOff(other, pin)) { + reported[key] = true; + pinnedRegisterConflicts.push({ first: other, second: pin }); + } + } + if (pin.reserved && pruByteArray[b] === 0) { pruByteArray[b] = 1; pinnedByteCount++; } + owners[b].push(pin); + } + } + poolAfterPins = Array.from(pruByteArray); +} + +/** + * emit position of each block (first instruction), after allocation. + */ +function pinnedEmitOrder() { + const order = {}; + (pruRegisterAllocationSummary.pruInstructionInstances || []).forEach((inst, i) => { + if (inst && inst.$name !== undefined && order[inst.$name] === undefined) order[inst.$name] = i; + }); + return order; +} + +function pinsOverlap(p, q) { + return p.firstByte < q.firstByte + q.bytes && q.firstByte < p.firstByte + p.bytes; +} + +const UNCONDITIONAL_EXIT_OPCODES = ["HALT", "JMP"]; // flow_control_block: HALT, or JMP to the end of the generated code + +/** the instance object of an allocated block, by name. */ +function allocatedInstanceNamed(name) { + const entry = moduleInstanceRegisterMap[name]; + return entry ? systemModules[entry.moduleName]?.$instances[entry.instanceNum] : undefined; +} + +/** names of the callable (non-loop) groups around a block, innermost first, as one comparable string. */ +function callableGroupChain(name) { + return enclosingContainers(name).filter(c => !(moduleInstanceRegisterMap[c] && moduleInstanceRegisterMap[c].groupIsLoop)).join("/"); +} + +/** + * true when EVERY control-flow path from `startName` (next, T and F ports) ends in an unconditional exit (a Flow Control + * HALT or JMP) without ever reaching `targetName`. A path that just ends (no successor, no exit) is NOT such a path: the emitted + * code falls through into whatever follows (an if/else whose T branch has no HALT runs on into the F branch). A conditional with its T or F port unconnected is likewise a non-exit (the missing branch is not provably an exit). + */ +function flowExitsBefore(startName, targetName) { + const seen = new Set(); + const walk = (name) => { + if (name === targetName) return false; + const inst = allocatedInstanceNamed(name); + if (!inst) return false; + if (/\/flow_control_block$/.test(moduleInstanceRegisterMap[name].moduleName) && UNCONDITIONAL_EXIT_OPCODES.includes(String(inst.opCode).toUpperCase())) return true; + if (/\/conditional_block$/.test(moduleInstanceRegisterMap[name].moduleName) && (!(inst.T || []).length || !(inst.F || []).length)) return false; + if (seen.has(name)) return true; + seen.add(name); + const successors = [].concat(inst.next || [], inst.T || [], inst.F || []).map(c => c?.inst?.$name).filter(Boolean); + return successors.length > 0 && successors.every(walk); + }; + return walk(startName); +} + +/** + * is the emit order of two blocks the order they run in? Only then can "no reader between them" prove a lost value. + * Not provable when the blocks sit in different callable groups (hand-written main.asm calls the groups in an order the generator + * cannot see), or when the earlier block's every path ends in HALT/JMP before the later block (mutually exclusive branches). + * Straight line, and the same loop body, stay provable. + */ +function executionOrderProvable(earlyName, lateName) { + return callableGroupChain(earlyName) === callableGroupChain(lateName) && !flowExitsBefore(earlyName, lateName); +} + +/** + * the pin conflicts, each marked `harmful` when the overlap provably destroys a value unread: two writers ('w') of + * overlapping bytes with NO reader ('r'/'rm') of them between the two in emit order (or two fields of one block). The first + * writer's result is then dead before anything uses it, whether the registers were typed or left at their defaults. + * Everything else (a reader between the writers, i.e. the register is reused one value at a time) is not provably harmful. + * "between" needs (a) the emit order to BE the execution order (executionOrderProvable) and (b) a reader whose window + * overlaps the bytes the second writer destroys (the overlap of the two writers), not merely the first writer's window. + * Must be called after allocation (needs the emit order). + */ +function getPinnedConflictsClassified() { + const order = pinnedEmitOrder(); + return pinnedRegisterConflicts.map(c => { + let harmful = false; + if (c.first.role === "w" && c.second.role === "w") { + if (c.first.block === c.second.block) { + harmful = true; + } else if (order[c.first.block] !== undefined && order[c.second.block] !== undefined) { + const early = order[c.first.block] < order[c.second.block] ? c.first : c.second; + const late = early === c.first ? c.second : c.first; + const lost = { firstByte: Math.max(early.firstByte, late.firstByte), + bytes: Math.min(early.firstByte + early.bytes, late.firstByte + late.bytes) - Math.max(early.firstByte, late.firstByte) }; + harmful = executionOrderProvable(early.block, late.block) && + !pinnedRegisters.some(r => (r.role === "r" || r.role === "rm") && pinsOverlap(r, lost) && + order[r.block] !== undefined && order[r.block] > order[early.block] && order[r.block] < order[late.block]); + c = { first: early, second: late }; + } + } + return { first: c.first, second: c.second, harmful: harmful }; + }); +} + +/** + * the first register window of `bytes` bytes (whole registers) that no pin (typed or default, reserved or not) covers and + * the architectural windows (XFR2PSI/SUM32) do not take, for the "type another register" advice; null when there is none. + */ +function suggestFreeRegister(bytes) { + const regs = Math.max(1, Math.ceil(bytes / numOfBytesPerRegister)); + for (let r = 0; r + regs <= numOfPruRegisters; r++) { + const win = { firstByte: r * numOfBytesPerRegister, bytes: regs * numOfBytesPerRegister }; + let free = true; + for (let b = win.firstByte; b < win.firstByte + win.bytes; b++) if (poolAfterPins[b] !== 0) free = false; + if (free && !pinnedRegisters.some(p => pinsOverlap(p, win))) return "R" + r; + } + return null; +} + +/** + * A hand-off through a register (one block writes it, another transmits/publishes/shifts it) only works when the + * writer runs first. A reader emitted before EVERY overlapping writer with no loop around both reads the register before it holds + * the value. A reader that an earlier writer + * already feeds is fine (consume A, publish A, consume B, publish B on one register), whatever runs after it. In a loop it is the + * previous iteration's value: a one-iteration lag (the first iteration reads the uninitialised register, the last value is + * never consumed), so it is exempt from the warning. Must be called after allocation (needs the emit order). + */ +function getPinnedHandOffOrderWarnings() { + const order = pinnedEmitOrder(); + const inLoop = (name) => enclosingContainers(name).filter(c => moduleInstanceRegisterMap[c] && moduleInstanceRegisterMap[c].groupIsLoop); + const out = []; + for (const r of pinnedRegisters) { + if ((r.role !== "r" && r.role !== "rm") || order[r.block] === undefined) continue; + const writers = pinnedRegisters.filter(w => w.role === "w" && w.block !== r.block && pinsOverlap(w, r) && order[w.block] !== undefined); + if (writers.some(w => order[w.block] < order[r.block])) continue; + for (const w of writers) { + const loopsOfW = inLoop(w.block); + if (inLoop(r.block).some(c => loopsOfW.includes(c))) continue; + out.push({ writer: w, reader: r }); + } + } + return out; +} /** * Returns PRU registers allocation summary @@ -78,6 +361,34 @@ function getFreeByteOffset(numOfBytes) { return -1; } +/** + * a request the pool could not satisfy is recorded here and reported by validation as an ERROR. + * Before, pushInstruction returned -1 and every caller dropped it, so the block's instruction vanished from the asm + * and the build stayed clean (a 24-stage running sum emitted 22 of its 24 ADDs). + * @param {Object} instance - block that asked for registers + * @param {number} numOfBytes - size of the request + * @param {string} what - what the registers were for + */ +function recordAllocationFailure(instance, numOfBytes, what, contractFailure) { + // a block that failed is revisited by every consumer; report each (block, request) once + if (allocationFailures.some(f => f.block === instance.$name && f.what === what)) return; + let freeBytes = 0; + for (let i = 0; i < totalBytes; i++) if (pruByteArray[i] === 0) freeBytes++; + allocationFailures.push({ + block: instance.$name, + bytes: numOfBytes, + what: what, + contractFailure: contractFailure === true, + freeBytes: freeBytes, + pinnedBytes: pinnedByteCount, + scratchBytes: scratchRegisterBytes, + scratchRegisters: scratchRegistersUsed.slice(), + pinnedBy: pinnedRegisters.filter(p => p.reserved && p.firstByte < totalBytes).map(p => p.block + "." + p.field + "=" + p.token), + budgetRegisters: numOfPruRegisters, + budgetBytes: totalBytes + }); +} + /** * Converts byte offset to PRU register notation * @param {number} byteOffset - Byte offset in PRU memory @@ -129,6 +440,94 @@ function disallocatePruRegisters(byteOffset, numOfBytes) { } } +// a connection names the SOURCE PORT it leaves from (conn.port === "output2"), and every output port a block +// allocates has its own register (byteOffsetOfOutput / numOfBytesReqByOutput). Reading only output1 would make +// a consumer wired to output2..N silently read output1's register. All consumer-side lookups go through these helpers. +let allocationFailures = []; // register-pool exhaustion, one record per request that could not be placed; reported by validation +let unroutedConnections = []; // consumers wired to an output port that owns no register; reported by validation, never rerouted to output1 +let loopBoundaryViolations = []; // loop contents consumed outside loop; reported by validation + +/** Number of output ports a block instance exposes (output1..outputN arrays), at least 1. */ +function outputPortCount(inst) { + let n = 1; + while (n < 32 && inst["output" + (n + 1)] !== undefined) n++; + return n; +} + +/** Total consumer connections over every output port of a block instance. */ +function outputConnectionCount(inst) { + let total = 0; + for (let n = 1; n <= outputPortCount(inst); n++) total += (inst["output" + n] || []).length; + return total; +} + +/** Every connection leaving any output port of a block instance. */ +function allOutputConnections(inst) { + let conns = []; + for (let n = 1; n <= outputPortCount(inst); n++) conns = conns.concat(inst["output" + n] || []); + return conns; +} + +/** Output port number a connection leaves from ("output3" -> 3). A missing or malformed port name is a loud + * Error naming the bad value (never a silent fallback to output1, which would read the wrong register). */ +function sourcePortNumber(conn, owner) { + const raw = conn && conn.port; + const m = /^output(\d+)$/.exec(raw || ""); + if (!m) throw new Error("pru_register_allocator: invalid source output port '" + String(raw) + "'" + (owner ? " on " + owner : "") + "; expected 'outputN'"); + return parseInt(m[1], 10); +} + +/** Registers (byteOffset, numBytes) allocated to output port N of a mapped block, or null when that port owns none. */ +function outputPortAllocation(entry, port) { + const off = entry["byteOffsetOfOutput" + port]; + const bytes = entry["numOfBytesReqByOutput" + port]; + return (off === undefined || off === -1 || !(bytes > 0)) ? null : { byteOffset: off, numBytes: bytes }; +} + +/** Releases every output port's registers of a mapped block. */ +function disallocateAllOutputPorts(entry) { + for (let port = 1; entry["byteOffsetOfOutput" + port] !== undefined; port++) { + const alloc = outputPortAllocation(entry, port); + if (alloc) disallocatePruRegisters(alloc.byteOffset, alloc.numBytes); + } +} + +/** Every allocated output register of a mapped block, in port order. */ +function outputRegisterNotations(entry) { + const regs = []; + for (let port = 1; entry["byteOffsetOfOutput" + port] !== undefined; port++) { + const alloc = outputPortAllocation(entry, port); + const notation = alloc ? getPruRegister(alloc.byteOffset, alloc.numBytes) : -1; + if (notation !== -1) regs.push(notation); + } + return regs; +} + +/** + * Register token a consumer reads for input wired to the source port named by `conn`. + * Returns the source output port's own register (lo and hi registers for a port wider than 4 bytes); a missing/malformed port name or a port that owns no register is + * recorded in unroutedConnections (so validation names the block and port) and yields a token that cannot assemble, + * because silently substituting output1's register would assemble into wrong code. + */ +function sourceRegisterToken(consumer, inputPort, conn) { + const source = conn.inst; + let port; + try { + port = sourcePortNumber(conn, "'" + source.$name + "' -> '" + consumer.$name + "." + inputPort + "'"); + } catch (err) { + unroutedConnections.push({ consumer: consumer.$name, inputPort: inputPort, source: source.$name, sourcePort: String(conn && conn.port) }); + return "UNROUTED_" + source.$name + "_" + String(conn && conn.port); + } + const alloc = outputPortAllocation(moduleInstanceRegisterMap[source.$name], port); + if (!alloc) { + unroutedConnections.push({ consumer: consumer.$name, inputPort: inputPort, source: source.$name, sourcePort: "output" + port }); + return "UNROUTED_" + source.$name + "_output" + port; + } + // a 64-bit port is two consecutive registers (lo, hi), as for output1 + if (alloc.numBytes > 4) return getPruRegister(alloc.byteOffset, 4) + " , " + getPruRegister(alloc.byteOffset + 4, 4); + return getPruRegister(alloc.byteOffset, alloc.numBytes); +} + /** * Inverts PRU instruction opcode for conditional branching * @param {string} opCode - Original opcode @@ -238,6 +637,15 @@ function insertPreInitInstructions(insertIndex, preInitData) { function processLoopCounterGroup(instance) { let groupContents = JSON.parse(JSON.stringify(instance)).$groupContents; let maxCycles = 0; + // a preInit value is emitted in front of the loop and read on every pass, so it must own its register before + // any body block is allocated. Reached lazily (through its first reader) it could be handed a register an earlier body + // temporary had just released, and that temporary's next-pass load would overwrite it. Allocate them all first. + for (const name of groupContents) { + const mapped = (instance.preInitBlocks || []).includes(name) && moduleInstanceRegisterMap[name]; + if (!mapped || !('blockOutputCalculated' in mapped) || mapped.blockOutputCalculated === 1 || !('output1ArrayLength' in mapped)) continue; + pushInstruction(systemModules[mapped.moduleName]?.$instances[mapped.instanceNum], null); + if (mapped.output1ArrayLength !== -1) mapped.output1ArrayLength++; // that call was not a consumer read + } // First pass: Process program control block (without output1 port or T and F ports and next ports) for (const instanceName of groupContents) { const { moduleName, instanceNum } = moduleInstanceRegisterMap[instanceName]; @@ -285,7 +693,7 @@ function processLoopCounterGroup(instance) { // skip blocks which are in group blocks (eg: loop block) if (moduleInstanceRegisterMap[instanceName]?.output1ArrayLength === -1) { const { moduleName, instanceNum } = moduleInstanceRegisterMap[instanceName]; - const instance = systemModules[moduleName]?.$instances[instanceNum]; + const instance = systemModules[moduleName]?.$instances[instanceNum]; // Process the instruction chain starting from this result node pushInstruction(instance, null); // Mark as calculated @@ -300,7 +708,27 @@ function processLoopCounterGroup(instance) { maxCycles = Math.max(maxCycles, moduleInstanceRegisterMap[instanceName].peakCycles); } } - + + // exported blocks are the loop body's results for the outside world. A body whose only data + // sink sits outside the loop (a post-loop store reading an exported accumulator) has no in-loop result + // node, so without this pass the update block would be emitted after the loop instead of inside it. + // Runs last on purpose: anything reachable from an in-loop sink keeps its existing emission order, and + // pushInstruction skips whatever is already calculated. + for (const instanceName of (instance.exportedBlocks || [])) { + const entry = moduleInstanceRegisterMap[instanceName]; + if (!entry || entry.blockOutputCalculated === 1 || entry.conditionCalculated === 1) continue; + const { moduleName, instanceNum } = entry; + const childInstance = systemModules[moduleName]?.$instances[instanceNum]; + if (!childInstance) continue; + pushInstruction(childInstance, null); + if ("conditionCalculated" in moduleInstanceRegisterMap[instanceName]) { + moduleInstanceRegisterMap[instanceName].conditionCalculated = 1; + } else if ("blockOutputCalculated" in moduleInstanceRegisterMap[instanceName]) { + moduleInstanceRegisterMap[instanceName].blockOutputCalculated = 1; + } + maxCycles = Math.max(maxCycles, moduleInstanceRegisterMap[instanceName].peakCycles); + } + return maxCycles; } function processGroupBlock(instance){ @@ -312,6 +740,7 @@ function processGroupBlock(instance){ // Allocate return address register on first group block (2 bytes for word address) if (groupReturnAddrByteOffset === -1) { groupReturnAddrByteOffset = getFreeByteOffset(2); + if (groupReturnAddrByteOffset === -1) recordAllocationFailure(instance, 2, "the group return address"); } let label = `${groupName}_start`; @@ -394,10 +823,8 @@ function collectBranchChain(instance) { if (nextInst) queue.push(nextInst); // Forward: data flow to consumers - if (inst.output1) { - for (const conn of inst.output1) { - if (conn?.inst) queue.push(conn.inst); - } + for (const conn of allOutputConnections(inst)) { + if (conn?.inst) queue.push(conn.inst); } } @@ -456,6 +883,226 @@ function collectInputPredecessors(instance) { return collectInputDependencies(chain); } +/** + * may the registers of this block be released now? + * Only the exact counter decides. output1ArrayLength is decremented on EVERY pushInstruction visit - including the + * control-flow `prev` walk - so it is over-decremented and goes negative in a single-consumer chain, which made + * `A_k = A_(k-1) + C_k` never release (a 24-stage running sum exhausted the pool). dataConsumersRemaining is decremented + * once per data connection in the consumer's input loop, so 0 means every data consumer has read the value. + * Callers release only AFTER the consumer's destination is allocated (see releaseConsumedInputs): a value released + * earlier would be handed to the consumer as its own destination, and a macro that writes before it has finished + * reading its input (Lookup Table reads R0.b2 and writes R0.b3) would corrupt itself. + */ +function releaseSafe(entry) { + return entry.dataConsumersRemaining === 0; +} + +/** + * blocks whose whole instruction is ONE native PRU ALU instruction. The hardware reads every source before it + * writes the destination, so such a block may reuse the register of an input it is the last consumer of + * (`LSR R0, R0, R1.b0`), exactly as before. Every other block - every `m_*` macro and anything not listed - may write + * its result before it has finished reading its inputs (Lookup Table reads R0.b2 and writes R0.b3), so it must not be + * given a register released by its own inputs. The default is the safe side: an unknown opCode releases AFTER allocation. + */ +const IN_PLACE_SAFE_OPCODES = ["ADD", "ADC", "SUB", "SUC", "AND", "OR", "XOR", "NOT", "LSL", "LSR"]; + +function readsAllInputsBeforeWriting(instance) { + return typeof instance.opCode === "string" && IN_PLACE_SAFE_OPCODES.includes(instance.opCode.toUpperCase()); +} + +/** Names of every loop/group block that contains `name`, innermost first. */ +function enclosingContainers(name) { + const chain = []; + for (let c = containerOf[name]; c !== undefined && !chain.includes(c); c = containerOf[c]) chain.push(c); + return chain; +} + +/** checks if consumer reads producer from outside producer's loop/group */ +function checkBoundary(producerName, consumerName, portName) { + if (!producerName || !consumerName) return; + const pContainers = enclosingContainers(producerName); + const cContainers = enclosingContainers(consumerName); + for (const c of pContainers) { + if (!cContainers.includes(c)) { + loopBoundaryViolations.push({ + consumer: consumerName, + producer: producerName, + container: c, + port: portName + }); + } + } +} + +/** + * the loop body is emitted ONCE but runs loopCount times, so a value produced OUTSIDE a loop and read by a block + * INSIDE it is read again on every iteration. Releasing it at its single textual consumer let the next block in the body + * take the register, and iteration 2 read the clobbered byte. Returns the outermost container of `consumer` that does not + * also contain `producer` (null when both sit in the same containers, i.e. the release is safe where it stands). + */ +function outermostForeignContainer(producerName, consumerName) { + // a preInit block is listed in the loop's $groupContents but its instructions are moved in front of the loop + // (extractPreInitInstructions), so for release purposes it is OUTSIDE that loop: it runs once and every iteration reads it. + const hoisted = hoistedOutOf[producerName] || []; + const producerChain = enclosingContainers(producerName).filter(c => !hoisted.includes(c)); + const foreign = enclosingContainers(consumerName).filter(c => !producerChain.includes(c)); + return foreign.length === 0 ? null : foreign[foreign.length - 1]; +} + +/** Releases the registers of every input of `instance` whose last data consumer has now been allocated. */ +function releaseConsumedInputs(instance) { + for (let iterator1 = 1; iterator1 <= instance.numOfInputPorts; iterator1++) { + const inputInstanceName = instance["input" + iterator1]?.[0]?.inst?.$name; + const entry = inputInstanceName && moduleInstanceRegisterMap[inputInstanceName]; + // registersReleased: a second release would zero bytes a later block (possibly this one) has since been given. + if (entry && !entry.registersReleased && releaseSafe(entry)) { + // an accumulator that took over a preInit register already waits for the end of that loop + if (entry.holdUntilLoopEnd && enclosingContainers(instance.$name).includes(entry.holdUntilLoopEnd)) continue; + const container = outermostForeignContainer(inputInstanceName, instance.$name); + if (container === null) { + disallocateAllOutputPorts(entry); + entry.registersReleased = true; + } else if (moduleInstanceRegisterMap[container]?.groupIsLoop) { + if (entry.dataConsumersTotal === 1 && readsAllInputsBeforeWriting(instance) && (hoistedOutOf[inputInstanceName] || []).includes(container)) { + // incremental_spi: `ADC R0.b1, R0.b1, R0.b2` accumulates in the preInit register. Only when the ALU is the value's + // SOLE consumer (any other reader would see the accumulator on iteration 2). Released now so the + // ALU may take it over; settleHoistedInPlace() decides, once the destination is known, whether it did. + disallocateAllOutputPorts(entry); entry.registersReleased = true; + (hoistedInPlace[instance.$name] = hoistedInPlace[instance.$name] || []).push({ entry, container }); + } else { + // Live for the whole loop: released once the loop body has been emitted (releaseDeferredForLoop). + (deferredLoopReleases[container] = deferredLoopReleases[container] || []).push(entry); + } + } + // A callable group can run from any call site, so a value it reads from outside is never released. + } + } +} + +/** Byte indexes of every output register of a mapped block. */ +function outputBytes(entry) { + const bytes = []; + for (let port = 1; entry["byteOffsetOfOutput" + port] !== undefined; port++) { + const alloc = outputPortAllocation(entry, port); + if (alloc) for (let i = alloc.byteOffset; i < alloc.byteOffset + alloc.numBytes; i++) bytes.push(i); + } + return bytes; +} + +/** + * after an ALU block released a preInit input to accumulate in it, check the destination really landed on that + * register. If so the result is the loop's accumulator and nothing else in the body may take the register until the loop + * has ended (a later constant would clobber it on iteration 2). If not, the preInit value was never overwritten, so it is + * pinned again exactly as for any other consumer: a later block must not be given its register. + */ +function settleHoistedInPlace(instance) { + const pending = hoistedInPlace[instance.$name]; + if (!pending) return; + delete hoistedInPlace[instance.$name]; + const own = moduleInstanceRegisterMap[instance.$name]; + const ownBytes = own ? outputBytes(own) : []; + for (const { entry, container } of pending) { + const list = (deferredLoopReleases[container] = deferredLoopReleases[container] || []); + if (outputBytes(entry).some(b => ownBytes.includes(b))) { + own.holdUntilLoopEnd = container; + list.push(own); + } else { + for (const b of outputBytes(entry)) pruByteArray[b] = 1; + entry.registersReleased = false; + list.push(entry); + } + } +} + +/** [{preInit, update}] from a loop's accumulators (an array, or a JSON string of one). + * Malformed entries are ignored here; loop_block.validate() rejects them. */ +function parseAccumulatorBindings(value) { + let list = value; + if (list === undefined || list === null) return []; + if (typeof list === "string") { + if (list.trim() === "") return []; + try { + list = JSON.parse(list); + } catch (e) { + return []; + } + } + if (!Array.isArray(list)) return []; + const out = []; + for (const entry of list) { + if (entry && typeof entry.preInit === "string" && typeof entry.update === "string") { + out.push({ preInit: entry.preInit, update: entry.update }); + } + } + return out; +} + +/** + * explicit accumulator binding (loop_block.accumulators). The update block's destination IS the preInit + * block's register, so the update reads and writes the same bytes on every iteration (zero extra cycles), instead + * of the allocator handing it a fresh register that recomputes preInit + x each pass. Anything less than an exact, + * already-released, same-size preInit register is a loud allocation failure, never a silent fallback to 0 + x. + */ +function coalesceAccumulator(instance, binding, numBytes) { + const fail = (why) => { + recordAllocationFailure(instance, numBytes, "its accumulator register (accumulators: '" + + binding.preInit + "' -> '" + instance.$name + "'): " + why, true); + return -1; + }; + const pre = moduleInstanceRegisterMap[binding.preInit]; + if (!pre) return fail("preInit block '" + binding.preInit + "' is not allocated"); + const alloc = outputPortAllocation(pre, 1); + if (!alloc) return fail("preInit block '" + binding.preInit + "' owns no output register"); + if (alloc.numBytes !== numBytes) { + return fail("preInit register is " + alloc.numBytes + " byte(s) but the update needs " + numBytes); + } + for (let b = alloc.byteOffset; b < alloc.byteOffset + alloc.numBytes; b++) { + if (pruByteArray[b] !== 0) { + return fail("preInit register byte " + b + " is still live (the preInit value has another consumer, so '" + + instance.$name + "' cannot take it over)"); + } + } + for (let b = alloc.byteOffset; b < alloc.byteOffset + alloc.numBytes; b++) pruByteArray[b] = 1; + return alloc.byteOffset; +} + +/** + * an ALU result is the loop's accumulator only in the incremental_spi shape: its LOW byte is the first byte of the + * preInit value it took over, and it covers no byte of any other released preInit value. Any other overlap (a carry byte + * landing on another preInit register, or a result starting inside one) would overwrite that value on every pass, so every + * released preInit value is pinned again and the result is placed elsewhere. Returns the (possibly re-chosen) offset. + */ +function repinUnlessAccumulatorShape(instance, byteOffset, numBytes) { + const pending = hoistedInPlace[instance.$name]; + if (!pending || byteOffset === -1) return byteOffset; + const result = []; + for (let b = byteOffset; b < byteOffset + numBytes; b++) result.push(b); + const accumulates = pending.some(({ entry }) => { + const own = outputBytes(entry); + if (own.length === 0 || result[0] !== Math.min(...own)) return false; + return pending.every(other => other.entry === entry || !outputBytes(other.entry).some(b => result.includes(b))); + }); + if (accumulates || !pending.some(({ entry }) => outputBytes(entry).some(b => result.includes(b)))) return byteOffset; + for (const b of result) pruByteArray[b] = 0; + for (const { entry, container } of pending) { + for (const b of outputBytes(entry)) pruByteArray[b] = 1; + entry.registersReleased = false; + (deferredLoopReleases[container] = deferredLoopReleases[container] || []).push(entry); + } + delete hoistedInPlace[instance.$name]; + return getFreeByteOffset(numBytes); +} + +/** Releases the values a loop body only read, now that the loop has been emitted and none of them can be read again. */ +function releaseDeferredForLoop(loopName) { + for (const entry of deferredLoopReleases[loopName] || []) { + if (entry.registersReleased || !releaseSafe(entry)) continue; + disallocateAllOutputPorts(entry); + entry.registersReleased = true; + } + deferredLoopReleases[loopName] = []; +} + /** * Gets PRU Instructions from blocks connected in the system * @param {Object} instance - The current block instance @@ -488,6 +1135,7 @@ function pushInstruction(instance, parentInstance) { let label; if (instance?.["prev"] && instance?.["prev"][0]?.["inst"]) { const prevPortInstanceName = instance["prev"][0]?.["inst"]?.$name; + checkBoundary(prevPortInstanceName, instanceName, "prev"); // Process the previous block label = pushInstruction(instance["prev"][0]["inst"], instance); moduleInstanceRegisterMap[instanceName].connectedNodesInPeakCyclePath = @@ -533,12 +1181,31 @@ function pushInstruction(instance, parentInstance) { // Process each input port for (let iterator1 = 1; iterator1 <= instance.numOfInputPorts; iterator1++) { - const inputInstance = instance[inputPort + iterator1]?.[0]?.inst; + const inputConn = instance[inputPort + iterator1]?.[0]; + const inputInstance = inputConn?.inst; const inputInstanceName = inputInstance?.$name; if (inputInstanceName) { + checkBoundary(inputInstanceName, instanceName, inputPort + iterator1); // Recursively process connected input - const noOfBytesUsedByInput = pushInstruction(inputInstance, instance); + let noOfBytesUsedByInput = pushInstruction(inputInstance, instance); + // this connection is one data read of the source, counted exactly once, here. + if (moduleInstanceRegisterMap[inputInstanceName].dataConsumersRemaining > 0) { + moduleInstanceRegisterMap[inputInstanceName].dataConsumersRemaining--; + } + // the width that matters is the source PORT's, not output1's. + // a missing/malformed source port leaves sourcePort null (never silent output1); the token + // lookup below records it in unroutedConnections so validation names the block and port. + let sourcePort = null; + try { + sourcePort = sourcePortNumber(inputConn, "'" + inputInstanceName + "' -> '" + instanceName + ".input" + iterator1 + "'"); + } catch (err) { + // Recorded once by sourceRegisterToken below; keep null so the output1 fast path is never taken + // for a connection whose source port name SysConfig did not provide. + } + if (sourcePort !== null && sourcePort !== 1) { + noOfBytesUsedByInput = moduleInstanceRegisterMap[inputInstanceName]["numOfBytesReqByOutput" + sourcePort] || 0; + } moduleInstanceRegisterMap[instanceName].connectedNodesInPeakCyclePath = new Set([ ...moduleInstanceRegisterMap[instanceName].connectedNodesInPeakCyclePath, @@ -548,17 +1215,25 @@ function pushInstruction(instance, parentInstance) { // Update max bytes used maxBytesUsed = noOfBytesUsedByInput > maxBytesUsed ? noOfBytesUsedByInput : maxBytesUsed; - // Get register notation for this input's output - const _srcByteOffset = moduleInstanceRegisterMap[inputInstanceName].byteOffsetOfOutput1; - const _srcNumBytes = moduleInstanceRegisterMap[inputInstanceName].numOfBytesReqByOutput1; - if (_srcNumBytes > 4) { - // 64-bit source: emit two consecutive registers (lo, hi) - const _loReg = getPruRegister(_srcByteOffset, 4); - const _hiReg = getPruRegister(_srcByteOffset + 4, 4); - inputValues = inputValues + _loReg + " , " + _hiReg + " , "; + if (sourcePort === 1) { + // Get register notation for this input's output + const _srcByteOffset = moduleInstanceRegisterMap[inputInstanceName].byteOffsetOfOutput1; + const _srcNumBytes = moduleInstanceRegisterMap[inputInstanceName].numOfBytesReqByOutput1; + if (_srcNumBytes > 4) { + // 64-bit source: emit two consecutive registers (lo, hi) + const _loReg = getPruRegister(_srcByteOffset, 4); + const _hiReg = getPruRegister(_srcByteOffset + 4, 4); + inputValues = inputValues + _loReg + " , " + _hiReg + " , "; + } else { + inputValues = inputValues + getPruRegister(_srcByteOffset, _srcNumBytes) + " , "; + } } else { - inputValues = inputValues + getPruRegister(_srcByteOffset, _srcNumBytes) + " , "; + // Register notation for the output PORT this input is wired to (output2 and up) + inputValues = inputValues + sourceRegisterToken(instance, inputPort + iterator1, inputConn) + " , "; } + } else { + // Unconnected input: provide dummy register to preserve macro argument positions + inputValues = inputValues + "R0 , "; } } @@ -586,21 +1261,12 @@ function pushInstruction(instance, parentInstance) { inputValues = inputValues + instance["constant" + iterator1] + " , "; } - // Disallocate registers for inputInstance ports that are no longer needed - for (let iterator1 = 1; iterator1 <= instance.numOfInputPorts; iterator1++) { - const inputInstance = instance[inputPort + iterator1]?.[0]?.inst; - const inputInstanceName = inputInstance?.$name; - if (inputInstanceName) { - // Skip disallocation for inputs that are still needed by other blocks - if (moduleInstanceRegisterMap[inputInstanceName].output1ArrayLength === 0) { - disallocatePruRegisters( - moduleInstanceRegisterMap[inputInstanceName].byteOffsetOfOutput1, - moduleInstanceRegisterMap[inputInstanceName].numOfBytesReqByOutput1 - ); - } - } + // a single native ALU instruction reads all sources before it writes, so it may take over the register of an + // input it was the last consumer of; every other block releases only after its destination is allocated (below). + if (readsAllInputsBeforeWriting(instance)) { + releaseConsumedInputs(instance); } - + let instruction; // Process loop blocks if (instance.$groupContents) { @@ -612,6 +1278,7 @@ function pushInstruction(instance, parentInstance) { //push instruction to init loop count const byteOffset = getFreeByteOffset(instance.loopCountRegSize); + if (byteOffset === -1) recordAllocationFailure(instance, instance.loopCountRegSize, "its loop counter"); // Track loop counter register allocation loopCounterRegisters[instanceName] = { byteOffset: byteOffset, @@ -625,6 +1292,7 @@ function pushInstruction(instance, parentInstance) { addToPruRegisterAllocationSummary(label, instruction, instance, instanceName, 0); //push all the blocks inside loop block let loopBlockCycleBudget = processLoopCounterGroup(instance); + releaseDeferredForLoop(instanceName); // Handle pre-init blocks: extract their instructions and move before loop counter // Note: preInitBlocks is an array of block name strings (from multi-select options) @@ -654,6 +1322,7 @@ function pushInstruction(instance, parentInstance) { addToPruRegisterAllocationSummary(label, instruction, instance, instanceName, 0); //push all the blocks inside loop block let loopBlockCycleBudget = processLoopCounterGroup(instance); + releaseDeferredForLoop(instanceName); // Handle pre-init blocks: extract their instructions and move before startloop label // Note: preInitBlocks is an array of block name strings (from multi-select options) @@ -716,17 +1385,31 @@ function pushInstruction(instance, parentInstance) { } else { - //create Instruction - instruction = `${instance.opCode} ${outputRegister}, ${inputValues}`; + if (inputValues === "") { + instruction = `${instance.opCode} ${outputRegister}`; + } else { + instruction = `${instance.opCode} ${outputRegister}, ${inputValues}`; + } } } // Generate PRU instruction for regular blocks else { // Allocate registers for output - const byteOffset = getFreeByteOffset(maxBytesUsed); - + // an explicitly bound accumulator reuses its preInit register instead of taking a fresh + // one. repinUnlessAccumulatorShape still runs below: the coalesced bytes trivially satisfy its + // accumulator shape, so the preInit settle logic keeps working unchanged. + let byteOffset; + if (accumulatorBindings[instanceName] !== undefined) { + byteOffset = coalesceAccumulator(instance, accumulatorBindings[instanceName], maxBytesUsed); + if (byteOffset === -1) return -1; // specific failure already recorded (validation reports it) + } else { + byteOffset = getFreeByteOffset(maxBytesUsed); + } + byteOffset = repinUnlessAccumulatorShape(instance, byteOffset, maxBytesUsed); + if (byteOffset === -1) { - return -1; // Failed to allocate registers + recordAllocationFailure(instance, maxBytesUsed, "its result"); + return -1; // Failed to allocate registers (validation reports it as an error) } // Update register map with allocation info @@ -743,15 +1426,44 @@ function pushInstruction(instance, parentInstance) { } else { outputRegister = getPruRegister(byteOffset, maxBytesUsed); } + // Blocks with a multi-result macro need distinct allocator-owned destinations + // before their input and constant arguments. + const macroOutputRegisters = [outputRegister]; + if (instance.multiOutputMacroArgs) { + for (let output = 2; output <= instance.numOfOutputPorts; output++) { + const width = parseInt(instance["output" + output + "Size"] || maxBytesUsed); + const extraOffset = getFreeByteOffset(width); + if (extraOffset === -1) { + recordAllocationFailure(instance, width, "result output" + output); + return -1; + } + moduleInstanceRegisterMap[instanceName]["byteOffsetOfOutput" + output] = extraOffset; + moduleInstanceRegisterMap[instanceName]["numOfBytesReqByOutput" + output] = width; + macroOutputRegisters.push(width > 4 + ? getPruRegister(extraOffset, 4) + ", " + getPruRegister(extraOffset + 4, 4) + : getPruRegister(extraOffset, width)); + } + } // Clean up input values string (remove trailing comma and space) if (inputValues.endsWith(" , ")) { inputValues = inputValues.slice(0, -3); } //create Instruction - instruction = `${instance.opCode} ${outputRegister}, ${inputValues}`; + const instMod = systemModules[moduleInstanceRegisterMap[instanceName].moduleName]; + if (typeof instMod?.getInstruction === "function") { + instruction = instMod.getInstruction(instance, outputRegister); + } else if (inputValues === "") { + instruction = `${instance.opCode} ${macroOutputRegisters.join(", ")}`; + } else { + instruction = `${instance.opCode} ${macroOutputRegisters.join(", ")}, ${inputValues}`; + } } } + // the destination (if any) is allocated now, so inputs whose last data consumer this was can be released + // (a no-op for the ALU blocks that already released them above). + releaseConsumedInputs(instance); + settleHoistedInPlace(instance); if("conditionCalculated" in moduleInstanceRegisterMap[instanceName]) { moduleInstanceRegisterMap[instanceName].conditionCalculated = 1; @@ -790,9 +1502,36 @@ function allocatePruRegisters() { }; moduleInstanceRegisterMap = {}; + unroutedConnections = []; + allocationFailures = []; + loopBoundaryViolations = []; pruByteArray = new Array(totalBytes).fill(0); + + // user-typed registers the blocks emit verbatim leave the pool before anything is allocated + reserveMacroScratchRegisters(); + reservePinnedRegisters(); loopCounterRegisters = {}; // Reset loop counter tracking groupReturnAddrByteOffset = -1; // Reset group return address register + containerOf = {}; + deferredLoopReleases = {}; + hoistedOutOf = {}; + hoistedInPlace = {}; + accumulatorBindings = {}; + for (const moduleName in systemModules) { + for (const instance of systemModules[moduleName]?.$instances || []) { + if (instance.$name && instance.$groupContents) { + // $groupContents holds instance objects; the JSON clone turns them into names, as everywhere else here. + for (const child of JSON.parse(JSON.stringify(instance)).$groupContents) containerOf[child] = instance.$name; + for (const pre of JSON.parse(JSON.stringify(instance)).preInitBlocks || []) (hoistedOutOf[pre] = hoistedOutOf[pre] || []).push(instance.$name); + // explicit accumulator bindings live on loops (infiniteLoop marks a loop container). + if ('infiniteLoop' in instance) { + for (const b of parseAccumulatorBindings(instance.accumulators)) { + accumulatorBindings[b.update] = { preInit: b.preInit, container: instance.$name }; + } + } + } + } + } let loopBlockInstanceNames = []; let groupBlockInstanceNames = []; groupMetadata = []; @@ -824,12 +1563,17 @@ function allocatePruRegisters() { // Handle blocks with output1 port if (instance.output1) { - // TODO: Currently assuming each block has only one output port - const outputArrayLength = instance.output1.length === 0 ? -1 : instance.output1.length; + // output1ArrayLength counts the consumer connections of EVERY output port (the name is historical): + // a block whose only consumer hangs off output2 is not a result node, and its registers are released after the last one. + const outputConnections = outputConnectionCount(instance); + const outputArrayLength = outputConnections === 0 ? -1 : outputConnections; moduleInstanceRegisterMap[instanceName] = { "blockOutputCalculated": 0, "output1ArrayLength": outputArrayLength, + // exact count of data consumers that have not read this block yet (see releaseSafe) + "dataConsumersRemaining": outputConnections, + "dataConsumersTotal": outputConnections, "byteOffsetOfOutput1": -1, "numOfBytesReqByOutput1": 0, "peakCycles": 0, @@ -869,6 +1613,7 @@ function allocatePruRegisters() { if(instance.$groupContents && ('infiniteLoop' in instance)){ let groupContents = JSON.parse(JSON.stringify(instance)).$groupContents loopBlockInstanceNames = loopBlockInstanceNames.concat(groupContents); + moduleInstanceRegisterMap[instanceName].groupIsLoop = true; } } //Handle program end block instances(without output1 port or T and F ports or next ports) @@ -889,6 +1634,7 @@ function allocatePruRegisters() { if(instance.$groupContents && ('infiniteLoop' in instance)){ let groupContents = JSON.parse(JSON.stringify(instance)).$groupContents loopBlockInstanceNames = loopBlockInstanceNames.concat(groupContents); + moduleInstanceRegisterMap[instanceName].groupIsLoop = true; } if(instance.$groupContents && !('infiniteLoop' in instance)){ let groupContents = JSON.parse(JSON.stringify(instance)).$groupContents @@ -899,6 +1645,17 @@ function allocatePruRegisters() { instanceName : instanceName, groupContents : groupContents }); + + // A group listed inside a loop's $groupContents is emitted outside the loop + // and never called. We reject this. + if (containerOf[instanceName]) { + loopBoundaryViolations.push({ + consumer: "generator", + producer: instanceName, + container: containerOf[instanceName], + port: "$groupContents" + }); + } } } } @@ -1007,10 +1764,11 @@ function getRegisterAllocationComments() { if (entry.byteOffsetOfOutput1 !== -1 && entry.numOfBytesReqByOutput1 > 0 && entry.conditionCalculated === undefined) { - const regNotation = getPruRegister(entry.byteOffsetOfOutput1, entry.numOfBytesReqByOutput1); - if (regNotation !== -1) { + // every output port that owns a register is listed, not just output1 + const regNotations = outputRegisterNotations(entry); + if (regNotations.length > 0) { allocationMap[instanceName] = { - registers: [regNotation], + registers: regNotations, peakCycles: entry.peakCycles }; } @@ -1086,12 +1844,26 @@ function getRegisterAllocationComments() { } } + // the CTPPR0 write runs once at sysconfig_generated_start and on every group + // CALL, ahead of the first block, so it adds to the worst case of either path. The + // per-block peakCycles in the allocation table do not include it. + const staticModule = systemModules["/pru_blocks/common/pru_blocks_static_module"]; + const ctSetupCycles = (staticModule && typeof staticModule.getConstantTableSetupCycles === "function") + ? staticModule.getConstantTableSetupCycles() : 0; + // getConstantTableSetupCycles() also counts the SPI CS idle `set r30` per pin; split it so the CTPPR0 wording stays exact. + const csIdleCycles = (staticModule && typeof staticModule.getSpiCsIdleSetupCycles === "function") + ? staticModule.getSpiCsIdleSetupCycles() : 0; + const ctppr0Cycles = ctSetupCycles - csIdleCycles; + overallPeakCycles += ctSetupCycles; + // 6. Add reserved registers and overall peak cycles summary if (sortedNames.length > 0) { comments.push(";"); } comments.push("; TEMP_REG1 (R28) -> Used by all macros"); comments.push("; TEMP_REG2 (R29) -> Used by all macros"); + if (scratchRegistersUsed.includes("TEMP_REG3")) comments.push("; TEMP_REG3 (R26) -> Used by some macros; not allocated to values"); + if (scratchRegistersUsed.includes("TEMP_REG4")) comments.push("; TEMP_REG4 (R27) -> Used by some macros; not allocated to values"); // Add overall peak cycles summary comments.push(";"); @@ -1099,7 +1871,7 @@ function getRegisterAllocationComments() { if (hasInfiniteLoop) { comments.push("; OVERALL PEAK CYCLES: Unknown - Infinite loop detected in configuration"); } else { - comments.push(`; OVERALL PEAK CYCLES: ${overallPeakCycles + 1} cycles (worst-case timing) including group and non-group code`); + comments.push(`; OVERALL PEAK CYCLES: ${overallPeakCycles + 1} cycles (worst-case timing) including group and non-group code${ctSetupCycles ? ` (includes ${[ctppr0Cycles ? `${ctppr0Cycles} cycles of CTPPR0 setup for c28` : "", csIdleCycles ? `${csIdleCycles} cycles of SPI CS idle-level setup` : ""].filter(Boolean).join(" and ")}, repeated on every group call; per-block cycle counts exclude it)` : ""}`); } comments.push(";--------------------------------------------------------------------------------------"); @@ -1125,10 +1897,7 @@ function getBlockRegisterAllocation(instanceName) { // Check for output register allocation if (entry.byteOffsetOfOutput1 !== -1 && entry.numOfBytesReqByOutput1 > 0) { - const regNotation = getPruRegister(entry.byteOffsetOfOutput1, entry.numOfBytesReqByOutput1); - if (regNotation !== -1) { - registers.push(regNotation); - } + registers.push(...outputRegisterNotations(entry)); } // Check for loop counter register @@ -1156,5 +1925,15 @@ exports = { allocatePruRegisters, getGroupMetadata: () => groupMetadata, getRegisterAllocationComments, - getBlockRegisterAllocation -} \ No newline at end of file + getBlockRegisterAllocation, + sourcePortNumber, + getUnroutedConnections: () => unroutedConnections, + getAllocationFailures: () => allocationFailures, + getScratchRegisters: () => scratchRegistersUsed, + getPinnedRegisters: () => pinnedRegisters, + getPinnedRegisterConflicts: getPinnedConflictsClassified, + suggestFreeRegister, + getPinnedRegisterProblems: () => pinnedProblems, + getPinnedHandOffOrderWarnings, + getLoopBoundaryViolations: () => loopBoundaryViolations +} diff --git a/.metadata/sysconfig/.meta/pru_blocks/data_handling/arithmetic_block.syscfg.js b/.metadata/sysconfig/.meta/pru_blocks/data_handling/arithmetic_block.syscfg.js index bbcbf94..8bf7d1e 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/data_handling/arithmetic_block.syscfg.js +++ b/.metadata/sysconfig/.meta/pru_blocks/data_handling/arithmetic_block.syscfg.js @@ -270,4 +270,4 @@ exports = { }, }, getAIContext -} \ No newline at end of file +} diff --git a/.metadata/sysconfig/.meta/pru_blocks/data_handling/bitwise_block.syscfg.js b/.metadata/sysconfig/.meta/pru_blocks/data_handling/bitwise_block.syscfg.js index dede553..9319fc1 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/data_handling/bitwise_block.syscfg.js +++ b/.metadata/sysconfig/.meta/pru_blocks/data_handling/bitwise_block.syscfg.js @@ -327,4 +327,4 @@ exports = { }, }, getAIContext -} \ No newline at end of file +} diff --git a/.metadata/sysconfig/.meta/pru_blocks/data_handling/load_constant_block.syscfg.js b/.metadata/sysconfig/.meta/pru_blocks/data_handling/load_constant_block.syscfg.js index bae4c7c..3645de4 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/data_handling/load_constant_block.syscfg.js +++ b/.metadata/sysconfig/.meta/pru_blocks/data_handling/load_constant_block.syscfg.js @@ -5,7 +5,13 @@ function validate(inst, report) { { report.logWarning("input"+iterator.toString()+" port not connected to output port",inst) } - } + } + // an explicit output width must be able to hold the constant + const width = inst["outputWidth"]; + if(width !== "auto" && getNumOfBytes(inst["constant1"]) > parseInt(width)) + { + report.logError("Constant " + inst["constant1"] + " needs " + getNumOfBytes(inst["constant1"]) + " byte(s) but Output Width is " + width + " byte(s). Choose a wider Output Width or Auto.", inst, "outputWidth"); + } } function getNumOfBytes(value) { @@ -44,6 +50,7 @@ const ldi1 = load_constant_block.addInstance(); | Parameter | Type | Valid Values | Default | Description | |-----------|------|--------------|---------|-------------| | constant1 | Integer | 0 to 0xFFFFFFFF | 0 | Constant value to load | +| outputWidth | String | "auto", "1", "2", "4" | "auto" | Register bytes reserved for the output (auto sizes from the value) | ### Example Configurations @@ -71,6 +78,13 @@ ldi1.$name = "Bit_Mask"; ldi1.constant1 = 0xFF00FF00; // Alternating byte mask \\\`\\\`\\\` +**Load a small value into its own full 32-bit register (never packed with another constant):** +\\\`\\\`\\\`javascript +ldi1.$name = "Coeff_0"; +ldi1.constant1 = 0x1000; +ldi1.outputWidth = "4"; // default "auto" sizes the register from the value +\\\`\\\`\\\` + **Load zero:** \\\`\\\`\\\`javascript ldi1.$name = "Zero_Value"; @@ -99,7 +113,9 @@ scripting.connect(ldi1, "next", next_block, "prev"); - 2 bytes for values 256-65535 - 4 bytes for values > 65535 -4. **Hexadecimal**: Values can be specified in hex (0x prefix) or decimal. +4. **Output Width**: outputWidth = "auto" (default) sizes the register from the value as above. "1", "2" or "4" reserves exactly that many bytes, so a small constant set to "4" gets a whole 32-bit register and is never packed with another constant. A width smaller than the value needs is a SysConfig error. The instruction is still LDI for values <= 0xFFFF (it zero-extends into the full register) and LDI32 above. + +5. **Hexadecimal**: Values can be specified in hex (0x prefix) or decimal. `; } @@ -119,6 +135,7 @@ Loads an immediate constant value into a register. This is typically the startin - **Constant Value**: Enter any value from 0 to 4,294,967,295 (0xFFFFFFFF) - Can be entered in decimal or hexadecimal format - Block automatically determines optimal instruction +- **Output Width**: Auto (default), 1, 2 or 4 bytes. Auto sizes the register from the value; 4 reserves a full 32-bit register even for a small value ### Technical Details (Additional Information) **Generated Assembly** (auto-selected based on value size): @@ -200,6 +217,18 @@ exports = { } } }, + { + name: "outputWidth", + displayName: "Output Width", + description: "Register bytes reserved for the output. Auto sizes from the constant value.", + default: "auto", + options: [ + { name: "auto", displayName: "Auto (from value)" }, + { name: "1", displayName: "One byte" }, + { name: "2", displayName: "Two bytes" }, + { name: "4", displayName: "Four bytes (full register)" }, + ], + }, { name: "opCode", default: "LDI", @@ -211,6 +240,11 @@ exports = { hidden: true, getValue: (inst) => { let bytes = getNumOfBytes(inst["constant1"]); + if(inst["outputWidth"] !== "auto") + { + // explicit width; a value that does not fit is reported by validate() + return Math.max(bytes == 3 ? 4 : bytes, parseInt(inst["outputWidth"])); + } if(bytes == 3) { //storing in 4 continous bytes to number of instructions to access @@ -271,4 +305,4 @@ exports = { }] }, }, -} \ No newline at end of file +} diff --git a/.metadata/sysconfig/.meta/pru_blocks/program_control/conditional_block.syscfg.js b/.metadata/sysconfig/.meta/pru_blocks/program_control/conditional_block.syscfg.js index e93f8ae..147a73f 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/program_control/conditional_block.syscfg.js +++ b/.metadata/sysconfig/.meta/pru_blocks/program_control/conditional_block.syscfg.js @@ -1,4 +1,11 @@ function validate(inst, report) { + // "_TRUE"/"_FALSE" branch labels are derived from $name; only the derived forms are checked (a reserved base such as c0 still assembles as c0_TRUE — checked with clpru 2.3.3). + if (typeof system !== "undefined" && system.getScript) { + var reservedNames = system.getScript("/pru_blocks/common/pru_reserved_names.js"); + if (reservedNames && typeof reservedNames.validateDerivedSymbol === "function") { + reservedNames.validateDerivedSymbol(inst, report, "$name", ["_TRUE", "_FALSE"], "This block emits '_TRUE' and '_FALSE' branch labels derived from the instance name."); + } + } for(let iterator = 1; iterator <= inst["numOfInputPorts"]; iterator++) { if(inst["input"+iterator.toString()].length == 0) @@ -9,12 +16,12 @@ function validate(inst, report) { //verifying if true port is connected true/false port(conditional input) are not if((inst["T"].length == 0)) { - report.logWarning("t_next port is not connected",inst); + report.logError("t_next port is not connected: the generated code branches to _TRUE, which would be an undefined symbol",inst); } //verifying if false port is connected true/false port(conditional input) are not if((inst["F"].length == 0)) { - report.logWarning("f_next port is not connected",inst); + report.logError("f_next port is not connected: the generated code falls through to _FALSE, which would be an undefined symbol",inst); } } @@ -165,7 +172,7 @@ Implements conditional logic (IF/ELSE statements) to control program flow based ### Usage Notes - Both input ports must be connected for conditional operation -- Both t_next and f_next ports should be connected to avoid warnings +- Both t_next and f_next ports must be connected: an unconnected branch is a validation error (it would otherwise assemble to an undefined cmp_TRUE/cmp_FALSE label) - Comparisons are unsigned - values treated as positive integers - The conditional check happens instantly (1 cycle) - Code on both branches is generated, only one path executes at runtime @@ -311,4 +318,4 @@ exports = { }] }, }, -} \ No newline at end of file +} diff --git a/.metadata/sysconfig/.meta/pru_blocks/program_control/flow_control_block.syscfg.js b/.metadata/sysconfig/.meta/pru_blocks/program_control/flow_control_block.syscfg.js index 37fc906..2083910 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/program_control/flow_control_block.syscfg.js +++ b/.metadata/sysconfig/.meta/pru_blocks/program_control/flow_control_block.syscfg.js @@ -235,4 +235,4 @@ exports = { }] }, }, -} \ No newline at end of file +} diff --git a/.metadata/sysconfig/.meta/pru_blocks/program_control/group_block.syscfg.js b/.metadata/sysconfig/.meta/pru_blocks/program_control/group_block.syscfg.js index bb6aa85..7e07e72 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/program_control/group_block.syscfg.js +++ b/.metadata/sysconfig/.meta/pru_blocks/program_control/group_block.syscfg.js @@ -1,4 +1,11 @@ function validate(inst, report) { + // the "_start" entry label is derived from groupName; only the derived form is checked (a reserved base such as c0 still assembles as c0_start — checked with clpru 2.3.3). + if (typeof system !== "undefined" && system.getScript) { + var reservedNames = system.getScript("/pru_blocks/common/pru_reserved_names.js"); + if (reservedNames && typeof reservedNames.validateDerivedSymbol === "function") { + reservedNames.validateDerivedSymbol(inst, report, "groupName", ["_start"], "The group entry label '_start' is derived from this name."); + } + } // Validate that group name is non-empty if (!inst.groupName || inst.groupName.trim() === "") { report.logError("Group name cannot be empty", inst); @@ -428,4 +435,4 @@ exports = { }] }, }, -} \ No newline at end of file +} diff --git a/.metadata/sysconfig/.meta/pru_blocks/program_control/loop_block.syscfg.js b/.metadata/sysconfig/.meta/pru_blocks/program_control/loop_block.syscfg.js index 4d428dd..388a231 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/program_control/loop_block.syscfg.js +++ b/.metadata/sysconfig/.meta/pru_blocks/program_control/loop_block.syscfg.js @@ -1,3 +1,30 @@ +// normalize loop_block.accumulators to {pairs, error}. Accepts an +// array of {preInit, update} (or a JSON string of one); anything else is a +// validation error. Malformed entries are ignored by the register allocator; +// validate() below rejects them so a typo can never silently disable the +// accumulator. +function parseLoopAccumulators(value) { + let list = value; + if (list === undefined || list === null) return { pairs: [], error: null }; + if (typeof list === "string") { + if (list.trim() === "") return { pairs: [], error: null }; + try { + list = JSON.parse(list); + } catch (e) { + return { pairs: [], error: "is not valid JSON (" + e.message + ")" }; + } + } + if (!Array.isArray(list)) return { pairs: [], error: "must be an array of {preInit, update} objects" }; + const pairs = []; + for (const entry of list) { + if (!entry || typeof entry.preInit !== "string" || typeof entry.update !== "string") { + return { pairs: [], error: "every entry must be a {preInit: \"\", update: \"\"} object" }; + } + pairs.push({ preInit: entry.preInit, update: entry.update }); + } + return { pairs: pairs, error: null }; +} + function validate(inst, report) { for(let iterator = 1; iterator <= inst["numOfInputPorts"]; iterator++) { @@ -10,7 +37,66 @@ function validate(inst, report) { // Validate loop count - PRU LOOP instruction supports up to 16-bit counters (max 65535) if(inst["infiniteLoop"] === false && inst["loopCount"] > 65535) { - report.logError("Loop count cannot exceed 65,535. Use nested Loop blocks for higher iteration counts.", inst, "loopCount"); + report.logError("Loop count cannot exceed 65,535. Split the work into sequential finite loops; PRU hardware LOOP instructions cannot nest.", inst, "loopCount"); + } + + /* + * The PRU has one hardware LOOP context. A LOOP emitted by a child + * overwrites its enclosing Loop's end address/counter, so the enclosing + * body falls out after its first pass. Walk through Group containers as + * well: a Delay hidden in a Group is still emitted physically in this + * Loop's body. + * + * Only a finite Loop emits LOOP (an infinite one emits QBA), and children + * listed in preInitBlocks are hoisted ahead of the LOOP, so both are exempt. + * + * Keep this capability-based rather than a name/path check. Delay marks + * itself below, and any future block that emits the hardware instruction + * can opt in with the same hidden configurable. + */ + function findNestedHardwareLoop(contents, seen, skipNames) { + for (const child of (contents || [])) { + if (!child || seen.indexOf(child) !== -1) continue; + if (skipNames && skipNames.has(child.$name)) continue; + seen.push(child); + if (child.emitsHardwareLoop === true) return child; + const nested = findNestedHardwareLoop(child.$groupContents, seen, skipNames); + if (nested) return nested; + } + return null; + } + // An infinite Loop emits QBA, not LOOP, and preInitBlocks are hoisted out + // of the body ahead of the LOOP, so neither can clobber the context. + const nestedHardwareLoop = inst["infiniteLoop"] === false + ? findNestedHardwareLoop(inst.$groupContents, [], new Set(inst["preInitBlocks"] || [])) + : null; + if (nestedHardwareLoop) { + report.logError( + `PRU hardware LOOP instructions cannot nest: ${nestedHardwareLoop.$name || "a block"} emits LOOP inside ${inst.$name || "this Loop"}. Move it outside the Loop or unroll the body.`, + inst, + "$groupContents"); + } + + // explicit accumulator bindings and exported result blocks. + const memberNames = new Set(((inst.$groupContents) || []).map((b) => (b && b.$name))); + for (const name of (inst.exportedBlocks || [])) { + if (!memberNames.has(name)) { + report.logError("Exported block '" + name + "' is not inside this loop. exportedBlocks must name blocks from this loop's contents.", inst, "exportedBlocks"); + } + } + const accumulators = parseLoopAccumulators(inst.accumulators); + if (accumulators.error) { + report.logError("Invalid accumulator bindings: " + accumulators.error + " Expected [{preInit: \"\", update: \"\"}].", inst, "accumulators"); + } + for (const pair of accumulators.pairs) { + if (!memberNames.has(pair.preInit)) { + report.logError("Accumulator preInit block '" + pair.preInit + "' is not inside this loop.", inst, "accumulators"); + } else if (!((inst.preInitBlocks) || []).includes(pair.preInit)) { + report.logError("Accumulator preInit block '" + pair.preInit + "' must also be listed in Pre-Initialization Blocks so its value is set once before the loop instead of on every iteration.", inst, "accumulators"); + } + if (!memberNames.has(pair.update)) { + report.logError("Accumulator update block '" + pair.update + "' is not inside this loop.", inst, "accumulators"); + } } } @@ -53,6 +139,8 @@ const loop_block1 = loop_block.addInstance(); | infiniteLoop | Boolean | true, false | false | Enable infinite loop mode | | loopCount | Integer | 1-65535 (0x1-0xFFFF) | 1 | Number of iterations (hidden if infiniteLoop=true) | | preInitBlocks | Array | Block $name values inside loop | [] | Blocks to execute once before loop starts | +| exportedBlocks | Array | Block $name values inside loop | [] | Blocks whose final-iteration output may be read outside the loop | +| accumulators | String (JSON) | [{"preInit": "k", "update": "a"}] | "" | Accumulator bindings coalescing each update block with its preInit register | | $size | Array | [width, height] | [500, 250] | Size of the loop container in pixels | ### Example Configurations @@ -80,6 +168,20 @@ loop_block1.loopCount = 10; loop_block1.preInitBlocks = ["If_Else_0", "Load_Constant_1", "PRU_GPI_0"]; \\\`\\\`\\\` +|**Loop-carried accumulator (repeat-add):** +\\\`\\\`\\\`javascript +// k_acc is set once before the loop; acc adds x into the SAME register each pass +loop_block1.$name = "Loop_0"; +loop_block1.loopCount = 10; +loop_block1.$groupContents = [k_acc, acc]; +loop_block1.preInitBlocks = ["k_acc"]; +loop_block1.accumulators = "[{\\"preInit\\": \\"k_acc\\", \\"update\\": \\"acc\\"}]"; +// The store sits AFTER the loop and reads the final sum +loop_block1.exportedBlocks = ["acc"]; +scripting.connect(wr, "input1", acc, "output1"); +scripting.connect(lp, "next", wr, "prev"); +\\\`\\\`\\\` + ### Adding Blocks Inside the Loop \\\`\\\`\\\`javascript @@ -156,7 +258,7 @@ scripting.connect(loop_block1, "next", next_block, "prev"); 1. **Container Block**: Loop is a container - use $groupContents to add blocks inside. -2. **Max Iterations**: Maximum loop count is 65,535 (16-bit limit). Use nested loops for more. +2. **Max Iterations**: Maximum loop count is 65,535 (16-bit limit). For more work, use sequential finite loops or host scheduling; PRU hardware loops cannot nest. 3. **Infinite Loop**: When infiniteLoop=true, the next port is hidden and loop never exits. @@ -164,7 +266,7 @@ scripting.connect(loop_block1, "next", next_block, "prev"); 5. **Loop Overhead**: Fixed loops add 2-3 cycles overhead plus 2 cycles per iteration. -6. **Nested Loops**: Place a Loop block inside another Loop for nested iteration. +6. **No hardware-loop nesting**: A Loop body must not contain another hardware Loop or a Delay whose count is greater than one. SysConfig rejects this because the PRU has only one hardware LOOP context. `; } @@ -198,6 +300,9 @@ Repeats a sequence of blocks multiple times. This is a container block that exec - Select blocks whose instructions should execute ONCE before the loop starts, not on every iteration - Useful for initializing accumulators, counters, or one-time setup operations - Selected blocks are still inside the loop visually, but their code is moved outside +- A pre-initialized value keeps its value on every iteration. It changes only in the accumulator pattern: the value is updated in place only when it has exactly ONE data reader, that reader is a single-instruction Arithmetic or Bitwise block, and the allocator places that block's result starting at the value's own register byte, so the result is stored back there and counts up each pass. In every other case (more than one reader, a different kind of reader, or a result placed anywhere else) the pre-initialized value stays constant for the whole loop. +- Declare that pattern explicitly with Accumulator Bindings (\`accumulators = "[{\\"preInit\\": \\"k_acc\\", \\"update\\": \\"acc\\"}]"\`): this is a declared, checked contract that requires the update to reuse the preInit register, so the update reads and writes the same register on every pass (for example \`ADD R0.w2, R0.w2, R0.w0\`) with zero extra cycles per iteration. The underlying single-reader coalescing is the same one used for pre-initialized values; the explicit binding additionally rejects invalid shapes loudly. The preInit block must also be listed in Pre-Initialization Blocks, both blocks must sit inside this loop, and the preInit value must have exactly one data reader (the update block); anything else is a build error, never a silent recompute. +- Let blocks after the loop read a final-iteration result with Exported Result Blocks (\`exportedBlocks = ["acc"]\`): a consumer downstream of the loop may read an exported block (for example a memory write storing the accumulated sum once, after the loop). A consumer outside the loop reading a block inside it that is NOT exported, or reading an export before the loop, is a validation error. **Common Use Cases for Pre-Initialization**: 1. **Accumulator Pattern**: Initialize a variable to 0 before accumulating values across iterations @@ -279,7 +384,7 @@ Blocks execute in the order they are connected via prev/next ports within the lo - Infinite loops are useful for continuous monitoring or periodic tasks - Be careful with infinite loops - they never exit - Loop counter uses a register - this register is reserved during loop execution -- Nested loops are possible (place a LOOP block inside another LOOP block) +- PRU hardware loops cannot nest. Do not place a fixed-count Loop or a Delay (count > 1) in a fixed-count Loop body; unroll the body or split it into sequential loops. An infinite Loop compiles to a branch rather than LOOP, so it may contain either, and blocks listed in preInitBlocks run once before the LOOP and are exempt. - Loop overhead is minimal (2-3 cycles setup, 2 cycles per iteration) ### Performance Calculation @@ -305,7 +410,7 @@ Where: - **Loop body**: The code/blocks executed each iteration - **Infinite loop**: Loop that never terminates (runs forever) - **Container block**: Block that contains other blocks (like a group) -- **Nested loop**: Loop inside another loop +- **Hardware-loop context**: PRU state used by the fixed-count \`LOOP\` instruction; only one is available, so fixed loops cannot nest - **Loop overhead**: Extra cycles required for loop management ---`; @@ -358,6 +463,14 @@ exports = { default: "loop", hidden : true }, + { + // Consumed by an enclosing Loop.validate(). Infinite loops use QBA, + // while finite loops consume the PRU's sole hardware LOOP context. + name: "emitsHardwareLoop", + default: true, + hidden: true, + getValue: (inst) => inst["infiniteLoop"] === false + }, { name : "loopCount", displayName: "Loop Count", @@ -379,6 +492,29 @@ exports = { })); } }, + { + name: "exportedBlocks", + displayName: "Exported Result Blocks", + description: "Blocks inside this loop whose final-iteration output may be read by blocks outside the loop.", + longDescription: "Blocks inside this loop whose final-iteration output may be read by blocks outside the loop (for example a memory write storing the accumulated sum once, after the loop). Without an entry here, a consumer outside the loop reading a block inside it is a container-boundary error.", + default: [], + minSelections: 0, + options: (inst) => { + // Return blocks that are inside this loop's $groupContents + const groupContents = inst.$groupContents || []; + return groupContents.map(blockInstance => ({ + name: blockInstance.$name, + displayName: blockInstance.$name + })); + } + }, + { + name: "accumulators", + displayName: "Accumulator Bindings", + description: "Loop-carried accumulator bindings (JSON): the update block reads and writes its preInit block's register on every pass.", + longDescription: "Loop-carried accumulator bindings as a JSON string '[{\\\"preInit\\\": \\\"BLOCK_NAME\\\", \\\"update\\\": \\\"BLOCK_NAME\\\"}]': the update (Arithmetic/Bitwise) block's destination register is coalesced with its preInit block's register, so the update reads and writes the same register on every pass with zero extra cycles per iteration. The preInit block must also be listed in Pre-Initialization Blocks, both blocks must sit inside this loop, and the preInit value must have exactly one data reader (the update block).", + default: "", + }, { name : "loopCountRegSize", default: 1, @@ -454,4 +590,4 @@ exports = { }] }, }, -} \ No newline at end of file +} diff --git a/.metadata/sysconfig/.meta/pru_blocks/pru_io_blocks/pru_gpi_block.syscfg.js b/.metadata/sysconfig/.meta/pru_blocks/pru_io_blocks/pru_gpi_block.syscfg.js index 581800c..8900185 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/pru_io_blocks/pru_gpi_block.syscfg.js +++ b/.metadata/sysconfig/.meta/pru_blocks/pru_io_blocks/pru_gpi_block.syscfg.js @@ -115,7 +115,7 @@ scripting.connect(gpi1, "next", next_block, "prev"); 3. **Read-Only**: R31 is a read-only register that reflects current pin states. -4. **Single Cycle**: Reading takes only 1 PRU cycle. +4. **Cycle Count**: Reading takes 3 PRU cycles. `; } @@ -138,9 +138,11 @@ Reads digital input signals from PRU input pins and outputs the pin state to the ### Technical Details (Additional Information) **Generated Assembly**: -- AND result, R31, (1 << pin_number) ; Mask the specific bit (1 cycle) +- ZERO &result, 4 ; Clear destination register (1 cycle) +- SET result, result, pin_number ; Create single-bit mask (1 cycle) +- AND result, R31, result ; Mask the specific bit (1 cycle) -**Performance**: 1 PRU cycle +**Performance**: 3 PRU cycles **Register**: R31 is the PRU's input register - Read-only register @@ -230,11 +232,19 @@ R31 Bit 10 - Timestamp Mode: `; } +function getInstruction(inst, outputRegister) { + const reg = outputRegister.replace(/\.[bw]\d+$/, ""); + const match = String(inst.constant1).match(/1\s*<<\s*(\d+)/); + const pin = match ? parseInt(match[1]) : 0; + return `zero &${reg}, 4\n set ${reg}, ${reg}, ${pin}\n and ${reg}, R31, ${reg}`; +} + exports = { displayName: "PRU GPI", defaultInstanceName: `${PRU_USED}_GPI_INSTANCE_`, longDescription: getLongDescription(), getAIContext: getAIContext, + getInstruction: getInstruction, uiView: "graph", templates: { //need to check what can be passed as argument to template file, right now no argument is required @@ -285,22 +295,16 @@ exports = { name: "noOfCycles", displayName: "Number Of Cycles", getValue: (inst) => { - return 1; + return 3; }, - default : 1 + default : 3 }, { name: "output1Size", - default: 1, + default: 4, hidden: true, getValue: (inst) => { - - let bytes = getNumOfBytes(eval(inst["constant1"].split(",")[1].trim())); - if(bytes == 3) - { - return bytes + 1; - } - return bytes + return 4; } }, ], @@ -320,6 +324,12 @@ exports = { }, validate, moduleStatic: { + + getPinUsage: function(inst) { + const match = String(inst.constant1).match(/1\s*<<\s*(\d+)/); + return match ? [{ pin: parseInt(match[1]), dir: "GPI" }] : []; + } + , modules: function(inst) { return [{ name: "pru_register_allocator_validator", @@ -327,4 +337,4 @@ exports = { }] }, }, -} \ No newline at end of file +} diff --git a/.metadata/sysconfig/.meta/pru_blocks/pru_io_blocks/pru_gpo_block.syscfg.js b/.metadata/sysconfig/.meta/pru_blocks/pru_io_blocks/pru_gpo_block.syscfg.js index 6ce7c6a..bffc347 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/pru_io_blocks/pru_gpo_block.syscfg.js +++ b/.metadata/sysconfig/.meta/pru_blocks/pru_io_blocks/pru_gpo_block.syscfg.js @@ -364,6 +364,12 @@ exports = { }, validate, moduleStatic: { + + getPinUsage: function(inst) { + const match = String(inst.constant1).match(/R30,\s*(\d+)/); + return match ? [{ pin: parseInt(match[1]), dir: "GPO" }] : []; + } + , modules: function(inst) { return [{ name: "pru_register_allocator_validator", @@ -371,4 +377,4 @@ exports = { }] }, }, -} \ No newline at end of file +} diff --git a/.metadata/sysconfig/.meta/pru_blocks/pru_io_blocks/pru_spi_read.syscfg.js b/.metadata/sysconfig/.meta/pru_blocks/pru_io_blocks/pru_spi_read.syscfg.js index 81bd34c..34f01d8 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/pru_io_blocks/pru_spi_read.syscfg.js +++ b/.metadata/sysconfig/.meta/pru_blocks/pru_io_blocks/pru_spi_read.syscfg.js @@ -18,7 +18,11 @@ function getPruNumberFromContext() { return "PRU1"; } const PRU_USED = getPruNumberFromContext(); +// CS Setup/Hold Time are nanoseconds; the macros take PRU cycles. One shared conversion. +const spiTiming = system.getScript("/pru_blocks/common/pru_spi_timing.js"); function validate(inst, report) { + spiTiming.validateCsTimes(inst, report); + spiTiming.validateIramBudget(inst, report); // Port connection warnings for(let iterator = 1; iterator <= inst["numOfInputPorts"]; iterator++) { @@ -110,13 +114,26 @@ function getNumOfBytes(value) { return 2; } - if(value <= 24) - { - return 3; - } + // The macro clears its result with one LDI. A 17..24-bit result must + // therefore own the whole register: allocating three bytes would leave b3 + // available for a live neighbour that LDI would clobber. return 4; } +// A full variant name (not the bare "m_read_packet_spi_" prefix that +// the block control-flow classifier probes with) must always carry the insert point. +const READ_VARIANT_NAME = /^m_read_packet_spi_mode[0-3]_(slave_)?(msb|lsb)_/; + +function insertReadResultInitialization(macroBody, dataReg, macroName) +{ + const insertPoint = /(\n\tldi\s+[^,\n]+,\s+[^\n]+\nREAD_BIT_LOOP\?:)/; + const initialized = macroBody.replace(insertPoint, "\n\tldi " + dataReg + ", 0$1"); + if (initialized === macroBody && READ_VARIANT_NAME.test(macroName || "")) { + throw new Error("pru_spi_read: could not find READ_BIT_LOOP initialization point"); + } + return initialized; +} + /** * Returns the body of a specific macro based on the macro name with parameters replaced * @param {string} pruInstructionMacro - The macro instruction string that contains the macro name and parameters @@ -169,8 +186,8 @@ function getMacro(pruInstructionMacro, opCode) { ; DELAY_COMPEN_1 - High pulse delay compensation (PRU cycles) ; DELAY_COMPEN_2 - Low pulse delay compensation (PRU cycles) ; CS_PIN - Chip select output pin number (GPO) - ; CS_SETUP_TIME - Setup time after CS assertion (PRU cycles) - ; CS_HOLD_TIME - Hold time before CS deassertion (PRU cycles) + ; CS_SETUP_TIME - Setup time after CS assertion (PRU cycles; the block converts its nanosecond setting, rounding up) + ; CS_HOLD_TIME - Hold time before CS deassertion (PRU cycles; the block converts its nanosecond setting, rounding up) ; DATA_SETUP_TIME - Data setup time after clock edge (PRU cycles) `; } @@ -211,13 +228,17 @@ SKIP_BIT_ENTRY_0?: set r30, r30, ${CS_PIN}` } if (macroName === "m_read_packet_spi_mode1_lsb_gpo_sclk") { + // The bit counter advances in the high half, as in the MSB variant, so both halves + // match delay_component1/2. bitId is a byte register (TEMP_REG1.b0): it starts at + // 0xFF and wraps to 0 on the first add, and the loop ends after bit PACKETSIZE-1. macroBody += ` clr r30, r30, ${CS_PIN} .loop ${CS_SETUP_TIME} nop .endloop - ldi ${bitId}, 0 + ldi ${bitId}, 0xFF READ_BIT_LOOP?: set r30, r30, ${SCLK_PIN} + add ${bitId}, ${bitId}, 1 .loop ${DATA_SETUP_TIME} nop .endloop @@ -232,11 +253,10 @@ BIT_ENTRY_0?: clr ${dataReg}, ${dataReg}, ${bitId} nop SKIP_BIT_ENTRY_0?: - add ${bitId}, ${bitId}, 1 .loop ${DELAY_COMPEN_2} ; 0 nop .endloop - qbne READ_BIT_LOOP?, ${bitId}, ${PACKETSIZE} + qbne READ_BIT_LOOP?, ${bitId}, ${PACKETSIZE}-1 .loop ${CS_HOLD_TIME} nop .endloop @@ -344,12 +364,15 @@ SKIP_BIT_ENTRY_0?: set r30, r30, ${CS_PIN}`; } if (macroName === "m_read_packet_spi_mode3_lsb_gpo_sclk") { + // Both sample paths take two instructions, and the bit counter advances in the low half, + // as the MSB variant's sub does, so both halves match delay_component1/2. bitId is a + // byte register (TEMP_REG1.b0): it starts at 0xFF and wraps to 0 on the first add. macroBody += ` set r30, r30, ${SCLK_PIN} clr r30, r30, ${CS_PIN} .loop ${CS_SETUP_TIME} nop .endloop - ldi ${bitId}, 0 + ldi ${bitId}, 0xFF READ_BIT_LOOP?: clr r30, r30, ${SCLK_PIN} .loop ${DATA_SETUP_TIME} @@ -358,19 +381,19 @@ READ_BIT_LOOP?: .loop ${DELAY_COMPEN_1} nop .endloop - nop + add ${bitId}, ${bitId}, 1 set r30, r30, ${SCLK_PIN} qbbc BIT_ENTRY_0?, r31, ${SDI_PIN} set ${dataReg}, ${dataReg}, ${bitId} qba SKIP_BIT_ENTRY_0? BIT_ENTRY_0?: clr ${dataReg}, ${dataReg}, ${bitId} + nop SKIP_BIT_ENTRY_0?: - add ${bitId}, ${bitId}, 1 .loop ${DELAY_COMPEN_2} nop .endloop - qbne READ_BIT_LOOP?, ${bitId}, ${PACKETSIZE} + qbne READ_BIT_LOOP?, ${bitId}, ${PACKETSIZE}-1 .loop ${CS_HOLD_TIME} nop .endloop @@ -701,6 +724,13 @@ SKIP_BIT_ENTRY_0?: qbne READ_BIT_LOOP?, ${bitId}, ${PACKETSIZE}`; } + // A partial packet must replace the whole destination word. The bit loop + // only writes bits [PACKETSIZE - 1:0], so clear the register once before it + // starts instead of leaving the prior value in the high bits. + if (macroName.startsWith("m_read_packet_spi_")) { + macroBody = insertReadResultInitialization(macroBody, dataReg, macroName); + } + if(pruInstructionMacro == "") { macroBody += "\n" + " .endm"; @@ -893,6 +923,7 @@ Cycles per bit: **(2+d1) + (5+d2)** ### Technical Details (Additional Information) **Performance**: +- **Result initialization:** The destination register is cleared once before sampling, so packets shorter than 32 bits are zero-extended. This adds one cycle to the transaction overhead. - Cycles per bit ≈ (high_width + low_width + data_setup_time + overhead) - Total cycles ≈ CS_setup + (packet_size × cycles_per_bit) + CS_hold @@ -1419,11 +1450,11 @@ exports = { // Cycles per bit: fixed overhead + DATA_SETUP_TIME + delay_component1 + delay_component2 const cyclesPerBit = 7 + dataSetupCycles + inst["delay_component1"] + inst["delay_component2"]; - return 1 + csSetupCycles + 1 + + return 2 + csSetupCycles + 1 + (inst["packetSize"] * cyclesPerBit) + csHoldCycles + 1; } else { - return 4 + inst["delay_component1"] + (inst["packetSize"] - 1) * (6 + inst["delay_component1"] + inst["delay_component2"]); + return 5 + inst["delay_component1"] + (inst["packetSize"] - 1) * (6 + inst["delay_component1"] + inst["delay_component2"]); } }, default : 1 @@ -1445,6 +1476,16 @@ exports = { }, validate, moduleStatic: { + + getPinUsage: function(inst) { + const isCtrl = inst["Device Mode"] === "controller"; + const pins = []; + if (inst["SCLK Signal"] !== undefined) pins.push({ pin: parseInt(inst["SCLK Signal"]), dir: isCtrl ? "GPO" : "GPI" }); + if (inst["SDI Signal"] !== undefined) pins.push({ pin: parseInt(inst["SDI Signal"]), dir: "GPI" }); + if (inst["CS Signal"] !== undefined) pins.push({ pin: parseInt(inst["CS Signal"]), dir: isCtrl ? "GPO" : "GPI" }); + return pins; + } + , modules: function(inst) { return [{ name: "pru_register_allocator_validator", @@ -1452,7 +1493,9 @@ exports = { }] }, }, - getMacro + getMacro, + // Exposed for this module's tests; production callers use getMacro. + _testInsertReadResultInitialization: insertReadResultInitialization } /* @@ -1466,4 +1509,4 @@ notes: 3 + delay component2 + 1 to set 1 + delay component1 + 1 to clear -*/ \ No newline at end of file +*/ diff --git a/.metadata/sysconfig/.meta/pru_blocks/pru_io_blocks/pru_spi_transfer.syscfg.js b/.metadata/sysconfig/.meta/pru_blocks/pru_io_blocks/pru_spi_transfer.syscfg.js index 7b95e68..e38e104 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/pru_io_blocks/pru_spi_transfer.syscfg.js +++ b/.metadata/sysconfig/.meta/pru_blocks/pru_io_blocks/pru_spi_transfer.syscfg.js @@ -18,7 +18,11 @@ function getPruNumberFromContext() { return "PRU1"; } const PRU_USED = getPruNumberFromContext(); +// CS Setup/Hold Time are nanoseconds; the macros take PRU cycles. One shared conversion. +const spiTiming = system.getScript("/pru_blocks/common/pru_spi_timing.js"); function validate(inst, report) { + spiTiming.validateCsTimes(inst, report); + spiTiming.validateIramBudget(inst, report); // Port connection warnings for(let iterator = 1; iterator <= inst["numOfInputPorts"]; iterator++) { @@ -136,13 +140,24 @@ function getNumOfBytes(value) { return 2; } - if(value <= 24) - { - return 3; - } + // r_dataReg is initialized by a whole-register LDI for packets wider than + // 16 bits, so reserve all four bytes rather than clobbering a live b3. return 4; } +function insertTransferResultInitialization(macroBody, r_dataReg, headerLength) +{ + // The clear goes INSIDE the macro: after the ".macro" header and parameter + // comments when the header form is requested, at the very start of the body + // for the inline form. headerLength is the number of characters that belong + // to the header (0 for the inline form). + // It cannot destroy the transmit word: the register allocator never gives a + // macro the register of an input it has not finished reading (macros are not + // in IN_PLACE_SAFE_OPCODES), so r_dataReg and s_dataReg are disjoint. + return macroBody.slice(0, headerLength) + "\tldi " + r_dataReg + ", 0\n" + + macroBody.slice(headerLength); +} + /** * Returns the body of a specific macro based on the macro name with parameters replaced * @param {string} pruInstructionMacro - The macro instruction string that contains the macro name and parameters @@ -201,13 +216,15 @@ function getMacro(pruInstructionMacro, opCode) { ; DELAY_COMPEN_1 - High pulse delay compensation (PRU cycles) ; DELAY_COMPEN_2 - Low pulse delay compensation (PRU cycles) ; CS_PIN - Chip select output pin number (GPO) - ; CS_SETUP_TIME - Setup time after CS assertion (PRU cycles) - ; CS_HOLD_TIME - Hold time before CS deassertion (PRU cycles) + ; CS_SETUP_TIME - Setup time after CS assertion (PRU cycles; the block converts its nanosecond setting, rounding up) + ; CS_HOLD_TIME - Hold time before CS deassertion (PRU cycles; the block converts its nanosecond setting, rounding up) ; DATA_SETUP_TIME - Data setup time after clock edge (PRU cycles) `; } } + const headerLength = macroBody.length; + // Controller MODE0 MSB (CPOL=0, CPHA=0) - Clock idles LOW, sample on rising, shift on falling if (macroName === "m_transfer_packet_spi_mode0_msb_gpo_sclk") { macroBody += ` clr r30, r30, ${CS_PIN} @@ -985,6 +1002,11 @@ wait_for_low_transition_loop2?: SEND_BIT_LOOP_END?:`; } + if (macroName.startsWith("m_transfer_packet_spi_") && macroBody.length > headerLength) + { + macroBody = insertTransferResultInitialization(macroBody, r_dataReg, headerLength); + } + if(pruInstructionMacro == "") { macroBody += "\n" + " .endm"; @@ -1675,12 +1697,12 @@ exports = { // Based on m_transfer_packet_spi_master_gpo_sclk: ~ (4+d1) + (6+d2) cycles per bit const cyclesPerBit = 10 + dataSetupCycles + inst["delay_component1"] + inst["delay_component2"]; - return 1 + csSetupCycles + 1 + + return 2 + csSetupCycles + 1 + (inst["packetSize"] * cyclesPerBit) + csHoldCycles + 1; } else { // Peripheral mode: 13 cycles per bit (6 shifting + 7 sampling) - return 6 + inst["CS Filter Cycles"] + (inst["packetSize"] * 13); + return 7 + inst["CS Filter Cycles"] + (inst["packetSize"] * 13); } }, default : 1 @@ -1702,6 +1724,17 @@ exports = { }, validate, moduleStatic: { + + getPinUsage: function(inst) { + const isCtrl = inst["Device Mode"] === "controller"; + const pins = []; + if (inst["SCLK Signal"] !== undefined) pins.push({ pin: parseInt(inst["SCLK Signal"]), dir: isCtrl ? "GPO" : "GPI" }); + if (inst["SDI Signal"] !== undefined) pins.push({ pin: parseInt(inst["SDI Signal"]), dir: "GPI" }); + if (inst["SDO Signal"] !== undefined) pins.push({ pin: parseInt(inst["SDO Signal"]), dir: "GPO" }); + if (inst["CS Signal"] !== undefined) pins.push({ pin: parseInt(inst["CS Signal"]), dir: isCtrl ? "GPO" : "GPI" }); + return pins; + } + , modules: function(inst) { return [{ name: "pru_register_allocator_validator", diff --git a/.metadata/sysconfig/.meta/pru_blocks/pru_io_blocks/pru_spi_write.syscfg.js b/.metadata/sysconfig/.meta/pru_blocks/pru_io_blocks/pru_spi_write.syscfg.js index c1ac2e8..3c52f05 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/pru_io_blocks/pru_spi_write.syscfg.js +++ b/.metadata/sysconfig/.meta/pru_blocks/pru_io_blocks/pru_spi_write.syscfg.js @@ -18,7 +18,11 @@ function getPruNumberFromContext() { return "PRU1"; } const PRU_USED = getPruNumberFromContext(); +// CS Setup/Hold Time are nanoseconds; the macros take PRU cycles. One shared conversion. +const spiTiming = system.getScript("/pru_blocks/common/pru_spi_timing.js"); function validate(inst, report) { + spiTiming.validateCsTimes(inst, report); + spiTiming.validateIramBudget(inst, report); // Port connection warnings for(let iterator = 1; iterator <= inst["numOfInputPorts"]; iterator++) { @@ -165,8 +169,8 @@ function getMacro(pruInstructionMacro, opCode) { ; DELAY_COMPEN_1 - High pulse delay compensation (PRU cycles) ; DELAY_COMPEN_2 - Low pulse delay compensation (PRU cycles) ; CS_PIN - Chip select output pin number (GPO) - ; CS_SETUP_TIME - Setup time after CS assertion (PRU cycles) - ; CS_HOLD_TIME - Hold time before CS deassertion (PRU cycles) + ; CS_SETUP_TIME - Setup time after CS assertion (PRU cycles; the block converts its nanosecond setting, rounding up) + ; CS_HOLD_TIME - Hold time before CS deassertion (PRU cycles; the block converts its nanosecond setting, rounding up) ; DATA_SETUP_TIME - Data setup time after clock edge (PRU cycles) `; } @@ -1466,6 +1470,16 @@ exports = { }, validate, moduleStatic: { + + getPinUsage: function(inst) { + const isCtrl = inst["Device Mode"] === "controller"; + const pins = []; + if (inst["SCLK Signal"] !== undefined) pins.push({ pin: parseInt(inst["SCLK Signal"]), dir: isCtrl ? "GPO" : "GPI" }); + if (inst["SDO Signal"] !== undefined) pins.push({ pin: parseInt(inst["SDO Signal"]), dir: "GPO" }); + if (inst["CS Signal"] !== undefined) pins.push({ pin: parseInt(inst["CS Signal"]), dir: isCtrl ? "GPO" : "GPI" }); + return pins; + } + , modules: function(inst) { return [{ name: "pru_register_allocator_validator", @@ -1487,4 +1501,4 @@ notes: 2 + delay component2 + 1 to set 3 + delay component1 + 1 to clear -*/ \ No newline at end of file +*/ diff --git a/.metadata/sysconfig/.meta/pru_blocks/utils/access_look_up_table.syscfg.js b/.metadata/sysconfig/.meta/pru_blocks/utils/access_look_up_table.syscfg.js index 8e0e7b6..5865730 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/utils/access_look_up_table.syscfg.js +++ b/.metadata/sysconfig/.meta/pru_blocks/utils/access_look_up_table.syscfg.js @@ -89,8 +89,23 @@ function getMacro(pruInstructionMacro, opCode) { `; } if (macroName && macroName.trim().startsWith("m_lookup_table")) { + // the index counts entries, the LBBO offset counts bytes. Entries of + // 2/4 bytes need the index scaled (<< 1 / << 2) before it is used, and the + // scaled offset (up to 4 * (tableSize - 1)) does not fit an LBBO register + // offset, so it is added into the base and the LBBO offset is 0. byteSize is + // a literal 1, 2 or 4 at expansion, so the .if picks the form at assembly time. macroBody += ` LDI32 TEMP_REG1, ${lutAddress} - LBBO &${result}, TEMP_REG1, ${index}, ${byteSize}`; + .if ${byteSize} == 1 + LBBO &${result}, TEMP_REG1, ${index}, ${byteSize} + .else + .if ${byteSize} == 2 + LSL TEMP_REG2, ${index}, 1 + .else + LSL TEMP_REG2, ${index}, 2 + .endif + ADD TEMP_REG1, TEMP_REG1, TEMP_REG2 + LBBO &${result}, TEMP_REG1, 0, ${byteSize} + .endif`; } if (pruInstructionMacro == "") { @@ -160,7 +175,7 @@ scripting.connect(access_lut1, "next", next_block, "prev"); 4. **Bounds Checking**: Validation warns if constant index is out of bounds. Runtime bounds checking is not performed. -5. **Performance**: 5 PRU cycles total (2 for LDI32 + 3 for LBBO). +5. **Performance**: byte table: 5 PRU cycles (2 for LDI32 + 3 for LBBO). ushort/uint table: 7 PRU cycles (2 LDI32 + 1 LSL index scale + 1 ADD + 3 LBBO). `; } @@ -185,10 +200,15 @@ Reads data from a lookup table stored in PRU Data Memory (DMEM) using an index v ### Technical Details (Additional Information) **Generated Assembly**: - LDI32 TEMP_REG1, \`LUT_BASE_ADDRESS\` ; Load table base address (2 cycles) -- LBBO &result, TEMP_REG1, index, size ; Load data from PRU DRAM (3 cycles for ≤4 bytes) - -**Performance**: 5 PRU cycles total (assuming word-aligned access to PRU DRAM) -- LDI32: 2 cycles +- byte table: LBBO &result, TEMP_REG1, index, 1 ; Load data from PRU DRAM (3 cycles) +- ushort/uint table: the index counts entries, so it is scaled to a byte offset first: + LSL TEMP_REG2, index, 1 (ushort) or 2 (uint) ; 1 cycle + ADD TEMP_REG1, TEMP_REG1, TEMP_REG2 ; 1 cycle + LBBO &result, TEMP_REG1, 0, 2 or 4 ; 3 cycles + +**Performance** (assuming word-aligned access to PRU DRAM) +- byte table: 5 PRU cycles (LDI32 2 + LBBO 3) +- ushort/uint table: 7 PRU cycles (LDI32 2 + LSL 1 + ADD 1 + LBBO 3) - LBBO: 3 cycles (2 + N where N=1 for reading ≤4 bytes) Note: Add +1 cycle if index results in non-word-aligned address @@ -314,8 +334,14 @@ exports = { // LDI32 takes 2 cycles (loads 32-bit value as two 16-bit moves) // LBBO from PRU DRAM takes 2 + N cycles where N is number of 4-byte words // For 1, 2, or 4 bytes (all ≤4 bytes): N = 1, so LBBO takes 2 + 1 = 3 cycles - // Total: 2 (LDI32) + 3 (LBBO) = 5 cycles + // Total: 2 (LDI32) + 3 (LBBO) = 5 cycles for a byte table + // ushort/uint tables also scale the index (LSL, 1 cycle) and add it + // into the base (ADD, 1 cycle) before the LBBO: 2 + 1 + 1 + 3 = 7 cycles. // Note: This assumes word-aligned access. Add +1 if non-aligned. + const lut = getReferencedLUT(inst); + if (lut && (lut["dataType"] == "ushort" || lut["dataType"] == "uint")) { + return 7; + } return 5; } } @@ -352,4 +378,4 @@ exports = { }, getMacro, validate, -} \ No newline at end of file +} diff --git a/.metadata/sysconfig/.meta/pru_blocks/utils/delay_block.syscfg.js b/.metadata/sysconfig/.meta/pru_blocks/utils/delay_block.syscfg.js index 864b2ad..db97065 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/utils/delay_block.syscfg.js +++ b/.metadata/sysconfig/.meta/pru_blocks/utils/delay_block.syscfg.js @@ -98,7 +98,7 @@ scripting.connect(delay1, "next", next_block, "prev"); ### Important Notes -1. **Maximum Delay**: Single Delay block limited to 255 cycles. Use Loop block for longer delays. +1. **Maximum Delay**: Single Delay block limited to 255 cycles. Do not put Delay(count > 1) inside a Loop: both emit PRU hardware LOOP. Chain Delays or split work into sequential finite loops instead. 2. **Timing**: At 200MHz PRU clock, 1 cycle = 5ns. delayCount of 200 = 1 microsecond. @@ -138,53 +138,30 @@ At 200MHz PRU clock (default on most TI devices): ### Technical Details (Additional Information) **Generated Assembly**: -- loop endloop, count - 1 ; Setup loop counter (2 cycles overhead) +- loop endloop, count - 1 ; Setup loop counter (1 cycle) - NOP ; No operation (count-1 times) - endloop: ; Loop end label -**Total Cycles**: count + 2 (2 cycle overhead for loop setup) +**Total Cycles**: count (the block takes exactly count cycles, matching noOfCycles) ### Usage Notes - Maximum delay per block: 255 cycles (hardware limitation) -- Minimum delay is 1 cycle (+ 2 overhead = 3 total) +- Minimum delay is 1 cycle (single NOP) - This is a **pass-through block** - connects input to output without modification ### For Delays Greater Than 255 Cycles -Since the Delay block is limited to 255 cycles maximum, use the **Loop block** for longer delays: +One Delay block waits at most 255 cycles. For longer waits: -**Method 1: Loop with single Delay block** +- **Chain Delay blocks.** Delay(255) -> Delay(255) -> Delay(100) waits 610 cycles. 10 us at 200 MHz is 2,000 cycles, so 8 chained blocks; 100 us is about 79. +- **Put the chain in an infinite Loop** for a periodic task. An infinite Loop compiles to a branch, not the hardware LOOP instruction, so Delay blocks of any length are legal inside it. +- **Use a software counter loop** (an Arithmetic block that decrements a counter and a Conditional block that branches back while it is non-zero) for very long waits. -Loop (count: N) → Delay (255 cycles) - -Total delay = N × 255 cycles -Example: Loop count 10 with Delay 255 = 2,550 cycles = 12.75μs @ 200MHz - -**Method 2: Loop with multiple Delay blocks in sequence** - -Loop (count: N) → Delay (255 cycles) → Delay (255 cycles) → Delay (100 cycles) - -Total delay = N × (255 + 255 + 100) = N × 610 cycles -Example: Loop count 100 with 610 cycles = 61,000 cycles = 305μs @ 200MHz - -**Method 3: Nested loops for very long delays** - -Outer Loop (count: 1000) -└─ Inner Loop (count: 200) - └─ Delay (255 cycles) - -Total delay = 1000 × 200 × 255 = 51,000,000 cycles = 255ms @ 200MHz - -**Delay Examples**: -- **10 microseconds**: Loop(40) → Delay(50) = 2,000 cycles -- **100 microseconds**: Loop(100) → Delay(200) = 20,000 cycles -- **1 millisecond**: Loop(200) → Loop(5) → Delay(200) = 200,000 cycles -- **10 milliseconds**: Loop(1000) → Loop(10) → Delay(200) = 2,000,000 cycles +Do not wrap a Delay longer than one cycle in a finite Loop: both emit the PRU hardware LOOP instruction, and the PRU has only one hardware-loop context, so SysConfig rejects it. Delay(1) is a plain NOP and is allowed inside any Loop. **Important Notes**: -- Loop overhead adds 2-3 cycles per iteration -- For precise timing, account for loop setup overhead -- Inner loop body executes sequentially (prev to next connections) +- Each Delay(count > 1) uses PRU hardware LOOP setup (1 cycle) plus count - 1 NOP iterations, totaling exactly count cycles matching noOfCycles +- For precise timing, check the generated cycle budget - Use simulation to verify exact cycle counts ### Terminology @@ -192,7 +169,7 @@ Total delay = 1000 × 200 × 255 = 51,000,000 cycles = 255ms @ 200MHz - **Cycle**: One tick of the PRU clock - **NOP**: No Operation - instruction that does nothing but consume time - **Loop overhead**: Extra cycles required for loop setup/teardown -- **Nested loops**: Loop block inside another Loop block for longer delays +- **Hardware-loop context**: PRU state used by the \`LOOP\` instruction; only one is available, so it cannot nest ---`; } @@ -232,7 +209,7 @@ exports = { default: 1, range: [1, 255], }, - { + { name: "opCode", displayName: "m_wait_cycles", default: "m_wait_cycles", @@ -245,6 +222,14 @@ exports = { }, hidden: true }, + { + // Consumed by Loop.validate(). A one-cycle Delay is a NOP and does + // not allocate the PRU's single hardware LOOP context. + name: "emitsHardwareLoop", + default: false, + hidden: true, + getValue: (inst) => inst["delayCount"] > 1 + }, { name: "numOfInputPorts", default: 0, diff --git a/.metadata/sysconfig/.meta/pru_blocks/utils/look_up_table.syscfg.js b/.metadata/sysconfig/.meta/pru_blocks/utils/look_up_table.syscfg.js index c040dfe..61d37e5 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/utils/look_up_table.syscfg.js +++ b/.metadata/sysconfig/.meta/pru_blocks/utils/look_up_table.syscfg.js @@ -23,6 +23,13 @@ function getDataSize(instance) { /*to do: figure out a way for the user to input large number of values without manually entering everything*/ function validate(inst, report) { + // $name is emitted as the table symbol (" .byte/.ushort/.uint ..." in .initdmem); reject clpru reserved identifiers via the shared helper. + if (typeof system !== "undefined" && system.getScript) { + var reservedNames = system.getScript("/pru_blocks/common/pru_reserved_names.js"); + if (reservedNames && typeof reservedNames.validateInstanceName === "function") { + reservedNames.validateInstanceName(inst, report, "This block emits the instance name as the lookup-table symbol in .initdmem."); + } + } if (inst["tableSize"] < 1 || inst["tableSize"] > 65536) { report.logError("Table size must be between 1 and 65536", inst); } @@ -840,4 +847,4 @@ Then click "Browse For JSON File" button above to select it.`; getMacro, validate, getLookUptable, -} \ No newline at end of file +} diff --git a/.metadata/sysconfig/.meta/pru_blocks/utils/memory_access_block.syscfg.js b/.metadata/sysconfig/.meta/pru_blocks/utils/memory_access_block.syscfg.js index 7b87d9a..3c4427c 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/utils/memory_access_block.syscfg.js +++ b/.metadata/sysconfig/.meta/pru_blocks/utils/memory_access_block.syscfg.js @@ -57,6 +57,22 @@ function getMemoryReserveSize(symbolName) { return -1; // Symbol not found } +/** + * Other Memory Access instances that resolve to the SAME auto-created memVar + * object as this one (neither picks a different symbol via symbolSelect). + * Setting labelName through any one of them renames the variable for all of + * them.. + */ +function getSharedMemVarPeers(inst) { + if (!inst.memVar || (inst.symbolSelect && inst.symbolSelect !== "")) return []; + const accessModule = system.modules["/pru_blocks/utils/memory_access_block"]; + if (!accessModule || !accessModule.$instances) return []; + return accessModule.$instances.filter(other => + other !== inst && + other.memVar === inst.memVar && + !(other.symbolSelect && other.symbolSelect !== "")); +} + function validate(inst, report) { // Check input port connections for write mode if (inst["operationMode"] === "write") { @@ -71,6 +87,17 @@ function validate(inst, report) { return; } + // Two blocks on one shared memVar object cannot hold different names. + const sharedPeers = getSharedMemVarPeers(inst); + if (sharedPeers.length > 0) { + const names = sharedPeers.map(p => p.$name).join(", "); + report.logWarning( + `Shares one Memory Variable ('${referencedMemVar.labelName}') with ${names}. Setting labelName through any one block's memVar renames the variable for every block that shares it. Use separate Memory Variable instances and select them with symbolSelect`, + inst, + "symbolSelect" + ); + } + // Validate data size if (inst["dataSize"] < 1 || inst["dataSize"] > 8) { report.logError("Data size must be between 1 and 8 bytes", inst, "dataSize"); @@ -364,6 +391,34 @@ Result: Writes 16 bytes to largeBuffer[0-15] using multiple registers - Input 1: data (value to store) - Input 2: offset (only in Register Input mode) +### Two buffers: use explicit Memory Variable instances + +Each Memory Access block's nested memVar is ONE shared object. Setting labelName through one block renames it for every block sharing it, so the second assignment silently wins and the first buffer is never reserved: + +\\\`\\\`\\\`javascript +// WRONG: both blocks end up on out_buf, in_buf is never reserved +rd.memVar.labelName = "in_buf"; +wr.memVar.labelName = "out_buf"; +\\\`\\\`\\\` + +Create one Memory Variable instance per buffer and assign it by reference: + +\\\`\\\`\\\`javascript +const memory_reserve = scripting.addModule("/pru_blocks/utils/memory_variable_block", {}, false); +const in_buf = memory_reserve.addInstance(); +in_buf.$name = "In_Buf"; +in_buf.labelName = "in_buf"; +in_buf.sizeInBytes = 64; +const out_buf = memory_reserve.addInstance(); +out_buf.$name = "Out_Buf"; +out_buf.labelName = "out_buf"; +out_buf.sizeInBytes = 64; +rd.memVar = in_buf; +wr.memVar = out_buf; +\\\`\\\`\\\` + +The block raises a validation warning when two Memory Access blocks share one memVar. + ### Important Notes 1. **Symbol Resolution**: The linker resolves the actual memory address at link time. The symbol points to the memory location defined by the Memory Variable block. diff --git a/.metadata/sysconfig/.meta/pru_blocks/utils/memory_variable_block.syscfg.js b/.metadata/sysconfig/.meta/pru_blocks/utils/memory_variable_block.syscfg.js index 13c3721..d88ce01 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/utils/memory_variable_block.syscfg.js +++ b/.metadata/sysconfig/.meta/pru_blocks/utils/memory_variable_block.syscfg.js @@ -6,6 +6,13 @@ */ function validate(inst, report) { + // labelName is emitted as a .usect reservation label; reject clpru reserved identifiers via the shared helper. + if (typeof system !== "undefined" && system.getScript) { + var reservedNames = system.getScript("/pru_blocks/common/pru_reserved_names.js"); + if (reservedNames && typeof reservedNames.validateSymbolField === "function") { + reservedNames.validateSymbolField(inst, report, "labelName", "This symbol is emitted as a .usect reservation label."); + } + } // Validate label name if (!inst.labelName || inst.labelName.trim() === "") { report.logError("Label name cannot be empty", inst, "labelName"); diff --git a/examples/conditional/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd b/examples/conditional/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd index 4a9418a..9c12a12 100644 --- a/examples/conditional/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd +++ b/examples/conditional/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd @@ -41,6 +41,8 @@ SECTIONS { .stack > PRU0_DMEM_0, PAGE 1 .bss > PRU0_DMEM_0, PAGE 1 .udmem > PRU0_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 + .udmem > PRU0_DMEM_0, PAGE 1 .initdmem > PRU0_DMEM_0, PAGE 1 .cio > PRU0_DMEM_0, PAGE 1 .data > PRU0_DMEM_0, PAGE 1 diff --git a/examples/conditional/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd b/examples/conditional/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd index 5d7910e..516c458 100644 --- a/examples/conditional/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd +++ b/examples/conditional/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd @@ -41,6 +41,8 @@ SECTIONS { .stack > PRU0_DMEM_0, PAGE 1 .bss > PRU0_DMEM_0, PAGE 1 .udmem > PRU0_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 + .udmem > PRU0_DMEM_0, PAGE 1 .initdmem > PRU0_DMEM_0, PAGE 1 .cio > PRU0_DMEM_0, PAGE 1 .data > PRU0_DMEM_0, PAGE 1 diff --git a/examples/empty/firmware/am243x-evm/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd b/examples/empty/firmware/am243x-evm/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd index ee5e44a..dbaad76 100644 --- a/examples/empty/firmware/am243x-evm/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd +++ b/examples/empty/firmware/am243x-evm/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd @@ -40,6 +40,8 @@ SECTIONS { .text > PRU_IMEM, PAGE 0 .stack > PRU0_DMEM_0, PAGE 1 .bss > PRU0_DMEM_0, PAGE 1 + .udmem > PRU0_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 .cio > PRU0_DMEM_0, PAGE 1 .data > PRU0_DMEM_0, PAGE 1 .switch > PRU0_DMEM_0, PAGE 1 diff --git a/examples/empty/firmware/am243x-evm/icss_g0_pru1_fw/ti-pru-cgt/linker.cmd b/examples/empty/firmware/am243x-evm/icss_g0_pru1_fw/ti-pru-cgt/linker.cmd index 47452ac..e157d56 100644 --- a/examples/empty/firmware/am243x-evm/icss_g0_pru1_fw/ti-pru-cgt/linker.cmd +++ b/examples/empty/firmware/am243x-evm/icss_g0_pru1_fw/ti-pru-cgt/linker.cmd @@ -40,6 +40,8 @@ SECTIONS { .text > PRU_IMEM, PAGE 0 .stack > PRU1_DMEM_1, PAGE 1 .bss > PRU1_DMEM_1, PAGE 1 + .udmem > PRU1_DMEM_1, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 .cio > PRU1_DMEM_1, PAGE 1 .data > PRU1_DMEM_1, PAGE 1 .switch > PRU1_DMEM_1, PAGE 1 diff --git a/examples/empty/firmware/am243x-evm/icss_g0_rtu_pru0_fw/ti-pru-cgt/linker.cmd b/examples/empty/firmware/am243x-evm/icss_g0_rtu_pru0_fw/ti-pru-cgt/linker.cmd index 8cc87ab..565f803 100644 --- a/examples/empty/firmware/am243x-evm/icss_g0_rtu_pru0_fw/ti-pru-cgt/linker.cmd +++ b/examples/empty/firmware/am243x-evm/icss_g0_rtu_pru0_fw/ti-pru-cgt/linker.cmd @@ -40,6 +40,8 @@ SECTIONS { .text > RTU_IMEM, PAGE 0 .stack > RTU0_DMEM_0, PAGE 1 .bss > RTU0_DMEM_0, PAGE 1 + .udmem > RTU0_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 .cio > RTU0_DMEM_0, PAGE 1 .data > RTU0_DMEM_0, PAGE 1 .switch > RTU0_DMEM_0, PAGE 1 diff --git a/examples/empty/firmware/am243x-evm/icss_g0_rtu_pru1_fw/ti-pru-cgt/linker.cmd b/examples/empty/firmware/am243x-evm/icss_g0_rtu_pru1_fw/ti-pru-cgt/linker.cmd index 7a0cbba..f9c36a6 100644 --- a/examples/empty/firmware/am243x-evm/icss_g0_rtu_pru1_fw/ti-pru-cgt/linker.cmd +++ b/examples/empty/firmware/am243x-evm/icss_g0_rtu_pru1_fw/ti-pru-cgt/linker.cmd @@ -40,6 +40,8 @@ SECTIONS { .text > RTU_IMEM, PAGE 0 .stack > RTU1_DMEM_1, PAGE 1 .bss > RTU1_DMEM_1, PAGE 1 + .udmem > RTU1_DMEM_1, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 .cio > RTU1_DMEM_1, PAGE 1 .data > RTU1_DMEM_1, PAGE 1 .switch > RTU1_DMEM_1, PAGE 1 diff --git a/examples/empty/firmware/am243x-evm/icss_g0_tx_pru0_fw/ti-pru-cgt/linker.cmd b/examples/empty/firmware/am243x-evm/icss_g0_tx_pru0_fw/ti-pru-cgt/linker.cmd index 9f5bb20..e080814 100644 --- a/examples/empty/firmware/am243x-evm/icss_g0_tx_pru0_fw/ti-pru-cgt/linker.cmd +++ b/examples/empty/firmware/am243x-evm/icss_g0_tx_pru0_fw/ti-pru-cgt/linker.cmd @@ -40,6 +40,8 @@ SECTIONS { .text > TX_PRU_IMEM, PAGE 0 .stack > TX_PRU0_DMEM_0, PAGE 1 .bss > TX_PRU0_DMEM_0, PAGE 1 + .udmem > TX_PRU0_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 .cio > TX_PRU0_DMEM_0, PAGE 1 .data > TX_PRU0_DMEM_0, PAGE 1 .switch > TX_PRU0_DMEM_0, PAGE 1 diff --git a/examples/empty/firmware/am243x-evm/icss_g0_tx_pru1_fw/ti-pru-cgt/linker.cmd b/examples/empty/firmware/am243x-evm/icss_g0_tx_pru1_fw/ti-pru-cgt/linker.cmd index 01e0e32..27780b9 100644 --- a/examples/empty/firmware/am243x-evm/icss_g0_tx_pru1_fw/ti-pru-cgt/linker.cmd +++ b/examples/empty/firmware/am243x-evm/icss_g0_tx_pru1_fw/ti-pru-cgt/linker.cmd @@ -40,6 +40,8 @@ SECTIONS { .text > TX_PRU_IMEM, PAGE 0 .stack > TX_PRU1_DMEM_1, PAGE 1 .bss > TX_PRU1_DMEM_1, PAGE 1 + .udmem > TX_PRU1_DMEM_1, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 .cio > TX_PRU1_DMEM_1, PAGE 1 .data > TX_PRU1_DMEM_1, PAGE 1 .switch > TX_PRU1_DMEM_1, PAGE 1 diff --git a/examples/empty/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd b/examples/empty/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd index ee5e44a..dbaad76 100644 --- a/examples/empty/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd +++ b/examples/empty/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd @@ -40,6 +40,8 @@ SECTIONS { .text > PRU_IMEM, PAGE 0 .stack > PRU0_DMEM_0, PAGE 1 .bss > PRU0_DMEM_0, PAGE 1 + .udmem > PRU0_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 .cio > PRU0_DMEM_0, PAGE 1 .data > PRU0_DMEM_0, PAGE 1 .switch > PRU0_DMEM_0, PAGE 1 diff --git a/examples/empty/firmware/am243x-lp/icss_g0_pru1_fw/ti-pru-cgt/linker.cmd b/examples/empty/firmware/am243x-lp/icss_g0_pru1_fw/ti-pru-cgt/linker.cmd index 47452ac..e157d56 100644 --- a/examples/empty/firmware/am243x-lp/icss_g0_pru1_fw/ti-pru-cgt/linker.cmd +++ b/examples/empty/firmware/am243x-lp/icss_g0_pru1_fw/ti-pru-cgt/linker.cmd @@ -40,6 +40,8 @@ SECTIONS { .text > PRU_IMEM, PAGE 0 .stack > PRU1_DMEM_1, PAGE 1 .bss > PRU1_DMEM_1, PAGE 1 + .udmem > PRU1_DMEM_1, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 .cio > PRU1_DMEM_1, PAGE 1 .data > PRU1_DMEM_1, PAGE 1 .switch > PRU1_DMEM_1, PAGE 1 diff --git a/examples/empty/firmware/am243x-lp/icss_g0_rtu_pru0_fw/ti-pru-cgt/linker.cmd b/examples/empty/firmware/am243x-lp/icss_g0_rtu_pru0_fw/ti-pru-cgt/linker.cmd index 8cc87ab..565f803 100644 --- a/examples/empty/firmware/am243x-lp/icss_g0_rtu_pru0_fw/ti-pru-cgt/linker.cmd +++ b/examples/empty/firmware/am243x-lp/icss_g0_rtu_pru0_fw/ti-pru-cgt/linker.cmd @@ -40,6 +40,8 @@ SECTIONS { .text > RTU_IMEM, PAGE 0 .stack > RTU0_DMEM_0, PAGE 1 .bss > RTU0_DMEM_0, PAGE 1 + .udmem > RTU0_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 .cio > RTU0_DMEM_0, PAGE 1 .data > RTU0_DMEM_0, PAGE 1 .switch > RTU0_DMEM_0, PAGE 1 diff --git a/examples/empty/firmware/am243x-lp/icss_g0_rtu_pru1_fw/ti-pru-cgt/linker.cmd b/examples/empty/firmware/am243x-lp/icss_g0_rtu_pru1_fw/ti-pru-cgt/linker.cmd index 7a0cbba..f9c36a6 100644 --- a/examples/empty/firmware/am243x-lp/icss_g0_rtu_pru1_fw/ti-pru-cgt/linker.cmd +++ b/examples/empty/firmware/am243x-lp/icss_g0_rtu_pru1_fw/ti-pru-cgt/linker.cmd @@ -40,6 +40,8 @@ SECTIONS { .text > RTU_IMEM, PAGE 0 .stack > RTU1_DMEM_1, PAGE 1 .bss > RTU1_DMEM_1, PAGE 1 + .udmem > RTU1_DMEM_1, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 .cio > RTU1_DMEM_1, PAGE 1 .data > RTU1_DMEM_1, PAGE 1 .switch > RTU1_DMEM_1, PAGE 1 diff --git a/examples/empty/firmware/am243x-lp/icss_g0_tx_pru0_fw/ti-pru-cgt/linker.cmd b/examples/empty/firmware/am243x-lp/icss_g0_tx_pru0_fw/ti-pru-cgt/linker.cmd index 9f5bb20..e080814 100644 --- a/examples/empty/firmware/am243x-lp/icss_g0_tx_pru0_fw/ti-pru-cgt/linker.cmd +++ b/examples/empty/firmware/am243x-lp/icss_g0_tx_pru0_fw/ti-pru-cgt/linker.cmd @@ -40,6 +40,8 @@ SECTIONS { .text > TX_PRU_IMEM, PAGE 0 .stack > TX_PRU0_DMEM_0, PAGE 1 .bss > TX_PRU0_DMEM_0, PAGE 1 + .udmem > TX_PRU0_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 .cio > TX_PRU0_DMEM_0, PAGE 1 .data > TX_PRU0_DMEM_0, PAGE 1 .switch > TX_PRU0_DMEM_0, PAGE 1 diff --git a/examples/empty/firmware/am243x-lp/icss_g0_tx_pru1_fw/ti-pru-cgt/linker.cmd b/examples/empty/firmware/am243x-lp/icss_g0_tx_pru1_fw/ti-pru-cgt/linker.cmd index 01e0e32..27780b9 100644 --- a/examples/empty/firmware/am243x-lp/icss_g0_tx_pru1_fw/ti-pru-cgt/linker.cmd +++ b/examples/empty/firmware/am243x-lp/icss_g0_tx_pru1_fw/ti-pru-cgt/linker.cmd @@ -40,6 +40,8 @@ SECTIONS { .text > TX_PRU_IMEM, PAGE 0 .stack > TX_PRU1_DMEM_1, PAGE 1 .bss > TX_PRU1_DMEM_1, PAGE 1 + .udmem > TX_PRU1_DMEM_1, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 .cio > TX_PRU1_DMEM_1, PAGE 1 .data > TX_PRU1_DMEM_1, PAGE 1 .switch > TX_PRU1_DMEM_1, PAGE 1 diff --git a/examples/empty/firmware/am261x-lp/icss_m0_pru0_fw/ti-pru-cgt/linker.cmd b/examples/empty/firmware/am261x-lp/icss_m0_pru0_fw/ti-pru-cgt/linker.cmd index ee5e44a..dbaad76 100644 --- a/examples/empty/firmware/am261x-lp/icss_m0_pru0_fw/ti-pru-cgt/linker.cmd +++ b/examples/empty/firmware/am261x-lp/icss_m0_pru0_fw/ti-pru-cgt/linker.cmd @@ -40,6 +40,8 @@ SECTIONS { .text > PRU_IMEM, PAGE 0 .stack > PRU0_DMEM_0, PAGE 1 .bss > PRU0_DMEM_0, PAGE 1 + .udmem > PRU0_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 .cio > PRU0_DMEM_0, PAGE 1 .data > PRU0_DMEM_0, PAGE 1 .switch > PRU0_DMEM_0, PAGE 1 diff --git a/examples/empty/firmware/am261x-lp/icss_m0_pru1_fw/ti-pru-cgt/linker.cmd b/examples/empty/firmware/am261x-lp/icss_m0_pru1_fw/ti-pru-cgt/linker.cmd index fbcb353..7a408fa 100644 --- a/examples/empty/firmware/am261x-lp/icss_m0_pru1_fw/ti-pru-cgt/linker.cmd +++ b/examples/empty/firmware/am261x-lp/icss_m0_pru1_fw/ti-pru-cgt/linker.cmd @@ -40,6 +40,8 @@ SECTIONS { .text > PRU_IMEM, PAGE 0 .stack > PRU1_DMEM_0, PAGE 1 .bss > PRU1_DMEM_0, PAGE 1 + .udmem > PRU1_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 .cio > PRU1_DMEM_0, PAGE 1 .data > PRU1_DMEM_0, PAGE 1 .switch > PRU1_DMEM_0, PAGE 1 diff --git a/examples/empty/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd b/examples/empty/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd index a6d3db5..21608dd 100644 --- a/examples/empty/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd +++ b/examples/empty/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd @@ -40,6 +40,8 @@ SECTIONS { .text > PRU_IMEM, PAGE 0 .stack > PRU0_DMEM_0, PAGE 1 .bss > PRU0_DMEM_0, PAGE 1 + .udmem > PRU0_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 .cio > PRU0_DMEM_0, PAGE 1 .data > PRU0_DMEM_0, PAGE 1 .switch > PRU0_DMEM_0, PAGE 1 diff --git a/examples/empty/firmware/am261x-lp/icss_m1_pru1_fw/ti-pru-cgt/linker.cmd b/examples/empty/firmware/am261x-lp/icss_m1_pru1_fw/ti-pru-cgt/linker.cmd index fbcb353..7a408fa 100644 --- a/examples/empty/firmware/am261x-lp/icss_m1_pru1_fw/ti-pru-cgt/linker.cmd +++ b/examples/empty/firmware/am261x-lp/icss_m1_pru1_fw/ti-pru-cgt/linker.cmd @@ -40,6 +40,8 @@ SECTIONS { .text > PRU_IMEM, PAGE 0 .stack > PRU1_DMEM_0, PAGE 1 .bss > PRU1_DMEM_0, PAGE 1 + .udmem > PRU1_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 .cio > PRU1_DMEM_0, PAGE 1 .data > PRU1_DMEM_0, PAGE 1 .switch > PRU1_DMEM_0, PAGE 1 diff --git a/examples/empty/firmware/am64x-evm/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd b/examples/empty/firmware/am64x-evm/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd index 38c6db7..c0fbaa7 100644 --- a/examples/empty/firmware/am64x-evm/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd +++ b/examples/empty/firmware/am64x-evm/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd @@ -40,6 +40,8 @@ SECTIONS { .text > PRU_IMEM, PAGE 0 .stack > PRU0_DMEM_0, PAGE 1 .bss > PRU0_DMEM_0, PAGE 1 + .udmem > PRU0_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 .cio > PRU0_DMEM_0, PAGE 1 .data > PRU0_DMEM_0, PAGE 1 .switch > PRU0_DMEM_0, PAGE 1 diff --git a/examples/empty/firmware/am64x-evm/icss_g0_pru1_fw/ti-pru-cgt/linker.cmd b/examples/empty/firmware/am64x-evm/icss_g0_pru1_fw/ti-pru-cgt/linker.cmd index e2533c2..04ca530 100644 --- a/examples/empty/firmware/am64x-evm/icss_g0_pru1_fw/ti-pru-cgt/linker.cmd +++ b/examples/empty/firmware/am64x-evm/icss_g0_pru1_fw/ti-pru-cgt/linker.cmd @@ -40,6 +40,8 @@ SECTIONS { .text > PRU_IMEM, PAGE 0 .stack > PRU1_DMEM_1, PAGE 1 .bss > PRU1_DMEM_1, PAGE 1 + .udmem > PRU1_DMEM_1, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 .cio > PRU1_DMEM_1, PAGE 1 .data > PRU1_DMEM_1, PAGE 1 .switch > PRU1_DMEM_1, PAGE 1 diff --git a/examples/empty/firmware/am64x-evm/icss_g0_rtu_pru0_fw/ti-pru-cgt/linker.cmd b/examples/empty/firmware/am64x-evm/icss_g0_rtu_pru0_fw/ti-pru-cgt/linker.cmd index 01e4fd0..ccc9ffc 100644 --- a/examples/empty/firmware/am64x-evm/icss_g0_rtu_pru0_fw/ti-pru-cgt/linker.cmd +++ b/examples/empty/firmware/am64x-evm/icss_g0_rtu_pru0_fw/ti-pru-cgt/linker.cmd @@ -40,6 +40,8 @@ SECTIONS { .text > RTU_IMEM, PAGE 0 .stack > RTU0_DMEM_0, PAGE 1 .bss > RTU0_DMEM_0, PAGE 1 + .udmem > RTU0_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 .cio > RTU0_DMEM_0, PAGE 1 .data > RTU0_DMEM_0, PAGE 1 .switch > RTU0_DMEM_0, PAGE 1 diff --git a/examples/empty/firmware/am64x-evm/icss_g0_rtu_pru1_fw/ti-pru-cgt/linker.cmd b/examples/empty/firmware/am64x-evm/icss_g0_rtu_pru1_fw/ti-pru-cgt/linker.cmd index b525561..5b5c23a 100644 --- a/examples/empty/firmware/am64x-evm/icss_g0_rtu_pru1_fw/ti-pru-cgt/linker.cmd +++ b/examples/empty/firmware/am64x-evm/icss_g0_rtu_pru1_fw/ti-pru-cgt/linker.cmd @@ -40,6 +40,8 @@ SECTIONS { .text > RTU_IMEM, PAGE 0 .stack > RTU1_DMEM_1, PAGE 1 .bss > RTU1_DMEM_1, PAGE 1 + .udmem > RTU1_DMEM_1, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 .cio > RTU1_DMEM_1, PAGE 1 .data > RTU1_DMEM_1, PAGE 1 .switch > RTU1_DMEM_1, PAGE 1 diff --git a/examples/empty/firmware/am64x-evm/icss_g0_tx_pru0_fw/ti-pru-cgt/linker.cmd b/examples/empty/firmware/am64x-evm/icss_g0_tx_pru0_fw/ti-pru-cgt/linker.cmd index 0007da3..5db1ac8 100644 --- a/examples/empty/firmware/am64x-evm/icss_g0_tx_pru0_fw/ti-pru-cgt/linker.cmd +++ b/examples/empty/firmware/am64x-evm/icss_g0_tx_pru0_fw/ti-pru-cgt/linker.cmd @@ -40,6 +40,8 @@ SECTIONS { .text > TX_PRU_IMEM, PAGE 0 .stack > TX_PRU0_DMEM_0, PAGE 1 .bss > TX_PRU0_DMEM_0, PAGE 1 + .udmem > TX_PRU0_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 .cio > TX_PRU0_DMEM_0, PAGE 1 .data > TX_PRU0_DMEM_0, PAGE 1 .switch > TX_PRU0_DMEM_0, PAGE 1 diff --git a/examples/empty/firmware/am64x-evm/icss_g0_tx_pru1_fw/ti-pru-cgt/linker.cmd b/examples/empty/firmware/am64x-evm/icss_g0_tx_pru1_fw/ti-pru-cgt/linker.cmd index 87d2670..5b0238e 100644 --- a/examples/empty/firmware/am64x-evm/icss_g0_tx_pru1_fw/ti-pru-cgt/linker.cmd +++ b/examples/empty/firmware/am64x-evm/icss_g0_tx_pru1_fw/ti-pru-cgt/linker.cmd @@ -40,6 +40,8 @@ SECTIONS { .text > TX_PRU_IMEM, PAGE 0 .stack > TX_PRU1_DMEM_1, PAGE 1 .bss > TX_PRU1_DMEM_1, PAGE 1 + .udmem > TX_PRU1_DMEM_1, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 .cio > TX_PRU1_DMEM_1, PAGE 1 .data > TX_PRU1_DMEM_1, PAGE 1 .switch > TX_PRU1_DMEM_1, PAGE 1 diff --git a/examples/gray_encoder_4_bit/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd b/examples/gray_encoder_4_bit/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd index 4a9418a..9c12a12 100644 --- a/examples/gray_encoder_4_bit/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd +++ b/examples/gray_encoder_4_bit/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd @@ -41,6 +41,8 @@ SECTIONS { .stack > PRU0_DMEM_0, PAGE 1 .bss > PRU0_DMEM_0, PAGE 1 .udmem > PRU0_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 + .udmem > PRU0_DMEM_0, PAGE 1 .initdmem > PRU0_DMEM_0, PAGE 1 .cio > PRU0_DMEM_0, PAGE 1 .data > PRU0_DMEM_0, PAGE 1 diff --git a/examples/gray_encoder_4_bit/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd b/examples/gray_encoder_4_bit/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd index 5d7910e..516c458 100644 --- a/examples/gray_encoder_4_bit/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd +++ b/examples/gray_encoder_4_bit/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd @@ -41,6 +41,8 @@ SECTIONS { .stack > PRU0_DMEM_0, PAGE 1 .bss > PRU0_DMEM_0, PAGE 1 .udmem > PRU0_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 + .udmem > PRU0_DMEM_0, PAGE 1 .initdmem > PRU0_DMEM_0, PAGE 1 .cio > PRU0_DMEM_0, PAGE 1 .data > PRU0_DMEM_0, PAGE 1 diff --git a/examples/incremental_spi/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd b/examples/incremental_spi/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd index 4a9418a..9c12a12 100644 --- a/examples/incremental_spi/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd +++ b/examples/incremental_spi/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd @@ -41,6 +41,8 @@ SECTIONS { .stack > PRU0_DMEM_0, PAGE 1 .bss > PRU0_DMEM_0, PAGE 1 .udmem > PRU0_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 + .udmem > PRU0_DMEM_0, PAGE 1 .initdmem > PRU0_DMEM_0, PAGE 1 .cio > PRU0_DMEM_0, PAGE 1 .data > PRU0_DMEM_0, PAGE 1 diff --git a/examples/incremental_spi/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd b/examples/incremental_spi/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd index 5d7910e..516c458 100644 --- a/examples/incremental_spi/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd +++ b/examples/incremental_spi/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd @@ -41,6 +41,8 @@ SECTIONS { .stack > PRU0_DMEM_0, PAGE 1 .bss > PRU0_DMEM_0, PAGE 1 .udmem > PRU0_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 + .udmem > PRU0_DMEM_0, PAGE 1 .initdmem > PRU0_DMEM_0, PAGE 1 .cio > PRU0_DMEM_0, PAGE 1 .data > PRU0_DMEM_0, PAGE 1 diff --git a/examples/spi_10mhz/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd b/examples/spi_10mhz/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd index 5d7910e..516c458 100644 --- a/examples/spi_10mhz/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd +++ b/examples/spi_10mhz/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd @@ -41,6 +41,8 @@ SECTIONS { .stack > PRU0_DMEM_0, PAGE 1 .bss > PRU0_DMEM_0, PAGE 1 .udmem > PRU0_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 + .udmem > PRU0_DMEM_0, PAGE 1 .initdmem > PRU0_DMEM_0, PAGE 1 .cio > PRU0_DMEM_0, PAGE 1 .data > PRU0_DMEM_0, PAGE 1 diff --git a/examples/spi_10mhz/firmware/am261x-lp/icss_m1_pru1_fw/ti-pru-cgt/linker.cmd b/examples/spi_10mhz/firmware/am261x-lp/icss_m1_pru1_fw/ti-pru-cgt/linker.cmd index 7340f73..d71396e 100644 --- a/examples/spi_10mhz/firmware/am261x-lp/icss_m1_pru1_fw/ti-pru-cgt/linker.cmd +++ b/examples/spi_10mhz/firmware/am261x-lp/icss_m1_pru1_fw/ti-pru-cgt/linker.cmd @@ -41,6 +41,8 @@ SECTIONS { .stack > PRU1_DMEM_0, PAGE 1 .bss > PRU1_DMEM_0, PAGE 1 .udmem > PRU1_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 + .udmem > PRU1_DMEM_0, PAGE 1 .initdmem > PRU1_DMEM_0, PAGE 1 .cio > PRU1_DMEM_0, PAGE 1 .data > PRU1_DMEM_0, PAGE 1 diff --git a/examples/spi_loopback/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd b/examples/spi_loopback/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd index 4a9418a..9c12a12 100644 --- a/examples/spi_loopback/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd +++ b/examples/spi_loopback/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd @@ -41,6 +41,8 @@ SECTIONS { .stack > PRU0_DMEM_0, PAGE 1 .bss > PRU0_DMEM_0, PAGE 1 .udmem > PRU0_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 + .udmem > PRU0_DMEM_0, PAGE 1 .initdmem > PRU0_DMEM_0, PAGE 1 .cio > PRU0_DMEM_0, PAGE 1 .data > PRU0_DMEM_0, PAGE 1 diff --git a/examples/spi_loopback/firmware/am243x-lp/icss_g0_pru1_fw/ti-pru-cgt/linker.cmd b/examples/spi_loopback/firmware/am243x-lp/icss_g0_pru1_fw/ti-pru-cgt/linker.cmd index f98cc9a..92f9306 100644 --- a/examples/spi_loopback/firmware/am243x-lp/icss_g0_pru1_fw/ti-pru-cgt/linker.cmd +++ b/examples/spi_loopback/firmware/am243x-lp/icss_g0_pru1_fw/ti-pru-cgt/linker.cmd @@ -41,6 +41,8 @@ SECTIONS { .stack > PRU1_DMEM_1, PAGE 1 .bss > PRU1_DMEM_1, PAGE 1 .udmem > PRU1_DMEM_1, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 + .udmem > PRU1_DMEM_1, PAGE 1 .initdmem > PRU1_DMEM_1, PAGE 1 .cio > PRU1_DMEM_1, PAGE 1 .data > PRU1_DMEM_1, PAGE 1 diff --git a/examples/spi_loopback/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd b/examples/spi_loopback/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd index 5d7910e..516c458 100644 --- a/examples/spi_loopback/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd +++ b/examples/spi_loopback/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd @@ -41,6 +41,8 @@ SECTIONS { .stack > PRU0_DMEM_0, PAGE 1 .bss > PRU0_DMEM_0, PAGE 1 .udmem > PRU0_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 + .udmem > PRU0_DMEM_0, PAGE 1 .initdmem > PRU0_DMEM_0, PAGE 1 .cio > PRU0_DMEM_0, PAGE 1 .data > PRU0_DMEM_0, PAGE 1 diff --git a/examples/spi_loopback/firmware/am261x-lp/icss_m1_pru1_fw/ti-pru-cgt/linker.cmd b/examples/spi_loopback/firmware/am261x-lp/icss_m1_pru1_fw/ti-pru-cgt/linker.cmd index 7340f73..d71396e 100644 --- a/examples/spi_loopback/firmware/am261x-lp/icss_m1_pru1_fw/ti-pru-cgt/linker.cmd +++ b/examples/spi_loopback/firmware/am261x-lp/icss_m1_pru1_fw/ti-pru-cgt/linker.cmd @@ -41,6 +41,8 @@ SECTIONS { .stack > PRU1_DMEM_0, PAGE 1 .bss > PRU1_DMEM_0, PAGE 1 .udmem > PRU1_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 + .udmem > PRU1_DMEM_0, PAGE 1 .initdmem > PRU1_DMEM_0, PAGE 1 .cio > PRU1_DMEM_0, PAGE 1 .data > PRU1_DMEM_0, PAGE 1 diff --git a/examples/uart_crc/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd b/examples/uart_crc/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd index 4a9418a..9c12a12 100644 --- a/examples/uart_crc/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd +++ b/examples/uart_crc/firmware/am243x-lp/icss_g0_pru0_fw/ti-pru-cgt/linker.cmd @@ -41,6 +41,8 @@ SECTIONS { .stack > PRU0_DMEM_0, PAGE 1 .bss > PRU0_DMEM_0, PAGE 1 .udmem > PRU0_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 + .udmem > PRU0_DMEM_0, PAGE 1 .initdmem > PRU0_DMEM_0, PAGE 1 .cio > PRU0_DMEM_0, PAGE 1 .data > PRU0_DMEM_0, PAGE 1 diff --git a/examples/uart_crc/firmware/am243x-lp/icss_g0_pru1_fw/example.syscfg b/examples/uart_crc/firmware/am243x-lp/icss_g0_pru1_fw/example.syscfg index 1bd7d2c..d6b0c1f 100644 --- a/examples/uart_crc/firmware/am243x-lp/icss_g0_pru1_fw/example.syscfg +++ b/examples/uart_crc/firmware/am243x-lp/icss_g0_pru1_fw/example.syscfg @@ -22,6 +22,7 @@ const uart_rx_op1 = uart_rx_op.addInstance(); const memory_access_block = scripting.addModule("/pru_blocks/utils/memory_access_block", {}, false); const memory_access_block1 = memory_access_block.addInstance(); const memory_access_block2 = memory_access_block.addInstance(); +const memory_access_block3 = memory_access_block.addInstance(); /** * Write custom configuration values to the imported modules. @@ -47,11 +48,15 @@ memory_access_block1.dataSize = 2; memory_access_block2.$name = "Memory_Access_1"; memory_access_block2.dataSize = 1; +memory_access_block3.$name = "Received_Frame"; +memory_access_block3.dataSize = 2; + const memory_variable_block = scripting.addModule("/pru_blocks/utils/memory_variable_block", {}, false); const memory_variable_block1 = memory_variable_block.addInstance({}, false); memory_variable_block1.$name = "Memory_Variable_0"; memory_access_block1.memVar = memory_variable_block1; memory_access_block2.memVar = memory_variable_block1; +memory_access_block3.memVar = memory_variable_block1; memory_variable_block1.sizeInBytes = 4; /** @@ -60,7 +65,8 @@ memory_variable_block1.sizeInBytes = 4; scripting.connect(crc_block1, "input1", load_constant_block1, "output1"); scripting.connect(crc_block1, "input2", memory_access_block2, "output1"); scripting.connect(crc_block1, "prev", memory_access_block2, "next"); -scripting.connect(crc_block1, "next", flow_control_block1, "prev"); +scripting.connect(crc_block1, "next", memory_access_block3, "prev"); +scripting.connect(memory_access_block3, "next", flow_control_block1, "prev"); scripting.connect(uart_rx_op1, "output1", memory_access_block1, "input1"); scripting.connect(uart_rx_op1, "prev", uart_config1, "next"); scripting.connect(uart_rx_op1, "next", memory_access_block1, "prev"); @@ -76,4 +82,5 @@ uart_rx_op1.$position = [-290,-80]; uart_config1.$position = [-410,-85]; memory_access_block1.$position = [-150,-65]; memory_access_block2.$position = [-10,-25]; +memory_access_block3.$position = [285,-25]; memory_variable_block1.$position = [-385,-250]; diff --git a/examples/uart_crc/firmware/am243x-lp/icss_g0_pru1_fw/ti-pru-cgt/linker.cmd b/examples/uart_crc/firmware/am243x-lp/icss_g0_pru1_fw/ti-pru-cgt/linker.cmd index f98cc9a..92f9306 100644 --- a/examples/uart_crc/firmware/am243x-lp/icss_g0_pru1_fw/ti-pru-cgt/linker.cmd +++ b/examples/uart_crc/firmware/am243x-lp/icss_g0_pru1_fw/ti-pru-cgt/linker.cmd @@ -41,6 +41,8 @@ SECTIONS { .stack > PRU1_DMEM_1, PAGE 1 .bss > PRU1_DMEM_1, PAGE 1 .udmem > PRU1_DMEM_1, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 + .udmem > PRU1_DMEM_1, PAGE 1 .initdmem > PRU1_DMEM_1, PAGE 1 .cio > PRU1_DMEM_1, PAGE 1 .data > PRU1_DMEM_1, PAGE 1 diff --git a/examples/uart_crc/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd b/examples/uart_crc/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd index 5d7910e..516c458 100644 --- a/examples/uart_crc/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd +++ b/examples/uart_crc/firmware/am261x-lp/icss_m1_pru0_fw/ti-pru-cgt/linker.cmd @@ -41,6 +41,8 @@ SECTIONS { .stack > PRU0_DMEM_0, PAGE 1 .bss > PRU0_DMEM_0, PAGE 1 .udmem > PRU0_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 + .udmem > PRU0_DMEM_0, PAGE 1 .initdmem > PRU0_DMEM_0, PAGE 1 .cio > PRU0_DMEM_0, PAGE 1 .data > PRU0_DMEM_0, PAGE 1 diff --git a/examples/uart_crc/firmware/am261x-lp/icss_m1_pru1_fw/example.syscfg b/examples/uart_crc/firmware/am261x-lp/icss_m1_pru1_fw/example.syscfg index a42173c..3aa35ca 100644 --- a/examples/uart_crc/firmware/am261x-lp/icss_m1_pru1_fw/example.syscfg +++ b/examples/uart_crc/firmware/am261x-lp/icss_m1_pru1_fw/example.syscfg @@ -22,6 +22,7 @@ const uart_rx_op1 = uart_rx_op.addInstance(); const memory_access_block = scripting.addModule("/pru_blocks/utils/memory_access_block", {}, false); const memory_access_block1 = memory_access_block.addInstance(); const memory_access_block2 = memory_access_block.addInstance(); +const memory_access_block3 = memory_access_block.addInstance(); /** * Write custom configuration values to the imported modules. @@ -47,11 +48,15 @@ memory_access_block1.dataSize = 2; memory_access_block2.$name = "Memory_Access_1"; memory_access_block2.dataSize = 1; +memory_access_block3.$name = "Received_Frame"; +memory_access_block3.dataSize = 2; + const memory_variable_block = scripting.addModule("/pru_blocks/utils/memory_variable_block", {}, false); const memory_variable_block1 = memory_variable_block.addInstance({}, false); memory_variable_block1.$name = "Memory_Variable_0"; memory_access_block1.memVar = memory_variable_block1; memory_access_block2.memVar = memory_variable_block1; +memory_access_block3.memVar = memory_variable_block1; memory_variable_block1.sizeInBytes = 4; /** @@ -60,7 +65,8 @@ memory_variable_block1.sizeInBytes = 4; scripting.connect(crc_block1, "input1", load_constant_block1, "output1"); scripting.connect(crc_block1, "input2", memory_access_block2, "output1"); scripting.connect(crc_block1, "prev", memory_access_block2, "next"); -scripting.connect(crc_block1, "next", flow_control_block1, "prev"); +scripting.connect(crc_block1, "next", memory_access_block3, "prev"); +scripting.connect(memory_access_block3, "next", flow_control_block1, "prev"); scripting.connect(uart_rx_op1, "output1", memory_access_block1, "input1"); scripting.connect(uart_rx_op1, "prev", uart_config1, "next"); scripting.connect(uart_rx_op1, "next", memory_access_block1, "prev"); @@ -76,4 +82,5 @@ uart_rx_op1.$position = [-290,-80]; uart_config1.$position = [-410,-85]; memory_access_block1.$position = [-150,-65]; memory_access_block2.$position = [-10,-25]; +memory_access_block3.$position = [285,-25]; memory_variable_block1.$position = [-385,-250]; diff --git a/examples/uart_crc/firmware/am261x-lp/icss_m1_pru1_fw/ti-pru-cgt/linker.cmd b/examples/uart_crc/firmware/am261x-lp/icss_m1_pru1_fw/ti-pru-cgt/linker.cmd index 7340f73..d71396e 100644 --- a/examples/uart_crc/firmware/am261x-lp/icss_m1_pru1_fw/ti-pru-cgt/linker.cmd +++ b/examples/uart_crc/firmware/am261x-lp/icss_m1_pru1_fw/ti-pru-cgt/linker.cmd @@ -41,6 +41,8 @@ SECTIONS { .stack > PRU1_DMEM_0, PAGE 1 .bss > PRU1_DMEM_0, PAGE 1 .udmem > PRU1_DMEM_0, PAGE 1 + .usmem > PRU_SHAREDMEM, PAGE 2 + .udmem > PRU1_DMEM_0, PAGE 1 .initdmem > PRU1_DMEM_0, PAGE 1 .cio > PRU1_DMEM_0, PAGE 1 .data > PRU1_DMEM_0, PAGE 1 diff --git a/examples/uart_crc/firmware/main.asm b/examples/uart_crc/firmware/main.asm index 5ab0d6e..85f3f2a 100644 --- a/examples/uart_crc/firmware/main.asm +++ b/examples/uart_crc/firmware/main.asm @@ -32,6 +32,20 @@ ;* MAIN * ;******** +; Registers this file reads after the generated code. The register allocator chooses them, so they are +; named once here. Only a block whose output no other block consumes keeps its register to the end (a +; consumed value is released and its register reused), which is why the pru1 design ends with the +; "Received_Frame" block. + .if $isdefed("PRU0") +; pru0: Memory_Access_3 loads the TX frame {data byte, CRC} from buffer; the UART TX block sends it + .asg R0.b0, TX_DATA_REG ; data byte of the frame that was sent (0xAA) + .elseif $isdefed("PRU1") +; pru1: CRC_0 = CRC computed over the received data byte; Received_Frame = {received data byte, received CRC} + .asg R0.b2, CRC_CALC_REG ; CRC computed by CRC_0 + .asg R0.b0, RX_DATA_REG ; received data byte + .asg R0.b1, CRC_RX_REG ; received CRC + .endif + main: .if $isdefed("PRU0") zero &r0, 120 ; Clear the register space @@ -43,7 +57,7 @@ sysconfig_generated_end: LDI32 R2, 0x48010000 .endif ; Store TX data sent at SMEM[4] using SBBO - SBBO &R0.b1, R2, 4, 1 ; Store R0.b1 at [smem + 0] + SBBO &TX_DATA_REG, R2, 4, 1 halt .elseif $isdefed("PRU1") zero &r0, 120 ; Clear the register space @@ -51,7 +65,7 @@ sysconfig_generated_end: sysconfig_generated_end: ; Store CRC values to SMEM using absolute address 0x30010000 - ; R0.b0 = RX calculated CRC, R0.b1 = TX received CRC + ; CRC_CALC_REG = CRC computed from the received data, CRC_RX_REG = CRC received with it ; Load SMEM base address into R2 .if $isdefed("SOC_AM243X") @@ -61,15 +75,16 @@ sysconfig_generated_end: .endif ; Store RX calculated CRC at SMEM[0] using SBBO - SBBO &R1.b1, R2, 0, 1 ; Store R0.b0 at [smem + 0] + SBBO &CRC_CALC_REG, R2, 0, 1 ; Store TX received CRC at SMEM[1] using SBBO - SBBO &R0.b1, R2, 1, 1 ; Store R0.b1 at [smem + 1] + SBBO &CRC_RX_REG, R2, 1, 1 - SBBO &R0.b0, R2, 3, 1 + ; Store the received data byte at SMEM[3] + SBBO &RX_DATA_REG, R2, 3, 1 ; Compare the two CRC values - QBEQ crc_match, R0.b1, R1.b1 + QBEQ crc_match, CRC_RX_REG, CRC_CALC_REG ; CRC mismatch - store 0xFF at SMEM[2] LDI R3.b0, 0xFF diff --git a/scripts/block_validate_runner.js b/scripts/block_validate_runner.js new file mode 100644 index 0000000..7e56b85 --- /dev/null +++ b/scripts/block_validate_runner.js @@ -0,0 +1,313 @@ +#!/usr/bin/env node +'use strict'; +/* + * block_validate_runner.js — reusable block-validator harness + * + * Generalizes scripts/static_module_validate_runner.js into a reusable + * harness that can exercise ANY block declaring validate(). + * + * That runner covers a single validator (validateDeviceCapabilities in + * pru_blocks_static_module.syscfg.js). This runner sweeps every block that + * declares validate() — see --sweep. + * + * Usage: + * node scripts/block_validate_runner.js '' + * node scripts/block_validate_runner.js --block --inst '' + * node scripts/block_validate_runner.js --inst-file + * node scripts/block_validate_runner.js --sweep + * node scripts/block_validate_runner.js --report + * + * blockPath forms: + * pru_io_blocks/pru_spi_read + * .metadata/sysconfig/.meta/pru_blocks/pru_io_blocks/pru_spi_read.syscfg.js + * pru_blocks_static_module (device-capability static module) + * + * instJson: JSON object for the block instance. Extra keys are ignored by + * validators that don't read them — keep error-case/control arms same-shape: + * only the field under test should differ. + * + * Output (single-block mode): JSON {errors:[], warnings:[], infos:[]} + * Each entry is {message, field} where field may be undefined. + * + * Output (--sweep): JSON array of {block, file, hasValidate, errorCount, warningCount} + * Output (--report): same as --sweep plus {validatorsFound, armedCount} summary + * armed = blocks whose error paths have a committed error-case/control test + * pair (listed in ARMED_BLOCKS below) + * + * Exit code 0 on success, 2 on harness error. + */ + +const fs = require('fs'); +const path = require('path'); +const vm = require('vm'); + +const ROOT = path.join(__dirname, '..'); +const BLOCKS_ROOT = path.join(ROOT, '.metadata', 'sysconfig', '.meta', 'pru_blocks'); + +// ------------------------------------------------------------------ helpers + +function makeStub() { + const fn = function () { return makeStub(); }; + return new Proxy(fn, { + get(_t, prop) { + if (prop === Symbol.toPrimitive || prop === 'toString' || prop === 'valueOf') return () => ''; + if (prop === Symbol.iterator) return undefined; + if (prop === 'then') return undefined; + return makeStub(); + }, + apply() { return makeStub(); } + }); +} + +function resolveBlockPath(blockPath) { + if (!blockPath) return null; + // Absolute or repo-relative .syscfg.js already + if (blockPath.endsWith('.syscfg.js') && fs.existsSync(blockPath)) { + return path.resolve(blockPath); + } + if (blockPath.endsWith('.syscfg.js') && fs.existsSync(path.join(ROOT, blockPath))) { + return path.join(ROOT, blockPath); + } + // Try under BLOCKS_ROOT + const candidates = [ + path.join(BLOCKS_ROOT, blockPath + '.syscfg.js'), + path.join(BLOCKS_ROOT, blockPath), + path.join(BLOCKS_ROOT, 'common', blockPath + '.syscfg.js'), + path.join(ROOT, blockPath), + ]; + for (const c of candidates) { + if (fs.existsSync(c)) return c; + } + // Fallback: search by basename + const base = path.basename(blockPath).replace(/\.syscfg\.js$/, ''); + const found = findAllBlockFiles().find(f => path.basename(f, '.syscfg.js') === base); + if (found) return found; + return null; +} + +function findAllBlockFiles() { + const out = []; + function walk(dir) { + if (!fs.existsSync(dir)) return; + for (const name of fs.readdirSync(dir)) { + const p = path.join(dir, name); + const st = fs.statSync(p); + if (st.isDirectory()) walk(p); + else if (p.endsWith('.syscfg.js')) out.push(p); + } + } + walk(BLOCKS_ROOT); + return out.sort(); +} + +function analyzeValidateSource(src) { + const hasValidate = /function\s+validate\s*\(|validate\s*:\s*validate|validate\s*\(/.test(src); + const errorCount = (src.match(/logError/g) || []).length; + const warningCount = (src.match(/logWarning/g) || []).length; + // Count distinct error branches — each logError is an error path + return { hasValidate, errorCount, warningCount }; +} + +// evaluate a pure shared helper (no `system` dependency) in a bare +// vm and return its `exports`, mirroring what SysConfig's getScript returns. +const _sharedHelperCache = {}; +function loadSharedHelper(filePath) { + if (_sharedHelperCache[filePath]) return _sharedHelperCache[filePath]; + const src = fs.readFileSync(filePath, 'utf8'); + const sandbox = { + exports: {}, + module: { exports: {} }, + console: { log() {}, error() {}, warn() {} }, + }; + sandbox.global = sandbox; + vm.createContext(sandbox); + vm.runInContext(src, sandbox, { filename: filePath, timeout: 5000 }); + const mod = (sandbox.exports && Object.keys(sandbox.exports).length) ? sandbox.exports + : (sandbox.module.exports && Object.keys(sandbox.module.exports).length) ? sandbox.module.exports + : sandbox.exports; + _sharedHelperCache[filePath] = mod; + return mod; +} + +function loadBlockModule(filePath) { + const src = fs.readFileSync(filePath, 'utf8'); + + // Mock system sufficient for pure field validators; for validators that + // inspect system.modules we provide an empty registry so duplicate-name + // checks see no collisions (the legal control passes). + const mockSystem = { + deviceData: { device: 'AM243x_ALV_beta', package: 'ALV' }, + modules: {}, + getOS: () => 'linux', // look_up_table.syscfg.js branches on this at load + getScript: (p) => { + if (p.includes('pru_device_caps')) { + try { return require(path.join(BLOCKS_ROOT, 'common', 'pru_device_caps.js')); } catch (_) { return makeStub(); } + } + // reserved-name helper is pure (no `system` use) — load + // it for real so block validate() calls exercise the real list. + if (String(p).includes('pru_reserved_names')) { + try { return loadSharedHelper(path.join(BLOCKS_ROOT, 'common', 'pru_reserved_names.js')); } catch (_) { return makeStub(); } + } + if (p === '/common' || p === 'common' || (typeof p === 'string' && p.includes('/common'))) { + return { getSocName: () => 'am243x', getSelfSysCfgCoreName: () => 'r5fss0-0' }; + } + return makeStub(); + }, + contexts: {}, + deviceData2: {}, + }; + + const sandbox = { + system: mockSystem, + exports: {}, + module: { exports: {} }, + console: { log() {}, error() {}, warn() {} }, + require: () => makeStub(), + }; + sandbox.global = sandbox; + vm.createContext(sandbox); + vm.runInContext(src, sandbox, { filename: filePath, timeout: 5000 }); + const mod = (sandbox.exports && Object.keys(sandbox.exports).length) ? sandbox.exports + : (sandbox.module.exports && Object.keys(sandbox.module.exports).length) ? sandbox.module.exports + : sandbox.exports; + return { mod, src, mockSystem }; +} + +function runValidate(blockPath, inst) { + const file = resolveBlockPath(blockPath); + if (!file) { + console.error(`Block not found: ${blockPath}`); + process.exit(2); + } + const { mod } = loadBlockModule(file); + // Support both exports.validate and exports being the module directly + const validate = mod.validate || (mod.exports && mod.exports.validate) || null; + if (!validate) { + // Check if it's the static module with validateDeviceCapabilities + if (mod.validateDeviceCapabilities) { + console.error(`Block ${blockPath} exposes validateDeviceCapabilities, not validate — use static_module_validate_runner.js or pass --device-cap`); + process.exit(2); + } + console.error(`validate not found in ${file}`); + process.exit(2); + } + const errors = []; + const warnings = []; + const infos = []; + const report = { + logError: (msg, _inst, field) => errors.push({ message: String(msg), field: field || null }), + logWarning: (msg, _inst, field) => warnings.push({ message: String(msg), field: field || null }), + logInfo: (msg, _inst, field) => infos.push({ message: String(msg), field: field || null }), + }; + // Inject system if validator expects it via closure — already mocked in sandbox, + // but some validators call system.modules directly via global system. + // The vm's global system is already set; for Node-side calls we also expose it. + const prevSystem = global.system; + // eslint-disable-next-line no-undef + try { global.system = loadBlockModule(file).mockSystem; } catch (_) {} + try { + validate(inst, report); + } catch (e) { + // A validator that throws is reported as an error, not as a clean result + errors.push({ message: `validator threw: ${e.message}`, field: null }); + } finally { + if (prevSystem !== undefined) global.system = prevSystem; + else delete global.system; + } + return { file, errors, warnings, infos }; +} + +// ------------------------------------------------------------------ CLI + +function printSweep(asReport) { + const files = findAllBlockFiles(); + const rows = []; + for (const file of files) { + const src = fs.readFileSync(file, 'utf8'); + const a = analyzeValidateSource(src); + if (!a.hasValidate) continue; + const rel = path.relative(ROOT, file).replace(/\\/g, '/'); + const block = path.basename(file, '.syscfg.js'); + rows.push({ + block, + file: rel, + hasValidate: a.hasValidate, + errorCount: a.errorCount, + warningCount: a.warningCount, + }); + } + if (!asReport) { + console.log(JSON.stringify(rows, null, 2)); + return; + } + // For --report we also emit armed summary based on committed test arms + // Blocks whose validator error paths have a committed error-case/control test pair, + // mapped to the number of error paths those tests cover. Kept here so the + // runner can report without importing Python; add a block when its + // validator test pair is added. + const ARMED_ERROR_PATHS_BY_BLOCK = { + }; + const ARMED_BLOCKS = Object.keys(ARMED_ERROR_PATHS_BY_BLOCK); + const validatorsFound = rows.length; + const armedCount = rows.filter(r => ARMED_BLOCKS.includes(r.block)).length; + const errorPathsFound = rows.reduce((s, r) => s + r.errorCount, 0); + const ARMED_ERROR_PATHS = ARMED_BLOCKS.reduce((s, b) => s + ARMED_ERROR_PATHS_BY_BLOCK[b], 0); + console.log(JSON.stringify({ + validators: rows, + summary: { + validatorsFound, + validatorsArmed: armedCount, + validatorsUnarmed: validatorsFound - armedCount, + errorPathsFound, + errorPathsArmed: ARMED_ERROR_PATHS, + armedBlocks: ARMED_BLOCKS, + } + }, null, 2)); +} + +function main() { + const args = process.argv.slice(2); + if (args.includes('--sweep')) { + printSweep(false); + return; + } + if (args.includes('--report')) { + printSweep(true); + return; + } + // Parse --block / --inst or positional + let blockPath = null; + let instJson = null; + let instFile = null; + for (let i = 0; i < args.length; i++) { + if (args[i] === '--block' && i + 1 < args.length) blockPath = args[++i]; + else if (args[i] === '--inst' && i + 1 < args.length) instJson = args[++i]; + else if (args[i] === '--inst-file' && i + 1 < args.length) instFile = args[++i]; + else if (!blockPath) blockPath = args[i]; + else if (!instJson) instJson = args[i]; + } + if (!blockPath) { + console.error('Usage: node scripts/block_validate_runner.js \'\' OR --sweep OR --report'); + console.error('Example: node scripts/block_validate_runner.js utils/delay_block \'{"delayCount":100}\''); + process.exit(2); + } + let inst = {}; + if (instJson) { + try { inst = JSON.parse(instJson); } catch (e) { + console.error(`Invalid inst JSON: ${e.message}`); + process.exit(2); + } + } else if (instFile) { + try { inst = JSON.parse(fs.readFileSync(instFile, 'utf8')); } catch (e) { + console.error(`Cannot read inst file ${instFile}: ${e.message}`); + process.exit(2); + } + } + const result = runValidate(blockPath, inst); + // Emit structured errors and exit 0 so the caller can inspect errors vs a clean result + console.log(JSON.stringify({ file: path.relative(ROOT, result.file).replace(/\\/g, '/'), errors: result.errors, warnings: result.warnings, infos: result.infos })); +} + +if (require.main === module) main(); + +module.exports = { findAllBlockFiles, analyzeValidateSource, loadBlockModule, runValidate, resolveBlockPath }; diff --git a/scripts/shared_memvar_runner.js b/scripts/shared_memvar_runner.js new file mode 100644 index 0000000..35eba76 --- /dev/null +++ b/scripts/shared_memvar_runner.js @@ -0,0 +1,68 @@ +"use strict"; + +/** + * Runner: Memory Access blocks that share one auto-created memVar. + * + * Loads the REAL memory_access_block.syscfg.js and runs its validate() over + * designs built from a shared / separate / symbolSelect'd memVar, emitting + * JSON on stdout: { cases: { : { warnings, errors } }, guidance }. + */ +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); + +const root = path.resolve(__dirname, ".."); +const blockFile = path.join(root, ".metadata", "sysconfig", ".meta", "pru_blocks", "utils", "memory_access_block.syscfg.js"); + +const memVars = { + shared: { labelName: "out_buf", sizeInBytes: 64, memoryLocation: "dmem" }, + inBuf: { labelName: "in_buf", sizeInBytes: 64, memoryLocation: "dmem" }, + outBuf: { labelName: "out_buf", sizeInBytes: 64, memoryLocation: "dmem" } +}; +const accessInstances = []; +const sandbox = { + exports: {}, module: { exports: {} }, console, + system: { modules: { + "/pru_blocks/utils/memory_variable_block": { $instances: Object.values(memVars) }, + "/pru_blocks/utils/memory_access_block": { $instances: accessInstances } + } }, + JSON, Math, Number, String, Array, Object, RegExp, parseInt, parseFloat, isNaN +}; +sandbox.global = sandbox; +sandbox.globalThis = sandbox; +vm.runInNewContext(fs.readFileSync(blockFile, "utf8"), sandbox, { filename: path.basename(blockFile) }); +const block = sandbox.exports; + +function access(name, mode, memVar, symbolSelect) { + return { $name: name, operationMode: mode, memVar, symbolSelect: symbolSelect || "", + offsetValue: 0, dataSize: 4, input1: mode === "write" ? [{}] : [] }; +} + +function run(insts) { + accessInstances.length = 0; + insts.forEach(i => accessInstances.push(i)); + const out = {}; + for (const inst of insts) { + const warnings = [], errors = []; + block.validate(inst, { + logError: m => errors.push(String(m)), + logWarning: m => warnings.push(String(m)), + logInfo: () => {} + }); + out[inst.$name] = { warnings, errors }; + } + return out; +} + +const cases = { + // the rename hazard: rd.memVar.labelName = in_buf; wr.memVar.labelName = out_buf on ONE object + sharedMemVar: run([access("rd", "read", memVars.shared), access("wr", "write", memVars.shared)]), + // the guided pattern: explicit instances assigned by reference + separateMemVars: run([access("rd", "read", memVars.inBuf), access("wr", "write", memVars.outBuf)]), + // both blocks pick the same named symbol deliberately: no rename hazard + symbolSelected: run([access("rd", "read", memVars.shared, "out_buf"), access("wr", "write", memVars.shared, "out_buf")]), + // a single block alone + single: run([access("rd", "read", memVars.shared)]) +}; + +process.stdout.write(JSON.stringify({ cases, guidance: block.longDescription })); diff --git a/scripts/static_module_validate_runner.js b/scripts/static_module_validate_runner.js new file mode 100644 index 0000000..3944c63 --- /dev/null +++ b/scripts/static_module_validate_runner.js @@ -0,0 +1,136 @@ +#!/usr/bin/env node +'use strict'; +/* + * Test runner: exercises validateDeviceCapabilities from the real + * pru_blocks_static_module.syscfg.js against a mocked SysConfig runtime. + * + * Usage: + * node scripts/static_module_validate_runner.js + * Example: + * node scripts/static_module_validate_runner.js AM261x_ZCZ am261x bswap + * node scripts/static_module_validate_runner.js AM243x_ALV_beta am243x bswap + * + * Degradation modes (prove the gate fails closed / errors visibly): + * --no-soc common.getSocName() returns null (SoC missing) + * --common-throws system.getScript("/common") throws + * --no-caps system.getScript(pru_device_caps) throws (table unloadable) + * --widget-throws caps.isWidgetAvailable() throws for the tested widget + * + * Prints JSON array of error messages on stdout. + */ +const fs = require('fs'); +const path = require('path'); +const vm = require('vm'); + +const MODES = ['--no-soc', '--common-throws', '--no-caps', '--widget-throws']; +const modeArgs = process.argv.slice(2).filter(a => MODES.includes(a)); +const posArgs = process.argv.slice(2).filter(a => !MODES.includes(a)); +const mode = modeArgs[0] || null; + +const device = posArgs[0] || 'AM261x_ZCZ'; +const soc = posArgs[1] || 'am261x'; +const widget = posArgs[2] || 'bswap'; + +const ROOT = path.join(__dirname, '..'); +const CAPS_PATH = path.join(ROOT, '.metadata', 'sysconfig', '.meta', 'pru_blocks', 'common', 'pru_device_caps.js'); +const STATIC_PATH = path.join(ROOT, '.metadata', 'sysconfig', '.meta', 'pru_blocks', 'common', 'pru_blocks_static_module.syscfg.js'); + +let caps; +try { + caps = require(CAPS_PATH); +} catch (e) { + console.error('Failed to load caps: ' + e.message); + process.exit(2); +} + +let errors = []; +const report = { + logError: (msg) => errors.push(String(msg)), + logWarning: () => {}, + logInfo: () => {}, +}; + +const mockCommon = { + getSocName: () => (mode === '--no-soc' ? null : soc), + getSelfSysCfgCoreName: () => 'r5fss0-0', +}; + +// proxy stub for unrelated system.getScript calls (register allocator etc.) +function makeStub() { + const fn = function () { return makeStub(); }; + return new Proxy(fn, { + get(_t, prop) { + if (prop === Symbol.toPrimitive || prop === 'toString' || prop === 'valueOf') return () => ''; + if (prop === Symbol.iterator) return undefined; + if (prop === 'then') return undefined; + return makeStub(); + }, + apply() { return makeStub(); }, + }); +} + +// The static module calls system.getScript("/common") during load (e.g. +// getPruNumberFromContext at line ~2056). Arm the --common-throws failure only +// AFTER the module has loaded so the degradation hits the validator's own +// failure path instead of blowing up module initialization. +let vmLoaded = false; + +const mockSystem = { + deviceData: { device: device, package: 'ZCZ' }, + modules: { + '/pru_blocks/test/fake_block': { + displayName: 'Fake Block', + requiresWidget: [widget], + $instances: [{ $name: 'FAKE_0' }], + }, + }, + getScript: (p) => { + if (p.includes('pru_device_caps')) { + if (mode === '--no-caps') throw new Error('pru_device_caps.js not found (simulated unloadable capability table)'); + if (mode === '--widget-throws') { + return Object.assign({}, caps, { + isWidgetAvailable: () => { throw new Error('capability lookup exploded (simulated)'); }, + }); + } + return caps; + } + if (p === '/common' || p === 'common') { + if (mode === '--common-throws' && vmLoaded) throw new Error('/common unavailable (simulated)'); + return mockCommon; + } + if (p.includes('/common')) { + // simulation scripts under /pru_blocks/common/ are unrelated to SoC lookup + return mockCommon; + } + return makeStub(); + }, + contexts: {}, + deviceData2: {}, +}; + +const src = fs.readFileSync(STATIC_PATH, 'utf8'); + +const sandbox = { + system: mockSystem, + exports: {}, + module: { exports: {} }, + console: { log() {}, error() {}, warn() {} }, + require: () => makeStub(), +}; +sandbox.global = sandbox; +vm.createContext(sandbox); +try { + vm.runInContext(src, sandbox, { filename: STATIC_PATH, timeout: 5000 }); +} catch (e) { + console.error('VM load failed: ' + e.stack); + process.exit(2); +} +vmLoaded = true; +const mod = sandbox.exports; +if (!mod.validateDeviceCapabilities) { + console.error('validateDeviceCapabilities not found in exports'); + process.exit(2); +} +// call validator with dummy inst +mod.validateDeviceCapabilities({}, report); +console.log(JSON.stringify(errors)); diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..d7b4437 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,7 @@ +import os +import sys + +_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +for p in (_REPO_ROOT,): + if p not in sys.path: + sys.path.insert(0, p) diff --git a/tests/test_block_validate_runner_inst_file.py b/tests/test_block_validate_runner_inst_file.py new file mode 100644 index 0000000..0af5fa1 --- /dev/null +++ b/tests/test_block_validate_runner_inst_file.py @@ -0,0 +1,53 @@ +"""scripts/block_validate_runner.js reads the instance from --inst-file. + +The flag used to fall through to the positional arguments, so `--inst-file f.json` was taken as the +block path (or as the instance JSON) and the file was never read. +""" + +import json +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +RUNNER = ROOT / "scripts" / "block_validate_runner.js" +BLOCK = "program_control/conditional_block" +INST = {"$name": "If_Else_0", "numOfInputPorts": 2, + "input1": ["a"], "input2": ["b"], "T": ["x"], "F": []} + + +def _run(args): + return subprocess.run(["node", str(RUNNER)] + args, capture_output=True, text=True, + cwd=str(ROOT), timeout=30) + + +def _inst_file(tmp_path): + f = tmp_path / "inst.json" + f.write_text(json.dumps(INST), encoding="utf-8") + return str(f) + + +def test_inst_file_after_block_path_is_validated(tmp_path): + proc = _run([BLOCK, "--inst-file", _inst_file(tmp_path)]) + assert proc.returncode == 0, proc.stdout + proc.stderr + data = json.loads(proc.stdout.strip()) + assert [e for e in data["errors"] if "f_next" in e["message"]], data + + +def test_inst_file_before_block_path_is_validated(tmp_path): + proc = _run(["--inst-file", _inst_file(tmp_path), BLOCK]) + assert proc.returncode == 0, proc.stdout + proc.stderr + data = json.loads(proc.stdout.strip()) + assert [e for e in data["errors"] if "f_next" in e["message"]], data + + +def test_inst_file_matches_inline_json(tmp_path): + inline = _run([BLOCK, json.dumps(INST)]) + from_file = _run([BLOCK, "--inst-file", _inst_file(tmp_path)]) + assert inline.returncode == 0 and from_file.returncode == 0 + assert json.loads(from_file.stdout) == json.loads(inline.stdout) + + +def test_unreadable_inst_file_is_reported(tmp_path): + proc = _run([BLOCK, "--inst-file", str(tmp_path / "missing.json")]) + assert proc.returncode == 2 + assert "inst file" in proc.stderr diff --git a/tests/test_conditional_unconnected_branch.py b/tests/test_conditional_unconnected_branch.py new file mode 100644 index 0000000..2788274 --- /dev/null +++ b/tests/test_conditional_unconnected_branch.py @@ -0,0 +1,56 @@ +"""An unconnected T or F port on If/Else is a validate() ERROR. + +Problem: SysConfig only warned 'f_next port is not connected', +then clpru failed with [E0300] undefined symbol cmp_FALSE. Fix: fail early, +at validate time, with a clear message naming the port. A fully connected +design must still validate without that error. + +The harness is scripts/block_validate_runner.js. The connected and the +unconnected cases differ only in the T/F lists. +""" + +import json +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +RUNNER = ROOT / "scripts" / "block_validate_runner.js" +BLOCK = "program_control/conditional_block" + + +def _validate(t, f): + inst = {"$name": "If_Else_0", "numOfInputPorts": 2, + "input1": ["a"], "input2": ["b"], "T": t, "F": f} + proc = subprocess.run(["node", str(RUNNER), BLOCK, json.dumps(inst)], + capture_output=True, text=True, cwd=str(ROOT), timeout=30) + assert proc.returncode == 0, proc.stdout + proc.stderr + return json.loads(proc.stdout.strip()) + + +def _branch_errors(data, port): + return [e for e in data["errors"] if port in e["message"]] + + +def test_connected_design_has_no_branch_error(): + data = _validate(["x"], ["y"]) + assert data["errors"] == [], data + + +def test_unconnected_f_port_is_an_error(): + data = _validate(["x"], []) + errs = _branch_errors(data, "f_next") + assert errs, "unconnected F must be a validate ERROR, got %s" % data + + +def test_unconnected_t_port_is_an_error(): + data = _validate([], ["y"]) + errs = _branch_errors(data, "t_next") + assert errs, "unconnected T must be a validate ERROR, got %s" % data + + +@pytest.mark.parametrize("t,f,port", [(["x"], [], "f_next"), ([], ["y"], "t_next")]) +def test_error_not_just_warning(t, f, port): + data = _validate(t, f) + assert not [w for w in data["warnings"] if port in w["message"]], data diff --git a/tests/test_fail_closed_gating.py b/tests/test_fail_closed_gating.py new file mode 100644 index 0000000..b68c9c5 --- /dev/null +++ b/tests/test_fail_closed_gating.py @@ -0,0 +1,126 @@ +""" +The device-capability gate must fail CLOSED or error VISIBLY on every failure path. + +Failure paths covered (each must report an error instead of silently skipping +validation and leaving the build clean): + failure path fail-open behaviour this test rejects + 1. getScript("/common") failure -> silent return + 2. missing SoC (getSocName() -> null) -> silent return + 3. isWidgetAvailable() exception -> available = true + 4. null pruDeviceCaps (caps unloadable) -> silent return + 5. unknown SoC -> default ICSSM caps + +Drives the REAL validator in +.metadata/sysconfig/.meta/pru_blocks/common/pru_blocks_static_module.syscfg.js +and the REAL capability table at +.metadata/sysconfig/.meta/pru_blocks/common/pru_device_caps.js +via scripts/static_module_validate_runner.js. + +If any fail-closed branch is changed to fail open, the corresponding test below +fails, because the runner then reports an empty error list ("clean build") for +that degraded mode. +""" +import json +import pathlib +import subprocess + +import pytest + +REPO_ROOT = pathlib.Path(__file__).resolve().parents[1] +RUNNER = REPO_ROOT / "scripts" / "static_module_validate_runner.js" +NODE = "node" + + +def run_validator(device, soc, widget, mode=None): + cmd = [NODE, str(RUNNER), device, soc, widget] + if mode: + cmd.append(mode) + proc = subprocess.run( + cmd, capture_output=True, text=True, cwd=str(REPO_ROOT), timeout=10 + ) + assert proc.returncode == 0, f"runner failed: {proc.stdout}\n{proc.stderr}" + return json.loads(proc.stdout.strip() or "[]") + + +def test_failure_path_getscript_common_errors_visibly(): + """Path 1: system.getScript('/common') throws -> ERROR, not a silent skip.""" + errors = run_validator("AM261x_ZCZ", "am261x", "mac", mode="--common-throws") + assert len(errors) == 1, "getScript('/common') failure silently skipped gating" + msg = " ".join(errors).lower() + assert "common" in msg and ("fail" in msg or "cannot" in msg) + + +def test_failure_path_missing_soc_errors_visibly(): + """Path 2: getSocName() returns null -> ERROR naming the missing SoC.""" + errors = run_validator("AM261x_ZCZ", "am261x", "mac", mode="--no-soc") + assert len(errors) == 1, "missing SoC silently skipped gating" + msg = " ".join(errors).lower() + assert "soc" in msg and ("could not be determined" in msg or "cannot" in msg) + + +def test_failure_path_widget_exception_fails_closed(): + """Path 3: isWidgetAvailable() throws -> widget treated UNAVAILABLE with a visible ERROR.""" + errors = run_validator("AM263x_beta", "am263x", "mac", mode="--widget-throws") + assert len(errors) == 1, "isWidgetAvailable exception silently treated widget as available" + msg = " ".join(errors).lower() + assert "mac" in msg and "unavailable" in msg + + +def test_failure_path_null_caps_errors_visibly(): + """Path 4: capability table unloadable (null pruDeviceCaps) -> ERROR; build is NOT clean.""" + errors = run_validator("AM243x_ALV_beta", "am243x", "mac", mode="--no-caps") + assert len(errors) == 1, "null pruDeviceCaps silently skipped gating" + msg = " ".join(errors).lower() + assert "capability table" in msg or "pru_device_caps" in msg + + +def test_failure_path_unknown_soc_errors_visibly(): + """Path 5: SoC not in the capability table -> ERROR naming the SoC; no silent default caps.""" + errors = run_validator("AM9999_fake", "am9999", "mac") + assert len(errors) == 1, "unknown SoC silently inherited a default capability set" + msg = " ".join(errors).lower() + assert "am9999" in msg + assert "capability table" in msg or "not in" in msg + + +def test_known_soc_positive_control_stays_clean(): + """Control: a known SoC with the widget present must still produce NO errors.""" + errors = run_validator("AM243x_ALV_beta", "am243x", "mac") + assert errors == [], f"known-good configuration must stay clean, got {errors}" + + +def test_caps_module_unknown_soc_has_no_widgets(): + """Unit-level: getCapsForSoc for unknown/missing SoC returns an EMPTY set (fail closed), + never a default capability set.""" + caps_path = REPO_ROOT / ".metadata" / "sysconfig" / ".meta" / "pru_blocks" / "common" / "pru_device_caps.js" + assert caps_path.exists() + proc = subprocess.run( + [NODE, "-e", + f"const c = require({json.dumps(str(caps_path))});" + "const unk = c.getCapsForSoc('am9999');" + "const nul = c.getCapsForSoc(null);" + "if (unk.size !== 0 || nul.size !== 0) {" + " console.error('FAIL size unk=' + unk.size + ' null=' + nul.size); process.exit(1); }" + "if (c.isKnownSoc('am9999') !== false || c.isKnownSoc('am64x') !== true) {" + " console.error('FAIL isKnownSoc'); process.exit(1); }" + "console.log('ok');"], + capture_output=True, text=True, cwd=str(REPO_ROOT), timeout=10 + ) + assert proc.returncode == 0, f"caps module fail-closed check failed: {proc.stdout}\n{proc.stderr}" + + +def test_validator_fail_closed_source_guards(): + """Lint only; the behavioural tests above are the real check. + + These are whitespace-literal substring checks. Re-introducing the exact + fail-open as `catch (e) { + available = true; +}` passes them both while + the behavioural arms above fail. Kept as cheap lint; the + check that the gate fails closed is the five run_validator arms. + """ + src = (REPO_ROOT / ".metadata" / "sysconfig" / ".meta" / "pru_blocks" / "common" + / "pru_blocks_static_module.syscfg.js").read_text(encoding="utf-8") + assert "available = true; }" not in src, "isWidgetAvailable exception must fail CLOSED, not available=true" + gating = src.split("function validateDeviceCapabilities")[1].split("\n}\n")[0] + assert "Failing closed." in gating, "visible fail-closed error messages must be present" diff --git a/tests/test_linker_section_rules.py b/tests/test_linker_section_rules.py new file mode 100644 index 0000000..e31e6e6 --- /dev/null +++ b/tests/test_linker_section_rules.py @@ -0,0 +1,41 @@ +"""Every shipped PRU linker command file places the Memory Variable block's sections. + +The Memory Variable block reserves its buffers with `.usect ".udmem"` (data RAM) +and `.usect ".usmem"` (shared RAM). A linker command file without a rule for a +section lets clpru put it in the first memory that fits, which is instruction +RAM directly after .text, so a "shared memory" variable silently lands where no +other core can reach it. Each file must place .udmem in the same data RAM as +its .bss and .usmem in PRU_SHAREDMEM. +""" + +import re +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] + + +def _linker_files(): + out = subprocess.run(["git", "ls-files", "*linker.cmd"], cwd=ROOT, capture_output=True, text=True, check=True).stdout + return sorted(out.split()) + + +def _placement(text, section): + m = re.search(r"^\s*%s\s*>\s*(\w+)\s*,\s*PAGE\s*(\d+)" % re.escape(section), text, re.M) + return m.groups() if m else None + + +def test_linker_files_are_found(): + assert len(_linker_files()) > 0 + + +@pytest.mark.parametrize("path", _linker_files()) +def test_memory_variable_sections_are_placed(path): + text = (ROOT / path).read_text(encoding="utf-8") + bss = _placement(text, ".bss") + assert bss, "%s has no .bss rule" % path + assert _placement(text, ".udmem") == bss, "%s: .udmem must go where .bss goes (%s, PAGE %s)" % (path, *bss) + assert re.search(r"^\s*PRU_SHAREDMEM\s*:", text, re.M), "%s does not declare PRU_SHAREDMEM" % path + assert _placement(text, ".usmem") == ("PRU_SHAREDMEM", "2"), "%s: .usmem must go to PRU_SHAREDMEM, PAGE 2" % path diff --git a/tests/test_pad_sharing_unavailable.py b/tests/test_pad_sharing_unavailable.py new file mode 100644 index 0000000..144ab07 --- /dev/null +++ b/tests/test_pad_sharing_unavailable.py @@ -0,0 +1,72 @@ +"""The GPO/GPI shared-pad check says so when the device carries no pad data. + +getDevicePadSharing returned an empty set for a device without usable pin data, which the validator +read as "no pad is shared": a design with a GPO and a GPI claim then got neither a collision warning nor +the "could not be checked" warning. Runs the real validateBidiPinCollisionsGlobal. +""" + +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +STATIC = ROOT / ".metadata" / "sysconfig" / ".meta" / "pru_blocks" / "common" / "pru_blocks_static_module.syscfg.js" + +pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node not installed") + +DRIVER = r""" +const fs = require("fs"), vm = require("vm"); +const [STATIC, DEVICE] = [process.argv[2], JSON.parse(process.argv[3])]; +function stub() { return new Proxy(function () { return stub(); }, { + get(_t, p) { if (p === Symbol.toPrimitive || p === "toString" || p === "valueOf") return () => ""; if (p === Symbol.iterator || p === "then") return undefined; return stub(); }, + apply() { return stub(); } }); } +const claimsOf = (dir) => ({ $instances: [{ $name: dir + "_0" }], + moduleStatic: { getPinUsage: () => [{ pin: 0, dir }] } }); +const system = { + deviceData: DEVICE, + modules: { "/pru_blocks/pru_io_blocks/pru_gpo_block": claimsOf("GPO"), "/pru_blocks/pru_io_blocks/pru_gpi_block": claimsOf("GPI") }, + getScript: (p) => (p === "/common" ? { getSelfSysCfgCoreName: () => "icss_g0_pru0" } + : new Proxy(stub(), { get: (t, k) => (k === "findDeviceInputConflicts" ? () => [] : t[k]) })), +}; +const sb = { system, exports: {}, module: { exports: {} }, console: { log() {}, error() {}, warn() {} }, require: () => stub() }; +sb.global = sb; +vm.createContext(sb); +vm.runInContext(fs.readFileSync(STATIC, "utf8"), sb, { filename: STATIC }); +const warnings = []; +sb.exports.validateBidiPinCollisionsGlobal({ $name: "design" }, { logWarning: (m) => warnings.push(m), logError: () => {}, logInfo: () => {} }); +console.log(JSON.stringify(warnings)); +""" + + +def _warnings(tmp_path, device): + drv = tmp_path / "driver.js" + drv.write_text(DRIVER, encoding="utf-8") + proc = subprocess.run(["node", str(drv), str(STATIC), json.dumps(device)], + capture_output=True, text=True, timeout=60) + assert proc.returncode == 0, proc.stdout + proc.stderr + return json.loads(proc.stdout.strip().splitlines()[-1]) + + +def _pad(sig_list): + return {"pinModeInfo": [{"signalName": s} for s in sig_list]} + + +def test_device_without_pad_data_warns_that_sharing_was_not_checked(tmp_path): + for device in ({}, {"pinCommonInfos": {}}): + warns = _warnings(tmp_path, device) + assert [w for w in warns if "could not be checked" in w], (device, warns) + + +def test_shared_pad_still_reports_the_collision(tmp_path): + device = {"pinCommonInfos": {"A": _pad(["PRG0_PRU0_GPO0", "PRG0_PRU0_GPI0"])}} + warns = _warnings(tmp_path, device) + assert [w for w in warns if "will contend on this pad" in w], warns + assert not [w for w in warns if "could not be checked" in w], warns + + +def test_available_data_without_a_shared_pad_stays_silent(tmp_path): + device = {"pinCommonInfos": {"A": _pad(["PRG0_PRU0_GPO1"]), "B": _pad(["PRG0_PRU0_GPI2"])}} + assert _warnings(tmp_path, device) == [] diff --git a/tests/test_register_allocator_scratch_registers.py b/tests/test_register_allocator_scratch_registers.py new file mode 100644 index 0000000..7edb3ec --- /dev/null +++ b/tests/test_register_allocator_scratch_registers.py @@ -0,0 +1,73 @@ +"""Register allocator: macro scratch registers TEMP_REG3 / TEMP_REG4. + + - TEMP_REG3 / TEMP_REG4 are R26 / R27. A design whose macros use them must not get those registers for + values (a macro would overwrite a live value, or its own result); a design that uses neither keeps them. + +The node driver runs the shipped allocator on hand-built instances. +""" + +import os +import shutil +import subprocess + +import pytest + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +ALLOC = os.path.join(ROOT, ".metadata", "sysconfig", ".meta", "pru_blocks", "common", + "register_allocation", "pru_register_allocator.js") + +pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node not installed") + +DRIVER = r""" +const fs = require("fs"), vm = require("vm"), assert = require("assert"); +const ALLOC = process.argv[2], CASE = process.argv[3]; +const modules = {}; +const sb = { system: { modules }, exports: {}, console }; +vm.createContext(sb); +vm.runInContext(fs.readFileSync(ALLOC, "utf8"), sb, { filename: "pru_register_allocator.js" }); +const alloc = sb.exports; +const ldi = (n, i) => ({ $name: n + i, opCode: "LDI", numOfInputPorts: 0, numOfConstants: 1, constant1: i, noOfCycles: 1, output1Size: "4" }); +const fill = (k) => { const a = []; for (let i = 0; i < k; i++) a.push(ldi("K", i)); return a; }; +const scratchModule = (regs, insts) => ({ $instances: insts, getMacro: () => ["SCR .macro a", " ldi " + regs.join(", ") + ", 1", " .endm"].join("\n") }); +const scratchBlock = () => ({ $name: "S0", opCode: "SCR", numOfInputPorts: 0, numOfConstants: 0, noOfCycles: 1, output1Size: "4" }); +const run = (mods) => { Object.keys(modules).forEach(k => delete modules[k]); Object.assign(modules, mods); alloc.allocatePruRegisters(); + const instrs = alloc.getPruRegisterAllocationSummary().pruInstructions.filter(x => typeof x === "string"); + return { failed: alloc.getAllocationFailures().length, instrs: instrs, used: instrs.join(" ") }; }; +const uses = (text, reg) => new RegExp(String.raw`\b${reg}\b`).test(text); + +if (CASE === "pool-without-scratch") { + const r = run({ "/f": { $instances: fill(28) } }); + assert.strictEqual(r.failed, 0, "28 values fit the full pool when no macro uses R26/R27"); + assert(uses(r.used, "R26") && uses(r.used, "R27")); +} else if (CASE === "scratch-both") { + const r = run({ "/f": { $instances: fill(27) }, "/s": scratchModule(["TEMP_REG3", "TEMP_REG4"], [scratchBlock()]) }); + assert.strictEqual(r.failed, 2, "28 values do not fit once R26 and R27 are held back: " + JSON.stringify(r)); + assert(!uses(r.used, "R26") && !uses(r.used, "R27"), "R26/R27 handed out: " + r.used); + assert.deepStrictEqual(Array.from(alloc.getScratchRegisters()), ["TEMP_REG3", "TEMP_REG4"]); +} else if (CASE === "scratch-reg3-only") { + const r = run({ "/f": { $instances: fill(27) }, "/s": scratchModule(["TEMP_REG3"], [scratchBlock()]) }); + assert.strictEqual(r.failed, 1, "only R26 is held back: " + JSON.stringify(r)); + assert(!uses(r.used, "R26") && uses(r.used, "R27"), r.used); +} else { throw new Error("unknown case " + CASE); } +console.log("DRIVER OK " + CASE); +""" + + +def _case(tmp_path, case): + driver = tmp_path / ("driver_%s.js" % case) + driver.write_text(DRIVER, encoding="utf-8") + res = subprocess.run(["node", str(driver), ALLOC, case], capture_output=True, text=True, timeout=120) + assert res.returncode == 0 and ("DRIVER OK " + case) in res.stdout, \ + "node driver case %r failed:\n%s\n%s" % (case, res.stdout, res.stderr) + + +def test_pool_keeps_r26_r27_when_no_macro_uses_them(tmp_path): + _case(tmp_path, "pool-without-scratch") + + +def test_scratch_registers_are_not_allocated_to_values(tmp_path): + _case(tmp_path, "scratch-both") + + +def test_only_the_scratch_register_in_use_is_held_back(tmp_path): + _case(tmp_path, "scratch-reg3-only") diff --git a/tests/test_register_allocator_wide_outputs.py b/tests/test_register_allocator_wide_outputs.py new file mode 100644 index 0000000..39abd4a --- /dev/null +++ b/tests/test_register_allocator_wide_outputs.py @@ -0,0 +1,58 @@ +"""Register allocator: wide secondary outputs. + + - A multi-output macro's output2 and up, and the consumers wired to them, get a lo/hi register pair when the port + is wider than 4 bytes (output1 already did). + +The node driver runs the shipped allocator on hand-built instances. +""" + +import os +import shutil +import subprocess + +import pytest + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +ALLOC = os.path.join(ROOT, ".metadata", "sysconfig", ".meta", "pru_blocks", "common", + "register_allocation", "pru_register_allocator.js") + +pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node not installed") + +DRIVER = r""" +const fs = require("fs"), vm = require("vm"), assert = require("assert"); +const ALLOC = process.argv[2], CASE = process.argv[3]; +const modules = {}; +const sb = { system: { modules }, exports: {}, console }; +vm.createContext(sb); +vm.runInContext(fs.readFileSync(ALLOC, "utf8"), sb, { filename: "pru_register_allocator.js" }); +const alloc = sb.exports; +const run = (mods) => { Object.keys(modules).forEach(k => delete modules[k]); Object.assign(modules, mods); alloc.allocatePruRegisters(); + const instrs = alloc.getPruRegisterAllocationSummary().pruInstructions.filter(x => typeof x === "string"); + return { failed: alloc.getAllocationFailures().length, instrs: instrs, used: instrs.join(" ") }; }; + +if (CASE === "wide-extra-output") { + const W = { $name: "W", opCode: "WIDE", multiOutputMacroArgs: true, numOfOutputPorts: 2, numOfInputPorts: 0, numOfConstants: 0, + noOfCycles: 1, output1Size: "4", output2Size: "8" }; + const C = { $name: "C", opCode: "ADD", numOfInputPorts: 1, numOfConstants: 0, noOfCycles: 1, output1Size: "4", + input1: [{ inst: W, port: "output2" }] }; + W.output1 = []; W.output2 = [{ inst: C, port: "input1" }]; + const r = run({ "/f": { $instances: [W, C] } }); + assert.strictEqual(r.failed, 0); + // W: output1 R0 (no consumer, released), output2 = R1/R2; C reads that pair + assert.strictEqual(r.instrs[0], "WIDE R0, R1, R2", r.instrs[0]); + assert.strictEqual(r.instrs[1], "ADD R0, R1 , R2", r.instrs[1]); +} else { throw new Error("unknown case " + CASE); } +console.log("DRIVER OK " + CASE); +""" + + +def _case(tmp_path, case): + driver = tmp_path / ("driver_%s.js" % case) + driver.write_text(DRIVER, encoding="utf-8") + res = subprocess.run(["node", str(driver), ALLOC, case], capture_output=True, text=True, timeout=120) + assert res.returncode == 0 and ("DRIVER OK " + case) in res.stdout, \ + "node driver case %r failed:\n%s\n%s" % (case, res.stdout, res.stderr) + + +def test_wide_secondary_output_gets_a_register_pair(tmp_path): + _case(tmp_path, "wide-extra-output") diff --git a/tests/test_shared_memvar_rename.py b/tests/test_shared_memvar_rename.py new file mode 100644 index 0000000..39f3ef3 --- /dev/null +++ b/tests/test_shared_memvar_rename.py @@ -0,0 +1,53 @@ +"""Renaming one Memory Access block's memVar renames it for every sharing block. + +`rd.memVar.labelName = in_buf; wr.memVar.labelName = out_buf` leaves both blocks on out_buf +and never reserves in_buf. The block must warn when two Memory Access blocks share one memVar +object, and its AI guidance must show explicit Memory Variable instances assigned by reference. +Runs the REAL memory_access_block.syscfg.js through scripts/shared_memvar_runner.js. +""" +import json +import os +import shutil +import subprocess + +import pytest + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +RUNNER = os.path.join(ROOT, "scripts", "shared_memvar_runner.js") + +needs_node = pytest.mark.skipif(shutil.which("node") is None, reason="node not installed") + + +@pytest.fixture(scope="module") +def result(): + res = subprocess.run(["node", RUNNER], capture_output=True, text=True) + assert res.stdout.strip(), res.stderr + return json.loads(res.stdout) + + +@needs_node +def test_two_blocks_sharing_one_memvar_warn_on_both(result): + for name in ("rd", "wr"): + entry = result["cases"]["sharedMemVar"][name] + assert entry["errors"] == [] + assert len(entry["warnings"]) == 1, entry + assert "Shares one Memory Variable" in entry["warnings"][0] + assert "renames the variable for every block" in entry["warnings"][0] + assert "wr" in result["cases"]["sharedMemVar"]["rd"]["warnings"][0] + assert "rd" in result["cases"]["sharedMemVar"]["wr"]["warnings"][0] + + +@needs_node +@pytest.mark.parametrize("case", ["separateMemVars", "symbolSelected", "single"]) +def test_no_warning_without_a_shared_rename_hazard(result, case): + for name, entry in result["cases"][case].items(): + assert entry["warnings"] == [], (case, name, entry) + assert entry["errors"] == [], (case, name, entry) + + +@needs_node +def test_guidance_shows_explicit_memory_variable_instances(result): + g = result["guidance"] + assert "WRONG" in g and "rd.memVar.labelName" in g + assert "rd.memVar = in_buf;" in g and "wr.memVar = out_buf;" in g + assert 'memory_reserve.addInstance()' in g diff --git a/tests/test_syscfg_add_instance.py b/tests/test_syscfg_add_instance.py new file mode 100644 index 0000000..1a64f9e --- /dev/null +++ b/tests/test_syscfg_add_instance.py @@ -0,0 +1,81 @@ +"""Exercise each block's real SysConfig 1.28 addInstance path when available.""" +from __future__ import annotations + +import os +from pathlib import Path +import subprocess +import tempfile + +import pytest + + +ROOT = Path(__file__).resolve().parents[1] +# Default install location of TI tools, as in imports.mak (TOOLS_PATH); override with SYSCONFIG_CLI / SYSCONFIG_NODE. +TOOLS = Path(os.environ.get("TOOLS_PATH", (os.environ.get("SystemDrive", "C:") + "/ti") if os.name == "nt" else Path.home() / "ti")) +SYS_CONFIG = Path(os.environ.get("SYSCONFIG_CLI", TOOLS / "sysconfig_1.28.0" / "dist" / "cli.js")) +NODE = Path(os.environ.get("SYSCONFIG_NODE", TOOLS / "sysconfig_1.28.0" / "nodejs" / ("node.exe" if os.name == "nt" else "node"))) +PRODUCT = ROOT / ".metadata" / "product.json" +DEVICE = "AM243x_ALX_beta" +PACKAGE = "ALX" +CONTEXT = "icss_g0_pru0" + + +def _available() -> bool: + return NODE.is_file() and SYS_CONFIG.is_file() + + +pytestmark = pytest.mark.skipif(not _available(), reason="SysConfig 1.28 CLI is not installed") + + +def _modules() -> list[str]: + base = ROOT / ".metadata" / "sysconfig" / ".meta" / "pru_blocks" + return [ + "/" + p.relative_to(base.parent).as_posix().removesuffix(".syscfg.js") + for p in sorted(base.rglob("*.syscfg.js")) + if p.name != "pru_blocks_static_module.syscfg.js" + ] + + +def _run(module_path: str) -> subprocess.CompletedProcess[str]: + temp = Path(tempfile.mkdtemp(prefix="syscfg-add-instance-")) + script = temp / "probe.syscfg" + script.write_text( + "// @cliArgs --device AM243x_ALX_beta --package ALX --part ALX " + "--context icss_g0_pru0 --product PRU_NO_CODE_TOOL@01.00.01\n" + f'scripting.addModule("{module_path}", {{}}, false).addInstance();\n', + encoding="utf-8", + ) + return subprocess.run( + [str(NODE), str(SYS_CONFIG), "--product", str(PRODUCT), "--device", DEVICE, + "--package", PACKAGE, "--part", PACKAGE, "--script", str(script), + "--context", CONTEXT, "--output", str(temp / "out")], + cwd=ROOT, text=True, capture_output=True, timeout=60, + ) + + +def _assert_instantiated(module_path: str, result: subprocess.CompletedProcess[str]) -> None: + output = result.stdout + result.stderr + assert "Running script..." in output, f"SysConfig did not run {module_path}:\n{output}" + assert "Exception occurred calling addInstance()" not in output, ( + f"SysConfig failed to instantiate {module_path}:\n{output}" + ) + assert "Exception occurred calling scripting.addModule()" not in output, ( + f"SysConfig rejected module path {module_path}:\n{output}" + ) + + +def test_every_block_module_adds_an_instance() -> None: + modules = _modules() + assert modules, "no pru_blocks modules discovered" + for module_path in modules: + _assert_instantiated(module_path, _run(module_path)) + + +def test_an_invalid_module_path_is_rejected() -> None: + """The add-instance assertion fails when a design names no module.""" + result = _run("/pru_blocks/this_module_does_not_exist") + output = result.stdout + result.stderr + assert "Exception occurred calling scripting.addModule()" in output + # The check used by the all-module test must reject this deliberately bad design. + with pytest.raises(AssertionError, match="rejected module path"): + _assert_instantiated("/pru_blocks/this_module_does_not_exist", result) diff --git a/tests/test_uart_crc_firmware.py b/tests/test_uart_crc_firmware.py new file mode 100644 index 0000000..305a06a --- /dev/null +++ b/tests/test_uart_crc_firmware.py @@ -0,0 +1,160 @@ +"""examples/uart_crc/firmware/main.asm must read the registers the generated code really uses. + +main.asm runs after the generated code and reads block results straight from registers the register +allocator chose. For am243x-lp and am261x-lp, pru0 and pru1, this test generates the example's own +example.syscfg with SysConfig, links it with the example's main.asm and runs the clpru ELF on TI +pru-simulator: + * The SMEM base address in main.asm (not mapped in the simulator) becomes a DMEM address, and on pru1 + the UART RX macro, which waits for a frame on a pin nothing drives here, is replaced by + `LDI R0, `: R0 is the register that macro delivers its frame in. + * pru1, frame 0xC041 (data 0x41, CRC 0xC0 = CRC-8 poly 0x07 of 0x41): SMEM[2] == 0x01 (match), + SMEM[0] == SMEM[1] == 0xC0, SMEM[3] == 0x41. + * pru1, frame 0x1141 (data 0x41, wrong CRC 0x11): SMEM[2] == 0xFF (mismatch), SMEM[0] == 0xC0, + SMEM[1] == 0x11. + * pru0: SMEM[4] == the TX data byte 0xAA (its CRC is 0x5F). +Expected values come from a plain-Python CRC-8, never from the asm. +Skips when SysConfig, clpru or the pru-simulator checkout (PRU_SIMULATOR_PATH) is missing. +""" +import os +import re +import shutil +import struct +import subprocess +import sys +import tempfile + +import pytest + +REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +EX = os.path.join(REPO_ROOT, "examples", "uart_crc", "firmware") +MAIN_ASM = os.path.join(EX, "main.asm") +PRODUCT_JSON = os.path.join(REPO_ROOT, ".metadata", "product.json") +SMEM_STUB = 0x1800 # DMEM address that stands in for the real SMEM base + +TI_DIR = os.path.join(os.environ.get("SystemDrive", "C:") + os.sep, "ti") +SYSCFG_DIR = os.environ.get("SYSCFG_PATH", os.path.join(TI_DIR, "sysconfig_1.28.0")) +SYSCFG_NODE = os.path.join(SYSCFG_DIR, "nodejs", "node.exe" if os.name == "nt" else "node") +SYSCFG_CLI = os.path.join(SYSCFG_DIR, "dist", "cli.js") +CGT_DIR = os.environ.get("CGT_TI_PRU_PATH") or os.environ.get("PRU_CGT_PATH") or os.path.join(TI_DIR, "ti-cgt-pru_2.3.3") +CLPRU = os.path.join(CGT_DIR, "bin", "clpru.exe" if os.name == "nt" else "clpru") +SIMDIR = os.environ.get("PRU_SIMULATOR_PATH", "") + +HAVE_TOOLS = all(os.path.exists(p) for p in (SYSCFG_NODE, SYSCFG_CLI, CLPRU)) and \ + os.path.isfile(os.path.join(SIMDIR, "simulator.py")) +needs_tools = pytest.mark.skipif(not HAVE_TOOLS, reason="SysConfig, clpru or TI pru-simulator (PRU_SIMULATOR_PATH) missing") + +CORES = [ + # id, firmware dir, SysConfig --device/--part/--package/--context, -D flags, SMEM base in main.asm + ("am243x-pru0", "am243x-lp/icss_g0_pru0_fw", ("AM243x_ALX_beta", "ALX", "ALX", "icss_g0_pru0"), "PRU0", "SOC_AM243X", "0x30010000"), + ("am243x-pru1", "am243x-lp/icss_g0_pru1_fw", ("AM243x_ALX_beta", "ALX", "ALX", "icss_g0_pru1"), "PRU1", "SOC_AM243X", "0x30010000"), + ("am261x-pru0", "am261x-lp/icss_m1_pru0_fw", ("AM261x_ZFG", "AM2612", "ZFG", "icss_m1_pru0"), "PRU0", "SOC_AM261X", "0x48010000"), + ("am261x-pru1", "am261x-lp/icss_m1_pru1_fw", ("AM261x_ZFG", "AM2612", "ZFG", "icss_m1_pru1"), "PRU1", "SOC_AM261X", "0x48010000"), +] +PRU0_CORES = [c for c in CORES if c[3] == "PRU0"] +PRU1_CORES = [c for c in CORES if c[3] == "PRU1"] +RX_MACRO = re.compile(r"^\s*m_uart_rx\S*.*$", re.M) + + +def crc8(data, poly=0x07): + """CRC-8 (MSB first, init 0): the CRC the example's lookup table implements.""" + crc = 0 + for byte in data: + crc ^= byte + for _ in range(8): + crc = ((crc << 1) ^ poly) & 0xFF if crc & 0x80 else (crc << 1) & 0xFF + return crc + + +def test_reference_crc_matches_the_documented_frames(): + assert crc8([0x41]) == 0xC0 + assert crc8([0x41]) != 0x11 + assert crc8([0xAA]) == 0x5F + + +def _run(cmd, cwd): + proc = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True) + assert proc.returncode == 0, "%s failed:\n%s\n%s" % (cmd[0], proc.stdout[-2000:], proc.stderr[-2000:]) + + +def build_and_run(core, patch_frame=None): + _id, fw, (device, part, package, context), pru, soc, smem = core + work = tempfile.mkdtemp(prefix="uart_crc_") + try: + _run([SYSCFG_NODE, SYSCFG_CLI, "--product", PRODUCT_JSON, "--device", device, "--context", context, + "--part", part, "--package", package, "--output", "syscfg", os.path.join(EX, fw, "example.syscfg")], work) + asm_path = os.path.join(work, "syscfg", "pru_syscfg.asm") + with open(asm_path, encoding="utf-8") as f: + asm = f.read() + if patch_frame is not None: + asm, n = RX_MACRO.subn(" LDI R0, %d" % patch_frame, asm) + assert n == 1, "the pru1 design must contain exactly one UART RX macro" + with open(asm_path, "w", encoding="utf-8") as f: + f.write(asm) + with open(MAIN_ASM, encoding="utf-8") as f: + main = f.read() + assert smem in main, "main.asm no longer loads the SMEM base %s" % smem + main_path = os.path.join(work, "main_dmem.asm") + with open(main_path, "w", encoding="utf-8") as f: + f.write(main.replace(smem, "0x%X" % SMEM_STUB)) + cflags = ["-v4", "-O2", "--include_path=" + os.path.join(CGT_DIR, "include"), "-D" + pru, "-DSLICE0", "-D" + soc, + "--include_path=" + os.path.dirname(asm_path)] + _run([CLPRU] + cflags + ["--output_file=main.obj", main_path], work) + _run([CLPRU] + cflags + ["--output_file=pru_syscfg.obj", asm_path], work) + _run([CLPRU, "-v4", "-O2", "-z", "main.obj", "pru_syscfg.obj", os.path.join(EX, fw, "ti-pru-cgt", "linker.cmd"), + "--warn_sections", "--reread_libs", "--ram_model", "--entry_point=main", "--disable_auto_rts", + "-o", "fw.out"], work) + with open(os.path.join(work, "fw.out"), "rb") as f: + elf = f.read() + finally: + shutil.rmtree(work, ignore_errors=True) + entry = struct.unpack_from("