diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc
index 86f2996f1..261a3c19c 100644
--- a/docs/modules/ROOT/nav.adoc
+++ b/docs/modules/ROOT/nav.adoc
@@ -8,3 +8,5 @@
* xref:bootloader.adoc[Bootloader]
* xref:examples.adoc[Examples]
+* xref:developer.adoc[Developer]
+** xref:developer_stm32.adoc[Developer: STM32]
\ No newline at end of file
diff --git a/docs/modules/ROOT/pages/developer.adoc b/docs/modules/ROOT/pages/developer.adoc
new file mode 100644
index 000000000..e03ee51a8
--- /dev/null
+++ b/docs/modules/ROOT/pages/developer.adoc
@@ -0,0 +1 @@
+= Developer Documentation
\ No newline at end of file
diff --git a/docs/modules/ROOT/pages/developer_stm32.adoc b/docs/modules/ROOT/pages/developer_stm32.adoc
new file mode 100644
index 000000000..7c04ab1a4
--- /dev/null
+++ b/docs/modules/ROOT/pages/developer_stm32.adoc
@@ -0,0 +1,79 @@
+= Developer Documentation: STM32
+
+== Understanding metapac
+
+When a project that imports `embassy-stm32` is compiled, that project selects the feature corresponding to the chip that project is using. Based on that feature, `embassy-stm32` selects supported link:https://anysilicon.com/ip-intellectual-property-core-semiconductors/[IP] for the chip, and enables the corresponding HAL implementations. But how does `embassy-stm32` know what IP the chip contains, out of the hundreds of chips that we support? It's a long story that starts with `stm32-data-sources`.
+
+== `stm32-data-sources`
+
+link:https://github.com/embassy-rs/stm32-data-sources[`stm32-data-sources`] is as mostly barren repository. It has no README, no documentation, and few watchers. But it's the core of what makes `embassy-stm32` possible. The data for every chip that we support is taken in part from a corresponding XML file like link:https://github.com/embassy-rs/stm32-data-sources/blob/b8b85202e22a954d6c59d4a43d9795d34cff05cf/cubedb/mcu/STM32F051K4Ux.xml[`STM32F051K4Ux.xml`]. In that file, you'll see lines like the following:
+
+[source,xml]
+----
+
+
+
+----
+
+These lines indicate that this chip has an i2c, and that it's version is "v1_1". It also indicates that it has a general purpose timer that with a version of "v2_x". From this data, it's possible to determine which implementations should be included in `embassy-stm32`. But actually doing that is another matter.
+
+
+== `stm32-data`
+
+While all users of this project are familiar with `embassy-stm32`, fewer are familiar with the project that powers it: `stm32-data`. This project doesn't just aim to generate data for `embassy-stm32`, but for machine consumption in general. To acheive this, information from multiple files from the `stm32-data-sources` project are combined and parsed to assign register block implementations for each supported IP. The core of this matching resides in `chips.rs`:
+
+[source,rust]
+----
+ (".*:I2C:i2c2_v1_1", ("i2c", "v2", "I2C")),
+ // snip
+ (r".*TIM\d.*:gptimer.*", ("timer", "v1", "TIM_GP16")),
+----
+
+In this case, the i2c version corresponds to our "v2" and the general purpose timer version corresponds to our "v1". Therefore, the `i2c_v2.yaml` and `timer_v1.yaml` register block implementations are assigned to those IP, respectively. The result is that these lines arr generated in `STM32F051K4.json`:
+
+[source,json]
+----
+ {
+ "name": "I2C1",
+ "address": 1073763328,
+ "registers": {
+ "kind": "i2c",
+ "version": "v2",
+ "block": "I2C"
+ },
+ // snip
+ }
+ // snip
+ {
+ "name": "TIM1",
+ "address": 1073818624,
+ "registers": {
+ "kind": "timer",
+ "version": "v1",
+ "block": "TIM_ADV"
+ },
+ // snip
+ }
+----
+
+In addition to register blocks, data for pin and RCC mapping is also generated and consumed by `embassy-stm32`. `stm32-metapac-gen` is used to package and publish the data as a crate.
+
+
+== `embassy-stm32`
+
+In the `lib.rs` file located in the root of `embassy-stm32`, you'll see this line:
+
+[source,rust]
+----
+#[cfg(i2c)]
+pub mod i2c;
+----
+
+And in the `mod.rs` of the i2c mod, you'll see this:
+
+[source,rust]
+----
+#[cfg_attr(i2c_v2, path = "v2.rs")]
+----
+
+Because i2c is supported for STM32F051K4 and its version corresponds to our "v2", the `i2c` and `i2c_v2`, configuration directives will be present, and `embassy-stm32` will include these files, respectively. This and other configuration directives and tables are generated from the data for chip, allowing `embassy-stm32` to expressively and clearly adapt logic and implementations to what is required for each chip. Compared to other projects across the embedded ecosystem, `embassy-stm32` is the only project that can re-use code across the entire stm32 lineup and remove difficult-to-implement unsafe logic to the HAL.
\ No newline at end of file
diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc
index 0a17c6739..805a1e70e 100644
--- a/docs/modules/ROOT/pages/index.adoc
+++ b/docs/modules/ROOT/pages/index.adoc
@@ -4,14 +4,9 @@ Embassy is a project to make async/await a first-class option for embedded devel
== What is async?
-Software written without async may block on I/O operations. In an std environment, such as a PC, software can handle this either by using threads or non-blocking operations.
+When handling I/O, software must call functions that block program execution until the I/O operation completes. When running inside of an OS such as Linux, such functions generally transfer control to the kernel so that another task, known as a thread, can be executed if available, or the CPU can be put to sleep until another such task is ready to perform more work. Because an OS cannot presume that threads will behave cooperatively, threads are relatively resource-intensive, and may be forcibly interrupted they do not transfer control back to the kernel within an allotted time. But if tasks could be presumed to behave cooperatively, or at least not maliciously, it would be possible to create tasks that appear to be almost free when compared to a traditional OS thread. In Rust, these lightweight tasks, known as 'coroutines' or 'goroutines' in other languages, are implemented with async.
-With threads, one thread blocks on an I/O operation, another is able to take its place. However, even on a PC, threads are relatively heavy, and therefore some programming languages, such as Go, have implemented a concept called coroutines or 'goroutines' that are much lighter and less-intensive than threads.
-
-The other way to handle blocking I/O operations is to support polling the state of the underlying peripherals to check whether it is available to perform the requested operation. In programming languages without builtin async support,
-this requires building a complex loop checking for events.
-
-In Rust, non-blocking operations can be implemented using async-await. Async-await works by transforming each async function into an object called a future. When a future blocks on I/O the future yields, and the scheduler, called an executor, can select a different future to execute. Compared to alternatives such as an RTOS, async can yield better performance and lower power consumption because the executor doesn't have to guess when a future is ready to execute. However, program size may be higher than other alternatives, which may be a problem for certain space-constrained devices with very low memory. On the devices Embassy supports, such as stm32 and nrf, memory is generally large enough to accommodate the modestly-increased program size.
+Async-await works by transforming each async function into an object called a future. When a future blocks on I/O the future yields, and the scheduler, called an executor, can select a different future to execute. Compared to alternatives such as an RTOS, async can yield better performance and lower power consumption because the executor doesn't have to guess when a future is ready to execute. However, program size may be higher than other alternatives, which may be a problem for certain space-constrained devices with very low memory. On the devices Embassy supports, such as stm32 and nrf, memory is generally large enough to accommodate the modestly-increased program size.
== What is Embassy?