i2cProject_with_Jetson

Jetson Nano와 아두이노 간의 I2C 통신 3)

찬영_00 2024. 11. 5. 09:56

이번 포스팅에서는 I2C로 마스터가 슬레이브에게 값을 요청하면 값을 전달해주는 작업을 해보겠다.

 

먼저 진행은 다음과 같이 진행할 예정이다.

- 젯슨과 아두이노와의 LED를 제어하고, 제어한 LED의 상태를 아두이노로 부터 받기

- start라는 신호를 젯슨나노가 보내면 아두이노는 0,0,0,0,0,0,0,0,0,0,0라는 값을 보내보기

 

젯슨과 아두이노와의 LED를 제어하고, 제어한 LED의 상태를 아두이노로 부터 받기

먼저 아두이노 코드를 수정하자

우리가 전에 짰던 코드는 마스터에게 요청을 받으면 값을 주긴하지만 ReadRegister를 적용하지 않았다.

따라서 그것을 적용하여 진행해볼 예정이다.

 

코드는 다음과 같다.

#include <Wire.h>

#define LED           13
#define SLAVE_ADDRESS 0x57

// Register addresses
#define WRITE_REGISTER 0x01
#define READ_REGISTER_1 0x02  // 첫 번째 읽기 레지스터
#define READ_REGISTER_2 0x04  // 두 번째 읽기 레지스터

// LED control
#define LED_ON         0xAC
#define LED_OFF        0x1F

bool ledState = false;
int requestedRegister = 0; // 요청된 레지스터 주소를 저장할 변수

void setup() {
  pinMode(LED, OUTPUT);
  Wire.begin(SLAVE_ADDRESS);
  Wire.onRequest(requestEvent); // 읽기 요청에 대한 핸들러
  Wire.onReceive(receiveEvent);  // 쓰기 요청에 대한 핸들러
  Serial.begin(9600);
}

void loop() {
  delay(100);
}

// 쓰기 요청을 처리하는 함수
void receiveEvent(int howMany) {
  int registerAddress = Wire.read(); // 마스터로부터 레지스터 주소를 읽음
  Serial.println(registerAddress);
  if (registerAddress == WRITE_REGISTER) {
    int value = Wire.read(); // LED 제어 값을 읽음
    if (value == LED_ON) {
      digitalWrite(LED, HIGH);
      ledState = true;
    } else if (value == LED_OFF) {
      digitalWrite(LED, LOW);
      ledState = false;
    }
  } 
  else if (registerAddress == READ_REGISTER_1 || registerAddress == READ_REGISTER_2) {
    requestedRegister = registerAddress; // 요청된 레지스터 주소 저장
  }
}

// 읽기 요청을 처리하는 함수
void requestEvent() {
  if (requestedRegister == READ_REGISTER_1) {
    Wire.write(ledState ? LED_ON : LED_OFF); // LED 상태 전송
  } else if (requestedRegister == READ_REGISTER_2) {
    // 추가적인 데이터를 여기에서 처리 (예: LED 상태 반전)
    Wire.write(ledState ? LED_OFF : LED_ON);
  }
}

 

마지막 부분에 읽는 레지스터와 값이 같을 경우 삼항연산자를 사용해 LED의 현재 상태를 출력하게끔 해두었다.

 

다음으로 젯슨나노 코드이다.

 

#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
#include <signal.h>
#include <cstring>
#include <chrono>
#include <thread>

#define I2C_DEVICE      "/dev/i2c-1"  // Jetson Nano의 I2C 포트
#define SLAVE_ADDRESS_1 0x57          // 첫 번째 아두이노의 I2C 주소
#define SLAVE_ADDRESS_2 0x60          // 두 번째 아두이노의 I2C 주소
#define WRITE_REGISTER  0x01          // LED 제어를 위한 레지스터 주소
#define READ_REGISTER   0x02          // LED 상태를 읽기 위한 레지스터 주소
#define LED_ON          0xAC          // LED 켜기 명령
#define LED_OFF         0x1F          // LED 끄기 명령

int file;  // I2C 파일 디스크립터

