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으로 지정한 후 남은 문자 순서대로 전송
  • 마지막 페이지+문자를 쓸 공간이 없을 경우 전송 중단

+ Recent posts