SK6812 RGBW LED 스트립

  • 사용 보드 : nRF52840-PDK
  • 사용 IDE : SEGGER Embedded Studio for ARM (노르딕 제품 사용하는 경우 무료 라이센스 이용 가능)
  • 작동 전압 : 4.5~5.5V
  • 로직 인풋 레벨 (Din에 연결할) : -0.5~VDD+0.5
  • (3V 작동 전압 + 3V 로직 레벨로도 작동이 가능하다)
  • nRF52840-PDK 보드의 VDD(3V)를 통해 SK6812 전압 공급
  • 신호 전송에 사용될 GPIO Output high voltage = VDD 이므로 문제없이 구동된다
  • 특별히 사용 지정된 통신 방법은 없음
  • SDK 내 I2S 예제 프로젝트를 기반으로 작성 (\examples\peripheral\i2s)
  • (SDK 다운로드 : https://developer.nordicsemi.com/)

[동작]

<SPI를 통한 제어 시도>

<I2S를 통한 SK6812 RGBW LED 구동>

- SCK 설정

  • SK6812 모듈에 유효한 데이터 전송을 위해서는 0.3us, 0.6us, 0.9us (허용 오차 ±0.15us)의 크기를 갖는 데이터 블럭을 전송해야 한다
  • 데이터 1비트를 전송하는데 필요한 클럭(SCK)을 설정하기 위해선 다음과 같은 과정이 필요하다

nRF I2S

  1. 주어진 클럭(32Mhz)을 8~125 사이의 값으로 나눠서 Master Clock를 구함
    • MCK = 32Mhz / 8~125 (MCK : Master Clock)
  2. MCK를 32~512 사이의 값으로 나눠서 LRCK를 구함
    • LRCK = MCK / CONFIG.RATIO (32~512)
  3. 위의 과정을 통해 계산된 LRCK 값에 Sample Width 및 2를 곱한 값이 Serial Clock 값이 된다
    1. SCK = 2 * LRCK * CONFIG.SWIDTH (Sample Width)
  • 실제 사용 예시)
  • config.mck_setup = NRF_I2S_MCK_32MDIV10;
  • config.ratio = NRF_I2S_RATIO_32X;
  • config.sample_width = NRF_I2S_SWIDTH_16BIT;
    • MCK = 32 Mhz / 10 = 3.2 Mhz
    • LRCK = 3.2 / 32 = 0.1 Mhz
    • SCK = 2 * 0.1 * 16 = 3.2 Mhz
  • 따라서 1비트를 전송하는데는 0.3125us 시간이 걸린다

- 전송 데이터 구성

SK6812 데이터 인식 순서 (실제로는 RGBW가 아닌 GRBW 순으로 인식)

  • LED 각 색깔은 8개의 데이터(0~255 표현)로 구성되어있고 RGBW 네 개의 색을 가지고 있으므로 총 32개의 데이터가 전송되어야 한다
  • 하나의 데이터는 0 or 1로 표현할 수 있고 이 데이터를 표현하기 위해선 4개의 비트가 필요한다
  • 예) R0=0 이라면 1000 (0x8), R0=1이라면 1100 (0xC)로 구성됨

SK6812를 작동 시키는데 필요한 데이터 타이밍

  • 데이터의 0을 표현하기 위해선 0.3us 시간 동안 HIGH + 0.9 us 시간 동안 LOW 의 신호를 가져야 한다 (0x8, 1000)
  • 데이터의 1을 표현하기 위해선 0.6us 시간 동안 HIGH + 0.6 us 시간 동안 LOW 의 신호를 가져야 한다 (0xC, 1100)
  • 그러므로 하나의 색깔에 대한 전체 데이터는 4 bits * 8 = 32 bits 를 가지고 하나의 모듈을 구동하는데 필요한 데이터는 32 * 4(RGBW) = 128 bits 가 필요하다
  • 예) 빨간색 126 단계(0~255)를 표현하기 위해선 1000 0000 (R7 -> R0)의 데이터가 필요하고 이 데이터를 표현하기 위해서 0xC + 0x8 + 0x8 + ... + 0x8 (0xC + 0x8*7) 총 32비트를 사용

- I2S 전송 순서

CONFIG.FORMAT = Aligned, CONFIG.SWIDTH = 8Bit, CONFIG.CHANNELS = Stereo

  • L, R 데이터가 번갈아가며 전송된다 (그래프의 경우엔 SWIDTH=8 Bit로 L, R 각각은 8비트 데이터를 갖는다)
  • (SK6812에 전송할 신호 설정은 위 그래프 설정에서 SWIDTH만 16 Bit로 바꾼 상태)
  • 위의 설정(그래프 설정에서 SWIDTH만 16비트로 변경)에서 32 비트 신호를 전송하는 경우 15~0 비트가 LEFT, 31~16 비트가 RIGHT 채널에 해당한다
  • 예를 들어, Red=97을 설정하는 경우에 0110 0001 의 데이터가 필요하고 이 데이터는 0x8CC8 888C 로 구성된다
  • 그리고 이 데이터를 888C 8CC8 순서로 전송해야 제대로 된 밝기 단계가 설정된다
  • (만약 순서를 바꾸지 않고 전송한다면 R=22 로 설정된다)

- SK6812 내에서 데이터 처리

SK6812 LED에서 데이터를 받아들이는 방법

  • 그래프에서처럼 3개의 SK6812 LED에 데이터를 전송하는 경우, 첫 32개의 데이터는 SDOUT이 연결된 첫번째 LED에 그 다음 32개의 데이터는 두번째 LED, 마지막 32개 데이터는 세번째 LED에 전송된다
  • (LED에 전송된 데이터의 MSB부터 32개의 데이터(128Bit)만 남기고 나머지 부분만 다음 LED에 전송)
  • 때문에 특정 위치의 LED 색상을 바꾸기 위해 필요한 만큼의 데이터를 전송하는 것은 불가능하다
  • SK6812에 LED 데이터를 전송한 후, 다음 데이터를 전송하기 위해선 80us 시간 이상의 리셋 시간이 필요

[코드]

- 전역변수 및 정의

#define sk6812_led_num 1	//SK6812 LED 갯수 설정
#define sk6812_rst_buffer 8	//리셋, 32bit*8= 256bit = 0.3125us*256 = 80us
#define sk6812_buf_len sk6812_led_num*4+8	//리셋을 포함한 전송할 LED 데이터 총 길이(32비트 기준)

/*
하나의 색을 표현하는데 32비트 데이터가 필요하므로 unsigned 32bit integer 배열 선언
[0][0]=Green, [0][1]=Red, [0][2]=Blue, [0][3]=White 의 색상 정보를 저장
이중배열이지만 첫번째 행(sk6812_buffer[0][x])만 사용
*/
static uint32_t sk6812_buffer[2][sk6812_buf_len]={0};

static bool i2s_running=false;	//데이터 중복 전송을 피하는데 사용

- 구조체

/*
LED 색상 정보를 저장하고 버퍼에 반영하는데 사용
*/
typedef struct sk6812_rgbw{
  uint8_t red;
  uint8_t green;
  uint8_t blue;
  uint8_t white;
}sk6812_rgbw_struct;

- I2S 데이터 핸들러

/*
I2S 초기화와 함께 설정된 데이터 핸들러는 전송 직후 및 전송 완료 후 호출된다
*/
static void i2s_data_handler(nrf_drv_i2s_buffers_t const * p_released,
                         uint32_t                      status)
{
    static uint8_t transfer_chk=0;

    /*
    p_released - 이전에 드라이버에 전달된 버퍼에 대한 포인터
    전송이 처음 실행되어 호출됐을 땐, 전송된 데이터가 없기 때문에 p_released->p_tx_buffer=0이다
    전송 이후엔 p_released->p_tx_buffer엔 마지막으로 전송된 데이터 버퍼가 존재
    */
    if(p_released->p_tx_buffer!=0)	//전송이 이뤄진 뒤엔 p_tx_buffer엔 데이터가 존재하므로 실행됨
    {
      transfer_chk++;	//데이터가 전송됐다면 값 증가해 데이터 전송이 일어났음을 표시
    }

    if(transfer_chk!=0)	//데이터 전송이 일어난 이후
    {
      nrf_drv_i2s_stop();	//I2S 전송 중단
      i2s_running=false;	//I2S 전송 중이 아님을 표시
      transfer_chk=0;		//데이터 전송 여부 
    }
}

- I2S 데이터 전송 함수

