SSD1306 드라이버 칩셋을 사용한 128x64 OLED 모듈

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

 

[기본 구동 방식]

Write Mode - 데이터 구성

  • Control Byte+Data Byte/Command Byte를 전송해 OLED 기능 또는 디스플레이에 표현될 데이터를 설정
  • Slave Address : 0x3C (011 110+SA0) (D/C 핀 HIGH, LOW 설정에 따라 SA0 비트를 0 or 1로 설정해 주소를 정할 수 있는데 사용 모듈의 경우 0으로 고정되어 있음)
  • 첫 바이트는 다른 I2C 통신과 동일하게 Slave Address + R/W bit (8 bit)로 구성
  • Slave Address 다음에 오는 Control byte는 뒤이어 전송될 데이터가 Command(명령어) / Data Byte일지를 결정
  • Control Byte는 Co(0) + D/C#(Command=0, Data=1) + 00000 로 구성된다
  • 명령어 전송 : Control Byte = Co(0) + D/C#(0) + 00000로 구성 (0x00)
  • GDDRAM(그래픽 표현)에 저장될 데이터 전송 : Control Byte는 Co(0)+D/C#(1)+00000 로 구성 (0x40)

[Graphic Display Data RAM (GDDRAM) - 그래픽 표현]

GDDRAM 구성표

  • GDDRAM은 디스플레이에 표현될 그래픽 데이터들을 저장하는데 사용된다
  • RAM의 크기는 128x64 bits이며 128x64 흑백 도트 매트릭스에 사용될 0~7페이지 (세로 8등분)로 나뉘어져있다
  • 가로 SEG0~SEG127, 세로 COM0~COM63 (1 페이지 = 8 줄로 구성)로 구성되어 있다
  • (페이지 및 세그먼트 순서는 리맵핑 명령어를 통해 변경할 수 있음)

GDDRAM 구성표 (2)

  • 각 페이지는 127개의 세그먼트로 구성되어 있고 각 세그먼트는 8 bit (1 Byte) 데이터를 가지고 있다
  • 세그먼트 1bit는 1 픽셀의 디스플레이 이미지를 표현하고 기본적으로 0 bit일 때는 OFF, 1 bit일 때는 ON 된다
  • (RGB가 아닌 단 하나의 색상만을 가지고 있기 때문에 단순히 0 or 1로만 표현된다)
  • 세그먼트의 MSB는 각 페이지의 최하단, LSB는 최상단이다
  • 예) 1100 1001 (0xC9)가 입력될 경우, 하단부터 1,2,5,8번째 픽셀이 켜진다

SEG0에 1100 1001 (0xC9)가 입력된 경우

 

[메모리 어드레싱 모드]

  • Page, Horizontal, Vertical 세 개의 메모리 주소 지정 모드 존재
  • 세 모드 중 어떤 모드를 사용하더라도 세로열이 0~7 페이지로 구분되어진다는 점은 변함이 없다
  • Page / Horizontal, Vertial 모드는 서로 다른 포인터 주소 지정 방법을 갖는다

1. Page Addressing Mode

Page Addressing Mode 에서 어드레스 포인터 이동

  • 디스플레이 RAM을 읽거나 쓴 뒤에는 Column address pointer가 자동으로 1 증가
  • 열 주소 포인터가 열 마지막 주소에 이르면 열 포인터 주소는 시작 주소로 리셋되고 페이지 어드레스 포인터는 변화하지 않는다
  • RAM access pointer 시작 위치를 지정하기 위해선 다음 과정이 필요
    1. B0~B7 명령어를 사용해 타겟 페이지를 설정
    2. 포인터의 하위 시작 열 주소를 00~0F 명령어 사용해 설정
    3. 포인터의 상위 시작 열 주소를 10~1F 명령어 사용해 설정 
      • ex) 83열(0x53)을 시작 주소로 설정한다고 했을 때 상위 4 bit를 0x1F, 하위 4bit를 0x0F의 하위 4bit 에 나누어 저장 후 전송한다고 생각하면 된다
      • 0x53 & 0x0F = 0x03 (하위 4bit)
      • 0x10 | ((0x53>>4)&0x0F) = 0x15 (상위 4bit)
  • ex) 2 페이지, 3열을 시작 주소로 지정했을 때 (0xB2, 0x03, 0x10 명령어를 전송한 뒤 0xFF 1바이트 데이터만 전송했을 때)

 

2. Horizontal Addressing Mode

Horizontal Mode에서 어드레스 포인터 이동

  • 디스플레이 RAM을 읽거나 쓴 뒤에는 Column address pointer가 자동으로 1 증가
  • 만약 열 주소 포인터가 열 마지막 주소에 이르면 열 주소 포인터는 열 시작 주소로 리셋 되고 페이지 주소 포인터는 1 증가됨
  • 열, 페이지 주소 포인터 둘 다 마지막 주소에 이르면 포인터들은 시작 주소로 리셋됨
  • 0x21, 0x22 명령어를 통해 페이지 및 열의 시작, 끝 주소를 지정할 수 있음

3. Vertical Addressing Mode

  • 위의 두 모드와는 다르게 열이 아닌 행이 증가되는 모드 (1행이 아닌 1페이지 단위로 증가)
  • 페이지 주소 포인터가 페이지 끝 주소에 이르면 페이지 주소 포인터는 페이지 시작 주소로 리셋되고 열 주소 포인터는 1증가
  • 열, 페이지 주소 포인터 둘 다 마지막 주소에 이르면 포인터들은 시작 주소로 리셋됨
  • 0x21, 0x22 명령어를 통해 페이지 및 열의 시작, 끝 주소를 지정할 수 있음

(*) Horizontal, Vertical Addressing Mode 에서의 포인터 주소 지정 방법

  • 0x21 명령어 + 시작 열 주소(0~127) + 마지막 열 주소(0~127) 를 전송해 그래픽 데이터를 저장할 열 범위를 지정
  • 0x22 명령어 + 시작 페이지 주소(0~7) + 마지막 페이지 주소(0~7) 를 전송해 그래픽 데이터를 저장할 페이지 범위 지정

Horizontal 모드에서 열 2~125, 페이지 1~6으로 설정된 경우

  • 페이지 및 열의 범위가 제한되어 있다고 하더라도 포인터 주소가 이동하는 방식은 원래의 모드와 동일하다
  • 시작 주소인 2 열에서 포인터가 시작되어 읽기/쓰기 후 자동으로 1열씩 주소가 증가하고 설정된 열의 끝 주소인 125 열에 도달했을 때 자동으로 페이지는 +1, 열은 시작 열로 리셋된다
  • 열 및 페이지의 마지막 주소인 6페이지, 125열에 도달했을 경우엔 시작 주소인 1페이지, 2열로 리셋된다

[CUBE MX 설정]

  • Clock Configuration 설정없이 Pin & Out Configuration > Connectivity 란에서 사용할 I2C 선택한 후 설정
  • I2C Speed Mode의 경우, 데이터 시트에 맞춰 400KHz의 속도를 갖는 Fast Mode를 선택했으나 1000KHz의 속도를 갖는(1us) Fast Mode Plus로 설정해도 정상적으로 작동하는 것을 확인

[초기화 설정]

SSD1306 데이터 시트에 나와있는 초기화 예
Sequential COM 핀 배열

void ssd1306_W_Command(uint8_t cmd)
{
	uint8_t buffer[2]={0};		//Control Byte + Command Byte
	buffer[0]=(0<<7)|(0<<6);	//Co=0 , D/C=0
	buffer[1]=cmd;

	if(HAL_I2C_Master_Transmit_DMA(&ssd1306_I2C_PORT,(uint16_t)(ssd1306_Address)<<1,(uint8_t*)buffer,2)!= HAL_OK)
	{
		Error_Handler();
	}
	while (HAL_I2C_GetState(&ssd1306_I2C_PORT) != HAL_I2C_STATE_READY)
	{
	}
}

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
}
  • 명령어 입력을 위해 전송할 배열의 첫번째 칸은 0x00 (Co=0, D/C=0)으로 고정시키고 두번째 칸엔 전달 받은 명령어를 저장한 뒤 배열 전송
  • 초기화의 경우, 데이터 시트에 나와있는 예를 참고해 코드 작성
  • 초기화 코드를 그대로 사용할 경우, 노란색 영역이 하단에 위치 (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 까지 화면에 순차적으로 표시됨

초기화 이후의 화면 상태

 

[화면에 표시될 데이터 전송 (GDDRAM에 저장될 데이터)]

  • GDDRAM에 저장된 데이터 형태 그대로 디스플레이와 1:1 매칭이 된다고 생각하면 편하다
  • GDDRAM의 형태는 크게 128x64 화면의 세로열(64열)을 8등분하여 각각 8개의 행을 가진 페이지로 구성되어 있으며 (순서는 화면 상단(0)->하단(7)) 각 페이지는 128개의 1바이트 크기 세로열을 가지고 있다
void ssd1306_W_Data(uint8_t* data_buffer, uint16_t buffer_size)
{
	if(HAL_I2C_Mem_Write_DMA(&ssd1306_I2C_PORT,(uint16_t)(ssd1306_Address<<1),0x40,1,data_buffer,buffer_size)!= HAL_OK)
	{
	}
	while(HAL_I2C_GetState(&ssd1306_I2C_PORT) != HAL_I2C_STATE_READY)
	{
	}
}
  • GDDRAM에 저장될 데이터를 전송하기 위해선 전송되는 첫번째 바이트가 0x40 (Co=0,D/C=1)이 되어 이후 전송될 데이터가 명령어가 아닌 GDDRAM에 저장될 데이터임을 나타내야 하고 그 뒤에 데이터 배열이 오는 형태가 되게끔 하면 된다
  • 데이터 전송 전, 후로 화면을 갱신하는데 필요한 명령어는 따로 없고 0x40+데이터 배열이 전송되는데로 포인터 주소가 위치한 곳부터 전송된 데이터 크기만큼 화면이 갱신된다
  • 예) 초기화 이후 화면 전체를 검은색(0x00, 픽셀 OFF)으로 전환
