从晶体管到处理器、单片机程序开发的前导知识——直观的计算机原理


本文的定位只是让具备基本电学、数制知识的读者明白裸机工作的大致流程,还有开发单片机裸机程序中遇到的基本概念等必要的前导知识
知识点深度很浅,不针对某款特定的芯片,不讲编程,实际中请以你使用的芯片的手册为准
全程尽量避免生硬说教
即使完全看懂本文,你也并不一定立即会开发单片机程序;就像看完高中物理课本可以知道裂变是怎么一回事,但不一定立即会造核弹
作者还没本科毕业,水平有限,读者如发现本文的错误、读不懂的地方,恳请提出
全文原创,转载请标明出处
修修补补了好多次,这篇文章越来越长,修改的内容包括新增了一些最近发生在业内的幽默的事,或者改善了过去对一些概念的讲解,总的来说更像在漫谈的过程中穿插知识,希望这能极大地降低阅读门槛。前面都强调了本文的定位是教初学者一些最基本的原理
2020年8月26日,总第10次更新

本文大致结构如下:

  • 一些基本、必要的数字电子技术知识,用逻辑门引出二进制
  • 引导读者尝试打草稿"设计"一款单片机,在这个过程中讲解:
    • 微机原理的重要概念比如内核、寄存器、中断、总线等
    • 单片机的重要概念比如调试、外设等
    • 汇编语言和C语言出现的原因、优势、工具链工作细节
  • 高端单片机可以用到的技术
    • 用硬件实现功能
    • 库函数
    • 操作系统(非常粗略地介绍)

电脑处理器(processor)、手机SoC(System on Chip)、机床/自动麻将机/智能门锁/电冰箱/恒温水箱的微控制器(micro controller),它们很像,只是由于性能、功能数量、成本等的区别,我们给它们不同的名称。其中微控制器是相对而言最简单的,也可以把微控制器叫单片机(single chip)。那就以最简单的单片机为例展开本文吧
单片机是一块芯片,单片机相当于一台小电脑,它能计算和控制、存储、输出输出

在引入单片机之前,先来了解一下简单、必要的电子技术知识吧,这些知识虽然和单片机原理没关系,但是能让你知道确实能用一些电子元件搭建出单片机
放心,这部分内容很简短,我会尽量让你觉得不枯燥、不困难
否则你看到后面的时候,好奇的你可能很想追问有关深入单片机内部的问题、但是却不知道该如何向搜索引擎表达你的好奇之心

1.芯片、模拟、数字

电信号是目前最容易处理的信号,我们能用模拟电路、数字电路来处理电信号
话筒产生幅度正比于接收到的声音的响度的电压,这个电压是模拟(analog)信号
把模拟信号每隔一段时间后的新值进行量化、编码,就得到了数字(digital)信号
模拟电路
能处理模拟信号,数字电路能处理数字信号,模数转换器ADC、数模转换器DAC架起了模拟和数字沟通的桥梁

电脑处理器、手机SoC、单片机都是数字电路
电路能被集成在一块芯片里成为集成电路(IC,integrated circuit),模拟电路、数字电路都能被集成
所以如果你认为芯片就是电脑CPU、显卡、内存条等等,那就错了,你忽略了广大的模拟集成电路,还有架起模拟电路和数字电路的桥梁的模数转换器(ADC)、数模转换器(DAC)

2.晶体管

模拟电路、数字电路里都有大量的晶体管(transistor)。晶体管是大名鼎鼎的贝尔实验室的几位工程师发明的,晶体管是20世纪最伟大的发明之一,它奠定了信息时代的硬件基础,发明晶体管的几位工程师都获得了诺贝尔物理学奖

现在的分立晶体管可能长这样,能被你用肉眼轻易地看到:

后来美国德克萨斯仪器公司的基尔比(Jack Kilby)发明了将上亿数量级晶体管集成到面积不到指甲盖大小的硅晶圆上的技术,于是有了集成电路,基尔比也因此获得诺贝尔物理学奖
你的电脑上可能贴着英特尔处理器的Logo,例如下图这样:

上图中四周蓝色的部分就是晶圆放大很多倍后的样子
芯片商家所说的7纳米、14纳米芯片指的就是加工集成电路的精度值。注意:精度值并不是两个晶体管之间的间隔,就像用精度1微米的刀去刻雕塑,并不是每个刻槽的宽度都是1微米
纳米级别的精度,你很难用肉眼直接观察到细节
正是集成电路技术的不断发展,才能让电子产品越来越轻量但性能反而越来越强,以至于你的口袋里的手机的性能远超20世纪的绝大多数重达几公斤的台式机的性能


晶体管在模拟电路里放大信号,在数字电路里当开关
晶体管学起来还是非常复杂的,学过模拟电子技术的同学肯定会赞同我,这里就简短地介绍一下晶体管,不讲深奥了


上面是常见的晶体管的电路符号
晶体管可以被看作受控电源,从上面的几种晶体管里抽出一个,它的各个引脚的名称如下图所示

对于上图这种晶体管,它的Gate的电压高于Source的电压VGS能控制Drain到Source的电流IDS
举个例子:下图电路(在下一节)中只有2条电流通路,分别被用橙色、蓝色箭头标出。对于上图中的这种晶体管,电流无法从Gate流到Drain、无法从Source流到Drain,VGS能控制Drain和Source之间的导电通道的"宽度",越"宽"的话则允许流过的最大电流越大
在模拟电路里,"宽度"可能的取值非常多,多得让你对学习丧失信心
在数字电路里,"宽度"的取值范围只限于2个值:0和最大

3.逻辑门、电平

下图电路中电池负极接着地(ground,简写为GND),当然这个"地"不一定是地球表面,GND是用来定义电路中电压为0V的点的
高中物理学过"定义无穷远处电势为0V"或"定义大地电势为0V",下图电路也类似地定义电池负极电压为0V
如果说某个点的电压是多少伏,那就是在说那个点的电压相对于0V点的电压;
如果说某个元件比如下图中电阻的电压,那就是在说这个电阻两端各自对0V点的电压的差值

  • 让上图电路中的信号源输出电压够小比如0V,那么"宽度"就能取到0,蓝箭头指示的电流就不存在了,那么Drain的电压等于电池的正极电压3.3V
  • 让信号源输出电压够大比如3.3V,那么"宽度"就能取到最大,蓝箭头指示的电流也能达到最大,那么这时Drain的电压就接近电池负极的电压0V

这样就实现了通过信号源的电压来控制蓝色箭头指示的电流通路的通断,晶体管的作用是不是很像开关呢
这个电路实现了将输入取反的功能,这样的数字电路叫非门,上图的非门存在一些问题比如输出0V时有电流流过电阻,那么电阻会消耗电能
以后就用下面这个符号来表示非门,左边输入,右边输出
显然下面的非门符号省略了电源引脚和地引脚。你要明白:任何电路的正常工作都需要电源

在上面的非门的例子中也可以把0V称作低电平或者用0表示,把3.3V称作高电平或者用1表示
实际上"不会导致数字电路错误工作"的低电平、高电平的电压值并不固定,而是在一定范围内,比如上面的非门的低电平范围可能是-0.3V~+0.5V、高电平范围可能是2.8V~3.5V

如果把晶体管换成耐压值更高的、电池换成5V的,那么低电平、高电平的范围也可能变化,可能高电平的范围变成4.5V~5.2V
低电平是0、高电平是1的数字逻辑称作正逻辑,否则称作反逻辑,大多数情况下选用正逻辑,如果用反逻辑,得特别说明
电平的电压范围是这种电平规则的特征,比如高电平是3.3V、低电平是0V的电平规则叫CMOS电平,高电平是5V、低电平是0V的电平规则叫TTL电平
如果要实现不同电平的互联,可能需要进行电平转换,否则可能烧毁晶体管或者晶体管不认前级发来的高电平

还能用晶体管搭建出与门、或门、异或门等等,这些电路叫逻辑门电路。这里没必要画出其它逻辑门的电路图了吧,就算画了,你也不一定看,总之你知道确实有这些逻辑门就行,你设计不出来不代表那些天才工程师、科学家们设计不出来
与门:可以多输入,所有输入都为1才输出1
或门:可以多输入,只要有输入为1就输出1
异或门:只能2输入,2个输入不同则输出1,否则输出0

逻辑门并非只能用电子元件来搭建,《三体》中就有让士兵充当逻辑门、用千军万马组成大规模逻辑电路来执行程序的情节,在电子计算机被发明之前,也有人用纯机械造出过二进制计算机
只是人肉和机械都没有电的速度快和便于被大规模集成


这些简单的逻辑门电路能构成功能更复杂的电路,比如加法电路
这个加法电路可能有2组输入、1组输出,每组输入可能有8位,输出可能也是8位,输入2个二进制数,自动输出相加结果
你可能会问如果8位不够表示相加结果怎么办?怎么算减法?怎么计算负数的运算?这些问题并不影响你理解本文后面的部分,你要是有兴趣的话,那就稍后去自行搜索这些问题吧

你知道能用逻辑门搭出加法器就行,你是否能搭建出加法器并不影响你认识单片机
类似地,用逻辑门还能设计出数据选择器、编码器、译码器等等逻辑电路

这个加法器既然是用逻辑门构成的,那么也可以用0、1来表示它的输入、输出,这种情况下用二进制表示数具备天然的优势

4.数制、存储空间单位

刚才提到了二进制(binary),二进制是满二进一的计数方式,我们最常用的数制是十进制,可能是因为人类有10支手指吧,我在幼儿园就学过用手指算100以内的加减法,用的数制是十进制
二进制的10读作"一零"而不是"十",也不是"二",它等于十进制的2
为了区分二进制的10和十进制的10,在二进制的10末尾加上字母"B"(就是binary的第一个字母),以后看到10B就知道是二进制数一零了

一个不算太大的十进制数转成的二进制数可能很长,于是有了十六进制(hexdecimal),十六进制比十进制更容易转换成二进制,你手动计算的时候就能体会到了
十六进制的10得与十进制的10有所区别,人们在十六进制数的开头加上"0x"或者在末尾加上字母"H"(就是hexdecimal的首字母)

十六进制要满十六进一,十六进制的0~9用数字0~9表示,10~15用字母A~F表示(不区分大小写)
15=1111B=0xF=0FH,这4个数分别是十进制、二进制、十六进制、十六进制,它们都等于绝大多数人类一只手的手指的数量的3倍
上面的0FH不写作"FH",是因为规定过以字母H结尾的十六进制数必须以数字开头,目的是避免在代码中与名为"FH"的变量名、函数名、标号等等弄混。在代码里给变量名、函数名、其它标号等等以数字开头的字符串命名会被报错

取二进制数的英文单词binary digit的首尾组成一个新词bit,用bit表示二进制数的长度,比如1001B占4bit,4bit简称4b(注意是小写b)
8bit=1Byte,1Byte简称1B(注意是大写B,但这样就和二进制数1B产生歧义了,但是只要Byte数大于1就没有歧义了,比如4B,二进制数中怎么可能出现4呢)
1024Byte=1KibiByte=8Kibibit,KibiByte简称KiB。好像和很多同学印象中Windows操作系统用的KB不一样啊?你可以去查ISO(国际标准组织)的文件,Ki表示210,K表示1000,Windows很久以前用错了单位,于是将错就错这么多年了
从Ki开始,每乘1024就依次得到新单位Mi、Gi、Ti、Pi、Ei、Zi、Yi、Bi

5.组合逻辑电路、时序逻辑电路

上面提到的数字电路的输出都只与输入有关,这样的数字电路叫组合逻辑电路
还有一类数字电路,它下一时刻的输出不仅与当前时刻的输入有关,还与过去时刻的输出有关。举个例子比如秒表,它在计时的时候:

  • 这个时刻显示的秒数是3,那么1秒后它显示的秒数得是4,再下1秒后显示的秒数得是5,这是下一时刻的输出与过去时刻的输出有关;
  • 如果你按下了清零键,那么下一时刻它显示的秒数就是0了,这是下一时刻的输出与这个时刻的输入有关

秒表电路需要知道到底有没有"过了1秒",可以每隔1秒发一个高脉冲或低脉冲或上升沿或下降沿让电路知道过了1秒,这个周期为1秒的信号就是这个时序逻辑电路的时钟信号
这种下一时刻的输出与这一时刻的输入和过去时刻的输出都有关的逻辑电路叫时序逻辑电路
时钟信号由振荡电路产生,时序逻辑电路的内部构造比组合逻辑电路的更复杂,设计振荡电路也有一定难度。这里就不打击大家的阅读兴趣了

现在你具备了一定的知识储备,那么来尝试设计你的单片机。会设计单片机的话,也能大致知道设计电脑处理器的流程

6.最小系统、板级设计

