[nRF52840, TWI를 사용한 MLX90614 제어]

https://rs29.tistory.com/16

 

nRF52 - TWI 이용 MLX90614 적외선 비접촉 온도 측정 센서

사용 보드 : nRF52840-PDK 사용 IDE : SEGGER Embedded Studio for ARM (노르딕 제품 사용하는 경우 무료 라이센스 이용 가능) 작동 전압 : 2.6~3.6V (MLX90614Bxx, MLX90614Dxx 모델 기준, MLX90614Axx은 4.5~5...

rs29.tistory.com

[개발 환경]

- 개발 보드 : nRF52840-PDK

- IDE : SEGGER Embedded Studio for ARM

- 센서 : MLX90614BCC, 통신 TWI (I2C) 사용

- 무선 통신 : BLE (UART 사용)

- SDK 예제 중 twi_master_using_nrf_twi_mngr(TWI 통신), ble_app_uart(BLE+UART 송수신), ble_app_hrs_freertos(FreeRTOS) 를 기반으로 작성

- 대부분의 코드(BLE, UART 등의 설정)는 예제 그대로 사용

- BLE 통신을 통해 명령어를 수신 및 실행하거나 온도, 방사율 값을 전송함

- 참고한 사이트 : https://www.freertos.org/a00106.html

 

[작동]

- 상시 작동할 태스크는 3개 (보드 0번 LED 토글, MLX90614 온도 측정, 측정된 온도 BLE 통신 전송)

- 각 태스크 후미에 사용한 vTaskDelay()에 입력한 시간 간격마다 LED 토글 및 MLX90614 Object 1 Temperature 레지스터 Read 함수 호출

- MLX90614로부터 온도 데이터를 수신한 직후 호출되는 콜백 함수에서 측정한 온도값을 전송할 BLE 전송 태스크에 알림 및 읽어 온 온도 데이터 전달 (BLE 전송 태스크는 if문을 사용해 알림을 수신하기 전까지는 BLE 전송이 실행되지 않음)

- BLE 전송 태스크에서 알림 및 데이터를 획득한 뒤, 온도 데이터를 섭씨 온도로 변환 및 BLE 통해 전송

 

- BLE 통신을 통해 MLX90614를 제어하는데 사용할 각종 명령어 및 명령어 데이터 수신

- 방사율을 전송할 경우, 그에 필요한 태스크들을 생성한 뒤 삭제 (지속적으로 갱신할 필요가 없는 데이터이기 때문)

 

[코드]

<FreeRTOS 태스크 핸들 생성>

static TaskHandle_t m_led_toggle_thread;		//nRF52840-DK LED0 토글
static TaskHandle_t m_mlx_rd_temp_thread;	//MLX90614 온도 Read
static TaskHandle_t m_mlx_rd_emi_thread;		//MLX90614 방사율 Read
static TaskHandle_t m_ble_mlx_temp_thread;	//읽어 온 온도를 BLE 통신을 사용해 전송
static TaskHandle_t m_ble_mlx_emi_thread;	//읽어 온 방사율을 BLE 통신을 사용해 전송

#if NRF_LOG_ENABLED
static TaskHandle_t m_logger_thread;	//Log 확인
#endif

- 태스크 핸들 생성 (태스크 생성, 중단, 삭제 등 API 호출 시 태스크를 참조하는 데 사용)

- sdk_config.h 에서 NRF_LOG_ENABLED 1 로 설정했을 경우, nrf_log module configuration 내에 활성화한 각종 드라이버 및 라이브러리들의 로그가 출력됨 (J-Link RTT Viewer 사용해 확인 가능)

 

<태스크 생성 함수>

static void task_create_func(void)
{
    if (pdPASS != xTaskCreate(mlx_rd_temp_thread, "MLX90614_RD_TEMP", configMINIMAL_STACK_SIZE+30, NULL, 2, &m_mlx_rd_temp_thread))
    {
        APP_ERROR_HANDLER(NRF_ERROR_NO_MEM);
    }

    if (pdPASS!=xTaskCreate(led_toggle_thread, "LED0", configMINIMAL_STACK_SIZE + 10, NULL, 2, &m_led_toggle_thread))
    {
        APP_ERROR_HANDLER(NRF_ERROR_NO_MEM);
    }

    if (pdPASS != xTaskCreate(ble_mlx_temp_thread, "BLE_MLX_TEMP", configMINIMAL_STACK_SIZE+30, NULL, 2, &m_ble_mlx_temp_thread))
    {
        APP_ERROR_HANDLER(NRF_ERROR_NO_MEM);
    }

//sdkconfig.h - NRF_LOG_ENABLED 1 로 활성화했을 때, Log 출력 태스크 생성
#if NRF_LOG_ENABLED
    if (pdPASS!=xTaskCreate(logger_thread, "LOGGER", 256, NULL, 1, &m_logger_thread))
    {
        APP_ERROR_HANDLER(NRF_ERROR_NO_MEM);
    }
#endif
}

태스크 생성 예)

- mlx_rd_temp_thread : 태스크를 실행할 함수명

- MLX90614_RD_TEMP : 태스크 설명 이름 (디버깅용)

- configMINIMAL_STACK_SIZE+30 : 태스크에 할당할 스택 사이즈

  (configMINIMAL_STACK_SIZE=60)

  (byte가 아닌 word 기준 (32bit MCU의 경우, 1 word=32bit), 스택 사이즈 90=360 bytes)

- NULL : 생성한 태스크에 전달 될 매개변수

- 2 : 태스크 우선 순위

- &m_mlx_rd_temp_thread : 태스크 핸들

 

<MLX90614 온도 Read 태스크>

static void mlx_rd_temp_thread(void *arg)
{
    mlx90614_reg_data_t obj_1_temp_struct={0,0,MLX90614_REG_OBJECT_1_TEMP};
    
    while(1)
    {
        mlx90614_reg_read(&mlx90614_instance,&obj_1_temp_struct,mlx90614_read_temp_cb);
        vTaskDelay(1000);
    }
}

- 센서로부터 온도 데이터를 읽어 오는데 사용할 구조체 변수 선언 및 Object 1 Temperature 레지스터 주소 설정

- MLX90614 레지스터 Read 함수 호출

- vTaskDelay(1000) : 1000ms 마다 태스크 실행

 

<MLX90614 온도 콜백 함수>

//MLX90614 - Object 1 레지스터 데이터 수신 완료 직후에 호출되는 함수
void mlx90614_read_temp_cb(ret_code_t result, mlx90614_reg_data_t * p_raw_data)
{
    uint8_t crc_value;
    
    if (result != NRF_SUCCESS)
    {
        NRF_LOG_WARNING("Read Temperature Callback Error : %d", (int)result);
        return;
    }

    if(p_raw_data->pec!=mlx90614_crc_cal(p_raw_data,MLX90614_Read))
    {
        NRF_LOG_WARNING("MLX90614 Read temperature CRC doesn't match");
        NRF_LOG_INFO("PEC : %X  CRC : %X",p_raw_data->pec,mlx90614_crc_cal(p_raw_data,MLX90614_Read));
    }
    else
    {
    	/*
    	TWI 통신을 통해 성공적으로 Object 1 레지스터에 저장된 온도 데이터를 수신 완료하고
        CRC 계산을 통과하면 BLE 통신을 통해 온도값을 전송하는 태스크에 알림 및 읽어 온 온도 데이터 전송
        m_ble_mlx_temp_thread : 알림을 수신할 태스크 생성에 사용된 태스크 핸들
        p_raw_data->reg_data : 전달할 데이터 (최대 32bit까지만 가능)
        eSetValueWithOverwrite : 전달될 데이터의 행동 설정 (무조건적으로 p_raw_data->reg_data 사용 설정)
        */
        xTaskNotify(m_ble_mlx_temp_thread, p_raw_data->reg_data, eSetValueWithOverwrite);
    }
}

- MLX90614의 온도 레지스터로부터 데이터를 읽어 온 직후 호출되는 함수

- 데이터 수신에 성공 및 CRC 계산값이 일치한다면 BLE 통신을 통해 온도값을 전송하는 태스크에 알림을 전송

- xTaskNotify의 경우, 최대 32bit의 데이터를 알림과 함께 전달 가능하므로 온도 데이터값을 가지고 있는 p_raw_data->reg_data의 값을 알림과 함께 전달

 

<BLE - 온도 전송 태스크>

/*
- xTaskNotify()를 통해 알림이 전달되었을 경우에만 전달된 데이터를 온도값으로 변환한 뒤
  BLE 통신을 통해 온도값을 전송하는 태스크
*/
static void ble_mlx_temp_thread(void *arg)
{
    ret_code_t err_code;
    char data_array[6]={};	//변환된 온도 데이터를 BLE 통신을 통해 전송할 때 사용할 배열
    				//정수 세 자리 + '.'+ 소수점 두자리 = 6
    double temperature;		//변환한 온도를 저장할 변수
    uint16_t temp;		//xTaskNotify를 통해 알림과 같이 전달된 온도 데이터(p_raw_data->reg_data)를 저장할 변수
    uint16_t length=sizeof(data_array);	//BLE 통신을 통해 전송할 데이터의 길이

    while(1)
    {
    	/*
    	- xTaskNotify()를 통해 전달된 알림이 있을 경우에만 ulTaskNotifyTake()를 통해 획득한
          데이터가 0이 아닌 값을 갖는다
        - 알림을 수신할 때까지 최대 10ms동안 Blocked 상태로 대기
          (Blocked 상태일 때 CPU 시간 소모 X)
        */
        if((temp=ulTaskNotifyTake(pdTRUE,10))!=0)
        {
            temperature=mlx90614_temp_conversion(temp);
            //UART 통신을 사용하므로 변환된 온도 데이터를 sprintf()함수를 이용해 문자열로 저장 후 전송
            sprintf(data_array,"%3.2f",temperature);
            
            //BLE 통신 연결이 된 상태 & MLX90614로부터 읽어 온 데이터가 유효할 때 온도값 전송
            //(MLX90614가 유효한 데이터를 측정하기 전에 데이터를 읽어 올 경우 온도 데이터=-273.15)
            if((m_conn_handle!=BLE_CONN_HANDLE_INVALID)&&(temperature!= -273.15))
            {
                err_code=ble_nus_data_send(&m_nus, data_array, &length, m_conn_handle);
                if ((err_code != NRF_ERROR_INVALID_STATE) &&
                    (err_code != NRF_ERROR_RESOURCES) &&
                    (err_code != NRF_ERROR_NOT_FOUND))
                {
                    APP_ERROR_CHECK(err_code);
                }
            }
        }
    }
}

 