void ssd1306_Clear(void)
{
	//1열은 1바이트로 구성되어 있고 1페이지는 128개의 열이 존재하므로
	//1바이트 크기의 원소 128개를 갖는 배열 선언
	uint8_t buffer[128]={0};	
    
	//0번째 열부터 갱신해야 하므로 0x00, 0x10을 명령어 함수를 통해 전송해
	//포인터 주소를 0열로 지정
	ssd1306_W_Command(0x00);	
	ssd1306_W_Command(0x10);

	for(uint8_t i=0;i<8;i++)
	{
    	//Page Addressing Mode의 경우 포인터 주소가 열의 끝부분에 도달했을 때
        //페이지 주소는 그대로 유지된채 열 주소만 리셋되므로 
        //전체 페이지를 갱신하기 위해선 포인터가 위치한 페이지 주소를 1씩 증가시켜줘야 한다
		ssd1306_W_Command(0xB0+i);
        
        //1바이트 크기의 0x00값을 가진 128 배열을 전송하므로 한 페이지 전체가 0으로 갱신된다
		ssd1306_W_Data(buffer,128);	
	}
}

 

[페이지와 열 포인터 시작 주소 지정]

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_W_Command(0xB0+page);
	ssd1306_W_Command(col_low);
	ssd1306_W_Command(col_high);
}
  • Page Addressing Mode의 경우 Horizontal, Vertical 모드와는 달리 페이지 및 열의 마지막 입력이 필요치 않으므로 시작 지점만을 입력한다
  • 페이지 시작 주소 설정은 0xB0 + 원하는 페이지의 값(0~7)을 더해 전송하면 된다
  • 열 시작 주소는 전달받은 열을 상위 4bit, 하위 4bit로 나누어 하위 4bit는 0x0F, 상위 4bit는 0x1F에 결합하여 전송
  • 이후 데이터를 전송하면 지정된 시작 지점부터 화면 데이터가 갱신된다

[폰트]

 

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

  • 링크의 프로그램을 이용해 폰트 배열을 생성한 뒤 사용
  • 시스템 내에 설치된 폰트를 바탕으로 입력된 문자(특문,대문자,소문자) 및 폰트 크기 설정에 따라 자동으로 배열을 생성해줌
  • 한 페이지당 한줄씩 문자열을 표시하려고 했으나 디스플레이 크기 자체가 너무 작아 보기가 힘들어져 2페이지 당 한줄씩 표현하기로 결정
  • 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, //하단 페이지에 저장

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_W_Data(&char_Buffer[i*font_width],font_width);
	}
}

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

 

[문자 전송 예시]

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)
{
	...
    
    ssd1306_W_String("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz{|}~",0,0);
    /*
    변수 사용 예시
    char print_buff[10];
    int print_num=123456;
    sprintf(print_buff,"%d",print_num);
    ssd1306_W_String(print_buff,0,0);
    */
    while(1)
    {
		
    }
}
  • 전달된 문자열의 주소를 1씩 증가시키며 ssd1306_W_Char 함수를 통해 문자 전송
  • 만약 페이지에 문자를 쓸 공간이 부족할 경우 다음 페이지(문자 한 줄을 표현하는데 두 페이지를 사용하므로 현재 페이지+2)로 이동하고 세로열 시작을 0으로 지정한 후 남은 문자 순서대로 전송
  • 마지막 페이지+문자를 쓸 공간이 없을 경우 전송 중단

사용한 DS3231 모듈

  • 작동 전압 : 2.3~5.5V
  • 통신 : I2C
  • Fast(400kHz) 모드 지원
  • 배터리 용량이 부족할 경우, 시간 흐름이 부정확해짐
  • (사용 모듈의 경우 INT/SQW 핀에 풀업 레지스터 구성되어 있음)

[내부 레지스터 주소 및 데이터]

DS3231이 제공하는 데이터들 (총 19 바이트)

  • 0x00~0x06 : 현재 설정되어 있는 시간
  • 0x07~0x0A : 현재 설정되어 있는 알람1 시간
  • 0x0B~0x0D : 현재 설정되어 있는 알람2 시간
  • 0x0E~0x10 : 제어 레지스터
  • 0x11~0x12 : 온도 (0x11 : 정수, 0x12 : 소수)
  • 0x02 시간(Hours) 레지스터에서 6비트를 HIGH로 설정하면 12시간, LOW라면 24시간 기준으로 시간을 표시
    • 12시간 모드에선 오후 시간일 때(PM) 5비트가 HIGH 상태가 된다
    • 24시간 모드에선 5비트는 20~23시를 표시하는데 사용된다

[Read]

MASTER가 DS3231로부터 데이터를 읽어올 때
레지스터 포인터에 읽어 올 주소를 Write 후 Read 시작
Read 모드 사용시 레지스터 포인터에 대한 주의사항

  • 데이터를 읽어 올 때, SDA 라인의 첫 1바이트는 MSB부터 7 비트는 DS3231(Slave) 주소 + Read(1) 비트로 구성된다
  • Write 모드 사용시에만 레지스터 포인터가 설정되는데 이는 DS3231로부터 데이터를 읽어오는 시작 위치가 된다
  • ex) Write 모드 마지막으로 분이 설정된 경우 레지스터 포인터엔 0x01 주소가 설정되고 DS3231로부터 데이터를 읽어올 때 0x00부터 읽어오는게 아니라 레지스터 포인터에 저장된 0x01부터 설정된 크기만큼의 데이터를 읽어온다
  • 따라서 필요에 따라 레지스터 포인터를 원하는 주소로 재설정한 후 Read 를 시작해야 한다

[Write]

Master로부터 DS3231로의 데이터 전송

  • Write 모드일 때, SDA 라인의 첫번째 바이트는 7 비트 Slave 주소 + Write(0) 비트로 구성
  • 그 이후엔 DS3231내의 데이터를 쓰고자 하는 주소(WORD ADDRESS) ((예)초:0x00, 분:0x01, 시:0x02, ...), 그리고 그 다음엔 해당 주소에 입력할 값이 와야 한다
  • WORD ADDRESS 다음에 바로 이어지는 1 바이트 데이터는 WORD ADDRESS 주소에 입력되고 그 이후에 이어지는 데이터는 순서대로 WORD ADDRESS+1, WORD ADDRESS+2, ..., WORD ADDRESS+X 의 주소에 저장된다
  • ex) 3바이트 크기의 배열 : [0]seconds 주소(0x00), [1]seconds, [2]minutes 의 배열을 전송하는 경우, [0]부터 순서대로 DS3231에 전송되어 0x00 주소에 seconds가 입력되고 그 다음으로 주소가 +1되어 0x01에 minutes가 전송됨
  • Write 모드를 사용할 때 마지막으로 사용된 DS3231 내부 주소(WORD ADDRESS)가 레지스터 포인터에 설정되어 이후 Read 모드 사용시 데이터를 읽어오기 시작하는 시작점으로 사용된다

[CubeMX 설정]

I2C 설정

  • Fast Mode 설정, 7-bit 주소 설정, 인터럽트 활성화

[코드]

- 정의