/*
I2S를 통한 데이터 전송에 사용되는 함수. 데이터는 DMA를 통해 전송된다
*/
void sk6812_send_buffer()
{
  uint32_t err_code = NRF_SUCCESS;
  
  //I2S 구조체 변수 선언 및 전송 버퍼에 전송할 데이터가 있는 SK6812 버퍼 주소 입력
  nrf_drv_i2s_buffers_t const i2s_buffers = {
      .p_tx_buffer=sk6812_buffer[0],
  };

  //I2S 통신이 사용 중이 아니라면 전송 실행
  if(i2s_running==false)
  {
    err_code = nrf_drv_i2s_start(&i2s_buffers, sk6812_buf_len, 0);
    //sk6812_buffer[0]부터 sk6812_buf_len 길이만큼 전송 (32bit 기준)
    
    APP_ERROR_CHECK(err_code);
    i2s_running==true;	//중복 전송이 일어나지 않게 하기 위해 전송 중임을 나타내는 true로 변경
  }
}

- SK6812 색상 설정 함수

/*
전달받은 색상 구조체 변수와 LED 위치(index)를 기반으로 
버퍼의 원하는 부분에 LED 색상 데이터를 설정함
*/
void sk6812_set_color(sk6812_rgbw_struct* set_color, uint8_t index)
{
  uint32_t temp_g=0, temp_r=0, temp_b=0, temp_w=0;
  //전달받은 구조체 색상값을 SK6812에서 인식하는 데이터로 변환 후 저장하는데 사용할 32비트 변수
  
  /*
  8비트 색상값의 MSB부터 0비트 자리로 비트 시프트 한 뒤에 0x1을 AND 연산
  나온 값이 1이면 0xC, 0이면 0x8을 32비트 temp_ 변수의 MSB부터 차례대로 입력
  */
  for(uint8_t i=0;i<8;i++)	//Green
  {
    if((set_color->green>>(7-i))&0x1==1)
    {
      temp_g|=0xC<<(7-i)*4;
    }
    else
    {
      temp_g|=0x8<<(7-i)*4;
    }

    if((set_color->red>>(7-i))&0x1==1)	//Red
    {
      temp_r|=0xC<<(7-i)*4;
    }
    else
    {
      temp_r|=0x8<<(7-i)*4;
    }

    if((set_color->blue>>(7-i))&0x1==1)	//Blue
    {
      temp_b|=0xC<<(7-i)*4;
    }
    else
    {
      temp_b|=0x8<<(7-i)*4;
    }

    if((set_color->white>>(7-i))&0x1==1)	//White
    {
      temp_w|=0xC<<(7-i)*4;
    }
    else
    {
      temp_w|=0x8<<(7-i)*4;
    }
  }
  
  /*
  I2S 통신은 L, R이 번갈아가면서 전송되고 L, R 각 크기는 SWIDTH 설정에 의해 결정됨
  SWIDTH=16 Bit 로 설정해 사용하므로 16비트 간격으로 L, R이 나뉘어 전송된다
  LEFT는 15~0비트, RIGHT는 31~16비트를 차지하는데 LEFT 먼저 전송이 시작되므로
  제대로 된 전송을 위해선 LEFT<->RIGHT 비트 위치 전환을 해야 한다 (MSB->LSB 순으로 전송)
  */
  sk6812_buffer[0][index*4]=((temp_g&0x0000FFFF)<<16)|((temp_g&0xFFFF0000)>>16);
  sk6812_buffer[0][index*4+1]=((temp_r&0x0000FFFF)<<16)|((temp_r&0xFFFF0000)>>16);
  sk6812_buffer[0][index*4+2]=((temp_b&0x0000FFFF)<<16)|((temp_b&0xFFFF0000)>>16);
  sk6812_buffer[0][index*4+3]=((temp_w&0x0000FFFF)<<16)|((temp_w&0xFFFF0000)>>16);

  sk6812_send_buffer();	//SK6812 버퍼 전송 함수 호출
  nrf_delay_ms(1);
}

- 메인

/*
SDK Example 폴더 내 I2S 프로젝트 기반
*/
int main(void)
{
    uint32_t err_code = NRF_SUCCESS;
    APP_ERROR_CHECK(err_code);

    //I2S 설정
    nrf_drv_i2s_config_t config = NRF_DRV_I2S_DEFAULT_CONFIG;	//I2S 환경설정 구조체 변수 선언
    config.sdout_pin = I2S_SDOUT_PIN;	//SDOUT PIN 설정 (예제 기준 P0.27)
    config.mck_setup = NRF_I2S_MCK_32MDIV10;	(MasterClock = 32Mhz / 10)
    config.ratio     = NRF_I2S_RATIO_32X;
    config.sample_width=NRF_I2S_SWIDTH_16BIT;	(L, R 각 채널이 갖는 데이터 비트 크기)
    config.channels  = NRF_I2S_CHANNELS_STEREO;	(스테레오 설정)
    
    //I2S 환경설정 변수와 데이터 핸들러를 기반으로 I2S 통신 초기화
    err_code = nrf_drv_i2s_init(&config, i2s_data_handler);	
    APP_ERROR_CHECK(err_code);

    while(true)
    {  

    }
}

- 단순 사용 예시) 8개의 LED를 R->G->B->W->R 순서로 색상 변환 (각 색상의 밝기는 128 단계로 설정)

#define sk6812_led_num 8

int main(void)
{
    uint32_t err_code = NRF_SUCCESS;
    APP_ERROR_CHECK(err_code);

    //I2S 설정
    nrf_drv_i2s_config_t config = NRF_DRV_I2S_DEFAULT_CONFIG;	//I2S 환경설정 구조체 변수 선언
    config.sdout_pin = I2S_SDOUT_PIN;	//SDOUT PIN 설정 (예제 기준 P0.27)
    config.mck_setup = NRF_I2S_MCK_32MDIV10;	(MasterClock = 32Mhz / 10)
    config.ratio     = NRF_I2S_RATIO_32X;
    config.sample_width=NRF_I2S_SWIDTH_16BIT;	(L, R 각 채널이 갖는 데이터 비트 크기)
    config.channels  = NRF_I2S_CHANNELS_STEREO;	(스테레오 설정)
    
    //I2S 환경설정 변수와 데이터 핸들러를 기반으로 I2S 통신 초기화
    err_code = nrf_drv_i2s_init(&config, i2s_data_handler);	
    APP_ERROR_CHECK(err_code);

    //SK6812 색상 설정에 사용할 색상 구조체 변수 선언
    sk6812_rgbw_struct temp_rgbw;
    
    //모든 색상에 대해 0의 값을 설정
    temp_rgbw.red=0;
    temp_rgbw.green=0;
    temp_rgbw.blue=0;
    temp_rgbw.white=0;
    
    //모든 LED 버퍼에 0의 색상값 입력 후 전송, i=LED 순서(SDOUT이 연결된 LED가 시작점)
    for(uint8_t i=0;i<sk6812_led_num;i++)
    {
      sk6812_set_color(&temp_rgbw,i);
    }

    int PAUSE_TIME=1000;
    
    /*
    모든 LED에 동일한 색상을 지정한 뒤 전송하고 PAUSE_TIME 시간동안 정지된 후 
    다음 색상으로 변환 (빨강 -> 초록 -> 파랑 -> 흰색 순으로 무한 루프)
    */
    while(true)
    {  
        temp_rgbw.red=128;
        temp_rgbw.green=0;
        temp_rgbw.blue=0;
        temp_rgbw.white=0;
        for(uint8_t i=0;i<sk6812_led_num;i++)
        {
          sk6812_set_color(&temp_rgbw,i);
        }
        nrf_delay_ms(PAUSE_TIME);

        temp_rgbw.red=0;
        temp_rgbw.green=128;
        temp_rgbw.blue=0;
        temp_rgbw.white=0;
        for(uint8_t i=0;i<sk6812_led_num;i++)
        {
          sk6812_set_color(&temp_rgbw,i);
        }
        nrf_delay_ms(PAUSE_TIME);

        temp_rgbw.red=0;
        temp_rgbw.green=0;
        temp_rgbw.blue=128;
        temp_rgbw.white=0;
        for(uint8_t i=0;i<sk6812_led_num;i++)
        {
          sk6812_set_color(&temp_rgbw,i);
        }
        nrf_delay_ms(PAUSE_TIME);

        temp_rgbw.red=0;
        temp_rgbw.green=0;
        temp_rgbw.blue=0;
        temp_rgbw.white=128;
        for(uint8_t i=0;i<sk6812_led_num;i++)
        {
          sk6812_set_color(&temp_rgbw,i);
        }
        nrf_delay_ms(PAUSE_TIME);
    }
}

FOR 문 하나에 해당하는 데이터 전송 신호 (8번의 LED 전체 색상 데이터 전송)
8개의 LED 색상 데이터에 해당하는 데이터 신호 (32*4*8=1024 Bit)
LED 8개의 색상 데이터 전송 중 첫번째 LED에 Blue=128가 입력

  • I2S 전송 함수(sk6812_send_buffer())를 색상 설정 함수(sk6812_set_color) 내부에 넣었기 때문에 하나의 FOR문이 실행될 때 LED 갯수인 8개만큼의 전체 버퍼 전송이 이루어진다
  • 단순히 사용의 편의를 위해 I2S 전송 함수를 색상 설정 함수 내부에 넣은 것이므로 전송 함수를 색상 설정 함수에서 제외하고 LED 색상 버퍼의 데이터를 전부 바꾼 뒤에 따로 전송 함수를 호출하는 식으로 사용해도 된다
