其他分享
首页 > 其他分享> > Gos —— 定时器8253

Gos —— 定时器8253

作者:互联网

文章目录

写在前面:自制操作系统Gos 第二章第十篇:主要内容是如何协调操作系统中各部件工作频率的组件定时器
Gos完整代码:Github

时钟

之前,我其实在中断的实现机制那篇博客中提到了时钟中断这个概念,而且它被放在RPQ0这个最为重要的位置。那它是干什么的呢?为什么会这么重要呢?

我们可以想想在平时生活中,我们如何跟其他人进行工作中的同步呢?

以下为虚拟场景:
“小龚,这个任务下周一前解决”
“好的”

其实主要就是依靠时间来完成这个过程,计算机系统中也一样,为了让所有设备之间的通信尽然有序,就必然有一个大家都遵守的时间规约,而这个就被称之为时钟。它并不是计算机处理速度的衡量,而是一种使设备相互配合而避免发生冲突的节拍。

一般来说时钟会分为两种:

其中内部时钟一般使无法改变的,我们想要在操作系统中协调组件就只能对外频进行更改。其实也有两种手段:

int clock_times = 1000;
while(clock_times>=0)
{
	clock_times--;
)

所以,综上来看:软件控制外部时钟太浪费CPU资源啦,我们最好的方法就是采用硬件控制外部时钟。

定时器8253

定时器主要使分为不可编程定时器和可编程定时器两种。我们主要用到的就是可编程定时器(programmable interval timer,PIT)。


不可编程定时器的有关资料我也没有找到,由同学找到可以在评论区@我一下。
常见的定时器有Intel 8253/8254/82C54A,我们只需要会使用最简单的8253就可以了。

定时器计时

定时器计时方式主要有两种:

int clock_times = 0;
int end_times = 1000;
while(clock_times <= end_times)
{
	clock_times++;
}
int clock_times = 1000;
while(clock_times >= 0)
{
	clock_times--;
}

其实倒计时是我们用的最多的,比如时间片到期、闹钟啊等等。

8253入门

8253内部有三分独立的计时器,其分别对应端口0x40~0x42这三个计时器相互之间不依赖,独立工作,每个都有自己一套寄存器资源(一个16位计数初值寄存器+一个计数器执行部件+一个输出锁寄存器),8253结构如下:
在这里插入图片描述

读/写逻辑寄存器

在这里插入图片描述

计数器

我们首先来看一下计数器的工作原理和组成:
在这里插入图片描述

当计数开始前,数值会通过总线保存到计数初值寄存器,每当CLK引脚收到一个脉冲信号就会通过减法计数器这个执行部件将计数初值减一,同时将当前值保存在输出锁寄存器。当计数值减到0,表示定时工作结束,这个时候通过OUT引脚发出信号。

而我们这里可以看到8253里面有三个计数器,它们的作用各不相同:

计数器端口作用
计数器 00x40专用于产生实时时钟信号,采用工作方式3
计数器 10x41专用于DRAM的定时刷新控制
计数器 20x42专用于内部扬声器发生不同声调的声音

控制字寄存器

控制字寄存器也被称之为模式控制寄存器,其操作端口是0x43。在控制器中保存的内容被称为控制字,控制字用来设置所指定的计数器的工作方式、读写格式以及数制。刚刚讲过的三个计数器是独立工作的,每个计数器都必须明确自己的控制模式才知道怎样去工作,而它们的工作模式其实也就是这个控制字寄存器来设定的。

首先,我们来看一下控制字的结构:
在这里插入图片描述

8253工作方式

详情如下表:
在这里插入图片描述

8253初始化

8253开始工作的方法很见到,只用通过控制字寄存器选择哪个计数器,指定其控制模式,再写入初值就可以啦:

  1. 往控制字寄存器端口0x43写入控制字
  2. 在所指定使用的计数器端口写入计数初值
/*
 * @brief 计时器属性以及初始值设置
 * @param counter_port 计时器端口
 * @param counter_no 计时器标号
 * @param rwl 读写方式
 * @param counter_mode 计时器模式
 * @param counter_value 计时器初始值 
 */
static void frequency_set(uint8_t counter_port, uint8_t counter_no, uint8_t rwl, uint8_t counter_mode, uint16_t counter_value)
{
    //往控制字寄存器0x43写入控制字 
    outb(PIT_CONTROL_PORT, (uint8_t)(counter_no << 6 | rwl << 4 | counter_mode << 1));
    //先写入低8位
    outb(counter_port, (uint8_t)counter_value);
    outb(counter_port, (uint8_t)counter_value >> 8);
}

定时器初始化

定时器初始化主要有两个步骤:

  1. 8253进行初始化
//这里写入的是0011 0100 -->表示0x40端口,计数器选择0,先读写低字节再读写高字节,工作方式为第二种
    frequency_set(COUNTER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE);


这里中断的初始值被设置为11932,这是我们想要中断信号的频率为100Hz(每秒发生100次中断)。
即1.19318Mhz(计数器的频率)/100 = 11932.

  1. 注册时间中断处理函数
    register_handler(0x20, intr_timer_handler);

这样每当中断0x20发生,即8253发送过来一个信号就会去调用中断处理函数。中断处理函数主要做的工作就是将当前进程的时间片-1,如果时间片无了,就从就绪队列中取出一个进程,执行取出的进程。这就是大名鼎鼎的时间片轮转法

/*
 * @brief 时钟的中断处理函数
 */
static void intr_timer_handler(void)
{
    struct task_struct *current_thread = running_thread();

    //检查是否栈溢出,0x20000314是个魔数,我生日,随便设置其他数也可以
    ASSERT(current_thread->stack_magic == 0x20000314);

    current_thread->elapsed_ticks++; //记录此线程占用的cpu时间数
    ticks++;                         //内核时间++

    if (current_thread->ticks == 0)
    {
        schedule();
    }
    else
    {
        current_thread->ticks--;
    }
}

参考文献

[1] 操作系统真相还原
[2] 百度文库.8253内部结构与工作方式

标签:定时器,8253,times,计数器,寄存器,Gos,时钟
来源: https://blog.csdn.net/shenmingxueIT/article/details/121628276