When Do We Write Our Chinese OS? (3)

本文介绍编写可接受用户键盘输入的操作系统内核,采用C++语言,中断处理用少量汇编代码。阐述了内核程序结构,包括TVideo类封装VGA显卡处理。还介绍了显卡处理、光标处理,重点讲述通过中断处理键盘输入,包括构造中断描述符表、初始化PIC等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在上一篇中我们实现了一个真正意义上的引导程序,此引导程序将计算机从启动状态时的
16位实模式转到了32位的保护模式下,并且将一个用C语言写的真正的内核载入了内存执
行,引导程序的工作已经完成,接下来的工作就是编写一个优良的操作系统的内核,这是
一个非常巨大的工程,我们需要一步一步的来完成,同样,今天我们只完成非常非常小的
一步。在上一篇中,内核只是显示出一个提示语,标志着内核已经启动了,它还不具有同
用户的交互功能,在这一篇中,我们将完成一个可以接受用户键盘输入的内核,这是内核
具有交互功能的第一步——能接受用户输入。

本篇所实现的内核主要采用C++语言书写,中断处理部分用到了很小一段汇编代码,因此
从本篇中你还将知道怎样实现C++与汇编语言的混合编程,本内核开放源码,采用的是C++
类模式的开放结构,你可以很轻松的修改它,让它功能更强,性能理好。

这里想首先介绍一下本内核的程序结构,这样你在阅读源代码的时候,会一目了然。
本内核定义了一个TVideo类,封装了对于VGA显卡的处理,它的声明如下:
class TVideo{
  public:
    static TPos GetPos() ;
    static void SetPos( TPos& pos ) ;
    static void SetPos( unsigned short X , unsigned short Y ) ;
    static void ClearScreen() ;
    static void Print( const char *msg , EColor FrontColor = clWhite , 
                                         EColor BackColor = clBlack ) ;
    static void Print( const char msg , EColor FrontColor = clWhite , 
                                        EColor BackColor = clBlack ) ;
    EColor BackColor ;
    EColor FrontColor ;
  protected:
    TVideo(){}
};
其中 TPos 也是一个类,(以T开头的,均是类名,以E开头的是枚举类型),其声明如下
class TPos{
  public:
    unsigned short X ;
    unsigned short Y ;
};
其 EColor是一个包含了色彩变量的枚举类型:
enum EColor{ clBlack , clBlue , clGreen , clCyan , clRed , clMagenta , 
             clBrown , clLightGray , clDarkGray , clLightBlue , clLightGreen , 
             clLightCyan , clLightRed , clLightMagenta , clYellow , clWhite } ;

由于C++语言的封装机制,这使得在程序中要想在屏幕上输出是非常简单的事,下面我们就
来看看主程序对它们调用的例子:
char* msg0 = "Welcome To HIT Operation System!Version 0.0003 by xyb" ;
char* msg1 = "Please input: " ;

EColor color[] = { clLightBlue , clLightGreen , clLightCyan ,
                   clLightRed , clLightMagenta , clYellow , clWhite , 
                   clLightBlue } ;
int i = 0 ;
while( msg0[ i ] != '/0' ){
  TVideo::Print( msg0[ i++ ] , color[ i % 8 ] ) ;
}
TVideo::SetPos( 0 , 2 ) ;
i = 0 ;
while( msg1[ i ] != '/0' ){
  TVideo::Print( msg1[ i++ ] , clWhite , color[ i % 8 ] ) ;
}
这段代码非常简单,就不详加解释了,它用各种色彩打印提示信息,下面就是它的执行效


下面是接受用户输入后的效果

下面,我们将来看看这都是怎么实现的。

阅读本文,最好有那么一点的汇编基础,另外,最好已经阅读过前两篇,因为很多东东是
同前两篇,特别是第二篇相关的~~~

好了,下面开始转如正题,首先,先介绍一下怎样处理显卡,
在上一篇中,我们已经知道了,通过把数据直接写到显存中就可以在屏幕上显示,这里我
们将更深入的介绍一下

