티스토리 뷰

I2C라는 통신 방식은 굉장히 일반적인 방식으로 메인 칩과 연결된 센서를 동작시키고 값을 읽어 올때 유용하게 사용할 수 있습니다. 속도가 크게 민감하지 않다면 I2C를 이용해서 조도센서, 거리센서, 가속도센서, 압력센서 등을 붙여 다양한 시나리오를 만들어 볼수 있고 정리하면 아래와 같은 특징이 있습니다.

  • 2개의 선을 이용하는 통신 방식
  • 하나의 마스터와 여러개의 슬레이브 기기가 연결되어 통신이 가능
  • 클럭 신호를 사용하는 동기식 통신 방식이라 시간에 자유로움
  • 슬레이브 선택을 위해 항상 주소 데이터가 붙기에 긴 데이터를 전송 하기에 부적합

마스터와 슬레이브를 규정하고, SDA 선(데이터를 주고 받기 위한 선)과 SCL 선(송수신 타이밍 동기화를 위한 클럭 선)을 통해서 통신하며 슬레이브는 127개 까지 연결 할 수 있지만 저도 이렇게 까지 많이 연동해본 적은 없습니다.

이런 통신 프로토콜을 위해 노르딕에서 제공하고 있는 것은 없을까요? 당연히 있습니다. SDK 안에서 예제 프로젝트도 쉽게 찾을 수 있는데 I2C라는 이름이 아닌 TWI 라는 이름으로 예제가 되어 있습니다. 간단하게 한번 살펴보도록 하겠습니다.

/** @file
 * @defgroup tw_sensor_example main.c
 * @{
 * @ingroup nrf_twi_example
 * @brief TWI Sensor Example main file.
 *
 * This file contains the source code for a sample application using TWI.
 *
 */

#include <stdio.h>
#include "boards.h"
#include "app_util_platform.h"
#include "app_error.h"
#include "nrf_drv_twi.h"
#include "nrf_delay.h"


#include "nrf_log.h"
#include "nrf_log_ctrl.h"
#include "nrf_log_default_backends.h"

/* TWI instance ID. */
#define TWI_INSTANCE_ID     0

/* Common addresses definition for temperature sensor. */
#define LM75B_ADDR          (0x90U >> 1)

#define LM75B_REG_TEMP      0x00U
#define LM75B_REG_CONF      0x01U
#define LM75B_REG_THYST     0x02U
#define LM75B_REG_TOS       0x03U

/* Mode for LM75B. */
#define NORMAL_MODE 0U

/* Indicates if operation on TWI has ended. */
static volatile bool m_xfer_done = false;

/* TWI instance. */
static const nrf_drv_twi_t m_twi = NRF_DRV_TWI_INSTANCE(TWI_INSTANCE_ID);

/* Buffer for samples read from temperature sensor. */
static uint8_t m_sample;

/**
 * @brief Function for setting active mode on MMA7660 accelerometer.
 */
void LM75B_set_mode(void)
{
    ret_code_t err_code;

    /* Writing to LM75B_REG_CONF "0" set temperature sensor in NORMAL mode. */
    uint8_t reg[2] = {LM75B_REG_CONF, NORMAL_MODE};
    err_code = nrf_drv_twi_tx(&m_twi, LM75B_ADDR, reg, sizeof(reg), false);
    APP_ERROR_CHECK(err_code);
    while (m_xfer_done == false);

    /* Writing to pointer byte. */
    reg[0] = LM75B_REG_TEMP;
    m_xfer_done = false;
    err_code = nrf_drv_twi_tx(&m_twi, LM75B_ADDR, reg, 1, false);
    APP_ERROR_CHECK(err_code);
    while (m_xfer_done == false);
}

/**
 * @brief Function for handling data from temperature sensor.
 *
 * @param[in] temp          Temperature in Celsius degrees read from sensor.
 */
__STATIC_INLINE void data_handler(uint8_t temp)
{
    NRF_LOG_INFO("Temperature: %d Celsius degrees.", temp);
}

/**
 * @brief TWI events handler.
 */
void twi_handler(nrf_drv_twi_evt_t const * p_event, void * p_context)
{
    switch (p_event->type)
    {
        case NRF_DRV_TWI_EVT_DONE:
            if (p_event->xfer_desc.type == NRF_DRV_TWI_XFER_RX)
            {
                data_handler(m_sample);
            }
            m_xfer_done = true;
            break;
        default:
            break;
    }
}