前面说了单片机是一块芯片,看上去显然它不能直接使用,就像把你的手机拆开,里面肯定不是只有芯片,而是焊了芯片和其它元件的几块电路板
如果要学习开发单片机程序,也肯定不能只用一块单片机芯片,肯定得用这块芯片制作电路板,这种用来学习单片机的电路板叫开发板(development board),注意是电路板的板而非版权的版

左为单片机芯片,右为开发板

设计手机用到的电路板、单片机开发板属于板级设计;设计单片机属于器件级设计
工业生产的电路板有各种颜色,比如绿、黑、白、红、蓝等等,这些颜色是阻焊层的颜色,颜色一般并不影响电路板性能

设计需要用到单片机的电子系统比如开发板、机床、自动麻将机:

  • 肯定会需要用到时钟信号,因为你的单片机的输出肯定不能只与输入信号有关,否则功能就太简单了
  • 肯定具备复位功能,比如上电复位,否则每次上电后你无法知道单片机里的这堆数字电路到底处在什么状态、这个状态是否能正确执行指令
  • 肯定有电源

上面提到的时钟信号、复位信号、电源,再加上单片机,就能组成单片机的最小系统

可以这么理解最小系统:用你设计的单片机制作产品,无论产品是什么,时钟信号、复位信号、电源、单片机是肯定会被包含在产品里的,这4个都具备时产品才可能正常工作,只要缺少一个,那么产品总有个时候会不能正常工作;任何产品都能被看作是在最小系统上继续搭建而成的
显然最小系统不只有一块单片机芯片,而是板级系统

最小系统板上
电源可能是电池、直流电源母座
复位电路可能能实现"按下按钮时输出一个电平、松开按钮时输出另一个电平"
时钟脉冲电路可能是单片机外、电路板上的振荡电路,但有的单片机比如STM32、MSP430片内可能集成了振荡电路,它们都允许用户选择是采用片内的时钟脉冲电路还是片外板载的时钟脉冲电路,一般来说片外板载的时钟脉冲电路的精度可以更高

7.指令集

你得让单片机获取你下达的指令并执行:这个"指令"是什么?以什么形式存在?怎么向单片机下达"指令"?
你的单片机的输出是由单片机内部的数字电路处理得到的,单片机里可以有很多具备不同功能的数字电路比如加法器、定时器等等,完成一件工作比如计算一个算式1+2×3+2×5肯定会用到一些步骤,计算这个算式的步骤肯定有先后顺序,单片机可能会这么做:

  • 把3送入加法器的输入1
  • →把3送入加法器的输入2
  • →暂存加法器的输出,记为数A
  • →把5送入加法器的输入1
  • →把5送入加法器的输入2
  • →暂存加法器的输出,记为数B
  • →把数A送入加法器的输入1
  • →把数B送入加法器的输入2
  • →暂存加法器的输出,记为数C
  • →把数C送入加法器的输入1
  • →把1送入加法器的输入2
  • →加法器的输出即为计算结果

你得设计一个"听话"的电路来完成上面的指令,还得设计一些存储器来暂存中间结果

你向单片机传达指令肯定是用二进制串表示的高低电平,因为数字电路只认高低电平,假设你设定0000表示算加法,那么你希望这个"听话"的电路:

  • 收到0000,知道要把接下来的2个输入都扔给加法器了
  • →时钟信号来了个上升沿,于是把这次收到的二进制串扔给加法器的输入1
  • →时钟信号又来了个上升沿,于是把这次收到的二进制串扔给加法器的输入2
  • →时钟信号又来了个上升沿,于是把加法器的输出取出来放到某个存储器里

是不是觉得这个"听话"的电路难以设计?没事,你不会设计不代表天才的工程师们、科学家们不会设计
假设你要求这个"听话"的电路能

  • 收到0000之后把再收到的2个二进制串扔给加法器的2个输入,然后把加法器的输出存在一个地方
  • 收到0001之后,再收到第1个二进制数假设是A,再收到第2个二进制数假设是B,把A连加B次,算完后把结果存在一个地方
  • 收到0010之后把再收到的第1个二进制数假设是A扔给加法器的输入1,把再收到的第2个二进制数假设是B进行某种操作后扔给加法器的输入2,加法器的输出必须是A-B,把结果存到一个地方
  • 收到0011之后……
  • ……

看你刚才提的要求:收到0000、0001等之后就做某某事,这就是你设计的一套指令集架构(Instruction Set Architecture,简称ISA)
上面紫色背景的描述就是对你的ISA的一种实现

8.内核、CPU、微架构、字长、RISC、CISC、大小端

其他人可能也设计出了能实现你的指令集的"听话"的电路,但你们设计的电路不一定完全相同,可能别人设计的电路的执行效率更高、执行某些指令执行所需的时钟周期数更少
将这个电路和实现其它功能的电路集成到一块芯片里,就是单片机/SoC/电脑处理器

用这个"听话"的电路继续搭建出完整的单片机还得烧更多钱,于是有些能设计这种电路的人认为不如以后只设计这种电路算了,把设计好的方案卖给设计芯片的公司,这样大家都能专注于各自的领域、节省精力,最终产品也能集各家之所长
为了方便芯片公司,他们继续完善这个电路,比如集成能实现其它功能的模块
完善之后的东西叫内核(Kernel),就是单片机/SoC/处理器的内核
实现内核的方式叫微架构(Microarchitecture),不同微架构可能可以实现共同的指令集
设计者给自己设计的内核设定独一无二的名字

内核中能算加法的东西叫CPU,当然CPU不只能算加法,还能控制、做其它计算

CPU、处理器

好像刚才讲的内容与你在电商网站搜"CPU"得到的结果不太一样,你搜到的结果应该是

这种带着金属散热壳、转接电路板的东西应该叫电脑(服务器、超级计算机、个人电脑都是电脑)处理器,只是绝大多数时候人们习惯称它们为CPU

CPU(Central Processing Unit,中央处理单元)由运算器和控制器组成,显然CPU的定义并不包含金属散热壳
撬开散热壳,可以看到有个芯片被焊在电路板上。这么设计的原因是这个芯片的引脚过于密集,为了让插着这块处理器的电脑主板可以把线路做得不这么密集从而降低主板造价,于是让一小块电路板分别对接引脚密集的芯片和线路更稀疏的主板
这体现出计算机科学里的一条哲理:加个中间层,问题就解决了
散热壳和这块芯片之间填充着导热材料

并非所有的处理器都带散热壳和转接电路板,你的手机的处理器(严格地说应该叫SoC),还有绝大多数笔记本电脑的处理器就没这待遇,因为需要轻薄;绝大多数笔记本电脑的处理器芯片也通过转接电路板对接主板
没事绝对不要撬开你还要用的处理器的散热壳,否则你一定会非常后悔的

x86

上图中的2款处理器的内核所实现的指令集家族是x86,现在,能实现x86指令集的处理器几乎占满了电脑处理器市场
x86源自1970s英特尔设计的名为8086的处理器,经过英特尔的80186、80286、80386、80486、奔腾1代系列(Pentium)、……、酷睿10代系列,还有AMD的毒龙系列、闪龙系列、速龙系列、……、锐龙线程撕裂者系列的不断升级换代,x86凭借极强的性能和极强的向过去兼容性(就是过去的程序可以完全不改,就能直接运行在新款CPU上)干掉了个人电脑处理器领域的所有竞争对手

以具体型号的处理器为例,英特尔酷睿i9 9900K
能实现的指令集家族:x86_64
内核架构:Coffee Lake

AMD 锐龙7 3800X
能实现的指令集家族:x86_64
内核架构:Zen 2

强调:x86指令集家族包含很多具体的指令集比如3DNOW!、SSE、AVX等等,具体某款内核只能实现x86_64指令集家族的一部分
上面的酷睿i9 9900K和锐龙7 3800X能运行非常多的共同的程序,因为这2个处理器能完美实现很多共同的指令集
同时也有些程序是只能在其中一款处理器上正常运行,因为这样的程序会用到只有那款处理器完美支持的指令集、且没有编写使用另一款处理器完美支持的指令集实现相同功能的程序

刚才我说x86是指令集家族,因为我在Wikipedia搜“x86”看到了这句话

x86 is a family of instruction set architectures initially developed by Intel based on the Intel 8086 microprocessor and its 8088 variant.

很多网友认为x86指具体的内核架构名称,比如称刚才的英特尔酷睿i9 9900K和AMD 锐龙7 3800X的内核是x86架构的,或者说它们是x86处理器。你只需要知道网友们实际上说的x86架构就是能实现x86指令集家族的内核架构就行

还有网友认为x86指令集是具体的指令集而非指令集家族,例如用著名的硬件测试软件AIDA64测出某款具体的处理器可以实现的指令集有

x86, x86-64, MMX, SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2, AVX, AVX2, FMA, AES

看上去AIDA64认为x86、x86-64指令集是x86实现加减法等最基本功能的指令集、和后面的各代SSE等指令集不同
总之你也只需要知道有些网友认为x86是最基本的一个指令集而非指令集家族即可

字长

8086算加法的指令最高只能算16bit+16bit,即它的加法指令的操作数的长度最高为16bit。同加法指令,8086其它的指令的操作数长度最高都是16bit,所以可以说8086的字长是16bit,或者说8086是16位的
经过不断升级换代,x86有了32位、64位、128位的指令集
支持64位指令集的x86也叫x64、x86_64、x86-64
英特尔率先做出不兼容32位x86的64位CPU架构Itanium,但市场反响很差,毕竟32位x86已有的生态与Itanium无关
后来AMD做出了完全兼容32位x86的64位CPU架构AMD64,根据市场反响,AMD64理所当然地取代了Itanium
但AMD64这个叫法显得偏向AMD,于是有了更中立些的称呼,例如x64、x86-64、x86_64

为了纪念32位的80386的大获成功为个人电脑的普及立下的汗马功劳,由英特尔率先发布的支持32位指令集的x86也叫i386
具体某款处理器,其内核属于i386还是amd64,与设计处理器的厂家是英特尔还是AMD没有关系
到了现在,由于16位处理器已退出电脑市场,所以默认认为x86表示32位x86

总结:"x64"、"x86_64"、"x86-64"、"amd64"表示64位x86,现在的"x86"、"i386"表示32位x86

你下载英雄联盟、守望先锋等游戏时,游戏官网提示你必须得用64位的操作系统

下面这张是英雄联盟的

64位操作系统会使用到处理器的64位指令。由于amd64可以执行i386的各种指令,所以32位的操作系统也能跑在amd64处理器上
用64位,表示小数能具备更高的精度,表示整数能具备更大的表示范围,相当于64位能具备更高的性能
游戏运行于操作系统之上,游戏的很多功能实际上是操作系统的功能组成的
所以,游戏可能是为了更好的游戏体验才要求你使用64位的操作系统

业界笑话

英特尔比AMD技术更强、更有钱、市场占有率更高,但是产品价格也更贵,最让广大DIY玩家感到不爽的是,英特尔发布新品就像挤牙膏一样,新品比上代产品只进步了一丁点,因此英特尔也被网友们戏称为牙膏厂
AMD的新品经常性能与英特尔的新品相当,甚至远超英特尔新品的性能,但价格低得多,吓得英特尔坐在牙膏袋子上。因此AMD被网友们戏称为挤牙膏的,国外某博主测评AMD的产品时的一句"AMD Yes!"也在DIY玩家中广泛流传
由于AMD出过推土机(Bulldozer)系列处理器,还做过类似于几万人手工打磨成万上亿张显卡的插槽的过于接地气的事,所以AMD常被称为农企

IP核授权、fabless、fab

ARM公司专注于设计内核,把内核设计方案卖给大名鼎鼎的ST、ADI、TI、NXP、三星、华为海思、高通、联发科、苹果等等芯片设计公司
这样的好处除了能节省成本,还能让各家公司的单片机/SoC都能实现共同的指令集,这样只要为ARM公司的内核适配好程序,那么程序就能轻易地在使用ARM公司内核的各家单片机/SoC上运行(当然得借助一些中间件来抹平各种手机/电脑型号之间的具体差异)
Anroid手机绝大多数使用的SoC都用到了ARM的内核,或者高通、华为海思、三星等有能力的公司修改之后的ARM内核,所以ARM公司借此建立了移动端(也就是手机、平板电脑)完善的应用生态,再想入局移动端SoC内核的厂商如果设计的内核无法兼容ARM内核的指令集,那么为ARM内核开发的应用程序全都得重做,即使这种内核再强,但没有足够的应用程序也不能长久地生存

也有公司从内核到芯片全都自行设计,比如大名鼎鼎的英特尔、AMD、英伟达
还有公司,内核设计、芯片设计、芯片制造全都做,这样的公司,全球唯英特尔一家

