AT89C52RC 마이크로컨트롤러 타이머/카운터 모듈 완벽 이해

AT89C52RC 마이크로컨트롤러 타이머/카운터 모듈 개요

8051 계열 마이크로컨트롤러의 핵심 기능 중 하나인 타이머/카운터 모듈은 정확한 시간 지연, 외부 펄스 폭 측정, PWM 파형 생성, 직렬 통신 등 다양한 응용 분야에 필수적인 요소입니다. 이 가이드는 AT89C52RC 칩의 세 가지 타이머/카운터(T0, T1, T2)에 대한 하드웨어 원리부터 구성 및 활용까지 심도 있게 다룹니다.

타이머/카운터의 기본 개념

타이머/카운터는 마이크로컨트롤러 내부에 독립적으로 내장된 하드웨어 모듈입니다. 본질적으로 프로그래밍 가능한 카운터이며, 입력되는 펄스 신호를 누적하여 계산합니다. 이는 CPU와 병렬로 동작하여 CPU의 연산 시간을 소모하지 않습니다.

  • 타이머 모드: 카운트 펄스 소스가 마이크로컨트롤러 내부의 시스템 클럭(12분주된 기계 사이클)에서 오는 경우, 이 모듈은 타이머로 작동합니다. 알려진 주파수의 클럭 펄스를 계산함으로써 정확한 시간 간격을 생성할 수 있습니다.
  • 카운터 모드: 카운트 펄스 소스가 외부 핀(예: T0/P3.4)의 전압 변화에서 오는 경우, 이 모듈은 외부 이벤트 카운터로 작동하여 외부 이벤트 발생 횟수를 세는 데 사용됩니다.

AT89C52RC의 타이머 자원

AT89C52RC 마이크로컨트롤러는 세 개의 독립적인 16비트 타이머/카운터, 즉 타이머 0 (T0), 타이머 1 (T1), 타이머 2 (T2)를 내장하고 있습니다.

  • T0 및 T1: 범용 타이머/카운터로, 네 가지 작동 모드(모드 0~3)를 가집니다. 이들은 TMOD 레지스터를 통해 설정됩니다. 작동 시작/정지, 오버플로우 플래그 등은 TCON 레지스터에서 관리됩니다. 현재 카운트 값은 각각 TH0/TL0TH1/TL1 특수 기능 레지스터(SFR)에 저장됩니다.
  • T2: 더 강력한 기능을 제공하며, 캡처 및 자동 재로드 모드를 지원합니다. 관련 설정 레지스터는 T2CONT2MOD입니다.

이러한 타이머 모듈은 일부 포트 핀과 멀티플렉싱됩니다:

  • T0의 외부 카운트 입력 핀: P3.4
  • T1의 외부 카운트 입력 핀: P3.5
  • T2의 외부 카운트 입력 핀: P1.0 (T2), 캡처 트리거 핀: P1.1 (T2EX)

발진 주파수와 타이머 기본

타이머의 카운트 펄스 주파수는 외부 발진기 주파수(f_osc)에 의해 결정됩니다. 하나의 기계 사이클(Machine Cycle)은 12개의 클럭 사이클과 같습니다.

  • 만약 f_osc = 12MHz라면, 클럭 사이클 = 1/12µs이며, 하나의 기계 사이클 = 1µs입니다.
  • 만약 f_osc = 11.0592MHz라면, 하나의 기계 사이클 ≈ 1.085µs입니다.

이는 12MHz 발진기 환경에서 타이머가 1µs마다 하나씩 카운트하고, 11.0592MHz 환경에서는 약 1.085µs마다 하나씩 카운트한다는 것을 의미합니다. 이는 모든 타이머 계산의 기본이 되며, 타이밍의 정확성에 직접적인 영향을 미칩니다. 본 가이드의 예제들은 이 두 가지 일반적인 발진기 주파수를 모두 고려합니다.

타이머의 동작 모드 상세 분석

발진 주파수와 기계 사이클의 관계를 이해했다면, 이제 정확한 시간 측정의 기초를 다진 것입니다. 다음으로, TMOD 레지스터를 구성하여 T0와 T1에 다양한 "작업 모드"를 부여하는 방법을 심층적으로 살펴보겠습니다. 이 장에서는 네 가지 동작 모드를 자세히 분석합니다.

2.1 TMOD 레지스터 심층 분석

TMOD(Timer Mode Register, 주소 0x89)는 타이머 모드 제어 레지스터로, T0 및 T1의 동작 모드를 독립적으로 설정하는 데 사용됩니다. 8비트 레지스터이며, 상위 4비트는 T1을, 하위 4비트는 T0를 제어하며 비트 주소 지정은 불가능합니다.

  • GATE: 게이트 제어 비트. GATE=0일 때 타이머는 TRx에 의해서만 시작됩니다. GATE=1일 때는 TRxINTx 핀이 동시에 하이여야 타이머가 시작됩니다.
  • C/T: 기능 선택 비트. C/T=0일 때 타이머 모드(기계 사이클 카운트). C/T=1일 때 카운터 모드(T0/P3.4 또는 T1/P3.5 핀의 펄스 카운트).
  • M1, M0: 동작 모드 선택 비트로, 네 가지 모드를 함께 결정합니다.

설정 예시: T0를 모드 1(16비트 타이머)로, T1을 모드 2(8비트 자동 재로드 타이머)로 설정합니다.

#include <reg52.h>

void configureTimers() {
    // T0: 모드 1 (16비트), 타이머 모드 (C/T=0), GATE=0
    // T1: 모드 2 (8비트 자동 재로드), 타이머 모드 (C/T=0), GATE=0
    // 모드 비트: T0(M1M0=01), T1(M1M0=10)
    TMOD = 0x21; // 이진수 0010 0001
}

2.2 모드 0: 13비트 타이머/카운터

모드 0은 초기 8048 칩과 호환되는 13비트 모드입니다. TLx의 상위 3비트는 사용되지 않고, THx와 함께 13비트 카운터를 구성합니다. 최대 카운트 값은 2^13 = 8192입니다. 비트 수가 불규칙하고 최대 타이밍이 짧아 현대 응용에서는 거의 사용되지 않습니다.

2.3 모드 1: 16비트 타이머/카운터

가장 일반적으로 사용되고 유연한 모드입니다. THxTLx가 16비트 카운터를 구성합니다. 카운터가 초기값부터 0xFFFF(65535)까지 카운트한 후, 다음 카운트 펄스가 들어오면 오버플로우가 발생하고 오버플로우 플래그 TFx가 설정되며 카운터는 0x0000으로 리셋됩니다.

// 12MHz 발진기, 기계 사이클 1µs, 50ms 타이밍 가정
// 초기값 = 65536 - 50000 = 15536 (0x3CB0)
void initTimer0Mode1() {
    TMOD &= 0xF0; // T0 제어 비트 초기화
    TMOD |= 0x01; // T0를 모드 1로 설정
    TH0 = 0x3C;   // 초기값 상위 8비트 로드
    TL0 = 0xB0;   // 초기값 하위 8비트 로드
    TR0 = 1;      // 타이머 0 시작
}

2.4 모드 2: 8비트 자동 재로드 모드