现在的显卡大多是VGA标准兼容显卡,它分字符与图形模式,本文只介绍字符模式。在字符
模式下,它有25行,每行有80列,显存的地址为于0xb8000处,对于需要显示的每一个字符
,用两个字节来描述,第一个字节是要显示的这个字符的ASCII码,第二个字节是要显示的
这个字符的色彩属性,其中高4位用来表示背景色,低4位用来表示前景色,也就是字符本
身的色彩,色彩的对照表如下:
0 Black 黑色
1 Blue 蓝色
2 Green 绿色
3 Cyan 青色
4 Red 红色
5 Magenta 洋红
6 Brown 棕色
7 Light Gray 高亮灰色
8 Dark Gray 暗灰色
9 Light Blue 高亮蓝色
A Light Green 高亮绿色
B Light Cyan 高亮青色
C Light Red 高亮红色
D Light Magenta 高亮洋红
E Yellow 黄色
F White 白色
因此,你可以组合出你想要的字体色彩,怎样组合?请参见源程序相关代码。

我们知道 0xb8000 是显存的起始地址,也就是 0,0 处的字符所在的地址,那么 x,y 处的
字符位置在哪儿呢?因为一行显示80个字符,共有25行,所以我们可以用如下的公式计算
出 x,y 处的字符在内存中的地址:0xb8000 + y*80 + x 
因此如果你想在 x,y处显示一个红色的‘A’你可以这样做
char *video = 0xb8000 ;
video += y*80 + x ;
*video = 'A' ;
video++;
*video = 0x04 ;

下面我们看看怎样处理光标
首先,我们要知道我们有两个端口,端口号分别是0x3d4,0x3d5。这第一个端口用于提供
索引,第二个端口用于提供数据。光标的位置存放在以14,15为索引值的两个端口寄存器中
。每一个端口寄存器只有8位,14号寄存器放存光标的低8位,15号寄存器存放光标的高8位

比如,我们要把光标定位到 x,y 处,首先我们要得到此处的偏移量:offset = y * 8 + x 

然后把低8位放到 14号寄存器里,高8位放到15号寄存器里,就像这样:
out 0x3d4 , 14 ;//指定访问14号寄存器
out 0x3d5 , offset >> 8 ;
out ox3d4 , 15 ;
out 0x3d5 , offset ;
(注,这不是最终可执行的汇编代码,只是一个示意代码,实际代码请参考源程序)
要得到光标位置可以读这两个寄存器的值,得到偏移量,然后换算成x,y,详情请参看源程
序。

下面,我们将进入主题,将讲述一下怎样处理键盘输入。
处理键盘输入有两种方式,一是通过循环就行,在主程序中不断的查询0x60端口是否有数
据,如果有数据则表示有键盘输入,且此数据就是输入的键的键盘扫描码,将扫描码转换
成相应的ASCII码,然后显示就行。 这种情况非常简单,我们就不详细描述了,你可以改
动本源程序用此种方式实现。这里,我们常用一种新的方式进行,这就是通过中断进行。

要使用中断方式,我们就必须编写自己的中断处理程序,并且要让CPU知道此中断的中断处
理程序在什么地方,这通过IDT(中断描述符表)完成。此表的每一个表项对应一个中断,
每一个表项都指明此中断的中断处理程序在什么地方。因此首要的任务是要构造一个中断
描述符表。

中断描述符表一共可有256项,即256个中断。头三十二项,也就是0~31号中断,已经被CPU
及硬件所占用了,因此我们需要从第三十三项即32号中断开始构造我们自己的中断及中断
服务程序
中断描述符每项占8个字节(64位),所以我们定义如下的一个结构来处理它:
typedef struct{
  unsigned long dword0 ;
  unsigned long dword1 ;
} segment_desc ;
下面是我们定义中断描述符表的程序:
  segment_desc idt[ 256 ] ; /* 中断号 0~255 */
  unsigned long idt_desc[ 2 ] ;
  unsigned long idt_addr ;
  unsigned long keyboard_addr ;
  unsigned long idt_offset = 0x8 ; /* IDT 在 GDT 中的位置,此程序中也是代码段在
                                      GDT中的位置 */

  // 发送4个ICW
  ToPort( 0x20 , 0x11 ) ;
  ToPort( 0xA0 , 0x11 ) ;
  ToPort( 0x21 , 0x20 ) ;
  ToPort( 0xA1 , 0x28 ) ;

  ToPort( 0x21 , 0x4 ) ;  // 在 Inter 出产的硬件中,PIC之间的联系是
  ToPort( 0xA1 , 0x2 ) ;  // 把 PIC1的IRQ2 同 PIC2 的IRQ1 联系起来

  ToPort( 0x21 , 0x1 ) ;
  ToPort( 0xA1 , 0x1 ) ;

  // 下面设定中断屏蔽字,只许可键盘中断
  ToPort( 0x21 , 0xFD ) ;
  ToPort( 0xA1 , 0xFF ) ;

  keyboard_addr = ( unsigned long )keyboard_interrupt ; /* 键盘中断处理程序
                                                           的位置 */
  idt[ 0x21 ].dword0 = ( keyboard_addr & 0xffff ) | ( idt_offset << 16 ) ;
  idt[ 0x21 ].dword1 = ( keyboard_addr & 0xffff0000 ) | 0x8e00 ;

  /* 得到整个IDT的位置描述 */
  idt_addr = ( unsigned long )idt ;
  idt_desc[ 0 ] = 0x800 + ( ( idt_addr & 0xffff ) << 16 ) ;
  idt_desc[ 1 ] = idt_addr >> 16 ;

  __asm__( "lidt %0/n""sti" : "=m"( idt_desc ) ) ; /* 用lidt指令载入 IDT 表,
                                                      并用 sti 指令开中断*/
