본문 바로가기
MCU/AVR

AVR128DA64/48/32/28 SPI 드라이버 설계 및 구현

by linuxgo 2025. 9. 3.
반응형

1.개요

본 문서는 Microchip AVR128DA64/48/32/28 시리즈 마이크로컨트롤러Serial Peripheral Interface (SPI) 모듈을 활용한 범용 드라이버의 설계 및 구현을 다룹니다.드라이버는 비버퍼 모드(BUFEN=0) 기반으로 설계되어 불필요한 메모리 점유를 최소화하며, 마스터(Host)/슬레이브(Client) 모드를 모두 지원합니다. 또한, 최대 12MHz 클럭 속도(마스터 기준, CLK_PER/2), 8비트 데이터 전송, 폴링/인터럽트 기반 통신, PORTMUX를 통한 유연한 핀 매핑 기능을 제공합니다.
특히, AVR128DA28은 패키지 핀 수 제약으로 SPI0만 지원하며, SPI1 관련 레지스터 및 인터럽트가 존재하지 않음을 명확히 하였습니다.드라이버는 Microchip Studio 및 AVR-GCC 환경에서 모든 AVR128DA 시리즈에 호환되도록 구현되었으며, Write Collision(WCOL) 오류 감지 및 복구 기능도 포함되어 안정적인 SPI 통신을 보장합니다.

AVR128DA SPI block diagram

참고:

  • 데이터시트(DS40002183E, 페이지 411~417)에 따라 INTFLAGS 레지스터의 중복 정의(WRCOL → WCOL, BUFOVF 제거)와 INTCTRL의 RXCIE 오류를 수정했습니다.
  • AVR128DA28은 SPI1 미지원(페이지 19, Table 22-1). SPI1_INT(0x48, 벡터 36)는 32/48/64핀 모델에서만 사용 가능.
  • 드라이버는 비버퍼 모드(BUFEN=0)만 사용하며, 버퍼 모드 관련 비트(RXCIE, TXCIE, DREIE, SSIE, RXCIF, TXCIF, DREIF, SSIF, BUFOVF)는 제외했습니다.

2. 사양

2.1 SPI 모듈 사양

  • 모듈 수:
    • AVR128DA64/48/32: SPI0, SPI1 (2개).
    • AVR128DA28: SPI0 (1개, SPI1 미지원 – 28핀 패키지의 핀 수 제약, SPI1 레지스터/인터럽트 부재).
  • 핀 매핑 (데이터시트 DS40002183E, Table 22-1, 페이지 19):
    • SPI0:
      • DEFAULT (0x0): PA4(MOSI)/PA5(MISO)/PA6(SCK)/PA7(SS) – 모든 모델 (28/32/48/64핀).
      • ALT1 (0x1): PC0(MOSI)/PC1(MISO)/PC2(SCK)/PC3(SS) – 32/48/64핀.
      • ALT2 (0x2): PF4(MOSI)/PF5(MISO)/PF6(SCK)/PF7(SS) – 32/48/64핀.
    • SPI1:
      • DEFAULT (0x0): PB0(MOSI)/PB1(MISO)/PB2(SCK)/PB3(SS) – 48/64핀.
      • ALT1 (0x1): PC4(MOSI)/PC5(MISO)/PC6(SCK)/PC7(SS) – 64핀.
  • 전압 범위: 1.8V ~ 5.5V (VDD).
  • 기능:
    • 마스터(Host)/슬레이브(Client) 모드.
    • 클럭 속도: 마스터 최대 CLK_PER/2 (24MHz CLK_PER 기준 12MHz), 슬레이브 최대 CLK_PER/4.
    • 데이터: 8비트 (DORD로 MSB/LSB 선택).
    • 클럭 모드: MODE 0~3 (CPOL/CPHA).
    • 인터럽트: SPI0 (0x24, 벡터 18), SPI1 (0x48, 벡터 36, 28핀 제외).
    • 에러: WCOL, 소프트웨어로 버스 충돌 관리.
  • 클럭 의존성: Peripheral Clock (CLK_PER, 최대 24MHz).
  • 리셋 상태: SPI 비활성화, 핀은 GPIO 입력 모드.
  • 전력 최적화: ENABLE=0으로 전력 절감.

2.2 인터럽트 벡터

데이터시트 DS40002183E (페이지 67~68):

Vector Number Program Address (word) Peripheral Source Description 28-Pin 32-Pin 48-Pin 64-Pin
18 0x24 SPI0_INT SPI0 Interrupt X X X X
36 0x48 SPI1_INT SPI1 Interrupt   X X X
  • SPI0_INT: 모든 모델 지원 (벡터 18, 0x24).
  • SPI1_INT: AVR128DA28 미지원, 32/48/64핀만 지원 (벡터 36, 0x48).

2.3 핀 매핑

SPI 핀은 PORTMUX.SPIROUTEA로 설정되며, 다른 주변 장치(USART, TWI, TCA)와 공유되므로 충돌 주의:

SPI 모듈 PORTMUX 설정 MOSI MISO SCK SS 지원 모델
SPI0 DEFAULT (0x0) PA4 PA5 PA6 PA7 28/32/48/64핀
SPI0 ALT1 (0x1) PC0 PC1 PC2 PC3 32/48/64핀
SPI0 ALT2 (0x2) PF4 PF5 PF6 PF7 32/48/64핀
SPI1 DEFAULT (0x0) PB0 PB1 PB2 PB3 48/64핀
SPI1 ALT1 (0x1) PC4 PC5 PC6 PC7 64핀
  • PORTMUX 설정: SPI0 (비트 0~1), SPI1 (비트 2~3). 예: PORTMUX.SPIROUTEA |= (1 << 0)로 SPI0 ALT1.
  • 풀업 저항: SS에 10kΩ 권장 (슬레이브).
  • Errata: 64핀 모델에서 PORTE/PG 관련 PORTMUX 제한 가능 (DxCore 문서 참조).

3. SPI 레지스터 상세