// I2C 장치 열기
int openI2CDevice(int address) {
    int f = open(I2C_DEVICE, O_RDWR);
    if (f < 0) {
        std::cerr << "I2C 장치를 열 수 없습니다." << std::endl;
        return -1;
    }

    // 슬레이브 주소 설정
    if (ioctl(f, I2C_SLAVE, address) < 0) {
        std::cerr << "슬레이브 주소를 설정할 수 없습니다." << std::endl;
        close(f);
        return -1;
    }
    return f;
}

// LED 제어 함수
void controlLED(int file, bool turnOn) {
    unsigned char buffer[2];
    buffer[0] = WRITE_REGISTER;
    buffer[1] = turnOn ? LED_ON : LED_OFF;

    if (write(file, buffer, 2) != 2) {
        std::cerr << "데이터 전송에 실패했습니다." << std::endl;
    } else {
        std::cout << "LED " << (turnOn ? "켜기" : "끄기") << " 명령을 보냈습니다." << std::endl;
    }
}

// 깜빡임 제어 함수
void blinkLED(int file, int delaySeconds) {
    for (int i = 0; i < 2; ++i) {  // 2번 깜빡임
        controlLED(file, true);
        std::this_thread::sleep_for(std::chrono::seconds(delaySeconds));
        controlLED(file, false);
        std::this_thread::sleep_for(std::chrono::seconds(delaySeconds));
    }
}

// 프로그램 종료 시 클린업
void cleanup(int signum) {
    if (file >= 0) {
        close(file);
    }
    std::cout << "\n프로그램을 종료합니다." << std::endl;
    exit(0);
}

// 명령어 파싱 함수
bool parseCommand(const std::string &command, int &arduinoNum, std::string &action, int &delaySeconds) {
    if (command.size() < 4 || command[0] != 'M' || command[2] != '_') return false;

    arduinoNum = command[1] - '0';  // 아두이노 번호 추출
    action = command.substr(3);     // 명령어 추출

    if (action[0] == 'D') {  // 깜빡임 명령어일 경우
        delaySeconds = std::stoi(action.substr(1));
    }
    return true;
}

bool readLEDStatus(int file) {
    unsigned char buffer[1];
    buffer[0] = READ_REGISTER;
    
    if (write(file, buffer, 1) != 1) {
        std::cerr << "데이터 전송에 실패했습니다." << std::endl;
        return false;
    }

    unsigned char status;
    if (read(file, &status, 1) != 1) {
        std::cerr << "데이터 수신에 실패했습니다." << std::endl;
        return false;
    }

    std::cout << "LED 상태: " << (status == LED_ON ? "켜짐" : "꺼짐") << std::endl;
    return true;
}

int main() {
    // 시그널 핸들러 등록
    signal(SIGINT, cleanup);

    std::string command;
    int address;
    int arduinoNum, delaySeconds;
    std::string action;

    std::cout << "M?_ON, M?_OFF, M?_D??으로 명령을 줄 수 있습니다.\n?는 아두이노 번호이고, ??는 깜빡이 딜레이(초)입니다." << std::endl;

    while (true) {
        std::cout << "명령을 입력하세요: ";
        std::cin >> command;

        // 명령어 파싱
        if (!parseCommand(command, arduinoNum, action, delaySeconds)) {
            std::cerr << "잘못된 명령 형식입니다." << std::endl;
            continue;
        }

        // 아두이노 주소 설정
        if(arduinoNum == 1){
            address = SLAVE_ADDRESS_1;
            std::cout << "첫 번째 아두이노를 선택했습니다." << std::endl;
        }else if(arduinoNum == 2){
            address = SLAVE_ADDRESS_2;
            std::cout << "두 번째 아두이노를 선택했습니다." << std::endl;
        }else{
            std::cerr << "잘못된 아두이노 번호입니다." << std::endl;
            continue;
        }

        file = openI2CDevice(address);
        if (file < 0) continue;

        // 명령에 따라 LED 제어
        if (action == "ON") {
            controlLED(file, true);
        } else if (action == "OFF") {
            controlLED(file, false);
        } else if (action[0] == 'D') {
            blinkLED(file, delaySeconds);
        } else {
            std::cerr << "알 수 없는 명령입니다." << std::endl;
        }

        close(file);  // I2C 장치 닫기
    }

    cleanup(0);  // 프로그램 종료 시 클린업 함수 호출
    return 0;
}

 