下面我们来解释一下这个程序
ToPort是程序定义的一个函数,具体代码请见源程序,这里只需要知道,它把由第二个参
数指定的数据,发到由第一个参数指定的端口中去。
先来看看这两行
  idt[ 0x21 ].dword0 = ( keyboard_addr & 0xffff ) | ( idt_offset << 16 ) ;
  idt[ 0x21 ].dword1 = ( keyboard_addr & 0xffff0000 ) | 0x8e00 ;
 一个 IDT 表项有64位长,0~15位是中断处理程序地址的低16位,16~31是中断处理程序所
在的段在GDT中的位置(参见第二篇)。
最高的16位是中断处理程序地址的高16位,而留下的中间的16位是用来表明此是一个中断
门还是一个陷阱门还是一个任务门,及是16位中断还是32位中断等,非常复杂,要想详细
了解请查看Intel CPU 开发人员手册(网上有下的,没找到的可以找我要)。幸运的是,
我们不需要管得太多,只需记住在正常情况下是置为0x8e00就行。
 
下面我们详细讲一下代码中的“// 发送4个ICW”部份

我们现在已经知道,要建立中断,我们需要填充 IDT( 中断描述符表),它需要指出当发
生中断时,应跳到哪儿去执行。

为了使中断系统起作用,我们需要对PIC(可编程的中断控制器)进行编程,PIC 是可编程
的中断控制器,它可以处理硬件中断请求(IRQ0,IRQ1..等等),如果没有PIC,我们就不得
不按规则去查询哪一个硬件发生了中断,有了PIC,当硬件发生中断时,PIC把中断信号送
到CPU,CPU处理中断。我们实际上有两上PIC,第一个PIC1(端口号0x20~0x21)处理IRQ0~IR
Q7的请求,第二个PIC2(端口号0xA0~0xA1)处理 IRQ8~IRQ15 的请求

CPU只知道逻辑意义上的中断,不区分是物理上的软件中断还是硬件中断,因此我们必须把
CPU不知道的物理中断,映射为CPU知道的逻辑意义上的中断。在实模式下,这项工作由BIO
S来做,在保护模式下需要我们自己做。

因此我们需要初使化PICs,这通过发送一些ICW(初始化命令字)来实现对PICs的控制,它
们必须被精确的依次序发送,因为,它们之间是相互依赖的
1. 发送 ICW1 到 PIC1(20h) 与 PIC2(A0h) 中
2. 发送 ICW2 到 PIC1(21h) 与 PIC2(A1h) 中
3. 发送 ICW3 到 PIC1(21h) 与 PIC2(A1h)中
4. 发送 ICW4 到 PIC1(21h) 与 PIC2(A1h)中