이 모드에서는 TLx가 8비트 카운터로 작동하고, THx는 재로드 초기값을 저장하는 데 사용됩니다. TLx가 오버플로우(0xFF에서 0x00으로)될 때, TFx가 설정될 뿐만 아니라, THx의 값이 TLx자동으로 재로드되어 새로운 카운트를 시작합니다. 이는 인터럽트 서비스 루틴에서 수동으로 초기값을 재로드하는 단계를 생략하게 해주므로, 정확한 주기적 인터럽트 생성이나 직렬 통신 보레이트 생성기로 매우 적합합니다.

// 11.0592MHz 발진기, 500µs 타이밍 가정 (직렬 통신 보레이트 생성기용)
// 초기값 계산은 필요한 보레이트에 따라 달라짐, 여기서는 예시
void initTimer1Mode2() {
    TMOD &= 0x0F; // T1 제어 비트 초기화
    TMOD |= 0x20; // T1을 모드 2로 설정
    TH1 = 0xFE;   // 자동 재로드 초기값
    TL1 = 0xFE;   // 첫 번째 초기값 로드
    TR1 = 1;      // 타이머 1 시작
}

2.5 모드 3: T0의 분할 모드

모드 3은 T0에만 적용됩니다. 이 모드에서 T0는 두 개의 독립적인 8비트 타이머/카운터로 분할됩니다. TL0은 T0의 원래 제어 비트와 핀을 사용하고, TH0은 T1의 시작 제어 비트 TR1 및 인터럽트 플래그 TF1을 빌려 사용합니다.

  • TL0: TR0로 제어되며, 오버플로우 시 TF0이 설정되는 독립적인 8비트 타이머/카운터로 작동할 수 있습니다.
  • TH0: TR1로 제어되며, 오버플로우 시 TF1이 설정되는 타이머(기계 사이클 사용)로만 작동할 수 있습니다.

T0가 모드 3으로 작동할 때, T1 자체는 모드 0, 1 또는 2로 계속 작동할 수 있지만, 인터럽트를 생성할 수 없습니다 (TF1TH0에 의해 사용됨). 또한 게이트(GATE) 기능도 사용할 수 없습니다. T1은 일반적으로 모드 2로 설정되어 직렬 통신 보레이트 생성기로 사용되는데, 이는 T1의 동작 및 오버플로우가 어떤 인터럽트 플래그에도 영향을 미치지 않기 때문입니다.

// T0를 모드 3 (분할 모드)으로 설정
TMOD |= 0x03; // M1M0 = 11
// 이 때, TL0은 독립적으로 작동하고, TH0은 T1의 자원을 사용합니다.

2.6 동작 모드 요약 비교

아래 표는 네 가지 모드의 주요 특징을 명확하게 요약합니다:

모드 카운터 비트 수 최대 카운트 값 재로드 방식 주요 응용 시나리오
모드 0 13비트 8192 수동 (ISR에서 재로드 필요) 이전 버전 호환용, 현대에는 거의 사용 안 됨
모드 1 16비트 65536 수동 (ISR에서 재로드 필요) 장시간 타이밍, 범용 카운팅
모드 2 8비트 256 자동 (오버플로우 시 자동 재로드) 정확한 주기적 인터럽트, 직렬 통신 보레이트 생성기
모드 3 T0가 2개의 8비트로 분할 각 256 수동 (TL0 및 TH0 각각) 추가 8비트 타이밍 자원이 필요하고, T1이 보레이트 생성기로만 사용될 때

이러한 모드의 구조와 특성을 이해하는 것은 정확한 타이밍 및 이벤트 카운팅의 핵심입니다. 다음 섹션에서는 가장 일반적으로 사용되는 모드 1을 통해 초기값을 계산하고 LED를 정확하게 깜빡이는 프로그램을 작성하는 실습 예제를 살펴보겠습니다.

모드 1 (16비트 타이머) 프로그래밍 및 활용

이전 장에서 타이머 모드의 원리와 구조를 익혔습니다. 이 장에서는 이론을 실전에 적용해 보겠습니다. 모드 1(16비트 타이머)은 가장 일반적으로 사용되는 동작 모드로, 요구 사항에 따라 초기값을 계산하고 오버플로우 플래그를 폴링하는 방식으로 정확한 타이밍 제어를 구현하는 방법을 배웁니다.

3.1 타이머 초기값 계산 공식

타이머는 기본적으로 덧셈 카운터입니다. T 시간 동안 타이밍하려면, 기계 사이클 단위의 타이밍이 T에 도달했을 때 오버플로우가 발생해야 합니다. 계산 공식은 다음과 같습니다:

초기값 = 2^N - (타이밍 시간 T * 발진 주파수 Fosc) / 12

여기서 N은 카운터의 비트 수(모드 1에서는 16), 단위는 통일되어야 합니다. 타이밍 시간 T의 단위는 초(s), 발진 주파수 Fosc의 단위는 헤르츠(Hz)입니다.

3.2 다른 발진 주파수에서의 초기값 계산 예시

12MHz (Fosc=12,000,000Hz)와 11.0592MHz (Fosc=11,059,200Hz) 두 가지 일반적인 발진기를 사용하여 1ms 및 50ms 타이밍의 초기값을 계산해 보겠습니다.

예시 1: 1ms 타이밍 (T=0.001s)

  • 12MHz 발진기: 초기값 = 65536 - (0.001 * 12,000,000) / 12 = 65536 - 1000 = 64536 (0xFC18) 로드: TH0 = 0xFC; TL0 = 0x18;
  • 11.0592MHz 발진기: 초기값 = 65536 - (0.001 * 11,059,200) / 12 ≈ 65536 - 921.6 ≈ 64614 (0xFC66) 로드: TH0 = 0xFC; TL0 = 0x66;

예시 2: 50ms 타이밍 (T=0.05s)

  • 12MHz 발진기: 초기값 = 65536 - (0.05 * 12,000,000) / 12 = 65536 - 50000 = 15536 (0x3CB0) 로드: TH0 = 0x3C; TL0 = 0xB0;
  • 11.0592MHz 발진기: 초기값 = 65536 - (0.05 * 11,059,200) / 12 = 65536 - 46080 = 19456 (0x4C00) 로드: TH0 = 0x4C; TL0 = 0x00;

참고: 계산 결과가 정수가 아닌 경우 반올림해야 합니다. 타이밍 정확도는 이에 영향을 받습니다.

3.3 초기화 코드 분석

모드 1과 12MHz 발진기(50ms 타이밍)를 기반으로 한 초기화 함수는 다음과 같습니다:

#include <reg52.h>

// 타이머 0을 모드 1 (16비트 타이머)으로 초기화
void setupTimer0Mode1(void) {
    TMOD = 0x01;  // T0를 모드 1로 설정 (T1 설정은 필요에 따라 변경 가능)
    // 12MHz 발진기에서 50ms 타이밍을 위한 초기값 계산 및 로드
    TH0 = 0x3C;   // 상위 8비트 초기값
    TL0 = 0xB0;   // 하위 8비트 초기값
    TF0 = 0;      // 오버플로우 플래그 초기화
    TR0 = 1;      // 타이머 0 시작
}