DS3231 타임 레지스터 구성 및 주소

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/*
DS3231 주소 
실제 통신에선 원 주소인 0x68을 MSB 방향으로 1비트 시프트한 값에 Read or Write 비트가 합해져 1바이트가 된다
*/
#define ds3231_addr 0x68<<1	
#define ds3231_sec_addr 0x00		//DS3231 내 초(sec) 레지스터 주소
#define ds3231_min_addr 0x01	//분(min) 레지스터 주소
#define ds3231_hour_addr 0x02	//시간
#define ds3231_day_addr 0x03	//요일
#define ds3231_date_addr 0x04	//날짜
#define ds3231_month_addr 0x05	//달
#define ds3231_year_addr 0x06	//년도
/*
DS3231 시간 레지스터(0x02)의 6번 비트는 12/24 시간 설정, 
5번 비트는 AM/PM or 24 시간 표시를 사용할 때 2x 시간일 경우 사용됨
*/
#define ds3231_AM 0		//시간 구조체의 시간 부분을 AM으로 설정할 때 사용
#define ds3231_PM 1		//시간 구조체의 시간 부분을 PM으로 설정할 때 사용
#define ds3231_24_hour 2	//DS3231에 쓸 구조체의 시간 부분을 24시간으로 설정할 때 사용
#define ds3231_day 1	//시간 알람 구조체의 날짜/요일 설정 부분을 요일로 설정할 때 사용
#define ds3231_date 0	//시간 알람 구조체의 날짜/요일 설정 부분을 날짜로 설정할 때 사용
#define ds3231_alarm1_addr 0x07	//DS3231 알람1 레지스터 주소
#define ds3231_alarm2_addr 0x0B	//DS3231 알람2 레지스터 주소
#define ds3231_ctrl_addr 0x0E	//DS3231 컨트롤 레지스터 주소
#define ds3231_temp_addr 0x11
/* USER CODE END PD */
  • DS3231 주소 및 DS3231 내의 레지스터 주소들 정의

- 구조체 

/*
시간 레지스터에서 AM/PM or 24시간 표기 설정에 사용
*/
typedef struct
{
	//12시간 표기를 사용할 경우엔 ds3231_AM or define ds3231_PM, 24시간 표기일 경우엔 ds3231_24_hour 입력
	uint8_t am_pm_24;	//AM or PM or 24시간 표기 사용 설정
	uint8_t hour;	//12, 24시간 표기에 상관없이 입력할 시간(Hour) 설정
}am_pm_24;

/*
DS3231로부터 시간을 읽어오거나 시간 데이터를 쓸 때 사용할 시간 구조체
*/
typedef struct
{
	uint8_t	sec;
	uint8_t min;
	am_pm_24 hour_select;
	uint8_t day;
	uint8_t date;
	uint8_t month;
	uint8_t year;
}ds3231_time;

/*
알람에서 요일 or 날짜를 쓸건지 구분 지을 구조체
*/
typedef struct
{
	uint8_t day_or_date;	//ds3231_day or ds3231_day 를 입력해 날짜 or 요일 설정
	uint8_t value;	//날짜 or 요일값 설정
}day_date;

/*
알람1을 읽어오거나 전송할 때 사용할 구조체
*/
typedef struct
{
	uint8_t sec;
	uint8_t min;
	am_pm_24 hour_select;
	day_date day_date_select;
}ds3231_Alarm1;

- 10진수, 2진수 변환 함수

/* USER CODE BEGIN PFP */

int decTobcd(uint8_t dec)
{
	return ((dec/10)*16)+(dec%10);
}
int bcdTodec(uint8_t bcd)
{
	return (bcd/16*10)+(bcd%16);
}
  • DS3231은 2진수를 사용하므로 10진수 사용을 위해선 DS3231로부터 읽어들인 데이터는 10진수로 변환해야 하고 Master 장치로부터 DS3231로 나가는 데이터는 2진화 해야 한다

- 시간 출력 함수, Read 함수 

/*
DS3231로부터 읽어 온 시간 데이터를 출력하는 함수
시간 영역에 저장된 값(시간 구조체->hour_select->am_pm_24)을 판단해
AM, PM, 24시간 표기로 구분한 뒤 출력
*/
void ds3231_print_time(ds3231_time *current_time)
{
	printf("year : %d   month : %d   date : %d   day : %d   ",current_time->year,current_time->month
			,current_time->date,current_time->day);
	switch (current_time->hour_select.am_pm_24)
	{
		//12시간 표기의 경우 0~4번 비트까지 유효한 시간 데이터, 5~6번 비트는 시간 표기 설정 비트
		case ds3231_AM :
			printf("AM : %d   ",(current_time->hour_select.hour)&0x1F);	
			break;
		case ds3231_PM :
			printf("PM : %d   ",(current_time->hour_select.hour)&0x1F);
			break;
		case ds3231_24_hour :
			printf("24 : %d   ",current_time->hour_select.hour);
			break;
		default :
			break;
	}
	printf("min : %d   sec : %d\r\n",current_time->min,current_time->sec);
}

/*
전달받은 시간 구조체 주소에 DS3231로부터 읽어 온 시간 데이터를 저장하는 함수
*/
void ds3231_read_time(ds3231_time *current_time)
{
	uint8_t ds3231_read_time_buff[7];	
	//DS3231로부터 읽어 올 데이터를 저장할 데이터 버퍼 (초,분,시간,요일,날짜,달,년도-총 7개)
    
	/*
	특정 메모리 주소에 액세스해서 원하는 갯수만큼 데이터를 읽어와서 데이터 버퍼에 저장
	*/
	HAL_I2C_Mem_Read_IT(&hi2c1,ds3231_addr,ds3231_sec_addr,I2C_MEMADD_SIZE_8BIT,ds3231_read_time_buff,7);
	//사용할 I2C, DS3231 주소, 접근할 DS3231 레지스터 주소, 메모리 블럭 크기, 읽어 온 데이터를 저장할 버퍼,
	//읽어 올 데이터 갯수
    
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
		//수신 완료 때까지 대기
	}

	/*
	DS3231로부터 읽어 온 데이터를 10진수로 변환한 뒤 전달받은 구조체에 입력
	*/
	current_time->sec=bcdTodec(ds3231_read_time_buff[0]);
	current_time->min=bcdTodec(ds3231_read_time_buff[1]);

	/*
	시간 레지스터(0x02)로부터 읽어 온 데이터의 6번 비트(0x40), 5번 비트(0x20)을 확인해 
	High(1)로 설정되있을 경우 구조체 변수에 AM, PM 설정을 한다 (24시간 표기일 경우 6번 비트 Low(0))
	*/
	if((ds3231_read_time_buff[2]&0x40)!=0)	//BIT6!=0 이므로 12시간 표기
	{
		if((ds3231_read_time_buff[2]&0x20)==0)	//BIT5==0 이므로 AM
		{
			current_time->hour_select.am_pm_24=ds3231_AM;
		}
		else	//BIT5!=0 이므로 PM
		{
			current_time->hour_select.am_pm_24=ds3231_PM;
		}
	}
	else
	{
		//24시간 표시 설정일 경우 (6번 비트 0, 5번 비트 2x 시간일 때만 1)
	}
	current_time->hour_select.hour=bcdTodec(ds3231_read_time_buff[2]);
	current_time->day=bcdTodec(ds3231_read_time_buff[3]);
	current_time->date=bcdTodec(ds3231_read_time_buff[4]);
	current_time->month=bcdTodec(ds3231_read_time_buff[5]);
	current_time->year=bcdTodec(ds3231_read_time_buff[6]);

	ds3231_print_time(current_time);	//터미널에 출력할 함수 호출
}
  • 사용자가 지정한 메모리 주소(레지스터 주소)로부터 데이터를 읽어오는 함수 사용 (HAL_I2C_Mem_Read_IT)
  • 레지스터 주소를 지정해서 데이터를 읽어오기 때문에 DS3231에 설정된 레지스터 포인터 위치를 신경 쓸 필요가 없다
  • 시간 레지스터(0x02) 자체에 AM/PM or 24시간 표기 설정을 저장할 수 있으므로 필요에 따라 6, 5번 비트를 설정해준다 (비트6 HIGH-12시간 표기, LOW-24시간 표기 /  비트5 LOW-AM, HIGH-PM (24시간 표기 사용할 경우 따로 설정할 필요는 없다. 시간이 2x시간 이상일 때 사용됨))

- Read 예시

DS3231로부터 데이터를 읽어올 때 (0x00(초)~0x04(날짜))
Start + DS3231 주소(7) + Read(1) + ACK bit(1) + Seconds(8) + ACK(1) + Minutes(8)
Minutes (8) + ACK bit (1) + Hours (8) + ACK bit (1)
Temp_MSB(8) + Temp_LSB(8) + NACK bit + Stop

  • SCL 라인이 HIGH일 때, SDA 라인이 HIGH에서 LOW로 이동하는 것을 스타트 상태로 규정
  • I2C 통신의 첫 데이터는 Slave 장치 주소가 온다
  • DS3231의 경우 1101000(0x68)의 주소를 갖는데 MSB부터 채워져야 하므로 0xD0 or 0x68<<1을 사용해야 한다
  • 7-bit 주소를 사용한다면 MSB부터 주소가 채워지고 마지막 0 bit 자리에 Read(1) 또는 Write(0) 비트가 채워진다
  • 전송이 중단되기 전까지는 SCL 라인의 9번째 비트에 상응하는 SDA 라인의 9번째 비트(Acknowledge bit)가 LOW를 유지
  • 마지막 Temp(LSB) 데이터 전송 후, SCL 라인의 9번째 신호에서 SDA 라인이 HIGH를 유지하며 Not Acknowledge 상태를 나타내고 Master가 전송을 중단하기 위해 STOP 상태 발생 시킴

해당 데이터값 (날짜/요일/시간/분/초)

