32位装载程序 - 基础视频驱动
1. 首先面临的问题
从16位装载程序切换到32位装载程序后,就没有BIOS可用了,原因就不多解释了,如果不服,你可以试试直接在32位保护模式下调用BIOS,让事实教育教育你。你会发现,想在屏幕输出字符都不行了。因为没有现成的API给你用,所以,进入32位保护模式后,首先面临的问题就是:一切从零开始。
2. 最先需要实现的模块 - 基础视频驱动
无论你想先开始干什么,你都需要看到结果,难道你认为你可以在什么都看不到的情况下,就能确认程序运行正确了?
所以,无论你想要干什么,你都要先想办法让自己能看到运行的信息,要看到信息,最直接的当然是在显示器上输出信息了,所以最先要实现的是视频驱动,当然这个视频驱动不需要很强大的功能,只需要具备几个输出文本信息的基础功能就可以了,所以我把它叫做:基础视频驱动(Basic Video Driver, BVD),BVD让自己能看到程序运行的信息。
如果你非要抬杠说Bochs能看到寄存器内容之类的调试信息,好吧,那你就先不要实现视频输出功能,就去看Bochs那个调试功能吧。
3. 最急需的功能 - 输出字符串
注意啊,最亟需的功能是输出字符串,并不是格式化输出字符串,虽然格式化输出字符串很方便,但却是不是最基础的,也不应该划入BVD的范围。
在程序运行过程中,给出符号信息是最简单的,随便在屏幕上给出一个符号信息就行了。但这在程序较复杂后,要通过少数的几个符号或字母来识别信息,就不合适了。要给出比较容易识别,实现还比较简单的方法,就是输出文字了。
4. 基础视频驱动(Basic Video Driver, BVD)
虽然BVD在正式的系统中也要用,但是正式的中需要更完整一些,所以就不考虑代码共享的事情了,这里就按最简单的功能来写。
5. CRT基础
视频基础知识就约等于VGA的知识了,引导阶段,不用搞图形,只需要处理字符界面即可,所以关注点就变成关注CRT了,CRT又是VGA的一个子集。
在现阶段,只要知道几个CRT寄存器和字符视频内存基址就差不多了,其他的,可以当作用不到。具体的内容就放到书里了。
控制显存起始位置
显存的物理起始地址是0x000B8000,但是实际的显示起始地址并不一定是从0x000B8000开始的,实际的显示起始地址=显存基址 + 显示起始偏移,所以需要提供一个操作显存起始偏移的功能。
其实,为了简单,我并不使用显示起始偏移,也就是把显示起始偏移当作0来处理。
#define CRT_REG_ADDR ((void *)0x03D4)
#define CRT_REG_DATA ((void *)0x03D5)
//
/* CRT Controller Register, CCR */
#define CCR_IDX_START_ADDR_H (0x0C)
#define CCR_IDX_START_ADDR_L (0x0D)
#define CCR_IDX_CURSOR_LOC_H (0x0E)
#define CCR_IDX_CURSOR_LOC_L (0x0F)
void IO_OutB(void * pPort, byte_t nData)
{
__asm{
mov edx, pPort;
mov al, nData
out dx, al
}
}
//
byte_t IO_InB(void * pPort)
{
byte_t nData;
__asm{
mov edx, pPort;
in al, dx
mov nData, al
}
return nData;
}
//
uint_t CRT_GetVdoBufStart(void)
{
byte_t nHigh;
//
IO_OutB(CRT_REG_ADDR, CCR_IDX_START_ADDR_H);
nHigh = IO_InB(CRT_REG_DATA);
//
IO_OutB(CRT_REG_ADDR, CCR_IDX_START_ADDR_L);
return ((uint_t)nHigh << 8) | IO_InB(CRT_REG_DATA);
}
//
void CRT_SetVdoBufStart(uint_t nStartAddr)
{
IO_OutB(CRT_REG_ADDR, CCR_IDX_START_ADDR_H);
IO_OutB(CRT_REG_DATA, (byte_t)(nStartAddr >> 8));
//
IO_OutB(CRT_REG_ADDR, CCR_IDX_START_ADDR_L);
IO_OutB(CRT_REG_DATA, (byte_t)nStartAddr);
}
控制光标及显示属性
在CRT中,光标的位置并不是以横纵坐标来表示的,而是使用一维坐标来表示的,对应的换算方法是:一维坐标(cursor) = 二维横坐标(x) + 二维纵坐标(y) * 水平分辨率
对应于一般的80*25的字符界面,cursor = x + y * 80
实际能看到的光标显示位置是光标位置 - 显示起始偏移,如果显示起始偏移是0,那么光标位置就是实际的光标显示位置
uint_t nCursorLoc = 0;
byte_t nAttr;
byte_t CRT_GetAttr(void)
{
return nAttr;
}
//
byte_t CRT_SetAttr(byte_t nNewAttr)
{
byte_t nOldAttr = nAttr;
//
nAttr = nNewAttr;
return nOldAttr;
}
//
uint_t CRT_GetCursorLoc(void)
{
byte_t nHigh;
//
IO_OutB(CRT_REG_ADDR, CCR_IDX_CURSOR_LOC_H);
nHigh = IO_InB(CRT_REG_DATA);
//
IO_OutB(CRT_REG_ADDR, CCR_IDX_CURSOR_LOC_L);
return ((uint_t)(nHigh) << 8) + IO_InB(CRT_REG_DATA);
}
//
void CRT_SetCursorLoc(uint_t nCursorLocation)
{
IO_OutB(CRT_REG_ADDR, CCR_IDX_CURSOR_LOC_H);
IO_OutB(CRT_REG_DATA, (byte_t)(nCursorLocation >> 8));
//
IO_OutB(CRT_REG_ADDR, CCR_IDX_CURSOR_LOC_L);
IO_OutB(CRT_REG_DATA, (byte_t)nCursorLocation);
}
//
void CRT_GetCursorXY(uint8_t * pX, uint8_t * pY)
{
*pX = nCursorLoc % 80;
*pY = nCursorLoc / 80;
}
//
void CRT_SetCursorXY(uint8_t nX, uint8_t nY)
{
if( nX >= 80 || nY >= 25 )
return ;
nCursorLoc = nY * 80 + nX;
CRT_SetCursorLoc(nCursorLoc);
}
6. 我的BVD实现的几个功能
我的BVD和核心功能就是输出字符串,在指定位置输出字符串和在光标位置输出字符串
定义视频对象
我是做了一个前提条件,那就是在16位初始化的时候,已将视频系统设置为文本模式,分辨率是80*25,BVD就以这个为基础开发。如果不是这个模式?哦,16位的装载程序会设置的。如果非要说设置失败了?那就显示错乱了。
在指定位置输出字符串 BVD_PrintAt
在BVD中,仅处理两个回车和换行控制字符,其他的控制字符当作普通字符处理。
#define VBUF ((word_t *)0x000B8000)
void BVD_PrintAt(uint8_t nX, uint8_t nY, byte_t nAttr, const char * str,
int nLen)
{
char c;
int i;
word_t * pVBuf = VBUF;
uint_t nLoc = 80 * nY + nX;
word_t nChar = nAttr << 8;
//
if( nLen == 0 )
return ;
if( nLen < 0 )
goto c_string;
//
for( i = 0; i < nLen; i++ ){
c = *str++;
switch( c ){
case '\n':
nLoc += 80;
case '\r':
nLoc -= nLoc % 80;
break;
default:
pVBuf[nLoc++] = nChar | c;
break;
}
if( nLoc >= 80*25 ){
BVD_Scroll( (nLoc - 80 * 25) / 80 + 1);
nLoc -= ((nLoc - 80 * 25) / 80 + 1) * 80;
}
}
pVBuf[nLoc] = nChar | ' ';
nCursorLoc = nLoc;
CRT_SetCursorLoc(nCursorLoc);
return ;
//
c_string:
while( *str ){
c = *str++;
switch( c ){
case '\n':
nLoc += 80;
case '\r':
nLoc -= nLoc % 80;
break;
default:
pVBuf[nLoc++] = nChar | c;
break;
}
if( nLoc >= 80*25 ){
BVD_Scroll( (nLoc - 80 * 25) / 80 + 1);
nLoc -= ((nLoc - 80 * 25) / 80 + 1) * 80;
}
}
pVBuf[nLoc] = nChar | ' ';
nCursorLoc = nLoc;
CRT_SetCursorLoc(nCursorLoc);
}
在光标位置输出字符串 BVD_Print
方法就是,先获得光标位置,然后在光标位置输出字符串。
void BVD_Print(const char * str, byte_t nAttr)
{
uint8_t nX, nY;
CRT_GetCursorXY(&nX, &nY);
BVD_PrintAt(nX, nY, nAttr, str, -1);
}
6. 格式化输出字符串
这个并不是BVD的功能,只是因为它会被大量用到,所以就和BVD放在一些写,
格式化字符串 LDR_NVSPrintf
功能和vsprintf一样的,只是没有提供浮点数支持。一个函数就有200多行,就不贴了,只贴一个函数原型
int LDR_NVSPrintf(char * buffer, const char * fmt, va_list al, int bufsize);
格式化输出 LDR_Printf
函数提供的缓冲区只有256个字节,也就是说,每次最多输出255个字符。也许会有人局的缓冲区太少了,其实,在引导程序中,用得着一次输出那么长的信息吗?如果真的需要,好吧,那你就分开几次来输出。
int LDR_Printf(const char * fmt,...)
{
static byte_t buf[256];
va_list arglist;
int len;
//
va_start(arglist,fmt);
len = LDR_NVSPrintf(buf, fmt, arglist, 256);
va_end(arglist);
BVD_Print(buf, 0x07);
//
return len;
}