/*
사용 LED 4개, sk6812_set_color() 함수 내의 I2S 전송 함수 제외한 뒤 따로 사용 예
SK6812에 전송할 데이터 버퍼의 색상 정보 전체를 원하는 값으로 바꾼 후에 I2S 전송 함수 호출
첫번째 LED : Red 128, 두번째 LED : Green 128, 세번째 LED : Blue 128, 네번째 LED : White 128 의 값을 가짐
*/
temp_rgbw.red=128;
temp_rgbw.green=0;
temp_rgbw.blue=0;
temp_rgbw.white=0;
sk6812_set_color(&temp_rgbw,0);	//첫번째 LED 색상 버퍼 변경

temp_rgbw.red=0;
temp_rgbw.green=128;
temp_rgbw.blue=0;
temp_rgbw.white=0;
sk6812_set_color(&temp_rgbw,1);	//두번째 LED 색상 버퍼 변경

temp_rgbw.red=0;
temp_rgbw.green=0;
temp_rgbw.blue=128;
temp_rgbw.white=0;
sk6812_set_color(&temp_rgbw,2);	//세번째 LED 색상 버퍼 변경

temp_rgbw.red=0;
temp_rgbw.green=0;
temp_rgbw.blue=0;
temp_rgbw.white=128;
sk6812_set_color(&temp_rgbw,3);	//네번째 LED 색상 버퍼 변경

sk6812_send_buffer();

 

사용한 MAX7219 7-sement 모듈

  • 통신 방법 : SPI (최대속도 : 10Mhz)
  • 작동 전압 : 4.0~5.5
  • 환경 : P-NUCLEO-WB55 개발 보드, Atollic TrueSTUDIO

[동작]

타이밍 다이어그램
데이터 포맷

  • 16 비트 데이터 포맷 사용 (D15~D12 사용 X)
  • D11~D8 ADDRESS(명령어 역할), D7~D0 DATA(MSB to LSB, 해당 명령어 설정값)
  • 작동 시작 됐을 때, 모든 컨트롤 레지스터는 리셋, 디스플레이는 비어있는 상태이고 MAX7219는 셧다운 모드에 진입한다
  • 디스플레이를 사용하기 전에 디스플레이 드라이버를 프로그래밍해야한다
  • 그렇지 않으면, 첫번째 자리를 스캔하도록 설정되고 데이터 레지스터의 데이터를 디코딩하지 않으며 밝기 레지스터는 최소값으로 설정된다

- 레지스터 어드레스 맵

  • MAX7219에 전달할 16비트 데이터 중 상위 8비트에 사용되는 어드레스 목록 (명령어 역할)

- 디코드 모드

디코드 모드 레지스터

  • 디코드 모드 레지스터는 BCD 코드 B(0-9, E, H, L, P, -)를 설정하거나 각 자리에 대해 디코드 미사용 설정
  • 레지스터의 각 비트는 디스플레이 숫자 자리에 해당 (D0=디스플레이 첫째 자리, D1=둘째 자리, ...)
  • 로직 하이(1)는 Code B 디코딩을 선택하고 로직 로우(0)는 디코더를 우회한다
  • 코드B 디코드 모드가 사용될 때, 디코더는 숫자 레지스터 데이터의 하위 니블(D3-D0)만을 보고 D4-D6 비트는 무시한다
  • 소수점을 설정하는 D7은 디코더 설정에 대해 독립적이고 로직 1일 때 소수점이 켜진다
  • 디코드 미사용 설정을 한 뒤, 해당 디스플레이 자리의 데이터 비트 D7-D0은 MAX7219의 세그먼트 라인에 해당
  • (ex : 0x0900 을 전송해 디스플레이 모든 자리를 디코드 미사용으로 설정하고 0x0130 (Digit 0+Segement Line B,C)을 전송하면 디스플레이 첫번째 자리에 세그먼트 라인 B, C가 켜진다)

숫자 레지스터 데이터 / 왼쪽 : 디코드 사용, 오른쪽 : 디코드 미사용
디코드 미사용 모드에서 세그먼트 라인 설정

- 밝기 조절

밝기 강도 레지스터

  • 디스플레이 밝기는 Intensity 레지스터를 통해 디지털 방식으로 조절할 수 있다
  • 디스플레이 밝기의 디지털 컨트롤은 intensity 레지스터의 하위 니블로 제어되는 내부 PWM을 통해 제공된다
  • 모듈레이터 스케일은 RSET에 의해 설정된 최대 전류의 최대 31/32에서 1/32까지 16단계로 평균 세그먼트 전류를 조정한다

- 스캔 리미트 레지스터

스캔 리미트 레지스터

  • 스캔 리미트 레지스터는 디스플레이의 1~8번째 자리 중 몇개의 자리를 사용할지를 설정한다
  • 스캔된 숫자의 갯수는 디스플레이 밝기에 영향을 주기 때문에 스캔 리미트 레지스터를 디스플레이의 여백 부분으로 사용해서는 안된다 (선두의 의미없는 0을 감추는 것처럼)
  • 표에서 보이는 것처럼 두번째 이상의 자리를 사용하면서 하위 자리의 디스플레이를 미사용하는 것은 불가능하다
  • (사용 설정을 한 뒤, 해당 자리를 공백으로 채우는 것과는 다름)

- 디스플레이 테스트 레지스터

디스플레이 테스트 레지스터

  • 디스플레이 테스트 레지스터는 테스트 모드, 노멀 모드 설정을 할 수 있다
  • 디스플레이 테스트 모드는 모든 컨트롤 및 자리 레지스터(셧다운 레지스터 포함)를 변경이 아닌 재정의 해서 모든 LED를 켠다
  • 디스플레이 테스트 모드에선 8자리가 스캔되고 31/32 듀티 사이클을 가진다
  • 디스플레이 테스트 레지스터가 노멀 동작으로 재설정되기 전까지는 테스트 모드 유지

- No-Op 레지스터

  • No-Op 레지스터는 MAX7219를 직렬로 연결할 때 사용
  • 모든 디바이스의 LOAD/CS 인풋을 묶어 연결하고 DOUT을 인접한 디바이스의 DIN에 연결한다
  • DOUT은 CMOS 로직 레벨 출력으로 연속적으로 직렬 연결된 파트의 DIN을 손쉽게 구동한다
  • 사용 예) 네 개의 MAX 7219가 직렬 연결되어있고 네 번째 칩에 데이터를 쓰기 위해 원하는 16비트 워드를 전송한 다음 3개의 비작동 코드를 전송한다
  • LOAD/CS 가 HIGH가 될 때, 모든 디바이스의 데이터가 잠긴다.
  • 처음 세 개의 칩은 No-Op 명령어를 수신하고 네번째 디바이스는 목표한 데이터를 수신한다

[CUBE MX 설정]

SPI 설정 / 데이터 사이즈 16 Bits, 보드 레이트 8.0Mbits/s, Hardware NSS Output Signal 사용
SPI 인터럽트 활성화

  • STM32WBXX 의 경우, SPI1 보드 레이트는 APB2 Peripheral Clock을 기준으로 정해진다
  • 디폴트 설정인 APB2 Peripheral clock 32Mhz, SPI1 Prescaler 4 의 값으로 SPI1 보드 레이트는 8.0Mbits/s
  • MAX7219는 데이터를 수신하는 역할만 하므로 Transmit Only Master 선택
  • NSS - CS 역할 (Master일 경우에만 Output 사용 가능, 사용 보드의 경우 NSS핀으로 PA4 자동 사용 설정)
  • (Hardware NSS를 사용하지 않고 GPIO를 소프트웨어 컨트롤해 CS 라인을 제어해도 된다)

[코드]

- 정의

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

#define max7219_SPI hspi1		//MAX7219 통신에 사용되는 SPI를 사용하기 편하게 재정의
//HAL_SPI_Transmit_IT(&max7219_SPI, ...) = HAL_SPI_Transmit_IT(&hspi1, ...) (SPI1 사용)

#define max7219_No_Op_Mode 0x00	//No-Op 모드

#define max7219_Decode_Mode 0x09	//디코드 모드

#define max7219_Intensity_Mode 0x0A	//디스플레이 밝기 모드

#define max7219_Scan_Limit_Mode 0x0B	//스캔 리미트 모드

