Devin.KR

임베디드 · 기본

MCU·GPIO·통신 기본

GPIO 출력 - LED 깜박이기

출력 모드, 푸시풀·오픈드레인, 지연 루프 vs 타이머

개발자KR · 원고 갱신

이 장에서 배우는 것

앞 장에서는 레지스터를 메모리 주소로 읽고 쓰는 방법을 다뤘다. 이 장에서는 그 레지스터 중 GPIO 출력 관련 레지스터를 실제로 조작해서 LED를 켜고 끄는 법을 다룬다. 스마트 화분의 상태 LED(정상일 때 켜짐, 경고일 때 깜박임)를 예로 들어, 출력 모드를 설정하는 절차와 시간 간격을 두고 반복 동작시키는 두 가지 방식을 비교한다.

  • GPIO 핀을 출력 모드로 설정하고 ODR(출력 데이터 레지스터) 비트로 LED를 켜고 끄는 절차를 이해한다
  • 푸시풀과 오픈드레인 출력의 전기적 차이와 각각을 언제 쓰는지 구분한다
  • 지연 루프(busy-wait)와 타이머 틱 기반 스케줄링의 차이를 코드로 구현하고 비교한다
  • PC 시뮬레이션 계층(hal_sim)으로 실제 보드 없이 GPIO 동작을 검증하는 방법을 익힌다

문제 상황

스마트 화분 시제품에 상태 LED 하나를 달았다. 정상이면 켜두고, 경고면 1초 간격으로 깜박이는 단순한 기능이다. 처음에는 지연 함수로 500ms씩 기다리며 LED를 토글하는 코드를 메인 루프에 넣었다. 그런데 같은 루프에 토양 습도를 확인하고 펌프를 켜는 코드를 추가하자 문제가 생겼다. LED가 깜박이는 동안 습도 확인 코드가 아예 실행되지 않아서, 토양이 말라도 펌프가 늦게 켜지는 일이 벌어졌다.

또 다른 문제는 확장 보드 팀이었다. LED 핀을 오픈드레인으로 설정해 둔 커넥터에 연결했는데, 코드에서 출력 레지스터에 1을 써도 LED가 켜지지 않았다. 풀업 저항을 달지 않았기 때문이다. 두 문제 모두 출력 모드의 전기적 의미와, 시간을 기다리는 두 가지 방식의 차이를 정확히 모르고 넘어가서 생긴 일이다.

출력 모드와 ODR 레지스터

GPIO 핀 하나는 보통 몇 개의 레지스터 비트로 제어된다. 방향을 정하는 레지스터(흔히 MODER 라고 부른다)로 해당 핀을 입력으로 둘지 출력으로 둘지 정하고, 출력으로 정했다면 출력 데이터 레지스터(ODR)의 해당 비트에 1 또는 0을 써서 핀의 전압을 높음/낮음으로 강제한다. 핀이 아직 입력 모드인데 ODR에 값을 써도, 전선의 실제 전압은 바뀌지 않는다. 방향 설정이 먼저이고 값 쓰기가 그다음이라는 순서를 지켜야 한다.

이 장의 예제는 실제 보드용 레지스터 주소 대신, 같은 역할을 하는 구조체 필드(moder, otyper, pupdr, odr, idr)를 PC 메모리에 두고 흉내 낸다. idr(입력 데이터 레지스터)는 실제 전선의 전압 상태를 나타내며, 뒤에서 다룰 푸시풀·오픈드레인 차이를 확인하는 데 쓴다.

실제 보드와의 차이

보드마다 레지스터 이름과 API는 다르지만, "방향을 정하고 값을 쓴다"는 개념은 같다. 공식 문서의 정확한 레지스터 이름은 각 제조사 레퍼런스 매뉴얼을 따로 확인한다.

