代码是如何控制硬件的？比如说在单片机中，你写 0，它输出低电平。
IO（Input/Output）和寄存器。

都知道，变量是放在内存中的，给一个变量赋了值，这个变量就会 **保持不变** 。

只要从这个内存地址单元 **往芯片外面连8根电线** ，那个 **给这个字节赋值不就相当于程序对外输出8根信号** 吗？

比如，我们随便在RAM中定一个地址（0x30001234），再往这个地址写数据：

            *(volatile uint8_t *)0x30001234 = 0b1111'1111;    

这8根线就会输出高电平；

读也是差不多，只不过这时候是由 **外部的电线来写入** ， **CPU去读** 。

这就是最简单的General Purpose IO。

当然实际上并 **不需要真的从RAM里面拖线出来** ，直接在该地址放一个寄存器：

1. Input：每个时钟把引脚上的电平锁存进寄存器，CPU一读，就能知道引脚电平；

2. Output：对寄存器赋值，它就能保持状态，把这个状态加上缓冲器，就能驱动输出。

就这么简单，程序就能读你按钮的信号，并驱动一颗小灯。

当然这种很直接的做法现在很少采用，因为 **一个外设模块需要很多根线来控制**：就算是最简单的GPIO控制小灯，同一个引脚既可以配置成输入也能配置成输出，输出分为开漏和推挽，输入分为高阻态和弱上拉、是否带filter。既然如此，**寄存器的信号一般就不直接连接到芯片外了，而是连接到芯片内的GPIO模块** ，GPIO模块再把电气的东西引到片外。

在这种模式下，我们就能做出大量的peripheral：

1. PWM模块用一根线使能，寄存器中就有一个bit叫“Enable”；用两三根线控制计数器向上/向下/双向计数，寄存器就有连着的几个bitfield叫“Mode”。

2. GPIO除了电平值，还有开漏、上拉、推挽、Hi-Z等等属性，一个引脚要用好几个bit才能表示清楚。

3. UART其中有两个“byte长度”的寄存器，一般叫TX_BUF和RX_BUF。UART模块自动工作，把串行输入进来的stream放进“收缓冲器”；CPU把字符放进“发缓冲器”后，UART模块就自动一位一位地把字符移位发送出去。

所以实际上CPU本身并不直接控制外设，它只能通过某个地址上放的寄存器来和外设交流（IO就是交流的接口）。 **外设靠着时钟自己就能工作**，听命于寄存器们，再把结果存到寄存器们里面。

正因为CPU与外设通过IO这个接口交流，在语言层面和内存管理层面都需要特殊处理。

给表示IO的变量加上 **volatile** ，确保：

1. 每次读都是真的去读，而不是读临时变量，因为变量里的值是被CPU以外的master修改过的；

2. 每次写都是真的写入，而不是写进临时变量中最后一次性写入，因为一个外设多个寄存器对操作有严格的顺序要求。

给IO地址所在的页表加上 **Device** 属性，或者标记为 **不可缓存** （比如Non-Cacheable，甚至是Non-Bufferable），因为CPU本身可能对访存指令进行重排，还有L1L2缓存，这些都是必须bypass的。（STM32（Cortex-M）好像没有这个过程？那是因为**ARM把IO和Mem的划分已经写死了** ，4G的空间绝对够用，还少了页表读取的性能损失和功耗。）

当然这是Memory-Mapped-IO的情况，单独IO空间的CPU（x86）没有上述过程，因为io指令本身就不走缓存，编程语言里根本没法像访问变量那样访问IO，也就没有volatile的必要了。

本文所说的IO是对CPU来说的IO（ **冯诺依曼架构里的那个IO** ），而 **不是通常软件说的“内存/IO”的IO**。前者是内核态（高权限）的，一般程序根本摸不到；后者（存储、网络）是内核抽象出来提供给程序用的。

本文所说的寄存器有多个含义， **一个是数电里面用来保存数据的那个东西** ， **一个是嵌入式中用来做“程序-硬件”的接口的东西**。这两个概念有一定交叉，本文都有涉及 **。**