0. 在此之前
2023 年, 我一直想给家里的 Mi 8 Lite 刷一个 Linux 系统。
当然不可能是 proot/chroot,那不好玩。要上直接上 Mainline。然后我根据网上的一篇教程,还真让我启动了。
我非常高兴,打算继续探索,乱改 DTS。结果最后发现,果不其然,除了 Simple DRM,没有一个移植乐成。
最近我又拾起了这个项目。我搞到了 Mainline 和 Android 的源代码,开始尝试移植。结果马上就撞上了知识的诅咒。
于是,我开始探究,驱动的本质是什么,为什么 Android 和 Mainline,内核名字都叫 Linux,设备树却不通用。
好了,话不多说,开始我们今天的主题吧: 时序逻辑。
1 时序和时序逻辑
我们知道,在数字电路中,电器元件的信号变动不是瞬间变动的,它通常有一个延迟(Delay),也就是说,从输入信号变动到输出信号稳定,需要一些时间。
但是问题就来了,信号这个东西,其实也可以当成数据。在集成电路中通常会把一个元件 O = f(I) 的 O 传递到下一个元器件。也就是:
定义 I = Input, On = 每个元器件的 Output
O1 = f1(I), O2 = f2(O1), O3 = f3(O2), ...
那如果 f2 接收不到稳定的 O1, 整个电路都得炸。更加棘手的是,f1 和 f2 的延迟不一样,这更难搞。
那怎么办? 或者我们可以这样,让硬件写入之后停一下,等电路稳定,然后再统一进行读取。
例如, 假设 f1 有延迟 0.5s, f2 有延迟 1s, 我们定义每隔 1s, 每个元器件进行一次读取和写入.
第 0 秒瞬间: f1 接收到信号 I
0-0.5 秒: f1 经 0.5 秒的延迟, 输出到 O1, 状态变稳定.
0.5-1 秒: f1 等待下一个操作时序.
第 1 秒瞬间: f2 接收 f1 输出的信号 O1, f1 接收到另一个输入信号 I2.
1-2 秒:
* f2 经过一秒的延迟, 输出到 O2, 状态变稳定.
* f1 经过 0.5 秒的延迟, 输出到 O1, 状态变稳定。
* 但是 f1 因为需要等 f2 变稳定, 以是它得等 f1 输出完毕.
以此类推。
这样就完美解决了因为"不同的元器件的电路延迟"而导致的"数据混乱"问题.
也就是说, 硬件在某一个时刻 t 进行一次 I/O 后, 必须强制等 x 秒, 等到第 t+x 秒的瞬间, 我们才答应硬件进行下一次数据的读取.
以是我们只需要包管元器件 fn 的延迟时间严格小于便是 x, 那就能包管每个元器件读取到的值都是稳定的.
这种电路, 我们姑且把它叫做时序逻辑. (当然我定义的确实不太精确, 不过这不是数字电路教材, 哈哈)
然后, 我们引用物理中的"周期"和"频率"的概念, 给这个 x 命名一下:
假设电路每隔 x 秒进行一次读取/写入操作, 那么我们把 freq = 1 / x 叫做这个电路的频率, 单位为 Hz.
2 最简单的时序电路
很好, 现在我们知道了很多元器件都需要一个时序. 那么时序逻辑怎么实现呢? 这个就必须得用一个外部电路元器件来实现.
这个元器件输出的信号是这样的:
我们进一步分成"红色"和"蓝色"的 部分:
可以看到, "红色"的部分是稳定态, 类似0-1秒; "蓝色"的部分是变革态, 类似 1秒瞬时变革.
我们可以让红色的部分当作电子元器件延迟反应时间, 包管电子元器件最后的输出是稳定的;
仅当输出信号变成蓝色的部分, 也就是所有的电子元器件输出都已经稳定了, 它们才可以进行读和写.
这样就乐成实现了 #1 中所说的"时序控制".
我们暂且把这个可以或许产生这种信号的元器件命个名, 叫做 XO.
那在真实的电路中, 每个电子元器件是协同工作的, 我们不可能让每一个电路都自己带一个 XO 去控制时序, 那样的话不但浪费, 还不利于每个元器件的协作.
既然这样, 我们可以在电路板上单独划分一个区域给 XO, 然后每个元器件附带一个 CLK 接口, 让一个外部 XO 接入这个 CLK, 这样就能实现外部电路控制内部元器件的时序.
由此我们可以画出这么一个图来表示一个很简单的时序逻辑电路:
这样就构成了一个最简单的时序逻辑电路.
3 问题: XO 太慢了, 怎么提速?
上面我们构建了一套超级简单的模型: **XO-元器件.
但是现在又有一个问题: XO 只能提供一个频率, 例如 19.9 Mhz. 但是所需要工作的元器件通常千奇百怪, 要提供的频率通常是不一样的.
例如这样:
XO(19.9 Mhz) - Box(1 Ghz)
可以看到 XO 并没有办法给 Box 提供它需要的时钟频率。以是我们肯定需要一个元器件,让 Box 接收到一个稳定的 1Ghz。
于是,一个新的元器件就诞生了---PLL。
PLL 的作用,就是将 XO 转换出来的低频信号,Boost 到一个高频信号。
关于 PLL 的实现,我们在此不提,因为这不是我们的重点。我们只需要抽象出一个模型,以供我们后续编程使用:
Output = PLL(Input)
其中 Output 是高频率信号,Input 是低频率信号。
那还有一个问题就是, PLL 它到底缩放多少倍呢? 我们首先构建出一个关系函数:
Freq(Output) = Freq(Input) + (N + (M / D))
其中 N + M / D 是倍数,M D 都是整数。
于是,我们知道了控制 PLL 这个器件需要 3 个参数: N, M, D. (这个在后续章节: 写驱动思路的时间会考!)
OK, 这就是我们新构建的模型:
XO(19.9 Mhz) - PLL(19.9 to 1 Ghz) - Box(1 Ghz)
4 问题: 如何给很多个元器件提供不同的频率?
在 #3 中我们加了一个 PLL。问题来了,现实中,Box 可以是:
- GPU
- Display Panel
- Sound Device
- Disk
- ...
这么多频率的元器件,总不可能都只需要 1 Ghz 吧? 可能 GPU 是 900 Mhz, Display Panel 是 600 Mhz, ...
总之,上面的模型管不了。
那我们可不可以构建出一个新的器件,将一些元件聚集在一起管呢?
你可能会说,我给 XO 并联上不同的 PLL。这很合理,问题是,这样太耗 PLL 了(因为 PLL 可以支持分数,可想而知它多复杂),也不好管。于是,我们就想办法将一些有倍数关系的元器件并连在一个 PLL 输出口,然后用一堆元器件,将频率等倍数降低。
很好,这个元器件就是 DIV。
DIV 的作用很简单,将 PLL 输出的高频率的信号,等比缩放,并且可以选择 DIV x(也就是缩放 X 倍)。
以是 DIV 元器件可以被简化为如下关系函数:
Freq(Output) = Freq(Input) / X
其中 X 是整数。
进一步推出,如果让我们去控制 DIV 这个元件,我们需要控制的是 X 这个变量。
例如:
<blockquote>
PLL 输出 900 Mhz 的信号,元器件 A 需要 300 Mhz,元器件 B 需要 450 Mhz。
于是,我们就接两个 Div:
BoxA |