본문 바로가기
임베디드 개발/리눅스 디바이스 드라이버

TCA9548A 데이터시트 및 리눅스 드라이버 분석 (Tested on Raspberry Pi 4)

by eteo 2025. 4. 6.

 

 

 

 

TCA9548A 데이터시트 분석

 

TCA9548A는 TI사의 8채널 I2C MUX(Multiplexer)로 하나의 I2C 마스터 버스를 통해 최대 8개의 하위 I2C 장치를 연결할 수 있게 해주는 칩이다.

 

I2C는 원래 멀티드롭 토폴로지라서 여러 슬레이브 장치를 하나의 버스에 연결할 수 있는데도 불구하고 이런 MUX가 필요한 이유는? 주로 동일한 주소를 가진 센서를 여러가 사용해야할 경우 이런 I2C MUX를 사용해 채널을 전환하여 장치를 구분한다.

 

 

 

 

1. 주요 특징

 

  • 1:8 양방향 I2C 스위치 구조
  • I2C 및 SMBus 호환
  • 3개의 주소 핀으로 한 I2C 버스에 최대 8개의 TCA9548A 칩 연결 가능
  • Active-low RESET 핀을 통한 시스템 초기화 및 복구 기능
  • I2C 명령을 통해 원하는 채널을 선택하거나 여러 채널을 동시에 선택할 수 있음
  • 전원 인가시 모든 채널이 비활성화 상태로 시작됨
  • 각 채널에 독립적으로 풀업 저항을 설정해 한 시스템에 1.8V, 2.5V, 3.3V, 5V 장치가 공존할 수 있음
  • 1.65V~5.5V 범위의 동작 전압 공급
  • 5V tolerant IO 핀
  • 최대 400kHz의 I2C Clock frequency 지원

 

 

 

 

 

2. 블락 다이어그램

 

1개의 업스트림 버스(SCL/SDA)가 8개의 다운스트림 채널(SC/SD0~SC/SD7)로 분기되는 구조이며 컨트롤 레지스터 설정을 통해 한 채널 또는 다충 채널을 활성화 할 수 있다.

그리고 그림이 좀 잘못 그려져 있는데 실제로 INT핀은 없고 주소핀은 A0, A1, A2 3개라서 8개의 주소 범위 중 하나를 선택할 수 있고, 셋 다 GND와 연결한 경우 TCA9548A의 디폴트 I2C 주소는 7h70이다.

 

 

 

 

 

 

 

 

3. 컨트롤 레지스터

 

TCA9548A의 레지스터는 8bit Control Register 뿐인 단순한 구조이며 제어 방법 또한 직관적이다. 활성화하고자 하는 채널의 BIT는 1로 비활성화하는 채널의 BIT는 0으로 설정하면된다. 예를 들어 컨트롤 레지스터에 0x25를 쓰면 0번, 2번, 5번 채널만 활성화되고 나머지 채널은 비활성화되는 식이다.

 

 

 

 

 

 

 

 

 

4. 컨트롤 레지스터 쓰기 I2C 패킷 구조와 타이밍

 

START 비트로 시작해서 연달아 '8비트의 데이터(첫 데이터는 Slave 주소 7비트 + R/W 비트) + 1비트의 ACK/NACK 조합'이 등장하고 STOP 비트로 통신이 종료되는 일반적인 I2C 패킷 구조이다.

 

 

 

 

 

 

 

 

 


리눅스 드라이버 분석

커널 v5.15.148 기준 TCA9548A는 i2c-mux 서브시스템에서 기본으로 제공되는 pca954x_driver에 의해 지원된다.

 

 

 

 

1. 드라이버 위치 :

drivers/i2c/muxes/i2c-mux-pca954x.c

 

 

 

 

2. 드라이버 소스코드

https://elixir.bootlin.com/linux/v5.15.148/source/drivers/i2c/muxes/i2c-mux-pca954x.c

 

i2c-mux-pca954x.c - drivers/i2c/muxes/i2c-mux-pca954x.c - Linux source code v5.15.148 - Bootlin Elixir Cross Referencer

/ drivers / i2c / muxes / i2c-mux-pca954x.c

elixir.bootlin.com

 

 

 

 

3. 드라이버 동작 방식

 

 

1) 디바이스 트리 설정

 