<MLX90614 방사율 Read 태스크>

static void mlx90614_rd_emi_thread(void *arg)
{
    mlx90614_reg_data_t emissivity_struct={0,0,MLX90614_REG_EMISSIVITY_1};
    while(1)
    { 
        mlx90614_reg_read(&mlx90614_instance, &emissivity_struct, mlx90614_read_emissivity_cb);
        vTaskDelete(m_mlx_rd_emi_thread);
    }
}

- 방사율을 읽어 오는데 사용할 구조체 변수 선언 및 Emissivity 1 레지스터 주소 설정

- MLX90614 레지스터 Read 함수를 호출하고 태스크 삭제

- 방사율의 경우, 주기적으로 읽어 올 필요가 없기 때문에 필요한 경우에만 태스크를 생성 및 삭제

 

<MLX90614 방사율 콜백 함수>

void mlx90614_read_emissivity_cb(ret_code_t result, mlx90614_reg_data_t * p_raw_data)
{
    uint8_t crc_value;

    if (result != NRF_SUCCESS)
    {
        NRF_LOG_WARNING("Read Emissivity Callback Error : %d", (int)result);
        return;
    }    

    if(p_raw_data->pec!=mlx90614_crc_cal(p_raw_data,MLX90614_Read))
    {
        NRF_LOG_WARNING("MLX90614 Read emissivity CRC doesn't match");
        NRF_LOG_INFO("PEC : %X  CRC : %X",p_raw_data->pec,mlx90614_crc_cal(p_raw_data,MLX90614_Read));
    }
    else
    {   
        xTaskNotify(m_ble_mlx_emi_thread, p_raw_data->reg_data, eSetValueWithOverwrite);
    }
}

- 온도 콜백 함수와 작동 방식 동일

 

<BLE - 방사율 전송 태스크>

static void ble_mlx90614_emi_thread(void *arg)
{
    ret_code_t err_code;
    static char data_array[4]={NULL};	//정수 한자리 + '.' + 소수점 두자리 = 4
    static float emissivity;
    static uint16_t temp;
    static uint16_t length=sizeof(data_array);

    while(1)
    {
        if((temp=ulTaskNotifyTake(pdTRUE,(TickType_t)10))!=0)
        {
            emissivity=mlx90614_emissivity_conversion(temp);
            sprintf(data_array,"%.2f",emissivity);

            if((m_conn_handle!=BLE_CONN_HANDLE_INVALID))
            {
                err_code=ble_nus_data_send(&m_nus, data_array, &length, m_conn_handle);
                if ((err_code != NRF_ERROR_INVALID_STATE) &&
                    (err_code != NRF_ERROR_RESOURCES) &&
                    (err_code != NRF_ERROR_NOT_FOUND))
                {
                    APP_ERROR_CHECK(err_code);
                }
            }
            vTaskDelete(m_ble_mlx_emi_thread);
        }
    }
}

- 마지막에 vTaskDelete() 함수로 전송 완료 후 방사율 전송 태스크 삭제

- 나머지 부분은 온도 전송 태스크와 동일

 

<BLE를 통한 UART 송수신에 사용되는 함수>

- SDK 예제 중 ble_app_uart의 코드 활용

static void nus_data_handler(ble_nus_evt_t * p_evt)
{
    char str_cmd[]={"000"};	//수신한 명령어 저장
    char str_data[]={"000"};	//수신한 데이터 저장 (명령어+명령어에 필요한 데이터 조합)

    //BLE UART를 통해 데이터를 수신했을 때
    if (p_evt->type == BLE_NUS_EVT_RX_DATA)
    {
        uint32_t err_code;

        NRF_LOG_DEBUG("Received data from BLE NUS. Writing data on UART.");
        NRF_LOG_HEXDUMP_DEBUG(p_evt->params.rx_data.p_data, p_evt->params.rx_data.length);

        for (uint32_t i = 0; i < p_evt->params.rx_data.length; i++)
        {
            do
            {
                err_code = app_uart_put(p_evt->params.rx_data.p_data[i]);
                if ((err_code != NRF_SUCCESS) && (err_code != NRF_ERROR_BUSY))
                {
                    NRF_LOG_ERROR("Failed receiving NUS message. Error 0x%x. ", err_code);
                    APP_ERROR_CHECK(err_code);
                }
            } while (err_code == NRF_ERROR_BUSY);
        }
        if (p_evt->params.rx_data.p_data[p_evt->params.rx_data.length - 1] == '\r')
        {
            while (app_uart_put('\n') == NRF_ERROR_BUSY);
        }
        //수신한 데이터의 1~3번째 바이트를 명령어 배열(str_cmd)에 저장
        memcpy(str_cmd,p_evt->params.rx_data.p_data,3);
        //수신한 데이터의 4~6번째 바이트를 데이터 배열(str_data)에 저장
        if(3<p_evt->params.rx_data.length)
        {
            memcpy(str_data,&p_evt->params.rx_data.p_data[3],3);
        }
    }
    
    /*
    - 수신한 명령어가 있을 때, MLX90614 명령어 실행 함수 호출
    BLE 데이터 송신으로 핸들러 함수가 호출되었을 때는 str_cmd 배열의 값이 "000"을 유지하므로
    MLX90614에 명령어를 실행하는 함수가 실행되지 않는다
     */
    if(strcmp(str_cmd,"000")!=0)
    {
        mlx90614_cmd_func(str_cmd, str_data);
    }
}

- BLE를 통해 UART 방식의 통신을 전송, 수신했을 때 호출되는 핸들러 함수

- MLX90614를 제어하는데 사용할 각종 명령어 및 데이터를 수신 및 배열에 저장한 뒤 제어 함수 호출

 

<MLX90614 명령어 실행 함수>

void mlx90614_cmd_func(char *str_cmd, char *str_data)
{
    if(strcmp(str_cmd,"REM")==0)   //Read Emissivity
    {
        if (pdPASS != xTaskCreate(mlx90614_rd_emi_thread, "MLX90614_RD_EMI", configMINIMAL_STACK_SIZE+10, NULL, 2, &m_mlx_rd_emi_thread))
        {
            APP_ERROR_HANDLER(NRF_ERROR_NO_MEM);
        }
        if (pdPASS != xTaskCreate(ble_mlx90614_emi_thread, "MLX90614_BLE_RD_EMI", configMINIMAL_STACK_SIZE+30, NULL, 2, &m_ble_mlx_emi_thread))
        {
            APP_ERROR_HANDLER(NRF_ERROR_NO_MEM);
        }
    }
    else
    {
        
    }
}

- "REM" 문자열을 수신한 경우, MLX90614로부터 방사율을 읽어오는 태스크와 읽어 온 방사율 데이터를 BLE 통신을 통해 전송하는 태스크를 생성

 

<Led 토글 태스크>

static void led_toggle_thread(void * pvParameter)
{
    UNUSED_PARAMETER(pvParameter);
    while (true)
    {
        bsp_board_led_invert(BSP_BOARD_LED_0);
        vTaskDelay(1000);
    }
}

- SDK-blinky_freertos 예제에 있는 태스크

- vTaskDelay에 입력한 시간(ms) 간격으로 nRF52840-DK 보드의 0번 LED를 토글

 

<스택 크기 측정>

-예) MLX90614 Object 1 Register Read 태스크

static void mlx_rd_temp_thread(void *arg)
{
    mlx90614_reg_data_t obj_1_temp_struct={0,0,MLX90614_REG_OBJECT_1_TEMP};

    UBaseType_t uxHighWaterMark;
    uxHighWaterMark=uxTaskGetStackHighWaterMark(NULL);
    
    while(1)
    {
        mlx90614_reg_read(&mlx90614_instance,&obj_1_temp_struct,mlx90614_read_temp_cb);
        vTaskDelay(1000);
        uxHighWaterMark=uxTaskGetStackHighWaterMark(NULL);
        printf("mlx_rd_temp_thread remaining stack space : %d\r\n",uxHighWaterMark);
    }
}

- FreeRTOSConfig.h 에서 INCLUDE_uxTaskGetStackHighWaterMark 1 로 설정 필요

- 태스크를 생성하며 할당한 스택 크기 중 태스크가 실행된 이후 측정된 사용 가능 스택 잔량의 최소 크기를 반환

- 반환값이 0에 근접할 수록 스택 오버 플로우에 가까워짐

 

<스마트폰을 통한 온도 및 방사율값 수신>

- nRF Toolbox 애플리케이션 사용

- nRF52840-DK와 연결된 직후부터 BLE 통신을 통해 온도값이 수신되기 시작

- 온도 측정 중 "REM"(방사율 측정 및 수신) 명령어를 전송해 MLX90614에 현재 설정된 방사율을 BLE 통신을 통해 수신

- MLX90614로부터 방사율을 읽어오는 TWI 통신은 TWI transaction manager 라이브러리의 nrf_twi_mngr_schedule() 함수를 사용해 TWI 전송 큐에 등록되므로 온도값을 읽어 오는 중간에 사용되어도 온도값 수신에 영향을 미치지 않는다

- MLX90614로부터 방사율을 읽어오는데 사용되는 mlx90614_rd_emi_thread() 태스크는 mlx90614_reg_read() 함수를 호출한 이후에 삭제되므로 MLX90614로부터 저장된 방사율 데이터를 읽어오는 TWI 통신에 영향을 미치지 않는다

- BLE 방사율 전송 태스크 또한 알림을 획득하고 BLE 전송을 완료한 이후에만 태스크 삭제가 실행되므로 BLE 통신을 통한 방사율값 전송에 영향을 미치지 않는다

 

https://github.com/lee35210/nRF52840_BLE_FREERTOS_MLX90614

