반응형

지금까지 I2C, SPI, UART, ADC, PWM 글을 하나씩 정리했다. 그런데 PWM을 파다 보니 결국 그 뒤에 있던 건 타이머였더라. 그래서 이번엔 미뤄뒀던 타이머 / 인터럽트를 제대로 판다. 사실 이거 두 개는 마이컴의 진짜 기본기라서, 여기까지 오면 기초 시리즈가 한 바퀴 도는 느낌이다.

왜 타이머 + 인터럽트인가

LED를 1초마다 깜빡이는 코드를 처음 짤 땐 다들 이렇게 짠다.

while(1)
{
    LED = 1;
    delay_ms(1000);   // 1초 기다림
    LED = 0;
    delay_ms(1000);   // 또 1초 기다림
}

동작은 한다. 그런데 문제는 저 delay_ms(1000) 안에서 CPU가 진짜 아무것도 안 하고 멍때린다는 거다. 1초 동안 그냥 숫자만 세면서 시간을 죽이고 있는 거다. 버튼도 못 읽고, 센서도 못 보고, 통신도 못 한다. LED 하나 깜빡이겠다고 CPU를 통째로 붙잡아두는 셈이다.

비싼 CPU를 스톱워치로 쓰는 격

여기서 타이머 + 인터럽트가 등장한다. 이 둘을 쓰면 CPU는 딴 일을 하면서도 정확한 타이밍에 LED를 깜빡일 수 있다. 시간 재는 건 타이머(하드웨어)한테 맡기고, 시간 다 되면 인터럽트가 "야, 됐어!" 하고 CPU를 잠깐 불러주는 방식이다.

타이머부터 — 그냥 숫자 세는 기계

타이머(Timer)는 CPU와 별개로 도는 하드웨어 카운터다. 하는 일은 단순하다. 클럭 펄스가 하나 들어올 때마다 카운터 값을 1씩 올린다. 그게 전부다. CPU가 while문 돌리든 말든 상관없이 얘는 자기 페이스로 계속 숫자를 센다.

그 카운터 값이 담기는 레지스터가 TCNT(Timer/Counter)다. 예를 들어 8비트 타이머라면 TCNT는 0부터 시작해서 1, 2, 3… 이렇게 올라가다가 255(8비트 최댓값)까지 간다. 그 다음 한 번 더 세면? 담을 자리가 없으니 0으로 뚝 떨어진다. 이걸 오버플로(overflow)라고 한다.

  • TCNT : 지금 몇까지 셌는지 들어있는 레지스터. 0 → 255 → 0 → 255 … 반복.
  • 오버플로 : 255에서 한 번 더 세서 0으로 넘칠 때. 이 순간 "이벤트"가 발생한다.
  • 컴페어 매치(compare match) : 미리 정해둔 비교값과 TCNT가 같아지는 순간. 이것도 이벤트다. (끝까지 안 가고 중간 특정 값에서 알림받고 싶을 때 쓴다.)

TCNT는 0에서 TOP(255)까지 올라갔다가 오버플로로 0으로 뚝 떨어진다. 이 톱니를 무한 반복한다.

여기서 자연스럽게 드는 의문. "그럼 8비트 타이머는 255밖에 못 세는데, 1초 같은 긴 시간은 어떻게 재?" 좋은 질문이다. 그래서 두 가지 장치가 있다.

① 프리스케일러로 세는 속도를 늦춘다

프리스케일러(prescaler)는 클럭을 분주(分周)한다. 쉽게 말해 클럭을 느리게 만들어서 타이머한테 넣어주는 거다. CPU 클럭이 그대로 들어가면 타이머가 너무 빨리 넘쳐서 시간 재기가 힘드니까, 중간에서 "8번에 1번만", "1024번에 1번만" 이런 식으로 걸러서 천천히 세게 한다.

한 틱(카운터 1 올라가는) 시간은 이렇게 정해진다.

한 틱 시간 = 프리스케일러 ÷ F_CPU

예를 들어 F_CPU가 8MHz이고 프리스케일러가 1024라면, 한 틱 = 1024 ÷ 8,000,000 ≈ 0.128ms. 이 속도면 8비트 타이머가 0→255 한 바퀴(256틱) 도는 데 대략 32.8ms 걸린다. 프리스케일러를 키우면 더 느리게, 줄이면 더 빠르게 센다. 세는 속도를 다이얼처럼 조절하는 놈이라고 보면 된다.

② 오버플로 횟수를 소프트웨어로 센다

그래도 한 바퀴가 32.8ms면 1초엔 한참 모자란다. 그래서 오버플로가 몇 번 났는지를 우리가 변수로 센다. 예를 들어 오버플로가 약 30번 쌓이면 대략 1초다. "타이머가 넘칠 때마다 카운트 하나 올리고, 그게 N번 되면 1초 지난 걸로 친다" — 이 발상이 뒤에 나올 코드의 핵심이다.

인터럽트 — 사건이 나면 하던 일 멈추고 달려가기

그럼 "타이머가 오버플로 났다"는 걸 CPU가 어떻게 아느냐? 여기서 인터럽트(Interrupt)가 나온다. 인터럽트는 말 그대로 끼어들기다. 어떤 사건이 나면,

  1. CPU가 지금 하던 코드를 그 자리에서 잠깐 멈추고,
  2. 미리 정해둔 함수(ISR, 인터럽트 서비스 루틴)로 점프해서 그 일을 처리하고,
  3. 다 끝나면 원래 멈췄던 자리로 복귀해서 하던 걸 이어서 한다.

포인트는 CPU가 "언제 사건이 나나?" 하고 지켜보고 있을 필요가 없다는 거다. 사건이 나면 하드웨어가 알아서 CPU를 불러준다. 이걸 폴링(polling)이랑 비교하면 확 와닿는다.

폴링은 "다 됐어?"를 계속 물어보느라 CPU를 낭비하고, 인터럽트는 사건이 날 때만 잠깐 처리한다.

 

  • 폴링 : while 루프 돌면서 계속 "다 됐어? 아니. 다 됐어? 아니…" 하고 직접 물어본다. 물어보는 동안 CPU가 계속 붙잡혀 있으니 낭비가 크다.
  • 인터럽트 : 평소엔 딴 일 하다가, 사건이 나면 알아서 불러준다. CPU 낭비가 없다.

인터럽트를 쓰려면 두 개의 스위치를 알아야 한다. sei()전역 인터럽트 허용(켜기), cli()전역 인터럽트 금지(끄기)다. sei()를 안 해주면 아무리 설정해도 인터럽트가 안 걸리니까 이거 빼먹으면 안 된다. (나도 여기서 한참 헤맸다 ㅡㅡ;;;)

그리고 실제 처리 함수는 ISR(벡터명){ … } 형태로 쓴다. 예를 들어 Timer0 오버플로용 핸들러는 이렇게 생겼다.

ISR(TIMER0_OVF_vect)
{
    // 타이머0이 오버플로 날 때마다 자동으로 여기로 점프한다
}

코드로 보자 — delay 없이 LED 깜빡이기

이제 진짜다. 목표는 delay 없이 Timer0 오버플로 인터럽트로 LED를 일정 주기로 깜빡이는 거다. 아이디어는 위에서 말한 그대로다. 오버플로가 N번 쌓일 때마다 LED를 토글한다.

먼저 타이머와 인터럽트를 설정하는 부분이다.

volatile unsigned char count = 0;   // 오버플로 횟수 세는 변수

void Timer0_Init(void)
{
    TCCR0  = (1<<CS02)|(1<<CS00);   // 프리스케일러 1024로 클럭 분주
    TIMSK  = (1<<TOIE0);            // Timer0 오버플로 인터럽트 허용
    sei();                         // 전역 인터럽트 ON
}
  • TCCR0 : Timer0의 동작을 정하는 레지스터. CS02, CS00 비트를 세우면 프리스케일러가 1024로 설정된다. (즉 클럭을 1024로 나눠서 천천히 센다.)
  • TIMSK : 어떤 타이머 인터럽트를 쓸지 켜는 레지스터. TOIE0Timer0 Overflow Interrupt Enable, 즉 "Timer0 오버플로 인터럽트를 켠다"는 뜻이다.
  • sei() : 이걸 해줘야 전역 인터럽트가 실제로 동작한다. 아까 말한 그거다.

다음은 오버플로가 날 때마다 자동으로 불려가는 ISR이다.

ISR(TIMER0_OVF_vect)
{
    count++;                    // 오버플로 한 번 더 났다

    if (count >= N)             // N번 쌓이면
    {
        count = 0;             // 카운트 초기화
        LED ^= 1;              // LED 토글 (^= 1 은 0↔1 뒤집기)
    }
}

타이머가 넘칠 때마다 CPU가 잠깐 여기로 들어와서 count를 하나 올리고 바로 나간다. 그러다 count가 N에 도달하면 그때 LED를 뒤집고 카운트를 0으로 되돌린다. N값을 조절하면 깜빡이는 주기가 바뀐다. (한 틱 시간 × 256 × N 이 대략 반주기가 된다.)

그리고 여기가 진짜 하고 싶었던 얘기다. main은 이렇게 생겼다.

void main(void)
{
    DDRB = 0xFF;        // LED 핀 출력 설정 (예시)
    Timer0_Init();      // 타이머 + 인터럽트 세팅

    while(1)
    {
        // 여기선 LED 깜빡임에 대해 아무것도 안 한다!
        // 버튼 읽기, 센서 처리, 통신 등 '딴 일'을 마음껏 한다
    }
}

보이는가. while(1) 안에 LED 관련 코드가 한 줄도 없다. delay_ms도 없다. LED 깜빡임은 전부 인터럽트가 뒤에서 알아서 처리하고, main은 그동안 다른 일에 집중할 수 있다. 이게 아까 delay 코드랑 결정적으로 다른 점이다. 시간 재기를 하드웨어에 떠넘기고 CPU는 자유로워진 것. 처음 이 구조를 이해했을 때 좀 감동했다 ^^

ISR 짤 때 딱 하나 기억할 것

ISR은 최대한 짧게 짜라. 안에서 오래 걸리는 일(긴 delay, 복잡한 계산, 통신 대기 등)을 하면 그동안 다른 인터럽트도 밀리고 시스템 전체가 버벅인다. ISR 안에서는 플래그 하나 세우거나 변수 하나 올리는 정도로 짧게 처리하고 얼른 빠져나오는 게 정석이다. 무거운 일은 main의 while 루프에서 그 플래그 보고 처리하면 된다.

정리하면

  • 타이머 = CPU와 별개로 도는 하드웨어 카운터. TCNT가 0→TOP→0 반복하고, 넘칠 때 오버플로 이벤트가 난다.
  • 프리스케일러로 세는 속도를 조절한다. 한 틱 시간 = 프리스케일러 ÷ F_CPU.
  • 인터럽트 = 사건이 나면 하던 일 멈추고 ISR로 점프 → 처리 → 원래 자리 복귀.
  • 폴링은 계속 물어봐서 CPU 낭비, 인터럽트는 사건 날 때만 불러줘서 낭비 없음.
  • sei()로 전역 인터럽트 켜고, ISR(TIMER0_OVF_vect){…}로 핸들러 작성. ISR은 짧게.

이렇게 해서 I2C, SPI, UART, ADC, PWM에 이어 타이머/인터럽트까지 정리했다. 돌아보니 통신도, 변환도, PWM도 결국 이 타이머랑 인터럽트 위에 얹혀 있던 거더라. 마이컴 기초 시리즈가 얼추 한 바퀴 돈 느낌이다. 다음엔 이걸 다 엮어서 뭔가 하나 제대로 만들어보든가 해야겠다.

'프로그래밍 > 마이컴' 카테고리의 다른 글

PWM (펄스폭 변조)  (0) 2026.07.19
ADC (아날로그 읽기)  (0) 2026.07.19
UART 통신  (0) 2026.07.19
SPI 통신  (0) 2026.07.19
ATMEGA32L External Interrupt  (0) 2013.08.14
반응형

그동안 통신 쪽(I2C, SPI, UART)이랑 ADC까지 정리했으니, 이번엔 방향을 좀 틀어서 PWM을 정리해본다. ADC가 바깥 세상의 아날로그 값을 마이컴 안으로 읽어들이는 거였다면, PWM은 반대로 마이컴이 아날로그처럼 바깥으로 내보내는 느낌이라 짝처럼 묶어서 보기 좋더라.

일단 이 궁금증부터

LED 밝기를 절반만 켜고 싶다. 모터를 천천히만 돌리고 싶다. 그런데 마이컴 핀이 내보낼 수 있는 건 0V(LOW) 아니면 5V(HIGH), 딱 두 가지뿐이다. 디지털이니까. 그럼 그 중간인 "반쯤"은 대체 어떻게 만들지? 나는 처음에 무슨 볼륨 노브처럼 전압을 슬슬 낮춰주는 뭔가가 따로 있는 줄 알았다 ㅋㅋ

답은 좀 얍삽하다(?). 아주 빠르게 껐다 켰다를 반복하면서, "켜져 있는 시간의 비율"로 평균 전압을 만드는 거다. 절반만 켜고 싶으면 절반의 시간만 HIGH로 두면 된다. 이렇게 켜짐/꺼짐의 폭을 조절해서 원하는 세기를 만드는 게 바로 PWM이다.

용어부터 풀어보자

PWM은 Pulse Width Modulation, 우리말로 펄스폭 변조다. 이름 그대로 "펄스(HIGH로 튀어나온 신호)의 폭(넓이)을 변조(조절)한다"는 뜻이다. "피-더블유-엠"이라고 그냥 읽으면 된다.