먼저 디바이스 트리 설정을 통해 하드웨어 장치 정보, 드라이버 매칭 정보, 드라이버 동작 방식의 설정 정보 등을 커널에 전달하여야 한다. TCA9548A의 경우 I2C 버스에 연결되는 MUX 장치이므로, 자신의 설정 정보와 하위 MUX 채널에 대한 각각의 정의가 필요하다.

 

다음 코드는 TCA9548A I2C MUX에 같은 슬레이브 주소를 가진 두 개의 하위 장치를 연결할 시 디바이스 트리 오버레이 작성 예시이다.

 

/dts-v1/;
/plugin/;

/ {
    fragment@0 {
        target = <&i2c1>;
        __overlay__ {
            #address-cells = <1>;
            #size-cells = <0>;
            status = "okay";

            tca9548@70 {
                compatible = "nxp,pca9548";
                reg = <0x70>;
                #address-cells = <1>;
                #size-cells = <0>;

                i2c@0 {
                    reg = <0>;
                    #address-cells = <1>;
                    #size-cells = <0>;

                    tmp102_0: temp_sensor0@48 {
                        compatible = "ti,tmp102";
                        reg = <0x48>;
                    };
                };

                i2c@1 {
                    reg = <1>;
                    #address-cells = <1>;
                    #size-cells = <0>;

                    tmp102_1: temp_sensor1@48 {
                        compatible = "ti,tmp102";
                        reg = <0x48>;
                    };
                };
            };
        };
    };
};

 

 

작성한 소스를 컴파일 후 dtbo를 커널에 적용하여 DT를 업데이트 한다. 이후 DT node의 compatible 속성 값이 드라이버 매칭 테이블에 정의되어 있으면 해당 드라이버가 자동으로 로드된다.

 

 

 

디바이스 트리 작성 예시 및 속성에 대한 설명은 아래 위치의 디바이스 트리 바인딩 문서에서 찾을 수 있다.

Documentation/devicetree/bindings/i2c/i2c-mux-pca954x.yaml

 

 

 

 

 

2) 가상 I2C bus 생성

 

pca954x_drvier는 I2C MUX 장치를 인식하고 pca954x_probe() 단계에서 각 채널을 독립된 가상의 I2C 버스로 매핑한다.

 

static int pca954x_probe(struct i2c_client *client,
			 const struct i2c_device_id *id)
{
	struct i2c_adapter *adap = client->adapter;
	struct device *dev = &client->dev;
	struct gpio_desc *gpio;
	struct i2c_mux_core *muxc;
	struct pca954x *data;
	int num;
	int ret;

	if (!i2c_check_functionality(adap, I2C_FUNC_SMBUS_BYTE))
		return -ENODEV;

	muxc = i2c_mux_alloc(adap, dev, PCA954X_MAX_NCHANS, sizeof(*data), 0,
			     pca954x_select_chan, pca954x_deselect_mux);
	if (!muxc)
		return -ENOMEM;
	data = i2c_mux_priv(muxc);

	i2c_set_clientdata(client, muxc);
	data->client = client;

	/* Reset the mux if a reset GPIO is specified. */
	gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_HIGH);
	if (IS_ERR(gpio))
		return PTR_ERR(gpio);
	if (gpio) {
		udelay(1);
		gpiod_set_value_cansleep(gpio, 0);
		/* Give the chip some time to recover. */
		udelay(1);
	}

	data->chip = device_get_match_data(dev);
	if (!data->chip)
		data->chip = &chips[id->driver_data];

	if (data->chip->id.manufacturer_id != I2C_DEVICE_ID_NONE) {
		struct i2c_device_identity id;

		ret = i2c_get_device_id(client, &id);
		if (ret && ret != -EOPNOTSUPP)
			return ret;

		if (!ret &&
		    (id.manufacturer_id != data->chip->id.manufacturer_id ||
		     id.part_id != data->chip->id.part_id)) {
			dev_warn(dev, "unexpected device id %03x-%03x-%x\n",
				 id.manufacturer_id, id.part_id,
				 id.die_revision);
			return -ENODEV;
		}
	}

	data->idle_state = MUX_IDLE_AS_IS;
	if (device_property_read_u32(dev, "idle-state", &data->idle_state)) {
		if (device_property_read_bool(dev, "i2c-mux-idle-disconnect"))
			data->idle_state = MUX_IDLE_DISCONNECT;
	}

	/*
	 * Write the mux register at addr to verify
	 * that the mux is in fact present. This also
	 * initializes the mux to a channel
	 * or disconnected state.
	 */
	ret = pca954x_init(client, data);
	if (ret < 0) {
		dev_warn(dev, "probe failed\n");
		return -ENODEV;
	}

	ret = pca954x_irq_setup(muxc);
	if (ret)
		goto fail_cleanup;

	/* Now create an adapter for each channel */
	for (num = 0; num < data->chip->nchans; num++) {
		ret = i2c_mux_add_adapter(muxc, 0, num, 0);
		if (ret)
			goto fail_cleanup;
	}

	if (data->irq) {
		ret = devm_request_threaded_irq(dev, data->client->irq,
						NULL, pca954x_irq_handler,
						IRQF_ONESHOT | IRQF_SHARED,
						"pca954x", data);
		if (ret)
			goto fail_cleanup;
	}

	/*
	 * The attr probably isn't going to be needed in most cases,
	 * so don't fail completely on error.
	 */
	device_create_file(dev, &dev_attr_idle_state);

	dev_info(dev, "registered %d multiplexed busses for I2C %s %s\n",
		 num, data->chip->muxtype == pca954x_ismux
				? "mux" : "switch", client->name);

	return 0;

