嵌入式linux-----ARM裸机(9)-----定时器、看门狗和RTC

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秒发出一次闹钟。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值