SPI 모듈은 5개의 레지스터(CTRLA, CTRLB, INTCTRL, INTFLAGS, DATA)로 제어됩니다. 아래는 데이터시트 DS40002183E (페이지 411~417)에 기반한 상세 설명입니다. 본 드라이버는 비버퍼 모드(BUFEN=0)를 사용하므로, 버퍼 모드 관련 비트(RXCIE, TXCIE, DREIE, SSIE, RXCIF, TXCIF, DREIF, SSIF, BUFOVF)는 제외하며, INTFLAGS는 Normal Mode 정의만 사용합니다.

3.1 CTRLA (Control A, 오프셋 0x00)

CTRLA는 SPI 모듈의 기본 동작을 제어하며, 활성화, 모드, 클럭 설정, 데이터 순서를 관리합니다(데이터시트 페이지 412).

  • Bit 7 - Reserved: 사용되지 않으며, 항상 0으로 유지.
  • Bit 6 - DORD (Data Order): 데이터 전송 순서. 0=MSB 먼저, 1=LSB 먼저. 대부분의 SPI 장치는 MSB 우선.
  • Bit 5 - MASTER (Host/Client Select): SPI 모드 선택. 1=마스터(Host), 0=슬레이브(Client). 슬레이브 모드에서 SS가 Low로 구동되면 MASTER가 0으로 클리어되고 INTFLAGS.IF가 설정됨(CTRLB.SSD로 제어).
  • Bit 4 - CLK2X (Clock Double): 마스터 모드에서 클럭 속도를 2배로 설정. 1일 때 \( f_{SCK} = f_{CLK_PER}/PRESC \), 0일 때 \( f_{SCK} = f_{CLK_PER}/(2 \cdot PRESC) \).
  • Bit 3:2 - PRESC[1:0] (Prescaler): 마스터 모드 클럭 분주 설정. 값: 0x0=DIV4, 0x1=DIV16, 0x2=DIV64, 0x3=DIV128. 슬레이브 모드에서는 무효.
  • Bit 1 - Reserved: 사용되지 않으며, 항상 0으로 유지.
  • Bit 0 - ENABLE (SPI Enable): SPI 모듈 활성화. 1=활성화(MOSI, MISO, SCK, SS가 SPI 모드로 전환), 0=비활성화(핀은 GPIO로 복귀).

사용 예제:

SPI0.CTRLA = (1 << SPI_MASTER_bp) | (1 << SPI_CLK2X_bp) | (0x0 << SPI_PRESC_gp) | (1 << SPI_ENABLE_bp);
// 마스터 모드, CLK2X=1, PRESC=4, SPI 활성화 (SCK=6MHz, CLK_PER=24MHz)

3.2 CTRLB (Control B, 오프셋 0x01)

CTRLB는 슬레이브 선택(SS), 버퍼 모드, 클럭 모드(CPOL/CPHA)를 설정합니다(데이터시트 페이지 413). 본 드라이버는 비버퍼 모드(BUFEN=0)만 사용.

  • Bit 7 - Reserved: 사용되지 않으며, 항상 0으로 유지.
  • Bit 6 - BUFEN (Buffer Mode Enable): 버퍼 모드 활성화. 1=두 개의 수신 버퍼와 하나의 송신 버퍼 활성화, 0=비버퍼 모드. 본 드라이버는 0.

Normal mode
buffer mode

  • Bit 5 - BUFWR (Buffer Mode Wait for Receive): 버퍼 모드에서 첫 데이터 전송 동작. 0=더미 샘플 전송, 1=첫 쓰기가 즉시 시프트 레지스터로 이동. 본 드라이버는 사용 안 함(BUFEN=0).

buffer mode

  • Bit 4:3 - Reserved: 사용되지 않으며, 항상 0으로 유지.
  • Bit 2 - SSD (Client Select Disable): 마스터 모드에서 SS 핀 비활성화. 1=SS 무시, 0=SS 활성화(SS가 Low로 구동 시 MASTER 비트 클리어).
  • Bit 1:0 - MODE[1:0] (Mode): 클럭 모드 설정:
    • 0x0: Mode 0 (CPOL=0, CPHA=0, 상승 에지 샘플링, 하강 에지 설정).
    • 0x1: Mode 1 (CPOL=0, CPHA=1, 하강 에지 샘플링, 상승 에지 설정).
    • 0x2: Mode 2 (CPOL=1, CPHA=0, 하강 에지 샘플링, 상승 에지 설정).
    • 0x3: Mode 3 (CPOL=1, CPHA=1, 상승 에지 샘플링, 하강 에지 설정).

사용 예제:

SPI0.CTRLB = (1 << SPI_SSD_bp) | (0x0 << SPI_MODE_gp);
// 슬레이브 선택 비활성화, Mode 0 (CPOL=0, CPHA=0)

3.3 INTCTRL (Interrupt Control, 오프셋 0x02)

INTCTRL은 인터럽트를 제어하며, 비버퍼 모드에서는 IE만 사용합니다(데이터시트 페이지 414).

  • Bit 7 - RXCIE (Receive Complete Interrupt Enable): 버퍼 모드에서 수신 완료 인터럽트 활성화. 본 드라이버는 비버퍼 모드(0).
  • Bit 6 - TXCIE (Transfer Complete Interrupt Enable): 버퍼 모드에서 전송 완료 인터럽트 활성화. 본 드라이버는 비버퍼 모드(0).
  • Bit 5 - DREIE (Data Register Empty Interrupt Enable): 버퍼 모드에서 데이터 레지스터 비움 인터럽트 활성화. 본 드라이버는 비버퍼 모드(0).
  • Bit 4 - SSIE (Client Select Trigger Interrupt Enable): 버퍼 모드에서 SS 상태 변화 인터럽트 활성화. 본 드라이버는 비버퍼 모드(0).
  • Bit 3:1 - Reserved: 사용되지 않으며, 항상 0으로 유지.
  • Bit 0 - IE (Interrupt Enable): 비버퍼 모드에서 SPI 인터럽트 활성화. INTFLAGS.IF 설정 시 SPI0_INT(0x24) 또는 SPI1_INT(0x48, 28핀 제외) 트리거.

사용 예제:

SPI0.INTCTRL = (1 << SPI_IE_bp);
// SPI 전체 인터럽트 활성화 (비버퍼 모드)