코드 설명:

  • TMOD = 0x01;: TMOD를 구성합니다. 하위 4비트는 T0를 제어하며, 0x01은 T0가 모드 1(M1M0=01), 타이머 모드(C/T=0), 외부 INT0 핀에 의해 제어되지 않음(GATE=0)을 의미합니다.
  • TH0/TL0: 계산된 초기값을 로드합니다. 타이머는 이 초기값부터 카운트하여 65536번 카운트 후 오버플로우됩니다.
  • TF0 = 0;: 시작하기 전에 오버플로우 플래그가 클리어 상태인지 확인합니다.
  • TR0 = 1;: 실행 제어 비트를 설정하여 타이머를 시작합니다.

3.4 폴링 방식 타이밍 구현

폴링 방식은 가장 간단한 타이밍 방법입니다: 메인 루프에서 오버플로우 플래그 TF0를 계속 확인합니다. TF0가 1이면 타이밍 시간이 만료되었음을 의미하며, 해당 처리를 수행한 후 수동으로 플래그를 초기화합니다.

3.5 응용 예시: 폴링 방식으로 LED 깜빡이기 (500ms)

완전한 예시: 타이머 0 모드 1 (12MHz 발진기, 단일 타이밍 50ms)을 사용하여 소프트웨어로 10번 카운트하여 500ms LED 깜빡임을 구현합니다.

#include <reg52.h>

sbit led_status = P1^0; // LED는 P1.0 핀에 연결

// 타이머 0 모드 1 초기화 함수
void setupTimer0Mode1(void) {
    TMOD = 0x01; // T0를 모드 1로 설정
    TH0 = 0x3C;  // 50ms 초기값 (12MHz)
    TL0 = 0xB0;
    TF0 = 0;
    TR0 = 1;     // 타이머 시작
}

void main(void) {
    unsigned char timer_ticks = 0; // 소프트웨어 카운터

    setupTimer0Mode1(); // 타이머 초기화

    while (1) {
        // 오버플로우 플래그 확인
        if (TF0 == 1) {
            TF0 = 0;         // 수동으로 플래그 클리어!
            TH0 = 0x3C;      // 다음 타이밍을 위해 초기값 재로드
            TL0 = 0xB0;
            timer_ticks++;
            if (timer_ticks >= 10) { // 10 * 50ms = 500ms
                timer_ticks = 0;
                led_status = ~led_status; // LED 상태 토글
            }
        }
        // 대기 시간 동안 CPU는 다른 작업을 수행할 수 있습니다.
    }
}

프로그램 로직:

  • 타이머를 초기화하고 50ms 타이밍을 시작합니다.
  • 메인 루프에서 TF0가 설정(50ms 만료)되면 즉시 플래그를 클리어하고, 초기값을 재로드하며, 카운터를 증가시킵니다.
  • 카운터가 10번(500ms) 채워지면 LED를 토글하여 정확한 깜빡임을 구현합니다.
  • 폴링 대기 시간 동안 CPU는 메인 루프의 다른 코드를 실행할 수 있지만, 작업이 복잡하면 폴링 시기를 놓칠 수 있다는 것이 폴링 방식의 한계입니다. 더 나은 방법은 다음 장에서 소개할 인터럽트 방식입니다.

타이머 인터럽트 시스템 및 인터럽트 서비스 루틴

이전 장에서는 폴링 방식(TF0 플래그를 계속 확인)을 사용하여 타이밍을 구현하는 방법을 배웠지만, 이 방법은 CPU 자원을 지속적으로 점유하여 효율성이 낮습니다. 이 장에서는 더 강력하고 효율적인 인터럽트 방식을 소개합니다. 타이머가 오버플로우되면 자동으로 인터럽트가 트리거되고, CPU는 현재 실행 중인 작업을 일시 중단하고 미리 작성된 인터럽트 서비스 루틴(ISR)을 실행한 후 원래 작업으로 돌아갑니다. 이 방법은 CPU 활용도와 시스템의 실시간 응답 능력을 크게 향상시킵니다.

4.1 8051 인터럽트 시스템 개요

인터럽트는 마이크로컨트롤러가 긴급하거나 비동기적인 이벤트를 처리하는 메커니즘입니다. 특정 인터럽트 소스(예: 타이머 오버플로우)가 요청을 생성하고 인터럽트 조건이 충족되면, CPU는 메인 프로그램을 일시 중단하고 해당 인터럽트 소스에 해당하는 고정 주소(인터럽트 벡터 주소)로 점프하여 인터럽트 서비스 함수를 실행합니다. AT89C52RC에서 타이머/카운터와 관련된 주요 인터럽트 소스는 다음과 같습니다:

  • 타이머 0 인터럽트: T0 오버플로우로 발생, 인터럽트 번호 1, 인터럽트 벡터 주소 000BH.
  • 타이머 1 인터럽트: T1 오버플로우로 발생, 인터럽트 번호 3, 인터럽트 벡터 주소 001BH.
  • 타이머 2 인터럽트: T2 오버플로우 또는 T2EX 핀의 음의 엣지 트리거로 발생, 인터럽트 번호 5, 인터럽트 벡터 주소 002BH.

4.2 인터럽트 제어 레지스터 (IE, IP)

인터럽트를 사용하려면 관련 제어 레지스터를 올바르게 구성해야 합니다.

  • 인터럽트 활성화 레지스터 IE (A8H): 인터럽트를 허용하거나 금지하는 데 사용됩니다.
    • EA (IE.7): 전체 인터럽트 허용 비트. 1로 설정되어야 CPU가 어떤 인터럽트에도 응답할 수 있습니다.
    • ET0 (IE.1): 타이머 0 인터럽트 허용 비트. 1로 설정하면 T0 인터럽트를 허용합니다.
    • ET1 (IE.3): 타이머 1 인터럽트 허용 비트. 1로 설정하면 T1 인터럽트를 허용합니다.
    • ET2 (IE.5): 타이머 2 인터럽트 허용 비트. 1로 설정하면 T2 인터럽트를 허용합니다.
  • 인터럽트 우선순위 레지스터 IP (B8H): 인터럽트 소스의 우선순위를 설정하는 데 사용됩니다. 1로 설정하면 높은 우선순위입니다.

C51에서는 일반적으로 IE 레지스터에 비트 연산을 직접 수행하여 인터럽트를 활성화합니다. 예를 들어, 전체 인터럽트와 타이머 0 인터럽트를 활성화하려면: EA = 1; ET0 = 1;

4.3 인터럽트 서비스 함수 (ISR) 작성

Keil C51 또는 SDCC에서는 특정 키워드를 사용하여 인터럽트 서비스 함수를 정의합니다.

  • Keil C51 구문: void 함수명(void) interrupt 인터럽트_번호
    // 타이머 0 인터럽트 서비스 함수 예시 (Keil C51)
    void timer0ServiceRoutine(void) interrupt 1 {
        // 인터럽트 서비스 코드
    }
    
  • SDCC 구문: void 함수명(void) __interrupt(인터럽트_번호)
    // 타이머 0 인터럽트 서비스 함수 예시 (SDCC)
    void timer0ServiceRoutine(void) __interrupt(1) {
        // 인터럽트 서비스 코드
    }
    

