지금까지 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

CloudZip — Download & Installation Guide

A quick guide for first-time users. On the Mac, an app downloaded from outside the App Store may show a security prompt the first time you open it. Follow the steps below and it will run without any issue.

 

1. Download

Get the latest version (1.0.0) from the link below.

👉 [download link — CloudZip-1.0.0.dmg]

  • macOS 13 Ventura or later
  • Universal binary — runs natively on both Apple silicon (M1 and later) and Intel Macs

 

2. Install

Double-click the downloaded .dmg, then drag the CloudZip icon into your Applications folder. That's it.

 

3. First Launch (clearing the security prompt)

CloudZip is distributed by an independent developer, so the first launch may show "unidentified developer" or "cannot be opened." This is expected — allow it once and it opens normally afterward.

▶ Option 1 — Open via right-click (macOS 14 and earlier)

  1. In Applications, right-click (or Control-click) the CloudZip icon
  2. Choose Open
  3. Click Open again in the dialog

▶ Option 2 — Allow in System Settings (macOS 15 Sequoia or later)

  1. Try launching CloudZip once (the prompt appears and closes)
  2. Open System Settings → Privacy & Security
  3. Scroll down and click Open Anyway next to the "CloudZip was blocked" notice
  4. Confirm with Open

▶ Option 3 — If it says "damaged and can't be opened" (Terminal)

Rarely you may see "CloudZip is damaged and can't be opened." This is due to the download quarantine attribute. Open Terminal (Applications → Utilities → Terminal), paste the command, and press

Enter

:

xattr -dr com.apple.quarantine /Applications/CloudZip.app

Then launch it again — it will open normally.

 

4. Enable the Finder Right-Click Menu

To use right-click → Compress / Extract in Finder, enable the extension once.

  1. Launch CloudZip once
  2. Open System Settings → General → Login Items & Extensions
  3. Under Finder extensions, turn on CloudZip

Right-clicking files or folders now shows Extract Here / Extract to "…" / Compress with CloudZip.

 

5. (Optional) Install the 'cloudzip' Terminal Command

To compress/extract from the terminal or scripts:

  1. Launch CloudZip
  2. Choose CloudZip → Install Command Line Tool… from the menu (enter your admin password once)

This creates a /usr/local/bin/cloudzip link so you can use it right away:

cloudzip -h                    # help
cloudzip l file.zip            # list
cloudzip x file.zip -o ./out   # extract
cloudzip c backup.zip folder -l 9   # compress

 

6. Updating