사용한 MLX90614ESF-BCC 모델 탑재 모듈

  • 사용 보드 : nRF52840-PDK
  • 사용 IDE : SEGGER Embedded Studio for ARM (노르딕 제품 사용하는 경우 무료 라이센스 이용 가능)
  • 작동 전압 : 2.6~3.6V (MLX90614Bxx, MLX90614Dxx 모델 기준, MLX90614Axx은 4.5~5.5V)
  • (사용 모듈의 경우, Low Dropout Voltage 레귤레이터 탑재로 3~5V에서 작동)
  • 로직 레벨 : Input High (Min : (VDD-0.1)V), Input Low (Max : 0.6V) (MLX90614Bxx, Dxx 기준)
  • 통신 방식 : PWM, SMBus Compatible 2-Wire Interface (10~100KHz)
  • Slave Address : 0x5A
  • SDK 내 예제 프로젝트를 기반으로 작성 (\examples\peripheral\twi_master_using_nrf_twi_mngr)
  • (SDK 다운로드 : https://developer.nordicsemi.com/)

[센서]

  • Power On Reset 이후, 0.25초 지난 후부터 유효 출력 (RAM내 데이터)
  • 주변 온도 -40°C~+125°C, 물체 온도 -70°C~+380°C 까지 측정 가능
  • 측정 정확도 : 0.5°C
  • 측정 해상도 : 0.02°C
  • PWM 출력의 경우, 0.14°C 해상도로 -20~120°C까지 온도 측정 가능
  • Factory calibrated 된 출력을 갖는다
  • 측정값은 FOV 내 모든 물체의 평균 온도값
  • 센서 장착 주변부에 센서를 뜨겁게 or 차갑게 만드는 요소가 있어선 안되고 뜨겁거나 차가운 측정 물체에 너무 가깝게 근접 시켜서도 안된다
  • xCx(ex:MLX90614BCC) 버전의 경우 내부적으로 측정된 열경사도와 측정된 온도를 보상에 사용한다
  • 이 경우 xCx버전은 열경사도에 덜 민감해지지만 영향이 완전히 없어진 상태는 아니다
  • 따라서 열경사도의 원인을 가능한 피하거나 원인으로부터 센서를 보호해주는 것이 중요하다
  • 물체 방사율 1에 대해 교정(Calibration)되어 있지만 재교정없이 0.1~1.0 사이의 방사율로 조정할 수 있다

[작동 방식]

 

STM32 - I2C 통신 MLX90614 적외선 비접촉 온도 측정 센서

작동 전압 : 2.6~3.6V 통신 방식 : PWM, I2C(10~100KHz) 주변 온도 -40°C~+125°C, 물체 온도 -70°C~+380°C 까지 측정 가능 측정 정확도 : 0.5°C 측정 해상도 : 0.02°C PWM 출력의 경우,..

rs29.tistory.com

 

[사전 설정]

- TWI 통신에 사용할 TWI Sensor module 라이브러리를 이용하기 위해 필요

- nrf_twi_sensor.c 추가 (SDK 폴더 내 \components\libraries\twi_sensor 에 위치)

- 프로젝트에 twi_sensor 라이브러리 폴더 추가

 

- sdk_config.h 에 TWI Sensor 활성화 부분 추가 및 1로 설정 (nRF_Drivers와 nRF_Libraries 사이에 추가)

(<h>nRF_Drivers_External ~ </h> 까지)

// <e> UART1_ENABLED - Enable UART1 instance
//==========================================================
#ifndef UART1_ENABLED
#define UART1_ENABLED 0
#endif
// </e>

// </e>

// </h> 
//==========================================================

// <h> nRF_Drivers_External 

//==========================================================
// <q> NRF_TWI_SENSOR_ENABLED  - nrf_twi_sensor - nRF TWI Sensor module
 

#ifndef NRF_TWI_SENSOR_ENABLED
#define NRF_TWI_SENSOR_ENABLED 1	//0 비활성화
#endif

// </h> 
//==========================================================

// <h> nRF_Libraries 

//==========================================================
// <e> APP_SCHEDULER_ENABLED - app_scheduler - Events scheduler
//==========================================================
#ifndef APP_SCHEDULER_ENABLED
#define APP_SCHEDULER_ENABLED 1
#endif

 

[코드]

- SDK 폴더 내 components\drivers_ext 에 위치한 CCS811, LPS22HB 드라이버 등을 바탕으로 작성됨

- mlx90614.c, mlx90614.h, mlx90614_internal.h 하나의 소스 파일, 두 개의 헤더 파일로 구성

 

- 매크로 선언

/*
main.c
*/

//MLX90614와의 통신에 사용할 TWI 인스턴스 ID
#define TWI_INSTANCE_ID_1 1

//TWI trasaction manager 인스턴스 Queue 최대 크기 (대기 전송 갯수)
#define MAX_PENDING_TRANSACTIONS 255

/*
- TWI transaction manager 인스턴스 정의 매크로
생성할 TWI transaction manager 인스턴스 이름, 트랜잭션 Queue 크기, 사용할 TWI 인스턴스
*/
NRF_TWI_MNGR_DEF(m_nrf_twi_mngr_1, MAX_PENDING_TRANSACTIONS, TWI_INSTANCE_ID_1);

/*
- 공통 TWI 센서 인스턴스 생성 매크로
공통 센서 인스턴스 이름, 사용할 TWI transaction manager 인스턴스, 버퍼 크기
(버퍼 크기의 경우 TWI manager Queue 크기보다 작거나 같아야 한다)
*/
NRF_TWI_SENSOR_DEF(sensor_instance_1, &m_nrf_twi_mngr_1, MAX_PENDING_TRANSACTIONS);

/*
- 센서 인스턴스 생성 매크로
센서 인스턴스 이름, 사용할 공통 TWI 센서 인스턴스, 센서 주소
*/
MLX90614_INSTANCE_DEF(mlx90614_instance, &sensor_instance_1, MLX90614_ADDR);
/*
mlx90614.h
*/

//MLX90614xCx 모델 사용하는 경우 주석 해제
//#define MLX90614xCx

//CRC-8 계산에 이용
#define MLX90614_Write  0
#define MLX90614_Read   1

//센서 주소
#define MLX90614_ADDR 0x5A

//통신에 사용할 TWI manager instance 에 요구하는 최소 Queue 크기
#define MLX90614_MIN_QUEUE_SIZE 4

//MLX90614 통신에 사용할 SDA, SCL PIN
#define MLX90614_SDA_PIN 28
#define MLX90614_SCL_PIN 29

//데이터 콜백 프로토타입
typedef void (* mlx90614_data_callback_t)(ret_code_t result, mlx90614_reg_data_t * p_raw_data);

//MLX90614 인스턴스 정의 매크로
#define MLX90614_INSTANCE_DEF(_mlx90614_inst_name, _p_twi_sensor, _sensor_address)                \
    MLX90614_INTERNAL_INSTANCE_DEF(_mlx90614_inst_name, _p_twi_sensor, _sensor_address)
/*
mlx90614_internal.h
*/

//전송할 데이터 바이트 크기 (송수신 동일. LSB+MSB+PEC)
#define MLX90614_REGISTER_BYTES 3

//MLX90614 RAM 및 EEPROM Register 주소
#define MLX90614_REG_AMBIENT_TEMP   0x06	//주변 온도
#define MLX90614_REG_OBJECT_1_TEMP  0x07	//물체 온도 1
#define MLX90614_REG_OBJECT_2_TEMP  0x08	//물체 온도 2
#define MLX90614_REG_EMISSIVITY_1 0x24	//방사율 주소
#define MLX90614_REG_EMISSIVITY_2 0x2F	//MLX90614xCx 모델을 사용할 경우에만 이용할 추가 방사율 주소
#define MLX90614_REG_SLEEP  0xFF	//슬립 모드 진입에 사용

/*
MLX90614 인스턴스 구조체
p_sensor_data : NRF_TWI_SENSOR_DEF을 통해 생성한 common twi sensor instance
sensor_addr : MLX90614_INSTANCE_DEF를 통해 MLX90614 인스턴스를 생성할 때 입력한 센서 주소
*/
typedef struct
{
    nrf_twi_sensor_t * const p_sensor_data;
    uint8_t const            sensor_addr;
} mlx90614_instance_t;


/*
센서 인스턴스 생성 매크로
*/
#define MLX90614_INTERNAL_INSTANCE_DEF(_mlx90614_inst_name, _p_twi_sensor, _sensor_address)\
    static mlx90614_instance_t _mlx90614_inst_name =                                     \
    {                                                                                    \
        .p_sensor_data = _p_twi_sensor,                                                  \
        .sensor_addr   = _sensor_address,                                                \
    }

- 구조체

/*
mlx90614.h
*/

/*
reg_data : MLX90614로부터 읽어 온 데이터를 저장하거나 MLX90614 EEPROM 레지스터에 입력할 데이터를 저장
	RAM, EEPROM 의 각 레지스터는 16bit 크기를 갖는다
pec : MLX90614로부터 읽어 온 데이터가 제대로 전송이 됐는지 확인하기 위한 Packet Error Code 
	CRC-8 계산 필요
reg_addr : 해당 변수 구조체가 접근할 MLX90614의 레지스터 주소를 저장하는 변수
	PEC 계산에도 이용된다 (단순 레지스터 주소 저장용일뿐. 데이터 전송에는 이용되지 않음)
구조체 변수를 통해 레지스터 데이터를 읽거나 쓸 때, reg_data의 LSB -> MSB -> pec 순으로 송수신 된다
MLX90614의 데이터 송수신은 LSB부터 시작되므로 reg_data 의 MSB, LSB 스왑은 필요없다
*/
typedef struct
{
    uint16_t reg_data;
    uint8_t pec;
    uint8_t reg_addr;
} mlx90614_reg_data_t;

- TWI 설정 및 초기화 (main.c)

//MLX90614와의 통신에 사용할 TWI 통신 설정
static void twi_config(void)
{
    uint32_t err_code;
    
    //통신에 사용할 핀, 속도, 인터럽트 우선 순위 설정
    nrf_drv_twi_config_t const config={
      .scl=29,
      .sda=28,
      .frequency=NRF_DRV_TWI_FREQ_100K,
      .interrupt_priority=APP_IRQ_PRIORITY_MID,
      };
    
    //앞서 생성한 설정을 바탕으로 TWI transaction manager instance 초기화
    err_code=nrf_twi_mngr_init(&m_nrf_twi_mngr_1,&config);
    APP_ERROR_CHECK(err_code);
}

- Register Data Read 함수

/*
mlx90614_instance_t : MLX90614_INSTANCE_DEF을 통해 생성한 MLX90614 센서 인스턴스
mlx90614_reg_data_t : 읽어 온 데이터를 저장할 센서 데이터 구조체
mlx90614_data_callback_t : 데이터 수신 완료 후 호출될 콜백 함수
*/
ret_code_t mlx90614_reg_read(mlx90614_instance_t const * p_instance,
                             mlx90614_reg_data_t *       p_reg_data,
                             mlx90614_data_callback_t    user_cb
                            )
{
    ASSERT(p_instance != NULL);
    ASSERT(p_reg_data != NULL);

    /*
    p_instance->p_sensor_data : NRF_TWI_SENSOR_DEF을 통해 설정된 공통 센서 인스턴스
    p_instance->sensor_addr : MLX90614 센서 인스턴스에 설정된 MLX90614 센서 주소
    p_reg_data->reg_addr : 전달받은 구조체에 저장된 레지스터 주소
    			해당 레지스터 주소에 접근해 데이터를 읽어온다
    nrf_twi_sensor_reg_cb_t : 데이터 수신 완료 후 호출 할 콜백 함수
    MLX90614_REGISTER_BYTES : 읽어 올 데이터의 바이트 크기 (uint8_t 기준)
    			(Data 16bit(LSB(8)+MSB(8)) + PEC(8bit)=24bit)                
    */
    return nrf_twi_sensor_reg_read(p_instance->p_sensor_data,
                                   p_instance->sensor_addr,
                                   p_reg_data->reg_addr,
                                   (nrf_twi_sensor_reg_cb_t) user_cb,
                                   (uint8_t *) p_reg_data,
                                   MLX90614_REGISTER_BYTES);
}
  • MLX90614 RAM, EEPROM의 레지스터 주소에 액세스해 레지스터에 저장된 16bit 데이터를 읽어오는 함수
  • nrf_twi_sensor_reg_read 함수는 nrf_twi_mngr_schedule 함수를 이용하므로 수신이 완료될 때까지 대기하는게 아니라 TWI transaction manager instance의 transaction queue에 데이터 수신을 대기시킨다
  • transaction queue에 대기해있던 데이터 수신이 완료된 이후에 콜백 함수가 호출된다
  • (콜백 함수 호출이 필요없다면 NULL 값을 입력)

- 온도 데이터, 방사율 데이터 변환 함수

//읽어 온 16bit 방사율 데이터를 데이터 시트에 나와있는 식을 통해 실제 방사율값으로 변환
inline float mlx90614_emissivity_conversion(uint16_t emissivity_val)
{
    return (emissivity_val+1)/65536.0;
}

//읽어 온 16bit 온도 데이터를 데이터 시트에 나와있는 식을 통해 실제 온도값으로 변환 (섭씨 기준)
inline double mlx90614_temp_conversion(uint16_t temperature_val)
{
    return (temperature_val*0.02)-273.15;
}
  • 콜백 함수에서 사용

- CRC-8 테이블 및 계산 함수

/*
속도면에서 필요할 때마다 CRC 값을 계산하는 방식보다 계산된 값이 저장된 테이블을 통해 
CRC 계산값을 찾는 것이 더 유리해 테이블 사용이 권장되므로 테이블 사용
*/
static const uint8_t crc8_table[256]=
{
    0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D,
    0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D,
    0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD,
    0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD,
    0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA,
    0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A,
    0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A,
    0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A,
    0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4,
    0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4,
    0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44,
    0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34,
    0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63,
    0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13,
    0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,
    0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3
};

/*
mlx90614_reg_data_t : 전달받은 구조체의 데이터값을 기반으로 PEC 계산
write_read : 읽어 온 데이터의 PEC 계산을 할 것인지 
		MLX90614에 입력할 PEC값을 계산할 것인지를 결정
*/
int mlx90614_crc_cal(mlx90614_reg_data_t * p_raw_data, uint8_t write_read)
{
    //CRC-8 계산 결과값을 저장할 변수
    uint8_t sum_val=0;

    /*
    읽어 온 데이터에 대한 CRC-8 계산을 순차적으로 실행하면서 sum_val 변수에 저장
    */
    if(write_read==MLX90614_Read)
    {
        uint8_t pec_array[5]={(MLX90614_ADDR<<1), p_raw_data->reg_addr, ((MLX90614_ADDR<<1)|1),
                            LSB_16(p_raw_data->reg_data), MSB_16(p_raw_data->reg_data)};
        for(int i=0; i<5; i++)
        {
            sum_val=crc8_table[(sum_val^pec_array[i])];
        }        
    }
    /*
    레지스터에 입력할 PEC값을 순차적으로 CRC-8 계산하면서 sum_val 변수에 저장
    */
    else if(write_read==MLX90614_Write)
    {
        uint8_t pec_array[4]={(MLX90614_ADDR<<1), p_raw_data->reg_addr, 
                            LSB_16(p_raw_data->reg_data), MSB_16(p_raw_data->reg_data)};
        for(int i=0; i<4; i++)
        {
            sum_val=crc8_table[(sum_val^pec_array[i])];
        }
    }
    //CRC-8 계산 결과 반환
    return sum_val;
}
  • nRF5 SDK는 CRC-16, CRC-32 계산 라이브러리만 제공
  • CRC-8 테이블은 http://www.sunshine2k.de/coding/javascript/crc/crc_js.html 사이트를 참고해 작성
  • MLX90614로부터 읽어 온 PEC는
  • (MLX90614 Address<<1+W(0)) + Register Address(Command) + (MLX90614 Address<<1+R(1)) + Register Data (LSB) + Register Data (MSB) 의 CRC-8 계산
  • MLX90614에 데이터를 쓸 때 함께 입력할 PEC는
  • (MLX90614 Address<<1+W(0)) + Register Address(Command) + Register Data (LSB) + Register Data (MSB) 의 CRC-8 계산

- 방사율 변경 함수

  • EEPROM으로부터 데이터를 읽어오거나 데이터를 쓸 경우엔 데이터 시트의 EEPROM 테이블에 나와있는 레지스터 주소에서 0x20 의 값을 더해 준 값을 사용해야 한다
  • ex) EEPROM 방사율 주소 = 0x04 번지, 실제 사용은 0x04 | 0x20 = 0x24 (CMD) 이용
  • MLX90614xCx 모델의 경우, EEPROM 0x0F 번지의 데이터도 변경해줘야 온도가 제대로 측정되는데 이 데이터는 0x04 번지의 데이터를 계산할 때와는 다르게 기존 0x0F 번지에 저장되어있던 값을 이용해 계산되므로 우선적으로 0x0F에 저장된 값을 따로 기록해 둘 것을 권장 (사용 모듈의 경우, 0x0F = 0x079A 인 상태였음)
  • EEPROM에 새로운 데이터를 입력하기 위해선 우선 해당 레지스터 주소에 0x0000 데이터를 입력해 기존 데이터를 지워줘야 한다
