diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot
index 2f15d70b12254..e895cb731144d 100644
--- a/locale/circuitpython.pot
+++ b/locale/circuitpython.pot
@@ -73,6 +73,7 @@ msgstr ""
 #: extmod/moductypes.c ports/atmel-samd/common-hal/pulseio/PulseIn.c
 #: ports/cxd56/common-hal/pulseio/PulseIn.c
 #: ports/nrf/common-hal/pulseio/PulseIn.c
+#: ports/raspberrypi/common-hal/pulseio/PulseIn.c
 #: ports/stm/common-hal/pulseio/PulseIn.c py/obj.c py/objstr.c
 #: py/objstrunicode.c
 msgid "%q index out of range"
@@ -406,6 +407,10 @@ msgstr ""
 msgid "AnalogOut not supported on given pin"
 msgstr ""
 
+#: ports/stm/common-hal/audiopwmio/PWMAudioOut.c
+msgid "Another PWMAudioOut is already active"
+msgstr ""
+
 #: ports/atmel-samd/common-hal/pulseio/PulseOut.c
 #: ports/cxd56/common-hal/pulseio/PulseOut.c
 msgid "Another send is already active"
@@ -520,6 +525,7 @@ msgid "Buffer is too small"
 msgstr ""
 
 #: ports/nrf/common-hal/audiopwmio/PWMAudioOut.c
+#: ports/stm/common-hal/audiopwmio/PWMAudioOut.c
 #, c-format
 msgid "Buffer length %d too big. It must be less than %d"
 msgstr ""
@@ -962,6 +968,7 @@ msgstr ""
 #: ports/cxd56/common-hal/pulseio/PulseIn.c
 #: ports/esp32s2/common-hal/pulseio/PulseIn.c
 #: ports/nrf/common-hal/pulseio/PulseIn.c
+#: ports/raspberrypi/common-hal/pulseio/PulseIn.c
 #: ports/stm/common-hal/pulseio/PulseIn.c
 #, c-format
 msgid "Failed to allocate RX buffer of %d bytes"
@@ -975,6 +982,10 @@ msgstr ""
 msgid "Failed to allocate wifi scan memory"
 msgstr ""
 
+#: ports/stm/common-hal/audiopwmio/PWMAudioOut.c
+msgid "Failed to buffer the sample"
+msgstr ""
+
 #: ports/nrf/common-hal/_bleio/Adapter.c
 msgid "Failed to connect: internal error"
 msgstr ""
@@ -1247,7 +1258,7 @@ msgstr ""
 msgid "Invalid format chunk size"
 msgstr ""
 
-#: ports/esp32s2/common-hal/pwmio/PWMOut.c
+#: ports/esp32s2/common-hal/busio/I2C.c ports/esp32s2/common-hal/pwmio/PWMOut.c
 msgid "Invalid frequency"
 msgstr ""
 
@@ -2291,7 +2302,7 @@ msgstr ""
 msgid "Unsupported format"
 msgstr ""
 
-#: py/moduerrno.c
+#: ports/raspberrypi/common-hal/pulseio/PulseOut.c py/moduerrno.c
 msgid "Unsupported operation"
 msgstr ""
 
@@ -3699,6 +3710,7 @@ msgstr ""
 #: ports/atmel-samd/common-hal/pulseio/PulseIn.c
 #: ports/cxd56/common-hal/pulseio/PulseIn.c
 #: ports/nrf/common-hal/pulseio/PulseIn.c
+#: ports/raspberrypi/common-hal/pulseio/PulseIn.c
 #: ports/stm/common-hal/pulseio/PulseIn.c py/objdict.c py/objlist.c py/objset.c
 #: shared-bindings/ps2io/Ps2.c
 msgid "pop from empty %q"
@@ -3724,6 +3736,8 @@ msgstr ""
 #: ports/esp32s2/boards/espressif_kaluga_1/mpconfigboard.h
 #: ports/esp32s2/boards/espressif_saola_1_wroom/mpconfigboard.h
 #: ports/esp32s2/boards/espressif_saola_1_wrover/mpconfigboard.h
+#: ports/esp32s2/boards/franzininho_wifi_wroom/mpconfigboard.h
+#: ports/esp32s2/boards/franzininho_wifi_wrover/mpconfigboard.h
 #: ports/esp32s2/boards/lilygo_ttgo_t8_s2_st7789/mpconfigboard.h
 #: ports/esp32s2/boards/microdev_micro_s2/mpconfigboard.h
 #: ports/esp32s2/boards/muselab_nanoesp32_s2/mpconfigboard.h