3.4 INTFLAGS (Interrupt Flags, 오프셋 0x03, Normal Mode)

INTFLAGS는 비버퍼 모드에서 인터럽트 플래그와 에러 상태를 제공합니다(데이터시트 페이지 415). 본 드라이버는 비버퍼 모드만 사용하므로 Normal Mode 정의 적용.

  • Bit 7 - IF (Interrupt Flag): 전송/수신 완료 플래그. DATA 레지스터로 1바이트 송수신 완료 시 1. 마스터 모드에서 SS가 Low로 구동 시에도 설정됨(SSD=0). 1 쓰기 또는 INTFLAGS.IF 읽기 후 DATA 접근으로 클리어.
  • Bit 6 - WCOL (Write Collision): 쓰기 충돌 에러. DATA 레지스터에 이전 데이터가 처리 중일 때 쓰기 시 1. INTFLAGS.WCOL 읽기 후 DATA 접근으로 클리어.
  • Bit 5:0 - Reserved: 사용되지 않으며, 항상 0으로 유지.

사용 예제:

if (SPI0.INTFLAGS & SPI_WCOL_bm) {
    SPI0.INTFLAGS |= SPI_WCOL_bm; // WCOL 클리어
}

3.5 DATA (Data Register, 오프셋 0x04)

DATA는 8비트 송수신 레지스터입니다(데이터시트 페이지 417). 비버퍼 모드에서 쓰기는 시프트 레지스터로, 읽기는 수신 레지스터에서 수행됩니다.

  • Bit 7:0 - DATA[7:0]: 송수신 데이터. 쓰기 시 마스터는 SCK 클럭 생성, 슬레이브는 준비 데이터 저장. 읽기 시 수신 데이터 반환. CTRLA.DORD로 MSB/LSB 순서 결정.

사용 예제:

SPI0.DATA = 0xAA; // 0xAA 전송
while (!(SPI0.INTFLAGS & SPI_IF_bm)); // 전송 완료 대기
uint8_t rx = SPI0.DATA; // 수신 데이터 읽기

레지스터 수정 사항:

  • INTFLAGS 중복 정의 제거: Normal Mode(IF, WCOL)만 사용, Buffer Mode(RXCIF, TXCIF, DREIF, SSIF, BUFOVF) 제외.
  • WRCOLWCOL로 수정(페이지 415).
  • INTCTRL.RXCIE는 버퍼 모드 전용이므로 비버퍼 모드 드라이버에서 제외.

4. 드라이버 설계

4.1 설계 목표

  • 호환성: AVR128DA28 (SPI0), AVR128DA32/48/64 (SPI0, SPI1).
  • 간소화: 링버퍼 없이 비버퍼 모드(BUFEN=0)로 최소 메모리 사용.
  • 유연성: 마스터/슬레이브, 폴링/인터럽트, 최대 12MHz.
  • 핀 매핑: PORTMUX로 DEFAULT, ALT1, ALT2.
  • 에러 처리: WCOL 감지 및 클리어.
  • 테스트 용이성: Microchip Studio 시뮬레이터 및 실제 SPI 장치로 검증.

4.2 주요 기능

  • spi_init: 초기화 (포트, 클럭, 모드, PORTMUX).
  • spi_transceive: 단일 바이트 송수신 (폴링).
  • spi_transceive_block: 다중 바이트 송수신 (폴링).
  • spi_transceive_it: 인터럽트 기반 송수신.
  • spi_check_errors: WCOL 확인 및 클리어.
  • spi_enable/disable: SPI 활성화/비활성화.
  • 인터럽트: SPI0_INT_vect (0x24), SPI1_INT_vect (0x48, 28핀 제외).

4.3 설정 절차

  1. CLKCTRL로 CLK_PER 활성화 (24MHz OSCHF).
  2. PORTMUX로 MOSI/MISO/SCK/SS 설정, SS 풀업 (슬레이브).
  3. CTRLA/CTRLB로 마스터/슬레이브, 클럭 모드 설정.
  4. CTRLA.PRESC/CLK2X로 클럭 설정.
  5. INTCTRL.IE로 인터럽트 활성화.
  6. CTRLA.ENABLE로 SPI 활성화.
  7. PORTMUX로 주변 장치 충돌 확인.
  8. INTFLAGS.WCOL로 에러 처리.

5. 드라이버 구현

spi_driver.h

/**
 * @file spi_driver.h
 * @brief AVR128DA64/48/32/28 SPI 드라이버 헤더 파일
 * @details 비버퍼 모드(BUFEN=0)로 링버퍼 없이 마스터/슬레이브 통신.
 *          SPI0/SPI1 지원 (28핀은 SPI0만, SPI1 미지원).
 *          데이터시트 DS40002183E (페이지 411~417) 기반.
 * @author linuxgo
 * @date 2025-09-03
 */
#ifndef SPI_DRIVER_H
#define SPI_DRIVER_H

#include <avr/io.h>
#include <stdint.h>

/** 
 * @brief SPI 클럭 속도 정의
 * @details 데이터시트 DS40002183E (페이지 412): 
 *          f_SCK = f_CLK_PER / (2 * PRESC) (CLK2X=0), f_SCK = f_CLK_PER / PRESC (CLK2X=1).
 *          예: F_CPU=24MHz, CLK2X=1, PRESC=4 → f_SCK=6MHz.
 */
#define SPI_BAUD_125KHZ (F_CPU / 128) // 125kHz (PRESC=128, CLK2X=0)
#define SPI_BAUD_500KHZ (F_CPU / 32)  // 500kHz (PRESC=16, CLK2X=0)
#define SPI_BAUD_4MHZ   (F_CPU / 4)   // 4MHz (PRESC=4, CLK2X=1)
#define SPI_BAUD_12MHZ  (F_CPU / 2)   // 12MHz (PRESC=2, CLK2X=1)

/** 
 * @brief SPI 모드 정의
 * @details 마스터(Host, 0) 또는 슬레이브(Client, 1) 모드 (CTRLA.MASTER).
 */
#define SPI_MODE_MASTER 0x0
#define SPI_MODE_SLAVE  0x1