*
mlx90614_instance_t : MLX90614_INSTANCE_DEF을 통해 생성한 MLX90614 센서 인스턴스
new_emissivity : EEPROM에 입력할 새 방사율
*/
ret_code_t mlx90614_emissivity_write(mlx90614_instance_t const * p_instance,
                                    float new_emissivity
                                    )
{
    ASSERT(p_instance != NULL);

    //방사율은 0.1~1.0까지만 설정 가능
    if(new_emissivity<0.10 || 1.0<new_emissivity)
    {
        NRF_LOG_WARNING("Invaild Emissivity Value");
        return NRF_ERROR_INVALID_DATA;
    }

    //TWI transaction manager instance의 Queue 상태를 확인하기 위한 부울 변수
    bool idle_chk=false;

    //에러 코드 저장
    ret_code_t err_code;
    
    //레지스터에 새 방사율 데이터가 제대로 써졌는지 확인용
    uint16_t emissivity_chk_data;
    
    /*
    새 방사율값을 저장할 구조체
    reg_addr : 계산된 새 방사율값을 저장할 EEPROM 레지스터 주소 (0x04 | 0x20 = 0x24)
    reg_data : 데이터 시트에 나와있는 식을 통해 계산된 16비트 데이터
    pec : 데이터에 이어서 전송될 PEC 데이터
    	만약 제대로 계산된 PEC값이 전송되지 않는다면 EEPROM은 기존 데이터를 유지한다
    */
    mlx90614_reg_data_t new_emissivity_1;
    new_emissivity_1.reg_addr=MLX90614_REG_EMISSIVITY_1;
    new_emissivity_1.reg_data=(int)(round(65536*(new_emissivity)-1.0));
    new_emissivity_1.pec=mlx90614_crc_cal(&new_emissivity_1,MLX90614_Write);

    //0x04 번지에 새 방사율 데이터가 제대로 저장이 됐는지 확인하기 위해 계산된 새 방사율 데이터 저장
    emissivity_chk_data=new_emissivity_1.reg_data;

    /*
    EEPROM에 데이터를 쓰기 위해선 우선적으로 해당 레지스터에 0x0000 값을 갖는 16비트 데이터를 입력해야 한다
    0x0000 의 값을 전송할 구조체 선언 후 레지스터 주소 입력, PEC 계산값 저장
    */
    mlx90614_reg_data_t emissivity_zero;
    emissivity_zero.reg_addr=MLX90614_REG_EMISSIVITY_1;
    emissivity_zero.pec=mlx90614_crc_cal(&emissivity_zero,MLX90614_Write);

    /*
    - MLX90614xCx 모델을 사용하는 경우에만 실행되는 부분
    현재 EEPROM 0x04 번지에 입력되어있는 방사율 데이터가 필요하다
    */
    #ifdef MLX90614xCx
    
    //현재 EEPROM에 저장되어있는 0x04 번지의 방사율 데이터를 저장할 구조체 변수 선언 및 레지스터 주소 입력
    mlx90614_reg_data_t old_emissivity_1;
    old_emissivity_1.reg_addr=MLX90614_REG_EMISSIVITY_1;

    //old_emissivity_1 구조체에 EEPROM 0x04 (CMD 0x24) 번지에 저장된 현재 방사율값 읽어와 저장
    err_code=nrf_twi_sensor_reg_read(p_instance->p_sensor_data,
                           p_instance->sensor_addr,
                           MLX90614_REG_EMISSIVITY_1,
                           (nrf_twi_sensor_reg_cb_t) NULL,
                           (uint8_t *) &old_emissivity_1,
                           MLX90614_REGISTER_BYTES);

    //nrf_twi_sensor_reg_read 함수 실행 에러 발생시
    if (err_code != NRF_SUCCESS)
    {
        NRF_LOG_WARNING("MLX90614xCx : Old emissivity 1 read failed.");
        return err_code;
    }

    /*
    데이터 송수신에 사용하는 TWI Sensor module 라이브러리는 
    TWI trasaction manager 라이브러리의 nrf_twi_mngr_schedule 함수를 이용해 데이터를 전송한다
    때문에 old_emissivity_1 구조체에 레지스터로부터 읽어 온 데이터가 저장되기까지 대기를 한 후
    (=TWI transaction manager queue==idle 상태)
    CRC 값을 계산 및 읽어 온 PEC값과 비교한다
    */
    do
    {
        idle_chk=nrf_twi_mngr_is_idle(p_instance->p_sensor_data->p_twi_mngr);
        nrf_delay_us(10);
    }while(idle_chk!=true);
    
    //수신이 제대로 됐는지 확인을 위해 읽어 온 PEC 값과 CRC-8 계산 결과를 비교
    if(old_emissivity_1.pec!=mlx90614_crc_cal(&old_emissivity_1,MLX90614_Read))
    {
        NRF_LOG_WARNING("MLX90614xCx : Old emissivity 1 CRC doesn't match");
        return NRF_ERROR_INVALID_DATA;
    }
    #endif

	//모델 상관없이 공통적으로 진행 시작
    
    //0x0000의 데이터값을 먼저 전송해 방사율 레지스터 주소(0x04)의 데이터를 지운다
    err_code=nrf_twi_sensor_reg_write(p_instance->p_sensor_data,
                               p_instance->sensor_addr,
                               MLX90614_REG_EMISSIVITY_1,
                               (uint8_t *)&emissivity_zero,
                               sizeof(emissivity_zero)/sizeof(uint8_t)
                               );
    //nrf_twi_sensor_reg_write 함수 실행 에러 발생시
    if (err_code != NRF_SUCCESS)
    {
        NRF_LOG_WARNING("MLX90614  : Emissivity 1 erase failed");
        return err_code;
    }

    /*
    0x0000 데이터 입력 후, 새 방사율 데이터를 전송하기 위해선 최소 5ms, 권장 10ms의
    여유 시간이 필요하므로 0x0000 데이터 전송 완료까지 기다린 뒤에 딜레이 함수를 이용해 10ms 대기
    */
    do
    {
        idle_chk=nrf_twi_mngr_is_idle(p_instance->p_sensor_data->p_twi_mngr);
        nrf_delay_us(10);
    }while(idle_chk!=true);
    nrf_delay_ms(10);

    //데이터 시트에 나와있는 식을 통해 계산된 새 방사율값 EEPROM 0x04 번지에 전송
    err_code=nrf_twi_sensor_reg_write(p_instance->p_sensor_data,
                           p_instance->sensor_addr,
                           MLX90614_REG_EMISSIVITY_1,
                           (uint8_t*)&new_emissivity_1,
                           sizeof(new_emissivity_1)/sizeof(uint8_t)
                           );
    if (err_code != NRF_SUCCESS)
    {
        NRF_LOG_WARNING("MLX90614 : New emissivity 1 write failed");
        return err_code;
    }
    do
    {
        idle_chk=nrf_twi_mngr_is_idle(p_instance->p_sensor_data->p_twi_mngr);
        nrf_delay_us(10);
    }while(idle_chk!=true);
    
    //새 방사율 데이터를 레지스터에 쓴 이후에도 똑같이 최소 5ms, 권장 10ms의 대기가 필요하다
    nrf_delay_ms(10);

    /*
    EEPROM 0x04 번지에 저장된 방사율 데이터를 읽어와 앞서 emissivity_chk_data에 저장한 
    새 방사율 데이터와 비교해 EEPROM에 새 방사율 데이터가 제대로 입력됐는지 확인
    */
    err_code=nrf_twi_sensor_reg_read(p_instance->p_sensor_data,
                                   p_instance->sensor_addr,
                                   MLX90614_REG_EMISSIVITY_1,
                                   (nrf_twi_sensor_reg_cb_t) NULL,
                                   (uint8_t *) &new_emissivity_1,
                                   MLX90614_REGISTER_BYTES);
    if (err_code != NRF_SUCCESS)
    {
        NRF_LOG_WARNING("MLX90614 : New emissivity 1 read failed", err_code);
        return err_code;
    }
    /*
    TWI transaction manager instance의 queue에 데이터 수신이 대기된 상태로 존재하기 때문에
    수신 완료까지(Queue==0) 대기한 후에 데이터 비교를 진행한다
    */
    do
    {
        idle_chk=nrf_twi_mngr_is_idle(p_instance->p_sensor_data->p_twi_mngr);
        nrf_delay_us(10);
    }while(idle_chk!=true);
    
    //EEPROM 0x04 번지에 새 방사율 데이터가 제대로 입력됐는지 확인
    if(new_emissivity_1.reg_data!=emissivity_chk_data)
    {
        NRF_LOG_INFO("New Emissivity 1 data doesn't match");
        return NRF_ERROR_INVALID_DATA;
    }

    /*
    - MLX90614xCx 모델을 사용하는 경우에만 실행되는 부분
    MLX90614xCx 모델의 경우, 방사율 데이터를 변경할 때 EEPROM 0x04 번지의 데이터만 바꾸는 것이 아니라 
    0x0F 번지에 저장되어 있는 값도 데이터 시트에 나와있는 식을 통해 얻은 값으로 변경해야 
    방사율에 따른 옳바른 온도값이 측정된다
    */
    #ifdef MLX90614xCx
    
    /*
    앞서 0x04 번지의 데이터를 지우기 위해 선언 및 사용한 0x0000 데이터를 가진 구조체 변수에 
    0x0F 주소 입력 및 PEC값 재계산해 저장
    */
    emissivity_zero.reg_addr=MLX90614_REG_EMISSIVITY_2;
    emissivity_zero.pec=mlx90614_crc_cal(&emissivity_zero, MLX90614_Write);

    /*
    new_emissivity_2 : EEPROM 0x0F 번지에 쓸 새 방사율 데이터를 저장할 구조체 변수
    old_emissivity_2 : 현재 EEPROM 0x0F 번지에 저장된 방사율 데이터를 읽어와 저장할 구조체 변수
    */
    mlx90614_reg_data_t new_emissivity_2, old_emissivity_2;
    
    //EEPROM 0x0F 번지 주소 입력 (0x0F | 0x20 = 0x2F)
    new_emissivity_2.reg_addr=MLX90614_REG_EMISSIVITY_2;
    old_emissivity_2.reg_addr=MLX90614_REG_EMISSIVITY_2;

    //현재 EEPROM 0x0F 번지에 입력되어있는 방사율 데이터를 읽어와 old_emissivity_2 구조체 변수에 저장
    err_code=nrf_twi_sensor_reg_read(p_instance->p_sensor_data,
                           p_instance->sensor_addr,
                           MLX90614_REG_EMISSIVITY_2,
                           (nrf_twi_sensor_reg_cb_t) NULL,
                           (uint8_t *) &old_emissivity_2,
                           MLX90614_REGISTER_BYTES);
    if (err_code != NRF_SUCCESS)
    {
        NRF_LOG_WARNING("MLX90614xCx : Old emissivity 2 read failed");
        return err_code;
    }
    do
    {
        idle_chk=nrf_twi_mngr_is_idle(p_instance->p_sensor_data->p_twi_mngr);
        nrf_delay_us(10);
    }while(idle_chk!=true);
    if(old_emissivity_2.pec!=mlx90614_crc_cal(&old_emissivity_2,MLX90614_Read))
    {
        NRF_LOG_WARNING("Old emissivity 2 CRC doesn't match");
        return NRF_ERROR_INVALID_DATA;
    }

    /*
    기존에 저장되어있는 0x04 번지의 방사율 데이터(old_emissivity.reg_data)와 앞서 0x04 번지에 입력한
    새로운 방사율 데이터(new_emissivity_1.reg_data) 그리고 0x0F 번지에 저장되어있던 데이터
    (old_emissivity_2.reg_data)를 이용해 0x0F 번지에 입력될 새로운 데이터 값을 구한다
    */
    //0x0F 번지에 입력할 새로운 데이터값 계산 후 저장
    new_emissivity_2.reg_data=
	(round(((double)old_emissivity_1.reg_data/new_emissivity_1.reg_data)*old_emissivity_2.reg_data));
        
    //0x0F 번지에 전송될 PEC값을 CRC-8 계산 후 저장
    new_emissivity_2.pec=mlx90614_crc_cal(&new_emissivity_2,MLX90614_Write);
    
    //위에서 계산된 0x0F에 입력할 새 방사율 데이터를 데이터 확인용 변수에 저장
    emissivity_chk_data=new_emissivity_2.reg_data;
 
    //0x000 데이터를 먼저 전송해 0x0F 번지 데이터 지움
    err_code=nrf_twi_sensor_reg_write(p_instance->p_sensor_data,
                               p_instance->sensor_addr,
                               MLX90614_REG_EMISSIVITY_2,
                               (uint8_t *)&emissivity_zero,
                               sizeof(emissivity_zero)/sizeof(uint8_t)
                               );
    if (err_code != NRF_SUCCESS)
    {
        NRF_LOG_WARNING("MLX90614xCx : Emissivity 2 erase failed", err_code);
        return err_code;
    }
    do
    {
        idle_chk=nrf_twi_mngr_is_idle(p_instance->p_sensor_data->p_twi_mngr);
        nrf_delay_us(10);
    }while(idle_chk!=true);
    nrf_delay_ms(10);

    //0x0F 번지에 새롭게 계산된 방사율 데이터를 전송
    err_code=nrf_twi_sensor_reg_write(p_instance->p_sensor_data,
                           p_instance->sensor_addr,
                           MLX90614_REG_EMISSIVITY_2,
                           (uint8_t *)&new_emissivity_2,
                           sizeof(new_emissivity_2)/sizeof(uint8_t)
                           );
    if (err_code != NRF_SUCCESS)
    {
        NRF_LOG_WARNING("MLX90614xCx : New emissivity 2 write failed");
        return err_code;
    }
    do
    {
        idle_chk=nrf_twi_mngr_is_idle(p_instance->p_sensor_data->p_twi_mngr);
        nrf_delay_us(10);
    }while(idle_chk!=true);
    nrf_delay_ms(10);
	
    //새로운 방사율 데이터가 제대로 써졌는지 확인하기 위해 0x0F 번지에 저장된 데이터를 읽어 옴
    err_code=nrf_twi_sensor_reg_read(p_instance->p_sensor_data,
                           p_instance->sensor_addr,
                           MLX90614_REG_EMISSIVITY_2,
                           (nrf_twi_sensor_reg_cb_t) NULL,
                           (uint8_t *) &new_emissivity_2,
                           MLX90614_REGISTER_BYTES);
    if (err_code != NRF_SUCCESS)
    {
        NRF_LOG_WARNING("MLX90614xCx : New emissivity 2 read failed");
        return err_code;
    }
    do
    {
        idle_chk=nrf_twi_mngr_is_idle(p_instance->p_sensor_data->p_twi_mngr);
        nrf_delay_us(10);
    }while(idle_chk!=true);
	
    //0x0F 번지에 새 방사율 데이터가 제대로 써졌는지 확인
    if(new_emissivity_2.reg_data=!emissivity_chk_data)
    {
        NRF_LOG_INFO("New emissivity 2 data doesn't match");
        return NRF_ERROR_INVALID_DATA;
    }
    #endif
    
    return NRF_SUCCESS;
}