ICW1 用来告诉PIC, 存在ICW4,(当两个PIC串联工作时,这是必须的)
ICW2 用来告诉PIC,把 IRQ0 与 IRQ7 映射到什么地方
     (每一个PIC有八个管脚处理中断(IRQ0~IRQ7)
ICW3 用来告诉PIC,它们之间应当用几号IRQ(第几根管脚)进行同信
ICW4 用来告诉PIC,我们工作在保护模式,并且是由软件来处理还是自动处理中断

ICW1的结构
7   6   5   4   3   2   1   0
0   0   0   1   M   0   C   I

I : 如果 ICW3 后面还有 ICW4,则置位
C : 如果不置位,表明这两个 PIC 工作在串联状态下
M : 表明 IR0 到 IR7 的线是水平触发,在PC机中,这位应为0(边沿触发)

ICW2 表明了 IRQ0 在中断向量表中的地址,在保护模式下,你应当改变它
7   6   5   4   3   2   1   0
A7  A6  A5  A4  A3  0   0   0

IRQ1 在中断向量表中的地址为 IRQ0的+1,IRQ2~IRQ7以此类推

ICW3 只在 这两个PIC是在串联工作状态下才被发送(ICW1 的C位置0),它的目的是在两
个PIC间建立联系

ICW3 关于 PIC1 的结构
7   6   5   4   3   2   1   0
IR7 IR6 IR5 IR4 IR3 IR2 IR1 IR0

如果 IR0 是置0的,则表明此根线联到一个外围设备
如果 IR0 是置1的,则表明此根线与PIC2联结
其余的与此类似

ICW3 关于 PIC2 的结构
7   6   5   4   3   2   1   0
0   0   0   0   I            R          Q
最后3位是PIC1的,与PIC2相联结的IRQ号

ICW4 的结构
7   6   5   4   3   2   1   0
0   0   0   0   0   0   EOI 80x86
EOI 表明中断的最后是自动处理还是由软件辅助处理。在PC中此位通常置0,表示软件必须
处理中断的最后扫尾工作
80x86 表明PIC是否工作在80x86的体系结构下

有了上述基础知识,你现在应当可以理解了吧。
下面我们再来看看:中断屏蔽字
PIC 1 处理的中断有
0 系统时钟
1 键盘
2 重定向到IRQ9 (PIC2的IRQ1),如果此位被置1,则屏幕掉所有来自PIC2的中断
3 串口 1 (COM2/4)
4 串口 2 (COM1/3)
5 声卡
6 软驱
7 并行端口

PIC 2 处理的中断有
0 实时时钟
1 来自 IRQ2 (PIC1)
2 保留
3 保留
4 鼠标
5 数学协处理器
6 硬盘
7 保留

当某位置位0表示允许其发出中断请求,置为1表示屏蔽其中断请救
程序中,有这样两行代码:
ToPort( 0x21 , 0xFD ) ;
ToPort( 0xA1 , 0xFF ) ;
其中 0xFD 就是 11111101 ;即只允许 PIC1的第二个中断请求,即键盘中断请求。

完工!本篇任务已经胜利完成~~~ ^_^,所有源代码可在如下地址下载
ftp://202.118.239.46/Incoming/Other/BTC/temp/pyos/pyos3.zip
BTW:
在本实验进行的过程中,在BBS上得到了很多老师同学的鼓励,正是由于这种支持力量的
存在,使我获得了将本实验进行下去的力量,在此深表感谢!同时对于此中不计其数的错
误,非常希望各位老师、同学、大牛小牛:P~~,批评指教!
仍然留个mail:
xieyubo@126.com  

原文:http://purec.binghua.com/Article/ShowArticle.asp?ArticleID=5

Now, we have a way to perceive that the user is pressing a key. We know when we want to move up, down, left, and right. We know at each iteration of the main loop exactly, what the user wants; we just have to update the circle with a new position depending on this input. This method gives us a great advantage. So finally we can write something in our update() function, namely, the movement of our player. We check which of the four Boolean member variables is true, and determine the movement accordingly. By using += (instead of =) and if (instead of else if), we implicitly handle the case where two opposite keys, such as right and left are pressed at the same time—the movement stays zero. The update() function is shown in the following code snippet: void Game::update() { sf::Vector2f movement(0.f, 0.f); if (mIsMovingUp) movement.y -= 1.f; if (mIsMovingDown) movement.y += 1.f; if (mIsMovingLeft) movement.x -= 1.f; if (mIsMovingRight) movement.x += 1.f; mPlayer.move(movement); } We introduce two new things here: a vector and the move() function on the circle shape. The move() function does what its name says, it moves the shape by the amount we provide it. Vector algebra Vectors are an important part of algebraic mathematics. They imply lots of rules and definitions, which go beyond the scope of our book. However, SFML&#39;s sf::Vector2 class template is way more practical, both in concept and functionality. To be as simple as we could possibly be, we know that a coordinate in a two-dimensional Cartesian system would need two components: x and y. Because in graphics all coordinates are expressed with the decimal float data type, sf::Vector2 is instantiated as sf::Vector2<float>, which conveniently has a typedef named sf::Vector2f. Such an object is made to contain two member variables, x and y. This makes our life simpler, because now we don&#39;t need to pass two variables to functions, as we can fit both in a single sf::Vector2f object. sf::Vector2f also defines common vector operations, such as additions and subtractions with other vectors, or multiplications and divisions with scalars (single values), effectively shortening our code.翻译,要求每行中英文对照
最新发布
03-08
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值