- Write 함수

/*
전달받은 구조체 주소를 통해 DS3231에 전송할 시간 데이터 버퍼를 채움
*/
void ds3231_write_time(ds3231_time *ds3231_write_time_struct)
{
	uint8_t write_buf[7];	//전송에 사용할 데이터 버퍼 배열 선언
	
	//입력한 10진수 시간 데이터를 2진수화
	write_buf[0]=decTobcd(ds3231_write_time_struct->sec);
	write_buf[1]=decTobcd(ds3231_write_time_struct->min);
	write_buf[2]=decTobcd(ds3231_write_time_struct->hour_select.hour);
	write_buf[3]=decTobcd(ds3231_write_time_struct->day);
	write_buf[4]=decTobcd(ds3231_write_time_struct->date);
	write_buf[5]=decTobcd(ds3231_write_time_struct->month);
	write_buf[6]=decTobcd(ds3231_write_time_struct->year);

	/*
	24시간 표기가 아닌 12시간 표기(AM/PM)을 사용하는 경우엔 
	DS3231 시간(Hour) 레지스터(0x02)에 씌여질 데이터에 추가로 6, 5번 비트를 설정해줘야 한다
	*/
	switch(ds3231_write_time_struct->hour_select.am_pm_24)
	{
		case ds3231_AM :	//AM인 경우 6번 비트(12/24)만 High로 설정해주면 된다
			write_buf[2]|=0x40;
			break;
		case ds3231_PM :	//PM인 경우 6, 5번 비트를 High로 설정해줘야 한다
			write_buf[2]|=0x60;
			break;
		case ds3231_24_hour :
			break;
		default :
			break;
	}

	/*
	DS3231 초(Sec) 레지스터(0x00)부터 7개의 8비트 데이터 배열을 입력
	각 타임 레지스터는 8비트의 크기를 가지므로 0x00부터 0x06까지 순차적으로 8비트 데이터 7개가 입력된다
	*/
	HAL_I2C_Mem_Write_IT(&hi2c1,ds3231_addr,ds3231_sec_addr,I2C_MEMADD_SIZE_8BIT,
			write_buf,7);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
		//입력 완료까지 대기
	}
}

int main(void)
{
	...
  ds3231_time ds_time_default;	//구조체 변수 선언

  //구조체 변수 데이터 입력
  ds_time_default.sec=0;
  ds_time_default.min=43;
  ds_time_default.hour_select.am_pm_24=ds3231_PM;
  ds_time_default.hour_select.hour=7;
  ds_time_default.day=6;
  ds_time_default.date=21;
  ds_time_default.month=12;
  ds_time_default.year=19;

  ds3231_write_time(&ds_time_default);	//원하는 시간으로 설정한 구조체 변수 DS3231에 전송
}
  • 사용자가 지정한 시간 위치를 사용자가 입력한 데이터로 변경
  • 0x00 번지에 액세스해 8비트 크기의 배열 7개를 입력한다 (레지스터 주소는 8비트 입력 후 자동으로 1씩 증가한다)

- 예시

int main(void)
{
	...
  ds3231_time ds_time_default;	//구조체 변수 선언
  ds3231_read_time(&ds_time_default);	
  //현재 DS3231에 저장되어있는 시간 데이터를 읽어와 선언한 구조체 변수에 저장

  //DS3231에 새롭게 입력할 시간 데이터들을 설정 (위에서 선언한 변수 사용)
  ds_time_default.sec=0;
  ds_time_default.min=43;
  ds_time_default.hour_select.am_pm_24=ds3231_PM;
  ds_time_default.hour_select.hour=7;
  ds_time_default.day=6;
  ds_time_default.date=21;
  ds_time_default.month=12;
  ds_time_default.year=19;

  ds3231_write_time(&ds_time_default);	//구조체를 이용해 DS3231에 시간 데이터 입력
  ds3231_read_time(&ds_time_default);	
  //시간이 변경됐는지 확인하기 위해 타임 레지스터에 저장되어있는 시간 데이터 읽어옴
	...
}

첫줄 : 기존에 저장되어있던 시간 / 두번째 줄 : 변경 직후. 이후부턴 새로 설정된 시간에 기반해 작동

[알람]

알람 레지스터 테이블

  • DS3231엔 두 개의 알람이 존재한다
  • 알람1 - 초, 분, 시간, 요일 or 날짜. 총 4개의 데이터
  • 알람2 - 분, 시간, 요일 or 날짜. 총 3개의 데이터
  • 설정에 따라 타임 키핑 레지스터(DS3231에서 진행 중인 시간)값이 알람 시간과 일치하면 INT/SQW 핀이 High 에서 Low로 전환되면서 인터럽트가 발생한다
  • INT/SQW핀을 사용하려면 INT/SQW핀에 외부 풀업 레지스터를 연결해야 한다

인터럽트 감지에 사용될 GPIO 설정 (PINB4, 인터럽트 모드(Falling edge 감지), 풀업/풀다운 X)

- 알람 1

알람1 레지스터 테이블
알람1 마스크 비트 설정에 따른 알람 매칭

  • 알람1은 DS3231내 0x07 부터 0x0A까지 총 4바이트의 주소를 사용한다
  • 0x09에 저장되는 시간(Hour) 데이터의 6번 및 5번 비트 설정에 따라 12시간/24시간 표기 및 AM, PM이 설정된다
  • 0x0A에 저장되는 데이터의 6번 비트 설정에 따라 날짜를 기준으로 알람을 설정할지 아니면 요일을 기준으로 설정할지가 결정된다
  • 각 알람1 레지스터의 7번 비트 설정에 따라 (테이블 순서대로)
  • 1. 매 초마다 알람
  • 2. 타임 키핑 레지스터에 저장되어있는 시간과 알람1에 저장된 초가 일치할 때 알람
  • 3. 타임 키핑 레지스터에 저장되어있는 시간과 알람1에 저장된 분, 초가 일치할 때 알람
  • 4. 타임 키핑 레지스터에 저장되어있는 시간과 알람1에 저장된 시간, 분, 초가 일치할 때 알람
  • 5. 타임 키핑 레지스터에 저장되어있는 시간과 알람1에 저장된 날짜, 시간, 분, 초가 일치할 때 알람
  • 6. 타임 키핑 레지스터에 저장되어있는 시간과 알람1에 저장된 요일, 시간, 분, 초가 일치할 때 알람

- 알람1 설정 함수

void ds3231_set_alarm1(ds3231_Alarm1 *alarm1_data)
{
	uint8_t alarm1_buff[4];	//DS3231에 전송할 알람1 데이터 버퍼

	alarm1_buff[0]=decTobcd(alarm1_data->sec);	//초
	alarm1_buff[1]=decTobcd(alarm1_data->min);	//분
	alarm1_buff[2]=decTobcd(alarm1_data->hour_select.hour);	//시간
	alarm1_buff[3]=decTobcd(alarm1_data->day_date_select.value);	//날짜 or 요일값

	/*
	12/24 시간 및 AM, PM 설정
	*/
	switch(alarm1_data->hour_select.am_pm_24)
	{
		case ds3231_AM :	//전달된 구조체의 알람 시간이 AM일 경우
			alarm1_buff[2]|=0x40;	//BIT6 Logic High=12시간 표기, BIT5 Logic Low=AM
			break;
		case ds3231_PM :	//전달된 구조체의 알람 시간이 PM일 경우
			alarm1_buff[2]|=0x60;	//BIT6 Logic High=12시간 표기, BIT5 Logic High=PM
			break;
		case ds3231_24_hour :	//전달된 구조체의 알람 시간이 24시간 표기일 경우 (별도 설정 필요 X)
			break;
		default :
			break;
	}

	/*
	알람 기준을 요일로 할지 아니면 날짜로 할지를 설정
	*/
	switch(alarm1_data->day_date_select.day_or_date)
	{
		case ds3231_date :	//날짜 설정, BIT6 - Low
			break;
		case ds3231_day :
			alarm1_buff[3]|=0x40;	//요일 설정, BIT6 - High
			break;
		default :
			break;
	}

	HAL_I2C_Mem_Write_IT(&hi2c1,(uint16_t)ds3231_addr,(uint16_t)ds3231_alarm1_addr,I2C_MEMADD_SIZE_8BIT,
			(uint8_t*)alarm1_buff,4);
	//ds3231_alarm1_addr (0x07)에 액세스해 각 1바이트 크기를 가진 알람1 시간 데이터 4개를 전송
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
		//전송 완료까지 대기
	}
}

- DS3231에 저장된 알람1 데이터를 읽어오는 함수와 알람1 구조체 출력 함수

