Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d8bbe2a
mtm_computer: Add DAC audio out module
todbot Mar 21, 2026
38b0233
mtm_hardware.c added
todbot Mar 21, 2026
a5f9f3b
mtm_hardware.dacout: add gain=1 or gain=2 argument
todbot Mar 23, 2026
02675cb
rework mcp4822 module from mtm_hardware.DACOut
todbot Mar 23, 2026
3b9eaf9
mtm_computer: Add DAC audio out module
todbot Mar 21, 2026
f63ea5c
mtm_hardware.c added
todbot Mar 21, 2026
c7fc0c0
mtm_hardware.dacout: add gain=1 or gain=2 argument
todbot Mar 23, 2026
2e4fc89
rework mcp4822 module from mtm_hardware.DACOut
todbot Mar 23, 2026
9fdf9dc
Merge branch 'mtm_computer_dac_audio' of github.com:todbot/circuitpyt…
todbot Mar 23, 2026
6651ac1
restore deleted circuitpython.pot, update translation
todbot Mar 23, 2026
8bf51db
mtm_computer: set DAC gain 2x
todbot Mar 23, 2026
bde1ee4
Revert "mtm_computer: set DAC gain 2x"
todbot Mar 24, 2026
c2395d9
fix zephyr builds by running update_boardnfo.py
todbot Mar 24, 2026
2de067b
mcp4822 gain argument fix, as per requested
todbot Mar 24, 2026
ad21609
mcp4822 gain argument fix, as per requested
todbot Mar 24, 2026
dd1c957
mtm_computer: make board.DAC() an actual singleton
todbot Mar 24, 2026
5bd2a42
mtm_computer: make sure board.DAC() gets deallocated on board deinit
todbot Mar 24, 2026
13d6fea
mcp4822 CS/MOSI argument fix, as per requested
todbot Mar 24, 2026
62ffc25
mcp4822 CS/MOSI argument fix, as per requested
todbot Mar 24, 2026
bec871e
mtm_computer: Add DAC audio out module
todbot Mar 21, 2026
7e66d51
mtm_hardware.c added
todbot Mar 21, 2026
eb3fd70
mtm_hardware.dacout: add gain=1 or gain=2 argument
todbot Mar 23, 2026
8d282d5
rework mcp4822 module from mtm_hardware.DACOut
todbot Mar 23, 2026
d0e286a
restore deleted circuitpython.pot, update translation
todbot Mar 23, 2026
a20f1f9
mtm_computer: set DAC gain 2x
todbot Mar 23, 2026
2793baa
Revert "mtm_computer: set DAC gain 2x"
todbot Mar 24, 2026
32165ce
fix zephyr builds by running update_boardnfo.py
todbot Mar 24, 2026
b66baec
mcp4822 gain argument fix, as per requested
todbot Mar 24, 2026
16285c1
mcp4822 gain argument fix, as per requested
todbot Mar 24, 2026
afb64e0
mtm_computer: make board.DAC() an actual singleton
todbot Mar 24, 2026
072c277
mtm_computer: make sure board.DAC() gets deallocated on board deinit
todbot Mar 24, 2026
8309a23
mcp4822 CS/MOSI argument fix, as per requested
todbot Mar 24, 2026
74aee24
mcp4822 CS/MOSI argument fix, as per requested
todbot Mar 24, 2026
981a818
Merge branch 'mtm_computer_dac_audio' of github.com:todbot/circuitpyt…
todbot Mar 25, 2026
4439b66
Merge branch 'adafruit:main' into mtm_computer_dac_audio
todbot Mar 25, 2026
0f690b7
fix zephyr builds by running update_boardnfo.py
todbot Mar 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
276 changes: 276 additions & 0 deletions ports/raspberrypi/boards/mtm_computer/module/DACOut.c
Original file line number Diff line number Diff line change
@@ -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 <stdint.h>
#include <string.h>

#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;
}
38 changes: 38 additions & 0 deletions ports/raspberrypi/boards/mtm_computer/module/DACOut.h
Original file line number Diff line number Diff line change
@@ -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;
Loading
Loading