我说我是周末从山里回来 公交路上写的 大伙信吗?上图
好啦 来正文哦
一、CPU怎么识别代码
从底层硬件角度出发剖析了一下CPU对代码的识别和读取,内容之精彩,读完感觉学到的很多东西瞬间联系起来了,分享给大家。
先说一下半导体,啥叫半导体?就是介于导体和绝缘体中间的一种东西,比如二极管。
电流可以从A端流向C端,但反过来则不行。你可以把它理解成一种防止电流逆流的东西。
当C端10V,A端0V,二极管可以视为断开。
当C端0V,A端10V,二极管可以视为导线,结果就是A端的电流源源不断的流向C端,导致最后的结果就是A端=C端=10V。
等等,不是说好的C端0V,A端10V么?咋就变成结果是A端=C端=10V了?你可以把这个理解成初始状态,当最后稳定下来之后就会变成A端=C端=10V。
文科的童鞋们对不住了,实在不懂问高中物理老师吧。反正你不能理解的话就记住这种情况下它相当于导线就行了。
利用半导体的这个特性,我们可以制作一些有趣的电路,比如【与门】。
此时A端B端只要有一个是0V,那Y端就会和0V地方直接导通,导致Y端也变成0V。只有AB两端都是10V,Y和AB之间才没有电流流动,Y端也才是10V。
我们把这个装置成为【与门】,把有电压的地方计为1,0电压的地方计为0。至于具体几V电压,那不重要。也就是AB必须同时输入1,输出端Y才是1;AB有一个是0,输出端Y就是0。
其他还有【或门】【非门】和【异或门】,跟这个都差不多,或门就是输入有一个是1输出就是1,输入00则输入0。
非门也好理解,就是输入1输出0,输入0输出1。
异或门难理解一些,不过也就那么回事,输入01或者10则输出1,输入00或者11则输出0。(即输入两个一样的值则输出0,输入两个不一样的值则输出1)。
这几种门都可以用二极管或者三极管做出来,具体怎么做就不演示了,有兴趣的童鞋可以自己试试。当然实际并不是用二极管三极管做的,因为它们太费电了。实际是用场效应管(也叫MOS管)做的。
然后我们就可以用门电路来做CPU了。当然做CPU还是挺难的,我们先从简单的开始:加法器。加法器顾名思义,就是一种用来算加法的电路,最简单的就是下面这种。
AB只能输入0或者1,也就是这个加法器能算0+0,1+0或者1+1。
输出端S是结果,而C则代表是不是发生进位了,二进制1+1=10嘛。这个时候C=1,S=0。
费了大半天的力气,算个1+1是不是特别有成就感?
那再进一步算个1+2吧(二进制01+10),然后我们就发现了一个新的问题:第二位需要处理第一位有可能进位的问题,所以我们还得设计一个全加法器。
每次都这么画实在太麻烦了,我们简化一下。
也就是有3个输入2个输出,分别输入要相加的两个数和上一位的进位,然后输入结果和是否进位。然后我们把这个全加法器串起来:
我们就有了一个4位加法器,可以计算4位数的加法也就是15+15,已经达到了幼儿园中班水平,是不是特别给力?
做完加法器我们再做个乘法器吧,当然乘任意10进制数是有点麻烦的,我们先做个乘2的吧。
乘2就很简单了,对于一个2进制数数我们在后面加个0就算是乘2了。比如:
5=101(2)
10=1010(2)
以我们只要把输入都往前移动一位,再在最低位上补个零就算是乘2了。具体逻辑电路图我就不画,你们知道咋回事就行了。
那乘3呢?简单,先位移一次(乘2)再加一次。乘5呢?先位移两次(乘4)再加一次。
所以一般简单的CPU是没有乘法的,而乘法则是通过位移和加算的组合来通过软件来实现的。这说的有点远了,我们还是继续做CPU吧。
现在假设你有8位加法器了,也有一个位移1位的模块了。串起来你就能算(A+B)×2了!激动人心,已经差不多到了准小学生水平。
那我要是想算A×2+B呢?简单,你把加法器模块和位移模块的接线改一下就行了,改成输入A先过位移模块,再进加法器就可以了。
你的意思是我改个程序还得重新接线?
所以你以为呢?
实际上,编程就是把线来回插啊。惊喜不惊喜?意外不意外?
早期的计算机就是这样编程的,几分钟就算完了但插线好几天。而且插线是个细致且需要耐心的工作,所以那个时候的程序员都是清一色的漂亮女孩子,穿制服的那种,就像照片上这样。是不是有种生不逢时的感觉?
插线也是个累死人的工作。所以我们需要改进一下,让CPU可以根据指令来相加或者乘2。这里再引入两个模块,一个叫flip-flop,简称FF,中文好像叫触发器,如下图这样。
这个模块的作用是存储1bit数据。比如上面这个RS型的FF,R是Reset,输入1则清零。S是Set,输入1则保存1。RS都输入0的时候,会一直输出刚才保存的内容。
我们用FF来保存计算的中间数据(也可以是中间状态或者别的什么),1bit肯定是不够的,不过我们可以并联嘛,用4个或者8个来保存4位或者8位数据。这种我们称之为寄存器(Register)。另外一个叫MUX,中文叫选择器,如下图就是一个选择器。
这个就简单了,sel输入0则输出i0的数据,i0是什么就输出什么,01皆可。同理sel如果输入1则输出i1的数据。当然选择器可以做的很长,比如这种四进一出的具体原理不细说了,其实看看逻辑图琢磨一下就懂了,知道有这个东西就行了。下图是一个四进一出-选择器。
有这个东西我们就可以给加法器和乘2模块(位移)设计一个激活针脚。
这个激活针脚输入1则激活这个模块,输入0则不激活。这样我们就可以控制数据是流入加法器还是位移模块了。
于是我们给CPU先设计8个输入针脚,4位指令,4位数据。
我们再设计3个指令:
- 0100,数据读入寄存器
- 0001,数据与寄存器相加,结果保存到寄存器
- 0010,寄存器数据向左位移一位(乘2)
为什么这么设计呢,刚才也说了,我们可以为每个模块设计一个激活针脚。然后我们可以分别用指令输入的第二第三第四个针脚连接寄存器,加法器和位移器的激活针脚。
这样我们输入0100这个指令的时候,寄存器输入被激活,其他模块都是0没有激活,数据就存入寄存器了。同理,如果我们输入0001这个指令,则加法器开始工作,我们就可以执行相加这个操作了。
这里就可以简单回答这个问题的第一个小问题了:CPU是为什么能看懂这些二级制的数呢?
为什么CPU能看懂,因为CPU里面的线就是这么接的呗。你输入一个二进制数,就像开关一样激活CPU里面若干个指定的模块以及改变这些模块的连同方式,最终得出结果。
几个可能会被问的问题
Q:CPU里面可能有成千上万个小模块,一个32位/64位的指令能控制那么多吗?
A:我们举例子的CPU里面只有3个模块,就直接接了。真正的CPU里会有一个解码器(decoder),把指令翻译成需要的形式。
Q:你举例子的简单CPU,如果我输入指令0011会怎么样?
A:当然是同时激活了加法器和位移器从而产生不可预料的后果,简单的说因为你使用了没有设计的指令,所以后果自负呗。在真正的CPU上这么干大概率就是崩溃呗,不过肯定会有各种保护性的设计。
细心的小伙伴可能发现一个问题:你设计的指令【0001,数据与寄存器相加,结果保存到寄存器】这个一步做不出来吧?
毕竟还有一个回写的过程,实际上确实是这样。我们设计的简易CPU执行一个指令差不多得三步,读取指令,执行指令,写寄存器。
经典的RISC设计则是分5步:读取指令(IF),解码指令(ID),执行指令(EX),内存操作(MEM),写寄存器(WB)。我们平常用的x86的CPU有的指令可能要分将近20个步骤。
你可以理解有这么一个开关,我们啪的按一下,CPU就走一步,你按的越快CPU就走的越快。咦?听说你有个想法?少年,你这个想法很危险啊,姑且不说你能不能按那么快。拿现代的CPU来说,也就2GHz多吧,大概一秒也就按个20亿下吧。
就算你能按那么快,虽然速度是上去了,但功耗会大大增加,发热上升稳定性下降。江湖上确实有这种玩法,名曰超频,不过新手不推荐你尝试哈。
那CPU怎么知道自己走到哪一步了呢?前面不是介绍了FF么,这个不光可以用来存中间数据,也可以用来存中间状态,也就是走到哪了。
具体的设计涉及到FSM(finite-state machine),也就是有限状态机理论,以及怎么用FF实装。这个也是很重要的一块,考试必考哈,只不过跟题目关系不大,这里就不展开讲了。
我们再继续刚才的讲,现在我们有3个指令了。我们来试试算个(1+4)X2+3吧。
0100 0001 ;寄存器存入1
0001 0100 ;寄存器的数字加4
0010 0000 ;乘2
0001 0011 ;再加三
太棒了,靠这台计算机我们应该可以打败所有的幼儿园小朋友,称霸大班了。而且现在我们用的是4位的,如果换成8位的CPU完全可以吊打低年级小学生了!
实际上用程序控制CPU是个挺高级的想法,再此之前计算机(器)的CPU都是单独设计的。
1969年一家日本公司BUSICOM想搞程控的计算器,而负责设计CPU的美国公司也觉得每次都重新设计CPU是个挺傻X的事,于是双方一拍即合,于1970年推出一种划时代的产品,世界上第一款微处理器4004。
这个架构改变了世界,那家负责设计CPU的美国公司也一步一步成为了业界巨头。哦对了,它叫Intel,对,就是噔噔噔噔的那个。
我们把刚才的程序整理一下:
"01000001000101000010000000010011"
你来把它输入CPU,我去准备一下去幼儿园大班踢馆的工作。
什么!?等我们输完了人家小朋友掰手指都能算出来了?
没办法机器语言就是这么反人类。哦,忘记说了,这种只有01组成的语言被称之为机器语言(机器码),是CPU唯一可以理解的语言。不过你把机器语言让人读,绝对一秒变典韦,这谁也受不了。
所以我们还是改进一下吧。不过话虽这么讲,也就往前个30年,直接输入01也是个挺普遍的事情。
于是我们把我们机器语言写成的程序:
0100 0001 ;寄存器存入1
0001 0100 ;寄存器的数字加4
0010 0000 ;乘2
0001 0011 ;再加三
改写成:
MOV 1 ;寄存器存入1
ADD 4 ;寄存器的数字加4
SHL 0 ;乘2(介于我们设计的乘法器暂时只能乘2,这个0是占位的)
ADD 3 ;再加三
是不是容易读多了?这就叫汇编语言。
汇编语言的好处在于它和机器语言一一对应。
也就是我们写的汇编可以完美的改写成机器语言,直接指挥cpu,进行底层开发;我们也可以把内存中的数据dump出来,以汇编语言的形式展示出来,方便调试和debug。
汇编语言极大的增强了机器语言的可读性和开发效率,但对于人类来说也依然是太晦涩了,于是我们又发明了高级语言,以近似于人类的语法来表现数据结构和算法。
比如很多语言都可以这么写:
a=(1+4)*2+3;
当然这样计算机是不认识的,我们要把它翻译成计算机认识的形式,这个过程叫编译,用来做这个事的东西叫编译器。
具体怎么把高级语言弄成汇编语言/机器语言的,一本书都写不完,我们就举个简单的例子。
我们把:
(1+4)*2+3
转换成:
1,4,+,2,*,3,+
这种写法叫后缀表示法,也成为逆波兰表示法。相对的,我们平常用的表示法叫中缀表示法,也就是符号方中间,比如1+4。而后缀表示法则写成1,4,+。
转换成这种写法的好处是没有先乘除后加减的影响,也没有括号了,直接算就行了。
具体怎么转换的可以找本讲编译原理的书看看,这里不展开讲了。
转换成这种形式之后我们就可以把它改成成汇编语言了。
从头开始处理,最开始是1,一个数字,那就存入寄存器:
MOV 1
之后是4,+,那就加一下:
ADD 4
然后是2,*,那就乘一下(介于我们设计的乘法器暂时只能乘2,这个0是占位的):
SHL 0
最后是3,+,那再加一下:
ADD 3
最后我们把翻译好的汇编整理一下:
MOV 1
ADD 4
SHL 0
ADD 3
再简单的转换成机器语言,就可以拿到我们设计的简单CPU上运行了。
其实到了这一步,应该把这个问题都讲清楚了:C语言写出来的东西是怎么翻译成二进制的,电脑又是怎么运行这个二进制的。
只不过题主最后还提到栈和硬件的关系,这里就再多说几句。
其实栈是一种数据结构,跟CPU无关。只不过栈这个数据结构实在太常用了,以至于CPU会针对性的进行优化。为了能让我们的CPU也能用栈,我们给它增加几个组件。
第一,增加一组寄存器。现在有两组寄存器了,我们分别成为A和B。
第二,增加两个指令,RDA/RDB和WRA/WRB,分别为把指定内存地址的数据读到寄存器A/B,和把寄存器A/B的内容写到指定地址。
顺便再说下内存,内存有个地址总线,有个数据总线。比如你要把1100这个数字存到0011这个地址,就把1100接到数据总线,0011接到地址总线,都准备好了啪嚓一按开关(对,就是我们前面提到的那个开关),就算是存进去了。
什么叫DDR内存呢,就是你按这个开关的时候存进去一个数字,抬起来之前你把地址和数据都更新一下,然后一松手,啪!又进去一个。也就是正常的内存你按一下进去1个数据,现在你按一下进去俩数据,这就叫双倍速率(Double Data Rate,简称DDR)
加了这几个命令之后我们发现按原来的设计,CPU每个指令针脚控制一个模块的方式的话针脚不够用了。所以我们就需要加一个解码器了(decoder)。
于是我们选择用第二个位作为是否选择寄存器的针脚。如果为0,则第三第四位可以正常激活位移器和加法器;如果为1则只激活寄存器而不激活位移和加法器,然后用第四位来决定是寄存器A还是B。这样变成了:
- 0100,数据读入寄存器A
- 0101,数据读入寄存器B (我们把汇编指令定义为MOVB)
- 0001,数据与寄存器A相加,结果保存到寄存器A
- 0011,数据与寄存器B相加,结果保存到寄存器B(我们把汇编指令定义为ADDB)
- 0010,寄存器A数据向左位移一位(乘2)
最后我们可以用第一位来控制是不是进行内存操作。如果第一位为1则也不激活位移和加法器模块,然后用第三个针脚来控制是读还是写。这样就有了:
- 1100,把寄存器B的地址数据读入寄存器A(我们把汇编指令定义为RD)
- 1110,寄存器A的数据写到寄存器B指定的地址(我们把汇编指令定义为WR)
我们加了个解码器之后,加法器的激活条件从p4变成了(NOT (p1 OR p2)) AND p4。
加法器的输入则由第三个针脚判断,0则为寄存器A,1为寄存器B。这就是简单的指令解码啦。
当然我们也可以选择不向下兼容,另外设计一套指令。不过放到现实世界恐怕就要出大乱子了,所以你也可以想象我们平常用的x86背了个多大的历史包袱。
这个时候我们用栈的话,先栈地址初始化:
0101 1000 ; MOVB 16; 把栈底地址定义为1000
之后入栈的话,比如把数字3,4入栈:
1111 0011 ; WR 03; 把3写到内存,地址为1000
0011 0001 ; ADDB 01; 栈地址+1
1111 0100 ; WR 04; 把3写到内存,地址为1001
0011 0001 ; ADDB 01; 栈地址+1
这样就把3,4都保存到栈里了。
出栈的话反过来:
0011 1111 ; ADDB -1; 栈地址-1
1101 0000 ; RD 00; 把内容读入寄存器A,00是占位
0011 1111 ; ADDB -1; 栈地址-1
1101 0000 ; RD 00; 把内容读入寄存器A,00是占位
这样就依次得到4,3两个值。
所以,入栈出栈其实就是把数据写道指定的内存位置,CPU其实不知道你是在干啥。当然我们也可以让CPU知道。
接下来我们再改进一下,给CPU再加一个寄存器SP,并定义两个指令:一个PUSH,一个POP。动作分别是把数据写入SP的地址,然后SP=SP+1,POP的话反过来。
这样有什么好处呢?好处在于PUSH/POP这样的指令消耗特别少,速度特别快。而栈这种数据结构在各种程序里用的又特别频繁,设计成专用的指令则可以很大程度上提升效率。
当然前提是编译器知道这个指令,并且做了优化,所以同样的程序(c语言写的),编译参数不一样(打开/关闭某些特性),编译出来的东西也就不一样,在不同硬件上的运行的效率也就会不一样。
比如上古时代的mmx,今天的SSE4.2,AVX-512,给力不给力?特别给力,但你平常用的程序支不支持是另一码事,要支持怎么办?重新编译呗。
这个时候开源的优势就显示出来了,重新编译很方便。闭源的话你就要指望作者开恩啦。
对于大多数人来说,电脑就是个黑箱,我们很难理解它到底是怎用工作的。这个问题又很难一句两句解释清楚,因为它是一环扣一环的,每一环都很抽象,每一环都是基础值俩个学分,展开了讲没上限的那种。
这就导致了即使是系统学过计算机的人也不见得就有一个明确而清晰的思路。想用尽量短的篇幅和尽量简单的语言把这个事从头到位解释了一下,希望能给大家解答一些疑惑。关于软硬件结合
二、代码是如何控制硬件的?
先说代码:
我们是用电脑的键盘来输入的指令,每一个指令都对应一个ASCII码,而这里的ASCII码就是有序的电压的高低(或电流的有无,下面只提电压的高低),即我们输入的是电压的高低,你所看到代码是这些电压的高低控制显示器所显示的图像,其实电脑也不知道它是什么,只知道这样显示。
结论:代码其实就是存储在存储器(内存、硬盘或者闪存等等)中有序的电压的高低。
再说编译:
编译是一个有序的电压的高低向另一种有序的电压高低的一种转换过程,下面以52单片机为例,我们编译是从表示ASCII码的那种有序电压高低转换为52单片机能够识别的另一种规定好的有序电压高低,即表示HEX文件的电压高低。
结论:编译出的结果还是电脑中存储的有序电压高低。
程序烧录到单片机:
接下俩就是烧录,理解了上面两点就很容易理解下面的内容,烧录就是电脑中的有序电压高低通过数据线传输到单片机中的ROM中。
接下来ROM就可以释放其中的电压来控制外围的电路。
总结:从代码的编辑到最后对电路的控制都是电压在起作用,只是为了方面我们而给我们展现的形式不一样而已,而其本质都是电压,这样也就不存在转换。
理解这句话:世界上没有软件,软件只是对硬件的一种反映,就像意识是对世界的一种反映是一样的!
相信这样就很容易理解了。
单片机中的0与1:
只要你提到0/1,提到软件,这个问题就没法理解...因为软件【包括0/1】和硬件始终存在一道无法跨越的鸿沟;
你说你在单片机中写0,请问你是如何写0的?在键盘上敲个0?实际还是电平【和我们理解的数字没关系】,那个0只是你在电脑显示器上电平的呈现形式,那个所谓的0【实质是电平】可以传输到单片机中的ROM中,电平控制电平没什么疑问吧,这样就输出低电平了...
翻开数字电路相关教材,最前面几页。
一般它都会告诉你,三极管/场效应管类似继电器(一种通过线圈产生磁场、然后用磁场控制物理开关的通断与否的设备);在它一个管脚上输入/切断电压信号,另一个管脚就会出现高/低电平。
PS:继电器是一种利用电磁铁控制的开关;当向电磁铁通电时就产生磁场,而这个磁场就会吸合或者分离开关,从而实现“以微弱电流控制另一条电路的通断”这个功能。
其中,平常触点接触使得被控制电路导通、给控制它的电磁铁通电后就使得开关断开的那种继电器,就等效于非门。三极管拿来当开关使用时,和这种继电器效果几乎一样。
以上,就是数字电路的基础。
指令
你敲入的任何东西,最终就是通过类似的东西/机制储存的;所谓“指令”,其实就是“某个命令码“(一般叫机器码),这个”命令码”会改变CPU内部一堆“开关”的状态,以激活不同的电路;然后数据(前面提到过,它也是用三极管/场效应管的导通与否“记忆”的)利用类似的机制,被送入这个被“指令”激活的电路——这些电路是工程师们利用最最基础的三极管控制原理,用一大堆三极管组合出来的:当数据(某种高低电平的组合)经过这些电路后,就会变成另外一组高低电平的组合:这个组合刚好和“指令”代表的功能所应该给出的结果一致。
这段话可能有点难以理解。那么,看下最简单的与门吧:数据有两个,分别通过两条不同的线路进入与门;输出只有一个,必须给它输入两个高电平,它才会输出高电平;否则就输出低电平(这一般简化表述为:只有输入两个1,它才输出1,否则输出0)。
——这就是所谓的“与”逻辑;一组这样的“与”逻辑就与计算机指令/高级语言里的“按位与”直接对应。
——而按位与这个指令,意思就是选择一组线路,把数据导通到这组“与”逻辑电路之上;然后这组与逻辑电路就会输出两组数据的按位与的结果。
——类似的,二进制加法,1+1=0(同时进位);1+0=1;0+1=1;0+0=0:这可以用一个异或电路来模拟(因为异或电路的规则就是1+1=0、1+0=1、0+1=1、0+0=0);但这样(同时进位)这个说明就会丢失了,所以需要同时用一个与门模拟高位进位(前面说过,与门就是只有两个1才会输出1,其它输出0;综合异或的说明:这是不是就和二进制加法的规则刚好一致了呢?)
然后更高一位就成了两根输入线上的数据相加、再加上进位数据……依此类推:这就是用开关做加法的思路。
更多位数的数字的加法,只不过是对应位的二进制加法再加上前一位的进位位罢了,没什么特别的——这样堆起来的一组开关,就叫加法器。
——add指令呢,就是选中上面做的那一堆用来做加法的开关们;然后给它们输入数据(不要忘了,两组高低电平而已),这些数据就驱动着构成加法器的那些开关们,噼里啪啦一阵乱响之后(嗯,如果是老掉牙的继电器计算机的话:还记得BUG的故事吗?),电路就稳定在某个状态了:此时,加法器的输出,恰恰就是输入数据的和(当然是这样了。前面讲过,我们是刻意用异或门和与门精心组合,让它们刚好和加法的效果一致)。
——其它种种指令,莫不大同小异(更复杂/高级的时钟、流水线啥的……暂时就无视吧)
你可以翻翻课本,讲过加法器的实现。
而加法器和另外一些逻辑电路加起来,就是所谓的ALU(算术逻辑单元,一下子就高大上了有木有)。(当然了,实际上没这么简单。比如至少还要加上时钟信号来打拍子协调开关们的动作、加上锁存器来暂存数据之类——前面提到过,给加法器输入数据,构成加法器的一堆开关需要噼里啪啦一阵才能进入稳定态,然后就可以读出答案:时钟信号就是用来协调这些开关,保证它们都能得到足以达到稳定态的时间用的)
简而言之,代码在计算机内部,本身就是一组特定的高低电平组合;而计算机是精心设计的、海量的、用高低电平控制通断的开关组;当给这个开关组输入不同的电平组合时,就会导致它内部出现复杂的开关动作,最终产生另外一组高低电平的组合作为输出;这些开关动作经过精心设计,使得它的行为是可解释、可预测的——解释/预测的规则,就是CPU的指令集。
——换言之,在机器内部,一切本来就是高低电平,不存在转换问题。
——反而是键盘/鼠标/mic的输入要经过机械过程到数字信号的转换;而视频、音频之类的输出,要经过数模转换再通过其它机制才能变成人可辨识的信息
图灵机原理——CPU的三板斧
图灵的贡献就是,他证明了,如果一台机器,可以接受一系列的输入、并按输入指示完成运算;那么,当这台机器可支持的操作满足“图灵完备”的要求时,它就可以模拟任何其它数学/逻辑运算!
这实在是太关键了。要知道,人类早就想利用机械装置代替一些脑力工作了。比如说,算盘,按照口诀机械的一阵摆弄,答案就出来了;还有老外的各种机械计算器,比如手摇计算机到炮兵用的弹道计算机、再到德军的机械加/解密机等等,这种尝试可以说是数不胜数。
但,再怎么的,这些东西也只能解决特定的问题。想做能解决全部问题的通用机?天哪,那得有多复杂。
而图灵,就在这时候,为人类指出了一条通向机械智能的可行道路……
——一台只会做加法的机器,只要能想办法让它实现“连续做指定次数加法”,那它就可以模拟一台乘法机(模拟二进制乘法会更容易一些)。而能够模拟任何数学/逻辑运算的机器,并不比加法机复杂太多。
换句话说,要搞出一台“无所不能”的计算机器,并不需要穷尽一切可能,而是只要支持程序输入、再支持少的令人发指的几条指令,就可以办到了。
比如说,CPU,它根本上其实只会三招:与、或、非。
与就是全为真,则输出真;或是只要一个为真,则输出真;非则是输入真它就输出假、输入假就输出真——所谓的真假,一般写作1、0,在计算机内部就是高低电平。
别看CPU只会这三板斧;可当它们巧妙的组合起来后(构造成计数器、指令寄存器等等等等再组合成CPU),就达到了图灵完备的要求,产生了质变——比如,前面提到过的加法器,就是“如何用这类基本逻辑模拟多位二进制数的加法”的一个实例。
更具体是怎么做的,这就不是三言两语能说清楚的了。还是仔细看看自己的数字电路这本书吧。
——数字电路研究的,就是如何用与或非这三板斧,来实现各种高级运算甚至CPU指令集这么复杂的事物(甚至是直接实现某些算法,如加密、视频编码等等)
——而CPU指令集呢,则形成了另外一个强大得多的图灵机(体现在能够支持更多比原始的与或非更”高阶“的操作上):这就是机器码(和汇编指令几乎一一对应)
——然后呢,诸如c/c++、java等高级语言,就是利用CPU指令集形成的、另一个更加强大的图灵机(编译器/解释器负责两种图灵机之间的翻译工作)。
——而程序员们研究的,就是如何用编程语言这样一个强大的图灵机,去实现office、photoshop、wow甚至人工智能这样复杂的事物。
这是一个层层模拟的过程。
————————————————————————————
总之,开关的通断是基础;而各种神奇的功能是如何用这么简单的东西组合出来的呢,那就必须理解“程序”原理(也就是图灵机原理)了。
如果说,计算机是一个人,那么,软件就是他掌握的知识。这个知识使得他不仅能掰着手指头数数(相当于硬件直接提供的基础功能),甚至还可以去洞悉宇宙的奥秘(相当于利用软件“模拟”出来的、无穷无尽的扩展功能)。
软件控制硬件的原理
前面说过,程序本身就是高低电平的组合;它通过在CPU上执行来模拟各种决策过程;同时,计算机就是一堆开关;那么,通过指令向某些地址写出数据(访问特定地址是通过各种寻址机制/指令完成的,归根结底也可以说是通过开关切换,改变了电路拓扑),就等于开启/关闭了对应地址上的某个开关;这个开关可以是类似CPU内部那样的一组三极管,也可以是通向另外一个继电器的信号线——这个信号就促使继电器闭合,于是电机导通……
就好象人开汽车一样,神经发出的微不足道的电脉冲经过肌肉放大,影响了涉及数百甚至数千马力的能量洪流的发动机/变速箱的运转,然后汽车就开走了。
计算机也一样:它通过向控制特定地址上的开关输出0/1(高低电平),就可以通过事先准备的物理设施驱动诸如航模电机、舵机等等机构,这就完成了航模控制。
完整的控制回路甚至可以是:
航模上的传感器采集飞行姿态、地形、位置等等数据(最终转换成高低电平构成的信号)----信号通过某些端口送到CPU-----CPU执行程序,程序读取传感器发来的信号,决定下一步的行动-----经过程序的智能判断后,通过控制特定地址上的开关(前面提过,向这个地址发一组高低电平构成的数据就行了),驱动诸如航模电机、舵机等等机构,完成航模控制。
这,就是所谓的“机器人”(当然,只是最简化的机器人原理而已)。
我们就用代码展示一下怎么会显示低电平
以51单片机举例
我把题主的意思先用51单片机C语言写出来,可以在keil中运行的
好了,题主说在单片机控制里,写0就会输出低电平,是这样的。
题主说的输出低电平就是在其中的一个引脚上输出低电平
我想看不懂代码的人也能够看到
代码第七行里,p1.0这个变量被赋予了0值
那么咱们深入的看一下给他赋0值单片机内部发生了什么变化
首先给大家展示一下单片机一个引脚内部到底是什么东东,如下图。
左边的大家就不用看了
右边给大家解释一下,最右边的就是引脚了
虽然引脚是一个,但是大家可以看到
右边是有两个装置的,上边的装置是用来保持内部输出到引脚的电平不会被外部的信号所干扰。下边的装置会把从外部收集来的信号临时存储起来,这里存的不是0就是1。怎么判断?大于某一电压就是1,小于某一电压就是0。这两个装置互不干扰。
第七行的代码就是将某一引脚输出低电平并用上边的保持元件将其维持到低电平。
那么,就有人想问了,为什么写成这样单片机就会认识呢?还会奇怪为什么单片机认识的语言和程序员认识的语言一样呢?
这里就牵扯到了计算机组成原理了。我就简单的介绍一下:
首先,我写的这段代码会在一个软件里运行,这个软件会编译我的代码形成枯燥难懂但是70年代时会被人认为高大上的汇编语言,类似下图这样的(除绿色字部分,解释用的):
这还不够,形成这样的语言会让计算机中的低等编译器认识,低等编译器会将代码翻译成如下图所示的东东,如下图。
注意,这是16进制的数,具体怎么转化为二进制我就不详细展开了。为什么要编译成图3的语言再编译呢?说白了我感觉就是跟水厂一样,水厂把我制作的水放到一个通用的大水管里然后通到不同单片机的家里,单片机按照自己家的情况把水引到厨房等地。(就是这样吧 - -)
那么,我们就可以让单片机或者叫做计算机来执行这段代码了。
对不起,现在才进入到计算机组成原理(对不起计组老师)
现如今,大家所用到的计算机都是冯诺依曼型计算机。
什么是冯诺依曼型计算机?书上解释说:
采取 存储程序的方式让 控制器从 存储器中读取二进制并解释然后让 运算器去计算数值。
我来再解释一下,首先让我们了解运算器是什么东东,如下图。
最下面的就是运算器,运算器能够进行加减乘除逻辑运算,控制器会从存储器中读取数据放到上图运算器上边的框框里,一个框框放一个数据。
怎么放?
看到左右的两条道道了吗?数据会在控制器的控制下被放到这些框框里,当然控制器会控制最下面的运算器做出各种运算然后放回到上边的框框里
那么数据是怎么回去的呢?
废话,当然是怎么来就怎么滚了,通过左右两条道道啊亲
让我们来解释一下最开始楼主说的输出低电平,上边的框框有一些是不能随便放数据的,这些框框用来引出引脚,即有些框框里的数据连接着引脚啊亲
讲到这里,我想我已经比较清楚的解释了0是怎么控制低电平的了。
如果哪些地方没讲明白,大家可以交流一下,我会再详细讲讲我理解的一些内容。
首先看一下“低电平”是怎么形成的。
可以知道,引脚输出的电平来自右下方那一对互补输出级。
所以当PMOS关断,NMOS导通,那么I/O口输出低电平。
这个“控制信号”来自单片机的输出寄存器(output control)。
那么这个信号的根源是怎么来的呢?
以STM32控制器为例
STM32是ARM系列RISC的微处理器。
我们看到I/O的部分链接在APB(peripheral bus 外围总线)上。
看来这个信号就是来自这个总线。
不难知道,所有数据的调度都来自STM32的核心--Cortex-M3.
我们就可以从微处理器如何执行指令的角度去看。
实际上,每一段程序都被保存在ROM里,这个ROM里保存的就是我们软件传达下来所赋予的“信息”,微处理器通过总线在ROM里提取所需要的指令,然后再一定的时钟调配下,最后执行指令的。
而指令的实质居然是
没有错,mem[]里表述的是地址,而右边的二进制码“16‘hd000”就是本质的0和1的组合,是能够被机器识别的,故称机器码。
我这里注释的是他们分别代表的含义亦即汇编语言。
具体起来,这段"机器码"的不同位置的01排序代表不同的含义
当然,这个含义是约定俗成的,就是指令集嘛!
这样我们知道了,其实在储存器里保存的01序列,我们通过机器识别即取指令,可以了解到不同含义,进而执行不同的操作。
那么这个储存器的01序列怎么来的呢?
一般就储存原理来说,每一个ROM都是一个个小房间,而房间的排列组合就是信息,他们是有序的。是通过一定模式或者条件下“烧写进去”的。即使没有外部触发,依旧能保持原有电位。
EEPROM存储原理
EEPROM基本存储单元电路的工作原理如下图所示。与EPROM相似,它是在EPROM基本单元电路的浮空栅的上面再生成一个浮空栅,前者称为第一级浮空栅,后者称为第二级浮空栅。可给第二级浮空栅引出一个电极,使第二级浮空栅极接某一电压VG。若VG为正电压,第一浮空栅极与漏极之间产生隧道效应,使电子注入第一浮空栅极,即编程写入。若使VG为负电压,强使第一级浮空栅极的电子散失,即擦除。擦除后可重新写入。
隧道效应:量子力学则认为,即使粒子能量小于阈值能量,很多粒子冲向势垒,一部分粒子反弹,还会有一些粒子能过去,好象有一个隧道,称作“量子隧道(quantum tunneling)”。
那这个有序的信息就是"0"和“1”的组合。
这个序列如何烧写进单片机,当然是有外围电路啦,具体就是通过一定的时序打开单片机的储存通路,然后把信息烧写进去。
如何烧写呢?
JTAG(Joint Test Action Group;联合测试工作组)是一种国际标准测试协议(IEEE 1149.1兼容),主要用于芯片内部测试。现在多数的高级器件都支持JTAG协议,如DSP、FPGA器件等。标准的JTAG接口是4线:TMS、TCK、TDI、TDO,分别为模式选择、时钟、数据输入和数据输出线。
然后那些高高低低的01信息就这样在时序里被输出、被接收。
接下来我们说软件。
我们写的软件都是高级语言,离机器很远,但是很容易被人的逻辑所理解,聪明的我们由一些翻译官完成更繁杂的工作:
这些翻译官就是编译器,比如KEIL,就是把C语言翻译成汇编语言,再通过汇编器,把汇编语言变成机器码,然后把机器码烧写进单片机的ROM,单片机上电之后,运行程序,读取指令也就是那些信息,然后执行,控制IO口的寄存器,最后使IO口接地,哈哈,低电平就这样完成了。
我们写的软件经过这几个步骤 高级语言-->汇编语言--->机器语言。机器语言是二进制的,每一种指令操作都有对应的二进制编码,比如我们执行 ADD R1,R2 指令, ADD有一个唯一的二进制编码假设为编码1 ,R1 R2是CPU寄存器地址也有唯一的编码设为编码2 编码3.这些编码的具体格式和数值是根据指令格式和具体cpu架构确定的。比如arm的指令字长就固定为32位,特定的位代表着条件码操作码等。arm的指令可参考《arm 体系结构与编程》杜春雷编
我们的程序就是以这种二进制编码格式存储在cpu的存储器里。
有了这几个唯一编码之后呢?cpu就开始译码操作,进行一些数字电路的组合运算,假设编码1是 0x10(只是假设,实际各个指令集编号不同),当译码电路发现指令的操作码是0x10时就知道是进行加法运算,此时会输出一个有效信号选通加法器;同时也对编码2和编码3进行译码,选通对应的寄存器(哪一个是源寄存器哪一个是目标寄存器是由指令集格式规定的),然后就将寄存器输出的数据通过CPU内部的数据线送入加法器进行加法运算,运算的结果送入目标寄存器。这就运行了一个加法运算。
直接回答题主的问题,当你在程序中对IO管脚的寄存器写0时,单片机将通过类似上述的步骤对指令进行译码,然后将0这个数据写入到IO管脚寄存器中。寄存器的数值如何送到对应的IO管脚?一般是通过D 触发器(如图):
在单片机内部IO寄存器的数据口连接到D触发器的D管脚(实际上还有其他电路,用来增大驱动能力等),D管脚下面有小三角的管脚是时钟信号管脚,当时钟信号上升沿来临时,D触发器D端口的数据将输出到Q端口,Q端口是连接着外部的管脚的。所以只要IO寄存器不改变,Q管脚将一直保持着高电平或者低电平,即你程序表现出来的写0就使管脚输出低电平。
总结一下:
你的程序编写完后通过编译器将变成一堆二进制的机器编码----->单片机对这些编码进行译码,知道你要对哪一个寄存器进行什么样的操作----->对应的寄存器被写入正确的值,如果是IO管脚的话将根据时钟将寄存器的值输出到外部IO管脚。所以实际上单片机也就是一堆数字电路的组合,只不过我们人为的规定什么样的编码要进行什么样的操作而已。
cpu内部就是一堆门电路,门电路导通和闭合对应着输出为1或者0;
那怎么让它导通呢?用电压让它导通,你可以认为这个电压是一个能量,用能量驱动这个道理很通俗了吧;
那你可能又要说了,那电压导通那它输出是一个具体的电压啊它也不是1呀,这个就是数电和模电之间的联系和区别了:我们之所以制造数字电路,是要通过数字电路得到一种逻辑实现,而模电才是想得到一个电压输出,这就是模电和数电的天壤之别。那什么又是逻辑实现?简单的说其实也就是数学实现,所谓编程就是把我们的需求变成数学问题,用编程语言编辑出来,给到cpu,让它计算并驱动终端,最终把我们的逻辑显示出来。
至于数模之间的联系,它们之间的联系就是器件都是靠电压驱动,那你又要问了,那么电压为什么可以驱动半导体器件?well,这个你要去看电磁场+半导体物理,可能还得看一点量子力学,我也都没看呢Orz;
说到这总结一下:我们制造数字电路,就是想得到一个能让我们自由表达逻辑,并能让我们眼睁睁看见我们的逻辑实现了的一个工具,至于这个工具是数字电路,还是量子路,还是光路,只要你低功耗性能好,是啥都无所谓,最好是真空才好呢,对人类来说,空气都智能了才好呢。
接下来就要说说cpu架构+指令集。
我们常常听说,一种cpu架构对应着一种指令集,那这是为什么呢?
我们说所谓数字系统,其实很简单,你给我输入,我就给你输出;你想要什么样的输出,那你就要分析分析你要给到我什么样的输入我才能输出你想要看到的输出,编程也就是这个过程;可是问题来了,你随随便便给我什么输入我都hold住吗?很明显是hold不住的,这个例子,我就不举了。。。。
给这段下个结论就是:所谓指令集,其实就是给cpu这个数字系统一套驱动编码
说到这其实大的框架就差不多了,剩下的比较重要的部分就是布尔代数和数学之间的联系,数学和实际需求之间的联系,然后就是显示这一部分,慢慢来吧
从高级语言网下到晶体管开关都有直接的映射关系,于是代码就这样控制硬件了。
详细说一下,高级语言可以通过编译器转换成汇编语言。汇编语言就是硬件的指令,可以直接转换成0101010101。而这些010101就是电路中的低电平和高电平。这些电平控制开关的打开关断,于是各种组合就产生了复杂的逻辑电路。
三、H桥电机驱动电路
什么是H桥
因为电路长得像字母H而得名,通常它会包含四个独立控制的开关元器件,例如下图有四个MOSFET开关元器件Q1、Q2、Q3、Q4。
它们通常用于驱动电流较大的负载,比如电机。
H桥电路中间有一个直流电机M。
D1、D2、D3、D4是MOS-FET的续流二极管;
开关状态
下面以控制一个直流电机为例,对H桥的几种开关状态进行简单的介绍,其中正转和反转是人为规定的方向,实际工程中按照实际情况进行划分即可。
正转
通常H桥用来驱动感性负载,这里我们来驱动一个直流电机:
- 打开Q1和Q4
- 关闭Q2和Q3
此时假设电机正转,电流依次经过Q1、M、Q4 ,如下图中红色线条所示。
反转
另外一种状态则是电机反转,此时四个开关元器件的状态如下:
- 关闭Q1和Q4
- 打开Q2和Q3
此时电机反转,电流依次经过Q2、M、Q3 ,如下图中红色线条所示。
调速
如果要对直流电机调速,其中的一种方案就是:
- 关闭Q2和Q3
- 打开Q1 ,Q4上给它输入50%占空比的PWM波形
这样就达到了降低转速的效果,如果需要增加转速,则将输入PWM的占空比设置为100%
电流方向如下图中红色线条所示。
停止状态
这里以电机从正转切换到停止状态为例。
正转时Q1和Q4是打开状态,这时候如果关闭Q1和Q4,直流电机内部可以等效成电感,也就是感性负载,电流不会突变,那么电流将继续保持原来的方向进行流动,这时候我们希望电机里的电流可以快速衰减。
这里有两种办法。
第一种:
关闭Q1和Q4,这时候电流仍然会通过反向续流二极管进行流动,此时短暂打开Q1和Q3从而达到快速衰减电流的目的,电流方向如下图中红色线条所示。
第二种:
准备停止的时候,关闭Q1、打开Q2,这时候电流并不会衰减地很快,电流循环在Q2、M、Q4之间流动,通过MOS-FET的内阻将电能消耗掉。
补充-另外一种H桥电路
上文中是包含4个N型MOS管的H桥,另外还有包含2个N型、2个P型MOS管的H桥,下图就是这种H桥电路。它由2个P型场效应管Q1、Q2与2个N型场效应管Q3、Q4组成,桥臂上的4个场效应管相当于四个开关。
相对于前文4个N型MOS管的H桥电路,此电路的一个优点就是无论控制臂状态如何(绝不允许悬空状态),H桥都不会出现“共态导通”(短路)。
MOS管开关电路原理
P型MOS管在栅极为低电平时导通,高电平时关闭。
N型MOS管在栅极为高电平时导通,低电平时关闭。
正转
场效应管是电压控制型元件,栅极通过的电流几乎为“零”。
正因为这个特点,在连接好上图电路后,控制臂1置高电平(U=VCC)、控制臂2置低电平(U=0)时,Q1、Q4关闭,Q2、Q3导通。
此时,电机左端低电平、右端高电平,所以电流沿箭头方向流动,设定此时为电机正转。
关于PWM的配置与应用详解 以后我在搜一下相关吧
四、ESP8266做的RGB时钟,文末附代码、电路图等资源
来把又是最爱的esp8266
用ESP8266做一个高颜值的RGB时钟的项目!
这个时钟还具有自动亮度控制功能并配备了温度传感器!
材料准备
- PCB
- ESP8266或Nodemcu
- 跳线
- 焊接工具
第1步: 准备工作
Neo Pixel是可寻址的LED,我们可以通过编程,让它显示任何一种颜色或者数字。
Neo Pixel有不同的smd封装,这个项目使用的是Ws2812b-5050 mini RGB。
这种迷你LED的额定电压为3.0V到5.5V,电流为16mA(每个LED)。
NodeMCU有3.3V的稳压器,可以正常驱动所有的LED。
第2步:使用Neo Pixel Led制作7段显示器
在这里,我们需要把所有的电源并联起来,把所有的数据连接串联起来,使用7段显示方法,如上图连接所有的LED。
每段有2个LED,整个面板总共有14个LED。
我们需要4个面板来显示时间(2个显示小时,2个显示分钟)。
当然还可以再连接两个面板来显示秒/任何其他数值,或者温度。
不管怎么连接,记住总是要把第一个面板的DOUT连接到第二个面板的DIN。
第3步:连接仪表盘
为了连接小时和分钟面板,在两面板之间有一个小的PCB板,名为Dash,包含了2个LED作为二进制数字,这2个LED灯每隔一秒就会发光一次。
第4步:NodeMCU/ESP8266介绍
ESP8266集成了一个32位Tensilica处理器,标准的数字外围接口。
我们的ESP8266具有板载Wi-Fi支持,通过它我们可以连上互联网调整时间,而不需要任何RTC(实时时钟)模块。
这样子的话可以减少连接,使整个项目变得更简单一些。
第5步:代码中支持的功能
如果使用本文提供的代码,那么我们可以在这个7段时钟中增加2个额外的功能:
- 使用触觉开关的温度和湿度显示
在13号针脚上添加一个DHT11传感器,在12号针脚上添加一个触觉按钮,可以在屏幕上获得摄氏或华氏的温度值。
用一个10k电阻将按钮的第12针脚连接到5V,另一端连接到GND。也就是说,当按钮针脚被拉到GND时,显示器将显示温度读数。如果没有这个温度传感器,代码也可以工作,所以如果你想让项目简单一点,也可以不需要这些连接。
- 使用引脚A0的LDR传感器进行亮度控制
通过在A0引脚上做一个电阻分压器网络,添加一个带有10k电阻的LDR传感器,可以相应地改变亮度。
白天的亮度高,晚上的亮度低。如果你不想要可调节亮度,这部分代码也可以在没有这些传感器的情况下工作,它将会锁定在默认设置。
第6步:视频演示
,时长08:33
第7步:7段时钟
现在,我们有4个面板和一个Dash(仪表盘)。
根据上面GIF图来连接面板和仪表盘;将2个面板串联在一起。
然后使用上面给出的原理图连接NodeMCU。
第8步:代码
- 首先使用库来初始化代码:
#include <ESP8266WiFi.h>
#include <Adafruit_NeoPixel.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
#include <TimeLib.h>
#include <DHT.h>
#include <Adafruit_Sensor.h>
- 定义所有像素、I/O引脚、传感器引脚:
#define PIXEL_PER_SEGMENT 2 // Number of LEDs in each Segment
#define PIXEL_DIGITS 4 // Number of connected Digits
#define PIXEL_PIN 2 // GPIO Pin
#define PIXEL_DASH 1 // Binary segment
#define LDR_PIN A0 // LDR pin
#define DHT_PIN 13 // DHT Sensor pin
#define BUTTON_PIN 12 // Button pin
3. 对于时间格式,使用Wi-Fi把ESP8266连接到互联网:
WiFi.begin(ssid, password);
Serial.print("Connecting.");
while ( WiFi.status() != WL_CONNECTED )
- 时间设置:
void disp_Time() {
clearDisplay();
writeDigit(0, Hour / 10);
writeDigit(1, Hour % 10);
writeDigit(2, Minute / 10);
writeDigit(3, Minute % 10);
writeDigit(4, Second / 10);
writeDigit(5, Second % 10);
disp_Dash();
5. 面板上的颜色设置:
if (index == 0 || index == 1 ) color = strip.Color(0, Brightness, 0);
if (index == 2 || index == 3 ) color = strip.Color(0, Brightness, 0);
if (index == 4 || index == 5 ) color = strip.Color(Brightness, 0, 0);
这只是一个简单的介绍,同时代码还有温度和自动时间选项。
温度模式可以通过数字针脚12的开关来选择。
第9步:完整代码
#include <ESP8266WiFi.h>
#include <Adafruit_NeoPixel.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
#include <TimeLib.h>
#include <DHT.h>
#include <Adafruit_Sensor.h>
#define PIXEL_PER_SEGMENT 2 // Number of LEDs in each Segment
#define PIXEL_DIGITS 4 // Number of connected Digits
#define PIXEL_PIN 2 // GPIO Pin
#define PIXEL_DASH 1 // Binary segment
#define LDR_PIN A0 // LDR pin
#define DHT_PIN 13 // DHT Sensor pin
#define BUTTON_PIN 12 // Button pin
// Uncomment the type of sensor in use
#define DHT_TYPE DHT11 // DHT 11
//#define DHT_TYPE DHT22 // DHT 22 (AM2302)
//#define DHT_TYPE DHT21 // DHT 21 (AM2301)
#define TIME_FORMAT 12 // 12 = 12 hours format || 24 = 24 hours format
Adafruit_NeoPixel strip = Adafruit_NeoPixel((PIXEL_PER_SEGMENT * 7 * PIXEL_DIGITS) + (PIXEL_DASH * 2), PIXEL_PIN, NEO_GRB + NEO_KHZ800);
DHT dht(DHT_PIN, DHT_TYPE);
// set Wi-Fi SSID and password
const char *ssid = "Hackster";
const char *password = "Sainisagar7294";
WiFiUDP ntpUDP;
// 'time.nist.gov' is used (default server) with +1 hour offset (3600 seconds) 60 seconds (60000 milliseconds) update interval
NTPClient timeClient(ntpUDP, "time.nist.gov", 19800, 60000); //GMT+5:30 : 5*3600+30*60=19800
int period = 2000; //Update frequency
unsigned long time_now = 0;
int Second, Minute, Hour;
// set default brightness
int Brightness = 40;
// current temperature, updated in loop()
int Temperature;
bool Show_Temp = false;
//Digits array
byte digits[12] = {
//abcdefg
0b1111110, // 0
0b0110000, // 1
0b1101101, // 2
0b1111001, // 3
0b0110011, // 4
0b1011011, // 5
0b1011111, // 6
0b1110000, // 7
0b1111111, // 8
0b1110011, // 9
0b1001110, // C
0b1000111, // F
};
//Clear all the Pixels
void clearDisplay() {
for (int i = 0; i < strip.numPixels(); i++) {
strip.setPixelColor(i, strip.Color(0, 0, 0));
}
strip.show();
}
void setup() {
Serial.begin(115200);
strip.begin();
strip.show();
dht.begin();
pinMode(BUTTON_PIN, INPUT);
WiFi.begin(ssid, password);
Serial.print("Connecting.");
while ( WiFi.status() != WL_CONNECTED ) {
delay(500);
Serial.print(".");
}
Serial.println("connected");
timeClient.begin();
delay(10);
}
void loop() {
if (WiFi.status() == WL_CONNECTED) { // check WiFi connection status
int sensor_val = analogRead(LDR_PIN);
Brightness =40;
timeClient.update();
int Hours;
unsigned long unix_epoch = timeClient.getEpochTime(); // get UNIX Epoch time
Second = second(unix_epoch); // get seconds
Minute = minute(unix_epoch); // get minutes
Hours = hour(unix_epoch); // get hours
if (TIME_FORMAT == 12) {
if (Hours > 12) {
Hour = Hours - 12;
}
else
Hour = Hours;
}
else
Hour = Hours;
}
if (digitalRead(BUTTON_PIN) == LOW) {
Show_Temp = true;
}
else
Show_Temp = false;
if (Show_Temp) {
Temperature = dht.readTemperature();
Serial.println(Temperature);
clearDisplay();
writeDigit(0, Temperature / 10);
writeDigit(1, Temperature % 10);
writeDigit(2, 10);
strip.setPixelColor(28, strip.Color(Brightness, Brightness, Brightness));
strip.show();
delay(3000);
clearDisplay();
Show_Temp = false;
}
while (millis() > time_now + period) {
time_now = millis();
disp_Time(); // Show Time
}
}
void disp_Time() {
clearDisplay();
writeDigit(0, Hour / 10);
writeDigit(1, Hour % 10);
writeDigit(2, Minute / 10);
writeDigit(3, Minute % 10);
writeDigit(4, Second / 10);
writeDigit(5, Second % 10);
disp_Dash();
strip.show();
}
void disp_Dash() {
int dot, dash;
for (int i = 0; i < 2; i++) {
dot = 2 * (PIXEL_PER_SEGMENT * 7) + i;
for (int j = 0; j < PIXEL_DASH; j++) {
dash = dot + j * (2 * (PIXEL_PER_SEGMENT * 7) + 2);
Second % 2 == 0 ? strip.setPixelColor(dash, strip.Color(0,Brightness ,0)) : strip.setPixelColor(dash, strip.Color(0, Brightness,0));
}
}
}
void writeDigit(int index, int val) {
byte digit = digits[val];
int margin;
if (index == 0 || index == 1 ) margin = 0;
if (index == 2 || index == 3 ) margin = 1;
if (index == 4 || index == 5 ) margin = 2;
for (int i = 6; i >= 0; i--) {
int offset = index * (PIXEL_PER_SEGMENT * 7) + i * PIXEL_PER_SEGMENT + margin * 2;
uint32_t color;
if (digit & 0x01 != 0) {
if (index == 0 || index == 1 ) color = strip.Color(Brightness, 0, Brightness);
if (index == 2 || index == 3 ) color = strip.Color(Brightness, 0,Brightness);
if (index == 4 || index == 5 ) color = strip.Color(Brightness, 0, 0);
}
else
color = strip.Color(0, 0, 0);
for (int j = offset; j < offset + PIXEL_PER_SEGMENT; j++) {
strip.setPixelColor(j, color);
}
digit = digit >> 1;
}
}
第10步:完整电路图(高清版本文末下载)
第11步:PCB设计(面板部分)
主要的PCB设计,用于显示数字和其他字母。
文末可以下载源文件。
第12步:PCB设计(Dash部分)
第13步:故障排除
- DIN总是与DOUT串联在一起,如果接反了或从任何地方断开了,整个装置就会停止工作;
- 按上面的图连接Dash;
- 确保所有的连接都焊接好,干焊会导致数据值和颜色的改变;
- 在焊接时,不要把印刷电路板加热太多,温度保持在300度。
第14步:完整展示
五、PID是什么 鸭
PID,就是“比例(proportional)、积分(integral)、微分(derivative)”,是一种很常见的控制算法。
PID已经有107年的历史了
它并不是什么很神圣的东西,大家一定都见过PID的实际应用。
比如四轴飞行器,再比如平衡小车......还有汽车的定速巡航、3D打印机上的温度控制器....
就是类似于这种:需要将某一个物理量“保持稳定”的场合(比如维持平衡,稳定温度、转速等),PID都会派上大用场。
那么问题来了:
比如,我想控制一个“热得快”,让一锅水的温度保持在50℃,这么简单的任务,为啥要用到微积分的理论呢
你一定在想:
这不是so easy嘛~ 小于50度就让它加热,大于50度就断电,不就行了?几行代码用Arduino分分钟写出来。
没错~在要求不高的情况下,确实可以这么干~ But!如果换一种说法,你就知道问题出在哪里了:
如果我的控制对象是一辆汽车呢?
要是希望汽车的车速保持在50km/h不动,你还敢这样干么。
设想一下,假如汽车的定速巡航电脑在某一时间测到车速是45km/h。它立刻命令发动机:加速!
结果,发动机那边突然来了个100%全油门,嗡的一下,汽车急加速到了60km/h。
这时电脑又发出命令:刹车!
结果,吱...............哇............(乘客吐)
所以,在大多数场合中,用“开关量”来控制一个物理量,就显得比较简单粗暴了。有时候,是无法保持稳定的。因为单片机、传感器不是无限快的,采集、控制需要时间。
而且,控制对象具有惯性。比如你将一个加热器拔掉,它的“余热”(即热惯性)可能还会使水温继续升高一小会。
这时,就需要一种『算法』:
- 它可以将需要控制的物理量带到目标附近
- 它可以“预见”这个量的变化趋势
- 它也可以消除因为散热、阻力等因素造成的静态误差
- ....
于是,当时的数学家们发明了这一历久不衰的算法——这就是PID。
你应该已经知道了,P,I,D是三种不同的调节作用,既可以单独使用(P,I,D),也可以两个两个用(PI,PD),也可以三个一起用(PID)。
这三种作用有什么区别呢?客官别急,听我慢慢道来
我们先只说PID控制器的三个最基本的参数:kP,kI,kD。
kP
P就是比例的意思。它的作用最明显,原理也最简单。我们先说这个:
需要控制的量,比如水温,有它现在的『当前值』,也有我们期望的『目标值』。
- 当两者差距不大时,就让加热器“轻轻地”加热一下。
- 要是因为某些原因,温度降低了很多,就让加热器“稍稍用力”加热一下。
- 要是当前温度比目标温度低得多,就让加热器“开足马力”加热,尽快让水温到达目标附近。
这就是P的作用,跟开关控制方法相比,是不是“温文尔雅”了很多
实际写程序时,就让偏差(目标减去当前)与调节装置的“调节力度”,建立一个一次函数的关系,就可以实现最基本的“比例”控制了~
kP越大,调节作用越激进,kP调小会让调节作用更保守。
要是你正在制作一个平衡车,有了P的作用,你会发现,平衡车在平衡角度附近来回“狂抖”,比较难稳住。
如果已经到了这一步——恭喜你!离成功只差一小步了~
kD
D的作用更好理解一些,所以先说说D,最后说I。
刚才我们有了P的作用。你不难发现,只有P好像不能让平衡车站起来,水温也控制得晃晃悠悠,好像整个系统不是特别稳定,总是在“抖动”。
你心里设想一个弹簧:现在在平衡位置上。拉它一下,然后松手。这时它会震荡起来。因为阻力很小,它可能会震荡很长时间,才会重新停在平衡位置。
请想象一下:要是把上图所示的系统浸没在水里,同样拉它一下 :这种情况下,重新停在平衡位置的时间就短得多。
我们需要一个控制作用,让被控制的物理量的“变化速度”趋于0,即类似于“阻尼”的作用。
因为,当比较接近目标时,P的控制作用就比较小了。越接近目标,P的作用越温柔。有很多内在的或者外部的因素,使控制量发生小范围的摆动。
D的作用就是让物理量的速度趋于0,只要什么时候,这个量具有了速度,D就向相反的方向用力,尽力刹住这个变化。
kD参数越大,向速度相反方向刹车的力道就越强。
如果是平衡小车,加上P和D两种控制作用,如果参数调节合适,它应该可以站起来了~欢呼吧
。
等等,PID三兄弟好像还有一位。看起来PD就可以让物理量保持稳定,那还要I干嘛?
因为我们忽视了一种重要的情况:
kI
还是以热水为例。假如有个人把我们的加热装置带到了非常冷的地方,开始烧水了。需要烧到50℃。
在P的作用下,水温慢慢升高。直到升高到45℃时,他发现了一个不好的事情:天气太冷,水散热的速度,和P控制的加热的速度相等了。
这可怎么办?
- P兄这样想:我和目标已经很近了,只需要轻轻加热就可以了。
- D兄这样想:加热和散热相等,温度没有波动,我好像不用调整什么。
于是,水温永远地停留在45℃,永远到不了50℃。
作为一个人,根据常识,我们知道,应该进一步增加加热的功率。可是增加多少该如何计算呢?
前辈科学家们想到的方法是真的巧妙。
设置一个积分量。只要偏差存在,就不断地对偏差进行积分(累加),并反应在调节力度上。
这样一来,即使45℃和50℃相差不太大,但是随着时间的推移,只要没达到目标温度,这个积分量就不断增加。系统就会慢慢意识到:还没有到达目标温度,该增加功率啦!
到了目标温度后,假设温度没有波动,积分值就不会再变动。这时,加热功率仍然等于散热功率。但是,温度是稳稳的50℃。
kI的值越大,积分时乘的系数就越大,积分效果越明显。
所以,I的作用就是,减小静态情况下的误差,让受控物理量尽可能接近目标值。
I在使用时还有个问题:需要设定积分限制。防止在刚开始加热时,就把积分量积得太大,难以控制。