#define max7219_Display_Test_Mode 0x0F	//디스플레이 테스트 모드
#define max7219_Display_Test_Enter 1	//테스트 모드 진입
#define max7219_Display_Test_Exit 0		//테스트 모드 종료, 노멀 모드로 전환

#define max7219_Shutdown_Mode 0x0C	//셧다운 모드
#define max7219_Shutdown_Enter 0		//셧다운 모드 진입
#define max7219_Shutdown_Exit 1		//셧다운 모드 종료, 노멀 모드로 전환

#define max7219_blank 0x0F			//디코드 모드에서 디스플레이 공백 표현에 사용
/* USER CODE END PD */

- 명령 함수

/*
명령어와 명령어에 따른 설정값을 받아와 전송하는 함수
*/
static void max7219_cmd(int cmd, int data)	//cmd=ADDRESS(명령어 역할), data=명령어 설정값
{
	uint16_t write_buffer=0;	//MAX7219에 전송할 16비트 변수
    
	write_buffer=(cmd<<8)|data;		
	//전달받은 명령어는 상위 8비트, 명령어에 따른 설정값은 하위 8비트에 저장
    
	HAL_SPI_Transmit_IT(&max7219_SPI,(uint8_t *)&write_buffer,1);	//MAX7219에 전송

	while(HAL_SPI_GetState(&max7219_SPI)==HAL_SPI_STATE_BUSY_TX)
	{
		//전송 완료까지 대기
	}
}
  • 16비트 포맷을 사용해야 하므로 MAX7219에 전송할 16비트 변수를 선언
  • 명령어와 명령어에 따른 설정값을 받아와서 앞서 선언한 16비트 변수의 상위 8비트에는 명령어(ADDRESS)를 하위 8비트에는 데이터(명령어 설정값)을 입력한다
  • MAX7219의 모든 기능이 동일한 데이터 수신 방법을 사용하므로 이 함수만으로도 모든 기능을 사용할 수 있다

- 숫자 입력 함수

/*
숫자를 입력할 자리와 입력할 데이터를 받아와 전송하는 함수
단순히 명령어를 입력하는 것과 디스플레이에 표현할 숫자를 입력하는 것을 구분 짓기 위해 만든 것으로
데이터 전달 방식에서 max7219_cmd() 함수와의 차이는 없다
*/
static void max7219_write_value(int digit, int value, int decimal_point)
//digit=숫자를 입력할 자리(1~8), value=입력할 데이터, 소수점 표현 (0=미사용, 1=사용)
{
	uint16_t write_buffer=0;	//MAX7219에 전달할 16비트 변수 선언
    
    /*
    디코드 모드를 사용할 때, (-, E, H, L, P, ' ')은 아스키 코드값으로 10이 넘는 인티저 값을 갖는 것을 이용
    (value!=0x20) 은 디코드 미사용할 때, 세그먼트 라인 B를 표기하는데 문제가 생기는 것을 막기 위함
    (' ' 과 동일한 값을 같기 때문에 사용자가 0x20을 입력하면 ' ' 취급되어 실제론 0x0F가 입력된다)
    */
	if((20<value)&&(value!=0x20))
	{
		char w_char=(char)value;
		switch(w_char)
		{
			case '-' :
				value=0x0A;	//디코드 모드에서 '-' 를 표현하기 위한 레지스터 데이터 값
				break;
			case 'e' :
			case 'E' :
				value=0x0B;
				break;
			case 'h' :
			case 'H' :
				value=0x0C;
				break;
			case 'l' :
			case 'L' :
				value=0x0D;
				break;
			case 'p' :
			case 'P' :
				value=0x0E;
				break;
			case ' ' :
				value=0x0F;
				break;
			default :
				break;
		}
	}
	write_buffer=((digit+1)<<8)|value;
	//디스플레이의 자릿수는 0x01부터 시작해 0x08까지 사용

	if(decimal_point==1)
	{
		write_buffer|=0x80;
	}
	//소수점을 사용할 경우, 7번째 비트를 High(1)로 설정

	HAL_SPI_Transmit_IT(&max7219_SPI,(uint8_t *)&write_buffer,1);
	while(HAL_SPI_GetState(&max7219_SPI)==HAL_SPI_STATE_BUSY_TX)
	{

	}
}
  • 단순히 명령어 전달과 디스플레이 표현을 구분지어 사용하기 위해 선언한 함수
  • 전달하는 데이터의 크기 및 데이터 구조는 명령 함수와 동일하다
  • 소수점 표현의 경우, 7번 비트를 로직 High(1)로 설정해주면 된다
  • ex ) 디스플레이 2번째 자리에 2. 을 표현하기 위해선 (디스플레이 첫번째 자리 = Digit 0 이라 가정)
  • 상위 8비트 : 디스플레이 4번째 자리 Digit 3 = 0x04 (Digit0=0x01 부터 시작 (0x00 (X))
  • 하위 8비트 : 숫자 2 = 0x02, 소수점 = 0x70 => 0x72

- 초기화 예시

int main(void)
{
  	...
  max7219_cmd(max7219_Shutdown_Mode,max7219_Shutdown_Exit);	//셧다운 모드 종료, 노멀 작동 시작
  max7219_cmd(max7219_Scan_Limit_Mode,0x07);	//스캔 리미트 모드, 0x07=디스플레이 8자리 모두 사용
  max7219_cmd(max7219_Decode_Mode, 0xFF);	//디코드 모드, 디스플레이 8자리 모두 디코드 모드로 설정
  max7219_cmd(max7219_Intensity_Mode, 0x08);	//디스플레이 밝기, 중간으로 설정 (0x00~0x0F 16단계)
  
}
  • 작동이 시작 됐을 때  MAX7219는 셧다운 모드에 진입한다고 되있었으므로 셧다운 모드 종료를 먼저 실행
  • 디스플레이를 사용하기 전에 프로그래밍을 하지 않을 경우, 첫째 자리를 스캔하도록 설정되고 데이터 레지스터의 데이터를 디코딩하지 않으며 밝기 레지스터는 최소값으로 설정되므로
  • 스캔 리미트 모드에서 디스플레이 8자리 모두 사용 설정 (0x00~0x07)
  • 디코드 모드에서 디스플레이 8자리 모두 디코드 모드 사용 설정 (데이터 비트=디스플레이 자릿수, 1=디코드 사용)
  • 디스플레이 밝기 모드에서 16단계 중 9단계로 설정

- 단순 사용 예시

int main(void)
{
	...
  /* USER CODE BEGIN 2 */
  
  max7219_cmd(max7219_Shutdown_Mode,max7219_Shutdown_Exit);
  max7219_cmd(max7219_Scan_Limit_Mode,0x07);
  max7219_cmd(max7219_Decode_Mode, 0x00);	//디스플레이 모든 자리 디코드 미사용
  max7219_cmd(max7219_Intensity_Mode, 0x08);
  
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    
	  max7219_cmd(max7219_Decode_Mode, 0);	//모든 자리 디코드 미사용
      
	  for(uint8_t i=0;i<8;i++)	//디스플레이 모든 자리 공백으로 초기화
	  {
	  	max7219_write_value(i,0,0);
	  	//디코드 모드 공백 : 0x0F, 디코드 미사용 공백 : 0x00
  	  }
  
 	  for(int i=0;i<8;i++)
  	  {
	  	  int k=0;
	  	  for(int j=0;j<8;j++)
	  	  {
		  	  max7219_write_value(i,k|=(0x01)<<j,0)	
		  	  //비트0 부터 MSB로 1비트씩 비트 시프트 및 OR 연산으로 해당 자리의 모든 세그먼트 라인을 켬
		  	  HAL_Delay(100);
	  	  }
	  }
      
      
	  max7219_cmd(max7219_Decode_Mode,0xFF);	//모든 자리 디코드 사용
      
	  for(uint8_t i=0;i<8;i++)
	  {
		  max7219_write_value(i,max7219_blank,0);	
		  //디스플레이 모든 자리 공백으로 초기화 (DATA : 0x0F)
	  }
      
	  for(uint8_t i=0;i<8;i++)
	  {
		  max7219_write_value(i,'-',1);	//디스플레이 -. (소수점 표기 사용)으로 채움
		  HAL_Delay(500);
	  }
      
	  for(uint8_t i=0;i<8;i++)
	  {
		  max7219_write_value(i,i,0);	//디스플레이 자릿수와 일치하는 숫자로 채움
		  HAL_Delay(500);
	  }
	  max7219_write_value(7,'H',0);
	  HAL_Delay(500);
	  max7219_write_value(6,'E',0);
	  HAL_Delay(500);
	  max7219_write_value(5,'L',0);
	  HAL_Delay(500);
	  max7219_write_value(4,'P',0);
	  HAL_Delay(500);
  }
  /* USER CODE END 3 */
}
  • 디스플레이 모든 자리 사용, 디코드 미사용, 밝기 9단계로 초기화
  • 디스플레이 모든 자리를 공백으로 초기화 (디코드 미사용일 때 공백을 표현하기 위한 데이터값은 0x00)
  • 디스플레이 0번부터 7번까지 순차적으로 세그먼트 라인을 하나씩 채워나감
  • 디스플레이 모든 자리를 디코드 사용으로 바꾼 뒤 공백으로 초기화 (디코드 모드 공백 데이터 : 0x0F)
  • (디코드 사용/미사용 전환을 해도 각 자리의 레지스터 데이터는 유지된 상태로 존재하기 때문에 디코드 사용/미사용에 따라 디스플레이 표현만 변한다)
  • 디스플레이 0번부터 7번까지 순차적으로 '-.' 문자를 표시
  • 디스플레이 자리와 동일한 숫자값을 디스플레이 0번부터 7번까지 순차적으로 표현
  • 디스플레이 7번부터 4번까지 순차적으로 H, E, L, P 문자 데이터 입력

