i2cProject_with_Jetson

i2c공부 1)

찬영_00 2024. 11. 29. 17:25

i2c에 대해 좀 더 공부가 필요하다고 느꼈다.

그래서 i2c에 대해 리뷰를 해보려한다. ( 현재 프로젝트에서는 Linux와 Arduino IDE에서 i2c통신을 하고 있으니 해당 함수들에 대해 자세히 파헤치고, 프로젝트에 적용하려 한다. )

 

시작해보자

Linux에서 사용하는 라이브러리는 i2c-dev를 이용한다.

참고는 두곳에서 참고하여 해석해보았다.

 

<커널 문서 i2c인터페이스>

https://www.kernel.org/doc/Documentation/i2c/dev-interface

 

<설명을 정리해둔 사람의 깃허브>

https://github.com/Leonamin/Sensor-Test/blob/master/Guide/Linux%20I2C%20Guide.md

 

Sensor-Test/Guide/Linux I2C Guide.md at master · Leonamin/Sensor-Test

Sensor I2C Communication Test on Linux. Contribute to Leonamin/Sensor-Test development by creating an account on GitHub.

github.com

 

i2c의 너무 기본은 아실거라고 생각하고, 진행한다는 점 양해바란다.

 

먼저 터미널에서 쓸수 있는 명령어들을 확인해보자

전부 다 알아보긴 어렵고, 유용한 몇가지 명령어들을 확인해보자

 

명령어 설명
i2cdetect 현재 I2C 버스에서 연결된 모든 디바이스를 찾을 수 있음
ex) i2cdetect -y 1
기본적으로 i2cdetect 명령어는 모든 가능한 주소를 스캔하지만, -r 옵션을 사용하면 일부 레지스터 주소 범위만 검색할 수 있음
i2cget I2C 슬레이브 장치의 레지스터에서 값을 읽어옴
ex) i2cget -y 1 0x44 0xD0
-y 옵션은 사용자 확인 없이 명령을 자동으로 실행
1은 I2C 버스 번호, 0x44는 슬레이브 주소, 0xD0은 레지스터 주소
i2cset I2C 슬레이브 장치의 레지스터에 값을 씀
ex) i2cset -y 1 0x44 0xD0 0x60
-y 옵션은 사용자 확인 없이 명령을 자동으로 실행
1은 I2C 버스 번호, 0x44는 슬레이브 주소, 0xD0은 레지스터 주소, 0x60은 쓰고자 하는 값
i2cdump I2C 장치의 전체 레지스터 값을 덤프
ex) i2cdump -y 1 0x44
1은 I2C 버스 번호, 0x44는 슬레이브 주소

 

여기서 dump를 쓰면 원하는 슬레이브 주소의 레지스터 값을 전부 알 수 있겠다. 라고 생각할 수 있지만,

i2cdump 명령어는 I2C 슬레이브 장치에서 레지스터 값을 읽어오는 도구로, 슬레이브 장치가 가지고 있는 레지스터 주소 및 값을 덤프하는 것이다.

중요한 점은, i2cdump가 자동으로 슬레이브의 레지스터 주소를 인식하여 출력하는 것이 아니라, 사용자가 어떤 레지스터를 읽을지를 직접 지정해줘야 한다는 것임을 잊지말자

 

이건 그냥 tool에서 유용한 명령어라 그냥 적어둔거고, 실제 코드에서 어떻게 쓰고, 읽고를 진행하는지 파헤쳐보자

 

먼저 예시로 i2c를 사용하기 위해 버스를 개설하고, 통신을 초기화해야하는데 그 예제를 보겠다.

int file;
char *filename = "/dev/i2c-0"; // I2C 버스 파일 지정
if ((file = open(filename, O_RDWR)) < 0) { // I2C 버스를 열고 읽기/쓰기 권한 부여
    perror("Failed to open the i2c bus");   // 에러 발생 시 메시지 출력
    exit(1);                               // 프로그램 종료
}


int addr = 0x44;          // 슬레이브 디바이스의 주소
if (ioctl(file, I2C_SLAVE, addr) < 0) { // 슬레이브 디바이스와 통신 설정
    printf("Failed to acquire bus access and/or talk to slave.\n");
    exit(1);                           // 실패 시 프로그램 종료
}

이건 깃허브에서 가져온 코드이다.

설명이 잘 되어있지만 한번 더 이해하기 위해 설명한다.

주석에서 잘 설명을 했다. 

open을 이용하여 읽고 쓰기 모드로 i2c버스를 개설한다.

그리고 슬레이브 주소를 ioctl를 이용하여 통신을 설정한다.


 

이것이 초기 설정이고, 기본적인 WriteData와 ReadData를 보겠다.

int WriteData(uint8_t reg_addr, uint8_t *data, int size) {
    uint8_t *buf;
    buf = malloc(size + 1);               // 쓰기 버퍼를 동적으로 할당 (레지스터 주소 + 데이터 크기)
    buf[0] = reg_addr;                    // 버퍼 첫 번째 바이트에 레지스터 주소 저장
    memcpy(buf + 1, data, size);          // 버퍼 두 번째 위치부터 데이터를 복사
    write(fd, buf, size + 1);             // fd(파일 디스크립터)를 통해 I2C 버스로 데이터 전송
    free(buf);                            // 동적 할당된 버퍼 메모리 해제

    return 1;                             // 성공적으로 실행되었음을 나타냄
}

 