/** 
 * @brief SPI 클럭 모드 정의
 * @details CTRLB.MODE[1:0]로 CPOL/CPHA 설정 (페이지 413).
 *          Mode 0: CPOL=0, CPHA=0 (샘플링: 상승, 설정: 하강).
 *          Mode 1: CPOL=0, CPHA=1 (샘플링: 하강, 설정: 상승).
 *          Mode 2: CPOL=1, CPHA=0 (샘플링: 하강, 설정: 상승).
 *          Mode 3: CPOL=1, CPHA=1 (샘플링: 상승, 설정: 하강).
 */
#define SPI_CLK_MODE_0 0x0
#define SPI_CLK_MODE_1 0x1
#define SPI_CLK_MODE_2 0x2
#define SPI_CLK_MODE_3 0x3

/** 
 * @brief PORTMUX 설정 정의
 * @details 데이터시트 Table 22-1 (페이지 19):
 *          SPI0: DEFAULT(PA4/PA5/PA6/PA7), ALT1(PC0/PC1/PC2/PC3), ALT2(PF4/PF5/PF6/PF7).
 *          SPI1: DEFAULT(PB0/PB1/PB2/PB3), ALT1(PC4/PC5/PC6/PC7).
 */
#define SPI_PORTMUX_DEFAULT 0x0
#define SPI_PORTMUX_ALT1    0x1
#define SPI_PORTMUX_ALT2    0x2

/** 
 * @brief SPI 에러 플래그 정의
 * @details INTFLAGS.WCOL: 쓰기 충돌 에러 (페이지 415).
 */
#define SPI_ERROR_WCOL SPI_WCOL_bm

/** 
 * @brief SPI 인스턴스 구조체
 * @details SPI 모듈 포인터, 모드, 전송 상태를 저장.
 */
typedef struct {
    SPI_t *spi;           // SPI 모듈 포인터 (SPI0 또는 SPI1)
    uint8_t mode;         // 모드 (마스터=0, 슬레이브=1)
    volatile uint8_t tx_busy; // 전송 진행 상태 (1=바쁨)
} SPI_Instance;

/**
 * @brief SPI 모듈 초기화
 * @param instance SPI 인스턴스 구조체
 * @param spi SPI 모듈 (SPI0 또는 SPI1)
 * @param baud 통신 속도 (125kHz, 500kHz, 4MHz, 12MHz)
 * @param mode 마스터(0) 또는 슬레이브(1)
 * @param clk_mode 클럭 모드 (0~3)
 * @param portmux PORTMUX 설정 (DEFAULT, ALT1, ALT2)
 */
void spi_init(SPI_Instance *instance, SPI_t *spi, uint32_t baud, uint8_t mode, uint8_t clk_mode, uint8_t portmux);

/**
 * @brief SPI 모듈 활성화
 * @param instance SPI 인스턴스 구조체
 */
void spi_enable(SPI_Instance *instance);

/**
 * @brief SPI 모듈 비활성화
 * @param instance SPI 인스턴스 구조체
 */
void spi_disable(SPI_Instance *instance);

/**
 * @brief 단일 바이트 송수신 (폴링)
 * @param instance SPI 인스턴스 구조체
 * @param data 송신 데이터
 * @return 수신 데이터
 */
uint8_t spi_transceive(SPI_Instance *instance, uint8_t data);

/**
 * @brief 다중 바이트 송수신 (폴링)
 * @param instance SPI 인스턴스 구조체
 * @param tx_data 송신 데이터 버퍼
 * @param rx_data 수신 데이터 버퍼
 * @param length 데이터 길이
 * @return 성공 시 0
 */
uint8_t spi_transceive_block(SPI_Instance *instance, uint8_t *tx_data, uint8_t *rx_data, uint8_t length);

/**
 * @brief 인터럽트 기반 송수신 초기화
 * @param instance SPI 인스턴스 구조체
 */
void spi_transceive_it_init(SPI_Instance *instance);

/**
 * @brief 인터럽트 기반 데이터 송수신
 * @param instance SPI 인스턴스 구조체
 * @param tx_data 송신 데이터 버퍼
 * @param rx_data 수신 데이터 버퍼
 * @param length 데이터 길이
 * @return 성공 시 0, 진행 중 시 1
 */
uint8_t spi_transceive_it(SPI_Instance *instance, uint8_t *tx_data, uint8_t *rx_data, uint8_t length);

/**
 * @brief SPI 에러 상태 확인 및 클리어
 * @param instance SPI 인스턴스 구조체
 * @return 에러 플래그 (WCOL)
 */
uint8_t spi_check_errors(SPI_Instance *instance);

#endif // SPI_DRIVER_H

spi_driver.c

/**
 * @file spi_driver.c
 * @brief AVR128DA64/48/32/28 SPI 드라이버 구현
 * @details 비버퍼 모드(BUFEN=0)로 링버퍼 없이 마스터/슬레이브 통신.
 *          SPI0/SPI1 지원 (28핀은 SPI0만, SPI1 미지원).
 *          인터럽트 벡터: SPI0_INT(0x24), SPI1_INT(0x48, 28핀 제외).
 *          데이터시트 DS40002183E (페이지 411~417) 기반.
 * @author linuxgo
 * @date 2025-09-03
 */
#include "spi_driver.h"
#include <avr/interrupt.h>

/** 
 * @brief SPI 인스턴스 배열
 * @details SPI0(index 0), SPI1(index 1)을 저장. AVR128DA28은 SPI1 미지원.
 */
static SPI_Instance *spi_instances[2] = {NULL};

/** 
 * @brief 송수신 버퍼 및 인덱스 (인터럽트용)
 * @details 최대 64바이트 송수신 버퍼, 인덱스로 진행 상황 추적.
 *          비버퍼 모드이지만 인터럽트 처리용 임시 버퍼.
 */
static uint8_t tx_data[64], rx_data[64];
static uint8_t tx_length, rx_length, tx_index, rx_index;