diff --git a/ports/stm/boards/espruino_pico/mpconfigboard.mk b/ports/stm/boards/espruino_pico/mpconfigboard.mk
index d6118adf88332..91481602861b0 100644
--- a/ports/stm/boards/espruino_pico/mpconfigboard.mk
+++ b/ports/stm/boards/espruino_pico/mpconfigboard.mk
@@ -20,6 +20,8 @@ LD_FILE = boards/STM32F401xd_fs.ld
 # lto for this port, and if other stuff hasn't been added in the
 # meantime
 CIRCUITPY_ULAB = 0
+CIRCUITPY_AUDIOCORE = 0
+CIRCUITPY_AUDIOPWMIO = 0
 CIRCUITPY_BUSDEVICE = 0
 CIRCUITPY_BITMAPTOOLS = 0
 CIRCUITPY_FRAMEBUFFERIO = 0
diff --git a/ports/stm/boards/espruino_wifi/mpconfigboard.mk b/ports/stm/boards/espruino_wifi/mpconfigboard.mk
index 9500a5f18583d..a2ceb1ae3843a 100644
--- a/ports/stm/boards/espruino_wifi/mpconfigboard.mk
+++ b/ports/stm/boards/espruino_wifi/mpconfigboard.mk
@@ -11,3 +11,7 @@ MCU_PACKAGE = UFQFPN48
 
 LD_COMMON = boards/common_default.ld
 LD_FILE = boards/STM32F411_fs.ld
+
+# Too big for the flash
+CIRCUITPY_AUDIOCORE = 0
+CIRCUITPY_AUDIOPWMIO = 0
diff --git a/ports/stm/boards/pyb_nano_v2/mpconfigboard.mk b/ports/stm/boards/pyb_nano_v2/mpconfigboard.mk
index cff8eea70500f..8dfc9f0036a0d 100644
--- a/ports/stm/boards/pyb_nano_v2/mpconfigboard.mk
+++ b/ports/stm/boards/pyb_nano_v2/mpconfigboard.mk
@@ -12,3 +12,7 @@ MCU_PACKAGE = UFQFPN48
 
 LD_COMMON = boards/common_default.ld
 LD_FILE = boards/STM32F411_fs.ld
+
+# Too big for the flash
+CIRCUITPY_AUDIOCORE = 0
+CIRCUITPY_AUDIOPWMIO = 0
diff --git a/ports/stm/boards/stm32f411ce_blackpill/mpconfigboard.mk b/ports/stm/boards/stm32f411ce_blackpill/mpconfigboard.mk
index 1d533e30f3118..bfb88ab0ea0dd 100644
--- a/ports/stm/boards/stm32f411ce_blackpill/mpconfigboard.mk
+++ b/ports/stm/boards/stm32f411ce_blackpill/mpconfigboard.mk
@@ -15,3 +15,7 @@ MCU_PACKAGE = UFQFPN48
 
 LD_COMMON = boards/common_default.ld
 LD_FILE = boards/STM32F411_fs.ld
+
+# Too big for the flash
+CIRCUITPY_AUDIOCORE = 0
+CIRCUITPY_AUDIOPWMIO = 0
diff --git a/ports/stm/boards/stm32f411ve_discovery/mpconfigboard.mk b/ports/stm/boards/stm32f411ve_discovery/mpconfigboard.mk
index 3cb7162a4add7..3180fbd0845e0 100644
--- a/ports/stm/boards/stm32f411ve_discovery/mpconfigboard.mk
+++ b/ports/stm/boards/stm32f411ve_discovery/mpconfigboard.mk
@@ -11,3 +11,7 @@ MCU_PACKAGE = LQFP100_f4
 
 LD_COMMON = boards/common_default.ld
 LD_FILE = boards/STM32F411_fs.ld