매우 중요한 점: 대부분의 8051 아키텍처에서 타이머 오버플로우 인터럽트 플래그(예: TF0)는 인터럽트 서비스 루틴 진입 시 자동으로 초기화되지 않습니다. ISR 내부에서 소프트웨어로 수동으로 초기화해야 합니다 (TF0 = 0;). 그렇지 않으면 인터럽트가 계속 반복되어 프로그램이 멈출 수 있습니다.

4.4 인터럽트 방식 vs 폴링 방식

특성 폴링 방식 인터럽트 방식
CPU 점유율 지속적 점유 (바쁜 대기) 필요할 때만 점유 (이벤트 구동)
실시간성 낮음, 폴링 주파수에 의존 높음, 즉각적인 응답
코드 구조 간단하고 직관적, 이해하기 쉬움 논리 분리 (메인 루프와 ISR), 더 명확함
적용 시나리오 간단한 작업, 학습 이해 복잡한 시스템, 다중 작업, 높은 실시간성 요구

4.5 실전: 인터럽트 방식으로 LED 깜빡이기

이제 인터럽트 방식을 사용하여 3장의 LED 깜빡임 실험을 다시 작성해 보겠습니다. 500ms마다 P1.0에 연결된 LED를 한 번씩 토글합니다.

#include <reg52.h>

// 하드웨어 핀 정의
sbit status_led = P1^0;

// 전역 변수: 인터럽트 카운터
unsigned int int_counter = 0;

// 타이머 0 초기화 함수 (모드 1, 50ms 타이밍)
void initTimer0Interrupt(void) {
    TMOD = 0x01; // T0를 모드 1 (16비트 타이머)로 설정
    // 50ms 타이밍 초기값 계산 (12MHz 발진기 가정)
    TH0 = 0x3C; // 초기값: 0x3CB0은 12MHz에서 약 50ms에 해당
    TL0 = 0xB0;
    ET0 = 1;    // 타이머 0 인터럽트 활성화
    EA = 1;     // 전체 인터럽트 활성화
    TR0 = 1;    // 타이머 0 시작
}

// 타이머 0 인터럽트 서비스 함수 (Keil C51 버전)
void timer0_isr(void) interrupt 1 {
    TH0 = 0x3C; // 초기값 재로드 (모드 1은 자동 재로드되지 않음)
    TL0 = 0xB0;
    int_counter++; // 카운터 증가

    // 10번의 인터럽트마다 (10 * 50ms = 500ms) LED 토글
    if(int_counter >= 10) {
        int_counter = 0;    // 카운터 초기화
        status_led = ~status_led; // LED 상태 토글
    }
    // TF0 플래그는 일반적으로 하드웨어에 의해 자동으로 처리되지만, 안전을 위해 명시적으로 TF0=0;을 추가할 수도 있습니다.
    // 이 예제에서는 초기값 재로드 및 소프트웨어 카운트 로직이 닫혀있어 TF0는 다음 오버플로우 전에 하드웨어에 의해 유지되므로 명시적 초기화는 불필요합니다.
}

void main(void) {
    status_led = 0; // 초기 LED 상태 OFF
    initTimer0Interrupt(); // 타이머 0 초기화

    while(1) {
        // 메인 루프는 다른 작업을 수행할 수 있으며, LED 깜빡임은 백그라운드에서 인터럽트 서비스 루틴에 의해 처리됩니다.
        // 예: 버튼 스캔, 디스플레이 갱신 등
    }
}

폴링 방식 코드와의 주요 차이점:

  • 구조 분리: 초기화 함수에서 인터럽트를 활성화했습니다 (ET0=1; EA=1;). main 함수는 매우 간결해졌으며 초기화와 빈 루프 실행만 담당합니다. 모든 타이밍 처리 로직은 timer0_isr 인터럽트 함수로 이동했습니다.
  • 실행 메커니즘: LED의 토글은 더 이상 메인 루프의 폴링 코드에 의해 실행되지 않고, 타이머 오버플로우 순간 하드웨어에 의해 인터럽트 서비스 함수가 자동으로 "삽입"되어 완료됩니다.
  • 초기값 재로드: 모드 1을 사용했으므로 인터럽트 서비스 함수에서 수동으로 초기값을 재로드해야 합니다 (TH0=0x3C; TL0=0xB0;).
  • 인터럽트 서비스 루틴 주의사항:
    • 가능한 짧게: ISR 실행 시간은 다른 인터럽트를 방해하거나 메인 프로그램 흐름에 영향을 주지 않도록 가능한 짧아야 합니다.
    • 복잡한 작업 피하기: ISR 내에서 시간이 많이 걸리는 계산, 부동 소수점 연산, 또는 블로킹될 수 있는 함수(예: 일부 직렬 송신 함수) 호출을 피해야 합니다.
    • 컨텍스트 보호: 인터럽트에서 메인 프로그램과 공유하는 변수를 수정하는 경우, 컴파일러 최적화 오류를 방지하기 위해 해당 변수를 volatile 키워드로 선언해야 합니다.
    • 인터럽트 중첩 신중하게 사용: 명확하게 설계되지 않은 한, ISR 내에서 다른 인터럽트를 활성화하는 것은 권장되지 않습니다.

이 장을 통해 인터럽트 방식을 사용하여 타이밍을 구현하는 기본 방법을 익혔습니다. 이는 실시간 임베디드 시스템을 구축하는 데 중요한 토대입니다. 다음 장에서는 타이머 1의 특별한 응용, 즉 직렬 통신 보레이트 생성기로서의 역할을 탐구합니다.

T1 타이머: 모드 2 활용 및 직렬 통신 보레이트 생성기

이전 장에서 타이머 인터럽트를 사용하여 정확한 제어를 구현하는 방법을 익혔습니다. 다음으로, 타이머 1(T1)의 매우 중요한 응용 시나리오인 직렬 통신 보레이트 생성기로서의 역할을 탐구하겠습니다. 이 장을 통해 T1의 모드 2(8비트 자동 재로드 모드)가 표준 보레이트를 생성하는 데 왜 이상적인 선택인지 이해하고, 직렬 통신 초기화 설정을 독립적으로 완료할 수 있을 것입니다.

직렬 통신 기본 복습

직렬 통신(UART)은 마이크로컨트롤러가 PC 또는 다른 장치와 데이터를 교환하는 주요 방법입니다. 그 핵심은 두 개의 레지스터에 있습니다: SCON(직렬 제어 레지스터)은 작동 모드와 상태 플래그를 설정하는 데 사용되고, SBUF(직렬 데이터 버퍼 레지스터)는 데이터 송수신의 포트입니다. 통신을 구현하려면 양측이 동일한 보레이트(초당 전송 비트 수)를 사용해야 하며, T1 타이머는 여기에서 정확한 클럭 소스를 제공하는 역할을 합니다.

T1 모드 2를 보레이트 생성기로서 사용

T1의 모드 2는 8비트 자동 재로드 타이머입니다. 카운트 오버플로우 시, TH1의 초기값이 TL1로 자동으로 로드되어 소프트웨어 개입 없이 연속적이고 정확한 주기적 오버플로우를 생성합니다. 이 안정적인 오버플로우 속도는 분주된 후 보레이트 생성에 필요한 기준 클럭이 됩니다.