같은 개념을 보드마다 어떻게 부르고 다루는지 비교한다
항목STM32ESP32Arduino(AVR)
출력 모드 설정MODER 레지스터 2비트/핀gpio_config_t.mode 필드DDRx 레지스터 비트(또는 pinMode)
오픈드레인 설정OTYPER 레지스터 1비트/핀gpio_config_t.mode 를 OPEN_DRAIN 값으로전용 비트 없음, 소프트웨어로 흉내
값 쓰기ODR 또는 BSRR 레지스터gpio_set_level() 함수PORTx 레지스터(또는 digitalWrite)

푸시풀과 오픈드레인

출력 타입은 핀이 전선을 어떻게 구동하는지를 정한다. 푸시풀(push-pull)은 하이와 로우를 모두 능동적으로 밀어붙인다. 1을 쓰면 내부 스위치가 전원 쪽에 붙어 전선을 강제로 끌어올리고, 0을 쓰면 반대쪽 스위치가 접지에 붙어 강제로 끌어내린다. LED를 직접 켜고 끄는 용도라면 대부분 이 방식이면 충분하다.

오픈드레인(open-drain)은 로우만 능동적으로 구동한다. 0을 쓰면 접지 쪽 스위치가 전선을 끌어내리지만, 1을 쓰면 그 스위치가 그냥 떨어질 뿐이다. 전선이 저절로 하이가 되지 않으므로, 외부에 풀업 저항을 달아서 전원 쪽으로 끌어올려 줘야 진짜로 하이가 된다. 풀업이 없으면 전선은 직전 상태를 유지하거나 주변 잡음에 흔들리는, 정해지지 않은 상태가 된다.

푸시풀은 하이와 로우를 모두 직접 구동하지만 오픈드레인은 로우만 구동하고 하이는 외부 풀업에 맡긴다

오픈드레인을 쓰는 이유

그냥 LED만 켤 거라면 오픈드레인을 고를 이유가 없다. 오픈드레인이 쓸모 있는 경우는 여러 장치가 같은 전선을 공유할 때다. 한 장치라도 전선을 강제로 끌어내리면 전체가 로우가 되고, 아무도 끌어내리지 않을 때만 풀업이 전선을 하이로 되돌린다. 이런 구조는 여러 센서가 한 선을 같이 쓰는 통신 방식에서 자주 등장한다. 지금은 개념만 짚고 넘어가고, 실제 활용은 뒤에서 통신 주변장치를 다룰 때 다시 만난다.

두 출력 타입의 전기적 차이를 정리한다
구분푸시풀오픈드레인
하이 구동능동적으로 전원에 연결연결 안 함(하이임피던스)
로우 구동능동적으로 접지에 연결능동적으로 접지에 연결
풀업 저항필요 없음하이 상태를 만들려면 필요
대표 용도LED, 모터 드라이버 신호여러 장치가 공유하는 신호선

지연 루프와 타이머 틱

LED를 일정 간격으로 깜박이려면 코드 어딘가에서 시간을 기다려야 한다. 가장 단순한 방법은 지연 루프(delay loop)다. 아무 의미 없는 카운터를 정해진 횟수만큼 돌리면서 시간을 흘려보낸다. 코드는 짧지만, 이 루프가 도는 동안 CPU는 다른 어떤 일도 하지 못한다. 문제 상황에서 나온 것처럼, LED를 깜박이는 지연 루프 안에 있는 동안에는 펌프나 센서를 확인하는 코드가 끼어들 틈이 없다.

다른 방법은 타이머 틱(tick)을 기준으로 시간을 확인하는 것이다. 실제 보드에는 일정 간격마다 값을 하나씩 늘리는 하드웨어 타이머가 있고, 코드는 그 값을 읽어서 "마지막으로 LED를 바꾼 뒤 얼마나 지났는지"만 짧게 확인한다. 아직 충분히 지나지 않았으면 바로 다음 할 일로 넘어간다. 메인 루프 한 바퀴가 짧게 끝나므로, 그 사이사이에 펌프나 센서 확인 같은 다른 작업을 끼워 넣을 수 있다.