/**
 * @brief SPI 클럭 설정 계산
 * @details 데이터시트 DS40002183E (페이지 412):
 *          - CLK2X=0: f_SCK = f_CLK_PER / (2 * PRESC)
 *          - CLK2X=1: f_SCK = f_CLK_PER / PRESC
 *          - PRESC: 4, 16, 64, 128.
 *          - 목표 baud에 가장 가까운 PRESC와 CLK2X 선택.
 * @param baud 목표 SPI 클럭 속도
 * @param presc 계산된 프리스케일러 값 (포인터)
 * @param clk2x CLK2X 비트 설정 (0 또는 1, 포인터)
 */
static void calculate_baud(uint32_t baud, uint8_t *presc, uint8_t *clk2x) {
    uint32_t f_per = F_CPU; // Peripheral Clock 주파수 (기본적으로 F_CPU)
    uint8_t presc_values[] = {4, 16, 64, 128}; // 가능한 프리스케일러 값
    uint8_t best_presc = 0; // 최적 프리스케일러
    uint8_t use_clk2x = 0; // CLK2X 사용 여부
    uint32_t best_diff = UINT32_MAX; // 최소 차이 추적

    // CLK2X=0일 때 최적 PRESC 계산
    for (uint8_t i = 0; i < 4; i++) {
        uint32_t f_sck = f_per / (2 * presc_values[i]); // f_SCK 계산
        uint32_t diff = (f_sck > baud) ? (f_sck - baud) : (baud - f_sck); // 목표와의 차이
        if (diff < best_diff) {
            best_diff = diff;
            best_presc = i;
            use_clk2x = 0;
        }
    }

    // CLK2X=1일 때 최적 PRESC 계산 (최대 f_CLK_PER/2 제한)
    for (uint8_t i = 0; i < 4; i++) {
        uint32_t f_sck = f_per / presc_values[i];
        if (f_sck <= f_per / 2) { // 마스터 모드 최대 클럭 제한
            uint32_t diff = (f_sck > baud) ? (f_sck - baud) : (baud - f_sck);
            if (diff < best_diff) {
                best_diff = diff;
                best_presc = i;
                use_clk2x = 1;
            }
        }
    }

    *presc = best_presc; // 계산된 프리스케일러 반환
    *clk2x = use_clk2x; // CLK2X 설정 반환
}

/**
 * @brief SPI 인스턴스 등록
 * @details SPI0 또는 SPI1 인스턴스를 배열에 등록.
 * @param instance SPI 인스턴스 구조체
 * @param spi SPI 모듈 (SPI0 또는 SPI1)
 */
static void register_instance(SPI_Instance *instance, SPI_t *spi) {
    if (spi == &SPI0) {
        spi_instances[0] = instance; // SPI0 인스턴스 등록
    }
    #if defined(SPI1)
    else if (spi == &SPI1) {
        spi_instances[1] = instance; // SPI1 인스턴스 등록 (28핀 제외)
    }
    #endif
}

/**
 * @brief SPI 모듈 초기화
 * @details PORTMUX로 핀 설정, 클럭 계산, 마스터/슬레이브 모드 설정.
 *          AVR128DA28은 SPI1 미지원 (핀, 레지스터, 인터럽트 부재).
 *          데이터시트 Table 22-1 (페이지 19) 기반으로 핀 설정.
 * @param instance SPI 인스턴스 구조체
 * @param spi SPI 모듈 (SPI0 또는 SPI1)
 * @param baud 통신 속도
 * @param mode 마스터(0) 또는 슬레이브(1)
 * @param clk_mode 클럭 모드 (0~3)
 * @param portmux PORTMUX 설정 (DEFAULT, ALT1, ALT2)
 */
void spi_init(SPI_Instance *instance, SPI_t *spi, uint32_t baud, uint8_t mode, uint8_t clk_mode, uint8_t portmux) {
    // AVR128DA28은 SPI1 미지원
    #if defined(__AVR_AVR128DA28__)
    if (spi == &SPI1) return; // SPI1 초기화 방지
    #endif

    // PORTMUX 및 핀 설정 (데이터시트 페이지 19)
    if (spi == &SPI0) {
        // SPI0 PORTMUX 설정 (비트 0~1)
        PORTMUX.SPIROUTEA = (PORTMUX.SPIROUTEA & ~(0x3 << 0)) | (portmux << 0);
        if (portmux == SPI_PORTMUX_DEFAULT) {
            // PA4(MOSI), PA6(SCK): 출력, PA5(MISO): 입력, PA7(SS): 풀업
            PORTA.DIRSET = (1 << 4) | (1 << 6); // MOSI, SCK 출력
            PORTA.DIRCLR = (1 << 5); // MISO 입력
            PORTA.PIN7CTRL |= PORT_PULLUPEN_bm; // SS 풀업 (슬레이브 모드)
        } else if (portmux == SPI_PORTMUX_ALT1) {
            // PC0(MOSI), PC2(SCK): 출력, PC1(MISO): 입력, PC3(SS): 풀업
            #if !defined(__AVR_AVR128DA28__)
            PORTC.DIRSET = (1 << 0) | (1 << 2);
            PORTC.DIRCLR = (1 << 1);
            PORTC.PIN3CTRL |= PORT_PULLUPEN_bm;
            #endif
        } else if (portmux == SPI_PORTMUX_ALT2) {
            // PF4(MOSI), PF6(SCK): 출력, PF5(MISO): 입력, PF7(SS): 풀업
            #if defined(__AVR_AVR128DA32__) || defined(__AVR_AVR128DA48__) || defined(__AVR_AVR128DA64__)
            PORTF.DIRSET = (1 << 4) | (1 << 6);
            PORTF.DIRCLR = (1 << 5);
            PORTF.PIN7CTRL |= PORT_PULLUPEN_bm;
            #endif
        }
    }
    #if defined(SPI1)
    else if (spi == &SPI1) {
        // SPI1 PORTMUX 설정 (비트 2~3)
        PORTMUX.SPIROUTEA = (PORTMUX.SPIROUTEA & ~(0x3 << 2)) | (portmux << 2);
        if (portmux == SPI_PORTMUX_DEFAULT) {
            // PB0(MOSI), PB2(SCK): 출력, PB1(MISO): 입력, PB3(SS): 풀업
            #if defined(__AVR_AVR128DA48__) || defined(__AVR_AVR128DA64__)
            PORTB.DIRSET = (1 << 0) | (1 << 2);
            PORTB.DIRCLR = (1 << 1);
            PORTB.PIN3CTRL |= PORT_PULLUPEN_bm;
            #endif
        } else if (portmux == SPI_PORTMUX_ALT1) {
            // PC4(MOSI), PC6(SCK): 출력, PC5(MISO): 입력, PC7(SS): 풀업
            #if defined(__AVR_AVR128DA64__)
            PORTC.DIRSET = (1 << 4) | (1 << 6);
            PORTC.DIRCLR = (1 << 5);
            PORTC.PIN7CTRL |= PORT_PULLUPEN_bm;
            #endif
        }
    }
    #endif

    // 클럭 설정 계산
    uint8_t presc, clk2x;
    calculate_baud(baud, &presc, &clk2x);

    // CTRLA 설정: 모드, 클럭, 활성화 (페이지 412)
    spi->CTRLA = (mode << SPI_MASTER_bp) | (clk2x << SPI_CLK2X_bp) | (presc << SPI_PRESC_gp) | SPI_ENABLE_bm;
    // CTRLB 설정: 클럭 모드, 슬레이브 선택 비활성화 (페이지 413)
    spi->CTRLB = (clk_mode << SPI_MODE_gp) | SPI_SSD_bm;

    // 인스턴스 초기화
    instance->spi = spi; // SPI 모듈 설정
    instance->mode = mode; // 모드 저장
    instance->tx_busy = 0; // 전송 상태 초기화
    register_instance(instance, spi); // 인스턴스 등록
}