디스플레이의 5번째 자리에 숫자 5를 입력했을 때

  • 디스플레이 자리는 0x01(=0번째)부터 시작하므로 상위 8비트 0x06은 Digit 5(다섯번째 자리)를 ADDRESS로 사용했음을 의미한다
  • 하위 8비트는 0x05이므로 디스플레이 다섯번째 자리에 숫자 5가 표현된다

MLX90614ESF-BCC 모델을 탑재한 모듈

  • 작동 전압 : 2.6~3.6V
  • 통신 방식 : PWM, I2C(10~100KHz)
  • 주변 온도 -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 사이의 방사율로 조정할 수 있다

[작동]

  • 측정된 온도값은 RAM내 지정된 주소에 16비트 16진수 데이터로 저장된다
  • 명령어를 통해 RAM, EEPROM에 저장된 각종 데이터들을 읽거나 필요에 따라 변경할 수 있다
  • 작동 시작 후 첫번째 데이터는 0.25초 뒤에 얻을 수 있다
  • 각 주소는 16bit의 데이터를 갖는다
  • 읽기/쓰기 둘 다 LSByte(16비트 데이터의 하위 8비트), MSByte(16비트 데이터의 상위 8비트) 순으로 데이터 전송이 진행된다

- EEPROM

EEPROM 주소표

  • EEPROM 내에 제한된 주소들만이 변경 가능하지만 전체 주소를 읽는 것은 가능하다
  • 원하는 주소에 값을 쓰기 전에 반드시 해당 주소에 0을 입력해 기존 데이터를 지워준 뒤 원하는 값을 입력해야 한다
  • 0x04 방사율의 경우, 0.1~1.0 까지 조정 가능하고 변경에 따른 재교정은 하지 않아도 된다
  • Factory calibration 을 유지하기 위해 특별한 도구 없이는 다음 비트와 레지스터를 변경해서는 안된다
  • Emissivity [15...0]; Config Register1 [14...11;7;3]; addresses 0x0F and 0x19

- RAM

RAM 주소표

  • 쓰기는 불가능하고 제한된 램 레지스터만 읽는 것이 가능하다
  • 0x06 - 주변 온도, 0x07 - 물체 온도1, 0x08 - 물체 온도2 가 저장된다

- Command

명령어표

  • RAM, EEPROM의 접근을 원하는 경우, x xxxx 부분에 Read/Write를 원하는 주소를 입력한다
  • (ex : RAM Obj1 온도 : 0x07, EEPROM Emissivity(방사율) : 0x24)

- Read

데이터 읽기 예) 램 0x07 주소에 저장된 Object1 온도 측정값을 읽어올 때

  • 센서 주소 : 0x5A (MSB부터 주소가 채워져야 하므로 MSB로 1비트 시프트 한 값이 0xB4)
  • 사용자가 수신할 데이터는 LSByte, MSByte, PEC 총 3 바이트
  • SDA 라인에서 첫번째 바이트는 0xB4(0x5A<<1+W(0))
  • 두번째 바이트는 사용자가 입력한 명령어
  • Repeated Start Condition 이후 세번째 바이트는 센서 주소+Read 비트=0xB5 (0x5A<<1+R(1))
  • 네번째 해당 주소의 LSByte, 다섯번째 해당 주소의 MSByte (LSB가 먼저 오기 때문에 이후에 순서를 바꿔줘야 한다)
  • 마지막 여섯번째 바이트는 0xB4(첫번째 바이트)~MSByte까지 총 5바이트를 CRC-8 계산을 통해 얻은 CRC값

-Write

데이터 쓰기 예) EEPROM 0x02 주소(PWMCTRL)에 데이터를 입력할 때

  • EEPROM에만 데이터 쓰기가 가능하다
  • 원하는 주소에 데이터를 쓰려면 반드시 해당 주소에 0x0000(LSByte(0x00)+MSByte(0x00))을 먼저 입력해 기존 16비트 데이터를 지운 뒤에 원하는 데이터를 입력해야 한다
  • 0x0000을 입력하고 최소 5ms(권장 10ms) 기다린 뒤 원하는 16비트 데이터를 입력하고 다시 5ms(권장 10ms) 기다린 후에 제대로 값이 입력됐는지 Read 명령어 통해 확인
  • 사용자가 전송할 데이터는 Command, LSByte, MSByte, PEC 총 4바이트
  • 첫번째 바이트는 센서 주소 0x5A<<1 + W(0) = 0xB4
  • 두번째 바이트는 명령어 (위의 경우 EEPROM 액세스 001x xxxx + PWMCTRL에 데이터를 쓸 것이므로 0x02 -> 0010 0000 + 0000 0010 = 0010 0010 = 0x22 가 된다)
  • 세번째 바이트는 입력할 16비트 데이터의 하위 8비트
  • 네번째 바이트는 입력할 16비트 데이터의 상위 8비트
  • 다섯번째 바이트는 첫번째 0xB4 바이트부터 네번째 MSByte까지 총 4바이트를 CRC-8 계산을 통해 얻은 CRC 값

[CUBE MX 설정]

  • 센서와의 통신에 사용할 I2C 설정 및 데이터 검증에 CRC-8 사용하므로 CRC 사용 설정

 Standard Mode(100KHz) 사용
인터럽트 활성화
CRC 설정 (8-bit, X2+X1+X0, Init Value=0)

[코드]

- 사용에 필요한 함수들 정의

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include <math.h>	//pow 함수 사용
/* USER CODE END Includes */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define mlx90614xCx	//MLX90614xCx 타입을 사용하지 않는 경우 마스크 처리 필요
#define MLX90614_addr 0x5A	// MLX90614 Slave Address
#define mlx_cmd_pointer 1	// 수신, 전송 버퍼내 명령어(CMD)를 입력할 배열 위치
#define mlx_cmd_amb 0x06	// Ta(Ambient Temperature) Address (RAM)
#define mlx_cmd_obj_1 0x07	// Tobj1(Object1 Temperature) Address (RAM)
#define mlx_cmd_obj_2 0x08	// Tobj2(Object1 Temperature) Address (RAM)
#define mlx_cmd_emissivity 0x24	// Emissivity Address (EEPROM)
#define mlx_cmd_emissivity_2 0x2F	// MLX90614xCx 버전에 한해 방사율을 변경했을 때 함께 조정해줘야 하는 레지스터 (EEPROM)
/* USER CODE END PD */

- 전역 변수 선언

/* USER CODE BEGIN PV */
uint8_t mlx_read_buffer[6]={0xB4,0x00,0xB5,0};	//ADDR((0x5A<<1)+W(0)), CMD, ADDR((0x5A<<1)+R(1)), LSByte, MSByte, PEC
uint8_t mlx_write_buffer[5]={0xB4,0};	//ADDR((0x5A<<1)+W(0)), CMD, LSByte, MSByte, PEC
uint8_t mlx_write_zero[5]={0xB4,0};	//ADDR((0x5A<<1)+W(0)), CMD, LSByte, MSByte, PEC
/* USER CODE END PV */
  • Read/Write에 사용할 전역 변수 배열 선언
  • 용이한 PEC 계산을 위해 Read/Write에서 PEC에 사용되는 데이터의 갯수만큼 배열 선언
  • Read의 경우 첫번째, 세번째 배열은 0xB4, 0xB5의 값을 사용하므로 그에 맞춰 배열 선언 및 초기화
  • Write의 경우 첫번째 바이트만 0xB4로 고정되므로 그에 맞춰 배열 선언 및 초기화

- 측정된 온도를 읽어오는 함수