PC에는 그런 하드웨어 타이머가 없으므로, 이 장의 시뮬레이션은 가상의 밀리초 카운터(sim_tick_ms)를 하나 두고, 지연 루프 방식은 그 값을 직접 통째로 전진시키고, 타이머 틱 방식은 작은 단위로 조금씩 전진시키면서 목표 시간이 됐는지 반복해서 확인한다.

지연 루프는 대기 시간 동안 CPU를 묶어두지만 타이머 틱 방식은 짧게 확인만 하고 다른 일을 할 수 있다
두 방식이 CPU 시간을 쓰는 방식을 비교한다
구분지연 루프타이머 틱
대기 중 CPU루프만 돌며 계속 점유짧게 확인하고 곧 반환
다른 작업 처리대기가 끝나야 가능매 반복마다 가능
정확도루프 횟수에 의존, 컴파일러 최적화에 민감타이머 하드웨어 정확도를 따라감

완성 코드

세 파일을 같은 디렉터리에 두고 컴파일한다. hal_sim.h/.c 는 GPIO 레지스터와 가상 시계를 흉내 내는 시뮬레이션 계층이고, main.c 가 실제 시나리오를 실행한다.

hal_sim.h

#ifndef HAL_SIM_H
#define HAL_SIM_H

#include <stdint.h>
#include <stdbool.h>

typedef enum {
    GPIO_MODE_INPUT = 0,
    GPIO_MODE_OUTPUT = 1
} gpio_mode_t;

typedef enum {
    GPIO_OTYPE_PUSH_PULL = 0,
    GPIO_OTYPE_OPEN_DRAIN = 1
} gpio_otype_t;

typedef struct {
    uint32_t moder;
    uint32_t otyper;
    uint32_t pupdr;
    uint32_t odr;
    uint32_t idr;
} gpio_port_t;

void gpio_init(gpio_port_t *port);
void gpio_set_mode(gpio_port_t *port, unsigned pin, gpio_mode_t mode);
void gpio_set_otype(gpio_port_t *port, unsigned pin, gpio_otype_t otype);
void gpio_set_pullup(gpio_port_t *port, unsigned pin, bool enable);
void gpio_write(gpio_port_t *port, unsigned pin, int level);
int gpio_read(gpio_port_t *port, unsigned pin);

uint32_t sim_get_tick_ms(void);
void sim_tick_advance(uint32_t ms);
void delay_loop_ms(uint32_t ms);

#endif

hal_sim.c

#include "hal_sim.h"

static uint32_t g_tick_ms = 0;

void gpio_init(gpio_port_t *port)
{
    port->moder = 0;
    port->otyper = 0;
    port->pupdr = 0;
    port->odr = 0;
    port->idr = 0;
}

static void gpio_update_line(gpio_port_t *port, unsigned pin)
{
    uint32_t mask = (1u << pin);

    if (!(port->moder & mask)) {
        return; /* 입력 모드에서는 드라이버가 전선에 관여하지 않는다 */
    }

    int driven = (port->odr & mask) != 0;
    int is_open_drain = (port->otyper & mask) != 0;

    if (!is_open_drain) {
        if (driven) {
            port->idr |= mask;
        } else {
            port->idr &= ~mask;
        }
        return;
    }

    if (!driven) {
        port->idr &= ~mask;
        return;
    }

    if (port->pupdr & mask) {
        port->idr |= mask; /* 외부 풀업이 전선을 끌어올린다 */
    }
    /* 풀업이 없으면 전선은 직전 상태를 유지한다(플로팅) */
}

void gpio_set_mode(gpio_port_t *port, unsigned pin, gpio_mode_t mode)
{
    uint32_t mask = (1u << pin);
    if (mode == GPIO_MODE_OUTPUT) {
        port->moder |= mask;
    } else {
        port->moder &= ~mask;
    }
}

