时基
理解成一个带中断触发的定时器会好一点,定时器有非常多复杂的配置,如果只用来计时很多东西用不上,用这个时基模块就可以满足开发需求了。
由于目前的功力有限,还没有研究过RTOS那种嵌入式系统,另外一方面就是这种性能不高的芯片,用时间轮询的伪任务系统也可以满足大部分开发需求,我个人习惯把这个时间轮询称作时基碎片功能
。
该款MCU拥有2个独立的时基模块,对应的属性也是非常简单的,可以大致理解成:
- 模块时钟
- 溢出周期
- 模块使能
- 中断使能
程序配置
Timebase.h
#ifndef _TIMEBASE_H_
#define _TIMEBASE_H_
/*-------------------变量声明------------------- */
typedef struct
{
unsigned int FragmentCnt;
unsigned char Cnt1s;
unsigned char Cnt100ms;
unsigned char Cnt10ms;
unsigned char Cnt2ms;
unsigned char FragmentFlag;
}Timebase_Fragment_Task;
#define Fragment_1ms_Mask 0x01
#define Fragment_2ms_Mask 0x02
#define Fragment_10ms_Mask 0x04
#define Fragment_100ms_Mask 0x08
#define Fragment_1s_Mask 0x10
typedef struct
{
unsigned int PWM_Cnt;
unsigned int Duty;
unsigned int Period;
unsigned char Status;
}Timebase_PWM_Task;
#define TIMEBASE_PWM_PERIOD 250
#define TIMEBASE_PWM_DUTY 100
extern Timebase_Fragment_Task TB0;
extern Timebase_PWM_Task TB1;
/*-------------------函数声明------------------- */
void Timebase_0_Init();
void Timebase_1_Init();
void Timebase_PWM_Output();
void Timebase_PWM_Duty_Update();
void Timebase_PWM_Parameter_Update(unsigned int duty, unsigned int period);
/*-------------------宏封装------------------- */
#define Timebase_0_IntAddress 0x1C
#define Timebase_1_IntAddress 0x20
#define Timebase_0_Cmd(x) _tb0on = x
#define Timebase_0_InterruptCmd(x) _tb0e = x
#define Timebase_0_Clock(x) _psc0r = (_psc0r&(~(REG_2_BIT<<0)))|(x<<0)
#define Timebase_0_Period(x) _tb0c = (_tb0c&(~(REG_3_BIT<<0)))|(x<<0)
#define Timebase_0_FlagReset() _tb0f = 0
#define Timebase_1_Cmd(x) _tb1on = x
#define Timebase_1_InterruptCmd(x) _tb1e = x
#define Timebase_1_Clock(x) _psc1r = (_psc1r&(~(REG_2_BIT<<0)))|(x<<0)
#define Timebase_1_Period(x) _tb1c = (_tb1c&(~(REG_3_BIT<<0)))|(x<<0)
#define Timebase_1_FlagReset() _tb1f = 0
typedef enum
{
Timebase_Clock_Fsys = 0,
Timebase_Clock_Fsys_Div4,
Timebase_Clock_Sub,
}Timebase_Clock_T;
typedef enum
{
Timebase_Period_2e8 = 0,
Timebase_Period_2e9,
Timebase_Period_2e10,
Timebase_Period_2e11,
Timebase_Period_2e12,
Timebase_Period_2e13,
Timebase_Period_2e14,
Timebase_Period_2e15,
}Timebase_Period_T;
#endif
时基0相关程序配置
时基0初始化
前面说了,时基0用来做那个时基碎片功能,对应的任务结构体就是这个Timebase_Fragment_Task
,在对应的C文件中通过这个结构体类型新建一个变量Timebase_Fragment_Task TB0
,结构体里面的变量用法如下,主要有两部分,一部分体现在中断里面,另一部分体现在主函数里面。
void Timebase_0_Init()
{
Timebase_0_Cmd(Disable);
Timebase_0_InterruptCmd(Disable);
Timebase_0_Clock(Timebase_Clock_Fsys);
if((Sys.Clock == HXT_8M)||(Sys.Clock == HIRC_8M))
{
Timebase_0_Period(Timebase_Period_2e13);
}
else if((Sys.Clock == HXT_16M)||(Sys.Clock == HIRC_16M))
{
Timebase_0_Period(Timebase_Period_2e14);
}
Timebase_0_Cmd(Enable);
Timebase_0_InterruptCmd(Enable);
TB0.FragmentCnt = 0;
TB0.Cnt1s = 0;
TB0.Cnt100ms = 0;
TB0.Cnt10ms = 0;
TB0.Cnt2ms = 0;
TB0.FragmentFlag = 0;
}
先看时基0的初始化部分,一般我是把系统时钟直接给到时基0这边不用分频。
至于Sys.Clock
这个结构体是保存一些系统信息的结构体,目前还没有构思好要放哪些东西,先放置一个变量记录当前的时钟频率,后续会单独写一篇介绍这个结构体。
这个变量只要记录,系统时钟频率,以及是内部时钟还是外部时钟这种。之所以要保存时钟的信息,是因为如果时基0的溢出周期固定的话,但是不同工程选择的时钟可能不一样,就导致进入中断的时间就不一致了,整个时基碎片功能就出错了。
所以这里设置溢出周期之前要先判断当前的时钟频率是多少。
时基0中断
DEFINE_ISR(timebase_0_int, Timebase_0_IntAddress)
{
Timebase_0_FlagReset();
TB0.FragmentFlag |= Fragment_1ms_Mask;
TB0.FragmentCnt += 1;
if(TB0.FragmentCnt >= 2)
{
TB0.FragmentCnt = 0;
TB0.FragmentFlag |= Fragment_2ms_Mask;
TB0.Cnt2ms += 1;
if(TB0.Cnt2ms >= 5)
{
TB0.Cnt2ms = 0;
TB0.FragmentFlag |= Fragment_10ms_Mask;
TB0.Cnt10ms += 1;
if(TB0.Cnt10ms >= 10)
{
TB0.Cnt10ms = 0;
TB0.FragmentFlag |= Fragment_100ms_Mask;
TB0.Cnt100ms += 1;
if(TB0.Cnt100ms >= 10)
{
TB0.Cnt100ms = 0;
TB0.FragmentFlag |= Fragment_1s_Mask;
}
}
}
}
}
这个逻辑也是比较简单的,每次中断进来就是1ms,累计2次就是2ms,然后2ms累计5次就是10ms,这样层层递进下来。
我预设的时钟是1ms,但是时基的溢出周期没有办法像时钟那样设定得非常精准,所以只能把溢出周期配置成最接近的1.024ms,8M和16M的时钟都能配置出来,但是12M的时钟就有点尴尬了。
因为这个是1.024ms的中断,所以后续累加上来的时间比如2ms,10ms,100ms,1s这种都是有一定误差在里面的,但是这个功能后续对接的不应该是那种实时性特别强的开发任务。
时基0新写法降低误差思路
如果强迫症犯了非得要把这个误差降下来也不是不行,换一种思路就好了。不要按照上面那种层层递进的关系,从把所有标志位都绑在一起的关系变成,2ms绑定1ms,10ms绑定1ms,100ms绑定1ms,1s绑定1ms这种,单独跟1ms对应。
就是每次进入中断之后,每个级别都自加一次,2ms这种累加2次,2ms自己的计数变量清零,拉起对应标志位。
10ms这种累加10次1ms,但是实际上应该是1.024 x 10 = 10.24ms,如果累加成9次再清零的话,时间应该是1.024 x 9 = 9.216秒。按照误差来算肯定是累加10次比较合理的。
同理,100ms的话累加100次的时间是102.4ms,99次是101.376,98次是100.352,97次是99.328,这里取98次比较合理。
1s的标志位呢,需要注意一个点,unsigned char
是8位变量,最大值为255,要换成unsigned int
才能记录到1000,剩下的自己算。
如果强迫症达到丧心病狂的地步的话,那个时基溢出周期选择更低一些,按照自己喜欢然后计算,尽可能把每一个级别的误差降到最低🤡太过频繁进入中断并不是特别合适,后续有空的话也可以举个例子讲讲。
时基0主函数操作
void main()
{
System_Init();
Timebase_0_Init();
while(1)
{
if(TB0.FragmentFlag & Fragment_1ms_Mask)
{
TB0.FragmentFlag ^= Fragment_1ms_Mask;
}
if(TB0.FragmentFlag & Fragment_2ms_Mask)
{
TB0.FragmentFlag ^= Fragment_2ms_Mask;
}
if(TB0.FragmentFlag & Fragment_10ms_Mask)
{
TB0.FragmentFlag ^= Fragment_10ms_Mask;
}
if(TB0.FragmentFlag & Fragment_100ms_Mask)
{
TB0.FragmentFlag ^= Fragment_100ms_Mask;
}
if(TB0.FragmentFlag & Fragment_1s_Mask)
{
TB0.FragmentFlag ^= Fragment_1s_Mask;
}
Feed_Dog();
};
}
这里就是按照那种常见任务轮询来写的。看需求然后把要操作的程序放置到对应的时间里面。
关于标志位清零的操作,只要能进入对应的分支就证明对应的标志位一定是1,直接通过异或进行清零运算。
时基1配置
之前写的时候预留了时基1作为模拟PWM控制呼吸灯,随便写了点东西程序不是很完善,这里就不展开写了,后面涉及到GPIO的时候再展开写清楚。