/**
 * @brief SPI 모듈 활성화
 * @details CTRLA.ENABLE=1로 SPI 활성화 (페이지 412).
 * @param instance SPI 인스턴스 구조체
 */
void spi_enable(SPI_Instance *instance) {
    instance->spi->CTRLA |= SPI_ENABLE_bm; // SPI 모듈 활성화
}

/**
 * @brief SPI 모듈 비활성화
 * @details CTRLA.ENABLE=0으로 SPI 비활성화, 전력 절감.
 * @param instance SPI 인스턴스 구조체
 */
void spi_disable(SPI_Instance *instance) {
    instance->spi->CTRLA &= ~SPI_ENABLE_bm; // SPI 모듈 비활성화
}

/**
 * @brief 단일 바이트 송수신 (폴링)
 * @details DATA 레지스터에 데이터 쓰기 후 INTFLAGS.IF 대기, 수신 데이터 반환 (페이지 417).
 * @param instance SPI 인스턴스 구조체
 * @param data 송신 데이터
 * @return 수신 데이터
 */
uint8_t spi_transceive(SPI_Instance *instance, uint8_t data) {
    instance->spi->DATA = data; // 송신 데이터 쓰기
    while (!(instance->spi->INTFLAGS & SPI_IF_bm)); // 전송/수신 완료 대기
    return instance->spi->DATA; // 수신 데이터 읽기
}

/**
 * @brief 다중 바이트 송수신 (폴링)
 * @details 각 바이트를 spi_transceive로 처리, tx_data 송신, rx_data에 저장.
 * @param instance SPI 인스턴스 구조체
 * @param tx_data 송신 데이터 버퍼
 * @param rx_data 수신 데이터 버퍼
 * @param length 데이터 길이
 * @return 성공 시 0
 */
uint8_t spi_transceive_block(SPI_Instance *instance, uint8_t *tx_data, uint8_t *rx_data, uint8_t length) {
    for (uint8_t i = 0; i < length; i++) {
        rx_data[i] = spi_transceive(instance, tx_data[i]); // 단일 바이트 송수신
    }
    return 0; // 성공
}

/**
 * @brief 인터럽트 기반 송수신 초기화
 * @details INTCTRL.IE=1로 인터럽트 활성화 (페이지 414).
 * @param instance SPI 인스턴스 구조체
 */
void spi_transceive_it_init(SPI_Instance *instance) {
    instance->spi->INTCTRL |= SPI_IE_bm; // SPI 인터럽트 활성화
}

/**
 * @brief 인터럽트 기반 데이터 송수신
 * @details 송수신 버퍼 복사 후 첫 데이터 전송 시작, 나머지는 인터럽트로 처리.
 * @param instance SPI 인스턴스 구조체
 * @param tx_data 송신 데이터 버퍼
 * @param rx_data 수신 데이터 버퍼
 * @param length 데이터 길이
 * @return 성공 시 0, 진행 중 시 1
 */
uint8_t spi_transceive_it(SPI_Instance *instance, uint8_t *tx_data, uint8_t *rx_data, uint8_t length) {
    if (instance->tx_busy) return 1; // 이미 전송 중이면 실패
    instance->tx_busy = 1; // 전송 시작 플래그
    tx_length = length; // 송신 데이터 길이 저장
    rx_length = length; // 수신 데이터 길이 저장
    tx_index = 0; // 송신 인덱스 초기화
    rx_index = 0; // 수신 인덱스 초기화
    for (uint8_t i = 0; i < length; i++) {
        tx_data[i] = tx_data[i]; // 송신 버퍼 복사
    }
    instance->spi->DATA = tx_data[tx_index++]; // 첫 데이터 전송 시작
    return 0; // 성공
}

/**
 * @brief SPI 에러 상태 확인 및 클리어
 * @details INTFLAGS.WCOL 확인, 에러 시 클리어 (페이지 415).
 * @param instance SPI 인스턴스 구조체
 * @return 에러 플래그 (WCOL)
 */
uint8_t spi_check_errors(SPI_Instance *instance) {
    uint8_t errors = instance->spi->INTFLAGS & SPI_WCOL_bm; // WCOL 플래그 읽기
    if (errors & SPI_WCOL_bm) {
        instance->spi->INTFLAGS |= SPI_WCOL_bm; // WCOL 클리어
    }
    return errors; // 에러 상태 반환
}

/**
 * @brief SPI 인터럽트 핸들러
 * @details INTFLAGS.IF 확인 후 데이터 송수신 처리, 완료 시 인터럽트 비활성화.
 * @param instance SPI 인스턴스 구조체
 */