ARM公司设计的内核可以被看做是ARM公司的知识产权(intellectual property,简称IP),ARM的内核可以被称为IP核,把内核设计授权给其它公司的行为可以叫IP核授权
IP也不仅仅局限于内核设计,IP还可以是游戏、动漫、电影、小说。例如可以说用到了Tom和Jerry的形象的游戏、物品使用了猫和老鼠这个IP,印有英雄联盟里的英雄的图案的衣服使用了英雄联盟这个IP
此外IP还是因特网协议(Internet Proctol)的简写。一个缩写词具备多个含义是信息熵低下的拼音文字的通病,相比之下,通常情况下用汉字写的缩写词的语义就明确多了

像ARM、高通、华为海思、联发科等只做设计、不从事制造的公司叫fabless公司
这些fabless公司把设计交给台积电、英特尔、三星等厂商代工,像台积电这种只从事制造、不做设计的公司叫fab公司

ARM

ARM公司现在设计的内核的名字都是Cortex ??,第1个问号是"A"、"R"或"M",第2个问号是十进制数,可能是1位也可能是2位十进制数,以后可能会是更长的十进制数。例如上图中的Cortex A53、Cortex M3
高通买来Cortex A53的授权,开发出骁龙625;ST买来Cortex M3的授权,开发出STM32F1系列
骁龙625常被称为片上系统(System on Chip,简称SoC)或者微处理器(Micro Processor),常被用于手机;STM32F1系列常被成为单片机(Siingle Chip)或微控制器(Micro Control Unit,简称MCU),被广泛应用于家电、玩具等等
由于A53和M3的性能、功耗、成本的差异,人们对它们构成的芯片的称呼不同,芯片的用途也不同
不要以为是个人都可以买来ARM的授权从而轻易设计芯片,芯片里可不只有ARM的内核,还可能有其他黑科技IP,就像你的手机SoC如果里面只有ARM的内核,那为什么还叫SoC而不叫处理器算了

ARM也可以叫AARCH
第8代ARM的A系列可以被称为ARMv8A,它是64位的,可以说它属于ARM64或AARCH64

下图中的Cortex A53、M3是ARM公司设计的2款内核。发现下图中有很多东西是你不认识的?没关系,在学习使用一款具体型号的单片机的调试、存储扩展等功能时,你总会认识的。同时可见CPU是内核的一部分,而不是那个带着金属壳子的台式机处理器
同时还可以看到一款具体的内核能实现的指令集可以是多个

 

多核心、死机

从上面的Cortex A53内核示意图可以看出,Cortex A53内核有4个Core(核心),每个核心都包含1个CPU

CPU本质上是个只要不断地给它通时钟脉冲、它就会不断地取指令、取数据、执行的数字电路,CPU数决定在微观上某个时刻能同时进行多少个"只要来时钟脉冲就取指令、取数据、执行"的工作,所以多核心意味着可以有多个"听话的电路"同时取指令、取数据、执行
你买电脑手机时,卖家宣传的处理器四核、八核说的就是上面的多核心

那么如何让电脑同时做数量大于处理器核心数的工作呢?答案是分时复用。只要切换得足够快,人类就会认为机器在同时做多件事,但在微观上某个具体的时刻,同时做的工作的数量不超过核心数
这就是并发执行的特点:宏观上并行,微观上串行

既然只要通时钟脉冲、处理器就会干活,那么怎么会有死机这种事?
一般来说不会有时钟信号突然没了这种事的
那就说明执行的程序出了问题,比如执行C语言里的while(1){}这种什么事都不做的死循环,而且还是负责指挥CPU如何并发执行的操作系统在做空的死循环
比如正在执行"从硬盘读取出指令到内存并执行"的时候硬盘反应得太慢太慢,而程序又没要求CPU在等待硬盘的回应时对鼠标键盘的操作作出响应
比如突然一阵超强的电磁脉冲将内存条上的内容给毁得面目全非,导致CPU完全不认识接下来要执行的二进制串

所以死机时砸键盘、踢机箱、摇显示器、捶桌子并不能真正地解决死机问题,应该检查软硬件是否完美配合、是否有物理因素干扰电脑等等

大小端

存储二进制数,高位在前的方式叫大端(big endian),低位在前的方式叫小端(little endian)
32位ARM有大端、小端2种不同的架构,其中小端的32位ARM叫ARMEL
就像"x86"默认表示32位x86,"ARM"默认表示大端32位ARM

其实程序开发者不用太在意使用的32位ARM处理器是大端还是小端的,后文会提到的汇编器、编译器会自动解决大小端的问题,人类需要做的是选定合适的编译器,如果用的芯片是ARMEL,就选用专为ARMEL设计的编译器

 

CISC、RISC、微码

x86取胜的关键之一是极强的向过去兼容性,但这同时也让x86背负了巨大的历史包袱,现在的新款x86处理器依然要保留过去的低下效率的指令
同时指令集的不断迭代也让x86支持的指令集的集合看上去非常复杂,比如指令长短不一、操作数数量千奇百怪、实现的功能莫名其妙等等,这样的指令集叫复杂指令集(Complecated Instruction-Set Computer,简称CISC)
根据统计,在x86平台,大约20%的指令占了软件的指令总量的80%,大约80%的指令只占软件的指令总量的20%
x86的指令集过于复杂,于是有了精简指令集(Reduced Instruction-Set Computer,简称RISC)

RISC让指令的长度尽可能相同,这样对指令进行解码的实现可以更简单
使劲优化最常用的指令,用这些最常用的指令去拼凑罕见的功能,而不必给罕见的功能也设计专用的指令。这样可以保护CPU设计者的大脑

到了2020年,指令集是RISC还是CISC对性能的影响不算太大,内核架构才是对性能影响最大
在RISC和CISC这么多年的发展历程中,它们都借鉴和吸收了对方的优势

RISC吸收了CISC的优势。随着集成电路制造精度的提升越来越困难,CPU的时钟信号频率再也不能像20世纪时那样飞速增长了
但是如果能让CPU的一条指令能做更多事情的话,可以让CPU在时钟信号频率不变的情况下提升性能

CISC吸收了RISC的优势,CISC CPU里的指令译码器会把一条CISC指令分为多条RISC指令来执行,这种方式叫微码(Microcode)执行

微码的优势包括但不限于:

  • 假如某10条CISC指令都可以分解出某条RISC指令,那么只需优化那1条RISC指令的实现就能优化10条CISC指令的实现,而不必繁琐地优化10条指令的实现
  • 2018年英特尔的很多处理器被曝出代号分别为幽灵(Spectre)和熔断(Meltdown)的2个硬件漏洞,修复这种漏洞的方法是修正对出问题的CISC指令的微码解码、绕开实际上出问题的RISC指令。这样,用到被曝出问题的CISC指令的软件就能再次正常运行

MIPS、开源指令集RISC-V、其它指令集

MIPS是一种RISC指令集架构,我只知道它在路由器和移动播放器领域大放光彩,还用在我国的龙芯处理器中
20世纪时,MIPS曾经非常辉煌,具备的技术也很先进,比ARM早好几十年出现64位架构,ARM的64位架构的一部分知识产权是从MIPS收购的。可能是因为创造MIPS的团队缺乏商业头脑吧,导致MIPS现在不温不火

 

顾名思义,RISC-V是第5代RISC指令集,它的特殊之处在于它是免费、开源的,任何人任何企业都可以免费使用RISC-V指令集

指令集也算知识产权,如果拿不到ARM、英特尔、AMD的授权,贸然开发能实现ARM指令集、x86指令集的商用处理器是违法的。对指令集享有的版权也是国外阻碍我国发展商用处理器的障碍之一
不过开发军用的就没关系,毕竟军用的只要能完美实现就行,不管成本、不管技术怎么来的

RISC-V最近几年很火热,它的生态正在慢慢完善,这为我国开发基于RISC-V指令集的处理器提供了很大方便。当然想要在短期内革x86和ARM的命还很不现实

还有一些指令集,比如非常多的单片机程序开发者接触的第一款单片机STC89C51使用的指令集8051、德克萨斯仪器公司的超低功耗单片机MSP430使用的指令集MSP430、在20世纪还能在性能上暴打x86的POWER,还有一些超级计算机专有的指令集等等

9.存储器

你使用CPU时,要是得全程手动地

  • 把内核的各个输入拨到合适的电平
  • →按一下时钟信号按钮,告诉指令译码器你传完指令了
  • →再把内核的各个输入拨到合适的电平
  • →再按一下时钟信号按钮,告诉内核你传完第1个操作数了
  • →再把内核的输入拨到合适的电平
  • →……

这不得烦死你,这样算算式比你手算还慢

于是你想把指令存起来,让时钟信号能自己不断地跳变,你只需把指令编好存起来就行
你需要开发存储芯片,假设你要求你的存储芯片(现实世界中的存储器芯片的部分特性可能与下面描述的不同):

  • 能存储海量的0、1状态
  • 有1个读写控制引脚,比如高电平表示写数据,低电平表示读数据
  • 每8bit为一个存储单元,给存储单元们编号
  • 有7根地址线引脚,给它们输入7个bit就能选中这7个bit组成的二进制数编号的存储单元
  • 有8根数据线引脚,读数据时8根数据线输出选通的存储单元的8个bit,写数据时存储器把8根数据线上的8个bit写入存储单元
  • 输入存储单元的编号到地址线,给读写控制信号输入读,那么存储器的数据线输出指定存储单元里的内容
  • 输入存储单元的编号到地址线,给读写控制信号输入写,输入你想写入的二进制串到数据线,那么存储器的指定存储单元的内容就更新成你写入的二进制串
  • 有1个高电平使能引脚(使能(enable),可以理解为使……能……,比如高电平使能,那么使能脚为高电平时则存储器能工作,否则不工作,高电平就叫使能脚的有效电平)
  • 使能脚电平无效时存储器芯片的数据线引脚呈高阻态(高阻态也称Hi-Z,如果你认识"高"的英文单词、知道电学用Z表示阻抗,就不难理解Hi-Z就是高阻态。稍后让你直观理解高阻态)

可以这么理解高阻态:

上图电路中如果电阻阻值越大,那么测试点的电压就越接近信号源的输出电压,当阻值无穷大比如开路时,测试点的电压就是信号源的输出电压

存储器芯片不工作时其数据线引脚呈高阻态的好处:
假设一个电路中存储器A、B都有8根数据线D0~D7,把它们各自的Dx(x是0~7的整数)接在一起,当只有一块存储器假设是存储器A工作时,那么Dx相连的点的电压就是存储器A的数据线的输出电压
至于为什么要这么连接存储器A、B,把内核和存储器联调成功后,你就知道为什么了

NAND Flash、NOR Flash、RAM、XIP

设计个控制电路(当然是个数字电路),再把存储介质大规模集成为阵列,将连接在一起的控制电路和存储介质阵列封装到一块芯片里,就是存储芯片
或者将控制电路、带磁头的机械臂、电机、磁盘等东西封装在真空盒子里,就是电脑的机械硬盘
有的控制电路很简单,也有的更复杂,更复杂的控制电路可能可以自动地帮用户处理很多事情

2020年,断电后数据会丢失的存储介质的读写速度非常快而且造价高昂,可以每次只读写1个存储单元,想读写哪个单元就读写哪个单元。这样的存储器被命名为随机访问存储器(Random Access Memory,简称RAM)
最初的RAM存储芯片产品叫静态随机访问存储器(SRAM),其控制电路是组合逻辑电路
后来又有了容量更大、速度更快的SDRAM、DDR等RAM存储芯片,它们的控制电路都是时序逻辑电路
断电后数据不丢失的存储介质有的只能写一次,有个可以用紫外线照很久后擦掉内容后再写,有的可以用电飞快地擦除后再写,有的可以用磁头读写,比如电脑机械硬盘的存储介质磁盘

2020年,断电后不丢失数据、可以用电擦除内容的存储介质的信息:

  • EEPROM(Electrical Erasable Programable Read Only Memory)

    • 可读写任一存储单元
    • 集成度很低
    • 速度慢
  • NOR Flash
    • 可只读任一存储单元
    • 存储区分块,比如每4096个存储单元为一个块,各块内部连续、外部相邻
    • 如果要写入的存储单元的内容是0xFF就能直接写,否则要把存储单元所在的块的所有存储单元写为0xFF再继续操作
    • 集成度高些
    • 同容量下成本低些
  • NAND Flash
    • 不能只读写1个存储单元,每次得至少读或写一定数量个存储单元
    • 集成度非常高
    • 同容量下成本非常低。2020年的电脑固态硬盘的售价(元)与容量(GiB)之比已逼近1:1,2015年是大约3:1

上面的NOR Flash、NAND Flash的名字都以"Flash"结尾,所以可以把存储介质是NOR Flash、NAND Flash的存储芯片统称为闪速存储器,简称闪存