핵심 규칙이 하나 있다. 주기(한 번 껐다 켜지는 데 걸리는 시간)는 고정해두고, 그 한 주기 안에서 HIGH로 켜져 있는 시간만 늘렸다 줄였다 한다는 거다. 켜짐과 꺼짐이 세트로 한 번 도는 게 한 주기고, 그 주기가 1초에 몇 번 도느냐가 주파수다.

한 주기 = ON시간 + OFF시간. 주기는 그대로 두고 ON시간(펄스 폭)만 조절한다.

 

듀티비(duty cycle)가 진짜 주인공

여기서 제일 중요한 놈이 듀티비(duty cycle)다. 한 주기 중에 HIGH로 켜져 있는 시간의 비율(%)을 말한다. 앞 그림에 있던 그 공식이다.

듀티비 = (ON 시간 / 전체 주기) × 100%

이 듀티비가 결국 평균 전압을 결정한다. 5V 기준으로 계산하면 이렇게 된다.

듀티비 평균 전압 (5V 기준) 체감
25% ≈ 1.25V LED 어둑, 모터 느릿
50% ≈ 2.5V 딱 절반
75% ≈ 3.75V LED 밝음, 모터 빠릿
100% 5V 계속 켜짐 (그냥 HIGH)

계산도 간단하다. 평균 전압 = 전원 전압 × 듀티비. 듀티 25%면 5V × 0.25 = 1.25V다. 이 평균값이 LED한테는 밝기로, 모터한테는 속도로 느껴진다.

세 파형 모두 주기(가로 폭)는 똑같다. 파란 칸(HIGH 시간)이 넓어질수록 평균 전압(주황 점선)이 올라간다.

 

근데 이게 왜 되는 거지?

솔직히 처음엔 "껐다 켰다 하면 깜빡거리기만 하지 어떻게 중간 밝기가 되냐"가 이해가 안 됐다. 포인트는 속도다. 스위칭을 눈이나 모터가 도저히 못 따라올 만큼 빠르게(보통 수백 Hz에서 수십 kHz) 반복하면, 사람 눈이나 모터의 관성이 그 깜빡임을 하나하나 못 쫓아가고 그냥 평균값으로 뭉뚱그려 느끼게 된다.

형광등이 사실은 미친 듯이 깜빡이는데 우리 눈엔 그냥 계속 켜진 걸로 보이는 거랑 비슷하다. 모터도 마찬가지로, 전기가 워낙 빨리 끊겼다 이어지니까 회전 관성이 그걸 평균으로 받아서 "적당한 속도"로 돈다. 그래서 PWM에서 주파수를 충분히 높게 잡는 게 중요하다. 너무 낮으면 LED가 눈에 띄게 깜빡거리거나 모터가 덜덜거린다.

AVR에서는 어떻게 만드나 (타이머)

이걸 소프트웨어로 PORT 켰다 껐다 delay 주면서 직접 만들 수도 있긴 한데, 그러면 CPU가 그 짓만 하고 있어야 해서 낭비다. 그래서 AVR은 타이머(Timer)가 하드웨어로 알아서 PWM을 뽑아준다. 우리는 레지스터 몇 개만 세팅하면 끝이고, 그 뒤론 CPU가 다른 일 해도 핀에서 파형이 계속 나온다. 이게 진짜 편하다.

PWM에 관여하는 레지스터를 정리하면 이렇다. (Timer0 기준으로 설명한다.)

  • WGM 비트 (TCCR0 안) : 타이머 동작 모드를 고른다. PWM을 쓰려면 Fast PWM이나 Phase-correct PWM 모드로 맞춘다.
  • COM 비트 (TCCR0 안) : 출력핀(OCn)을 어떻게 토글할지. 보통 "비교값 만나면 LOW로" 하는 non-inverting 모드를 쓴다.
  • CS 비트 (TCCR0 안) : 프리스케일러. 타이머 클럭을 몇으로 나눌지 정해서 PWM 주파수를 조절한다.
  • OCR0 레지스터 : 비교값. 이게 바로 듀티비를 정하는 값이다. 타이머가 0부터 세다가 이 값에 도달하면 핀을 내린다.

PWM 주파수는 이 공식으로 정해진다.

PWM 주파수 = F_CPU / (프리스케일러 × (TOP + 1))

8비트 타이머면 TOP이 255라서 (TOP+1)은 256이 된다. 여기까지가 세팅이고, 실제로 밝기를 바꾸는 건 오로지 OCR0 값 하나다.

코드로 보자 (Timer0, 8비트 Fast PWM)

말로 하면 복잡하니 코드가 마음 편하다. Timer0을 Fast PWM으로 세팅하는 예시다.

void PWM_Init(void)
{
    // OC0 핀(PWM 출력)을 출력으로. (ATmega128 기준 PB4)
    DDRB |= (1 << PB4);

    // Fast PWM 모드 : WGM01, WGM00 둘 다 1
    // non-inverting 출력 : COM01 = 1  (비교값 만나면 LOW)
    // 프리스케일러 8       : CS01 = 1
    TCCR0 = (1 << WGM00) | (1 << WGM01) | (1 << COM01) | (1 << CS01);

    OCR0 = 64;   // 듀티 ≈ 64/255 ≈ 25%  → LED 어둑하게
}

여기서 WGM00+WGM01로 Fast PWM 모드가 되고, COM01로 "OCR0에 도달하면 출력핀을 LOW로 내리는" 동작이 켜진다. 즉 타이머가 0→255를 계속 반복하는데, 0부터 OCR0까지는 HIGH, OCR0 넘으면 255까지는 LOW가 된다. 그래서 OCR0가 클수록 HIGH 구간이 길어지고 = 듀티가 커지고 = 밝아진다.

핵심은 세팅은 한 번만 하고, 그 다음부턴 OCR0 값만 바꾸면 밝기/속도가 실시간으로 바뀐다는 거다.

void main(void)
{
    PWM_Init();

    while (1)
    {
        OCR0 = 64;    // 듀티 25%  (5V × 0.25 ≈ 1.25V) → 어둑
        // OCR0 = 128; // 듀티 50%  (≈ 2.5V)  → 절반
        // OCR0 = 191; // 듀티 75%  (≈ 3.75V) → 밝음
    }
}

OCR0 한 줄만 0부터 255 사이로 바꿔주면 밝기가 쭉 조절된다. 만약 가변저항 값을 ADC로 읽어서 그 값을 OCR0에 그대로 넣어주면? 노브를 돌리는 대로 LED 밝기가 따라오는 디머(dimmer)가 된다. ADC랑 PWM이 이렇게 만난다 ^^

어디에 쓰나

  • LED 디밍 : OCR 값만 바꿔서 밝기 조절. 부드럽게 밝아졌다 어두워지는 숨쉬기 효과도 이걸로 만든다.
  • DC 모터 속도 제어 : 듀티비로 평균 전압 = 속도 조절. (실제론 모터 전류 때문에 트랜지스터/MOSFET를 하나 끼워서 구동한다.)
  • 서보모터 : 얘는 좀 특이한데, 주기 20ms(50Hz)에 펄스폭 1~2ms를 실어서 그 폭으로 각도를 정한다. (밝기가 아니라 위치를 PWM으로 지정하는 케이스 — 이건 나중에 따로 다뤄야겠다.)

정리하면

  • 마이컴 핀은 0V/5V만 내보내지만, 빠르게 껐다 켰다 하면 그 평균으로 중간값을 만들 수 있다. 그게 PWM.
  • 주기는 고정, 한 주기 안의 HIGH 시간(펄스 폭)만 조절한다.
  • 듀티비 = ON시간 / 주기. 평균 전압 = 전원 전압 × 듀티비. → LED 밝기, 모터 속도가 된다.
  • AVR에선 타이머가 하드웨어로 만들어준다. OCR 값만 바꾸면 밝기/속도가 바뀐다.

정리하다 보니 결국 PWM은 타이머가 다 해주는 거였다. 주파수도 프리스케일러+TOP으로 정하고, 듀티도 비교값으로 정하고… 죄다 타이머 이야기다. 그래서 다음엔 타이머와 인터럽트를 제대로 파봐야겠다. PWM도 결국 타이머로 만드는 거니까, 순서상 그게 맞는 것 같다.

'프로그래밍 > 마이컴' 카테고리의 다른 글

타이머/인터럽트  (0) 2026.07.19
ADC (아날로그 읽기)  (0) 2026.07.19
UART 통신  (0) 2026.07.19
SPI 통신  (0) 2026.07.19
ATMEGA32L External Interrupt  (0) 2013.08.14
반응형

UART, I2C, SPI 통신 글은 이미 다 써버렸다. 근데 이번엔 통신 얘기가 아니다. 지금까지 다룬 건 죄다 디지털 신호(0/1)였는데, 세상엔 0과 1로 딱 안 떨어지는 값이 훨씬 많더라. 온도, 밝기, 가변저항 돌린 정도… 이런 건 전부 연속적인 아날로그 전압이다. 오늘은 이걸 마이컴이 어떻게 읽는지, ADC를 정리해본다.

일단 문제부터

마이컴은 기본적으로 0 아니면 1밖에 모른다. 핀에 전압이 걸렸냐(HIGH) 안 걸렸냐(LOW), 딱 두 가지다. 그런데 예를 들어 가변저항을 천천히 돌리면 핀에 걸리는 전압이 0V에서 5V까지 스르륵 바뀐다. 2.3V, 2.31V, 2.317V… 무한히 잘게 쪼갤 수 있는 값이다.

이걸 마이컴한테 "지금 몇 볼트야?" 하고 물어보면, 디지털만 아는 녀석이 답을 못 한다. 그래서 아날로그 전압을 숫자로 바꿔주는 통역기가 필요하다. 그게 ADC다.

용어적 설명부터

ADC는 Analog to Digital Converter, 말 그대로 "아날로그를 디지털로 바꾸는 변환기"다. "에이-디-씨"라고 읽으면 된다. 반대로 디지털을 아날로그로 뱉는 DAC라는 것도 있는데 그건 나중에 얘기하고, 오늘은 읽는 쪽(ADC)만 본다.

ADC를 이해하려면 딱 3가지 용어만 잡으면 된다.

  • 해상도(resolution) : 전압을 몇 단계로 쪼개서 표현하냐. AVR은 보통 10비트라서 0 ~ 1023, 즉 1024단계로 나눈다.
  • 기준전압(Vref) : 측정의 "만점" 기준. 예를 들어 5V로 잡으면, "5V가 곧 1023(만점)"이라는 뜻이다.
  • 변환식 : 실제로 얼마가 나오냐. 디지털값 = (Vin / Vref) × 1023. 거꾸로 전압을 알고 싶으면 Vin = 디지털값 × Vref / 1023.

0 ~ Vref 사이의 전압을 0 ~ 1023 숫자에 나눠 대응시킨다. 전압이 높을수록 값도 커진다.

 

숫자로 감 잡기

말로만 하면 안 와닿으니 숫자를 넣어보자. Vref = 5V, 10비트(0~1023)라고 하자.

  • 한 칸이 몇 볼트냐? → 5V를 1024단계로 나누니까 5V / 1024 ≈ 약 4.9mV. 즉 디지털값이 1 올라갈 때마다 약 4.9mV씩 차이 난다.
  • 0V를 넣으면 → 0
  • 2.5V(절반)를 넣으면 → (2.5 / 5) × 1023 ≈ 512
  • 5V(만점)를 넣으면 → 1023

딱 절반 넣으면 딱 절반 값이 나온다. 처음엔 이게 왜 1024가 아니라 1023을 곱하나 싶어서 좀 헷갈렸는데, 단계는 1024개지만 표현할 수 있는 최댓값(번호)은 0부터 세서 1023이라 그렇다. (0번 칸이 있으니까.)

어떻게 숫자로 바뀌나 — 표본화와 양자화

ADC가 전압을 숫자로 바꾸는 과정은 크게 두 단계다. 이름은 거창한데 뜻은 별거 없다.

  • 표본화(sampling) : 연속으로 흐르는 전압을 일정 간격으로 콕콕 찍는다. 계속 보는 게 아니라 순간순간 사진 찍듯 값을 잡는 거다.
  • 양자화(quantization) : 찍은 값을 정해진 계단값 중 가장 가까운 걸로 반올림한다. 5V/1024 칸 중에 제일 가까운 칸으로 몰아준다.

그래서 아무리 매끈한 아날로그 곡선이라도, ADC를 거치고 나면 계단 모양으로 근사된 값이 나온다. 해상도가 낮으면(계단이 크면) 실제 곡선이랑 좀 어긋나고, 높으면(계단이 촘촘하면) 원본에 가까워진다.

연속 전압을 일정 간격으로 찍어(표본화) 가까운 계단값으로 반올림(양자화)한다. 그래서 계단 근사가 된다.

 

핀은 여러 개, 채널로 고른다

아날로그 입력핀은 보통 하나가 아니다. AVR을 보면 ADC0, ADC1, ADC2… 여러 개가 있다. 그런데 실제 변환기(ADC 회로)는 칩 안에 딱 하나다. 그럼 어떻게 여러 핀을 읽나?

MUX(멀티플렉서)라는 스위치로 "이번엔 ADC0 읽어줘", "다음엔 ADC3 읽어줘" 하고 한 번에 하나씩 골라서 변환기에 연결한다. 그래서 가변저항은 ADC0, 조도센서는 ADC1… 이렇게 꽂아두고 채널만 바꿔가며 돌려 읽으면 된다. 이 채널 선택은 코드에서 레지스터로 지정한다.