/*
전달받은 구조체에 저장된 알람1 시간을 출력하는 함수
*/
void ds3231_print_alarm1(ds3231_Alarm1 *current_alarm1)
{
	switch (current_alarm1->day_date_select.day_or_date)	//0x0A - Alarm1 DAY/DATE Register
	{
		case ds3231_date :	//BIT6 Low - Date
			printf("Date : %d   ",(current_alarm1->day_date_select.value)&0x3F);
			break;
		case ds3231_day :	//BIT6 High - Day
			printf("Day : %d   ",(current_alarm1->day_date_select.value)&0x0F);
			break;
		default :
			break;
	}

	switch (current_alarm1->hour_select.am_pm_24)	//0x09 - Alarm1 Hour Register
	{
		case ds3231_AM :	//BIT6 - High, BIT5 - Low
			printf("AM : %d   ",current_alarm1->hour_select.hour);
			break;
		case ds3231_PM :	//BIT6 - High, BIT5 - High
			printf("PM : %d   ",current_alarm1->hour_select.hour);
			break;
		case ds3231_24_hour :	//BIT6 - Low, BIT5 - 2x 시간일 때 High
			printf("24 : %d   ",current_alarm1->hour_select.hour);
			break;
		default :
			break;
	}
	printf("min : %d   sec : %d\r\n",current_alarm1->min,current_alarm1->sec);
}

/*
DS3231로부터 읽어 온 알람1 시간을 전달받은 구조체에 입력하는 함수
*/
void ds3231_read_alarm1(ds3231_Alarm1 *current_alarm1)
{
	uint8_t read_alarm1_buff[4];	//DS3231로부터 읽어 온 알람1 시간을 저장할 데이터 버퍼
    
	HAL_I2C_Mem_Read_IT(&hi2c1,ds3231_addr,ds3231_alarm1_addr,I2C_MEMADD_SIZE_8BIT,read_alarm1_buff,4);
	//ds3231_alarm1_addr (0x07)에 액세스해 0x0A까지 1바이트 크기를 가진 4개의 알람1 데이터를 읽어 옴
    
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
		//수신 완료까지 대기
	}
    
	/*
	수신한 데이터를 10진수로 변환 후 전달받은 구조체에 입력
	*/
	current_alarm1->sec=bcdTodec(read_alarm1_buff[0]);
	current_alarm1->min=bcdTodec(read_alarm1_buff[1]);

	if((read_alarm1_buff[2]&0x40)!=0)	//12시간 표기를 사용할 경우
	{
		if((read_alarm1_buff[2]&0x20)==0)	//알람 설정 시간이 AM일 경우
		{
			current_alarm1->hour_select.am_pm_24=ds3231_AM;
			current_alarm1->hour_select.hour=bcdTodec(read_alarm1_buff[2]&0x1F);
			//12시간 표기를 사용할 경우, 시간 레지스터에서 유효한 시간 데이터는 BIT 0 ~ BIT 4 까지
		}
		else	//알람 설정 시간이 PM일 경우
		{
			current_alarm1->hour_select.am_pm_24=ds3231_PM;
			current_alarm1->hour_select.hour=bcdTodec(read_alarm1_buff[2]&0x1F);
		}
	}
	else	//24시간 표기
	{
		current_alarm1->hour_select.hour=bcdTodec(read_alarm1_buff[2]);	
	}

	if((read_alarm1_buff[3]&0x40)!=0)	//Day일 경우 (BIT 6 - High)
	{
		current_alarm1->day_date_select.day_or_date=ds3231_day;
		current_alarm1->day_date_select.value=bcdTodec(read_alarm1_buff[3]&0x0F);
		//Day 사용할 경우, 유효한 날짜 데이터는 BIT 0 ~ BIT 3까지
	}
	else	//Date일 경우 (BIT 6 - Low)
	{
		current_alarm1->day_date_select.day_or_date=ds3231_date;
		current_alarm1->day_date_select.value=bcdTodec(read_alarm1_buff[3]&0x3F);
		//Date 사용할 경우, 유효한 날짜 데이터는 BIT 0 ~ BIT 5까지
	}

	ds3231_print_alarm1(current_alarm1);	//읽어 온 알람1 데이터를 출력하기 위한 함수 호출
}

- 알람1 활성화 함수

/*
알람1을 활성화 or 비활성화하는 함수
Control(0x0E), Control/Status(0x0F)에 현재 설정되어있는 값을 읽어 온 뒤
이 값에 활성화 또는 비활성화에 필요한 설정값만을 수정&입력하고 이 값을 DS3231에 재전송
*/
void ds3231_enable_alarm1(int enable)
{
	uint8_t ctrl_stat_buff[2];	//읽어 온 Control(0x0E), Contorl/Status(0x0F) 값을 저장할 데이터 버퍼 배열
    
	HAL_I2C_Mem_Read_IT(&hi2c1,ds3231_addr,ds3231_ctrl_addr,I2C_MEMADD_SIZE_8BIT,ctrl_stat_buff,2);
	//컨트롤 레지스터(0x0E)에 액세스해 컨트롤/스테이터스(0x0F) 레지스터까지 2바이트 데이터를 읽어온다
    
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
		//수신 완료까지 대기
	}

	if(enable==0)	//알람1 비활성화
	{
		ctrl_stat_buff[0]&=~(0x01);	//컨트롤 레지스터 비트0 A1IE 비활성화
	}
	else	//알람1 활성화
	{
		ctrl_stat_buff[0]|=(0x05);	//컨트롤 레지스터 비트3 INTCN, 비트0 A1IE 활성화
		ctrl_stat_buff[1]&=~(0x03);	//컨트롤/스테이터스 레지스터 비트1(A2F), 비트0(A1F) Clear
	}
    
	HAL_I2C_Mem_Write_IT(&hi2c1,ds3231_addr,ds3231_ctrl_addr,I2C_MEMADD_SIZE_8BIT,ctrl_stat_buff,2);
	//컨트롤 레지스터(0x0E)에 액세스해 필요에 따라 수정된 컨트롤, 컨트롤/스테이터스 레지스터 값을 전송
    
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
		//전송 완료까지 대기
	}
}

컨트롤 레지스터 (0x0E)
스테이터스 레지스터 (0x0F)

  • 알람1을 사용하기 위해선 컨트롤 레지스터(0x0E)에 A1IE(Alarm 1 Interrupt Enable) 비트와 INTCN (Interrupt Control) 비트를 활성화(Logic 1) 해야 한다
  • 위 설정 상태에서 타임 키핑 레지스터에 저장된 값과 알람1에 저장된 값이 일치할 때, 스테이터스 레지스터(0x0F)의 A1F (Alarm 1 Flag)비트가 Logic 1로 변경되고 High 상태를 유지하던 INT/SQW 핀이 Low로 전환되면서 인터럽트가 발생한다
  • (A1F=1 일 때, SQW-HIGH / A1F=0 일 때, SQW-LOW)
  • 알람을 재활성화 하기 위해선 A1F 비트를 0으로 재설정해야 한다

- 예시

int main(void)
{
	...
  ds3231_time ds_time_default;	//시간 구조체 변수 선언
  
  //DS3231에 입력할 구조체 변수 시간 설정
  ds_time_default.sec=50;
  ds_time_default.min=43;
  ds_time_default.hour_select.am_pm_24=ds3231_PM;
  ds_time_default.hour_select.hour=7;
  ds_time_default.day=6;
  ds_time_default.date=21;
  ds_time_default.month=12;
  ds_time_default.year=19;
  
  ds3231_write_time(&ds_time_default);	//DS3231에 입력한 구조체 변수 값들을 전송
  
  
  ds3231_Alarm1 alarm1_default;	//알람1 구조체 변수 선언
  
  ds3231_read_alarm1(&alarm1_default);	//DS3231에 현재 설정된 알람1 데이터 읽어오는 함수 호출
  
  //DS3231에 입력할 알람1 구조체 변수 시간 설정
  alarm1_default.sec=0;
  alarm1_default.min=44;
  alarm1_default.hour_select.am_pm_24=ds3231_PM;
  alarm1_default.hour_select.hour=7;
  alarm1_default.day_date_select.value=6;
  alarm1_default.day_date_select.day_or_date=ds3231_day;
  
  ds3231_set_alarm1(&alarm1_default);	//DS3231에 입력한 알람1 구조체 변수 값들 전송
  ds3231_read_alarm1(&alarm1_default);	//DS3231에 설정된 알람1 값 읽어옴

  ds3231_enable_alarm1(1);	//알람1 활성화 (0 입력시 비활성화)
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
	  ds3231_read_time(&ds_time_default);
	  HAL_Delay(1000);
  }
}