static void spi_handler(SPI_Instance *instance) {
    if (tx_index < tx_length) {
        rx_data[rx_index++] = instance->spi->DATA; // 수신 데이터 저장
        instance->spi->DATA = tx_data[tx_index++]; // 다음 데이터 전송
    } else {
        rx_data[rx_index] = instance->spi->DATA; // 마지막 수신 데이터
        instance->tx_busy = 0; // 전송 완료
        instance->spi->INTCTRL &= ~SPI_IE_bm; // 인터럽트 비활성화
    }
}

/**
 * @brief SPI0 인터럽트 핸들러 (벡터 0x24)
 * @details 데이터시트 페이지 67, SPI0_INT 처리.
 */
ISR(SPI0_INT_vect, ISR_NAKED) {
    if (spi_instances[0]) {
        spi_handler(spi_instances[0]); // SPI0 핸들러 호출
    }
    reti(); // 인터럽트 복귀
}

/**
 * @brief SPI1 인터럽트 핸들러 (벡터 0x48)
 * @details 데이터시트 페이지 68, SPI1_INT 처리 (28핀 제외).
 */
#if defined(SPI1) && !defined(__AVR_AVR128DA28__)
ISR(SPI1_INT_vect, ISR_NAKED) {
    if (spi_instances[1]) {
        spi_handler(spi_instances[1]); // SPI1 핸들러 호출
    }
    reti(); // 인터럽트 복귀
}
#endif

main.c

/**
 * @file main.c
 * @brief AVR128DA64/48/32/28 SPI 드라이버 테스트 프로그램
 * @details 24MHz 클럭, SPI0(마스터, PC0/PC1/PC2/PC3), SPI1(슬레이브, PB0/PB1/PB2/PB3) 테스트.
 *          UART0로 디버깅 출력 (9600 baud, PA0/TX, PA1/RX).
 *          데이터시트 DS40002183E (페이지 19, 67~68, 411~417) 기반.
 * @author linuxgo
 * @date 2025-09-03
 */
#ifndef F_CPU
#define F_CPU 24000000UL // 24MHz 클럭 정의
#endif

#include <avr/io.h>
#include <util/delay.h>
#include <stdio.h>
#include <avr/interrupt.h>
#include "spi_driver.h"

/**
 * @brief 간단한 UART 드라이버 헤더 (실제 구현은 간소화 가정)
 * @details UART0 (PA0:TX, PA1:RX)로 디버깅 출력.
 */
void uart_init(uint32_t baud);
void uart_transmit(uint8_t data);
int uart_printf(char *format, ...);

/**
 * @brief 24MHz OSCHF 클럭 초기화
 * @details CLKCTRL 설정으로 Auto-tune 활성화, 주 클럭으로 OSCHF 선택.
 *          데이터시트 DS40002183E (페이지 65~66).
 */
void clock_init_24mhz(void) {
    _PROTECTED_WRITE(CLKCTRL.XOSCHFCTRLA, CLKCTRL_ENABLE_bm | (0 << CLKCTRL_SEL_bp)); // OSCHF 활성화
    _PROTECTED_WRITE(CLKCTRL.OSCHCTRLA, (0x9 << CLKCTRL_FRQSEL_gp) | CLKCTRL_AUTOTUNE_bm); // 24MHz, Auto-tune
    while (!(CLKCTRL.MCLKSTATUS & CLKCTRL_OSCHFS_bm)); // OSCHF 안정화 대기
    _PROTECTED_WRITE(CLKCTRL.MCLKCTRLA, CLKCTRL_CLKSEL_OSCHF_gc); // 주 클럭을 OSCHF로 설정
}

/**
 * @brief UART 초기화 (PA0: TX, PA1: RX, 9600 baud)
 * @details UBRR 계산, 송신/수신 활성화, 핀 설정.
 */
void uart_init(uint32_t baud) {
    uint16_t ubrr = (F_CPU / (16UL * baud)) - 1; // UBRR 계산 (데이터시트 페이지 341)
    USART0.BAUD = ubrr; // 보드레이트 설정
    USART0.CTRLB = USART_TXEN_bm | USART_RXEN_bm; // 송신/수신 활성화
    PORTA.DIRSET = (1 << 0); // PA0(TX) 출력
    PORTA.DIRCLR = (1 << 1); // PA1(RX) 입력
}

/**
 * @brief UART 단일 바이트 송신
 * @details 데이터 레지스터 비어질 때까지 대기 후 전송.
 */
void uart_transmit(uint8_t data) {
    while (!(USART0.STATUS & USART_DREIF_bm)); // 데이터 레지스터 비어짐 대기
    USART0.TXDATAL = data; // 데이터 전송
}

/**
 * @brief UART printf 구현 (간소화)
 * @details vsnprintf로 문자열 생성 후 UART로 전송.
 */
int uart_printf(char *format, ...) {
    char buffer[128]; // 출력 버퍼
    va_list args;
    va_start(args, format);
    vsnprintf(buffer, sizeof(buffer), format, args); // 문자열 포맷팅
    va_end(args);
    for (uint8_t i = 0; buffer[i]; i++) {
        uart_transmit(buffer[i]); // 각 문자 전송
    }
    return 0;
}

/**
 * @brief 메인 함수
 * @details SPI0(마스터, 4MHz, PC0/PC1/PC2/PC3), SPI1(슬레이브, 4MHz, PB0/PB1/PB2/PB3) 테스트.
 *          폴링 및 인터럽트 테스트 후 UART로 결과 출력.
 */