fail_cleanup:
	pca954x_cleanup(muxc);
	return ret;
}

 

 

가상의 I2C 버스를 생성하는 과정에서 probe() 함수 내 i2c_mux_alloc()과 i2c_mux_add_adapter()가 사용된다.

 

  • i2c_mux_alloc : 가상의 I2C 버스를 위한 메모리를 할당하고 I2C MUX가 채널을 전환하거나 해제할 때 자동으로 호출되는 콜백 함수를 등록한다.

 

  • i2c_mux_add_adapter : I2C MUX의 각 채널에 대해 실제 가상의 버스를 생성하는 함수이다. 실제로 슬레이브 장치가 붙어있는지 여부 또는 DT 설정과 관계없이 고정적으로 모든 채널에 대해 가상의 I2C 버스를 생성하고 있다.

 

 

 

 

3) I2C MUX 채널 전환 동작

 

가상의 I2C 버스에 접근할 때 pca954x_select_chan() 함수가 호출되고, I2C 통신이 끝나면 pca954x_deselect_mux() 함수가 호출되어 자동으로 MUX전환을 수행한다. 따라서 사용자는 MUX 전환을 의식하지 않고, 마치 독립된 I2C 버스를 사용하는 것처럼 느낄 수 있다.

 

 

static int pca954x_select_chan(struct i2c_mux_core *muxc, u32 chan)
{
	struct pca954x *data = i2c_mux_priv(muxc);
	struct i2c_client *client = data->client;
	u8 regval;
	int ret = 0;

	regval = pca954x_regval(data, chan);
	/* Only select the channel if its different from the last channel */
	if (data->last_chan != regval) {
		ret = pca954x_reg_write(muxc->parent, client, regval);
		data->last_chan = ret < 0 ? 0 : regval;
	}

	return ret;
}

static int pca954x_deselect_mux(struct i2c_mux_core *muxc, u32 chan)
{
	struct pca954x *data = i2c_mux_priv(muxc);
	struct i2c_client *client = data->client;
	s32 idle_state;

	idle_state = READ_ONCE(data->idle_state);
	if (idle_state >= 0)
		/* Set the mux back to a predetermined channel */
		return pca954x_select_chan(muxc, idle_state);

	if (idle_state == MUX_IDLE_DISCONNECT) {
		/* Deselect active channel */
		data->last_chan = 0;
		return pca954x_reg_write(muxc->parent, client,
					 data->last_chan);
	}

	/* otherwise leave as-is */

	return 0;
}

 

 

  • pca954x_select_chan() : 가상의 I2C 버스에 접근 시 호출되며, 현재 선택된 채널(data->last_chan)과 접근하려는 채널(chan)을 비교해서 다를 경우 pca954x_reg_write() 함수를 호출해 MUX의 채널을 전환한다.

 

  • pca954x_deselect_mux() : I2C 통신이 끝난 후 호출되는 함수로 DT에서 설정한 속성값에 따라 다음의 작업을 수행한다.
    • MUX_IDLE_AS_IS (0) : 기본값, 통신이 끝나도 현재 채널을 그대로 유지한다.
    • MUX_IDLE_DISCONNECT (-1) : 통신이 끝난 후 채널을 비활성화한다. dt에서 i2c-mux-idle-disconnect;가 설정된 경우 이 값이 사용된다.
    • idle_state >= 0 (특정 채널) : 통신이 끝난 후 특정 채널로 복귀한다. 

 

 

 

 

 

 

 

 

 


