diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..bb51ea1
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @rust-embedded/libs
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9297b84..391eae4 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,9 +1,9 @@
 name: Build
 
 on:
+  merge_group:
+  workflow_dispatch:
   pull_request:
-    branches:
-      - master
   push:
     branches:
       - master
@@ -11,40 +11,70 @@ on:
     - cron: "0 5 * * *"
 
 jobs:
+
   build:
     name: Build Library
-    runs-on: ubuntu-20.04
-
+    runs-on: ubuntu-latest
+    env: {"RUSTFLAGS": "-D warnings"}
     strategy:
       matrix:
         target:
-          [aarch64-unknown-none-softfloat, riscv64gc-unknown-none-elf, x86_64-unknown-linux-gnu]
+          - "aarch64-unknown-none-softfloat"
+          - "riscv64gc-unknown-none-elf"
+          - "x86_64-unknown-linux-gnu"
     steps:
-      - name: Rust setup
-        uses: actions-rs/toolchain@v1
+      - uses: dtolnay/rust-toolchain@master
         with:
           toolchain: nightly
-          override: true
           target: ${{ matrix.target }}
-          components: rustfmt
 
-      - name: Check out repository
-        uses: actions/checkout@v2
+      - uses: actions/checkout@v4
 
-      - name: Cargo fmt
-        uses: actions-rs/cargo@v1
-        with:
-          command: fmt
-          args: -- --check
+      - name: cargo build
+        run: cargo build --target ${{ matrix.target }}
 
-      - name: Cargo check
-        uses: actions-rs/cargo@v1
+  format:
+    name: Format
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: dtolnay/rust-toolchain@master
         with:
-          command: check
-          args: --target ${{ matrix.target }}
+          toolchain: nightly
+          components: rustfmt
+      - run: cargo fmt --all -- --check
+
+  test:
+    name: Test QEMU Exit
+    runs-on: ubuntu-latest
 
-      - name: Cargo build
-        uses: actions-rs/cargo@v1
+    strategy:
+      matrix:
+        target:
+          - "aarch64-unknown-none-softfloat"
+          - "riscv64gc-unknown-none-elf"
+
+    steps:
+      - uses: dtolnay/rust-toolchain@master
         with:
-          command: build
-          args: --target ${{ matrix.target }}
+          toolchain: nightly
+          target: ${{ matrix.target }}
+          components: llvm-tools-preview
+
+      - name: Check out repository
+        uses: actions/checkout@v4
+
+      - name: Install QEMU
+        run: |
+          sudo apt update
+          sudo apt install qemu-system
+
+      - name: Run tests AARCH64
+        if: matrix.target == 'aarch64-unknown-none-softfloat'
+        run: |
+          RUSTFLAGS="-D warnings -C link-arg=-Ttests/aarch64_raspi3/link.ld" cargo test --target ${{ matrix.target }} --release
+
+      - name: Run tests RISCV64
+        if: matrix.target == 'riscv64gc-unknown-none-elf'
+        run: |
+          RUSTFLAGS="-D warnings -C link-arg=-Ttests/riscv64_virt/link.ld" cargo test --target ${{ matrix.target }} --release
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
deleted file mode 100644
index ea06c6f..0000000
--- a/.github/workflows/test.yml
+++ /dev/null
@@ -1,47 +0,0 @@
-name: Test
-
-on:
-  pull_request:
-    branches:
-      - master
-  push:
-    branches:
-      - master
-  schedule:
-    - cron: "0 5 * * *"
-
-jobs:
-  build:
-    name: Test QEMU Exit
-    runs-on: ubuntu-20.04
-
-    strategy:
-      matrix:
-        target: [aarch64-unknown-none-softfloat, riscv64gc-unknown-none-elf]
-
-    steps:
-      - name: Rust setup
-        uses: actions-rs/toolchain@v1
-        with:
-          toolchain: nightly
-          override: true
-          target: ${{ matrix.target }}
-          components: llvm-tools-preview
-
-      - name: Check out repository
-        uses: actions/checkout@v2
-
-      - name: Install QEMU
-        run: |
-          sudo apt update
-          sudo apt install qemu-system
-
-      - name: Run tests AARCH64
-        if: matrix.target == 'aarch64-unknown-none-softfloat'
-        run: |
-          RUSTFLAGS="-C link-arg=-Ttests/aarch64_raspi3/link.ld" cargo test --target ${{ matrix.target }} --release
-
-      - name: Run tests RISCV64
-        if: matrix.target == 'riscv64gc-unknown-none-elf'
-        run: |
-          RUSTFLAGS="-C link-arg=-Ttests/riscv64_virt/link.ld" cargo test --target ${{ matrix.target }} --release
diff --git a/Cargo.toml b/Cargo.toml
index 84e37f4..9ef515e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,8 +3,8 @@ name = "qemu-exit"
 version = "3.0.2"
 authors = ["Andre Richter <andre.o.richter@gmail.com>"]
 description = "Exit QEMU with user-defined code"