void gpio_set_otype(gpio_port_t *port, unsigned pin, gpio_otype_t otype)
{
    uint32_t mask = (1u << pin);
    if (otype == GPIO_OTYPE_OPEN_DRAIN) {
        port->otyper |= mask;
    } else {
        port->otyper &= ~mask;
    }
    gpio_update_line(port, pin);
}

void gpio_set_pullup(gpio_port_t *port, unsigned pin, bool enable)
{
    uint32_t mask = (1u << pin);
    if (enable) {
        port->pupdr |= mask;
    } else {
        port->pupdr &= ~mask;
    }
    gpio_update_line(port, pin);
}

void gpio_write(gpio_port_t *port, unsigned pin, int level)
{
    uint32_t mask = (1u << pin);
    if (level) {
        port->odr |= mask;
    } else {
        port->odr &= ~mask;
    }
    gpio_update_line(port, pin);
}

int gpio_read(gpio_port_t *port, unsigned pin)
{
    return (port->idr & (1u << pin)) != 0;
}

uint32_t sim_get_tick_ms(void)
{
    return g_tick_ms;
}

void sim_tick_advance(uint32_t ms)
{
    g_tick_ms += ms;
}

void delay_loop_ms(uint32_t ms)
{
    /* 실제 보드에서는 코어 클록에 맞춰 보정한 빈 루프를 돈다.
       PC 에는 그런 클록이 없으므로 가상 시계만 그만큼 흘려보낸다. */
    volatile uint32_t busy = 0;
    for (uint32_t i = 0; i < ms * 1000u; i++) {
        busy++;
    }
    (void)busy;
    sim_tick_advance(ms);
}

main.c

#include <stdio.h>
#include <inttypes.h>
#include "hal_sim.h"

#define PIN_LED_PP 0
#define PIN_LED_OD 1
#define BLINK_INTERVAL_MS 500u

static void print_line_state(const char *label, gpio_port_t *port, unsigned pin)
{
    printf("%s: ODR=%u, 선(IDR)=%d\n", label,
           (unsigned)((port->odr >> pin) & 1u), gpio_read(port, pin));
}

int main(void)
{
    gpio_port_t gpioa;
    gpio_init(&gpioa);

    printf("=== 출력 모드: 푸시풀 vs 오픈드레인 ===\n");
    gpio_set_mode(&gpioa, PIN_LED_PP, GPIO_MODE_OUTPUT);
    gpio_set_otype(&gpioa, PIN_LED_PP, GPIO_OTYPE_PUSH_PULL);
    gpio_set_mode(&gpioa, PIN_LED_OD, GPIO_MODE_OUTPUT);
    gpio_set_otype(&gpioa, PIN_LED_OD, GPIO_OTYPE_OPEN_DRAIN);

    gpio_write(&gpioa, PIN_LED_PP, 1);
    gpio_write(&gpioa, PIN_LED_OD, 1);
    print_line_state("PP HIGH 요청 (풀업 없음)", &gpioa, PIN_LED_PP);
    print_line_state("OD HIGH 요청 (풀업 없음)", &gpioa, PIN_LED_OD);

    gpio_set_pullup(&gpioa, PIN_LED_OD, true);
    gpio_write(&gpioa, PIN_LED_OD, 1);
    print_line_state("OD HIGH 요청 (풀업 켬)", &gpioa, PIN_LED_OD);

    gpio_write(&gpioa, PIN_LED_PP, 0);
    gpio_write(&gpioa, PIN_LED_OD, 0);
    print_line_state("PP LOW 요청 (풀업 켬)", &gpioa, PIN_LED_PP);
    print_line_state("OD LOW 요청 (풀업 켬)", &gpioa, PIN_LED_OD);

    printf("\n=== 지연 루프로 LED 3회 깜박이기 (블로킹) ===\n");
    int led_state = 0;
    for (int i = 0; i < 6; i++) {
        led_state = !led_state;
        gpio_write(&gpioa, PIN_LED_PP, led_state);
        delay_loop_ms(BLINK_INTERVAL_MS);
        printf("tick=%4" PRIu32 " ms  LED=%-3s\n",
               sim_get_tick_ms(), led_state ? "ON" : "OFF");
    }

    printf("\n=== 타이머 틱 기반으로 LED 3회 깜박이기 (논블로킹) ===\n");
    led_state = 0;
    gpio_write(&gpioa, PIN_LED_PP, led_state);
    uint32_t last_toggle = sim_get_tick_ms();
    int toggles = 0;
    while (toggles < 6) {
        sim_tick_advance(100); /* 타이머가 100ms마다 인터럽트를 걸었다고 가정 */
        if (sim_get_tick_ms() - last_toggle >= BLINK_INTERVAL_MS) {
            led_state = !led_state;
            gpio_write(&gpioa, PIN_LED_PP, led_state);
            last_toggle = sim_get_tick_ms();
            toggles++;
            printf("tick=%4" PRIu32 " ms  LED=%-3s\n",
                   sim_get_tick_ms(), led_state ? "ON" : "OFF");
        }
    }

    return 0;
}

