1.什么是定时器
(1)定时器介绍
定时器是SoC中常见外设,计数器计数乘以固定的计数周期时间就相当于定时器的功能。定时器的作用相当于定个闹钟。计时完成后会产生中断,从而执行事先设定的事件。单核的CPU就要用定时器,因为它只能同时做一件事,不能一直盯着时间(定了时间后定时器就专门负责到时间提醒CPU来处理事情)。
(2)定时器原理
定时器内有计数器,计数器根据时钟来工作,时钟源来自于ARM的APB总线,经过时钟模块内部的分频器来分频得到。每隔一个时钟周期就计数一次。
定时器内部有一个寄存器TCNT,假设开始设置为300,时钟周期为1ms,每隔一个时 钟周期会硬件自动减一,直到减为0会触发中断。
(3)定时器和看门狗、RTC、蜂鸣器的关系
①看门狗就是一个定时器,只不过不只是中断,还可以复位CPU。
②RTC是实时时钟,它和定时器的差别就好像闹钟和钟表。
③蜂鸣器是用定时器模块来驱动的。
2.S5PV210中的定时器
(1)PWM定时器
即一般所说的定时器。一般SoC中产生PWM信号都是靠这个定时器模块,所以叫PWM定时器(一种典型用法)。
(2)系统定时器
也是用来产生固定时间间隔信号的,称为systick,它用来给操作系统提供tick信号。产生systick作为操作系统的时间片(time slice),和PWM的区别只是用途不同,分别用于外设和系统。一般用不到。
(3)看门狗定时器
该种应用的定时器常用在工业领域,环境复杂机器容易出问题,为了防止带来严重后果,所以用其来进行系统复位。
(4)实时时钟(real time clock)
定时器关注的是时间段,时间段结束即产生中断,而RTC关注的是时间点。
3.S5PV210的PWM定时器①
(1)PWM定时器天然是用来产生PWM波形的。
(2)S5PV210有5个PWM定时器,其中0/1/2/3各自对应一个外部GPIO,可以通过对应的GPIO产生PWM波形信号并输出;timer4没有对应的GPIO,目的是为了产生内部定时器的中断。
(3)定时器的时钟源是PCLK_PSYS,timer0和1共同使用一个预分频器,timer2、3、4共同使用一个预分频器;每个timer各自还有一个专用的分频器;预分频器和分频器共同构成2级分频系统,将PCLK_PSYS两级分频后的时钟作为各timer的时钟周期。
以下左到右分别为时钟源、预分频器、分频器、TCMPB&TCNTB、dedzone。
4.S5PV210的PWM定时器②
(1)两级分频器的分频系数分别在寄存器TCFG0和TCFG1中设置
prescaler value范围是1-255,所以预分频器的分频范围是2-256(实际分频值为value+1)
MUX分频器可选的有1/(1/2/4/8/16)。
PCLK_PSYS为66MHz,两级分频后的时钟周期范围为0.03us到62.061us;再结合TCNTB的值(范围为1-2^32),可知能定出来的时间最长为266548.27s。
(2)TCNT&TCMP、TCNT&TCNTB、TCNTO
TCNTB是有地址的寄存器,可写;TCNT没有寄存器,不能编程访问,它负责复制
TCNTB的值并每次减一;TCNTO提供读取功能,通过它可以随时读取TCNT中的值。定 时功能只需要TCNTB和TCNT,TCNTO寄存器用来做一些捕获计时,TCMP用来生成PWM 波形。
(3)自动重载和双缓冲(auto-reload and double buffering)
现实中定时器需要循环工作时,早期的单片机是通过写代码反复重置TCNTB寄存器的值来实现的,现在高级的SoC中已经内置了这种循环定时工作模式,叫自动装载机制。自动装载机制当一个周期到了后会自动将TCNTB的值装载到TCNT中进行循环。
5.S5PV210的PWM定时器③
(1)PWM(脉宽调制)
PWM波形周期为T,占空比duty。该波形可以用来调制电流进行调光,也可以驱动蜂鸣器等设备。
(2)生成原理
在早期单片机中通过中断用定时器来控制电平高低的维持时间。后来在SoC中直接把定时器和一个GPIO引脚绑定起来了,然后在定时器内部给我们设置了产生PWM的机制,可以方便利用定时器产生PWM波形,缺点是GPIO引脚是固定的,不能随便换,好处是不用中断就可以直接生成。
TCNTB决定了PWM的周期,TCMPB决定了占空比。PWM波形的周期为TCNTB*时钟周期。占空比=TCMPB/TCNTB。在TCNT变化的过程中(也可能是从0加)和TCMPB进行比较,当TCNT大/小时为高电平,TCNT小/大时为低电平。
当占空比duty从30%变为70%时,TCMPB的值就需要更改(设TCNTB为300),此时计算的话有点麻烦,210中PWM定时器提供了电平翻转器。电平翻转器实质是电平取反,在编程上对应一个寄存器位ie,写0关闭翻转,写1开启翻转。开启后30%就会编程70%。对应电路图如下。
(3)死区生成器
PWM有一个应用就是对交流电压进行整流,整流时2路PWM电平相反,都是在正电平时导通工作,不能同时导通(会短路烧毁)。由于实际电路不理想,为了避免产生冒险,可以留有死区。死区不宜太长。
S5PV210自带死区生成器,开启即可。
6.蜂鸣器和PWM定时器编程实践①
(1)蜂鸣器工作原理
蜂鸣器中有两个金属片,通电时异性相吸,给以快速的频率使其对周围的空气
发生扰动产生声音。频率高低影响声音的频率。
因此用PWM波形驱动时,把周期设为1/频率,占空比确保能驱动蜂鸣器即可(一般引脚驱动能力都不够,所以会用额外的三极管放大电流来供电)
(2)原理图
分别为底板的原理图和核心板的引脚的对应。
GPD0_2引脚通过限流电阻接到三极管的基极上,引脚有电蜂鸣器就会有电(三级管导通)。
根据寄存器的要求,把bit8-11设置为0010,即选择为TOUT2,实现PWM输出功能。
从GPD0_2引脚可以反推出使用的是timer2
(3)PWM定时器的主要寄存器
TCFG0、TCFG1、CON、TCNTB2、TCMPB2、TCNTO2
使用chapter7中的uart_c_printf程序,命名为1.buzzer_pwm1。
新建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)
7.蜂鸣器和PWM定时器编程实践②
继续添加如下初始化函数
//初始化PWM timer2,使其输出PWM波形:频率是2KHz
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,即TCNTB中应该写入x/2us
rCON |= (1<<15); //使能auto-reload,反复定时才能发出连续的PWM波形
rTCNTB2 = 250; //0.5ms/200us=500us/2us=250
rTCMPB2 = 125; //duty=50%
//第一次需要手工将TCNTB中的值刷新到TCNT中,然后就可以auto-reload了
rCON |= (1<<13); //打开自动刷新功能
rCON &= ~(1<<13); //关闭自动刷新功能
rCON |= (1<<12); //最后开timer2定时器,要先把其他都设置好才能开定时器
}
在Makefile中添加pwm.c,由于这里不需要printf,所以下面这个注释掉
objs := start.o led.o clock.o uart.o main.o pwm.o
#objs += lib/libc.a
然后把其中uart.bin中的uart名字改为pwm
pwm.bin: $(objs)
$(LD) -Tlink.lds -o pwm.elf $^
$(OBJCOPY) -O binary pwm.elf pwm.bin
$(OBJDUMP) -D pwm.elf > pwm_elf.dis
gcc mkv210_image.c -o mkx210
./mkx210 pwm.bin 210.bin
在main.c中声明
void timer2_pwm_init(void);
在函数体中调用函数
timer2_pwm_init();
现象:烧录时蜂鸣器发出响声
8.看门狗定时器
(1)看门狗介绍
本质也是定时器,定时器设置一个时间,在这个时间完成之前不断计时,时间到的时候定时器会复位CPU(重启)。在一些系统容易受到干扰、极端环境等情况下可能会产生异常(跑飞),此时解决办法就是重启。
通常在应用程序中打开看门狗设备,给它初始化一个时间,然后应用程序使用一个线程来喂狗,这个线程的执行时间应安全短于看门狗的复位时间,当系统(或应用程序)异常时,喂狗线程自然不工作了,此时就会进行复位。有时为了绝对的可靠,会使用外置的看门狗芯片而不用SoC内置看门狗。
(2)结构框图
PCLK_PSYS经过两级分频后生成WDT(watchdog timer)的时钟周期,把要定的试卷写到WTDAT中,刷到WTCNT中去减一,减到0时产生复位信号或者中断信号。典型应用中配置为产生复位信号,要在定时结束前给WTCNT中重新写值以喂狗。
(3)看门狗主要寄存器
WTCON、WTDAT、WTCNT、WTCLRINT
基于chapter8的6.key_interrupt_stdio4,命名2.wdt_interrupt。
新建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)
9.看门狗定时器的编程实践
看门狗定时器的两种结果:中断、复位。
(1)产生中断信号
打开main.c,修改如下,删除按键宏定义;保留一个绑定isr的函数,保留一个使能中断,把这两个中的中断号改成NUM_WDT,修改对应的isr_wdt;删除while中的心跳包,使其进行死循环即可;修改初始化看门狗名为wdt_init_interrupt;
#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(); //初始化看门狗中断
//如果程序中要中断,则要先初步初始化中断控制器
system_init_exception();
printf("-------------wdt interrupt test--------------");
//绑定isr到中断控制器硬件
intc_setvectaddr(NUM_WDT, isr_wdt);
//使能中断(这里使能的中断是中断控制器中使能中断)
intc_enable(NUM_WDT);
while (1);
return 0;
}
打开wdt.c,写中断处理程序isr_wdt,包括实际做事和清中断。WTCLRINT只要写一个数就可以实现清中断。
//wdt的中断处理程序
static int i = 0;
void isr_wdt(void)
{
//看门狗定时器时间到了时候应该做的有意义的事情
printf("wdt interrupt......, i = %d...", i++);
//清中断
intc_clearvectaddr(); //系统清中断
rWTCLRINT = 1; //看门狗清中断
}
写wdt中断初始化程序。
//初始化WDT使之可以产生中断
void wdt_init_interrupt(void)
{
//1.设置好预分频器和分频器,得到时钟周期128us
rWTCON &= ~(0xff<<8);
rWTCON |= (65<<8); // 66/(65+1)=1MHz
rWTCON &= ~(3<<3);
rWTCON |= (3<<3); // 1/128MHz, T=128us
//2.使能中断,禁止复位
rWTCON |= (1<<2);
rWTCON &= (1<<0);
//3.设置定时时间,定时=时钟周期*这里的值
rWTDAT = 10000; //定时1.28s
rWTCNT = 10000; //定时1.28s,如果不设置的话就会用默认值0x8000
//4.先把所有寄存器设置好之后再去开看门狗
rWTCON |= (1<<5);
}
在Makefile中把key.o换成wdt.o。
objs := start.o led.o clock.o uart.o main.o int.o wdt.o
在main.h中声明wdt.c的函数。
//wdt.c
void wdt_init_interrupt(void);
void isr_wdt(void);
实验现象
如果不设置WTCNT,则在打出第一个test信号后,会隔好几秒才会进行之后的打印。
(2)产生复位信号
基于2.wdt_interrupt,新建3.wdt_reset。
复位和上一个中断不同,如果复位会不停打印wdt reset test。
把wdt.c中的wdt_init_interrupt改为wdt_init_reset,在main.c中同样改一下。
wdt.c中第2步修改如下
//2.禁止中断,使能复位
rWTCON &= ~(1<<2);
rWTCON |= (1<<0);
删除isr_wdt。
实验现象:每隔1.28s打印一次
注:当main函数中设置i = 0,打印i++时,结果是不会递增打印的,因为每次都是复位为0。
10.实时时钟RTC(real time clock)
(1)介绍
实时时钟就是xx年x月x日x时x分x秒星期x。RTC是SoC的一个内部外设,有自己独立的晶振提供时钟源(32.768KHz),内部有一些寄存器用来记录时间。为了让系统关机时仍在运行,会给RTC提供一个电池供电。
(2)结构框图
时间寄存器7个;闹钟发生器
(3)闹钟发生器
到实际会产生RTC alarm interrupt,通知系统闹钟定时到了,注意这里定的是时间点,而之前定的都是时间段。
(4)主要寄存器
①INTP,中断挂起寄存器
②RTCCON,RTC控制寄存器
③RTCALM,ALMxxx闹钟功能有关的寄存器
④BCDxxx,时间寄存器
(5)BCD码
RTC中所有时间都是用BCD码编码的。BCD码就是用4位二进制数来表示1位十进制数中的0~9这10个数码,BCD码最常用于对很长的数字进行准确的计算。
假如十进制56要存到计算机中,则用十六进制数0x56(01010110)存入,取出0x56时则转为十进制56。这里就需要实现两个函数,分别是十进制转BCD和BCD转十进制。(实际上56≠0x56)
11.RTC编程实战
基于3.wdt_reset,新建4.rtc。
(1)设置时间与读取显示时间
在main.h中定义一个结构体,包含年到秒的信息。声明两个读写函数。
struct rtc_time
{
unsigned int year;
unsigned int month;
unsigned int date; //几号
unsigned int hour;
unsigned int minute;
unsigned int second;
unsigned int day; //星期几
};
void rtc_set_time(const struct rtc_time *p);
void rtc_get_time(struct rtc_time *p);
在rtc.c中定义RTC的寄存器的宏,并加上头文件
#include "main.h"
#define RTC_BASE (0xE2800000)
#define rINTP (*((volatile unsigned long *)(RTC_BASE + 0x30)))
#define rRTCCON (*((volatile unsigned long *)(RTC_BASE + 0x40)))
#define rTICCNT (*((volatile unsigned long *)(RTC_BASE + 0x44)))
#define rRTCALM (*((volatile unsigned long *)(RTC_BASE + 0x50)))
#define rALMSEC (*((volatile unsigned long *)(RTC_BASE + 0x54)))
#define rALMMIN (*((volatile unsigned long *)(RTC_BASE + 0x58)))
#define rALMHOUR (*((volatile unsigned long *)(RTC_BASE + 0x5c)))
#define rALMDATE (*((volatile unsigned long *)(RTC_BASE + 0x60)))
#define rALMMON (*((volatile unsigned long *)(RTC_BASE + 0x64)))
#define rALMYEAR (*((volatile unsigned long *)(RTC_BASE + 0x68)))
#define rRTCRST (*((volatile unsigned long *)(RTC_BASE + 0x6c)))
#define rBCDSEC (*((volatile unsigned long *)(RTC_BASE + 0x70)))
#define rBCDMIN (*((volatile unsigned long *)(RTC_BASE + 0x74)))
#define rBCDHOUR (*((volatile unsigned long *)(RTC_BASE + 0x78)))
#define rBCDDATE (*((volatile unsigned long *)(RTC_BASE + 0x7c)))
#define rBCDDAY (*((volatile unsigned long *)(RTC_BASE + 0x80)))
#define rBCDMON (*((volatile unsigned long *)(RTC_BASE + 0x84)))
#define rBCDYEAR (*((volatile unsigned long *)(RTC_BASE + 0x88)))
#define rCURTICCNT (*((volatile unsigned long *)(RTC_BASE + 0x90)))
#define rRTCLVD (*((volatile unsigned long *)(RTC_BASE + 0x94)))
定义设置时间的函数,传一个指针进来,因为这个只会去读它,所以加const,这就是输入型参数;定义获取时间的函数,因为这个需要往里面放,所以这里不加const,这就是输入项参数(输入输出型参数在c高级的4.3.10中介绍)。这里RTCCON的RTCEN是读写使能而不是RTC使能,一般保持禁止,所以读写前后需要打开和关闭。
void rtc_set_time(const struct rtc_time *p)
{
//1.打开RTC的读写开关
rRTCCON |= (1<<0);
//2.写RTC时间寄存器
rBCDDATE = num_2_bcd(p->year - 2000); //BCDYEAR寄存器存的是基于2000年的偏移量的年份
rBCDMON = num_2_bcd(p->month);
rBCDDATE = num_2_bcd(p->date);
rBCDHOUR = num_2_bcd(p->hour);
rBCDMIN = num_2_bcd(p->minute);
rBCDSEC = num_2_bcd(p->second);
rBCDDAY = num_2_bcd(p->day);
//关上读写开关
rRTCCON &= ~(1<<0);
}
void rtc_get_time(struct rtc_time *p)
{
//1.打开RTC的读写开关
rRTCCON |= (1<<0);
//2.写RTC时间寄存器
p->year = bcd_2_num(rBCDYEAR) + 2000;
p->month = bcd_2_num(rBCDMON);
p->date = bcd_2_num(rBCDDATE);
p->hour = bcd_2_num(rBCDHOUR);
p->minute = bcd_2_num(rBCDMIN);
p->second = bcd_2_num(rBCDSEC);
p->day = bcd_2_num(rBCDDAY);
//关上读写开关
rRTCCON &= ~(1<<0);
}
实现BCD码和十进制的相互转换。
//实现十进制转BCD码
static unsigned int num_2_bcd(unsigned int num)
{
//比如把56拆分成5和6,并组成0x56
return(((num / 10)<<4) | (num % 10));
}
//实现BCD码转十进制
static unsigned int bcd_2_num(unsigned int bcd)
{
//比如把0x56拆分成5和6,并组成56
return(((bcd & 0xf0)>>4)*10 + (bcd & (0x0f))); //这里后面个位由于运算优先级的问题要加括号
}
在Makefile中的objs修改wdt.o为rtc.o,.bin文件中的uart改为rtc。
在main.c中保留如下,然后定义结构体写变量并赋值,把时间写入,再定义结构体读变量,读取时间并把时间打印出来。
#include "stdio.h"
#include "int.h"
#include "main.h"
int main(void)
{
uart_init(); //初始化串口
printf("---rtc write time test---");
struct rtc_time tWrite =
{
.year = 2021,
.month = 5,
.date = 17,
.hour = 15,
.minute = 15,
.second = 3,
.day = 1,
};
rtc_set_time(&tWrite);
printf("---rtc read time test---");
struct rtc_time tRead; //定义结构体变量
while(1)
{
rtc_get_time(&tRead);
printf("The time read is: %d:%d:%d:%d:%d:%d:%d.", tRead.year, tRead.month, tRead.date,
tRead.hour, tRead.minute, tRead.second, tRead.day);
//做点延时
volatile int i, j;
for (i=1000; i>0; i--)
for (j=10000; j>0; j--);
}
return 0;
}
实验现象:
(2)闹钟实验
找到中断号是NUM_RTC_ALARM。在main.c中把第一节实验屏蔽,进行初始化,设置中断的规则,调用isr,声明结构体读变量,设置循环把时间的秒定时打印出来。
#include "stdio.h"
#include "int.h"
#include "main.h"
int main(void)
{
uart_init(); //初始化串口
intc_init();
system_init_exception();
rtc_set_alarm();
intc_enable(NUM_RTC_ALARM);
intc_setvectaddr(NUM_RTC_ALARM, isr_rtc_alarm);
struct rtc_time tRead;
while(1)
{
rtc_get_time(&tRead);
printf("The time read is: %d.", tRead.second);
volatile int i, j;
for (i=1000; i>0; i--)
for (j=10000; j>0; j--);
}
return 0;
}
在rtc.c中加中端设置,每到23秒时产生中断,且把RTCALM的0和6bit位置一使能(时间使能加秒使能)。编写中断处理程序。
void rtc_set_alarm(void)
{
rALMSEC = num_2_bcd(23);
rRTCALM |= 1<<0;
rRTCALM |= 1<<6;
}
void isr_rtc_alarm(void)
{
static int i = 0;
printf("rtc alarm, i = %d...", i++);
rINTP |= 1<<1; //清除中断挂起PEND(外部中断和总中断都要清除)
intc_setvectaddr(); //清总中断
}
在main.h中声明中断处理程序和中断设置。
void rtc_set_time(const struct rtc_time *p);
void rtc_get_time(struct rtc_time *p);
void isr_rtc_alarm(void);
void rtc_set_alarm(void);
实验现象:每到23秒发出一次闹钟。