diff --git a/firmware/rust1-bootloader/src/main.rs b/firmware/rust1-bootloader/src/main.rs
index 4fb4e7d07dc91fdb3d2b8af6107ded9fded7358f..73e995e50e85882ec5e72dbc8348ecd477cb934a 100644
--- a/firmware/rust1-bootloader/src/main.rs
+++ b/firmware/rust1-bootloader/src/main.rs
@@ -44,6 +44,12 @@ fn main() -> ! {
         }
     }
 
+    unsafe {
+        // dirty trick to keep PROGRAM_INFO despite `--gc-sections` because we cannot add
+        // KEEP(...) in that part of the predefined linker script
+        let _ = core::ptr::read_volatile(PROGRAM_INFO.as_ptr());
+    }
+
     let mut led_b = Output::new(p.PIN_4, Level::Low);
     let _led_g = Output::new(p.PIN_5, Level::Low);
     let mut led_r = Output::new(p.PIN_6, Level::Low);
@@ -91,3 +97,27 @@ unsafe fn DefaultHandler(_: i16) -> ! {
 fn panic(_info: &core::panic::PanicInfo) -> ! {
     cortex_m::asm::udf();
 }
+
+
+// Program information.
+// This must be in the first 256 bytes after BOOT2 (including the end marker). The first thing in
+// the predefined linker script is .Reset so let's add it to that.
+const BINARY_INFO_MARKER_START: u32 = 0x7188ebf2;
+const BINARY_INFO_MARKER_END: u32 = 0xe71aa390;
+#[used]
+#[link_section = ".Reset"]
+pub static PROGRAM_INFO: [u32; 5] = {
+    extern "C" {
+        static __bootloader_active_program_info_start: u32;
+        static __bootloader_active_program_info_end: u32;
+    }
+    [
+        BINARY_INFO_MARKER_START,
+        //&__bootloader_active_program_info_start as *const u32 as u32,
+        //&__bootloader_active_program_info_end as *const u32 as u32,
+        0x10086000,
+        0x10087000,
+        0x10087000 - 128,  // copy table can have up to 10 entries of 3*u32 each
+        BINARY_INFO_MARKER_END
+    ]
+};
diff --git a/firmware/rust1/build.rs b/firmware/rust1/build.rs
index 52730bc0d2e83371ac5472879a23132a77e7b29b..a6eaa3e0f68d41c3c274d6062fc3ab3e9af9ac1f 100644
--- a/firmware/rust1/build.rs
+++ b/firmware/rust1/build.rs
@@ -25,6 +25,10 @@ fn main() {
         .unwrap()
         .write_all(include_bytes!("clear-bootloader-state.x"))
         .unwrap();
+    File::create(out.join("program-info.x"))
+        .unwrap()
+        .write_all(include_bytes!("program-info.x"))
+        .unwrap();
     println!("cargo:rustc-link-search={}", out.display());
 
     // By default, Cargo will re-run a build script whenever
@@ -33,10 +37,12 @@ fn main() {
     // `memory.x` is changed.
     println!("cargo:rerun-if-changed=memory.x");
     println!("cargo:rerun-if-changed=clear-bootloader-state.x");
+    println!("cargo:rerun-if-changed=program-info.x");
 
     println!("cargo:rustc-link-arg-bins=--nmagic");
     println!("cargo:rustc-link-arg-bins=-Tlink.x");
     //println!("cargo:rustc-link-arg-bins=-Tlink-rp.x");  // This is for boot2 but the bootloader handles that for us.
     println!("cargo:rustc-link-arg-bins=-Tdefmt.x");
     println!("cargo:rustc-link-arg-bins=-Tclear-bootloader-state.x");
+    println!("cargo:rustc-link-arg-bins=-Tprogram-info.x");
 }
diff --git a/firmware/rust1/build.sh b/firmware/rust1/build.sh
index 86f5cded96f2e7289c2e940cbe342e8e0258ac1f..294af8bd3a682f4fbf9d2203ae30301e69b9c025 100755
--- a/firmware/rust1/build.sh
+++ b/firmware/rust1/build.sh
@@ -15,7 +15,7 @@ if ! [ -e .cargo-tools/bin/probe-rs-debugger ] ; then
     cargo install --root "$PWD/.cargo-tools/" probe-rs-debugger
 fi
 
-( cd ../rust1-bootloader && cargo build --release )
+( cd ../rust1-bootloader && cargo build --release && cargo objdump --release -- -xd >target/thumbv6m-none-eabi/release/heizung-bootloader.map )
 ./.cargo-tools/bin/elf2uf2-rs ../rust1-bootloader/target/thumbv6m-none-eabi/release/heizung-bootloader
 
 cargo build --bin heizung
diff --git a/firmware/rust1/memory.x b/firmware/rust1/memory.x
index 5e049f023dfbf91dec5785f12439f19d70cb0d28..86cda41f4fea2986bbe646cec5493cd3c9f2bb69 100644
--- a/firmware/rust1/memory.x
+++ b/firmware/rust1/memory.x
@@ -3,7 +3,8 @@ MEMORY
   /* NOTE 1 K = 1 KiBi = 1024 bytes */
   BOOT2                             : ORIGIN = 0x10000000, LENGTH = 0x100
   BOOTLOADER_STATE                  : ORIGIN = 0x10006000, LENGTH = 4K
-  FLASH                             : ORIGIN = 0x10007000, LENGTH = 512K
+  FLASH                             : ORIGIN = 0x10007000, LENGTH = 512K - 4K
+  PROGRAM_INFO                      : ORIGIN = 0x10086000, LENGTH = 4K
   DFU                               : ORIGIN = 0x10087000, LENGTH = 516K
   RAM                               : ORIGIN = 0x20000000, LENGTH = 256K
 }
diff --git a/firmware/rust1/src/bin/heizung.rs b/firmware/rust1/src/bin/heizung.rs
index 757e1883449990c49da7060bde7c888c155198d3..cd9d7306e060f85335fdbf78a3842b879d1d7c6c 100644
--- a/firmware/rust1/src/bin/heizung.rs
+++ b/firmware/rust1/src/bin/heizung.rs
@@ -694,3 +694,129 @@ fn init_early() {
     // release spinlock 31 because we sometimes block on this in the init code
     unsafe { pac::SIO.spinlock(31).write_value(1); }
 }
+
+
+
+// Add program info.
+#[used]
+#[link_section = ".program_info"]
+pub static PROGRAM_INFO: [u8; 4096] = make_program_info();
+
+struct ProgramInfoBuilder {
+    addr: u32,
+    buf: [u8; 4096],
+    info_pos: usize,
+    data_pos: usize,
+}
+
+impl ProgramInfoBuilder {
+    const fn new(addr: u32, copy_table_size: usize) -> Self {
+        ProgramInfoBuilder {
+            addr,
+            buf: {
+                let mut x = [0xff; 4096];
+                // mark end of copy table with zero in first entry
+                x[4096 - copy_table_size + 0] = 0;
+                x[4096 - copy_table_size + 1] = 0;
+                x[4096 - copy_table_size + 2] = 0;
+                x[4096 - copy_table_size + 3] = 0;
+                x
+            },
+            info_pos: 0,
+            data_pos: 4096 - copy_table_size,
+        }
+    }
+
+    const fn build(self: Self) -> [u8; 4096] {
+        self.buf
+    }
+
+    const fn push(mut self: Self, value: u8) -> Self {
+        self.buf[self.info_pos] = value;
+        self.info_pos += 1;
+        if self.info_pos > self.data_pos {
+            core::panic!("ProgramInfoBuilder: too much data")
+        }
+        self
+    }
+
+    const fn push_u16(self: Self, value: u16) -> Self {
+        self
+            .push((value & 0xff) as u8)
+            .push(((value >> 8) & 0xff) as u8)
+    }
+
+    const fn push_u32(self: Self, value: u32) -> Self {
+        self
+            .push((value & 0xff) as u8)
+            .push(((value >> 8) & 0xff) as u8)
+            .push(((value >> 16) & 0xff) as u8)
+            .push(((value >> 24) & 0xff) as u8)
+    }
+
+    const fn push_raspberry_tag(self: Self) -> Self {
+        self.push('R' as u8)
+            .push('P' as u8)
+    }
+
+    const fn push_extra_data(mut self: Self, value: &[u8]) -> (Self, u32) {
+        if self.data_pos - self.info_pos < value.len() {
+            core::panic!("ProgramInfoBuilder: too much data")
+        }
+        let offset = self.data_pos - value.len();
+        //self.buf[offset..self.data_pos].copy_from_slice(value);
+        //for i in 0..value.len() {
+        //    self.buf[offset+i] = value[i];
+        //}
+        if value.len() > 0 { self.buf[offset+0] = value[0]; }
+        if value.len() > 1 { self.buf[offset+1] = value[1]; }
+        if value.len() > 2 { self.buf[offset+2] = value[2]; }
+        if value.len() > 3 { self.buf[offset+3] = value[3]; }
+        if value.len() > 4 { self.buf[offset+4] = value[4]; }
+        if value.len() > 5 { self.buf[offset+5] = value[5]; }
+        if value.len() > 6 { self.buf[offset+6] = value[6]; }
+        if value.len() > 7 { self.buf[offset+7] = value[7]; }
+        if value.len() > 8 { core::panic!("too long") }
+
+        self.data_pos -= value.len();
+        let addr = self.addr + offset as u32;
+        (self, addr)
+    }
+
+    const fn push_string(mut self: Self, value: &str) -> Self {
+        if self.data_pos - self.info_pos < value.len() {
+            core::panic!("ProgramInfoBuilder: too much data")
+        }
+        (self, _) = self.push_extra_data(&[0]);
+        let (self2, addr) = self.push_extra_data(value.as_bytes());
+        self2.push_u32(addr)
+    }
+
+    const BINARY_INFO_TYPE_ID_AND_INT: u16 = 5;
+    const BINARY_INFO_TYPE_ID_AND_STRING: u16 = 6;
+    const BINARY_INFO_ID_RP_PROGRAM_NAME: u32 = 0x02031c86;
+    const BINARY_INFO_ID_RP_BINARY_END: u32 = 0x68f465de;
+
+    const fn push_program_name(self: Self, value: &str) -> Self {
+        self.push_raspberry_tag()
+            .push_u16(Self::BINARY_INFO_TYPE_ID_AND_STRING)
+            .push_u32(Self::BINARY_INFO_ID_RP_PROGRAM_NAME)
+            .push_string(value)
+    }
+
+    const fn push_binary_end(self: Self, value: &str) -> Self {
+        self.push_raspberry_tag()
+            .push_u16(Self::BINARY_INFO_TYPE_ID_AND_INT)
+            .push_u32(Self::BINARY_INFO_ID_RP_BINARY_END)
+            .push_string(value)
+    }
+}
+
+const fn make_program_info() -> [u8; 4096] {
+    const PROGRAM_INFO_START: u32 = 0x10086000;
+    const COPY_TABLE_SIZE: usize = 128;  // must match bootloader
+
+    ProgramInfoBuilder::new(PROGRAM_INFO_START, COPY_TABLE_SIZE)
+        .push_program_name("Test 123")
+        .build()
+}