void mlx_read_temperature(void)
{
	double temp_obj_1, temp_obj_2, temp_amb; //물체1, 물체2, 주변 온도를 저장할 변수
	uint8_t crc_obj_1,crc_obj_2,crc_amb;	 //물체1, 물체2, 주변 온도 CRC값 저장할 변수

	/*
	Ambient temperature(센서 주변 온도), RAM-0x06
	*/
	mlx_read_buffer[mlx_cmd_pointer]=mlx_cmd_amb;
	//데이터 버퍼의 CMD 부분에 데이터 수신에 사용할 명령어 입력 (단순 PEC계산용)
    
	HAL_I2C_Mem_Read_IT(&hi2c1,(uint16_t)MLX90614_addr<<1,mlx_cmd_amb,I2C_MEMADD_SIZE_8BIT,
			(uint8_t *)&mlx_read_buffer[3],3);
	/*
	인터럽트 사용해 데이터 수신, 슬레이브의 특정 메모리 주소로부터 데이터를 읽어오는 함수
	사용할 I2C, 슬레이브 주소(MSB로 1비트 시프트 필요), 접근할 내부 메모리 주소(CMD), 내부 메모리 주소 크기, 
	수신할 데이터 저장할 포인터 주소(mlx_read_buffer[3]), 수신할 데이터 갯수(mlx_read_buffer[3]~[5])
	*/
    
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	//수신 완료(I2C Ready 상태)까지 대기
    
	temp_amb=(mlx_read_buffer[4]<<8|mlx_read_buffer[3]);
	/*
	16비트로 구성된 센서 데이터가 하위 8비트->상위 8비트 순으로 수신되므로 mlx_read_buffer[4]에
	저장된 MSByte를 MSB로 8비트 시프트 한 뒤 LSByte(mlx_read_buffer[3])와 합친다
	*/
    
	temp_amb=(temp_amb*0.02)-273.15;
	//10진수로 변환한 온도 측정값을 섭씨 온도로 변환하는 과정이 필요하다
	//Ta[°C] = (Tareg*0.02)-273.15	//(Ta register*0.02)=켈빈온도
    
	crc_amb=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_read_buffer,5);
	//mlx_read_buffer에 저장된 0xB5, 0x07(CMD), 0xB4, LSByte, MSByte를 CUBE MX를 통해 설정한
	//값들을 기준으로 CRC값을 계산한다
    
	if(crc_amb==mlx_read_buffer[5])
	{
		printf("Ambient Temperature : %.2lf\n",temp_amb);
        //센서로부터 수신한 PEC(Packet Error Code, mlx_read_buffer[5])와 계산한 CRC값이 일치하는 경우 출력
	}
	else
	{
    		//PEC값과 CRC계산값이 일치하지 않는 경우
	}
	
	/*
	Object 1 temperature, RAM-0x07
	*/
	mlx_read_buffer[mlx_cmd_pointer]=mlx_cmd_obj_1;
	HAL_I2C_Mem_Read_IT(&hi2c1,(uint16_t)MLX90614_addr<<1,mlx_cmd_obj_1,I2C_MEMADD_SIZE_8BIT,
    	(uint8_t *)&mlx_read_buffer[3],3);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	temp_obj_1=(mlx_read_buffer[4]<<8|mlx_read_buffer[3]);
	temp_obj_1=(temp_obj_1*0.02)-273.15;
	crc_obj_1=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_read_buffer,5);

	if(crc_obj_1==mlx_read_buffer[5])
	{
		printf("Obj 1 Temperature : %.2lf\n",temp_obj_1);
	}
	else
	{
    		//PEC값과 CRC계산값이 일치하지 않는 경우
	}

	/*
	Object 2 temperature, RAM-0x07
	*/
	mlx_read_buffer[mlx_cmd_pointer]=mlx_cmd_obj_2;
	HAL_I2C_Mem_Read_IT(&hi2c1,(uint16_t)MLX90614_addr<<1,mlx_cmd_obj_2,I2C_MEMADD_SIZE_8BIT,
    	(uint8_t *)&mlx_read_buffer[3],3);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	temp_obj_2=(mlx_read_buffer[4]<<8|mlx_read_buffer[3]);
	temp_obj_2=(temp_obj_2*0.02)-273.15;
	crc_obj_2=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_read_buffer,5);

	if(crc_obj_2==mlx_read_buffer[5])
	{
		printf("Obj 2 Temperature : %.2lf\n",temp_obj_2);
	}
	else
	{
    		//PEC값과 CRC계산값이 일치하지 않는 경우
	}
}

 

  • 센서로부터 온도값을 읽어오는 함수
  • 값을 읽어오는데 사용할 데이터 버퍼의 형태는
  • {0xB4, 0x00, 0xB5, 0x00, 0x00, 0x00} //(센서주소<<1+W(0), CMD, 센서주소<<1+R(1), LSByte, MSByte, PEC)
  • 용이한 CRC 계산을 위해 첫번째 0xB4, 세번째 0xB5는 값을 고정한 상태로 두고 두번째 0x00(CMD)는 사용할 명령어에 따라 변경. 그리고 나머지 배열 세칸에 센서로부터 수신한 데이터를 저장한 뒤 [0]~[4]까지 CRC 계산
  • RAM에 저장된 Ambient Temperature, Object1 Temperature, Object2 Temperature 데이터의 수신 및 처리 방식은 명령어(CMD)를 제외하고는 전부 동일

- 방사율(Emissivity) 변경 함수

void mlx_change_emissivity(float new_emissivity)
{
	/*
	방사율(Emissivity) 변경은 EEPROM 내 0x04 번지의 16비트 데이터를 변경하는 것으로 이뤄질 수 있다
	(xCx 모델의 경우 0x0F 번지의 데이터 변경도 필요하다)
	*/
    
	uint8_t crc_emissivity, crc_emissivity_2;	
	/*
	MLX90614xCx 센서의 경우, 방사율을 변경할 때 EEPROM - 0x04(Emissivity)에 더해
	0x0F 번지에 저장된 데이터값도 변경해야 되기 때문에 0x04, 0x0F CRC 값을 저장하는 변수를 두개 생성
	*/
    
	uint16_t new_04, new_0f;//변경할 방사율에 따른 계산값을 0x04, 0x0F 번지에 저장하는데 사용할 변수
	double old_04,old_0f;	//기존에 저장된 0x04, 0x0F 주소의 데이터를 저장할 변수 (xCx 타입에만 필요한 부분)

	printf("New E : %.2lf\r\n",new_emissivity);	//사용자가 입력한 새 방사율값 출력
	new_04=round(65536*(new_emissivity)-1.0);	
	//데이터 시트에 나와있는 공식을 사용해 EEPROM-0x04 번지에 저장할 새 방사율값 계산
	printf("New E (Calculated) : %d, %X\r\n",new_04,new_04);	//계산된 새 방사율값 출력

/*
MLX90614xCx 타입을 사용하는 경우 0x0F 번지의 데이터 변경도 필요하므로 아래의 과정들이 필요하다
xAx, xBx 타입을 사용하는 경우 사용할 필요가 없는 부분
*/
#ifdef mlx90614xCx
	//EEPROM - 0x04 번지에 저장된 현재 방사율값을 읽어온다 (새 방사율값 계산에 필요, xCx 모델만 필요한 부분)
	mlx_read_buffer[mlx_cmd_pointer]=mlx_cmd_emissivity;
	HAL_I2C_Mem_Read_IT(&hi2c1,(uint16_t)MLX90614_addr<<1,mlx_cmd_emissivity,I2C_MEMADD_SIZE_8BIT,
    	(uint8_t *)&mlx_read_buffer[3],3);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	crc_emissivity=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_read_buffer,5);
	old_04=((mlx_read_buffer[4]<<8)|mlx_read_buffer[3]);
	if(mlx_read_buffer[5]==crc_emissivity)	//CRC 계산값과 수신한 PEC값 비교
	{
		printf("Old 0x04 : %d, %X\r\n",(int)old_04,(int)old_04);	
		//계산된 CRC값과 수신한 PEC값이 일치, 현재 0x04 번지에 저장된 방사율 출력
	}
	else	
	{
    		//계산된 CRC값과 수신한 PEC값이 일치하지 않는 경우
	}

	//EEPROM - 0x0F 번지에 저장된 새로운 방사율값을 계산하는데 필요한 데이터를 읽어온다 (xCx 모델만 필요한 부분)
	mlx_read_buffer[mlx_cmd_pointer]=mlx_cmd_emissivity_2;
	HAL_I2C_Mem_Read_IT(&hi2c1,(uint16_t)MLX90614_addr<<1,mlx_cmd_emissivity_2,I2C_MEMADD_SIZE_8BIT,
    	(uint8_t *)&mlx_read_buffer[3],3);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	crc_emissivity_2=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_read_buffer,5);
	old_0f=((mlx_read_buffer[4]<<8)|mlx_read_buffer[3]);
	if(mlx_read_buffer[5]==crc_emissivity_2)	//CRC 계산값과 수신한 PEC값 비교
	{
		printf("Old 0x0F : %d, %X\r\n",(int)old_0f,(int)old_0f);
		//계산된 CRC값과 수신한 PEC값이 일치, 현재 0x0F 번지에 저장된 데이터 출력
	}
	else	
	{
    		//계산된 CRC값과 수신한 PEC값이 일치하지 않는 경우
	}
	
	new_0f=round((old_04/new_04)*old_0f);	//xCx 타입만 해당 (xAx, xBx는 이 변수 자체가 필요 없음)
	//제조사에서 제공하는 데이터 시트에 나와있는 식을 기반으로 EEPROM-0x0F 번지에 저장할 값 계산
    printf("New 0x0F : %d, %X\r\n",new_0f,new_0f);
    