你可能会问:EEPROM是可写的,为什么叫ROM(Read Only Memory)?等下你会分析该如何把掉电不丢失数据的存储器用在你的单片机里,分析的时候你就知道了
这么一看,NOR Flash、NAND Flash也属于EEPROM的行列,但是现在EEPROM一般指上表中能随意读写任一存储单元的低集成度的存储器

NOR Flash、RAM能被读取任一存储单元,这种特性使得它们能作为单片机内核的指令、数据来源,也就是说单片机内核能执行它们存储的指令。这种特性叫就地执行(eXcute In Place,简称XIP)

运行内存、硬盘(磁盘、固态硬盘、U盘、闪存芯片)

电脑内存条上的内存芯片、手机主板上焊的内存芯片都能XIP
也就是说处理器内核可以直接执行、很方便地读写RAM上的二进制串,相当于RAM上的二进制串是可以被直接运行的,所以把RAM的存储空间叫运行内存,简称内存、运存。"内存"这个词是歧义泛滥的重灾区
一般情况下,网友们把也能XIP的NOR Flash不叫运行内存

电脑固态硬盘、手机闪存、U盘的存储介质都是NAND Flash,这些NAND Flash和电脑机械硬盘一样都不能XIP
所以你打电脑游戏时,游戏被从固态硬盘的NAND Flash或者机械硬盘的磁盘复制到能XIP的运行内存,如果运行内存太小、装不下运行游戏所必须的指令和数据,那么就不能打。显然如果已经安装好了游戏到NAND Flash或者磁盘,那么硬盘的容量与能不能打游戏没关系。有的同学经常说"安装了100个游戏,占了电脑1TiB的内存",这种表达是错的。明明是安装到硬盘,和运行内存有什么关系。极少有个人电脑具备1TiB运行内存

同理,你的手机用来存储照片、音乐的存储器也是NAND Flash,它的容量只能决定你能存多少照片、音乐、游戏,和能同时运行多少个游戏没关系。但是好多商家也把手机的NAND Flash称作内存,导致很多消费者完全分不清概念
在我的印象中:

  • 在2010年左右,Android手机如果需要存储照片、音乐,需要插存储卡(依然是NAND Flash)
  • 在2013年左右,有了自带能让用户随意存储照片、音乐、游戏的存储器的Android手机,同时也支持插存储卡,那时流行把手机自带的能随意存照片、音乐、游戏的存储器叫内置存储卡,把手动插入手机的存储卡叫外置存储卡
  • 在2015年左右至今,Android手机的内置存储卡容量越来越大,慢慢地可以不再需要外置存储卡。看上去现在一般把Android手机自带的能存照片、音乐、游戏的存储器就叫闪存

卖手机的商家声称的8G+256G存储说的就是8GiB运行内存+256GiB可以让你随意存储照片、音乐、游戏的闪存

具有片外NAND Flash控制器的单片机能直接读写单片机外面的NAND Flash存储芯片,但是写得不好的操作NAND Flash芯片的代码还容易让NAND Flash的某些存储单元被磨损得快没用了、而其它某些存储单元没有任何磨损
于是在单片机和NAND Flash芯片之间再加个单片机,这个新加的单片机和NAND Flash芯片组成的东西可以是电脑固态硬盘、U盘、移动硬盘等,新加的单片机叫固态硬盘、U盘、移动硬盘等的主控芯片(其实任何电子系统里能控制所有设备的单片机都叫主控芯片),还有的芯片将主控和NAND Flash集成在了同一块芯片里,比如EMMC芯片
专业人员给硬盘/U盘/移动硬盘/EMMC的主控芯片写入高效的管理算法,让开发电脑程序的开发者不用再担心NAND Flash的磨损不均衡等问题。就像你用U盘、电脑固态硬盘、电脑机械硬盘时不需要关心存储介质是磁盘还是NAND Flash,硬件接口是USB 2.0、USB 3.0、SATA 3.0还是NVMe协议的M.2,文件系统是NTFS、ext4、exFAT,反正复制粘贴文件都是Ctrl+C、Ctrl+V,反正完全不用担心存储介质磨损是否均衡,各存储设备的使用方法完全一样
这种设计再次反映出计算机科学里的哲学思想:加个中间层,问题就解决了

不用太担心恶意程序仅在不能XIP的存储器里

如果有个U盘插到你的电脑上后,杀毒软件说U盘里有恶意程序,可以不用担心,因为U盘不能XIP,只要恶意程序不跑到能XIP的运行内存里就行
如果恰好你的电脑的运行内存里已经有正在执行的恶意程序、且这个恶意程序一听U盘里有恶意程序就高兴地将U盘里的恶意程序请进可以XIP的运行内存了,那么就需要担心了
在我的印象中,2005年左右以前的电脑默认自动执行刚被插入电脑的U盘、光盘里的自动播放脚本,这给了恶意程序可乘之机,自动播放脚本可以把U盘、光盘里的恶意程序加载到可以XIP的运行

10.总线、寻址、内存空间、冯·诺依曼结构、哈佛结构

总线、寻址

现在对内核做出修改,让它能自动地读取你存储的指令和数据、执行完后就继续取下一条指令及其数据

  • 伸出8根线用于选择存储器里的存储单元,把这8根线命名为地址总线
  • 伸出8根线用于读写存储器里的存储单元,把这8根线命名为数据总线

假设你选用的NOR Flash、SRAM的数据线都有8根、地址线都有7根,还各有1个名为EN的使能引脚(假设高电平有效),像下图这样连接内核和存储器

上图中内核最多能访问到28=256个不同的地址,可以这么说:这个内核的寻址范围是0x00~0xFF(把0x0非得写成0x00,目的是体现出地址总线是8位的)
如果用二进制数表示内核的地址总线上的值,高位(D7最高,D0最低)在前低位在后,那么显然内核访问地址0000 0000B~0111 1111B就能访问到SRAM里的内容、访问地址1000 0000B~1111 1111B就能访问到NOR Flash里的内容
这样做的好处显而易见:内核用8根数据线就能既读写NOR Flash又能读写SRAM,而不是用16根数据线分别接到NOR Flash和SRAM各自的8根数据线
现在你知道为什么要让存储器在使能电平无效时让数据线呈高阻态了吧

我本科的微机原理教材说了很多寻址方式,比如变址寻址、基址寻址、寄存器寻址等等,除了寄存器寻址,其它的本质上都是用地址总线去寻址,只是计算要用地址总线寻的地址所需的数的来源不同而已

现在修改复位功能,让CPU在单片机掉电再上电后从地址0x80开始取指令、取数据、执行、取指令、取数据、执行……
可以这么实现:

  • 用8bit的存储器(假设命名为IP(Instruction Point),指令指针)记录内核正在执行的二进制所在的地址
  • →内核读出IP存储器记录的地址里的二进制串并执行,读出的二进制串可能是指令,也可能是指令需要的数据或数据的地址
  • →每来一个时钟周期就让IP存储器器记录的地址+1,但是内核正在执行指令时不能让IP的内容变化,否则内核会漏执行、错执行指令

你以后把编写好的程序从NOR Flash的首地址开始存储,内核就能按照你的意愿工作了

现在已设定内核在复位后从0x80处开始执行。NOR Flash的读写速率比SRAM的更慢,假设你写了一份占用0x40个存储空间的代码,你想让程序运行得更快些,实现的方法之一是让内核从读写更快的SRAM取指令、取数据,那么你可以:

    • 把你写的占用0x40个存储空间的代码放在地址0xA0~0xDF(在NOR Flash)
    • →在0x80(在NOR Flash)处放上实现这个功能的指令:把地址0xA0~0xDF处的内容复制到地址0x00~0x3F(在SRAM)、以后从0x00开始不断取指令、取数据、执行

内存空间

地址总线能寻到很多地址,但并不是每个地址都会被用到,被用到的地址的功能也可能各不相同。可以说地址总线能寻到的所有地址构成的集合是CPU的内存空间(看上去网友们所说的“内存”在绝大多数时候指的是能XIP的RAM。虽然觉得这里的“内存空间”太容易引起歧义,但业界真的就这么说了。我觉得叫“地址空间”或“寻址空间”更好)
例如大名鼎鼎的STM32的数据手册就标明了STM32的内存空间的分布:

取自STM32F103x8、STM32F103xB datasheet Rev 17 第34页  第4章 Memory Mapping
STM32的地址线有32根,这32根地址线能访问到232个不同的地址,上图指出了每个地址范围对应哪个外设的寄存器(下下章讲)、哪个存储器的内容,比如0x20000000对应单片机内部的NOR Flash的首地址,0x40012400~0x40012800对应ADC1的寄存器
232个地址并非每个都会被用到,那些没被用到的地址被标记为Reserved(保留)

 冯·诺依曼结构、哈佛结构

20世纪中叶,计算机刚问世时,可没有鼠标键盘显示器这么方便的东西。操作者需要在纸带上打很多孔,这些孔洞的位置可以表示要传达给计算机的指令和数据
2条纸带,一条记录指令,另一条记录数据,把2条纸带送进那时的计算机,计算机根据指令纸带的要求,对数据纸带进行读写,执行完后抽出数据纸带,根据计算机写在数据指代上的数据来推测程序的运行结果
这种指令和数据的存储器分家的结构叫哈佛结构


类似地,还有指令和数据存储器不分家的结构,这种结构叫冯·诺依曼结构
直到现在,我们使用的x86个人电脑依然基于冯·诺依曼很久以前提出的计算机模型

冯·诺伊曼结构和哈佛结构各有优劣,比如冯·诺伊曼结构实现起来更简单,但也更容易导致软件出现"任意代码执行漏洞"这种非常危险的漏洞
既然数据、指令不分家嘛,攻击者向你发一串不知所云的数据,你的软件把这串字符当作数据加载到内存里,看上去没什么风险吧。但是攻击者可以利用软件的漏洞让CPU把这串数据当作指令去执行,这不就可以为所欲为了?所以这种漏洞极其危险
如果你注意过Windows 10推送的更新的说明,应该注意到过修复这种漏洞的更新

11.外设、典型外设、时钟树、芯片内部示意图、电路板示意图

如果想让单片机能具备更多功能呢?那就塞入更多电路到单片机里

  • 塞入IO口控制器,这样可以控制单片机的任一引脚的功能,比如用来输出高低电平来控制LED的亮灭
  • 塞入模数转换器ADC,这样单片机就能感知外面的物理世界的变化
  • 塞入存储控制器,这样就能非常方便地使用外接的更大的存储器
  • ……

这些被塞入的电路模块叫外设(peripheral),由于它们都在单片机内部、在内核外部,所以也叫片内核外外设
有的外设比如LED灯,它并不在单片机内部,所以叫片外外设或者板载外设
有的外设在内核里,这样的外设叫核内外设

 

列举一些常用外设:

核内:

  • FPU:Float Point Unit,浮点运算单元。浮点数就是小数点的位置会变化的小数,用专门用来算小数运算的电路来算小数,当然能比前面提到的简单的加法器电路更高效
  • 中断控制器:后面有一章专门讲硬件中断。中断控制器能用来设定CPU需要响应哪些中断源、中断源的优先级等等
  • 硬件随机数生成器:如果所有输入不变、程序不变,那么输出一定不变。这既是数字信号处理系统的优势,又是软件不能生成真正的随机数的原因。但是模拟电路可以真正地实现随机数。随机数在加密算法中的运用很广泛,软件生成的随机数的算法可以被破解,但硬件随机数生成器生成的算法极难被破解,所以硬件随机数生成器更加安全
  • MPU/MMU:Memory Protect Unit/Memory Management Unit,可以用这个外设给跑在操作系统上的程序抹除内存地址布局的差异
  • 调试接口:后面的章节会讲硬件调试。硬件调试器通过单片机核内的调试接口来实时监测单片机的寄存器、内存值等等
  • 时钟控制器:看完下一章有关时钟树的内容后你就知道了。时钟控制器负责配置时钟树,具体工作包括选择时钟源、设定倍频分频值等等
  • NOR Flash控制器、NAND Flash控制器、DRAM控制器:顾名思义,内核通过它们来高速访问片外板载的存储器芯片
  • GPIO控制器:芯片不是有很多引脚(也可以叫IO口,因为能输入输出)吗,可以用这些引脚控制(注意是控制而非直接操作。单片机的IO口一般不能输出较大功率,但是可以接个功率放大器、开关、继电器来控制大功率电路)LED的亮灭、电火锅的通断,还能模拟(modle,相当于模仿,不是模拟信号的analog)出与其它芯片通信。看上去只要编写合适的程序,这样的引脚能做数字电路能做的各种事情
  • DMA:直接内存访问(Direct Memory Access,简称DMA。这里的"内存"不限于RAM,还可以是挂在总线上的寄存器。前面讲"STM32F1的内存空间布局"时提到过),它能承担在不同地址之间传递值的工作,从而让内核专注于计算和控制。比如想把单片机里的ADC转换得到的值通过单片机里的一个通信外设实时发送出去,那么可以让DMA来承担这份工作:不断地将ADC的转换结果寄存器的值写入那个通信外设的发送值寄存器。如果没有DMA,那么得让CPU来不断地把ADC转换得到的值复制给通信外设;有了DMA,CPU就不必浪费精力在这更加机械重复的工作上