AVR 코드로 보자

이제 실제 레지스터를 만져보자. 통신 글들에서 핀을 직접 두드렸던 것처럼, ADC도 결국 레지스터 몇 개 설정 → 변환 시작 → 끝날 때까지 대기 → 결과 읽기 순서다. 관련 레지스터부터 정리한다.

레지스터 하는 일
ADMUX 기준전압 선택(REFS 비트) + 읽을 채널 선택(MUX 비트)
ADCSRA ADC 켜기(ADEN), 변환 시작(ADSC), 클럭 프리스케일러(ADPS)
ADC (ADCH:ADCL) 변환 결과가 담기는 16비트 값. 여기서 최종 0~1023을 읽는다

먼저 초기화다. 기준전압은 AVCC(보통 5V)를 쓰고, 변환 클럭이 너무 빠르면 결과가 부정확해서 프리스케일러를 128로 팍 줄여준다.

void ADC_init(void)
{
    // 기준전압 = AVCC (REFS0 = 1)
    ADMUX = (1 << REFS0);

    // ADC 켜고(ADEN), 프리스케일러 128 (ADPS2:0 = 111)
    ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
}

다음은 실제로 한 채널을 읽는 함수다. 채널 선택 → 변환 시작 → 끝날 때까지 대기 → 결과 리턴, 이 흐름 그대로다.

unsigned int ADC_read(unsigned char ch)
{
    // 하위 4비트만 채널 번호로 갈아끼운다 (상위 비트는 그대로 유지)
    ADMUX = (ADMUX & 0xF0) | (ch & 0x0F);

    // 변환 시작
    ADCSRA |= (1 << ADSC);

    // ADSC가 자동으로 0이 되면 변환 끝 → 그때까지 대기
    while (ADCSRA & (1 << ADSC));

    return ADC;   // 0 ~ 1023
}

포인트는 while (ADCSRA & (1 << ADSC)); 이 한 줄이다. 변환을 시작하면 ADSC 비트가 1이 되고, 변환이 끝나면 하드웨어가 알아서 0으로 내려준다. 그래서 0이 될 때까지 기다리기만 하면 된다. I2C/SPI에서 클럭 굴리며 비트 세던 것보다 오히려 편하다.

마지막에 그냥 ADC라고 쓰면 결과 16비트를 통째로 읽어준다. 컴파일러가 ADCL, ADCH를 알아서 합쳐주는 거다.

주의 : 만약 ADCL, ADCH직접 따로 읽는다면 반드시 ADCL(하위)을 먼저, ADCH(상위)를 나중에 읽어야 한다. 순서를 바꾸면 값이 갱신되며 꼬인다. ADC로 한 번에 읽으면 이 걱정은 없다.

실제로 써먹으면

읽은 0~1023 값 하나로 할 수 있는 게 은근 많다.

  • 가변저항 : 돌린 만큼 0~1023이 바뀌니 볼륨 노브, 밝기 조절 같은 데 쓴다.
  • 조도센서(CDS) : 밝으면 저항이 변해서 전압이 바뀐다 → 밝기 측정.
  • 온도센서 : 온도에 비례하는 전압을 뱉는 센서를 읽어서 온도로 환산.

이걸 응용하면 "어두워지면 LED 켜기", "가변저항으로 모터 속도 조절" 같은 게 다 ADC 값 하나에서 출발한다. 값을 그대로 쓰거나, 아까 그 변환식으로 실제 전압/물리량으로 바꿔 쓰면 된다.

void main(void)
{
    unsigned int val;

    ADC_init();

    while (1)
    {
        val = ADC_read(0);      // ADC0 채널(예: 가변저항) 읽기

        if (val > 512) LED_ON;  // 절반(약 2.5V) 넘으면 LED 켜기
        else           LED_OFF;
    }
}

정리하면

  • ADC = 아날로그 전압을 디지털 숫자로 바꾸는 변환기. 센서값을 읽으려면 필수다.
  • 핵심 3가지 : 해상도(AVR 10비트=0~1023), 기준전압(Vref), 변환식(디지털값 = Vin/Vref × 1023).
  • 과정은 표본화(일정 간격으로 찍기) + 양자화(가까운 계단값으로 반올림). 그래서 계단 근사가 된다.
  • 여러 핀은 MUX로 채널을 골라 하나씩 읽는다.
  • 코드는 ADMUX로 기준전압/채널, ADCSRA로 켜고 시작, ADSC가 0되면 끝, 결과는 ADC에서 읽기.

통신 3종(UART/I2C/SPI) 정리하고 나서 아날로그까지 오니까, 이제 센서 웬만한 건 다 읽을 수 있게 됐다. 읽는 건 이걸로 됐으니, 다음엔 반대로 아날로그처럼 출력하는 PWM을 정리해봐야겠다. (LED 밝기 조절이나 모터 속도, 이게 다 PWM이더라.)

'프로그래밍 > 마이컴' 카테고리의 다른 글

타이머/인터럽트  (0) 2026.07.19
PWM (펄스폭 변조)  (0) 2026.07.19
UART 통신  (0) 2026.07.19
SPI 통신  (0) 2026.07.19
ATMEGA32L External Interrupt  (0) 2013.08.14
반응형

I2C 보고 SPI 봤으니 이번엔 UART다. 사실 셋 중에 제일 처음 만나게 되는 게 이 녀석인데, "시리얼 모니터에 글자 찍기"가 바로 UART다. 근데 앞에 둘이랑 결정적으로 다른 점이 하나 있다. 클럭선이 없다.

클럭선이 없다고?

I2C엔 SCL, SPI엔 SCK 라는 클럭선이 있었다. 이게 "지금이 1비트를 읽을 타이밍이야" 하고 박자를 딱딱 찍어주는 선이었다. 그래서 둘 다 동기식(synchronous) 통신이라고 불렀다.

그런데 UART는 그 클럭선이 없다. 그래서 비동기(asynchronous)다. 이름부터가 그렇다. UART = Universal Asynchronous Receiver/Transmitter. 우리가 흔히 "시리얼 통신", "RS-232" 라고 부르는 게 다 이거다. "유아트" 라고 읽으면 된다.

여기서 바로 드는 의문. 클럭이 없는데 대체 언제가 1비트인지 어떻게 알아? 이게 UART의 핵심이라 뒤에서 따로 짚는다. 결론만 먼저 말하면, 양쪽이 통신 속도를 미리 약속해두는 방식으로 해결한다.

선은 딱 2개

UART는 데이터선이 2개다. (GND 빼고)

  • TX : Transmit. 내가 내보내는(송신) 선.
  • RX : Receive. 내가 받는(수신) 선.

그리고 배선할 때 여기서 한 번 다들 삽질한다. TX는 TX끼리, RX는 RX끼리 연결하는 게 아니다. 내가 내보낸 걸 상대는 받아야 하니까, X자로 교차해서 이어줘야 한다. 내 TX → 상대 RX, 상대 TX → 내 RX. 여기에 GND만 공통으로 하나 이어주면 끝이다.

UART 결선도. 내 TX는 상대 RX로 교차 연결. 클럭선이 없다는 게 핵심이다.

SPI처럼 전이중(full-duplex)이라 TX/RX가 따로 있어서 보내기와 받기를 동시에 할 수 있다. 그리고 SPI/I2C랑 다르게 마스터/슬레이브 개념이 없다. 그냥 양쪽이 대등하게 각자 TX로 쏘고 RX로 받는다. 처음엔 TX끼리 이어놓고 "왜 글자가 안 나오지" 하고 한참 헤맸다 ㅡㅡ;;;

그래서 I2C / SPI랑 뭐가 다른데?

앞에 둘을 봤으니 표로 비교하는 게 제일 빠르다.

  I2C SPI UART
클럭선 있음 (SCL) 있음 (SCK) 없음 (비동기)
선 개수 2선 4선 2선 (TX, RX)
역할 구분 마스터/슬레이브 마스터/슬레이브 대등 (없음)
통신 방향 반이중 전이중 전이중
속도 약속 클럭이 알아서 클럭이 알아서 보드레이트로 미리

핵심은 역시 클럭선의 유무다. 클럭이 없는 대신 UART는 다른 방법으로 박자를 맞춘다. 그게 보드레이트다.

클럭 없이 어떻게 비트를 나누나 — 보드레이트

클럭선이 없으니 "지금 읽어" 하고 알려주는 신호가 없다. 그럼 어떻게 하냐면, 양쪽이 1비트에 쓸 시간을 미리 똑같이 약속해둔다. 이 약속한 속도가 보드레이트(baud rate)다. 단위는 bps(초당 비트).

  • 흔히 쓰는 값: 9600, 115200 bps.
  • 1비트에 쓰는 시간 = 1 / 보드레이트. 예를 들어 9600bps면 1비트가 약 104μs.

즉 받는 쪽은 클럭을 볼 필요 없이 "신호가 시작되면 정해진 시간 간격으로 뚝뚝 끊어서 비트를 읽으면 된다." 양쪽이 같은 보드레이트로 맞춰놨으니 타이밍이 딱딱 들어맞는 거다.

그럼 "신호가 시작되는 순간"은 어떻게 아느냐? 여기서 프레임 얘기가 나온다.

프레임 구조 (8N1)

평소에 아무것도 안 보낼 때, UART 라인은 HIGH를 유지한다. 이걸 idle 상태라고 한다. 그러다 데이터를 보낼 때 다음 순서로 한 묶음(프레임)을 쏜다.

  • 시작비트 (1개) : 라인을 HIGH에서 LOW로 뚝 떨어뜨린다. "이제 시작한다!"는 신호. 받는 쪽은 이 떨어지는 순간을 보고 "아 시작이구나" 하고 타이머를 돌린다.
  • 데이터비트 (보통 8개) : 진짜 데이터. 여기서 주의할 게, LSB first다. 즉 낮은 자리 비트부터 나간다. (SPI는 보통 MSB first였는데 UART는 반대라 헷갈린다.)
  • 패리티비트 (옵션) : 에러 체크용. 안 쓰는 경우가 더 많다.
  • 정지비트 (1~2개) : 라인을 다시 HIGH로 올려서 "한 프레임 끝" 표시.

이 설정을 짧게 "8N1" 이라고 적는다. 8비트 데이터, No 패리티, 정지비트 1개. 제일 흔한 설정이다.

문자 'A'(0x41)를 보내는 프레임. idle(HIGH) → 시작(LOW) → 데이터 8비트 → 정지(HIGH).

위 그림은 문자 'A'를 보내는 예시다. 'A'는 아스키로 0x41 = 0b01000001인데, LSB first라서 실제 라인에는 1, 0, 0, 0, 0, 0, 1, 0 순서(b0→b7)로 나간다. 비트 순서가 눈으로 보던 이진수랑 뒤집혀서 나가는 게 처음엔 좀 어지럽다.

제일 흔한 삽질: 보드레이트 안 맞음

UART 하다 보면 거의 무조건 한 번은 겪는 증상이 있다. 시리얼 모니터에 글자가 ????? 이렇게 깨진 문자로 나오는 거다.

원인은 십중팔구 양쪽 보드레이트가 안 맞아서다. 한쪽은 9600으로 보내는데 받는 쪽은 115200으로 읽으면, 1비트 시간이 서로 다르니까 비트를 엉뚱한 지점에서 끊어 읽는다. 그러니 글자가 와장창 깨진다. 클럭선이 없어서 순전히 시간 약속에 의존하는 UART의 대가인 셈이다ㅋㅋ. 글자 깨지면 제일 먼저 양쪽 보드레이트부터 확인하자.

코드로 보자 (AVR)

AVR에선 하드웨어 UART가 내장돼 있어서 레지스터만 세팅하면 된다. SPI 때 비트뱅잉으로 직접 두드린 것보단 오히려 편하다. 레지스터 이름부터 정리하면,

  • UBRRH / UBRRL : 보드레이트 설정값(16비트)을 상/하위로 나눠 넣는다.
  • UDR : 데이터 레지스터. 여기에 쓰면 송신, 여기서 읽으면 수신.
  • UCSRA : 상태 확인. UDRE(송신버퍼 비었나), RXC(수신 완료됐나) 비트를 본다.
  • UCSRB : TXEN(송신 켜기), RXEN(수신 켜기).
  • UCSRC : 프레임 형식(데이터 비트 수, 패리티, 정지비트) 설정. 여기서 8N1을 잡는다.

먼저 초기화다. 보드레이트값(UBRR)은 공식이 정해져 있다. UBRR = F_CPU / (16 × baud) − 1.

void UART_init(unsigned long baud)
{
    // 보드레이트 계산 : UBRR = F_CPU/(16*baud) - 1
    unsigned int ubrr = F_CPU / (16UL * baud) - 1;

    UBRRH = (unsigned char)(ubrr >> 8);   // 상위 8비트
    UBRRL = (unsigned char)(ubrr);         // 하위 8비트

    // 송신(TXEN), 수신(RXEN) 둘 다 켠다
    UCSRB = (1 << TXEN) | (1 << RXEN);

    // 프레임 형식 : 8비트 데이터, 패리티 없음, 정지 1비트 (8N1)
    UCSRC = (1 << UCSZ1) | (1 << UCSZ0);
}

이제 한 글자 보내기. 송신 버퍼가 빌 때까지 기다렸다가 UDR에 쓰면 된다. 버퍼가 아직 차 있는데 덮어쓰면 데이터가 날아가니까 UDRE 비트를 확인하는 거다.