/* USER CODE BEGIN 4 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	printf("external interrupt occurred\r\n");
	if (GPIO_Pin == Alarm_Interrupt_Line_Pin)
	{
		printf("Alarm interrupt occurred\r\n");
		HAL_GPIO_TogglePin(LD1_GPIO_Port,LD1_Pin);
	}
}

알람1으로 설정한 Day 6, PM 7, 44분, 0초에 인터럽트가 발생
INT/SQW 핀이 HIGH 상태를 유지하다 타임 키핑 레지스터에 저장된 시간과 알람1에 설정된 시간이 일치하면서 LOW로 전환되어 인터럽트 발생

  • A1F를 0으로 재설정해주지 않는다면 INT/SQW 핀은 LOW 상태를 계속 유지한다
  • 따라서 알람을 재활성화 하기 위해선 A1F 비트를 0으로 재설정해줘야 한다

- 알람2

알람2 레지스터 테이블
알람2 마스크 비트 테이블

  • 0x0B~0x0D까지 1바이트씩 총 3바이트의 레지스터 주소를 사용
  • 알람1과의 차이점은 초를 설정할 수 없다는 점이다 (분, 시간, 요일 or 날짜. 세 개만 설정 가능)
  • 그외에 컨트롤 레지스터에서 A2IE 비트, 스테이터스 비트에서 A2F 비트를 설정해줘야 한다는 것과
  • 마스크 비트 설정에 따른 알람 차이점이 존재한다 (마스크 비트 테이블 순서대로)
  • 1. 매 분마다 알람 발생 (매 분 00초)
  • 2. 타임 키핑 레지스터에 저장되어있는 시간과 알람2에 저장된 분이 일치할 때 알람
  • 3. 타임 키핑 레지스터에 저장되어있는 시간과 알람2에 저장된 시간, 분이 일치할 때 알람
  • 4. 타임 키핑 레지스터에 저장되어있는 시간과 알람2에 저장된 날짜, 시간, 분이 일치할 때 알람
  • 5. 타임 키핑 레지스터에 저장되어있는 시간과 알람2에 저장된 요일, 시간, 분이 일치할 때 알람

- 알람2 코드 (알람1에서 초를 설정하는 부분이 빠진 것과 알람2 설정에 따른 레지스터 주소 변경 외엔 차이점이 없다)

void ds3231_print_alarm2(ds3231_Alarm2 *current_alarm2)
{
	switch (current_alarm2->day_date_select.day_or_date)
	{
		case ds3231_date :	//bit6 Low
			printf("Date : %d   ",(current_alarm2->day_date_select.value)&0x3F);
			break;
		case ds3231_day :	//bit6 High
			printf("Day : %d   ",(current_alarm2->day_date_select.value)&0x0F);
			break;
		default :
			break;
	}

	switch (current_alarm2->hour_select.am_pm_24)
	{
		case ds3231_AM :
			printf("AM : %d   ",current_alarm2->hour_select.hour);
			break;
		case ds3231_PM :
			printf("PM : %d   ",current_alarm2->hour_select.hour);
			break;
		case ds3231_24_hour :
			printf("24 : %d   ",current_alarm2->hour_select.hour);
			break;
		default :
			break;
	}
	printf("min : %d\r\n",current_alarm2->min);
}

void ds3231_read_alarm2(ds3231_Alarm2 *current_alarm2)
{
	uint8_t read_alarm2_buff[3];
	HAL_I2C_Mem_Read_IT(&hi2c1,ds3231_addr,ds3231_alarm2_addr,I2C_MEMADD_SIZE_8BIT,read_alarm2_buff,3);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{

	}
	current_alarm2->min=bcdTodec(read_alarm2_buff[0]);

	if((read_alarm2_buff[1]&0x40)!=0)
	{
		if((read_alarm2_buff[1]&0x20)==0)
		{
			current_alarm2->hour_select.am_pm_24=ds3231_AM;
			current_alarm2->hour_select.hour=bcdTodec(read_alarm2_buff[1]&0x1F);
		}
		else
		{
			current_alarm2->hour_select.am_pm_24=ds3231_PM;
			current_alarm2->hour_select.hour=bcdTodec(read_alarm2_buff[1]&0x1F);
		}
	}
	else
	{
		current_alarm2->hour_select.hour=bcdTodec(read_alarm2_buff[2]);	//24시간 표기
	}

	if((read_alarm2_buff[2]&0x40)!=0)	//Day or Date
	{
		current_alarm2->day_date_select.day_or_date=ds3231_day;	//Day=BIT6 High
		current_alarm2->day_date_select.value=bcdTodec(read_alarm2_buff[2]&0x0F);
	}
	else
	{
		current_alarm2->day_date_select.day_or_date=ds3231_date;	//Date=BIT6 Low
		current_alarm2->day_date_select.value=bcdTodec(read_alarm2_buff[2]&0x3F);
	}

	ds3231_print_alarm2(current_alarm2);
}

void ds3231_set_alarm2(ds3231_Alarm2 *alarm2_data)
{
	uint8_t alarm2_buff[4];

	alarm2_buff[0]=decTobcd(alarm2_data->min);
	alarm2_buff[1]=decTobcd(alarm2_data->hour_select.hour);
	alarm2_buff[2]=decTobcd(alarm2_data->day_date_select.value);

	switch(alarm2_data->hour_select.am_pm_24)
	{
		case ds3231_AM :
			alarm2_buff[1]|=0x40;	//BIT5 AM/PM, Logic High=PM
			break;
		case ds3231_PM :
			alarm2_buff[1]|=0x60;	//BIT6 12/24, Logic High=12
			break;
		case ds3231_24_hour :
			break;
		default :
			break;
	}

	switch(alarm2_data->day_date_select.day_or_date)
	{
		case ds3231_date :
			break;
		case ds3231_day :
			alarm2_buff[2]|=0x40;	//day-BIT6 High
			break;
		default :
			break;
	}

	HAL_I2C_Mem_Write_IT(&hi2c1,(uint16_t)ds3231_addr,(uint16_t)ds3231_alarm2_addr,I2C_MEMADD_SIZE_8BIT,
			(uint8_t*)alarm2_buff,3);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{

	}
}

void ds3231_enable_alarm2(int enable)
{
	uint8_t ctrl_stat_buff[2];
	HAL_I2C_Mem_Read_IT(&hi2c1,ds3231_addr,ds3231_ctrl_addr,I2C_MEMADD_SIZE_8BIT,ctrl_stat_buff,2);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{

	}

	if(enable==0)
	{
		ctrl_stat_buff[0]&=~(0x02);	//A2IE disable
	}
	else
	{
		ctrl_stat_buff[0]|=(0x06);	//INTCN, A2IE enable
		ctrl_stat_buff[1]&=~(0x03);	//A1F, A2F Clear
	}
	HAL_I2C_Mem_Write_IT(&hi2c1,ds3231_addr,ds3231_ctrl_addr,I2C_MEMADD_SIZE_8BIT,ctrl_stat_buff,2);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{

	}
}

- 알람2 예시

int main(void)
{
	...
  ds3231_time ds_time_default;
  ds_time_default.sec=50;
  ds_time_default.min=29;
  ds_time_default.hour_select.am_pm_24=ds3231_AM;
  ds_time_default.hour_select.hour=10;
  ds_time_default.day=6;
  ds_time_default.date=21;
  ds_time_default.month=12;
  ds_time_default.year=19;
  ds3231_write_time(&ds_time_default);

  /*
  알람2 구조체 변수 선언 및 설정 후 DS3231에 전송
  */
  ds3231_Alarm2 alarm2_default;
  alarm2_default.min=30;
  alarm2_default.hour_select.am_pm_24=ds3231_AM;
  alarm2_default.hour_select.hour=10;
  alarm2_default.day_date_select.day_or_date=ds3231_date;
  alarm2_default.day_date_select.value=21;
  ds3231_set_alarm2(&alarm2_default);
  ds3231_read_alarm2(&alarm2_default);
  ds3231_enable_alarm2(1);
  
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
	  ds3231_read_time(&ds_time_default);
	  HAL_Delay(1000);
  }
}

/* USER CODE BEGIN 4 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	printf("external interrupt occurred\r\n");
	if (GPIO_Pin == Alarm_Interrupt_Line_Pin)
	{
		printf("Alarm interrupt occurred\r\n");
		HAL_GPIO_TogglePin(LD1_GPIO_Port,LD1_Pin);
	}
}

설정된 알람2 값이 타임 키핑 레지스터에 저장된 값과 일치하면서 인터럽트 발생

  • 알람2도 알람1과 같이 인터럽트 발생 후 스테이터스 레지스터의 A2F 비트를 0으로 재설정해줘야 알람이 재활성화된다

- 마스크 비트 단순 예시 (매 초마다 알람 인터럽트가 발생하도록 설정했을 때)

int alarm_every_sec=0;	//알람 인터럽트 발생 후 알람 재활성화 하기 위해 사용할 전역 변수

/*
매 초마다 알람을 발생 시키려면 알람1의 모든 시간 레지스터들의 7번 비트를 1로 설정해줘야 한다
*/
void ds3231_alarm_every_sec()
{
	uint8_t alarm_buff[4]={0x80,0x80,0x80,0x80};	//매 초마다 알람 발생 시키기 위한 마스크 비트 설정
	HAL_I2C_Mem_Write_IT(&hi2c1,ds3231_addr,ds3231_alarm1_addr,I2C_MEMADD_SIZE_8BIT,alarm_buff,4);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{

	}
}

