diff --git a/firmware/rust1/src/bin/heizung.rs b/firmware/rust1/src/bin/heizung.rs
index 2ce1775c035a086c55a32dcb0f6b0f52d65401e7..bb32d66b76316620077c62540bfbe81b02f605eb 100644
--- a/firmware/rust1/src/bin/heizung.rs
+++ b/firmware/rust1/src/bin/heizung.rs
@@ -151,8 +151,35 @@ async fn i2c_write_zero_bytes<SCL: gpio::Pin, SDA: gpio::Pin, const FREQ : u64>(
     scl.set_low();
 
     Timer::after(delay).await;
-    if sda.is_low() || scl.is_low() {
-        return Err(Abort(ArbitrationLoss))
+
+    // abort if SCL is stuck low
+    handle_clock_stretching(&mut scl).await?;
+
+    // handle SDA being stuck low
+    // (This can happen for very simple I2C devices that only use SCL and don't have any other clocks that
+    // they can use to handle timeouts, e.g. simple port expanders.)
+    if sda.is_low() {
+        // I think I have read that DW_apb_i2c does nine clocks. This seems resonable so let's do the same.
+        for _ in 0..9 {
+            scl.set_as_output();
+            Timer::after(delay).await;
+            scl.set_as_input();
+            Timer::after(delay).await;
+
+            if sda.is_high() {
+                break;
+            }
+        }
+
+        if sda.is_low() || scl.is_low() {
+            // There is isn't much that we can do here. A start condition would reset the state but we cannot
+            // generate it while one of the lines is stuck low.
+            return Err(Abort(ArbitrationLoss))
+        }
+
+        // SDA is high, again. Good. We will generate a start condition soon. This should abort any
+        // partial transfers that some devices might think are ongoing. We have already waited for half
+        // a clock so we are good to go.
     }
 
     let result = i2c_write_zero_bytes_middle_part::<_, _, FREQ>(addr, delay, &mut scl, &mut sda).await;