아두이노로 I2C 주소 스캔하기

아두이노로 I2C 주소를 스캔하는 기능뿐 아니라
속도까지 모두 찾아주는 코드를 발견하였다.

MultiSpeed I2C Scanner - 50,100,200,400 KHz.
There are several good I2C scanners available for the Arduino. However I missed the feature to scan if a device is reachable at different speeds. So time to roll my own ... (I2C EEPROM 24LC256 attached to I2C bus) when #define PRINTALL false the output looks like Multispeed - I2C Scanner - V0.1 - ADDR ADDR 10 50 100 200 400 [KHz] ----------------------------------------------------------------------- 80 0x50 V V V V V done... when #define PRINTALL true the output looks like Multi…

사용되는 라이브러리

#include <Wire.h>
#include <Arduino.h>

코드의 전역 변수는 다음과 같이 사용된다.

const char version[] = "0.1.06"; // version[7]

// scans devices from 50 to 800kHz I2C speeds.
// lower than 50 is not possible
// DS3231 RTC wroks on 800 KHz. TWBR = 2; (?)

// I2C 속도를 50 ~ 800kHz 변경하며 대상을 스캔한다.
// 50kHz 이하는 정확하지 않거나 지원하지 않는다.
// DS3231 RTC 모듈은 800kHz 작동하였으나 그 이상은 보장되지 않음
// TWBR = 2; AVR 계열의 ATmega328P 에서는
//  I2C 속도를 설정하는 레지스터인 Two-Wire Bit Rate Register 값이 작을수록 빠르다.

const long allSpeed[] = {
    50, 100, 200, 250, 400, 500, 800, 1000
}; // 헤르츠 속도 종류
long speed[sizeof(allSpeed) / sizeof(allSpeed[0])]; // speed[(32 / 4)] = 8개 있음
int speeds;

int addressStart = 0;
int addressEnd = 127;

// Delay Between Tests
// for delay between tests of found devices
// 장치를 찾은 후 잠깐 쉬는 시간이다
#define RESTORE_LATENCY 5 
bool delayFlag = false;

// MINIMIZE OUTPUT
bool printAll = true;
bool header = true;

// START MACHINE
enum states {
    STOP, ONCE, CONT, HELP
};
states state = STOP;

uint32_t startScan;
uint32_t stopScan;

스캔 대상의 I2C 주소를 설정한다.

void setAddress()
{
    if (addressStart == 0) // 스타트가 0인 경우 스텝 건너띄기
    {
        addressStart = 8;
        addressEnd = 120;
    }
    else
    {
        addressStart = 0;
        addressEnd = 127;
    }
    Serial.print(F("<address Range = "));
    Serial.print(addressStart);
    Serial.print(F(".."));
    Serial.print(addressEnd);
    Serial.println(F(">"));
}

스캔 대상의 I2C 헤르츠를 설정한다.

// speed 변수 헤르츠 값 할당하기
void setSpeed(char sp)
{
    switch (sp)
    {
        case '1':
            speed[0] = 100;
            speeds = 1;
            break;
        case '2':
            speed[0] = 200;
            speeds = 1;
            break;
        case '4':
            speed[0] = 400;
            speeds = 1;
            break;
        case '8':
            speed[0] = 800;
            speeds = 1;
            break;
        case '0': // reset
            speeds = sizeof(allSpeed) / sizeof(allSpeed[0]);
            for (int i = 0; i < speeds; i++)
            {
                speed[i] = allSpeed[i]; // allSpeed 헤르츠 8 개 모두 speed 변수로 할당
            }
            break;
    }
}

호출자으로부터 입력받은 문자열을 받는다.

// 시리얼에서 문자 받기
char getCommand()
{
    // 비어있는 경우 종료
    char c = '\0';

    if (Serial.available())
    {
        c = Serial.read();
    }

    return c;
}

프로그램 메뉴판을 호출자에게 전달한다.

// 메뉴 표시
void displayHelp()
{
    Serial.print(F("\nArduino I2C Scanner - "));
    Serial.println(version);
    Serial.println();
    Serial.println(F("Scanmode:"));
    Serial.println(F("\ts = single scan"));
    Serial.println(F("\tc = continuous san - 1 second delay"));
    Serial.println(F("\tq = quit continuous scan"));
    Serial.println(F("\td = toggle latency delay between successful tests. 0 - 5 ms"));
    Serial.println(F("Output:"));
    Serial.println(F("\tp = toggle printAll - printFound."));
    Serial.println(F("\tn = toggle header - noHeader."));
    Serial.println(F("\ta = toggle address range, 0..127 - 8..120"));
    Serial.println(F("Speeds:"));
    Serial.println(F("\t0 = 50 - 800kHz"));
    Serial.println(F("\t1 = 100kHz only"));
    Serial.println(F("\t2 = 200kHz only"));
    Serial.println(F("\t4 = 400kHz only"));
    Serial.println(F("\t8 = 800kHz only"));
    Serial.println(F("\n\t? = help - this page"));
    Serial.println();
}

스캔 대상의 0~127 크기의 주소와 헤르츠만큼 전체 스캔한다.