void UART_putc(char c)
{
    // 송신 버퍼가 빌 때까지 대기 (UDRE = 1 되면 비었다는 뜻)
    while ( !(UCSRA & (1 << UDRE)) );

    UDR = c;   // 버퍼 비었으니 이제 쓴다 -> 하드웨어가 알아서 쏴준다
}

받기는 반대다. 수신이 완료될 때까지 기다렸다가 UDR을 읽는다. 한 프레임이 다 들어오면 RXC 비트가 1이 된다.

char UART_getc(void)
{
    // 데이터가 들어올 때까지 대기 (RXC = 1 되면 도착했다는 뜻)
    while ( !(UCSRA & (1 << RXC)) );

    return UDR;   // 도착했으니 읽어서 돌려준다
}

패턴이 SPI 때랑 비슷하다. 플래그 비트를 while로 기다렸다가 → 준비되면 데이터 레지스터를 건드린다. 송신은 "버퍼 빌 때까지", 수신은 "올 때까지" 기다리는 것만 다르다.

문자열 출력 예시

한 글자짜리를 만들었으니 문자열은 그냥 반복이다. 시리얼 모니터에 Hello 찍는 정도는 이걸로 끝난다.

void UART_puts(const char *s)
{
    while (*s)              // 널 문자('\0') 만날 때까지
        UART_putc(*s++);    // 한 글자씩 보낸다
}

void main(void)
{
    UART_init(9600);            // 보드레이트 9600으로 시작
    UART_puts("Hello UART!\r\n");

    while (1)
    {
        char c = UART_getc();   // 뭔가 들어오면
        UART_putc(c);           // 그대로 되돌려 보낸다 (에코)
    }
}

PC 시리얼 모니터를 열 때 보드레이트를 꼭 9600으로 맞춰야 글자가 제대로 보인다. (안 맞추면 아까 그 ????? 향연이다.) 위 while 루프는 받은 글자를 그대로 돌려보내는 에코인데, 통신이 잘 되는지 확인할 때 딱 좋다.

정리하면

  • UART = 비동기 직렬통신. I2C/SPI와 달리 클럭선이 없다.
  • 선은 TX, RX 2개 + GND. X자로 교차 연결(내 TX → 상대 RX).
  • 클럭이 없는 대신 보드레이트로 1비트 시간을 미리 약속한다. (1비트 = 1/보드레이트)
  • 프레임 = 시작비트(LOW) + 데이터 8비트(LSB first) + (패리티) + 정지비트(HIGH). 흔히 8N1.
  • 글자 깨지면(?????) 제일 먼저 양쪽 보드레이트를 의심하자.

클럭선 없이 시간 약속만으로 데이터를 주고받는다는 게 처음엔 신기했는데, 프레임 구조를 보고 나니 납득이 됐다. I2C, SPI, UART까지 봤으니 기본적인 통신 3종은 얼추 정리된 느낌이다. 다음엔 통신 말고 ADC(아날로그 값 읽기)를 정리해봐야겠다.

'프로그래밍 > 마이컴' 카테고리의 다른 글

PWM (펄스폭 변조)  (0) 2026.07.19
ADC (아날로그 읽기)  (0) 2026.07.19
SPI 통신  (0) 2026.07.19
ATMEGA32L External Interrupt  (0) 2013.08.14
I2C 통신  (0) 2013.08.14
반응형

저번엔 I2C를 정리했으니 이번엔 SPI를 정리해본다. 둘이 사촌지간 같은 녀석들이라 I2C 알고 나면 SPI는 오히려 쉽다.

라고 나도 처음엔 그렇게 생각했다 ㅋㅋ

용어적 설명부터

SPI(Serial Peripheral Interface)는 모토로라에서 개발한 동기식 직렬 통신 방식이다. 마이크로컨트롤러와 주변 기기(센서, EEPROM, SD카드, LCD 등) 사이에서 짧은 거리 고속 통신에 널리 쓰인다. "에스-피-아이" 라고 그냥 읽으면 된다. 이건 발음 시비 걸릴 게 없어서 마음이 편하다 ^^

SPI는 기본적으로 4개의 라인을 사용한다.

  • SCLK (SCK) : 클럭. 마스터가 만들어서 내보낸다.
  • MOSI : Master Out Slave In. 마스터 → 슬레이브 방향 데이터.
  • MISO : Master In Slave Out. 슬레이브 → 마스터 방향 데이터.
  • SS (CS) : Slave Select(Chip Select). 마스터가 "너랑 얘기할래" 하고 슬레이브를 지목하는 선. 보통 LOW일 때 선택(active low)이다.

SPI 4선 결선도. 클럭은 마스터가 만들고, MOSI/MISO는 마스터 기준 이름이다.

여기서 이름 헷갈리지 말자. MOSI, MISO는 마스터 기준이다. 마스터든 슬레이브든 칩에 적힌 핀 이름은 똑같이 MOSI/MISO라서, "마스터의 MOSI ↔ 슬레이브의 MOSI" 이렇게 그냥 같은 이름끼리 이어주면 된다. (몇몇 칩은 SDI/SDO로 적혀있기도 한데 그건 또 반대로 이어야 해서… 처음에 여기서 배선 한 번 틀렸다 ㅡㅡ;;;)

그래서 I2C랑 뭐가 다른데?

I2C를 먼저 봤으니 비교로 이해하는 게 제일 빠르다.

  I2C SPI
선 개수 2선 (SDA, SCL) 4선 (SCK, MOSI, MISO, SS)
슬레이브 구분 주소(7비트) SS선을 따로 하나씩
통신 방향 반이중 (한 선으로 주고받음) 전이중 (MOSI/MISO 동시에)
속도 100k ~ 3.4Mbit/s 보통 수~수십 MHz, 더 빠름
START/STOP, ACK 있음 없음 (그냥 SS로 시작/끝)

즉 SPI는 주소 개념이 없다. 슬레이브가 여러 개면 SS선을 슬레이브 수만큼 따로 뽑아서 "이번엔 너!" 하고 하나만 LOW로 내려주는 방식이다. 그래서 슬레이브가 많아지면 선이 늘어난다는 단점이 있다. 대신 START/STOP이나 ACK 같은 규칙이 없어서 프로토콜 자체는 훨씬 단순하다.

데이터 전송은 어떻게?

SPI는 START/STOP이 없다. 그럼 언제 시작하고 끝나느냐?

  • 시작 : 마스터가 SS를 LOW로 내리면 그 슬레이브랑 통신 시작.
  • : 다시 SS를 HIGH로 올리면 끝.

이게 I2C의 START/STOP 역할을 대신한다. 훨씬 직관적이다.

데이터는 마스터가 만드는 클럭(SCK)에 맞춰 한 비트씩 흘러간다. 핵심은 전이중이라는 거다. 마스터가 MOSI로 1비트 내보낼 때, 동시에 MISO로 슬레이브가 보낸 1비트가 들어온다. 즉 보내기와 받기가 한 번에 일어난다. (I2C는 쓸 때 쓰고 읽을 때 읽었지만 SPI는 동시다.)

SCK 8펄스면 마스터와 슬레이브의 바이트가 통째로 맞교환된다.

 

여기서 처음에 나를 괴롭혔던 게 CPOL / CPHA 라는 놈이다.

  • CPOL (클럭 극성) : 통신 안 할 때(idle) SCK가 LOW냐(0) HIGH냐(1).
  • CPHA (클럭 위상) : 데이터를 클럭의 앞쪽 엣지에서 읽냐(0) 뒷쪽 엣지에서 읽냐(1).

이 둘 조합으로 모드 0~3이 나온다. 뭐 복잡해 보이지만, 실무에선 데이터시트에 "이 칩은 SPI 모드 0" 이런 식으로 적혀 있으니 그거 맞춰주면 된다. 제일 흔한 게 모드 0 (CPOL=0, CPHA=0)이다.

  • 모드 0 기준: 평소 SCK는 LOW, SCK가 올라가는(상승) 엣지에서 데이터를 읽는다. MOSI/MISO 값은 SCK가 내려가 있을 때 바꿔놓는다.

모드 0: SS를 내리고, SCK 상승 엣지(↑)마다 데이터를 읽는다. (● = 읽는 순간)

말로 하면 헷갈리니까 그냥 코드로 보자. (이게 마음이 편하다.)

코드로 보자 (비트뱅잉, 모드 0 기준)

I2C 때처럼 PORTC 핀을 직접 두드리는 방식이다. 핀 배치는 예시니까 본인 보드에 맞게 바꾸면 된다.

#define MOSI        PORTC_Bit4   // 마스터가 내보내는 데이터
#define MISO_IN     PINC_Bit5    // 슬레이브가 보낸 데이터 (입력)
#define SCK         PORTC_Bit6   // 클럭 (마스터가 생성)
#define SS          PORTC_Bit7   // 슬레이브 선택 (active low)

// 슬레이브 선택 : SS를 LOW로
void SPI_Start(void)
{
    DDRC = 0xDC;    // MOSI, SCK, SS = 출력 / MISO = 입력
    SCK = 0;        // 모드 0 : idle 클럭은 LOW
    SS  = 0;        // 슬레이브 선택 시작
    USEC_Delay(1);
}

// 슬레이브 선택 해제 : SS를 HIGH로
void SPI_Stop(void)
{
    SS = 1;         // 통신 끝
    USEC_Delay(1);
}

이제 핵심인 데이터 주고받기다. SPI는 전이중이라 보내면서 동시에 받는다. 그래서 함수 하나로 끝난다. val을 내보내면서, 그 사이에 들어온 값을 리턴한다.

// 1바이트 전송 + 동시에 1바이트 수신 (MSB first, 모드 0)
BYTE SPI_Transfer(BYTE val)
{
    BYTE i;
    BYTE recv = 0;

    for(i = 0x80; i > 0; i /= 2)   // MSB부터 8비트
    {
        // 1) SCK가 LOW인 동안 MOSI에 보낼 비트를 올려둔다
        if (i & val) MOSI = 1;
        else         MOSI = 0;
        USEC_Delay(1);

        // 2) SCK 상승 엣지 : 이 순간 서로 데이터를 읽는다
        SCK = 1;
        if (MISO_IN) recv = (recv | i);   // 들어온 비트를 저장
        USEC_Delay(1);

        // 3) SCK 하강 : 다음 비트 준비
        SCK = 0;
        USEC_Delay(1);
    }

    return recv;   // 나가는 동안 들어온 8비트
}

I2C와 비교해보면 재밌다. I2C는 I2CWrite_Data(), I2CRead_Data()가 따로 있었는데, SPI는 SPI_Transfer() 하나가 쓰기/읽기를 겸한다. "보내는 만큼 받는다"가 SPI의 성격이다.

  • 쓰기만 하고 싶다 → 리턴값 무시하면 된다.
  • 읽기만 하고 싶다 → 아무 값(보통 0x00, 더미 바이트)이나 보내면서 리턴값을 받으면 된다. 어차피 클럭을 굴려야 슬레이브가 데이터를 실어주니까.

전체 흐름 예시

예를 들어 어떤 SPI EEPROM에서 데이터를 하나 읽는다고 해보자. (명령 → 주소 → 더미 보내며 읽기)

void main(void)
{
    BYTE data;

    SPI_Start();              // SS LOW : 슬레이브 선택

    SPI_Transfer(0x03);       // READ 명령 (예시)
    SPI_Transfer(0x00);       // 읽을 주소 상위
    SPI_Transfer(0x10);       // 읽을 주소 하위

    data = SPI_Transfer(0x00); // 더미(0x00) 보내면서 실제 데이터 수신

    SPI_Stop();               // SS HIGH : 선택 해제
}

포인트는 마지막 SPI_Transfer(0x00)이다. 보낼 데이터는 없지만 클럭을 굴려야 슬레이브가 MISO로 값을 실어주기 때문에 더미 바이트를 하나 흘려보내는 거다. 이게 처음엔 "왜 굳이 0을 보내?" 싶었는데, 전이중이라 클럭이 있어야만 데이터가 나온다는 걸 생각하면 납득이 된다.

정리하면

  • SPI = 4선(SCK, MOSI, MISO, SS), 마스터가 클럭을 만든다.
  • 슬레이브 구분은 주소가 아니라 SS선. LOW로 내리면 선택, HIGH로 올리면 끝.
  • 전이중 — 보내면서 동시에 받는다. 그래서 함수 하나(SPI_Transfer)로 끝.
  • CPOL/CPHA(모드)는 데이터시트 보고 맞춰주면 된다. 흔한 건 모드 0.

I2C보다 선은 많지만 프로토콜은 오히려 단순하다. START/STOP/ACK 같은 거 신경 안 쓰고 SS만 잘 내렸다 올리면 되니까. 다음엔 SPI 모드(CPOL/CPHA) 4개를 파형으로 하나씩 뜯어보든가 해야겠다.

'프로그래밍 > 마이컴' 카테고리의 다른 글

PWM (펄스폭 변조)  (0) 2026.07.19
ADC (아날로그 읽기)  (0) 2026.07.19
UART 통신  (0) 2026.07.19
ATMEGA32L External Interrupt  (0) 2013.08.14
I2C 통신  (0) 2013.08.14
반응형

C#에서 16bit grayscale PNG 파일을 Bitmap.FromFile 또는 Image.FromFile로 읽을 시 32bit argb 4바이트로 변환되어 읽게 된다.