When a new version is out, download it the same way and replace the existing CloudZip in your Applications folder. (Quit the app first if it's running.)

 

반응형

CloudZip — A Lightweight, Native Archive Manager for Mac

⚙️ Requirements — macOS 13 Ventura or later · Universal (Apple silicon & Intel)

Handling archives on the Mac has always been a bit frustrating. The built-in tools garble non-Latin filenames, struggle with anything beyond ZIP, and offer almost no options. So I built an archive manager that is intuitive and designed for the Mac. It's called CloudZip.

 

ㅁ Supported Formats

Extract (open) — virtually every major format is supported.

  • Common: ZIP, ZIPX, 7Z, RAR, TAR, GZ (TGZ), BZ2 (TBZ), XZ (TXZ), LZ, LZMA, Z, ZST
  • Disk & installer: ISO, IMG, CAB, WIM, XAR, CPIO
  • Others: LHA/LZH, ARJ, SIT/SITX

Compress (create)

  • ZIP, 7Z, TAR, TAR.GZ, TAR.BZ2, TAR.XZ, TAR.ZST
  • ZIP also supports password protection (encrypted ZIP) and split archives

 

ㅁ Key Features

Fast, Powerful Compression

  • Multi-core parallel compression for high speed
  • Fast archiving — already-compressed files (photos, videos, PDFs, etc.) are stored without re-compression, saving significant time
  • Password (encrypted ZIP) and split archives (10MB / 100MB / 700MB / 1GB / 4GB)

 

Edit ZIP Without Extracting

No need to fully unpack and repack — edit directly inside a ZIP.

  • Add files and folders (drag & drop onto the list)
  • Delete (Delete key / right-click)
  • Rename and create a new folder
  • Move items into another folder (drag onto the folder tree on the left)

Existing compressed data is left untouched and only the changes are written, so it's extremely fast.

 

No More Garbled Filenames

Legacy filename encodings (CP949, Shift-JIS, GBK) are automatically detected, so CJK filenames display and extract without corruption.

Deep Finder Integration

  • Right-click → Compress / Extract right in Finder
  • Drag files out of an archive straight into Finder to extract them
  • Double-click an archive to open it in CloudZip

 

Quick Look Preview

Preview photos, documents, and more without extracting the archive.

 

Simple Drag & Drop

Just drag files and folders onto the window to queue them, then hit Compress.

 

Fine-Grained Preferences

Set your default format, compression level, filename encoding, fast archiving, and file associations. Available in 5 languages: English, Korean, Japanese, Chinese (Simplified/Traditional).

 

Command-Line Tool

Beyond the GUI, the cloudzip terminal command can compress, extract, and list archives — great for scripts and automation.

cloudzip l archive.zip                   # list
cloudzip x archive.7z -o ./out           # extract
cloudzip c backup.tar.zst docs photos -l 9   # compress

Enable it once from the app menu: CloudZip → Install Command Line Tool…

 

ㅁ  System Requirements

  • macOS 13 Ventura or later
  • Universal binary — runs natively on both Apple silicon (M1 and later) and Intel Macs

 

ㅁ  Download & Install

On first launch macOS may show a security prompt. See the [install guide link] for step-by-step instructions.

[Download Link]

반응형

'CloudZIP' 카테고리의 다른 글

[ENG] How to Install CloudZIP on Mac  (0) 2026.07.19
구름집 설치 가이드  (0) 2026.07.19
구름집 1.0.0 배포 - 맥용 무료 압축 프로그램  (0) 2026.07.19

구름집(CloudZip) 다운로드 & 설치 가이드

구름집을 처음 설치하는 분들을 위한 안내입니다. 맥에서는 외부에서 받은 앱을 처음 열 때 보안 경고가 뜰 수 있는데, 아래 순서대로 하면 문제없이 실행됩니다.

 

1. 다운로드

아래 링크에서 최신 버전(1.0.0)을 내려받으세요.

 [다운로드 링크 — CloudZip-1.0.0.dmg]

  • macOS 13 Ventura 이상
  • Apple 실리콘(M1~)·인텔 맥 모두 지원 (유니버설 바이너리)

 

2. 설치하기

내려받은 .dmg 파일을 두 번 눌러 열고, 나타나는 구름집 아이콘을 응용 프로그램(Applications) 폴더로 끌어다 놓으면 설치가 끝납니다.

 

3. 처음 실행하기 (보안 경고 해제)

구름집은 개인 개발자가 배포하는 앱이라, 처음 열 때 "확인되지 않은 개발자" 또는 "열 수 없습니다" 경고가 뜰 수 있습니다. 정상이며, 아래 방법으로 한 번만 허용하면 이후로는 그냥 열립니다.

▶ 방법 1 — 우클릭으로 열기 (macOS 14 이하)

  1. 응용 프로그램 폴더에서 구름집 아이콘을 우클릭(또는 Control+클릭)
  2. 메뉴에서 [열기] 선택
  3. 경고창에서 다시 [열기] 클릭

▶ 방법 2 — 시스템 설정에서 허용 (macOS 15 Sequoia 이상)

  1. 구름집을 한 번 실행 시도 (경고가 뜨고 닫힘)
  2. 시스템 설정 → 개인정보 보호 및 보안 열기
  3. 아래로 스크롤 → "구름집이(가) 차단되었습니다" 안내의 [그래도 열기] 클릭
  4. 다시 뜨는 확인창에서 [열기]

▶ 방법 3 — "손상되어 열 수 없습니다"가 뜰 때 (터미널)

드물게 "손상되었기 때문에 열 수 없습니다" 메시지가 나오면 다운로드 격리 속성 때문입니다. 터미널(응용 프로그램 → 유틸리티 → 터미널)에서 아래를 붙여넣고

Enter

:

xattr -dr com.apple.quarantine /Applications/CloudZip.app

그런 다음 다시 실행하면 정상적으로 열립니다.

 

4. 파인더 우클릭 메뉴 켜기

파인더에서 우클릭 → 압축 / 압축 풀기를 쓰려면 확장 기능을 한 번 켜줘야 합니다.

  1. 구름집을 한 번 실행합니다
  2. 시스템 설정 → 일반 → 로그인 항목 및 확장 프로그램
  3. Finder 확장 프로그램에서 구름집을 켭니다(체크)

이제 파일·폴더를 우클릭하면 여기에 압축 풀기 / ‘파일명’ 폴더에 압축 풀기 / CloudZip으로 압축하기 메뉴가 나타납니다.

 

5. (선택) 터미널 명령 'cloudzip' 설치

터미널·스크립트에서 압축·풀기를 하고 싶다면:

  1. 구름집을 실행합니다
  2. 메뉴 구름집 → 명령줄 도구 설치… 클릭 (관리자 암호 1회)

그러면 /usr/local/bin/cloudzip 링크가 생기고, 터미널에서 바로 쓸 수 있습니다.

cloudzip -h                  # 도움말
cloudzip l 파일.zip           # 목록
cloudzip x 파일.zip -o ./out  # 풀기
cloudzip c 백업.zip 폴더 -l 9  # 압축

 

6. 업데이트

새 버전이 나오면 같은 방법으로 내려받아 응용 프로그램 폴더의 기존 구름집을 덮어쓰면 됩니다. (실행 중이면 먼저 종료해 주세요.)

 

설치 중 막히는 부분이 있으면 댓글로 남겨주세요. 

반응형

구름집(CloudZip) — 가볍고 편한 맥용 압축 프로그램

⚙️ 요구 사양 — macOS 13 Ventura 이상 · Apple 실리콘(M1~)/인텔 맥 모두 지원 (유니버설 바이너리)

맥에서 압축 파일을 다루다 보면 늘 아쉬웠습니다. 기본 압축 유틸리티는 한글 파일명이 깨지고, ZIP 말고는 제대로 못 열고, 세밀한 옵션도 없죠. 그래서 직관적이면서 맥에 딱 맞는 압축 관리자를 직접 만들었습니다. 이름은 구름집입니다.



ㅁ 지원 형식

압축 풀기(열기) — 사실상 모든 주요 형식을 지원합니다.

  • 일반: ZIP, ZIPX, 7Z, RAR, TAR, GZ(TGZ), BZ2(TBZ), XZ(TXZ), LZ, LZMA, Z, ZST
  • 디스크·설치: ISO, IMG, CAB, WIM, XAR, CPIO
  • 기타·국내: LHA/LZH, ARJ, SIT/SITX — 그리고 국내에서 많이 쓰는 EGG·ALZ

압축(만들기)

  • ZIP, 7Z, TAR, TAR.GZ, TAR.BZ2, TAR.XZ, TAR.ZST
  • ZIP은 암호 설정(암호화 ZIP)분할 압축도 지원



ㅁ 주요 기능

빠르고 강력한 압축

  • 멀티코어 병렬 압축으로 빠른 속도
  • 고속 압축 — 이미 압축된 파일(사진·영상·PDF 등)은 다시 압축하지 않고 담아 시간을 크게 절약
  • 암호(암호화 ZIP)분할 압축(10MB·100MB·700MB·1GB·4GB) 지원


압축 해제 없이 ZIP 수정

압축을 통째로 풀었다 다시 압축할 필요 없이, ZIP 안에서 바로 편집합니다.

  • 파일·폴더 추가 (목록에 드래그&드롭)
  • 삭제 (Delete 키 / 우클릭)
  • 이름 바꾸기, 새 폴더 만들기
  • 파일을 다른 폴더로 이동 (왼쪽 폴더 트리로 끌어다 놓기)

기존 압축 데이터를 그대로 두고 바뀐 부분만 처리하기 때문에 매우 빠릅니다.

 

한글 파일명 안 깨짐

구형 압축 파일의 CP949(EUC-KR) 한글 이름은 물론 일본어·중국어 인코딩까지 자동으로 인식해 깨짐 없이 보여주고 풀어줍니다.

Finder(파인더)와 완벽 통합

  • 파인더에서 우클릭 → 압축 / 압축 풀기
  • 압축 파일 안의 파일을 파인더로 바로 끌어내 꺼내기
  • 파일을 더블클릭하면 구름집으로 열기



미리보기 (Quick Look)

압축을 풀지 않고도 사진·문서 등 파일 내용을 바로 미리보기할 수 있습니다.



드래그 & 드롭으로 간편하게

파일·폴더를 창에 끌어다 놓기만 하면 압축 대기 목록에 담기고, [압축] 버튼으로 바로 압축됩니다.

 

세밀한 환경설정

기본 압축 형식·압축률·파일명 인코딩, 고속 압축, 파일 연결까지 원하는 대로 설정할 수 있습니다. 그리고 한국어·영어·일본어·중국어(간체/번체) 5개 언어를 지원합니다.

 

명령줄(터미널)에서도 사용

GUI뿐 아니라 터미널 명령 cloudzip으로도 압축·풀기·목록을 처리할 수 있어 스크립트·자동화에 활용하기 좋습니다.

cloudzip l archive.zip                    # 목록
cloudzip x archive.7z -o ./out            # 풀기
cloudzip c backup.tar.zst 문서 사진 -l 9   # 압축

앱 메뉴 구름집 → 명령줄 도구 설치…를 한 번 누르면 활성화됩니다.


ㅁ 지원 환경

  • macOS 13 Ventura 이상
  • Apple 실리콘(M1~)·인텔 맥 모두 지원 (유니버설 바이너리)


ㅁ 다운로드 & 설치

처음 실행할 때 보안 경고가 뜰 수 있습니다. 자세한 설치 방법은 [설치 가이드 글 링크]를 참고하세요.

CloudZip-1.0.0.dmg
9.02MB

반응형

'CloudZIP' 카테고리의 다른 글

[ENG] How to Install CloudZIP on Mac  (0) 2026.07.19
[ENG] CloudZIP 1.0.0 - Zip,Unzip & Extract Archives on Mac  (0) 2026.07.19
구름집 설치 가이드  (0) 2026.07.19

어느날 메시지앱을 켰더니 먹통이 되더니 아무것도 안된다.

아이폰-> 설정 -> 메시지 를 클릭해도 얼음땡
아이폰-> 설정 -> FaceTime을 클릭해도 얼음땡

아이폰을 몇번을 꼈다 켜도 안되더라..

혹시나, 아이클라우드에 메시지 백업 해 놓은 기능 때문에 그런건지 싶어서
아이클라우드 백업 기능도 모두 끄기 또는 비활성 했는데도 여전히 먹통

Apple ID 로그아웃 을 했더니 되더라

씨발 애플아, 제대로좀 만들어라..

3시간동안 삽질 했다...


반응형

'소소한 재미' 카테고리의 다른 글

전기요금 개편  (0) 2013.08.21
설국열차를 보고&hellip;(스포 작렬)  (0) 2013.08.16
공주님 안기  (0) 2013.08.14
소프트웨어 GS 인증  (0) 2013.08.14
티스토리로 블로그 이전  (0) 2013.08.14

+ Recent posts