T1을 보레이트 생성기로 구성하는 핵심 단계는 TMOD 레지스터에서 T1을 제어하는 부분을 모드 2로 설정하고, 타이머 모드(C/T=0)로 작동하도록 하는 것입니다. 우리가 사용하는 표준 설정 값은 TMOD = 0x21이며, 이 중 상위 4비트 0010은 T1을 모드 2 타이머로 구성합니다.

보레이트 계산 및 초기값

보레이트 계산 공식은 다음과 같습니다:

보레이트 = (2^SMOD / 32) * (T1 오버플로우율)

일반적으로 SMOD=0 (PCON 레지스터의 최상위 비트)이므로 공식은 다음과 같이 단순화됩니다:

보레이트 = T1 오버플로우율 / 32

T1 오버플로우율 = 발진 주파수 / (12 * (256 - TH1 초기값)). 따라서,

TH1 초기값 = 256 - (발진 주파수) / (384 * 보레이트)

여기서 11.0592MHz 발진기의 중요성을 강조해야 합니다. 이 주파수를 사용하여 일반적인 보레이트를 계산하면 TH1 초기값이 정확한 정수로 얻어지므로 통신 오류가 없습니다. 다음은 일반적인 설정 표입니다:

보레이트 (bps) 발진 주파수 (MHz) TH1 초기값 (SMOD=0)
9600 11.0592 0xFD (253)
4800 11.0592 0xFA (250)
19200 11.0592 0xFE (254)
9600 12 0xF3 (243, 오차 있음)

참고: 12MHz 발진기를 사용하여 9600 보레이트를 계산할 때, 이론값은 253이지만 실제로는 0xF3(243)을 자주 사용합니다. 이는 오차율이 허용 범위(-3.6%) 내에 있기 때문입니다.

완전한 직렬 통신 초기화 코드

아래는 11.0592MHz 발진기를 기반으로 9600 보레이트를 구성하는 완전한 초기화 함수 및 송신 예시입니다.

#include <reg52.h>

sbit indicator_led = P1^0; // 작업 표시 LED

// 직렬 통신 및 T1 초기화 함수
void initSerialAndTimer1(void) {
    TMOD = 0x20;        // T1 모드 2 (자동 재로드), T0는 기본 설정 (필요시 TMOD=0x21)
    TH1 = 0xFD;         // 11.0592MHz 발진기, 9600 보레이트 초기값 (256 - 11059200 / (384 * 9600) = 253 = 0xFD)
    TL1 = 0xFD;         // 모드 2에서 TH1 값은 자동으로 TL1에 로드됨, 명시적으로 할당
    SCON = 0x50;        // 직렬 통신 모드 1 (8비트 UART), 수신 허용 (REN=1)
    PCON &= 0x7F;       // SMOD=0, 보레이트 배율 없음
    TR1 = 1;            // 타이머 1 시작
}

// 직렬 통신으로 한 문자 전송
void sendSerialChar(char data_char) {
    SBUF = data_char;
    while (!TI);        // 전송 완료 대기 (TI 플래그는 하드웨어에 의해 1로 설정됨)
    TI = 0;             // 전송 완료 플래그는 수동으로 초기화해야 함
}

// 직렬 통신으로 문자열 전송
void sendSerialString(char *text_ptr) {
    while (*text_ptr) {
        sendSerialChar(*text_ptr++);
    }
}

void main(void) {
    initSerialAndTimer1(); // 직렬 통신 및 T1 초기화
    indicator_led = 0;     // LED 켜서 작동 표시

    while (1) {
        sendSerialString("Hello AT89C52RC!\r\n"); // 문자열 전송 및 줄 바꿈
        // 간단한 지연, 실제 프로젝트에서는 타이머 인터럽트를 사용하는 것이 좋습니다.
        { unsigned int loop_idx; for(loop_idx=0; loop_idx<30000; loop_idx++); }
    }
}

코드 설명:

  • TMOD = 0x20: T1을 모드 2로 설정합니다.
  • TH1 = 0xFD: 11.0592MHz 발진기에서 9600 보레이트를 생성하기 위한 초기값입니다.
  • SCON = 0x50: 직렬 통신을 모드 1로 설정하고 수신을 허용합니다.
  • main 함수는 "Hello AT89C52RC!" 문자열을 반복해서 전송하며, LED는 시스템이 작동 중임을 나타내기 위해 켜집니다.

T1을 모드 2로 구성함으로써 안정적이고 신뢰할 수 있는 보레이트 클럭 소스를 얻었습니다. 이는 신뢰할 수 있는 직렬 통신을 구현하는 첫 단계입니다. 실제 프로젝트에서는 보통 인터럽트를 사용하여 수신을 처리하여 CPU가 다른 작업을 동시에 수행할 수 있도록 합니다. 이 부분은 다음 장에서 다룰 것입니다. T1을 보레이트 생성기로 구성하는 방법을 마스터하는 것은 마이크로컨트롤러 통신 시스템을 구축하는 데 중요한 토대입니다.

정확한 소프트웨어 지연 구현

이전 장에서 우리는 타이머 1의 모드 2를 활용하여 직렬 통신에 안정적인 클럭을 제공하는 방법을 배웠습니다. 타이머의 역할은 여기에 그치지 않습니다. 이 장에서는 또 다른 고전적인 응용 분야인 고정밀 소프트웨어 지연을 구축하여 비효율적이고 부정확한 루프 지연 방식에서 완전히 벗어나는 방법을 탐구합니다.

6.1 루프 지연의 단점

전통적인 for 또는 while 루프 지연은 명확한 단점을 가집니다. 첫째, 지연 시간은 발진 주파수와 컴파일러의 최적화 수준에 크게 의존하여 이식성이 좋지 않습니다. 둘째, CPU를 완전히 점유하여 프로그램이 지연 시간 동안 다른 이벤트나 인터럽트에 응답할 수 없게 됩니다. 가장 중요한 것은, 더 높은 우선순위의 인터럽트가 발생하여 실행되면 지연 시간이 방해받아 예측할 수 없는 오류가 발생할 수 있다는 점입니다.

6.2 타이머 인터럽트 지연 방식 설계

위에서 언급한 문제들을 해결하기 위해, 우리는 타이머 인터럽트 기반의 정확한 지연 시스템을 설계할 수 있습니다. 핵심 아이디어는 다음과 같습니다:

  • 타이머(예: T0)를 모드 1로 구성하여 고정되고 정확한 짧은 간격의 인터럽트(예: 1ms)를 생성합니다.
  • 타이머 인터럽트 서비스 루틴(ISR)에서 전역으로 감소하는 카운터를 관리합니다.
  • 주 프로그램은 필요한 지연 시간(밀리초 단위)을 설정한 후, 카운터가 0으로 감소되었다는 플래그가 설정될 때까지 기다립니다.
  • 이 방법은 지연을 "병렬화"하여, CPU가 대기 시간 동안 다른 작업을 처리할 수 있게 하며, 지연 정확성은 하드웨어 타이머에 의해 보장됩니다.

6.3 핵심 코드 구현

다음은 완전한 구현 코드입니다. T0를 11.0592MHz 발진기에서 1ms 인터럽트를 생성하도록 사용하고, delay_ms 함수를 제공합니다.

