diff --git a/Cargo.lock b/Cargo.lock
index 32abb9fcd73ca6960922090e2cc36e3edfda0889..8ad2739498c3ca05f968e9984d7b74778be6e17c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -83,18 +83,18 @@ dependencies = [
 
 [[package]]
 name = "cpufeatures"
-version = "0.2.2"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
+checksum = "dc948ebb96241bb40ab73effeb80d9f93afaad49359d159a5e61be51619fe813"
 dependencies = [
  "libc",
 ]
 
 [[package]]
 name = "crypto-common"
-version = "0.1.3"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
 dependencies = [
  "generic-array",
  "typenum",
@@ -113,9 +113,9 @@ dependencies = [
 
 [[package]]
 name = "generic-array"
-version = "0.14.5"
+version = "0.14.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
+checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
 dependencies = [
  "typenum",
  "version_check",
@@ -123,9 +123,9 @@ dependencies = [
 
 [[package]]
 name = "libc"
-version = "0.2.121"
+version = "0.2.132"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f"
+checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
 
 [[package]]
 name = "linked_list_allocator"
@@ -148,9 +148,9 @@ dependencies = [
 
 [[package]]
 name = "log"
-version = "0.4.16"
+version = "0.4.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
 dependencies = [
  "cfg-if",
 ]
@@ -163,9 +163,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
 
 [[package]]
 name = "raw-cpuid"
-version = "10.3.0"
+version = "10.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "738bc47119e3eeccc7e94c4a506901aea5e7b4944ecd0829cbebf4af04ceda12"
+checksum = "6aa2540135b6a94f74c7bc90ad4b794f822026a894f3d7bcd185c100d13d4ad6"
 dependencies = [
  "bitflags",
 ]
@@ -174,6 +174,7 @@ dependencies = [
 name = "redox_bootloader"
 version = "1.0.0"
 dependencies = [
+ "bitflags",
  "linked_list_allocator",
  "log",
  "redox_syscall",
@@ -192,9 +193,9 @@ checksum = "c4e4404b4e54e59e7bb5f5236b61d8e822c2a77b2e955be8072002ff7ff8d69c"
 
 [[package]]
 name = "redox_syscall"
-version = "0.2.13"
+version = "0.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
 dependencies = [
  "bitflags",
 ]
@@ -254,9 +255,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
 
 [[package]]
 name = "spin"
-version = "0.9.2"
+version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "511254be0c5bcf062b019a6c89c01a664aa359ded62f78aa72c6fc137c0590e5"
+checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09"
 dependencies = [
  "lock_api",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 34848d2a73f417cb79ca8775a2182b3661a1653d..6f9a00fe6a8c7e401eef02d04c8f1e821f2a9daf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,9 +15,10 @@ path = "src/main.rs"
 crate-type = ["staticlib"]
 
 [dependencies]
+bitflags = "1.3.2"
 linked_list_allocator = "0.9.1"
 log = "0.4.14"
-redox_syscall = "0.2.13"
+redox_syscall = "0.2.16"
 spin = "0.9.2"
 
 [dependencies.redoxfs]
@@ -35,3 +36,4 @@ x86 = "0.47.0"
 [features]
 default = []
 live = []
+serial_debug = []
diff --git a/src/main.rs b/src/main.rs
index 4bedad79dd14280fed91e5fac76165da8b862a6f..8135ad8dee50525264042a840f3f8601c695c05f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -35,6 +35,7 @@ mod os;
 
 mod arch;
 mod logger;
+mod serial_16550;
 
 const KIBI: usize = 1024;
 const MIBI: usize = KIBI * KIBI;
diff --git a/src/os/bios/macros.rs b/src/os/bios/macros.rs
index a8d28d471ca0c69046902180d6157c62cb9f885e..38f4a313ffe228fd9f7f9967be1f446bee4436c9 100644
--- a/src/os/bios/macros.rs
+++ b/src/os/bios/macros.rs
@@ -3,6 +3,10 @@
 macro_rules! print {
     ($($arg:tt)*) => ({
         use core::fmt::Write;
+        #[cfg(feature = "serial_debug")]
+        {
+            let _ = write!($crate::os::serial::COM1.lock(), $($arg)*);
+        }
         let _ = write!($crate::os::VGA.lock(), $($arg)*);
     });
 }
diff --git a/src/os/bios/mod.rs b/src/os/bios/mod.rs
index efc2c5ee11d0d3284eb0c1cbe463dc9334a03b55..9dd929607fac0f2d56b2774beca52ea9775ba585 100644
--- a/src/os/bios/mod.rs
+++ b/src/os/bios/mod.rs
@@ -22,6 +22,7 @@ mod macros;
 mod disk;
 mod memory_map;
 mod panic;
+pub(crate) mod serial;
 mod thunk;
 mod vbe;
 mod vga;
@@ -184,6 +185,13 @@ pub unsafe extern "C" fn start(
     thunk15: extern "C" fn(),
     thunk16: extern "C" fn(),
 ) -> ! {
+    #[cfg(feature = "serial_debug")]
+    {
+        let mut com1 = serial::COM1.lock();
+        com1.init();
+        com1.write(b"SERIAL\n");
+    }
+
     {
         // Make sure we are in mode 3 (80x25 text mode)
         let mut data = ThunkData::new();
diff --git a/src/os/bios/serial.rs b/src/os/bios/serial.rs
new file mode 100644
index 0000000000000000000000000000000000000000..60505a00d2d35d339365df202be58aec7f96644d
--- /dev/null
+++ b/src/os/bios/serial.rs
@@ -0,0 +1,9 @@
+use spin::Mutex;
+use syscall::{Io, Pio};
+
+use crate::serial_16550::SerialPort;
+
+pub static COM1: Mutex<SerialPort<Pio<u8>>> = Mutex::new(SerialPort::<Pio<u8>>::new(0x3F8));
+pub static COM2: Mutex<SerialPort<Pio<u8>>> = Mutex::new(SerialPort::<Pio<u8>>::new(0x2F8));
+pub static COM3: Mutex<SerialPort<Pio<u8>>> = Mutex::new(SerialPort::<Pio<u8>>::new(0x3E8));
+pub static COM4: Mutex<SerialPort<Pio<u8>>> = Mutex::new(SerialPort::<Pio<u8>>::new(0x2E8));
diff --git a/src/serial_16550.rs b/src/serial_16550.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c255abc18107c4e9c5d0640db89a79ef8bbc3f21
--- /dev/null
+++ b/src/serial_16550.rs
@@ -0,0 +1,140 @@
+use bitflags::bitflags;
+use core::convert::TryInto;
+use core::fmt;
+use syscall::io::{Io, Mmio, ReadOnly};
+#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+use syscall::io::Pio;
+
+bitflags! {
+    /// Interrupt enable flags
+    struct IntEnFlags: u8 {
+        const RECEIVED = 1;
+        const SENT = 1 << 1;
+        const ERRORED = 1 << 2;
+        const STATUS_CHANGE = 1 << 3;
+        // 4 to 7 are unused
+    }
+}
+
+bitflags! {
+    /// Line status flags
+    struct LineStsFlags: u8 {
+        const INPUT_FULL = 1;
+        // 1 to 4 unknown
+        const OUTPUT_EMPTY = 1 << 5;
+        // 6 and 7 unknown
+    }
+}
+
+#[allow(dead_code)]
+#[repr(packed)]
+pub struct SerialPort<T: Io> {
+    /// Data register, read to receive, write to send
+    data: T,
+    /// Interrupt enable
+    int_en: T,
+    /// FIFO control
+    fifo_ctrl: T,
+    /// Line control
+    line_ctrl: T,
+    /// Modem control
+    modem_ctrl: T,
+    /// Line status
+    line_sts: ReadOnly<T>,
+    /// Modem status
+    modem_sts: ReadOnly<T>,
+}
+
+#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+impl SerialPort<Pio<u8>> {
+    pub const fn new(base: u16) -> SerialPort<Pio<u8>> {
+        SerialPort {
+            data: Pio::new(base),
+            int_en: Pio::new(base + 1),
+            fifo_ctrl: Pio::new(base + 2),
+            line_ctrl: Pio::new(base + 3),
+            modem_ctrl: Pio::new(base + 4),
+            line_sts: ReadOnly::new(Pio::new(base + 5)),
+            modem_sts: ReadOnly::new(Pio::new(base + 6)),
+        }
+    }
+}
+
+impl SerialPort<Mmio<u32>> {
+    pub unsafe fn new(base: usize) -> &'static mut SerialPort<Mmio<u32>> {
+        &mut *(base as *mut Self)
+    }
+}
+
+impl<T: Io> SerialPort<T>
+where
+    T::Value: From<u8> + TryInto<u8>,
+{
+    pub fn init(&mut self) {
+        // Disable all interrupts
+        self.int_en.write(0x00.into());
+        // Use DLAB register
+        self.line_ctrl.write(0x80.into());
+        // Set divisor to 1 (115200 baud)
+        self.data.write(0x01.into());
+        self.int_en.write(0x00.into());
+        // 8 bits, no parity, one stop bit
+        self.line_ctrl.write(0x03.into());
+        // Enable FIFO, clear FIFO, 14-byte threshold
+        self.fifo_ctrl.write(0xC7.into());
+        // Enable IRQs and set RTS/DSR
+        self.modem_ctrl.write(0x0B.into());
+    }
+
+    fn line_sts(&self) -> LineStsFlags {
+        LineStsFlags::from_bits_truncate(
+            (self.line_sts.read() & 0xFF.into())
+                .try_into()
+                .unwrap_or(0),
+        )
+    }
+
+    pub fn receive(&mut self) -> Option<u8> {
+        if self.line_sts().contains(LineStsFlags::INPUT_FULL) {
+            Some(
+                (self.data.read() & 0xFF.into())
+                    .try_into()
+                    .unwrap_or(0),
+            )
+        } else {
+            None
+        }
+    }
+
+    pub fn send(&mut self, data: u8) {
+        while !self.line_sts().contains(LineStsFlags::OUTPUT_EMPTY) {}
+        self.data.write(data.into())
+    }
+
+    pub fn write(&mut self, buf: &[u8]) {
+        for &b in buf {
+            match b {
+                8 | 0x7F => {
+                    self.send(8);
+                    self.send(b' ');
+                    self.send(8);
+                }
+                b'\n' => {
+                    self.send(b'\r');
+                    self.send(b'\n');
+                }
+                _ => {
+                    self.send(b);
+                }
+            }
+        }
+    }
+}
+
+impl<T: Io> fmt::Write for SerialPort<T>
+where T::Value: From<u8> + TryInto<u8> {
+    fn write_str(&mut self, s: &str) -> fmt::Result {
+        self.write(s.as_bytes());
+        Ok(())
+    }
+}
\ No newline at end of file