본문 바로가기
MCU/STM32

[STM32H503RB] 로 I3C 프로토콜 구현: 초보자를 위한 상세 가이드

by linuxgo 2025. 8. 10.
반응형

소개

MIPI I3C(Improved Inter-Integrated Circuit)는 I2C의 후속 프로토콜로, 최대 12.5MHz의 데이터 전송 속도, 저전력 설계, 동적 주소 지정, 인-밴드 인터럽트(IBI)를 제공합니다. STM32H5 시리즈는 I3C 하드웨어 컨트롤러를 내장하여 센서, 메모리, 카메라 등과 효율적으로 통신할 수 있습니다. 이 가이드는 **STM32H5(NUCLEO-H503RB)**와 **LSM6DSO 센서(X-NUCLEO-IKS01A3)**를 사용해 I3C 통신을 구현하는 방법을 단계별로 설명합니다. 동적 주소 지정, 가속도 데이터 읽기, IBI 처리, UART 디버깅을 포함하며, STM32CubeMX와 STM32CubeIDE를 활용합니다. 모든 코드에는 이해를 돕도록 상세 주석을 추가하였습니다.

키워드: STM32H5 I3C, I3C 프로토콜, STM32 I3C 구현, LSM6DSO, 동적 주소 지정, I3C 가이드


준비물

하드웨어

  • NUCLEO-H503RB: STM32H503RB 마이크로컨트롤러(I3C1 지원).

  • X-NUCLEO-IKS01A3: LSM6DSO 가속도/자이로 센서 포함 쉴드.
    연결:
    • SCL: PB6 (CN5 pin 10, Arduino SCL/D15).
    • SDA: PB7 (CN5 pin 9, Arduino SDA/D14).
    • GND: CN5 pin 7 또는 CN10 pin 20.
    • 풀업 저항: 1.5kΩ~4.7kΩ (I3C pure bus에서는 생략 가능).
  • 디버깅: ST-LINK(NUCLEO 내장), USB-TTL 어댑터(UART용, 선택).

소프트웨어


STM32CubeMX 설정

STM32CubeMX를 사용하여 I3C와 UART를 설정합니다.

1. 프로젝트 생성

  1. STM32CubeIDE에서 File > New > STM32 Project.
  2. Board Selector에서 NUCLEO-H503RB 선택.
  3. 프로젝트 이름 입력(예: I3C_LSM6DSO), Finish.
  4. "Initialize all peripherals with their default mode?"에서 Yes.

2. I3C1 설정

  1. Pinout & Configuration > Connectivity > I3C1.
  2. Mode: Controller.
  3. 핀: PB6(SCL), PB7(SDA) 자동 할당.
  4. 파라미터:
    • Frequency I3C controller: 3000000 (3MHz, LSM6DSO 권장).
    • Bus Type: I3C pure bus.
    • BusFreeDuration: 200ns (기본값).
  5. NVIC Settings: I3C1 event and error interrupt 활성화.

3. UART 설정

  1. Connectivity > USART2.
  2. Mode: Asynchronous.
  3. Baud Rate: 115200.
  4. 핀: PA2(TX), PA3(RX).
  5. NVIC: USART2 global interrupt 활성화.

4. 클럭 설정

  1. Clock Configuration:
    • HSI: 64MHz.
    • PLL1: PLLM = 4, PLLN = 31, PLLP = 2 → SYSCLK = 250MHz.
    • HCLK: 250MHz.
    • APB1 Prescaler: 1 (PCLK1 = 250MHz).
    • I3C1 Clock Mux: PCLK.

5. 코드 생성

  • Project Manager > Code Generator: Generate peripheral initialization as a pair of '.c/.h' files per peripheral 체크.
  • Generate Code 클릭.

하드웨어 연결

  1. NUCLEO-H503RB와 X-NUCLEO-IKS01A3 연결:
    • SCL(PB6) → Arduino SCL/D15 (CN5 pin 10).
    • SDA(PB7) → Arduino SDA/D14 (CN5 pin 9).
    • GND → CN5 pin 7 또는 CN10 pin 20.
  2. 풀업 저항: I3C pure bus에서는 생략 가능. I2C 공존 시 1.5kΩ 권장.
  3. UART 디버깅: PA2(TX), PA3(RX)를 USB-TTL 어댑터에 연결.
  4. 권장사항: 신호 무결성을 위해 와이어 길이 10cm 이하, GND 꼬임.

코드 구현

아래는 LSM6DSO 센서와 I3C 통신을 구현하는 코드이며,동작을 명확히 설명하기 위해서 각 줄에 상세 주석을 추가하였습니다.

1. 타겟 디스크립터 (target.h)

타겟(LSM6DSO)의 정보를 정의합니다.

#ifndef __STM32_I3C_DESC_TARGET1_H
#define __STM32_I3C_DESC_TARGET1_H