줄별 해설

  • gpio_port_t: STM32 계열 레지스터 이름을 본뜬 필드(moder, otyper, pupdr, odr, idr)를 가진 구조체 하나가 GPIO 포트 하나를 흉내 낸다. 실제 보드에서는 이 자리에 메모리 매핑된 주소가 온다.
  • gpio_update_line: 전선의 실제 상태(idr)를 계산하는 핵심 함수다. 입력 모드면 아무것도 하지 않고, 푸시풀이면 odr 값을 그대로 idr에 강제하고, 오픈드레인이면 로우 요청만 강제하고 하이 요청은 풀업 여부에 따라 달라진다.
  • gpio_set_mode / gpio_set_otype / gpio_set_pullup / gpio_write: 모두 레지스터 필드를 비트 단위로 |=, &= 연산해서 바꾼 뒤 gpio_update_line 을 호출해 전선 상태를 다시 계산한다. 값 전체를 덮어쓰지 않는 이유는 뒤에 나오는 실무 실수에서 다룬다.
  • delay_loop_ms: 의미 없는 카운터를 ms * 1000 번 돌려 시간을 흘려보낸 뒤, 가상 시계(sim_tick_advance)도 같은 양만큼 전진시킨다. 이 함수가 반환할 때까지 호출한 쪽은 다른 일을 전혀 할 수 없다.
  • 지연 루프 블록(main.c): 6번 반복하며 매번 delay_loop_ms(500)을 통째로 기다린 뒤 LED를 토글한다. 토글과 토글 사이에 다른 코드가 끼어들 자리가 없다.
  • 타이머 틱 블록(main.c): while 루프가 매번 가상 시계를 100ms만 전진시키고, 마지막 토글 이후 500ms가 지났는지만 짧게 확인한다. 조건이 거짓이면 바로 다음 반복으로 넘어가므로, 이 자리에 펌프나 센서 확인 코드를 끼워 넣어도 LED 간격이 밀리지 않는다.

실행 결과

$ cc -std=c11 -Wall -Wextra -o led_blink hal_sim.c main.c
$ ./led_blink
=== 출력 모드: 푸시풀 vs 오픈드레인 ===
PP HIGH 요청 (풀업 없음): ODR=1, 선(IDR)=1
OD HIGH 요청 (풀업 없음): ODR=1, 선(IDR)=0
OD HIGH 요청 (풀업 켬): ODR=1, 선(IDR)=1
PP LOW 요청 (풀업 켬): ODR=0, 선(IDR)=0
OD LOW 요청 (풀업 켬): ODR=0, 선(IDR)=0

=== 지연 루프로 LED 3회 깜박이기 (블로킹) ===
tick= 500 ms  LED=ON 
tick=1000 ms  LED=OFF
tick=1500 ms  LED=ON 
tick=2000 ms  LED=OFF
tick=2500 ms  LED=ON 
tick=3000 ms  LED=OFF

