티스토리 뷰

지난 시간에 nRF 칩에서 SDK 로 제공하는 twi driver를 이용해서 센서를 I2C로 제어하는 방법에 대해서 알아봤었는데요, 오늘은 좀더 Low Lever 방식으로 제어하는 방식에 대해서 알아보도록 하겠습니다. 

https://hero-space.tistory.com/93

 

nRF칩에서 I2C 연동하기 twi

I2C라는 통신 방식은 굉장히 일반적인 방식으로 메인 칩과 연결된 센서를 동작시키고 값을 읽어 올때 유용하게 사용할 수 있습니다. 속도가 크게 민감하지 않다면 I2C를 이용해서 조도센서, 거리

hero-space.tistory.com

 왜 Low Lever Driver 가 필요 할까요?

I2C 자체는 필립스에 제안한 IC 간 통신 방식으로 클럭(SCL)과 데이터(SDA)의 2 라인을 사용하는 동기 양방향 2선식 Bus로, 버스에 연결된 각 디바이스는 고유의 어드레스를 갖으며 필요에 따라 receiver와 transmitter로 동작하는데 전송속도는 standard mode에서 100kbps까지이고 fast mode에서는 400kbps까지 가능합니다. 다만, IC에 맞게 살짝씩 튜닝해서 통신하는 경우도 있어서 클럭을 다투는 방식에서는 간헐 적으로 통신이 잘 안되서 데이터를 못 읽어오는 경우가 발생하는데 이럴때 Low Level Driver를 작성해두면 그에맞게 살짝식 변형해서 동작시킬 수 있습니다. 실제 이전 글에서 작성한 nRF SDK에서 제공하는 twi driver로는 예제로 작성한 가속도센서의 경우에도 실시간 x, y, z 값을 을 제대로 읽어오지 못하고 가속도 센서가 움직임이 발생했을 때 Interrupt가 오는 것은 잡아낼 수 있었습니다. 이 문제에 대해서 깊이 알아보기 위해서는 nRF SDK의 twi driver 내부 코드를 디버깅해야할 필요가 있는데 고치는 것이 맞다면 이 후에 SDK 버전을 업데이트 하는 경우 수정한 내용이 머지되지 못한다면 이후에 또 문제가 발생할 수 있겠지요. 

I2C로 연결된 IC의 상세 통신 방식을 알아봅시다.

* start condition
클럭과 데이터가 모두 high인 일반 상태에서 데이타 라인이 low로 떨어지는 상태.

* stop condition
클럭이 high인 동안에 데이타 라인이 low에서 high로 바뀌는 상태.

* 일반 데이터 전송에서는 클럭이 low일 때만 데이타의 상태를 바꿀 수 있습니다. 따라서 클럭이 high인 구간에만 유효한 데이타이며 master는 먼저 데이타 라인을 적당한 데이타로 바꾸고 클럭 라인을 일정시간 high로 하고 다음 SDA 라인의 상태를 바꾸기 전에 클럭을 low로 만들어야 합니다. 데이타는 8bit이고 MSB가 먼저 전송되는데 start 와 stop condition 사이에 전송되는 데이타의 수는 제한이 없습니다.

* acknowledge 조건
master든 slave든 transmitter는 8 bit 전송을 마치고 버스선을 high로 합니다. 9번째 사이클 동안 receiver는 데이타 선을 low로 유지하여 acknowledge 를 표시합니다.

아래는 mc3416에 데이터 시트에 나온 Read/Write 시 I2C 메시지 포맷인데, 아래와 같은 방식으로 데이터를 읽고 쓰고 해야 정상적으로 동작하고 각각의 클럭이 제어되는 것을 일일히 만들어 내는 것이 Low Lever의 방법이라고 볼 수 있습니다.

* Read

* Write

우선 standard mode로 할 것인지, fast mode로 할 것인지를 결정해야 하는지라 가장 기본적인 standard mode로 Low Lever Driver를 만들어 보도록 하겠습니다. 속도 관련에서는 delay 함수를 만드는 것이 가장 중요합니다.

delay 함수는 어떤 것을 써야하고, Standard mode를 위해선 어느정도의 delay를 주어야 할까요?

각각의 시스템마다 delay 가 지원하는 규모가 다르고, 물리적인 클럭이 다르기 때문에 잘 계산해서 넣어주어야 하며, 이부분이 Low Lever Driver를 개발할 때 가장 까다로운 것이므로 잘 고려해야합니다. 제가 만드는 시스템의 32Mhz의 외부 크리스털을 사용하므로 아래와 같이 게산해 볼 수 있습니다. 100khz의 경우 4~5마이크로세크가 딜레이에 적합한 시간인데, ms만을 지원하는 시스템이 있다보니 마이크로세크의 딜레이를 만들려면 직접 만들필요가 있는데 아래와 같이 만들 수 있습니다.

  • 외부크리스털 32Mhz를 사용 : 1클럭/32Mhz = 0.00000003125 / sec
  • 어셈 명령어는 1개당 4개의 클럭을 사용 : fetch, decode, excute, write-back
    • 일반적인 어셈 명령어 1개는 4 * 0.00000003125 = 0.000000125 초 → 0.125 마이크로 세크
    • 32 x 0.125 = 4 마이크로 세크, 즉 NOP 명령어 32개로 4마이크로 세크의 delay를 구현 적용
