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번으로

 

 

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