核外片内:

  • 定时器/计数器:顾名思义,不做解释
  • SPI、I2C、UART、USRT等串行通信接口:单片机和板载外设通信时,可以用GPIO模拟通信,但编写GPIO模拟通信的程序很繁琐,而且模拟通信的速率难以很高,还浪费内核的资源。通信接口外设知道需要收发的数据后,可以在不需要内核干预的情况下自动地高速收发数据。业界常把UART(Universal Asynchronous Receiver/Transmitter,通用异步收发器)简称为串口,路由器刷机用户们又喜欢把USB转UART的模块叫TTL模块,但这样太容易引起歧义。"串口"应该是串行接口的简称才对;前文说过,TTL应该是高低电平分别为5V和0V的逻辑电平
  • LTDC:RGB显示屏控制器,能高速地刷新RGB显示屏
  • Chrom-ART:专门用来算图形的,比如算73%透明度的蓝+19%透明度的绿是什么颜色,相当于单片机的集成显卡

片外板载,这就非常多了,比如摄像头、震动马达、扬声器等等,如果嫌片内的外设比如ADC不够强,可以买个更高级的ADC芯片作为片外板载外设。这些片外板载外设通过单片机的GPIO、SPI等通信接口、LTDC等显示屏控制接口等核外片内外设与单片机内核进行交互

时钟树

核内外设、核外片内外设也都需要时钟脉冲,它们所需的时钟脉冲的频率一般不相同
实现的方式就是时钟树。给个稳定的脉冲源,将这个脉冲源倍频、分频后传给内核和各外设
倍频一般使用锁相环Phase-Locked Loop,简称PLL),这玩意有点复杂,你知道它能倍频就行
分频可以用时序逻辑电路里的计数器等等,比如让计数器实现六分频,那就让计数器每收到3个周期的脉冲信号后反转它自己输出的脉冲信号

以大名鼎鼎的STM32为例:

芯片内部示意图

下面是华为海思的Hi3798MV200 SoC的内部模块示意图(block diagram),很容易看懂这个图吧
中上部有Cortex A53多核心CPU、多核心GPU(计算图形显示用的)、华为海思自研的HiVXE 2.0视频解码器等等
芯片内还有一些片内外设,比如I2C接口、UART接口等等
芯片以外是存储卡、DDR RAM芯片等片外外设

取自Hi3798M V200 Brief datasheet 2016-09-18 第2页

下面是STM32F103x8、STM32F103xB系列的内部框图(有修改),也不复杂

电路板示意图

下面是STM3210E-EVAL评估板示意图
显然也很容易看懂。最中间的黑方框是单片机,这个黑方框以外的是板载外设,板载外设通过单片机黑框内壁的外设接口连接到单片机

取自STM3210E-EVAL评估板 用户手册 第7页 第2章 硬件布局和配置

12.寄存器

CPU怎么控制众多的外设呢?给它们每个分配一个内核?这样太复杂了,外设需要实现的功能一般很单一,那就给外设们分配一些寄存器吧

  • 让内核的地址总线能定位到片内外设们的寄存器
  • →把"用于控制片内外设的寄存器"的值作为“控制片内外设的逻辑电路”的输入
  • →片内外设也能把它的计算结果、工作状态写入为它分配的数据寄存器、状态寄存器
  • →CPU读取片内外设的数据寄存器、状态寄存器就能知道片内外设的工作结果和状态
  • 有的板载外设也有寄存器,但CPU可能无法直接用地址总线找到板载外设的寄存器,可以和板载外设做个约定:
    • →单片机以某种规则把板载外设的寄存器的地址(是板载外设用它自己的寻址方式寻到的地址)、单片机想读还是写板载外设的寄存器的信息发给板载外设
    • →板载外设知道单片机想访问的寄存器地址
    • →单片机通过这种方式访问板载外设的寄存器

至此你再次知道了那个非常重要的思想:寄存器能反映、控制单片机的运行状态
这里的片内外设寄存器挂在内核的地址总线上,但是寄存器并不总是需要挂在地址总线上,比如我们用的电脑的处理器Intel、AMD公司的处理器,它的CPU的很多寄存器就没挂在地址总线上,它的一些指令能读写这样的寄存器

拿大名鼎鼎的STM32单片机来举例:
如果要了解某个外设的寄存器的每一bit的意义、名称,那就去查参考手册上描述那个外设的寄存器的每一bit的功能的部分

取自STM32F101、2、3、5、7  参考手册 Rev. 20 第237页

  • 上图中的寄存器有32bit,STM32的32bit的寄存器会占用32÷8=4个地址(即STM32把每8bit作为一个存储空间来编址)
  • 这32bit组成的寄存器的名字是ADC_SR
  • bit31~bit5是Reserved的,不使用
  • bit4~bit0都有各自的名称和用途
  • 图的上部标注了Reset value(复位值),即单片机复位后这32bit寄存器的值
  • 图的上部有个Address offset(地址偏移)

可以这么理解Address offset:

  • 假设芯片设计者把地址范围0x10~0x1F总计16个地址分配给某个外设的寄存器,那么0x10就是这个外设的寄存器地址基址
  • →这16个地址中的第3、4个地址分别是0x12、0x13,假设这2个地址上的寄存器被命名为REG
  • →那么名为REG的寄存器的Address offset就是0x12-0x10=0x02

编写程序读写外设的寄存器,就能控制外设

13.硬件中断

ADC等一众外设工作得远比CPU慢,那么CPU想要实时了解各外设的工作进度和结果,该怎么操作呢?不断地读好多外设的寄存器?这样确实可以,但是太浪费CPU性能了
可以让这些外设伸出信号线连接内核里的硬件中断控制器这个核内外设,有事找CPU的外设只需向硬件中断控制器发个信号,硬件中断控制器就会根据发出信号的外设是谁来让CPU立即去帮谁做事。这种技术叫硬件中断(Hardware Interrupt)

硬件中断的重要之处就在于“发生某某事后,可以让CPU立即去处理”,而不用让CPU执行完判断“某某事”有没有发生的代码、确信“某某事”确实发生了、再去执行应对“某某事”的代码
重要的不仅仅是能省下一小段响应时间,更重要的是CPU不用执行判断“某某事”有没有发生的代码
感觉有点绕?举个例子,假设:

需要CPU并发执行多个任务,要求CPU每隔精确的1ms就切换正在执行的任务
让定时器这个片内外设以严格的1ms为单位进行记时
如果让CPU自己去读定时器的寄存器、来判断有没有经过严格的1ms,那么可能CPU去读,发现还没到1ms;过了一会儿再去读,还没到1ms;……;再过一会去读,已经超出1ms了

CPU执行了很多去读取定时器寄存器的代码来判断有没有过严格的1ms,不如把这精力拿来执行任务噢
而且难以保证做到“每隔精确的1ms就切换正在执行的任务”
所以应该让定时器每隔精确的1ms就给内核发一个硬件中断信号,内核里负责管理硬件中断的外设确认收到定时器发来的硬件中断信号后,让CPU立即去执行切换任务的代码
这里如果没有硬件中断,还真不能完成按时切换任务的要求

同理,如果需要CPU及时响应成百上千个可能随时给CPU找事的外设,那么让CPU不断执行读取成百上千个外设的寄存器真的是非常浪费CPU的精力,而且如果某外设在CPU读完它的寄存器之后的一瞬间突然有事,那么CPU只能在下次读完这个外设的寄存器后才可能处理这个外设的事
这里依然“如果没有硬件中断就可能办不好事”

硬件中断还被广泛地应用于低功耗领域。前面说了,只要给CPU通入时钟脉冲,CPU就会一直有事情做,即使做的事情始终是"跳转到正在执行的指令"(类似于C语言里的while(1){}什么都不做的死循环),CPU依然没有停下来休息
设计者可以这么设计:允许CPU休眠(是真的什么也不做。刚才说的while(1){}看上去什么也没做,实际上CPU会不停地做"跳转到正在执行的指令",就像发呆一样),允许一些硬件中断唤醒正在休眠的CPU,CPU被唤醒后去做该做的事,这件"该做的事"的最后一步是让CPU休眠
这样就可以尽可能地省电

你自己也具备中断功能。假设你爸妈不让你玩电脑游戏,你最近你又必须得上网课,你准备在上网课时偷偷摸摸地玩游戏

匹配玩家的过程中,你去看剧了,但是如果听到耳机里游戏开始的声音,你立马暂停视频→去打游戏→游戏结束后再次匹配玩家的同时去看剧
如果听到你爸妈走向你的房间的脚步声,那么不管你在打游戏还是看剧,都得立马恢复到上网课的界面,等爸妈走了再继续看剧或者玩游戏

(要认真学习,劳逸结合,上网课就认真上网课哈)

上例中,游戏开始的声音、爸妈走近你的脚步声都是中断信号,它们都能打断你当前正在做的事让你去做其它事,等做好其它事了,你再回到刚才被打断的地方继续
而游戏开始的声音让你转而去做的事(就是玩游戏。它是游戏开始的声音这个中断信号的中断服务函数)能被爸妈走近你的脚步声打断,然后你得立即去做听到爸妈的脚步声之后该做的事(就是看网课,也是中断服务函数),这里爸妈的脚步声这个中断信号具备更高的优先级,本例中的2个中断信号实现了中断嵌套

14.烧录/下载/编程

假设你使用电脑为你的单片机开发程序,那么你可以设计一个专门用来连接电脑和单片机的NOR Flash的外设放到单片机里,通过这个外设,电脑把程序发给单片机,内核把外设收到的二进制串写到单片机内的Flash
比如很多同学学习单片机知识用的第1块单片机STC89C51就在单片机里集成了名为通用异步收发器(Universal Asynchronous Receiver Transmitter,简称UART)的外设,它负责单片机与单片机之间、单片机与电脑之间以符合UART的规则的方式通信。STC公司开发了运行在Windows的专门用来向STC单片机写入程序的软件STC-ISP,STC单片机的UART外设收到某串二进制后就产生一个中断告诉内核把稍后UART接到的二进制串写入NOR Flash
这个过程叫烧录或者下载或者编程

在很久以前,给单片机的存储器写入程序需要高压,一不小心就烧毁了单片机,所以有了"烧录"这个名称
单片机从电脑获取指令,像不像你通过因特网从服务器下载游戏?所以给单片机的存储器写入程序也可以叫"下载"
"编程"这个词一般表示人写代码,但也可以表示写Flash。比如你的单片机在工作时收到了服务器发来的更新程序,那么单片机就会用服务器发来的程序覆盖掉原先的程序。可以说这个过程包含给单片机的Flash编程的过程,单片机的这种在工作时能写Flash的特性叫应用内编程(In-Application Programming,简称IAP)
所以"编程"的新含义是针对Flash的。既CPU、串口、内存空间之后,又有了"编程"这个有歧义的词
市面上也有专门用来给Flash写入数据的工具,它们叫编程器(Programmer,嗯和程序员是同一个英文单词)

这是一款编程器,它的电路板上负责对接电脑和需要被编程的存储器的芯片是CH341A。如果你要给路由器安装强大的OpenWRT操作系统,很可能用到这款编程器

15.汇编语言

有没有觉得总是用二进制串给单片机下达指令非常麻烦?不如用助记符代替这些二进制指令吧,于是汇编语言产生了

  • CPU肯定不认识你创造的助记符,得用软件把助记符转成内核能直接执行的二进制,实现这个功能的软件叫汇编器(assembler)
  • 如果用汇编开发程序的过程中,为了遵循"高内聚,低耦合"的原则,可以把实现不同功能的代码分为多个源代码文件,以便维护。每个汇编源代码文件都会被汇编器转为一个中间态的文件,有的代码并不会被用到,最终得把多个需要用到的中间态文件合为一个能直接放进单片机的Flash的文件,做合并工作的工具叫连接器(linker)

16.C语言、工具链和IDE、其它高级语言、编译型语言、解释型语言、虚拟机

C语言

汇编语言有个很大的缺陷:过于依赖内核的工作流程、存储器的布局
你用汇编语言写程序的话,得先打草稿来划分存储空间,比如地址0x12~0x21用来暂存计算的中间结果,地址0x80~0x8F用来存实现某个功能的指令,你希望掉电后依然能存储用户设定的一些变量,那么得在Flash里找一块空间用来存这些变量……
把用汇编语言写的程序移植到其它内核、其它存储器布局上可是个天大的工程
算个加法还要把数据在内存和好几个寄存器之间移来移去,从内存读个数据还要考虑到底应该用哪条读二进制串的指令,太繁琐了,没必要浪费大量精力在这种无意义的复杂事情上
不如创造一种简明、接近人类的语言、不依赖于内核和存储器的编程语言吧,C语言诞生了

