diff --git a/ports/raspberrypi/boards/mtm_computer/module/DACOut.c b/ports/raspberrypi/boards/mtm_computer/module/DACOut.c new file mode 100644 index 0000000000000..84f4296cb00cd --- /dev/null +++ b/ports/raspberrypi/boards/mtm_computer/module/DACOut.c @@ -0,0 +1,276 @@ +// 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 driver for the MTM Workshop Computer. +// 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 "boards/mtm_computer/module/DACOut.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 +// +// On the MTM Workshop Computer: MOSI=GP19, CS=GP21, SCK=GP18. +// The SET group spans GP19..GP21 (3 pins). GP20 is unused and driven low. +// +// SET PINS bit mapping (bit0=MOSI/GP19, bit1=GP20, bit2=CS/GP21): +// 0 = CS low, MOSI low 1 = CS low, MOSI high 4 = 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) +// [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 0b0011, then all 16 sample bits from upper half-word +// Channel B: cmd nibble 0b1011, 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 ────────────────────────────────── + // Send 4 cmd bits via SET, then all 16 sample bits via OUT. + // MCP4822 captures exactly 16 bits per CS frame (4 cmd + 12 data); + // the extra 4 clocks shift out the LSBs which the DAC ignores. + // This gives correct 16→12 bit scaling (top 12 bits become DAC data). + // 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) + 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 ────────────────────────────────── + // 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) + 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 + + +void common_hal_mtm_hardware_dacout_construct(mtm_hardware_dacout_obj_t *self, + const mcu_pin_obj_t *clock, const mcu_pin_obj_t *mosi, + const mcu_pin_obj_t *cs) { + + // SET pins span from MOSI to CS. MOSI must have a lower GPIO number + // than CS, with at most 4 pins between them (SET count max is 5). + if (cs->number <= mosi->number || (cs->number - mosi->number) > 4) { + mp_raise_ValueError( + MP_COMPRESSED_ROM_TEXT("cs pin must be 1-4 positions above mosi pin")); + } + + uint8_t set_count = cs->number - mosi->number + 1; + + // 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, + mcp4822_pio_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_mtm_hardware_dacout_deinited(mtm_hardware_dacout_obj_t *self) { + return common_hal_rp2pio_statemachine_deinited(&self->state_machine); +} + +void common_hal_mtm_hardware_dacout_deinit(mtm_hardware_dacout_obj_t *self) { + if (common_hal_mtm_hardware_dacout_deinited(self)) { + return; + } + if (common_hal_mtm_hardware_dacout_get_playing(self)) { + common_hal_mtm_hardware_dacout_stop(self); + } + common_hal_rp2pio_statemachine_deinit(&self->state_machine); + audio_dma_deinit(&self->dma); +} + +void common_hal_mtm_hardware_dacout_play(mtm_hardware_dacout_obj_t *self, + mp_obj_t sample, bool loop) { + + if (common_hal_mtm_hardware_dacout_get_playing(self)) { + common_hal_mtm_hardware_dacout_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_COMPRESSED_ROM_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); + + // DMA feeds unsigned 16-bit samples. The PIO discards the top 4 bits + // of each 16-bit half and uses the remaining 12 as DAC data. + // RP2040 narrow-write replication: 16-bit DMA write → same value in + // both 32-bit FIFO halves → mono-to-stereo for free. + 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_mtm_hardware_dacout_stop(self); + mp_raise_RuntimeError(MP_COMPRESSED_ROM_TEXT("No DMA channel found")); + } else if (result == AUDIO_DMA_MEMORY_ERROR) { + common_hal_mtm_hardware_dacout_stop(self); + mp_raise_RuntimeError(MP_COMPRESSED_ROM_TEXT("Unable to allocate buffers for signed conversion")); + } else if (result == AUDIO_DMA_SOURCE_ERROR) { + common_hal_mtm_hardware_dacout_stop(self); + mp_raise_RuntimeError(MP_COMPRESSED_ROM_TEXT("Audio source error")); + } + + self->playing = true; +} + +void common_hal_mtm_hardware_dacout_pause(mtm_hardware_dacout_obj_t *self) { + audio_dma_pause(&self->dma); +} + +void common_hal_mtm_hardware_dacout_resume(mtm_hardware_dacout_obj_t *self) { + audio_dma_resume(&self->dma); +} + +bool common_hal_mtm_hardware_dacout_get_paused(mtm_hardware_dacout_obj_t *self) { + return audio_dma_get_paused(&self->dma); +} + +void common_hal_mtm_hardware_dacout_stop(mtm_hardware_dacout_obj_t *self) { + audio_dma_stop(&self->dma); + common_hal_rp2pio_statemachine_stop(&self->state_machine); + self->playing = false; +} + +bool common_hal_mtm_hardware_dacout_get_playing(mtm_hardware_dacout_obj_t *self) { + bool playing = audio_dma_get_playing(&self->dma); + if (!playing && self->playing) { + common_hal_mtm_hardware_dacout_stop(self); + } + return playing; +} diff --git a/ports/raspberrypi/boards/mtm_computer/module/DACOut.h b/ports/raspberrypi/boards/mtm_computer/module/DACOut.h new file mode 100644 index 0000000000000..f7c0c9eb8062c --- /dev/null +++ b/ports/raspberrypi/boards/mtm_computer/module/DACOut.h @@ -0,0 +1,38 @@ +// 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; +} mtm_hardware_dacout_obj_t; + +void common_hal_mtm_hardware_dacout_construct(mtm_hardware_dacout_obj_t *self, + const mcu_pin_obj_t *clock, const mcu_pin_obj_t *mosi, + const mcu_pin_obj_t *cs); + +void common_hal_mtm_hardware_dacout_deinit(mtm_hardware_dacout_obj_t *self); +bool common_hal_mtm_hardware_dacout_deinited(mtm_hardware_dacout_obj_t *self); + +void common_hal_mtm_hardware_dacout_play(mtm_hardware_dacout_obj_t *self, + mp_obj_t sample, bool loop); +void common_hal_mtm_hardware_dacout_stop(mtm_hardware_dacout_obj_t *self); +bool common_hal_mtm_hardware_dacout_get_playing(mtm_hardware_dacout_obj_t *self); + +void common_hal_mtm_hardware_dacout_pause(mtm_hardware_dacout_obj_t *self); +void common_hal_mtm_hardware_dacout_resume(mtm_hardware_dacout_obj_t *self); +bool common_hal_mtm_hardware_dacout_get_paused(mtm_hardware_dacout_obj_t *self); + +extern const mp_obj_type_t mtm_hardware_dacout_type; diff --git a/ports/raspberrypi/boards/mtm_computer/module/mtm_hardware.c b/ports/raspberrypi/boards/mtm_computer/module/mtm_hardware.c new file mode 100644 index 0000000000000..a3dafbcf9415a --- /dev/null +++ b/ports/raspberrypi/boards/mtm_computer/module/mtm_hardware.c @@ -0,0 +1,267 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tod Kurt +// +// SPDX-License-Identifier: MIT +// +// Python bindings for the mtm_hardware module. +// Provides DACOut: a non-blocking audio player for the MCP4822 SPI DAC. + +#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/util.h" +#include "boards/mtm_computer/module/DACOut.h" + +// ───────────────────────────────────────────────────────────────────────────── +// DACOut class +// ───────────────────────────────────────────────────────────────────────────── + +//| class DACOut: +//| """Output audio to the MCP4822 dual-channel 12-bit SPI DAC.""" +//| +//| def __init__( +//| self, +//| clock: microcontroller.Pin, +//| mosi: microcontroller.Pin, +//| cs: microcontroller.Pin, +//| ) -> None: +//| """Create a DACOut 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 +//| +//| Simple 8ksps 440 Hz sine wave:: +//| +//| import mtm_hardware +//| 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 = mtm_hardware.DACOut(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 mtm_hardware +//| +//| f = open("sound.wav", "rb") +//| wav = audiocore.WaveFile(f) +//| +//| dac = mtm_hardware.DACOut(clock=board.GP18, mosi=board.GP19, cs=board.GP21) +//| dac.play(wav) +//| while dac.playing: +//| pass""" +//| ... +//| +static mp_obj_t mtm_hardware_dacout_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 }; + 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_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); + + mtm_hardware_dacout_obj_t *self = mp_obj_malloc_with_finaliser(mtm_hardware_dacout_obj_t, &mtm_hardware_dacout_type); + common_hal_mtm_hardware_dacout_construct(self, clock, mosi, cs); + + return MP_OBJ_FROM_PTR(self); +} + +static void check_for_deinit(mtm_hardware_dacout_obj_t *self) { + if (common_hal_mtm_hardware_dacout_deinited(self)) { + raise_deinited_error(); + } +} + +//| def deinit(self) -> None: +//| """Deinitialises the DACOut and releases any hardware resources for reuse.""" +//| ... +//| +static mp_obj_t mtm_hardware_dacout_deinit(mp_obj_t self_in) { + mtm_hardware_dacout_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_mtm_hardware_dacout_deinit(self); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(mtm_hardware_dacout_deinit_obj, mtm_hardware_dacout_deinit); + +//| def __enter__(self) -> DACOut: +//| """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 mtm_hardware_dacout_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} }, + }; + mtm_hardware_dacout_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_mtm_hardware_dacout_play(self, sample, args[ARG_loop].u_bool); + + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_KW(mtm_hardware_dacout_play_obj, 1, mtm_hardware_dacout_obj_play); + +//| def stop(self) -> None: +//| """Stops playback.""" +//| ... +//| +static mp_obj_t mtm_hardware_dacout_obj_stop(mp_obj_t self_in) { + mtm_hardware_dacout_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + common_hal_mtm_hardware_dacout_stop(self); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(mtm_hardware_dacout_stop_obj, mtm_hardware_dacout_obj_stop); + +//| playing: bool +//| """True when the audio sample is being output. (read-only)""" +//| +static mp_obj_t mtm_hardware_dacout_obj_get_playing(mp_obj_t self_in) { + mtm_hardware_dacout_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bool(common_hal_mtm_hardware_dacout_get_playing(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(mtm_hardware_dacout_get_playing_obj, mtm_hardware_dacout_obj_get_playing); + +MP_PROPERTY_GETTER(mtm_hardware_dacout_playing_obj, + (mp_obj_t)&mtm_hardware_dacout_get_playing_obj); + +//| def pause(self) -> None: +//| """Stops playback temporarily while remembering the position. Use `resume` to resume playback.""" +//| ... +//| +static mp_obj_t mtm_hardware_dacout_obj_pause(mp_obj_t self_in) { + mtm_hardware_dacout_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + if (!common_hal_mtm_hardware_dacout_get_playing(self)) { + mp_raise_RuntimeError(MP_COMPRESSED_ROM_TEXT("Not playing")); + } + common_hal_mtm_hardware_dacout_pause(self); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(mtm_hardware_dacout_pause_obj, mtm_hardware_dacout_obj_pause); + +//| def resume(self) -> None: +//| """Resumes sample playback after :py:func:`pause`.""" +//| ... +//| +static mp_obj_t mtm_hardware_dacout_obj_resume(mp_obj_t self_in) { + mtm_hardware_dacout_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + if (common_hal_mtm_hardware_dacout_get_paused(self)) { + common_hal_mtm_hardware_dacout_resume(self); + } + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(mtm_hardware_dacout_resume_obj, mtm_hardware_dacout_obj_resume); + +//| paused: bool +//| """True when playback is paused. (read-only)""" +//| +static mp_obj_t mtm_hardware_dacout_obj_get_paused(mp_obj_t self_in) { + mtm_hardware_dacout_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bool(common_hal_mtm_hardware_dacout_get_paused(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(mtm_hardware_dacout_get_paused_obj, mtm_hardware_dacout_obj_get_paused); + +MP_PROPERTY_GETTER(mtm_hardware_dacout_paused_obj, + (mp_obj_t)&mtm_hardware_dacout_get_paused_obj); + +// ── DACOut type definition ─────────────────────────────────────────────────── + +static const mp_rom_map_elem_t mtm_hardware_dacout_locals_dict_table[] = { + // Methods + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&mtm_hardware_dacout_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&mtm_hardware_dacout_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(&mtm_hardware_dacout_play_obj) }, + { MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&mtm_hardware_dacout_stop_obj) }, + { MP_ROM_QSTR(MP_QSTR_pause), MP_ROM_PTR(&mtm_hardware_dacout_pause_obj) }, + { MP_ROM_QSTR(MP_QSTR_resume), MP_ROM_PTR(&mtm_hardware_dacout_resume_obj) }, + + // Properties + { MP_ROM_QSTR(MP_QSTR_playing), MP_ROM_PTR(&mtm_hardware_dacout_playing_obj) }, + { MP_ROM_QSTR(MP_QSTR_paused), MP_ROM_PTR(&mtm_hardware_dacout_paused_obj) }, +}; +static MP_DEFINE_CONST_DICT(mtm_hardware_dacout_locals_dict, mtm_hardware_dacout_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + mtm_hardware_dacout_type, + MP_QSTR_DACOut, + MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, + make_new, mtm_hardware_dacout_make_new, + locals_dict, &mtm_hardware_dacout_locals_dict + ); + +// ───────────────────────────────────────────────────────────────────────────── +// mtm_hardware module definition +// ───────────────────────────────────────────────────────────────────────────── + +//| """Hardware interface to Music Thing Modular Workshop Computer peripherals. +//| +//| Provides the `DACOut` class for non-blocking audio output via the +//| MCP4822 dual-channel 12-bit SPI DAC. +//| """ + +static const mp_rom_map_elem_t mtm_hardware_module_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_mtm_hardware) }, + { MP_ROM_QSTR(MP_QSTR_DACOut), MP_ROM_PTR(&mtm_hardware_dacout_type) }, +}; + +static MP_DEFINE_CONST_DICT(mtm_hardware_module_globals, mtm_hardware_module_globals_table); + +const mp_obj_module_t mtm_hardware_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&mtm_hardware_module_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_mtm_hardware, mtm_hardware_module); diff --git a/ports/raspberrypi/boards/mtm_computer/mpconfigboard.mk b/ports/raspberrypi/boards/mtm_computer/mpconfigboard.mk index 718c393d1686f..74d9baac879b4 100644 --- a/ports/raspberrypi/boards/mtm_computer/mpconfigboard.mk +++ b/ports/raspberrypi/boards/mtm_computer/mpconfigboard.mk @@ -11,3 +11,7 @@ EXTERNAL_FLASH_DEVICES = "W25Q16JVxQ" CIRCUITPY_AUDIOEFFECTS = 1 CIRCUITPY_IMAGECAPTURE = 0 CIRCUITPY_PICODVI = 0 + +SRC_C += \ + boards/$(BOARD)/module/mtm_hardware.c \ + boards/$(BOARD)/module/DACOut.c