#include <reg52.h>

sbit system_led = P1^0; // LED는 P1.0에 연결

// 전역 변수, 인터럽트에서 값이 변경될 수 있음을 컴파일러에 알리기 위해 volatile 키워드 사용
volatile unsigned int ms_delay_counter = 0; // 지연 카운터
volatile bit delay_complete_flag = 0; // 지연 완료 플래그

/* 타이머 0 초기화 함수, 모드 1, 1ms 인터럽트 구성 (11.0592MHz) */
void initializeTimerForDelay(void)
{
    TMOD = (TMOD & 0xF0) | 0x01; // T0를 모드 1 (16비트 타이머)으로 설정, T1에 영향 없음
    // 1ms 초기값 계산: 65536 - (11059200/12/1000) = 65536 - 921 = 64615 (0xFC67)
    TH0 = 0xFC;
    TL0 = 0x67;
    EA = 1;  // 전체 인터럽트 활성화
    ET0 = 1; // T0 인터럽트 활성화
    TR0 = 1; // T0 시작
}

/* 타이머 0 인터럽트 서비스 루틴, 매 1ms마다 실행 */
void timer0_isr_delay(void) interrupt 1
{
    // 초기값 재로드, 다음 1ms 정확성 보장
    TH0 = 0xFC;
    TL0 = 0x67;
    
    if (ms_delay_counter > 0) {
        ms_delay_counter--; // 지연 카운터 감소
        if (ms_delay_counter == 0) {
            delay_complete_flag = 1; // 카운터가 0이 되면 지연 완료 플래그 설정
        }
    }
}

/* 정확한 밀리초 지연 함수 */
void precise_delay_ms(unsigned int milliseconds)
{
    ms_delay_counter = milliseconds;  // 지연 시간 설정
    delay_complete_flag = 0;    // 플래그 초기화
    while (delay_complete_flag == 0); // 플래그 설정될 때까지 대기
}

/* 메인 함수 */
void main(void)
{
    initializeTimerForDelay(); // 타이머 초기화
    
    while (1)
    {
        system_led = ~system_led; // LED 상태 토글
        precise_delay_ms(500); // 500ms 정확하게 지연
    }
}

코드 분석:

  • volatile 키워드는 매우 중요합니다. 컴파일러가 ms_delay_counterdelay_complete_flag에 대해 최적화를 수행하는 것을 방지하여, 매번 메모리에서 최신 값을 읽도록 보장합니다.
  • initializeTimerForDelay 함수는 T0 모드를 구성하고, 1ms 인터럽트를 생성하는 초기값(11.0592MHz 발진기 기반)을 로드합니다.
  • timer0_isr_delay는 인터럽트의 핵심으로, 초기값 재로드 및 소프트웨어 카운터 유지를 담당합니다. 인터럽트가 트리거되면 하드웨어는 자동으로 TF0 플래그를 초기화합니다.
  • precise_delay_ms 함수는 지연 매개변수를 설정하고 바쁜 대기를 수행하며, 이 시간 동안 CPU는 다른 인터럽트에 응답할 수 있습니다. 메인 루프는 500ms 간격으로 LED를 정확하게 깜빡입니다.

6.4 정확성 검증 및 개선

오실로스코프나 로직 분석기를 사용하여 P1.0 핀을 측정하면, LED 토글 주기가 매우 높은 정확성(500ms ±1 기계 사이클 내)을 가짐을 확인할 수 있으며, 이는 루프 지연보다 훨씬 우수합니다. 이 방식의 정확성은 1ms 기본 인터럽트의 정확성에 직접적으로 의존하며, 이는 발진기 및 타이머 하드웨어에 의해 보장됩니다.

주의사항:

  • 시스템에 여러 개의 시간이 많이 소요되는 작업이나 더 높은 우선순위 인터럽트가 자주 발생하면, 기본 지연 정확성은 변하지 않더라도 precise_delay_ms 함수의 실제 반환 시점(while 루프 종료 시간)은 약간 지연될 수 있습니다.
  • 더 높은 실시간성이 요구되는 중요한 작업의 경우, 소프트웨어 카운터 방식 대신 타이머를 직접 제어하는 것을 고려해야 합니다.

이 섹션 실습을 통해 우리는 타이머의 하드웨어 정확성을 유연하고 프로그래밍 가능한 소프트웨어 지연 기능으로 전환하는 방법을 구현했습니다. 이는 복잡한 임베디드 시스템의 시간 관리 프레임워크를 구축하는 기초입니다. 이 방법을 마스터하면 시간 정확성이 요구되는 대부분의 응용 시나리오에 유연하게 대처할 수 있을 것입니다.

T2 타이머의 고급 기능 및 응용

이전 섹션에서는 타이머 인터럽트를 활용하여 정확한 소프트웨어 지연 시스템을 구축하는 방법을 익혔습니다. 하드웨어 타이밍 기능을 유연한 소프트웨어 리소스로 전환하는 이러한 아이디어는 임베디드 개발의 핵심입니다. 이제 AT89C52RC에서 T0/T1보다 훨씬 강력하지만 초보자들이 종종 간과하는 모듈인 타이머 2(Timer 2)에 주목해 보겠습니다. T2는 16비트 캡처 및 자동 재로드와 같은 고급 기능을 제공하며, 복잡한 타이밍 측정 및 파형 생성에 유용한 도구입니다.

7.1 T2 타이머 특성 개요

타이머 2는 두 개의 8비트 레지스터 TH2TL2로 구성된 16비트 타이머/카운터입니다. 가장 눈에 띄는 차이점은 두 개의 전용 핀을 가지고 있다는 것입니다: T2(P1.0)는 외부 카운트 펄스 입력 또는 클럭 출력으로 사용되고, T2EX(P1.1)는 캡처 트리거 또는 외부 제어에 사용됩니다. 이의 작동 모드는 특수 기능 레지스터 T2CONT2MOD에 의해 제어됩니다.

7.2 T2CON 및 T2MOD 레지스터 상세 분석

T2CON은 T2의 핵심 제어 레지스터입니다. 주요 비트는 다음과 같습니다:

  • CP/RL2: 캡처/재로드 선택 비트. 1로 설정하면 캡처 모드, 0으로 클리어하면 자동 재로드 모드입니다.
  • C/T2: 기능 선택 비트. 0으로 클리어하면 타이머(내부 기계 사이클 카운트), 1로 설정하면 카운터(T2 핀의 외부 펄스 카운트)입니다.
  • TR2: T2의 시작 제어 비트, 1로 설정하면 시작됩니다.
  • EXEN2: T2 외부 활성화 비트. 1로 설정하면 T2EX 핀의 하강 에지가 캡처 또는 재로드를 트리거합니다.

T2MOD는 T2의 자동 재로드 모드에서 카운트 방향을 제어하는 방향 제어 비트(DCEN)를 포함합니다.

7.3 자동 재로드 모드 응용

CP/RL2 = 0일 때, T2는 16비트 자동 재로드 모드로 작동합니다. 이때 RCAP2HRCAP2L 레지스터는 재로드 값을 저장하는 데 사용됩니다. 카운트 오버플로우 시, 재로드 값은 자동으로 RCAP2H/L에서 TH2/TL2로 로드되어 연속적인 타이밍 인터럽트를 생성하며, 소프트웨어 개입 없이 정확한 주기적 이벤트를 생성하는 데 매우 적합합니다.

