diff --git a/Cargo.toml b/Cargo.toml
index cc19c9389..dadfb5c5a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,6 +7,9 @@ edition = "2021"
 defmt = ["dep:defmt"]
 log = ["dep:log"]
 
+# Fetch console logs from the WiFi firmware and forward them to `log` or `defmt`.
+firmware-logs = []
+
 [dependencies]
 embassy-time = { version = "0.1.0" }
 embassy-sync = { version = "0.1.0" }
diff --git a/examples/rpi-pico-w/Cargo.toml b/examples/rpi-pico-w/Cargo.toml
index bb44667de..b817289e5 100644
--- a/examples/rpi-pico-w/Cargo.toml
+++ b/examples/rpi-pico-w/Cargo.toml
@@ -5,7 +5,7 @@ edition = "2021"
 
 
 [dependencies]
-cyw43 = { path = "../../", features = ["defmt"]}
+cyw43 = { path = "../../", features = ["defmt", "firmware-logs"]}
 embassy-executor = { version = "0.1.0",  features = ["defmt", "integrated-timers"] }
 embassy-time = { version = "0.1.0",  features = ["defmt", "defmt-timestamp-uptime"] }
 embassy-rp = { version = "0.1.0",  features = ["defmt", "unstable-traits", "nightly", "unstable-pac", "time-driver"] }
diff --git a/src/lib.rs b/src/lib.rs
index 430821752..883e669de 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -575,6 +575,17 @@ pub struct Runner<'a, PWR, SPI> {
     backplane_window: u32,
 
     sdpcm_seq_max: u8,
+
+    #[cfg(feature = "firmware-logs")]
+    log: LogState,
+}
+
+#[cfg(feature = "firmware-logs")]
+struct LogState {
+    addr: u32,
+    last_idx: usize,
+    buf: [u8; 256],
+    buf_count: usize,
 }
 
 pub async fn new<'a, PWR, SPI>(
@@ -598,6 +609,14 @@ where
         backplane_window: 0xAAAA_AAAA,
 
         sdpcm_seq_max: 1,
+
+        #[cfg(feature = "firmware-logs")]
+        log: LogState {
+            addr: 0,
+            last_idx: 0,
+            buf: [0; 256],
+            buf_count: 0,
+        },
     };
 
     runner.init(firmware).await;
@@ -715,12 +734,74 @@ where
         //while self.read8(FUNC_BACKPLANE, REG_BACKPLANE_CHIP_CLOCK_CSR).await & 0x80 == 0 {}
         //info!("clock ok");
 
+        #[cfg(feature = "firmware-logs")]
+        self.log_init().await;
+
         info!("init done ");
     }
 
+    #[cfg(feature = "firmware-logs")]
+    async fn log_init(&mut self) {
+        // Initialize shared memory for logging.
+
+        let shared_addr = self
+            .bp_read32(CHIP.atcm_ram_base_address + CHIP.chip_ram_size - 4 - CHIP.socram_srmem_size)
+            .await;
+        info!("shared_addr {:08x}", shared_addr);
+
+        let mut shared = [0; SharedMemData::SIZE];
+        self.bp_read(shared_addr, &mut shared).await;
+        let shared = SharedMemData::from_bytes(&shared);
+        info!("shared: {:08x}", shared);
+
+        self.log.addr = shared.console_addr + 8;
+    }
+
+    #[cfg(feature = "firmware-logs")]
+    async fn log_read(&mut self) {
+        // Read log struct
+        let mut log = [0; SharedMemLog::SIZE];
+        self.bp_read(self.log.addr, &mut log).await;
+        let log = SharedMemLog::from_bytes(&log);
+
+        let idx = log.idx as usize;
+
+        // If pointer hasn't moved, no need to do anything.
+        if idx == self.log.last_idx {
+            return;
+        }
+
+        // Read entire buf for now. We could read only what we need, but then we
+        // run into annoying alignment issues in `bp_read`.
+        let mut buf = [0; 0x400];
+        self.bp_read(log.buf, &mut buf).await;
+
+        while self.log.last_idx != idx as usize {
+            let b = buf[self.log.last_idx];
+            if b == b'\r' || b == b'\n' {
+                if self.log.buf_count != 0 {
+                    let s = unsafe { core::str::from_utf8_unchecked(&self.log.buf[..self.log.buf_count]) };
+                    debug!("LOGS: {}", s);
+                    self.log.buf_count = 0;
+                }
+            } else if self.log.buf_count < self.log.buf.len() {
+                self.log.buf[self.log.buf_count] = b;
+                self.log.buf_count += 1;
+            }
+
+            self.log.last_idx += 1;
+            if self.log.last_idx == 0x400 {
+                self.log.last_idx = 0;
+            }
+        }
+    }
+
     pub async fn run(mut self) -> ! {
         let mut buf = [0; 512];
         loop {
+            #[cfg(feature = "firmware-logs")]
+            self.log_read().await;
+
             // Send stuff
             // TODO flow control not yet complete
             if !self.has_credit() {
diff --git a/src/structs.rs b/src/structs.rs
index 6d4525a46..41a340661 100644
--- a/src/structs.rs
+++ b/src/structs.rs
@@ -18,6 +18,32 @@ macro_rules! impl_bytes {
     };
 }
 
+#[derive(Clone, Copy)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+#[repr(C)]
+pub struct SharedMemData {
+    pub flags: u32,
+    pub trap_addr: u32,
+    pub assert_exp_addr: u32,
+    pub assert_file_addr: u32,
+    pub assert_line: u32,
+    pub console_addr: u32,
+    pub msgtrace_addr: u32,
+    pub fwid: u32,
+}
+impl_bytes!(SharedMemData);
+
+#[derive(Clone, Copy)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+#[repr(C)]
+pub struct SharedMemLog {
+    pub buf: u32,
+    pub buf_size: u32,
+    pub idx: u32,
+    pub out_idx: u32,
+}
+impl_bytes!(SharedMemLog);
+
 #[derive(Clone, Copy)]
 #[cfg_attr(feature = "defmt", derive(defmt::Format))]
 #[repr(C)]