diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 0a11b8d400b86630ace923b54ad29ea545e70387..cfbfd15789725e1aa1947fc2e6f09eabecacf177 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -19,6 +19,7 @@ jobs:
           "1.65.0",
           "1.66.0",
           "1.67.0",
+          "1.68.0",
           "latest"
         ]
     runs-on: ubuntu-latest
@@ -46,11 +47,11 @@ jobs:
     strategy:
       matrix:
         rust-version: [
-          "1.63.0",
           "1.64.0",
           "1.65.0",
           "1.66.0",
           "1.67.0",
+          "1.68.0",
           "latest"
         ]
         node-version: ["16"]
@@ -79,3 +80,44 @@ jobs:
           node-version: ${{ matrix.node-version }}
       - name: Build and lint Web code
         run: cd frontends/web && npm install && npm run build && npm run lint
+  build-sdl:
+    name: Build SDL
+    timeout-minutes: 30
+    strategy:
+      matrix:
+        rust-version: [
+          "1.61.0",
+          "1.62.0",
+          "1.63.0",
+          "1.64.0",
+          "1.65.0",
+          "1.66.0",
+          "1.67.0",
+          "1.68.0",
+          "latest"
+        ]
+    runs-on: ubuntu-latest
+    container: rust:${{ matrix.rust-version }}
+    steps:
+      - name: Checkout code from repository
+        uses: actions/checkout@v3
+      - name: Install Dependencies
+        run: |
+          apt-get update
+          apt-get install -y -q zip
+      - name: Install Rust components
+        run: |
+          rustup component add rustfmt
+          rustup component add clippy
+      - name: Print Rust information
+        run: rustc --version
+      - name: Install SDL dependencies
+        run: cd frontends/sdl && cargo install cargo-vcpkg && cargo vcpkg build
+      - name: Verify Rust code format
+        run: cd frontends/sdl && cargo fmt --all -- --check
+      - name: Verify Rust code linting
+        run: cd frontends/sdl && cargo clippy -- -D warnings -A unknown-lints
+      - name: Build development version
+        run: cd frontends/sdl && cargo build
+      - name: Build release version
+        run: cd frontends/sdl && cargo build --release
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 715499972b48f242eb26c1566e7b6c916544632a..daff0a9883bdb764f0ae44a17460bba4f954639a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -42,7 +42,7 @@ build-wasm:
   stage: build
   parallel:
     matrix:
-      - RUST_VERSION: ["1.63.0"]
+      - RUST_VERSION: ["1.64.0"]
   script:
     - rustup toolchain install $RUST_VERSION
     - rustup override set $RUST_VERSION
