如何确定时间?实际上就是计数周期性事件发生的次数:太阳升起落下就是一天、春夏秋冬一个轮回就是一年.....所以计数和计时可以说就是一个东西。
MUC中就是用计数器数脉冲的个数,如果这些脉冲是周期性的,计数值就可以用来计时。
定时计数器主要功能是计数脉冲,如果输入的脉冲是周期性的脉冲那么知道输入的脉冲个数也就知道了从开始输入脉冲位置到计数结束经历了多长时间。
何时开始计数呢?何时结束计数呢?计数结束以后MCU要做什么呢?
捕获通常用于测量,比较通常用于计时
时钟系统:LK32T有两类定时器:1是控制定时器(高级定时器)ETIMER,主要用于测量信号宽度、捕获信号、输出波形等;2是通用定时器TIM6(为什么是这样啊?因为LK32T是模仿STM32的,LK32T好像是杭州士兰微的贴牌),主要用于定时。(不严格说来还有第3类定时器SysTick定时器,这个定时器一般用于OS中,用于产生周期性的中断请求,以便OS周期性任务调度;当然也可以用于其他作用,比如精确延时ST_delay_us,ST_dalay_ms)
讲到计时就必然讲到时钟系统,LTK320有4个时钟源,内部RCH(内部高精度,16MHZ)和RCL(内部低精度32KHZ),PLL(锁相环)及外部振荡HOSC
例程中采用了外部振荡RCH(16MHZ)经PLL变为144MHZ,作为系统时钟,然后hdiv和mtdiv都为2分频,成72MHZ
SYSTICK时钟采用RCH经过16分频为1MHZ
XCLKOUT选择的是sysclk(144MHZ),并进行了16分频,即为9MHZ
mtclk信号可以输往定时计数器作为定时计数脉冲
LK320T有多个定时计数器
输入功能 | 输入捕获功能 |
该模式用于检测指定输入信号的边沿跳变。
|
PWM 输入模式
|
该模式可以测量输入 PWM 信号的周期和占空比。该模式是输入模式的特例
|
|
编码器接口模式
|
加减数,测试,测位移 | |
定时器输入异或功能
|
||
TIM 定时器的外部触发同步
|
与一个外部触发信号同步
|
|
输出功能 |
强制输出模式
|
|
输出比较模式
|
当计数器与捕获 / 比较寄存器的内容相同时,亦即发生比较匹配时,输出比较功能会做一定的动作
|
|
单脉冲模式
|
单脉冲模式是输出比较模式的一个特例
|
|
PWM 输出模式
|
可以产生一个由 (TIM_ARR) 决定周期, (TIM_CCRx) 决定占空比的脉宽调制信号。
|
|
互补输出和死区插入
|
||
其他功能 |
LTK320T的定时计数器与MCS51的定时计数器不同,它的定时计数器在溢出时不但会发出中断信号(与MCS51相同),而且可以影响某些管脚的电平,触发一系列的动作。
其工作过程为:计数(定时)脉冲(可选择)经分频后进入计数器(CNT),CNT可增(0~ARR-1),可减(ARR-1~0),也可以先增再减(0~ARR-1~0),每次溢出产生一个信号,可作为中断信号,也可作为事件信号,同时在计数过程中也可以进行将CNT值与TIM_CCRx进行比较或在某个信号来临时将CNT值保存在CCRx中(捕获),也可以直接将CCRx设定一个值用于与CNT值进行比较,比较的结果可以用来控制某些管脚的值,比如翻转,或产生PWM波等。
另外RCR(循环计数寄存器)在每次CNT溢出时减1,直至 该值减至 0 时,产生一个更新事件。如果循环计数寄存器的初始值为 0,则每次溢出(下溢)时都产生更新事件。不需要太过关注。
例.计时---超声测距
超声测距的原理是超声传感器发出波,同时计时,一旦接收到回波,计数器停止计时,计算距离。本例并没有使用定时计数器捕获功能。
/*
超声测距,通过串口显示距离
*/
//一定需要的2个头文件
#include
#include
//使用模板才需要的头文件
#include "keyboard4x4.h"
#include "delay.h"
#include "UART.h"
#include "printf.h"
#include "ds18b20.h"
#include "LCD12864.h"
#include "segment.h"
#include "motor.h"
/*
LED1-main主程序运行指示
LED2-PB14超声波接收中断指示
LED7-定时器0捕获中断
PA07-超声波模块VIN,发出超声波
PB14-超声波模块OUT,接收超声回波
串口-打印距离信息(115200-8-N-1)
*/
int main(void){
Device_Init();
//配置TIM0 - CH2通道(PA07)输出PWM信号,驱动超声波发生器发出超声波
TIM0_Init_PWM(4,0);//TIM0 - CH2通道输出PWM
//外部配置,PB14一旦接收到回波,引发中断,测量距离
PB_INT_ENABLE(14);//开启PB14中断,接收反射波
PB_INT_EGDE(14);//PB14边沿触发
PB_INT_BE_DISABLE(14);//PB14单边沿触发
PB_INT_POL_LOW(14);//PB14下降沿触发
PB_INT_FLAG_CLR(14);//清除PB14中断标志
PB -> OUTEN |= 0x00ff;//PB输出使能
PB -> OUT = 0xffff;
PA_OUT_ENABLE(12);//蜂鸣器输出使能
delay_ms(500);
BUZZER_OFF;//蜂鸣器关断
IRQ_Enable();//开总中断
while(1){
LED2_OFF;
LED1_ON;
ST_delay_ms(1000);
//通过PA07发出超声波
TIMER0 -> TIM_ARR = 3;
TIM_ARPE_ENABLE;
EGR_CNT_UPDATE;
GPIO_AF_SEL(DIGITAL, PA, 7, 3);// T0CH2 PA07作为定时计数器的输出脚,发出超声波
//定时器0,测量超声回波时间
TIMER0 -> TIM_CCER_b.CC2E = CC2E_ENABLE; //定时器0捕获/比较使能
TIMER0 -> TIM_DIER_b.UIE = UIE_ENABLE; //定时器0中更新中断使能
TIMER0 -> TIM_CR1_b.CEN = 1; //定时器0使能
LED1_OFF;
ST_delay_ms(1000);
}
}
PB14管脚一旦收到回波,引发中断GPIO1,系统执行中断服务程序,服务程序立刻暂停定时器0,并读出计数器值,计算距离并通过串口显示距离。利用其比较功能。
void GPIO1_IRQHandler()
{
if(!(PB -> PIN & (1 << 14))){//PB14是否接收到回波
TIMER0 -> TIM_CR1_b.CEN = 0;//定时器0停止
PB_INT_DISABLE(14);//关闭PA10中断
printf("\n%d/TIM0 CNT=%d\n", ult_times, TIMER0 -> TIM_CNT);
ult_distance = (TIMER0 -> TIM_CNT) * 6.25 * 0.34 / 2 / 10;//(1 / 0.16MHz = 6.25us),声速0.34mm/us,/2 两倍距离 /10 毫米变成厘米
printf("%d超声距离:%5.2f cm\n", ult_times++, ult_distance);
PB_INT_FLAG_CLR(14);//清除中断
PB_INT_ENABLE(14);//开启PA10中断
}
LED2_ON;
NVIC_ClearPendingIRQ(PB_IRQn);//清除中断
}
而其中非常关键的定时计数器的初始化如下:
/**
* @brief Initialize the TIM0 CH3输出PWM波
* @param None
* @retval None
*/
void TIM0_Init_PWM(uint32_t PRD, uint32_t DB_CFG){
//时基单元设置
TIMER0 -> TIM_PSC = 450 - 1;//预分频值 72Mhz / 450 = 160Khz
TIMER0 -> TIM_CR1_b.CMS = CMS_EDGE_ALIGN;// 边沿对齐模式
TIMER0 -> TIM_CR1_b.DIR = 0;//计数器增计数
TIMER0 -> TIM_ARR = PRD - 1;// 自动重装载寄存器的值
TIM_ARPE_ENABLE;// 周期计数预装载允许位:TIM_ARR 寄存器有缓冲
//PWM输出比较
TIMER0 -> TIM_CCMR1 |= CC2S_OUTPUT; // OC2M CH2通道输出
TIMER0 -> TIM_CCMR1 |= OC2M_PWM_MODE2; // OC2M CH2选择PWM模式1(2?)
TIMER0 -> TIM_CCMR1 |= OC2PE_PRELOAD_ENABLE;// OC2PE 开启 TIM_CCR1 寄存器的预装载功能
//TIMER0 -> TIM_CCER_b.CC2E = CC2E_ENABLE; // 捕捉/比较1输出使能
//TIMER0 -> TIM_CCER_b.CC2NE = CC2NE_ENABLE; // 捕捉/比较 1 互补输出使能
TIMER0 -> TIM_CCER_b.CC2P = CC2P_OUTPUT_LOW ; // 捕捉/比较1输出极性:OC1 高电平有效;
TIMER0 -> TIM_CCER_b.CC2NP = CC2NP_OUTPUT_HIGH; // 捕捉/比较1互补输出极性:OC1N 高电平有效;
TIMER0 -> TIM_CCR2 = PRD >> 1;// 占空比设置/2
//死区设置
TIMER0 -> TIM_BDTR_b.MOE = MOE_ENABLE; //主输出使能
// TIMER0 -> TIM_BDTR_b.AOE = AOE_SW_SET;
// TIMER0 -> TIM_BDTR_b.OSSR = 0; //运行模式下“关闭状态”选择
// TIMER0 -> TIM_BDTR_b.DTG = DB_CFG; //死区设置 OC1上升沿后延时2us,具体设置参考寄存器说明
// TIMER0 -> TIM_DTG1 = DB_CFG; //死区设置 OC1N下降沿后延时2us,具体设置参考寄存器说明
NVIC_ClearPendingIRQ(TIMER0_IRQn);//TIMER0 清除中断标志位
//TIMER0 -> TIM_CR1_b.UDIS = 0;//禁止UEV
//TIMER0 -> TIM_DIER_b.UIE = UIE_ENABLE;//更新中断使能
EGR_CNT_UPDATE; // TIMER0->TIM_EGR_b.UG = 1; 重新初始化计数器,并产生一个 ( 寄存器 ) 更新事件
//CNT_ENABLE; // TIMER0->TIM_CR1_b.CEN=1;使能计数器
}
这个函数设置了输出PWM的基本参数:其中关键设置了周期、占空比等:第9行设置了预分频为PSC(=PRD),表示计数脉冲为160Khz,第13行设定ARR(=PRD)表示每ARR个计数脉冲输出1个PWM脉冲,即PWM波的频率为160Khz/ARR,第26行设定了CCR2(=PRD>>1),设定了输出高电平的时间(因为是模式2).例程中PRD为4,所以设定的PWM波的频率为40Khz,占空比为50%。
这个函数定义和使用例程中有个非常怪异的地方:两个参数,一个表示预分频,一个表示死区时间,main函数调用的时候分别为4和0,这也没什么, 但是要注意预分频的值也是要分给ARR的,而ARR表示自动重装寄存器,ARR的值就代表了CNT的上限,难道CNT只能计数4个脉冲?而有意思的地方来了:在GPIO1_IRQHandler中是要读取CNT的值的......ARR在什么地方改了?在这里:
void TIMER0_IRQHandler()
{
//如果没有进入捕获,此段程序只执行一次
if(TIMER0 -> TIM_SR_b.UIF == 1){
//if((TIMER0 -> TIM_SR_b.UIF == 1) && TIM0_Send_Receive == 0 )//发生更新中断,等待脉冲发送完成
TIM0_UIE_Times++;//更新中断进入次数
if(TIM0_UIE_Times >= 10){
TIMER0 -> TIM_CR1_b.CEN = 0; //定时器失能
TIMER0 -> TIM_DIER_b.UIE = UIE_DISABLE;//更新中断失能
TIMER0 -> TIM_CCER_b.CC2E = CC2E_DISABLE;// 捕捉/比较2输出失能
GPIO_AF_SEL(DIGITAL, PA, 7, 0);// 关闭T0CH2功能
PA_OUT_DISABLE(7);//PA7 输入模式
TIMER0 -> TIM_ARR = 0xffff;
TIM_ARPE_ENABLE;// 预装载使能
EGR_CNT_UPDATE;
TIMER0 -> TIM_ISR &= ~(3 << 15);// 通道1捕获清零
TIMER0 -> TIM_ISR |= (1 << 17); // 计数器捕获清零使能
TIMER0 -> TIM_CR1_b.CEN = 1;//定时器计数使能
TIM0_UIE_Times = 0;//更新中断进入次数清零
TIM0_Send_Receive = 1;//40KHz脉冲发送完成
}
}
/*************************************************************************
TIM0捕获脉冲频率/宽度
*************************************************************************/
// if( (TIMER0 -> TIM_SR_b.UIF == 1) && TIM0_Send_Receive == 1 && CaptureNumber == 1 ){ //更新中断,40KHz发送完成,并发生过一次输入捕获溢出
// // TIM0_ORFW_times ++;//更新中断前没有完成捕获次数
// TIMER0 -> TIM_SR_b.UIF = 0;//清除更新中断标志位
// }
// //有捕获中断发生
// if( TIMER0 -> TIM_SR_b.CC1IF == 1 ){ //判断通道1有无捕获中断产生
// //第一次发生捕获中断
// if( CaptureNumber == 0 ){
// IC1ReadValue1 = TIMER0 -> TIM_CCR1;//第一次读取输入捕获比较值
// CaptureNumber = 1;//发生一次捕获中断标志
// }
// //第二次发生捕获中断
// else if( (TIMER0 -> TIM_SR_b.UIF == 0) && CaptureNumber == 1 )//没有发生更新中断并且已经有一次捕获中断
// {
// IC1ReadValue2 = TIMER0 -> TIM_CCR1;//第2次读取输入捕获比较值
// //捕获时间计算
// CaptureValue = ( ( 5 * TIM0_ORFW_times - IC1ReadValue1 ) + IC1ReadValue2 ); //5为TIM_ARR
//
// //完成捕获,标志位清零
// TIM0_ORFW_times = 0;//捕获阶段,更新中断发生次数清零
// CaptureNumber = 0; //捕获标志清零,完成一次捕获操作
// TIM0_Send_Receive = 0;//40Khz 发送及接受标志位清零,等待脉冲发送
// }
//
// TIMER0 -> TIM_SR_b.CC1IF = 0; //清除捕获中断标志位
// }
TIMER0 -> TIM_SR_b.UIF = 0;//清除中断标志位
TIMER0 -> TIM_SR_b.CC1IF = 1; //清除捕获中断标志位
NVIC_ClearPendingIRQ(TIMER0_IRQn);//TIMER0 清除中断标志位
}
硬件跟踪也确实发现ARR的值改为0xFFFF了。为什么这样设计呢?TIMER0确实也在IRQ_Init()中使能了。
为什么不直接在TIM0_PWM_Init()中直接设置ARR为0xFFFF呢?
红框中翻译一下:相应的预装载寄存器必须通过设定TIMx_CCMRx[OCxPE]使能,最终通过设定TIMx_CR1[APRE]使能了自动重装预载寄存器(在升计数模式或中间对齐模中)。因为只有在更新时间发生时预装载寄存器才转送到影子寄存器中,所以在启动计数器时,用户必须通过设定TIMx_EGR[UG]的方式来初始化寄存器。
是不是说影子寄存器起到了ARR的作用?
搞的这样复杂啊!我们实际上只是需要产生一个PWM波,能够设定其频率和占空比就可以了!是不是为了规避专利和知识产权问题啊?大概率如此!!!简单的方法都被申请专利了!
现阶段会利用其例程代码即可,不要纠结,知道如何设定PWM的频率和占空比即可!看全文加粗及加下划线部分!
TIM6 为通用定时器模块,主要用于软件定时控制。作为 slaver 挂载在 APB 总线上。硬件配置 2 组独立的定时器, 定时器时钟为系统时钟,与 Cortex-M0 内核时钟一致。其功能相对简单,类似MCS51中的定时器。
注意其时钟源为系统时钟,与内核的时钟一致!
....
//主函数
int main( void ){
Device_Init();
TIM0_Init_PWM(1000,196);//TIM0 - CH2通道输出PWM
TIM6_T0_Init();//定时器T6-T0初始化
TIM6_T1_Init();
...
while(1){
...
}
}
两个定时器的初始化:
/**************************************************************************
配置TIM6通用定时器
***************************************************************************/
void TIM6_T0_Init(){
TIM6 -> COMPARE0 = 5000; // 计数比较值----定时的时间(周期)
TIM6 -> CTC0_b.Freerun = 1; // timer在使能后一直计数
TIM6 -> CTC0_b.COUNT0INT_EN = 1; // 中断使能位,高电平有效
TIM6 -> CTCSEL0 = 0x02; // 4分频
TIM6 -> CTC0_b.COUNTEN = 1; // 使能,开始计数
}
void TIM6_T1_Init(){
TIM6 -> COMPARE1 = 5000; // 计数比较值----定时的时间(周期)
TIM6 -> CTC1_b.Freerun = 1; // timer在使能后一直计数
TIM6 -> CTC1_b.COUNT0INT_EN = 1; // 中断使能位,高电平有效
TIM6 -> CTCSEL1 = 0x04; // 16分频
TIM6 -> CTC1_b.COUNTEN = 1; // 使能,开始计数
}
上述代码中的第一行就是设定了定时的时间,因为采用的是内核时钟(72MHZ),所以定时的时间为5000/(72000000/16)秒,约为1ms。(有些奇怪,理解错了?)
中断服务程序:
void TIM6_T0_IRQHandler(){//
TIM6 -> CTC0_b.COUNT0INT_EN = 0;
//TODO
//
TIM6 -> CTC0_b.COUNTFW = 0;
TIM6 -> COUNT0 = 0;
NVIC_ClearPendingIRQ(TIM6_T0_IRQn);//清除中断
TIM6 -> CTC0_b.COUNT0INT_EN = 1;
}
void TIM6_T1_IRQHandler(){//
uint8_t i;
TIM6 -> CTC1_b.COUNT0INT_EN = 0;
//TODO
//
TIM6 -> CTC1_b.COUNTFW = 0;
TIM6 -> COUNT1 = 0;
NVIC_ClearPendingIRQ(TIM6_T1_IRQn);//清除中断
TIM6 -> CTC1_b.COUNT0INT_EN = 1;
}
/**
* @brief 实现us精确延时
* @param None
* @retval None
*/
void ST_delay_us( uint32_t nus ){
uint32_t temp;
CHIPKEY_ENABLE;
CHIPCTL -> CLKCFG1_b.SYSTICKSEL = 2;// systick时钟源选择rch16M的16分频,相当于计时脉冲为1MHZ
SysTick -> LOAD = 1 * nus; // 1Mhz下为1us,因为1个脉冲代表1us,n个脉冲代表nus
SysTick -> VAL = 0x00ul;//清空计数器
SysTick -> CTRL = 0x01ul;//使能 减到零是无动作 采用内部时钟源
do{
temp = SysTick -> CTRL;//读取当前倒计数值
}while( ( temp & 0x01 ) && ( ! ( temp & ( 1 << 16 ) ) ) );//等待时间到达
SysTick -> CTRL = 0x00; //关闭计数器
SysTick -> VAL = 0X00; //清空计数器
}
/**
* @brief 实现ms精确延时
* @param None
* @retval None
*/
void ST_delay_ms( uint16_t nms ){
uint32_t temp;
CHIPKEY_ENABLE;
CHIPCTL -> CLKCFG1_b.SYSTICKSEL = 2;// systick时钟源选择rch16M的16分频
SysTick -> LOAD = 999 * nms;//这里怪怪的,不是1千个脉冲代表1ms吗?!差了1个脉冲,估计是为了补偿其他操作的时间---确凿无疑!
SysTick -> VAL = 0x00ul;//清空计数器
SysTick -> CTRL = 0x01ul;//使能 减到零是无动作 采用内部时钟源
do{
temp=SysTick -> CTRL;读取当前倒计数值
}while( ( temp & 0x01 ) && ( !( temp & ( 1 << 16 ) ) ) );//等待时间到达
SysTick -> CTRL = 0x00; //关闭计数器
SysTick -> VAL = 0X00; //清空计数器
}
/****************************************/
很遗憾,有很多的东西我没有办法一一给大家回复,所以我建立了一个群,供大家一起交流!