diff --git a/nrf-hal-common/src/lib.rs b/nrf-hal-common/src/lib.rs
index 1e085125..a8468a53 100644
--- a/nrf-hal-common/src/lib.rs
+++ b/nrf-hal-common/src/lib.rs
@@ -1,6 +1,7 @@
 //! Implementation details of the nRF HAL crates. Don't use this directly, use one of the specific
 //! HAL crates instead (`nrfXYZ-hal`).
 
+#![doc(html_root_url = "https://docs.rs/nrf-hal-common/0.11.1")]
 #![no_std]
 
 use embedded_hal as hal;
diff --git a/nrf51-hal/Cargo.toml b/nrf51-hal/Cargo.toml
index 85c7fd78..8a11a7bc 100644
--- a/nrf51-hal/Cargo.toml
+++ b/nrf51-hal/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "nrf51-hal"
-version = "0.11.0"
+version = "0.11.1"
 edition = "2018"
 description = "HAL for nRF51 microcontrollers"
 repository = "https://github.com/nrf-rs/nrf-hal"
@@ -23,7 +23,7 @@ nrf51 = "0.9.0"
 path = "../nrf-hal-common"
 default-features = false
 features = ["51"]
-version = "0.11.0"
+version = "=0.11.1"
 
 [dependencies.embedded-hal]
 features = ["unproven"]
diff --git a/nrf51-hal/src/lib.rs b/nrf51-hal/src/lib.rs
index 18bbca70..683158df 100644
--- a/nrf51-hal/src/lib.rs
+++ b/nrf51-hal/src/lib.rs
@@ -1,4 +1,5 @@
 #![no_std]
+#![doc(html_root_url = "https://docs.rs/nrf51-hal/0.11.1")]
 
 use embedded_hal as hal;
 pub use nrf_hal_common::*;
diff --git a/nrf52810-hal/Cargo.toml b/nrf52810-hal/Cargo.toml
index a3b29355..0d5e3413 100644
--- a/nrf52810-hal/Cargo.toml
+++ b/nrf52810-hal/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "nrf52810-hal"
-version = "0.11.0"
+version = "0.11.1"
 edition = "2018"
 description = "HAL for nRF52810 microcontrollers"
 repository = "https://github.com/nrf-rs/nrf-hal"
@@ -22,7 +22,7 @@ nrf52810-pac = "0.9.0"
 path = "../nrf-hal-common"
 default-features = false
 features = ["52810"]
-version = "0.11.0"
+version = "=0.11.1"
 
 [dependencies.embedded-hal]
 features = ["unproven"]
diff --git a/nrf52810-hal/src/lib.rs b/nrf52810-hal/src/lib.rs
index 8aed2c26..2557cf29 100644
--- a/nrf52810-hal/src/lib.rs
+++ b/nrf52810-hal/src/lib.rs
@@ -1,4 +1,5 @@
 #![no_std]
+#![doc(html_root_url = "https://docs.rs/nrf52810-hal/0.11.1")]
 
 use embedded_hal as hal;
 pub use nrf_hal_common::*;
diff --git a/nrf52832-hal/Cargo.toml b/nrf52832-hal/Cargo.toml
index f03d21d3..11461be5 100644
--- a/nrf52832-hal/Cargo.toml
+++ b/nrf52832-hal/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "nrf52832-hal"
-version = "0.11.0"
+version = "0.11.1"
 description = "HAL for nRF52832 microcontrollers"
 
 repository = "https://github.com/nrf-rs/nrf-hal"
@@ -21,7 +21,7 @@ nrf52832-pac = "0.9.0"
 path = "../nrf-hal-common"
 default-features = false
 features = ["52832"]
-version = "0.11.0"
+version = "=0.11.1"
 
 [dependencies.embedded-hal]
 features = ["unproven"]
diff --git a/nrf52832-hal/src/lib.rs b/nrf52832-hal/src/lib.rs
index 581c842c..5af862a7 100644
--- a/nrf52832-hal/src/lib.rs
+++ b/nrf52832-hal/src/lib.rs
@@ -1,4 +1,5 @@
 #![no_std]
+#![doc(html_root_url = "https://docs.rs/nrf52832-hal/0.11.1")]
 
 use embedded_hal as hal;
 pub use nrf_hal_common::*;
diff --git a/nrf52833-hal/Cargo.toml b/nrf52833-hal/Cargo.toml
index 77be6ccb..2009b89e 100644
--- a/nrf52833-hal/Cargo.toml
+++ b/nrf52833-hal/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "nrf52833-hal"
-version = "0.11.0"
+version = "0.11.1"
 description = "HAL for nRF52833 microcontrollers"
 
 repository = "https://github.com/nrf-rs/nrf-hal"
