diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8c53e4ac91074b387b34e440adc7ef3bb96f1f0e..b167ce8a392b8c65297607cff8beefab087e9a35 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -215,6 +215,12 @@ add_executable(test-sx1278-mavlink
 )
 sbs_target(test-sx1278-mavlink stm32f407vg_stm32f4discovery)
 
+add_executable(test-sx1278-serial
+    src/tests/drivers/sx1278/test-sx1278-serial.cpp
+    src/tests/drivers/sx1278/test-sx1278-core.cpp
+)
+sbs_target(test-sx1278-serial stm32f407vg_stm32f4discovery)
+
 #-----------------------------------------------------------------------------#
 #                               Tests - Sensors                               #
 #-----------------------------------------------------------------------------#
diff --git a/src/tests/drivers/sx1278/test-sx1278-serial.cpp b/src/tests/drivers/sx1278/test-sx1278-serial.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..062eb72080c3854a538504825574b32562a88986
--- /dev/null
+++ b/src/tests/drivers/sx1278/test-sx1278-serial.cpp
@@ -0,0 +1,134 @@
+/* Copyright (c) 2021 Skyward Experimental Rocketry
+ * Author: Davide Mor
+ *
+ * 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/SX1278/SX1278.h>
+#include <drivers/interrupt/external_interrupts.h>
+#include <filesystem/console/console_device.h>
+
+#include <thread>
+
+#include "test-sx1278-core.h"
+
+using namespace Boardcore;
+using namespace miosix;
+
+SPIBus bus(SPI3);
+
+GpioPin sck(GPIOC_BASE, 10);
+GpioPin miso(GPIOC_BASE, 11);
+GpioPin mosi(GPIOC_BASE, 12);
+GpioPin cs(GPIOA_BASE, 1);
+GpioPin dio(GPIOC_BASE, 15);
+
+SX1278* sx1278 = nullptr;
+
+void __attribute__((used)) EXTI15_IRQHandlerImpl()
+{
+    if (sx1278)
+        sx1278->handleDioIRQ();
+}
+
+/// Initialize stm32f407g board.
+void initBoard()
+{
+    {
+        miosix::FastInterruptDisableLock dLock;
+
+        // Enable SPI3
+        RCC->APB1ENR |= RCC_APB1ENR_SPI3EN;
+        RCC_SYNC();
+
+        // Setup SPI pins
+        sck.mode(miosix::Mode::ALTERNATE);
+        sck.alternateFunction(6);
+        miso.mode(miosix::Mode::ALTERNATE);
+        miso.alternateFunction(6);
+        mosi.mode(miosix::Mode::ALTERNATE);
+        mosi.alternateFunction(6);
+
+        cs.mode(miosix::Mode::OUTPUT);
+        dio.mode(miosix::Mode::INPUT);
+    }
+
+    cs.high();
+    enableExternalInterrupt(dio.getPort(), dio.getNumber(),
+                            InterruptTrigger::RISING_EDGE);
+}
+
+void recvLoop()
+{
+    uint8_t msg[256];
+    auto console = miosix::DefaultConsole::instance().get();
+
+    while (1)
+    {
+        int len = sx1278->receive(msg, sizeof(msg));
+        if (len > 0)
+        {
+            console->writeBlock(msg, len, 0);
+            // TODO: Flushing?
+        }
+    }
+}
+
+void sendLoop()
+{
+    uint8_t msg[256];
+    auto console = miosix::DefaultConsole::instance().get();
+
+    while (1)
+    {
+        int len = console->readBlock(msg, sizeof(msg), 0);
+        if (len > 0)
+        {
+            sx1278->send(msg, len);
+        }
+    }
+}
+
+int main()
+{
+    initBoard();
+
+    SX1278::Config config;
+    SX1278::Error err;
+
+    sx1278 = new SX1278(bus, cs);
+
+    printf("\n[sx1278] Configuring sx1278...\n");
+    printConfig(config);
+    if ((err = sx1278->init(config)) != SX1278::Error::NONE)
+    {
+        printf("[sx1278] sx1278->init error: %s\n", stringFromErr(err));
+        return -1;
+    }
+
+    std::thread recv([]() { recvLoop(); });
+    std::thread send([]() { sendLoop(); });
+
+    printf("\n[sx1278] Initialization complete!\n");
+
+    while (1)
+        miosix::Thread::wait();
+
+    return 0;
+}