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 가이드
Project Manager > Code Generator: Generate peripheral initialization as a pair of '.c/.h' files per peripheral 체크.
Generate Code 클릭.
하드웨어 연결
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.
풀업 저항: I3C pure bus에서는 생략 가능. I2C 공존 시 1.5kΩ 권장.
UART 디버깅: PA2(TX), PA3(RX)를 USB-TTL 어댑터에 연결.
권장사항: 신호 무결성을 위해 와이어 길이 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.
타이밍: LSM6DSO는 3MHz 이하에서 안정. 12.5MHz 사용 시 신호 무결성 점검.
IBI 설정: LSM6DSO의 CTRL3_C 레지스터로 IBI 활성화.
전력: VDDIO2 핀(1.08V~1.2V) 사용 시 전원 안정화.
에러 처리: 타임아웃, CRC 오류는 HAL_I3C_GetError로 확인.
결론
이 가이드는 STM32H5로 I3C 프로토콜을 구현하는 완전한 과정을 다뤘습니다. STM32CubeMX로 하드웨어를 설정하고, HAL 라이브러리로 동적 주소 지정, 데이터 송수신, IBI를 처리하였으며, 상세한 주석 처리는 초보자도 쉽게 따라 할 수 있도록 상세하게 기입하였습니다.