-homepage = "https://github.com/andre-richter/qemu-exit"
-repository = "https://github.com/andre-richter/qemu-exit"
+homepage = "https://github.com/rust-embedded/qemu-exit"
+repository = "https://github.com/rust-embedded/qemu-exit"
 readme = "README.md"
 keywords = ["aarch64", "x86_64", "risc-v", "qemu", "exit"]
 categories = ["embedded", "hardware-support", "no-std"]
diff --git a/Makefile b/Makefile
index 24e92b6..733855a 100644
--- a/Makefile
+++ b/Makefile
@@ -1,16 +1,20 @@
 TARGET_AARCH64 := aarch64-unknown-none-softfloat
+TARGET_AARCH32 := thumbv7em-none-eabi
 LINKER_SCRIPT_AARCH64 := tests/aarch64_raspi3/link.ld
+LINKER_SCRIPT_AARCH32 := tests/armv7m_mps2an500/link.ld
 
 TARGET_RISCV64 := riscv64gc-unknown-none-elf
 LINKER_SCRIPT_RISCV64 := tests/riscv64_virt/link.ld
 
 default:
 	cargo build --target $(TARGET_AARCH64) --release
+	cargo build --target $(TARGET_AARCH32) --release
 	cargo build --target $(TARGET_RISCV64) --release
 	cargo build --release
 
 clippy:
 	cargo clippy --target $(TARGET_AARCH64)
+	cargo clippy --target $(TARGET_AARCH32)
 	cargo clippy --target $(TARGET_RISCV64)
 	cargo clippy
 
@@ -19,6 +23,10 @@ test:
                 cargo test \
                 --target $(TARGET_AARCH64) \
                 --release
+	RUSTFLAGS="-C link-arg=-T$(LINKER_SCRIPT_AARCH32) -Clink-args=-Map=/tmp/qemuexit-mapfile.map" \
+                cargo test \
+                --target $(TARGET_AARCH32) \
+                --release
 	RUSTFLAGS="-C link-arg=-T$(LINKER_SCRIPT_RISCV64)" \
                 cargo test \
                 --target $(TARGET_RISCV64) \