/**
 * @brief UART initialization.
 */
void twi_init (void)
{
    ret_code_t err_code;

    const nrf_drv_twi_config_t twi_lm75b_config = {
       .scl                = ARDUINO_SCL_PIN,
       .sda                = ARDUINO_SDA_PIN,
       .frequency          = NRF_DRV_TWI_FREQ_100K,
       .interrupt_priority = APP_IRQ_PRIORITY_HIGH,
       .clear_bus_init     = false
    };

    err_code = nrf_drv_twi_init(&m_twi, &twi_lm75b_config, twi_handler, NULL);
    APP_ERROR_CHECK(err_code);

    nrf_drv_twi_enable(&m_twi);
}

/**
 * @brief Function for reading data from temperature sensor.
 */
static void read_sensor_data()
{
    m_xfer_done = false;

    /* Read 1 byte from the specified address - skip 3 bits dedicated for fractional part of temperature. */
    ret_code_t err_code = nrf_drv_twi_rx(&m_twi, LM75B_ADDR, &m_sample, sizeof(m_sample));
    APP_ERROR_CHECK(err_code);
}

/**
 * @brief Function for main application entry.
 */
int main(void)
{
    APP_ERROR_CHECK(NRF_LOG_INIT(NULL));
    NRF_LOG_DEFAULT_BACKENDS_INIT();

    NRF_LOG_INFO("\r\nTWI sensor example started.");
    NRF_LOG_FLUSH();
    twi_init();
    LM75B_set_mode();

    while (true)
    {
        nrf_delay_ms(500);

        do
        {
            __WFE();
        }while (m_xfer_done == false);

        read_sensor_data();
        NRF_LOG_FLUSH();
    }
}

/** @} */

 

이 예제는 peripheral 안에 twi_scanner 라는 것인데 LM75B라는 특정 이름나오고 검색해보니 온도 센서임을 알 수 있습니다.

https://www.nxp.com/docs/en/data-sheet/LM75B.pdf

nrf SDK에서 제공하는 twi 라는 드라이버를 사용하여 LM75B의 정보를 주기적으로 읽어 오는 것인데 메인 코드만 보면 twi_init() 하고 LM75B_set_mode를 통해 설정을 해주고 while 문안에서 500ms 폴링 방식으로 read_sensor_data()를 통해 값을 읽어 오고 있습니다. 마스터 슬레이브 이런이야기를 앞서 했지만 SDK 단순히 instance를 생성해서 그 안에서 이걸 커버해주고 있어서 굉장히 쉽게 제어가 가능하게 느껴집니다. 다만 twi_init()을 자세히 보면 핀 설정과, frequency 설정등 자신이 구축한 시스템에 원하는 스펙으로 동작하도록 해야하는데 너무 블랙박스 식이다보니 과연 모든 센서와의 타이밍을 nrf가 제공하는 SDK에서 잘 맞춰 줄 수 있을지 살짝 걱정이됩니다. 우선 I2C는 크게 스탠다드 모드와  패스트모드가 있어서 100Khz와 400Khz로 나눌 수 있는데, 기본모드로 많이 사용하기에 이부분은 나두고 제가 구축한 하드웨어 클럭 소스가 문제없는지 확인이 해봐야 할 것 같습니다. I2C 통신시 가장 중요한 것은 타이밍이고, 타이밍은 이런 클럭속도에 따라서 좌지우지되는데 100Khz로 동작할 수 있도록 제어해주어야하나, 외부클럭/내부클럭 설정이 달라서 nrf sdk과연 이걸 잘 해줄지 굉장히 의심됩니다. 또 한가지 I2C가 통신프로토콜 규격화해서 이를 따르라고 하고있지만 센서마다 규격을 조금씩 상이하게 해서 안맞을 때도 있는데 nrf SDK안에 twi를 직접 수정하지 않는 이상 사용하는 관점에서 이를 맞출 수는 없어보여 우려가 됩니다.

우선, LM75B는 제가 가지고 있지 않아서 가속도 센서를 이용한 코드를 작성해보도록 하겠습니다.