@@ -24,7 +24,7 @@ nrf52833-pac = "0.9.0"
 path = "../nrf-hal-common"
 default-features = false
 features = ["52833"]
-version = "0.11.0"
+version = "=0.11.1"
 
 [dependencies.embedded-hal]
 features = ["unproven"]
diff --git a/nrf52833-hal/src/lib.rs b/nrf52833-hal/src/lib.rs
index 8aed2c26..146fa6e8 100644
--- a/nrf52833-hal/src/lib.rs
+++ b/nrf52833-hal/src/lib.rs
@@ -1,4 +1,5 @@
 #![no_std]
+#![doc(html_root_url = "https://docs.rs/nrf52833-hal/0.11.1")]
 
 use embedded_hal as hal;
 pub use nrf_hal_common::*;
diff --git a/nrf52840-hal/Cargo.toml b/nrf52840-hal/Cargo.toml
index 79af482a..00c33f7a 100644
--- a/nrf52840-hal/Cargo.toml
+++ b/nrf52840-hal/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "nrf52840-hal"
-version = "0.11.0"
+version = "0.11.1"
 description = "HAL for nRF52840 microcontrollers"
 
 repository = "https://github.com/nrf-rs/nrf-hal"
@@ -23,7 +23,7 @@ nrf52840-pac = "0.9.0"
 path = "../nrf-hal-common"
 default-features = false
 features = ["52840"]
-version = "0.11.0"
+version = "=0.11.1"
 
 [dependencies.embedded-hal]
 features = ["unproven"]
diff --git a/nrf52840-hal/src/lib.rs b/nrf52840-hal/src/lib.rs
index 8aed2c26..5b74ebf2 100644
--- a/nrf52840-hal/src/lib.rs
+++ b/nrf52840-hal/src/lib.rs
@@ -1,4 +1,5 @@
 #![no_std]
+#![doc(html_root_url = "https://docs.rs/nrf52840-hal/0.11.1")]
 
 use embedded_hal as hal;
 pub use nrf_hal_common::*;
diff --git a/nrf9160-hal/Cargo.toml b/nrf9160-hal/Cargo.toml
index 1e3290bb..8f711ecf 100644
--- a/nrf9160-hal/Cargo.toml
+++ b/nrf9160-hal/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "nrf9160-hal"
-version = "0.11.0"
+version = "0.11.1"
 description = "HAL for nRF9160 system-in-package"
 
 repository = "https://github.com/nrf-rs/nrf-hal"
@@ -20,7 +20,7 @@ nrf9160-pac = "0.2.0"
 path = "../nrf-hal-common"
 default-features = false
 features = ["9160"]
-version = "0.11.0"
+version = "=0.11.1"
 
 [dependencies.embedded-hal]
 features = ["unproven"]
diff --git a/nrf9160-hal/src/lib.rs b/nrf9160-hal/src/lib.rs
index 78aff180..af520856 100644
--- a/nrf9160-hal/src/lib.rs
+++ b/nrf9160-hal/src/lib.rs
@@ -1,4 +1,5 @@
 #![no_std]
+#![doc(html_root_url = "https://docs.rs/nrf9160-hal/0.11.1")]
 
 use embedded_hal as hal;
 pub use nrf_hal_common::*;
diff --git a/xtask/src/lib.rs b/xtask/src/lib.rs
index 81b08023..2c390ea2 100644
--- a/xtask/src/lib.rs
+++ b/xtask/src/lib.rs
@@ -1,4 +1,4 @@
-use std::process::Command;
+use std::{fs, process::Command};
 
 pub static HALS: &[(&str, &str)] = &[
     ("nrf51-hal", "thumbv6m-none-eabi"),
@@ -62,3 +62,105 @@ pub fn install_targets() {
         cmd
     );
 }
