Skip to content

Commit 38b0233

Browse files
committed
mtm_hardware.c added
1 parent d8bbe2a commit 38b0233

1 file changed

Lines changed: 267 additions & 0 deletions

File tree

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
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+
// Python bindings for the mtm_hardware module.
8+
// Provides DACOut: a non-blocking audio player for the MCP4822 SPI DAC.
9+
10+
#include <stdint.h>
11+
12+
#include "shared/runtime/context_manager_helpers.h"
13+
#include "py/binary.h"
14+
#include "py/objproperty.h"
15+
#include "py/runtime.h"
16+
#include "shared-bindings/microcontroller/Pin.h"
17+
#include "shared-bindings/util.h"
18+
#include "boards/mtm_computer/module/DACOut.h"
19+
20+
// ─────────────────────────────────────────────────────────────────────────────
21+
// DACOut class
22+
// ─────────────────────────────────────────────────────────────────────────────
23+
24+
//| class DACOut:
25+
//| """Output audio to the MCP4822 dual-channel 12-bit SPI DAC."""
26+
//|
27+
//| def __init__(
28+
//| self,
29+
//| clock: microcontroller.Pin,
30+
//| mosi: microcontroller.Pin,
31+
//| cs: microcontroller.Pin,
32+
//| ) -> None:
33+
//| """Create a DACOut object associated with the given SPI pins.
34+
//|
35+
//| :param ~microcontroller.Pin clock: The SPI clock (SCK) pin
36+
//| :param ~microcontroller.Pin mosi: The SPI data (SDI/MOSI) pin
37+
//| :param ~microcontroller.Pin cs: The chip select (CS) pin
38+
//|
39+
//| Simple 8ksps 440 Hz sine wave::
40+
//|
41+
//| import mtm_hardware
42+
//| import audiocore
43+
//| import board
44+
//| import array
45+
//| import time
46+
//| import math
47+
//|
48+
//| length = 8000 // 440
49+
//| sine_wave = array.array("H", [0] * length)
50+
//| for i in range(length):
51+
//| sine_wave[i] = int(math.sin(math.pi * 2 * i / length) * (2 ** 15) + 2 ** 15)
52+
//|
53+
//| sine_wave = audiocore.RawSample(sine_wave, sample_rate=8000)
54+
//| dac = mtm_hardware.DACOut(clock=board.GP18, mosi=board.GP19, cs=board.GP21)
55+
//| dac.play(sine_wave, loop=True)
56+
//| time.sleep(1)
57+
//| dac.stop()
58+
//|
59+
//| Playing a wave file from flash::
60+
//|
61+
//| import board
62+
//| import audiocore
63+
//| import mtm_hardware
64+
//|
65+
//| f = open("sound.wav", "rb")
66+
//| wav = audiocore.WaveFile(f)
67+
//|
68+
//| dac = mtm_hardware.DACOut(clock=board.GP18, mosi=board.GP19, cs=board.GP21)
69+
//| dac.play(wav)
70+
//| while dac.playing:
71+
//| pass"""
72+
//| ...
73+
//|
74+
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) {
75+
enum { ARG_clock, ARG_mosi, ARG_cs };
76+
static const mp_arg_t allowed_args[] = {
77+
{ MP_QSTR_clock, MP_ARG_OBJ | MP_ARG_KW_ONLY | MP_ARG_REQUIRED },
78+
{ MP_QSTR_mosi, MP_ARG_OBJ | MP_ARG_KW_ONLY | MP_ARG_REQUIRED },
79+
{ MP_QSTR_cs, MP_ARG_OBJ | MP_ARG_KW_ONLY | MP_ARG_REQUIRED },
80+
};
81+
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
82+
mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
83+
84+
const mcu_pin_obj_t *clock = validate_obj_is_free_pin(args[ARG_clock].u_obj, MP_QSTR_clock);
85+
const mcu_pin_obj_t *mosi = validate_obj_is_free_pin(args[ARG_mosi].u_obj, MP_QSTR_mosi);
86+
const mcu_pin_obj_t *cs = validate_obj_is_free_pin(args[ARG_cs].u_obj, MP_QSTR_cs);
87+
88+
mtm_hardware_dacout_obj_t *self = mp_obj_malloc_with_finaliser(mtm_hardware_dacout_obj_t, &mtm_hardware_dacout_type);
89+
common_hal_mtm_hardware_dacout_construct(self, clock, mosi, cs);
90+
91+
return MP_OBJ_FROM_PTR(self);
92+
}
93+
94+
static void check_for_deinit(mtm_hardware_dacout_obj_t *self) {
95+
if (common_hal_mtm_hardware_dacout_deinited(self)) {
96+
raise_deinited_error();
97+
}
98+
}
99+
100+
//| def deinit(self) -> None:
101+
//| """Deinitialises the DACOut and releases any hardware resources for reuse."""
102+
//| ...
103+
//|
104+
static mp_obj_t mtm_hardware_dacout_deinit(mp_obj_t self_in) {
105+
mtm_hardware_dacout_obj_t *self = MP_OBJ_TO_PTR(self_in);
106+
common_hal_mtm_hardware_dacout_deinit(self);
107+
return mp_const_none;
108+
}
109+
static MP_DEFINE_CONST_FUN_OBJ_1(mtm_hardware_dacout_deinit_obj, mtm_hardware_dacout_deinit);
110+
111+
//| def __enter__(self) -> DACOut:
112+
//| """No-op used by Context Managers."""
113+
//| ...
114+
//|
115+
// Provided by context manager helper.
116+
117+
//| def __exit__(self) -> None:
118+
//| """Automatically deinitializes the hardware when exiting a context. See
119+
//| :ref:`lifetime-and-contextmanagers` for more info."""
120+
//| ...
121+
//|
122+
// Provided by context manager helper.
123+
124+
//| def play(self, sample: circuitpython_typing.AudioSample, *, loop: bool = False) -> None:
125+
//| """Plays the sample once when loop=False and continuously when loop=True.
126+
//| Does not block. Use `playing` to block.
127+
//|
128+
//| Sample must be an `audiocore.WaveFile`, `audiocore.RawSample`, `audiomixer.Mixer` or `audiomp3.MP3Decoder`.
129+
//|
130+
//| The sample itself should consist of 8 bit or 16 bit samples."""
131+
//| ...
132+
//|
133+
static mp_obj_t mtm_hardware_dacout_obj_play(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
134+
enum { ARG_sample, ARG_loop };
135+
static const mp_arg_t allowed_args[] = {
136+
{ MP_QSTR_sample, MP_ARG_OBJ | MP_ARG_REQUIRED },
137+
{ MP_QSTR_loop, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} },
138+
};
139+
mtm_hardware_dacout_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]);
140+
check_for_deinit(self);
141+
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
142+
mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
143+
144+
mp_obj_t sample = args[ARG_sample].u_obj;
145+
common_hal_mtm_hardware_dacout_play(self, sample, args[ARG_loop].u_bool);
146+
147+
return mp_const_none;
148+
}
149+
MP_DEFINE_CONST_FUN_OBJ_KW(mtm_hardware_dacout_play_obj, 1, mtm_hardware_dacout_obj_play);
150+
151+
//| def stop(self) -> None:
152+
//| """Stops playback."""
153+
//| ...
154+
//|
155+
static mp_obj_t mtm_hardware_dacout_obj_stop(mp_obj_t self_in) {
156+
mtm_hardware_dacout_obj_t *self = MP_OBJ_TO_PTR(self_in);
157+
check_for_deinit(self);
158+
common_hal_mtm_hardware_dacout_stop(self);
159+
return mp_const_none;
160+
}
161+
MP_DEFINE_CONST_FUN_OBJ_1(mtm_hardware_dacout_stop_obj, mtm_hardware_dacout_obj_stop);
162+
163+
//| playing: bool
164+
//| """True when the audio sample is being output. (read-only)"""
165+
//|
166+
static mp_obj_t mtm_hardware_dacout_obj_get_playing(mp_obj_t self_in) {
167+
mtm_hardware_dacout_obj_t *self = MP_OBJ_TO_PTR(self_in);
168+
check_for_deinit(self);
169+
return mp_obj_new_bool(common_hal_mtm_hardware_dacout_get_playing(self));
170+
}
171+
MP_DEFINE_CONST_FUN_OBJ_1(mtm_hardware_dacout_get_playing_obj, mtm_hardware_dacout_obj_get_playing);
172+
173+
MP_PROPERTY_GETTER(mtm_hardware_dacout_playing_obj,
174+
(mp_obj_t)&mtm_hardware_dacout_get_playing_obj);
175+
176+
//| def pause(self) -> None:
177+
//| """Stops playback temporarily while remembering the position. Use `resume` to resume playback."""
178+
//| ...
179+
//|
180+
static mp_obj_t mtm_hardware_dacout_obj_pause(mp_obj_t self_in) {
181+
mtm_hardware_dacout_obj_t *self = MP_OBJ_TO_PTR(self_in);
182+
check_for_deinit(self);
183+
if (!common_hal_mtm_hardware_dacout_get_playing(self)) {
184+
mp_raise_RuntimeError(MP_COMPRESSED_ROM_TEXT("Not playing"));
185+
}
186+
common_hal_mtm_hardware_dacout_pause(self);
187+
return mp_const_none;
188+
}
189+
MP_DEFINE_CONST_FUN_OBJ_1(mtm_hardware_dacout_pause_obj, mtm_hardware_dacout_obj_pause);
190+
191+
//| def resume(self) -> None:
192+
//| """Resumes sample playback after :py:func:`pause`."""
193+
//| ...
194+
//|
195+
static mp_obj_t mtm_hardware_dacout_obj_resume(mp_obj_t self_in) {
196+
mtm_hardware_dacout_obj_t *self = MP_OBJ_TO_PTR(self_in);
197+
check_for_deinit(self);
198+
if (common_hal_mtm_hardware_dacout_get_paused(self)) {
199+
common_hal_mtm_hardware_dacout_resume(self);
200+
}
201+
return mp_const_none;
202+
}
203+
MP_DEFINE_CONST_FUN_OBJ_1(mtm_hardware_dacout_resume_obj, mtm_hardware_dacout_obj_resume);
204+
205+
//| paused: bool
206+
//| """True when playback is paused. (read-only)"""
207+
//|
208+
static mp_obj_t mtm_hardware_dacout_obj_get_paused(mp_obj_t self_in) {
209+
mtm_hardware_dacout_obj_t *self = MP_OBJ_TO_PTR(self_in);
210+
check_for_deinit(self);
211+
return mp_obj_new_bool(common_hal_mtm_hardware_dacout_get_paused(self));
212+
}
213+
MP_DEFINE_CONST_FUN_OBJ_1(mtm_hardware_dacout_get_paused_obj, mtm_hardware_dacout_obj_get_paused);
214+
215+
MP_PROPERTY_GETTER(mtm_hardware_dacout_paused_obj,
216+
(mp_obj_t)&mtm_hardware_dacout_get_paused_obj);
217+
218+
// ── DACOut type definition ───────────────────────────────────────────────────
219+
220+
static const mp_rom_map_elem_t mtm_hardware_dacout_locals_dict_table[] = {
221+
// Methods
222+
{ MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&mtm_hardware_dacout_deinit_obj) },
223+
{ MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&mtm_hardware_dacout_deinit_obj) },
224+
{ MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) },
225+
{ MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&default___exit___obj) },
226+
{ MP_ROM_QSTR(MP_QSTR_play), MP_ROM_PTR(&mtm_hardware_dacout_play_obj) },
227+
{ MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&mtm_hardware_dacout_stop_obj) },
228+
{ MP_ROM_QSTR(MP_QSTR_pause), MP_ROM_PTR(&mtm_hardware_dacout_pause_obj) },
229+
{ MP_ROM_QSTR(MP_QSTR_resume), MP_ROM_PTR(&mtm_hardware_dacout_resume_obj) },
230+
231+
// Properties
232+
{ MP_ROM_QSTR(MP_QSTR_playing), MP_ROM_PTR(&mtm_hardware_dacout_playing_obj) },
233+
{ MP_ROM_QSTR(MP_QSTR_paused), MP_ROM_PTR(&mtm_hardware_dacout_paused_obj) },
234+
};
235+
static MP_DEFINE_CONST_DICT(mtm_hardware_dacout_locals_dict, mtm_hardware_dacout_locals_dict_table);
236+
237+
MP_DEFINE_CONST_OBJ_TYPE(
238+
mtm_hardware_dacout_type,
239+
MP_QSTR_DACOut,
240+
MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS,
241+
make_new, mtm_hardware_dacout_make_new,
242+
locals_dict, &mtm_hardware_dacout_locals_dict
243+
);
244+
245+
// ─────────────────────────────────────────────────────────────────────────────
246+
// mtm_hardware module definition
247+
// ─────────────────────────────────────────────────────────────────────────────
248+
249+
//| """Hardware interface to Music Thing Modular Workshop Computer peripherals.
250+
//|
251+
//| Provides the `DACOut` class for non-blocking audio output via the
252+
//| MCP4822 dual-channel 12-bit SPI DAC.
253+
//| """
254+
255+
static const mp_rom_map_elem_t mtm_hardware_module_globals_table[] = {
256+
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_mtm_hardware) },
257+
{ MP_ROM_QSTR(MP_QSTR_DACOut), MP_ROM_PTR(&mtm_hardware_dacout_type) },
258+
};
259+
260+
static MP_DEFINE_CONST_DICT(mtm_hardware_module_globals, mtm_hardware_module_globals_table);
261+
262+
const mp_obj_module_t mtm_hardware_module = {
263+
.base = { &mp_type_module },
264+
.globals = (mp_obj_dict_t *)&mtm_hardware_module_globals,
265+
};
266+
267+
MP_REGISTER_MODULE(MP_QSTR_mtm_hardware, mtm_hardware_module);

0 commit comments

Comments
 (0)