- Sleep 함수

/*
슬립 모드에 진입하기 위해선 Command=0xFF, PEC=0xE8 2바이트를 전송하면 된다
Command는 nrf_twi_sensor_reg_write의 세번째 인자로 쓰인 MLX90614_REG_SLEEP가 사용되므로
실제 전송할 바이트 크기는 PEC 1바이트가 된다
*/
ret_code_t mlx90614_sleep_enter(mlx90614_instance_t const * p_instance)
{
    uint8_t sleep_pec=0xE8;

    return nrf_twi_sensor_reg_write(p_instance->p_sensor_data,
                       p_instance->sensor_addr,
                       MLX90614_REG_SLEEP,
                       (uint8_t*)&sleep_pec,
                       sizeof(sleep_pec)/sizeof(uint8_t)
                       );
}

/*
슬립 모드에서 빠져나오기 위해선 SDA 핀을 33ms를 초과하는 시간동안 LOW로 유지해야 한다
이를 위해 MLX90614 와의 통신에 사용하던 TWI를 해제하고 SDA핀을 LOW 출력 설정으로 바꾼 후
딜레이 함수를 통해 34ms 동안 LOW 상태를 유지함으로 슬립 모드에서 탈출
이후에 다시 twi_config() 함수를 실행해 통신에 사용할 TWI를 재초기화해줘야 한다
하지 않을 경우, 센서와의 통신 불가
*/
void mlx90614_sleep_exit(mlx90614_instance_t const * p_instance, uint8_t sda_pin)
{
    bool idle_chk;
    
    //현재 TWI manager instance Queue에 대기해있는 전송이 완료될 때까지 대기
    do
    {
        idle_chk=nrf_twi_mngr_is_idle(p_instance->p_sensor_data->p_twi_mngr);
        nrf_delay_us(10);
    }while(idle_chk!=true);

	//TWI manager instance 해제
    nrf_twi_mngr_uninit(p_instance->p_sensor_data->p_twi_mngr);
    
    //SDA 핀 Output 설정 및 LOW 출력으로 설정
    nrf_gpio_cfg_output(sda_pin);
    nrf_gpio_pin_write(sda_pin,0);
    
    //34ms 동안 SDA=LOW 유지
    nrf_delay_ms(34);
}