汇编语言是紧贴处理器、内存的低级语言
C语言单凭代码来说可以无视处理器和寻址范围的细节,属于高级语言。具体点说就是可以无视处理器有哪些寄存器、是什么架构、实现什么指令集,可以无视运行内存芯片是SRAM还是DRAM、在所用的寻址地址不影响程序功能的前提下可以一个寻址地址都不出现在代码里

CPU更不可能认识C语言,所以需要用软件把C语言代码转成内核能直接执行的二进制,需要多个软件来实现:

  • 编译器(compiler)
    • 把C语言代码转成汇编语言,就是上面说的用助记符写的程序。再用上面说的汇编器来处理编译器生成的汇编程序
  • 连接器(linker)
    • 告诉连接器哪个地址范围用来存指令及其数据,哪个地址范围用来存变量,连接器来完成地址分配的工作。你也可以在C语言代码中告诉连接器把某些数据或指令放到哪个或哪段地址上
    • C语言代码里可能有一些指令并不会被执行,可能有一些数据并不会被用到,连接器可以只挑出会被执行的指令、会被用到的数据来连接,这样最终生成的一份二进制的体积就能尽量小

如果你在Windows操作系统上用Visual Studio、Code::Blocks、Dev C++等IDE写C语言程序,那么连接器得连接处能让Windows操作系统调用的可执行(executable,Windows下这种文件的扩展名是.exe)文件,这个步骤与连接处单片机能直接运行的一份二进制有所不同,你去了解一下Windows操作系统执行EXE文件的步骤就能大致猜出到底哪里"有所不同"了

机器性能足够但必须用到汇编的场景

  • 实际上C语言的编译器可能会在编译过程中"夹带私货",例如有个外设会把它的最新运行结果存在某个地址的寄存器里。你用C代码不断读出那个寄存器的值,如果发现这个值具备一定特征就做某事
    编译器发现你根本没写修改那个地址下的内容的代码,编译器可能认为那个地址下的内容全程都不会变化,于是把你用于"不断读出"的代码改成只读出一次再不断判断这个"只读出一次"的值是否符合特征
    这就是C语言编译器的优化功能,很多时候这样的优化确实很有价值,但也有时候这种优化反而会把事情搞砸,你可以更改编译器的优化等级设定,也可以在代码里告诉编译器不许优化某些步骤
    所以还是看看内核实际执行的二进制串对应的汇编指令更能帮助你准确判断内核的行为。这是调试中分析反汇编的过程,下一章会提到反汇编
  • 编译器也可能为了让编译得到的程序能在更低级的处理器运行,而不使用高级处理器特有的黑科技指令。所以如果你的程序只需要运行在具备黑科技指令的高级处理器,那么你可以用汇编语言来调用黑科技指令
    很多编译器支持在C语言代码中嵌入汇编代码
  • 有的寄存器并没挂在地址总线上,比如内核里的CPU的寄存器。用C语言无法读写CPU的一个具体的寄存器。但一般会有能读写CPU寄存器的汇编指令
  • 弄清楚没开放C语言源代码的程序的逻辑,就得将别人弄清楚的二进制串反汇编,但反汇编得到的代码里的函数名、变量名全都是地址、变量编号,所以可读性非常差
    破解付费软件、根据竞争对手公司已编译的二进制串来反推对方的C语言代码、黑客实在没办法了的时候挖掘软件的漏洞、信息安全人员分析恶意程序的行为,这些逆向分析的行为都需要反汇编
    付费软件商、竞争对手公司为了尽可能阻止被破解,可以用到代码混淆技术,就是团队内部合作开发时使用可维护性高的代码,对外发布二进制串时先用混淆器让高级语言代码变得毫无可读性但不影响功能、再编译后发布,这样就能尽可能烦死想要破解的人

工具链、IDE

一套能完美配合的编译器、连接器组成一个工具链(toolchain),比如ARM公司为CPU是Cortex M系列的芯片开发的裸机(就是没有操作系统,后面的"操作系统"章会再次提到)工具链有MDK裸机C语言工具链
还有其它适用于Cortex M系列的工具链,比如IAR ARM裸机C语言工具链、GCC ARM裸机C语言工具链等等
除此之外还有很多其它工具链,比如IAR 8051裸机C语言工具链、GCC MSP430裸机C语言工具链、GCC x64 Linux C语言工具链、GCC x64 Windows C语言工具链、Visual C++ x64 Windows C语言工具链等等

不同工具链的编译器的语法细节、编译细节等可能有差异,比如告诉编译器你写的函数是中断服务函数,得在函数声明前加上特殊标记,不同工具链认的标记可能不同
还有想在RAM上找片区域存变量,声明了你选中了那片区域后但还没给区域里写值时,有的工具链的编译器编译得到的汇编代码包含将这片区域里的值清零的指令,而有的编译器却会让这片区域里的值保持不变
不同编译器编译得到的中间文件还可能不能互通
总之,尽量不要在你的C语言代码中使用编译器的隐含特性,比如申请了RAM的一片区域后默认这片区域里的值被清零了、在这个被你默认的条件下写后面的代码。这样的奇技淫巧只会降低工程的可维护性、可移植性
国内很多大学广泛使用的C语言教材谭浩强的《C程序设计》就很喜欢在C语言代码中使用Visual C++ 6.0 Windows编译器的隐含特性,其实这并不值得推崇
编译器的有些隐含特性必须被使用,比如声明函数为中断服务函数的标记。这时就应该告诉别人你的代码是基于哪个工具链编写的

直接操作工具链需要使用者会使用命令行界面、对自己使用的芯片很了解,于是有了更易用的集成开发环境(Integrated Development Environment,简称IDE),IDE包含代码编辑器、调试器软件、工具链里的编译器和连接器、等等
下图是用命令行界面直接操作MDK裸机工具链中的编译器

我觉得,如果让对电脑一窍不通的同学初学单片机程序开发时使用这种东西来编译、连接、调试,应该能轻易劝退非常多的同学
等下STM32的硬件调试的例子中就会出现一款IDE

有的IDE可以换工具链,比如Code::Blocks这款C语言IDE
前提是你的电脑安装了对应的工具链

有的代码编辑器可以通过插件来变成IDE,比如微软开发的Visual Studio Code这款代码编辑器

其它高级语言、跨平台

由于C语言会被编译、连接成处理器内核直接跑的汇编代码,所以编译C语言得到的二进制串无法跨支持不同指令集的处理器,比如用ARM的C语言编译器编译得到的二进制串无法被x86处理器看懂

不如创造与处理器无关的编程语言吧,Java、Python等更高级的语言诞生了,它们能轻易地做到跨平台,具体实现是:
先实现具体某个处理器家族和某个操作系统(例如ARM64+Android、AMD64+Windows 10、AMD64+其它Linux等等)上的虚拟机(Virtual Machine)或解释器(Interpreter)
这样,用户编写代码时就可以不用关心所用的处理器和操作系统的细节了,让虚拟机/解释器负责对接(处理器+操作系统)和用户代码
于是实现跨平台、做到"编写一次、到处执行"
当然这些更高级的语言不局限于跨平台。它们还为编写代码的人员自动处理好了很多细节问题、提供了大量的功能丰富的库,它们在各自的领域大放光彩,但依然难以撼动C语言在它最擅长的硬件编程/操作系统与驱动程序开发等贴近底层硬件的领域的地位,因为C语言能直接操作给定的地址、可以严格控制代码的执行顺序

其实C语言也可以跨平台,只不过是源代码级别的跨平台,而非编译后的二进制级别的跨平台。移植C语言程序到其它操作系统、处理器时,需要修改与操作系统调用、处理器细节有关的代码
而移植Java、Python等程序,几乎可以不用修改任何代码

编译型语言、解释型语言、运行时环境

C语言会被编译为能让CPU直接看懂并执行的二进制串
Python解释器一边读Python源代码,一边向操作系统、CPU传达指令
Java与Python有点区别,Java有编译器,只不过编译得到的并不是处理器能直接看懂的二进制串,而是Java虚拟机(Java Virtual Machine,简称JVM)能看懂的字节码(Byte Code,就是这么命名的),实际上是JVM通过读字节码来向操作系统、处理器传达指令

C语言这样的语言叫编译型语言,类似的还有C++语言、Google开发的Go语言、Mozilla开发的Rust语言
Python这样的语言叫解释型语言,类似的还有MATLAB语言
Java这样的语言叫混合型语言,类似地还有微软开发的C#语言

显然,编译型语言能做到更高的执行效率。但是,开发逻辑复杂工程量浩大的程序时,绝大多数人并没有能力将编译型语言的代码优化到极致,所以这些人不一定能用编译型语言写出比用解释型语言或混合型语言写出更具执行效率和可维护性的代码

将用C语言编写的代码编译为二进制串,执行时可能会调用其它的函数库,比如操作系统提供的让光盘驱动器弹出的函数、让打印机开工的函数等等,这些会被调用的函数称为运行时环境(Runtime)
Java虚拟机、Python解释器,还有你写的代码用到的函数库,也分别是用Java代码编写的程序、用Python代码编写的程序的运行时环境

所以,如果在Windows编写了需要调用Windows操作系统特有的函数的C语言程序(已编译成了二进制串),那么这个已编译的二进制串并不能在相同处理器的Linux操作系统上直接正常运行,因为缺正确的运行时环境;当然用其它方法补上合适的运行时环境后是可以运行的

虚拟机

虚拟机是软件,顾名思义,它的作用是虚拟

Java虚拟机虚拟出对接编写代码的人员和(操作系统+处理器这些底层的东西)的中间层
还有其它的虚拟机,比如虚拟操作系统的VMWare Workstation。如果你的电脑的处理器太新、不支持很久以前的操作系统,那么可以在VMWare Workstation里安装很久以前的操作系统,由VMWare Workstation负责对接很久以前的操作系统和(你的电脑现用的操作系统+处理器)
还有虚拟处理器/指令集的Qemu,你可以用它在你的AMD64电脑上虚拟出ARM64的处理器,这样就能在你的AMD64电脑上运行ARM64的二进制串了

再次体现出计算机科学的那一伟大哲学思想:加个中间层,问题就解决了

17.调试

假设写了个非常长的程序,虽然单片机能执行,但是执行的最终结果却是错的。编程人员特别想让单片机慢点执行,以便观察到单片机执行完关键步骤后的结果是否正确,这些"结果"可能在寄存器,可能在RAM,也可能是某个板载外设的工作状态等等
上述的过程叫调试(debug)
硬件调试这么重要,赶紧把硬件调试控制器加到你设计的内核里

再拿大名鼎鼎的STM32单片机来举例:
有一种叫硬件调试器的电子系统,支持STM32的硬件调试器有很多种,比如ST-Link、J-LInk、CMSIS DAP-Link等等
硬件调试器能对接单片机和电脑,通过硬件调试器,能下载程序到单片机,电脑也能控制单片机控制单片机做这些事:

  • 一步步地执行指令(即单步执行)
  • 执行到某条指令前停住(即设定断点)
  • 实时查看和修改某些寄存器、变量的值
  • 查看C代码被编译成的二进制代码及其汇编形式的代码
  • ……

下图是用Keil μVision MDK-ARM、硬件调试器ST-Link硬件调试STM32F407ZG芯片的界面,上半部分是反汇编(Disassembly),下半部分是源代码

  • 看上图中更靠上的青色、绿色、红色方框:
    • 地址0x0800 7318和0x0800 7319处的2个字节0xB530被内核当作一条指令,这条二进制指令对应的助记符是PUSH (r4-r5,lr)
  • 再看图中的紫色方框:
    • 图中下半部分对应一个C源文件,其第135行的内容出现在了紫色方框中,
  • 再看上图中更靠下的青色、绿色、红色方框:
    • 刚才说的C代码对应的二进制指令被存储在地址0x0800 7310~0x0800 7320+(4-1)即0x0800 7323

Keil μVision MDK-ARM的调试器还有很多很方便的工具,比如能实时查看某个变量的值的Watch、能查看正在执行的指令是怎么被一路调用来的调用栈Call Stack等等

IDE把用命令行操作工具链、调试器软件等等的操作封装为用鼠标点击按钮,把输出到命令行界面的字符串封装为直观、美观、易用的功能窗口
显然如果我了解了IDE到底为我自动处理好了什么操作、IDE能为我提供哪些操作,那么我完全没必要非得用命令行界面去操作工具链、调试器软件,IDE的操作简单、直观极了
是不是又像"加个中间层,问题就解决了"?
纯字符的命令行,比简洁、直观、美观,怎么可能比得过图形界面的IDE呢