inline static void i2c_delay(void)
{
    __asm volatile(
        "nop                    \n"
        "nop                    \n"
        "nop                    \n"
        "nop                    \n"
        "                       \n"
        "nop                    \n"
        "nop                    \n"
        "nop                    \n"
        "nop                    \n"
        "                       \n"
        "nop                    \n"
        "nop                    \n"
        "nop                    \n"
        "nop                    \n"
        "                       \n"
        "nop                    \n"
        "nop                    \n"
        "nop                    \n"
        "nop                    \n"
        "                       \n"
        "nop                    \n"
        "nop                    \n"
        "nop                    \n"
        "nop                    \n"
        "                       \n"
        "nop                    \n"
        "nop                    \n"
        "nop                    \n"
        "nop                    \n"
        "                       \n"
        "nop                    \n"
        "nop                    \n"
        "nop                    \n"
        "nop                    \n"
        "                       \n"
        "nop                    \n"
        "nop                    \n"
        "nop                    \n"
        "nop                    \n"
        "                       \n"
        "bx lr                  \n"
    );
}

delay 함수를 만들었으면 기본적인 i2c_start, stop, restart 함수를 아래와 같이 만들 수 있습니다.

void i2c1_start(void)
{
    nrf_gpio_pin_set(GPIO_I2C1_SDA);
    nrf_gpio_pin_set(GPIO_I2C1_SCL);
    
    nrf_gpio_pin_clear(GPIO_I2C1_SDA);
    nrf_gpio_pin_clear(GPIO_I2C1_SCL);
}

void i2c1_restart(void)
{
    nrf_gpio_cfg_output(GPIO_I2C1_SDA);
    nrf_gpio_pin_clear(GPIO_I2C1_SDA);
    nrf_gpio_pin_clear(GPIO_I2C1_SCL);
    
    nrf_gpio_pin_set(GPIO_I2C1_SDA);
    nrf_gpio_pin_set(GPIO_I2C1_SCL);

    i2c1_start();
}

void i2c1_stop(void)
{
    nrf_gpio_pin_clear(GPIO_I2C1_SDA);
    nrf_gpio_pin_clear(GPIO_I2C1_SCL);
    
    nrf_gpio_pin_set(GPIO_I2C1_SCL);
    nrf_gpio_pin_set(GPIO_I2C1_SDA);
}

그리고 send data와 read data 하는 부분은 8bit를 읽어갈 수 있도록 아래와 같이 포트제어를 합니다.

void i2c1_send_data(uint8_t Data)
{
    uint8_t index = 8;
        
    nrf_gpio_pin_clear(GPIO_I2C1_SCL);
        
    while(index--)
    {
        if((Data & 0x80) == 0)
        {
            nrf_gpio_pin_clear(GPIO_I2C1_SDA);
        }
        else
        {
            nrf_gpio_pin_set(GPIO_I2C1_SDA);
        }
        
        nrf_gpio_pin_set(GPIO_I2C1_SCL);
        i2c_delay();
    
        nrf_gpio_pin_clear(GPIO_I2C1_SCL);
        i2c_delay();

        Data <<= 1;
    }
}

uint8_t i2c1_read_data(void)
{
    uint8_t index = 8;
    uint8_t data = 0;

    nrf_gpio_cfg_input(GPIO_I2C1_SDA, NRF_GPIO_PIN_PULLUP);
    nrf_gpio_pin_clear(GPIO_I2C1_SCL);	  	
    
    while(index--)
    {
        data <<= 1;
        
        nrf_gpio_pin_set(GPIO_I2C1_SCL);
        i2c_delay();
        
        if(nrf_gpio_pin_read(GPIO_I2C1_SDA) == HIGH)
        {
            data |= HIGH;
        }
        
        nrf_gpio_pin_clear(GPIO_I2C1_SCL);
        i2c_delay();
    }

    nrf_gpio_cfg_output(GPIO_I2C1_SDA);

    return data;
}

기본적으로 위와같은 Low Level Driver를 작성하면 nrf SDK의 twi driver가 제공하는 것처럼 I2C를 이용해서 센서와 같은 IC를 제어할 수 있게됩니다. 실제 위 방식을 통해 twi driver를 통해 나오지 않았던 실시간 x, y, z 값이 정상적으로 나오는 것을 확인했습니다. 좀더 상세한 내용이 궁금하시다면 댓글 또는 연락주세요!

댓글