젯슨나노 LED 상태 읽는 함수에서 write로 읽는 레지스터에 접근하고 아두이노에서는 읽는 레지스터에 들어오면 그 주소를 저장했다가 이후 read을 받으면 그 레지스터를 사용하여 지정된 읽기 주소의 코드를 수행한다.

 

명령어가 실행된 부분

 

아래는 시연영상이다.

 

https://youtube.com/shorts/aDiXlNO-s1Q

 

start라는 신호를 젯슨나노가 보내면 아두이노는 0,0,0,0,0,0,0,0,0,0,0라는 값을 보내보기

 

최대한 주석 달아두었으니 참고바란다.

 

아두이노 코드

#include <Wire.h>

#define LED           13
#define SLAVE_ADDRESS 0x57

// Register addresses
#define WRITE_REGISTER 0x01
#define READ_REGISTER 0x02  // 첫 번째 읽기 레지스터
#define START_SIGNAL 0xA0   // 두 번째 읽기 레지스터

String con_data = "0,0,0,0,0,0,0,0,0";

bool startSignal = false;
bool ledState = false; // LED 상태를 저장할 변수

void setup() {
  pinMode(LED, OUTPUT); // LED 핀 모드 설정
  Wire.begin(SLAVE_ADDRESS);
  Wire.onRequest(requestEvent); // 읽기 요청에 대한 핸들러
  Wire.onReceive(receiveEvent);  // 쓰기 요청에 대한 핸들러
  Serial.begin(9600);
}

void loop() {
  delay(100);
}

void receiveEvent(int howMany) {
    int registerAddress = Wire.read(); // 마스터로부터 레지스터 주소를 읽음

    if (registerAddress == WRITE_REGISTER) {
        char buffer[32]; // 최대 32 바이트 문자열 수신
        int index = 0;

        while (Wire.available() > 0 && index < sizeof(buffer) - 1) { 
            buffer[index++] = Wire.read();
        }
        buffer[index] = '\0'; // 문자열 종료 문자 추가

        String receivedString = String(buffer);
        Serial.print("수신된 문자열: ");
        Serial.println(receivedString);

        // LED 상태 제어
        if (receivedString == "P1_1000") {
            digitalWrite(LED, HIGH);
            ledState = true; // LED 상태 업데이트
        } else if (receivedString == "P1_2000") {
            digitalWrite(LED, LOW);
            ledState = false; // LED 상태 업데이트
        }
    } 
    else if (registerAddress == READ_REGISTER && Wire.available() > 0) {
        int signal = Wire.read();
        if (signal == START_SIGNAL) {
            startSignal = true; // start 신호를 받은 것으로 설정
        }
    }
}

void requestEvent() {
    if (startSignal) {
        startSignal = false; // 요청 처리 후 초기화
        
        int len = con_data.length() + 1; // NULL 문자 포함
        char buffer[len];
        con_data.toCharArray(buffer, len);

        Wire.write((uint8_t*)buffer, len); // 문자열 전송
        Serial.print("전송할 문자열: ");
        Serial.println(buffer); // 전송할 문자열 디버깅 출력
    }
}

 

젯슨나노 코드

#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
#include <signal.h>
#include <cstring>
#include <string>

#define I2C_DEVICE      "/dev/i2c-1"  // Jetson Nano의 I2C 포트
#define SLAVE_ADDRESS_1 0x57          // 첫 번째 아두이노의 I2C 주소
#define SLAVE_ADDRESS_2 0x60          // 두 번째 아두이노의 I2C 주소
#define WRITE_REGISTER  0x01          // 사용자 명령어를 보내기 위한 레지스터 주소
#define READ_REGISTER   0x02          // start 신호를 보내 슬레이브에게 데이터를 읽어오라고 요청하기 위한 레지스터 주소
#define START_SIGNAL    0xA0          // start 신호를 보내기 위한 명령어