+
+fn file_replace(path: &str, from: &str, to: &str, dry_run: bool) {
+    let old_contents = fs::read_to_string(path).unwrap();
+    let new_contents = old_contents.replacen(from, to, 1);
+    if old_contents == new_contents {
+        panic!("failed to replace `{}` -> `{}` in `{}`", from, to, path);
+    }
+
+    if !dry_run {
+        fs::write(path, new_contents).unwrap();
+    }
+}
+
+/// Bumps the versions of all HAL crates and the changelog to `new_version`.
+///
+/// Dependency declarations are updated automatically. `html_root_url` is updated automatically.
+pub fn bump_versions(new_version: &str, dry_run: bool) {
+    let common_toml_path = "nrf-hal-common/Cargo.toml";
+    let toml = fs::read_to_string(common_toml_path).unwrap();
+
+    let needle = "version = \"";
+    let version_pos = toml.find(needle).unwrap() + needle.len();
+    let version_rest = &toml[version_pos..];
+    let end_pos = version_rest.find('"').unwrap();
+    let old_version = &version_rest[..end_pos];
+
+    {
+        // Bump the changelog first, also check that it isn't empty.
+        let changelog_path = "CHANGELOG.md";
+        let changelog = fs::read_to_string(changelog_path).unwrap();
+        // (ignore empty changelog when this is a dry_run, since that runs in normal CI)
+        assert!(
+            dry_run || !changelog.contains("(no entries)"),
+            "changelog contains `(no entries)`; please fill it"
+        );
+
+        // Prepend empty "Unreleased" section, promote the current one.
+        let from = String::from("## Unreleased");
+        let to = format!("## Unreleased\n\n(no changes)\n\n## [{}]", new_version);
+        file_replace(changelog_path, &from, &to, dry_run);
+
+        // Append release link at the end.
+        let mut changelog = fs::read_to_string(changelog_path).unwrap();
+        changelog.push_str(&format!(
+            "[{vers}]: https://github.com/nrf-rs/nrf-hal/releases/tag/v{vers}\n",
+            vers = new_version
+        ));
+        if !dry_run {
+            fs::write(changelog_path, changelog).unwrap();
+        }
+    }
+
+    {
+        println!("nrf-hal-common: {} -> {}", old_version, new_version);
+
+        // Bump `nrf-hal-common`'s version.
+        let from = format!(r#"version = "{}""#, old_version);
+        let to = format!(r#"version = "{}""#, new_version);
+        file_replace("nrf-hal-common/Cargo.toml", &from, &to, dry_run);
+
+        // Bump the `html_root_url`.
+        let from = format!(
+            r#"#![doc(html_root_url = "https://docs.rs/nrf-hal-common/{old_version}")]"#,
+            old_version = old_version
+        );
+        let to = format!(
+            r#"#![doc(html_root_url = "https://docs.rs/nrf-hal-common/{new_version}")]"#,
+            new_version = new_version
+        );
+        let librs_path = "nrf-hal-common/src/lib.rs";
+        file_replace(librs_path, &from, &to, dry_run);
+    }
+
+    for (hal, _) in HALS {
+        println!("{}: {} -> {}", hal, old_version, new_version);
+        let toml_path = format!("{}/Cargo.toml", hal);
+
+        // Bump the HAL's version.
+        let from = format!(r#"version = "{}""#, old_version);
+        let to = format!(r#"version = "{}""#, new_version);
+        file_replace(&toml_path, &from, &to, dry_run);
+
+        // Bump the HAL's dependency on `nrf-hal-common`.
+        let from = format!(r#"version = "={}""#, old_version);
+        let to = format!(r#"version = "={}""#, new_version);
+        file_replace(&toml_path, &from, &to, dry_run);
+
+        // Bump the HAL's `html_root_url`.
+        let from = format!(
+            r#"#![doc(html_root_url = "https://docs.rs/{crate}/{old_version}")]"#,
+            crate = hal,
+            old_version = old_version
+        );
+        let to = format!(
+            r#"#![doc(html_root_url = "https://docs.rs/{crate}/{new_version}")]"#,
+            crate = hal,
+            new_version = new_version
+        );
+        let librs_path = format!("{}/src/lib.rs", hal);
+        file_replace(&librs_path, &from, &to, dry_run);
+    }
+}
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
new file mode 100644
index 00000000..fd2f0984
--- /dev/null
+++ b/xtask/src/main.rs
@@ -0,0 +1,18 @@
+use std::env;
+
+fn main() {
+    let mut args = env::args().skip(1);
+    let subcommand = args.next();
+    match subcommand.as_deref() {
+        Some("bump") => {
+            let new_version = args.next().expect("missing <semver> argument");
+            xtask::bump_versions(&new_version, false);
+        }
+        _ => {
+            eprintln!("usage: cargo xtask <subcommand>");
+            eprintln!();
+            eprintln!("subcommands:");
+            eprintln!("    bump <semver> - bump crate versions to <semver>");
+        }
+    }
+}
diff --git a/xtask/tests/ci.rs b/xtask/tests/ci.rs
index 4495e889..6ff7068a 100644
--- a/xtask/tests/ci.rs
+++ b/xtask/tests/ci.rs
@@ -28,6 +28,9 @@ fn main() {
     // We execute from the `xtask` dir, so `cd ..` so that we can find `examples` etc.
     env::set_current_dir("..").unwrap();
 
+    // Make sure all the tomls are formatted in a way that's compatible with our tooling.
+    xtask::bump_versions("0.0.0", true);
+
     // Build-test every HAL.
     for (hal, target) in HALS {
         let mut cargo = Command::new("cargo");