- Sleep 실행 예시

1. 방사율, Object 1 온도 읽음

2. 슬립 모드 진입

3. 다섯 번의 Object 1 Read 실행

4. 슬립 모드 탈출

5. twi_config() 실행

6. Object 1 Read 시작

슬립 모드 진입 후 다섯번의 Object 1 Temperature 레지스터 접근 시도가 있었지만 실패
35ms 동안 SDA 핀 LOW 유지해 슬립 모드 탈출

소스 코드 : https://github.com/lee35210/nRF52840-MLX90614

SSD1306 드라이버 탑재 128x64 OLED 모듈

  • 사용 보드 : nRF52840-PDK
  • 사용 IDE : SEGGER Embedded Studio for ARM (노르딕 제품 사용하는 경우 무료 라이센스 이용 가능)
  • 사용 모듈 작동 전압 : 3.3~5V (보드 VDD 사용)
  • 로직 레벨 (SDA, SCK) : GPIO Output high voltage = VDD (3.0V)
  • 통신 방법 : TWI (Two Wire Interface, I2C compatible two-wire interface)
  • Slave Address : 0x3C (사용 모듈 0x3C 고정)
  • SDK 내 예제 프로젝트를 기반으로 작성 (\examples\peripheral\twi_master_using_nrf_twi_mngr)
  • (SDK 다운로드 : https://developer.nordicsemi.com/)

[구동 방식]

 

STM32 - I2C 통신을 이용한 SSD1306 128x64 OLED 제어

작동 전압 : 3.3~5V 통신 방식 : I2C (칩셋이 I2C, SPI를 포함한 5개의 통신 방식을 지원하지만 사용 모듈은 I2C 전용으로 설계됨) 세로 16(Yellow) + 48(Blue) 줄로 구성된 모듈 사용 [기본 구동 방식] Control..

rs29.tistory.com

  • SDK 폴더 내 TWI Transaction Manager Example 를 베이스로 필요에 따라 코드를 수정해 모듈 구동
  • TWI Sensor module 라이브러리를 메인으로 사용
  • (TWI transaction manager 라이브러리에도 데이터 전송 함수들이 존재하지만 사용 방식이 조금 다름,
  • 슬레이브 장치의 레지스터 주소를 입력해 데이터 송수신이 가능한 함수는 TWI Sensor 라이브러리에만 존재)
  • (+ TWI transaction manager 라이브러리는 TWI Sensor module 라이브러리를 사용하기 위해 필요한 라이브러리)

[사전 설정]

- nrf_twi_sensor.c 추가 (SDK 폴더 내 \components\libraries\twi_sensor 에 위치)

- 프로젝트에 twi_sensor 라이브러리 폴더 추가

 

- sdk_config.h 에 TWI Sensor 활성화 부분 추가 및 1로 설정 (nRF_Drivers와 nRF_Libraries 사이에 추가)

(<h>nRF_Drivers_External ~ </h> 까지)

// <e> UART1_ENABLED - Enable UART1 instance
//==========================================================
#ifndef UART1_ENABLED
#define UART1_ENABLED 0
#endif
// </e>

// </e>

// </h> 
//==========================================================

// <h> nRF_Drivers_External 

//==========================================================
// <q> NRF_TWI_SENSOR_ENABLED  - nrf_twi_sensor - nRF TWI Sensor module
 

#ifndef NRF_TWI_SENSOR_ENABLED
#define NRF_TWI_SENSOR_ENABLED 1	//0 비활성화
#endif

// </h> 
//==========================================================

// <h> nRF_Libraries 

//==========================================================
// <e> APP_SCHEDULER_ENABLED - app_scheduler - Events scheduler
//==========================================================
#ifndef APP_SCHEDULER_ENABLED
#define APP_SCHEDULER_ENABLED 1
#endif

- nrf_twi_sensor.h 내 NRF_TWI_SENSOR_SEND_BUF_SIZE 수정 (전송 가능한 최대 버퍼 크기)

/**
 * @brief Internal write operation buffer length.
 *
 * Defines how many bytes can be stored internally.
 * 16 bytes were selected so that nrf_twi_sensor_write_cmd_t size
 * matches nrf_twi_sensor_read_cmd_t size.
 */
#define NRF_TWI_SENSOR_SEND_BUF_SIZE   255  //default 16

 

[코드]

- 정의 및 전역 변수

#define TWI_INSTANCE_ID             0	//TWI instance id used for driver
#define MAX_PENDING_TRANSACTIONS    20	//TWI transaction manager 최대 Queue 개수 (현재 진행 중 제외)

#define SSD1306_ADDR 0x3C		//SSD1306 Slave Address
#define ssd1306_data_select 0x40	//SSD1306 Data 입력할 때 사용
#define ssd1306_cmd_select 0x00		//SSD1306 Command 입력할 때 사용

#define font_width 12	//사용할 폰트의 문자 너비

//Macro that simplifies defining a TWI transaction manager instance.
//생성할 인스턴스 이름, 트랜잭션 큐 크기(최대 보류 트랜잭션 개수), 사용할 하드웨어 TWI 인스턴스 색인
NRF_TWI_MNGR_DEF(m_nrf_twi_mngr, MAX_PENDING_TRANSACTIONS, TWI_INSTANCE_ID);

//Macro creating common twi sensor instance.
//TWI 공통 센서 인스턴스 이름, TWI Manager 인스턴스 포인터, 통신에 사용될 버퍼의 크기
NRF_TWI_SENSOR_DEF(twi_ssd1306, &m_nrf_twi_mngr, MAX_PENDING_TRANSACTIONS);
  • TWI Sensor module 라이브러리 사용은 TWI transaction manager 라이브러리 사용을 전제로 함
  • MAX_PENDING_TRANSACTIONS은 트랜잭션 매니저 큐에 최대 보류 가능한 트랜잭션 개수
  • NRF_TWI_SENSOR_DEF 에서 MAX_PENDING_TRANSACTIONS 은 TWI manager 큐 크기보다 작거나 같아야 함
  • 데이터 송수신을 실행할 경우 트랜잭션 매니저 큐에 실행 순서대로 스케줄링 된 뒤 통신이 이루어진다

- TWI 설정

static void twi_config(void)
{
    uint32_t err_code;
    
    nrf_drv_twi_config_t const config={
      .scl=27,	//SCL PIN
      .sda=26,	//SDA PIN
      .frequency=NRF_DRV_TWI_FREQ_400K,		//TWI Frequency = 400KHz
      .interrupt_priority=APP_IRQ_PRIORITY_MID	//Interrupt Priority
      };
    
    err_code=nrf_twi_mngr_init(&m_nrf_twi_mngr,&config);
    APP_ERROR_CHECK(err_code);
}
  • TWI 통신 환경 설정
  • 통신에 사용할 핀, 속도, 인터럽트 우선 순위 등을 설정한 뒤 이 환경 설정을 기반으로 TWI 트랜잭션 매니저 인스턴스 초기화

- SSD1306 Command 입력 함수

void ssd1306_write_cmd(uint8_t ssd1306_cmd)
{
    //센서 인스턴스 포인터, 슬레이브 장치 주소, 레지스터 주소(슬레이브 내), 전송할 데이터 포인터, 전송할 바이트 크기
    nrf_twi_sensor_reg_write(&twi_ssd1306, SSD1306_ADDR, ssd1306_cmd_select, &ssd1306_cmd, 1);
}
  • TWI 트랜잭션 매니저 라이브러리 내에도 데이터 전송 함수가 존재하지만 슬레이브 장치내 특정 레지스터 주소에 접근해서 데이터 송수신이 가능한 함수는 TWI 센서 모듈 라이브러리에만 존재한다
  • (SSD1306 사용의 경우, TWI sensor 라이브러리 전송 함수의 레지스터 주소 부분을 이용해 사용자가 전송할 데이터 버퍼가 명령어인지 아니면 디스플레이 데이터인지를 결정하는데 사용)
  • 예) 명령어 0xA8을 전송하는 경우,