=== 타이머 틱 기반으로 LED 3회 깜박이기 (논블로킹) ===
tick=3500 ms  LED=ON 
tick=4000 ms  LED=OFF
tick=4500 ms  LED=ON 
tick=5000 ms  LED=OFF
tick=5500 ms  LED=ON 
tick=6000 ms  LED=OFF

오픈드레인 핀은 풀업 없이 1을 요청해도 전선이 그대로 0으로 남는다. 풀업을 켠 뒤에야 1로 올라가고, 반대로 0을 요청하면 풀업과 상관없이 항상 0으로 내려간다는 점도 마지막 두 줄에서 확인할 수 있다. 타이머 틱 블록의 시각이 3000 이후부터 시작하는 이유는, 가상 시계가 프로그램 시작 때 한 번만 초기화되고 이후에는 계속 누적되기 때문이다.

실무에서 자주 틀리는 것

오픈드레인 핀에 풀업을 빼먹는 실수

오픈드레인으로 설정해 놓고 하이 상태를 기대하며 값만 쓰는 경우가 흔하다.

/* 틀린 코드: 풀업 설정을 빼먹었다 */
gpio_set_otype(&gpioa, PIN_STATUS, GPIO_OTYPE_OPEN_DRAIN);
gpio_write(&gpioa, PIN_STATUS, 1);
if (gpio_read(&gpioa, PIN_STATUS)) {
    /* 여기 들어오지 않는다: 전선이 계속 0으로 남는다 */
}
/* 고친 코드: 풀업을 켜준다 */
gpio_set_otype(&gpioa, PIN_STATUS, GPIO_OTYPE_OPEN_DRAIN);
gpio_set_pullup(&gpioa, PIN_STATUS, true);
gpio_write(&gpioa, PIN_STATUS, 1);

레지스터를 통째로 덮어써서 다른 핀을 날리는 실수

레지스터 필드에 |=, &= 대신 = 를 써 버리면, 그 핀만 바꾸려던 것이 다른 핀 상태까지 지워 버린다.

/* 틀린 코드: ODR 전체를 대입해서 다른 핀이 전부 꺼진다 */
gpioa.odr = (1u << PIN_LED_PP);
/* 고친 코드: 원하는 비트만 바꾼다 */
gpio_write(&gpioa, PIN_LED_PP, 1);
/* 내부적으로 odr |= mask; 또는 odr &= ~mask; 만 수행한다 */

지연 루프로 메인 루프 전체를 막는 실수

LED 깜박임을 지연 루프로 짜 놓고, 같은 루프에 센서 확인이나 펌프 제어를 붙이면 그 코드가 지연 시간만큼 밀린다.

/* 틀린 코드: LED가 깜박이는 동안 센서를 확인하지 못한다 */
for (;;) {
    gpio_write(&gpioa, PIN_LED_PP, 1);
    delay_loop_ms(500);
    gpio_write(&gpioa, PIN_LED_PP, 0);
    delay_loop_ms(500);
    check_soil_and_pump(); /* 최소 1초에 한 번만 불린다 */
}
/* 고친 코드: 타이머 틱으로 LED와 센서 확인을 함께 처리한다 */
for (;;) {
    sim_tick_advance(10);
    if (sim_get_tick_ms() - last_toggle >= 500) {
        led_state = !led_state;
        gpio_write(&gpioa, PIN_LED_PP, led_state);
        last_toggle = sim_get_tick_ms();
    }
    check_soil_and_pump(); /* 매 반복마다 불린다 */
}

출력 모드로 바꾸기 전에 값부터 쓰는 실수

방향을 정하지 않은 상태에서 값을 써도 아무 효과가 없다. 순서가 바뀌면 조용히 무시된다.

