diff --git a/CMakeLists.txt b/CMakeLists.txt
index f1ed2c694c70e53b8453119b438d14b6715bef71..ed973ac7367ae16918f03fcb9b17d1263fe6c610 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -284,6 +284,9 @@ sbs_target(test-usart-f4 stm32f429zi_stm32f4discovery)
 add_executable(test-usart-f7 src/tests/drivers/usart/test-usart.cpp)
 sbs_target(test-usart-f7 stm32f767zi_nucleo)
 
+add_executable(test-dma-mem-to-mem src/tests/drivers/test-dma-mem-to-mem.cpp)
+sbs_target(test-dma-mem-to-mem stm32f769ni_discovery)
+
 add_executable(test-i2c-driver-f4 src/tests/drivers/i2c/test-i2c-driver.cpp)
 sbs_target(test-i2c-driver-f4 stm32f429zi_stm32f4discovery)
 
diff --git a/cmake/boardcore.cmake b/cmake/boardcore.cmake
index 12c0eff4291a2b0d1f656dee99b25d96a76c083e..8bd4c6b27c57ce4b0b7cc01993586353fc3368c4 100644
--- a/cmake/boardcore.cmake
+++ b/cmake/boardcore.cmake
@@ -56,6 +56,7 @@ set(BOARDCORE_SRC
     ${BOARDCORE_PATH}/src/shared/drivers/canbus/CanDriver/CanDriver.cpp
     ${BOARDCORE_PATH}/src/shared/drivers/canbus/CanDriver/CanInterrupt.cpp
     ${BOARDCORE_PATH}/src/shared/drivers/canbus/CanProtocol/CanProtocol.cpp
+    ${BOARDCORE_PATH}/src/shared/drivers/dma/DMA.cpp
     ${BOARDCORE_PATH}/src/shared/drivers/interrupt/external_interrupts.cpp
     ${BOARDCORE_PATH}/src/shared/drivers/timer/PWM.cpp
     ${BOARDCORE_PATH}/src/shared/drivers/timer/CountedPWM.cpp
diff --git a/src/shared/drivers/dma/DMA.cpp b/src/shared/drivers/dma/DMA.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3848389799c375926a0ec004c201b0b8fab0ce41
--- /dev/null
+++ b/src/shared/drivers/dma/DMA.cpp
@@ -0,0 +1,575 @@
+/* Copyright (c) 2023 Skyward Experimental Rocketry
+ * Author: Alberto Nidasio
+ *
+ * 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 "DMA.h"
+
+#include <kernel/logging.h>
+#include <utils/ClockUtils.h>
+
+#include <map>
+
+using namespace miosix;
+
+namespace Boardcore
+{
+
+void __attribute__((naked)) DMA1_Stream0_IRQHandler()
+{
+    saveContext();
+    asm volatile("bl _Z20DMA1_Stream0_IRQImplv");
+    restoreContext();
+}
+
+void __attribute__((used)) DMA1_Stream0_IRQImpl()
+{
+    DMADriver::instance().IRQhandleInterrupt(DMAStreamId::DMA1_Str0);
+}
+
+void __attribute__((naked)) DMA1_Stream1_IRQHandler()
+{
+    saveContext();
+    asm volatile("bl _Z20DMA1_Stream1_IRQImplv");
+    restoreContext();
+}
+
+void __attribute__((used)) DMA1_Stream1_IRQImpl()
+{
+    DMADriver::instance().IRQhandleInterrupt(DMAStreamId::DMA1_Str1);
+}
+
+void __attribute__((naked)) DMA1_Stream2_IRQHandler()
+{
+    saveContext();
+    asm volatile("bl _Z20DMA1_Stream2_IRQImplv");
+    restoreContext();
+}
+
+void __attribute__((used)) DMA1_Stream2_IRQImpl()
+{
+    DMADriver::instance().IRQhandleInterrupt(DMAStreamId::DMA1_Str2);
+}
+
+void __attribute__((naked)) DMA1_Stream3_IRQHandler()
+{
+    saveContext();
+    asm volatile("bl _Z20DMA1_Stream3_IRQImplv");
+    restoreContext();
+}
+
+void __attribute__((used)) DMA1_Stream3_IRQImpl()
+{
+    DMADriver::instance().IRQhandleInterrupt(DMAStreamId::DMA1_Str3);
+}
+
+void __attribute__((naked)) DMA1_Stream4_IRQHandler()
+{
+    saveContext();
+    asm volatile("bl _Z20DMA1_Stream4_IRQImplv");
+    restoreContext();
+}
+
+void __attribute__((used)) DMA1_Stream4_IRQImpl()
+{
+    DMADriver::instance().IRQhandleInterrupt(DMAStreamId::DMA1_Str4);
+}
+
+void __attribute__((naked)) DMA1_Stream5_IRQHandler()
+{
+    saveContext();
+    asm volatile("bl _Z20DMA1_Stream5_IRQImplv");
+    restoreContext();
+}
+
+void __attribute__((used)) DMA1_Stream5_IRQImpl()
+{
+    DMADriver::instance().IRQhandleInterrupt(DMAStreamId::DMA1_Str5);
+}
+
+void __attribute__((naked)) DMA1_Stream6_IRQHandler()
+{
+    saveContext();
+    asm volatile("bl _Z20DMA1_Stream6_IRQImplv");
+    restoreContext();
+}
+
+void __attribute__((used)) DMA1_Stream6_IRQImpl()
+{
+    DMADriver::instance().IRQhandleInterrupt(DMAStreamId::DMA1_Str6);
+}
+
+void __attribute__((naked)) DMA1_Stream7_IRQHandler()
+{
+    saveContext();
+    asm volatile("bl _Z20DMA1_Stream7_IRQImplv");
+    restoreContext();
+}
+
+void __attribute__((used)) DMA1_Stream7_IRQImpl()
+{
+    DMADriver::instance().IRQhandleInterrupt(DMAStreamId::DMA1_Str7);
+}
+
+void __attribute__((naked)) DMA2_Stream0_IRQHandler()
+{
+    saveContext();
+    asm volatile("bl _Z20DMA2_Stream0_IRQImplv");
+    restoreContext();
+}
+
+void __attribute__((used)) DMA2_Stream0_IRQImpl()
+{
+    DMADriver::instance().IRQhandleInterrupt(DMAStreamId::DMA2_Str0);
+}
+
+void __attribute__((naked)) DMA2_Stream1_IRQHandler()
+{
+    saveContext();
+    asm volatile("bl _Z20DMA2_Stream1_IRQImplv");
+    restoreContext();
+}
+
+void __attribute__((used)) DMA2_Stream1_IRQImpl()
+{
+    DMADriver::instance().IRQhandleInterrupt(DMAStreamId::DMA2_Str1);
+}
+
+void __attribute__((naked)) DMA2_Stream2_IRQHandler()
+{
+    saveContext();
+    asm volatile("bl _Z20DMA2_Stream2_IRQImplv");
+    restoreContext();
+}
+
+void __attribute__((used)) DMA2_Stream2_IRQImpl()
+{
+    DMADriver::instance().IRQhandleInterrupt(DMAStreamId::DMA2_Str2);
+}
+
+// void __attribute__((naked)) DMA2_Stream3_IRQHandler() {
+//     saveContext();
+//     asm volatile("bl _Z20DMA2_Stream3_IRQImplv");
+//     restoreContext();
+// }
+
+// void __attribute__((used)) DMA2_Stream3_IRQImpl() {
+//     DMADriver::instance().IRQhandleInterrupt(DMAStreamId::DMA2_Str3);
+// }
+
+void __attribute__((naked)) DMA2_Stream4_IRQHandler()
+{
+    saveContext();
+    asm volatile("bl _Z20DMA2_Stream4_IRQImplv");
+    restoreContext();
+}
+
+void __attribute__((used)) DMA2_Stream4_IRQImpl()
+{
+    DMADriver::instance().IRQhandleInterrupt(DMAStreamId::DMA2_Str4);
+}
+
+// void __attribute__((naked)) DMA2_Stream5_IRQHandler() {
+//     saveContext();
+//     asm volatile("bl _Z20DMA2_Stream5_IRQImplv");
+//     restoreContext();
+// }
+
+// void __attribute__((used)) DMA2_Stream5_IRQImpl() {
+//     DMADriver::instance().IRQhandleInterrupt(DMAStreamId::DMA2_Str5);
+// }
+
+void __attribute__((naked)) DMA2_Stream6_IRQHandler()
+{
+    saveContext();
+    asm volatile("bl _Z20DMA2_Stream6_IRQImplv");
+    restoreContext();
+}
+
+void __attribute__((used)) DMA2_Stream6_IRQImpl()
+{
+    DMADriver::instance().IRQhandleInterrupt(DMAStreamId::DMA2_Str6);
+}
+
+// void __attribute__((naked)) DMA2_Stream7_IRQHandler() {
+//     saveContext();
+//     asm volatile("bl _Z20DMA2_Stream7_IRQImplv");
+//     restoreContext();
+// }
+
+// void __attribute__((used)) DMA2_Stream7_IRQImpl() {
+//     DMADriver::instance().IRQhandleInterrupt(DMAStreamId::DMA2_Str7);
+// }
+
+void DMADriver::IRQhandleInterrupt(DMAStreamId id)
+{
+    auto stream = streams[id];
+
+    stream->readFlags();
+    stream->clearAllFlags();
+
+    // Run the callbacks if neccessary
+    if (stream->halfTransferCallback && stream->halfTransferFlag)
+    {
+        stream->halfTransferCallback();
+    }
+    if (stream->transferCompleteCallback && stream->transferCompleteFlag)
+    {
+        stream->transferCompleteCallback();
+    }
+    if (stream->errorCallback &&
+        (stream->transferErrorFlag || stream->fifoErrorFlag ||
+         stream->directModeErrorFlag))
+    {
+        stream->errorCallback();
+    }
+
+    // Wakeup the thread if the user is waiting
+    if (stream->waitingThread)
+    {
+        IRQwakeupThread(stream);
+    }
+}
+
+void DMADriver::IRQwakeupThread(DMAStream* stream)
+{
+    // Wakeup the waiting thread
+    stream->waitingThread->wakeup();
+
+    // If the waiting thread has a higher priority than the current
+    // thread then reschedule
+    if (stream->waitingThread->IRQgetPriority() >
+        miosix::Thread::IRQgetCurrentThread()->IRQgetPriority())
+    {
+        miosix::Scheduler::IRQfindNextThread();
+    }
+
+    // Clear the thread pointer, this way the thread will be sure it is
+    // not a spurious wakeup
+    stream->waitingThread = nullptr;
+}
+
+DMADriver& DMADriver::instance()
+{
+    static DMADriver instance;
+    return instance;
+}
+
+bool DMADriver::tryChannel(DMAStreamId id)
+{
+    Lock<FastMutex> l(mutex);
+
+    // Return true, meaning that the channel is free, only if it is not yet
+    // allocated
+    return streams.count(id) == 0;
+}
+
+DMAStream& DMADriver::acquireStream(DMAStreamId id)
+{
+    Lock<FastMutex> l(mutex);
+
+    // Wait until the channel is free
+    while (streams.count(id) != 0)
+        cv.wait(l);
+
+    // Enable the clock if not already done
+    // TODO: Enable DMA1 or DMA2
+    // if (streams.size() == 0)
+    //     RCC->AHB1ENR |= RCC_AHB1ENR_DMA1EN;
+
+    return *(streams[id] = new DMAStream(id));
+}
+
+void DMADriver::releaseStream(DMAStreamId id)
+{
+    Lock<FastMutex> l(mutex);
+
+    if (streams.count(id) != 0)
+    {
+        delete streams[id];
+        streams.erase(id);
+        cv.broadcast();
+    }
+
+    // Disable the clock if there are no more channels
+    // TODO: Disable DMA1 or DMA2
+    // if (streams.size() == 0)
+    //     RCC->AHB1ENR &= ~RCC_AHB1ENR_DMA1EN;
+}
+
+DMADriver::DMADriver()
+{
+    // For now enable the clock always
+    ClockUtils::enablePeripheralClock(DMA1);
+    ClockUtils::enablePeripheralClock(DMA2);
+
+    // Reset interrupts flags
+    // TODO: Change this magic number
+    DMA1->HIFCR = 0x0f7d0f7d;
+    DMA1->LIFCR = 0x0f7d0f7d;
+    DMA2->HIFCR = 0x0f7d0f7d;
+    DMA2->LIFCR = 0x0f7d0f7d;
+}
+
+void DMAStream::setup(DMATransaction transaction)
+{
+    currentSetup = transaction;
+
+    // Reset the configuration
+    registers->CR = 0;
+
+    // Wait for the stream to actually be disabled
+    while (registers->CR & DMA_SxCR_EN)
+        ;
+
+    registers->CR |= static_cast<uint32_t>(transaction.channel);
+    registers->CR |= static_cast<uint32_t>(transaction.direction);
+    registers->CR |= static_cast<uint32_t>(transaction.priority);
+    if (transaction.circularMode)
+        registers->CR |= DMA_SxCR_CIRC;
+    registers->NDTR = transaction.numberOfDataItems;
+
+    if (transaction.direction == DMATransaction::Direction::MEM_TO_PER)
+    {
+        // In memory to peripheral mode, the source address is the memory
+        // address
+
+        registers->CR |= static_cast<uint32_t>(transaction.srcSize)
+                         << DMA_SxCR_MSIZE_Pos;
+        registers->CR |= static_cast<uint32_t>(transaction.dstSize)
+                         << DMA_SxCR_PSIZE_Pos;
+
+        if (transaction.srcIncrement)
+            registers->CR |= DMA_SxCR_MINC;
+        if (transaction.dstIncrement)
+            registers->CR |= DMA_SxCR_PINC;
+
+        registers->M0AR = reinterpret_cast<uint32_t>(transaction.srcAddress);
+        registers->PAR  = reinterpret_cast<uint32_t>(transaction.dstAddress);
+    }
+    else
+    {
+        // In peripheral to memory or memory to memory mode, the source address
+        // goes into the peripheral address register
+
+        registers->CR |= static_cast<uint32_t>(transaction.srcSize)
+                         << DMA_SxCR_PSIZE_Pos;
+        registers->CR |= static_cast<uint32_t>(transaction.dstSize)
+                         << DMA_SxCR_MSIZE_Pos;
+
+        if (transaction.srcIncrement)
+            registers->CR |= DMA_SxCR_PINC;
+        if (transaction.dstIncrement)
+            registers->CR |= DMA_SxCR_MINC;
+
+        registers->PAR  = reinterpret_cast<uint32_t>(transaction.srcAddress);
+        registers->M0AR = reinterpret_cast<uint32_t>(transaction.dstAddress);
+    }
+
+    if (transaction.doubleBufferMode)
+    {
+        registers->CR |= DMA_SxCR_DBM;
+        registers->M1AR =
+            reinterpret_cast<uint32_t>(transaction.secondMemoryAddress);
+    }
+
+    bool enableInterrupt = false;
+    if (transaction.enableHalfTransferInterrupt)
+    {
+        clearHalfTransferFlag();
+        registers->CR |= DMA_SxCR_HTIE;
+        enableInterrupt = true;
+    }
+    if (transaction.enableTransferCompleteInterrupt)
+    {
+        clearTransferCompleteFlag();
+        registers->CR |= DMA_SxCR_TCIE;
+        enableInterrupt = true;
+    }
+    if (transaction.enableTransferErrorInterrupt)
+    {
+        clearTransferErrorFlag();
+        registers->CR |= DMA_SxCR_TEIE;
+        enableInterrupt = true;
+    }
+    if (transaction.enableFifoErrorInterrupt)
+    {
+        clearFifoErrorFlag();
+        registers->CR |= DMA_SxFCR_FEIE;
+        enableInterrupt = true;
+    }
+    if (transaction.enableDirectModeErrorInterrupt)
+    {
+        clearDirectModeErrorFlag();
+        registers->CR |= DMA_SxCR_DMEIE;
+        enableInterrupt = true;
+    }
+
+    if (enableInterrupt)
+    {
+        NVIC_SetPriority(irqNumber, 8);
+        NVIC_ClearPendingIRQ(irqNumber);
+        NVIC_EnableIRQ(irqNumber);
+    }
+    else
+    {
+        NVIC_DisableIRQ(irqNumber);
+    }
+}
+
+void DMAStream::enable()
+{
+    // Reset all saved flags
+    halfTransferFlag     = false;
+    transferCompleteFlag = false;
+    transferErrorFlag    = false;
+    fifoErrorFlag        = false;
+    directModeErrorFlag  = false;
+
+    // Enable the peripheral
+    registers->CR |= DMA_SxCR_EN;
+}
+
+void DMAStream::disable() { registers->CR &= ~DMA_SxCR_EN; }
+
+void DMAStream::waitForHalfTransfer()
+{
+    waitForInterruptEventImpl(
+        currentSetup.enableHalfTransferInterrupt,
+        std::bind(&DMAStream::getHalfTransferFlagStatus, this),
+        std::bind(&DMAStream::clearHalfTransferFlag, this), halfTransferFlag,
+        -1);
+}
+
+void DMAStream::waitForTransferComplete()
+{
+    waitForInterruptEventImpl(
+        currentSetup.enableTransferCompleteInterrupt,
+        std::bind(&DMAStream::getTransferCompleteFlagStatus, this),
+        std::bind(&DMAStream::clearTransferCompleteFlag, this),
+        transferCompleteFlag, -1);
+}
+
+bool DMAStream::timedWaitForHalfTransfer(uint64_t timeout_ns)
+{
+    return waitForInterruptEventImpl(
+        currentSetup.enableHalfTransferInterrupt,
+        std::bind(&DMAStream::getHalfTransferFlagStatus, this),
+        std::bind(&DMAStream::clearHalfTransferFlag, this), halfTransferFlag,
+        timeout_ns);
+}
+
+bool DMAStream::timedWaitForTransferComplete(uint64_t timeout_ns)
+{
+    return waitForInterruptEventImpl(
+        currentSetup.enableTransferCompleteInterrupt,
+        std::bind(&DMAStream::getTransferCompleteFlagStatus, this),
+        std::bind(&DMAStream::clearTransferCompleteFlag, this),
+        transferCompleteFlag, timeout_ns);
+}
+
+void DMAStream::setHalfTransferCallback(std::function<void()> callback)
+{
+    halfTransferCallback = callback;
+}
+
+void DMAStream::resetHalfTransferCallback() { halfTransferCallback = nullptr; }
+
+void DMAStream::setTransferCompleteCallback(std::function<void()> callback)
+{
+    transferCompleteCallback = callback;
+}
+
+void DMAStream::resetTransferCompleteCallback()
+{
+    transferCompleteCallback = nullptr;
+}
+
+void DMAStream::setErrorCallback(std::function<void()> callback)
+{
+    errorCallback = callback;
+}
+
+void DMAStream::resetErrorCallback() { errorCallback = nullptr; }
+
+void DMAStream::readFlags()
+{
+    uint8_t flags = *ISR >> IFindex;
+
+    halfTransferFlag     = flags & DMA_LISR_HTIF0;
+    transferCompleteFlag = flags & DMA_LISR_TCIF0;
+    transferErrorFlag    = flags & DMA_LISR_TEIF0;
+    fifoErrorFlag        = flags & DMA_LISR_DMEIF0;
+    directModeErrorFlag  = flags & DMA_LISR_DMEIF0;
+}
+
+int DMAStream::getCurrentBufferNumber()
+{
+    return (registers->CR & DMA_SxCR_CT) != 0 ? 2 : 1;
+}
+
+DMAStream::DMAStream(DMAStreamId id) : id(id)
+{
+    // Get the channel registers base address and the interrupt flags clear
+    // register address
+    if (id < DMAStreamId::DMA2_Str0)
+    {
+        registers = reinterpret_cast<DMA_Stream_TypeDef*>(
+            DMA1_BASE + 0x10 + 0x18 * static_cast<int>(id));
+
+        if (id < DMAStreamId::DMA1_Str4)
+        {
+            IFCR = &DMA1->LIFCR;
+            ISR  = &DMA1->LISR;
+        }
+        else
+        {
+            IFCR = &DMA1->HIFCR;
+            ISR  = &DMA1->HISR;
+        }
+    }
+    else
+    {
+        registers = reinterpret_cast<DMA_Stream_TypeDef*>(
+            DMA2_BASE + 0x10 + 0x18 * (static_cast<int>(id) - 8));
+
+        if (id < DMAStreamId::DMA2_Str4)
+        {
+            IFCR = &DMA2->LIFCR;
+            ISR  = &DMA2->LISR;
+        }
+        else
+        {
+            IFCR = &DMA2->HIFCR;
+            ISR  = &DMA2->HISR;
+        }
+    }
+
+    // Compute the index for the interrupt flags clear register
+    // Refer to reference manual for the register bits structure
+    int offset = static_cast<int>(id) % 4;
+    IFindex    = (offset % 2) * 6 + (offset / 2) * 16;
+
+    // Select the interrupt
+    irqNumber = static_cast<IRQn_Type>(
+        static_cast<int>(IRQn_Type::DMA1_Stream0_IRQn) + static_cast<int>(id));
+}
+
+}  // namespace Boardcore
diff --git a/src/shared/drivers/dma/DMA.h b/src/shared/drivers/dma/DMA.h
new file mode 100644
index 0000000000000000000000000000000000000000..b0e31d3df58c2936e624c61cb6f144efa6ea92ab
--- /dev/null
+++ b/src/shared/drivers/dma/DMA.h
@@ -0,0 +1,375 @@
+/* Copyright (c) 2023 Skyward Experimental Rocketry
+ * Author: Alberto Nidasio
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include <interfaces/arch_registers.h>
+#include <kernel/scheduler/scheduler.h>
+#include <kernel/sync.h>
+#include <utils/TimedPollingFlag.h>
+
+#include <functional>
+#include <map>
+
+namespace Boardcore
+{
+
+enum class DMAStreamId
+{
+    DMA1_Str0 = 0,
+    DMA1_Str1,
+    DMA1_Str2,
+    DMA1_Str3,
+    DMA1_Str4,
+    DMA1_Str5,
+    DMA1_Str6,
+    DMA1_Str7,
+    DMA2_Str0,
+    DMA2_Str1,
+    DMA2_Str2,
+    DMA2_Str3,
+    DMA2_Str4,
+    DMA2_Str5,
+    DMA2_Str6,
+    DMA2_Str7,
+};
+
+struct DMATransaction
+{
+    enum class Channel : uint32_t
+    {
+        CHANNEL0 = 0,
+        CHANNEL1 = DMA_SxCR_CHSEL_0,
+        CHANNEL2 = DMA_SxCR_CHSEL_1,
+        CHANNEL3 = DMA_SxCR_CHSEL_1 | DMA_SxCR_CHSEL_0,
+        CHANNEL4 = DMA_SxCR_CHSEL_2,
+        CHANNEL5 = DMA_SxCR_CHSEL_2 | DMA_SxCR_CHSEL_0,
+        CHANNEL6 = DMA_SxCR_CHSEL_2 | DMA_SxCR_CHSEL_1,
+        CHANNEL7 = DMA_SxCR_CHSEL,
+    };
+
+    enum class Direction : uint16_t
+    {
+        MEM_TO_MEM = DMA_SxCR_DIR_1,
+        MEM_TO_PER = DMA_SxCR_DIR_0,
+        PER_TO_MEM = 0,
+    };
+
+    enum class Priority : uint32_t
+    {
+        VERY_HIGH = DMA_SxCR_PL,
+        HIGH      = DMA_SxCR_PL_1,
+        MEDIUM    = DMA_SxCR_PL_0,
+        LOW       = 0,
+    };
+
+    enum class DataSize : uint8_t
+    {
+        BITS_8 = 0,
+        BITS_16,
+        BITS_32,
+    };
+
+    Channel channel                      = Channel::CHANNEL0;
+    Direction direction                  = Direction::MEM_TO_MEM;
+    Priority priority                    = Priority::LOW;
+    DataSize srcSize                     = DataSize::BITS_32;
+    DataSize dstSize                     = DataSize::BITS_32;
+    volatile void* srcAddress            = nullptr;
+    volatile void* dstAddress            = nullptr;
+    volatile void* secondMemoryAddress   = nullptr;
+    uint16_t numberOfDataItems           = 0;
+    bool srcIncrement                    = false;
+    bool dstIncrement                    = false;
+    bool circularMode                    = false;
+    bool doubleBufferMode                = false;
+    bool enableHalfTransferInterrupt     = false;
+    bool enableTransferCompleteInterrupt = false;
+    bool enableTransferErrorInterrupt    = false;
+    bool enableFifoErrorInterrupt        = false;
+    bool enableDirectModeErrorInterrupt  = false;
+};
+
+// Forward declaration
+class DMAStream;
+
+class DMADriver
+{
+public:
+    void IRQhandleInterrupt(DMAStreamId id);
+
+    static DMADriver& instance();
+
+    bool tryChannel(DMAStreamId id);
+
+    DMAStream& acquireStream(DMAStreamId id);
+
+    void releaseStream(DMAStreamId id);
+
+private:
+    DMADriver();
+
+    void IRQwakeupThread(DMAStream* stream);
+
+    miosix::FastMutex mutex;
+    miosix::ConditionVariable cv;
+    std::map<DMAStreamId, DMAStream*> streams;
+
+public:
+    DMADriver(const DMADriver&)            = delete;
+    DMADriver& operator=(const DMADriver&) = delete;
+};
+
+class DMAStream
+{
+    friend DMADriver;
+
+public:
+    void setup(DMATransaction transaction);
+
+    void enable();
+
+    void disable();
+
+    void waitForHalfTransfer();
+
+    void waitForTransferComplete();
+
+    bool timedWaitForHalfTransfer(uint64_t timeout_ns);
+
+    bool timedWaitForTransferComplete(uint64_t timeout_ns);
+
+    void setHalfTransferCallback(std::function<void()> callback);
+
+    void resetHalfTransferCallback();
+
+    void setTransferCompleteCallback(std::function<void()> callback);
+
+    void resetTransferCompleteCallback();
+
+    void setErrorCallback(std::function<void()> callback);
+
+    void resetErrorCallback();
+
+    /**
+     * @brief Reads the current flags status.
+     *
+     * The values can be read with the get***FlagStatus functions.
+     */
+    void readFlags();
+
+    /**
+     * @brief Returns the last read status of the half transfer flag.
+     *
+     * TODO: Explain what this flag intails and what to do.
+     */
+    inline bool getHalfTransferFlagStatus() { return halfTransferFlag; }
+
+    /**
+     * @brief Returns the last read status of the transfer complete flag.
+     *
+     * TODO: Explain what this flag intails and what to do.
+     */
+    inline bool getTransferCompleteFlagStatus() { return transferCompleteFlag; }
+
+    /**
+     * @brief Returns the last read status of the transfer error flag.
+     *
+     * TODO: Explain what this flag intails and what to do.
+     */
+    inline bool getTransferErrorFlagStatus() { return transferErrorFlag; }
+
+    /**
+     * @brief Returns the last read status of the fifo error flag.
+     *
+     * TODO: Explain what this flag intails and what to do.
+     */
+    inline bool getFifoErrorFlagStatus() { return fifoErrorFlag; }
+
+    /**
+     * @brief Returns the last read status of the direct mode error flag.
+     *
+     * TODO: Explain what this flag intails and what to do.
+     */
+    inline bool getDirectModeErrorFlagStatus() { return directModeErrorFlag; }
+
+    /**
+     * @brief Returns the number of the buffer currently in use.
+     *
+     * @return 1 or 2 depending on the buffer currently in use.
+     */
+    int getCurrentBufferNumber();
+
+    inline void clearHalfTransferFlag()
+    {
+        *IFCR |= DMA_LIFCR_CHTIF0 << IFindex;
+    }
+
+    inline void clearTransferCompleteFlag()
+    {
+        *IFCR |= DMA_LIFCR_CTCIF0 << IFindex;
+    }
+
+    inline void clearTransferErrorFlag()
+    {
+        *IFCR |= DMA_LIFCR_CTEIF0 << IFindex;
+    }
+
+    inline void clearFifoErrorFlag() { *IFCR |= DMA_LIFCR_CFEIF0 << IFindex; }
+
+    inline void clearDirectModeErrorFlag()
+    {
+        *IFCR |= DMA_LIFCR_CDMEIF0 << IFindex;
+    }
+
+    inline void clearAllFlags()
+    {
+        *IFCR |= (DMA_LIFCR_CHTIF0 | DMA_LIFCR_CTCIF0 | DMA_LIFCR_CTEIF0 |
+                  DMA_LIFCR_CFEIF0 | DMA_LIFCR_CDMEIF0)
+                 << IFindex;
+    }
+
+private:
+    DMAStream(DMAStreamId id);
+
+    DMATransaction currentSetup;
+    miosix::Thread* waitingThread = nullptr;
+
+    // These flags are set by the interrupt routine and tells the user
+    // which event were triggered
+    bool halfTransferFlag     = false;
+    bool transferCompleteFlag = false;
+    bool transferErrorFlag    = false;
+    bool fifoErrorFlag        = false;
+    bool directModeErrorFlag  = false;
+
+    std::function<void()> halfTransferCallback;
+    std::function<void()> transferCompleteCallback;
+    std::function<void()> errorCallback;
+
+    DMAStreamId id;
+    IRQn_Type irqNumber;
+    DMA_Stream_TypeDef* registers;
+
+    volatile uint32_t* ISR;   ///< Interrupt status register
+    volatile uint32_t* IFCR;  ///< Interrupt flags clear register
+    int IFindex;              ///< Interrupt flags index
+
+    inline bool waitForInterruptEventImpl(
+        bool isInterruptEnabled, std::function<bool()> getEventStatus,
+        std::function<void()> clearEventStatus, bool& eventTriggered,
+        long long timeout_ns)
+    {
+        // Return value: true if the event was triggered, false if the timeout
+        // expired
+        bool result = false;
+
+        // If the interrupt is enabled we can just pause and wait for it.
+        // Otherwise we need to pool the flag.
+        if (isInterruptEnabled)
+        {
+            // Here we have 2 cases:
+            // - This function has been called after the interrupt fired. In
+            // this case the interrupt saves the flags status and we check them
+            // - The interrupt has not yet fired. We pause the thread and the
+            // interrupt will wake us up
+
+            if (eventTriggered)
+            {
+                result = true;
+            }
+            else
+            {
+                // Save the current thread pointer
+                waitingThread = miosix::Thread::getCurrentThread();
+
+                // Wait until the thread is woken up and the pointer is cleared
+                miosix::FastInterruptDisableLock dLock;
+                if (timeout_ns >= 0)
+                {
+                    do
+                    {
+                        // TODO: Wait Miosix 2.7 or do in another way?
+                        // if (miosix::Thread::IRQenableIrqAndTimedWait(
+                        //         dLock, timeout_ns + miosix::getTime()) ==
+                        //     miosix::TimedWaitResult::Timeout)
+                        // {
+                        //     result = false;
+
+                        //     // If the timeout expired we clear the thread
+                        //     // pointer so that the interrupt, if it will
+                        //     occur,
+                        //     // will not wake up the thread (and we can exit
+                        //     the
+                        //     // while loop)
+                        //     waitingThread = nullptr;
+                        // }
+                        // else
+                        // {
+                        //     result = true;
+                        // }
+                    } while (waitingThread);
+                }
+                else
+                {
+                    do
+                    {
+                        miosix::Thread::IRQenableIrqAndWait(dLock);
+                    } while (waitingThread);
+                    result = true;
+                }
+            }
+
+            // Before returning we need to clear the flags otherwise we could
+            // get misfires
+            eventTriggered = false;
+        }
+        else
+        {
+            // Pool the flag if the user did not enable the interrupt
+            if (timeout_ns >= 0)
+            {
+                result = timedPollingFlag(getEventStatus, timeout_ns);
+            }
+            else
+            {
+                while (!getEventStatus())
+                    ;
+                result = true;
+            }
+
+            if (result)
+            {
+                // Clear the flag
+                clearEventStatus();
+            }
+        }
+
+        return result;
+    }
+
+public:
+    DMAStream(const DMAStream&)            = delete;
+    DMAStream& operator=(const DMAStream&) = delete;
+};
+
+}  // namespace Boardcore
diff --git a/src/shared/utils/TimedPollingFlag.h b/src/shared/utils/TimedPollingFlag.h
new file mode 100644
index 0000000000000000000000000000000000000000..de0574e5bdf396b876efc7d00b7a7d72ff2ade9a
--- /dev/null
+++ b/src/shared/utils/TimedPollingFlag.h
@@ -0,0 +1,54 @@
+/* Copyright (c) 2023 Skyward Experimental Rocketry
+ * Author: Alberto Nidasio
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include <kernel/kernel.h>
+
+#include <functional>
+
+namespace Boardcore
+{
+
+/**
+ * @brief Pools a flag until it is set or the timeout is reached.
+ *
+ * @return true if the flag was set, false if the timeout was reached.
+ */
+inline bool timedPollingFlag(std::function<bool()> readFlag,
+                             uint64_t timeout_ns)
+{
+    // TODO: When Miosix 2.7 will be supported, change this with getTime() in ns
+    uint64_t start = miosix::getTick();
+
+    while (miosix::getTick() - start < timeout_ns * 1e6)
+    {
+        if (readFlag())
+        {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+}  // namespace Boardcore
\ No newline at end of file
diff --git a/src/tests/drivers/test-dma-mem-to-mem.cpp b/src/tests/drivers/test-dma-mem-to-mem.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..97f43983c3a013a1b47eed16cf1df4af9412960e
--- /dev/null
+++ b/src/tests/drivers/test-dma-mem-to-mem.cpp
@@ -0,0 +1,79 @@
+/* Copyright (c) 2023 Skyward Experimental Rocketry
+ * Authors: Alberto Nidasio
+ *
+ * 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 <drivers/dma/DMA.h>
+#include <miosix.h>
+#include <util/util.h>
+
+using namespace miosix;
+using namespace Boardcore;
+
+DMAStream &stream = DMADriver::instance().acquireStream(DMAStreamId::DMA2_Str0);
+
+void printBuffer(uint8_t *buffer, size_t size);
+
+int main()
+{
+    /**
+     * In this test we want to copy a buffer1 into buffer2 with the DMA.
+     */
+
+    uint8_t buffer1[8] = {1, 2, 3, 4, 5, 6, 7, 8};
+    uint8_t buffer2[8] = {0};
+
+    printf("Buffer 1:\n");
+    printBuffer(buffer1, sizeof(buffer1));
+    printf("Buffer 2:\n");
+    printBuffer(buffer2, sizeof(buffer2));
+
+    DMATransaction trn{
+        .direction         = DMATransaction::Direction::MEM_TO_MEM,
+        .srcSize           = DMATransaction::DataSize::BITS_8,
+        .dstSize           = DMATransaction::DataSize::BITS_8,
+        .srcAddress        = buffer1,
+        .dstAddress        = buffer2,
+        .numberOfDataItems = sizeof(buffer1),
+        .srcIncrement      = true,
+        .dstIncrement      = true,
+    };
+    stream.setup(trn);
+    stream.enable();
+    // stream.waitForTransferComplete();
+    delayMs(10);
+
+    delayMs(100);
+
+    printf("Buffer 1:\n");
+    printBuffer(buffer1, sizeof(buffer1));
+    printf("Buffer 2:\n");
+    printBuffer(buffer2, sizeof(buffer2));
+}
+
+void printBuffer(uint8_t *buffer, size_t size)
+{
+    for (size_t i = 0; i < size - 1; i++)
+    {
+        printf("%x,", buffer[i]);
+    }
+
+    printf("%x\n", buffer[size - 1]);
+}
\ No newline at end of file