슬레이브 장치 주소, 컨트롤 바이트(커맨드 설정), 0xA8 (커맨드)

- SSD1306 초기화 함수

static void ssd1306_init(void)
{
	ssd1306_W_Command(0xA8);	//Set Mux Ratio
	ssd1306_W_Command(0x3F);	//64MUX

	ssd1306_W_Command(0xD3);	//Set Display Offset
	ssd1306_W_Command(0x00);	//COM0

	ssd1306_W_Command(0x40);	//Set Display Start Line

	ssd1306_W_Command(0xA1);	//Set Segment re-map, Default 0xA0
					//column address 127 is mapped to SEG0 (좌우 반전)

	ssd1306_W_Command(0xC8);	//Set COM Output Scan Direction, default 0xC0
					//remapped mode. Scan from COM[N-1] to COM0 (상하 반전)

	ssd1306_W_Command(0xDA);	//Set COM Pins hardware configuration
	ssd1306_W_Command(0x12);

	ssd1306_W_Command(0x20);	//Set Memory Addressing Mode
	ssd1306_W_Command(0x02);	//Page Addressing Mode

	ssd1306_W_Command(0x81);	//Set Contrast Control
	ssd1306_W_Command(0x7F);	//1~256

	ssd1306_W_Command(0xA4);	//Disable Entire Display On

	ssd1306_W_Command(0xA6);	//Set Normal Display

	ssd1306_W_Command(0xD5);	//Set Osc Frequency
	ssd1306_W_Command(0x80);

	ssd1306_W_Command(0x8D);	//Enable charge pump regulator
	ssd1306_W_Command(0x14);

	ssd1306_W_Command(0xAF);	//Display ON
}
  • 데이터 시트에 나와있는 초기화 예를 기반으로 작성
  • 페이지 어드레싱 모드 사용
  • 사용 모듈의 경우, 초기화 코드를 그대로 사용하면 노란색 영역이 하단에 위치 (Page 6,7)
  • 노란색 영역을 상단으로 사용하기 위해 0xC8 명령어를 사용해 COM 출력 스캔 방향을 COM63 to COM0 으로 바꿔 상하 반전을 시키고 0xA1 명령어를 통해 127 열을 SEG0 으로 리맵핑 하여 좌우를 반전 시켜줌
  • Set COM Pins hardware configuration의 경우, COM0~COM63이 순차적으로 화면 최상단에서 최하단까지 이어지는 Sequential 핀 배열과 COM0~COM31, COM32~COM63 이 교차되어 나열되는 (COM0, COM31, COM2... / COM31, COM0, COM32...) Alternative 핀 배열의 두 방법이 있다
  • Sequential 배열을 사용하기 위해 데이터 시트를 참고해 0xDA+0x02 명령어를 사용했지만 실제 결과는 Alternative 배열처럼 COM0~COM31/COM32~COM63이 서로 교차되어 표시되는 결과를 얻음
  • 0xDA+0x12(Alternative COM 핀 배열) 명령어를 사용한 결과, COM0~COM63 까지 화면에 순차적으로 표시됨

- SSD1306 데이터 전송 함수

static void ssd1306_write_data(uint8_t* data_buffer, int buf_length)
{
    //센서 인스턴스 포인터, 센서 주소, 레지스터 주소, 전송할 데이터 버퍼 주소, 버퍼 길이
    nrf_twi_sensor_reg_write(&twi_ssd1306,SSD1306_ADDR,ssd1306_data_select,&data_buffer[0],buf_length);
}
  • 디스플레이에 표시될 데이터를 전송하는 함수 (GDDRAM에 저장될 데이터)
  • ssd1306_data_select의 값은 0x40 으로 센서에 전송되는 첫번째 바이트이며 뒤이어 전송될 데이터 버퍼가 디스플레이 표현에 사용될 데이터임을 나타낸다
  • 전송 데이터 버퍼는 내부 버퍼로 복사된다 (nRF52 내)
  • 버퍼 길이의 경우, nrf_twi_sensor.h 내 NRF_TWI_SENSOR_SEND_BUF_SIZE 의 값에 -1된 값이 전송 가능한 최대 길이가 된다. 따라서 전송할 버퍼의 길이는 NRF_TWI_SENSOR_SEND_BUF_SIZE 값보다 작아야 한다

- SSD1306 디스플레이 클리어 함수

static void ssd1306_clear(void)
{
    static uint8_t clear_buffer[128]={0};	//GDDRAM에 전송할 데이터 버퍼

    //데이터 입력이 시작될 열 주소를 0번째 열로 설정
    ssd1306_write_cmd(0x00);
    ssd1306_write_cmd(0x10);

    for(int i=0;i<8;i++)
    {
        ssd1306_write_cmd(0xB0+i);	//데이터를 입력할 페이지 설정
        ssd1306_write_data(clear_buffer,sizeof(clear_buffer)/sizeof(uint8_t));	//데이터 버퍼 전송
    }
}
  • 디스플레이를 빈 화면으로 초기화하는 함수
  • 0x00, 0x10 명령어를 먼저 전송해 시작 열을 0으로 설정
  • FOR문 - 0xB0 명령어를 입력해 시작 페이지를 0번째 페이지로 설정
  • 0x00 값을 갖는 128 개의 데이버 버퍼를 데이터 전송 함수를 통해 SSD1306에 전송
  • 전송 완료 후, 열의 위치는 0번째 열이 된다
  • (열에 데이터를 입력하고 나면 열 주소가 자동으로 1 증가되는데 마지막 127열의 경우에는 데이터를 입력하고 난 뒤에 첫번째 열로 주소가 변경됨)
  • 7번째 페이지까지 반복해 GDDRAM에 저장된 디스플레이 데이터를 전부 0으로 설정해 화면을 비움

- SSD1306 화면 좌표 설정 함수

static void ssd1306_Set_Coord(uint8_t page, uint8_t col)
{
    uint8_t col_low=0x0F,col_high=0x1F;
    col_low=(col&0x0F);			//열 주소 하위 4비트 계산
    col_high=0x10|((col>>4)&0x0F);	//열 주소 상위 4비트 계산
    ssd1306_write_cmd(0xB0+page);	//페이지 주소 전송
    ssd1306_write_cmd(col_low);		//열 주소 하위 4비트 전송
    ssd1306_write_cmd(col_high);	//열 주소 상위 4비트 전송
}
  • 디스플레이 데이터를 쓰기 시작할 위치를 지정하는 함수
  • Page Addressing Mode의 경우 Horizontal, Vertical 모드와는 달리 페이지 및 열의 마지막 입력이 필요치 않으므로 시작 지점만을 입력한다
  • 페이지 시작 주소 설정은 0xB0 + 원하는 페이지의 값(0~7)을 더해 전송하면 된다
  • 열 시작 주소는 전달받은 열을 상위 4bit, 하위 4bit로 나누어 하위 4bit는 0x0F, 상위 4bit는 LSB 방향으로 4비트 시프트 한 뒤 0x1F와 AND 연산한 결과값을 전송
  • ex) 100번째 열을 시작 주소로 설정할 때 100 = 0x64
  • 하위 4비트 : 0x64를 0x0F와 AND 연산해 하위 4비트 주소를 만듦 (0x64 & 0x0F = 0x04)
  • 상위 4비트 : 0x64를 LSB 방향으로 4비트 시프트 한 뒤, 0x0F와 AND 연산해 4비트 주소값을 구하고 이 값에 0x10을 더해 상위 4비트 주소를 만듦

[폰트]

 

The Dot Factory: An LCD Font and Image Generator

The Dot Factory is a small open source tool (MIT licensed) intended to generate the required C language information to store many fonts and images, as efficiently as possible, on a microcontroller. These fonts are then uploaded via the LCD driver (see the

www.eran.io

  • 링크의 프로그램을 이용해 폰트 배열을 생성한 뒤 사용
  • 시스템 내에 설치된 폰트를 바탕으로 입력된 문자(특문,대문자,소문자) 및 폰트 크기 설정에 따라 자동으로 문자 배열을 생성해줌
  • Sequential 핀 배열을 설정했으므로 연속된 두 페이지(0,1/2,3/...)에 폰트를 상하로 1Byte씩 나눠서 저장함
  • 두 페이지를 한 줄 기준으로 삼았기 때문에 변환된 폰트의 높이가 16bit 이하여야 한다
  • 예) 12x16 bit 크기로 변환된 A를 지정된 좌표에 입력
  • (같은 폰트 내에서도 각 글자마다 크기가 서로 다르므로 12x16 bit 내에서 차지하는 높이나 너비가 다르다)

/* @792 'A' (12 pixels wide) */
//
//      #
//     # #
//     # #
//     # #
//    #   #
//    #   #
//    #####
//   #     #
//   #     #
//   #     #
//  #       #
//
//
//
//
0x00, 0x00, 0x00, 0xE0, 0x9C, 0x82, 0x9C, 0xE0, 0x00, 0x00, 0x00, 0x00, //상단 페이지에 저장
0x00, 0x08, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x08, 0x00, 0x00, //하단 페이지에 저장

 

- SSD1306 문자 전송 함수

//문자와 입력 주소를 전달받아 디스플레이에 표현하는 함수
static void ssd1306_W_Char(uint8_t character_Code, uint8_t page, uint16_t column)
{
    uint8_t char_Buffer[font_width*2]={0};

    for(uint8_t i=0;i<font_width*2;i++)
    {
        char_Buffer[i]=ssd1306_Fonts[(character_Code-32)*(font_width*2)+i];
    }

    for(uint8_t i=0;i<2;i++)
    {
        ssd1306_Set_Coord(page+i,column);
        ssd1306_write_data(&char_Buffer[i*font_width],font_width);
    }
}
  1. 변환된 폰트의 너비*2만큼의 원소 개수를 갖는 배열 선언 (*2는 글자를 상/하 페이지로 나누어 저장하기 위함)
  2. 입력된 문자를 int 변수로 받아 ASCII 코드값을 기반으로 저장한 폰트 배열에서 매칭되는 문자를 찾는다
    • 폰트 배열의 경우 아스키 코드 32번 space부터 시작하여 126번 ~ 까지 순서대로 변환, 저장해서 사용했다
    • space의 아스키 코드값(32)과 폰트 배열 0번의 차인 32가 폰트 배열에서 입력된 문자를 찾는 기준점이 된다
    • 하나의 문자에서 다음 문자로 넘어가는데는 문자 너비*2 만큼의 수가 필요
    • (전달된 문자의 아스키 코드값-32)*(폰트 너비*2)를 하면 문자와 매칭되는 폰트 배열의 시작점이 나온다
  3. 해당 문자가 저장된 폰트 배열 시작점에서부터 순서대로 앞서 생성한 char_Buffer 배열에 폰트 데이터를 저장한다
  4. 페이지 및 열 포인터 시작 주소를 지정하고 우선 폰트 너비만큼의 데이터를 전송해 폰트의 상단 부분을 표시
  5. 페이지 주소 1 증가 및 열 시작 주소를 재지정한 뒤 남은 배열을 전송해 폰트 하단 부분을 완성

