单片机(Microcontroller, MCU)是一种集成了计算机功能的微型计算机,通常由一个微处理器(CPU)、存储器(ROM、RAM)、输入/输出接口、定时器/计数器等功能模块集成在同一芯片上。单片机是一种常用于嵌入式系统中的控制器,它被广泛应用于家电、汽车、工业自动化、医疗设备、消费电子、物联网(IoT)设备等多个领域。
单片机
先说一个概念:调试,在企业程序设计里(我把企业商务类型的软件开发叫企业程序设计,把单片机与驱动程序这样接触底层汇编与硬件相关的程序设计叫底层程序设计),调试一般都用来跟踪变量的赋值过程,以及查看内存堆栈的内容,查看这些内容的目的在于观察变量的赋值过程与赋值情况从而达到调试的目的。由于企业程序的宿主就是开发它的计算机本身,因此企业程序设计比起底层程序设计,特别是单片机的程序设计调试来的更直观,调试也更方便。
单片机的程序设计调试分为两种,一种是使用软件模拟调试,意思就是用开发单片机程序的计算机去模拟单片机的指令执行,并虚拟单片机片内资源,从而实现调试的目的,但是软件调试存在一些问题,如计算机本身是多任务系统,划分执行时间片是由操作系统本身完成的,无法得到控制,这样就无法时时的模拟单片机的执行时序,也就是说 ,不可能像真正的单片机运行环境那样执行的指令在同样一个时间能完成(往往要完成的比单片机慢)。为了解决软件调试的问题,第二种是硬件调试,硬件调试其实也需要计算机软件的配合,大致过程是这样的:计算机软件把编译好的程序通过串行口、并行口或者USB口传输到硬件调试设备中(这个设备叫仿真器),仿真器仿真全部的单片机资源(所有的单片机接口,并且有真实的引脚输出),仿真器可以接入实际的电路中,然后与单片机一样执行。同时,仿真器也会返回单片机内部内存与时序等情况给计算机的辅助软件,这样 就可以在软件里看到真实的执行情况。不仅如此,还可以通过计算机断的软件实现单步、全速、运行到光标的常规调试手段。
总结一下两者的不同与相同:
相同点:
1:都可以检测单片机执行时序下的片内资源情况(如R0-R7 、PC计数器等)
2:可以实现断点、全速、单步、运行到光标等常规调试手段。
不同:
1:软件调试无法实现直接连接硬件电路的调试,只能通过软件窗口虚拟硬件端口的电平输出情况而仿真器可以实现与单片机一样的功能的硬件连接,从某种意义上说这个时候仿真器就是一个单片机。
2:软件调试执行单片机指令的时间无法与真实的单片机执行时间画上等号,也就是说如果一个程序在单片机中要执行300us,可能在计算机中执行的时间可能会比这个长很多,而且无法预料。仿真器则是完全与单片机相同。
3:软件调试只能是一种初步的,小型工程的调试,比如一个只有几百上千行的代码的程序,软件调试能很好的完成,如果是一个协调系统,可能还需要借助几个单片机仿真器和相关的仪器才能解决。
4:软件仿真不需要额外花钱,而硬件需要,一个仿真器一般都上千元,同时可以仿真许多种单片机的工作。
最后,调试一般都是在发生错误与意外的情况下使用的,如果程序能正常执行调试很多时候是用不上的,所以最高效率的程序开发还是程序员自己做好规范,而不是指望调试来解决问题。
下面将具体介绍如何使用Keil uVision 来软件调试单片机程序。
首先:打开一个已经编译通过的单片机项目(如何新建与编译单片机程序项目这里省略)
选择Debug下面的Start/Stop Debug Session,这个选项可以打开调试也可以关闭调试
接下来看到的窗口就是调试窗口了:
下面具体说说相关子窗口的功能:
1:左侧的ProjectWorkspace
Regs是片内内存的相关情况值,Sys是系统一些累加器、计数器等。Regs很简单就不多说。具体介绍一下Sys
a 累加器ACC,往往在运算前暂存一个操作数(如被加数),而运算后又保存其结果(如代数和)。
b 寄存器B ,主要用于乘法和除法操作
sp
sp_max
dptr 数据指针DPTR
PC $
states 执行指令的数量
sec 执行指令的时间累计(单位 秒)
psw 程序状态标志寄存器PSW,八位寄存器,用来存放运算结果的一些特征,如有无进位、借位等。
p 奇偶标志P。反映累加器ACC内容的奇偶性,如果ACC中的运算结果有偶数个1(如11001100B,其中有4个1),则P为0,否则,P=1。
f1
ov 溢出标志位OV。MCS-51反映带符号数的运算结果是否有溢出,有溢出时,此位为1,否则为0。
rs
f0
ac 辅助进位标志AC。又称半进位标志,它反映了两个八位数运算低四位是否有半进位,即低四位相加(或减)有否进位(或借位),如有则AC为1状态,否则为0。
cy 进位标志CY(PSW7)。它表示了运算是否有进位(或借位)。如果操作结果在最高位有进位(加法)或者借位(减法),则该位为1,否则为0
由于PSW存放程序执行中的状态,故又叫程序状态字?运算器中还有一个按位(bit)进行逻辑运算的逻辑处理机(又称布尔处理机)
根据指令执行的不同上述值会有相应的变化,也正是为了监测这些在单片机中看不到的值而达到调试的目的。
虽然软件调试无法实现硬件调试那样的信号输出,但是可以通过软件窗口的模拟监测输出信号的高低电平以及单片机相关端口的变化。
上图所示,Port0,Port1,Port2,Port3就对应于单片机的四个P0,P1,P2,P3 口,共32个针脚。
这是全部打开后的效果。
有了输出,就应该有输入的设置:
这个按钮可以打开输入预设窗口,输入值窗口如下:
选择不同的Int Source 会有不同的 Selected Interrupt的变化,通过选择与赋值达到模拟输入的目的。
接下来是串口的设置:
这是设置串口的窗口
监测窗口数据还有一个窗口:
点击:
关于串口的问题,以后我会有专门的文章介绍,这里就这样大致介绍一下。
最下面还有一个定时器的设置:
3个定时器与一个看门狗,设置定时器的数量与工程选择的单片机种类有关系,如果是8051就只有2个定时器,如果是选择8052 就有3个定时器了。
定时器的设置很简单:
参考的数很多,这里暂时省略了,以后以专门的篇幅介绍。
下面再介绍一下一些常用的调试按钮:
可以在代码所在窗口的最左边右击按钮插入一个断点,如下图所示:
有了这个功能,你就可以控制监控要执行到某位置时系统的状态。
最后在介绍一下一个很实用的功能:
上图中就可以看出,C51 代码是如何被解释为汇编的。
其实调试还有很多的功能,我这里只是介绍了一小部分,希望能起到抛砖引玉的作用,更多的细节需要你自己去发掘与学习。
文章来源:博客园
一、定义
1、上拉就是将不确定的信号通过一个电阻嵌位在高电平!“电阻同时起限流作用”!下拉同理!
2、上拉是对器件注入电流,下拉是输出电流。
3、弱强只是上拉电阻的阻值不同,没有什么严格区分。
4、对于非集电极(或漏极)开路输出型电路(如普通门电路)提升电流和电压的能力是有限的,上拉电阻的功能主要是为集电极开路输出型电路输出电流通道。
二、拉电阻作用
1、一般作单键触发使用时,如果IC本身没有内接电阻,为了使单键维持在不被触发的状态或是触发后回到原状态,必须在IC外部另接一电阻。
2、数字电路有三种状态:高电平、低电平、和高阻状态,有些应用场合不希望出现高阻状态,可以通过上拉电阻或下拉电阻的方式使处于稳定状态,具体视设计要求而定!
3、一般说的是I/O端口,有的可以设置,有的不可以设置,有的是内置,有的是需要外接,I/O端口的输出类似与一个三极管的C,当C接通过一个电阻和电源连接在一起的时候,该电阻成为上C拉电阻,也就是说,如果该端口正常时为高电平;C通过一个电阻和地连接在一起的时候,该电阻称为下拉电阻,使该端口平时为低电平,作用吗:比如:“当一个接有上拉电阻的端口设为输入状态时,他的常态就为高电平,用于检测低电平的输入”。
4、上拉电阻是用来解决总线驱动能力不足时提供电流的。一般说法是拉电流,下拉电阻是用来吸收电流的,也就是我们通常所说的灌电流。
5、接电阻就是为了防止输入端悬空。
6、减弱外部电流对芯片产生的干扰。
7、保护cmos内的保护二极管,一般电流不大于10mA。
8、通过上拉或下拉来增加或减小驱动电流。
9、改变电平的电位,常用在TTL-CMOS匹配。
10、在引脚悬空时有确定的状态。
11、增加高电平输出时的驱动能力。
12、为OC门提供电流。
三、上拉电阻应用原则
1、当TTL电路驱动COMS电路时,如果TTL电路输出的高电平低于COMS电路的最低高电平(一般为3。5V),这时就需要在TTL的输出端接上拉电阻,以提高输出高电平的值。
2、OC门电路“必须加上拉电阻,才能使用”。
3、为加大输出引脚的驱动能力,有的单片机管脚上也常使用上拉电阻。
4、在COMS芯片上,为了防止静电造成损坏,不用的管脚不能悬空,一般接上拉电阻产生降低输入阻抗,提供泄荷通路。
5、芯片的管脚加上拉电阻来提高输出电平,从而提高芯片输入信号的噪声容限增强抗干扰能力。
6、提高总线的抗电磁干扰能力。管脚悬空就比较容易接受外界的电磁干扰。
7、长线传输中电阻不匹配容易引起反射波干扰,加上下拉电阻是电阻匹配,有效的抑制反射波干扰。
8、在数字电路中不用的输入脚都要接固定电平,通过1k电阻接高电平或接地。
四、上拉电阻阻值选择原则
1、从节约功耗及芯片的灌电流能力考虑应当足够大;电阻大,电流小。
2、从确保足够的驱动电流考虑应当足够小;电阻小,电流大。
3、对于高速电路,过大的上拉电阻可能边沿变平缓。综合考虑。
以上三点,通常在1k到10k之间选取。对下拉电阻也有类似道理。
对上拉电阻和下拉电阻的选择应“结合开关管特性和下级电路的输入特性进行设定,主要需要考虑以下几个因素”:
1、驱动能力与功耗的平衡。以上拉电阻为例,一般地说,上拉电阻越小,驱动能力越强,但功耗越大,设计是应注意两者之间的均衡。
2、下级电路的驱动需求。同样以上拉电阻为例,当输出高电平时,开关管断开,上拉电阻应适当选择以能够向下级电路提供足够的电流。
3、高低电平的设定。不同电路的高低电平的门槛电平会有不同,电阻应适当设定以确保能输出正确的电平。以上拉电阻为例,当输出低电平时,开关管导通,上拉电阻和开关管导通电阻分压值应确保在零电平门槛之下。
4、频率特性。以上拉电阻为例,上拉电阻和开关管漏源级之间的电容和下级电路之间的输入电容会形成“RC延迟”,电阻越大,延迟越大。上拉电阻的设定应考虑电路在这方面的需求。
下拉电阻的设定的原则和上拉电阻是一样的。
OC门输出高电平时是一个高阻态,其上拉电流要由上拉电阻来提供,设输入端每端口不大于100uA,设输出口驱动电流约500uA,标准工作电压是5V,输入口的高低电平门限为0.8V(低于此值为低电平);2V(高电平门限值)。
选上拉电阻时:500uA x 8.4K= 4.2即选大于8.4K时输出端能下拉至0.8V以下,此为最小阻值,再小就拉不下来了。
如果输出口驱动电流较大,则阻值可减小,保证下拉时能低于 0.8V即可。当输出高电平时,忽略管子的漏电流,两输入口需200uA,200uA x15K=3V即上拉电阻压降为3V,输出口可达到2V,此阻值为最大阻值,再大就拉不到2V了。选10K可用。
COMS门的可参考74HC系列设计时管子的漏电流不可忽略,IO口实际电流在不同电平下也是不同的,上述仅仅是原理,一句话概括为:“输出高电平时要喂饱后面的输入口,输出低电平不要把输出口喂撑了”(否则多余的电流喂给了级联的输入口,高于低电平门限值就不可靠了)。
此外,还应注意以下几点:
A、要看输出口驱动的是什么器件,如果该器件需要高电压的话,而输出口的输出电压又不够,就需要加上拉电阻。
B、如果有上拉电阻那它的端口在默认值为高电平,你要控制它必须用低电平才能控制如三态门电路三极管的集电极,或二极管正极去控制把上拉电阻的电流拉下来成为低电平。
C、尤其用在接口电路中,为了得到确定的电平,一般采用这种方法,以保证正确的电路状态,以免发生意外,比如,在电机控制中,逆变桥上下桥臂不能直通,如果它们都用同一个单片机来驱动,必须设置初始状态。防止直通!驱动尽量用灌电流。
电阻在选用时,选用经过计算后与标准值最相近的一个!
P0为什么要上拉电阻原因有:
1、 P0口片内无上拉电阻。
2、 P0为I/O口工作状态时,上方FET被关断,从而输出脚浮空,因此P0用于输出线时为开漏输出。
3、 由于片内无上拉电阻,上方FET又被关断,P0输出1时无法拉升端口电平。
P0是双向口,其它P1,P2,P3是准双向口。准双向口是因为在读外部数据时要先“准备”一下,为什么要准备一下呢?
单片机在读准双向口的端口时,先应给端口锁存器赋1,目的是使FET关断,不至于因片内FET导通使端口钳制在低电平。
上下拉一般选10k!
文章来源:嵌入式资讯精选
MCU由于内部资源的限制,软件设计有其特殊性,程序一般没有复杂的算法以及数据结构,代码量也不大, 通常不会使用 OS (Operating System), 因为对于一个只有若干K ROM、一百多byte RAM 的MCU来说,一个简单OS 也会吃掉大部分的资源。
对于无OS的系统,流行的设计是主程序(主循环 )+(定时)中断,这种结构虽然符合自然想法,不过却有很多不利之处,首先是中断可以在主程序的任何地方发生,随意打断主程序。其次主程序与中断之间的耦合性(关联度)较大,这种做法 使得主程序与中断缠绕在一起,必须仔细处理以防不测。
那么换一种思路,如果把主程序全部放入(定时)中断中会怎么样?这么做至少可以立即看到几个好处:系统可以处于低功耗的休眠状态,将由中断唤醒进入主程序;如果程序跑飞,则中断可以拉回;没有了主从之分(其他中断另计),程序易于模块化。
为了把主程序全部放入(定时)中断中,必须把程序化分成一个个的模块,即任务,每个任务完成一个特定的功能,例如扫描键盘并检测按键。 设定一个合理的时基 (tick),例如 5、10 或 20 ms,每次定时中断,把所有任务执行一遍,为减少复杂性,一般不做动态调度(最多使用固定数组以简化设计,做动态调度就接近 OS了),这实际上是一种无优先级时间片轮循的变种。来看看主程序的构成:
void main()
{
…. // Initialize
while (true) {
IDLE; //sleep
}
}
这里的 IDLE 是一条sleep 指令,让MCU进入低功耗模式。中断程序的构成:
void Timer_Interrupt()
{
SetTimer();
ResetStack();
Enable_Timer_Interrupt;
….
进入中断后,首先重置Timer,这主要针对8051、8051 自动重装分频器只有 8-bit,难以做到长时间定时;复位 stack ,即把stack指针赋值为栈顶或栈底,用以表示与过去决裂,而且不准备返回到中断点,保证不会保留程序在跑飞时stack 中的遗体。Enable_Timer_Interrupt 也主要是针对8051。8051 由于中断控制较弱,只有两级中断优先级,而且使用了如果中断程序不用 reti 返回,则不能响应同级中断这种偷懒方法,所以对于 8051,必须调用一次 reti 来开放中断:
_Enable_Timer_Interrupt:
acall _reti
_reti: reti
下面就是任务的执行了,这里有几种方法。第一种是采用固定顺序,由于MCU程序复杂度不高,多数情况下可以采用这种方法:
…
Enable_Timer_Interrupt;
ProcessKey();
RunTask2();
…
RunTaskN();
while (1) IDLE;
可以看到中断把所有任务调用一遍,至于任务是否需要运行,由程序员自己控制。另一种做法是通过函数指针数组:
#define CountOfArray(x) (sizeof(x)/sizeof(x[0]))
typedef void (*FUNCTIONPTR)();
const FUNCTIONPTR[] tasks = {
ProcessKey,
RunTask2,
…
RunTaskN
};
void Timer_Interrupt()
{
SetTimer();
ResetStack();
Enable_Timer_Interrupt;
for (i=0; i (*tasks[i])(); while (1) IDLE; } 使用const 是让数组内容位于 code segment (ROM) 而非 data segment (RAM) 中,8051 中使用 code 作为 const 的替代品。 (题外话:关于函数指针赋值时是否需要取地址操作符 & 的问题,与数组名一样,取决于 compiler. 对于熟悉汇编的人来说,函数名和数组名都是常数地址,无需也不能取地址。对于不熟悉汇编的人来说,用 & 取地址是理所当然的事情。Visual C++ 2005对此两者都支持) 这种方法在汇编下表现为散转,一个小技巧是利用 stack 获取跳转表入口: mov A, state acall MultiJump ajmp state0 ajmp state1 ... MultiJump: pop DPH pop DPL rl A jmp @A+DPTR 还有一种方法是把函数指针数组(动态数组,链表更好,不过在 mcu 中不适用)放在 data segment 中,便于修改函数指针以运行不同的任务,这已经接近于动态调度了: FUNCTIONPTR[COUNTOFTASKS] tasks; tasks[0] = ProcessKey; tasks[0] = RunTaskM; tasks[0] = NULL; ... FUNCTIONPTR pFunc; for (i=0; i< COUNTOFTASKS; i++) { pFunc = tasks[i]); if (pFunc != NULL) (*pFunc)(); } 通过上面的手段,一个中断驱动的框架形成了,下面的事情就是保证每个 tick 内所有任务的运行时间总和不能超过一个 tick的时间。为了做到这一点,必须把每个任务切分成一个个的时间片,每个 tick 内运行一片。这里引入了状态机 (state machine)来实现切分。关于 state machine, 很多书中都有介绍, 这里就不多说了。 (题外话:实践升华出理论,理论再作用于实践。我很长时间不知道我一直沿用的方法就是state machine,直到学习UML/C++,书中介绍 tachniques for identifying dynamic behvior,方才豁然开朗。功夫在诗外,掌握 C++, 甚至C# JAVA, 对理解嵌入式程序设计,会有莫大的帮助) 状态机的程序实现相当简单,第一种方法是用 swich-case 实现: void RunTaskN() { switch (state) { case 0: state0(); break; case 1: state1(); break; … case M: stateM(); break; default: state = 0; } } 另一种方法还是用更通用简洁的函数指针数组: const FUNCTIONPTR[] states = { state0, state1, …, stateM }; void RunTaskN() { (*states[state])(); } 下面是 state machine 控制的例子: void state0() { } void state1() { state++; } // next state; void state2() { state+=2; } // go to state 4; void state3() { state--; } // go to previous state; void state4() { delay = 100; state++; } void state5() { delay--; if (delay <= 0) state++; } //delay 100*tick void state6() { state=0; } // go to the first state 一个小技巧是把第一个状态 state0 设置为空状态,即: void state0() { } 这样,state =0可以让整个task 停止运行,如果需要投入运行,简单的让 state = 1 即可。 以下是一个键盘扫描的例子,这里假设 tick = 20 ms,ScanKeyboard() 函数控制口线的输出扫描,并检测输入转换为键码,利用每个state 之间 20 ms 的间隔去抖动。 enum EnumKey { EnumKey_NoKey = 0, … }; struct StructKey { int keyValue; bool keyPressed; } ; struct StructKeyProcess key; void ProcessKey() { (*states[state])(); } void state0() { } void state1() { key.keyPressed = false; state++; } void state2() { if (ScanKey() != EnumKey_NoKey) state++; } //next state if a key pressed void state3() { //debouncing state key.keyValue = ScanKey(); if (key.keyValue == EnumKey_NoKey) state--; else { key.keyPressed = true; state++; } } void state4() { if (ScanKey() == EnumKey_NoKey) state++; } //next state if the key released void state5() { ScanKey() == EnumKey_NoKey? state = 1 : state--; } 上面的键盘处理过程显然比通常使用标志去抖的程序简洁清晰,而且没有软件延时去抖的困扰。以此类推,各个任务都可以划分成一个个的state,每个state 实际上占用不多的处理时间。某些任务可以划分成若干个子任务,每个子任务再划分成若干个状态。 (题外话:对于常数类型,建议使用 enum 分类组织,避免使用大量 #define 定义常数) 对于一些完全不能分割,必须独占的任务来说,比如我以前一个低成本应用中红外遥控器的软件解码任务,这时只能牺牲其他的任务了。两种做法:一种是关闭中断,完全的独占; void RunTaskN() { Disable_Interrupt; … Enable_Interrupt; } 第二种,允许定时中断发生,保证某些时基 register 得以更新; void Timer_Interrupt() { SetTimer(); Enable_Timer_Interrupt; UpdateTimingRegisters(); if (watchDogCounter = 0) { ResetStack(); for (i=0; i (*tasks[i])(); while (1) IDLE; } else watchDogCounter--; } 只要watchDogCounter 不为 0,那么中断正常返回到中断点,继续执行先前被中断的任务,否则,复位 stack,重新进行任务循环。这种状况下,中断处理过程极短,对独占任务的影响也有限。 中断驱动多任务配合状态机的使用,我相信这是mcu 下无os 系统较好的设计结构。对于绝大多数 mcu 程序设计来说,可以极大的减轻程序结构的安排,无需过多的考虑各个任务之间的时间安排,而且可以让程序简洁易懂。缺点是,程序员必须花费一定的时间考虑如何切分任务。 下面是一段用 C 改写的CD Player 中检测 disc 是否存在的伪代码,用以展示这种结构的设计技巧,原源代码为Z8 mcu 汇编,基于 Sony 的 DSP, Servo and RF 处理芯片,通过送出命令字来控制主轴/滑板/聚焦/寻迹电机,并读取状态以及 CD 的sub Q 码。这个处理任务只是一个大任务下用state machine切开的一个二级子任务,tick = 20 ms。 state1() { InitializeMotor(); state++; } state2() { if (innerSwitch != ON) { SendCommand(EnumCommand_SlidingMotorBackward); timeout = MILLISECOND(10000); state++; // 滑板电机向内运动, 直至触及最内开关。 } else state += 2; } state3() { if ((--timeout) == 0) { //note: some C compliers do not support (--timeout) == SendCommand(EnumCommand_SlidingMotorStop) systemErrorCode = EnumErrorCode_InnerSwitch; state = 0; // 10 s 超时错误, } else { if (innerSwitch == ON) { SendCommand(EnumCommand _SlidingMotorStop) timeout = MILLISECOND(200); // 200ms电机停止时间 state++; } } } state4() { if ((--timeout) == 0) state++; } //等待电机完全停止 state5() { SendCommand(EnumCommand_SlidingMotorForward); timeout = MILLISECOND(2000); state++; } // 滑板电机向外运动,脱离inner switch state6() { if ((--timeout) == 0) { SendCommand(EnumCommand_SlidingMotorStop) systemErrorCode = EnumErrorCode_InnerSwitch; state = 0; // 2 s 超时错误, } else { if (innerSwitch == OFF) { SendCommand(EnumCommand_SlidingMotorStop) timeout = MILLISECOND(200); // 200ms电机停止时间 state++; } } } state7() { state4(); } state8() { LaserOn(); state++; retryCounter = 3;} //打开激光器 state9() { SendCommand(FocusUp); state++; timeout = MILLISECOND(2000); } //光头上举,检测聚焦过零 3 次,判断cd 是否存在 state10() { if (FocusCrossZero) { systemStatus.Disc = EnumStatus_DiscExist; SendCommand(EnumCommand_AutoFocusOn); //有cd, 打开自动聚焦。 state = 0; //本任务结束。 playProcess.state = 1; //启动 play 任务 } else if ((--timeout) == 0) { SendCommand(EnumCommand_ FocusClose); //光头聚焦复位 if ((--retryCounter) == 0) { systemStatus.Disc = EnumStatus_Nodisc; //无盘 displayProcess.state = EnumDisplayState_NoDisc; //显示闪烁的无盘 LaserOff(); state = 0; //任务停止 } else state--; //再试 } } stateStop() { SendCommand(EnumCommand_SlidingMotorStop); SendCommand(EnumCommand_FocusClose); state = 0; } 文章来源: CDSN
大家都用过计算器,有没有想过它是怎么实现的呢?这里我不详述计算器的原理,而只对思路进行简单介绍。等我们学会了单片机,也可以亲手制作一个计算器。
用电路进行数学计算
通过电路进行数学计算,应该怎么做呢?为了便于理解,下面我举个很简单的例子。
在这个电路中,电阻R1=R2,我给A、B两点分别接入3V和5V电压,这个时候,C点的电压则为(5+3)/2=4V。这个电路完成了一个求平均值的操作,如果我们用1V表示数字1,它计算出来3和5的平均值是4;如果我们定义1mV表示数字1,这个电路就计算出了3000和5000的平均值是4000。如果我能通过巧妙的方法,利用电阻电容乃至晶体管等元器件的特性,设计出很多类似这样的电路,它就可以完成很复杂的四则运算,以及平方、开方、对数等运算。这就是通过电路来帮助我们进行数学计算的简单例子。在这个例子中,并不见得能体现到电路计算相比于我们用笔纸计算的优势。但是如果我们把电路做的足够复杂,它的计算速度是相当快的,并且只要有电能供应,它就永远不知疲倦的计算,而且不容易出错。
上面我们设计了一个简单的模拟电路计算器,它能计算两个数的平均值,我们用电压值直接表示数字。但是这个电路在实际中工作并没有那么理想。做基本电学实验测量电压的时候,大家会发现,电压的测量总是有误差的,电压表有误差,读数也有误差,并且基本上无法避免。自然界中很多东西都是有误差的。在这里除了电压表测出来的值和实际值不同,实际C点电压值也并不完全等于AB电压值的平均,因为我们很难保证R1和R2阻值完全一致,并且导线也有电阻。于是我们计算出来的结果,更可能是3.99或者4.01而不是精确的4.00,这就导致我们的计算出了误差。如果电路复杂了,误差会逐步累积,越来越大,最后导致计算结果完全没有意义,而减小电路的误差也是相当不容易的。
模拟电路与数字电路、十进制与二进制
于是数字电路诞生了。相较于模拟电路的不精确,数字电路就有很大优势了。注意,数字电路是相对于模拟电路而言的,数字电路的本质也是模拟电路。通常我们所说的模拟电路,指的是除数字电路以外的电路。
我们人类用十进制计数法表示数字,原因是我们有十个手指。而数字电路中使用二进制数字来进行运算,因为很多电子器件往往会有两种很确定的状态,比如开关的“开”和“关”,灯的“亮”和“灭”。二进制数其实比十进制数简单多了。十进制中,从0到9,满10就向高位进位,即9+1=10;而二进制满二进一,所以二进制中1+1=10。一开始我们会感觉这样很别扭,实际上并非二进制有多难,只是我们习惯了十进制而已。
数字电路中,我们使用的比较多的一种用电压表示二进制数字的方式,称为TTL电平(TTL = Transistor-Transistor Logic,原意为逻辑门电路)。它规定+5V电压为高电平,表示数字“1”,0V电压为低电平,表示数字“0”。由于电路自身特点,实际上这种TTL电平电路输出的电压,并非绝对准确的5V和0V,而是规定将>2.4V的电压视为高电平, 电压<0.4V则视为低电平。也正是因为这样的特点,我们根本不需要将电压控制的很准确,就能很准确的表示出我们想要表示的数字。和前面的模拟平均数计算电路相比,明显很有优势。而这也正是数字电路得以广泛应用的根本原因。
传统数字电路和单片机
数字电路的介绍就到此为止,在原理篇中会有更详细的介绍。事实上,单片机的本质也是数字电路。下面我们要说的传统数字电路,指的是除单片机这类可编程器件以外的数字电路。下面我们来看看单片机和传统数字电路的区别。
利用一些常用的传统数字电路器件(一般都是集成电路芯片),我们可以设计出如下图的电路。它是一个电子表,有六个数码管显示时间,图中正显示的就是00:00:18。可以看出来这个电路还是挺复杂的,设计起来也是很费时间的。
但是单片机的出现,使得实现相同功能的电路设计难度大大降低。下图就是使用单片机设计的电路。同样是电子表,不仅显示效果比前面的那个要好,而且功能更强大了,两个按键可以像市面上常见的两个按键的手表一样调整时间和日期;而电路却简单了很多。我们只需要给单片机写进去特定的程序,就可以让它按照我们设计好的方式工作。
如果某天,我们想要给这个电子表增加马表计时的功能,对于前面的那个数字电路,恐怕整个电路都得重新设计制作;但是对于单片机制作的这个电路,我们只需要修改程序代码,然后重新写进去就可以了,就像在电脑上安装软件一样,根本不需要修改电路,十分方便。
传统数字电路和单片机的关系就像非智能手机和智能手机的关系一样,智能手机最大的优势在于它可以安装各种软件游戏,而非智能手机就没有这么强大的功能。单片机也是如此,同样的电路,你可以给它下载各种程序,让它按照你的想法去工作。对于单片机来说,硬件电路是单片机的躯体,而程序才是它的灵魂,而写程序的你,就是它的上帝。
文章来源: Hainter