int main(void)
{
	...
    ds3231_enable_alarm1(1);	//매 초마다 알람을 발생 시킬 수 있는 건 알람1 뿐이다 (알람2는 매 분마다)
    ds3231_alarm_every_sec();
    
    while(1)
    {
      if((HAL_I2C_GetState(&hi2c1)==HAL_I2C_STATE_READY)&&(alarm_every_sec==0))
	  {
		  ds3231_read_time(&ds_time_default);
          /*
          알람 발생 이후 A1F=1, INT/SQW핀=LOW 상태를 유지해 INT/SQW핀을 통한 새 인터럽트가 발생하지 
          못하므로 A1F를 0으로 재설정해 INT/SQW핀을 다시 HIGH 상태로 만들어 준다
          */
		  ds3231_enable_alarm1(1); 
		  alarm_every_sec=1;
	  }
    }
}

/* USER CODE BEGIN 4 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	printf("external interrupt occurred\r\n");
	if (GPIO_Pin == Alarm_Interrupt_Line_Pin)
	{
		printf("Alarm interrupt occurred\r\n");
		alarm_every_sec=0;
	}
}

매 초마다 알람 인터럽트가 발생하도록 설정한 결과
알람 인터럽트 발생 후, 스테이터스 레지스터의 A1F 비트 값을 0으로 재설정해 INT/SQW 핀을 다시 HIGH로 만들어 다음 인터럽트가 발생할 수 있게끔 만들어준다

- 온도

온도 레지스터

  • DS3231의 온도 레지스터엔 64초를 주기로 갱신되는 0.25℃ 해상도를 가진 온도값이 저장된다
  • 정수 부분(레지스터 0x11), 소수 부분(0x12) 두 부분으로 나뉘어 온도값이 저장된다
  • 소수 부분은 상위 2비트(비트6, 비트7)만 사용된다
float ds3231_temperature;	//읽어 온 온도값을 저장할 float 전역 변수

void ds3231_read_temperature()
{
	uint8_t ds3231_temp_buff[2];	//상위 8비트, 하위 8비트를 읽어와야 하므로 2개의 배열을 가진 변수 생성
	
	HAL_I2C_Mem_Read_IT(&hi2c1,ds3231_addr,ds3231_temp_addr,I2C_MEMADD_SIZE_8BIT,
			ds3231_temp_buff,2);
	//ds3231_temp_addr(0x11)에 액세스해 정수(0x11), 소수(0x12) 레지스터를 읽어온다
    
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{

	}
	ds3231_temperature=ds3231_temp_buff[0]+((ds3231_temp_buff[1]>>6)*0.25);
	//온도값을 저장하기 위해 선언한 float 전역 변수에 정수 부분과 소수 부분을 합산해 저장한다
	//소수 부분은 상위 2비트에 소수 온도 데이터가 저장되므로 이를 LSB 방향으로 6비트 비트 시프트 한 뒤 계산
	//00=0.00, 01=0.25, 10=0.5, 11=0.75
    
	printf("%.2f\r\n",ds3231_temperature);
}

SK6812 RGBW 모델

  • RED, GREEN, BLUE + WHITE 네 부분으로 구성된 RGBW 모듈
  • 작동 전압 5V
  • RGBW 각각의 값은 0~255 사이의 8비트 데이터로 구성됨
  • VDD, DIN, DOUT, GND 네 부분으로 구성되며 DIN에 신호를 보내 컨트롤
  • DIN에만 제어 신호를 전달하면 되므로 SPI통신을 이용할 경우 MOSI를 DIN에 연결하여 사용

RGBW 각각의 비트에 0, 1을 구현하기 위한 방법
SK6812 RGBW LED가 필요로 하는 데이터 구조 및 인식하는 순서 (위의 표와 다르게 실제로는 GRBW 순으로 인식함)

  • RGBW 각각은 8비트 데이터로 구성되고 각각의 비트는 T0H, T0L, T1H, T1L 조합으로 0과 1을 표현함
  • 0의 경우 : 0.3us(HIGH)+0.9us(LOW) 로 구성 (T0H + T0L + T0L +T0L)
  • 1의 경우 : 0.6us(HIGH)+0.6us(LOW) 로 구성 (T1H + T1H + T1L + T1L)
  • 총 길이 : 1.2us

구동 조건

  • 여러 개의 모듈을 구동하기 위해선 32bit * 구동할 LED의 갯수만큼의 데이터를 한번에 전송해야 함
  • LED 데이터 갱신이 필요한 경우, 이전 전송 이후 80us 이상의 리셋(LOW) 상태를 유지한 뒤에 데이터 재전송해야 함
  • 데이터 라인에 첫번째로 연결된 LED가 첫 32비트(first)를 받고 나머지 값은 그 다음 LED로 넘김

구동 방식

  • DIN으로 들어간 신호가 32비트 데이터(RGBW값)를 해당 순서의 LED에 전달된 뒤 나머지 값들은 DOUT으로 나가면서 계속 이어져가는 방식

[CUBE MX 설정]

데이터 사이즈 : 8 Mbits, 보드 레이트 3.0Mbits/s, NSSP Mode : 미사용

  • 3.0Mbit/s 의 경우, 하나의 신호당 요구되는 0.3us +-0.15 오차 안에 포함되는 0.333us의 길이를 갖음
  • 이 경우 LED 데이터의 0 or 1의 비트를 표현하기 위해 4개의 비트를 필요로 함 (0.333us*4)
  • 예)RED=1의 경우, 0000 0001 -> 0=0.333us(HIGH*1)+0.999us(LOW*3) -> 1000 *7(7~1비트)
  •                                       -> 1=0.666us(HIGH*2)+0.666us(LOW*2) -> 1100 *1(0비트)
  • 총 32(R8+G8+B8+W8)*4(0과 1 구현 위한 LOW와 HIGH 조합)*구동하고자 하는 LED의 갯수만큼의 비트가 필요
  • 예) LED 하나 사용하는 경우, 32*4*1=128비트로 총 16바이트이며 각 색상별로 4바이트를 사용
  • (4.0Mbits/s, 8.0Mbits/s, 16.0Mbits/s을 사용했을 때, 출력 형태가 오차 범위내에 포함 or 걸치는 형태였으나 정상  작동 X (ex: 0=0.25us+0.75us, 1=0.5us+0.5us))

  • STM32WB에서 SPI1의 경우 APB2에 속해있으므로 APB2의 클럭을 조절해줘야 함
  • 0.333us의 신호 길이를 갖기 위해 peripheral 클럭을 24MHz로 설정한 뒤, prescaler 값을 8로 설정 (1s/3.0M=0.333us)

  • 그 외, SPI1 Interrupt 체크 및 DMA 설정

[코드]

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

#define led_num 4
//led 하나당 16바이트 사용 (4bit*8(=하나의 색)*RGBW=16바이트),+4는 리셋을 위함
#define led_spi_buffer_bytes led_num*4*4+4

/* USER CODE END PD */
  • 사용할 LED의 갯수와 그에 따른 버퍼의 크기를 define
  • MOSI의 신호가 데이터 전송 후 LOW로 떨어지지 않고 HIGH를 유지하여 LED 작동에 문제를 가져다주는 문제를 해결하기 위해 4바이트의 LOW신호를 LED 데이터 마지막에 이어서 전송

추가 LOW 신호가 없을 때, MOSI는 데이터 전송 이후 HIGH를 유지 (LED 3개 사용으로 총 48바이트의 길이)
추가로 2바이트 길이의 LOW 신호를 줬을 때, 2바이트 길이의 LOW신호 이후 MOSI 신호는 다시 HIGH로 바뀜
추가로 4바이트 길이의 LOW 신호를 줬을 때, 4바이트 길이의 LOW 신호 이후에도 LOW 상태를 유지 (정상 작동)

  • 데이터 시트의 80us 이상의 Reset(LOW) 신호를 준수하기 위해서는 4바이트가 아닌 30바이트 정도의 추가 LOW 신호를 사용 (80us/0.333us=240bits=30bytes)
  • Reset에 필요한 30바이트가 아닌 4바이트만을 사용한 이유는 단순히 개인적으로 80us 이하로 잦은 LED 색상 변경을 사용하는 경우가 없어 MOSI 신호를 LOW로 유지 시켜주는 최소한의 추가 바이트만 사용해도 괜찮겠다 생각했기 때문
/* USER CODE BEGIN PV */

struct sk_RGBW
{
	uint8_t red;
	uint8_t green;
	uint8_t blue;
	uint8_t white;
};
struct sk_RGBW sk6812_led[led_num]={0};		
//사용할 LED 갯수만큼 구조체 변수 생성
//ex) sk6812_led[0]의 경우, 첫번째 순서의 LED가 가질 색상들을 의미
uint8_t led_buffer[led_spi_buffer_bytes]={0};	//SPI를 이용해 LED에 전달될 데이터 배열

/* USER CODE END PV */
  • LED 값을 저장할 구조체와 배열을 생성