왼쪽 : 폰트 상단 부분 (0페이지), 오른쪽 : 폰트 상단 (0 페이지) + 하단 (1 페이지)

- SSD1306 문자열 전송 함수

//문자열과 입력 주소를 전달받아 디스플레이에 표현하는 함수
static void ssd1306_W_String(char *str, uint8_t page, uint8_t col)
{
    while(*str)
    {
        //문자가 입력될 열 주소에 폰트 너비를 더한 값이 열의 최대 값인 127을 넘는 경우
        //제대로 된 문자 표현이 불가능하므로 다음 페이지로 넘기거나 or 마지막 페이지일 경우, 전송 중단
        if((127<col+font_width))	
        {
            if(page==6)
            {
                    break;	//마지막 페이지일 경우, 전송 중단
            }
            page+=2;	//다음 페이지에 이어서 문자 입력
            col=0;	//첫번째 열부터 문자 입력 시작
        }
        ssd1306_W_Char(*str,page,col);	//문자 전송 함수에 문자 및 문자 입력 주소 전달

        col+=font_width;	//폰트 너비만큼 열 증가
        str++;			//문자열 주소 +1 (다음 문자로)
    }
}
  • NULL 값이 나올 때까지 전달받은 문자열 주소 1씩 증가시키며 while 문 반복
  • 문자 전송 함수를 사용해 입력 주소에 첫번째 문자부터 순차적으로 전송
  • 문자 입력 위치에 문자열을 입력할 공간이 부족하다면 다음 페이지로 넘어가 첫번째 열부터 문자 입력 시작
  • 마지막 페이지일 경우, 더 이상 문자를 입력할 공간이 없다면 전송 중단

- 전체 코드 및 간단한 반복 구동 예시 (twi_master_using_nrf_twi_mngr 예제 프로젝트 기반)

/**
 * Copyright (c) 2015 - 2019, Nordic Semiconductor ASA
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form, except as embedded into a Nordic
 *    Semiconductor ASA integrated circuit in a product or a software update for
 *    such product, must reproduce the above copyright notice, this list of
 *    conditions and the following disclaimer in the documentation and/or other
 *    materials provided with the distribution.
 *
 * 3. Neither the name of Nordic Semiconductor ASA nor the names of its
 *    contributors may be used to endorse or promote products derived from this
 *    software without specific prior written permission.
 *
 * 4. This software, with or without modification, must only be used with a
 *    Nordic Semiconductor ASA integrated circuit.
 *
 * 5. Any software provided in binary form under this license must not be reverse
 *    engineered, decompiled, modified and/or disassembled.
 *
 * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
 * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */
/** @file
 * @defgroup nrf_twi_master_example main.c
 * @{
 * @ingroup nrf_twi_example
 * @brief TWI Example Application main file.
 *
 * This file contains the source code for a sample application using TWI.
 */

#include "boards.h"
#include "app_util_platform.h"
#include "nrf_drv_clock.h"
#include "bsp.h"
#include "app_error.h"
#include "nrf_twi_mngr.h"

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

#include "nrf_twi_sensor.h"
#include "fonts.h"
#include "nrf_delay.h"

#define TWI_INSTANCE_ID             0
#define MAX_PENDING_TRANSACTIONS    20

#define SSD1306_ADDR 0x3C
#define ssd1306_data_select 0x40
#define ssd1306_cmd_select 0x00

#define font_width 12

static uint8_t ssd1306_w_buffer[2]={0};
static uint8_t ssd1306_display_buf[128]={0};

//Macro that simplifies defining a TWI transaction manager instance.
NRF_TWI_MNGR_DEF(m_nrf_twi_mngr, MAX_PENDING_TRANSACTIONS, TWI_INSTANCE_ID);

//Macro creating common twi sensor instance.
NRF_TWI_SENSOR_DEF(twi_ssd1306,&m_nrf_twi_mngr,MAX_PENDING_TRANSACTIONS);

void log_init(void)
{
    ret_code_t err_code;

    err_code = NRF_LOG_INIT(NULL);
    APP_ERROR_CHECK(err_code);

    NRF_LOG_DEFAULT_BACKENDS_INIT();
}

static void twi_config(void)
{
    uint32_t err_code;
    
    nrf_drv_twi_config_t const config={
      .scl=27,
      .sda=26,
      .frequency=NRF_DRV_TWI_FREQ_400K,
      .interrupt_priority=APP_IRQ_PRIORITY_MID
      };
    
    err_code=nrf_twi_mngr_init(&m_nrf_twi_mngr,&config);
    APP_ERROR_CHECK(err_code);
}

void ssd1306_write_cmd(uint8_t ssd1306_cmd)
{    
    nrf_twi_sensor_reg_write(&twi_ssd1306,SSD1306_ADDR,ssd1306_cmd_select,&ssd1306_cmd,1);
}

static void ssd1306_init(void)
{
    ssd1306_write_cmd(0xA8);
    ssd1306_write_cmd(0x3F);

    ssd1306_write_cmd(0xD3);
    ssd1306_write_cmd(0x00);

    ssd1306_write_cmd(0x40);

    ssd1306_write_cmd(0xA1);

    ssd1306_write_cmd(0xC8);

    ssd1306_write_cmd(0xDA);
    ssd1306_write_cmd(0x12);

    ssd1306_write_cmd(0x20);
    ssd1306_write_cmd(0x02);

    ssd1306_write_cmd(0x81);
    ssd1306_write_cmd(0x7F);

    ssd1306_write_cmd(0xA4);

    ssd1306_write_cmd(0xA6);

    ssd1306_write_cmd(0xD5);
    ssd1306_write_cmd(0x80);

    ssd1306_write_cmd(0x8D);
    ssd1306_write_cmd(0x14);

    ssd1306_write_cmd(0xAF);
}


void ssd1306_write_data(uint8_t* data_buffer, int buf_length)
{
    nrf_twi_sensor_reg_write(&twi_ssd1306,SSD1306_ADDR,ssd1306_data_select,&data_buffer[0],buf_length);
}

void ssd1306_clear(void)
{
    static uint8_t clear_buffer[128]={0};

    ssd1306_write_cmd(0x00);
    ssd1306_write_cmd(0x10);

    for(int i=0;i<8;i++)
    {
        ssd1306_write_cmd(0xB0+i);
        ssd1306_write_data(clear_buffer,sizeof(clear_buffer)/sizeof(uint8_t));
    }
}

static void ssd1306_fill(void)
{
    static uint8_t fill_buffer[128];

    for(int i=0;i<128;i++)
    {
        fill_buffer[i]=0xff;
    }

    ssd1306_write_cmd(0x00);
    ssd1306_write_cmd(0x10);

    for(int i=0;i<8;i++)
    {
        ssd1306_write_cmd(0xB0+i);
        ssd1306_write_data(fill_buffer,sizeof(fill_buffer)/sizeof(uint8_t));
    }
}

static void ssd1306_Set_Coord(uint8_t page, uint8_t col)
{
    uint8_t col_low=0x0F,col_high=0x1F;
    col_low=(col&0x0F);
    col_high=0x10|((col>>4)&0x0F);
    ssd1306_write_cmd(0xB0+page);
    ssd1306_write_cmd(col_low);
    ssd1306_write_cmd(col_high);
}

static void ssd1306_W_Char(uint8_t character_Code, uint8_t page, uint16_t column)
{
    uint8_t char_Buffer[font_width*2]={0};

    for(uint8_t i=0;i<font_width*2;i++)
    {
        char_Buffer[i]=ssd1306_Fonts[(character_Code-32)*(font_width*2)+i];
    }

    for(uint8_t i=0;i<2;i++)
    {
        ssd1306_Set_Coord(page+i,column);
        ssd1306_write_data(&char_Buffer[i*font_width],font_width);
    }
}

static void ssd1306_W_String(char *str, uint8_t page, uint8_t col)
{
    while(*str)
    {
        if((127<col+font_width))
        {
            if(page==6)
            {
                    break;
            }
            page+=2;
            col=0;
        }
        ssd1306_W_Char(*str,page,col);

        col+=font_width;
        str++;
    }
}

int main(void)
{
    ret_code_t err_code;
    
    log_init();

    NRF_LOG_RAW_INFO("\r\nTWI master example started. \r\n");
    NRF_LOG_FLUSH();
    
    twi_config();
    err_code=nrf_twi_sensor_init(&twi_ssd1306);
    APP_ERROR_CHECK(err_code);
    
    ssd1306_init();
    
    char number_str[48];
    sprintf(number_str,"0123456789!@#$%^&*()0123456789!@#$%^&*()012345",sizeof(number_str));

    while (true)
    {
        nrf_delay_ms(2000);
        ssd1306_clear();
        nrf_delay_ms(2000);
        ssd1306_W_String("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz{|}~",0,0);
        nrf_delay_ms(2000);
        ssd1306_W_String(number_str,0,0);
        nrf_delay_ms(2000);
        ssd1306_fill();
        
        NRF_LOG_FLUSH();
    }
}
/** @} */
  1. SSD1306 초기화
  2. 48개의 배열을 갖는 char 변수 선언 및 sprintf 함수를 사용해 숫자, 특수문자를 입력 (폰트 너비 12, 문자 4줄 입력)
  3. 디스플레이 빈 화면으로 초기화
  4. 문자열 입력 (첫번째 페이지, 0번째 열부터 마지막 페이지까지 순서대로 표현 가능한 부분까지만 입력)
  5. 앞서 선언 및 문자열을 입력한 char 변수를 SSD1306에 전송 (동일하게 첫 페이지, 0번째 열부터 마지막 페이지까지 표현 가능한 부분만 입력)
  6. 0xFF 배열 128개를 0~7페이지까지 전송해 디스플레이의 모든 LED를 켬
  7. 다시 3번으로

 

 

+ Recent posts