+
+# Too big for the flash
+CIRCUITPY_AUDIOCORE = 0
+CIRCUITPY_AUDIOPWMIO = 0
diff --git a/ports/stm/common-hal/audiopwmio/PWMAudioOut.c b/ports/stm/common-hal/audiopwmio/PWMAudioOut.c
new file mode 100644
index 0000000000000..69632181ff54f
--- /dev/null
+++ b/ports/stm/common-hal/audiopwmio/PWMAudioOut.c
@@ -0,0 +1,350 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2021 Artyom Skrobov for Adafruit Industries
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+#include <string.h>
+
+#include "py/runtime.h"
+#include "common-hal/audiopwmio/PWMAudioOut.h"
+#include "shared-bindings/audiopwmio/PWMAudioOut.h"
+
+#include "timers.h"
+
+// TODO: support multiple concurrently active outputs.
+STATIC TIM_HandleTypeDef tim_handle;
+STATIC audiopwmio_pwmaudioout_obj_t* active_audio = NULL;
+
+STATIC void set_pin(uint8_t channel, GPIO_PinState state) {
+    HAL_GPIO_WritePin(pin_port(active_audio->pin[channel]->port),
+                      pin_mask(active_audio->pin[channel]->number), state);
+}
+
+STATIC void toggle_pin(uint8_t channel) {
+    HAL_GPIO_TogglePin(pin_port(active_audio->pin[channel]->port),
+                       pin_mask(active_audio->pin[channel]->number));
+}
+
+STATIC void start_timer(audiopwmio_pwmaudioout_obj_t *self) {
+    if (self->buffer_ptr[0] >= self->buffer_length[0]) // no more pulses
+        return;
+
+    self->period = self->buffer[0][self->buffer_ptr[0]];
+    if (self->pin[1] && self->period > self->buffer[1][self->buffer_ptr[1]])
+        self->period = self->buffer[1][self->buffer_ptr[1]];
+
+    // Set the new period
+    tim_handle.Init.Period = self->period - 1;
+    HAL_TIM_Base_Init(&tim_handle);
+
+    // TIM7 has limited HAL support, set registers manually
+    tim_handle.Instance->SR = 0; // Prevent the SR from triggering an interrupt
+    tim_handle.Instance->CR1 |= TIM_CR1_CEN; // Resume timer
+    tim_handle.Instance->CR1 |= TIM_CR1_URS; // Disable non-overflow interrupts
+    __HAL_TIM_ENABLE_IT(&tim_handle, TIM_IT_UPDATE);
+}
+
+STATIC bool fill_buffers(audiopwmio_pwmaudioout_obj_t *self) {
+    // Naive PCM-to-PWM conversion
+    int16_t threshold = 0x666; // 0.05; TODO: make configurable
+    uint8_t *buffer;
+    uint32_t buffer_length;
+    audioio_get_buffer_result_t get_buffer_result;
+
+    bool average = (self->sample_channel_count > 1) && !self->pin[1];
+    bool replicate = (self->sample_channel_count == 1) && self->pin[1];
+    int8_t effective_channels = average ? 1 : self->sample_channel_count;
+
+    do {
+        get_buffer_result = audiosample_get_buffer(self->sample, false, 0, &buffer, &buffer_length);
+        if (get_buffer_result == GET_BUFFER_ERROR)
+            return false;
+
+        uint32_t num_samples = buffer_length / self->bytes_per_sample / self->sample_channel_count;
+        int16_t *buffer16 = (int16_t*)buffer;
+
+        while (num_samples--) {
+            for (int8_t channel=0; channel < effective_channels; channel++) {
+                int16_t val;
+                if (self->bytes_per_sample == 1)
+                    val = *buffer++ << 8;
+                else
+                    val = *buffer16++;
+		val += self->sample_offset;
+
+                if (average) {
+                    int16_t next;
+                    if (self->bytes_per_sample == 1)
+                        next = *buffer++ << 8;
+                    else
+                        next = *buffer16++;
+                    next += self->sample_offset;
+                    val += (next - val) / 2;
+                }
+
+                int8_t new_pos = (val > threshold) - (val < -threshold);
+                if (new_pos == -self->pos[channel]) {
+                    self->buffer[channel][self->buffer_length[channel]++] = self->len[channel];
+                    if (replicate) {
+                        self->buffer[1-channel][self->buffer_length[1-channel]++] = self->len[channel];
+                    }
+                    self->pos[channel] = new_pos;
+                    self->len[channel] = 0;
+                }
+                self->len[channel]++;
+            }
+        }
+    } while (get_buffer_result == GET_BUFFER_MORE_DATA &&
+             (!self->buffer_length[0] || (self->pin[1] && !self->buffer_length[1])));
+
+    if (get_buffer_result == GET_BUFFER_DONE) {
+        // It's the final countdown
+        for (int8_t channel=0; channel < effective_channels; channel++) {
+            self->buffer[channel][self->buffer_length[channel]++] = self->len[channel];
+            if (replicate) {
+                self->buffer[1-channel][self->buffer_length[1-channel]++] = self->len[channel];
+            }
+        }
+
+        if (self->loop)
+            audiosample_reset_buffer(self->sample, false, 0);
+        else
+            self->stopping = true;
+    }
+    return true;
+}
+
+STATIC void move_to_beginning(uint16_t *buffer, uint16_t *buffer_length, uint16_t *buffer_ptr) {
+    if (*buffer_ptr < *buffer_length) {
+        memmove(buffer, buffer + *buffer_ptr, *buffer_length - *buffer_ptr);
+        *buffer_length -= *buffer_ptr;
+    } else {
+        *buffer_length = 0;
+    }
+    *buffer_ptr = 0;
+}
+
+STATIC void pwmaudioout_event_handler(void) {
+    // Detect TIM Update event
+    if (__HAL_TIM_GET_FLAG(&tim_handle, TIM_FLAG_UPDATE) != RESET)
+    {
+        if (__HAL_TIM_GET_IT_SOURCE(&tim_handle, TIM_IT_UPDATE) != RESET)
+        {
+            __HAL_TIM_CLEAR_IT(&tim_handle, TIM_IT_UPDATE);
+            if (!active_audio || active_audio->paused) {
+                __HAL_TIM_DISABLE_IT(&tim_handle, TIM_IT_UPDATE);
+                return;
+            }
+
+            bool refill = false;
+
+            active_audio->buffer[0][active_audio->buffer_ptr[0]] -= active_audio->period;
+            if (!active_audio->buffer[0][active_audio->buffer_ptr[0]]) {
+                toggle_pin(0);
+                if (++(active_audio->buffer_ptr[0]) >= active_audio->buffer_length[0])
+                    refill = true;
+            }
+            if (active_audio->pin[1]) {
+                active_audio->buffer[1][active_audio->buffer_ptr[1]] -= active_audio->period;
+                if (!active_audio->buffer[1][active_audio->buffer_ptr[1]]) {
+                    toggle_pin(1);
+                    if (++(active_audio->buffer_ptr[1]) >= active_audio->buffer_length[1])
+                        refill = true;
+                }
+            }
+
+            if (refill) {
+                __HAL_TIM_DISABLE_IT(&tim_handle, TIM_IT_UPDATE);
+
+                move_to_beginning(active_audio->buffer[0], &active_audio->buffer_length[0], &active_audio->buffer_ptr[0]);
+                if (active_audio->pin[1])
+                    move_to_beginning(active_audio->buffer[1], &active_audio->buffer_length[1], &active_audio->buffer_ptr[1]);
+
+                if (active_audio->stopping || !fill_buffers(active_audio)) {
+                    // No more audio. Turn off output and don't restart.
+                    common_hal_audiopwmio_pwmaudioout_stop(active_audio);
+                    return;
+                }
+            }
+
+            // Count up to the next given value.
+            start_timer(active_audio);
+        }
+    }
+}
+
+void audiopwmout_reset() {
+    if (active_audio) {
+        common_hal_audiopwmio_pwmaudioout_stop(active_audio);
+    }
+}
+
+// Caller validates that pins are free.
+void common_hal_audiopwmio_pwmaudioout_construct(audiopwmio_pwmaudioout_obj_t* self,
+        const mcu_pin_obj_t* left_channel, const mcu_pin_obj_t* right_channel, uint16_t quiescent_value) {
+
+    // Set up the pin(s) for output
+    self->pin[0] = left_channel;
+    self->pin[1] = right_channel;
+    set_drive_mode(left_channel, GPIO_MODE_OUTPUT_PP);
+    if (right_channel)
+        set_drive_mode(right_channel, GPIO_MODE_OUTPUT_PP);
+
+    self->buffer[0] = NULL;
+    self->buffer[1] = NULL;
+
+    self->quiescent_value = quiescent_value;
+}
+
+bool common_hal_audiopwmio_pwmaudioout_deinited(audiopwmio_pwmaudioout_obj_t* self) {
+    return !self->pin[0];
+}
+
+STATIC void free_buffers(audiopwmio_pwmaudioout_obj_t *self) {
+    m_free(self->buffer[0]);
+    self->buffer[0] = NULL;
+    m_free(self->buffer[1]);
+    self->buffer[1] = NULL;
+}
+
+void common_hal_audiopwmio_pwmaudioout_deinit(audiopwmio_pwmaudioout_obj_t* self) {
+    if (common_hal_audiopwmio_pwmaudioout_deinited(self)) {
+        return;
+    }
+    common_hal_audiopwmio_pwmaudioout_stop(self);
+
+    free_buffers(self);
+
+    self->pin[0] = 0;
+    self->pin[1] = 0;
+}
+
+void common_hal_audiopwmio_pwmaudioout_play(audiopwmio_pwmaudioout_obj_t* self, mp_obj_t sample, bool loop) {
+    common_hal_audiopwmio_pwmaudioout_stop(self);
+    if (active_audio) {
+        mp_raise_RuntimeError(translate("Another PWMAudioOut is already active")); // TODO
+    }
+    self->sample = sample;
+    self->loop = loop;
+
+    uint32_t sample_rate = audiosample_sample_rate(sample);
+    self->bytes_per_sample = audiosample_bits_per_sample(sample) / 8;
+
+    uint32_t max_buffer_length;
+    uint8_t spacing;
+    bool single_buffer;
+    bool samples_signed;
+    audiosample_get_buffer_structure(sample, /* single channel */ false,
+        &single_buffer, &samples_signed, &max_buffer_length, &spacing);
+    self->sample_channel_count = audiosample_channel_count(sample);
+    self->sample_offset = (samples_signed ? 0x8000 : 0) - self->quiescent_value;
+
+    free_buffers(self);
+
+    if (max_buffer_length > UINT16_MAX) {
+        mp_raise_ValueError_varg(translate("Buffer length %d too big. It must be less than %d"), max_buffer_length, UINT16_MAX);
+    }
+    uint16_t buffer_length = (uint16_t)max_buffer_length;
+    self->buffer[0] = m_malloc(buffer_length * sizeof(uint16_t), false);
+    self->buffer_ptr[0] = self->buffer_length[0] = 0;
+    if (self->pin[1]) {
+        self->buffer[1] = m_malloc(buffer_length * sizeof(uint16_t), false);
+        self->buffer_ptr[1] = self->buffer_length[1] = 0;
+    }
+
+    self->pos[0] = self->pos[1] = 1; // initially on
+    self->len[0] = self->len[1] = 0;
+
+    audiosample_reset_buffer(self->sample, false, 0);
+    self->stopping = false;
+    self->paused = false;
+    if (!fill_buffers(self))
+        mp_raise_RuntimeError(translate("Failed to buffer the sample"));
+
+    // Calculate period (TODO: supersample to 1 MHz?)
+    TIM_TypeDef * tim_instance = stm_peripherals_find_timer();
+    uint32_t source = stm_peripherals_timer_get_source_freq(tim_instance);
+    uint32_t prescaler = source/sample_rate;
+
+    // Activate timer
+    active_audio = self;
+    stm_peripherals_timer_reserve(tim_instance);
+    stm_peripherals_timer_preinit(tim_instance, 4, pwmaudioout_event_handler);
+
+    tim_handle.Instance = tim_instance;
+    tim_handle.Init.Period = 100; //immediately replaced.
+    tim_handle.Init.Prescaler = prescaler - 1;
+    tim_handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
+    tim_handle.Init.CounterMode = TIM_COUNTERMODE_UP;
+    tim_handle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
+
+    HAL_TIM_Base_Init(&tim_handle);
+    tim_handle.Instance->SR = 0;
+
+    // Alternate on and off, starting with on.
+    set_pin(0, GPIO_PIN_SET);
+    if (self->pin[1])
+        set_pin(1, GPIO_PIN_SET);
+
+    // Count up to the next given value.
+    start_timer(self);
+}
+
+void common_hal_audiopwmio_pwmaudioout_stop(audiopwmio_pwmaudioout_obj_t* self) {
+    if (active_audio != self)
+        return;
+
+    // Turn off timer counter.
+    tim_handle.Instance->CR1 &= ~TIM_CR1_CEN;
+    stm_peripherals_timer_free(tim_handle.Instance);
+
+    active_audio = NULL;
+    self->stopping = false;
+    self->paused = false;
+
+    // Make sure pins are left low.
+    set_pin(0, GPIO_PIN_RESET);
+    if (self->pin[1])
+        set_pin(1, GPIO_PIN_RESET);
+
+    // Cannot free buffers here because we may be called from
+    // the interrupt handler, and the heap is not reentrant.
+}
+
+bool common_hal_audiopwmio_pwmaudioout_get_playing(audiopwmio_pwmaudioout_obj_t* self) {
+    return active_audio == self;
+}
+
+void common_hal_audiopwmio_pwmaudioout_pause(audiopwmio_pwmaudioout_obj_t* self) {
+    self->paused = true;
+}
+
+void common_hal_audiopwmio_pwmaudioout_resume(audiopwmio_pwmaudioout_obj_t* self) {
+    self->paused = false;
+    if (active_audio == self)
+        start_timer(self);
+}
+
+bool common_hal_audiopwmio_pwmaudioout_get_paused(audiopwmio_pwmaudioout_obj_t* self) {
+    return self->paused;
+}
diff --git a/ports/stm/common-hal/audiopwmio/PWMAudioOut.h b/ports/stm/common-hal/audiopwmio/PWMAudioOut.h
new file mode 100644
index 0000000000000..a137a2f4f1ea2
--- /dev/null
+++ b/ports/stm/common-hal/audiopwmio/PWMAudioOut.h
@@ -0,0 +1,58 @@
+/*
+ * This file is part of the MicroPython project, http://micropython.org/
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2021 Artyom Skrobov for Adafruit Industries
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#ifndef MICROPY_INCLUDED_STM_COMMON_HAL_AUDIOPWM_AUDIOOUT_H
+#define MICROPY_INCLUDED_STM_COMMON_HAL_AUDIOPWM_AUDIOOUT_H
+
+#include "common-hal/microcontroller/Pin.h"
+
+typedef struct {
+    mp_obj_base_t base;
+    const mcu_pin_obj_t* pin[2];
+    uint16_t quiescent_value;
+
+    uint16_t *buffer[2];
+    uint16_t buffer_length[2];
+    uint16_t buffer_ptr[2];
+
+    mp_obj_t *sample;
+    int16_t sample_offset;
+    uint8_t sample_channel_count;
+    uint8_t bytes_per_sample;
+
+    // PCM-to-PWM conversion state
+    int8_t pos[2];   // -1 for off, +1 for on
+    uint16_t len[2];
+
+    uint16_t period;
+    bool stopping;
+    bool paused;
+    bool loop;
+} audiopwmio_pwmaudioout_obj_t;
+
+void audiopwmout_reset(void);
+
+#endif
diff --git a/ports/stm/common-hal/audiopwmio/__init__.c b/ports/stm/common-hal/audiopwmio/__init__.c
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/ports/stm/common-hal/digitalio/DigitalInOut.c b/ports/stm/common-hal/digitalio/DigitalInOut.c
index a676aeb155734..931223412aeff 100644
--- a/ports/stm/common-hal/digitalio/DigitalInOut.c
+++ b/ports/stm/common-hal/digitalio/DigitalInOut.c
@@ -117,13 +117,8 @@ bool common_hal_digitalio_digitalinout_get_value(
 digitalinout_result_t common_hal_digitalio_digitalinout_set_drive_mode(
         digitalio_digitalinout_obj_t *self,
         digitalio_drive_mode_t drive_mode) {
-    GPIO_InitTypeDef GPIO_InitStruct = {0};
-    GPIO_InitStruct.Pin = pin_mask(self->pin->number);
-    GPIO_InitStruct.Mode = (drive_mode == DRIVE_MODE_OPEN_DRAIN ?
+    set_drive_mode(self->pin, drive_mode == DRIVE_MODE_OPEN_DRAIN ?
         GPIO_MODE_OUTPUT_OD : GPIO_MODE_OUTPUT_PP);
-    GPIO_InitStruct.Pull = GPIO_NOPULL;
-    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
-    HAL_GPIO_Init(pin_port(self->pin->port), &GPIO_InitStruct);
     return DIGITALINOUT_OK;
 }
 
diff --git a/ports/stm/common-hal/microcontroller/Pin.c b/ports/stm/common-hal/microcontroller/Pin.c
index 37c202d86ecf0..700f1bead0e8c 100644
--- a/ports/stm/common-hal/microcontroller/Pin.c
+++ b/ports/stm/common-hal/microcontroller/Pin.c
@@ -194,3 +194,12 @@ void common_hal_mcu_pin_claim(const mcu_pin_obj_t* pin) {
 void common_hal_mcu_pin_reset_number(uint8_t pin_no) {
     reset_pin_number(pin_no / 16, pin_no % 16);
 }
+
+void set_drive_mode(const mcu_pin_obj_t *pin, uint32_t mode) {
+    GPIO_InitTypeDef GPIO_InitStruct = {0};
+    GPIO_InitStruct.Pin = pin_mask(pin->number);
+    GPIO_InitStruct.Mode = mode;
+    GPIO_InitStruct.Pull = GPIO_NOPULL;
+    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
+    HAL_GPIO_Init(pin_port(pin->port), &GPIO_InitStruct);
+}
diff --git a/ports/stm/common-hal/microcontroller/Pin.h b/ports/stm/common-hal/microcontroller/Pin.h
index f461d3813fdec..ee99de04b1135 100644
--- a/ports/stm/common-hal/microcontroller/Pin.h
+++ b/ports/stm/common-hal/microcontroller/Pin.h
@@ -49,4 +49,6 @@ void never_reset_pin_number(uint8_t pin_port, uint8_t pin_number);
 GPIO_TypeDef * pin_port(uint8_t pin_port);
 uint16_t pin_mask(uint8_t pin_number);
 
+void set_drive_mode(const mcu_pin_obj_t *pin, uint32_t mode);
+
 #endif // MICROPY_INCLUDED_STM32_COMMON_HAL_MICROCONTROLLER_PIN_H
diff --git a/ports/stm/mpconfigport.mk b/ports/stm/mpconfigport.mk
index 4d76d03c92bc9..a3216f26783e1 100644
--- a/ports/stm/mpconfigport.mk
+++ b/ports/stm/mpconfigport.mk
@@ -12,9 +12,13 @@ ifeq ($(MCU_VARIANT),$(filter $(MCU_VARIANT),STM32F405xx STM32F407xx))
 endif
 
 ifeq ($(MCU_SERIES),F4)
+	# Audio via PWM
+	CIRCUITPY_AUDIOIO = 0
+	CIRCUITPY_AUDIOCORE ?= 1
+	CIRCUITPY_AUDIOPWMIO ?= 1
+
 	# Not yet implemented common-hal modules:
 	CIRCUITPY_AUDIOBUSIO ?= 0
-	CIRCUITPY_AUDIOIO ?= 0
 	CIRCUITPY_COUNTIO ?= 0
 	CIRCUITPY_FREQUENCYIO ?= 0
 	CIRCUITPY_I2CPERIPHERAL ?= 0
diff --git a/ports/stm/supervisor/port.c b/ports/stm/supervisor/port.c
index 3103a071606ec..0abce426c3913 100644
--- a/ports/stm/supervisor/port.c
+++ b/ports/stm/supervisor/port.c
@@ -32,6 +32,9 @@
 
 #include "common-hal/microcontroller/Pin.h"
 
+#ifdef CIRCUITPY_AUDIOPWMIO
+#include "common-hal/audiopwmio/PWMAudioOut.h"
+#endif
 #if CIRCUITPY_BUSIO
 #include "common-hal/busio/I2C.h"
 #include "common-hal/busio/SPI.h"
@@ -229,6 +232,9 @@ void SysTick_Handler(void) {
 
 void reset_port(void) {
     reset_all_pins();
+#if CIRCUITPY_AUDIOPWMIO
+    audiopwmout_reset();
+#endif
 #if CIRCUITPY_BUSIO
     i2c_reset();
     spi_reset();
diff --git a/shared-bindings/audiopwmio/PWMAudioOut.c b/shared-bindings/audiopwmio/PWMAudioOut.c
index 06571fae1e6a0..543649125b6c0 100644
--- a/shared-bindings/audiopwmio/PWMAudioOut.c
+++ b/shared-bindings/audiopwmio/PWMAudioOut.c
@@ -155,8 +155,7 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(audiopwmio_pwmaudioout___exit___obj,
 //|         Sample must be an `audiocore.WaveFile`, `audiocore.RawSample`, `audiomixer.Mixer` or `audiomp3.MP3Decoder`.
 //|
 //|         The sample itself should consist of 16 bit samples. Microcontrollers with a lower output
-//|         resolution will use the highest order bits to output. For example, the SAMD21 has a 10 bit
-//|         DAC that ignores the lowest 6 bits when playing 16 bit samples."""
+//|         resolution will use the highest order bits to output."""
 //|         ...
 //|
 STATIC mp_obj_t audiopwmio_pwmaudioout_obj_play(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {