|
| 1 | +// This file is part of the CircuitPython project: https://circuitpython.org |
| 2 | +// |
| 3 | +// SPDX-FileCopyrightText: Copyright (c) 2026 Tod Kurt |
| 4 | +// |
| 5 | +// SPDX-License-Identifier: MIT |
| 6 | +// |
| 7 | +// MCP4822 dual-channel 12-bit SPI DAC driver for the MTM Workshop Computer. |
| 8 | +// Uses PIO + DMA for non-blocking audio playback, mirroring audiobusio.I2SOut. |
| 9 | + |
| 10 | +#include <stdint.h> |
| 11 | +#include <string.h> |
| 12 | + |
| 13 | +#include "mpconfigport.h" |
| 14 | + |
| 15 | +#include "py/gc.h" |
| 16 | +#include "py/mperrno.h" |
| 17 | +#include "py/runtime.h" |
| 18 | +#include "boards/mtm_computer/module/DACOut.h" |
| 19 | +#include "shared-bindings/microcontroller/Pin.h" |
| 20 | +#include "shared-module/audiocore/__init__.h" |
| 21 | +#include "bindings/rp2pio/StateMachine.h" |
| 22 | + |
| 23 | +// ───────────────────────────────────────────────────────────────────────────── |
| 24 | +// PIO program for MCP4822 SPI DAC |
| 25 | +// ───────────────────────────────────────────────────────────────────────────── |
| 26 | +// |
| 27 | +// Pin assignment: |
| 28 | +// OUT pin (1) = MOSI — serial data out |
| 29 | +// SET pins (N) = MOSI through CS — for CS control & command-bit injection |
| 30 | +// SIDE-SET pin (1) = SCK — serial clock |
| 31 | +// |
| 32 | +// On the MTM Workshop Computer: MOSI=GP19, CS=GP21, SCK=GP18. |
| 33 | +// The SET group spans GP19..GP21 (3 pins). GP20 is unused and driven low. |
| 34 | +// |
| 35 | +// SET PINS bit mapping (bit0=MOSI/GP19, bit1=GP20, bit2=CS/GP21): |
| 36 | +// 0 = CS low, MOSI low 1 = CS low, MOSI high 4 = CS high, MOSI low |
| 37 | +// |
| 38 | +// SIDE-SET (1 pin, SCK): side 0 = SCK low, side 1 = SCK high |
| 39 | +// |
| 40 | +// MCP4822 16-bit command word: |
| 41 | +// [15] channel (0=A, 1=B) [14] don't care [13] gain (1=1x) |
| 42 | +// [12] output enable (1) [11:0] 12-bit data |
| 43 | +// |
| 44 | +// DMA feeds unsigned 16-bit audio samples. RP2040 narrow-write replication |
| 45 | +// fills both halves of the 32-bit PIO FIFO entry with the same value, |
| 46 | +// giving mono→stereo for free. |
| 47 | +// |
| 48 | +// The PIO pulls 32 bits, then sends two SPI transactions: |
| 49 | +// Channel A: cmd nibble 0b0011, then all 16 sample bits from upper half-word |
| 50 | +// Channel B: cmd nibble 0b1011, then all 16 sample bits from lower half-word |
| 51 | +// The MCP4822 captures exactly 16 bits per CS frame (4 cmd + 12 data), |
| 52 | +// so only the top 12 of the 16 sample bits become DAC data. The bottom |
| 53 | +// 4 sample bits clock out harmlessly after the DAC has latched. |
| 54 | +// This gives correct 16-bit → 12-bit scaling (effectively sample >> 4). |
| 55 | +// |
| 56 | +// PIO instruction encoding with .side_set 1 (no opt): |
| 57 | +// [15:13] opcode [12] side-set [11:8] delay [7:0] operands |
| 58 | +// |
| 59 | +// Total: 26 instructions, 86 PIO clocks per audio sample. |
| 60 | +// ───────────────────────────────────────────────────────────────────────────── |
| 61 | + |
| 62 | +static const uint16_t mcp4822_pio_program[] = { |
| 63 | + // side SCK |
| 64 | + // 0: pull noblock side 0 ; Get 32 bits or keep X if FIFO empty |
| 65 | + 0x8080, |
| 66 | + // 1: mov x, osr side 0 ; Save for pull-noblock fallback |
| 67 | + 0xA027, |
| 68 | + |
| 69 | + // ── Channel A: command nibble 0b0011 ────────────────────────────────── |
| 70 | + // Send 4 cmd bits via SET, then all 16 sample bits via OUT. |
| 71 | + // MCP4822 captures exactly 16 bits per CS frame (4 cmd + 12 data); |
| 72 | + // the extra 4 clocks shift out the LSBs which the DAC ignores. |
| 73 | + // This gives correct 16→12 bit scaling (top 12 bits become DAC data). |
| 74 | + // 2: set pins, 0 side 0 ; CS low, MOSI=0 (bit15=0: channel A) |
| 75 | + 0xE000, |
| 76 | + // 3: nop side 1 ; SCK high — latch bit 15 |
| 77 | + 0xB042, |
| 78 | + // 4: set pins, 0 side 0 ; MOSI=0 (bit14=0: don't care) |
| 79 | + 0xE000, |
| 80 | + // 5: nop side 1 ; SCK high |
| 81 | + 0xB042, |
| 82 | + // 6: set pins, 1 side 0 ; MOSI=1 (bit13=1: gain 1x) |
| 83 | + 0xE001, |
| 84 | + // 7: nop side 1 ; SCK high |
| 85 | + 0xB042, |
| 86 | + // 8: set pins, 1 side 0 ; MOSI=1 (bit12=1: output active) |
| 87 | + 0xE001, |
| 88 | + // 9: nop side 1 ; SCK high |
| 89 | + 0xB042, |
| 90 | + // 10: set y, 15 side 0 ; Loop counter: 16 sample bits |
| 91 | + 0xE04F, |
| 92 | + // 11: out pins, 1 side 0 ; Data bit → MOSI; SCK low (bitloopA) |
| 93 | + 0x6001, |
| 94 | + // 12: jmp y--, 11 side 1 ; SCK high, loop back |
| 95 | + 0x108B, |
| 96 | + // 13: set pins, 4 side 0 ; CS high — DAC A latches |
| 97 | + 0xE004, |
| 98 | + |
| 99 | + // ── Channel B: command nibble 0b1011 ────────────────────────────────── |
| 100 | + // 14: set pins, 1 side 0 ; CS low, MOSI=1 (bit15=1: channel B) |
| 101 | + 0xE001, |
| 102 | + // 15: nop side 1 ; SCK high |
| 103 | + 0xB042, |
| 104 | + // 16: set pins, 0 side 0 ; MOSI=0 (bit14=0) |
| 105 | + 0xE000, |
| 106 | + // 17: nop side 1 ; SCK high |
| 107 | + 0xB042, |
| 108 | + // 18: set pins, 1 side 0 ; MOSI=1 (bit13=1: gain 1x) |
| 109 | + 0xE001, |
| 110 | + // 19: nop side 1 ; SCK high |
| 111 | + 0xB042, |
| 112 | + // 20: set pins, 1 side 0 ; MOSI=1 (bit12=1: output active) |
| 113 | + 0xE001, |
| 114 | + // 21: nop side 1 ; SCK high |
| 115 | + 0xB042, |
| 116 | + // 22: set y, 15 side 0 ; Loop counter: 16 sample bits |
| 117 | + 0xE04F, |
| 118 | + // 23: out pins, 1 side 0 ; Data bit → MOSI; SCK low (bitloopB) |
| 119 | + 0x6001, |
| 120 | + // 24: jmp y--, 23 side 1 ; SCK high, loop back |
| 121 | + 0x1097, |
| 122 | + // 25: set pins, 4 side 0 ; CS high — DAC B latches |
| 123 | + 0xE004, |
| 124 | +}; |
| 125 | + |
| 126 | +// Clocks per sample: 2 (pull+mov) + 42 (chanA) + 42 (chanB) = 86 |
| 127 | +// Per channel: 8(4 cmd bits × 2 clks) + 1(set y) + 32(16 bits × 2 clks) + 1(cs high) = 42 |
| 128 | +#define MCP4822_CLOCKS_PER_SAMPLE 86 |
| 129 | + |
| 130 | + |
| 131 | +void common_hal_mtm_hardware_dacout_construct(mtm_hardware_dacout_obj_t *self, |
| 132 | + const mcu_pin_obj_t *clock, const mcu_pin_obj_t *mosi, |
| 133 | + const mcu_pin_obj_t *cs) { |
| 134 | + |
| 135 | + // SET pins span from MOSI to CS. MOSI must have a lower GPIO number |
| 136 | + // than CS, with at most 4 pins between them (SET count max is 5). |
| 137 | + if (cs->number <= mosi->number || (cs->number - mosi->number) > 4) { |
| 138 | + mp_raise_ValueError( |
| 139 | + MP_COMPRESSED_ROM_TEXT("cs pin must be 1-4 positions above mosi pin")); |
| 140 | + } |
| 141 | + |
| 142 | + uint8_t set_count = cs->number - mosi->number + 1; |
| 143 | + |
| 144 | + // Initial SET pin state: CS high (bit at CS position), others low |
| 145 | + uint32_t cs_bit_position = cs->number - mosi->number; |
| 146 | + pio_pinmask32_t initial_set_state = PIO_PINMASK32_FROM_VALUE(1u << cs_bit_position); |
| 147 | + pio_pinmask32_t initial_set_dir = PIO_PINMASK32_FROM_VALUE((1u << set_count) - 1); |
| 148 | + |
| 149 | + common_hal_rp2pio_statemachine_construct( |
| 150 | + &self->state_machine, |
| 151 | + mcp4822_pio_program, MP_ARRAY_SIZE(mcp4822_pio_program), |
| 152 | + 44100 * MCP4822_CLOCKS_PER_SAMPLE, // Initial frequency; play() adjusts |
| 153 | + NULL, 0, // No init program |
| 154 | + NULL, 0, // No may_exec |
| 155 | + mosi, 1, // OUT: MOSI, 1 pin |
| 156 | + PIO_PINMASK32_NONE, PIO_PINMASK32_ALL, // OUT state=low, dir=output |
| 157 | + NULL, 0, // IN: none |
| 158 | + PIO_PINMASK32_NONE, PIO_PINMASK32_NONE, // IN pulls: none |
| 159 | + mosi, set_count, // SET: MOSI..CS |
| 160 | + initial_set_state, initial_set_dir, // SET state (CS high), dir=output |
| 161 | + clock, 1, false, // SIDE-SET: SCK, 1 pin, not pindirs |
| 162 | + PIO_PINMASK32_NONE, // SIDE-SET state: SCK low |
| 163 | + PIO_PINMASK32_FROM_VALUE(0x1), // SIDE-SET dir: output |
| 164 | + false, // No sideset enable |
| 165 | + NULL, PULL_NONE, // No jump pin |
| 166 | + PIO_PINMASK_NONE, // No wait GPIO |
| 167 | + true, // Exclusive pin use |
| 168 | + false, 32, false, // OUT shift: no autopull, 32-bit, shift left |
| 169 | + false, // Don't wait for txstall |
| 170 | + false, 32, false, // IN shift (unused) |
| 171 | + false, // Not user-interruptible |
| 172 | + 0, -1, // Wrap: whole program |
| 173 | + PIO_ANY_OFFSET, |
| 174 | + PIO_FIFO_TYPE_DEFAULT, |
| 175 | + PIO_MOV_STATUS_DEFAULT, |
| 176 | + PIO_MOV_N_DEFAULT |
| 177 | + ); |
| 178 | + |
| 179 | + self->playing = false; |
| 180 | + audio_dma_init(&self->dma); |
| 181 | +} |
| 182 | + |
| 183 | +bool common_hal_mtm_hardware_dacout_deinited(mtm_hardware_dacout_obj_t *self) { |
| 184 | + return common_hal_rp2pio_statemachine_deinited(&self->state_machine); |
| 185 | +} |
| 186 | + |
| 187 | +void common_hal_mtm_hardware_dacout_deinit(mtm_hardware_dacout_obj_t *self) { |
| 188 | + if (common_hal_mtm_hardware_dacout_deinited(self)) { |
| 189 | + return; |
| 190 | + } |
| 191 | + if (common_hal_mtm_hardware_dacout_get_playing(self)) { |
| 192 | + common_hal_mtm_hardware_dacout_stop(self); |
| 193 | + } |
| 194 | + common_hal_rp2pio_statemachine_deinit(&self->state_machine); |
| 195 | + audio_dma_deinit(&self->dma); |
| 196 | +} |
| 197 | + |
| 198 | +void common_hal_mtm_hardware_dacout_play(mtm_hardware_dacout_obj_t *self, |
| 199 | + mp_obj_t sample, bool loop) { |
| 200 | + |
| 201 | + if (common_hal_mtm_hardware_dacout_get_playing(self)) { |
| 202 | + common_hal_mtm_hardware_dacout_stop(self); |
| 203 | + } |
| 204 | + |
| 205 | + uint8_t bits_per_sample = audiosample_get_bits_per_sample(sample); |
| 206 | + if (bits_per_sample < 16) { |
| 207 | + bits_per_sample = 16; |
| 208 | + } |
| 209 | + |
| 210 | + uint32_t sample_rate = audiosample_get_sample_rate(sample); |
| 211 | + uint8_t channel_count = audiosample_get_channel_count(sample); |
| 212 | + if (channel_count > 2) { |
| 213 | + mp_raise_ValueError(MP_COMPRESSED_ROM_TEXT("Too many channels in sample.")); |
| 214 | + } |
| 215 | + |
| 216 | + // PIO clock = sample_rate × clocks_per_sample |
| 217 | + common_hal_rp2pio_statemachine_set_frequency( |
| 218 | + &self->state_machine, |
| 219 | + (uint32_t)sample_rate * MCP4822_CLOCKS_PER_SAMPLE); |
| 220 | + common_hal_rp2pio_statemachine_restart(&self->state_machine); |
| 221 | + |
| 222 | + // DMA feeds unsigned 16-bit samples. The PIO discards the top 4 bits |
| 223 | + // of each 16-bit half and uses the remaining 12 as DAC data. |
| 224 | + // RP2040 narrow-write replication: 16-bit DMA write → same value in |
| 225 | + // both 32-bit FIFO halves → mono-to-stereo for free. |
| 226 | + audio_dma_result result = audio_dma_setup_playback( |
| 227 | + &self->dma, |
| 228 | + sample, |
| 229 | + loop, |
| 230 | + false, // single_channel_output |
| 231 | + 0, // audio_channel |
| 232 | + false, // output_signed = false (unsigned for MCP4822) |
| 233 | + bits_per_sample, // output_resolution |
| 234 | + (uint32_t)&self->state_machine.pio->txf[self->state_machine.state_machine], |
| 235 | + self->state_machine.tx_dreq, |
| 236 | + false); // swap_channel |
| 237 | + |
| 238 | + if (result == AUDIO_DMA_DMA_BUSY) { |
| 239 | + common_hal_mtm_hardware_dacout_stop(self); |
| 240 | + mp_raise_RuntimeError(MP_COMPRESSED_ROM_TEXT("No DMA channel found")); |
| 241 | + } else if (result == AUDIO_DMA_MEMORY_ERROR) { |
| 242 | + common_hal_mtm_hardware_dacout_stop(self); |
| 243 | + mp_raise_RuntimeError(MP_COMPRESSED_ROM_TEXT("Unable to allocate buffers for signed conversion")); |
| 244 | + } else if (result == AUDIO_DMA_SOURCE_ERROR) { |
| 245 | + common_hal_mtm_hardware_dacout_stop(self); |
| 246 | + mp_raise_RuntimeError(MP_COMPRESSED_ROM_TEXT("Audio source error")); |
| 247 | + } |
| 248 | + |
| 249 | + self->playing = true; |
| 250 | +} |
| 251 | + |
| 252 | +void common_hal_mtm_hardware_dacout_pause(mtm_hardware_dacout_obj_t *self) { |
| 253 | + audio_dma_pause(&self->dma); |
| 254 | +} |
| 255 | + |
| 256 | +void common_hal_mtm_hardware_dacout_resume(mtm_hardware_dacout_obj_t *self) { |
| 257 | + audio_dma_resume(&self->dma); |
| 258 | +} |
| 259 | + |
| 260 | +bool common_hal_mtm_hardware_dacout_get_paused(mtm_hardware_dacout_obj_t *self) { |
| 261 | + return audio_dma_get_paused(&self->dma); |
| 262 | +} |
| 263 | + |
| 264 | +void common_hal_mtm_hardware_dacout_stop(mtm_hardware_dacout_obj_t *self) { |
| 265 | + audio_dma_stop(&self->dma); |
| 266 | + common_hal_rp2pio_statemachine_stop(&self->state_machine); |
| 267 | + self->playing = false; |
| 268 | +} |
| 269 | + |
| 270 | +bool common_hal_mtm_hardware_dacout_get_playing(mtm_hardware_dacout_obj_t *self) { |
| 271 | + bool playing = audio_dma_get_playing(&self->dma); |
| 272 | + if (!playing && self->playing) { |
| 273 | + common_hal_mtm_hardware_dacout_stop(self); |
| 274 | + } |
| 275 | + return playing; |
| 276 | +} |
0 commit comments