예시: T2를 사용하여 1ms 주기 인터럽트 생성.

발진 주파수가 11.0592MHz라고 가정하면, 기계 사이클은 12/11.0592µs입니다.

#include <reg52.h>

// T2 인터럽트 서비스 함수
void Timer2_AutoReload_ISR(void) interrupt 5 {
    TF2 = 0; // T2 오버플로우 인터럽트 플래그 수동 초기화
    P1_0 = ~P1_0; // P1.0 핀 토글, 오실로스코프에서 1ms 구형파 관찰 가능
}

void Initialize_Timer2_AutoReload(void) {
    // T2를 타이머 모드, 자동 재로드 모드로 설정
    T2CON = 0x04; // T2 시작, CP/RL2=0
    // 1ms 초기값 계산: 65536 - (11059200/12/1000) = 65536 - 921 = 64615 = 0xFC67
    RCAP2H = 0xFC;
    RCAP2L = 0x67;
    TH2 = 0xFC;
    TL2 = 0x67;
    ET2 = 1; // T2 인터럽트 허용
    EA = 1;  // 전체 인터럽트 활성화
}

void main(void) {
    Initialize_Timer2_AutoReload();
    while(1); // 메인 루프는 인터럽트를 기다림
}

7.4 캡처 모드 응용

CP/RL2 = 1이고 EXEN2 = 1일 때, T2는 캡처 모드로 작동합니다. 이때 T2EX 핀에 하강 에지가 발생하면 T2의 현재 카운트 값 TH2/TL2가 즉시 RCAP2H/RCAP2L에 래치되고, 동시에 EXF2 플래그가 설정됩니다. 이 기능은 외부 펄스의 폭을 측정하는 데 자주 사용됩니다.

예시: T2EX 핀에 연결된 양의 펄스 폭 측정.

원리: 펄스 상승 에지에서 T2 카운트를 시작하고, 하강 에지에서 캡처를 트리거하여 카운트 값을 읽습니다.

#include <reg52.h>

unsigned int pulse_capture_value = 0;
bit pulse_captured = 0;

// T2 인터럽트 서비스 함수 (캡처 인터럽트와 오버플로우 인터럽트가 인터럽트 번호 5를 공유)
void Timer2_Capture_ISR(void) interrupt 5 {
    if (EXF2) { // 캡처 인터럽트인지 확인
        EXF2 = 0; // 캡처 플래그 초기화
        pulse_capture_value = (RCAP2H << 8) | RCAP2L; // 캡처 값 읽기
        pulse_captured = 1;
    }
    TF2 = 0; // 오버플로우 플래그도 초기화하여 간섭 방지
}

void Initialize_Timer2_Capture(void) {
    T2MOD = 0; // T2MOD 초기화
    T2CON = 0x09; // 캡처 모드로 설정: CP/RL2=1, EXEN2=1, TR2=1
    // 타이머 모드, 기계 사이클 카운트
    TH2 = 0;
    TL2 = 0;
    ET2 = 1; // T2 인터럽트 허용
    EA = 1;  // 전체 인터럽트 활성화
}

void main(void) {
    Initialize_Timer2_Capture();
    // 메인 함수는 캡처 인터럽트 후에 pulse_captured 플래그를 통해 데이터를 처리할 수 있습니다.
    // 펄스 폭 = (pulse_capture_value * 12) / 발진 주파수 (초).
    while(1);
}

T2 타이머의 캡처 및 자동 재로드 기능을 마스터함으로써, 시간을 더 정확하게 제어할 수 있게 되며, 모터 제어, 신호 분석, 통신 프로토콜 타이밍 생성 등 고급 응용 분야를 위한 견고한 기반을 마련할 수 있습니다.

외부 이벤트 카운터 모드 실습

이전 섹션에서는 T2 타이머의 강력한 캡처 및 재로드 기능을 심층적으로 탐구했습니다. 이 섹션에서는 타이머/카운터 모듈의 또 다른 기본적인 핵심 기능인 이벤트 카운팅으로 돌아가겠습니다. 이는 제품 카운트, 회전 속도 측정, 버튼 통계 등과 같은 실제 응용 분야에서 매우 흔하게 사용됩니다.

타이머에서 카운터로: 클럭 소스 전환

타이머와 카운터의 핵심 차이점은 클럭 소스에 있습니다. TMOD 레지스터의 C/T (Counter/Timer) 비트를 구성함으로써 쉽게 전환할 수 있습니다. C/T=0일 때 입력 클럭은 시스템 발진기에서 오므로 타이머 모드입니다. C/T=1일 때 입력 클럭은 외부 핀 T0(P3.4) 또는 T1(P3.5)의 펄스에서 오므로 카운터 모드입니다. 외부 신호의 높은/낮은 레벨 지속 시간은 최소한 하나의 기계 사이클보다 길어야 안정적으로 감지될 수 있습니다.

응용 예시: 버튼 누름 카운터

클래식한 사례를 설계해 보겠습니다: T0의 카운터 모드를 사용하여 P3.4 핀에 연결된 버튼의 누름 횟수를 세고, 이 값을 P1 포트의 LED를 통해 이진 형식으로 표시합니다.

하드웨어 연결 요점

버튼의 한쪽 끝은 P3.4에, 다른 쪽 끝은 GND에 연결합니다. 마이크로컨트롤러 I/O 포트의 내부 풀업 저항 또는 외부 풀업 저항을 사용하여 버튼이 눌리지 않았을 때 P3.4가 높은 레벨인지 확인합니다. 버튼을 누르면 P3.4가 낮아지고, 하강 에지가 발생하여 카운터가 1 증가합니다.

완전한 코드 구현

다음 코드는 카운터 초기화부터 LED 표시까지의 모든 기능을 구현합니다.

#include <reg52.h>

// 버튼 핀 정의 (T0 카운터 입력 핀 사용)
sbit user_button = P3^4;

// 카운터 초기화 함수, T0를 카운터 모드 1 (16비트)로 구성
void Initialize_Counter0(void) {
    TMOD &= 0xF0;        // T0 제어 비트 초기화
    TMOD |= 0x05;        // T0를 카운터 모드, 모드 1 (C/T=1, M1M0=01)으로 설정
    TH0 = 0;             // 0부터 카운트 시작
    TL0 = 0;
    TR0 = 1;             // T0 카운터 시작
}

void main(void) {
    unsigned int current_button_count;
    
    Initialize_Counter0(); // T0를 외부 카운터로 초기화
    
    while(1) {
        // 16비트 카운트 값 읽기
        current_button_count = (TH0 << 8) | TL0;
        // 카운트 값의 하위 8비트를 P1 포트 LED로 출력하여 이진수로 표시
        P1 = (unsigned char)(current_button_count & 0xFF);
    }
}