#endif

	/*
	EEPROM에 데이터를 쓰기 전에 우선적으로 데이터를 쓸 주소에 0x000 값을 입력해
	저장된 16비트 데이터를 지워야 할 필요가 있다
	mlx_write_zero 배열의 첫번째 바이트만 0xB4 (센서 주소0x5A<<1+W(0), PEC 계산에 사용)로 고정해 두고
	mlx_write_zero[1] - CMD(필요에 따라 변경, PEC 계산뿐만이 아니라 실제 전송에서 CMD로 사용된다)
	mlx_write_zero[2], [3] - LSByte, MSByte (0x0000을 입력해야되므로 둘 다 0x00의 값을 가진다)
	mlx_write_zero[4] - PEC (0xB4, CMD를 CRC 계산한 결과)
	mlx_write_zero[1]~mlx_write_zero[4]까지의 배열이 센서로 전송된다
	*/
	mlx_write_zero[1]=mlx_cmd_emissivity;	//Write CMD, 0x24(0010 0000 + 0x04)
	mlx_write_zero[4]=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_write_zero,4);  //[0]~[3] CRC 계산 결과
    
	/*
	-센서에 전송될 데이터 버퍼 설정, mlx_write_buffer[0] - 0xB4로 고정 (단순 PEC 계산용)
	mlx_write_buffer[1] - CMD 입력 (실제 전송에서 CMD로 사용 및 PEC 계산에 사용됨) 
	mlx_write_buffer[2] - 새로 계산한 방사율의 LSByte 저장
	mlx_write_buffer[3] - 새로 계산한 방사율의 MSByte 저장
	mlx_write_buffer[4] - mlx_write_buffer[0]~[3]까지의 CRC 계산값 저장
	mlx_write_buffer[1]~mlx_write_buffer[4]까지의 배열이 센서로 전송된다
	*/
	mlx_write_buffer[1]=mlx_cmd_emissivity;	//0x24(0010 0000 + 0x04) (EEPROM - 0x04 (Emissivity)
	mlx_write_buffer[2]=new_04&0xff;	//LSByte
	mlx_write_buffer[3]=(new_04>>8)&0xff;	//MSByte
	mlx_write_buffer[4]=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_write_buffer,4);

	printf("New_04 : %d, %X, %X, %X	crc : %X\r\n",new_04,new_04,mlx_write_buffer[2],mlx_write_buffer[3],mlx_write_buffer[4]);
    
	HAL_I2C_Master_Transmit_IT(&hi2c1,MLX90614_addr<<1,(uint8_t *)&mlx_write_zero[1],4);
	//쓰기를 원하는 주소에 먼저 0x0000 16비트 데이터를 써서 현재 저장된 값을 지운다
	//값을 지울 때도 PEC를 같이 전송해야 한다 (CMD, 0x00, 0x00, PEC)
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	//I2C Ready 상태까지 대기
	HAL_Delay(10);
	//딜레이 사용해 쓰기 완료 후 10ms 대기 (최소 5ms, 권장 10ms)
    
	HAL_I2C_Master_Transmit_IT(&hi2c1,MLX90614_addr<<1,(uint8_t *)&mlx_write_buffer[1],4);
	//CMD, 계산된 방사율을 상/하 8비트씩 나눈 값과 계산된 PEC값을 전송
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	HAL_Delay(10);
	//딜레이 사용해 쓰기 완료 후 10ms 대기 (최소 5ms, 권장 10ms)

	/*
	EEPROM - 0x0F 주소에 데이터 전송
	xCx 타입만 해당되는 사항으로 new_0f=round((old_04/new_04)*old_0f); 를 통해 얻은 16비트 데이터를
	0x0F 주소에 전송한다. 전송 순서 및 방법은 0x04에 데이터 쓸 때와 동일
	*/
    
//MLX90614xCx 타입을 사용할 경우, 0x0F 번지에 위에서 계산한 새로운 데이터를 입력
//과정은 위의 0x04 번지에 데이터를 쓸 때와 CMD가 다른 것 빼고는 동일
#ifdef mlx90614xCx
	mlx_write_zero[1]=mlx_cmd_emissivity_2;	//0x2F (001x xxxx + 0x0F)
	mlx_write_zero[4]=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_write_zero,4);
	mlx_write_buffer[1]=mlx_cmd_emissivity_2;	//0x2F (001x xxxx + 0x0F)
	mlx_write_buffer[2]=new_0f&0xff;
	mlx_write_buffer[3]=(new_0f>>8)&0xff;
	mlx_write_buffer[4]=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_write_buffer,4);
    
	HAL_I2C_Master_Transmit_IT(&hi2c1,MLX90614_addr<<1,(uint8_t *)&mlx_write_zero[1],4);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	HAL_Delay(10);

	HAL_I2C_Master_Transmit_IT(&hi2c1,MLX90614_addr<<1,(uint8_t *)&mlx_write_buffer[1],4);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	HAL_Delay(10);
#endif

	//EEPROM-0x04 번지에 값이 변경됐는지 확인
	mlx_read_buffer[mlx_cmd_pointer]=mlx_cmd_emissivity;
	HAL_I2C_Mem_Read_IT(&hi2c1,(uint16_t)MLX90614_addr<<1,mlx_cmd_emissivity,I2C_MEMADD_SIZE_8BIT,
    	(uint8_t *)&mlx_read_buffer[3],3);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	crc_emissivity=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_read_buffer,5);
	if((mlx_read_buffer[3]==(new_04&0xff))&&(mlx_read_buffer[4]==((new_04>>8)&0xff)))
	{
		printf("0x04 : Successfully Changed\r\n");	
		//0x04번지에 입력한 새 방사율 데이터와 읽어 온 0x04번지의 방사율 데이터가 동일할 때
	}
	else
	{	
		//0x04번지에 저장된 방사율이 사용자가 입력한 방사율과 다를 때
	}

#ifdef mlx90614xCx
	//EEPROM-0x0F 번지에 값이 변경됐는지 확인
	mlx_read_buffer[mlx_cmd_pointer]=mlx_cmd_emissivity_2;
	HAL_I2C_Mem_Read_IT(&hi2c1,(uint16_t)MLX90614_addr<<1,mlx_cmd_emissivity_2,I2C_MEMADD_SIZE_8BIT,
    	(uint8_t *)&mlx_read_buffer[3],3);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	crc_emissivity=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_read_buffer,5);
	if((mlx_read_buffer[3]==(new_0f&0xff))&&(mlx_read_buffer[4]==((new_0f>>8)&0xff)))
	{
		printf("0x0F : Successfully Changed\r\n");
		//0x0F번지에 입력한 새 방사율 데이터와 읽어 온 0x0F번지의 방사율 데이터가 동일할 때
	}
	else
	{
		//0x0F 번지에 저장된 데이터가 사용자가 입력한 데이터와 다른 경우
	}
#endif
}
  • Emissivity(방사율) 변경에 사용할 함수
  • 0.1~1.0까지 변경 가능하고 방사율 변경에 따른 온도 측정값 교정은 필요하지 않다
  • EEPROM에 데이터를 쓰기 전에 반드시 해당 주소에 0x0000을 전송해 데이터를 지운 뒤 새롭게 입력할 데이터를 전송해야 한다
  • MLX90614xAx, xBx 타입은 EEPROM - 0x04(Emissivity) 번지의 값만을 주어진 계산식을 통해 얻은 새 방사율값으로 변경하면 되지만 xCx 타입은 0x04 및 0x0F 번지에 현재 저장되어있는 데이터를 기반으로 하는 공식(0x04와는 다른)을 활용해 값을 산출한 뒤 이를 0x0F 번지에 추가로 전송해줘야 한다
  • xCx 타입을 사용할 경우, 혹시 모를 상황을 대비해 EEPROM-0x0F 번지에 저장되어있는 데이터를 따로 기록해 두는 것을 권장 (0x0F 번지에 저장되는 값은 0x04, 0x0F 번지의 데이터를 기반으로 계산된 값이라 한번 잘못 저장하면 돌이킬 수 없다)
  • EEPROM에 데이터를 쓴 직후와 그 다음 쓰기 or 읽기 사이에 적어도 5ms(10ms-안전)의 대기 시간이 필요하다
  • Read 함수와는 달리 센서 내 특정 메모리에 접근해서 데이터를 쓰는 방식이 아니기 때문에 CMD도 전송 데이터 버퍼 안에 포함되어 있어야 한다. (CMD, LSByte, MSByte, PEC 순으로 총 4개의 배열을 전송)
  • 센서로부터 데이터를 읽어 올 때와 동일하게 LSByte->MSByte 순으로 전송이 진행된다
  • 쉬운 CRC 계산을 위해 첫번째 배열([0],0x5A<<1)을 고정값으로 둔 채 두번째 배열([1])부터 마지막 배열까지만을 센서에 데이터를 쓰는데 사용
  • 방사율을 변경한 이후, 전원을 껏다 켜서 변경한 방사율값이 유지되는지 확인한다
  • (방사율을 변경하고 전원을 재인가 하지 않았을 때 온도 측정이 부정확해지는 현상 겪음, 재인가 후 온도 측정 정상 및 변경된 방사율 유지 확인)

