diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4932f7f1f3a25e1726bdec45d8da6f5e6056bdb8..0bb0627ccfb530d355b92a2a55717515bbfef075 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added
 
-*
+* Support for serial data transfer - [#19](https://gitlab.stage.hive.pt/joamag/boytacean/-/issues/19)
+* Support for printing of images using Printer emulation - [#19](https://gitlab.stage.hive.pt/joamag/boytacean/-/issues/19)
 
 ### Changed
 
diff --git a/README.md b/README.md
index 5506448669d561bbf8d6f9487df007f96683e5ef..e0b5316e569c16368e3752f378ab355ee2143cdc 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,8 @@ A Game Boy emulator that is written in Rust 🦀.
 * Simple navigable source-code
 * Web and SDL front-ends
 * Audio, with a pretty accurate APU
+* Serial Data Transfer (Link Cable) support
+* Game Boy Printer emulation
 * Support for multiple MBCs: MBC1, MBC2, MBC3, and MBC5
 * Variable CPU clock speed
 * Accurate PPU - passes [dmg-acid2](https://github.com/mattcurrie/dmg-acid2) tests
diff --git a/doc/inspiration.md b/doc/inspiration.md
index 2ca9d3f86d306b9e90d047429112d68ab2f12283..b097ac1a6a98ae3624712ddae1b57f93dbf7624e 100644
--- a/doc/inspiration.md
+++ b/doc/inspiration.md
@@ -26,6 +26,7 @@
 * [GitHub - simias/gb-rs (Rust)](https://github.com/simias/gb-rs)
 * [GitHub - RubenG123/frosty (Rust)](https://github.com/RubenG123/frosty)
 * [GitHub - calvinbaart/gameboy (TypeScript)](https://github.com/calvinbaart/gameboy)
+* [GitHub - HFO4/gameboy.live (Go)](https://github.com/HFO4/gameboy.live)
 * [GitHub - djhworld/gomeboycolor-wasm (Go)](https://github.com/djhworld/gomeboycolor-wasm)
 * [GitHub - torch2424/wasmboy (WASM)](https://github.com/torch2424/wasmboy)
 
@@ -33,10 +34,15 @@
 
 * [YouTube - The Ultimate Game Boy Talk (33c3)](https://www.youtube.com/watch?v=HyzD8pNlpwI)
 
+## Peripherals
+
+* [In Depth: The Game Boy Printer](https://shonumi.github.io/articles/art2.html)
+
 ## Other
 
 * [GitHub - gbdev/awesome-gbdev](https://github.com/gbdev/awesome-gbdev)
 * [GitHub - Hacktix/Bootix](https://github.com/Hacktix/Bootix)
+* [GitHub - gbdk-2020/gbdk-2020 (Best GB C SDK)](https://github.com/gbdk-2020/gbdk-2020)
 * [GitHub - fcambus/jsemu (Emulators written in JavaScript)](https://github.com/fcambus/jsemu)
 * [Gameboy Doctor: debug and fix your gameboy emulator](https://robertheaton.com/gameboy-doctor)
 * [Game Boy Boot ROMs](https://gbdev.gg8.se/files/roms/bootroms)
diff --git a/frontends/sdl/Cargo.toml b/frontends/sdl/Cargo.toml
index 4c68892c503034d1a0266613cd999d9180cba59b..5a68376361c2cd4fe2ec12e9279e6ab2f44b97de 100644
--- a/frontends/sdl/Cargo.toml
+++ b/frontends/sdl/Cargo.toml
@@ -7,9 +7,18 @@ license = "Apache-2.0"
 keywords = ["gameboy", "emulator", "rust", "sdl"]
 edition = "2018"
 
+[features]
+debug = ["boytacean/debug"]
+
 [dependencies.boytacean]
 path = "../.."
 
+[dependencies.image]
+version = "0.24"
+
+[dependencies.chrono]
+version = "0.4"
+
 [dependencies.sdl2]
 version = "0.35"
 features = ["ttf", "image", "gfx", "mixer", "static-link", "use-vcpkg"]
diff --git a/frontends/sdl/README.md b/frontends/sdl/README.md
index 75c153a41485dcef6f204b8b9d1450bde176794e..163d1b2e9ff09f07c11d104e7a28e0beea1101c3 100644
--- a/frontends/sdl/README.md
+++ b/frontends/sdl/README.md
@@ -23,3 +23,9 @@ To reload the code continuously use the cargo watch tool:
 cargo install cargo-watch
 cargo watch -x run
 ```
+
+There are some feature flags that control the verbosity of the emulator to run in debug mode use:
+
+```bash
+cargo run --features debug
+```
diff --git a/frontends/sdl/src/main.rs b/frontends/sdl/src/main.rs
index 9c6f6d0a0f0bc90136b6f4152f7a676c45200d24..f6d1fc838e0a703f84ea7d6f80e4d2f0b564db8c 100644
--- a/frontends/sdl/src/main.rs
+++ b/frontends/sdl/src/main.rs
@@ -6,13 +6,16 @@ pub mod graphics;
 
 use audio::Audio;
 use boytacean::{
+    devices::printer::PrinterDevice,
     gb::{AudioProvider, GameBoy},
     pad::PadKey,
     ppu::{PaletteInfo, PpuMode, DISPLAY_HEIGHT, DISPLAY_WIDTH},
 };
+use chrono::Utc;
 use graphics::{surface_from_bytes, Graphics};
+use image::ColorType;
 use sdl2::{event::Event, keyboard::Keycode, pixels::PixelFormatEnum, Sdl};
-use std::{cmp::max, time::SystemTime};
+use std::{cmp::max, path::Path, time::SystemTime};
 
 /// The scale at which the screen is going to be drawn
 /// meaning the ratio between Game Boy resolution and
@@ -20,11 +23,11 @@ use std::{cmp::max, time::SystemTime};
 const SCREEN_SCALE: f32 = 2.0;
 
 /// The base title to be used in the window.
-static TITLE: &str = "Boytacean";
+const TITLE: &str = "Boytacean";
 
 /// Base audio volume to be used as the basis of the
 /// amplification level of the volume
-static VOLUME: f32 = 64.0;
+const VOLUME: f32 = 64.0;
 
 pub struct Benchmark {
     count: usize,
@@ -209,7 +212,7 @@ impl Emulator {
     }
 
     pub fn toggle_audio(&mut self) {
-        let apu_enabled = self.system.get_apu_enabled();
+        let apu_enabled = self.system.apu_enabled();
         self.system.set_apu_enabled(!apu_enabled);
     }
 
@@ -444,6 +447,19 @@ fn main() {
     // creates a new Game Boy instance and loads both the boot ROM
     // and the initial game ROM to "start the engine"
     let mut game_boy = GameBoy::new();
+    let mut printer = Box::<PrinterDevice>::default();
+    printer.set_callback(|image_buffer| {
+        let file_name = format!("printer-{}.png", Utc::now().format("%Y%m%d-%H%M%S"));
+        image::save_buffer(
+            Path::new(&file_name),
+            image_buffer,
+            160,
+            (image_buffer.len() / 4 / 160) as u32,
+            ColorType::Rgba8,
+        )
+        .unwrap();
+    });
+    game_boy.attach_serial(printer);
     game_boy.load_boot_default();
 
     // creates a new generic emulator structure then starts
@@ -451,7 +467,7 @@ fn main() {
     // ROM file and starts running it
     let mut emulator = Emulator::new(game_boy);
     emulator.start(SCREEN_SCALE);
-    emulator.load_rom(Some("../../res/roms/demo/pocket.gb"));
+    emulator.load_rom(Some("../../res/roms/test/gbprinter.gb"));
     emulator.toggle_palette();
     emulator.run();
 }
diff --git a/res/roms/test/gbprinter.gb b/res/roms/test/gbprinter.gb
new file mode 100644
index 0000000000000000000000000000000000000000..aa8c6d2b565f7808ddf0eec31765436bc2dca61c
Binary files /dev/null and b/res/roms/test/gbprinter.gb differ
diff --git a/src/cpu.rs b/src/cpu.rs
index 9be9b16944dfd31c5fd9c66401c6058f64551e71..ba115c2e54d6ffa5077b0db04d48539d976701d7 100644
--- a/src/cpu.rs
+++ b/src/cpu.rs
@@ -7,6 +7,7 @@ use crate::{
     mmu::Mmu,
     pad::Pad,
     ppu::Ppu,
+    serial::Serial,
     timer::Timer,
 };
 
@@ -191,6 +192,26 @@ impl Cpu {
                 return 24;
             }
             // @TODO aggregate the handling of these interrupts
+            else if (self.mmu.ie & 0x08 == 0x08) && self.mmu.serial().int_serial() {
+                debugln!("Going to run Serial interrupt handler (0x58)");
+
+                self.disable_int();
+                self.push_word(pc);
+                self.pc = 0x58;
+
+                // acknowledges that the serial interrupt has been
+                // properly handled
+                self.mmu.serial().ack_serial();
+
+                // in case the CPU is currently halted waiting
+                // for an interrupt, releases it
+                if self.halted {
+                    self.halted = false;
+                }
+
+                return 24;
+            }
+            // @TODO aggregate the handling of these interrupts
             else if (self.mmu.ie & 0x10 == 0x10) && self.mmu.pad().int_pad() {
                 debugln!("Going to run JoyPad interrupt handler (0x60)");
 
@@ -297,6 +318,11 @@ impl Cpu {
         self.mmu().timer()
     }
 
+    #[inline(always)]
+    pub fn serial(&mut self) -> &mut Serial {
+        self.mmu().serial()
+    }
+
     #[inline(always)]
     pub fn halted(&self) -> bool {
         self.halted
diff --git a/src/devices/mod.rs b/src/devices/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ada19f9138884470af7746f6fa424080b2601f8a
--- /dev/null
+++ b/src/devices/mod.rs
@@ -0,0 +1,2 @@
+pub mod printer;
+pub mod stdout;
diff --git a/src/devices/printer.rs b/src/devices/printer.rs
new file mode 100644
index 0000000000000000000000000000000000000000..23c46b4e0c8b2d03d85826217f634289934d03f1
--- /dev/null
+++ b/src/devices/printer.rs
@@ -0,0 +1,321 @@
+use std::fmt::{self, Display, Formatter};
+
+use crate::{ppu::PaletteAlpha, serial::SerialDevice, warnln};
+
+const PRINTER_PALETTE: PaletteAlpha = [
+    [0xff, 0xff, 0xff, 0xff],
+    [0xaa, 0xaa, 0xaa, 0xff],
+    [0x55, 0x55, 0x55, 0xff],
+    [0x00, 0x00, 0x00, 0xff],
+];
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+enum PrinterState {
+    MagicBytes1 = 0x00,
+    MagicBytes2 = 0x01,
+    Identification = 0x02,
+    Compression = 0x03,
+    LengthLow = 0x04,
+    LengthHigh = 0x05,
+    Data = 0x06,
+    ChecksumLow = 0x07,
+    ChecksumHigh = 0x08,
+    KeepAlive = 0x09,
+    Status = 0x0a,
+    Other = 0xff,
+}
+
+impl PrinterState {
+    pub fn description(&self) -> &'static str {
+        match self {
+            PrinterState::MagicBytes1 => "Magic Bytes 1",
+            PrinterState::MagicBytes2 => "Magic Bytes 2",
+            PrinterState::Identification => "Identification",
+            PrinterState::Compression => "Compression",
+            PrinterState::LengthLow => "Length Low",
+            PrinterState::LengthHigh => "Length High",
+            PrinterState::Data => "Data",
+            PrinterState::ChecksumLow => "Checksum Low",
+            PrinterState::ChecksumHigh => "Checksum High",
+            PrinterState::KeepAlive => "Keep Alive",
+            PrinterState::Status => "Status",
+            PrinterState::Other => "Other",
+        }
+    }
+
+    fn from_u8(value: u8) -> Self {
+        match value {
+            0x00 => PrinterState::MagicBytes1,
+            0x01 => PrinterState::MagicBytes2,
+            0x02 => PrinterState::Identification,
+            0x03 => PrinterState::Compression,
+            0x04 => PrinterState::LengthLow,
+            0x05 => PrinterState::LengthHigh,
+            0x06 => PrinterState::Data,
+            0x07 => PrinterState::ChecksumLow,
+            0x08 => PrinterState::ChecksumHigh,
+            0x09 => PrinterState::KeepAlive,
+            0x0a => PrinterState::Status,
+            _ => PrinterState::Other,
+        }
+    }
+}
+
+impl Display for PrinterState {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.description())
+    }
+}
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+enum PrinterCommand {
+    Init = 0x01,
+    Print = 0x02,
+    Data = 0x04,
+    Status = 0x0f,
+    Other = 0xff,
+}
+
+impl PrinterCommand {
+    pub fn description(&self) -> &'static str {
+        match self {
+            PrinterCommand::Init => "Init",
+            PrinterCommand::Print => "Print",
+            PrinterCommand::Data => "Data",
+            PrinterCommand::Status => "Status",
+            PrinterCommand::Other => "Other",
+        }
+    }
+
+    fn from_u8(value: u8) -> Self {
+        match value {
+            0x01 => PrinterCommand::Init,
+            0x02 => PrinterCommand::Print,
+            0x04 => PrinterCommand::Data,
+            0x0f => PrinterCommand::Status,
+            _ => PrinterCommand::Other,
+        }
+    }
+}
+
+impl Display for PrinterCommand {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.description())
+    }
+}
+
+pub struct PrinterDevice {
+    state: PrinterState,
+    command: PrinterCommand,
+    compression: bool,
+    command_length: u16,
+    length_left: u16,
+    checksum: u16,
+    status: u8,
+    byte_out: u8,
+    data: [u8; 0x280],
+    image: [u8; 160 * 200],
+    image_offset: u16,
+    callback: fn(image_buffer: &Vec<u8>),
+}
+
+impl PrinterDevice {
+    pub fn new() -> Self {
+        Self {
+            state: PrinterState::MagicBytes1,
+            command: PrinterCommand::Other,
+            compression: false,
+            command_length: 0,
+            length_left: 0,
+            checksum: 0x0,
+            status: 0x0,
+            byte_out: 0x0,
+            data: [0x00; 0x280],
+            image: [0x00; 160 * 200],
+            image_offset: 0,
+            callback: |_| {},
+        }
+    }
+
+    pub fn reset(&mut self) {
+        self.state = PrinterState::MagicBytes1;
+        self.command = PrinterCommand::Other;
+        self.compression = false;
+        self.command_length = 0;
+        self.length_left = 0;
+        self.checksum = 0x0;
+        self.status = 0x0;
+        self.byte_out = 0x0;
+        self.data = [0x00; 0x280];
+        self.image = [0x00; 160 * 200];
+        self.image_offset = 0;
+    }
+
+    pub fn set_callback(&mut self, callback: fn(image_buffer: &Vec<u8>)) {
+        self.callback = callback;
+    }
+
+    fn run_command(&mut self, command: PrinterCommand) {
+        match command {
+            PrinterCommand::Init => {
+                self.status = 0x00;
+                self.byte_out = self.status;
+                self.image_offset = 0;
+            }
+            PrinterCommand::Print => {
+                let mut image_buffer = Vec::new();
+                let palette_index = self.data[2];
+
+                for index in 0..self.image_offset {
+                    let value = self.image[index as usize];
+                    let pixel_offset = (palette_index >> (value << 1)) & 0x03;
+                    let pixel = PRINTER_PALETTE[pixel_offset as usize];
+                    image_buffer.push(pixel[0]);
+                    image_buffer.push(pixel[1]);
+                    image_buffer.push(pixel[2]);
+                    image_buffer.push(pixel[3]);
+                }
+
+                (self.callback)(&image_buffer);
+
+                self.byte_out = self.status;
+                self.status = 0x06;
+            }
+            PrinterCommand::Data => {
+                if self.command_length == 0x280 {
+                    self.flush_image();
+                }
+                // in case the command is of size 0 we assume this is
+                // an EOF and we ignore this data operation
+                else if self.command_length == 0x0 {
+                } else {
+                    warnln!(
+                        "Printer: Wrong size for data: {:04x} bytes",
+                        self.command_length
+                    );
+                }
+                self.status = 0x08;
+                self.byte_out = self.status;
+            }
+            PrinterCommand::Status => {
+                self.byte_out = self.status;
+
+                // in case the current status is printing let's
+                // mark it as done, resetting the status back to
+                // the original value
+                if self.status == 0x06 {
+                    self.status = 0x00;
+                }
+            }
+            PrinterCommand::Other => {
+                warnln!("Printer: Invalid command: {:02x}", self.state as u8);
+            }
+        }
+    }
+
+    fn flush_image(&mut self) {
+        // sets the initial value of the index that will point to
+        // the data that is going to be copied to the image buffer
+        let mut index = 0;
+
+        // iterates over the two rows that are going to be printed
+        // keep in mind that the printer only allows 2 lines at each
+        // time to be printed
+        for _ in 0..2 {
+            for col in 0..20 {
+                for y in 0..8 {
+                    let mut first = self.data[index];
+                    let mut second = self.data[index + 1];
+                    for x in 0..8 {
+                        let offset = self.image_offset as usize + (col * 8) + (y * 160) + x;
+                        self.image[offset] = (first >> 7) | ((second >> 6) & 0x02);
+
+                        first <<= 1;
+                        second <<= 1;
+                    }
+                    index += 2
+                }
+            }
+
+            self.image_offset += 160 * 8;
+        }
+    }
+}
+
+impl SerialDevice for PrinterDevice {
+    fn send(&mut self) -> u8 {
+        self.byte_out
+    }
+
+    fn receive(&mut self, byte: u8) {
+        self.byte_out = 0x00;
+
+        match self.state {
+            PrinterState::MagicBytes1 => {
+                if byte != 0x88 {
+                    warnln!("Printer: Invalid magic byte 1: {:02x}", byte);
+                    return;
+                }
+                self.command = PrinterCommand::Other;
+                self.command_length = 0;
+            }
+            PrinterState::MagicBytes2 => {
+                if byte != 0x33 {
+                    if byte != 0x88 {
+                        self.state = PrinterState::MagicBytes1;
+                    }
+                    warnln!("Printer: Invalid magic byte 2: {:02x}", byte);
+                    return;
+                }
+            }
+            PrinterState::Identification => self.command = PrinterCommand::from_u8(byte),
+            PrinterState::Compression => {
+                self.compression = byte & 0x01 == 0x01;
+                if self.compression {
+                    warnln!("Printer: Using compressed data, currently unsupported");
+                }
+            }
+            PrinterState::LengthLow => self.length_left = byte as u16,
+            PrinterState::LengthHigh => self.length_left |= (byte as u16) << 8,
+            PrinterState::Data => {
+                self.data[self.command_length as usize] = byte;
+                self.command_length += 1;
+                self.length_left -= 1;
+            }
+            PrinterState::ChecksumLow => self.checksum = byte as u16,
+            PrinterState::ChecksumHigh => {
+                self.checksum |= (byte as u16) << 8;
+                self.byte_out = 0x81;
+            }
+            PrinterState::KeepAlive => {
+                self.run_command(self.command);
+            }
+            PrinterState::Status => {
+                self.state = PrinterState::MagicBytes1;
+                return;
+            }
+            PrinterState::Other => {
+                warnln!("Printer: Invalid state: {:02x}", self.state as u8);
+                return;
+            }
+        }
+
+        if self.state != PrinterState::Data {
+            self.state = PrinterState::from_u8(self.state as u8 + 1);
+        }
+
+        if self.state == PrinterState::Data && self.length_left == 0 {
+            self.state = PrinterState::from_u8(self.state as u8 + 1);
+        }
+    }
+
+    fn allow_slave(&self) -> bool {
+        false
+    }
+}
+
+impl Default for PrinterDevice {
+    fn default() -> Self {
+        Self::new()
+    }
+}
diff --git a/src/devices/stdout.rs b/src/devices/stdout.rs
new file mode 100644
index 0000000000000000000000000000000000000000..a2f7528c0ba72eef722536923dc8f5c6651fe722
--- /dev/null
+++ b/src/devices/stdout.rs
@@ -0,0 +1,36 @@
+use std::io::{stdout, Write};
+
+use crate::serial::SerialDevice;
+
+pub struct StdoutDevice {
+    flush: bool,
+}
+
+impl StdoutDevice {
+    pub fn new(flush: bool) -> Self {
+        Self { flush }
+    }
+}
+
+impl SerialDevice for StdoutDevice {
+    fn send(&mut self) -> u8 {
+        0xff
+    }
+
+    fn receive(&mut self, byte: u8) {
+        print!("{}", byte as char);
+        if self.flush {
+            stdout().flush().unwrap();
+        }
+    }
+
+    fn allow_slave(&self) -> bool {
+        false
+    }
+}
+
+impl Default for StdoutDevice {
+    fn default() -> Self {
+        Self::new(true)
+    }
+}
diff --git a/src/gb.rs b/src/gb.rs
index 5fc389dc5f42b3f1f09b86c1a0e84c4e67b24209..af0735a72d6f91991b4329749bd1564fc8767b50 100644
--- a/src/gb.rs
+++ b/src/gb.rs
@@ -2,11 +2,13 @@ use crate::{
     apu::Apu,
     cpu::Cpu,
     data::{BootRom, CGB_BOOT, DMG_BOOT, DMG_BOOTIX, MGB_BOOTIX, SGB_BOOT},
+    devices::{printer::PrinterDevice, stdout::StdoutDevice},
     gen::{COMPILATION_DATE, COMPILATION_TIME, COMPILER, COMPILER_VERSION},
     mmu::Mmu,
     pad::{Pad, PadKey},
     ppu::{Ppu, PpuMode, Tile, FRAME_BUFFER_SIZE},
     rom::Cartridge,
+    serial::{Serial, SerialDevice},
     timer::Timer,
     util::read_file,
 };
@@ -44,6 +46,9 @@ pub struct GameBoy {
     /// If the timer is enabled, it will be clocked.
     timer_enabled: bool,
 
+    /// If the serial is enabled, it will be clocked.
+    serial_enabled: bool,
+
     /// The current frequency at which the Game Boy
     /// emulator is being handled. This is a "hint" that
     /// may help components to adjust their internal
@@ -86,13 +91,15 @@ impl GameBoy {
         let apu = Apu::default();
         let pad = Pad::default();
         let timer = Timer::default();
-        let mmu = Mmu::new(ppu, apu, pad, timer);
+        let serial = Serial::default();
+        let mmu = Mmu::new(ppu, apu, pad, timer, serial);
         let cpu = Cpu::new(mmu);
         Self {
             cpu,
             ppu_enabled: true,
             apu_enabled: true,
             timer_enabled: true,
+            serial_enabled: true,
             clock_freq: GameBoy::CPU_FREQ,
         }
     }
@@ -100,6 +107,8 @@ impl GameBoy {
     pub fn reset(&mut self) {
         self.ppu().reset();
         self.apu().reset();
+        self.timer().reset();
+        self.serial().reset();
         self.mmu().reset();
         self.cpu.reset();
     }
@@ -115,6 +124,9 @@ impl GameBoy {
         if self.timer_enabled {
             self.timer_clock(cycles);
         }
+        if self.serial_enabled {
+            self.serial_clock(cycles);
+        }
         cycles
     }
 
@@ -142,6 +154,10 @@ impl GameBoy {
         self.timer().clock(cycles)
     }
 
+    pub fn serial_clock(&mut self, cycles: u8) {
+        self.serial().clock(cycles)
+    }
+
     pub fn ppu_ly(&mut self) -> u8 {
         self.ppu().ly()
     }
@@ -325,6 +341,14 @@ impl GameBoy {
         self.timer_enabled = value;
     }
 
+    pub fn serial_enabled(&self) -> bool {
+        self.serial_enabled
+    }
+
+    pub fn set_serial_enabled(&mut self, value: bool) {
+        self.serial_enabled = value;
+    }
+
     pub fn clock_freq(&self) -> u32 {
         self.clock_freq
     }
@@ -333,6 +357,14 @@ impl GameBoy {
         self.clock_freq = value;
         self.apu().set_clock_freq(value);
     }
+
+    pub fn attach_stdout_serial(&mut self) {
+        self.attach_serial(Box::<StdoutDevice>::default());
+    }
+
+    pub fn attach_printer_serial(&mut self) {
+        self.attach_serial(Box::<PrinterDevice>::default());
+    }
 }
 
 /// Gameboy implementations that are meant with performance
@@ -378,6 +410,10 @@ impl GameBoy {
         self.cpu.timer()
     }
 
+    pub fn serial(&mut self) -> &mut Serial {
+        self.cpu.serial()
+    }
+
     pub fn frame_buffer(&mut self) -> &[u8; FRAME_BUFFER_SIZE] {
         &(self.ppu().frame_buffer)
     }
@@ -423,6 +459,10 @@ impl GameBoy {
         let data = read_file(path);
         self.load_rom(&data)
     }
+
+    pub fn attach_serial(&mut self, device: Box<dyn SerialDevice>) {
+        self.serial().set_device(device);
+    }
 }
 
 #[cfg(feature = "wasm")]
diff --git a/src/lib.rs b/src/lib.rs
index 7decfab2b80948c67ae0c8c3765191dda99b7743..80f4f5b8e995c1081e8319a0c9b396a73503375a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -3,6 +3,7 @@
 pub mod apu;
 pub mod cpu;
 pub mod data;
+pub mod devices;
 pub mod gb;
 pub mod gen;
 pub mod inst;
@@ -11,5 +12,6 @@ pub mod mmu;
 pub mod pad;
 pub mod ppu;
 pub mod rom;
+pub mod serial;
 pub mod timer;
 pub mod util;
diff --git a/src/mmu.rs b/src/mmu.rs
index bc4a9ddd43ca2d8a814f4d0af2c09bce448d2a6d..f57748c29388a5f66619880ef89397db8bb94f99 100644
--- a/src/mmu.rs
+++ b/src/mmu.rs
@@ -1,4 +1,4 @@
-use crate::{apu::Apu, debugln, pad::Pad, ppu::Ppu, rom::Cartridge, timer::Timer};
+use crate::{apu::Apu, debugln, pad::Pad, ppu::Ppu, rom::Cartridge, serial::Serial, timer::Timer};
 
 pub const BOOT_SIZE: usize = 2304;
 pub const RAM_SIZE: usize = 8192;
@@ -26,6 +26,10 @@ pub struct Mmu {
     /// that is memory mapped.
     timer: Timer,
 
+    /// The serial data transfer controller to be used to control the
+    /// link cable connection, this component is memory mapped.
+    serial: Serial,
+
     /// The cartridge ROM that is currently loaded into the system,
     /// going to be used to access ROM and external RAM banks.
     rom: Cartridge,
@@ -49,12 +53,13 @@ pub struct Mmu {
 }
 
 impl Mmu {
-    pub fn new(ppu: Ppu, apu: Apu, pad: Pad, timer: Timer) -> Self {
+    pub fn new(ppu: Ppu, apu: Apu, pad: Pad, timer: Timer, serial: Serial) -> Self {
         Self {
             ppu,
             apu,
             pad,
             timer,
+            serial,
             rom: Cartridge::new(),
             boot_active: true,
             boot: [0u8; BOOT_SIZE],
@@ -91,6 +96,10 @@ impl Mmu {
         &mut self.timer
     }
 
+    pub fn serial(&mut self) -> &mut Serial {
+        &mut self.serial
+    }
+
     pub fn boot_active(&self) -> bool {
         self.boot_active
     }
@@ -141,6 +150,9 @@ impl Mmu {
                 | 0xa00 | 0xb00 | 0xc00 | 0xd00 => self.ram[(addr & 0x1fff) as usize],
                 0xe00 => self.ppu.read(addr),
                 0xf00 => match addr & 0x00ff {
+                    // 0xFF01-0xFF02 - Serial data transfer
+                    0x01..=0x02 => self.serial.read(addr),
+
                     // 0xFF0F — IF: Interrupt flag
                     0x0f =>
                     {
@@ -148,6 +160,7 @@ impl Mmu {
                         (if self.ppu.int_vblank() { 0x01 } else { 0x00 }
                             | if self.ppu.int_stat() { 0x02 } else { 0x00 }
                             | if self.timer.int_tima() { 0x04 } else { 0x00 }
+                            | if self.serial.int_serial() { 0x08 } else { 0x00 }
                             | if self.pad.int_pad() { 0x10 } else { 0x00 })
                     }
 
@@ -216,11 +229,15 @@ impl Mmu {
                 }
                 0xe00 => self.ppu.write(addr, value),
                 0xf00 => match addr & 0x00ff {
+                    // 0xFF01-0xFF02 - Serial data transfer
+                    0x01..=0x02 => self.serial.write(addr, value),
+
                     // 0xFF0F — IF: Interrupt flag
                     0x0f => {
                         self.ppu.set_int_vblank(value & 0x01 == 0x01);
                         self.ppu.set_int_stat(value & 0x02 == 0x02);
                         self.timer.set_int_tima(value & 0x04 == 0x04);
+                        self.serial.set_int_serial(value & 0x08 == 0x08);
                         self.pad.set_int_pad(value & 0x10 == 0x10);
                     }
 
diff --git a/src/pad.rs b/src/pad.rs
index 36c10a1715f80741916a3d6dd55bce5e28d67532..f9e6ef97d3b6ab49edec7ea9f1a8f778958a6597 100644
--- a/src/pad.rs
+++ b/src/pad.rs
@@ -131,14 +131,17 @@ impl Pad {
         }
     }
 
+    #[inline(always)]
     pub fn int_pad(&self) -> bool {
         self.int_pad
     }
 
+    #[inline(always)]
     pub fn set_int_pad(&mut self, value: bool) {
         self.int_pad = value;
     }
 
+    #[inline(always)]
     pub fn ack_pad(&mut self) {
         self.set_int_pad(false);
     }
diff --git a/src/ppu.rs b/src/ppu.rs
index 651e0626170b091143e3721d8cf72f152be81fb6..99f6042cb14dd7b8fe9df3deb1e2bab20b3efa6c 100644
--- a/src/ppu.rs
+++ b/src/ppu.rs
@@ -14,6 +14,7 @@ pub const HRAM_SIZE: usize = 128;
 pub const OAM_SIZE: usize = 260;
 pub const PALETTE_SIZE: usize = 4;
 pub const RGB_SIZE: usize = 3;
+pub const RGBA_SIZE: usize = 4;
 pub const TILE_WIDTH: usize = 8;
 pub const TILE_HEIGHT: usize = 8;
 pub const TILE_DOUBLE_HEIGHT: usize = 16;
@@ -48,10 +49,18 @@ pub const PALETTE_COLORS: Palette = [[255, 255, 255], [192, 192, 192], [96, 96,
 /// with the size of RGB (3 bytes).
 pub type Pixel = [u8; RGB_SIZE];
 
+/// Defines a transparent Game Boy pixel type as a buffer
+/// with the size of RGBA (4 bytes).
+pub type PixelAlpha = [u8; RGBA_SIZE];
+
 /// Defines a type that represents a color palette
 /// within the Game Boy context.
 pub type Palette = [Pixel; PALETTE_SIZE];
 
+/// Defines a type that represents a color palette
+/// with alpha within the Game Boy context.
+pub type PaletteAlpha = [PixelAlpha; PALETTE_SIZE];
+
 /// Represents a palette with the metadata that is
 /// associated with it.
 #[cfg_attr(feature = "wasm", wasm_bindgen)]
@@ -642,26 +651,32 @@ impl Ppu {
         self.frame_index
     }
 
+    #[inline(always)]
     pub fn int_vblank(&self) -> bool {
         self.int_vblank
     }
 
+    #[inline(always)]
     pub fn set_int_vblank(&mut self, value: bool) {
         self.int_vblank = value;
     }
 
+    #[inline(always)]
     pub fn ack_vblank(&mut self) {
         self.set_int_vblank(false);
     }
 
+    #[inline(always)]
     pub fn int_stat(&self) -> bool {
         self.int_stat
     }
 
+    #[inline(always)]
     pub fn set_int_stat(&mut self, value: bool) {
         self.int_stat = value;
     }
 
+    #[inline(always)]
     pub fn ack_stat(&mut self) {
         self.set_int_stat(false);
     }
diff --git a/src/serial.rs b/src/serial.rs
new file mode 100644
index 0000000000000000000000000000000000000000..cd8c53c90c8a4f9af93b766893df04ff8d080160
--- /dev/null
+++ b/src/serial.rs
@@ -0,0 +1,209 @@
+use crate::warnln;
+
+pub trait SerialDevice {
+    fn send(&mut self) -> u8;
+    fn receive(&mut self, byte: u8);
+
+    /// Whether the serial device "driver" supports slave mode
+    /// simulating an external clock source. Or if instead the
+    /// clock should always be generated by the running device.
+    fn allow_slave(&self) -> bool;
+}
+
+pub struct Serial {
+    data: u8,
+    control: u8,
+    shift_clock: bool,
+    clock_speed: bool,
+    transferring: bool,
+    timer: i16,
+    length: u16,
+    bit_count: u8,
+    byte_receive: u8,
+    int_serial: bool,
+    device: Box<dyn SerialDevice>,
+}
+
+impl Serial {
+    pub fn new() -> Self {
+        Self {
+            data: 0x0,
+            control: 0x0,
+            shift_clock: false,
+            clock_speed: false,
+            transferring: false,
+            timer: 0,
+            length: 512,
+            bit_count: 0,
+            byte_receive: 0x0,
+            int_serial: false,
+            device: Box::<NullDevice>::default(),
+        }
+    }
+
+    pub fn reset(&mut self) {
+        self.data = 0x0;
+        self.control = 0x0;
+        self.shift_clock = false;
+        self.clock_speed = false;
+        self.transferring = false;
+        self.timer = 0;
+        self.length = 512;
+        self.bit_count = 0;
+        self.byte_receive = 0x0;
+        self.int_serial = false;
+    }
+
+    pub fn clock(&mut self, cycles: u8) {
+        if !self.transferring {
+            return;
+        }
+
+        self.timer = self.timer.saturating_sub(cycles as i16);
+        if self.timer <= 0 {
+            let bit = (self.byte_receive >> (7 - self.bit_count)) & 0x01;
+            self.data = (self.data << 1) | bit;
+
+            self.tick_transfer();
+
+            self.timer = self.length as i16;
+        }
+    }
+
+    pub fn read(&mut self, addr: u16) -> u8 {
+        match addr & 0x00ff {
+            0x01 => self.data,
+            0x02 => {
+                (if self.shift_clock { 0x01 } else { 0x00 }
+                    | if self.clock_speed { 0x02 } else { 0x00 }
+                    | if self.transferring { 0x80 } else { 0x00 })
+            }
+            _ => {
+                warnln!("Reding from unknown Serial location 0x{:04x}", addr);
+                0xff
+            }
+        }
+    }
+
+    pub fn write(&mut self, addr: u16, value: u8) {
+        match addr & 0x00ff {
+            0x01 => self.data = value,
+            0x02 => {
+                self.shift_clock = value & 0x01 == 0x01;
+                self.clock_speed = value & 0x02 == 0x02;
+                self.transferring = value & 0x80 == 0x80;
+
+                // in case the clock is meant to be set by the attached device
+                // and the current Game Boy is meant to be running in slave mode
+                // then checks if the attached device "driver" allows clock set
+                // by external device and if not then ignores the transfer request
+                // by immediately disabling the transferring flag
+                if !self.shift_clock && !self.device.allow_slave() {
+                    self.transferring = false;
+                }
+
+                // in case a transfer of byte has been requested and
+                // this is the then we need to start the transfer setup
+                if self.transferring {
+                    // @TODO: if the GBC mode exists there should
+                    // be special check logic here
+                    //self.length = if self.gb.is_cgb() && self.clock_speed { 16 } else { 512 };
+                    self.length = 512;
+                    self.bit_count = 0;
+                    self.timer = self.length as i16;
+
+                    // executes the send and receive operation immediately
+                    // this is considered an operational optimization with
+                    // no real effect on the emulation (ex: not timing issues)
+                    self.byte_receive = self.device.send();
+                    self.device.receive(self.data);
+                }
+            }
+            _ => warnln!("Writing to unknown Serial location 0x{:04x}", addr),
+        }
+    }
+
+    pub fn send(&self) -> bool {
+        if self.shift_clock {
+            true
+        } else {
+            self.data & 0x80 == 0x80
+        }
+    }
+
+    pub fn receive(&mut self, bit: bool) {
+        if !self.shift_clock {
+            self.data = (self.data << 1) | bit as u8;
+            self.tick_transfer();
+        }
+    }
+
+    #[inline(always)]
+    pub fn int_serial(&self) -> bool {
+        self.int_serial
+    }
+
+    #[inline(always)]
+    pub fn set_int_serial(&mut self, value: bool) {
+        self.int_serial = value;
+    }
+
+    #[inline(always)]
+    pub fn ack_serial(&mut self) {
+        self.set_int_serial(false);
+    }
+
+    pub fn device(&self) -> &dyn SerialDevice {
+        self.device.as_ref()
+    }
+
+    pub fn set_device(&mut self, device: Box<dyn SerialDevice>) {
+        self.device = device;
+    }
+
+    fn tick_transfer(&mut self) {
+        self.bit_count += 1;
+        if self.bit_count == 8 {
+            self.transferring = false;
+            self.length = 0;
+            self.bit_count = 0;
+
+            // signals the interrupt for the serial
+            // transfer completion, indicating that
+            // a new byte is ready to be read
+            self.int_serial = true;
+        }
+    }
+}
+
+impl Default for Serial {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+pub struct NullDevice {}
+
+impl NullDevice {
+    pub fn new() -> Self {
+        Self {}
+    }
+}
+
+impl SerialDevice for NullDevice {
+    fn send(&mut self) -> u8 {
+        0xff
+    }
+
+    fn receive(&mut self, _: u8) {}
+
+    fn allow_slave(&self) -> bool {
+        false
+    }
+}
+
+impl Default for NullDevice {
+    fn default() -> Self {
+        Self::new()
+    }
+}
diff --git a/src/timer.rs b/src/timer.rs
index 537c14c6714406aa2045ab6a578a8f121f59a119..e423663620d9d65a02d48345a681b45a27d9c4fe 100644
--- a/src/timer.rs
+++ b/src/timer.rs
@@ -100,14 +100,17 @@ impl Timer {
         }
     }
 
+    #[inline(always)]
     pub fn int_tima(&self) -> bool {
         self.int_tima
     }
 
+    #[inline(always)]
     pub fn set_int_tima(&mut self, value: bool) {
         self.int_tima = value;
     }
 
+    #[inline(always)]
     pub fn ack_tima(&mut self) {
         self.set_int_tima(false);
     }