TI/TDA3

I2C SLAVE MODE

rorosi 2025. 6. 3. 15:11
728x90

테스트 환경

  • 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가 비워졌을 때

대부분 슬레이브는 RRDYXRDY를 중심으로 코드를 작성합니다.

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 요청

  1. raw & I2C_IRQSTATUS_XRDY_MASK 참
  2. toSend = regs[regOffset] → 현재 선택된 regOffset 레지스터 값 읽기
  3. I2CSlaveDataPut(base, toSend) → 한 바이트를 송신 FIFO로 넣어 호스트에게 전송
  4. I2CSlaveIntRawStatusClearEx(...), I2CSlaveIntClearEx(...) → 인터럽트 클리어
  5. gotOffset = false; → 다음 트랜잭션을 위해 반드시 “다시 offset을 받아야 한다” 상태로 초기화
  6. 로그 출력 → 어떤 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;
}

 

외부 마스터로 동작 검증하기

  1. 하드웨어 연결
    • TDA3에서 물리적으로 있는 핀은 I2C1, I2C2
    • TDA3의 원하는 I²C SDA/SCL 핀을 외부 마스터(예: Raspberry Pi, Arduino, USB-I²C 어댑터 등)에 물립니다.
  2. 마스터에서 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 검사”, “상태 레지스터 갱신” 등을 추가하면 됩니다.

728x90