16bit gray 이미지를 얻기 위해서는 다음과 같이 처리한다.

 


 

Stream imageStreamSource = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);PngBitmapDecoder decoder = new PngBitmapDecoder(imageStreamSource, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
BitmapSource bitmapSource = decoder.Frames[0];
int stride = (int)bitmapSource.PixelWidth * (bitmapSource.Format.BitsPerPixel / 8);
byte[] data = new byte[(int)bitmapSource.PixelHeight * stride];


bitmapSource.CopyPixels(data, stride, 0);

PNG 파일을 FileStream으로 읽고 PngBitmapDecoder로 BitmapSource를 얻는다.

그후 CopyPixels로 raw데이터를 복사한다.

 

'프로그래밍 > C#' 카테고리의 다른 글

Window class name is not valid  (0) 2014.08.06
C# array char to String 변환  (0) 2013.08.14
쓰레드 종료 지연 문제  (0) 2013.08.14
변수 타입  (0) 2013.08.14
반응형

개인적인 일과 OS 만드는 과정에서 너무 지식이 부족함을 느끼고.. 한동안 공부만 했다.

이해 안되는 개념은 이해 될때까지 예제와 문서 읽으면서 공부를 하긴 했는데 역시나 OS 만드는 것에 대한 지식이 너무 방대함을 느끼고…좌절(?).

오래된 노트북 책장속에 꼽아 놓고, 가끔 켤 때 마다 보이는 바탕화면에 OS 만들기 폴더를 볼 때 마다 내 자신이 한심해지는 것 같아 더더욱 의도적으로 외면하게 되었다.

그러다가 죽은 노트북 살리면서 생각지도 않는 의욕이 불타 올라 , 다시 시작.


우선 페이징을 설명하기 전에 가상 메모리에 대해서 설명이 필요하다.

가상 메모리 단어에서 어느 정도 무엇을 내포된 의미를 알 수 있듯이 메모리를 가상화 한다는 거다.

?????? 메모리??? 내컴퓨터에 달려있는 램???

그렇다. 이 램을 가상화 한다는 거다. 가상화 라는 단어가 어려울진 모르겠는데 쉽게 내컴퓨터에 2G짜리 램 하나 달려 있는데 OS에서 4G 달려 있는 것처럼 속인다(?)라는 거다. 사기???? 맞다.. 사기치는 거다. 짜임새 있고 논리적으로…. 프로세스(프로그램)며 OS까지 전부 다 개별적으로 4G를 쓸 수 있게 해주는 황금 알을 낳게 해주는 기술이다. (배를 갈라보자…)

그럼 왜 사기 치는 걸까???
(당연하지 않나? 돈이 없어서.. ㅋㅋㅋㅋㅋ)

점점 소프트웨어들의(프로그램,OS) 기능이 커짐에 따라 메모리도 많이 소모하게 되었는데 이게 물리적인 메모리 램은 한정되어 있는 상황에서 서로 메모리 가져오기 전쟁을 벌어야 했기 때문이다.
MS-DOS 시절 한글 띄워 보겠다고 한글 램 상주 프로그램 실행 후 게임을 실행할 때를 생각해 보자… 메모리 부족으로 실행되지 않는다. 그러면 램 상주 프로그램 종료 시키고… 이것저것 최적화 해서 게임 실행 했었던 시절을 떠 올려 보니 이해가 쉬워 진다. 프로그램들은 메모리를 쓰고 싶은데 딴놈이 그놈의 메모리를 차지 하고 있으니 사용자가 간섭해서 종료 시키고 최적화 시켰던 그 시절…(추억이 새록새록하다.)

유한한 메모리 공간이 가상 메모리 기술을 사용한다고 해서 갑자기 없던 메모리가 3차원 우주공간에 생성되어 그 공간을 사용하는 그런 공상 과학적인 그런건 아니다. 앞에서 말했던 것처럼 속이는 거다. 어떻게????

우선 가상메모리는 개별 프로그램 및 OS에게 너에게 주어진 메모리 공간은 4G이다.(난 32bit OS를 목표로 하기 때문에 4G로 표기한다.) 라고 넉넉한(?) 메모리 공간을 제시한다. 그러면 좋아라 하겠지…. 마구마구 메모리 할당하고 쓸것이다. 지지고 볶고 . 어라 저놈도 메모리 크게 잡네?? 나도 잡자 … 마구 마구 할당 하면 그래 버블이 발생한다. 버블이 발생하면? 죽는거지 뭐… (갑자기 옆길로……샜다.)

가상 메모리는 개별 프로그램들에게 메모리 공간 4G와 독립적 메모리 공간을 약속한다. 4G공간은 물론 가상 공간인 것일 테고 독립적 메모리 공간이라 함은 각 프로그램들의 메모리 공간은 서로 접근할 수도 없고 독립적으로 메모리 0번지 ~0xFFFFFFFF 까지 나만의 공간을 만들어 준다는 것이다.
예를 들어 A프로그램이 메모리 0x100000에 어떤 데이터를 써 놨다고 하자. 그게 온라인 커뮤니티 아이디 , 패스워드라고 한다면 B라는 프로그램은 A프로그램이 0x100000 메모리 아이디, 패스워드를 저장해 놓은 걸 알고 있는 상황에서 메모리 0x100000 를 읽어 보면???? 아무것도 없다.  이는 각 프로그램마다 제공된 메모리 공간은 독립적으로 타 프로그램과 공유하지 되지 않는다는 의미인 것이다.

(독립된 가상 메모리는 실제 컴퓨터 메모리상의 주소와 일치 하지 않는다. A프로그램 0x100000 가상 메모리 주소는 실제 메모리 0x100000에 적재되지 않는다. 가상 메모리상의 주소는 실제 메모리 주소가 결정되지 않는 논리적 가상의 주소일 뿐이다. 이렇게 되지 않는다면 A프로그램과 B프로그램이 0x100000을 사용하게 된다면 메모리가 독립적 사용이 불가능 하게 된다. 이는 페이징에서 좀더 설명한다.)

그리고 가상 메모리를 프로그램 별 4G로 준다는 의미는 무엇인가? 분명히 앞서 설명한 바에 의하면 각 프로그램은 실제 램 2G가 설치된 컴퓨터에도 사기친(?) 4G를 제공한다고 했다. 그렇다면 위에서 예를 들었던 A,B 프로그램 두개가 독립적인 메모리 공간으로 4G를 가지고 있으므로 4G X 2 = 8G 라는 공간이 컴퓨터에 발생이 필요 할 텐데 가상 메모리는 물리적 메모리 2G로 어떻게 8G라는 공간을 만들어 놓을까?? (사기는 그럴듯 해야 사기빨이 받는 법이다…)

이런저런 아키텍처를 연구하는 사람들이 가만히 프로그램들을 살펴보니 4G가 공간을 주더라도 0x00000000 ~ 0xFFFFFFFF 메모리 공간에 차곡차곡 메모리를 쌓아 쓰지 않는 점을 밝혀 냈단다. 또한 프로그램 이라는게 4G메모리를 전체 할당해서 사용한다고 해도 메모리를 사용(CPU가 사용 시점)하는 시점에는 전체가 아닌 부분만 사용한다는 패턴을 찾아 냈다.  그러니까 메모리를 4G 전체 할당 한 상태에서 실제 CPU가 사용하는 메모리는 극히 작은 부분만 필요하다는 것이다.

   
image

즉, A,B 프로그램에서 8G를 사용할 경우, 실제 CPU가 사용하는 부분만 메모리에 올리고 나머지 부분은 우리가 흔히 OS사용하면서 보았던 페이징 파일로 디스크에 저장해 놓는다. 즉 실제 컴퓨터 메모리 2G는 A프로그램과 B프로그램 전체의 메모리를 저장하지 않고 페이징 파일로 저장해 놨다가 필요 할 때 실제 컴퓨터 메모리로 필요한 만큼 올리는 것이다. (필요할 때 필요한 만큼 꺼내 쓴다….역시나 실제 메모리에 올려 놓은 데이터가 더이상 쓰지 않거나 당분간 쓰지 않을 것 같을 때는 다시 파일로 보낸다.) 이렇게 실제 메모리와 디스크에 메모리 데이터를 서로 교환 작업함으로써 각 프로세스가 독립된 4G 공간의 메모리를 확보 할수 있게 되는 것이다.


페이징이란 앞서 설명한 가상 메모리를 구현 할 수 있게 만드는 메모리를 관리 기법을 말한다. 위 가상 메모리 설명에서 CPU가 필요로 할 때 페이징 파일에서 실제 메모리로 필요한 만큼 꺼내 쓴다라고 설명했다. 이때 실제 메모리로 페이징 파일에서 데이터를 옮기는 데이터량의 최소 단위를 페이지 말한다. 다시 말하면 메모리의 크기를 페이지 단위로 나눠서 데이터를 이동시킨다 라고 할 수 있다. 여기서 메모리를 페이지 단위로 나눠서 관리하는 기법을 페이징 이라고 말한다고 한다. (메모리를 페이지 단위 쪼개는 작업이라고 볼 수 있다.) 페이지는 2 제곱 값으로 지정 될 수 있으며 보통 4096byte(4kb) 으로 설정된다. (리눅스나 윈도우도 4KB)
메모리를 페이지 단위로 나누는 작업 자체가 페이징이라고 했지만 좀더 심화된 작업이 있다. 위에 가상 메모리 4G 를 구현하기 위해서는 페이징 이라는 작업을 해줘야 내 OS에 4G 가상 메모리 공간을 만들 수 있다. 조금 많이 난해하고 어렵다. 가상 메모리라는 용어하고 페이징이 결합하면서 실제 주소, 가상 주소 단어가 등장하게 된다. 이 단어가 글에서 왕복된 설명이 나오는 순간.. 머리가 글을 읽는 눈을 쫒아 가지 못하고 … 퍼진다. (그래도 진행해 보자….)

각 프로세스별 4G 가상 메모리 공간을 만들기 위해선 가상 메모리 4G 공간을 페이지 단위로 나눈다. 이 나눠진 페이지들을 표로 만들고 각 페이지 마다 번호를 붙여 보면 0부터 1048574 까지 번호가 부여 될 것이다. (4294967295(4G) ÷ 4096(페이지) = 1048575가 나온다. ) 1048575개의 페이지는 1MB 공간을 뜻하는 것이다. 즉 가상 메모리 4G를 구현하기 위해서는 실제 메모리 1MB의 할당이 필요해지며 여기에 가상 메모리 4G를 실제 메모리에 연결하기 위해서 각 페이지 마다 연결 참조 포인트 4Byte를 추가하면 4M 바이트의 공간이 각 프로세스 마다 고정적으로 생긴다는 결혼이다.

image
image

프로세스 마다 4MB 공간을 필요로 하다는 말은 프로세스 마다 고정적인 4MB의 메모리를 사용한다는 말인데 이건 정말 큰 메모리이다. 위 계산은 한 프로그램이 4G를 다 사용 할 때의 계산으로 작은 메모리(100kb 미만 메모리 할당)만 사용하는 프로그램도 4M의 고정적인 메모리를 할당 하고 실행된다.
이에 작은 메모리를 사용하는 프로그램과 많은 메모리를 사용하는 프로그램들의 독립적 메모리 공간과 4G의 가상 메모리를 제공하고 각각의 사용 메모리별 형평성(?), 계산 속도 최적화를 밤낮 연구하다가 방법을 만들어 냈다. (밤낮 연구했는지 그냥 알고 있는 내용을 구현했는지 난 모른다. 똥 싸다가 생각해 냈는지도 모른다. 또는 그냥 어렵게 만들려고 했을지도 모르겠고.. 그냥 이런 방식이 있다는걸 제안했다..)

우선 4바이트를 가진 1024개의 배열을 만든다. 총 크기는 4kb 이다. 이걸 프로그램 실행 때마다 할당 한다. 4M에서 4kb로 1024배로 줄었다. 오~~~~~~~~~~~. 다이어트를 제대로 해 버렸다. 이건 다이어트 수준이 아니라 병수준이다… 흠… 여튼… 4M를 4kb로 줄이는 대신 복잡해 졌다. 뭐든 복잡한게 간단해지면 그 반대 급부로 무언가가 발생한다…(절대량은 변하지 않는다??? ㅋ)

자 , 이 4kb가 어떻게 4G를 공간을 표현하는지 살펴 보자.

우선 4byte 크기로 1024 개의 배열을 만든다. 이 배열의 각 인덱스(요소) 들은 또다른 4byte크기의 배열의 시작 주소를 가르킨다. 어렵지~~~ ㅋㅋㅋㅋㅋ… 1024개의 배열이 있는데 배열의 각 인덱스는 4byte크기의 1024 배열을 가지고 있다 라고 표현해도 어려울 것이다. 이건 그림으로…

image
1024개의 배열의 각 인덱스는 1024개의 배열의 첫 시작 주소를 가지고 있다. 1024의 배열의 크기가 4096 이고 각 배열 인덱스가 4byte 1024 배열을 가지고 있으므로 4096 + (1024 x 4096) 은??? 약 4Mb이다. 기존 4MB 고정 보다 4G 전체 할당 했을 시 보다 4kb 더 많긴 하지만 이렇게 하면 1Mb의 작은 메모리를 할당하는 프로그램들은 4kb + 4kb 즉 8kb만 프로그램 할당 되므로 뭐… 나름 괜찮다고 볼수 있다. 
왜 8kb 냐고??? 음… 프로그램 실행때 기본 생성되는 4kb와 첫번째 배열 인덱스 하나는 총 4Mb까지의 메모리를 가르킨다. 주소를 가르키는 4byte가 1024개 있으므로 4MB이다.
즉 첫번째 배열의 각각의 인덱스는 메모리 주소 4MB 제곱으로 메모리 주소를 가르킨다라고 할 수 있다.


이제 4G 주소 공간 표현을 개념적으로 알아봤으니 실제로는 어떻게 계산되는지를 알아보자.

주소를 나타내는 포인터는 4바이트이다. (당연하지 않겠나… 4G까지 가르킬려면 4바이트가 필요하다) 이 주소를 32비트로 표현해서 비트 23번째 비트부터 32번째 비트를 첫번째 배열 인덱스값. 13번째 부터 22번째 비트까지를 첫번째 배열이 가르키는 배열의 인덱스값으로 구분한다. 그리고 나머지 1번째 부터 12번째 비트는 오프셋 값으로 구분하면 된다.

image
이해를 돕기 위해 그림을 그렸다. 위 그림은 가상 주소 0x1234가 어떻게 실제 메모리를 가르키게 되는지를 표현한 그림이다.
1. 23bit~32bit가 0 이므로 첫번째 배열(주황색) 인덱스 0을 가르킨다.
2. 첫번째 인덱스 0은 두번째 배열(연두색)의 주소를 가르키고 있다.
3. 13bit~22bit는 1 번째 인덱스를 가르킨다.
4. 그러면 첫번째 배열 인덱스0이 가르키는 두번째 배열 1번째 인덱스를 가르킨다고 볼수 있다.
5. 두번째 배열 1번째 인덱스는 실제 메모리 주소를 가르키고 있다. 어느 주소인지는 모른다. 할당 할 당시에 주소이기 때문에 임의의 주소일 것이다. 이 임의의 주소에서 564번째를 가르킨다.
실제 메모리도 4096 단위로 나눠져 있는 주소 단위이다. 두번째 배열 1이 가르키고 있는 주소는 4096 단위로 정렬된 주소이므로 이 주소에서 564 오프셋 만큼 떨어진 지점을 가르키고 있다.


이 가상 주소 변환 작업은 고맙게도 CPU가 해준다고 한다. 아니 CPU안에 있는 MMU 라는 놈이 해준단다. 그리고 위 첫번째 배열 이니 두번째 배열이니 하는 배열도 각자 용어가 따로 존재한다. 
난 이해를 돋기 위해 배열 이라고 사용했다. (뭐…..프로그램상으로 배열로 처리…) 이는 배열이 아닌 테이블이라고 불린다. 또한 첫번째 배열이라고 칭했던건 Page Directory Entry(PDE)라고 하며 첫번째 배열 PDE가 가르키는 배열(테이블)은 Page Table Entry(PTE) 라고 불리고 있다.
각 프로그램마다 첫번째 배열. 즉 PDE 4kb가 할당되어져 있다. 이를 CPU가 어느 프로그램을 선택해서 실행이라는 나쁜(?)짓을 할려고 할 때 이 PDE를 CPU에 등록한다. 즉 CPU가 작업 하는 프로그램을 변경하는 시점(컨텍스트 스위칭 이라고 하는데…..후에….)에 PDE 테이블 주소만 설정해 주면 알아서 다 해 준다.




이로써 개념 설명은 끝났다.

지금까지 바람 OS는 보호모드로 진입후 키보드 처리 하는 단계까지 구현되어 있는데 이제 가상 메모리 처리를 함으로써 명실상부한 32비트 OS로 거듭날 예정이다. 물론 구현에서 제대로 된 동작까지는 ……휴…갈길이 멀다… 어떤일이 펼쳐 질지는… 또 한 2년 잠수할지도….





'프로그래밍 > OS 만들기' 카테고리의 다른 글

OS 만들기 #19 키보드 처리 및 쉬어가기.  (0) 2015.08.16
OS 만들기 #18 IDT - 3  (0) 2015.08.12
OS 만들기 #17 IDT - 2  (0) 2015.08.10
OS 만들기 #16 IDT - 1  (0) 2015.07.30
OS 만들기 #15 커널-키보드 입력  (0) 2015.02.09
반응형

IDT까지 어찌어찌 해서 처리는 해 놓은 것 같다. 그런데 갈수록 함수가 늘고 이것저것 변수들도 늘고 이러니 메인 파일이 지저분 하다. “뭐… 언제 시간 나면 한번 정리 해야지..~

그런데… 다음에 할 내용이 조금 무겁고 어렵다.. 페이징. 많이 어렵다. 이걸 구현 하느리 기존 소스 정리나 하는 게 나을 것 같다. 쉴겸…..~~

우선.. StartKernel() 함수를 좀더 간단하게 정리했다.

int  StartKernel(int argc, char *argv[])
{
	char *hello = "Hello OS!";
	char *complete = "complete.";
	BYTE* vidmem = (BYTE*)0xB8000;
	BYTE data = 0;
	int b = 5;
	int c = 0;


	kprint(hello);

	InitPIC();
	Init_IDT();
	Init_kbd();
	
	while(1) ;
	return 0;
}

InitPIC() PIC 초기화 함수

Init_IDT() IDT 초기화 함수

Init_kbd() 키보드 관련 초기화 함수.

많이 간단해졌다. 

printf 함수도 다시 구현하였다… 커널에서 프린트 한다는 의미로 kprint (많은 OS 들도 kprint 라고 하더라 ^^ ㅋㅋㅋ. 그래서 이름을 차용했다.)로 함수명을 바구고.. 문자열만이 아닌 가변 인자를 받아 출력 할 수 있도록 수정하였다.

문자열 관련 함수들 . strlen, strcpy, strcmp, memcpy, memcmp, memset 이런 함수들은 그냥 기존 코드들을 가져왔다. 이런걸로 시간 뺏기기 싫어서..(라고 쓰고… 만들 줄 모르고… 다시 작성한다는게 귀찮아서~^^)..^^

그리고 IDT 최종 키보드 입력 부분까지 구현했었는데..

문제는 대문자와 특수문자( !~@#등) 출력이 되지 않았다… 그래서 키보드 테이블을 대문자와 특수문자를 출력 할 수 있도록 SHIFT키와 CAPS LOCK키를 지원하도록 수정했다.

#include "stdio.h"
#include "keyboard.h"
#include "8042.h"
#include "asm.h"

BOOL IsLShift = FALSE;
BOOL IsRShift = FALSE;
BOOL IsCapslock = FALSE;
BOOL IsAlt = FALSE;
BOOL IsCtrl = FALSE;
BOOL IsNumLock = FALSE;


KeyboardInfo kbd_scancode[] = 
{
	{0x00,	0x00, 0x00},	// ESC
	{'1','!',	0x00, 0x00},
	{'2','@',	0x00, 0x00},
	{'3','#',  	0x00, 0x00},
	{'4','$', 	0x00, 0x00},
	{'5','%', 	0x00, 0x00},
	{'6','^',	0x00, 0x00},
	{'7','&',	0x00, 0x00},
	{'8','*',	0x00, 0x00},
	{'9','(',	0x00, 0x00},
	{'0',')',	0x00, 0x00},
	{'-','_',	0x00, 0x00},
	{'=','+',	0x00, 0x00},
	{0x00,0x00, 	0x00, 0x00},	// BACKSPACE
	{0x00,0x00, 	0x00, 0x00},	// TAB
	{'q','Q', 	0x00, 0x00},
	{'w','W', 	0x00, 0x00},
	{'e','E', 	0x00, 0x00},
	{'r','R', 	0x00, 0x00},
	{'t','T', 	0x00, 0x00},
	{'y','Y', 	0x00, 0x00},
	{'u','U', 	0x00, 0x00},
	{'i','I', 	0x00, 0x00},
	{'o','O', 	0x00, 0x00},
	{'p','P', 	0x00, 0x00},
	{'[','{', 	0x00, 0x00},
	{']','}', 	0x00, 0x00},
	{0x00,0x00, 	0x00, 0x00},	// ENTER
	{0x00,0x00, 	0x00, 0x00},	// CTRL
	{'a','A', 	0x00, 0x00},
	{'s','S', 	0x00, 0x00},
	{'d','D', 	0x00, 0x00},
	{'f','F', 	0x00, 0x00},
	{'g','G', 	0x00, 0x00},
	{'h','H', 	0x00, 0x00},
	{'j','J', 	0x00, 0x00},
	{'k','K', 	0x00, 0x00},
	{'l','L', 	0x00, 0x00},
	{';',':', 	0x00, 0x00},
	{'\'','"', 	0x00, 0x00},
	{'`','~', 	0x00, 0x00},
	{0x00,0x00, 	0x00, 0x00},	// Left Shift
	{'\\','|', 	0x00, 0x00},
	{'z','Z', 	0x00, 0x00},
	{'x','X', 	0x00, 0x00},
	{'c','C', 	0x00, 0x00},
	{'v','V', 	0x00, 0x00},
	{'b','B', 	0x00, 0x00},
	{'n','N', 	0x00, 0x00},
	{'m','M', 	0x00, 0x00},
	{',','<', 	0x00, 0x00},
	{'.','>', 	0x00, 0x00},
	{'/','?', 	0x00, 0x00},
	{0x00,0x00,	0x00, 0x00},	// right shift
	{0x00,0x00, 	0x00, 0x00},	// PrtSc
	{0x00,0x00, 	0x00, 0x00},	// ALT
	{' ',' ', 	0x00, 0x00},	// space
	{0x00,0x00, 	0x00, 0x00},	// caps
	{0x00,0x00, 	0x00, 0x00},	// F1
	{0x00,0x00, 	0x00, 0x00},	// F2
	{0x00,0x00, 	0x00, 0x00},	// F3
	{0x00,0x00, 	0x00, 0x00},	// F4
	{0x00,0x00, 	0x00, 0x00},	// F5
	{0x00,0x00, 	0x00, 0x00},	// F6
	{0x00,0x00, 	0x00, 0x00},	// F7
	{0x00,0x00, 	0x00, 0x00},	// F8
	{0x00,0x00, 	0x00, 0x00},	// F9
	{0x00,0x00, 	0x00, 0x00},	// F10
	{0x00,0x00, 	0x00, 0x00},	// number lock
	{0x00,0x00, 	0x00, 0x00},	// scroll
	{0x00,0x00, 	0x00, 0x00},	// home { numlook = 7 }
	{0x00,0x00, 	0x00, 0x00},	// up	{ numlook = 8 }
	{0x00,0x00, 	0x00, 0x00},	// pgup	{ numlook = 9 }
	{'-','-', 	0x00, 0x00},	// -
	{0x00,0x00, 	0x00, 0x00},	// left	{ numlook = 4 }
	{0x00,0x00, 	0x00, 0x00},	// center	{ numlook = 5 }
	{0x00,0x00, 	0x00, 0x00},	// right	{ numlook = 6 }
	{'+','*', 	0x00, 0x00},	
	{0x00,0x00, 	0x00, 0x00},	// end	{ numlook = 1 }
	{0x00,0x00, 	0x00, 0x00},	// down	{ numlook = 2 }
	{0x00,0x00, 	0x00, 0x00},	// pgdn	{ numlook = 3 }
	{0x00,0x00, 	0x00, 0x00},	// ins	{ numlook = 0 }
	{0x00,0x00, 	0x00, 0x00},	// del	{ numlook = . }
	{'/','/', 	0x00, 0x00},	// /
	{0x00,0x00, 	0x00, 0x00},	// enter
	{0x00,0x00, 	0x57, 0xD7},	// F11
	{0x00,0x00, 	0x58, 0xD8},	// F12
	{0x00,0x00, 	0x52, 0xD2},	// ins
	{0x00,0x00, 	0x53, 0xD3},	// del
	{0x00,0x00, 	0x47, 0xC7},	// home
	{0x00,0x00, 	0x4F, 0xCF},	// end
	{0x00,0x00, 	0x49, 0xC9},	// pgup
	{0x00,0x00, 	0x51, 0xD1},	// pgdn
	{0x00,0x00, 	0x4B, 0xCB},	// left
	{0x00,0x00, 	0x4D, 0xCD},	// right
	{0x00,0x00, 	0x48, 0xC8},	// up
	{0x00,0x00, 	0x50, 0xD0},	// down
	{0x00,0x00, 	0x38, 0xB8},	// right alt
	{0x00,0x00, 	0x1D, 0x9D},	// right ctrl
	{0x11,0x00, 	0x00, 0x00}};	// pause	// 99 index

void Init_kbd()
{
	int i = 0;

	for(i = 0; i < 0x54; i++ )
	{
		kbd_scancode[i].Press = i+1;
		kbd_scancode[i].Release = 0x81 + i;
	}

	Write_port_byte(0x21, 0xFD);
}

int GetScanCodeInfo(BYTE code, KeyboardInfo *pkbdInfo)
{
	int i;

	if( code > 0xD9 )	// Unknown code
		return UNKNOWN_SCANCODE;

	for( i = 0; i < 0x64; i++ )
	{
		if( kbd_scancode[i].Press == code )
		{
			*pkbdInfo = kbd_scancode[i];
			return PRESS_SCANCODE;
		}

		if(kbd_scancode[i].Release == code)
		{
			*pkbdInfo = kbd_scancode[i];
			return RELEASE_SCANCODE;
		}
	}

	return UNKNOWN_SCANCODE;
}

void Kbd_ScanCode()
{
	BYTE scancode;
	int scancode_type;
	KeyboardInfo	Info;

	scancode = GetKeyboardScanCode();

	scancode_type = GetScanCodeInfo(scancode, &Info);
		
	if(scancode_type == UNKNOWN_SCANCODE )
	{
		return;
	}

	if( scancode_type == RELEASE_SCANCODE )
	{
		switch(Info.Press)
		{
		case KEY_LSHIFT:
			IsLShift = FALSE;
		case KEY_RSHIFT:
			IsRShift = FALSE;
			break;
		case KEY_ALT:
			IsAlt = FALSE;
			break;
		case KEY_CTRL:
			IsCtrl = FALSE;
			break;
		}
		return;
	}

	if( scancode_type == PRESS_SCANCODE )
	{
		switch(Info.Press)
		{
		case KEY_LSHIFT:
			IsLShift = TRUE;
			break;
		case KEY_RSHIFT:
			IsRShift = TRUE;
			break;
		case KEY_ALT:
			IsAlt = TRUE;
			break;
		case KEY_CTRL:
			IsCtrl = TRUE;
			break;
		case KEY_CAPSLOCK:
			IsCapslock = !IsCapslock;
			break;
		case KEY_NUMBERLOCK:
			IsNumLock = !IsNumLock;
			break;
		case KEY_ENTER:
			kprint("\n");
			return;
		}

		if( IsCtrl || IsAlt )
		{
			// function key process
		}

		if( IsNumLock )
		{
			switch( Info.Press )
			{
			case 0x47:
				putc('7');
				return;
			case 0x48:
				putc('8');
				return;
			case 0x49:
				putc('9');
				return;
			case 0x4B:
				putc('4');
				return;
			case 0x4C:
				putc('5');
				return;
			case 0x4D:
				putc('6');
				return;
			case 0x4F:
				putc('1');
				return;
			case 0x50:
				putc('2');
				return;
			case 0x51:
				putc('3');
				return;
			case 0x52:
				putc('0');
				return;
			case 0x53:
				putc('.');
				return;
			}
		}

		if( IsCapslock )
		{
			if( Info.Lowercase >= 'a' && Info.Lowercase <= 'z')
			{
				if( IsLShift || IsRShift )
					putc(Info.Lowercase);
				else
					putc(Info.Uppercase);

				return;
			}
		}
		if( IsLShift || IsRShift )
		{
			if(Info.Lowercase != 0x00 )
				putc(Info.Uppercase);
			else
			{
				// function key process

				// otherwise
			}

			return;
		}

		if( Info.Lowercase != 0x00 )
			putc(Info.Lowercase);
	}
}

잘 된다.

그후 이것저것 바꿨다.

 

반응형

전번에 IDT를 구성하고 커널까지 컴파일 하고 부팅까지 해 보았다. 물론 아무런 기능도(?) 넣지 않아 눈으로 보기에는 아무 변화도 없었다. 재부팅 안하는게 어디야~~~. 우선 인터럽트 동작이 잘 동작 하는지 확인 해 보는 코드를 넣어서 내가 만든게 잘 동작하는지 확인해 보잣!!!! 아자~


int  StartKernel(int argc, char *argv[])
{
	char *hello = "Hello OS!";
	char *complete = "complete.";
	unsigned char* vidmem = (unsigned char*)0xB8000;
	unsigned char data = 0;
	int b = 5;
	int c = 0;


	printf(hello);

	InitPIC();
	Init_IDT();

	b = b/0;
	
	while(1) ;
	return 0;
}


void _declspec(naked) interrupt_handler()
{
	_asm pushad;

	printf("interrupt handler");
	_asm {
		popad
		iretd
	}		
}

인터럽트 중에 0으로 나누면 예외상황 인터럽트가 발생하는 인터럽트가 있었다. 그래서 0으로 나누면 분명히 인터럽트가 발생 할것이고 interrupt_handler함수에서 인터럽트가 발생하면 interrupt handler라는 문구를 찍도록 해보았다. 자~~ 컴파일..

헉!!!!! interrupt handler!! 무한 출력 후 재부팅 된다.

결과야 어찌되었든 인터럽트가 발생하여 화면에 출력이 되었다는 거에 만족하자~~ ㅋㅋㅋㅋㅋ 대충 했는데도 된다… ㅋ 오늘은 확인 했으니 이만 잘까??? 음… 뭐 IDT를 구현하게 된데에는 키보드 입력 받아서 출력 하는데 목적을 두었으니 키보드로 입력하면 키출력 하는데까지 하고 자야 겠다.

우선 0으로 나누는 부분을 지우고!~ 

키가 눌려지면 interrupt_handler함수가 호출 될테고 전번 포스팅 에서 했었던 것 처럼 0x60 포트를 읽으면 될것이다. 그러면 기존 코드를 붙여서..


void _declspec(naked) interrupt_handler()
{
	char keycode;

	_asm pushad;


	keycode = Read_port_byte(0x60);
	if(keycode >= 0 )
	{
		putc((char)kbd_scancode[keycode]);
	}
	_asm {
		popad
		iretd
	}		
}

짠,,… 컴파일은 잘 된다… 자 그럼!~

5..4..3..2..1 ……. 음… 안돼! 키 입력해도 아무런 변화가 없어!~~

왜???????????????????????????????????????????????????????????????????????????

IDT가 잘못 구성 되었나?? ….

소스를 봐도 뭐가 잘못 된 건지 모르겠다.. 디버깅도 안되고…ㅠㅠ 키보드 관련 검색부터 해보자..흠..

아….INIT_PIC함수에서 잘못 되었나? 그러고 보니 하드웨어 인터럽트 즉.. 8259 PIC는 하드웨어 인터럽트와 충돌이 나서 주소를 옮겨야 된다고 했다. 안 옮겨줬나?? 아닌데… 키보드 입력 되었는데… 너무 인터넷에서 긁어서 복사만 했나~~~ 흠. 그러면 다시 INIT_PIC 함수를 분석해보자 ..

void InitPIC()
{
	 
	Write_port_byte(0x20, 0x11);       
	Write_port_byte(0x21, 0x20);       
	Write_port_byte(0x21, 0x04);       
	Write_port_byte(0x21, 0x01);       
	Write_port_byte(0x21, 0xFF);         

	
	Write_port_byte(0xA0, 0x11);       
	Write_port_byte(0xA1, 0x28);       
	Write_port_byte(0xA1, 0x02);       
	Write_port_byte(0xA1, 0x01);       
	Write_port_byte(0xA1, 0xFF);        
}
      

Write_port_byte 함수야.. 뭐..첫번째 인자는 포트이고 두번째는 데이터…라는 것이고

0x20 포트에 0x11 데이터를 쓴다…. 왜?? 음.. 그냥 첫번째 PIC 아니 마스터 PIC 너를 초기화 할것이다. 라는 의미인것 같다.

Write_port_byte(0x21, 0x20) 이건 너의 인터럽트 시작 주소는 0x20 이다 라고 지정해주는 것이고.
Write_port_byte(0x21, 0x04) 요것은 음.. 마스터 PIC 3번째하고 슬레이브 PIC가 연결되어 있다라는걸 설정하는 것이란다. 3번째 인데 왜 4냐고?? PIC는 8개 비트로 구성되어 있는데~~~ 3번재 연결 핀이..

8bit 7bit 6bit 5bit 4bit 3bit 2bit 1bit
          1    

4가 된다..
Write_port_byte(0x21, 0x01) 이것은 8086 모드라는 것인데… 모르겠다.. 1로 설정 할 경우는 8086. 0인경우 80/85 모드라는데 .. 모르겠다. ㅡㅡ;;;;

Write_port_byte(0x21, 0xFF) ?????? 마스터 PIC 에서 모든 인터럽트를 막는다????? 이것 때문에 키보드가 입력 안되었던 것인가

Write_port_byte(0xA0, 0x11)  슬레이브 PIC 너를 초기화 해주겠어
Write_port_byte(0xA1, 0x28)  인터럽트 시작주소는 28 이다…
Write_port_byte(0xA1, 0x02);  넌 마스터PIC와 두번째 핀과 연결되어 있다.
Write_port_byte(0xA1, 0x01)  ?? 8086 ㅡㅡ;;;;;
Write_port_byte(0xA1, 0xFF)   모든 인터럽트를 막는다!!!!

그러니까… 저기 뻘건색으로 줄쳐진 이놈들 때문에 키보드가 동작하지 않은 것인가??? 왜???? 왜 막아야 하는건가??? 모르겠다… 지랄같은 영어.. 여튼.. 키보드는 마스터 PIC 두번째 핀에 연결되어 있다고 하니 0xFF를 0xFD로 바꿔보자… 그리고 컴파일 . 확인.

오!!!!!!!!!!!!!!

보라… Hello OS! 옆에 작은 a 으흐흐흐 동작한다… IDT 구성은 문제 없고 키보드 인터럽트도 잘 받고…??? 어라 그런데 한번만 입력된다…. 검색해보니 이러더라 EOI 를 보내라고 …. 그냥 EOI를 보내서 PIC 를 리셋 시켜야 된다고 한다.  마스터 PIC에서 인터럽트가 발생했으면 0x20 포트에다가 보내고… 슬레이브 PIC에서 발생했으면 0xA0과 , 마스터 PIC 0x20 둘다 보내야 한다고 한다. 그러면 인터럽트가 발생하면 발생한 인터럽트 IRQ를 알아야 마스터인지 슬래이브 인지 아는것 아닌가…흠… CPU가 인자로 넘겨주나??? 그런가 보다.. 넘겨주는 듯… 귀찮으니 우선은 키보드만 입력 받고… EOI를 보내라고 했으니 마스터PIC에 0x20을 보내고 다시 테스트.

흠…

잘 된다..


 

 

8042 키보드 컨트롤러

모르는건 인터넷에서 검색하다가 그것마저 귀찮으면 소스 찾아서 긁어서 사용하다 보니, 진행하다 보면 이게 뭐지? 하는 내용들이 종종 생긴다. “이것까지 내가 알아야 하나?” 하고 어물쩡 넘어갔는데 그런게 너무 반복 되다 보니  “내가 지금 뭐하고 있지?” 라는 생각이 요즘 많이 든다. 단순히 OS 만들것이면 인터넷에 널려 있는 소스들 수정해서 만들면 된다. 그냥 내가 목표로 하는 OS만 결과로 던져 내면 되는 것이다.  자… 그러면 난 지금 무엇을 하고 있는 것인가? OS를 만드는 것인가… OS를 만들기 위해서 공부를 하고 있는 것인가… 흠 그러다가 OS 만들기의 대단원의 시작인 첫 포스팅 글을 방금 다시 한번 찾아봤다.

OS를 만들기 위해 부랴부랴 인터넷 검색을 해봤다. 우와! 책도 많고 관련 정보들이 상당하다. 수준급인 OS 부터 기초 정보 또한 대단히 많다. ‘그냥 이거 가져다 수정하면서 공부해 보면 되겠네…’ 라고 생각했다가 접었다.. 왜? 내 코드가 아니고 내 구조가 아니고… 내 영혼(?)이 아니다. 그냥 하나 하나 내가 부딪치며 만들어야 내것 같다는 생각이 들어서다. 또 뭔가 고비가 있어야 내 지식이지, 단순히 복사 & 붙이기는 그냥 영혼없는 결과물 만들기일 뿐이라고 생각해서 이다.

ㅎㅎㅎ 무척 단순한 생각으로 시작했다. 남의 코드는 내것이 아니므로….^^ 영혼까지 언급했다.

복사 & 붙이기 로 결과물을 만들지 말자고 했는데 지금은 복사 & 붙이기 로 만들고 있다….. 그래… 다 공부해서 만들기로 했었지..~~~ㅎㅎㅎ

내가 왜 이런 심각한 고민을 하게 된건 , 8042 키보드 컨트롤러가 키보드 관련해서 검색해 보다가 등장해서 이다. 키보드에서 인터럽트가 발생하게 될 경우, 8259 에서 인터럽트를 CPU에 알리고 다시 0x60 포트에서 키 값을 읽으면 된다. 라는 이 부분에서 0x60 이 포트가 궁금해서 찾아보다가 찾았다. 그냥 0x60에서 키 값을 읽으면 된다 라고 하고 넘어갈까 말까 망설였었다. 왜?? 귀찮으니까~~~

ㅎㅎㅎㅎ

여하튼.. 그냥 하자!!!~~~~~~

키보드와 마우스는 시리얼 통신을 한다고 배웠고 그렇게 되어 있다고 한다. 이 키보드와 마우스는 우리가 쉽게 얘기하는 PS/2 라고 불리우는 8042 컨트롤러와 연결 되어 있고 말이다. 키보드/마우스 제어 외에도 앞선 포스팅에서 언급한 주소 확장 관련해서 A20 Enable 기능도 8042 컨트롤러가 제어한다고 한다.

위 그림은 8042와 8259PIC와 CPU 연결을 도식화 한 것이다. 키보드나 마우스 입력 시 8042는 8259 PIC 마스터나 슬레이브에 IRQ 신호를 보내고 8259는 이를 CPU에 보낸다. 또 CPU는 8042를 0x60 또는 0x64 포트를 통해 데이터를 읽던지 제어를 한다고 한다.

IO 포트

접근 구분

용도

0x60 Read/Write 데이터 읽고 쓰기
0x64 Read Status 읽기
0x64 Write 명령 요청

전번 포스팅에서 키보드에 데이터 입력이 있는지 없는지 알기 위해서 0x64 포트의 Status를 읽어서 사용한 적이 있었다.

또 키보드 관련해서 알아야 할 내용이 생겼다. Set1, Set2, Set3… 이게 뭐냐면.. IBM 에서 콤퓨타를 만들어 낼 때 마다 새로운 키보드 인터페이스를 만들었었는데 XT 키보드, AT 키보드, PS/2 키보드 등으로 구분된다고 한다. 또한 이 키보드 타입 들은 같은 문자에 따라 다른 값들을 보낸다고 한단다… 그래서 각 값들을 Set1, Set2, Set3에 맞게 찾아서 문자를 대입해야 한다고 한다. Set1은 XT , Set2는 AT, Set3은 PS/2.
내가 코드에서 사용했던 건 Set1 이다. 8042를 통해 현재 사용하고 있는 키보드의 인터페이스를 얻어 낼 수 있고 지정 할 수 있다고 한다.

음.. 8042 명령어들과 기능들도 있는데 이건 필요할 때 마다 참조 하는 것으로 하고 8042에 대해서 여기서 마쳐야 겠다.

반응형
Tistory 태그: ,,,,

저번에 Interrupt Descriptor Table에 대해서 대충(?) 알아봤다. 그래서 이번에는 구현해 보기로 했다. 막상 구현하려고 하니 뭘 해야 할지 답답… ㅎㅎㅎ 우선 저번에 간단하게 작성해 놓은 IDT 구조체 다시 한번 작성해 놓고..

typedef struct _IDTDesc
{
	unsigned short	Offset_low;
	unsigned short	Selector;
	unsigned char	Unused;
	unsigned char	Access;
	unsigned short	Offset_high;
} IDTDesc;

음… 이제… 이 구조체가 여러개 존재 한다고 하니… 배열로… 구성해서…근데 몇개??? 몇개를 해야 하지??? 음… 그냥 1바이트 255개..ㅡㅡ;;;;; 대충해 보자.. 그래….

#define MAX_IDT	255

IDTDesc IDT[MAX_IDT];

음…그리고….에.. 그러니까.. 테이블을 만들었으니까 GDT 처럼 GDTR 이라는 레지스터에 이 테이블 주소를 넣어주는 …아.. 그래 IDTR에 이 테이블 주소를 넣어줘야지!!! ㅎㅎㅎ 

그러면 IDTR 구조.

32bit 16bit            0bit
IDT의 메모리상에서 위치 IDT의 크기

뭐… 별거 없다.. GDTR 구조와 같다…그러면..

typedef struct _IDTRegistor
{
	unsigned short	Size;
	unsigned int	Address;
} IDTRegistor;

IDTRegistor	IDTR;

음… 이렇게 만들었으면 예외상황 인터럽트, 하드웨어 인터럽트, 시스템콜 들을 등록 하라고 하는데 ….. ??? 어떻게 하지?? 검색해 보자…!!!!!!…………………………………………………………

저번에 포스팅한 예외상황 인터럽트 번호 0 부터 … 채울 수 있을 때까지 채우란다.. 시스템콜들은 뭐라뭐라 하는데 그냥 건너뛰고… 예외상황 하고 하드웨어 입력….!!! 그런데 말이다…. IDT 각각의 항목이 뭔지를 알아야 채우지~~~

Offset_low(2Byte)
Offset_high(2Byte)

요 두 놈들은 인터럽트 발생 시 인터럽트가 해야 할 (ISR이라고 저번에 적었지!~~^^ 잊어버렸었다.) 작업 위치 란다. 즉, 해당 함수의 메모리 상 위치.

Selector(2Byte)

요놈은 보호모드 진입시 GDT 테이블에 적어 놓은 CODE, DATA 셀렉터를 입력하는 놈이라는 데… 난 아직도 CODE와 DATA가 어떤 의미인지 모르겠다.. 그냥, CODE 셀렉터… 음… GDT 테이블에서 0x08 가 CODE 니까.. 저긴 0x08로 입력하는 걸로.

Unused(1Byte)

요넘은 사용 안하니 패쓰하고

Access(1Byte)

요놈은 그러니까… 전번에 한번 적었던 기억이 나는데…...http://manggong.org/41 여기…

P, DPL, S, TYPE 요렇게 되어 있고. P는 1로 하라고 하니 1. DPL 특권 레벨에 대한 것이라고 하고 레벨 0, 유저는 3이라고 하는데 0. S는 그냥 0.
TYPE은 여러 종류가 있는 것 같은데 보호 모드로 진입해서 32비트 라고 하고 Trap Gate는 소프트웨어 인터럽트나 예외 인터럽트, Interrupt Gate는 하드웨어 인터럽트. 그러면 0xE 아니면 0xF 인것 같다.

자, 그러면 Access 이놈한테 줄 수 있는 값은 10001110(0x8E : 소프트웨어 인터럽트, 예외인터럽트), 또는 10001111(0x8F : 하드웨어 인터럽트)

흠.. 이제 IDT를 채워보자.

아… 그러고 보니 Offset_low, high에 입력 해 줄 인터럽트 서비스 루틴(ISR) 함수가 없다.. 음… 더미 함수 하나 만들어 보고……근데 그냥 만들면 되나??? ……….이거 뭐 하나 쉽게 진행이 안되니…
http://wiki.osdev.org/Interrupt_Service_Routines에 의하면

void _declspec(naked) interrupt_handler()
{
    _asm pushad;
 
    /* do something */
 
    _asm{
        popad
        iretd
    }
}

이런식으로 만들면 된다고 한다. 그래 그냥 저 함수 저대로 쓰자~~ ^^

자 그러면.. 입력!

void Init_IDT()
{
	int i;
	unsigned int high, low;
	
	for(i = 0; i < MAX_IDT; i++ )
	{
		IDT[i].Selector = 0x08;
		if( i < 0x14 )
			IDT[i].Access = 0x8E;		// 예외 인터럽트
	    else
			IDT[i].Access = 0x8F;		// 하드웨어

		IDT[i].Unused = 0x00;

		low = (unsigned int)&interrupt_handler;
		low = low & 0xFFFF;

		high = (unsigned int)&interrupt_handler;
		high = (high >> 16);

		IDT[i].Offset_high = (unsigned short)high;
		IDT[i].Offset_low = (unsigned short)low;
	}
}

그리고 내친김에 IDTR도…

	IDTR.Address = (unsigned int)&IDT;
	IDTR.Size  = MAX_IDT * sizeof( IDTDesc );

다 입력했다… 마지막으로 이제 어셈블리어 LIDT 로 IDTR을 로딩시켜 주면…~~~~~~~ 된다고 하는데 … 어셈은 어려워 ㅠㅠ … 대충 해보자….

void LoadIDTR(unsigned int addr)
{
	_asm
	{
		mov	eax, [addr]
		lidt	[eax]
		sti
	}
}

ㅎㅎㅎㅎ lidt에 idtr 주소 넣어 주고 sti 명령어로 인터럽트를 활성화 시켜주면 된다고 한다. 자…. 한번 해볼까??

void InitPIC();
void Init_IDT();
void interrupt_handler();
void LoadIDTR(unsigned int addr);

unsigned char Read_port_byte(unsigned short port);
void Write_port_byte(unsigned short port, unsigned char data);


typedef struct _IDTDesc
{
	unsigned short	Offset_low;
	unsigned short	Selector;
	unsigned char	Unused;
	unsigned char	Access;
	unsigned short	Offset_high;
} IDTDesc;

typedef struct _IDTRegistor
{
	unsigned short	Size;
	unsigned int	Address;
} IDTRegistor;


#define MAX_IDT		255

IDTDesc	IDT[MAX_IDT];
IDTRegistor	IDTR;

unsigned char	*Screen = (unsigned char*)0xB8000;

#define	KEY_F1		0x80
#define	KEY_F2		(KEY_F1 + 1)
#define	KEY_F3		(KEY_F2 + 1)
#define	KEY_F4		(KEY_F3 + 1)
#define	KEY_F5		(KEY_F4 + 1)
#define	KEY_F6		(KEY_F5 + 1)
#define	KEY_F7		(KEY_F6 + 1)
#define	KEY_F8		(KEY_F7 + 1)
#define	KEY_F9		(KEY_F8 + 1)
#define	KEY_F10		(KEY_F9 + 1)
#define	KEY_F11		(KEY_F10 + 1)
#define	KEY_F12		(KEY_F11 + 1)

#define	KEY_INS		0x90
#define	KEY_DEL		(KEY_INS + 1)
#define	KEY_HOME	(KEY_DEL + 1)
#define	KEY_END		(KEY_HOME + 1)
#define	KEY_PGUP	(KEY_END + 1)
#define	KEY_PGDN	(KEY_PGUP + 1)
#define	KEY_LFT		(KEY_PGDN + 1)
#define	KEY_UP		(KEY_LFT + 1)
#define	KEY_DN		(KEY_UP + 1)
#define	KEY_RT		(KEY_DN + 1)
#define	KEY_PRNT	(KEY_RT + 1)
#define	KEY_PAUSE	(KEY_PRNT + 1)
#define	KEY_LWIN	(KEY_PAUSE + 1)
#define	KEY_RWIN	(KEY_LWIN + 1)
#define	KEY_MENU	(KEY_RWIN + 1)

	static const unsigned char kbd_scancode[] =
	{
0,	0x1B,	'1',	'2',	'3',	'4',	'5',	'6',
'7',	'8',	'9',	'0',	'-',	'=',	'\b',	'\t',
'q',	'w',	'e',	'r',	't',	'y',	'u',	'i',

'o',	'p',	'[',	']',	'\n',	0,	'a',	's',
'd',	'f',	'g',	'h',	'j',	'k',	'l',	';',

'\'',	'`',	0,	'\\',	'z',	'x',	'c',	'v',
'b',	'n',	'm',	',',	'.',	'/',	0,	0,
0,	' ',	0,	KEY_F1,	KEY_F2,	KEY_F3,	KEY_F4,	KEY_F5,
KEY_F6,	KEY_F7,	KEY_F8,	KEY_F9,	KEY_F10,0,	0,	KEY_HOME,
KEY_UP,	KEY_PGUP,'-',	KEY_LFT,'5',	KEY_RT,	'+',	KEY_END,
KEY_DN,	KEY_PGDN,KEY_INS,KEY_DEL,0,	0,	0,	KEY_F11,
KEY_F12
	};

void printf( char *msg )
{

	while(*msg != '\0')
	{
		*Screen++ = *msg++;
		*Screen++ = 7;
	}
}

void putc(char Code)
{
	*Screen++ = Code;
	*Screen++ = 7;
}



int  StartKernel(int argc, char *argv[])
{
	char *hello = "Hello OS!";
	char *complete = "complete.";
	unsigned char* vidmem = (unsigned char*)0xB8000;
	unsigned char data = 0;
	int b = 5;
	int c = 0;


	printf(hello);

	InitPIC();
	Init_IDT();


	while(1) ;
	return 0;
}

unsigned char Read_port_byte(unsigned short port)
{
	unsigned char KeyCode = 0;

	__asm
	{
		mov		dx, port
		xor		eax, eax
		in		ax, dx
		lea		ebx, KeyCode
		mov		[ebx], al
	}

	return KeyCode;
}

void Write_port_byte(unsigned short port, unsigned char data)
{
	__asm
	{
        mov		dx, [ebp+8]
        mov		al, [ebp+12]
        out		dx, al
	}
}

void InitPIC()
{
	 
	Write_port_byte(0x20, 0x11);       
	Write_port_byte(0x21, 0x20);       
	Write_port_byte(0x21, 0x04);       
	Write_port_byte(0x21, 0x01);       
	Write_port_byte(0x21, 0xFF);         

	
	Write_port_byte(0xA0, 0x11);       
	Write_port_byte(0xA1, 0x28);       
	Write_port_byte(0xA1, 0x02);       
	Write_port_byte(0xA1, 0x01);       
	Write_port_byte(0xA1, 0xFF);        
}


void _declspec(naked) interrupt_handler()
{
	_asm pushad;


	_asm {
		popad
		iretd
	}		
}

void Init_IDT()
{
	int i;
	unsigned int high, low;
	
	for(i = 0; i < MAX_IDT; i++ )
	{
		IDT[i].Selector = 0x08;
		if( i < 0x14 )
			IDT[i].Access = 0x8E;		// 예외 인터럽트
	    else
			IDT[i].Access = 0x8F;		// 하드웨어

		IDT[i].Unused = 0x00;

		low = (unsigned int)&interrupt_handler;
		low = low & 0xFFFF;

		high = (unsigned int)&interrupt_handler;
		high = (high >> 16);

		IDT[i].Offset_high = (unsigned short)high;
		IDT[i].Offset_low = (unsigned short)low;
	}

	IDTR.Address = (unsigned int)&IDT;
	IDTR.Size  = MAX_IDT * sizeof( IDTDesc );

	LoadIDTR((unsigned int)&IDTR);
}

void LoadIDTR(unsigned int addr)
{
	_asm
	{
		mov	eax, [addr]
		lidt	[eax]
		sti
	}
}


예전에 키보드 입력 받았던 코드에서 PIC를 초기화 해주고…!!! (하드웨어 인터럽트 발생해야 하니깐…) 컴파일 해서 확인!!!

아무 반응 없다…

뭐… 재부팅 안된것만 해도 다행이다.

키보드 입력을 받았으면 좋겠는데… 피곤해서 오늘은 그냥 여기까지…

다음시간에는 키보드 입력 루틴까지..

 

+ Recent posts