/* 틀린 코드: 아직 입력 모드라 반영되지 않는다 */
gpio_write(&gpioa, PIN_LED_PP, 1);
gpio_set_mode(&gpioa, PIN_LED_PP, GPIO_MODE_OUTPUT);
/* 고친 코드: 방향을 먼저 정한다 */
gpio_set_mode(&gpioa, PIN_LED_PP, GPIO_MODE_OUTPUT);
gpio_write(&gpioa, PIN_LED_PP, 1);

한눈에 보기

이 장에서 다룬 핵심 개념을 정리한다
개념핵심주의할 점
출력 모드 설정방향 레지스터를 출력으로 바꾼 뒤 ODR에 값을 쓴다순서가 바뀌면 값 쓰기가 조용히 무시된다
푸시풀하이·로우 모두 능동 구동대부분의 LED·모터 신호에 적합
오픈드레인로우만 능동 구동, 하이는 풀업이 만든다풀업을 빼먹으면 전선이 정해지지 않은 상태로 남는다
지연 루프busy-wait로 시간을 흘려보낸다대기 중 CPU가 다른 일을 못 한다
타이머 틱경과 시간을 짧게 확인하고 넘어간다메인 루프에 다른 작업을 함께 넣을 수 있다

연습 문제

  1. PIN_LED_OD를 오픈드레인으로 설정하고 풀업도 켠 상태에서, gpio_write(&gpioa, PIN_LED_OD, 0)을 호출한 다음 gpio_read 결과를 예상해 보라. 그 이유도 함께 설명하라.
  2. 이 장의 delay_loop_ms는 호출할 때마다 가상 시계를 정확히 ms만큼 전진시킨다. 실제 보드에서 빈 루프로 지연을 구현할 때, 이 시뮬레이션과 달리 부정확해지는 이유를 두 가지 들어보라.
  3. 타이머 틱 블록의 while 루프에서 sim_tick_advance(100)을 sim_tick_advance(1000)으로 바꾸면 첫 번째 LED 토글이 일어나는 tick 값이 어떻게 달라지는지 계산하라.
  4. gpio_write 함수 안에서 gpio_update_line을 호출하지 않도록 지운다면, 이 장의 실행 결과 중 어느 줄부터 달라지는지 설명하라.

정답과 해설

  1. 0으로 읽힌다. gpio_update_line은 driven이 거짓(로우 요청)이면 풀업 여부와 상관없이 곧바로 idr 비트를 0으로 만들기 때문이다. 오픈드레인에서 로우 요청은 풀업이 있든 없든 항상 우선한다.
  2. 첫째, 실제 컴파일러 최적화 수준에 따라 빈 루프의 반복 횟수와 걸리는 시간의 관계가 달라진다. 둘째, 같은 보드라도 코어 클록 설정이 바뀌면 같은 루프 횟수라도 실제 경과 시간이 달라진다. 이 장의 시뮬레이션은 sim_tick_advance로 시간을 직접 지정하므로 이런 흔들림이 없다.
  3. 간격 조건은 500 이상 차이가 나야 토글하므로, 한 번에 1000씩 전진하면 첫 sim_tick_advance 호출만으로 조건을 만족한다. 즉 첫 토글이 tick=4000(직전 지연 루프가 끝난 tick 3000에서 1000을 더한 값)에서 일어나며, 지연 루프 방식과 달리 정확히 500 간격이 아니라 한 번에 건너뛴 값이 된다.
  4. 바로 다음 print_line_state 호출부터 달라진다. gpio_write가 odr만 바꾸고 idr을 갱신하지 않으므로, 첫 번째 출력 블록의 "선(IDR)=1" 부분이 전부 이전 값인 0으로 남아 ODR과 IDR이 어긋난 상태로 출력된다.
오탈자·오류 제보 비공개로 접수되어 원고 수정에 반영됩니다

이메일 등 개인정보는 받지 않습니다. 답변이 필요한 질문은 아래 댓글을 이용해 주세요.

READER FEEDBACK

질문·의견

내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.