#include "stm32h5xx_hal.h" // STM32H5 HAL 라이브러리 포함

// 타겟 식별을 위한 상수 정의
#define DEVICE_ID1 0U // 첫 번째 타겟의 ID
#define TARGET1_DYN_ADDR 0x32 // LSM6DSO에 할당할 동적 주소

// 타겟 정보를 저장하는 구조체
typedef struct {
    char *TARGET_NAME;          // 타겟의 마케팅 이름 (예: "LSM6DSO")
    uint32_t TARGET_ID;         // 버스 상의 타겟 식별자
    uint64_t TARGET_BCR_DCR_PID;// PID, BCR, DCR의 결합 값
    uint8_t STATIC_ADDR;        // 정적 주소 (I2C 호환 시 사용, 기본 0)
    uint8_t DYNAMIC_ADDR;       // 동적 주소
} TargetDesc_TypeDef;

// LSM6DSO 타겟 정보 초기화
TargetDesc_TypeDef TargetDesc1 = {
    "LSM6DSO",              // 타겟 이름
    DEVICE_ID1,             // 타겟 ID
    0x0000000000000000,     // 초기 PID/BCR/DCR (DAA 후 업데이트)
    0x00,                   // 정적 주소 없음
    TARGET1_DYN_ADDR        // 동적 주소
};

#endif /* __STM32_I3C_DESC_TARGET1_H */

주석 설명:

  • #include "stm32h5xx_hal.h": I3C 및 기타 HAL 함수 사용을 위한 헤더.
  • TargetDesc_TypeDef: 타겟의 메타데이터를 구조체로 정의.
  • TargetDesc1: LSM6DSO 센서의 초기 정보. PID는 동적 주소 지정(DAA) 후 업데이트.

2. 메인 코드 (main.c)

I3C 초기화, 동적 주소 지정, 데이터 송수신, IBI 처리를 구현합니다.

#include "main.h"           // STM32CubeMX 생성 헤더
#include "target.h"         // 타겟 디스크립터 헤더
#include <stdio.h>          // printf를 위한 표준 입출력 헤더

// 전역 변수
I3C_HandleTypeDef hi3c1;    // I3C1 핸들
UART_HandleTypeDef huart2;  // UART2 핸들
TargetDesc_TypeDef *aTargetDesc[1] = {&TargetDesc1}; // 타겟 디스크립터 배열
uint8_t aTxBuffer[4] = {0x20, 0x00, 0x10, 0x00}; // LSM6DSO CTRL1_XL 설정 (가속도 활성화)
uint8_t aRxBuffer[14];      // CCC 수신 버퍼
uint8_t aAccelData[6];      // 가속도 데이터 (X, Y, Z)

// 함수 선언
void SystemClock_Config(void);      // 시스템 클럭 설정
static void MX_I3C1_Init(void);     // I3C1 초기화
static void MX_USART2_UART_Init(void); // UART2 초기화
void Error_Handler(void);           // 에러 처리 함수

