一、基础知识概念与原理
1、什么是定时器(timer)
1、定时器是SoC中常见外设
(1)定时器与计数器。计数器是用来计数的(每隔一个固定时间会计一个数);因为计数器的计数时间周期是固定的,因此到了一定时间只要用计数值×计数时间周期,就能得到一个时间段,这个时间段就是我们定的时间(这就是定时器了)。
(2)定时器/计数器作为SoC的外设,主要用来实现定时执行代码的功能。 定时器相对于SoC来说,就好象闹钟相对于人来说意义一样。
2、定时器有什么用
(1)定时器可以让SoC在执行主程序的同时,可以(通过定时器)具有计时功能,到了一定时间(计时结束)后,定时器会产生中断提醒CPU,CPU会去处理中断并执行定时器中断的ISR。从而去执行预先设定好的事件。
(2)定时器就好象是CPU的一个秘书一样,这个秘书专门管帮CPU来计时,并到时间后提醒CPU要做某件事情。所以CPU有了定时器之后,只需要预先把自己(xx)时间之后必须要做的事情绑定到定时器中断ISR即可,到了时间之后定时器就会以中断的方式提醒CPU来处理这个事情。
3、定时器的原理
(1)定时器计时其实是通过计数来实现的。定时器内部有一个计数器,这个计数器根据一个时钟(这个时钟源来自于ARM的APB总线,然后经过时钟模块内部的分频器来分频得到)来工作。每隔一个时钟周期,计数器就计数一次,定时器的时间就是计数器计数值×时钟周期。
(2)定时器内部有1个寄存器TCNT,计时开始时我们会把一个总的计数值(譬如说300)放入TCNT寄存器中,然后每隔一个时钟周期(假设为1ms)TCNT中的值会自动减1(硬件自动完成,不需要CPU软件去干预),直到TCNT中减为0的时候,TCNT就会触发定时器中断。
(3)定时时间是由2个东西共同决定的:一个是TCNT中的计数值,一个是时钟周期。譬如上例中,定时周期就为300×1ms = 300ms。
这样我们就可以通过修改TCNT中的值和时钟周期来设定我们需要的时间。
4、定时器和看门狗、RTC、蜂鸣器的关系
(1)这几个东西都是和时间有关的部件。
(2)看门狗其实就是一个定时器,只不过定时时间到了之后不只是中断,还可以复位CPU。
(3)RTC是实时时钟,它和定时器的差别就好象闹钟(定时器)和钟表(RTC)的差别一样。
(4)蜂鸣器是一个发声设备,在ARM里面蜂鸣器是用定时器模块来驱动的。
二、S5PV210中的定时器及介绍
1、S5PV210中的定时器
在S5PV210内部,一共有4类定时器件。这4类定时器件的功能、特征是不同的。
1、PWM定时器
(1)这种是最常用的,平时所说的定时器一般指的是这个。像简单单片机(譬如51单片机)中的定时器也是这类。
(2)为什么叫PWM定时器,因为一般SoC中产生PWM信号都是靠这个定时器模块的。(最典型的一种用法就是用它来生成PWM信号)
2、系统定时器
(1)系统(指的是操作系统)定时器,系统定时器也是用来产生固定时间间隔(TCNT×时钟周期)信号的,称为systick,这个systick用来给操作系统提供tick信号。
(2)产生systick作为操作系统的时间片(time slice)的。
(3)一般做操作系统移植的时候,这里不会由我们自己来做,一般原厂提供的基础移植部分就已经包含了,所以这里我也从来没有研究过。
3、看门狗定时器
(1)看门狗定时器本质上也是一个定时器,和上面2个没有任何本质区别。
(2)看门狗定时器可以设置在时间到了的时候产生中断,也可以选择发出复位信号复位CPU。
(3)看门狗定时器在实践中应用很多,尤其是工业领域(环境复杂、干扰多)机器容易出问题,而且出问题后后果很严重,此时一般都会用看门狗来进行系统复位。
4、实时时钟RTC(real time clock)
(1)区分时间段和时间点。时间段是相对的,两个时间点相减就会得到一个时间段;而时间点是绝对的,是绝无仅有的一个时间点。
(2)定时器关注的是时间段(而不是时间点),定时器计时从开启定时器的那一刻开始,到定的时间段结束为止产生中断;RTC中工作用的是时间点(xx年x月x日x时x分x秒星期x)得到一个具体的时间。
(3)RTC和定时器的区别,就相当于是钟表和闹钟的区别。
2、S5PV210的PWM定时器
1、为什么叫PWM定时器
(1)叫定时器说明它本质上的原理是定时器
(2)叫PWM定时器,是因为这个定时器天然是用来产生PWM波形的。
2、PWM定时器介绍
(1)S5PV210有5个PWM定时器。其中0、1、2、3各自对应一个外部GPIO,可以通过这些对应的GPIO产生PWM波形信号并输出;timer4没有对应的外部GPIO(因此不是为了生成PWM波形而是为了产生内部定时器中断而生的)
(2)S5PV210的5个PWM定时器的时钟源为PCLK_PSYS,timer0和timer1共同使用一个预分频器、timer2、3、4共同使用一个预分频器;每个timer有一个专用的独立的分频器;预分频器和分频器构成了2级分频系统,将PCLK_PSYS两级分频后生成的时钟供给timer模块作为时钟周期。
(3)数据手册的第七章timer的PULSE WIDTH MODULATION TIMER开头简介中做了介绍。
3、S5PV210的PWM定时器框图简介
(1)关键点:时钟源、预分频器、分频器、TCMPB&TCNTB(TCNTB这个就是前面说过的将一个总计数放到这个寄存器中,每个一个时钟周期就减一直到减到零为止)、dead zone(死区生成器)
4、预分频器与分频器
(1)两级分频是串联(级联)的,所以两级分频的分频数是相乘的。
(2)两级分频的分频系数分别在TCFG0(预分频器)和TCFG1(分频器)两个寄存器中设置。
(3)预分频器有2个,prescaler0为timer0&timer1共用;prescaler1为timer2、3、4共用;两个prescaler都是8个bit位,因此prescaler value范围为0~255;所以预分频器的分频值范围为1~256(注意实际分频值为prescaler value + 1)。
(4)分频器实质上是一个MUX开关,多选一开关决定了走哪个分频系数路线。可以选择的有1/1,1/2,1/4,1/8,1/16等。
(5)计算一下,两级分频下来,分频最小为1/1(也可能是1/2),最大分频为1/256×16(1/4096)。
(6)在PCLK_PSYS为66MHz的情况下(默认时钟设置就是66MHz的),此时两级分频后的时钟周期范围为0.03us到62.061us;再结合TCNTB寄存器的值的设置(范围为1~2的32次方),可知能定出来的时间最长为266548.27s(折合74小时多,远远够用了)。
5、TCNT&TCMP、TCNTB&TCMPB、TCNTO
(1)TCNT和TCNTB是相对应的,TCNTB是有地址的寄存器,供程序员操作;TCNT在内部和TCNTB相对应,它没有寄存器地址,程序员不能编程访问这个寄存器。
(2)TCNT寄存器功能就是用来减1的,它是内部的不能读写;我们向TCNT中写要通过TCNTB往进写;读取TCNT寄存器中的值要通过读取相对应的TCNTO寄存器。
(3)工作流程就是:我们事先算好TCNT寄存器中开始减的那个数(譬如300),然后将之写入TCNTB寄存器中,在启动timer前,将TCNTB中的值刷到TCNT寄存器中(有一位寄存器专门用来操作刷数据过去的),刷过去后就可以启动定时器开始计时;在计时过程中如果想知道TCNT寄存器中的值减到多少了,可以读取相应的TCNTO寄存器来得知。
(4)定时功能只需要TCNT、TCNTB两个即可;TCNTO寄存器用来做一些捕获计时;TCMPB用来生成PWM波形。
6、自动重载和双缓冲(auto-reload and double buffering)
(1)定时器工作的时候,一次定时算一个工作循环。定时器默认是单个循环工作的,也就是说定时一次,计时一次,到期中断一次就完了。下次如果还要再定时中断,需要另外设置。
(2)但是现实中用定时器来做的时候往往是循环的,最简单最笨的方法就是写代码反复重置定时器寄存器的值(在每次中断处理的isr中再次给TCNTB中赋值,再次刷到TCNT中再次启动定时器),早期的单片机定时器就是这样的;但是现在的高级SoC中的定时器已经默认内置了这种循环定时工作模式,就叫自动装载(auto-reload)机制。
(3)自动装载机制就是当定时器初始化好开始计时后再不用管了,他一个周期到了后会自己从TCNTB中再次装载值到TCNT中,再次启动定时器开始下个循环。
7、什么是PWM?
(1)PWM(pulse wide modulation 脉宽调制)
(2)PWM波形是一个周期性波形,周期为T,在每个周期内波形是完全相同的。每个周期内由一个高电平和一个低电平组成。
(3)PWM波形有2个重要参数:一个是周期T,另一个是占空比duty(占空比就是一个周期内高电平的时间除以周期时间的商)。
(4)对于一个PWM波形,知道了周期T和占空比duty,就可以算出这个波形的所有细节。譬如高电平时间为Tduty,低电平时间为T(1-duty)。
(5)PWM波形有很多用处,譬如通信上用PWM来进行脉宽调制对基波进行载波调制;在发光二极管LED照明领域可以用PWM波形来调制电流进行调光;用来驱动蜂鸣器等设备。
8、PWM波形的生成原理
(1)PWM波形其实就是用时间来控制电平高低,所以用定时器来实现PWM波形是天经地义的。
(2)早期的简单单片机里(譬如51单片机)是没有专用的PWM定时器的,那时候我们需要自己结合GPIO和定时器模块来手工生产PWM波形(流程是这样:先将GPIO引脚电平拉高、同时启动定时器定Tduty时间,时间到了在isr中将电平拉低,然后定时T(1-duty)后再次启动定时器,然后时间到了后在isr中将电平拉高,然后再定时T*duty时间再次启动定时器····如此循环即可得到周期为T,占空比为duty的PWM波形)。
(3)**后来因为定时器经常和PWM产生纠结一起,所以设计SoC的时候就直接把定时器和一个GPIO引脚内部绑定起来了,然后在定时器内部给我们设置了PWM产生的机制,可以更方便的利用定时器产生PWM波形。**此时我们利用PWM定时器来产生PWM波形再不用中断了。绑定了之后坏处就是GPIO引脚是固定的、死板的、不能随便换的;好处是不用进入中断isr中,直接可以生成PWM。
(4)在S5PV210中,PWM波形产生有2个寄存器很关键,一个是TCNTB、一个是TCMPB。其中,TCNTB决定了PWM波形的周期,TCMPB决定了PWM波形的占空比。
(5)最终生成的PWM波形的周期是:TCNTB×时钟周期(PCLK_PSYS经过两极分频后得到的时钟周期)。注意这个周期是PWM中高电平+低电平的总时间,不是其中之一。
(6)最终生成的PWM波形的占空比是:TCMPB/TCNTB
10、输出电平翻转器
(1)PWM定时器可以规定:当TCNT》TCMPB时为高电平,当TCNT《TCMPB时为低电平。也可以规定:当TCNT》TCMPB时为低电平,当TCNT《TCMPB时为高电平。在这两种规定下,计算时TCMP寄存器的值会变化。
(2)基于上面讲的,当duty从30%变到70%时,我们TCMPB寄存器中的值就要改(譬如TCNTB中是300时,TCMPB就要从210变化到90)。这样的改变可以满足需要,但是计算有点麻烦。于是乎210的PWM定时器帮我们提供了一个友好的工具叫做电平翻转器。
(3)电平翻转器在电路上的实质就是一个电平取反的部件,在编程上反映为一个寄存器位。写0就关闭输出电平反转,写1就开启输出电平反转。开启后和开启前输出电平刚好高低反转。(输出电平一反转30%的duty就变成70%了)
(4)实战中到底是TCNT和TCMPB谁大谁小时高电平还是低电平,一般不用理论分析,只要写个代码然后用示波器实际看一下出来的波形就知道了;如果反了就直接开启电平翻转器即可。
11、死区生成器
(1)PWM有一个应用就是用在功率电路中用来对交流电压进行整流。整流时2路整流分别在正电平和负电平时导通工作,不能同时导通(同时导通会直接短路,瞬间的同时导通都会导致电路烧毁)。大功率的开关电源、逆变器等设备广泛使用了整流技术。特别是逆变器,用SoC的GPIO输出的PWM波形来分别驱动2路整流的IGBT。
(2)PWM波形用来做整理时要求不能同时高或低,因为会短路。但是实际电路是不理想的,不可能同时上升/下降沿,所以比较安全的做法是留死区。
(3)死区这东西离不了也多不了。死区少了容易短路,死区多了控制精度低了不利于产品性能的提升。
(4)S5PV210给大家提供了自带的死区生成器,只要开启死区生成器,生产出来的PWM波形就自带了死区控制功能,用户不用再自己去操心死区问题。
(5)大部分人工作是用不到这个的,直接关掉死区生成器即可。
三、蜂鸣器和PWM定时器编程实践
1、蜂鸣器的工作原理
(1)蜂鸣器里面有2个金属片,离的很紧但没挨着;没电的时候两个片在弹簧本身张力作用下分开彼此平行;有电的时候两边分别充电,在异性电荷的吸力作用下两个片挨着;
(2)我们只要以快速的频率给蜂鸣器的正负极:供电、断电。进行这样的循环,蜂鸣器的两个弹簧片就会挨着分开挨着分开···形成敲击,发出声音。
(3)因为人的耳朵能听见的声音频率有限制(20Hz-20000Hz),我们做实验时一般给个2KHz的频率。
(4)频率高低会影响声音的音频,一般是音频越低声音听起来越低沉、音频越高听起来越尖锐。
(5)根据以上的分析,可以看出,只要用PWM波形的电压信号来驱动蜂鸣器,把PWM波形的周期T设置为要发出的声音信号的1/频率即可;PWM的占空比只要确保能驱动蜂鸣器即可(驱动能力问题,一般引脚驱动能力都不够,所以蜂鸣器会额外用三极管来放大电流来供电)。
2、原理图和硬件信息及PWM定时器相关的寄存器
1、原理图和硬件信息
(1)查阅原理图可知,开发板底板上的蜂鸣器通过GPD0_2(XpwmTOUT2)引脚连接在SoC上。
(2)GPD0_2引脚通过限流电阻接在三极管基极上,引脚有电蜂鸣器就会有电(三极管导通);引脚没电蜂鸣器就会没电(三极管关闭)。这些都是硬件问题,软件工程师不用管,软件工程师只要写程序控制GPD0_2引脚的电平产生PWM波形即可。
(3)GPD0CON(0xE02000A0),要把bit8~bit11设置为0b0010(功能选择为TOUT_2,就是把这个引脚设置为PWM输出功能)
(4)从GPD0_2引脚可以反推出使用的是timer2这个PWM定时器。
2、PWM定时器的主要寄存器详解
(1)相关的寄存器有TCFG0、TCFG1、TCON、TCNTB2、TCMPB2、TCNTO2
TCFG0:预分频器有关的寄存器
TCFG1:分频器有关的寄存器
TCON:对于一些必要初始化的设置
TCNTB2:设置PWM波形的周期
TCMPB2:用来设置占空比
TCNTB2、TCMPB2的计算:
我们的时钟频率是500KHz,对应的时钟周期是2us。也就是说每隔2us 计一次数。如果要定的时间是x,则TCNTB中应该写入
rTCNTB2 = 250; // 0.5ms/2us = 500us/2us = 250
rTCMPB2 = 125; // duty = 50%
3、代码实践演示
1.添加pwm.c
#define GPD0CON (0xE02000A0)
#define TCFG0 (0xE2500000)
#define TCFG1 (0xE2500004)
#define CON (0xE2500008)
#define TCNTB2 (0xE2500024)
#define TCMPB2 (0xE2500028)
#define rGPD0CON (*(volatile unsigned int *)GPD0CON)
#define rTCFG0 (*(volatile unsigned int *)TCFG0)
#define rTCFG1 (*(volatile unsigned int *)TCFG1)
#define rCON (*(volatile unsigned int *)CON)
#define rTCNTB2 (*(volatile unsigned int *)TCNTB2)
#define rTCMPB2 (*(volatile unsigned int *)TCMPB2)
// 初始化PWM timer2,使其输出PWM波形:频率是2KHz、duty为50%
void timer2_pwm_init(void)
{
// 设置GPD0_2引脚,将其配置为XpwmTOUT_2
rGPD0CON &= ~(0xf《《8);
rGPD0CON |= (2《《8);
// 设置PWM定时器的一干寄存器,使其工作
rTCFG0 &= ~(0xff《《8);
rTCFG0 |= (65《《8); // prescaler1 = 65, 预分频后频率为1MHz
rTCFG1 &= ~(0x0f《《8);
rTCFG1 |= (1《《8); // MUX2设置为1/2,分频后时钟周期为500KHz
// 时钟设置好,我们的时钟频率是500KHz,对应的时钟周期是2us。也就是说每隔2us
// 计一次数。如果要定的时间是x,则TCNTB中应该写入x/2us
rCON |= (1《《15); // 使能auto-reload,反复定时才能发出PWM波形
//rTCNTB2 = 250; // 0.5ms/2us = 500us/2us = 250,
//rTCMPB2 = 125; // duty = 50%
rTCNTB2 = 50;
rTCMPB2 = 25;
// 第一次需要手工将TCNTB中的值刷新到TCNT中去,以后就可以auto-reload了
rCON |= (1《《13); // 打开自动刷新功能
rCON &= ~(1《《13); // 关闭自动刷新功能
rCON |= (1《《12); // 开timer2定时器。要先把其他都设置好才能开定时器
}
2.main.c
#include “stdio.h”
void uart_init(void);
void timer2_pwm_init(void);
int main(void)
{
uart_init();
timer2_pwm_init();
while(1)
{
putc(‘a’);
delay();
}
return 0;
}
注意:PWM定时器来产生PWM波形时是不需要中断干预的。
四、看门狗定时器的介绍及编程实践
1、看门狗定时器
1、什么是看门狗、有什么用
(1)看门狗定时器和普通的定时器并无本质区别。定时器可以设定一个时间,在这个时间完成之前定时器不断计时,时间到的时候定时器会复位CPU(重启系统)。
(2)系统正常工作的时候当然不希望被重启,但是系统受到干扰、极端环境等可能会产生异常工作或者不工作,这种状态可能会造成不良影响(至少是不工作),此时解决方案就是重启系统。
(3)普通设备重启不是问题,但是有些设备人工重启存在困难。这时候我们希望系统能够自己检验自己是否已经跑飞,并且在意识到自己跑飞的时候,可以很快的(几个ms或者更短)自我重启。这个功能就要靠看门狗定时器来实现。
(4)典型应用的情景是:我们在应用程序中打开看门狗设备,初始化好给它一个时间,然后应用程序使用一个线程来喂狗,这个线程的执行时间安全短于看门狗的复位时间。当系统(或者应用程序)异常后,喂狗线程自然就不工作了,然后到时候看门狗就会复位。
(5)补充:实战中有时候为了绝对的可靠,我们并不会用SoC中自带的看门狗,而是使用专门的外置的看门狗芯片来实现看门狗。
2、S5PV210看门狗定时器的结构框图
(1)PCLK_PSYS经过两级分频后生成WDT(watchdog timer)的时钟周期,然后把要定的时间写到WTDAT寄存器中,刷到WTCNT寄存器中去减1,减到0时(定时时间到)产生复位信号或中断信号。
(2)典型应用中是配置为产生复位信号,我们应该在WTCNT寄存器减到0之前给WTDAT寄存器中重新写值以喂狗。
3、看门狗定时器的主要寄存器
WTCON寄存器
WTDAT寄存器
WTCNT 寄存器
WTCLRINT寄存器
2、看门狗定时器的编程实践
1、产生中断信号
wdt.c代码如下:
#define WTCON (0xE2700000)
#define WTDAT (0xE2700004)
#define WTCNT (0xE2700008)
#define WTCLRINT (0xE270000C)
#define rWTCON (*(volatile unsigned int *)WTCON)
#define rWTDAT (*(volatile unsigned int *)WTDAT)
#define rWTCNT (*(volatile unsigned int *)WTCNT)
#define rWTCLRINT (*(volatile unsigned int *)WTCLRINT)
// 初始化WDT使之可以产生中断
void wdt_init_interrupt(void)
{
// 第一步,设置好预分频器和分频器,得到时钟周期是128us
rWTCON &= ~(0xff《《8);
rWTCON |= (65《《8); // 1MHz
rWTCON &= ~(3《《3);
rWTCON |= (3《《3); // 1/128 MHz, T = 128us
// 第二步,设置中断和复位信号的使能或禁止
rWTCON |= (1《《2); // enable wdt interrupt
rWTCON &= ~(1《《0); // disable wdt reset
// 第三步,设置定时时间
// WDT定时计数个数,最终定时时间为这里的值×时钟周期
//rWTDAT = 10000; // 定时1.28s
//rWTCNT = 10000; // 定时1.28s
// 其实WTDAT中的值不会自动刷到WTCNT中去,如果不显式设置WTCON中的值,它的值就是
// 默认值(数据手册中规定的默认值),然后以这个默认值开始计数,所以这个时间比较久。如果我们自己显式的
// 设置了WTCNT和WTDAT一样的值,则第一次的定时值就和后面的一样了。
rWTDAT = 1000; // 定时0.128s
//rWTCNT = 1000; // 定时0.128s
// 第四步,先把所有寄存器都设置好之后,再去开看门狗
rWTCON |= (1《《5); // enable wdt
}
// wdt的中断处理程序
void isr_wdt(void)
{
static int i = 0;
// 看门狗定时器时间到了时候应该做的有意义的事情
printf(“wdt interrupt, i = %d.。.”, i++);
// 清中断
intc_clearvectaddr();
rWTCLRINT = 1;
}
main.c代码
#include “stdio.h”
#include “int.h”
#include “main.h”
void uart_init(void);
void delay(int i)
{
volatile int j = 10000;
while (i--)
while(j--);
}
int main(void)
{
uart_init();
//key_init();
wdt_init_interrupt(); //wdt中断处理的初始化程序
// 如果程序中要使用中断,就要调用中断初始化来初步初始化中断控制器
system_init_exception();
printf(“-------------wdt interrupt test--------------”);
// 绑定isr到中断控制器硬件
intc_setvectaddr(NUM_WDT, isr_wdt); //绑定中断处理程序对应看门狗对应的中断号
// 使能中断
intc_enable(NUM_WDT);
while (1);
return 0;
}
2、产生复位信号
wdt.c代码
#define WTCON (0xE2700000)
#define WTDAT (0xE2700004)
#define WTCNT (0xE2700008)
#define WTCLRINT (0xE270000C)
#define rWTCON (*(volatile unsigned int *)WTCON)
#define rWTDAT (*(volatile unsigned int *)WTDAT)
#define rWTCNT (*(volatile unsigned int *)WTCNT)
#define rWTCLRINT (*(volatile unsigned int *)WTCLRINT)
// 初始化WDT使之可以复位
void wdt_init_reset(void)
{
// 第一步,设置好预分频器和分频器,得到时钟周期是128us
rWTCON &= ~(0xff《《8);
rWTCON |= (65《《8); // 1MHz
rWTCON &= ~(3《《3);
rWTCON |= (3《《3); // 1/128 MHz, T = 128us
// 第二步,设置中断和复位信号的使能或禁止
rWTCON &= ~(1《《2); // disable wdt interrupt
rWTCON |= (1《《0); // enable wdt reset
// 第三步,设置定时时间
// WDT定时计数个数,最终定时时间为这里的值×时钟周期
rWTDAT = 10000; // 定时1.28s
rWTCNT = 10000; // 定时1.28s
// 其实WTDAT中的值不会自动刷到WTCNT中去,如果不显式设置WTCON中的值,它的值就是
// 默认值,然后以这个默认值开始计数,所以这个时间比较久。如果我们自己显式的
// 设置了WTCNT和WTDAT一样的值,则第一次的定时值就和后面的一样了。
//rWTDAT = 1000; // 定时0.128s
//rWTCNT = 1000; // 定时0.128s
// 第四步,先把所有寄存器都设置好之后,再去开看门狗
rWTCON |= (1《《5); // enable wdt
}
main.c代码
#include “stdio.h”
#include “int.h”
#include “main.h”
void uart_init(void);
void delay(int i)
{
volatile int j = 10000;
while (i--)
while(j--);
}
int main(void)
{
static int i = 0; //我们在这里定义一个变量
uart_init();
//key_init();
wdt_init_reset();
printf(“---wdt interrupt test---, i = %d.”, i++); //然后再打印里面对变量i进行累加,
//实验证明这里的累加并不起作用,不像中断处理程序中的i++会累加,这里就要知道中断与复位的区别了,中断还是哪个程序只是被打断了被打断之后会回到
//原来的程序中,而复位表明每一次程序都是从头开始。
while (1);
return 0;
}
一、基础知识概念与原理
1、什么是定时器(timer)
1、定时器是SoC中常见外设
(1)定时器与计数器。计数器是用来计数的(每隔一个固定时间会计一个数);因为计数器的计数时间周期是固定的,因此到了一定时间只要用计数值×计数时间周期,就能得到一个时间段,这个时间段就是我们定的时间(这就是定时器了)。
(2)定时器/计数器作为SoC的外设,主要用来实现定时执行代码的功能。 定时器相对于SoC来说,就好象闹钟相对于人来说意义一样。
2、定时器有什么用
(1)定时器可以让SoC在执行主程序的同时,可以(通过定时器)具有计时功能,到了一定时间(计时结束)后,定时器会产生中断提醒CPU,CPU会去处理中断并执行定时器中断的ISR。从而去执行预先设定好的事件。
(2)定时器就好象是CPU的一个秘书一样,这个秘书专门管帮CPU来计时,并到时间后提醒CPU要做某件事情。所以CPU有了定时器之后,只需要预先把自己(xx)时间之后必须要做的事情绑定到定时器中断ISR即可,到了时间之后定时器就会以中断的方式提醒CPU来处理这个事情。
3、定时器的原理
(1)定时器计时其实是通过计数来实现的。定时器内部有一个计数器,这个计数器根据一个时钟(这个时钟源来自于ARM的APB总线,然后经过时钟模块内部的分频器来分频得到)来工作。每隔一个时钟周期,计数器就计数一次,定时器的时间就是计数器计数值×时钟周期。
(2)定时器内部有1个寄存器TCNT,计时开始时我们会把一个总的计数值(譬如说300)放入TCNT寄存器中,然后每隔一个时钟周期(假设为1ms)TCNT中的值会自动减1(硬件自动完成,不需要CPU软件去干预),直到TCNT中减为0的时候,TCNT就会触发定时器中断。
(3)定时时间是由2个东西共同决定的:一个是TCNT中的计数值,一个是时钟周期。譬如上例中,定时周期就为300×1ms = 300ms。
这样我们就可以通过修改TCNT中的值和时钟周期来设定我们需要的时间。
4、定时器和看门狗、RTC、蜂鸣器的关系
(1)这几个东西都是和时间有关的部件。
(2)看门狗其实就是一个定时器,只不过定时时间到了之后不只是中断,还可以复位CPU。
(3)RTC是实时时钟,它和定时器的差别就好象闹钟(定时器)和钟表(RTC)的差别一样。
(4)蜂鸣器是一个发声设备,在ARM里面蜂鸣器是用定时器模块来驱动的。
二、S5PV210中的定时器及介绍
1、S5PV210中的定时器
在S5PV210内部,一共有4类定时器件。这4类定时器件的功能、特征是不同的。
1、PWM定时器
(1)这种是最常用的,平时所说的定时器一般指的是这个。像简单单片机(譬如51单片机)中的定时器也是这类。
(2)为什么叫PWM定时器,因为一般SoC中产生PWM信号都是靠这个定时器模块的。(最典型的一种用法就是用它来生成PWM信号)
2、系统定时器
(1)系统(指的是操作系统)定时器,系统定时器也是用来产生固定时间间隔(TCNT×时钟周期)信号的,称为systick,这个systick用来给操作系统提供tick信号。
(2)产生systick作为操作系统的时间片(time slice)的。
(3)一般做操作系统移植的时候,这里不会由我们自己来做,一般原厂提供的基础移植部分就已经包含了,所以这里我也从来没有研究过。
3、看门狗定时器
(1)看门狗定时器本质上也是一个定时器,和上面2个没有任何本质区别。
(2)看门狗定时器可以设置在时间到了的时候产生中断,也可以选择发出复位信号复位CPU。
(3)看门狗定时器在实践中应用很多,尤其是工业领域(环境复杂、干扰多)机器容易出问题,而且出问题后后果很严重,此时一般都会用看门狗来进行系统复位。
4、实时时钟RTC(real time clock)
(1)区分时间段和时间点。时间段是相对的,两个时间点相减就会得到一个时间段;而时间点是绝对的,是绝无仅有的一个时间点。
(2)定时器关注的是时间段(而不是时间点),定时器计时从开启定时器的那一刻开始,到定的时间段结束为止产生中断;RTC中工作用的是时间点(xx年x月x日x时x分x秒星期x)得到一个具体的时间。
(3)RTC和定时器的区别,就相当于是钟表和闹钟的区别。
2、S5PV210的PWM定时器
1、为什么叫PWM定时器
(1)叫定时器说明它本质上的原理是定时器
(2)叫PWM定时器,是因为这个定时器天然是用来产生PWM波形的。
2、PWM定时器介绍
(1)S5PV210有5个PWM定时器。其中0、1、2、3各自对应一个外部GPIO,可以通过这些对应的GPIO产生PWM波形信号并输出;timer4没有对应的外部GPIO(因此不是为了生成PWM波形而是为了产生内部定时器中断而生的)
(2)S5PV210的5个PWM定时器的时钟源为PCLK_PSYS,timer0和timer1共同使用一个预分频器、timer2、3、4共同使用一个预分频器;每个timer有一个专用的独立的分频器;预分频器和分频器构成了2级分频系统,将PCLK_PSYS两级分频后生成的时钟供给timer模块作为时钟周期。
(3)数据手册的第七章timer的PULSE WIDTH MODULATION TIMER开头简介中做了介绍。
3、S5PV210的PWM定时器框图简介
(1)关键点:时钟源、预分频器、分频器、TCMPB&TCNTB(TCNTB这个就是前面说过的将一个总计数放到这个寄存器中,每个一个时钟周期就减一直到减到零为止)、dead zone(死区生成器)
4、预分频器与分频器
(1)两级分频是串联(级联)的,所以两级分频的分频数是相乘的。
(2)两级分频的分频系数分别在TCFG0(预分频器)和TCFG1(分频器)两个寄存器中设置。
(3)预分频器有2个,prescaler0为timer0&timer1共用;prescaler1为timer2、3、4共用;两个prescaler都是8个bit位,因此prescaler value范围为0~255;所以预分频器的分频值范围为1~256(注意实际分频值为prescaler value + 1)。
(4)分频器实质上是一个MUX开关,多选一开关决定了走哪个分频系数路线。可以选择的有1/1,1/2,1/4,1/8,1/16等。
(5)计算一下,两级分频下来,分频最小为1/1(也可能是1/2),最大分频为1/256×16(1/4096)。
(6)在PCLK_PSYS为66MHz的情况下(默认时钟设置就是66MHz的),此时两级分频后的时钟周期范围为0.03us到62.061us;再结合TCNTB寄存器的值的设置(范围为1~2的32次方),可知能定出来的时间最长为266548.27s(折合74小时多,远远够用了)。
5、TCNT&TCMP、TCNTB&TCMPB、TCNTO
(1)TCNT和TCNTB是相对应的,TCNTB是有地址的寄存器,供程序员操作;TCNT在内部和TCNTB相对应,它没有寄存器地址,程序员不能编程访问这个寄存器。
(2)TCNT寄存器功能就是用来减1的,它是内部的不能读写;我们向TCNT中写要通过TCNTB往进写;读取TCNT寄存器中的值要通过读取相对应的TCNTO寄存器。
(3)工作流程就是:我们事先算好TCNT寄存器中开始减的那个数(譬如300),然后将之写入TCNTB寄存器中,在启动timer前,将TCNTB中的值刷到TCNT寄存器中(有一位寄存器专门用来操作刷数据过去的),刷过去后就可以启动定时器开始计时;在计时过程中如果想知道TCNT寄存器中的值减到多少了,可以读取相应的TCNTO寄存器来得知。
(4)定时功能只需要TCNT、TCNTB两个即可;TCNTO寄存器用来做一些捕获计时;TCMPB用来生成PWM波形。
6、自动重载和双缓冲(auto-reload and double buffering)
(1)定时器工作的时候,一次定时算一个工作循环。定时器默认是单个循环工作的,也就是说定时一次,计时一次,到期中断一次就完了。下次如果还要再定时中断,需要另外设置。
(2)但是现实中用定时器来做的时候往往是循环的,最简单最笨的方法就是写代码反复重置定时器寄存器的值(在每次中断处理的isr中再次给TCNTB中赋值,再次刷到TCNT中再次启动定时器),早期的单片机定时器就是这样的;但是现在的高级SoC中的定时器已经默认内置了这种循环定时工作模式,就叫自动装载(auto-reload)机制。
(3)自动装载机制就是当定时器初始化好开始计时后再不用管了,他一个周期到了后会自己从TCNTB中再次装载值到TCNT中,再次启动定时器开始下个循环。
7、什么是PWM?
(1)PWM(pulse wide modulation 脉宽调制)
(2)PWM波形是一个周期性波形,周期为T,在每个周期内波形是完全相同的。每个周期内由一个高电平和一个低电平组成。
(3)PWM波形有2个重要参数:一个是周期T,另一个是占空比duty(占空比就是一个周期内高电平的时间除以周期时间的商)。
(4)对于一个PWM波形,知道了周期T和占空比duty,就可以算出这个波形的所有细节。譬如高电平时间为Tduty,低电平时间为T(1-duty)。
(5)PWM波形有很多用处,譬如通信上用PWM来进行脉宽调制对基波进行载波调制;在发光二极管LED照明领域可以用PWM波形来调制电流进行调光;用来驱动蜂鸣器等设备。
8、PWM波形的生成原理
(1)PWM波形其实就是用时间来控制电平高低,所以用定时器来实现PWM波形是天经地义的。
(2)早期的简单单片机里(譬如51单片机)是没有专用的PWM定时器的,那时候我们需要自己结合GPIO和定时器模块来手工生产PWM波形(流程是这样:先将GPIO引脚电平拉高、同时启动定时器定Tduty时间,时间到了在isr中将电平拉低,然后定时T(1-duty)后再次启动定时器,然后时间到了后在isr中将电平拉高,然后再定时T*duty时间再次启动定时器····如此循环即可得到周期为T,占空比为duty的PWM波形)。
(3)**后来因为定时器经常和PWM产生纠结一起,所以设计SoC的时候就直接把定时器和一个GPIO引脚内部绑定起来了,然后在定时器内部给我们设置了PWM产生的机制,可以更方便的利用定时器产生PWM波形。**此时我们利用PWM定时器来产生PWM波形再不用中断了。绑定了之后坏处就是GPIO引脚是固定的、死板的、不能随便换的;好处是不用进入中断isr中,直接可以生成PWM。
(4)在S5PV210中,PWM波形产生有2个寄存器很关键,一个是TCNTB、一个是TCMPB。其中,TCNTB决定了PWM波形的周期,TCMPB决定了PWM波形的占空比。
(5)最终生成的PWM波形的周期是:TCNTB×时钟周期(PCLK_PSYS经过两极分频后得到的时钟周期)。注意这个周期是PWM中高电平+低电平的总时间,不是其中之一。
(6)最终生成的PWM波形的占空比是:TCMPB/TCNTB
10、输出电平翻转器
(1)PWM定时器可以规定:当TCNT》TCMPB时为高电平,当TCNT《TCMPB时为低电平。也可以规定:当TCNT》TCMPB时为低电平,当TCNT《TCMPB时为高电平。在这两种规定下,计算时TCMP寄存器的值会变化。
(2)基于上面讲的,当duty从30%变到70%时,我们TCMPB寄存器中的值就要改(譬如TCNTB中是300时,TCMPB就要从210变化到90)。这样的改变可以满足需要,但是计算有点麻烦。于是乎210的PWM定时器帮我们提供了一个友好的工具叫做电平翻转器。
(3)电平翻转器在电路上的实质就是一个电平取反的部件,在编程上反映为一个寄存器位。写0就关闭输出电平反转,写1就开启输出电平反转。开启后和开启前输出电平刚好高低反转。(输出电平一反转30%的duty就变成70%了)
(4)实战中到底是TCNT和TCMPB谁大谁小时高电平还是低电平,一般不用理论分析,只要写个代码然后用示波器实际看一下出来的波形就知道了;如果反了就直接开启电平翻转器即可。
11、死区生成器
(1)PWM有一个应用就是用在功率电路中用来对交流电压进行整流。整流时2路整流分别在正电平和负电平时导通工作,不能同时导通(同时导通会直接短路,瞬间的同时导通都会导致电路烧毁)。大功率的开关电源、逆变器等设备广泛使用了整流技术。特别是逆变器,用SoC的GPIO输出的PWM波形来分别驱动2路整流的IGBT。
(2)PWM波形用来做整理时要求不能同时高或低,因为会短路。但是实际电路是不理想的,不可能同时上升/下降沿,所以比较安全的做法是留死区。
(3)死区这东西离不了也多不了。死区少了容易短路,死区多了控制精度低了不利于产品性能的提升。
(4)S5PV210给大家提供了自带的死区生成器,只要开启死区生成器,生产出来的PWM波形就自带了死区控制功能,用户不用再自己去操心死区问题。
(5)大部分人工作是用不到这个的,直接关掉死区生成器即可。
三、蜂鸣器和PWM定时器编程实践
1、蜂鸣器的工作原理
(1)蜂鸣器里面有2个金属片,离的很紧但没挨着;没电的时候两个片在弹簧本身张力作用下分开彼此平行;有电的时候两边分别充电,在异性电荷的吸力作用下两个片挨着;
(2)我们只要以快速的频率给蜂鸣器的正负极:供电、断电。进行这样的循环,蜂鸣器的两个弹簧片就会挨着分开挨着分开···形成敲击,发出声音。
(3)因为人的耳朵能听见的声音频率有限制(20Hz-20000Hz),我们做实验时一般给个2KHz的频率。
(4)频率高低会影响声音的音频,一般是音频越低声音听起来越低沉、音频越高听起来越尖锐。
(5)根据以上的分析,可以看出,只要用PWM波形的电压信号来驱动蜂鸣器,把PWM波形的周期T设置为要发出的声音信号的1/频率即可;PWM的占空比只要确保能驱动蜂鸣器即可(驱动能力问题,一般引脚驱动能力都不够,所以蜂鸣器会额外用三极管来放大电流来供电)。
2、原理图和硬件信息及PWM定时器相关的寄存器
1、原理图和硬件信息
(1)查阅原理图可知,开发板底板上的蜂鸣器通过GPD0_2(XpwmTOUT2)引脚连接在SoC上。
(2)GPD0_2引脚通过限流电阻接在三极管基极上,引脚有电蜂鸣器就会有电(三极管导通);引脚没电蜂鸣器就会没电(三极管关闭)。这些都是硬件问题,软件工程师不用管,软件工程师只要写程序控制GPD0_2引脚的电平产生PWM波形即可。
(3)GPD0CON(0xE02000A0),要把bit8~bit11设置为0b0010(功能选择为TOUT_2,就是把这个引脚设置为PWM输出功能)
(4)从GPD0_2引脚可以反推出使用的是timer2这个PWM定时器。
2、PWM定时器的主要寄存器详解
(1)相关的寄存器有TCFG0、TCFG1、TCON、TCNTB2、TCMPB2、TCNTO2
TCFG0:预分频器有关的寄存器
TCFG1:分频器有关的寄存器
TCON:对于一些必要初始化的设置
TCNTB2:设置PWM波形的周期
TCMPB2:用来设置占空比
TCNTB2、TCMPB2的计算:
我们的时钟频率是500KHz,对应的时钟周期是2us。也就是说每隔2us 计一次数。如果要定的时间是x,则TCNTB中应该写入
rTCNTB2 = 250; // 0.5ms/2us = 500us/2us = 250
rTCMPB2 = 125; // duty = 50%
3、代码实践演示
1.添加pwm.c
#define GPD0CON (0xE02000A0)
#define TCFG0 (0xE2500000)
#define TCFG1 (0xE2500004)
#define CON (0xE2500008)
#define TCNTB2 (0xE2500024)
#define TCMPB2 (0xE2500028)
#define rGPD0CON (*(volatile unsigned int *)GPD0CON)
#define rTCFG0 (*(volatile unsigned int *)TCFG0)
#define rTCFG1 (*(volatile unsigned int *)TCFG1)
#define rCON (*(volatile unsigned int *)CON)
#define rTCNTB2 (*(volatile unsigned int *)TCNTB2)
#define rTCMPB2 (*(volatile unsigned int *)TCMPB2)
// 初始化PWM timer2,使其输出PWM波形:频率是2KHz、duty为50%
void timer2_pwm_init(void)
{
// 设置GPD0_2引脚,将其配置为XpwmTOUT_2
rGPD0CON &= ~(0xf《《8);
rGPD0CON |= (2《《8);
// 设置PWM定时器的一干寄存器,使其工作
rTCFG0 &= ~(0xff《《8);
rTCFG0 |= (65《《8); // prescaler1 = 65, 预分频后频率为1MHz
rTCFG1 &= ~(0x0f《《8);
rTCFG1 |= (1《《8); // MUX2设置为1/2,分频后时钟周期为500KHz
// 时钟设置好,我们的时钟频率是500KHz,对应的时钟周期是2us。也就是说每隔2us
// 计一次数。如果要定的时间是x,则TCNTB中应该写入x/2us
rCON |= (1《《15); // 使能auto-reload,反复定时才能发出PWM波形
//rTCNTB2 = 250; // 0.5ms/2us = 500us/2us = 250,
//rTCMPB2 = 125; // duty = 50%
rTCNTB2 = 50;
rTCMPB2 = 25;
// 第一次需要手工将TCNTB中的值刷新到TCNT中去,以后就可以auto-reload了
rCON |= (1《《13); // 打开自动刷新功能
rCON &= ~(1《《13); // 关闭自动刷新功能
rCON |= (1《《12); // 开timer2定时器。要先把其他都设置好才能开定时器
}
2.main.c
#include “stdio.h”
void uart_init(void);
void timer2_pwm_init(void);
int main(void)
{
uart_init();
timer2_pwm_init();
while(1)
{
putc(‘a’);
delay();
}
return 0;
}
注意:PWM定时器来产生PWM波形时是不需要中断干预的。
四、看门狗定时器的介绍及编程实践
1、看门狗定时器
1、什么是看门狗、有什么用
(1)看门狗定时器和普通的定时器并无本质区别。定时器可以设定一个时间,在这个时间完成之前定时器不断计时,时间到的时候定时器会复位CPU(重启系统)。
(2)系统正常工作的时候当然不希望被重启,但是系统受到干扰、极端环境等可能会产生异常工作或者不工作,这种状态可能会造成不良影响(至少是不工作),此时解决方案就是重启系统。
(3)普通设备重启不是问题,但是有些设备人工重启存在困难。这时候我们希望系统能够自己检验自己是否已经跑飞,并且在意识到自己跑飞的时候,可以很快的(几个ms或者更短)自我重启。这个功能就要靠看门狗定时器来实现。
(4)典型应用的情景是:我们在应用程序中打开看门狗设备,初始化好给它一个时间,然后应用程序使用一个线程来喂狗,这个线程的执行时间安全短于看门狗的复位时间。当系统(或者应用程序)异常后,喂狗线程自然就不工作了,然后到时候看门狗就会复位。
(5)补充:实战中有时候为了绝对的可靠,我们并不会用SoC中自带的看门狗,而是使用专门的外置的看门狗芯片来实现看门狗。
2、S5PV210看门狗定时器的结构框图
(1)PCLK_PSYS经过两级分频后生成WDT(watchdog timer)的时钟周期,然后把要定的时间写到WTDAT寄存器中,刷到WTCNT寄存器中去减1,减到0时(定时时间到)产生复位信号或中断信号。
(2)典型应用中是配置为产生复位信号,我们应该在WTCNT寄存器减到0之前给WTDAT寄存器中重新写值以喂狗。
3、看门狗定时器的主要寄存器
WTCON寄存器
WTDAT寄存器
WTCNT 寄存器
WTCLRINT寄存器
2、看门狗定时器的编程实践
1、产生中断信号
wdt.c代码如下:
#define WTCON (0xE2700000)
#define WTDAT (0xE2700004)
#define WTCNT (0xE2700008)
#define WTCLRINT (0xE270000C)
#define rWTCON (*(volatile unsigned int *)WTCON)
#define rWTDAT (*(volatile unsigned int *)WTDAT)
#define rWTCNT (*(volatile unsigned int *)WTCNT)
#define rWTCLRINT (*(volatile unsigned int *)WTCLRINT)
// 初始化WDT使之可以产生中断
void wdt_init_interrupt(void)
{
// 第一步,设置好预分频器和分频器,得到时钟周期是128us
rWTCON &= ~(0xff《《8);
rWTCON |= (65《《8); // 1MHz
rWTCON &= ~(3《《3);
rWTCON |= (3《《3); // 1/128 MHz, T = 128us
// 第二步,设置中断和复位信号的使能或禁止
rWTCON |= (1《《2); // enable wdt interrupt
rWTCON &= ~(1《《0); // disable wdt reset
// 第三步,设置定时时间
// WDT定时计数个数,最终定时时间为这里的值×时钟周期
//rWTDAT = 10000; // 定时1.28s
//rWTCNT = 10000; // 定时1.28s
// 其实WTDAT中的值不会自动刷到WTCNT中去,如果不显式设置WTCON中的值,它的值就是
// 默认值(数据手册中规定的默认值),然后以这个默认值开始计数,所以这个时间比较久。如果我们自己显式的
// 设置了WTCNT和WTDAT一样的值,则第一次的定时值就和后面的一样了。
rWTDAT = 1000; // 定时0.128s
//rWTCNT = 1000; // 定时0.128s
// 第四步,先把所有寄存器都设置好之后,再去开看门狗
rWTCON |= (1《《5); // enable wdt
}
// wdt的中断处理程序
void isr_wdt(void)
{
static int i = 0;
// 看门狗定时器时间到了时候应该做的有意义的事情
printf(“wdt interrupt, i = %d.。.”, i++);
// 清中断
intc_clearvectaddr();
rWTCLRINT = 1;
}
main.c代码
#include “stdio.h”
#include “int.h”
#include “main.h”
void uart_init(void);
void delay(int i)
{
volatile int j = 10000;
while (i--)
while(j--);
}
int main(void)
{
uart_init();
//key_init();
wdt_init_interrupt(); //wdt中断处理的初始化程序
// 如果程序中要使用中断,就要调用中断初始化来初步初始化中断控制器
system_init_exception();
printf(“-------------wdt interrupt test--------------”);
// 绑定isr到中断控制器硬件
intc_setvectaddr(NUM_WDT, isr_wdt); //绑定中断处理程序对应看门狗对应的中断号
// 使能中断
intc_enable(NUM_WDT);
while (1);
return 0;
}
2、产生复位信号
wdt.c代码
#define WTCON (0xE2700000)
#define WTDAT (0xE2700004)
#define WTCNT (0xE2700008)
#define WTCLRINT (0xE270000C)
#define rWTCON (*(volatile unsigned int *)WTCON)
#define rWTDAT (*(volatile unsigned int *)WTDAT)
#define rWTCNT (*(volatile unsigned int *)WTCNT)
#define rWTCLRINT (*(volatile unsigned int *)WTCLRINT)
// 初始化WDT使之可以复位
void wdt_init_reset(void)
{
// 第一步,设置好预分频器和分频器,得到时钟周期是128us
rWTCON &= ~(0xff《《8);
rWTCON |= (65《《8); // 1MHz
rWTCON &= ~(3《《3);
rWTCON |= (3《《3); // 1/128 MHz, T = 128us
// 第二步,设置中断和复位信号的使能或禁止
rWTCON &= ~(1《《2); // disable wdt interrupt
rWTCON |= (1《《0); // enable wdt reset
// 第三步,设置定时时间
// WDT定时计数个数,最终定时时间为这里的值×时钟周期
rWTDAT = 10000; // 定时1.28s
rWTCNT = 10000; // 定时1.28s
// 其实WTDAT中的值不会自动刷到WTCNT中去,如果不显式设置WTCON中的值,它的值就是
// 默认值,然后以这个默认值开始计数,所以这个时间比较久。如果我们自己显式的
// 设置了WTCNT和WTDAT一样的值,则第一次的定时值就和后面的一样了。
//rWTDAT = 1000; // 定时0.128s
//rWTCNT = 1000; // 定时0.128s
// 第四步,先把所有寄存器都设置好之后,再去开看门狗
rWTCON |= (1《《5); // enable wdt
}
main.c代码
#include “stdio.h”
#include “int.h”
#include “main.h”
void uart_init(void);
void delay(int i)
{
volatile int j = 10000;
while (i--)
while(j--);
}
int main(void)
{
static int i = 0; //我们在这里定义一个变量
uart_init();
//key_init();
wdt_init_reset();
printf(“---wdt interrupt test---, i = %d.”, i++); //然后再打印里面对变量i进行累加,
//实验证明这里的累加并不起作用,不像中断处理程序中的i++会累加,这里就要知道中断与复位的区别了,中断还是哪个程序只是被打断了被打断之后会回到
//原来的程序中,而复位表明每一次程序都是从头开始。
while (1);
return 0;
}
举报