코드 분석:

  • 초기화 함수 Initialize_Counter0:
    • TMOD = 0x05: T0를 카운터(C/T=1)로, 모드 1(16비트 카운터)로 작동하도록 구성합니다. 초기값 TH0 및 TL0는 모두 0으로 설정되어 0부터 카운트하여 65535번 카운트 후 오버플로우됩니다.
    • TR0 = 1: 타이머/카운터 T0를 시작합니다.
  • 메인 함수 로직:
    • 프로그램은 메인 루프에서 T0의 현재 카운트 값 current_button_count를 계속 읽습니다.
    • current_button_count의 하위 8비트(current_button_count & 0xFF)를 P1 포트로 출력하여 8개의 LED를 구동합니다. 카운트 값의 각 1비트는 해당 LED의 켜짐/꺼짐에 해당하여 버튼 누름 횟수를 이진 형식으로 직관적으로 표시합니다.

이 예제를 통해 타이머를 카운터 모드로 구성하는 방법을 익혔을 뿐만 아니라, 이론과 가장 기본적인 I/O 제어(LED 표시)를 결합하여 미래에 회전 속도계, 유량계 등과 같은 응용을 구현하기 위한 견고한 기반을 마련했습니다.

디버깅 기술 및 종합 연습

이전 실습을 통해 타이머/카운터의 핵심 응용 프로그램을 마스터했습니다. 이 섹션에서는 일반적인 디버깅 문제를 요약하고, 모든 지식을 통합할 수 있는 종합 프로젝트를 제공합니다.

일반적인 오류 및 문제 해결 체크리스트

타이머 개발 중 기능 이상이 발생하면 다음 체크리스트를 따라 문제를 해결하십시오:

  • 타이머 미시작: 제어 레지스터 TCONTR0, TR1 또는 T2CONTR2가 1로 설정되었는지 확인합니다.
  • 인터럽트 비활성화: 전체 인터럽트 EA 및 해당 타이머 인터럽트 허용 비트(예: ET0)가 1로 설정되었는지 확인합니다.
  • 발진 주파수 불일치: 프로그램의 발진 주파수(예: 11.0592MHz)가 실제 하드웨어와 일치해야 합니다. 그렇지 않으면 모든 타이밍 계산이 잘못됩니다.
  • 인터럽트 서비스 루틴에서 플래그 미초기화: Keil C51에서는 하드웨어가 TF0와 같은 플래그를 자동으로 초기화하는 경우가 많지만, 수동으로 초기화하는 것이 더 안전한 습관입니다. 초기화하지 않으면 인터럽트가 계속해서 트리거됩니다.
  • 초기값 계산 오류: 공식을 다시 확인하고 16진수를 사용하여 THxTLx 레지스터에 올바르게 값을 할당했는지 확인합니다.

종합 프로젝트: 디지털 시계 디자인

이 프로젝트는 두 개의 타이머를 함께 사용하여 시계 디스플레이를 구현하는 것을 목표로 합니다.

프로젝트 요구 사항: 4자리 공통 애노드 세그먼트 디스플레이에 "시-분", 예를 들어 "12-05"를 표시합니다.

설계 계획:

  • T0 (모드 1): 1ms 타이밍 인터럽트를 생성합니다. 인터럽트 서비스 루틴에서 1000번 카운트(즉, 1초)를 통해 시간 변수(시, 분, 초)를 업데이트합니다.
  • T1 (모드 1): 2ms 타이밍 인터럽트를 생성하여 세그먼트 디스플레이의 동적 스캔을 수행하고, 4개의 세그먼트를 순차적으로 갱신하여 깜빡임을 방지합니다.

핵심 코드 구조:

#include <reg52.h>

// 시간 변수 및 표시 버퍼 정의
unsigned char current_hour = 12, current_minute = 5, current_second = 0;
unsigned char display_buffer[4]; // 표시할 숫자를 저장

// 세그먼트 디스플레이의 자리 선택 및 세그먼트 코드 정의 (하드웨어에 따라 다름)
sbit DIGIT_EN_0 = P2^0; // 첫째 자리 활성화
sbit DIGIT_EN_1 = P2^1; // 둘째 자리 활성화
sbit DIGIT_EN_2 = P2^2; // 셋째 자리 활성화
sbit DIGIT_EN_3 = P2^3; // 넷째 자리 활성화
unsigned char seg_codes[] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90}; // 0-9의 공통 애노드 세그먼트 코드

// 초기화 함수
void initializeClockTimers(void) {
    TMOD = 0x11; // T0 및 T1 모두 모드 1로 작동
    // T0 초기값 계산: 1ms @ 11.0592MHz
    TH0 = 0xFC;  TL0 = 0x67;
    // T1 초기값 계산: 2ms @ 11.0592MHz
    TH1 = 0xF8;  TL1 = 0xCD;
    EA = 1;   // 전체 인터럽트 활성화
    ET0 = 1;  ET1 = 1; // T0 및 T1 인터럽트 활성화
    TR0 = 1;  TR1 = 1; // T0 및 T1 시작
}

// T0 인터럽트 서비스 프로그램 - 시간 업데이트
void Timer0_UpdateTime_ISR(void) interrupt 1 {
    static unsigned int ms_count_for_sec = 0;
    TH0 = 0xFC; TL0 = 0x67; // 초기값 재로드
    if (++ms_count_for_sec >= 1000) {   // 1초 경과
        ms_count_for_sec = 0;
        if (++current_second >= 60) {
            current_second = 0;
            if (++current_minute >= 60) {
                current_minute = 0;
                if (++current_hour >= 24) current_hour = 0;
            }
        }
        // 표시 버퍼 업데이트
        display_buffer[0] = current_hour / 10;
        display_buffer[1] = current_hour % 10;
        display_buffer[2] = current_minute / 10;
        display_buffer[3] = current_minute % 10;
    }
}

// 모든 자리 비활성화
void disableAllDigits(void) {
    DIGIT_EN_0 = 1; DIGIT_EN_1 = 1; DIGIT_EN_2 = 1; DIGIT_EN_3 = 1;
}

// T1 인터럽트 서비스 프로그램 - 세그먼트 디스플레이 스캔
void Timer1_ScanDisplay_ISR(void) interrupt 3 {
    static unsigned char current_digit_pos = 0;
    TH1 = 0xF8; TL1 = 0xCD; // 초기값 재로드

    disableAllDigits(); // 모든 자리 비활성화

    // 현재 자리에 숫자 표시
    P0 = seg_codes[display_buffer[current_digit_pos]]; // P0를 세그먼트 출력 포트로 가정
    if (current_digit_pos == 1) { // 시와 분 사이에 점 표시
        P0 &= ~(1 << 7); // 세그먼트 P7 (점) 활성화 (공통 애노드이므로 0)
    }

    // 현재 자리 활성화
    switch (current_digit_pos) {
        case 0: DIGIT_EN_0 = 0; break;
        case 1: DIGIT_EN_1 = 0; break;
        case 2: DIGIT_EN_2 = 0; break;
        case 3: DIGIT_EN_3 = 0; break;
    }

    current_digit_pos = (current_digit_pos + 1) % 4; // 다음 자리로 이동
}

void main(void) {
    initializeClockTimers();
    while(1) {
        // 메인 루프는 버튼 교정 등 다른 작업을 처리할 수 있습니다.
    }
}

태그: AT89C52RC 8051 마이크로컨트롤러 타이머 카운터

6월 1일 02:06에 게시됨