int main(void) {
    HAL_Init(); // HAL 라이브러리 초기화
    SystemClock_Config(); // 시스템 클럭 설정 (250MHz)
    MX_I3C1_Init(); // I3C1 초기화
    MX_USART2_UART_Init(); // UART2 초기화

    // 동적 주소 할당 시작
    printf("Starting DAA...\r\n");
    // ENTDAA CCC를 전송하여 타겟에 동적 주소 할당 (인터럽트 모드)
    if (HAL_I3C_Ctrl_DynAddrAssign_IT(&hi3c1, I3C_RSTDAA_THEN_ENTDAA) != HAL_OK) {
        Error_Handler();
    }
    // DAA 완료까지 대기
    while (HAL_I3C_GetState(&hi3c1) != HAL_I3C_STATE_READY) {
        /* 대기 */
    }

    // LSM6DSO 설정: 가속도 활성화 (CTRL1_XL 레지스터)
    I3C_XferTypeDef aContextBuffers[1]; // 전송 프레임 구조체
    aContextBuffers[0].CtrlBuf.pBuffer = aTxBuffer; // 전송 버퍼 설정
    aContextBuffers[0].CtrlBuf.Size = sizeof(aTxBuffer); // 버퍼 크기
    aContextBuffers[0].TxBuf.pBuffer = NULL; // 송신 데이터 없음
    aContextBuffers[0].TxBuf.Size = 0; // 송신 데이터 크기 0
    // 프레임에 전송 설명 추가 (Private 전송, Repeated START)
    if (HAL_I3C_AddDescToFrame(&hi3c1, NULL, NULL, &aContextBuffers[0], 0, I3C_PRIVATE_WITHOUT_DEFBYTE_RESTART) != HAL_OK) {
        Error_Handler();
    }
    // 타겟(0x32)으로 데이터 전송
    if (HAL_I3C_Ctrl_Transmit_IT(&hi3c1, TARGET1_DYN_ADDR, &aContextBuffers[0]) != HAL_OK) {
        Error_Handler();
    }
    // 전송 완료까지 대기
    while (HAL_I3C_GetState(&hi3c1) != HAL_I3C_STATE_READY) {
        /* 대기 */
    }

    // IBI 활성화
    if (HAL_I3C_ActivateNotification(&hi3c1, I3C_NOTIFICATION_IBI) != HAL_OK) {
        Error_Handler();
    }

    // 주기적 데이터 읽기 (100ms 간격)
    while (1) {
        aTxBuffer[0] = 0x28; // OUTX_L_G 레지스터 (가속도 데이터 시작)
        aContextBuffers[0].CtrlBuf.pBuffer = aTxBuffer; // 레지스터 주소 설정
        aContextBuffers[0].CtrlBuf.Size = 1; // 레지스터 주소 1바이트
        aContextBuffers[0].RxBuf.pBuffer = aAccelData; // 수신 버퍼
        aContextBuffers[0].RxBuf.Size = sizeof(aAccelData); // 6바이트 (X, Y, Z)
        // 프레임에 수신 설명 추가
        if (HAL_I3C_AddDescToFrame(&hi3c1, NULL, NULL, &aContextBuffers[0], 0, I3C_PRIVATE_WITHOUT_DEFBYTE_RESTART) != HAL_OK) {
            Error_Handler();
        }
        // 타겟으로부터 데이터 수신
        if (HAL_I3C_Ctrl_Receive_IT(&hi3c1, TARGET1_DYN_ADDR, &aContextBuffers[0]) != HAL_OK) {
            Error_Handler();
        }
        HAL_Delay(100); // 100ms 대기
    }
}

// 동적 주소 요청 콜백
void HAL_I3C_TgtReqDynamicAddrCallback(I3C_HandleTypeDef *hi3c, uint64_t targetPayload) {
    TargetDesc1.TARGET_BCR_DCR_PID = targetPayload; // 타겟의 PID/BCR/DCR 저장
    HAL_I3C_Ctrl_SetDynAddr(hi3c, TargetDesc1.DYNAMIC_ADDR); // 동적 주소 설정
    printf("DAA Complete: Target PID = 0x%llX, Assigned Address = 0x%02X\r\n", 
           targetPayload, TargetDesc1.DYNAMIC_ADDR); // DAA 결과 출력
}

// 전송 완료 콜백
void HAL_I3C_CtrlTxCpltCallback(I3C_HandleTypeDef *hi3c) {
    printf("Transmission Complete\r\n"); // 전송 완료 메시지
}

// 수신 완료 콜백
void HAL_I3C_CtrlRxCpltCallback(I3C_HandleTypeDef *hi3c) {
    // 가속도 데이터 변환 (16비트, 리틀 엔디안)
    printf("Acceleration Data: X=%d, Y=%d, Z=%d\r\n",
           (int16_t)(aAccelData[1] << 8 | aAccelData[0]), // X축
           (int16_t)(aAccelData[3] << 8 | aAccelData[2]), // Y축
           (int16_t)(aAccelData[5] << 8 | aAccelData[4])); // Z축
}

// IBI 콜백
void HAL_I3C_NotifyCallback(I3C_HandleTypeDef *hi3c, uint32_t event) {
    I3C_CCCInfoTypeDef cccInfo; // CCC 정보 구조체
    if (HAL_I3C_GetCCCInfo(hi3c, &cccInfo) == HAL_OK) {
        printf("IBI from Target Address: 0x%02X, Payload: 0x%02X\r\n", 
               cccInfo.IBICRTgtAddr, cccInfo.IBITgtPayload); // IBI 정보 출력
    }
}

// 에러 콜백
void HAL_I3C_ErrorCallback(I3C_HandleTypeDef *hi3c) {
    printf("I3C Error: 0x%08lX\r\n", HAL_I3C_GetError(hi3c)); // 에러 코드 출력
    Error_Handler();
}

// 시스템 클럭 설정 (250MHz)
void SystemClock_Config(void) {
    RCC_OscInitTypeDef RCC_OscInitStruct = {0}; // 오실레이터 구조체
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; // 클럭 구조체

    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI; // HSI 사용
    RCC_OscInitStruct.HSIState = RCC_HSI_ON; // HSI 활성화
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; // PLL 활성화
    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI; // PLL 소스 HSI
    RCC_OscInitStruct.PLL.PLLM = 4; // PLL 분주기
    RCC_OscInitStruct.PLL.PLLN = 31; // PLL 곱셈기
    RCC_OscInitStruct.PLL.PLLP = 2; // PLL 출력 분주기
    RCC_OscInitStruct.PLL.PLLQ = 2; // PLL Q 출력
    RCC_OscInitStruct.PLL.PLLR = 2; // PLL R 출력
    if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
        Error_Handler();
    }

    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1;
    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // SYSCLK 소스 PLL
    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB 클럭 분주기
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; // APB1 클럭 분주기
    if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK) {
        Error_Handler();
    }
}