/* USER CODE BEGIN PFP */
void set_color(struct sk_RGBW *led_arr, uint8_t led_index)
{
	uint8_t i;
	uint32_t led_temp_buff[4]={0};
    //하나의 배열당 하나의 색을 담당 ex)led_temp_buff[0]=Green, led_temp_buff[1]=Red
    //LED 데이터 하나의 비트를 표현하기 위해 4개의 H, L 비트가 필요하고 총 8자리의 LED 데이터가 필요하므로
    //unsigned int 32bits 배열 사용 (4*8=32)
    
    //LED 비트 0을 표현하기 위해선 1000=0x8, 1을 표현하기 위해선 1100=0xc
    //예) Red=128일 때(1000 0000), led_temp_buff[1]=1100 1000 1000 ... 1000
	
    /*
    MSB부터 출력되므로 led_temp_buff의 MSB부터 전달받은 색 데이터의 0, 1을 구분한 뒤 채워나감
    전달받은 포인터 구조체 주소를 참조해 LED 배열의 MSB부터 데이터를 채워나감
    예) led_arr->green=1100 1000일 때 (Green=200), 
    첫번째(i=0) 루프에서 0만큼 MSB 방향으로 전체 비트를 시프트하고
    1000 0000을 곱한 뒤, 나온 값이 0이냐 1이냐에 따라 0x8(0일 때) or 0xC(1일 때)를
    (7-0)*4만큼 MSB 방향으로 시프트 시킨 뒤 32비트 배열에 저장
    i=0일 때, led_temp_buff[0]=(31,MSB) 1100 0000 0000 .... 0000 (0,LSB)
    i=1일 때, led_temp_buff[0]=(31,MSB) 1100 1100 0000 .... 0000 (0,LSB)
    i=7일 때, led_temp_buff[0]=(31,MSB) 1100 1100 1000 1000 1100 1000 1000 1000 (0,LSB)
    
    SK6812는 데이터를 RGBW순이 아닌 GRBW순으로 인식함
    */
	for(i=0;i<8;i++)
	{
		if((led_arr->green<<i)&0x80)	//Green
			led_temp_buff[0]+=(0xc<<(7-i)*4);
		else
			led_temp_buff[0]+=(0x8<<(7-i)*4);
		if((led_arr->red<<i)&0x80)	//Red
			led_temp_buff[1]+=(0xc<<(7-i)*4);
		else
			led_temp_buff[1]+=(0x8<<(7-i)*4);
		if((led_arr->blue<<i)&0x80)	//Blue
			led_temp_buff[2]+=(0xc<<(7-i)*4);
		else
			led_temp_buff[2]+=(0x8<<(7-i)*4);
		if((led_arr->white<<i)&0x80)	//WHITE
			led_temp_buff[3]+=(0xc<<(7-i)*4);
		else
			led_temp_buff[3]+=(0x8<<(7-i)*4);
	}


	/*
    led_index는 실제 LED의 순서를 의미
    ex) led_index=0 : MOSI에 연결된 첫번째 LED
    
    led_index*16에서 *16은 하나의 LED당 16바이트의 색상 데이터를 사용하기 때문
    ex) led_index=2일 경우(실제로는 세번째에 위치한 LED),
    전역변수로 선언된 led_buffer[] 배열 값 중에서 led_index[32]~led_index[47]까지의 배열만 값이 바뀌고
    나머지 배열들의 값은 유지된 채로 출력됨
    
	led_buffer 배열에 LED 데이터를 입력한 후, DMA를 이용해 LED에 출력
    각 색깔별로 4(LOW+HIGH)*8(LED 데이터(=LED 강도))=32bits=4bytes의 데이터 크기를 갖음
    ex) GREEN=200=1100 1000=1100 1100 1000 1000 ... 1000 일 때,
    led_buffer[0]=led_temp_buff[0] 31~24 비트값 1100 1100
    led_buffer[1]=led_temp_buff[0] 24~16 비트값 1000 1000
    led_buffer[2]=led_temp_buff[0] 15~8 비트값 1100 1000
    led_buffer[3]=led_temp_buff[0] 7~0 비트값 1000 1000
    (MSB부터 출력되므로 MSB를 [0]번 배열에 저장)
    
    led_buffer[4]~[7]=RED, led_buffer[8]~[11]=BLUE, led_buffer[12]~[15]=WHITE
    */
    
	for(i=0;i<4;i++)
	{
		led_buffer[(i+led_index*16)]=(led_temp_buff[0]>>(3-i)*8);	//GREEN
		led_buffer[(i+led_index*16+4)]=(led_temp_buff[1]>>(3-i)*8);	//RED
		led_buffer[(i+led_index*16+8)]=(led_temp_buff[2]>>(3-i)*8);	//BLUE
		led_buffer[(i+led_index*16+12)]=(led_temp_buff[3]>>(3-i)*8);	//WHITE
	}
	HAL_SPI_Transmit_DMA(&hspi1,led_buffer,led_spi_buffer_bytes);
    //DMA를 이용해 데이터 보냄, SPI1 사용, led_buffer 보낼 데이터, led_spi_buffer_bytes 전송할 byte의 크기
}
/* USER CODE END PFP */
  • SPI 통신을 사용하고 DMA를 이용해 RGBW 데이터를 출력하는 함수
void set_rgb_value(struct sk_RGBW *current_rgb,uint8_t red,uint8_t green,uint8_t blue,uint8_t white)
{
	current_rgb->red=red;
	current_rgb->green=green;
	current_rgb->blue=blue;
	current_rgb->white=white;
}
  • 구조체 배열 변수 주소를 전달받아 해당 배열의 RGBW색상을 설정하는 함수
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    
	  set_rgb_value(&sk6812_led[0],0,0,0,0);	//구조체 배열 변수(sk6812_led[0])에 RGBW값 설정
	  for(i=0;i<led_num;i++)
	  {
		  set_color(&sk6812_led[0],i);	//설정된 LED 갯수만큼 sk6812_led[0]에 저장된 RGBW값을 출력
	  }
	  HAL_Delay(1000);

	  set_rgb_value(&sk6812_led[0],255,0,0,0);	//RED=255, 나머지는 0으로 설정
	  for(i=0;i<led_num;i++)
	  {
		  set_color(&sk6812_led[0],i);
          //ex) LED 갯수 3, sk6812_led[0]=RED 255를 제외한 나머지 GBW값이 0일 때, 
          //	세 개의 LED가 동일한 RED=255만큼의 빛 세기를 가진채로 켜짐
	  }
	  HAL_Delay(1000);
      
	  set_rgb_value(&sk6812_led[1],0,255,0,0);	//sk6812_led[1]에 GREEN=255, 나머지=0 입력
	  set_color(&sk6812_led[1],1);	
      /*
      sk6812_led[1]에 저장된 RGBW값을 2번째 LED에 전달 
      led_index값이 0부터 시작되므로 led_index=1은 두번째 LED를 의미
      sk6812_led[1]의 값을 두번째 LED에 할당한 이유는 단순히 
      sk6812_led[0]은 첫번째 LED, sk6812_led[1]은 두번째 LED의 값을 갖는다는 순서를 맞춰준것일뿐
      중요한 부분은 set_color(&sk6812_led[1],1); 에서 LED 순서를 의미하는 led_index=1
      */
	  HAL_Delay(1000);
   }
  • 사용할 LED의 갯수를 네 개라 가정 (led_num=4)
  • 처음엔 네 개의 LED가 꺼진 상태로 시작 (RGBW=0,0,0,0)
  • 1초의 딜레이 후, 출력에 이용될 LED 배열에 RGBW 값을 입력 (RED=255, GBW=0)
  • FOR문이 네 차례 반복되며 SK6812 RGBW LED에 led_buffer[0]~led_buffer[68]까지의 버퍼를 출력
  • i=0일 때, led_buffer 중 led_buffer[0]~led_buffer[15]만 값을 가진채로 출력되므로 첫번째 LED만 켜짐 (나머지 배열은 초기화 값인 0 유지)
  • i=1일 때, led_buffer 중 led_buffer[16]~led_buffer[31]의 값이 입력되어 led_buffer[0]~led_buffer[31]까지 유효한 LED 데이터 값을 가지므로 두 번째 LED까지 켜짐
  • i=2,3 도 위의 상황이 반복되므로 결국 네 개의 LED가 켜짐
  • 출력에 사용될 RGBW 배열을 sk6812_led[0]으로 고정시켰기 때문에 네 개의 LED가 동일하게 RED=255 값의 밝기를 가진다
  • 1초의 딜레이 후, sk6812_led[1]의 값을 GREEN=255, RBW=0으로 변경
  • sk6812_led[1]의 값을 두 번째 LED(led_index=1)에 출력하기 위해 출력 함수인 set_color 사용
  • led_buffer 배열 중 두 번째 LED값에 해당하는 led_buffer[16]~led_buffer[31]의 값만이 sk6812_led[1]의 값으로 치환되고 HAL_SPI_Transmit_DMA 함수를 통해 led_buffer[0]~led_buffer[64] 네 개의 LED 전체 RGBW값을 출력함
  • 첫번째와 세번째, 네번째 LED의 값은 RED=255의 값을 유지하고 두 번째 LED만이 GREEN=255, RBW=0의 값으로 변한다

+ Recent posts