From a5d2c9d950d0c5506140f58b127c14adec3c18ca Mon Sep 17 00:00:00 2001 From: Simon Sievert Date: Sun, 30 Mar 2025 16:33:51 +0200 Subject: [PATCH 1/3] nRF52: Add support for sound output over I2S --- Makefile | 5 + make/common/NRF5X.make | 4 + src/jsi2s.h | 37 +++++ src/jswrap_i2s.c | 326 +++++++++++++++++++++++++++++++++++++++++ src/jswrap_i2s.h | 35 +++++ targets/nrf5x/jsi2s.c | 209 ++++++++++++++++++++++++++ 6 files changed, 616 insertions(+) create mode 100644 src/jsi2s.h create mode 100644 src/jswrap_i2s.c create mode 100644 src/jswrap_i2s.h create mode 100644 targets/nrf5x/jsi2s.c diff --git a/Makefile b/Makefile index 956bf1948..c200d1e1b 100755 --- a/Makefile +++ b/Makefile @@ -679,6 +679,11 @@ ifeq ($(USE_JIT),1) SOURCES += src/jsjit.c src/jsjitc.c endif +ifeq ($(USE_I2S),1) + DEFINES += -DUSE_I2S + WRAPPERSOURCES += src/jswrap_i2s.c +endif + endif # BOOTLOADER ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DON'T USE STUFF ABOVE IN BOOTLOADER diff --git a/make/common/NRF5X.make b/make/common/NRF5X.make index 7854fed04..31fff7aad 100644 --- a/make/common/NRF5X.make +++ b/make/common/NRF5X.make @@ -83,6 +83,10 @@ else # no BOOTLOADER # Neopixel support (only NRF52) SOURCES += targets/nrf5x/i2s_ws2812b_drive.c endif + ifeq ($(USE_I2S),1) + # I2S support + SOURCES += targets/nrf5x/jsi2s.c + endif endif # Careful here.. All these includes and sources assume a SoftDevice. Not efficient/clean if softdevice (ble) is not enabled... diff --git a/src/jsi2s.h b/src/jsi2s.h new file mode 100644 index 000000000..10f07e2c5 --- /dev/null +++ b/src/jsi2s.h @@ -0,0 +1,37 @@ +/* + * This file is part of Espruino, a JavaScript interpreter for Microcontrollers + * + * Copyright (C) 2025 Simon Sievert + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * ---------------------------------------------------------------------------- + * I2S support + * ---------------------------------------------------------------------------- + */ + +#ifndef JSI2S_H +#define JSI2S_H + +#include "jspin.h" + +typedef void (*jsi2s_data_cb_t)(void); + +typedef void (*jsi2s_buffer_release_cb_t)(uint32_t *buf_ptr); + +bool jsi2s_init(Pin bclk, Pin lrck, Pin dout, int sample_width_bytes); + +void jsi2s_uninit(); + +bool jsi2s_start(uint32_t *buf_ptr, uint32_t buffer_size_bytes, jsi2s_data_cb_t data_callback, + jsi2s_buffer_release_cb_t buffer_released_callback); + +void jsi2s_stop(); + +bool jsi2s_set_next_buffer(uint32_t *buf_ptr, uint32_t buffer_size_bytes); + +bool jsi2s_idle(); + +#endif // JSI2S_H diff --git a/src/jswrap_i2s.c b/src/jswrap_i2s.c new file mode 100644 index 000000000..b957afb27 --- /dev/null +++ b/src/jswrap_i2s.c @@ -0,0 +1,326 @@ +/* + * This file is part of Espruino, a JavaScript interpreter for Microcontrollers + * + * Copyright (C) 2025 Simon Sievert + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * ---------------------------------------------------------------------------- + * JavaScript I2S Functions + * ---------------------------------------------------------------------------- + */ + +#include "jswrap_i2s.h" +#include "jsinteractive.h" +#include "jsi2s.h" + +bool jswrap_i2s_initialized = false; +bool jswrap_i2s_active = false; +JsVar *jswrap_i2s_data_callback = NULL; +JsVar *jswrap_i2s_buffer_released_callback = NULL; +volatile bool jswrap_i2s_needs_more_data_flag = false; + +struct jswrap_i2s_buf_t { + JsVar *buf; + volatile uint32_t *buf_ptr; + volatile bool release; +}; + +#define JSWRAP_I2S_BUFFER_COUNT 2 +struct jswrap_i2s_buf_t jswrap_i2s_buffers[JSWRAP_I2S_BUFFER_COUNT]; + +bool jswrap_i2s_acquire_buf(JsVar *buf, uint32_t *buf_ptr) { + for (size_t i = 0; i < JSWRAP_I2S_BUFFER_COUNT; i++) { + struct jswrap_i2s_buf_t *buf_entry = &jswrap_i2s_buffers[i]; + if (buf_entry->buf == NULL) { + jsvLockAgain(buf); + buf_entry->release = false; + buf_entry->buf = buf; + buf_entry->buf_ptr = buf_ptr; + return true; + } + } + return false; +} + +bool jswrap_i2s_mark_buf_for_release(const uint32_t *buf_ptr) { + for (size_t i = 0; i < JSWRAP_I2S_BUFFER_COUNT; i++) { + struct jswrap_i2s_buf_t *buf_entry = &jswrap_i2s_buffers[i]; + if (buf_entry->buf_ptr == buf_ptr) { + buf_entry->release = true; + return true; + } + } + return false; +} + +void jswrap_i2s_on_buffer_released(JsVar *buf) { + if (jswrap_i2s_buffer_released_callback != NULL) { + jspExecuteFunction(jswrap_i2s_buffer_released_callback, NULL, 1, &buf); + } +} + +void jswrap_i2s_release_marked_bufs() { + for (size_t i = 0; i < JSWRAP_I2S_BUFFER_COUNT; i++) { + struct jswrap_i2s_buf_t *buf_entry = &jswrap_i2s_buffers[i]; + if (buf_entry->release && (buf_entry->buf != NULL)) { + JsVar *buf = buf_entry->buf; + buf_entry->buf = NULL; + buf_entry->buf_ptr = NULL; + buf_entry->release = false; + jswrap_i2s_on_buffer_released(buf); + jsvUnLock(buf); + } + } +} + +void jswrap_i2s_release_all_buffers() { + for (size_t i = 0; i < JSWRAP_I2S_BUFFER_COUNT; i++) { + struct jswrap_i2s_buf_t *buf_entry = &jswrap_i2s_buffers[i]; + if (buf_entry->buf != NULL) { + JsVar *buf = buf_entry->buf; + buf_entry->buf = NULL; + jswrap_i2s_on_buffer_released(buf); + jsvUnLock(buf); + } + } + memset(jswrap_i2s_buffers, 0, sizeof(struct jswrap_i2s_buf_t) * JSWRAP_I2S_BUFFER_COUNT); +} + +bool jswrap_i2s_acquire_callbacks(JsVar *data_callback, JsVar *buffer_released_callback) { + if ((data_callback == NULL) || (buffer_released_callback == NULL)) { + return false; + } + jsvLockAgain(data_callback); + jswrap_i2s_data_callback = data_callback; + jsvLockAgain(buffer_released_callback); + jswrap_i2s_buffer_released_callback = buffer_released_callback; + return true; +} + +void jswrap_i2s_release_callbacks() { + if (jswrap_i2s_data_callback != NULL) { + jsvUnLock(jswrap_i2s_data_callback); + jswrap_i2s_data_callback = NULL; + } + if (jswrap_i2s_buffer_released_callback != NULL) { + jsvUnLock(jswrap_i2s_buffer_released_callback); + jswrap_i2s_buffer_released_callback = NULL; + } +} + +void jswrap_i2s_on_buffer_released_callback(uint32_t *buf_ptr) { + if (jswrap_i2s_mark_buf_for_release(buf_ptr)) { + jshHadEvent(); + } +} + +void jswrap_i2s_on_more_data_requested_callback() { + jswrap_i2s_needs_more_data_flag = true; + jshHadEvent(); +} + +/*JSON{ + "type" : "staticmethod", + "class" : "I2S", + "name" : "init", + "generate" : "jswrap_i2s_init", + "params" : [ + ["bclk","pin","Bit Clock Pin"], + ["lrck","pin","Left/Right Clock Pin"], + ["dout","pin","Data Out Pin"], + ["sample_width_bytes","int","Sample width in Bytes, one of 1, 2 or 3 (so 8 Bits, 16 Bits or 24 Bits)"] + ], + "return" : ["bool","true on success, false on error"] +} +Initialize I2S. +*/ +bool jswrap_i2s_init(Pin bclk, Pin lrck, Pin dout, int sample_width_bytes) { + if (jswrap_i2s_initialized) { + jswrap_i2s_uninit(); + } + memset(jswrap_i2s_buffers, 0, sizeof(struct jswrap_i2s_buf_t) * JSWRAP_I2S_BUFFER_COUNT); + bool init_ok = jsi2s_init(bclk, lrck, dout, sample_width_bytes); + jswrap_i2s_initialized = init_ok; + jswrap_i2s_active = false; + return init_ok; +} + +/*JSON{ + "type" : "staticmethod", + "class" : "I2S", + "name" : "uninit", + "generate" : "jswrap_i2s_uninit" +} +Uninitialize I2S. +*/ +void jswrap_i2s_uninit() { + if (!jswrap_i2s_initialized) { + return; + } + jsi2s_uninit(); + jswrap_i2s_release_callbacks(); + jswrap_i2s_release_all_buffers(); + jswrap_i2s_initialized = false; + jswrap_i2s_active = false; +} + +/*JSON{ + "type" : "staticmethod", + "class" : "I2S", + "name" : "start", + "generate" : "jswrap_i2s_start", + "params" : [ + ["buf","JsVar","Data buffer"], + ["data_callback","JsVar","Callback for requesting more data"], + ["buffer_released_callback","JsVar","Called when a buffer is not used anymore (with that buffer as an argument)"] + ], + "return" : ["bool","true on success, false on error"] +} +Start an I2S transfer. +*/ +bool jswrap_i2s_start(JsVar *buf, JsVar *data_callback, JsVar *buffer_released_callback) { + if (!jswrap_i2s_initialized) { + jsiConsolePrint("I2S: not initialized\n"); + return false; + } + if (jswrap_i2s_active) { + jswrap_i2s_stop(); + } + if (!jsvIsArrayBuffer(buf)) { + jsiConsolePrint("I2S start: buffer is not an arraybuffer\n"); + return false; + } + size_t buf_len = 0; + uint32_t *buf_ptr = (uint32_t *) jsvGetDataPointer(buf, &buf_len); + if (buf_ptr == NULL) { + jsiConsolePrint("I2S start: failed to get buffer address\n"); + return false; + } + if (!jsvIsFunction(data_callback)) { + jsiConsolePrint("I2S: data_callback must be a function\n"); + return false; + } + if (!jsvIsFunction(buffer_released_callback)) { + jsiConsolePrint("I2S: buffer_released_callback must be a function\n"); + return false; + } + jswrap_i2s_release_callbacks(); + jswrap_i2s_release_all_buffers(); + if (!jswrap_i2s_acquire_callbacks(data_callback, buffer_released_callback)) { + jsiConsolePrint("I2S: failed to acquire callbacks\n"); + return false; + } + if (!jswrap_i2s_acquire_buf(buf, buf_ptr)) { + jsiConsolePrint("I2S: failed to acquire buffer\n"); + jswrap_i2s_release_callbacks(); + return false; + } + jswrap_i2s_active = true; + bool started = jsi2s_start(buf_ptr, buf_len, jswrap_i2s_on_more_data_requested_callback, + jswrap_i2s_on_buffer_released_callback); + if (!started) { + jswrap_i2s_active = false; + jswrap_i2s_release_callbacks(); + jswrap_i2s_release_all_buffers(); + } + return started; +} + +/*JSON{ + "type" : "staticmethod", + "class" : "I2S", + "name" : "stop", + "generate" : "jswrap_i2s_stop" +} +Stop an I2S transfer. +*/ +void jswrap_i2s_stop() { + if (!jswrap_i2s_initialized) { + jsiConsolePrint("I2S: not initialized\n"); + return; + } + jsi2s_stop(); + jswrap_i2s_release_callbacks(); + jswrap_i2s_release_all_buffers(); + jswrap_i2s_active = false; +} + +/*JSON{ + "type" : "staticmethod", + "class" : "I2S", + "name" : "setNextBuffer", + "generate" : "jswrap_i2s_set_next_buffer", + "params" : [ + ["buf","JsVar","Data buffer"] + ], + "return" : ["bool","true on success, false on error"] +} +Supply the next buffer used for an active I2S transfer. +*/ +bool jswrap_i2s_set_next_buffer(JsVar *buf) { + if (!jswrap_i2s_initialized) { + jsiConsolePrint("I2S: not initialized\n"); + return false; + } + if (!jswrap_i2s_active) { + jsiConsolePrint("I2S: not active\n"); + return false; + } + if (!jsvIsArrayBuffer(buf)) { + jsiConsolePrint("I2S set next buffer: buffer is not an arraybuffer\n"); + return false; + } + size_t buf_len = 0; + uint32_t *buf_ptr = (uint32_t *) jsvGetDataPointer(buf, &buf_len); + if (buf_ptr == NULL) { + jsiConsolePrint("I2S set next buffer: failed to get buffer address\n"); + return false; + } + jswrap_i2s_release_marked_bufs(); + if (!jswrap_i2s_acquire_buf(buf, buf_ptr)) { + jsiConsolePrint("I2S: failed to acquire buffer\n"); + return false; + } + bool ok = jsi2s_set_next_buffer(buf_ptr, buf_len); + if (!ok) { + jswrap_i2s_mark_buf_for_release(buf_ptr); + jswrap_i2s_release_marked_bufs(); + return false; + } + return ok; +} + +/*JSON{ + "type" : "idle", + "generate" : "jswrap_i2s_idle" +}*/ +bool jswrap_i2s_idle() { + if (!jswrap_i2s_initialized) { + return false; + } + bool was_busy = jsi2s_idle(); + jswrap_i2s_release_marked_bufs(); + if (jswrap_i2s_needs_more_data_flag) { + jswrap_i2s_needs_more_data_flag = false; + if (jswrap_i2s_data_callback != NULL) { + jspExecuteFunction(jswrap_i2s_data_callback, NULL, 0, NULL); + } + } + return was_busy; +} + +/*JSON{ + "type" : "kill", + "generate" : "jswrap_i2s_kill" +}*/ +void jswrap_i2s_kill() { + if (jswrap_i2s_active) { + jswrap_i2s_stop(); + } + if (jswrap_i2s_initialized) { + jswrap_i2s_uninit(); + } +} diff --git a/src/jswrap_i2s.h b/src/jswrap_i2s.h new file mode 100644 index 000000000..8adc79501 --- /dev/null +++ b/src/jswrap_i2s.h @@ -0,0 +1,35 @@ +/* + * This file is part of Espruino, a JavaScript interpreter for Microcontrollers + * + * Copyright (C) 2025 Simon Sievert + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * ---------------------------------------------------------------------------- + * JavaScript I2S Functions + * ---------------------------------------------------------------------------- + */ + +#ifndef JSWRAP_I2S_H +#define JSWRAP_I2S_H + +#include "jsvar.h" +#include "jspin.h" + +bool jswrap_i2s_init(Pin bclk, Pin lrck, Pin dout, int sample_width_bytes); + +void jswrap_i2s_uninit(); + +bool jswrap_i2s_start(JsVar *buf, JsVar *data_callback, JsVar *buffer_released_callback); + +void jswrap_i2s_stop(); + +bool jswrap_i2s_set_next_buffer(JsVar *buf); + +bool jswrap_i2s_idle(); + +void jswrap_i2s_kill(); + +#endif //JSWRAP_I2S_H diff --git a/targets/nrf5x/jsi2s.c b/targets/nrf5x/jsi2s.c new file mode 100644 index 000000000..54ed35965 --- /dev/null +++ b/targets/nrf5x/jsi2s.c @@ -0,0 +1,209 @@ +/* + * This file is part of Espruino, a JavaScript interpreter for Microcontrollers + * + * Copyright (C) 2025 Simon Sievert + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * ---------------------------------------------------------------------------- + * I2S support + * ---------------------------------------------------------------------------- + */ + +#include "jsi2s.h" + +#include +#include +#include +#include + +bool jsi2s_initialized = false; +bool jsi2s_active = false; +uint32_t jsi2s_buffer_size_bytes = 0; +jsi2s_data_cb_t jsi2s_data_callback = NULL; +jsi2s_buffer_release_cb_t jsi2s_buffer_released_callback = NULL; + +void jsi2s_on_buffer_released(JsVar *buf) { + if (jsi2s_buffer_released_callback != NULL) { + jsi2s_buffer_released_callback(buf); + } +} + +void jsi2s_set_callbacks(jsi2s_data_cb_t data_callback, + jsi2s_buffer_release_cb_t buffer_released_callback) { + jsi2s_data_callback = data_callback; + jsi2s_buffer_released_callback = buffer_released_callback; +} + +void jsi2s_unset_callbacks() { + jsi2s_data_callback = NULL; + jsi2s_buffer_released_callback = NULL; +} + +static void jsi2s_data_handler(nrfx_i2s_buffers_t const *p_released, + uint32_t status) { + // ToDo: handle buffer underrun (buffers requested, but weren't set quickly enough) + if (p_released != NULL) { + if (p_released->p_tx_buffer != NULL) { + if (jsi2s_buffer_released_callback != NULL) { + jsi2s_buffer_released_callback(p_released->p_tx_buffer); + } + } + } + if (status & NRFX_I2S_STATUS_NEXT_BUFFERS_NEEDED) { + if (jsi2s_data_callback != NULL) { + jsi2s_data_callback(); + } + } +} + +bool jsi2s_init(Pin bclk, Pin lrck, Pin dout, int sample_width_bytes) { + jsi2s_uninit(); + + uint32_t bclkPin = (uint32_t) pinInfo[bclk].pin; + uint32_t lrckPin = (uint32_t) pinInfo[lrck].pin; + uint32_t doutPin = (uint32_t) pinInfo[dout].pin; + + nrfx_i2s_config_t config = NRF_DRV_I2S_DEFAULT_CONFIG; + config.sck_pin = bclkPin; + config.lrck_pin = lrckPin; + config.mck_pin = NRFX_I2S_PIN_NOT_USED; + config.sdout_pin = doutPin; + config.sdin_pin = NRFX_I2S_PIN_NOT_USED; + config.irq_priority = 7; + config.mode = NRF_I2S_MODE_MASTER; + config.format = NRF_I2S_FORMAT_I2S; + config.alignment = NRF_I2S_ALIGN_LEFT; + config.sample_width = NRF_I2S_SWIDTH_8BIT; + switch (sample_width_bytes) { + case 1: + config.sample_width = NRF_I2S_SWIDTH_8BIT; + break; + case 2: + config.sample_width = NRF_I2S_SWIDTH_16BIT; + break; + case 3: + config.sample_width = NRF_I2S_SWIDTH_24BIT; + break; + default: + // unsupported sample width + return false; + } + config.channels = NRF_I2S_CHANNELS_LEFT; + config.mck_setup = NRF_I2S_MCK_DISABLED; + // configure sample rate + // see https://docs.nordicsemi.com/bundle/ps_nrf52840/page/i2s.html#ariaid-title6 (section "Master clock (MCK)") + config.mck_setup = NRF_I2S_MCK_32MDIV31; + config.ratio = NRF_I2S_RATIO_32X; + + uint32_t err_code = nrfx_i2s_init(&config, jsi2s_data_handler); + if (err_code != NRF_SUCCESS) { + nrfx_i2s_uninit(); + jsi2s_initialized = false; + return false; + } + jsi2s_initialized = true; + return true; +} + +void jsi2s_uninit() { + if (!jsi2s_initialized) { + return; + } + if (jsi2s_active) { + jsi2s_stop(); + } + nrfx_i2s_uninit(); + jsi2s_initialized = false; +} + +bool jsi2s_start(uint32_t *buf_ptr, uint32_t buffer_size_bytes, jsi2s_data_cb_t data_callback, + jsi2s_buffer_release_cb_t buffer_released_callback) { + if (!jsi2s_initialized) { + jsiConsolePrint("I2S: not initialized\n"); + return false; + } + if (jsi2s_active) { + jsi2s_stop(); + } + if (buf_ptr == NULL) { + return false; + } + if (buffer_size_bytes == 0) { + jsiConsolePrint("I2S start: buffer must not be empty\n"); + return false; + } + if ((buffer_size_bytes % 4) != 0) { + jsiConsolePrint("I2S: buffer must be a multiple of 32 Byte words in size\n"); + return false; + } + if (data_callback == NULL) { + jsiConsolePrint("I2S: data_callback not provided\n"); + return false; + } + if (buffer_released_callback == NULL) { + jsiConsolePrint("I2S: buffer_released_callback not provided\n"); + return false; + } + jsi2s_set_callbacks(data_callback, buffer_released_callback); + nrfx_i2s_buffers_t buffers; + memset(&buffers, 0, sizeof(buffers)); + buffers.p_rx_buffer = NULL; + buffers.p_tx_buffer = buf_ptr; + uint8_t flags = 0; + jsi2s_buffer_size_bytes = buffer_size_bytes; + jsi2s_active = true; + uint32_t err_code = nrfx_i2s_start(&buffers, buffer_size_bytes / 4, flags); + if (err_code != NRF_SUCCESS) { + jsi2s_buffer_size_bytes = 0; + jsi2s_unset_callbacks(); + jsi2s_active = false; + return false; + } + return true; +} + +void jsi2s_stop() { + if (!jsi2s_initialized) { + jsiConsolePrint("I2S: not initialized\n"); + return; + } + nrfx_i2s_stop(); + jsi2s_unset_callbacks(); + jsi2s_buffer_size_bytes = 0; + jsi2s_active = false; +} + +bool jsi2s_set_next_buffer(uint32_t *buf_ptr, uint32_t buffer_size_bytes) { + if (!jsi2s_initialized) { + jsiConsolePrint("I2S: not initialized\n"); + return false; + } + if (!jsi2s_active) { + jsiConsolePrint("I2S: not active\n"); + return false; + } + if (buffer_size_bytes != jsi2s_buffer_size_bytes) { + jsiConsolePrintf("I2S: Buffers must have the same size! (expected: %d, got: %d)\n", jsi2s_buffer_size_bytes, + buffer_size_bytes); + return false; + } + if (buf_ptr == NULL) { + return false; + } + nrfx_i2s_buffers_t buffers; + memset(&buffers, 0, sizeof(buffers)); + buffers.p_rx_buffer = NULL; + buffers.p_tx_buffer = buf_ptr; + uint32_t err_code = nrfx_i2s_next_buffers_set(&buffers); + if (err_code != NRF_SUCCESS) { + return false; + } + return true; +} + +bool jsi2s_idle() { + return false; // we'll be woken up by the I2S interrupt +} From 47b473adda2479ba3303c71554be228826436d53 Mon Sep 17 00:00:00 2001 From: Simon Sievert Date: Sun, 6 Apr 2025 17:39:58 +0200 Subject: [PATCH 2/3] XiaoBLE: Enable I2S --- boards/XIAOBLE.py | 1 + 1 file changed, 1 insertion(+) diff --git a/boards/XIAOBLE.py b/boards/XIAOBLE.py index cf46ddfd9..225e7beb1 100644 --- a/boards/XIAOBLE.py +++ b/boards/XIAOBLE.py @@ -47,6 +47,7 @@ # "TENSORFLOW", "NEOPIXEL", "JIT", + "I2S", ], "makefile": [ "DEFINES += -DESPR_LSE_ENABLE", # Ensure low speed external osc enabled From 096781d511bf97a08e46a435b2cdf7fe40ccc824 Mon Sep 17 00:00:00 2001 From: Simon Sievert Date: Sun, 6 Apr 2025 17:15:37 +0200 Subject: [PATCH 3/3] Jolt.js: Enable I2S --- boards/JOLTJS.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/boards/JOLTJS.py b/boards/JOLTJS.py index a22246e9f..33ac3f226 100644 --- a/boards/JOLTJS.py +++ b/boards/JOLTJS.py @@ -35,7 +35,8 @@ 'GRAPHICS', # 'NFC', 'NEOPIXEL', - 'JIT' # JIT compiler enabled + 'JIT', # JIT compiler enabled + 'I2S' ], 'makefile' : [ 'DEFINES+=-DESPR_OFFICIAL_BOARD', # Don't display the donations nag screen