diff --git a/README.md b/README.md
index 31076ef..9339d1a 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,6 @@
 [![crates.io](https://img.shields.io/crates/d/qemu-exit.svg)](https://crates.io/crates/qemu-exit)
 [![crates.io](https://img.shields.io/crates/v/qemu-exit.svg)](https://crates.io/crates/qemu-exit)
-![Build](https://github.com/andre-richter/qemu-exit/workflows/Build/badge.svg)
-![Test](https://github.com/andre-richter/qemu-exit/workflows/Test/badge.svg)
+![Build](https://github.com/rust-embedded/qemu-exit/workflows/Build/badge.svg)
 
 # qemu-exit
 
@@ -10,6 +9,8 @@ Exit QEMU with user-defined code.
 Quit a running QEMU session with user-defined exit code. Useful for unit or integration tests using
 QEMU.
 
+This project is developed and maintained by the [libs team].
+
 ## TL;DR
 
 ```rust
@@ -34,11 +35,13 @@ qemu_exit_handle.exit_failure();
 
 ## Architecture Specific Configuration
 
-### AArch64
+### AArch64/AArch32
 
 Pass the `-semihosting` argument to the QEMU invocation, e.g.:
 ```
 qemu-system-aarch64 -M raspi3 -serial stdio -semihosting -kernel kernel8.img
+qemu-system-arm -nographic -M mps2-an500 -cpu cortex-m7 -serial mon:stdio -semihosting -kernel
+kernel.img
 ```
 
 ### RISCV64
@@ -55,7 +58,8 @@ Add the special ISA debug exit device by passing the flags:
 -device isa-debug-exit,iobase=0xf4,iosize=0x04
 ```
 
-When instantiating the handle, `iobase` must be given as the first parameter.
+When instantiating the handle with `qemu_exit::X86::new()`, `iobase` must be given as the first
+parameter.
 
 The second parameter must be an `EXIT_SUCCESS` code of your choice that is an odd number, aka
 bit number zero must be `1`. This is needed because in QEMU, the provided code is internally
@@ -66,11 +70,23 @@ possible to let QEMU invoke `exit(0)`.
 let qemu_exit_handle = qemu_exit::X86::new(io_base, custom_exit_success);
 ```
 
+#### x86/x86_64 Linux
+
+To use this mechanism from Linux userspace, the kernel must be compiled with
+`CONFIG_X86_IOPL_IOPERM=y` (which is the default) and the process must start with root privileges
+(or `CAP_SYS_RAWIO`) and call: [`ioperm(2)`](https://man7.org/linux/man-pages/man2/ioperm.2.html):
+```rust
+nix::errno::Errno::result(unsafe { libc::ioperm( 0xf4, 4, 1 )}).expect("ioperm failed");
+```
+
+Privileges/capabilities can then be dropped. Normal users can subsequently call
+`qemu_exit_handle.exit*()`.
+
 ## Literature
 
-- [Semihosting for AArch32 and AArch64](https://static.docs.arm.com/dui0003/b/semihosting.pdf)
-- [QEMU isa-debug-exit source](https://git.qemu.org/?p=qemu.git;a=blob;f=hw/misc/debugexit.c)
-- [QEMU sifive_test source](https://git.qemu.org/?p=qemu.git;a=blob;f=hw/misc/sifive_test.c)
+- [Semihosting for AArch32 and AArch64](https://github.com/ARM-software/abi-aa/blob/main/semihosting/semihosting.rst)
+- [QEMU isa-debug-exit source](https://gitlab.com/qemu-project/qemu/-/blob/master/hw/misc/debugexit.c)
+- [QEMU sifive_test source](https://gitlab.com/qemu-project/qemu/-/blob/master/hw/misc/sifive_test.c)
 
 ## Authors
 
@@ -94,3 +110,5 @@ party projects and their respective licenses.
 Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the
 work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any
 additional terms or conditions.
+
+[libs team]: https://github.com/rust-embedded/wg#the-libs-team
diff --git a/src/aarch32.rs b/src/aarch32.rs
new file mode 100644
index 0000000..2f86d51
--- /dev/null
+++ b/src/aarch32.rs
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+//
+// Copyright (c) 2024 Philipp Schulz <schulz.phil@gmx.de>
+
+//! AArch32.
+
+use crate::QEMUExit;
+use core::arch::asm;
+
+const EXIT_SUCCESS: u32 = 0;
+const EXIT_FAILURE: u32 = 1;
+
+#[allow(non_upper_case_globals)]
+const ADP_Stopped_ApplicationExit: u32 = 0x20026;
+
+/// The parameter block layout that is expected by QEMU.
+///
+/// If QEMU finds `ADP_Stopped_ApplicationExit` in the first parameter, it uses the second parameter
+/// as exit code.
+///
+/// If first paraemter != `ADP_Stopped_ApplicationExit`, exit code `1` is used.
+#[repr(C)]
+struct QEMUParameterBlock {
+    arg0: u32,
+    arg1: u32,
+}
+
+/// AArch32 configuration.
+pub struct AArch32 {}
+
+/// A Semihosting call using `0x20` - `SYS_EXIT_EXTENDED`.
+fn semihosting_sys_exit_call(block: &QEMUParameterBlock) -> ! {
+    unsafe {
+        asm!(
+            "bkpt #0xab",
+            in("r0") 0x20,
+            in("r1") block as *const _ as u32,
+            options(nostack)
+        );
+
+        // For the case that the QEMU exit attempt did not work, transition into an infinite loop.
+        // Calling `panic!()` here is unfeasible, since there is a good chance this function here is
+        // the last expression in the `panic!()` handler itself. This prevents a possible
+        // infinite loop.
+        loop {
+            asm!("wfe", options(nomem, nostack));
+        }
+    }
+}
+
+impl AArch32 {
+    /// Create an instance.
+    pub const fn new() -> Self {
+        AArch32 {}
+    }
+}
+
+impl QEMUExit for AArch32 {
+    fn exit(&self, code: u32) -> ! {
+        let block = QEMUParameterBlock {
+            arg0: ADP_Stopped_ApplicationExit,
+            arg1: code as u32,
+        };
+
+        semihosting_sys_exit_call(&block)
+    }
+
+    fn exit_success(&self) -> ! {
+        self.exit(EXIT_SUCCESS)
+    }
+
+    fn exit_failure(&self) -> ! {
+        self.exit(EXIT_FAILURE)
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 97b7515..5cf1347 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -15,6 +15,9 @@
 //! #[cfg(target_arch = "aarch64")]
 //! let qemu_exit_handle = qemu_exit::AArch64::new();
 //!
+//! #[cfg(target_arch = "arm")]
+//! let qemu_exit_handle = qemu_exit::Aarch32::new();
+//!
 //! // addr: The address of sifive_test.
 //! #[cfg(target_arch = "riscv64")]
 //! let qemu_exit_handle = qemu_exit::RISCV64::new(addr);
@@ -33,14 +36,24 @@
 //!
 //! ### AArch64
 //!
-//! Pass the `-semihosting` argument to the QEMU invocation, e.g.:
+//! Pass the `-semihosting` argument to the QEMU invocation, e.g.
+//!
 //! ```
 //! qemu-system-aarch64 -M raspi3 -serial stdio -semihosting -kernel kernel8.img
 //! ```
 //!
+//! ### AArch32
+//!
+//! Pass the `-semihosting` argument to the QEMU invocation, e.g.
+//!
+//! ```
+//! qemu-system-arm -m 16M -nographic -M mps2-an500 -cpu cortex-m7 -serial mon:stdio -semihosting -kernel kernel.img
+//! ```
+//!
 //! ### RISCV64
 //!
 //! You need to chose a machine with the `sifive_test` device, for exemple `-M virt`:
+//!
 //! ```
 //! qemu-system-riscv64 -M virt -nographic -monitor none -serial stdio -kernel kernel.elf
 //! ```
@@ -48,6 +61,7 @@
 //! ### x86_64
 //!
 //! Add the special ISA debug exit device by passing the flags:
+//!
 //! ```
 //! -device isa-debug-exit,iobase=0xf4,iosize=0x04
 //! ```
@@ -65,9 +79,9 @@
 //!
 //! ## Literature
 //!
-//! - [Semihosting for AArch32 and AArch64](https://static.docs.arm.com/dui0003/b/semihosting.pdf)
-//! - [QEMU isa-debug-exit source](https://git.qemu.org/?p=qemu.git;a=blob;f=hw/misc/debugexit.c)
-//! - [QEMU sifive_test source](https://git.qemu.org/?p=qemu.git;a=blob;f=hw/misc/sifive_test.c)
+//! - [Semihosting for AArch32 and AArch64](https://github.com/ARM-software/abi-aa/blob/main/semihosting/semihosting.rst)
+//! - [QEMU isa-debug-exit source](https://gitlab.com/qemu-project/qemu/-/blob/master/hw/misc/debugexit.c)
+//! - [QEMU sifive_test source](https://gitlab.com/qemu-project/qemu/-/blob/master/hw/misc/sifive_test.c)
 
 #![deny(missing_docs)]
 #![no_std]
@@ -78,6 +92,12 @@ pub mod aarch64;
 #[cfg(target_arch = "aarch64")]
 pub use aarch64::*;
 
+#[cfg(target_arch = "arm")]
+pub mod aarch32;
+
+#[cfg(target_arch = "arm")]
+pub use aarch32::*;
+
 #[cfg(target_arch = "riscv64")]
 pub mod riscv64;
 
diff --git a/tests/armv7m_mps2an500/link.ld b/tests/armv7m_mps2an500/link.ld
new file mode 100644
index 0000000..e46b951
--- /dev/null
+++ b/tests/armv7m_mps2an500/link.ld
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: MIT OR Apache-2.0
+ *
+ * Copyright (c) 2024 Philipp Schulz <schulz.phil@gmx.de>
+ */
+ENTRY(_vectors)
+
+SECTIONS
+{
+    . = 0x0;
+    .boot :
+    {
+        KEEP(*(.text.boot))
+        KEEP(*(.data.boot))
+    }
+
+    .text :
+    {
+        *(.vectors)
+        *(.text._start)
+        *(.text)
+    }
+}
diff --git a/tests/armv7m_mps2an500/mod.rs b/tests/armv7m_mps2an500/mod.rs
new file mode 100644
index 0000000..c221c10
--- /dev/null
+++ b/tests/armv7m_mps2an500/mod.rs
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+//
+// Copyright (c) 2024 Philipp Schulz <schulz.phil@gmx.de>
+
+//! AArch32 specific setup code.
+
+use core::arch::global_asm;
+
+global_asm!(include_str!("./startup.S"));
+
+#[no_mangle]
+extern "C" fn entry() {
+    super::test_main()
+}
diff --git a/tests/armv7m_mps2an500/startup.S b/tests/armv7m_mps2an500/startup.S
new file mode 100644
index 0000000..00507f7
--- /dev/null
+++ b/tests/armv7m_mps2an500/startup.S
@@ -0,0 +1,12 @@
+.section ".text.boot","ax"
+
+.thumb
+.thumb_func
+.globl _vectors
+_vectors:
+.word 0x2000
+.word reset
+
+.thumb_func
+reset:
+    bl entry
diff --git a/tests/exit_13.rs b/tests/exit_13.rs
index 0883764..cec0490 100644
--- a/tests/exit_13.rs
+++ b/tests/exit_13.rs
@@ -20,6 +20,16 @@ const QEMU_EXIT_HANDLE: qemu_exit::AArch64 = qemu_exit::AArch64::new();
 #[cfg(target_arch = "aarch64")]
 mod aarch64_raspi3;
 
+//--------------------------------------------------------------------------------------------------
+// Aarch32
+//--------------------------------------------------------------------------------------------------
+
+#[cfg(target_arch = "arm")]
+const QEMU_EXIT_HANDLE: qemu_exit::AArch32 = qemu_exit::AArch32::new();
+
+#[cfg(target_arch = "arm")]
+mod armv7m_mps2an500;
+
 //--------------------------------------------------------------------------------------------------
 // RISCV64
 //--------------------------------------------------------------------------------------------------
diff --git a/tests/test_runner_wrapper.sh b/tests/test_runner_wrapper.sh
index ec9f6cd..3b2a18e 100755
--- a/tests/test_runner_wrapper.sh
+++ b/tests/test_runner_wrapper.sh
@@ -1,9 +1,11 @@
 #!/usr/bin/env bash
 
 TIMEOUT="timeout 10"
-
 if [[ $1 == *"aarch64"* ]]; then
-    $TIMEOUT qemu-system-aarch64 -M raspi3 -display none -semihosting -kernel $1
+    $TIMEOUT qemu-system-aarch64 -M raspi3b -display none -semihosting -kernel $1
+elif [[ $1 == *"thumbv7em"* ]]; then
+    arm-none-eabi-objcopy -O binary $1 /tmp/qemu_exit_armv7em.bin
+    qemu-system-arm -m 16M -nographic -M mps2-an500 -cpu cortex-m7 -serial mon:stdio -semihosting -kernel /tmp/qemu_exit_armv7em.bin
 elif [[ $1 == *"riscv64"* ]]; then
     $TIMEOUT qemu-system-riscv64 -M virt -bios tests/riscv64_virt/fw_jump.elf -display none -kernel $1
 fi