- 현재 센서에 설정된 방사율을 읽어오는 함수

void mlx_read_emissivity()
{
	uint8_t crc_emissivity;		//CRC 계산값 저장에 사용
	uint16_t read_emissivity;	//EEPROM-0x04 번지로부터 읽어 방사율 데이터를 저장할 변수
	float cur_emissivity;		//읽어 온 방사율 데이터를 실제 방사율로 변환한 값을 저장할 변수
    
	mlx_read_buffer[mlx_cmd_pointer]=mlx_cmd_emissivity;	//EEPROM-0x04
	HAL_I2C_Mem_Read_IT(&hi2c1,(uint16_t)MLX90614_addr<<1,mlx_cmd_emissivity,I2C_MEMADD_SIZE_8BIT,
    	(uint8_t *)&mlx_read_buffer[3],3);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	crc_emissivity=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_read_buffer,5);	//CRC 계산
	
	if(crc_emissivity==mlx_read_buffer[5])	//계산한 CRC값과 읽어 온 PEC 값이 일치할 때
	{
		read_emissivity=(mlx_read_buffer[4]<<8|mlx_read_buffer[3]);	
		//8비트씩 나누어진 방사율 데이터를 비트 시프트와 OR을 사용해 하나로 합침
        
		cur_emissivity=(read_emissivity+1)/65536.0;	
		//식을 통해 실제 방사율 값으로 변환
        
		printf("Current Emissivity : %.2f\r\n",cur_emissivity);	
	}
	else
	{
    		//읽어 온 PEC값과 계산한 CRC값이 일치하지 않을 때
	}
}

- 슬립 모드

슬립모드 예시

  • 슬립 모드에 진입하기 위해선 0xFF 명령어와 0xB4+0xFF를 CRC 계산한 결과값인 0xE8만 센서에 전송하면 된다
  • 전류 소비를 2.5μA로 제한하기 위해서 슬립 상태를 유지하는 동안 SCL핀을 Low로 유지하는 것을 권장. SCL 핀에 연결된 내부 통합 제너 다이오를 통한 누설 전류가 있기 때문. SCL 핀을 Push-Pull로 설정하고 SCL 핀에 Pull-Up 레지스터가 연결되어있지 않은 상태여야 한다.
  • 슬립 모드를 종료하기 위해선 SCL이 High인 상태에서 SDA를 최소 33ms 동안 Low로 유지하거나
  • or POR (Power on Reset)
void mlx_sleep()
{
	uint8_t enter_sleep[2]={0xFF,0xE8};	//0xFF-Sleep CMD, 0xE8-PEC (0xB4+0xFF)
	HAL_I2C_Master_Transmit_IT(&hi2c1,(uint16_t)MLX90614_addr<<1,(uint8_t *)&enter_sleep,2);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
}

void mlx_wakeup()
{
	/*
	MLX90614와의 통신에 사용하는 I2C를 해제한 뒤, SDA로 사용하던 GPIO 핀을 새로운 설정으로 초기화
	SDA핀이 Low를 출력하도록 설정한 뒤 딜레이 함수를 이용해 33ms를 대기함으로써 슬립 모드에서 벗어난다
	이후 SDA핀에 대한 GPIO설정을 해제한 뒤 MLX90614를 사용하기 위해 I2C를 다시 초기화한다
	*/
	HAL_I2C_DeInit(&hi2c1);	//MLX90614와의 통신에 사용하는 I2C 해제
    
	/*
	SCL로 사용되던 GPIO 핀을 새롭게 설정
	*/
	GPIO_InitTypeDef GPIO_InitStruct = {0};
	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_RESET);	//통신에 사용하던 I2C SDA 핀과 핀이 속한 포트
	GPIO_InitStruct.Pin = GPIO_PIN_9;	//SDA로 사용하던 핀 번호
	GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;	//Output Mode
	GPIO_InitStruct.Pull = GPIO_NOPULL;	//No Pull-Up
	HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);	//GPIO 초기화 시작
	HAL_Delay(33);	//33ms 대기 (SDA핀은 0인 상태)
	HAL_GPIO_DeInit(GPIOB,GPIO_PIN_9);	//GPIO 설정 해제
    
	HAL_I2C_Init(&hi2c1);	//MLX90614 통신에 사용하던 I2C 초기화 시작
	HAL_Delay(250);	
	//센서가 정상 작동을 시작하고 0.25초 후에 유효한 첫 데이터를 얻을 수 있으므로 250ms 동안 대기
}
  • I2C 통신을 통해 슬립 모드를 벗어나는 방법은 찾지 못했기 때문에 I2C 설정을 해제하는 방법으로 진행
  • MLX90614와의 통신에 사용하던 I2C를 해제한 뒤 I2C SDA핀으로 사용되던 GPIO핀을 LOW 출력 설정
  • (SCL핀의 경우 High 상태를 유지하는 것을 확인했으므로 SDA핀만 재설정)
  • 딜레이 함수를 통해 슬립 모드에서 벗어나기 위한 최소 시간인 33ms 동안 대기 (SDA핀 LOW 상태)
  • 이후 GPIO핀 설정을 해제하고 센서와의 통신을 위해 I2C를 다시 초기화한다

mlx_sleep() 함수 실행 이후 바로 wakeup 함수 호출한 상황
wake up 이후 0.25초 대기
대기 후 온도 mlx_read_temperature() 함수 시작

- 예시

void main(void)
{
	...
	/* USER CODE BEGIN 2 */
	HAL_Delay(1000);
	mlx_read_emissivity();	//EEPROM-0x04 번지에 현재 설정되어있는 방사율 읽어옴
	mlx_change_emissivity(1.0);	//방사율 1.0으로 변경
	printf("\n");
  	mlx_read_emissivity();	//현재 설정되어있는 방사율 읽기 (설정 변경 확인용)
	mlx_change_emissivity(0.9);	//방사율 0.9로 변경
	printf("\n");
	mlx_read_emissivity();	//현재 설정되어있는 방사율 읽기 (설정 변경 확인용)
	mlx_sleep();	//슬립 모드 진입
	printf("Sleep\n");
	mlx_wakeup();	//슬립 모드에서 빠져나옴
	printf("Wake up\n");
	mlx_read_emissivity();	//현재 설정되어있는 방사율 읽기 (설정 변경 확인용)
  	/* USER CODE END 2 */

  	/* Infinite loop */
  	/* USER CODE BEGIN WHILE */
  	while (1)
  	{
  	/* USER CODE END WHILE */

  	/* USER CODE BEGIN 3 */

  		mlx_read_temperature();
  		HAL_Delay(1000);
  	}
  	/* USER CODE END 3 */
}
  • 방사율 1.0 설정 후, 0.9로 재변경

출력 결과

- xCx 타입의 경우

  • 딜레이가 없는 경우, 디버그가 실행 직후부터 main() 함수가 재실행되어 mlx_change_emissivity() 함수가 호출 및 실행되다가 디버그 창이 뜨면서 도중에 중단되는 상황이 발생
  • 이로인해, EEPROM 내에 데이터를 쓰던 중에 통신이 중단되면서 내부 데이터가 기존 데이터+새로운 데이터가 혼재된 상태가 되었다
  • EEPROM-0x04 번지의 방사율은 고정된 값과 주어진 방사율을 기반으로 계산된 값이 들어가기 때문에 이에따른 문제가 없었지만 0x0F 번지의 데이터는 기존에 저장된 0x04, 0x0F 번지의 데이터를 기반으로 새로운 값이 만들어지기에 비정상적인 값이 저장되는 현상이 발생했다
  • 때문에 딜레이 함수를 사용해 mlx_change_emissivity() 함수가 실행되는 시기를 늦춤으로써 디버그 준비 직후부터 실행 직전까지의 mlx_change_emissivity() 함수 호출이 안되게끔 했다

 

+ Recent posts