// I2C 장치 열기
int openI2CDevice(int address) {
    int f = open(I2C_DEVICE, O_RDWR);
    if (f < 0) {
        std::cerr << "I2C 장치를 열 수 없습니다: " << strerror(errno) << std::endl;
        return -1;
    }

    // 슬레이브 주소 설정
    if (ioctl(f, I2C_SLAVE, address) < 0) {
        std::cerr << "슬레이브 주소를 설정할 수 없습니다: " << strerror(errno) << std::endl;
        close(f);
        return -1;
    }
    return f;
}

bool readData(int file) {
    unsigned char buffer[2] = {READ_REGISTER, START_SIGNAL};

    if (write(file, buffer, sizeof(buffer)) != sizeof(buffer)) {
        std::cerr << "데이터 요청 전송에 실패했습니다: " << strerror(errno) << std::endl;
        return false;
    }

    char readBuffer[32] = {0};  // 읽을 버퍼 초기화
    ssize_t bytesRead = read(file, readBuffer, sizeof(readBuffer) - 1);
    if (bytesRead <= 0) {
        std::cerr << "데이터 수신에 실패했습니다: " << strerror(errno) << std::endl;
        return false;
    }

    readBuffer[bytesRead] = '\0';  // 종료 문자 설정
    std::cout << "받은 데이터: " << readBuffer << std::endl;
    return true;
}

// 사용자 명령어 전송 함수
void sendCommand(int file, const std::string &command) {
    unsigned char buffer[1 + command.size()];
    buffer[0] = WRITE_REGISTER;  // 첫 번째 바이트에 레지스터 주소 저장
    std::memcpy(buffer + 1, command.c_str(), command.size());

    if (write(file, buffer, sizeof(buffer)) != sizeof(buffer)) {
        std::cerr << "명령어 전송에 실패했습니다: " << strerror(errno) << std::endl;
    } else {
        std::cout << "명령어 '" << command << "'을(를) 보냈습니다." << std::endl;
    }
}

// 프로그램 종료 시 클린업
void cleanup(int signum) {
    std::cout << "\n프로그램을 종료합니다." << std::endl;
    exit(0);
}

// 명령어 파싱 함수
bool parseCommand(const std::string &command, int &arduinoNum, std::string &action) {
    if (command.size() < 4 || command[0] != 'M' || command[2] != '_') return false;

    arduinoNum = command[1] - '0';
    action = command.substr(3);

    return true;
}

int main() {
    signal(SIGINT, cleanup);

    std::string command;
    int address, arduinoNum;
    std::string action;

    std::cout << "M?_ON, M?_OFF, M?_D??으로 명령을 줄 수 있습니다.\n?는 아두이노 번호이고, ??는 깜빡이 딜레이(초)입니다." << std::endl;

    while (true) {
        std::cout << "명령을 입력하세요: ";
        std::cin >> command;

        if (!parseCommand(command, arduinoNum, action)) {
            std::cerr << "잘못된 명령 형식입니다. 형식: M?_ACTION" << std::endl;
            continue;
        }

        // 아두이노 번호에 따라 주소 설정
        if (arduinoNum == 1) {
            address = SLAVE_ADDRESS_1;
        } else if (arduinoNum == 2) {
            address = SLAVE_ADDRESS_2;
        } else {
            std::cerr << "잘못된 아두이노 번호입니다. 1 또는 2를 입력하십시오." << std::endl;
            continue;
        }

        int file = openI2CDevice(address);
        if (file < 0) continue;

        if (action == "P1_1000" || action == "P1_2000") {
            sendCommand(file, action);
        } else if (action == "READ") {
            readData(file);
        } else {
            std::cerr << "알 수 없는 명령입니다: " << action << std::endl;
        }

        close(file);
    }

    cleanup(0);
    return 0;
}

 

 

나중에는 i2c가 기본적으로 100khz로 설정되어있는데 (젯슨, 메가 둘다) 이것을 한번 400khz로 변경해보고 시간적으로 얼마나 차이가 있나 확인해보겠다.

(사실 그냥 궁금해서 해보는거다..)

 

아마 다음 포스팅에서는 서버에 값을 보내보는 것을 해보겠다.
이것은 마스터에서만 진행해주면 됨으로 포스팅이 짧아질 것 같다