IDE也有不如用命令行操作工具链的地方,比如对于打字飞快、思路飞快、能非常熟练使用命令行操作工具链的人来说,用命令行操作工具链确实快得多
有的工程项目没有与之对应的图形界面IDE,这时也不得不使用命令行界面来操作工具链
还有命令行界面总是绝对优于图形界面的一点:运行起来占用的硬件资源少多了

一点建议

包括在学习Linux操作系统的时候,很多人无脑吹命令行界面多么方便,就像被洗脑了的传销组织人员一样
其实在电脑配置足够的情况下,真正值得鼓吹的应该是自动化操作易用性,简而言之就是提升生产力而非撞壁、引战、抬杠
建议大家不要盲目跟风无脑站队吹捧或贬低各有所长的优秀工具,比如不要无脑鼓吹
"命令行就是比图形界面高端、好用,图形界面就是个***"
"Android就是比iPhone好用,iPhone就是个***"
"谷歌就是比微软伟大,微软就是个***"
这样的行为就是破坏技术氛围的小丑行为

现在你的单片机已经很完善了,成功面世
但是有客户在用你的单片机设计产品时遇到了一些问题:

  • 单片机做某些事情非常慢
  • 你的单片机的寄存器太多,如果要用到很多片内外设,那么在初始化外设时得写非常多的寄存器,容易写错
  • 单片机同时处理多件事时不够流畅

接下来的3章分别解决上述3个问题:

18.软件实现、硬件实现、通用计算、ASIC、超频

软件实现、硬件实现

那些让单片机工作得很慢的代码可能频繁用到某些操作,CPU可能得执行很多条指令才能完成这样的1次操作,比如没有内置乘法器电路的单片机算整数的乘法得用连加
连加就是对乘法的软件实现,用乘法器电路算乘法就是硬件实现

再比如某个板载外设需要以某种规定与单片机通信,编程人员可以分析那个板载外设的通信规则之后,编写程序让单片机的某些引脚在适当的时刻产生适当的电平来与板载外设进行通信,编程人员得非常仔细地分析这种通信规则,一个细节都不能出错
如果这种通信规则非常常用,那么单片机厂商可以用电路来实现这种通信规则。把这个电路做成一个片内外设,单片机内核给这个片内外设的控制寄存器写入控制字,之后内核只需把要发送的内容传给这个外设,这个外设就能在不需要内核干预的情况下正确地将信息发送出去
事实上有很多通信规则已经被硬件化,比如SPI、SAI、I2C、I2S、USART、USB、HDMI、PCIe、SATA等等

还比如在"外设、典型外设"章讲过的极难被破解的硬件随机数生成器能极大地提升加密安全性

用硬件实现的优势有:

  • 可以执行得更快,例如对一块具体的单片机/SoC/处理器,硬件SPI可以达到的最大通信速度远超软件模拟的
  • 可以减轻CPU负担。毕竟硬件实现在执行时只要有时钟脉冲就能执行,不需要CPU细到“来个时钟脉冲就应该怎么做”的粒度的指挥,而软件实现实质上是CPU一步步地从能XIP的存储器里读出指令,这些指令告诉CPU该如何做每一步

用软件实现的优势当然是金钱成本更低
但是硬件实现并非总是优于软件实现,例如机械按键的延时消抖,软件实现更简单、成本更低,学过单片机编程的入门例程的同学应该有感触,没学过的话以后就去学吧

调试也能不用到硬件调试器,而纯软件实现,这样的调试叫模拟器调试

 通用计算、ASIC、异构计算

如果你对消费电子产品有点研究,而且还必须要玩高画质、高帧率的游戏,那么你买打游戏用的电脑时肯定会选带独立显卡的

 

电脑处理器里负责计算的部件是CPU,独立显卡里负责计算的是GPU(Graphics Processing Unit)
处理能被分解成前后有关联、需要被依次执行的代码,CPU很擅长
处理能被分解为大量没有关系的小组、且不同小组被处理的先后次序无关紧要的数值计算,GPU更擅长,因为它可以用非常多的核心同时计算

其实计算显示画面的过程并没有很多复杂的逻辑运算(就是判断对错、如果怎样就做什么等等,相当于有跳转),然而简单粗暴的数值运算却非常多,而且这些数值运算可以被合理地分为很多个没关系的小组
对于这种大量的可以并行的计算,让CPU干这种没什么技术含量的累活真是太浪费人才了,而且CPU还不一定很擅长

某有4个核心的CPU就像是4个博士毕业生,某有640个(不要被吓到,还有有几千个核心的GPU)核心的GPU就像640个小学生

  • 如果他们比赛做640道10以内的加减法,那么640个小学生确实可以秒杀4个博士毕业生;体现出并行计算的巨大优势
  • 如果他们比做博士毕业生的专业课试题,那么1个博士毕业生就能秒杀640个小学生;这里并行计算并没有优势
 

