diff --git a/platform/drivers/audio/stm32_adc.cpp b/platform/drivers/audio/stm32_adc.cpp
new file mode 100644
index 00000000..d5920b42
--- /dev/null
+++ b/platform/drivers/audio/stm32_adc.cpp
@@ -0,0 +1,271 @@
+/***************************************************************************
+ * Copyright (C) 2023 by Federico Amedeo Izzo IU2NUO, *
+ * Niccolò Izzo IU2KIN *
+ * Frederik Saraci IU2NRO *
+ * Silvano Seva IU2KWO *
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program; if not, see *
+ ***************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+#include "stm32_adc.h"
+
+struct AdcPeriph
+{
+ ADC_TypeDef *adc;
+ StreamHandler *stream;
+ Timer tim;
+};
+
+using Dma2_Stream0 = DmaStream< DMA2_BASE, 0, 0, 2 >; // DMA 2, Stream 2, channel 0, high priority (ADC1)
+using Dma2_Stream1 = DmaStream< DMA2_BASE, 1, 2, 2 >; // DMA 2, Stream 2, channel 2, high priority (ADC3)
+using Dma2_Stream2 = DmaStream< DMA2_BASE, 2, 1, 2 >; // DMA 2, Stream 2, channel 1, high priority (ADC2)
+
+StreamHandler Dma2_Stream0_hdl = Dma2_Stream0::init(10, DataSize::_16BIT, false);
+StreamHandler Dma2_Stream1_hdl = Dma2_Stream1::init(10, DataSize::_16BIT, false);
+StreamHandler Dma2_Stream2_hdl = Dma2_Stream2::init(10, DataSize::_16BIT, false);
+
+struct streamCtx *AdcContext[3];
+
+static constexpr AdcPeriph periph[] =
+{
+ {(ADC_TypeDef *) ADC1_BASE, &Dma2_Stream0_hdl, Timer(TIM2_BASE)},
+ {(ADC_TypeDef *) ADC2_BASE, &Dma2_Stream2_hdl, Timer(TIM2_BASE)},
+ {(ADC_TypeDef *) ADC3_BASE, &Dma2_Stream1_hdl, Timer(TIM2_BASE)},
+};
+
+
+/**
+ * \internal
+ * Stop an ongoing transfer, deactivating timers and DMA stream.
+ */
+static void stopTransfer(const uint8_t instNum)
+{
+ periph[instNum].tim.stop();
+ periph[instNum].adc->CR2 &= ~ADC_CR2_ADON;
+ AdcContext[instNum]->running = 0;
+}
+
+/**
+ * \internal
+ * Actual implementation of DMA interrupt handler.
+ */
+void __attribute__((used)) ADC_DMA_Handler(uint32_t instNum)
+{
+ switch(instNum)
+ {
+ case 0:
+ Dma2_Stream0::IRQhandleInterrupt(periph[instNum].stream);
+ break;
+
+ case 1:
+ Dma2_Stream2::IRQhandleInterrupt(periph[instNum].stream);
+ break;
+
+ case 2:
+ Dma2_Stream1::IRQhandleInterrupt(periph[instNum].stream);
+ break;
+ }
+}
+
+// DMA 2, Stream 0: data transfer for ADC1
+void __attribute__((used)) DMA2_Stream0_IRQHandler()
+{
+ saveContext();
+ asm volatile("mov r0, #0");
+ asm volatile("bl _Z15ADC_DMA_Handlerm");
+ restoreContext();
+}
+
+// DMA 2, Stream 2: data transfer for ADC2
+void __attribute__((used)) DMA2_Stream2_IRQHandler()
+{
+ saveContext();
+ asm volatile("mov r0, #1");
+ asm volatile("bl _Z15ADC_DMA_Handlerm");
+ restoreContext();
+}
+
+// DMA 2, Stream 1: data transfer for ADC3
+void __attribute__((used)) DMA2_Stream1_IRQHandler()
+{
+ saveContext();
+ asm volatile("mov r0, #2");
+ asm volatile("bl _Z15ADC_DMA_Handlerm");
+ restoreContext();
+}
+
+
+
+void stm32adc_init(const uint8_t instance)
+{
+ // Enable peripherals
+ switch(instance)
+ {
+ case 0:
+ RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
+ break;
+
+ case 1:
+ RCC->APB2ENR |= RCC_APB2ENR_ADC2EN;
+ break;
+
+ case 2:
+ RCC->APB2ENR |= RCC_APB2ENR_ADC3EN;
+ break;
+ }
+
+ /*
+ * Use TIM2 as trigger source for all the ADCs.
+ * TODO: change this with a dedicated timer for each ADC.
+ */
+ RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
+ uint32_t adcTrig = ADC_CR2_EXTSEL_1 // 0b0110 TIM2_TRGO trig. source
+ | ADC_CR2_EXTSEL_2;
+
+ __DSB();
+
+ /*
+ * ADC configuration.
+ *
+ * ADC clock is APB2 frequency divided by 4, giving 21MHz.
+ * Channel sample time set to 84 cycles, total conversion time is 100
+ * cycles: this leads to a maximum sampling frequency of 210kHz.
+ * Convert one channel only, no overrun interrupt, 12-bit resolution,
+ * no analog watchdog, discontinuous mode, no end of conversion interrupts.
+ */
+ ADC_TypeDef *adc = periph[instance].adc;
+
+ ADC->CCR |= ADC_CCR_ADCPRE_0;
+ adc->SMPR2 = ADC_SMPR2_SMP2_2
+ | ADC_SMPR2_SMP1_2;
+ adc->SQR1 = 0; // Convert one channel
+ adc->CR1 |= ADC_CR1_DISCEN;
+ adc->CR2 |= ADC_CR2_EXTEN_0 // Trigger on rising edge
+ | adcTrig // Trigger source
+ | ADC_CR2_DDS // Continuous DMA requests
+ | ADC_CR2_DMA; // Enable DMA data transfer
+
+ // Register end-of-transfer callbacks
+ periph[instance].stream->setEndTransferCallback(std::bind(stopTransfer, instance));
+}
+
+void stm32adc_terminate()
+{
+ // Terminate streams before shutting of the peripherals
+ for(int i = 0; i < 2; i++)
+ {
+ if(AdcContext[i] != NULL)
+ {
+ if(AdcContext[i]->running != 0)
+ periph[i].stream->halt();
+ }
+ }
+
+ // TODO: turn off peripherals
+ RCC->APB1ENR &= ~RCC_APB1ENR_TIM2EN;
+ __DSB();
+}
+
+static int stm32adc_start(const uint8_t instance, const void *config, struct streamCtx *ctx)
+{
+ if(ctx == NULL)
+ return -EINVAL;
+
+ if((ctx->running != 0) || (periph[instance].stream->running()))
+ return -EBUSY;
+
+ __disable_irq();
+ ctx->running = 1;
+ __enable_irq();
+
+ ctx->priv = (void *) &periph[instance];
+ AdcContext[instance] = ctx;
+
+ // Set conversion channel and enable the ADC
+ periph[instance].adc->SQR3 = (uint32_t) config;
+ periph[instance].adc->CR2 |= ADC_CR2_ADON;
+
+ // Start DMA stream
+ bool circ = false;
+ if(ctx->bufMode == BUF_CIRC_DOUBLE)
+ circ = true;
+
+ periph[instance].stream->start(&(periph[instance].adc->DR), ctx->buffer,
+ ctx->bufSize, circ);
+
+ // Configure ADC trigger
+ periph[instance].tim.setUpdateFrequency(ctx->sampleRate);
+ periph[instance].tim.start();
+
+ return 0;
+}
+
+static int stm32adc_data(struct streamCtx *ctx, stream_sample_t **buf)
+{
+ AdcPeriph *p = reinterpret_cast< AdcPeriph * >(ctx->priv);
+
+ if(ctx->bufMode == BUF_CIRC_DOUBLE)
+ {
+ *buf = reinterpret_cast< stream_sample_t *>(p->stream->idleBuf());
+ return ctx->bufSize/2;
+ }
+
+ // Linear mode: return the full buffer
+ *buf = ctx->buffer;
+ return ctx->bufSize;
+}
+
+static int stm32adc_sync(struct streamCtx *ctx, uint8_t dirty)
+{
+ (void) dirty;
+
+ AdcPeriph *p = reinterpret_cast< AdcPeriph * >(ctx->priv);
+ bool ok = p->stream->sync();
+ if(ok)
+ return 0;
+
+ return -1;
+}
+
+static void stm32adc_stop(struct streamCtx *ctx)
+{
+ if(ctx->running == 0)
+ return;
+
+ reinterpret_cast< AdcPeriph * >(ctx->priv)->stream->stop();
+}
+
+static void stm32adc_halt(struct streamCtx *ctx)
+{
+ if(ctx->running == 0)
+ return;
+
+ reinterpret_cast< AdcPeriph * >(ctx->priv)->stream->halt();
+}
+
+#pragma GCC diagnostic ignored "-Wpedantic"
+const struct audioDriver stm32_adc_audio_driver =
+{
+ .start = stm32adc_start,
+ .data = stm32adc_data,
+ .sync = stm32adc_sync,
+ .stop = stm32adc_stop,
+ .terminate = stm32adc_halt
+};
+#pragma GCC diagnostic pop
diff --git a/platform/drivers/audio/stm32_adc.h b/platform/drivers/audio/stm32_adc.h
new file mode 100644
index 00000000..e2dfed40
--- /dev/null
+++ b/platform/drivers/audio/stm32_adc.h
@@ -0,0 +1,73 @@
+/***************************************************************************
+ * Copyright (C) 2023 by Federico Amedeo Izzo IU2NUO, *
+ * Niccolò Izzo IU2KIN *
+ * Frederik Saraci IU2NRO *
+ * Silvano Seva IU2KWO *
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program; if not, see *
+ ***************************************************************************/
+
+#ifndef STM32_ADC_H
+#define STM32_ADC_H
+
+#include
+#include
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Driver for STM32F4xx ADC peripheral used as audio input stream device.
+ * Output data format is signed 16-bit, hardware outputs positive-only 12-bit
+ * samples.
+ *
+ * This driver has three instances:
+ *
+ * - instance 0: ADC1
+ * - instance 1: ADC2
+ * - instance 2: ADC3
+ *
+ * The configuration parameter for each instance is the ADC input number.
+ */
+
+
+enum Stm32AdcInstance
+{
+ STM32_ADC_ADC1 = 0,
+ STM32_ADC_ADC2,
+ STM32_ADC_ADC3,
+};
+
+
+extern const struct audioDriver stm32_adc_audio_driver;
+
+
+/**
+ * Initialize the driver and the peripherals.
+ */
+void stm32adc_init(const uint8_t instance);
+
+/**
+ * Shutdown the driver and the peripherals.
+ */
+void stm32adc_terminate();
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* STM32_DAC_H */