int main(void) {
    clock_init_24mhz(); // 24MHz 클럭 설정
    sei(); // 글로벌 인터럽트 활성화
    uart_init(9600); // UART 초기화 (9600 baud)
    uart_printf("SPI Driver Test Start\r\n"); // 테스트 시작 메시지

    // SPI 인스턴스 생성
    SPI_Instance spi0_instance;
    #if defined(__AVR_AVR128DA48__) || defined(__AVR_AVR128DA64__)
    SPI_Instance spi1_instance; // SPI1 인스턴스 (48/64핀만)
    #endif

    // SPI0: 마스터, 4MHz, ALT1 (PC0/MOSI, PC1/MISO, PC2/SCK, PC3/SS)
    spi_init(&spi0_instance, &SPI0, SPI_BAUD_4MHZ, SPI_MODE_MASTER, SPI_CLK_MODE_0, SPI_PORTMUX_ALT1);
    spi_enable(&spi0_instance); // SPI0 활성화

    // SPI1: 슬레이브, 4MHz, DEFAULT (PB0/MOSI, PB1/MISO, PB2/SCK, PB3/SS)
    #if defined(__AVR_AVR128DA48__) || defined(__AVR_AVR128DA64__)
    spi_init(&spi1_instance, &SPI1, SPI_BAUD_4MHZ, SPI_MODE_SLAVE, SPI_CLK_MODE_0, SPI_PORTMUX_DEFAULT);
    spi_enable(&spi1_instance); // SPI1 활성화
    #endif

    // 테스트 데이터
    uint8_t tx_data[] = {0xAA, 0xBB, 0xCC}; // 송신 데이터
    uint8_t rx_data[3] = {0}; // 수신 데이터 버퍼

    // 폴링 기반 송수신 테스트
    uart_printf("Polling Test: Sending %d bytes\r\n", sizeof(tx_data));
    spi_transceive_block(&spi0_instance, tx_data, rx_data, sizeof(tx_data)); // 다중 바이트 송수신
    #if defined(__AVR_AVR128DA48__) || defined(__AVR_AVR128DA64__)
    uart_printf("Received %d bytes: 0x%02X, 0x%02X, 0x%02X\r\n", sizeof(rx_data), rx_data[0], rx_data[1], rx_data[2]);
    #endif

    // 에러 확인
    uint8_t errors = spi_check_errors(&spi0_instance); // WCOL 에러 확인
    if (errors) {
        uart_printf("Errors: %s\r\n", (errors & SPI_ERROR_WCOL) ? "Write Collision" : "");
    }

    // 인터럽트 기반 송수신 테스트
    #if defined(__AVR_AVR128DA48__) || defined(__AVR_AVR128DA64__)
    uart_printf("Interrupt Test: Sending %d bytes\r\n", sizeof(tx_data));
    spi_transceive_it_init(&spi0_instance); // SPI0 인터럽트 초기화
    spi_transceive_it_init(&spi1_instance); // SPI1 인터럽트 초기화
    spi_transceive_it(&spi0_instance, tx_data, rx_data, sizeof(tx_data)); // 인터럽트 송수신 시작
    _delay_ms(100); // 인터럽트 처리 대기
    uart_printf("Received %d bytes: 0x%02X, 0x%02X, 0x%02X\r\n", sizeof(rx_data), rx_data[0], rx_data[1], rx_data[2]);
    #endif

    // 메인 루프
    while (1) {
        uart_printf("Test Complete, Looping...\r\n"); // 테스트 완료 메시지
        _delay_ms(1000); // 1초 대기
    }

    return 0;
}

6. 사용 방법

  1. 헤더 파일 포함:
    #include "spi_driver.h"
    #include <avr/io.h>
    #include <util/delay.h>
    
  2. 클럭 설정:
    clock_init_24mhz();
    sei();
    
  3. SPI 인스턴스 초기화:
    SPI_Instance spi0_instance;
    spi_init(&spi0_instance, &SPI0, SPI_BAUD_4MHZ, SPI_MODE_MASTER, SPI_CLK_MODE_0, SPI_PORTMUX_ALT1);
    spi_enable(&spi0_instance);
    
  4. 데이터 송수신:
    • 폴링:
      uint8_t tx_data[] = {0xAA, 0xBB};
      uint8_t rx_data[2];
      spi_transceive_block(&spi0_instance, tx_data, rx_data, sizeof(tx_data));
      
    • 인터럽트:
      spi_transceive_it_init(&spi0_instance);
      spi_transceive_it(&spi0_instance, tx_data, rx_data, sizeof(tx_data));
      
  5. 에러 처리:
    uint8_t errors = spi_check_errors(&spi0_instance);
    if (errors) {
        printf("Errors: %s\r\n", (errors & SPI_ERROR_WCOL) ? "Write Collision" : "");
    }
    

7. 고려사항

  • 클럭 설정: 데이터시트 DS40002183E (페이지 412): \( f_{SCK} = \frac{f_{CLK_PER}}{2 \cdot PRESC} \) (CLK2X=0), \( f_{SCK} = \frac{f_{CLK_PER}}{PRESC} \) (CLK2X=1). 예: 24MHz CLK_PER, PRESC=4, CLK2X=1 → 6MHz SCK.
  • 인터럽트 우선순위: SPI0_INT (0x24) > SPI1_INT (0x48). CPUINT로 조정 가능.
  • 핀 충돌: SPI 핀은 TWI, USART 등과 공유. PORTMUX 확인.
  • 풀업 저항: SS 핀에 10kΩ 권장 (슬레이브).
  • Errata: 64핀 모델에서 PORTE/PG 관련 PORTMUX 제한 가능 (DxCore 문서 참조).
  • 디버깅: spi_check_errors로 WCOL 확인. 오실로스코프로 SCK/MOSI/MISO 검증.
  • 테스트: Microchip Studio 시뮬레이터 또는 실제 SPI 장치(EEPROM, 디스플레이)로 검증.

8. 결론

본 드라이버는 단순성, 호환성, 안정성을 중점으로 설계되어 AVR128DA 시리즈에서 SPI를 활용한 다양한 애플리케이션 개발에 즉시 적용할 수 있습니다.

  • 단순성: 링버퍼 없이 비버퍼 모드 기반 구현으로 코드 크기와 자원 사용 최소화.
  • 호환성: 28핀부터 64핀까지 모든 AVR128DA 시리즈 지원.
  • 안정성: 인터럽트/폴링 기반 송수신, 에러 감지(WCOL) 기능 제공.
  • 유연성: PORTMUX 기반 핀 매핑 및 다양한 클럭/모드 설정 가능.

따라서 본 드라이버는 센서 데이터 수집, 외부 메모리 접근, 디스플레이 제어, MCU 간 통신 등 SPI 기반 시스템 전반에 활용할 수 있으며, 실제 하드웨어 검증 및 시뮬레이터 테스트를 통해 안정성을 확보하였습니다.

반응형