I2C SLAVE MODE
테스트 환경
- Ubuntu 22.04
- TDA3XEVM
- PROCESSOR_SDK_VISION_03_08_00_00
TI의 TDA3 플랫폼(특히 IPU1_1, IPU1_0 코어)에서는 표준 TI-RTOS(BIOS)를 통해 I²C 드라이버를 제공하지만, 대부분 예제는 “마스터(Master)” 모드 위주로 되어 있습니다. 본 글은 TDA3에서 “I²C 슬레이브(Slave)”로 동작하도록 설정하는 방법부터, 실제 데이터 송수신을 설명하는 글입니다.
I²C 슬레이브 모드란?
일반적으로 I²C 통신은 마스터(Master)가 버스를 제어하고, 슬레이브(Slave)는 마스터의 요청에 응답하는 구조입니다.
- 마스터 모드: 클럭 신호(SCL)를 생성하고, 원하는 슬레이브 주소에 읽기/쓰기를 요청
- 슬레이브 모드: 자신에게 할당된 주소에 마스터가 읽기(Read)나 쓰기(Write) 요청을 했을 때, 해당 데이터를 보내거나 받아서 내부 로직을 수행
TDA3 IPU 같은 SoC에서는, IPU 코어가 카메라·센서 데이터 처리만 하는 게 아니라, 다른 프로세서(예: A15) 또는 외부 MCU가 “IPU 위의 어떤 레지스터”를 읽거나 쓰도록 I²C 슬레이브로 동작시킬 수 있습니다. 예를 들어:
- 외부 MCU가 IPU 메모리 맵에 명령을 쓰면, IPU가 슬레이브로서 해당 커맨드를 인식하고 내부 로직을 수행
- IPU에서 센서 데이터나 상태를 외부에 전송하고 싶을 때, 외부 MCU가 슬레이브에서 읽기 요청을 하면 데이터를 응답
이처럼 IPU를 I²C 슬레이브로 동작시키면, IPU-외부 통신 인터페이스가 확장되어 응용 범위가 넓어집니다.
TDA3 하드웨어 구조 및 I²C 컨트롤러 개요
위 구조는 기본 각 I²C 설정 블록이다. 하지만 master mode 기준으로 설정되어 있다. 블록은 다음과 같은 주요 레지스터를 포함합니다.
- I2C_CON: 모드(Master/Slave), 트랜스미터/리시버 모드, 7/10비트 주소, I²C_EN 등 제어 비트
- I2C_OA: (Own Address): 슬레이브 모드 시 장치 주소 설정
- I2C_SA (Slave Address): 마스터 모드 시 슬레이브 주소 레지스터
- I2C_IRQSTATUS / IRQENABLE: 인터럽트 플래그 및 허용 비트(RRDY, XRDY, ARDY, RDR, OVR 등)
- I2C_DATA: 읽기/쓰기 시 실제 데이터가 오가는 레지스터
- I2C_SYSC / I2C_PSC / I2C_SCLL / I2C_SCLH: 클럭 프리스케일, 버스 속도 등 설정 레지스터
- I2C_FIFOCTL: FIFO 트리거(Threshold) 및 플러시(Flush) 설정
슬레이브 모드에서는 I2C_CON.MST=0, I2C_CON.TRX=0(Rx) 로 설정하여, CPU가 마스터의 읽기/쓰기 요청에만 반응하도록 만듭니다.
그러나 슬레이브 모드 예제는 거의 없거나 최소 수준입니다. TI BIOS 드라이버가 제공하지 않는 디테일을 다루려면 직접 레지스터 값을 세팅하고, 인터럽트를 폴링하여 처리하는 코드가 반드시 필요합니다.
I²C 슬레이브 초기화 순서
TDA3 <ti/csl/src/ip/i2c/V2/i2c.h> 경로 파일을 열어보면 레지스터 값을 세팅하는 API가 구현되어 있다. 아래는 API를 참고하여 만든 간단한 SlaveMode 코드이다.
static void i2cModuleReset(uintptr_t base)
{
I2CSoftReset(base);
I2CSyscInit(base, I2C_SYSC_AUTOIDLE_ENABLE);
I2CFlushFifo(base);
}
Int32 Bsp_deviceI2cSlaveMode(UInt32 instId, UInt8 slaveAddr)
{
uintptr_t base;
if (instId >= NUM_I2C) return BSP_EFAIL;
base = gI2cBaseAddr[instId];
/* 1) 소프트 리셋 후 재설정 */
i2cModuleReset(base);
/* 2) 모듈 Disable (슬레이브 모드 진입 준비) */
I2CMasterDisable(base);
/* 3) Own Address (슬레이브 주소) 설정 */
I2COwnAddressSet(base, slaveAddr, I2C_OWN_ADDR_0);
/* 4) 슬레이브 모드: MST=0(슬레이브), TRX=0(리시버), 7비트 주소, I2C_EN=1 */
I2CConfig(base,
(0 << I2C_CON_MST_SHIFT) | /* slave mode */
(0 << I2C_CON_TRX_SHIFT) | /* receiver */
(0 << I2C_CON_XSA_SHIFT) | /* 7-bit address */
(1 << I2C_CON_I2C_EN_SHIFT) /* enable */
);
/* 5) FIFO Threshold 설정 (TX/RX) */
I2CFIFOThresholdConfig(base, 0, I2C_TX_MODE);
I2CFIFOThresholdConfig(base, 0, I2C_RX_MODE);
/* 6) 슬레이브 전용 인터럽트 클리어 */
I2CSlaveIntClearEx(base, I2C_INT_ALL);
I2CSlaveIntRawStatusClearEx(base,
I2C_IRQSTATUS_ARDY_MASK |
I2C_IRQSTATUS_XRDY_MASK |
I2C_IRQSTATUS_RRDY_MASK |
I2C_IRQSTATUS_RDR_MASK
);
I2CSlaveIntClearEx(base,
I2C_INT_ADRR_READY_ACESS |
I2C_INT_TRANSMIT_READY |
I2C_INT_RECV_READY |
I2C_INT_RECV_DRAIN
);
/* 7) 모듈 Enable (이제 슬레이브로 동작) */
I2CMasterEnable(base);
System_printf("I2C%u: Slave 모드 완료, 주소=0x%02X\n", instId, slaveAddr);
return BSP_SOK;
}
슬레이브 모드로 진입하기 전에 반드시 모듈을 비활성화한 후 설정을 변경, 이 후 활성화 과정을 거쳐야 정상적으로 동작합니다. 그렇지 않으면 설정이 반영되지 않을 수 있습니다.
슬레이브 이벤트 처리—RRDY/XRDY/ARDY 등
I²C Slave Mode에는 Polling, Interrupt 두가지가 있지만, 본 글은 Polling 기준으로 설명한다.
슬레이브 모드 인터럽트 주요 플래그를 간단히 정리하면:
인터럽트 | 의미 | 설명 |
RRDY | Receive Ready | 호스트가 “Write”로 데이터를 보냈을 때, 버퍼에 데이터가 준비됨 |
XRDY | Transmit Ready | 호스트가 “Read”요청을 보냈을 때, 슬레이브가 데이터를 준비해야 함 |
ARDY | Address Ready (Quick-Write 완료) | 호스트가 “주소+값”만 짧게 쓰고 완료했을 때 |
RDR | Receive Drain | Burst Read 완료 후 FIFO가 비워졌을 때 |
대부분 슬레이브는 RRDY 와 XRDY를 중심으로 코드를 작성합니다.
Int32 Bsp_deviceI2cSlavePollHandler(UInt32 instId)
{
uintptr_t base;
uint32_t raw;
// 트랜잭션 상태 저장용 (함수 안에서만 유지)
static bool gotOffset = false;
static uint8_t regOffset = 0;
static uint8_t regs[256]; // 0x00~0xFF 레지스터
if (instId >= NUM_I2C) return BSP_EFAIL;
base = gI2cBaseAddr[instId];
while (1) {
raw = I2CSlaveIntRawStatus(base);
/* === XRDY: Read-Byte 요청 === */
if (raw & I2C_IRQSTATUS_XRDY_MASK) {
uint8_t toSend = regs[regOffset];
I2CSlaveDataPut(base, toSend);
I2CSlaveIntRawStatusClearEx(base, I2C_IRQSTATUS_XRDY_MASK);
I2CSlaveIntClearEx (base, I2C_INT_TRANSMIT_READY);
gotOffset = false;
System_printf("I2C%u: XRDY → sent reg[0x%x]=0x%x\n",
instId, regOffset, toSend);
return 1;
}
/* === RDR: Burst Read 완료 === */
if (raw & I2C_IRQSTATUS_RDR_MASK) {
uint8_t b = I2CSlaveDataGet(base);
I2CSlaveIntRawStatusClearEx(base, I2C_IRQSTATUS_RDR_MASK);
I2CSlaveIntClearEx(base, I2C_INT_RECV_DRAIN);
System_printf("I2C%u: RDR → receive done 0x%x\n", instId, b);
return 1;
}
/* === RRDY: Write 바이트 수신 === */
if (raw & I2C_IRQSTATUS_RRDY_MASK) {
uint8_t b = I2CSlaveDataGet(base);
I2CSlaveIntRawStatusClearEx(base, I2C_IRQSTATUS_RRDY_MASK);
I2CSlaveIntClearEx (base, I2C_INT_RECV_READY);
if (!gotOffset) {
// 첫 번째 바이트: 레지스터 오프셋
regOffset = b;
gotOffset = true;
System_printf("I2C%u: got offset=0x%x\n", instId, b);
}
else {
// 두 번째 바이트: 레지스터 쓰기
regs[regOffset] = b;
gotOffset = false;
System_printf("I2C%u: wrote reg[0x%x]=0x%x\n",
instId, regOffset, b);
}
return 1;
}
/* === ARDY: Quick-Write 완료 신호 === */
if (raw & I2C_IRQSTATUS_ARDY_MASK) {
I2CSlaveIntRawStatusClearEx(base, I2C_IRQSTATUS_ARDY_MASK);
I2CSlaveIntClearEx(base, I2C_INT_ADRR_READY_ACESS);
gotOffset = false;
System_printf("I2C%u: ARDY → quickwrite complete\n", instId);
return 1;
}
BspOsal_sleep(1);
}
}
주요 인터럽트별 처리 흐름
XRDY (Transmit Ready): Host가 Read 요청
- raw & I2C_IRQSTATUS_XRDY_MASK 참
- toSend = regs[regOffset] → 현재 선택된 regOffset 레지스터 값 읽기
- I2CSlaveDataPut(base, toSend) → 한 바이트를 송신 FIFO로 넣어 호스트에게 전송
- I2CSlaveIntRawStatusClearEx(...), I2CSlaveIntClearEx(...) → 인터럽트 클리어
- gotOffset = false; → 다음 트랜잭션을 위해 반드시 “다시 offset을 받아야 한다” 상태로 초기화
- 로그 출력 → 어떤 offset, 어떤 값을 보냈는지 확인
if (raw & I2C_IRQSTATUS_XRDY_MASK) {
uint8_t toSend = regs[regOffset];
I2CSlaveDataPut(base, toSend);
I2CSlaveIntRawStatusClearEx(base, I2C_IRQSTATUS_XRDY_MASK);
I2CSlaveIntClearEx(base, I2C_INT_TRANSMIT_READY);
gotOffset = false;
System_printf("I2C%u: XRDY → sent reg[0x%02x] = 0x%02x\n",
instId, regOffset, toSend);
return 1;
}
RRDY (Receive Ready): Host가 Write하여 바이트가 들어온 경우
- 첫 번째 바이트(!gotOffset)
- Host가 보낸 첫 바이트를 “오프셋(offset = b)”으로 간주
- regOffset = b; gotOffset = true;
- 두 번째 바이트(gotOffset == true)
- Host가 보낸 두 번째 바이트를 “해당 오프셋에 쓸 값(value = b)”으로 간주
- regs[regOffset] = b; gotOffset = false;
- Return: 한 쌍(offset/value)이 처리되면 즉시 함수 리턴 → BIOS scheduler에게 CPU 리턴
if (raw & I2C_IRQSTATUS_RRDY_MASK) {
uint8_t b = I2CSlaveDataGet(base); // FIFO에서 바이트 1개 꺼냄
I2CSlaveIntRawStatusClearEx(base, I2C_IRQSTATUS_RRDY_MASK);
I2CSlaveIntClearEx (base, I2C_INT_RECV_READY);
if (!gotOffset) {
// 첫 번째 바이트 → 레지스터 오프셋 지정
regOffset = b;
gotOffset = true;
System_printf("I2C%u: got offset = 0x%02x\n", instId, b);
}
else {
// 두 번째 바이트 → regs[regOffset]에 값 저장
regs[regOffset] = b;
gotOffset = false;
System_printf("I2C%u: wrote reg[0x%02x] = 0x%02x\n",
instId, regOffset, b);
}
return 1;
}
외부 마스터로 동작 검증하기
- 하드웨어 연결
- TDA3에서 물리적으로 있는 핀은 I2C1, I2C2
- TDA3의 원하는 I²C SDA/SCL 핀을 외부 마스터(예: Raspberry Pi, Arduino, USB-I²C 어댑터 등)에 물립니다.
- 마스터에서 Read/Write 시도
- 마스터 장치에 AGX ORIN 사용
- TDA3 Slave 장치 주소를 0x33으로 설정
server@server-desktop:~$ i2cdetect -r -y 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: UU -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- 33 -- -- -- -- -- -- -- -- -- -- -- --
40: UU UU -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- 74 -- -- --
server@server-desktop:~$ i2cget -f -y 1 0x33 0x00
0x00
server@server-desktop:~$ i2cset -f -y 1 0x33 0x00 0x01 i
server@server-desktop:~$ i2cget -f -y 1 0x33 0x00
0x01
<MASTER>
[IPU1-1] 15.231165 s: I2C1: XRDY → sent reg[0x0]=0x0
[IPU1-1] 15.241170 s: I2C1: ARDY → quickwrite complete
[IPU1-1] 25.655401 s: I2C1: got offset=0x0
[IPU1-1] 25.665436 s: I2C1: XRDY → sent reg[0x0]=0x0
[IPU1-1] 25.675440 s: I2C1: ARDY → quickwrite complete
[IPU1-1] 32.321570 s: I2C1: got offset=0x0
[IPU1-1] 32.331604 s: I2C1: wrote reg[0x0]=0x1
[IPU1-1] 32.341639 s: I2C1: ARDY → quickwrite complete
[IPU1-1] 40.768864 s: I2C1: XRDY → sent reg[0x0]=0x1
[IPU1-1] 40.778838 s: I2C1: got offset=0x0
[IPU1-1] 40.788873 s: I2C1: ARDY → quickwrite complete
<SLAVE>
위와 같이 MASTER에서 SLAVE 장치를 인식하여 0x33장치 0x00 레지스터에 값을 write/read 할 수 있습니다.
위 예제 코드를 참고하여, TDA3에서 I²C 슬레이브 모드를 안정적으로 구현할 수 있습니다. 실질적인 커맨드 프로토콜(CMD/LEN/DATA/CRC 등)이 필요하다면, 이 기본 핸들러 로직 위에 “CRC 검사”, “상태 레지스터 갱신” 등을 추가하면 됩니다.