void twi_acc_init(void)
{
    ret_code_t err_code;

    const nrf_drv_twi_config_t twi_config = {
       .scl                = GPIO_I2C1_SCL,
       .sda                = GPIO_I2C1_SDA,
       .frequency          = NRF_DRV_TWI_FREQ_100K,
       .interrupt_priority = APP_IRQ_PRIORITY_HIGH,
       .clear_bus_init     = false
    };

    err_code = nrf_drv_twi_init(&m_twi, &twi_config, twi_handler, NULL);
    APP_ERROR_CHECK(err_code);

    nrf_drv_twi_enable(&m_twi);
}

Init 코드는 위와 같이 해서 간단하게 작성하고 twi_handler는 아래와 같이 작성합니다.

void twi_handler(nrf_drv_twi_evt_t const * p_event, void * p_context)
{
    switch (p_event->type)
    {
        case NRF_DRV_TWI_EVT_DONE:
            if (p_event->xfer_desc.type == NRF_DRV_TWI_XFER_RX) {
                if (!acc_lock) {
                    data_handler(m_sample);
                }
            }
            m_xfer_done = true;
            break;
        default:
            break;
    }
}

센서에 대한 초기 세팅은 아래와 같이 진행하고

void accel_mc3416_setup(void)
{
	uint8_t index;

	twi_send(MC3416_ADDR, g_add_reg[index].addr, g_add_reg[index].data);
	nrf_delay_ms(20);

	if(acc_read_byte(g_add_reg[index].addr) != g_add_reg[index].data)
	{
		return;
	}
	twi_read(MC3416_ADDR, g_add_reg[index].addr);
	nrf_delay_ms(20);
}

마지막으로 read 를 아래와 같이 구현해서 출력해보도록 합니다.

void periph_accel_read(acc_data_st *acc)
{
	uint8_t data[6];

	acc_enable();
	twi_lock_data(FALSE);

	twi_read(MC3416_ADDR, XOUT_EX_L);
	data[0] = m_sample;
	nrf_delay_ms(5);

	twi_read(MC3416_ADDR, XOUT_EX_H);
	data[1] = m_sample;
	nrf_delay_ms(5);

	twi_read(MC3416_ADDR, YOUT_EX_L);
	data[2] = m_sample;
	nrf_delay_ms(5);

	twi_read(MC3416_ADDR, YOUT_EX_H);
	data[3] = m_sample;
	nrf_delay_ms(5);

	twi_read(MC3416_ADDR, ZOUT_EX_L);
	data[4] = m_sample;
	nrf_delay_ms(5);

	twi_read(MC3416_ADDR, ZOUT_EX_H);
	data[5] = m_sample;
	nrf_delay_ms(5);

	data[0] = acc_read_byte(XOUT_EX_L);
	data[1] = acc_read_byte(XOUT_EX_H);
	data[2] = acc_read_byte(YOUT_EX_L);
	data[3] = acc_read_byte(YOUT_EX_H);
	data[4] = acc_read_byte(ZOUT_EX_L);
	data[5] = acc_read_byte(ZOUT_EX_H);
	twi_lock_data(TRUE);

	acc_disable();

	acc->ax = (int16_t) (data[0] | ((uint16_t) data[1] << 8));
	acc->ay = (int16_t) (data[2] | ((uint16_t) data[3] << 8));
	acc->az = (int16_t) (data[4] | ((uint16_t) data[5] << 8));
}

결과는, acc->ax, acc->ay, acc->az 값을 제대로 읽어오지 못하고 있습니다. twi driver 자체의 문제는 아니라고 보여지는데, 사용하는 센서가 처음 시작시 우려한 규격이 살짝 안맞아서 이러는 것일지, 클럭소스에 잘 설정되어서 타이밍이 원하는대로 제대로 동작하고 있을지 여러가지 문제점이 예상되나, twi 드라이버 자체를 직접 손대서 하는 것은 좋은 방향성이 아니라서 다음 시간에 low driver를 통해서 센서를 제어하는 방법으로 진행해보도록 하겠습니다.

SDK 제공자체는 굉장히 노력이 수반되나 하드웨어를 컨트롤하는 SDK는 이처럼 모든 하드웨어와 확인이 되지 않았다면 장담할 수 없고 각각에 포팅작업이 필요합니다. 따라서 이번에 제가 사용한 센서자체도 기본적으로는 SDK를 통해서 쉽게 되는게 맞으나, 한가지만 비끗해도 제대로 정보를 읽어오고 세팅할 수 없으므로, 좀더 low level의 개발을 통해서 면밀히 확인할 필요가 있습니다.

댓글