[OS개발] 8일차 - 움직여줘 마우스!
8일차
7일차에서 마우스가 보내는 데이터를 FIFO에 훌륭히 저장했으므로 드디어 움직이는 처리를 해본다!
# 일단 데이터부터 분석해볼까!
/* bootpack.c 메인루프 중 일부 */ } else if (fifo8_status(&mousefifo) != 0) { i = fifo8_get(&mousefifo); io_sti(); if (mouse_phase == 0) { /* 마우스의 0xfa를 기다리고 있는 단계 */ if (i == 0xfa) { mouse_phase = 1; } } else if (mouse_phase == 1) { /* 마우스의 1바이트째를 기다리고 있는 단계 */ mouse_dbuf[0] = i; mouse_phase = 2; } else if (mouse_phase == 2) { /* 마우스의 2바이트째를 기다리고 있는 단계 */ mouse_dbuf[1] = i; mouse_phase = 3; } else if (mouse_phase == 3) { /* 마우스의 3바이트째를 기다리고 있는 단계 */ mouse_dbuf[2] = i; mouse_phase = 1; /* 데이터가 3바이트 모였으므로 표시 */ sprintf(s, "%02X %02X %02X", mouse_dbuf[0], mouse_dbuf[1], mouse_dbuf[2]); boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 32 + 8 * 8 - 1, 31); putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s); } }
위와 같은 코드를 짜서 일단 0xfa가 들어오기를 기다린다. 이는 마우스가 들어온다고 신호하는 것일 뿐이므로 넘기고 phase를 1로 만든다. phase 1에서는 마우스가 보내는 3개의 데이터중 첫번째를 받아 데이터 버퍼에 넘긴다. 같은 방법으로 phase 2, 3 까지 처리한다. phase 3에서는 다시 phase 1으로 되돌려서 이후 들어오는 데이터를 3개씩 묶어 계속 처리한다.
마우스 데이터 삼형제
이제 마우스를 이리저리 움직여서 데이터를 분석해 본다.
먼저 첫번째 데이터를 보면 앞자리는 마우스가 움직이는 방향을 나타내는 것 같다.
이를 2진법으로 나타내면 위(0)아래(1)를 앞비트, 좌(1)우(0)를 뒷비트로 정의 할 수 있다. 그래서 왼쪽 아래로 이동할시 11(2)이 되어 0x3이 나타나는 것이다.
이어서 첫번째 데이터의 뒷자리는 마우스의 클릭과 관련되있는 것 같다.
(1)(가운데)(오른)(왼)
기본 비트 1에 가운데 클릭, 오른쪽 클릭, 왼쪽 클릭 순으로 비트가 정해져 있는듯 하다.
ex) 왼클릭+가운데클릭 = 1101 = 0xD, 왼클릭+오른클릭 = 1011 = 0xB
다음으로 2번째 데이터이다. 2번째 데이터는 마우스의 좌우 이동량을 나타내는 것으로 보인다.
오른쪽으로 이동하면 0x01 ~ 0x41까지 이므로 바이트 단위 기준 10진법으로는 1 ~ 65이다.
왼쪽쪽으로 이동하면 0xFF ~ 0xBE까지 나타나는데 이는 -1 ~ -66의 범위이다.
3번째 데이터도 비슷하나 방향은 위아래이고 범위가 다르다.
윗방향으로는 0x01 ~ 0x49(1 ~ 73), 아랫방향으로는 0xFF~0xB8(-1 ~ -72)이다.
위의 분석결과를 이용하여 이쁘게 함수를 만들면 다음과 같다.
int mouse_decode(struct MOUSE_DEC *mdec, unsigned char dat) { if (mdec->phase == 0) { /* 마우스의 0 xfa를 기다리고 있는 단계 */ if (dat == 0xfa) { mdec->phase = 1; } return 0; } if (mdec->phase == 1) { /* 마우스의 1바이트째를 기다리고 있는 단계 */ if ((dat & 0xc8) == 0x08) { /* 올바른 1바이트째였다 */ mdec->buf[0] = dat; mdec->phase = 2; } return 0; } if (mdec->phase == 2) { /* 마우스의 2바이트째를 기다리고 있는 단계 */ mdec->buf[1] = dat; mdec->phase = 3; return 0; } if (mdec->phase == 3) { /* 마우스의 3바이트째를 기다리고 있는 단계 */ mdec->buf[2] = dat; mdec->phase = 1; mdec->btn = mdec->buf[0] & 0x07; mdec->x = mdec->buf[1]; mdec->y = mdec->buf[2]; if ((mdec->buf[0] & 0x10) != 0) { mdec->x |= 0xffffff00; } if ((mdec->buf[0] & 0x20) != 0) { mdec->y |= 0xffffff00; } mdec->y = - mdec->y; /* 마우스에서는 y방향의 부호가 화면과 반대 */ return 1; } return -1; /* 여기에 올 일은 없을 것 */ }
phase 1에서 첫 번째 데이터가 정상적인 범위내에서 전달되었는지를 확인한다. 0xc8 = 1100 1000 이므로 앞자리는 1~3이 아니면 &0xc에서 0x0이 아니게 되고 뒷자리는 8~F여야만 &0x8에서 0x8을 넘기므로 저러한 조건문이 나온다. 레알 눈돌아가는 발상이다.
x, y가 음수가 되는 경우는 따로 처리하여 4Byte 값을 넘겨도 음의 값이 유지되도록 하였다. 이에 관해서는 2진법에서의 보수에 관한 글을 읽어보면 도움이 될 것이다. (링크: 위키피디아 - 2의보수법)
그리고 메인 함수에서는 이쁘게 그린다.
/* 방금전의 루프를 수정 */ } else if (fifo8_status(&mousefifo) != 0) { i = fifo8_get(&mousefifo); io_sti(); if (mouse_decode(&mdec, i) != 0) { /* 데이터가 3바이트 모였으므로 표시 */ sprintf(s, "[lcr %4d %4d]", mdec.x, mdec.y); if ((mdec.btn & 0x01) != 0) { s[1] = 'L'; } if ((mdec.btn & 0x02) != 0) { s[3] = 'R'; } if ((mdec.btn & 0x04) != 0) { s[2] = 'C'; } boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 32 + 15 * 8 - 1, 31); putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s); } }
# 그리고 고대하던 무브먼트의 시간!
방금의 그리기 루프에 다음 코드를 추가한다.
/* 마우스 커서의 이동 */ boxfill8(binfo->vram, binfo->scrnx, COL8_008484, mx, my, mx + 15, my + 15); /* 마우스 지운다 */ mx += mdec.x; my += mdec.y; if (mx < 0) { mx = 0; } if (my < 0) { my = 0; } if (mx > binfo->scrnx - 16) { mx = binfo->scrnx - 16; } if (my > binfo->scrny - 16) { my = binfo->scrny - 16; } sprintf(s, "(%3d, %3d)", mx, my); boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 0, 79, 15); /* 좌표 지운다 */ putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, s); /* 좌표 쓴다 */ putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16); /* 마우스 그린다 */
데이터만 받을 수 있다면 실제 움직이는 처리는 어렵지 않...
뭐 테스크 바를 처음에 한번만 그리니 당연한가... 마우스 배경도 처리 안했고...
우선은 책 순서에 맞추어 넘어가도록 하자.
# 남은 페이지에서는 지금까지 미지의 세계로 남아있던 asmhead.nas를 분석하였다.
asmhead.nas 옆에 두고 보면 좋다
우선 16 라인까지는 필요한 상수를 설정하고 ORG 명령을 실행한다. bootpack.hrb가 로드 될 위치라던가 플로피디스크를 로드할 장소 및 전에 보았던 BOOT_INFO가 설정되있다.
이후 32 라인까지는 그래픽 모드를 설정하고 LED를 설정하는데 쓰인다. 필요한 값을 저장하기도 한다.
; PIC가 일절의 인터럽트를 받아들이지 않게 한다 ; AT호환기의 사양에서는 PIC의 초기화를 한다면, ; 이것들을 CLI앞에 해 두지 않으면 이따금 행업 한다 ; PIC의 초기화는 나중에 한다 MOV AL,0xff OUT 0x21,AL NOP ; OUT명령을 연속하면 잘 되지 않는 기종이 있는 것 같기 때문에 OUT 0xa1,AL CLI ; CPU레벨에서도 인터럽트 금지
44 라인까지는 32bit모드로의 전환 중에 인터럽트가 오면 곤란하므로 PIC를 비활성화 해두는 명령어이다. 각각 마스터, 슬레이브를 막아두는 명령이다. 그리고 CLI 까지 실행하여 인터럽트를 ★완전봉쇄★
; CPU로부터 1MB이상의 메모리에 액세스 할 수 있도록, A20GATE를 설정 CALL waitkbdout MOV AL,0xd1 OUT 0x64,AL CALL waitkbdout MOV AL, 0xdf ; enable A20 OUT 0x60,AL CALL waitkbdout
다음은 54 라인까지 KBC(키보드 컨트롤러)를 건드리고 있다. 0xd1로 길을 뚫고 0xdf를 보내서 A20GATE를 활성화하여 메모리를 1MB을 사용할 수 있게 만든다고 생각하면 된다. 중간중간 waitkbdout으로 커맨드가 확실하게 보내지도록 한다.
; 프로텍트 모드 이행 [INSTRSET "i486p"] ; 486명령까지 사용하고 싶다고 하는 기술 LGDT [GDTR0] ; 잠정 GDT를 설정 MOV EAX,CR0 AND EAX, 0x7fffffff ; bit31를 0으로 한다(페이징 금지를 위해) OR EAX, 0x00000001 ; bit0를 1로 한다(프로텍트 모드 이행이므로) MOV CR0,EAX JMP pipelineflush pipelineflush: MOV AX,1*8 ; read, write 가능 세그먼트(segment) 32bit MOV DS,AX MOV ES,AX MOV FS,AX MOV GS,AX MOV SS,AX
58 라인에서는 486이상 CPU의 명령어를 쓸 수 있도록 설정하고 그 다음에 LGDT로 뭔가 불러오는데 이건 이따가.
그리고 61 라인부터 CR0(Control Register) 레지스터의 값을 31번째 비트를 0, 0번째 비트를 1로 만든다. 이것은 '페이징을 사용하지 않는 프로텍트 모드'로 진입하는 것으로써 지금까지 16을 곱해서 주소를 표시하는데 쓰이던 ES등 세그먼트 레지스터를 GDT에 등록된 세그먼트를 가리켜서 사용하도록 만든다. 이로 인해 애플리케이션이 OS전용인 세그먼트를 건드리지 못하도록 '프로텍트'한다.
프로텍트 모드로 전환한 후에는 JMP명령을 통해 기계어를 다시 해석하도록 만들어야 한다. pipelineflush로 점프 후 CS를 제외한 세그먼트 레지스터들을 초기화한다.
; bootpack의 전송 MOV ESI, bootpack ; 전송원 MOV EDI, BOTPAK ; 전송처 MOV ECX,512*1024/4 CALL memcpy ; 하는 김에 디스크 데이터도 본래의 위치에 전송 ; 우선은 boot sector로부터 MOV ESI, 0x7c00 ; 전송원 MOV EDI, DSKCAC ; 전송처 MOV ECX,512/4 CALL memcpy ; 나머지 전부 MOV ESI, DSKCAC0+512; 전송원 MOV EDI, DSKCAC+512 ; 전송처 MOV ECX,0 MOV CL,BYTE [CYLS] IMUL ECX,512*18*2/4 ; 실린더수로부터 바이트수/4로 변환 SUB ECX,512/4 ; IPL분만큼 공제한다 CALL memcpy
74 라인 부터는 단순한 복사이다. bootpack, 부트섹터, 그외의 실린더 데이터를 메모리에 로드하기 위해 memcpy 함수를 호출하고 있다. memcpy는 125 라인에 정의되어 있다.
; bootpack의 기동 MOV EBX,BOTPAK MOV ECX,[EBX+16] ADD ECX, 3 ; ECX += 3; SHR ECX, 2 ; ECX /= 4; JZ skip ; 전송해야 할 것이 없다 MOV ESI,[EBX+20] ; 전송원 ADD ESI,EBX MOV EDI,[EBX+12] ; 전송처 CALL memcpy skip: MOV ESP,[EBX+12] ; 스택 초기치 JMP DWORD 2*8:0x0000001b
다음으로 103 라인에서 bootpack의 기동을 시작한다. 이 부분은 나중에 자세히 알아보는 대신 새로운 명령어나 익혀보자.
SUB(SUBstract) : 빼기!
SHR(SHift Right) : 오른쉬프트 연산자(>>)이다.
JZ(Jump if Zero) : 비교값이 0이면 점프한다.
ALIGNB : 번지수 정리 명령어(좀 있다 설명한다)
144 라인에서는 스택 포인터를 초기화 하고 있다. 그리고 2*8:0x0000001b로 점프하는데 이는 2번세그먼트의 0x1b 주소이다.
waitkbdout와 memcpy는 생략하고...
ALIGNB 16 GDT0: RESB 8 ; null selector DW 0xffff, 0x0000, 0x9200, 0x00cf ; read/write 가능 세그먼트(segment) 32bit DW 0xffff, 0x0000, 0x9a28, 0x0047 ; 실행 가능 세그먼트(segment) 32 bit(bootpack용) DW 0 GDTR0: DW 8*3-1 DD GDT0 ALIGNB 16 bootpack:
135 라인에 ALIGNB라는 명령어가 보이는데 이녀석은 주어진 숫자로 주소가 나눠지게끔 깔끔하게 정리하는 역할을 한다. 그래서 GDT0이 작성되기전에 바이트를 0으로 채워서 정리한다. 136 라인에서 GDT를 임의로 등록하는 과정을 볼 수 있다. GDT0은 null로 초기화하고 1번과 2번을 이전에 c언어로 작성한 부분을 먼저 등록하고 있었다. 아까 지나쳤던 GDTR이 요기있네? GDT0를 넘겨 GDT를 등록하도록 한다.
이것으로 asmhead.nas 를 좀더 알아보았다. 아직도 비밀이 많은 코드지만 오늘은 여기까지. 여기서 정지시킨 인터럽트는 main 함수에서 sit로 풀리게 된다.
30일 프로젝트인데 이러다 300일 걸리게 생겼다
참조: <OS 구조와 원리> - 카와이 히데미