pico6502 Developer's Guide
Note: The pico6502 project is in early development. The driver framework and memory model are fully implemented and share the same architecture as the picoZ80. However, only a placeholder BBC Model B persona exists at this time — this guide explains how to build upon that foundation to add your own drivers. Where the pico6502 is architecturally identical to the picoZ80, cross-references to the picoZ80 Developer's Guide are provided.
Overview
The pico6502 firmware is structured identically to the picoZ80 firmware. It uses the same dual-core RP2350B architecture, the same PSRAM memory model, the same JSON configuration system, and the same driver framework — the primary differences are:
- M6502CPU.c / M6502CPU.h replace Z80CPU.c / Z80CPU.h as the main dispatch and driver-framework files.
- The 6502 has no separate I/O address space — there is no
ioPtr[]array and noiomapJSON section. All peripherals are virtualised asFUNC-type blocks in the 64KB memory map. - The PIO bus interface (
M6502.pio) implements dual-phase PHI1/PHI2 clocking rather than the Z80 single-clock model. - The build target directory is
BaseM6502rather thanBaseZ80. - The JSON CPU section key is
"6502"rather than"z80".
Everything else — the inter-core queue pattern, driver lifecycle callbacks (
reset_ptr, poll_ptr, task_ptr), the virtualFuncMap[] registration table, memory block type constants, the t_6502PSRAM struct, the ESP32 co-processor protocol, and the build system — is shared with the picoZ80 codebase.
Source Tree
All source code lives under
projects/tzpuPico/ within the repository root. The layout below highlights files specific to the pico6502:
tzpuPico/
├── CMakeLists.txt Top-level build file
├── src/
│ ├── CMakeLists.txt Source-level build file — add new driver files here
│ ├── M6502CPU.c *** KEY FILE: 6502 dispatch, driver framework, virtualFuncMap
│ ├── M6502CPU.h (legacy — included via include/)
│ ├── M6502.c PIO initialisation and bus cycle entry points
│ ├── M6502.pio PIO programs: clock, addr, data, cycle, fetch, read, write, irq, nmi, so
│ ├── Z80CPU.c / Z80CPU.h Z80 counterpart (not used for pico6502 builds)
│ ├── FSPI.c / FSPI.h Flash SPI interface (shared)
│ ├── ESP.c / ESP.h ESP32 communication layer (shared)
│ ├── psram.c / psram.h PSRAM allocation and management (shared)
│ ├── cJSON.c / cJSON.h JSON parser for config.json (shared)
│ ├── include/
│ │ ├── M6502CPU.h *** KEY FILE: all 6502 type definitions and macros
│ │ ├── M6502.h PIO helper definitions
│ │ └── drivers/
│ │ └── BBC/
│ │ └── ModelB.h BBC Model B driver header (early placeholder)
│ ├── drivers/
│ │ └── BBC/
│ │ └── ModelB.c *** EXAMPLE/PLACEHOLDER DRIVER (BBC Model B persona)
│ └── model/
│ ├── BaseM6502/
│ │ ├── CMakeLists.txt Per-model build targets
│ │ ├── main.c Entry point (Core 0 + Core 1 launch)
│ │ ├── main_memmap_partition_1.ld Linker script for Slot 1
│ │ └── main_memmap_partition_2.ld Linker script for Slot 2
│ └── Bootloader/
│ ├── CMakeLists.txt
│ └── main.c Shared bootloader (same as picoZ80)
└── esp32/
├── CMakeLists.txt
└── main/
└── ... ESP32 firmware (shared with picoZ80)
Key Differences from picoZ80
No Separate I/O Address Space
The most important architectural difference is that the 6502 has no
IN/OUT instructions and no IORQ signal. Every peripheral must appear somewhere in the 64KB memory map. In the picoZ80, virtual devices can be registered via either memioPtr[] (memory-mapped) or ioPtr[] (I/O-mapped). In the pico6502 there is only memioPtr[] — but since all peripheral accesses come through the memory bus, this covers all cases.
The
t_6502PSRAM struct therefore has no ioPtr[] array. The JSON configuration has no "io" array. The dispatch loop in M6502CPU.c checks only the _membankPtr[] array and, for MEMBANK_TYPE_FUNC blocks, calls the corresponding memioPtr[] handler.
PIO Bus Interface Differences
The 6502 uses a two-phase non-overlapping clock scheme. PHI0 is an external clock input; the RP2350 generates PHI1 and PHI2 from it via a dedicated PIO 2 state machine (
m6502_clock_6502). This is different from the Z80, which uses a single CLK signal.
| PIO Block | State Machines | Purpose |
|---|---|---|
| PIO 0 | m6502_addr, m6502_data |
Address bus (A0–A15), data bus (D0–D7) |
| PIO 1 | m6502_cycle, m6502_fetch, m6502_read, m6502_write, m6502_irq, m6502_nmi, m6502_so |
Bus cycle sequencing and control signal monitoring |
| PIO 2 | m6502_clock_6502 |
PHI1/PHI2 two-phase clock generation from external PHI0 |
The IRQ convention differs slightly from the Z80 PIO interface:
| IRQ Flag | Signal | Meaning |
|---|---|---|
| IRQ 0 | Address/cycle start | New 6502 bus cycle — address valid on PHI1 rising edge |
| IRQ 1 | Data phase | Data bus active — read or write depending on RNW |
| IRQ 4 | NMI | Non-Maskable Interrupt asserted |
| IRQ 5 | IRQ | Maskable Interrupt asserted |
| IRQ 6 | SO | Set Overflow pin asserted |
| IRQ 7 | Loop exit | Core 1 execution loop exit request |
PIO Programming — How C Code Drives Bus Cycles
The pico6502 uses the same
out exec mechanism as the picoZ80 for dynamic instruction injection, but with 32-bit instructions (out exec, 32) instead of 16-bit. The C code on Core 1 pushes pre-encoded PIO instruction sequences into the cycle SM's TX FIFO to control each bus transaction.
The coordination flow for a 6502 memory read cycle:
Core 1 (C code) PIO State Machines
───────────── ──────────────────
1. Resolve address m6502_cycle: IRQ 0 set
from memory map (waiting on PHI2 low)
│
2. Push addr → TX FIFO ──────────────────→ m6502_addr: receives addr
Clear IRQ 0 outputs A0–A15 on pins
│
3. Push cycle instructions ──────────────→ m6502_cycle: out exec, 32
(read sequence) into executes: set R/W̄ high, SYNC low
cycle SM TX FIFO executes: wait PHI2 high
│ executes: check RDY
4. Wait for RX FIFO ←──────────────────── m6502_data: samples D0–D7
(data byte from bus) at PHI2 falling edge
│
5. Read data from m6502_cycle: JMP start_cycle
RX FIFO (ready for next cycle)
│
6. Dispatch to PSRAM
or driver handler
For write cycles, Core 1 pushes the data byte to the
m6502_data SM's TX FIFO after the address. The data SM drives D0–D7 during the PHI2 high phase, then tristates the data bus when PHI2 falls. The cycle SM sets R/W̄ low to indicate a write.
For a detailed explanation of PIO fundamentals, the out exec mechanism, and state machine coordination patterns, see the picoZ80 Technical Guide — PIO Architecture Deep Dive. The 6502-specific PIO differences are:
- 32-bit
out exec— allows full PIO instruction encoding including side-set and delay fields in a single FIFO push. - Clock-phase synchronisation — cycle SMs synchronise to PHI2 edges (
wait 0/1 gpio CLK2) rather than a single CLK signal. - No BUSREQ/BUSACK — the 6502 has no bus request mechanism, so there is no equivalent of the Z80
z80_busrqSM. - RDY instead of WAIT — wait states are inserted by checking the RDY pin after PHI2 rising edge, rather than the Z80's
/WAITpin at T2 falling edge.
t_6502PSRAM Struct
Equivalent to
t_Z80PSRAM in the picoZ80, but without the ioPtr[] array:
// src/include/M6502CPU.h (simplified)
typedef struct {
uint8_t RAM[MAX_MEMORY_BANKS * MEMORY_PAGE_SIZE]; // 64 banks × 64KB = 4MB RAM/ROM area
MemoryFunc memPtr[MEMORY_PAGE_SIZE]; // 64K per-byte read redirect (PTR type)
MemoryFunc memioPtr[MEMORY_PAGE_SIZE]; // 64K memory-mapped handler array (FUNC type)
// NOTE: no ioPtr[] — the 6502 has no separate I/O space
} t_6502PSRAM;
Handler Function Signature
The
MemoryFunc typedef is the same as for the picoZ80:
// Handler called for MEMBANK_TYPE_FUNC blocks on every access typedef uint8_t (*MemoryFunc)(M6502CPU *cpu, bool read, uint16_t addr, uint8_t data); // Parameters: // cpu — pointer to the M6502CPU struct (access PSRAM, state, queues via this) // read — true for a 6502 read cycle (RNW=1), false for a write cycle (RNW=0) // addr — 16-bit address that triggered this handler // data — byte written (only meaningful when read=false) // Return: // byte to place on the data bus (only meaningful when read=true)
Because there is no I/O space, there is no separate
IOFunc typedef — MemoryFunc handles all peripheral accesses.
Driver Framework
The driver framework in
M6502CPU.c is structurally identical to the picoZ80 driver framework. Refer to the picoZ80 Developer's Guide — Driver Framework section for the full description of:
- The
virtualFuncMap[]registration table and how the JSON"name"field is matched to a driver init function. - The
VirtualFunctypedef andM6502CPU_getVirtualFunc()lookup. - The two-phase init flow (validation pass then configuration pass).
- Driver lifecycle callbacks:
reset_ptr,poll_ptr,task_ptr.
The only naming differences are:
| picoZ80 | pico6502 | Notes |
|---|---|---|
Z80CPU struct |
M6502CPU struct |
Same fields, same layout |
Z80CPU_getVirtualFunc() |
M6502CPU_getVirtualFunc() |
Same lookup logic |
Z80CPU_cpu() |
M6502CPU_cpu() |
Core 1 hot loop entry point |
Z80CPU_readMem() |
M6502CPU_readMem() |
Memory read dispatch |
Z80CPU_writeMem() |
M6502CPU_writeMem() |
Memory write dispatch |
Z80CPU_readIO() |
— | No equivalent — 6502 has no I/O space |
Z80CPU_writeIO() |
— | No equivalent — 6502 has no I/O space |
cpu->_z80PSRAM |
cpu->_6502PSRAM |
PSRAM struct pointer |
Core 1 Dispatch Loop
The Core 1 hot loop in
M6502CPU_cpu() follows the same pattern as the picoZ80. On each 6502 bus cycle:
- Wait for IRQ 0 (address valid on PHI1).
- Read the 16-bit address from the PIO 0 RX FIFO.
- Look up
_membankPtr[addr >> 9]to get the block type, bank, and base. - Determine RNW (read/not-write) from the control PIO.
- Dispatch based on block type: PHYSICAL (pass to host), RAM (PSRAM read/write), ROM (PSRAM read only), FUNC (call
memioPtr[addr]), PTR (redirect viamemPtr[addr]). - If read: place result on data bus via PIO 0 TX FIFO. Wait for IRQ 1 (data phase). If write: read data from PIO 0 RX FIFO after IRQ 1.
- Call
poll_ptrfor each registered driver (every ~2048 cycles).
// Simplified M6502CPU_readMem / M6502CPU_writeMem dispatch
uint8_t M6502CPU_readMem(M6502CPU *cpu, uint16_t addr)
{
uint32_t entry = cpu->_membankPtr[addr >> 9];
uint8_t type = (entry >> 24) & 0xFF;
uint8_t bank = (entry >> 16) & 0xFF;
uint32_t base = (entry & 0xFFFF) << 9;
switch(type)
{
case MEMBANK_TYPE_RAM:
case MEMBANK_TYPE_ROM:
return cpu->_6502PSRAM->RAM[(bank * MEMORY_PAGE_SIZE) + addr];
case MEMBANK_TYPE_FUNC:
if(cpu->_6502PSRAM->memioPtr[addr] != NULL)
return cpu->_6502PSRAM->memioPtr[addr](cpu, true, addr, 0);
return 0x00;
case MEMBANK_TYPE_PTR:
if(cpu->_6502PSRAM->memPtr[addr] != NULL)
return cpu->_6502PSRAM->memPtr[addr](cpu, true, addr, 0);
return 0x00;
case MEMBANK_TYPE_PHYSICAL:
default:
return 0xFF; // Pass-through to real host hardware
}
}
Writing a New Driver
The process for writing a pico6502 driver is identical to the picoZ80 process described in the picoZ80 Developer's Guide — Writing a New Driver. The six steps are the same:
- Create a header file in
src/include/drivers/<Family>/MyDriver.h - Create an implementation file in
src/drivers/<Family>/MyDriver.c - Add the source file to
src/CMakeLists.txt - Include the driver in
M6502CPU.cunder a#ifdef INCLUDE_<FAMILY>_DRIVERSguard - Register the driver in
virtualFuncMap[]inM6502CPU.c - Add a JSON driver entry in
config.json
The key difference is that instead of a
Z80CPU *cpu parameter, all functions receive an M6502CPU *cpu parameter, and you access PSRAM via cpu->_6502PSRAM rather than cpu->_z80PSRAM. There are no ioPtr[] slots to install — all virtual peripherals are registered in memioPtr[].
Minimal Driver Template
The following shows a minimal 6502 peripheral driver — a virtualised 6522 VIA (Versatile Interface Adapter) at address
0xC000–0xC00F:
// src/include/drivers/BBC/VIA6522.h #ifndef VIA6522_H #define VIA6522_H #include "M6502CPU.h" int VIA6522_Init(M6502CPU *cpu, t_drvConfig *drvConfig, int pass); #endif
// src/drivers/BBC/VIA6522.c
#include "../../include/M6502CPU.h"
#include "../../include/drivers/BBC/VIA6522.h"
// --- Internal state ---
typedef struct {
uint8_t regs[16]; // VIA internal registers (ORB, ORA, DDRB, DDRA, T1CL, T1CH, ...)
bool irqPending;
} t_VIA6522State;
static t_VIA6522State ViaState = { .irqPending = false };
// --- Memory handler (called for every access to 0xC000–0xC00F) ---
static uint8_t VIA6522_Handler(M6502CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
uint8_t reg = addr & 0x0F; // VIA has 16 internal registers
if(read)
{
return ViaState.regs[reg];
}
else
{
ViaState.regs[reg] = data;
// TODO: implement timer, shift register, port logic as needed
return 0;
}
}
// --- Reset handler (called when host system resets) ---
static uint8_t VIA6522_Reset(M6502CPU *cpu)
{
memset(&ViaState.regs, 0, sizeof(ViaState.regs));
ViaState.irqPending = false;
return 0;
}
// --- Init function (called during JSON config parse) ---
int VIA6522_Init(M6502CPU *cpu, t_drvConfig *drvConfig, int pass)
{
if(pass == 0)
{
// Validation pass — just check the name matches
return (strcasecmp(drvConfig->name, "via6522") == 0) ? 1 : 0;
}
// Configuration pass — install handlers
// Install the handler for all 16 VIA registers at 0xC000–0xC00F
for(uint16_t addr = 0xC000; addr <= 0xC00F; addr++)
{
cpu->_6502PSRAM->memioPtr[addr] = VIA6522_Handler;
}
// Register reset callback
cpu->reset_ptr = VIA6522_Reset;
debugf("VIA6522: Initialised at 0xC000–0xC00F\n");
return 1;
}
JSON Configuration
To activate the VIA6522 driver, add a
FUNC block to the memory map and a driver entry:
{
"rp2350": {
"6502": {
"memory": [
{ "enable":1, "addr":"0x0000", "size":"0xC000",
"type":"RAM", "bank":0, "task":"", "file":"" },
{ "enable":1, "addr":"0xC000", "size":"0x0010",
"type":"FUNC", "bank":0, "task":"via6522", "file":"" },
{ "enable":1, "addr":"0xE000", "size":"0x2000",
"type":"ROM", "bank":0, "task":"", "file":"/ROM/bios.rom" }
],
"drivers": [
{ "enable":1, "name":"via6522" }
]
}
}
}
Registering in virtualFuncMap
Add an entry to the
virtualFuncMap[] table in M6502CPU.c. The name string is matched case-insensitively against the JSON driver "name" field:
// In M6502CPU.c — add to virtualFuncMap[]
static const t_VirtualFuncMap virtualFuncMap[] = {
{ "BBCModelB", BBCModelB_Init }, // existing entry
{ "via6522", VIA6522_Init }, // your new driver
{ NULL, NULL } // terminator
};
CMakeLists.txt Change
Add the new source file and enable it via a compile definition in
src/CMakeLists.txt:
# In src/CMakeLists.txt
target_sources(tzpuPico PRIVATE
...
drivers/BBC/ModelB.c # existing
drivers/BBC/VIA6522.c # your new driver
)
target_compile_definitions(tzpuPico PRIVATE
INCLUDE_BBC_DRIVERS=1
)
Build and Test
# From the project root ./build_tzpuPico.sh # Flash via USB mass-storage cp build/bin/model/BaseM6502/BaseM6502_0x10020000.uf2 /media/$USER/RPI-RP2/ # Or OTA: upload via http://<device-ip>/ota-rp2350.htm
Memory Hook Patterns
Because the 6502 has no I/O space, all hook patterns use
memioPtr[]. The patterns are the same as those described in the picoZ80 Developer's Guide — Memory Hook Patterns, with the I/O port handler pattern replaced by a memory-mapped handler at any address range. The key patterns for 6502 drivers are:
FUNC Block — Virtual Peripheral
Install a handler for a contiguous address range. This is the standard pattern for all 6502 peripheral emulation:
// Install handler for address range 0xD000–0xD0FF (256-byte peripheral block)
for(uint16_t addr = 0xD000; addr <= 0xD0FF; addr++)
{
cpu->_6502PSRAM->memioPtr[addr] = MyPeripheral_Handler;
}
RAM Write Intercept
Intercept writes to a RAM region while still allowing reads from PSRAM. Useful for shadowing zero-page accesses or detecting writes to specific addresses:
// Handler intercepts writes; passes reads to PSRAM
static uint8_t ZeroPage_Intercept(M6502CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
if(read)
{
// Read from PSRAM normally
return cpu->_6502PSRAM->RAM[addr];
}
else
{
// Write to PSRAM and update shadow state
cpu->_6502PSRAM->RAM[addr] = data;
MyDriver_OnZeroPageWrite(addr, data);
return 0;
}
}
// In init: set the block type to FUNC and install the handler
cpu->_membankPtr[0x0000 >> 9] = MEMBANK_ENCODE(MEMBANK_TYPE_FUNC, 0, 0x0000);
cpu->_6502PSRAM->memioPtr[0x0000] = ZeroPage_Intercept;
// ... repeat for each 512-byte block in the range
Sparse / Individual Addresses
For hardware where only a few addresses matter within a larger region (e.g. a 4-bit address decoder), install the handler only on those specific addresses and let the rest return
0xFF or PSRAM data:
// Only addresses 0xFFFE and 0xFFFF (6502 reset vector) are intercepted cpu->_6502PSRAM->memioPtr[0xFFFE] = ResetVector_Lo_Handler; cpu->_6502PSRAM->memioPtr[0xFFFF] = ResetVector_Hi_Handler;
BBC Model B Persona (Placeholder)
The only driver currently present in the pico6502 firmware is the BBC Model B persona (
src/drivers/BBC/ModelB.c). This is an early-stage placeholder that demonstrates the driver registration pattern but does not yet implement the full BBC Micro hardware — the 6845 CRTC, 6522 VIAs, 6850 ACIA, SAA5050 teletext chip, and ULA are not virtualised.
The BBC Model B is an appropriate first target for the pico6502 because it uses a standard 6502 at 2MHz, has well-documented hardware, and relies entirely on memory-mapped peripherals — which aligns naturally with the pico6502's FUNC-based peripheral model. When the persona is complete, it will need to virtualise the following at minimum:
| Address | Width | Chip | Function |
|---|---|---|---|
0xFC00–0xFCFF |
256B | 6845 CRTC | Video timing / memory address generator |
0xFE00–0xFE0F |
16B | 6522 VIA A | System VIA (keyboard, sound, RTC) |
0xFE40–0xFE4F |
16B | 6522 VIA B | User VIA (parallel port, user I/O) |
0xFE60–0xFE6F |
16B | 6850 ACIA | Serial interface |
0xFC00–0xFCFF |
256B | SAA5050 / ULA | Teletext / video ULA |
0xFE30 |
1B | ROM select | Sideways ROM bank select register |
Core 0 / Core 1 Interaction
The inter-core queue pattern is identical to the picoZ80. Refer to the picoZ80 Developer's Guide — Core 0 / Core 1 Interaction for the full description and code example. The only naming difference is that the queue is accessed via an
M6502CPU *cpu pointer rather than Z80CPU *cpu.
Key rule: handler and poll callbacks run on Core 1. Any file I/O, UART command to the ESP32, or other blocking operation must be posted to Core 0 via
cpu->requestQueue. Results are returned via cpu->responseQueue and processed in task_ptr.
Common Pitfalls
- Installing a handler in ioPtr instead of memioPtr. The pico6502 has no
ioPtr[]array. All peripheral handlers go inmemioPtr[]. Attempting to useioPtr[]will cause a build error or undefined behaviour. - Expecting an I/O dispatch call. There is no
M6502CPU_readIO()orM6502CPU_writeIO(). If you are porting a Z80 driver that usesioPtr[], move all those handlers tomemioPtr[]at the appropriate memory addresses. - Blocking in a handler. Any call to
debugf,sleep_ms, orfopenfrom inside a handler will stall Core 1 and corrupt 6502 bus timing. Use the inter-core queue for all blocking operations. - Mismatched virtualFuncMap name. The JSON
"name"field is matched case-insensitively against thevirtualFuncMap[]table inM6502CPU.c. A typo in either will silently skip the driver. Add a temporarydebugfinM6502CPU_getVirtualFunc()to diagnose. - Off-by-one in handler range. Use
addr <= endAddrnotaddr < endAddr + 1— the latter risks integer overflow at0xFFFF. Useaddr <= 0xFFFFwith auint32_tloop variable if the range extends to the top of memory. - Forgetting to set the block type to FUNC. Installing a handler in
memioPtr[]has no effect if the corresponding_membankPtr[]entries still showMEMBANK_TYPE_RAMorMEMBANK_TYPE_ROM. Always callMEMBANK_ENCODE(MEMBANK_TYPE_FUNC, ...)for each 512-byte block in your handler range. - reset_ptr not cleared on driver shutdown. If your driver is reconfigured at runtime and the reset callback is no longer valid, clear
cpu->reset_ptrtoNULLbefore re-initialising, otherwise Core 1 will call a stale function pointer on the next reset.
Reference Sites
| Resource | Link |
|---|---|
| pico6502 project page | /pico6502/ |
| pico6502 User Manual | /pico6502-usermanual/ |
| pico6502 Technical Guide | /pico6502-technicalguide/ |
| picoZ80 Developer’s Guide | /picoz80-developersguide/ |
| picoZ80 project page | /picoz80/ |
| RP2350 Datasheet | datasheets.raspberrypi.com |
| Pico SDK Multicore API | raspberrypi.github.io/pico-sdk-doxygen |
| MOS 6502 Datasheet | archive.org |
| BBC Micro Hardware Guide | stardot.org.uk |
| cJSON Library | github.com/DaveGamble/cJSON |
Wireless Regulatory Notice
This device incorporates an ESP32-S3-PICO-1 wireless module that transmits in the 2.4 GHz ISM band, making it an intentional radiator under radio-frequency regulations worldwide (including FCC Part 15 Subpart C in the United States, and the Radio Equipment Directive 2014/53/EU in the European Union).
Although the ESP32-S3-PICO-1 module itself carries pre-existing regulatory certifications (FCC, CE, and others), those module-level certifications do not automatically extend to a finished product that incorporates the module. The pre-certified module exemption permits individual hobbyists to build a limited number of devices for personal, experimental, or educational use without obtaining separate equipment authorisation.
Important Limitations
It is the builder’s sole responsibility to ensure that any device constructed from these designs complies with all applicable radio-frequency regulations in their jurisdiction. The author provides these designs for personal, educational, and hobbyist use and makes no representation that a device built from them satisfies the regulatory requirements for commercial distribution.
- Assembled devices must not be sold, offered for sale, gifted, or otherwise distributed to third parties unless the finished product has been independently tested and granted its own equipment authorisation (e.g. FCC ID, CE marking with a Notified Body assessment) in the relevant jurisdiction.
- Building this project for personal use in limited quantities is generally permitted under hobbyist and experimental-use provisions (e.g. FCC § 15.23), provided the device does not cause harmful interference.
- Regulatory requirements vary by country. Builders outside the United States should consult their national radio-frequency authority for applicable rules.
It is the builder’s sole responsibility to ensure that any device constructed from these designs complies with all applicable radio-frequency regulations in their jurisdiction. The author provides these designs for personal, educational, and hobbyist use and makes no representation that a device built from them satisfies the regulatory requirements for commercial distribution.