이게 write함수의 내부이다.

  • 입력 매개변수
    • reg_addr: I2C 슬레이브 장치의 레지스터 주소.
    • data: 전송할 데이터가 담긴 포인터.
    • size: 전송할 데이터의 크기.
  • 버퍼 준비
    • malloc(size + 1): 레지스터 주소와 데이터를 포함하기 위해 동적으로 크기 size + 1의 버퍼를 할당.
    • 첫 번째 바이트(buf[0])는 레지스터 주소를 저장
    • 나머지 공간(buf[1]부터)은 전송할 데이터를 저장
  • I2C 데이터 전송
    • write(fd, buf, size + 1):
      • fd: 이전에 열었던 I2C 버스의 파일 디스크립터
      • buf: 전송할 데이터가 저장된 버퍼
      • size + 1: 레지스터 주소(1바이트) + 데이터 크기
  • 메모리 해제
    • free(buf): malloc으로 할당했던 메모리를 해제해 메모리 누수를 방지
  • 리턴값:
    • 성공적으로 데이터를 전송하면 1을 반환

int ReadData(uint8_t reg_addr, uint8_t *data, int size) {
    write(fd, &reg_addr, 1);         // 레지스터 주소를 I2C 슬레이브 장치로 전송
    read(fd, data, size);            // 레지스터에서 데이터를 읽어옴

    return 1;                        // 성공적으로 실행되었음을 나타냄
}

 

  • 입력 매개변수
    • reg_addr: 읽고자 하는 데이터가 저장된 슬레이브 장치의 레지스터 주소
    • data: 읽은 데이터를 저장할 버퍼 포인터
    • size: 읽고자 하는 데이터의 크기
  • 레지스터 주소 전송
    • write(fd, &reg_addr, 1)
      • 슬레이브 장치에 읽을 레지스터 주소를 전송
      • 주소는 1바이트(&reg_addr)로 전달
  • 데이터 읽기
    • read(fd, data, size)
      • 슬레이브 장치로부터 size 바이트의 데이터를 읽어와서 data 버퍼에 저장
  • 리턴값
    • 성공적으로 데이터를 읽으면 1을 반환

 

이렇게  해석해 본 것을 토대로 이전에 짰던 코드들을 해석해보고, i2c에 대해 좀 더 파헤쳐본 결과,

내가 이전에 짰던 코드에서의 문제점을 발견했다.

 

1. 아두이노에서의 write함수는 최대 32바이트 밖에 보내지 못한다.

  • 따라서 i2c에서의 데이터 전송 크기가 정해진 것이 아닌 아두이노에서 32바이트 밖에 보내지 못해 데이터 에러가 생겼던것이다.

2. i2c통신은 빠르게 데이터를 주고 받아야한다.

  • Arduino의 Wire.onRequest() 핸들러는 마스터가 데이터를 요청할 때 호출되며, 빠르게 실행되어야 한다
  • delay()가 I2C 요청 처리 중 실행되면, 슬레이브가 응답하지 않거나 응답이 너무 느려서 Jetson Nano에서 "데이터 수신 실패"로 처리된다

 

일단 이 문제점들을 해결해가면서 다시 프로젝트를 진행시켜보겠다.

 

일단 32바이트 이상의 데이터가 가는지 확인하고, 내가 보내는 바이트가 변동성이 있을 때를 대비해 0xFF인 문자있으면 더이상 데이터가 없다고 판단하고 문자열로 만들어주는 코드를 만들었다.

https://github.com/PCY00/KSensor/tree/main/24_11_8/I2C/over32ByteDataStop

 

KSensor/24_11_8/I2C/over32ByteDataStop at main · PCY00/KSensor

Contribute to PCY00/KSensor development by creating an account on GitHub.

github.com

 

다음으로 딜레이다. 센서중에서 1초정도 시간을 주어야 값을 가져오는 센서가 있다. 여러 방법중 2가지 방법이 생각난다.

1. 버퍼를 만들어 값을 20초마다 버퍼안에 저장해두고, 값을 달라고 할 때 꺼내서 준다.

2. millis써서 값을 호출받으면 센서에 값을 요청하고, 마스터측에서 read를 2초 뒤에 실행하도록 한다.

 

1번의 장점은 빠르게 데이터를 전달하여 딜레이를 없앤다.

하지만 단점은 내가 하는 프로젝트는 값을 달라고 요청했을 시점의 데이터가 중요하다.

이것을 최대한 줄인다고 버퍼를 더 크게 만들면 리소스가 낭비된다.

 

2번의 장점은 현재하고 있는 프로젝트에서의 매우 적합한 방법이라는 점이다.

하지만 단점은 시간 딜레이를 마스터측에서 주는 것이다.

 

이게 여러 장비에서 i2c로 데이터를 수집하는 방면에서 2번이 과연 효율적인가도 생각이 된다.

1번은 저장해둔 값을 뱉어내는 반면 2번은 수집하는 시간을 줘야하니 여러 개의 데이터를 한번에 수집하는 방면에서 각각 2초씩 딜레이가 되면 1번과 다를게 뭔가 싶기도 하다.

 

일단 둘 다 해보고 좋은 방법을 택할것이다.

코드는 아래에 올려두었다.

https://github.com/PCY00/KSensor/tree/main/24_11_8/I2C/DelayData

 

KSensor/24_11_8/I2C/DelayData at main · PCY00/KSensor

Contribute to PCY00/KSensor development by creating an account on GitHub.

github.com

 

위 코드는 일단 시험삼아 하나의 슬레이브에서만 데이터를 가져오는 방식이다.

따라서 1번과 2번 둘중 어떤것을 사용해야할지는 계속 고민해봐야한다.