电脑处理器的定位是做通用计算,通过支持复杂的指令集,让编程人员编写复杂的程序能实现复杂的功能
独立显卡的定位是计算显示画面(其实不限于计算显示画面)的专用集成电路(Application Specific Integrated Circuit,简称ASIC),通过硬件电路实现了可以非常高效地实现某些功能,而在其它功能的实现上并不一定强于CPU
还有其它各具特色的ASIC,比如路由器里天生适合飞速转发的硬件转发加速器、手机SoC里天生适合实现人工智能算法的NPU、刚才举例过的天生适合飞速通信的硬件通信接口等等
面临的任务适合用什么方式来处理,那就让擅长什么方式的硬件来处理,这就是异构计算(Heterogeneous Computing

你如果知道Linux之父Linus是个脾气暴躁、喜欢直言不讳地指出别人的错误的人,那么可能听说过2020年7月时英特尔准备把AVX 512指令集加到它家的CPU新品中,而Linus直呼希望AVX 512 “dies a painful death”
因为英特尔直到2020年7月都还不擅长设计GPU,但是AMD是GPU市场的两大玩家之一,此外2020年AMD的锐龙新品无论是从性能还是性价比都完胜英特尔的酷睿新品,看上去英特尔急了、于是准备搞AVX 512
AVX 512是个能提升并行计算性能的指令集,但是这个指令集再怎么强也强不过GPU轻轻松松就能达到的性能;AVX 512指令集可以处理的事,让GPU来处理可以获得好得多的效果
于是Linus批评英特尔不务正业,不想办法提升CPU产品的性能,反倒为了追求CPU跑分搞出个没什么用的的指令集,还不如把在芯片里实现AVX 512指令集所需的面积拿来提升通用计算性能

超频

刚才的例子也能说明有的工作通过并行执行能极大地提升效率,例如如果1个人1分钟可以吃1个馒头,那么10个人可以1分钟吃10个馒头
但有的工作却不能通过并行计算获得较大的效率提升,例如1个人1分钟可以走100米,但10个人1分钟并不能走1000米
当并行计算并不能带来更大的效率提升时,提升总体的计算速度最常见的方法之一是超频。处理器可以超频,独立显卡也能,几乎所有时序逻辑电路都能超频
其实比超频更有效的方法是优化程序代码、换用内核微架构更强的计算芯片、如果能高效地硬件实现/异构计算就不要软件实现/仅通用计算,只不过绝大多数情况下还是超频更简单也更危险
我曾看过国外小伙将电脑处理器超到8GHz的视频,用到了温度低于-100℃的液氮来冷却处理器。现在的电脑处理器/手机SoC默认开启了温度控制器,当温度过高时会主动降频。所以大热天时我用电脑打游戏会明显掉帧,但只要开空调就可以畅快游玩

时序逻辑电路是被时钟信号指挥的,如果时钟信号跳变得更快,那么时序逻辑电路就会运行得更快

CPU收到的时钟信号的频率叫CPU的主频,把主频提升到高于厂家建议的最高主频即可超频,但是不能无限制提超频,超频会让CPU运行得更不稳定甚至烧毁CPU
根据前面讲过的"时钟树"的知识,CPU的主频应该等于选用的时钟源(比如片外的振荡电路输出的脉冲)的频率乘一路上的倍频值、除一路上的分频值
现在的电脑CPU已经很智能了,比如英特尔的酷睿系列处理器能睿频,即任务繁重时提升主频来提升性能,任务少时降低主频来降低功耗
对功耗敏感的手机SoC更是如此。

以我的手机为例,某个时刻,CPU的8个核心的瞬时主频如下图,还可以设定CPU各核心的主频允许范围


还有自开机后,CPU所有核心的不同平均主频所占的时间长度。可以看到,CPU离线(offline)占的时间最长,我设定的最低频率480MHz次之。离线是比480MHz更节能的状态,和在"中断"章讲过的休眠有得一拼

上面用于监测我的手机CPU的软件是3C All-in-One Toolbox。这个软件的安装方法不同于"拿到1个APK就能安装",而是将主包、配置包、屏幕像素密度适配包分离为3个APK(网盘备份。这个版本适用于ARMv8A、屏幕像素密度为XHDPI。解压后得到分开的3个APK),需要用类似于Spilt APKs Installer(网盘备份,Google Play)的软件来安装

如果你的手机能正常使用Google Play,那么可以走Google Play一键安装3C All-in-One Toolbox

19.API、BSP、MSP、硬件无关代码

API

通过写单片机众多的寄存器来初始化众多外设确实容易出错还费时费力,可以把配置寄存器这种繁琐的工作用C语言函数来实现
例如ST公司为STM32系列单片机编写了丰富的库函数,用户可以用C语言调用库函数来配置STM32的外设寄存器
库函数的优势就是优秀的C语言代码的优势:可以用英语把代码读出来,听你这样读代码的人(如果Ta了解STM32的功能、听得懂英语)能轻易听懂你想如何设置STM32的核内外设和片内核外外设
劣势就是执行效率更低,你试试把用库函数初始化单片机的C代码反汇编就知道了:如果写寄存器只需要几行汇编指令,那么用库函数,得先用好多好多个RAM存储单元来暂存外设的某些功能的标志位,最终再把这些标志位做运算后赋值给寄存器
STM32的性能足够,所以牺牲这一丁点运行效率来极大地提升开发、维护效率是值得的

例如,写寄存器配置片内核外外设定时器/计数器1以计数器模式3工作、循环计时、每个计时周期为1000ms,需要查芯片的手册关于寄存器的说明,然后使用汇编语言或C语言对需要操作的寄存器依次赋值
有了库函数之后,可能只用这么写

TimerCounter1_Config(TIMER, 3, PERIODIC, 1000);

显然可维护性高多了

可以说库函数开发者和调用库函数的人约定了一个接口(interface),向这个接口里按顺序传入一系列值,就能实现某个具体功能
这个"接口"就是应用编程接口(Application Programing Interface,简称API)

这相当于制定了一种规范:应用程序开发者想轻松地使用芯片,那就调用非常熟悉芯片的库函数开发者的API
如果应用程序开发者不信邪、非要强行操作寄存器,可以,但是如果调教不好寄存器,那就自讨苦吃吧
但是:实际硬件调试时,最能定位出问题的根源的方法依然是看寄存器
应用程序开发者如果要把应用程序移植到其它芯片,那就调用新芯片的API
同理,无论芯片厂商怎么优化芯片的内部设计、优化库函数对接口的实现,只要新版芯片或新版API中的同名API的用法不变,那么以前的程序依然可以不做修改、编译通过、正确运行
再次体现"加个中间层,问题就解决了"的思想

形象点说:你在一个新的城市旅游,但不知道路线怎么走(相当于通过直接写寄存器来配置单片机的功能很繁琐)
于是呼叫出租车带你去你要去的地方(相当于找来单片机的API)
只需告诉司机"我要去XXXX"即可(相当于调用API)
如果
    道路施工(相当于芯片内部电路或寄存器功能变化了)
    接你的是另一辆出租车(相当于API的具体实现更新了)
只要
    出租车司机知道如何去你的目的地(新的API能实现与旧的同名API完全相同功能)
    “带我去XXX”这句话依然是叫司机送你去你的目的地的意思(新的API的调用方式与旧的同名API的完全相同)
那么你总能到你的目的地
如果你不信邪、非要自己照着手机地图找你的目的地,但手机地图里的地图信息是好多年以前的、很多道路早已改道,那么按照旧的地图走,可能掉进河里(就像寄存器的功能变化了之后、以前的通过写寄存器来配置单片机的代码可能会无法正常运行)

Java的功能库、Python的功能库也为开发人员提供了很多API,还有Android操作系统为应用程序开发者开放的调用操作系统功能的函数也可以看作是API
如果某Android应用程序基于旧版本Android操作系统开发、所有功能最终都通过调用操作系统开放的API来实现,如果新版本的Android操作系统依然支持让那个应用程序用到的所有API协调工作,那么那个应用程序就可以不做修改就能在新版本Android操作系统上完美运行
国内很多互联网大厂过于流氓,为了突破Android操作系统对应用程序的行为的限制,而不全程只调用Android操作系统为应用程序开发者开放的API,反而自行编写一些具备具体版本的操作系统特征、突破操作系统限制的功能,相当于强行调教一款具体型号的单片机的寄存器
新版本Android操作系统对某些功能的具体实现可能变化,就像新款单片机的寄存器的名字或功能变化了,所以那些互联网大厂自行编写的那些代码就会与新版本Android操作系统冲突,于是它们的软件闪退
这就是导致国内很多人会有"新版本操作系统极其不稳定"的片面印象的原因之一

 

Oasis Feng大佬开发了能限制Android手机上不安分的软件的软件绿色守护(Greenify),他曾发现国内一些互联网公司提供的SDK(Software Development Kil,能以二进制串而非源代码的形式给其他开发者提供API;由于是以二进制串的形式提供,所以其他开发者看不到也改不了被提供的API的具体实现,从而SDK开发者可能为所欲为)紧跟着AOSP(Android Open Source Project)的更新而更新出能对抗Android操作系统的功能,于是他向AOSP提出issue(人们向开放源代码的项目提出项目存在的问题叫issue),下图是他为获得网友们的支持而发布的动态
你如果点进这个动态,可能会认为Oasis Feng大佬会被沙雕网友们气死,因为Feng大佬强调了不要在他的issue下刷屏,但不少沙雕网友们依然刷屏
所以大家一定要养成认真阅读对你有帮助的说明性图文的习惯

MSP、BSP、硬件无关代码

对于一款具体型号的单片机,你为它编写驱动它的各个片内外设(包括核内的、核外片内的)的API之后,有个人可以基于你写的API来编写驱动各个片外外设(包括片外板载的、通过电缆连接到电路板上的)的API、而不用太关注你的API是如何实现的
同样,另一个人也可以基于有个人和你写的API来编写使用核内、核外片内、片外板载、板外外设来实现具体功能的代码

你写的代码与具体型号的单片机相关(MCU-specific),这样的代码叫MCU(前面说过,MCU是微处理器,微处理器又可以叫单片机)相关代码,同时你的MCU相关代码又为有个人提供调用片内外设的API,所以可以说你写的代码的集合是支持那款MCU的包(MCU Support Package,简称MSP,MCU支持包,和MSP430系列单片机名字里的"MSP"的意思不同)
同理,有个人写的代码是电路板相关代码、这些代码的集合可以叫BSP(Board Support Package,板级支持包)
同理,另一个人写的代码通过调用MSP、BSP的API来实现具体功能,而不直接操作具体外设的寄存器,这样的代码叫硬件无关代码;同理,MSP和BSP都叫硬件相关代码

例如,对于能玩贪吃蛇的游戏机,假设它的屏幕通过游戏机的主控芯片的LTDC接口来获得要显示的内容、游戏机的按键通过游戏机主控芯片的GPIO来向主控芯片传送按键状态
那么告诉主控芯片的内核如何设定LTDC的输出色彩制式、分辨率、帧率的代码属于MSP
告诉主控芯片的哪些IO口接到哪个电平意味着哪些按键被按下的代码属于BSP
定义游戏逻辑(咬到自己算游戏失败、吃到食物就延长身体等等)的代码属于硬件无关代码

MSP、BSP、硬件无关代码的层层调用,使得移植更加方便
再次体现出"加个中间层,问题就解决了"的思想

20.操作系统——存储管理、并发……

刚才说过,没有操作系统的时候,用汇编语言写代码需要打草稿划分存储空间,用C语言写直接运行在单片机内核上的代码虽不用打刚才的草稿,但连接器需要打这个草稿,这个草稿的存在使得最终得到的二进制代码不一定能正常运行在存储器地址分布与你的单片机的不同的单片机上
刚才还说过,处理器内核的工作就是不断地顺序地取指令、取数据、执行,还能响应中断,如果有多件事需要内核同时做,那么内核只得不断地交替做这些事情,只要交替得够快,人类就以为内核在同时做多件事(这种调度不同任务的方法叫时间片轮转),但是这样可能有些很闲的任务浪费内核的大量时间,而有些很忙的任务无法被内核及时响应。编程人员需要设计复杂的方法来决定在不同情况下各个任务应该获得多长的时间片、一个任务的时间片结束后应该切换到哪个任务
(前面也说过,实际上内核在同一时刻能做多少件事由核心数决定,如果只有1个核心,那么在某个具体的时刻,内核确实只能做1件事。多核心说的就是你买电脑的时候商家告诉你的双核、四核、八核等等)

操作系统就能很好地解决上面的2个问题

没有操作系统的单片机、电脑叫裸机
优秀的操作系统能充分利用机器性能,包括但不限于

  • 动态地合理管理存储器
  • 合理地分配处理器内核资源到不同任务、不同外设
  • 帮用户处理好稍有不慎就会导致致命错误的操作

对操作系统的详细描述已超出本文讨论的范围,你知道操作系统的优势就行

 

21.无处不在的计算机

前面把计算机拆分成处理器/Soc/微控制器+能XIP的存储器+外设,把处理器/SoC/微控制器又拆分成(CPU+总线+核内外设)+核外片内外设,最后来看看计算机的全貌吧

笔记本电脑/台式电脑,显然是计算机,它的板载外设可能有声卡、网卡、炫酷的LED彩灯、异构计算设备独立显卡等等,一些板外外设比如显示屏、打印机、鼠标等通过视频信号接口、USB接口等外设来与处理器通信
手机,不论是智能手机还是非智能手机,当然也是计算机。智能手机相当于小电脑
电视机,其实也是计算机。剔除手机的打电话相关的外设,加入能解码机顶盒/影碟机传来的媒体信号的外设,加入能解码红外遥控器发来的命令的外设,再换个大屏幕,就是电视机
无线路由器,还是计算机。剔除手机的屏幕、扬声器、震动马达等,加入网线接口、无线收发器等,就是路由器

非智能手机、非智能电视可能不需要操作系统,以非智能电视机为例,它的主控连接着显示屏控制电路、按键、红外接收器控制电路、扬声器控制电路、能解码机顶盒/影碟机输出的音视频信号的外设等等,非智能电视机的主控可能死循环做着这些事:

  • 电视机面板的按键被按下会触发硬件中断,中断服务函数是根据被按下的按键来决定应该执行换台、调节音量、切换信号源等当中的哪个函数
  • 红外接收器收到信号也会触发硬件中断,解码这个信号的任务可能是硬件实现的,也可能是软件实现的,中断服务函数是根据接收到的信号的解码值来决定应该执行换台、调节音量、切换信号源等当中的哪个函数
  • 如果信号源是机顶盒/影碟机,那就死循环将收到的信号转为显示屏控制电路能直接呈现在显示屏上的信号,这个转换过程可能是硬件实现的也可能是软件实现的。假设显示屏控制电路通过主控的片内外设SPI通信接口来与电视机的主控通信(也就是假设显示屏控制电路接收的信号是数字信号而非模拟信号),那么再用DMA死循环将转换好的数字信号送给SPI通信接口的数据发送寄存器
  • ……

电子表,功能非常简单、成本非常低的可能不包含计算机,控制电路是简单的逻辑电路。功能复杂些的,比如能打电话的儿童电话手表,相当于个屏幕非常小的智能手机,那么肯定是计算机
电热水壶,可能包含计算机。里面的计算机可能是单片机,有个温度传感器能实时将水温转为电压值,单片机的片内外设模数转换器(ADC)测出温度传感器的电压值。单片机内的Flash上的程序可能是死循环做这件事:如果ADC的转换结果寄存器的数值高于/低于多少,那就断开加热器的电源,否则啥也不干

计算器,包含计算机。里面的计算机也可能是单片机,单片机里的Flash的程序可能是死循环:

  • 按键被按下会触发硬件中断,中断服务函数是根据被按下的按键执行补充要计算的表达式、完成对表达式的计算、清除已接收的表达式等函数,将组成目前的表达式的各个运算符、各个数及其各个位的数字存在运行内存里
  • 给显示屏控制电路发送适当的信号,以将组成目前的表达式的各个运算符、各个数及其各位数字按正确的次序显示在屏幕上

那些没有学过计算机原理的人们看不到的计算机相当于所谓的嵌入式(Embedded)计算机。其实至今没有对嵌入式计算机的严格定义

不用对手机/电视机/路由器/机顶盒甚至智能电冰箱/智能空调/智能电饭煲可以接根线到电脑然后被电脑控制感到惊讶,无非就是那些设备的主控可以通过设备的通信外设来对接电脑的通信外设、那些主控执行着类似于“如果收到什么什么就执行什么什么”的程序嘛
也不用对手机/电视机/路由器/机顶盒可以被更换操作系统、安装新的软件、添加看上去毫不相关的外设比如电控车轮感到惊讶,无非就是更换它们的Flash里的程序、将新外设接到已有的可以控制新外设的外设上嘛


21.完结撒花

重点总结

  • 个人认为:计算机就是名为CPU的时序逻辑电路通过不断地执行能XIP的存储器上的二进制串,以读写寄存器、用总线控制等方式读取和控制其它电路的运行状态,还能以硬件中断等形式响应接收到的信号,这一切所组成的电子系统
  • 晶体管能构成逻辑门,进而构成功能复杂的组合逻辑电路和时序逻辑电路
  • 处理器内核能从可以XIP的存储器的某个地址开始不断地取指令、取数据、执行
  • 指令集规定某个二进制串让处理器内核实现什么功能
  • 不同架构的内核能实现相同的指令集
  • 内核内部、内核外部芯片内部、芯片外部都可以有外设
  • 单片机/SoC/脱去散热壳和转接电路板的处理器是芯片,开发板是电路板;芯片不是电路板
  • 内核的核心数决定并行执行数
  • 内核可以通过总线访问、控制存储器、外设等等
  • 寄存器能反映、控制电路的运行状态
  • 外设可以通过硬件中断来打断内核的工作、让内核去执行其它工作、做完其它工作后返回执行刚才被中断的工作。硬件中断提升了对内核性能的利用率
  • 汇编语言、C语言和其它高级语言极大地方便了开发程序
  • 调试功能能帮助你检查程序存在的逻辑错误
  • IDE极大地地简化了使用工具链
  • 为了提升你的代码的可维护性、可移植性,应该尽量不使用工具链的隐含特性
  • 功能可以被硬件实现或软件实现,各有优劣
  • 通用计算和专用计算各有优劣,异构计算能极大地提升计算系统总体的性能
  • 计算机科学的哲理之一:加个中间层,问题就解决了
  • API极大地方便了应用程序开发者开发、维护、移植程序,但硬件调试时最能定位出问题的根源的方法依然是对比寄存器值
  • 抵制无脑跟风地贬低或过誉各有所长的工具的小丑行为
  • 这些词有歧义:CPU、串口、TTL、内存空间、ROM、编程
  • 具体某款芯片的字长、存储器地址线数据线位数等等并不一定都与本文中我让你用的方案一致,本文中的方案只是示例,以你实际使用的芯片的手册为准

更新历史

版本 日期 改动 鸣谢
1 2020年2月28日 完成初稿  
2 2020年2月29日 添加硬件中断举例 Z1同学
3 2020年3月26日
  1. 将对NAND Flash的描述"不能读写任一存储单元"改为为"不能只读写1个存储单元"
  2. 修正"1间事"为"1件事"
L1同学
 4 2020年5月30日
  1. 修正全文格式,启用换行但不换段的格式
  2. 加入板级设计
  3. 加入对"CPU"消除歧义、几家芯片厂的特色、业界笑话、字长、多核心、死机、大小端、RISC和CISC、开源指令集RISC-V、对最常见的几种内核的举例
  4. 加入最常见的一些外设、时钟树
  5. 加入冯·诺依曼结构、哈佛结构和这些结构的优缺点
  6. 将"内存空间"从"寄存器"章移至"总线、寻址"章
  7. 加入引入C语言的原因、编译型语言和解释型语言、低级语言和高级语言、运行时环境和虚拟机
 
5 2020年6月3日

加入举例我的手机的CPU自动主频

 
6 2020年6月4日
  1. 加入芯片和电路板示意图
  2. 加入工具链
  3. 加入API的
 
7 2020年6月6日
  1. 加入必须使用汇编语言的场景
  2. 加入BSP、MSP、硬件无关代码
 
8 2020年7月21日

强调硬件中断的功能具体重要在什么地方

 
9 2020年7月24日
  1. 添加异构计算,更改对独立显卡的介绍,添加Linus批评英特尔推进对通用计算没什么用的AVX 512指令集的内容
  2. 添加Oasis Feng指出国内一些互联网公司的SDK紧跟AOSP推出对抗Android操作系统的功能的内容
 
10 2020年8月26日
  1. 修正对微码的描述
  2. 在文末添加对生活中常见的计算机的重新认识