diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index 08196993c366a..efa38764c9ab6 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -625,6 +625,7 @@ msgstr "" #: ports/raspberrypi/common-hal/audiobusio/I2SOut.c #: ports/raspberrypi/common-hal/audiopwmio/PWMAudioOut.c +#: ports/raspberrypi/common-hal/mcp4822/MCP4822.c msgid "Audio source error" msgstr "" @@ -1300,6 +1301,7 @@ msgstr "" msgid "Invalid %q" msgstr "" +#: ports/raspberrypi/common-hal/mcp4822/MCP4822.c #: ports/raspberrypi/common-hal/picodvi/Framebuffer_RP2350.c #: shared-module/aurora_epaper/aurora_framebuffer.c msgid "Invalid %q and %q" @@ -1537,6 +1539,7 @@ msgstr "" #: ports/atmel-samd/common-hal/audioio/AudioOut.c #: ports/raspberrypi/common-hal/audiobusio/I2SOut.c #: ports/raspberrypi/common-hal/audiopwmio/PWMAudioOut.c +#: ports/raspberrypi/common-hal/mcp4822/MCP4822.c msgid "No DMA channel found" msgstr "" @@ -1651,7 +1654,7 @@ msgid "Not connected" msgstr "" #: shared-bindings/audiobusio/I2SOut.c shared-bindings/audioio/AudioOut.c -#: shared-bindings/audiopwmio/PWMAudioOut.c +#: shared-bindings/audiopwmio/PWMAudioOut.c shared-bindings/mcp4822/MCP4822.c msgid "Not playing" msgstr "" @@ -2168,6 +2171,7 @@ msgid "Too many channels in sample" msgstr "" #: ports/raspberrypi/common-hal/audiobusio/I2SOut.c +#: ports/raspberrypi/common-hal/mcp4822/MCP4822.c msgid "Too many channels in sample." msgstr "" @@ -2266,6 +2270,7 @@ msgstr "" #: ports/atmel-samd/common-hal/audioio/AudioOut.c #: ports/raspberrypi/common-hal/audiobusio/I2SOut.c #: ports/raspberrypi/common-hal/audiopwmio/PWMAudioOut.c +#: ports/raspberrypi/common-hal/mcp4822/MCP4822.c msgid "Unable to allocate buffers for signed conversion" msgstr "" diff --git a/ports/raspberrypi/boards/mtm_computer/board.c b/ports/raspberrypi/boards/mtm_computer/board.c index e6a868ab21226..4fec0cd5b8b59 100644 --- a/ports/raspberrypi/boards/mtm_computer/board.c +++ b/ports/raspberrypi/boards/mtm_computer/board.c @@ -5,5 +5,36 @@ // SPDX-License-Identifier: MIT #include "supervisor/board.h" +#include "shared-bindings/mcp4822/MCP4822.h" +#include "shared-bindings/microcontroller/Pin.h" +#include "peripherals/pins.h" +#include "py/runtime.h" +// board.DAC() — factory function that constructs an mcp4822.MCP4822 with +// the MTM Workshop Computer's DAC pins (GP18=SCK, GP19=SDI, GP21=CS). + +static mp_obj_t board_dac_singleton = MP_OBJ_NULL; + +static mp_obj_t board_dac_factory(void) { + if (board_dac_singleton == MP_OBJ_NULL) { + mcp4822_mcp4822_obj_t *dac = mp_obj_malloc_with_finaliser( + mcp4822_mcp4822_obj_t, &mcp4822_mcp4822_type); + common_hal_mcp4822_mcp4822_construct( + dac, + &pin_GPIO18, // clock (SCK) + &pin_GPIO19, // mosi (SDI) + &pin_GPIO21, // cs + 1); // gain 1x + board_dac_singleton = MP_OBJ_FROM_PTR(dac); + } + return board_dac_singleton; +} +MP_DEFINE_CONST_FUN_OBJ_0(board_dac_obj, board_dac_factory); + +void board_deinit(void) { + if (board_dac_singleton != MP_OBJ_NULL) { + common_hal_mcp4822_mcp4822_deinit(MP_OBJ_TO_PTR(board_dac_singleton)); + board_dac_singleton = MP_OBJ_NULL; + } +} // Use the MP_WEAK supervisor/shared/board.c versions of routines not defined here. diff --git a/ports/raspberrypi/boards/mtm_computer/mpconfigboard.mk b/ports/raspberrypi/boards/mtm_computer/mpconfigboard.mk index 718c393d1686f..45c99711d72a3 100644 --- a/ports/raspberrypi/boards/mtm_computer/mpconfigboard.mk +++ b/ports/raspberrypi/boards/mtm_computer/mpconfigboard.mk @@ -11,3 +11,4 @@ EXTERNAL_FLASH_DEVICES = "W25Q16JVxQ" CIRCUITPY_AUDIOEFFECTS = 1 CIRCUITPY_IMAGECAPTURE = 0 CIRCUITPY_PICODVI = 0 +CIRCUITPY_MCP4822 = 1 diff --git a/ports/raspberrypi/boards/mtm_computer/pins.c b/ports/raspberrypi/boards/mtm_computer/pins.c index 6987818233102..ffdf2687702c6 100644 --- a/ports/raspberrypi/boards/mtm_computer/pins.c +++ b/ports/raspberrypi/boards/mtm_computer/pins.c @@ -6,6 +6,8 @@ #include "shared-bindings/board/__init__.h" +extern const mp_obj_fun_builtin_fixed_t board_dac_obj; + static const mp_rom_map_elem_t board_module_globals_table[] = { CIRCUITPYTHON_BOARD_DICT_STANDARD_ITEMS @@ -21,7 +23,6 @@ static const mp_rom_map_elem_t board_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_PULSE_2_IN), MP_ROM_PTR(&pin_GPIO3) }, { MP_ROM_QSTR(MP_QSTR_GP3), MP_ROM_PTR(&pin_GPIO3) }, - { MP_ROM_QSTR(MP_QSTR_NORM_PROBE), MP_ROM_PTR(&pin_GPIO4) }, { MP_ROM_QSTR(MP_QSTR_GP4), MP_ROM_PTR(&pin_GPIO4) }, @@ -105,6 +106,9 @@ static const mp_rom_map_elem_t board_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_A3), MP_ROM_PTR(&pin_GPIO29) }, { MP_ROM_QSTR(MP_QSTR_GP29), MP_ROM_PTR(&pin_GPIO29) }, + // Factory function: dac = board.DAC() returns a configured mcp4822.MCP4822 + { MP_ROM_QSTR(MP_QSTR_DAC), MP_ROM_PTR(&board_dac_obj) }, + // { MP_ROM_QSTR(MP_QSTR_EEPROM_I2C), MP_ROM_PTR(&board_i2c_obj) }, }; MP_DEFINE_CONST_DICT(board_module_globals, board_module_globals_table); diff --git a/ports/raspberrypi/common-hal/mcp4822/MCP4822.c b/ports/raspberrypi/common-hal/mcp4822/MCP4822.c new file mode 100644 index 0000000000000..7a8ad7d4df29c --- /dev/null +++ b/ports/raspberrypi/common-hal/mcp4822/MCP4822.c @@ -0,0 +1,286 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tod Kurt +// +// SPDX-License-Identifier: MIT +// +// MCP4822 dual-channel 12-bit SPI DAC audio output. +// Uses PIO + DMA for non-blocking audio playback, mirroring audiobusio.I2SOut. + +#include +#include + +#include "mpconfigport.h" + +#include "py/gc.h" +#include "py/mperrno.h" +#include "py/runtime.h" +#include "common-hal/mcp4822/MCP4822.h" +#include "shared-bindings/mcp4822/MCP4822.h" +#include "shared-bindings/microcontroller/Pin.h" +#include "shared-module/audiocore/__init__.h" +#include "bindings/rp2pio/StateMachine.h" + +// ───────────────────────────────────────────────────────────────────────────── +// PIO program for MCP4822 SPI DAC +// ───────────────────────────────────────────────────────────────────────────── +// +// Pin assignment: +// OUT pin (1) = MOSI — serial data out +// SET pins (N) = MOSI through CS — for CS control & command-bit injection +// SIDE-SET pin (1) = SCK — serial clock +// +// SET PINS bit mapping (bit0=MOSI, ..., bit N=CS): +// 0 = CS low, MOSI low 1 = CS low, MOSI high +// (1 << cs_bit_pos) = CS high, MOSI low +// +// SIDE-SET (1 pin, SCK): side 0 = SCK low, side 1 = SCK high +// +// MCP4822 16-bit command word: +// [15] channel (0=A, 1=B) [14] don't care [13] gain (1=1x, 0=2x) +// [12] output enable (1) [11:0] 12-bit data +// +// DMA feeds unsigned 16-bit audio samples. RP2040 narrow-write replication +// fills both halves of the 32-bit PIO FIFO entry with the same value, +// giving mono→stereo for free. +// +// The PIO pulls 32 bits, then sends two SPI transactions: +// Channel A: cmd nibble, then all 16 sample bits from upper half-word +// Channel B: cmd nibble, then all 16 sample bits from lower half-word +// The MCP4822 captures exactly 16 bits per CS frame (4 cmd + 12 data), +// so only the top 12 of the 16 sample bits become DAC data. The bottom +// 4 sample bits clock out harmlessly after the DAC has latched. +// This gives correct 16-bit → 12-bit scaling (effectively sample >> 4). +// +// PIO instruction encoding with .side_set 1 (no opt): +// [15:13] opcode [12] side-set [11:8] delay [7:0] operands +// +// Total: 26 instructions, 86 PIO clocks per audio sample. +// ───────────────────────────────────────────────────────────────────────────── + +static const uint16_t mcp4822_pio_program[] = { + // side SCK + // 0: pull noblock side 0 ; Get 32 bits or keep X if FIFO empty + 0x8080, + // 1: mov x, osr side 0 ; Save for pull-noblock fallback + 0xA027, + + // ── Channel A: command nibble 0b0011 (1x gain) ──────────────────────── + // 2: set pins, 0 side 0 ; CS low, MOSI=0 (bit15=0: channel A) + 0xE000, + // 3: nop side 1 ; SCK high — latch bit 15 + 0xB042, + // 4: set pins, 0 side 0 ; MOSI=0 (bit14=0: don't care) + 0xE000, + // 5: nop side 1 ; SCK high + 0xB042, + // 6: set pins, 1 side 0 ; MOSI=1 (bit13=1: gain 1x) [patched for 2x] + 0xE001, + // 7: nop side 1 ; SCK high + 0xB042, + // 8: set pins, 1 side 0 ; MOSI=1 (bit12=1: output active) + 0xE001, + // 9: nop side 1 ; SCK high + 0xB042, + // 10: set y, 15 side 0 ; Loop counter: 16 sample bits + 0xE04F, + // 11: out pins, 1 side 0 ; Data bit → MOSI; SCK low (bitloopA) + 0x6001, + // 12: jmp y--, 11 side 1 ; SCK high, loop back + 0x108B, + // 13: set pins, 4 side 0 ; CS high — DAC A latches + 0xE004, + + // ── Channel B: command nibble 0b1011 (1x gain) ──────────────────────── + // 14: set pins, 1 side 0 ; CS low, MOSI=1 (bit15=1: channel B) + 0xE001, + // 15: nop side 1 ; SCK high + 0xB042, + // 16: set pins, 0 side 0 ; MOSI=0 (bit14=0) + 0xE000, + // 17: nop side 1 ; SCK high + 0xB042, + // 18: set pins, 1 side 0 ; MOSI=1 (bit13=1: gain 1x) [patched for 2x] + 0xE001, + // 19: nop side 1 ; SCK high + 0xB042, + // 20: set pins, 1 side 0 ; MOSI=1 (bit12=1: output active) + 0xE001, + // 21: nop side 1 ; SCK high + 0xB042, + // 22: set y, 15 side 0 ; Loop counter: 16 sample bits + 0xE04F, + // 23: out pins, 1 side 0 ; Data bit → MOSI; SCK low (bitloopB) + 0x6001, + // 24: jmp y--, 23 side 1 ; SCK high, loop back + 0x1097, + // 25: set pins, 4 side 0 ; CS high — DAC B latches + 0xE004, +}; + +// Clocks per sample: 2 (pull+mov) + 42 (chanA) + 42 (chanB) = 86 +// Per channel: 8(4 cmd bits × 2 clks) + 1(set y) + 32(16 bits × 2 clks) + 1(cs high) = 42 +#define MCP4822_CLOCKS_PER_SAMPLE 86 + +// MCP4822 gain bit (bit 13) position in the PIO program: +// Instruction 6 = channel A gain bit +// Instruction 18 = channel B gain bit +// 1x gain: set pins, 1 (0xE001) — bit 13 = 1 +// 2x gain: set pins, 0 (0xE000) — bit 13 = 0 +#define MCP4822_PIO_GAIN_INSTR_A 6 +#define MCP4822_PIO_GAIN_INSTR_B 18 +#define MCP4822_PIO_GAIN_1X 0xE001 // set pins, 1 +#define MCP4822_PIO_GAIN_2X 0xE000 // set pins, 0 + +void mcp4822_reset(void) { +} + +// Caller validates that pins are free. +void common_hal_mcp4822_mcp4822_construct(mcp4822_mcp4822_obj_t *self, + const mcu_pin_obj_t *clock, const mcu_pin_obj_t *mosi, + const mcu_pin_obj_t *cs, uint8_t gain) { + + // The SET pin group spans from MOSI to CS. + // MOSI must have a lower GPIO number than CS, gap at most 4. + if (cs->number <= mosi->number || (cs->number - mosi->number) > 4) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("Invalid %q and %q"), MP_QSTR_CS, MP_QSTR_MOSI); + } + + uint8_t set_count = cs->number - mosi->number + 1; + + // Build a mutable copy of the PIO program and patch the gain bit + uint16_t program[MP_ARRAY_SIZE(mcp4822_pio_program)]; + memcpy(program, mcp4822_pio_program, sizeof(mcp4822_pio_program)); + uint16_t gain_instr = (gain == 2) ? MCP4822_PIO_GAIN_2X : MCP4822_PIO_GAIN_1X; + program[MCP4822_PIO_GAIN_INSTR_A] = gain_instr; + program[MCP4822_PIO_GAIN_INSTR_B] = gain_instr; + + // Initial SET pin state: CS high (bit at CS position), others low + uint32_t cs_bit_position = cs->number - mosi->number; + pio_pinmask32_t initial_set_state = PIO_PINMASK32_FROM_VALUE(1u << cs_bit_position); + pio_pinmask32_t initial_set_dir = PIO_PINMASK32_FROM_VALUE((1u << set_count) - 1); + + common_hal_rp2pio_statemachine_construct( + &self->state_machine, + program, MP_ARRAY_SIZE(mcp4822_pio_program), + 44100 * MCP4822_CLOCKS_PER_SAMPLE, // Initial frequency; play() adjusts + NULL, 0, // No init program + NULL, 0, // No may_exec + mosi, 1, // OUT: MOSI, 1 pin + PIO_PINMASK32_NONE, PIO_PINMASK32_ALL, // OUT state=low, dir=output + NULL, 0, // IN: none + PIO_PINMASK32_NONE, PIO_PINMASK32_NONE, // IN pulls: none + mosi, set_count, // SET: MOSI..CS + initial_set_state, initial_set_dir, // SET state (CS high), dir=output + clock, 1, false, // SIDE-SET: SCK, 1 pin, not pindirs + PIO_PINMASK32_NONE, // SIDE-SET state: SCK low + PIO_PINMASK32_FROM_VALUE(0x1), // SIDE-SET dir: output + false, // No sideset enable + NULL, PULL_NONE, // No jump pin + PIO_PINMASK_NONE, // No wait GPIO + true, // Exclusive pin use + false, 32, false, // OUT shift: no autopull, 32-bit, shift left + false, // Don't wait for txstall + false, 32, false, // IN shift (unused) + false, // Not user-interruptible + 0, -1, // Wrap: whole program + PIO_ANY_OFFSET, + PIO_FIFO_TYPE_DEFAULT, + PIO_MOV_STATUS_DEFAULT, + PIO_MOV_N_DEFAULT + ); + + self->playing = false; + audio_dma_init(&self->dma); +} + +bool common_hal_mcp4822_mcp4822_deinited(mcp4822_mcp4822_obj_t *self) { + return common_hal_rp2pio_statemachine_deinited(&self->state_machine); +} + +void common_hal_mcp4822_mcp4822_deinit(mcp4822_mcp4822_obj_t *self) { + if (common_hal_mcp4822_mcp4822_deinited(self)) { + return; + } + if (common_hal_mcp4822_mcp4822_get_playing(self)) { + common_hal_mcp4822_mcp4822_stop(self); + } + common_hal_rp2pio_statemachine_deinit(&self->state_machine); + audio_dma_deinit(&self->dma); +} + +void common_hal_mcp4822_mcp4822_play(mcp4822_mcp4822_obj_t *self, + mp_obj_t sample, bool loop) { + + if (common_hal_mcp4822_mcp4822_get_playing(self)) { + common_hal_mcp4822_mcp4822_stop(self); + } + + uint8_t bits_per_sample = audiosample_get_bits_per_sample(sample); + if (bits_per_sample < 16) { + bits_per_sample = 16; + } + + uint32_t sample_rate = audiosample_get_sample_rate(sample); + uint8_t channel_count = audiosample_get_channel_count(sample); + if (channel_count > 2) { + mp_raise_ValueError(MP_ERROR_TEXT("Too many channels in sample.")); + } + + // PIO clock = sample_rate × clocks_per_sample + common_hal_rp2pio_statemachine_set_frequency( + &self->state_machine, + (uint32_t)sample_rate * MCP4822_CLOCKS_PER_SAMPLE); + common_hal_rp2pio_statemachine_restart(&self->state_machine); + + audio_dma_result result = audio_dma_setup_playback( + &self->dma, + sample, + loop, + false, // single_channel_output + 0, // audio_channel + false, // output_signed = false (unsigned for MCP4822) + bits_per_sample, // output_resolution + (uint32_t)&self->state_machine.pio->txf[self->state_machine.state_machine], + self->state_machine.tx_dreq, + false); // swap_channel + + if (result == AUDIO_DMA_DMA_BUSY) { + common_hal_mcp4822_mcp4822_stop(self); + mp_raise_RuntimeError(MP_ERROR_TEXT("No DMA channel found")); + } else if (result == AUDIO_DMA_MEMORY_ERROR) { + common_hal_mcp4822_mcp4822_stop(self); + mp_raise_RuntimeError(MP_ERROR_TEXT("Unable to allocate buffers for signed conversion")); + } else if (result == AUDIO_DMA_SOURCE_ERROR) { + common_hal_mcp4822_mcp4822_stop(self); + mp_raise_RuntimeError(MP_ERROR_TEXT("Audio source error")); + } + + self->playing = true; +} + +void common_hal_mcp4822_mcp4822_pause(mcp4822_mcp4822_obj_t *self) { + audio_dma_pause(&self->dma); +} + +void common_hal_mcp4822_mcp4822_resume(mcp4822_mcp4822_obj_t *self) { + audio_dma_resume(&self->dma); +} + +bool common_hal_mcp4822_mcp4822_get_paused(mcp4822_mcp4822_obj_t *self) { + return audio_dma_get_paused(&self->dma); +} + +void common_hal_mcp4822_mcp4822_stop(mcp4822_mcp4822_obj_t *self) { + audio_dma_stop(&self->dma); + common_hal_rp2pio_statemachine_stop(&self->state_machine); + self->playing = false; +} + +bool common_hal_mcp4822_mcp4822_get_playing(mcp4822_mcp4822_obj_t *self) { + bool playing = audio_dma_get_playing(&self->dma); + if (!playing && self->playing) { + common_hal_mcp4822_mcp4822_stop(self); + } + return playing; +} diff --git a/ports/raspberrypi/common-hal/mcp4822/MCP4822.h b/ports/raspberrypi/common-hal/mcp4822/MCP4822.h new file mode 100644 index 0000000000000..53c8e862b63c5 --- /dev/null +++ b/ports/raspberrypi/common-hal/mcp4822/MCP4822.h @@ -0,0 +1,22 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tod Kurt +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "common-hal/microcontroller/Pin.h" +#include "common-hal/rp2pio/StateMachine.h" + +#include "audio_dma.h" +#include "py/obj.h" + +typedef struct { + mp_obj_base_t base; + rp2pio_statemachine_obj_t state_machine; + audio_dma_t dma; + bool playing; +} mcp4822_mcp4822_obj_t; + +void mcp4822_reset(void); diff --git a/ports/raspberrypi/common-hal/mcp4822/__init__.c b/ports/raspberrypi/common-hal/mcp4822/__init__.c new file mode 100644 index 0000000000000..48981fc88a713 --- /dev/null +++ b/ports/raspberrypi/common-hal/mcp4822/__init__.c @@ -0,0 +1,7 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tod Kurt +// +// SPDX-License-Identifier: MIT + +// No module-level init needed. diff --git a/ports/raspberrypi/mpconfigport.mk b/ports/raspberrypi/mpconfigport.mk index 8401c5d75453a..30fc75c106b54 100644 --- a/ports/raspberrypi/mpconfigport.mk +++ b/ports/raspberrypi/mpconfigport.mk @@ -16,6 +16,7 @@ CIRCUITPY_HASHLIB ?= 1 CIRCUITPY_HASHLIB_MBEDTLS ?= 1 CIRCUITPY_IMAGECAPTURE ?= 1 CIRCUITPY_MAX3421E ?= 0 +CIRCUITPY_MCP4822 ?= 0 CIRCUITPY_MEMORYMAP ?= 1 CIRCUITPY_PWMIO ?= 1 CIRCUITPY_RGBMATRIX ?= $(CIRCUITPY_DISPLAYIO) diff --git a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/autogen_board_info.toml b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/autogen_board_info.toml index 2c717fc83c169..5444c6dbdc2e2 100644 --- a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/autogen_board_info.toml @@ -64,6 +64,7 @@ locale = false lvfontio = true # Zephyr board has busio math = false max3421e = false +mcp4822 = false mdns = false memorymap = false memorymonitor = false diff --git a/ports/zephyr-cp/boards/native/native_sim/autogen_board_info.toml b/ports/zephyr-cp/boards/native/native_sim/autogen_board_info.toml index 1b82d3ba00e02..b0b06a157b433 100644 --- a/ports/zephyr-cp/boards/native/native_sim/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/native/native_sim/autogen_board_info.toml @@ -64,6 +64,7 @@ locale = false lvfontio = true # Zephyr board has busio math = false max3421e = false +mcp4822 = false mdns = false memorymap = false memorymonitor = false diff --git a/ports/zephyr-cp/boards/native/nrf5340bsim/autogen_board_info.toml b/ports/zephyr-cp/boards/native/nrf5340bsim/autogen_board_info.toml index f995c03c2a26f..ed327a945d458 100644 --- a/ports/zephyr-cp/boards/native/nrf5340bsim/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/native/nrf5340bsim/autogen_board_info.toml @@ -64,6 +64,7 @@ locale = false lvfontio = true # Zephyr board has busio math = false max3421e = false +mcp4822 = false mdns = false memorymap = false memorymonitor = false diff --git a/ports/zephyr-cp/boards/nordic/nrf5340dk/autogen_board_info.toml b/ports/zephyr-cp/boards/nordic/nrf5340dk/autogen_board_info.toml index 397d514a0dac5..51b4ad08a1ad2 100644 --- a/ports/zephyr-cp/boards/nordic/nrf5340dk/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/nordic/nrf5340dk/autogen_board_info.toml @@ -64,6 +64,7 @@ locale = false lvfontio = true # Zephyr board has busio math = false max3421e = false +mcp4822 = false mdns = false memorymap = false memorymonitor = false diff --git a/ports/zephyr-cp/boards/nordic/nrf54h20dk/autogen_board_info.toml b/ports/zephyr-cp/boards/nordic/nrf54h20dk/autogen_board_info.toml index 69a657b8c98be..8415fbf2b72fe 100644 --- a/ports/zephyr-cp/boards/nordic/nrf54h20dk/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/nordic/nrf54h20dk/autogen_board_info.toml @@ -64,6 +64,7 @@ locale = false lvfontio = true # Zephyr board has busio math = false max3421e = false +mcp4822 = false mdns = false memorymap = false memorymonitor = false diff --git a/ports/zephyr-cp/boards/nordic/nrf54l15dk/autogen_board_info.toml b/ports/zephyr-cp/boards/nordic/nrf54l15dk/autogen_board_info.toml index 378cbc07e9a2a..75eeafabab2c0 100644 --- a/ports/zephyr-cp/boards/nordic/nrf54l15dk/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/nordic/nrf54l15dk/autogen_board_info.toml @@ -64,6 +64,7 @@ locale = false lvfontio = true # Zephyr board has busio math = false max3421e = false +mcp4822 = false mdns = false memorymap = false memorymonitor = false diff --git a/ports/zephyr-cp/boards/nordic/nrf7002dk/autogen_board_info.toml b/ports/zephyr-cp/boards/nordic/nrf7002dk/autogen_board_info.toml index 892b3dbbbd447..fb8f60ba25cec 100644 --- a/ports/zephyr-cp/boards/nordic/nrf7002dk/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/nordic/nrf7002dk/autogen_board_info.toml @@ -64,6 +64,7 @@ locale = false lvfontio = true # Zephyr board has busio math = false max3421e = false +mcp4822 = false mdns = false memorymap = false memorymonitor = false diff --git a/ports/zephyr-cp/boards/nxp/frdm_mcxn947/autogen_board_info.toml b/ports/zephyr-cp/boards/nxp/frdm_mcxn947/autogen_board_info.toml index 718cd0bcfd353..72dfd79c44b2c 100644 --- a/ports/zephyr-cp/boards/nxp/frdm_mcxn947/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/nxp/frdm_mcxn947/autogen_board_info.toml @@ -64,6 +64,7 @@ locale = false lvfontio = true # Zephyr board has busio math = false max3421e = false +mcp4822 = false mdns = false memorymap = false memorymonitor = false diff --git a/ports/zephyr-cp/boards/nxp/frdm_rw612/autogen_board_info.toml b/ports/zephyr-cp/boards/nxp/frdm_rw612/autogen_board_info.toml index 10b0056c81dcc..3c81e2f693763 100644 --- a/ports/zephyr-cp/boards/nxp/frdm_rw612/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/nxp/frdm_rw612/autogen_board_info.toml @@ -64,6 +64,7 @@ locale = false lvfontio = true # Zephyr board has busio math = false max3421e = false +mcp4822 = false mdns = false memorymap = false memorymonitor = false diff --git a/ports/zephyr-cp/boards/nxp/mimxrt1170_evk/autogen_board_info.toml b/ports/zephyr-cp/boards/nxp/mimxrt1170_evk/autogen_board_info.toml index a3d6acdb79e71..812927ea9cc88 100644 --- a/ports/zephyr-cp/boards/nxp/mimxrt1170_evk/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/nxp/mimxrt1170_evk/autogen_board_info.toml @@ -64,6 +64,7 @@ locale = false lvfontio = true # Zephyr board has busio math = false max3421e = false +mcp4822 = false mdns = false memorymap = false memorymonitor = false diff --git a/ports/zephyr-cp/boards/renesas/da14695_dk_usb/autogen_board_info.toml b/ports/zephyr-cp/boards/renesas/da14695_dk_usb/autogen_board_info.toml index 3d5fbb08c6543..2baccbe67016a 100644 --- a/ports/zephyr-cp/boards/renesas/da14695_dk_usb/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/renesas/da14695_dk_usb/autogen_board_info.toml @@ -64,6 +64,7 @@ locale = false lvfontio = true # Zephyr board has busio math = false max3421e = false +mcp4822 = false mdns = false memorymap = false memorymonitor = false diff --git a/ports/zephyr-cp/boards/renesas/ek_ra6m5/autogen_board_info.toml b/ports/zephyr-cp/boards/renesas/ek_ra6m5/autogen_board_info.toml index ec96d53072699..742685c138ed9 100644 --- a/ports/zephyr-cp/boards/renesas/ek_ra6m5/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/renesas/ek_ra6m5/autogen_board_info.toml @@ -64,6 +64,7 @@ locale = false lvfontio = true # Zephyr board has busio math = false max3421e = false +mcp4822 = false mdns = false memorymap = false memorymonitor = false diff --git a/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml b/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml index 132899d424838..e20f6ecaad158 100644 --- a/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml @@ -64,6 +64,7 @@ locale = false lvfontio = true # Zephyr board has busio math = false max3421e = false +mcp4822 = false mdns = false memorymap = false memorymonitor = false diff --git a/ports/zephyr-cp/boards/st/nucleo_n657x0_q/autogen_board_info.toml b/ports/zephyr-cp/boards/st/nucleo_n657x0_q/autogen_board_info.toml index bc2a29aea088c..570f7a5197bb0 100644 --- a/ports/zephyr-cp/boards/st/nucleo_n657x0_q/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/st/nucleo_n657x0_q/autogen_board_info.toml @@ -64,6 +64,7 @@ locale = false lvfontio = true # Zephyr board has busio math = false max3421e = false +mcp4822 = false mdns = false memorymap = false memorymonitor = false diff --git a/ports/zephyr-cp/boards/st/nucleo_u575zi_q/autogen_board_info.toml b/ports/zephyr-cp/boards/st/nucleo_u575zi_q/autogen_board_info.toml index aacda400a305e..5a7a857fc4938 100644 --- a/ports/zephyr-cp/boards/st/nucleo_u575zi_q/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/st/nucleo_u575zi_q/autogen_board_info.toml @@ -64,6 +64,7 @@ locale = false lvfontio = true # Zephyr board has busio math = false max3421e = false +mcp4822 = false mdns = false memorymap = false memorymonitor = false diff --git a/ports/zephyr-cp/boards/st/stm32h750b_dk/autogen_board_info.toml b/ports/zephyr-cp/boards/st/stm32h750b_dk/autogen_board_info.toml index a8c03d5a4e8f5..cb705c48af51a 100644 --- a/ports/zephyr-cp/boards/st/stm32h750b_dk/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/st/stm32h750b_dk/autogen_board_info.toml @@ -64,6 +64,7 @@ locale = false lvfontio = true # Zephyr board has busio math = false max3421e = false +mcp4822 = false mdns = false memorymap = false memorymonitor = false diff --git a/ports/zephyr-cp/boards/st/stm32h7b3i_dk/autogen_board_info.toml b/ports/zephyr-cp/boards/st/stm32h7b3i_dk/autogen_board_info.toml index b66682c59238b..aba3d5edf04c8 100644 --- a/ports/zephyr-cp/boards/st/stm32h7b3i_dk/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/st/stm32h7b3i_dk/autogen_board_info.toml @@ -64,6 +64,7 @@ locale = false lvfontio = true # Zephyr board has busio math = false max3421e = false +mcp4822 = false mdns = false memorymap = false memorymonitor = false diff --git a/ports/zephyr-cp/boards/st/stm32wba65i_dk1/autogen_board_info.toml b/ports/zephyr-cp/boards/st/stm32wba65i_dk1/autogen_board_info.toml index cadd8b5ade3a2..9fdb018d501e5 100644 --- a/ports/zephyr-cp/boards/st/stm32wba65i_dk1/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/st/stm32wba65i_dk1/autogen_board_info.toml @@ -64,6 +64,7 @@ locale = false lvfontio = true # Zephyr board has busio math = false max3421e = false +mcp4822 = false mdns = false memorymap = false memorymonitor = false diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index 886ba96f3e5fa..14c8c52802e6e 100644 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -288,6 +288,9 @@ endif ifeq ($(CIRCUITPY_MAX3421E),1) SRC_PATTERNS += max3421e/% endif +ifeq ($(CIRCUITPY_MCP4822),1) +SRC_PATTERNS += mcp4822/% +endif ifeq ($(CIRCUITPY_MDNS),1) SRC_PATTERNS += mdns/% endif @@ -532,6 +535,8 @@ SRC_COMMON_HAL_ALL = \ i2ctarget/I2CTarget.c \ i2ctarget/__init__.c \ max3421e/Max3421E.c \ + mcp4822/__init__.c \ + mcp4822/MCP4822.c \ memorymap/__init__.c \ memorymap/AddressRange.c \ microcontroller/__init__.c \ diff --git a/shared-bindings/mcp4822/MCP4822.c b/shared-bindings/mcp4822/MCP4822.c new file mode 100644 index 0000000000000..f192caf4052a6 --- /dev/null +++ b/shared-bindings/mcp4822/MCP4822.c @@ -0,0 +1,243 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tod Kurt +// +// SPDX-License-Identifier: MIT + +#include + +#include "shared/runtime/context_manager_helpers.h" +#include "py/binary.h" +#include "py/objproperty.h" +#include "py/runtime.h" +#include "shared-bindings/microcontroller/Pin.h" +#include "shared-bindings/mcp4822/MCP4822.h" +#include "shared-bindings/util.h" + +//| class MCP4822: +//| """Output audio to an MCP4822 dual-channel 12-bit SPI DAC.""" +//| +//| def __init__( +//| self, +//| clock: microcontroller.Pin, +//| mosi: microcontroller.Pin, +//| cs: microcontroller.Pin, +//| *, +//| gain: int = 1, +//| ) -> None: +//| """Create an MCP4822 object associated with the given SPI pins. +//| +//| :param ~microcontroller.Pin clock: The SPI clock (SCK) pin +//| :param ~microcontroller.Pin mosi: The SPI data (SDI/MOSI) pin +//| :param ~microcontroller.Pin cs: The chip select (CS) pin +//| :param int gain: DAC output gain, 1 for 1x (0-2.048V) or 2 for 2x (0-4.096V). Default 1. +//| +//| Simple 8ksps 440 Hz sine wave:: +//| +//| import mcp4822 +//| import audiocore +//| import board +//| import array +//| import time +//| import math +//| +//| length = 8000 // 440 +//| sine_wave = array.array("H", [0] * length) +//| for i in range(length): +//| sine_wave[i] = int(math.sin(math.pi * 2 * i / length) * (2 ** 15) + 2 ** 15) +//| +//| sine_wave = audiocore.RawSample(sine_wave, sample_rate=8000) +//| dac = mcp4822.MCP4822(clock=board.GP18, mosi=board.GP19, cs=board.GP21) +//| dac.play(sine_wave, loop=True) +//| time.sleep(1) +//| dac.stop() +//| +//| Playing a wave file from flash:: +//| +//| import board +//| import audiocore +//| import mcp4822 +//| +//| f = open("sound.wav", "rb") +//| wav = audiocore.WaveFile(f) +//| +//| dac = mcp4822.MCP4822(clock=board.GP18, mosi=board.GP19, cs=board.GP21) +//| dac.play(wav) +//| while dac.playing: +//| pass""" +//| ... +//| +static mp_obj_t mcp4822_mcp4822_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + enum { ARG_clock, ARG_mosi, ARG_cs, ARG_gain }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_clock, MP_ARG_OBJ | MP_ARG_KW_ONLY | MP_ARG_REQUIRED }, + { MP_QSTR_mosi, MP_ARG_OBJ | MP_ARG_KW_ONLY | MP_ARG_REQUIRED }, + { MP_QSTR_cs, MP_ARG_OBJ | MP_ARG_KW_ONLY | MP_ARG_REQUIRED }, + { MP_QSTR_gain, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 1} }, + }; + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + const mcu_pin_obj_t *clock = validate_obj_is_free_pin(args[ARG_clock].u_obj, MP_QSTR_clock); + const mcu_pin_obj_t *mosi = validate_obj_is_free_pin(args[ARG_mosi].u_obj, MP_QSTR_mosi); + const mcu_pin_obj_t *cs = validate_obj_is_free_pin(args[ARG_cs].u_obj, MP_QSTR_cs); + const mp_int_t gain = mp_arg_validate_int_range(args[ARG_gain].u_int, 1, 2, MP_QSTR_gain); + + mcp4822_mcp4822_obj_t *self = mp_obj_malloc_with_finaliser(mcp4822_mcp4822_obj_t, &mcp4822_mcp4822_type); + common_hal_mcp4822_mcp4822_construct(self, clock, mosi, cs, (uint8_t)gain); + + return MP_OBJ_FROM_PTR(self); +} + +static void check_for_deinit(mcp4822_mcp4822_obj_t *self) { + if (common_hal_mcp4822_mcp4822_deinited(self)) { + raise_deinited_error(); + } +} + +//| def deinit(self) -> None: +//| """Deinitialises the MCP4822 and releases any hardware resources for reuse.""" +//| ... +//| +static mp_obj_t mcp4822_mcp4822_deinit(mp_obj_t self_in) { + mcp4822_mcp4822_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_mcp4822_mcp4822_deinit(self); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(mcp4822_mcp4822_deinit_obj, mcp4822_mcp4822_deinit); + +//| def __enter__(self) -> MCP4822: +//| """No-op used by Context Managers.""" +//| ... +//| +// Provided by context manager helper. + +//| def __exit__(self) -> None: +//| """Automatically deinitializes the hardware when exiting a context. See +//| :ref:`lifetime-and-contextmanagers` for more info.""" +//| ... +//| +// Provided by context manager helper. + +//| def play(self, sample: circuitpython_typing.AudioSample, *, loop: bool = False) -> None: +//| """Plays the sample once when loop=False and continuously when loop=True. +//| Does not block. Use `playing` to block. +//| +//| Sample must be an `audiocore.WaveFile`, `audiocore.RawSample`, `audiomixer.Mixer` or `audiomp3.MP3Decoder`. +//| +//| The sample itself should consist of 8 bit or 16 bit samples.""" +//| ... +//| +static mp_obj_t mcp4822_mcp4822_obj_play(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_sample, ARG_loop }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_sample, MP_ARG_OBJ | MP_ARG_REQUIRED }, + { MP_QSTR_loop, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, + }; + mcp4822_mcp4822_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + check_for_deinit(self); + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + mp_obj_t sample = args[ARG_sample].u_obj; + common_hal_mcp4822_mcp4822_play(self, sample, args[ARG_loop].u_bool); + + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_KW(mcp4822_mcp4822_play_obj, 1, mcp4822_mcp4822_obj_play); + +//| def stop(self) -> None: +//| """Stops playback.""" +//| ... +//| +static mp_obj_t mcp4822_mcp4822_obj_stop(mp_obj_t self_in) { + mcp4822_mcp4822_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + common_hal_mcp4822_mcp4822_stop(self); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(mcp4822_mcp4822_stop_obj, mcp4822_mcp4822_obj_stop); + +//| playing: bool +//| """True when the audio sample is being output. (read-only)""" +//| +static mp_obj_t mcp4822_mcp4822_obj_get_playing(mp_obj_t self_in) { + mcp4822_mcp4822_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bool(common_hal_mcp4822_mcp4822_get_playing(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(mcp4822_mcp4822_get_playing_obj, mcp4822_mcp4822_obj_get_playing); + +MP_PROPERTY_GETTER(mcp4822_mcp4822_playing_obj, + (mp_obj_t)&mcp4822_mcp4822_get_playing_obj); + +//| def pause(self) -> None: +//| """Stops playback temporarily while remembering the position. Use `resume` to resume playback.""" +//| ... +//| +static mp_obj_t mcp4822_mcp4822_obj_pause(mp_obj_t self_in) { + mcp4822_mcp4822_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + + if (!common_hal_mcp4822_mcp4822_get_playing(self)) { + mp_raise_RuntimeError(MP_ERROR_TEXT("Not playing")); + } + common_hal_mcp4822_mcp4822_pause(self); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(mcp4822_mcp4822_pause_obj, mcp4822_mcp4822_obj_pause); + +//| def resume(self) -> None: +//| """Resumes sample playback after :py:func:`pause`.""" +//| ... +//| +static mp_obj_t mcp4822_mcp4822_obj_resume(mp_obj_t self_in) { + mcp4822_mcp4822_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + + if (common_hal_mcp4822_mcp4822_get_paused(self)) { + common_hal_mcp4822_mcp4822_resume(self); + } + + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(mcp4822_mcp4822_resume_obj, mcp4822_mcp4822_obj_resume); + +//| paused: bool +//| """True when playback is paused. (read-only)""" +//| +//| +static mp_obj_t mcp4822_mcp4822_obj_get_paused(mp_obj_t self_in) { + mcp4822_mcp4822_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bool(common_hal_mcp4822_mcp4822_get_paused(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(mcp4822_mcp4822_get_paused_obj, mcp4822_mcp4822_obj_get_paused); + +MP_PROPERTY_GETTER(mcp4822_mcp4822_paused_obj, + (mp_obj_t)&mcp4822_mcp4822_get_paused_obj); + +static const mp_rom_map_elem_t mcp4822_mcp4822_locals_dict_table[] = { + // Methods + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&mcp4822_mcp4822_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&mcp4822_mcp4822_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) }, + { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&default___exit___obj) }, + { MP_ROM_QSTR(MP_QSTR_play), MP_ROM_PTR(&mcp4822_mcp4822_play_obj) }, + { MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&mcp4822_mcp4822_stop_obj) }, + { MP_ROM_QSTR(MP_QSTR_pause), MP_ROM_PTR(&mcp4822_mcp4822_pause_obj) }, + { MP_ROM_QSTR(MP_QSTR_resume), MP_ROM_PTR(&mcp4822_mcp4822_resume_obj) }, + + // Properties + { MP_ROM_QSTR(MP_QSTR_playing), MP_ROM_PTR(&mcp4822_mcp4822_playing_obj) }, + { MP_ROM_QSTR(MP_QSTR_paused), MP_ROM_PTR(&mcp4822_mcp4822_paused_obj) }, +}; +static MP_DEFINE_CONST_DICT(mcp4822_mcp4822_locals_dict, mcp4822_mcp4822_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + mcp4822_mcp4822_type, + MP_QSTR_MCP4822, + MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, + make_new, mcp4822_mcp4822_make_new, + locals_dict, &mcp4822_mcp4822_locals_dict + ); diff --git a/shared-bindings/mcp4822/MCP4822.h b/shared-bindings/mcp4822/MCP4822.h new file mode 100644 index 0000000000000..b129aec306124 --- /dev/null +++ b/shared-bindings/mcp4822/MCP4822.h @@ -0,0 +1,25 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tod Kurt +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "common-hal/mcp4822/MCP4822.h" +#include "common-hal/microcontroller/Pin.h" + +extern const mp_obj_type_t mcp4822_mcp4822_type; + +void common_hal_mcp4822_mcp4822_construct(mcp4822_mcp4822_obj_t *self, + const mcu_pin_obj_t *clock, const mcu_pin_obj_t *mosi, + const mcu_pin_obj_t *cs, uint8_t gain); + +void common_hal_mcp4822_mcp4822_deinit(mcp4822_mcp4822_obj_t *self); +bool common_hal_mcp4822_mcp4822_deinited(mcp4822_mcp4822_obj_t *self); +void common_hal_mcp4822_mcp4822_play(mcp4822_mcp4822_obj_t *self, mp_obj_t sample, bool loop); +void common_hal_mcp4822_mcp4822_stop(mcp4822_mcp4822_obj_t *self); +bool common_hal_mcp4822_mcp4822_get_playing(mcp4822_mcp4822_obj_t *self); +void common_hal_mcp4822_mcp4822_pause(mcp4822_mcp4822_obj_t *self); +void common_hal_mcp4822_mcp4822_resume(mcp4822_mcp4822_obj_t *self); +bool common_hal_mcp4822_mcp4822_get_paused(mcp4822_mcp4822_obj_t *self); diff --git a/shared-bindings/mcp4822/__init__.c b/shared-bindings/mcp4822/__init__.c new file mode 100644 index 0000000000000..bac2136d9e7c0 --- /dev/null +++ b/shared-bindings/mcp4822/__init__.c @@ -0,0 +1,36 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tod Kurt +// +// SPDX-License-Identifier: MIT + +#include + +#include "py/obj.h" +#include "py/runtime.h" + +#include "shared-bindings/mcp4822/__init__.h" +#include "shared-bindings/mcp4822/MCP4822.h" + +//| """Audio output via MCP4822 dual-channel 12-bit SPI DAC. +//| +//| The `mcp4822` module provides the `MCP4822` class for non-blocking +//| audio playback through the Microchip MCP4822 SPI DAC using PIO and DMA. +//| +//| All classes change hardware state and should be deinitialized when they +//| are no longer needed. To do so, either call :py:meth:`!deinit` or use a +//| context manager.""" + +static const mp_rom_map_elem_t mcp4822_module_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_mcp4822) }, + { MP_ROM_QSTR(MP_QSTR_MCP4822), MP_ROM_PTR(&mcp4822_mcp4822_type) }, +}; + +static MP_DEFINE_CONST_DICT(mcp4822_module_globals, mcp4822_module_globals_table); + +const mp_obj_module_t mcp4822_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&mcp4822_module_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_mcp4822, mcp4822_module); diff --git a/shared-bindings/mcp4822/__init__.h b/shared-bindings/mcp4822/__init__.h new file mode 100644 index 0000000000000..c4a52e5819d12 --- /dev/null +++ b/shared-bindings/mcp4822/__init__.h @@ -0,0 +1,7 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tod Kurt +// +// SPDX-License-Identifier: MIT + +#pragma once