diff --git a/meson.build b/meson.build
index 03e68d7b..02b0467d 100644
--- a/meson.build
+++ b/meson.build
@@ -261,6 +261,7 @@ at32f421_src = ['platform/mcu/AT32F421/boot/startup.cpp',
'platform/mcu/AT32F421/boot/libc_integration.cpp',
'platform/mcu/AT32F421/drivers/gpio.c',
'platform/mcu/AT32F421/drivers/delays.cpp',
+ 'platform/mcu/AT32F421/drivers/USART1.cpp',
'platform/mcu/CMSIS/Device/Artery/AT32F421/Source/system_at32f421.c']
at32f421_inc = ['platform/mcu/AT32F421',
diff --git a/platform/mcu/AT32F421/drivers/USART1.cpp b/platform/mcu/AT32F421/drivers/USART1.cpp
new file mode 100644
index 00000000..14f9dbe8
--- /dev/null
+++ b/platform/mcu/AT32F421/drivers/USART1.cpp
@@ -0,0 +1,197 @@
+/***************************************************************************
+ * Copyright (C) 2023 by Federico Amedeo Izzo IUNUO, *
+ * 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 "USART1.h"
+
+using namespace miosix;
+
+static constexpr int rxQueueMin = 16; // Minimum queue size
+
+static DynUnsyncQueue< char > rxQueue(128); // Queue for incoming data
+static Thread *rxWaiting = 0; // Thread waiting on RX
+static bool rxIdle = true; // Flag for RX idle
+static FastMutex rxMutex; // Mutex locked during reception
+static FastMutex txMutex; // Mutex locked during transmission
+
+/**
+ * \internal
+ * Wait until all characters have been written to the serial port.
+ * Needs to be callable from interrupts disabled (it is used in IRQwrite)
+ */
+static inline void waitSerialTxFifoEmpty()
+{
+ while((USART1->sts & (1 << 6)) == 0) ;
+}
+
+/**
+ * \internal
+ * Interrupt handler function, called by USART1_IRQHandler.
+ */
+void __attribute__((noinline)) usart1irqImpl()
+{
+ unsigned int status = USART1->sts;
+ char c;
+
+ // New character received
+ if(status & (1 << 5))
+ {
+ //Always read data, since this clears interrupt flags
+ c = USART1->dt;
+
+ //If no error put data in buffer
+ if((status & (1 << 1)) == 0)
+ {
+ if(rxQueue.tryPut(c) == false) {/*fifo overflow*/}
+ }
+
+ rxIdle = false;
+ }
+
+ // Idle line
+ if(status & (1 << 4))
+ {
+ c = USART1->dt; //clears interrupt flags
+ rxIdle = true;
+ }
+
+ // Enough data in buffer or idle line, awake thread
+ if((status & (1 << 4)) || rxQueue.size() >= rxQueueMin)
+ {
+ if(rxWaiting)
+ {
+ rxWaiting->IRQwakeup();
+ if(rxWaiting->IRQgetPriority()>
+ Thread::IRQgetCurrentThread()->IRQgetPriority())
+ Scheduler::IRQfindNextThread();
+ rxWaiting = 0;
+ }
+ }
+}
+
+void __attribute__((naked)) USART1_IRQHandler()
+{
+ saveContext();
+ asm volatile("bl _Z13usart1irqImplv");
+ restoreContext();
+}
+
+
+void usart1_init(unsigned int baudrate)
+{
+ CRM->apb2en |= (1 << 14);
+ __DSB();
+
+ // Get current frequency of APB2 clock
+ unsigned int freq = SystemCoreClock;
+ if(CRM->cfg_bit.apb2div != 0)
+ freq /= ((CRM->cfg_bit.apb2div & 0x03) + 1);
+
+ const unsigned int quot = 2*freq/baudrate; // 2*freq for round to nearest
+ USART1->baudr = quot/2 + (quot & 1); // Round to nearest
+ USART1->ctrl1 = (1 << 13) // Enable port
+ | (1 << 5) // Interrupt on data received
+ | (1 << 4) // Interrupt on idle line
+ | (1 << 3) // Transmission enbled
+ | (1 << 2); // Reception enabled
+
+ NVIC_SetPriority(USART1_IRQn, 15); // Lowest priority for serial
+ NVIC_EnableIRQ(USART1_IRQn);
+}
+
+void usart1_terminate()
+{
+ waitSerialTxFifoEmpty();
+
+ NVIC_DisableIRQ(USART1_IRQn);
+
+ USART1->ctrl1 &= ~(1 << 13);
+ CRM->apb2en &= ~(1 << 14);
+ __DSB();
+}
+
+ssize_t usart1_readBlock(void *buffer, size_t size, off_t where)
+{
+ (void) where;
+
+ miosix::Lock< miosix::FastMutex > l(rxMutex);
+ char *buf = reinterpret_cast< char* >(buffer);
+ size_t result = 0;
+ FastInterruptDisableLock dLock;
+
+ for(;;)
+ {
+ //Try to get data from the queue
+ for(; result < size; result++)
+ {
+ if(rxQueue.tryGet(buf[result])==false) break;
+ //This is here just not to keep IRQ disabled for the whole loop
+ FastInterruptEnableLock eLock(dLock);
+ }
+ if(rxIdle && result > 0) break;
+ if(result == size) break;
+ //Wait for data in the queue
+ do {
+ rxWaiting = Thread::IRQgetCurrentThread();
+ Thread::IRQwait();
+ {
+ FastInterruptEnableLock eLock(dLock);
+ Thread::yield();
+ }
+ } while(rxWaiting);
+ }
+
+ return result;
+}
+
+ssize_t usart1_writeBlock(void *buffer, size_t size, off_t where)
+{
+ (void) where;
+
+ miosix::Lock< miosix::FastMutex > l(txMutex);
+ const char *buf = reinterpret_cast< const char* >(buffer);
+ for(size_t i = 0; i < size; i++)
+ {
+ while((USART1->sts & (1 << 7)) == 0) ;
+ USART1->dt = *buf++;
+ }
+
+ return size;
+}
+
+void usart1_IRQwrite(const char *str)
+{
+ // We can reach here also with only kernel paused, so make sure
+ // interrupts are disabled. This is important for the DMA case
+ bool interrupts = areInterruptsEnabled();
+ if(interrupts) fastDisableInterrupts();
+
+ while(*str)
+ {
+ while((USART1->sts & (1 << 7)) == 0) ;
+ USART1->dt = *str++;
+ }
+
+ waitSerialTxFifoEmpty();
+ if(interrupts) fastEnableInterrupts();
+}
diff --git a/platform/mcu/AT32F421/drivers/USART1.h b/platform/mcu/AT32F421/drivers/USART1.h
new file mode 100644
index 00000000..b59f20a6
--- /dev/null
+++ b/platform/mcu/AT32F421/drivers/USART1.h
@@ -0,0 +1,81 @@
+/***************************************************************************
+ * Copyright (C) 2023 by Federico Amedeo Izzo IUNUO, *
+ * 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 USART1_H
+#define USART1_H
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Initialise USART1 peripheral with a given baud rate. Serial communication is
+ * configured for 8 data bits, no parity, one stop bit.
+ *
+ * @param baudrate: serial port baud rate, in bits per second.
+ */
+void usart1_init(unsigned int baudrate);
+
+/**
+ * Shut down USART1 peripheral.
+ */
+void usart1_terminate();
+
+/**
+ * Read a block of data.
+ *
+ * \param buffer buffer where read data will be stored.
+ * \param size buffer size.
+ * \param where where to read from.
+ * \return number of bytes read or a negative number on failure. Note that
+ * it is normal for this function to return less character than the amount
+ * asked.
+ */
+ssize_t usart1_readBlock(void *buffer, size_t size, off_t where);
+
+/**
+ * Write a block of data.
+ *
+ * \param buffer buffer where take data to write.
+ * \param size buffer size.
+ * \param where where to write to.
+ * \return number of bytes written or a negative number on failure.
+ */
+ssize_t usart1_writeBlock(void *buffer, size_t size, off_t where);
+
+/**
+ * Write a string.
+ * Can be used to write debug information before the kernel is started or in
+ * case of serious errors, right before rebooting.
+ * Can ONLY be called when the kernel is not yet started, paused or within
+ * an interrupt. This default implementation ignores writes.
+ *
+ * \param str the string to write. The string must be NUL terminated.
+ */
+void usart1_IRQwrite(const char *str);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* USART1_H */