// I2C 본격적으로 스캔하기
void I2Cscan()
{
    startScan = millis(); // 현재 시간 ms 단위로 저장
    uint8_t count = 0;

    if (header)
    {
        Serial.print(F("TIME\tDEC\tHEX\t")); // 시간 & 10진수 & 18진수 표시
        
        // for: 측정할 대상 kHz 전체 표시 50 - 800khZ 및 100, 200 등등
        for (uint8_t s = 0; s < speeds; s++)
        {
            Serial.print(F("\t"));
            Serial.print(speed[s]);
        }
        Serial.println(F("\t[kHz]"));
        
        for (uint8_t s = 0; s < speeds + 5; s++)
        {
            Serial.print(F("--------"));
        }
        Serial.println();
    }
        
    // TEST
    // 0.1.04: tests only address range 8..120
    // -------------------------------------------
    // Address          R/W Bit Description
    // 0000 000     0 General call address
    // 0000 000     1 START byte
    // 0000 001     X CBUS address
    // 0000 010     X reserved - differnt bus format
    // 0000 011     X reserved - future purposes
    // 0000 1XX     X High speed master code
    // 1111 1XX     X reserved - future purposes
    // 1111 0XX     X 10-bit slave addressing
    //
    // 예약된 I2C 주소 체계이다.
    // 실제 스캔 가능한 주소는 0x08 ~ 0x77 (8 ~ 119 or 120)
    // 
    //   General Call Address: 모든 슬레이브에게 브로드캐스트로 사용
    //             START byte: 특수한 시작 바이트로 예약
    //           CBUS address: CBUS 프로토콜용 (I2C 비슷한 통신 프로토콜)
    //               Reserved: 다른 버스 형식 또는 미래 용도로 예약
    // High-Speed Master Code: I2C 고속 코드로 사용
    //      10-bit Addressing: 7비트 대신 10비트 주소를 사용하는 경우의 시작 코드
    //               Reserved: 확장용 예약 주소
    for (uint8_t address = addressStart; address <= addressEnd; address++)
    {
        bool printLine = printAll; // true to true
        bool found[speeds];
        bool fnd = false;

        for (uint8_t s = 0; s < speeds; s++)
        {
            // 아두이노 버전이 1.5.8
            // setClock I2C 속도 설정. 예) 50KHz = 50 * 1000 = 50000
            // TWBR:
            //  ATmega328P CPU 클럭 주파수는 16000000(16MHz)
            //  TWBR = (16000000 / (50 * 1000) - 16) / 2
            //  TWBR = (16000000 / 50000 - 16) / 2
            //  TWBR = (320 - 16) / 2
            //  TWBR = 304 / 2 = 162
            // Wire.beginTransmission 데이터를 지정한 주소로 전송 준비하기
            //  내부에는 START 조건을 보내 슬레이브 주소를 전송
            //  write() 함수로 실제 데이터 전송할 수 있으나 스캔 용도이기에 아무 데이터도 보내지 않음.
            // Wire.endTransmission I2C 통신을 마치고 STOP 조건 보내기. (0 = ACK, 1~4 = NACK(실패))
            // 슬레이브 장치가 존재하는지 확인. 즉, 이 주소와 주파수에서 장치 발견
#if ARDUINO >= 158
            Wire.setClock(speed[s] * 1000);
#else
            TWBR = (F_CPU / (speed[s] * 1000) - 16) / 2;
#endif
            Wire.beginTransmission (address);
            found[s] = (Wire.endTransmission () == 0); // 통신 성공
            fnd |= found[s]; // true 인 경우 fnd 도 true
            // give device 5 millis
            if (fnd && delayFlag) delay(RESTORE_LATENCY); // 찾은 경우 지연
        }

        if (fnd) count++; // 찾은 경우 카운트 증가
        printLine |= fnd; // 스캔 대상을 찾은 경우 true 할당 / 못찾은 경우 false 할당

        if (printLine) // 찾은 경우
        {
            Serial.print(millis()); // 현재 시간 표시
            Serial.print(F("\t"));
            Serial.print(address, DEC); // 주소값 10진수 표시
            Serial.print(F("\t0x"));
            if (address < 0x10) Serial.print(0, HEX); // 0x0# 표시
            Serial.print(address, HEX); // 주소값 16진수 표시
            Serial.print(F("\t"));

            for (uint8_t s = 0; s < speeds ; s++)
            {
                Serial.print(F("\t"));
                Serial.print(found[s] ? F("V") : F(".")); // 찾은 경우 V 표시. 못찾은 경우 . 표시
            }
            Serial.println();
        }
    }    

    // 지연 시간과 결과 확인
    stopScan = millis(); // ms 시간 표시
    if (header)
    {
        Serial.println();
        Serial.print(count);
        Serial.print(F(" devices found in "));
        Serial.print(stopScan - startScan);
        Serial.println(F(" milliseconds."));
    }
}

아두이노의 loop 함수

void loop()
{
    char command = getCommand();
    switch (command)
    {
        case 's':
            state = ONCE;
            break;
        case 'c':
            state = CONT;
            break;
        case 'd':
            delayFlag = !delayFlag;
            Serial.print(F("<delay="));
            Serial.println(delayFlag ? F("5>") : F("0>"));
            break;
        case 'e':
            // TODO: eeprom test ?
            break;
        case 'h':
            header = !header;
            Serial.print(F("<header="));
            Serial.println(header ? F("yes>") : F("no>"));
            break;
        case 'p':
            printAll = !printAll;
            Serial.print(F("<print="));
            Serial.println(printAll ? F("all>") : F("found>"));
            break;
        
        case '0':
        case '1':
        case '2':
        case '4':
        case '8':
            setSpeed(command);
            break;
        
        case 'a':
            setAddress();
            break;
        
        case 'q':
        case '?':
            state = HELP;
            break;
        default:
            break;
    }
}

아두이노의 loop 가 종료되면 전달받은 state 변수의 상태를 확인하여 스캔 프로그램을 동작시킨다.

switch (state)
{
    case ONCE:
        I2Cscan();
        state = HELP;
        break;
    case CONT:
        I2Cscan();
        delay(1000);
        break;
    case HELP:
        displayHelp();
        state = STOP;
        break;
    case STOP:
        break;
    default: // ignore all non commands
        break;
}