// I3C1 초기화
static void MX_I3C1_Init(void) {
    hi3c1.Instance = I3C1; // I3C1 인스턴스
    hi3c1.Init.ClockSpeed = 3000000; // 3MHz (LSM6DSO 권장)
    hi3c1.Init.BusType = HAL_I3C_PURE_I3C_BUS; // I3C 전용 버스
    if (HAL_I3C_Init(&hi3c1) != HAL_OK) {
        Error_Handler();
    }
}

// UART2 초기화
static void MX_USART2_UART_Init(void) {
    huart2.Instance = USART2; // USART2 인스턴스
    huart2.Init.BaudRate = 115200; // 보드레이트
    huart2.Init.WordLength = UART_WORDLENGTH_8B; // 8비트 데이터
    huart2.Init.StopBits = UART_STOPBITS_1; // 1 스톱 비트
    huart2.Init.Parity = UART_PARITY_NONE; // 패리티 없음
    huart2.Init.Mode = UART_MODE_TX_RX; // 송수신 모드
    huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE; // 하드웨어 흐름 제어 없음
    if (HAL_UART_Init(&huart2) != HAL_OK) {
        Error_Handler();
    }
}

// 에러 처리
void Error_Handler(void) {
    while (1) {
        HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // LED2 토글 (에러 표시)
        HAL_Delay(500); // 500ms 대기
    }
}

// printf를 UART로 리다이렉션
int _write(int file, char *ptr, int len) {
    HAL_UART_Transmit(&huart2, (uint8_t *)ptr, len, HAL_MAX_DELAY); // UART 전송
    return len;
}
  • 전역 변수: I3C/UART 핸들, 타겟 디스크립터, 송수신 버퍼 정의.
  • main 함수:
    • 초기화 후 DAA 수행, LSM6DSO의 가속도 활성화, 주기적 데이터 읽기.
    • HAL_I3C_Ctrl_DynAddrAssign_IT: 동적 주소 할당.
    • HAL_I3C_Ctrl_Transmit_IT: CTRL1_XL 설정.
    • HAL_I3C_Ctrl_Receive_IT: 가속도 데이터 읽기.
  • 콜백 함수:
    • DAA, 전송/수신 완료, IBI, 에러 처리.
    • UART로 디버깅 정보 출력.
  • 초기화 함수: 클럭, I3C, UART 설정.
  • 에러 처리: LED2 토글로 시각적 피드백.

빌드 및 디버깅

1. 빌드

  • STM32CubeIDE에서 Build Project.
  • ST-LINK로 NUCLEO-H503RB에 펌웨어 업로드.

2. 디버깅

  • 터미널 설정:
    • Tera Term: 115200 보드, COM 포트 연결.
    • STM32CubeIDE: Window > Show View > Console, monitor arm semihosting enable.
  • 예상 출력:
    Starting DAA...
    DAA Complete: Target PID = 0xXXXXXXXXXXXX, Assigned Address = 0x32
    Transmission Complete
    Acceleration Data: X=1234, Y=5678, Z=9012
    IBI from Target Address: 0x32, Payload: 0xXX
    
  • LED 상태:
    • LED2(PA5) 느린 토글: 에러.
    • LED2 점등: 전송 완료.

3. 오류 디버깅

  • 에러 확인: HAL_I3C_ErrorCallback에서 에러 코드 출력.
  • 로직 분석기: Saleae Logic Analyzer로 SCL/SDA 신호 점검.
  • 타이밍: 3MHz 이상 사용 시 와이어 길이와 풀업 저항 확인.

주요 고려사항

  • 타이밍: LSM6DSO는 3MHz 이하에서 안정. 12.5MHz 사용 시 신호 무결성 점검.
  • IBI 설정: LSM6DSO의 CTRL3_C 레지스터로 IBI 활성화.
  • 전력: VDDIO2 핀(1.08V~1.2V) 사용 시 전원 안정화.
  • 에러 처리: 타임아웃, CRC 오류는 HAL_I3C_GetError로 확인.

결론

이 가이드는 STM32H5로 I3C 프로토콜을 구현하는 완전한 과정을 다뤘습니다. STM32CubeMX로 하드웨어를 설정하고, HAL 라이브러리로 동적 주소 지정, 데이터 송수신, IBI를 처리하였으며, 상세한 주석 처리는 초보자도 쉽게 따라 할 수 있도록 상세하게 기입하였습니다.

반응형