@@ -117,6 +117,8 @@ deploy-cloudfare-prod:
     - cd frontends/web/dist
     - cp -rp ../static/* .
     - npm_config_yes=true npx wrangler pages publish . --project-name=boytacean --branch prod
+    - npm_config_yes=true npx wrangler pages publish . --project-name=boytacean --branch production
+    - npm_config_yes=true npx wrangler pages publish . --project-name=boytacean --branch main
   dependencies:
     - build-wasm
   only:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3a7ac6225b842c8f5002717f6e0405405e1c01f4..11565a5c5e815edfaab2ec79f14844e30bb5b18f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,69 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 *
 
+## [0.8.0] - 2023-04-20
+
+### 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)
+* Support for display of logger and printer in Web panels
+* Converted serial-sections strategy to event driven
+
+### Fixed
+
+* `ButtonSwitch` issues by updating the value strategy nad bumping `emukit`
+* `AudioGB` with display of canvas with no visibility
+
+## [0.7.5] - 2023-04-11
+
+### Added
+
+* Support for variable clock speed for APU, means variable audio speed
+* Moved debug into the base emulator (from emukit)
+
+## [0.7.4] - 2023-04-08
+
+### Added
+
+* Support for audio channel 4 (noise) 🔈
+* Better trigger support for audio channels 🔈
+
+### Changed
+
+* Added CH4 public API method for WASM
+
+### Fixed
+
+* Envelope support for both channel 2 and 4 🔈
+* Issue related to the wave length stop flag 🔈
+
+## [0.7.3] - 2023-04-02
+
+### Added
+
+* Support for CGB flag parsing
+* Waveform plotting support
+
+### Fixed
+
+* Major JoyPad issue with Action/Select read in register
+* Small issue with channel 3 audio and DAC disable
+
+## [0.7.2] - 2023-03-04
+
+### Added
+
+* Support for stereo sound 🔊
+
+### Changed
+
+* APU `clock()` method with `cycles` parameter, improving performance by an order of magnitude 💪
+
+### Fixed
+
+* Added reset of APU, which fixes annoying "garbage" data in buffer when restarting the state of the emulator
+
 ## [0.7.1] - 2023-03-02
 
 ### Changed
diff --git a/Cargo.toml b/Cargo.toml
index ab7d0cc5425e3371bb6c18eec6366e2b82813e63..ef4b65745deac225ab54cd0532b8f681eae467d3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "boytacean"
 description = "A Game Boy emulator that is written in Rust."
-version = "0.7.1"
+version = "0.8.0"
 authors = ["João Magalhães <joamag@gmail.com>"]
 license = "Apache-2.0"
 repository = "https://github.com/joamag/boytacean"
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 5a9583e989ce17d9208b8c8ef171418d5ace0c90..b097ac1a6a98ae3624712ddae1b57f93dbf7624e 100644
--- a/doc/inspiration.md
+++ b/doc/inspiration.md
@@ -20,21 +20,29 @@
 
 * [GitHub - LIJI32/SameBoy (C)](https://github.com/LIJI32/SameBoy)
 * [GitHub - binji/binjgb (C)](https://github.com/binji/binjgb)
+* [GitHub - 7thSamurai/Azayaka (C++)](https://github.com/7thSamurai/Azayaka)
 * [GitHub - feo-boy/feo-boy (Rust)](https://github.com/feo-boy/feo-boy)
 * [GitHub - Rodrigodd/gameroy (Rust)](https://github.com/Rodrigodd/gameroy)
 * [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)
 
 ## Videos
 
 * [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 03177352b9e3ff885fb6eeba816ff0e0608531ff..0a07c0b6c22341ef812dfa436a70959ccf3b4463 100644
--- a/frontends/sdl/Cargo.toml
+++ b/frontends/sdl/Cargo.toml
@@ -1,15 +1,24 @@
 [package]
 name = "boytacean-sdl"
-version = "0.7.1"
+version = "0.8.0"
 authors = ["João Magalhães <joamag@gmail.com>"]
 description = "An SDL frontend for Boytacen"
 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 ac9ab6a5d8636e34a8274ef37750aedebac25738..163d1b2e9ff09f07c11d104e7a28e0beea1101c3 100644
--- a/frontends/sdl/README.md
+++ b/frontends/sdl/README.md
@@ -16,3 +16,16 @@ Then you can use the following command to build and run Boytacean SDL:
 cargo build
 cargo run
 ```
+
+To reload the code continuously use the cargo watch tool:
+
+```bash
+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 471e84f59dfafef77fd46fa5563b238f78a51b49..aae9f363b55a7b6a3727e24bb3d6809050bed4dd 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,
@@ -47,12 +50,13 @@ pub struct Emulator {
     graphics: Option<Graphics>,
     audio: Option<Audio>,
     title: &'static str,
+    rom_path: String,
     logic_frequency: u32,
     visual_frequency: f32,
     next_tick_time: f32,
     next_tick_time_i: u32,
     features: Vec<&'static str>,
-    palettes: [PaletteInfo; 3],
+    palettes: [PaletteInfo; 7],
     palette_index: usize,
 }
 
@@ -63,6 +67,7 @@ impl Emulator {
             graphics: None,
             audio: None,
             title: TITLE,
+            rom_path: String::from("invalid"),
             logic_frequency: GameBoy::CPU_FREQ,
             visual_frequency: GameBoy::VISUAL_FREQ,
             next_tick_time: 0.0,
@@ -96,6 +101,42 @@ impl Emulator {
                         [0x53, 0x4d, 0x57],
                     ],
                 ),
+                PaletteInfo::new(
+                    "goldsilver",
+                    [
+                        [0xc5, 0xc6, 0x6d],
+                        [0x97, 0xa1, 0xb0],
+                        [0x58, 0x5e, 0x67],
+                        [0x23, 0x52, 0x29],
+                    ],
+                ),
+                PaletteInfo::new(
+                    "pacman",
+                    [
+                        [0xff, 0xff, 0x00],
+                        [0xff, 0xb8, 0x97],
+                        [0x37, 0x32, 0xff],
+                        [0x00, 0x00, 0x00],
+                    ],
+                ),
+                PaletteInfo::new(
+                    "mariobros",
+                    [
+                        [0xf7, 0xce, 0xc3],
+                        [0xcc, 0x9e, 0x22],
+                        [0x92, 0x34, 0x04],
+                        [0x00, 0x00, 0x00],
+                    ],
+                ),
+                PaletteInfo::new(
+                    "pokemon",
+                    [
+                        [0xf8, 0x78, 0x00],
+                        [0xb8, 0x60, 0x00],
+                        [0x78, 0x38, 0x00],
+                        [0x00, 0x00, 0x00],
+                    ],
+                ),
             ],
             palette_index: 0,
         }
@@ -127,8 +168,9 @@ impl Emulator {
         self.audio = Some(Audio::new(sdl));
     }
 
-    pub fn load_rom(&mut self, path: &str) {
-        let rom = self.system.load_rom_file(path);
+    pub fn load_rom(&mut self, path: Option<&str>) {
+        let path_res = path.unwrap_or(&self.rom_path);
+        let rom = self.system.load_rom_file(path_res);
         println!(
             "========= Cartridge =========\n{}\n=============================",
             rom
@@ -139,6 +181,13 @@ impl Emulator {
             .window_mut()
             .set_title(format!("{} [{}]", self.title, rom.title()).as_str())
             .unwrap();
+        self.rom_path = String::from(path_res);
+    }
+
+    pub fn reset(&mut self) {
+        self.system.reset();
+        self.system.load_boot_default();
+        self.load_rom(None);
     }
 
     pub fn benchmark(&mut self, params: Benchmark) {
@@ -163,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);
     }
 
@@ -226,6 +275,10 @@ impl Emulator {
                         keycode: Some(Keycode::Escape),
                         ..
                     } => break 'main,
+                    Event::KeyDown {
+                        keycode: Some(Keycode::R),
+                        ..
+                    } => self.reset(),
                     Event::KeyDown {
                         keycode: Some(Keycode::B),
                         ..
@@ -264,8 +317,8 @@ impl Emulator {
                     }
                     Event::DropFile { filename, .. } => {
                         self.system.reset();
-                        self.system.load_cgb(true);
-                        self.load_rom(&filename);
+                        self.system.load_boot_cgb();
+                        self.load_rom(Some(&filename));
                     }
                     _ => (),
                 }
@@ -318,9 +371,9 @@ impl Emulator {
                         last_frame = self.system.ppu_frame();
                     }
 
+                    // in case the audio subsystem is enabled, then the audio buffer
+                    // must be queued into the SDL audio subsystem
                     if let Some(audio) = self.audio.as_mut() {
-                        // obtains the new audio buffer and queues it into the audio
-                        // subsystem ready to be processed
                         let audio_buffer = self
                             .system
                             .audio_buffer()
@@ -331,7 +384,7 @@ impl Emulator {
                     }
 
                     // clears the audio buffer to prevent it from
-                    // "exploding" in size
+                    // "exploding" in size, this is required GC operation
                     self.system.clear_audio_buffer();
                 }
 
@@ -394,14 +447,27 @@ 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();
-    game_boy.load_cgb(true);
+    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_cgb();
 
     // creates a new generic emulator structure then starts
     // both the video and audio sub-systems, loads default
     // ROM file and starts running it
     let mut emulator = Emulator::new(game_boy);
     emulator.start(SCREEN_SCALE);
-    emulator.load_rom("../../res/roms/pocket.gb");
+    emulator.load_rom(Some("../../res/roms/demo/pocket.gb"));
     emulator.toggle_palette();
     emulator.run();
 }
diff --git a/frontends/web/.parcelrc b/frontends/web/.parcelrc
index 1017d972ae95a264db5e230a4dc04e5cee3fc6ed..4fd2b68587dab082a05be3735294f53a5862b638 100644
--- a/frontends/web/.parcelrc
+++ b/frontends/web/.parcelrc
@@ -2,6 +2,6 @@
     "extends": "@parcel/config-default",
     "transformers": {
         "*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"],
-        "*.gb": ["@parcel/transformer-raw"]
+        "*.{gb,gbc}": ["@parcel/transformer-raw"]
     }
 }
diff --git a/frontends/web/index.ts b/frontends/web/index.ts
index 560d43e9424be72ac4b43973ccf29fbc8a637d00..bf4afbd1c5c7592b58c5b014907d69cabae2a583 100644
--- a/frontends/web/index.ts
+++ b/frontends/web/index.ts
@@ -50,5 +50,12 @@ const BACKGROUNDS = [
         background: background,
         backgrounds: BACKGROUNDS
     });
+
+    // sets the emulator in the global scope this is useful
+    // to be able to access the emulator from global functions
+    window.emulator = emulator;
+
+    // starts the emulator with the provided ROM URL, this is
+    // going to run the main emulator (infinite) loop
     await emulator.main({ romUrl: romUrl });
 })();
diff --git a/frontends/web/package.json b/frontends/web/package.json
index 45278e03938956f263aa6fffd3a58fcf899e2d96..4e83628ab7980c0fff801e6e84cc6f6098bfb6f6 100644
--- a/frontends/web/package.json
+++ b/frontends/web/package.json
@@ -1,6 +1,6 @@
 {
     "name": "boytacean-web",
-    "version": "0.7.1",
+    "version": "0.8.0",
     "description": "The web version of Boytacean",
     "repository": {
         "type": "git",
@@ -19,18 +19,19 @@
     "source": "index.ts",
     "devDependencies": {
         "@parcel/transformer-typescript-tsc": "^2.8.3",
-        "@types/react": "^18.0.28",
+        "@types/react": "^18.0.37",
         "@types/react-dom": "^18.0.11",
-        "@typescript-eslint/eslint-plugin": "^5.54.0",
-        "@typescript-eslint/parser": "^5.54.0",
-        "emukit": "^0.7.1",
-        "eslint": "^8.35.0",
-        "nodemon": "^2.0.20",
+        "@typescript-eslint/eslint-plugin": "^5.59.0",
+        "@typescript-eslint/parser": "^5.59.0",
+        "emukit": "^0.8.6",
+        "eslint": "^8.38.0",
+        "nodemon": "^2.0.22",
         "parcel": "^2.8.3",
-        "prettier": "^2.8.4",
+        "prettier": "^2.8.7",
         "process": "^0.11.10",
         "react": "^18.2.0",
         "react-dom": "^18.2.0",
-        "typescript": "^4.9.5"
+        "typescript": "^5.0.4",
+        "webgl-plot": "^0.7.0"
     }
 }
diff --git a/frontends/web/react/components/audio-gb/audio-gb.css b/frontends/web/react/components/audio-gb/audio-gb.css
new file mode 100644
index 0000000000000000000000000000000000000000..bcfefa0fcf22d37e0257724c993b14d239e6d4b5
--- /dev/null
+++ b/frontends/web/react/components/audio-gb/audio-gb.css
@@ -0,0 +1,13 @@
+.audio-gb > .section {
+    display: inline-block;
+    vertical-align: top;
+}
+
+.audio-gb > .section > .audio-wave {
+    display: inline-block;
+    margin-right: 5px;
+}
+
+.audio-gb > .section > .audio-wave > h4 {
+    margin: 4px 0px 4px 0px;
+}
diff --git a/frontends/web/react/components/audio-gb/audio-gb.tsx b/frontends/web/react/components/audio-gb/audio-gb.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..03a4854ca2f71d0ac414722f2ae093a9cf49fb99
--- /dev/null
+++ b/frontends/web/react/components/audio-gb/audio-gb.tsx
@@ -0,0 +1,195 @@
+import React, { FC, useEffect, useRef, useState } from "react";
+import { Canvas, CanvasStructure, PixelFormat } from "emukit";
+import { WebglPlot, WebglLine, ColorRGBA } from "webgl-plot";
+
+import "./audio-gb.css";
+
+type AudioGBProps = {
+    getAudioOutput: () => Record<string, number>;
+    interval?: number;
+    drawInterval?: number;
+    color?: number;
+    range?: number;
+    rangeVolume?: number;
+    engine?: "webgl" | "canvas";
+    style?: string[];
+    renderWave?: (name: string, key: string, styles?: string[]) => JSX.Element;
+};
+
+export const AudioGB: FC<AudioGBProps> = ({
+    getAudioOutput,
+    interval = 1,
+    drawInterval = 1000 / 60,
+    color = 0x50cb93ff,
+    range = 128,
+    rangeVolume = 32,
+    engine = "webgl",
+    style = [],
+    renderWave
+}) => {
+    const classes = () => ["audio-gb", ...style].join(" ");
+    const [audioOutput, setAudioOutput] = useState<Record<string, number[]>>(
+        {}
+    );
+    const intervalsRef = useRef<number>();
+    const intervalsExtraRef = useRef<number>();
+
+    useEffect(() => {
+        const updateAudioOutput = () => {
+            const _audioOutput = getAudioOutput();
+            for (const [key, value] of Object.entries(_audioOutput)) {
+                const values = audioOutput[key] ?? new Array(range).fill(0);
+                values.push(value);
+                if (values.length > range) {
+                    values.shift();
+                }
+                audioOutput[key] = values;
+            }
+            setAudioOutput(audioOutput);
+        };
+        setInterval(() => updateAudioOutput(), interval);
+        updateAudioOutput();
+        return () => {
+            if (intervalsRef.current) {
+                clearInterval(intervalsRef.current);
+            }
+            if (intervalsExtraRef.current) {
+                clearInterval(intervalsExtraRef.current);
+            }
+        };
+    }, []);
+    const renderAudioWave = (
+        name: string,
+        key: string,
+        styles: string[] = []
+    ) => {
+        const classes = ["audio-wave", ...styles].join(" ");
+        const onCanvas = (structure: CanvasStructure) => {
+            const drawWave = () => {
+                const values = audioOutput[key];
+                if (!values) {
+                    return;
+                }
+                structure.canvasImage.data.fill(0);
+                values.forEach((value, index) => {
+                    const valueN = Math.min(value, rangeVolume - 1);
+                    const line = rangeVolume - 1 - valueN;
+                    const offset = (line * range + index) * PixelFormat.RGBA;
+                    structure.canvasBuffer.setUint32(offset, color);
+                });
+                structure.canvasOffScreenContext.putImageData(
+                    structure.canvasImage,
+                    0,
+                    0
+                );
+                structure.canvasContext.clearRect(0, 0, range, rangeVolume);
+                structure.canvasContext.drawImage(
+                    structure.canvasOffScreen,
+                    0,
+                    0
+                );
+            };
+            drawWave();
+            intervalsExtraRef.current = setInterval(
+                () => drawWave(),
+                drawInterval
+            );
+        };
+        return (
+            <div className={classes}>
+                <h4>{name}</h4>
+                <Canvas
+                    width={range}
+                    height={rangeVolume}
+                    onCanvas={onCanvas}
+                />
+            </div>
+        );
+    };
+    const renderAudioWaveWgl = (
+        name: string,
+        key: string,
+        styles: string[] = []
+    ) => {
+        const canvasRef = useRef<HTMLCanvasElement>(null);
+        const classes = ["audio-wave", ...styles].join(" ");
+        useEffect(() => {
+            if (!canvasRef.current) return;
+
+            // converts the canvas to the expected size according
+            // to the device pixel ratio value
+            const devicePixelRatio = window.devicePixelRatio || 1;
+            canvasRef.current.width = range * devicePixelRatio;
+            canvasRef.current.height = rangeVolume * devicePixelRatio;
+
+            // creates the WGL Plot object with the canvas element
+            // that is associated with the current audio wave
+            const wglPlot = new WebglPlot(canvasRef.current);
+
+            const colorRgba = new ColorRGBA(...intToColor2(color));
+            const line = new WebglLine(colorRgba, range);
+
+            line.arrangeX();
+            wglPlot.addLine(line);
+
+            const drawWave = () => {
+                const values = audioOutput[key];
+                if (!values) {
+                    return;
+                }
+
+                values.forEach((value, index) => {
+                    const valueN = Math.min(value, rangeVolume - 1);
+                    line.setY(index, valueN / rangeVolume - 1);
+                });
+
+                wglPlot.update();
+            };
+            drawWave();
+            intervalsExtraRef.current = setInterval(
+                () => drawWave(),
+                drawInterval
+            );
+        }, [canvasRef]);
+        return (
+            <div className={classes}>
+                <h4>{name}</h4>
+                <Canvas
+                    canvasRef={canvasRef}
+                    width={range}
+                    height={rangeVolume}
+                    init={false}
+                />
+            </div>
+        );
+    };
+    let renderMethod =
+        engine === "webgl" ? renderAudioWaveWgl : renderAudioWave;
+    renderMethod = renderWave ?? renderMethod;
+    return (
+        <div className={classes()}>
+            <div className="section">
+                {renderMethod("Master", "master")}
+                {renderMethod("CH1", "ch1")}
+                {renderMethod("CH2", "ch2")}
+                {renderMethod("CH3", "ch3")}
+                {renderMethod("CH4", "ch4")}
+            </div>
+        </div>
+    );
+};
+
+const intToColor = (int: number): [number, number, number, number] => {
+    const r = (int >> 24) & 0xff;
+    const g = (int >> 16) & 0xff;
+    const b = (int >> 8) & 0xff;
+    const a = int & 0xff;
+    return [r, g, b, a];
+};
+
+const intToColor2 = (int: number): [number, number, number, number] => {
+    const color = intToColor(int);
+    return color.map((v) => v / 255) as [number, number, number, number];
+};
+
+export default AudioGB;
diff --git a/frontends/web/react/components/debug/debug.css b/frontends/web/react/components/debug/debug.css
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/frontends/web/react/components/debug/debug.tsx b/frontends/web/react/components/debug/debug.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..312de1c6ca15de640d83b7f7742c3d06d4227389
--- /dev/null
+++ b/frontends/web/react/components/debug/debug.tsx
@@ -0,0 +1,65 @@
+import React, { FC } from "react";
+import { AudioGB } from "../audio-gb/audio-gb";
+import { RegistersGB } from "../registers-gb/registers-gb";
+import { TilesGB } from "../tiles-gb/tiles-gb";
+import { GameboyEmulator } from "../../../ts";
+
+import "./debug.css";
+
+type EmulatorProps = {
+    emulator: GameboyEmulator;
+};
+
+export const DebugVideo: FC<EmulatorProps> = ({ emulator }) => {
+    return (
+        <>
+            {emulator.getTile && (
+                <div
+                    style={{
+                        display: "inline-block",
+                        verticalAlign: "top",
+                        marginRight: 32,
+                        width: 256
+                    }}
+                >
+                    <h3>VRAM Tiles</h3>
+                    <TilesGB
+                        getTile={(index) =>
+                            emulator.getTile
+                                ? emulator.getTile(index)
+                                : new Uint8Array()
+                        }
+                        tileCount={384}
+                        width={"100%"}
+                        contentBox={false}
+                    />
+                </div>
+            )}
+            <div
+                style={{
+                    display: "inline-block",
+                    verticalAlign: "top"
+                }}
+            >
+                <h3>Registers</h3>
+                <RegistersGB getRegisters={() => emulator.registers} />
+            </div>
+        </>
+    );
+};
+
+export const DebugAudio: FC<EmulatorProps> = ({ emulator }) => {
+    return (
+        <>
+            <div
+                style={{
+                    display: "inline-block",
+                    verticalAlign: "top"
+                }}
+            >
+                <h3>Audio Waveform</h3>
+                <AudioGB getAudioOutput={() => emulator.audioOutput} />
+            </div>
+        </>
+    );
+};
diff --git a/frontends/web/react/components/index.ts b/frontends/web/react/components/index.ts
index 5e2bfe9fe05e7cdc3f3e4804b4afec5968397e86..6ec9010a9a7d8099e4c715093e38cbf130dfcf2b 100644
--- a/frontends/web/react/components/index.ts
+++ b/frontends/web/react/components/index.ts
@@ -1 +1,6 @@
+export * from "./audio-gb/audio-gb";
+export * from "./debug/debug";
 export * from "./help/help";
+export * from "./registers-gb/registers-gb";
+export * from "./serial-section/serial-section";
+export * from "./tiles-gb/tiles-gb";
diff --git a/frontends/web/react/components/registers-gb/registers-gb.css b/frontends/web/react/components/registers-gb/registers-gb.css
new file mode 100644
index 0000000000000000000000000000000000000000..0801939b049a3955f55675c88db3af75cb271045
--- /dev/null
+++ b/frontends/web/react/components/registers-gb/registers-gb.css
@@ -0,0 +1,32 @@
+.registers-gb > .section {
+    display: inline-block;
+    margin-right: 32px;
+    vertical-align: top;
+}
+
+.registers-gb > .section:last-child {
+    margin-right: 0px;
+}
+
+.registers-gb > .section > h4 {
+    font-size: 22px;
+    margin: 0px 0px 8px 0px;
+}
+
+.registers-gb > .section > .register {
+    font-size: 0px;
+    line-height: 22px;
+}
+
+.registers-gb > .section > .register > .register-key {
+    display: inline-block;
+    font-size: 20px;
+    width: 40px;
+}
+
+.registers-gb > .section > .register > .register-value {
+    display: inline-block;
+    font-size: 20px;
+    text-align: right;
+    width: 66px;
+}
diff --git a/frontends/web/react/components/registers-gb/registers-gb.tsx b/frontends/web/react/components/registers-gb/registers-gb.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..988ba20ea37839ee3fa392000f66b1f6a532774f
--- /dev/null
+++ b/frontends/web/react/components/registers-gb/registers-gb.tsx
@@ -0,0 +1,79 @@
+import React, { FC, useEffect, useRef, useState } from "react";
+
+import "./registers-gb.css";
+
+type RegistersGBProps = {
+    getRegisters: () => Record<string, string | number>;
+    interval?: number;
+    style?: string[];
+};
+
+export const RegistersGB: FC<RegistersGBProps> = ({
+    getRegisters,
+    interval = 50,
+    style = []
+}) => {
+    const classes = () => ["registers-gb", ...style].join(" ");
+    const [registers, setRegisters] = useState<Record<string, string | number>>(
+        {}
+    );
+    const intervalsRef = useRef<number>();
+    useEffect(() => {
+        const updateRegisters = () => {
+            const registers = getRegisters();
+            setRegisters(registers);
+        };
+        setInterval(() => updateRegisters(), interval);
+        updateRegisters();
+        return () => {
+            if (intervalsRef.current) {
+                clearInterval(intervalsRef.current);
+            }
+        };
+    }, []);
+    const renderRegister = (
+        key: string,
+        value?: number,
+        size = 2,
+        styles: string[] = []
+    ) => {
+        const classes = ["register", ...styles].join(" ");
+        const valueS =
+            value?.toString(16).toUpperCase().padStart(size, "0") ?? value;
+        return (
+            <div className={classes}>
+                <span className="register-key">{key}</span>
+                <span className="register-value">
+                    {valueS ? `0x${valueS}` : "-"}
+                </span>
+            </div>
+        );
+    };
+    return (
+        <div className={classes()}>
+            <div className="section">
+                <h4>CPU</h4>
+                {renderRegister("PC", registers.pc as number, 4)}
+                {renderRegister("SP", registers.sp as number, 4)}
+                {renderRegister("A", registers.a as number)}
+                {renderRegister("B", registers.b as number)}
+                {renderRegister("C", registers.c as number)}
+                {renderRegister("D", registers.d as number)}
+                {renderRegister("E", registers.e as number)}
+                {renderRegister("H", registers.h as number)}
+                {renderRegister("L", registers.l as number)}
+            </div>
+            <div className="section">
+                <h4>PPU</h4>
+                {renderRegister("SCY", registers.scy as number)}
+                {renderRegister("SCX", registers.scx as number)}
+                {renderRegister("WY", registers.wy as number)}
+                {renderRegister("WX", registers.wx as number)}
+                {renderRegister("LY", registers.ly as number)}
+                {renderRegister("LYC", registers.lyc as number)}
+            </div>
+        </div>
+    );
+};
+
+export default RegistersGB;
diff --git a/frontends/web/react/components/serial-section/serial-section.css b/frontends/web/react/components/serial-section/serial-section.css
new file mode 100644
index 0000000000000000000000000000000000000000..ba1a95ad35c4da654c3324919935439d4c38e1ce
--- /dev/null
+++ b/frontends/web/react/components/serial-section/serial-section.css
@@ -0,0 +1,11 @@
+.serial-section .printer > .printer-lines {
+    font-size: 0px;
+}
+
+.serial-section .printer > .printer-lines > .printer-line {
+    display: block;
+}
+
+.serial-section .printer > .printer-lines > .placeholder {
+    font-size: initial;
+}
diff --git a/frontends/web/react/components/serial-section/serial-section.tsx b/frontends/web/react/components/serial-section/serial-section.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..03d2ffb5416b66cd8eb8758666fa5527f3ca704f
--- /dev/null
+++ b/frontends/web/react/components/serial-section/serial-section.tsx
@@ -0,0 +1,126 @@
+import React, { FC, useEffect, useRef, useState } from "react";
+import { ButtonSwitch, Emulator, Info, Pair, PanelTab } from "emukit";
+import { GameboyEmulator, SerialDevice, bufferToDataUrl } from "../../../ts";
+
+import "./serial-section.css";
+
+const DEVICE_ICON: { [key: string]: string } = {
+    null: "🛑",
+    logger: "📜",
+    printer: "🖨️"
+};
+
+type SerialSectionProps = {
+    emulator: GameboyEmulator;
+    style?: string[];
+};
+
+export const SerialSection: FC<SerialSectionProps> = ({
+    emulator,
+    style = []
+}) => {
+    const classes = () => ["serial-section", ...style].join(" ");
+    const [loggerData, setLoggerData] = useState<string>();
+    const [printerImageUrls, setPrinterImageUrls] = useState<string[]>();
+    const loggerDataRef = useRef<string[]>([]);
+    const printerDataRef = useRef<string[]>([]);
+    const loggerRef = useRef<HTMLDivElement>(null);
+    const imagesRef = useRef<HTMLDivElement>(null);
+
+    useEffect(() => {
+        const onLoggerData = (data: Uint8Array) => {
+            const byte = data[0];
+            const charByte = String.fromCharCode(byte);
+            loggerDataRef.current.push(charByte);
+            setLoggerData(loggerDataRef.current.join(""));
+        };
+        const onPrinterData = (imageBuffer: Uint8Array) => {
+            const imageUrl = bufferToDataUrl(imageBuffer, 160);
+            printerDataRef.current.unshift(imageUrl);
+            setPrinterImageUrls([...printerDataRef.current]);
+        };
+
+        const onLogger = (emulator: Emulator, _params: unknown = {}) => {
+            const params = _params as Record<string, unknown>;
+            onLoggerData(params.data as Uint8Array);
+        };
+        const onPrinter = (emulator: Emulator, _params: unknown = {}) => {
+            const params = _params as Record<string, unknown>;
+            onPrinterData(params.imageBuffer as Uint8Array);
+        };
+
+        emulator.bind("logger", onLogger);
+        emulator.bind("printer", onPrinter);
+
+        return () => {
+            emulator.unbind("logger", onLogger);
+            emulator.unbind("printer", onPrinter);
+        };
+    }, []);
+
+    const onEngineChange = (option: string) => {
+        emulator.loadSerialDevice(option as SerialDevice);
+        const optionIcon = DEVICE_ICON[option] ?? "";
+        emulator.handlers.showToast?.(
+            `${optionIcon} ${option} attached to the serial port & active`
+        );
+    };
+
+    const getTabs = () => {
+        return [
+            <Info>
+                <Pair
+                    key="button-device"
+                    name={"Device"}
+                    valueNode={
+                        <ButtonSwitch
+                            options={["null", "logger", "printer"]}
+                            value={emulator.serialDevice}
+                            uppercase={true}
+                            size={"large"}
+                            style={["simple"]}
+                            onChange={onEngineChange}
+                        />
+                    }
+                />
+                <Pair key="baud-rate" name={"Baud Rate"} value={"1 KB/s"} />
+            </Info>,
+            <div className="logger" ref={loggerRef}>
+                <div className="logger-data">
+                    {loggerData || "Logger contents are empty."}
+                </div>
+            </div>,
+            <div className="printer" ref={imagesRef}>
+                <div className="printer-lines">
+                    {printerImageUrls ? (
+                        printerImageUrls.map((url, index) => (
+                            <img
+                                key={index}
+                                className="printer-line"
+                                src={url}
+                            />
+                        ))
+                    ) : (
+                        <span className="placeholder">
+                            Printer contents are empty.
+                        </span>
+                    )}
+                </div>
+            </div>
+        ];
+    };
+    const getTabNames = () => {
+        return ["Settings", "Logger", "Printer"];
+    };
+    return (
+        <div className={classes()}>
+            <PanelTab
+                tabs={getTabs()}
+                tabNames={getTabNames()}
+                selectors={true}
+            />
+        </div>
+    );
+};
+
+export default SerialSection;
diff --git a/frontends/web/react/components/tiles-gb/tiles-gb.css b/frontends/web/react/components/tiles-gb/tiles-gb.css
new file mode 100644
index 0000000000000000000000000000000000000000..28c5a4f09e98069aab32e108f5aa8a06e41b3ebb
--- /dev/null
+++ b/frontends/web/react/components/tiles-gb/tiles-gb.css
@@ -0,0 +1,14 @@
+.tiles-gb > .canvas {
+    background-color: #1b1a17;
+    border: 2px solid #50cb93;
+    padding: 8px 8px 8px 8px;
+}
+
+.tiles-gb > .canvas.content-box {
+    box-sizing: content-box;
+    -o-box-sizing: content-box;
+    -ms-box-sizing: content-box;
+    -moz-box-sizing: content-box;
+    -khtml-box-sizing: content-box;
+    -webkit-box-sizing: content-box;
+}
diff --git a/frontends/web/react/components/tiles-gb/tiles-gb.tsx b/frontends/web/react/components/tiles-gb/tiles-gb.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..826ee1a4309ff98f0679a1788f01b4be46a3902e
--- /dev/null
+++ b/frontends/web/react/components/tiles-gb/tiles-gb.tsx
@@ -0,0 +1,99 @@
+import React, { FC, useEffect, useRef } from "react";
+import { Canvas, CanvasStructure, PixelFormat } from "emukit";
+
+import "./tiles-gb.css";
+
+type TilesGBProps = {
+    getTile: (index: number) => Uint8Array;
+    tileCount: number;
+    width?: number | string;
+    contentBox?: boolean;
+    interval?: number;
+    style?: string[];
+};
+
+export const TilesGB: FC<TilesGBProps> = ({
+    getTile,
+    tileCount,
+    width,
+    contentBox = true,
+    interval = 1000,
+    style = []
+}) => {
+    const classes = () =>
+        ["tiles-gb", contentBox ? "content-box" : "", ...style].join(" ");
+    const intervalsRef = useRef<number>();
+    useEffect(() => {
+        return () => {
+            if (intervalsRef.current) {
+                clearInterval(intervalsRef.current);
+            }
+        };
+    }, []);
+    const onCanvas = (structure: CanvasStructure) => {
+        const drawTiles = () => {
+            for (let index = 0; index < tileCount; index++) {
+                const pixels = getTile(index);
+                drawTile(index, pixels, structure);
+            }
+        };
+        drawTiles();
+        intervalsRef.current = setInterval(() => drawTiles(), interval);
+    };
+    return (
+        <div className={classes()}>
+            <Canvas
+                width={128}
+                height={192}
+                scale={2}
+                scaledWidth={width}
+                onCanvas={onCanvas}
+            />
+        </div>
+    );
+};
+
+/**
+ * Draws the tile at the given index to the proper vertical
+ * offset in the given context and buffer.
+ *
+ * @param index The index of the sprite to be drawn.
+ * @param pixels Buffer of pixels that contains the RGB data
+ * that is going to be drawn.
+ * @param structure The canvas context to which the tile is
+ * growing to be drawn.
+ * @param format The pixel format of the sprite.
+ */
+const drawTile = (
+    index: number,
+    pixels: Uint8Array,
+    structure: CanvasStructure,
+    format: PixelFormat = PixelFormat.RGB
+) => {
+    const line = Math.floor(index / 16);
+    const column = index % 16;
+    let offset =
+        (line * structure.canvasOffScreen.width * 8 + column * 8) *
+        PixelFormat.RGBA;
+    let counter = 0;
+    for (let i = 0; i < pixels.length; i += format) {
+        const color =
+            (pixels[i] << 24) |
+            (pixels[i + 1] << 16) |
+            (pixels[i + 2] << 8) |
+            (format === PixelFormat.RGBA ? pixels[i + 3] : 0xff);
+        structure.canvasBuffer.setUint32(offset, color);
+
+        counter++;
+        if (counter === 8) {
+            counter = 0;
+            offset += (structure.canvasOffScreen.width - 7) * PixelFormat.RGBA;
+        } else {
+            offset += PixelFormat.RGBA;
+        }
+    }
+    structure.canvasOffScreenContext.putImageData(structure.canvasImage, 0, 0);
+    structure.canvasContext.drawImage(structure.canvasOffScreen, 0, 0);
+};
+
+export default TilesGB;
diff --git a/frontends/web/res/serial.svg b/frontends/web/res/serial.svg
new file mode 100644
index 0000000000000000000000000000000000000000..578d6cb83ce6ee6cb657df805dfbb43e22df1f01
--- /dev/null
+++ b/frontends/web/res/serial.svg
@@ -0,0 +1 @@
+<svg width="48px" height="48px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-labelledby="swapHorizontalIconTitle" stroke="#ffffff" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" fill="none" color="#ffffff"> <title id="swapHorizontalIconTitle">Swap items (horizontally)</title> <path d="M16 4L19 7L16 10"/> <path d="M4 7L18 7"/> <path d="M7 20L4 17L7 14"/> <path d="M19 17L5 17"/> </svg>
\ No newline at end of file
diff --git a/frontends/web/ts/gb.ts b/frontends/web/ts/gb.ts
index dcde0e67798a246778a73e203c21ef4afb77f7a8..f223f000b5e9b2c4e2a6ad7c7019d68242c8ab90 100644
--- a/frontends/web/ts/gb.ts
+++ b/frontends/web/ts/gb.ts
@@ -1,8 +1,10 @@
 import {
     AudioSpecs,
     BenchmarkResult,
+    SectionInfo,
     Compilation,
     Compiler,
+    DebugPanel,
     Emulator,
     EmulatorBase,
     Entry,
@@ -16,7 +18,13 @@ import {
 } from "emukit";
 import { PALETTES, PALETTES_MAP } from "./palettes";
 import { base64ToBuffer, bufferToBase64 } from "./util";
-import { HelpFaqs, HelpKeyboard } from "../react";
+import {
+    DebugAudio,
+    DebugVideo,
+    HelpFaqs,
+    HelpKeyboard,
+    SerialSection
+} from "../react";
 
 import {
     Cartridge,
@@ -79,7 +87,17 @@ const KEYS_NAME: Record<string, number> = {
     B: PadKey.B
 };
 
-const ROM_PATH = require("../../../res/roms/pocket.gb");
+const ROM_PATH = require("../../../res/roms/demo/pocket.gb");
+
+/**
+ * Enumeration with the values for the complete set of available
+ * serial devices that can be used in the emulator.
+ */
+export enum SerialDevice {
+    Null = "null",
+    Logger = "logger",
+    Printer = "printer"
+}
 
 /**
  * Top level class that controls the emulator behaviour
@@ -116,6 +134,8 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
     private romSize = 0;
     private cartridge: Cartridge | null = null;
 
+    private _serialDevice: SerialDevice = SerialDevice.Null;
+
     /**
      * Associative map for extra settings to be used in
      * opaque local storage operations, associated setting
@@ -389,6 +409,10 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
         this.gameBoy.load_boot_default();
         const cartridge = this.gameBoy.load_rom_ws(romData);
 
+        // in case there's a serial device involved tries to load
+        // it and initialize for the current Game Boy machine
+        this.loadSerialDevice();
+
         // updates the name of the currently selected engine
         // to the one that has been provided (logic change)
         if (engine) this._engine = engine;
@@ -463,6 +487,18 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
         ];
     }
 
+    get sections(): SectionInfo[] {
+        return [
+            {
+                name: "Serial",
+                icon: require("../res/serial.svg"),
+                node: SerialSection({
+                    emulator: this
+                })
+            }
+        ];
+    }
+
     get help(): HelpPanel[] {
         return [
             {
@@ -476,6 +512,19 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
         ];
     }
 
+    get debug(): DebugPanel[] {
+        return [
+            {
+                name: "Video",
+                node: DebugVideo({ emulator: this })
+            },
+            {
+                name: "Audio",
+                node: DebugAudio({ emulator: this })
+            }
+        ];
+    }
+
     get engines(): string[] {
         return ["neo"];
     }
@@ -485,7 +534,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
     }
 
     get romExts(): string[] {
-        return ["gb"];
+        return ["gb", "gbc"];
     }
 
     get pixelFormat(): PixelFormat {
@@ -548,6 +597,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
     set frequency(value: number) {
         value = Math.max(value, 0);
         this.logicFrequency = value;
+        this.gameBoy?.set_clock_freq(value);
         this.trigger("frequency", value);
     }
 
@@ -562,22 +612,22 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
     get compiler(): Compiler | null {
         if (!this.gameBoy) return null;
         return {
-            name: this.gameBoy.get_compiler(),
-            version: this.gameBoy.get_compiler_version()
+            name: this.gameBoy.compiler(),
+            version: this.gameBoy.compiler_version()
         };
     }
 
     get compilation(): Compilation | null {
         if (!this.gameBoy) return null;
         return {
-            date: this.gameBoy.get_compilation_date(),
-            time: this.gameBoy.get_compilation_time()
+            date: this.gameBoy.compilation_date(),
+            time: this.gameBoy.compilation_time()
         };
     }
 
     get wasmEngine(): string | null {
         if (!this.gameBoy) return null;
-        return this.gameBoy.get_wasm_engine_ws() ?? null;
+        return this.gameBoy.wasm_engine_ws() ?? null;
     }
 
     get framerate(): number {
@@ -606,6 +656,18 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
         };
     }
 
+    get audioOutput(): Record<string, number> {
+        const output = this.gameBoy?.audio_all_output();
+        if (!output) return {};
+        return {
+            master: output[0],
+            ch1: output[1],
+            ch2: output[2],
+            ch3: output[3],
+            ch4: output[4]
+        };
+    }
+
     get palette(): string | undefined {
         const paletteObj = PALETTES[this.paletteIndex];
         return paletteObj.name;
@@ -618,6 +680,14 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
         this.updatePalette();
     }
 
+    get serialDevice(): SerialDevice {
+        return this._serialDevice;
+    }
+
+    set serialDevice(value: SerialDevice) {
+        this._serialDevice = value;
+    }
+
     toggleRunning() {
         if (this.paused) {
             this.resume();
@@ -660,7 +730,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
     }
 
     getVideoState(): boolean {
-        return this.gameBoy?.get_ppu_enabled() ?? false;
+        return this.gameBoy?.ppu_enabled() ?? false;
     }
 
     pauseAudio() {
@@ -672,7 +742,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
     }
 
     getAudioState(): boolean {
-        return this.gameBoy?.get_apu_enabled() ?? false;
+        return this.gameBoy?.apu_enabled() ?? false;
     }
 
     getTile(index: number): Uint8Array {
@@ -712,6 +782,46 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
         this.storeSettings();
     }
 
+    loadSerialDevice(device?: SerialDevice) {
+        device = device ?? this.serialDevice;
+        switch (device) {
+            case SerialDevice.Null:
+                this.loadNullDevice();
+                break;
+
+            case SerialDevice.Logger:
+                this.loadLoggerDevice();
+                break;
+
+            case SerialDevice.Printer:
+                this.loadPrinterDevice();
+                break;
+        }
+    }
+
+    loadNullDevice(set = true) {
+        this.gameBoy?.load_null_ws();
+        if (set) this.serialDevice = SerialDevice.Null;
+    }
+
+    loadLoggerDevice(set = true) {
+        this.gameBoy?.load_logger_ws();
+        if (set) this.serialDevice = SerialDevice.Logger;
+    }
+
+    loadPrinterDevice(set = true) {
+        this.gameBoy?.load_printer_ws();
+        if (set) this.serialDevice = SerialDevice.Printer;
+    }
+
+    onLoggerDevice(data: Uint8Array) {
+        this.trigger("logger", { data: data });
+    }
+
+    onPrinterDevice(imageBuffer: Uint8Array) {
+        this.trigger("printer", { imageBuffer: imageBuffer });
+    }
+
     /**
      * Tries to load game RAM from the `localStorage` using the
      * current cartridge title as the name of the item and
@@ -780,7 +890,14 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
 
 declare global {
     interface Window {
+        emulator: GameboyEmulator;
         panic: (message: string) => void;
+        loggerCallback: (data: Uint8Array) => void;
+        printerCallback: (imageBuffer: Uint8Array) => void;
+    }
+
+    interface Console {
+        image(url: string, size?: number): void;
     }
 }
 
@@ -788,6 +905,19 @@ window.panic = (message: string) => {
     console.error(message);
 };
 
+window.loggerCallback = (data: Uint8Array) => {
+    window.emulator.onLoggerDevice(data);
+};
+
+window.printerCallback = (imageBuffer: Uint8Array) => {
+    window.emulator.onPrinterDevice(imageBuffer);
+};
+
+console.image = (url: string, size = 80) => {
+    const style = `font-size: ${size}px; background-image: url("${url}"); background-size: contain; background-repeat: no-repeat;`;
+    console.log("%c     ", style);
+};
+
 const wasm = async () => {
     await _wasm();
     GameBoy.set_panic_hook_ws();
diff --git a/frontends/web/ts/util.ts b/frontends/web/ts/util.ts
index 37a00f53f35e731290ed7797421d93b7b6e12b81..c07d7ea99c1723db265bfab2bb2b0bb6d91e6e3d 100644
--- a/frontends/web/ts/util.ts
+++ b/frontends/web/ts/util.ts
@@ -15,3 +15,30 @@ export const base64ToBuffer = (base64: string) => {
     const buffer = new Uint8Array(array);
     return buffer;
 };
+
+export const bufferToImageData = (buffer: Uint8Array, width: number) => {
+    const clampedBuffer = new Uint8ClampedArray(buffer.length);
+
+    for (let index = 0; index < clampedBuffer.length; index += 4) {
+        clampedBuffer[index + 0] = buffer[index];
+        clampedBuffer[index + 1] = buffer[index + 1];
+        clampedBuffer[index + 2] = buffer[index + 2];
+        clampedBuffer[index + 3] = buffer[index + 3];
+    }
+
+    return new ImageData(clampedBuffer, width);
+};
+
+export const bufferToDataUrl = (buffer: Uint8Array, width: number) => {
+    const imageData = bufferToImageData(buffer, width);
+
+    const canvas = document.createElement("canvas");
+    canvas.width = imageData.width;
+    canvas.height = imageData.height;
+
+    const context = canvas.getContext("2d");
+    context?.putImageData(imageData, 0, 0);
+
+    const dataUrl = canvas.toDataURL();
+    return dataUrl;
+};
diff --git a/res/roms/_headers b/res/roms/_headers
deleted file mode 100644
index a292ad706e3c35f1cae544006acd43d1e72c119b..0000000000000000000000000000000000000000
--- a/res/roms/_headers
+++ /dev/null
@@ -1,4 +0,0 @@
-
-/*
-  X-Robots-Tag: all
-  Access-Control-Allow-Origin: *
diff --git a/res/roms/20y.gb b/res/roms/demo/20y.gb
similarity index 100%
rename from res/roms/20y.gb
rename to res/roms/demo/20y.gb
diff --git a/res/roms/gejmboj.gb b/res/roms/demo/gejmboj.gb
similarity index 100%
rename from res/roms/gejmboj.gb
rename to res/roms/demo/gejmboj.gb
diff --git a/res/roms/pocket.gb b/res/roms/demo/pocket.gb
similarity index 100%
rename from res/roms/pocket.gb
rename to res/roms/demo/pocket.gb
diff --git a/res/roms/opus5.gb b/res/roms/game/opus5.gb
similarity index 100%
rename from res/roms/opus5.gb
rename to res/roms/game/opus5.gb
diff --git a/res/roms/shocklobster.gb b/res/roms/game/shocklobster.gb
similarity index 100%
rename from res/roms/shocklobster.gb
rename to res/roms/game/shocklobster.gb
diff --git a/res/roms/thebouncingball.gb b/res/roms/game/thebouncingball.gb
similarity index 100%
rename from res/roms/thebouncingball.gb
rename to res/roms/game/thebouncingball.gb
diff --git a/res/roms/robots.txt b/res/roms/robots.txt
deleted file mode 100644
index a78466b3d66c7386c0ccc2293a67bccf4b6c85ff..0000000000000000000000000000000000000000
--- a/res/roms/robots.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-User-agent: *
-Allow: /
diff --git a/res/roms/test/blargg/cgb_sound/cgb_sound.gb b/res/roms/test/blargg/cgb_sound/cgb_sound.gb
new file mode 100644
index 0000000000000000000000000000000000000000..dc50471b00315edade129fc438517975a7036465
Binary files /dev/null and b/res/roms/test/blargg/cgb_sound/cgb_sound.gb differ
diff --git a/res/roms/paradius/cpu/01-special.gb b/res/roms/test/blargg/cpu/01-special.gb
similarity index 100%
rename from res/roms/paradius/cpu/01-special.gb
rename to res/roms/test/blargg/cpu/01-special.gb
diff --git a/res/roms/paradius/cpu/02-interrupts.gb b/res/roms/test/blargg/cpu/02-interrupts.gb
similarity index 100%
rename from res/roms/paradius/cpu/02-interrupts.gb
rename to res/roms/test/blargg/cpu/02-interrupts.gb
diff --git a/res/roms/paradius/cpu/03-op sp,hl.gb b/res/roms/test/blargg/cpu/03-op sp,hl.gb
similarity index 100%
rename from res/roms/paradius/cpu/03-op sp,hl.gb
rename to res/roms/test/blargg/cpu/03-op sp,hl.gb
diff --git a/res/roms/paradius/cpu/04-op r,imm.gb b/res/roms/test/blargg/cpu/04-op r,imm.gb
similarity index 100%
rename from res/roms/paradius/cpu/04-op r,imm.gb
rename to res/roms/test/blargg/cpu/04-op r,imm.gb
diff --git a/res/roms/paradius/cpu/05-op rp.gb b/res/roms/test/blargg/cpu/05-op rp.gb
similarity index 100%
rename from res/roms/paradius/cpu/05-op rp.gb
rename to res/roms/test/blargg/cpu/05-op rp.gb
diff --git a/res/roms/paradius/cpu/06-ld r,r.gb b/res/roms/test/blargg/cpu/06-ld r,r.gb
similarity index 100%
rename from res/roms/paradius/cpu/06-ld r,r.gb
rename to res/roms/test/blargg/cpu/06-ld r,r.gb
diff --git a/res/roms/paradius/cpu/07-jr,jp,call,ret,rst.gb b/res/roms/test/blargg/cpu/07-jr,jp,call,ret,rst.gb
similarity index 100%
rename from res/roms/paradius/cpu/07-jr,jp,call,ret,rst.gb
rename to res/roms/test/blargg/cpu/07-jr,jp,call,ret,rst.gb
diff --git a/res/roms/paradius/cpu/08-misc instrs.gb b/res/roms/test/blargg/cpu/08-misc instrs.gb
similarity index 100%
rename from res/roms/paradius/cpu/08-misc instrs.gb
rename to res/roms/test/blargg/cpu/08-misc instrs.gb
diff --git a/res/roms/paradius/cpu/09-op r,r.gb b/res/roms/test/blargg/cpu/09-op r,r.gb
similarity index 100%
rename from res/roms/paradius/cpu/09-op r,r.gb
rename to res/roms/test/blargg/cpu/09-op r,r.gb
diff --git a/res/roms/paradius/cpu/10-bit ops.gb b/res/roms/test/blargg/cpu/10-bit ops.gb
similarity index 100%
rename from res/roms/paradius/cpu/10-bit ops.gb
rename to res/roms/test/blargg/cpu/10-bit ops.gb
diff --git a/res/roms/paradius/cpu/11-op a,(hl).gb b/res/roms/test/blargg/cpu/11-op a,(hl).gb
similarity index 100%
rename from res/roms/paradius/cpu/11-op a,(hl).gb
rename to res/roms/test/blargg/cpu/11-op a,(hl).gb
diff --git a/res/roms/paradius/cpu/cpu_instrs.gb b/res/roms/test/blargg/cpu/cpu_instrs.gb
similarity index 100%
rename from res/roms/paradius/cpu/cpu_instrs.gb
rename to res/roms/test/blargg/cpu/cpu_instrs.gb
diff --git a/res/roms/test/blargg/dmg_sound/dmg_sound.gb b/res/roms/test/blargg/dmg_sound/dmg_sound.gb
new file mode 100644
index 0000000000000000000000000000000000000000..fe9131044031c45f404642f5dc8a9ec43ecd2065
Binary files /dev/null and b/res/roms/test/blargg/dmg_sound/dmg_sound.gb differ
diff --git a/res/roms/test/blargg/halt_bug/halt_bug.gb b/res/roms/test/blargg/halt_bug/halt_bug.gb
new file mode 100644
index 0000000000000000000000000000000000000000..38e36625d805e68cb3b8cbf562d6dff500060bdd
Binary files /dev/null and b/res/roms/test/blargg/halt_bug/halt_bug.gb differ
diff --git a/res/roms/paradius/instr_timing/instr_timing.gb b/res/roms/test/blargg/instr_timing/instr_timing.gb
similarity index 100%
rename from res/roms/paradius/instr_timing/instr_timing.gb
rename to res/roms/test/blargg/instr_timing/instr_timing.gb
diff --git a/res/roms/paradius/interrupt_time/interrupt_time.gb b/res/roms/test/blargg/interrupt_time/interrupt_time.gb
similarity index 100%
rename from res/roms/paradius/interrupt_time/interrupt_time.gb
rename to res/roms/test/blargg/interrupt_time/interrupt_time.gb
diff --git a/res/roms/test/blargg/mem_timing-2/mem_timing.gb b/res/roms/test/blargg/mem_timing-2/mem_timing.gb
new file mode 100644
index 0000000000000000000000000000000000000000..2665aa2d4f2b613b79eceef1a3365d238f919bb8
Binary files /dev/null and b/res/roms/test/blargg/mem_timing-2/mem_timing.gb differ
diff --git a/res/roms/paradius/mem_timing/mem_timing.gb b/res/roms/test/blargg/mem_timing/mem_timing.gb
similarity index 100%
rename from res/roms/paradius/mem_timing/mem_timing.gb
rename to res/roms/test/blargg/mem_timing/mem_timing.gb
diff --git a/res/roms/paradius/oam_bug/oam_bug.gb b/res/roms/test/blargg/oam_bug/oam_bug.gb
similarity index 100%
rename from res/roms/paradius/oam_bug/oam_bug.gb
rename to res/roms/test/blargg/oam_bug/oam_bug.gb
diff --git a/res/roms/dmg_acid2.gb b/res/roms/test/dmg_acid2.gb
similarity index 100%
rename from res/roms/dmg_acid2.gb
rename to res/roms/test/dmg_acid2.gb
diff --git a/res/roms/firstwhite.gb b/res/roms/test/firstwhite.gb
similarity index 100%
rename from res/roms/firstwhite.gb
rename to res/roms/test/firstwhite.gb
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/res/roms/test/jayro.gb b/res/roms/test/jayro.gb
new file mode 100644
index 0000000000000000000000000000000000000000..23960ad207779c4a2879f22277b844bdf6fc8325
Binary files /dev/null and b/res/roms/test/jayro.gb differ
diff --git a/res/roms/rtc3test.gb b/res/roms/test/rtc3test.gb
similarity index 100%
rename from res/roms/rtc3test.gb
rename to res/roms/test/rtc3test.gb
diff --git a/res/roms/sprite_priority.gb b/res/roms/test/sprite_priority.gb
similarity index 100%
rename from res/roms/sprite_priority.gb
rename to res/roms/test/sprite_priority.gb
diff --git a/res/videos/003-boot.gif b/res/videos/003-boot.gif
new file mode 100644
index 0000000000000000000000000000000000000000..0c1fbe45b4de73dd44c2950c52194506aeef026a
Binary files /dev/null and b/res/videos/003-boot.gif differ
diff --git a/res/videos/004-tetris.gif b/res/videos/004-tetris.gif
new file mode 100644
index 0000000000000000000000000000000000000000..befb6a950d9c3898b4e685f45df50d89233b71ce
Binary files /dev/null and b/res/videos/004-tetris.gif differ
diff --git a/src/apu.rs b/src/apu.rs
index deb08753c310f2060c07e8b1b0d3e9c2fa75c67c..07dc230b2ee3fda256092154b4047468e0eff7c6 100644
--- a/src/apu.rs
+++ b/src/apu.rs
@@ -1,6 +1,6 @@
 use std::collections::VecDeque;
 
-use crate::warnln;
+use crate::{gb::GameBoy, warnln};
 
 const DUTY_TABLE: [[u8; 8]; 4] = [
     [0, 0, 0, 0, 0, 0, 0, 1],
@@ -9,6 +9,8 @@ const DUTY_TABLE: [[u8; 8]; 4] = [
     [0, 1, 1, 1, 1, 1, 1, 0],
 ];
 
+const CH4_DIVISORS: [u8; 8] = [8, 16, 32, 48, 64, 80, 96, 112];
+
 pub enum Channel {
     Ch1,
     Ch2,
@@ -17,7 +19,7 @@ pub enum Channel {
 }
 
 pub struct Apu {
-    ch1_timer: u16,
+    ch1_timer: i16,
     ch1_sequence: u8,
     ch1_envelope_sequence: u8,
     ch1_envelope_enabled: bool,
@@ -35,7 +37,7 @@ pub struct Apu {
     ch1_length_stop: bool,
     ch1_enabled: bool,
 
-    ch2_timer: u16,
+    ch2_timer: i16,
     ch2_sequence: u8,
     ch2_envelope_sequence: u8,
     ch2_envelope_enabled: bool,
@@ -49,7 +51,7 @@ pub struct Apu {
     ch2_length_stop: bool,
     ch2_enabled: bool,
 
-    ch3_timer: u16,
+    ch3_timer: i16,
     ch3_position: u8,
     ch3_output: u8,
     ch3_dac: bool,
@@ -59,21 +61,44 @@ pub struct Apu {
     ch3_length_stop: bool,
     ch3_enabled: bool,
 
+    ch4_timer: i16,
+    ch4_envelope_sequence: u8,
+    ch4_envelope_enabled: bool,
+    ch4_output: u8,
+    ch4_length_timer: u8,
+    ch4_pace: u8,
+    ch4_direction: u8,
+    ch4_volume: u8,
+    ch4_divisor: u8,
+    ch4_width_mode: bool,
+    ch4_clock_shift: u8,
+    ch4_lfsr: u16,
+    ch4_length_stop: bool,
+    ch4_enabled: bool,
+
+    glob_panning: u8,
+
     right_enabled: bool,
     left_enabled: bool,
+    ch1_out_enabled: bool,
+    ch2_out_enabled: bool,
+    ch3_out_enabled: bool,
+    ch4_out_enabled: bool,
 
     wave_ram: [u8; 16],
 
     sampling_rate: u16,
     sequencer: u16,
     sequencer_step: u8,
-    output_timer: u16,
+    output_timer: i16,
     audio_buffer: VecDeque<u8>,
     audio_buffer_max: usize,
+
+    clock_freq: u32,
 }
 
 impl Apu {
-    pub fn new(sampling_rate: u16, buffer_size: f32) -> Self {
+    pub fn new(sampling_rate: u16, buffer_size: f32, clock_freq: u32) -> Self {
         Self {
             ch1_timer: 0,
             ch1_sequence: 0,
@@ -117,11 +142,39 @@ impl Apu {
             ch3_length_stop: false,
             ch3_enabled: false,
 
+            ch4_timer: 0,
+            ch4_envelope_sequence: 0,
+            ch4_envelope_enabled: false,
+            ch4_output: 0,
+            ch4_length_timer: 0x0,
+            ch4_pace: 0x0,
+            ch4_direction: 0x0,
+            ch4_volume: 0x0,
+            ch4_divisor: 0x0,
+            ch4_width_mode: false,
+            ch4_clock_shift: 0x0,
+            ch4_lfsr: 0x0,
+            ch4_length_stop: false,
+            ch4_enabled: false,
+
+            glob_panning: 0x0,
+
             left_enabled: true,
             right_enabled: true,
 
+            ch1_out_enabled: true,
+            ch2_out_enabled: true,
+            ch3_out_enabled: true,
+            ch4_out_enabled: true,
+
+            /// The RAM that is used to sore the wave information
+            /// to be used in channel 3 audio
             wave_ram: [0u8; 16],
 
+            /// The rate at which audio samples are going to be
+            /// taken, ideally this value should be aligned with
+            /// the sampling rate of the output device. A typical
+            /// sampling rate would be of 44.1kHz.
             sampling_rate,
 
             /// Internal sequencer counter that runs at 512Hz
@@ -133,6 +186,7 @@ impl Apu {
                 (sampling_rate as f32 * buffer_size) as usize * 2,
             ),
             audio_buffer_max: (sampling_rate as f32 * buffer_size) as usize * 2,
+            clock_freq,
         }
     }
 
@@ -179,6 +233,23 @@ impl Apu {
         self.ch3_length_stop = false;
         self.ch3_enabled = false;
 
+        self.ch4_timer = 0;
+        self.ch4_envelope_sequence = 0;
+        self.ch4_envelope_enabled = false;
+        self.ch4_output = 0;
+        self.ch4_length_timer = 0x0;
+        self.ch4_pace = 0x0;
+        self.ch4_direction = 0x0;
+        self.ch4_volume = 0x0;
+        self.ch4_divisor = 0x0;
+        self.ch4_width_mode = false;
+        self.ch4_clock_shift = 0x0;
+        self.ch4_lfsr = 0x0;
+        self.ch4_length_stop = false;
+        self.ch4_enabled = false;
+
+        self.glob_panning = 0x0;
+
         self.left_enabled = true;
         self.right_enabled = true;
 
@@ -190,16 +261,73 @@ impl Apu {
     }
 
     pub fn clock(&mut self, cycles: u8) {
-        // @TODO the performance here requires improvement
-        for _ in 0..cycles {
-            self.tick();
+        self.sequencer += cycles as u16;
+        if self.sequencer >= 8192 {
+            // each of these steps runs at 512/8 Hz = 64Hz,
+            // meaning a complete loop runs at 512 Hz
+            match self.sequencer_step {
+                0 => {
+                    self.tick_length_all();
+                }
+                1 => (),
+                2 => {
+                    self.tick_ch1_sweep();
+                    self.tick_length_all();
+                }
+                3 => (),
+                4 => {
+                    self.tick_length_all();
+                }
+                5 => (),
+                6 => {
+                    self.tick_ch1_sweep();
+                    self.tick_length_all();
+                }
+                7 => {
+                    self.tick_envelope_all();
+                }
+                _ => (),
+            }
+
+            self.sequencer -= 8192;
+            self.sequencer_step = (self.sequencer_step + 1) & 7;
+        }
+
+        self.tick_ch_all(cycles);
+
+        self.output_timer = self.output_timer.saturating_sub(cycles as i16);
+        if self.output_timer <= 0 {
+            // verifies if we've reached the maximum allowed size for the
+            // audio buffer and if that's the case an item is removed from
+            // the buffer (avoiding overflow) and then then the new audio
+            // volume item is added to the queue
+            if self.audio_buffer.len() >= self.audio_buffer_max {
+                self.audio_buffer.pop_front();
+                self.audio_buffer.pop_front();
+            }
+            if self.left_enabled {
+                self.audio_buffer.push_back(self.output());
+            }
+            if self.right_enabled {
+                self.audio_buffer.push_back(self.output());
+            }
+
+            // calculates the rate at which a new audio sample should be
+            // created based on the (base/CPU) clock frequency and the
+            // sampling rate, this is basically the amount of APU clock
+            // calls that should be performed until an audio sample is created
+            self.output_timer += (self.clock_freq as f32 / self.sampling_rate as f32) as i16;
         }
     }
 
     pub fn read(&mut self, addr: u16) -> u8 {
-        {
-            warnln!("Reading from unknown APU location 0x{:04x}", addr);
-            0xff
+        match addr {
+            // 0xFF25 — NR51: Sound panning
+            0xff25 => self.glob_panning,
+            _ => {
+                warnln!("Reading from unknown APU location 0x{:04x}", addr);
+                0xff
+            }
         }
     }
 
@@ -231,10 +359,18 @@ impl Apu {
             }
             // 0xFF14 — NR14: Channel 1 wavelength high & control
             0xff14 => {
+                let length_trigger = value & 0x40 == 0x40;
+                let trigger = value & 0x80 == 0x80;
                 self.ch1_wave_length =
                     (self.ch1_wave_length & 0x00ff) | (((value & 0x07) as u16) << 8);
-                self.ch1_length_stop |= value & 0x40 == 0x40;
+                self.ch1_length_stop = value & 0x40 == 0x40;
                 self.ch1_enabled |= value & 0x80 == 0x80;
+                if trigger {
+                    self.trigger_ch1();
+                }
+                if (length_trigger || trigger) && self.ch1_length_timer == 0 {
+                    self.ch1_length_timer = 0;
+                }
             }
 
             // 0xFF16 — NR21: Channel 2 length timer & duty cycle
@@ -254,10 +390,18 @@ impl Apu {
             }
             // 0xFF19 — NR24: Channel 2 wavelength high & control
             0xff19 => {
+                let length_trigger = value & 0x40 == 0x40;
+                let trigger = value & 0x80 == 0x80;
                 self.ch2_wave_length =
                     (self.ch2_wave_length & 0x00ff) | (((value & 0x07) as u16) << 8);
-                self.ch2_length_stop |= value & 0x40 == 0x40;
-                self.ch2_enabled |= value & 0x80 == 0x80;
+                self.ch2_length_stop = length_trigger;
+                self.ch2_enabled |= trigger;
+                if trigger {
+                    self.trigger_ch2();
+                }
+                if (length_trigger || trigger) && self.ch2_length_timer == 0 {
+                    self.ch2_length_timer = 0;
+                }
             }
 
             // 0xFF1A — NR30: Channel 3 DAC enable
@@ -278,10 +422,63 @@ impl Apu {
             }
             // 0xFF1E — NR34: Channel 3 wavelength high & control
             0xff1e => {
+                let length_trigger = value & 0x40 == 0x40;
+                let trigger = value & 0x80 == 0x80;
                 self.ch3_wave_length =
                     (self.ch3_wave_length & 0x00ff) | (((value & 0x07) as u16) << 8);
-                self.ch3_length_stop |= value & 0x40 == 0x40;
-                self.ch3_enabled |= value & 0x80 == 0x80;
+                self.ch3_length_stop = length_trigger;
+                self.ch3_enabled |= trigger;
+                if trigger {
+                    self.trigger_ch3();
+                }
+                if (length_trigger || trigger) && self.ch3_length_timer == 0 {
+                    self.ch3_length_timer = 0;
+                }
+            }
+
+            // 0xFF20 — NR41: Channel 4 length timer
+            0xff20 => {
+                self.ch4_length_timer = value & 0x3f;
+            }
+            // 0xFF21 — NR42: Channel 4 volume & envelope
+            0xff21 => {
+                self.ch4_pace = value & 0x07;
+                self.ch4_direction = (value & 0x08) >> 3;
+                self.ch4_volume = (value & 0xf0) >> 4;
+                self.ch4_envelope_enabled = self.ch4_pace > 0;
+                self.ch4_envelope_sequence = 0;
+            }
+            // 0xFF22 — NR43: Channel 4 frequency & randomness
+            0xff22 => {
+                self.ch4_divisor = value & 0x07;
+                self.ch4_width_mode = value & 0x08 == 0x08;
+                self.ch4_clock_shift = (value & 0xf0) >> 4;
+            }
+            // 0xFF23 — NR44: Channel 4 control
+            0xff23 => {
+                let length_trigger = value & 0x40 == 0x40;
+                let trigger = value & 0x80 == 0x80;
+                self.ch4_length_stop = length_trigger;
+                self.ch4_enabled |= trigger;
+                if trigger {
+                    self.trigger_ch4();
+                }
+                if (length_trigger || trigger) && self.ch4_length_timer == 0 {
+                    self.ch4_length_timer = 0;
+                }
+            }
+
+            // 0xFF24 — NR50: Master volume & VIN panning
+            0xff24 => {
+                //@TODO: Implement master volume & VIN panning
+            }
+            // 0xFF25 — NR51: Sound panning
+            0xff25 => {
+                self.glob_panning = value;
+            }
+            // 0xFF26 — NR52: Sound on/off
+            0xff26 => {
+                //@TODO: Implement sound on/off
             }
 
             // 0xFF30-0xFF3F — Wave pattern RAM
@@ -293,8 +490,61 @@ impl Apu {
         }
     }
 
+    #[inline(always)]
     pub fn output(&self) -> u8 {
-        self.ch1_output + self.ch2_output + self.ch3_output
+        self.ch1_output() + self.ch2_output() + self.ch3_output() + self.ch4_output()
+    }
+
+    #[inline(always)]
+    pub fn ch1_output(&self) -> u8 {
+        if self.ch1_out_enabled {
+            self.ch1_output
+        } else {
+            0
+        }
+    }
+
+    #[inline(always)]
+    pub fn ch2_output(&self) -> u8 {
+        if self.ch2_out_enabled {
+            self.ch2_output
+        } else {
+            0
+        }
+    }
+
+    #[inline(always)]
+    pub fn ch3_output(&self) -> u8 {
+        if self.ch3_out_enabled {
+            self.ch3_output
+        } else {
+            0
+        }
+    }
+
+    #[inline(always)]
+    pub fn ch4_output(&self) -> u8 {
+        if self.ch4_out_enabled {
+            self.ch4_output
+        } else {
+            0
+        }
+    }
+
+    pub fn set_ch1_enabled(&mut self, enabled: bool) {
+        self.ch1_out_enabled = enabled;
+    }
+
+    pub fn set_ch2_enabled(&mut self, enabled: bool) {
+        self.ch2_out_enabled = enabled;
+    }
+
+    pub fn set_ch3_enabled(&mut self, enabled: bool) {
+        self.ch3_out_enabled = enabled;
+    }
+
+    pub fn set_ch4_enabled(&mut self, enabled: bool) {
+        self.ch4_out_enabled = enabled;
     }
 
     pub fn audio_buffer(&self) -> &VecDeque<u8> {
@@ -309,63 +559,12 @@ impl Apu {
         self.audio_buffer.clear();
     }
 
-    #[inline(always)]
-    fn tick(&mut self) {
-        self.sequencer += 1;
-        if self.sequencer >= 8192 {
-            // each of these steps runs at 512/8 Hz = 64Hz,
-            // meaning a complete loop runs at 512 Hz
-            match self.sequencer_step {
-                0 => {
-                    self.tick_length_all();
-                }
-                1 => (),
-                2 => {
-                    self.tick_ch1_sweep();
-                    self.tick_length_all();
-                }
-                3 => (),
-                4 => {
-                    self.tick_length_all();
-                }
-                5 => (),
-                6 => {
-                    self.tick_ch1_sweep();
-                    self.tick_length_all();
-                }
-                7 => {
-                    self.tick_envelope_all();
-                }
-                _ => (),
-            }
-
-            self.sequencer = 0;
-            self.sequencer_step = (self.sequencer_step + 1) & 7;
-        }
-
-        self.tick_ch_all();
-
-        self.output_timer = self.output_timer.saturating_sub(1);
-        if self.output_timer == 0 {
-            // verifies if we've reached the maximum allowed size for the
-            // audio buffer and if that's the case an item is removed from
-            // the buffer (avoiding overflow) and then then the new audio
-            // volume item is added to the queue
-            if self.audio_buffer.len() >= self.audio_buffer_max {
-                self.audio_buffer.pop_front();
-                self.audio_buffer.pop_front();
-            }
-            if self.left_enabled {
-                self.audio_buffer.push_back(self.output());
-            }
-            if self.right_enabled {
-                self.audio_buffer.push_back(self.output());
-            }
+    pub fn clock_freq(&self) -> u32 {
+        self.clock_freq
+    }
 
-            // @TODO the CPU clock is hardcoded here, we must handle situations
-            // where there's some kind of overclock
-            self.output_timer = (4194304.0 / self.sampling_rate as f32) as u16;
-        }
+    pub fn set_clock_freq(&mut self, value: u32) {
+        self.clock_freq = value;
     }
 
     #[inline(always)]
@@ -403,13 +602,21 @@ impl Apu {
                     self.ch3_length_timer = 0;
                 }
             }
-            Channel::Ch4 => (),
+            Channel::Ch4 => {
+                self.ch4_length_timer = self.ch4_length_timer.saturating_add(1);
+                if self.ch4_length_timer >= 64 {
+                    self.ch4_enabled = !self.ch4_length_stop;
+                    self.ch4_length_timer = 0;
+                }
+            }
         }
     }
 
     #[inline(always)]
     fn tick_envelope_all(&mut self) {
         self.tick_envelope(Channel::Ch1);
+        self.tick_envelope(Channel::Ch2);
+        self.tick_envelope(Channel::Ch4);
     }
 
     #[inline(always)]
@@ -450,7 +657,23 @@ impl Apu {
                 }
             }
             Channel::Ch3 => (),
-            Channel::Ch4 => (),
+            Channel::Ch4 => {
+                if !self.ch4_enabled || !self.ch4_envelope_enabled {
+                    return;
+                }
+                self.ch4_envelope_sequence += 1;
+                if self.ch4_envelope_sequence >= self.ch4_pace {
+                    if self.ch4_direction == 0x01 {
+                        self.ch4_volume = self.ch4_volume.saturating_add(1);
+                    } else {
+                        self.ch4_volume = self.ch4_volume.saturating_sub(1);
+                    }
+                    if self.ch4_volume == 0 || self.ch4_volume == 15 {
+                        self.ch4_envelope_enabled = false;
+                    }
+                    self.ch4_envelope_sequence = 0;
+                }
+            }
         }
     }
 
@@ -477,15 +700,16 @@ impl Apu {
     }
 
     #[inline(always)]
-    fn tick_ch_all(&mut self) {
-        self.tick_ch1();
-        self.tick_ch2();
-        self.tick_ch3();
+    fn tick_ch_all(&mut self, cycles: u8) {
+        self.tick_ch1(cycles);
+        self.tick_ch2(cycles);
+        self.tick_ch3(cycles);
+        self.tick_ch4(cycles);
     }
 
     #[inline(always)]
-    fn tick_ch1(&mut self) {
-        self.ch1_timer = self.ch1_timer.saturating_sub(1);
+    fn tick_ch1(&mut self, cycles: u8) {
+        self.ch1_timer = self.ch1_timer.saturating_sub(cycles as i16);
         if self.ch1_timer > 0 {
             return;
         }
@@ -501,13 +725,13 @@ impl Apu {
             self.ch1_output = 0;
         }
 
-        self.ch1_timer = (2048 - self.ch1_wave_length) << 2;
+        self.ch1_timer += ((2048 - self.ch1_wave_length) << 2) as i16;
         self.ch1_sequence = (self.ch1_sequence + 1) & 7;
     }
 
     #[inline(always)]
-    fn tick_ch2(&mut self) {
-        self.ch2_timer = self.ch2_timer.saturating_sub(1);
+    fn tick_ch2(&mut self, cycles: u8) {
+        self.ch2_timer = self.ch2_timer.saturating_sub(cycles as i16);
         if self.ch2_timer > 0 {
             return;
         }
@@ -523,18 +747,18 @@ impl Apu {
             self.ch2_output = 0;
         }
 
-        self.ch2_timer = (2048 - self.ch2_wave_length) << 2;
+        self.ch2_timer += ((2048 - self.ch2_wave_length) << 2) as i16;
         self.ch2_sequence = (self.ch2_sequence + 1) & 7;
     }
 
     #[inline(always)]
-    fn tick_ch3(&mut self) {
-        self.ch3_timer = self.ch3_timer.saturating_sub(1);
+    fn tick_ch3(&mut self, cycles: u8) {
+        self.ch3_timer = self.ch3_timer.saturating_sub(cycles as i16);
         if self.ch3_timer > 0 {
             return;
         }
 
-        if self.ch3_enabled {
+        if self.ch3_enabled && self.ch3_dac {
             let wave_index = self.ch3_position >> 1;
             let mut output = self.wave_ram[wave_index as usize];
             output = if (self.ch3_position & 0x01) == 0x01 {
@@ -552,13 +776,73 @@ impl Apu {
             self.ch3_output = 0;
         }
 
-        self.ch3_timer = (2048 - self.ch3_wave_length) << 1;
+        self.ch3_timer += ((2048 - self.ch3_wave_length) << 1) as i16;
         self.ch3_position = (self.ch3_position + 1) & 31;
     }
+
+    #[inline(always)]
+    fn tick_ch4(&mut self, cycles: u8) {
+        self.ch4_timer = self.ch4_timer.saturating_sub(cycles as i16);
+        if self.ch4_timer > 0 {
+            return;
+        }
+
+        if self.ch4_enabled {
+            // obtains the current value of the LFSR based as
+            // the XOR of the 1st and 2nd bit of the LFSR
+            let result = ((self.ch4_lfsr & 0x0001) ^ ((self.ch4_lfsr >> 1) & 0x0001)) == 0x0001;
+
+            // shifts the LFSR to the right and in case the
+            // value is positive sets the 15th bit to 1
+            self.ch4_lfsr >>= 1;
+            self.ch4_lfsr |= if result { 0x0001 << 14 } else { 0x0 };
+
+            // in case the short width mode (7 bits) is set then
+            // the 6th bit will be set to value of the 15th bit
+            if self.ch4_width_mode {
+                self.ch4_lfsr &= 0xbf;
+                self.ch4_lfsr |= if result { 0x40 } else { 0x00 };
+            }
+
+            self.ch4_output = if result { self.ch4_volume } else { 0 };
+        } else {
+            self.ch4_output = 0;
+        }
+
+        self.ch4_timer +=
+            ((CH4_DIVISORS[self.ch4_divisor as usize] as u16) << self.ch4_clock_shift) as i16;
+    }
+
+    #[inline(always)]
+    fn trigger_ch1(&mut self) {
+        self.ch1_timer = ((2048 - self.ch1_wave_length) << 2) as i16;
+        self.ch1_envelope_sequence = 0;
+        self.ch1_sweep_sequence = 0;
+    }
+
+    #[inline(always)]
+    fn trigger_ch2(&mut self) {
+        self.ch2_timer = ((2048 - self.ch2_wave_length) << 2) as i16;
+        self.ch2_envelope_sequence = 0;
+    }
+
+    #[inline(always)]
+    fn trigger_ch3(&mut self) {
+        self.ch3_timer = 3;
+        self.ch3_position = 0;
+    }
+
+    #[inline(always)]
+    fn trigger_ch4(&mut self) {
+        self.ch4_timer =
+            ((CH4_DIVISORS[self.ch4_divisor as usize] as u16) << self.ch4_clock_shift) as i16;
+        self.ch4_lfsr = 0x7ff1;
+        self.ch4_envelope_sequence = 0;
+    }
 }
 
 impl Default for Apu {
     fn default() -> Self {
-        Self::new(44100, 1.0)
+        Self::new(44100, 1.0, GameBoy::CPU_FREQ)
     }
 }
diff --git a/src/cpu.rs b/src/cpu.rs
index 237ef622b82478e4fc6f4433c1eaa051cfd5153c..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,
 };
 
@@ -127,7 +128,7 @@ impl Cpu {
             self.halted = false;
         }
 
-        if self.ime {
+        if self.ime && self.mmu.ie != 0x00 {
             // @TODO aggregate all of this interrupts in the MMU, as there's
             // a lot of redundant code involved in here which complicates the
             // readability and maybe performance of this code
@@ -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
@@ -427,7 +453,7 @@ impl Cpu {
     }
 
     #[inline(always)]
-    pub fn get_zero(&self) -> bool {
+    pub fn zero(&self) -> bool {
         self.zero
     }
 
@@ -437,7 +463,7 @@ impl Cpu {
     }
 
     #[inline(always)]
-    pub fn get_sub(&self) -> bool {
+    pub fn sub(&self) -> bool {
         self.sub
     }
 
@@ -447,7 +473,7 @@ impl Cpu {
     }
 
     #[inline(always)]
-    pub fn get_half_carry(&self) -> bool {
+    pub fn half_carry(&self) -> bool {
         self.half_carry
     }
 
@@ -457,7 +483,7 @@ impl Cpu {
     }
 
     #[inline(always)]
-    pub fn get_carry(&self) -> bool {
+    pub fn carry(&self) -> bool {
         self.carry
     }
 
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..048a83f7a78e61df5245d8aa03cd04524d8c8ba0
--- /dev/null
+++ b/src/devices/stdout.rs
@@ -0,0 +1,46 @@
+use std::io::{stdout, Write};
+
+use crate::serial::SerialDevice;
+
+pub struct StdoutDevice {
+    flush: bool,
+    callback: fn(image_buffer: &Vec<u8>),
+}
+
+impl StdoutDevice {
+    pub fn new(flush: bool) -> Self {
+        Self {
+            flush,
+            callback: |_| {},
+        }
+    }
+
+    pub fn set_callback(&mut self, callback: fn(image_buffer: &Vec<u8>)) {
+        self.callback = callback;
+    }
+}
+
+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();
+        }
+        let data = vec![byte];
+        (self.callback)(&data);
+    }
+
+    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 399c386ebe3d8286af1378a11c71034ba537003e..1e970d4e6c356793385df1582f088061a424073b 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::{NullDevice, Serial, SerialDevice},
     timer::Timer,
     util::read_file,
 };
@@ -33,10 +35,31 @@ use std::{
 /// Should serve as the main entry-point API.
 #[cfg_attr(feature = "wasm", wasm_bindgen)]
 pub struct GameBoy {
+    /// Reference to the Game Boy CPU component to be
+    /// used as the main element of the system, when
+    /// clocked, the amount of ticks from it will be
+    /// used as reference or the rest of the components.
     cpu: Cpu,
+
+    /// If the PPU is enabled, it will be clocked.
     ppu_enabled: bool,
+
+    /// If the APU is enabled, it will be clocked.
     apu_enabled: bool,
+
+    /// 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
+    /// logic to match the current frequency. For example
+    /// the APU will adjust its internal clock to match
+    /// this hint.
+    clock_freq: u32,
 }
 
 #[cfg_attr(feature = "wasm", wasm_bindgen)]
@@ -72,19 +95,24 @@ 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,
         }
     }
 
     pub fn reset(&mut self) {
         self.ppu().reset();
         self.apu().reset();
+        self.timer().reset();
+        self.serial().reset();
         self.mmu().reset();
         self.cpu.reset();
     }
@@ -100,6 +128,9 @@ impl GameBoy {
         if self.timer_enabled {
             self.timer_clock(cycles);
         }
+        if self.serial_enabled {
+            self.serial_clock(cycles);
+        }
         cycles
     }
 
@@ -127,6 +158,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()
     }
@@ -210,6 +245,36 @@ impl GameBoy {
         buffer
     }
 
+    pub fn audio_output(&self) -> u8 {
+        self.apu_i().output()
+    }
+
+    pub fn audio_all_output(&self) -> Vec<u8> {
+        vec![
+            self.audio_output(),
+            self.audio_ch1_output(),
+            self.audio_ch2_output(),
+            self.audio_ch3_output(),
+            self.audio_ch4_output(),
+        ]
+    }
+
+    pub fn audio_ch1_output(&self) -> u8 {
+        self.apu_i().ch1_output()
+    }
+
+    pub fn audio_ch2_output(&self) -> u8 {
+        self.apu_i().ch2_output()
+    }
+
+    pub fn audio_ch3_output(&self) -> u8 {
+        self.apu_i().ch3_output()
+    }
+
+    pub fn audio_ch4_output(&self) -> u8 {
+        self.apu_i().ch4_output()
+    }
+
     pub fn cartridge_eager(&mut self) -> Cartridge {
         self.mmu().rom().clone()
     }
@@ -261,23 +326,23 @@ impl GameBoy {
     /// Obtains the name of the compiler that has been
     /// used in the compilation of the base Boytacean
     /// library. Can be used for diagnostics.
-    pub fn get_compiler(&self) -> String {
+    pub fn compiler(&self) -> String {
         String::from(COMPILER)
     }
 
-    pub fn get_compiler_version(&self) -> String {
+    pub fn compiler_version(&self) -> String {
         String::from(COMPILER_VERSION)
     }
 
-    pub fn get_compilation_date(&self) -> String {
+    pub fn compilation_date(&self) -> String {
         String::from(COMPILATION_DATE)
     }
 
-    pub fn get_compilation_time(&self) -> String {
+    pub fn compilation_time(&self) -> String {
         String::from(COMPILATION_TIME)
     }
 
-    pub fn get_ppu_enabled(&self) -> bool {
+    pub fn ppu_enabled(&self) -> bool {
         self.ppu_enabled
     }
 
@@ -285,7 +350,7 @@ impl GameBoy {
         self.ppu_enabled = value;
     }
 
-    pub fn get_apu_enabled(&self) -> bool {
+    pub fn apu_enabled(&self) -> bool {
         self.apu_enabled
     }
 
@@ -293,13 +358,42 @@ impl GameBoy {
         self.apu_enabled = value;
     }
 
-    pub fn get_timer_enabled(&self) -> bool {
-        self.apu_enabled
+    pub fn timer_enabled(&self) -> bool {
+        self.timer_enabled
     }
 
     pub fn set_timer_enabled(&mut self, value: bool) {
         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
+    }
+
+    pub fn set_clock_freq(&mut self, value: u32) {
+        self.clock_freq = value;
+        self.apu().set_clock_freq(value);
+    }
+
+    pub fn attach_null_serial(&mut self) {
+        self.attach_serial(Box::<NullDevice>::default());
+    }
+
+    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
@@ -345,6 +439,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)
     }
@@ -390,6 +488,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")]
@@ -407,6 +509,27 @@ impl GameBoy {
         self.load_rom(data).clone()
     }
 
+    pub fn load_null_ws(&mut self) {
+        let null = Box::<NullDevice>::default();
+        self.attach_serial(null);
+    }
+
+    pub fn load_logger_ws(&mut self) {
+        let mut logger = Box::<StdoutDevice>::default();
+        logger.set_callback(|data| {
+            logger_callback(data.to_vec());
+        });
+        self.attach_serial(logger);
+    }
+
+    pub fn load_printer_ws(&mut self) {
+        let mut printer = Box::<PrinterDevice>::default();
+        printer.set_callback(|image_buffer| {
+            printer_callback(image_buffer.to_vec());
+        });
+        self.attach_serial(printer);
+    }
+
     pub fn set_palette_colors_ws(&mut self, value: Vec<JsValue>) {
         let palette: Palette = value
             .into_iter()
@@ -417,7 +540,7 @@ impl GameBoy {
         self.ppu().set_palette_colors(&palette);
     }
 
-    pub fn get_wasm_engine_ws(&self) -> Option<String> {
+    pub fn wasm_engine_ws(&self) -> Option<String> {
         let dependencies = dependencies_map();
         if !dependencies.contains_key("wasm-bindgen") {
             return None;
@@ -448,6 +571,12 @@ impl GameBoy {
 extern "C" {
     #[wasm_bindgen(js_namespace = window)]
     fn panic(message: &str);
+
+    #[wasm_bindgen(js_namespace = window, js_name = loggerCallback)]
+    fn logger_callback(data: Vec<u8>);
+
+    #[wasm_bindgen(js_namespace = window, js_name = printerCallback)]
+    fn printer_callback(image_buffer: Vec<u8>);
 }
 
 #[cfg(feature = "wasm")]
diff --git a/src/inst.rs b/src/inst.rs
index b19487d5e681617e27ce0bb7e7de53389d3d4ab7..8b059a595d8a894afacebf4cbb24e369096ef15a 100644
--- a/src/inst.rs
+++ b/src/inst.rs
@@ -711,7 +711,7 @@ fn ld_d_u8(cpu: &mut Cpu) {
 }
 
 fn rla(cpu: &mut Cpu) {
-    let carry = cpu.get_carry();
+    let carry = cpu.carry();
 
     cpu.set_carry(cpu.a & 0x80 == 0x80);
 
@@ -769,7 +769,7 @@ fn ld_e_u8(cpu: &mut Cpu) {
 }
 
 fn rra(cpu: &mut Cpu) {
-    let carry = cpu.get_carry();
+    let carry = cpu.carry();
 
     cpu.set_carry((cpu.a & 0x1) == 0x1);
 
@@ -783,7 +783,7 @@ fn rra(cpu: &mut Cpu) {
 fn jr_nz_i8(cpu: &mut Cpu) {
     let byte = cpu.read_u8() as i8;
 
-    if cpu.get_zero() {
+    if cpu.zero() {
         return;
     }
 
@@ -836,16 +836,16 @@ fn daa(cpu: &mut Cpu) {
     let a = cpu.a;
     let mut adjust = 0;
 
-    if cpu.get_half_carry() {
+    if cpu.half_carry() {
         adjust |= 0x06;
     }
 
-    if cpu.get_carry() {
+    if cpu.carry() {
         // Yes, we have to adjust it.
         adjust |= 0x60;
     }
 
-    let res = if cpu.get_sub() {
+    let res = if cpu.sub() {
         a.wrapping_sub(adjust)
     } else {
         if a & 0x0f > 0x09 {
@@ -869,7 +869,7 @@ fn daa(cpu: &mut Cpu) {
 fn jr_z_i8(cpu: &mut Cpu) {
     let byte = cpu.read_u8() as i8;
 
-    if !cpu.get_zero() {
+    if !cpu.zero() {
         return;
     }
 
@@ -933,7 +933,7 @@ fn ld_sp_u16(cpu: &mut Cpu) {
 fn jr_nc_i8(cpu: &mut Cpu) {
     let byte = cpu.read_u8() as i8;
 
-    if cpu.get_carry() {
+    if cpu.carry() {
         return;
     }
 
@@ -986,7 +986,7 @@ fn scf(cpu: &mut Cpu) {
 fn jr_c_i8(cpu: &mut Cpu) {
     let byte = cpu.read_u8() as i8;
 
-    if !cpu.get_carry() {
+    if !cpu.carry() {
         return;
     }
 
@@ -1039,7 +1039,7 @@ fn ld_a_u8(cpu: &mut Cpu) {
 fn ccf(cpu: &mut Cpu) {
     cpu.set_sub(false);
     cpu.set_half_carry(false);
-    cpu.set_carry(!cpu.get_carry());
+    cpu.set_carry(!cpu.carry());
 }
 
 fn ld_b_b(_cpu: &mut Cpu) {}
@@ -1676,7 +1676,7 @@ fn cp_a_a(cpu: &mut Cpu) {
 }
 
 fn ret_nz(cpu: &mut Cpu) {
-    if cpu.get_zero() {
+    if cpu.zero() {
         return;
     }
 
@@ -1692,7 +1692,7 @@ fn pop_bc(cpu: &mut Cpu) {
 fn jp_nz_u16(cpu: &mut Cpu) {
     let word = cpu.read_u16();
 
-    if cpu.get_zero() {
+    if cpu.zero() {
         return;
     }
 
@@ -1708,7 +1708,7 @@ fn jp_u16(cpu: &mut Cpu) {
 fn call_nz_u16(cpu: &mut Cpu) {
     let word = cpu.read_u16();
 
-    if cpu.get_zero() {
+    if cpu.zero() {
         return;
     }
 
@@ -1731,7 +1731,7 @@ fn rst_00h(cpu: &mut Cpu) {
 }
 
 fn ret_z(cpu: &mut Cpu) {
-    if !cpu.get_zero() {
+    if !cpu.zero() {
         return;
     }
 
@@ -1746,7 +1746,7 @@ fn ret(cpu: &mut Cpu) {
 fn jp_z_u16(cpu: &mut Cpu) {
     let word = cpu.read_u16();
 
-    if !cpu.get_zero() {
+    if !cpu.zero() {
         return;
     }
 
@@ -1757,7 +1757,7 @@ fn jp_z_u16(cpu: &mut Cpu) {
 fn call_z_u16(cpu: &mut Cpu) {
     let word = cpu.read_u16();
 
-    if !cpu.get_zero() {
+    if !cpu.zero() {
         return;
     }
 
@@ -1782,7 +1782,7 @@ fn rst_08h(cpu: &mut Cpu) {
 }
 
 fn ret_nc(cpu: &mut Cpu) {
-    if cpu.get_carry() {
+    if cpu.carry() {
         return;
     }
 
@@ -1798,7 +1798,7 @@ fn pop_de(cpu: &mut Cpu) {
 fn jp_nc_u16(cpu: &mut Cpu) {
     let word = cpu.read_u16();
 
-    if cpu.get_carry() {
+    if cpu.carry() {
         return;
     }
 
@@ -1809,7 +1809,7 @@ fn jp_nc_u16(cpu: &mut Cpu) {
 fn call_nc_u16(cpu: &mut Cpu) {
     let word = cpu.read_u16();
 
-    if cpu.get_carry() {
+    if cpu.carry() {
         return;
     }
 
@@ -1832,7 +1832,7 @@ fn rst_10h(cpu: &mut Cpu) {
 }
 
 fn ret_c(cpu: &mut Cpu) {
-    if !cpu.get_carry() {
+    if !cpu.carry() {
         return;
     }
 
@@ -1848,7 +1848,7 @@ fn reti(cpu: &mut Cpu) {
 fn jp_c_u16(cpu: &mut Cpu) {
     let word = cpu.read_u16();
 
-    if !cpu.get_carry() {
+    if !cpu.carry() {
         return;
     }
 
@@ -1859,7 +1859,7 @@ fn jp_c_u16(cpu: &mut Cpu) {
 fn call_c_u16(cpu: &mut Cpu) {
     let word = cpu.read_u16();
 
-    if !cpu.get_carry() {
+    if !cpu.carry() {
         return;
     }
 
@@ -3132,7 +3132,7 @@ fn res(value: u8, bit: u8) -> u8 {
 /// byte (probably from a register) and updates the
 /// proper flag registers.
 fn rl(cpu: &mut Cpu, value: u8) -> u8 {
-    let carry = cpu.get_carry();
+    let carry = cpu.carry();
 
     cpu.set_carry((value & 0x80) == 0x80);
 
@@ -3161,7 +3161,7 @@ fn rlc(cpu: &mut Cpu, value: u8) -> u8 {
 /// byte (probably from a register) and updates the
 /// proper flag registers.
 fn rr(cpu: &mut Cpu, value: u8) -> u8 {
-    let carry = cpu.get_carry();
+    let carry = cpu.carry();
 
     cpu.set_carry((value & 0x1) == 0x1);
 
@@ -3259,7 +3259,7 @@ fn add_set_flags(cpu: &mut Cpu, first: u8, second: u8) -> u8 {
 fn add_carry_set_flags(cpu: &mut Cpu, first: u8, second: u8) -> u8 {
     let first = first as u32;
     let second = second as u32;
-    let carry = cpu.get_carry() as u32;
+    let carry = cpu.carry() as u32;
 
     let result = first.wrapping_add(second).wrapping_add(carry);
     let result_b = result as u8;
@@ -3290,7 +3290,7 @@ fn sub_set_flags(cpu: &mut Cpu, first: u8, second: u8) -> u8 {
 fn sub_carry_set_flags(cpu: &mut Cpu, first: u8, second: u8) -> u8 {
     let first = first as u32;
     let second = second as u32;
-    let carry = cpu.get_carry() as u32;
+    let carry = cpu.carry() as u32;
 
     let result = first.wrapping_sub(second).wrapping_sub(carry);
     let result_b = result as u8;
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/macros.rs b/src/macros.rs
index f1831675cc6702887dc6ee90b627410c82ababac..8180aa3a90626e50730dd29353ac902f4dba0afe 100644
--- a/src/macros.rs
+++ b/src/macros.rs
@@ -2,8 +2,10 @@
 #[macro_export]
 macro_rules! debugln {
     ($($rest:tt)*) => {
-        std::print!("[DEBUG] ");
-        std::println!($($rest)*)
+        {
+            std::print!("[DEBUG] ");
+            std::println!($($rest)*);
+        }
     }
 }
 
diff --git a/src/mmu.rs b/src/mmu.rs
index 17de8cb88a7a1d743c7baf291135337bf781d261..c659df2a8098f19d9c30e3e409cf90f24a876958 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_DMG: usize = 256;
 pub const BOOT_SIZE_CGB: usize = 2304;
@@ -29,6 +29,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,
@@ -58,12 +62,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: vec![],
@@ -118,6 +123,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
     }
@@ -171,6 +180,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 =>
                     {
@@ -178,6 +190,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 })
                     }
 
@@ -200,7 +213,7 @@ impl Mmu {
                                 0x00
                             }
                         },
-                        0x10..=26 | 0x30..=0x37 => self.apu.read(addr),
+                        0x10..=0x26 | 0x30..=0x37 => self.apu.read(addr),
                         0x40 | 0x50 | 0x60 | 0x70 => self.ppu.read(addr),
                         _ => {
                             debugln!("Reading from unknown IO control 0x{:04x}", addr);
@@ -246,11 +259,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);
                     }
 
@@ -281,7 +298,7 @@ impl Mmu {
                                 0x04..=0x07 => self.timer.write(addr, value),
                                 _ => debugln!("Writing to unknown IO control 0x{:04x}", addr),
                             },
-                            0x10..=26 | 0x30..=0x37 => self.apu.write(addr, value),
+                            0x10..=0x26 | 0x30..=0x37 => self.apu.write(addr, value),
                             0x40 | 0x60 | 0x70 => {
                                 match addr & 0x00ff {
                                     // 0xFF46 — DMA: OAM DMA source address & start
diff --git a/src/pad.rs b/src/pad.rs
index f99e0a85a2b43b5705845039b9908bbc780609f7..f9e6ef97d3b6ab49edec7ea9f1a8f778958a6597 100644
--- a/src/pad.rs
+++ b/src/pad.rs
@@ -5,6 +5,7 @@ use crate::warnln;
 
 #[derive(Clone, Copy, PartialEq, Eq)]
 pub enum PadSelection {
+    None,
     Action,
     Direction,
 }
@@ -45,7 +46,7 @@ impl Pad {
             select: false,
             b: false,
             a: false,
-            selection: PadSelection::Action,
+            selection: PadSelection::None,
             int_pad: false,
         }
     }
@@ -70,15 +71,12 @@ impl Pad {
                             | if self.up { 0x00 } else { 0x04 }
                             | if self.down { 0x00 } else { 0x08 })
                     }
+                    PadSelection::None => 0x0f,
                 };
-                value |= if self.selection == PadSelection::Direction {
-                    0x10
-                } else {
-                    0x00
-                } | if self.selection == PadSelection::Action {
-                    0x20
-                } else {
-                    0x00
+                value |= match self.selection {
+                    PadSelection::Action => 0x10,
+                    PadSelection::Direction => 0x20,
+                    PadSelection::None => 0x30,
                 };
                 value
             }
@@ -92,11 +90,12 @@ impl Pad {
     pub fn write(&mut self, addr: u16, value: u8) {
         match addr & 0x00ff {
             0x0000 => {
-                self.selection = if value & 0x10 == 0x00 {
-                    PadSelection::Direction
-                } else {
-                    PadSelection::Action
-                }
+                self.selection = match value & 0x30 {
+                    0x10 => PadSelection::Action,
+                    0x20 => PadSelection::Direction,
+                    0x30 => PadSelection::None,
+                    _ => PadSelection::None,
+                };
             }
             _ => warnln!("Writing to unknown Pad location 0x{:04x}", addr),
         }
@@ -115,7 +114,7 @@ impl Pad {
         }
 
         // signals that a JoyPad interrupt is pending to be
-        // handled as a key pressed has been done
+        // handled as a key press has been performed
         self.int_pad = true;
     }
 
@@ -132,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 af904c779421cfaed25074b128e296183e85dfec..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);
     }
@@ -873,7 +888,7 @@ impl Ppu {
         let mut index_buffer = [-256i16; DISPLAY_WIDTH];
 
         for index in 0..OBJ_COUNT {
-            // in case the limit on number of object to be draw per
+            // in case the limit on the number of objects to be draw per
             // line has been reached breaks the loop avoiding more draws
             if draw_count == 10 {
                 break;
diff --git a/src/rom.rs b/src/rom.rs
index 04ccbec44555a6b8bb3ef701c9350187b95dc71e..bdad478dfbb7cdfcefaa53e31cf02945436af22a 100644
--- a/src/rom.rs
+++ b/src/rom.rs
@@ -182,6 +182,29 @@ impl Display for RamSize {
     }
 }
 
+#[cfg_attr(feature = "wasm", wasm_bindgen)]
+pub enum CgbMode {
+    NoCgb = 0x00,
+    CgbCompatible = 0x80,
+    CgbOnly = 0xc0,
+}
+
+impl CgbMode {
+    pub fn description(&self) -> &'static str {
+        match self {
+            CgbMode::NoCgb => "No CGB support",
+            CgbMode::CgbCompatible => "CGB backwards compatible",
+            CgbMode::CgbOnly => "CGB only",
+        }
+    }
+}
+
+impl Display for CgbMode {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.description())
+    }
+}
+
 /// Structure that defines the ROM and ROM contents
 /// of a Game Boy cartridge. Should correctly address
 /// the specifics of all the major MBCs (Memory Bank
@@ -339,6 +362,16 @@ impl Cartridge {
             if *byte == 0u8 {
                 break;
             }
+
+            // in we're at the final byte of the title and the value
+            // is one that is reserved for CGB compatibility testing
+            // then we must ignore it for title processing purposes
+            if offset > 14
+                && (*byte == CgbMode::CgbCompatible as u8 || *byte == CgbMode::CgbOnly as u8)
+            {
+                break;
+            }
+
             offset += 1;
         }
         self.title_offset = 0x0134 + offset;
@@ -360,6 +393,14 @@ impl Cartridge {
         )
     }
 
+    pub fn cgb_flag(&self) -> CgbMode {
+        match self.rom_data[0x0143] {
+            0x80 => CgbMode::CgbCompatible,
+            0xc0 => CgbMode::CgbOnly,
+            _ => CgbMode::NoCgb,
+        }
+    }
+
     pub fn rom_type(&self) -> RomType {
         match self.rom_data[0x0147] {
             0x00 => RomType::RomOnly,
@@ -469,11 +510,12 @@ impl Display for Cartridge {
     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
         write!(
             f,
-            "Name => {}\nType => {}\nROM Size => {}\nRAM Size => {}",
+            "Name => {}\nType => {}\nROM Size => {}\nRAM Size => {}\nCGB Mode => {}",
             self.title(),
             self.rom_type(),
             self.rom_size(),
-            self.ram_size()
+            self.ram_size(),
+            self.cgb_flag()
         )
     }
 }
diff --git a/src/serial.rs b/src/serial.rs
new file mode 100644
index 0000000000000000000000000000000000000000..dd6388a3c65daddda4800dabbb2ead2851184322
--- /dev/null
+++ b/src/serial.rs
@@ -0,0 +1,211 @@
+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 =>
+            {
+                #[allow(clippy::bool_to_int_with_if)]
+                (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);
     }