Raspberry Pi B4에서 테스트

 

https://randomnerdtutorials.com/raspberry-pi-pinout-gpios/

 

 

1. 배선

 

Raspberry Pi 4B TCA9548A
3.3V VCC
GND GND
SDA (핀 3, GPIO2) SDA
SCL (핀 5, GPIO3) SCL

 

 

 

 

2. I2C 인터페이스 활성화

 

raspi-config를 실행한다.

$ sudo raspi-config

 

 

Interface Options > I2C > Enable 를 선택한다.

 

 

 

재부팅한다.

$ sudo reboot

 

 

 

이렇게 하면 /boot/firmware/config.txt 파일에 dtparam=i2c_arm=on 라인이 주석 해제되고,

 

 

i2c-1 버스가 활성화된다.

 

 

 

 

 

 

3. Device Tree Overlay 적용

 

/boot/firmware/config.txt 파일을 에디터로 연다.

$ sudo vim /boot/firmware/config.txt

 

 

파일 맨 아래에 다음 문장을 추가한다.

[all]
dtoverlay=i2c-mux,pca9548,addr=0x70

 

재부팅 한다.

$ sudo reboot

 

 

 

참고로 라즈베리 파이에서 간단히 config.txt 파일에 라인 한줄을 추가해 dt 오버레이를 적용할 수 있는 이유는 /boot/firmware/overlays/ 경로 안에 이미 수많은 .dtbo 파일이 존재하기 때문이다. 

i2c-mux-pca954x 드라이버를 지원하는 오버레이 파일 역시 i2c-mux.dtbo라는 이름으로 존재한다.

 

 

 

 

해당 dtbo의 소스 파일은 아래 링크에서 확인 가능하다. 파일 내 __overrides__ 섹션 부분을 보면 config.txt 파일에서 제공하는 값을 읽어 노드의 속성 값을 변경하는 동적 매개변수 방식을 사용하는 것을 알 수 있다.

 

https://github.com/raspberrypi/linux/blob/rpi-6.6.y/arch/arm/boot/dts/overlays/i2c-mux-overlay.dts

 

 

 

 

 

4. 드라이버 활성화 여부 확인

 

/proc/device-tree/ 에서 디바이스 트리 적용 여부를 확인한다.

 

 

dmesg를 통해 드라이버가 올바르게 로드되었는지 확인하고, 추가적인 i2c 버스가 생성되었는지 확인한다.

 

$ dmesg | grep i2c
$ ls /dev/i2c-*
$ i2cdetect -y -l

 

i2c-20 및 i2c-21 버스는 HDMI 포트의 Display Data Channel (DDC) 을 위한 I2C 인터페이스이며, i2c-1버스에 연결된 I2C MUX 디바이스의 버스 번호는 커널에 의해 그 이후 값인 i2c-22부터 i2c-29까지 할당된 것을 확인할 수 있다.

 

 

또한 TCA9548A의 SD1, SC1에 0x48, 0x49 주소를 갖는 다른 I2C 장치를 연결하고 i2c-tools를 사용해 i2c-23 버스를 스캔해 보았을 때 장치가 올바르게 인식되는 것을 확인하였다.

$ i2cdetect -y 23

 

 

이렇게 MUX를 통해 추가된 I2C 장치는 i2c-dev를 사용하여 직접 제어할 수 있다. 한편, 커널 드라이버를 적용하려면 보통 DTS를 수정하여 MUX 노드에서 해당 장치를 선언하는 방식을 사용한다. 이 외에도, I2C 버스 드라이버 방식을 사용하면 커널이 I2C 버스를 탐색하여 장치를 등록할 수 있으므로, DTS를 수정하지 않고도 드라이버를 로드할 수 있다.

 

 

 

✔️ i2cdetect 기본 사용법

sudo apt install i2c-tools
Usage: i2cdetect [-y] [-a] [-q|-r] I2CBUS [FIRST LAST]
       i2cdetect -F I2CBUS
       i2cdetect -l
  I2CBUS is an integer or an I2C bus name
  If provided, FIRST and LAST limit the probing range.