본문 바로가기
AVR

[AVR] LCD1602A를 ATmega128에서 쉽게 사용하기 with lcd1602a_h68 라이브러리 (+ 커스텀 문자 출력, Display Shift)

by hseoy 2020. 6. 27.
반응형

+ 2020.06.27 첫 작성
+ 2020.06.28 기준으로 lcd1602a_h68에 커스텀 문자를 생성하는 함수가 추가되어 이 부분을 다루는 내용을 추가함
+ 2020.06.30 기준으로 lcd1602a_h68에 LCD display를 shift 시키는 함수가 추가되어 이 부분을 다루는 내용을 추가함(이 부분에 대해서 오류랄까 문제를 발견하여 현재 수정 중에 있음)


[LCD1602A를 ATmega128에서 쉽게 사용하기 with lcd1602a_h68]

이번에는 ATmega128에서 LCD를 다뤄봤다. 블로그 글만 보면, LED만 실습하다가 갑자기 LCD로 넘어간 것처럼 보이는 데 블로그 실습은 개인적으로 실습한 내용을 다룬 것뿐이라 학교 수업 진도는 계속 나갔다. 어찌됐든 그리하여 LCD를 학교에서 ATmega128로 다루기 시작했는데 생각보다 별 거 없었다는 게 내 생각이다.

그 이유는 학교에서 자료로 배포한 LCD 라이브러리 코드를 복붙해서 그것들을 사용하기만 하면 되는 그런 상황이였기 때문이다. 구동 원리 자체는 알고 있다고 하지만, 그대로 복붙해서 사용하는 것은 구동 원리는 알지만 코드의 원리는 이해 못하고 사용하는 거 같았다. 이런 경우 나중에 내 생각을 코드로 옮기지 못하는 상황이 생길 수 있다고 생각해서 처음부터 다시 나만의 라이브러리를 작성하기로 했다. 그리고 이것이 lcd1602a_h68이다. 

구현해야 할 기능들은 다른 사람들이 만들어둔 LCD 라이브러리(https://liminia.tistory.com ,기타 등등)에서 구현한 함수들을 참고했다. 코드의 경우 학교에서 제공해준 LCD 라이브러리의 내부 소스, 데이터 시트, 이외 다른 사람들이 작성한 코드 등을 읽고 이해해 가면서 나만의 코드로 바꿔나가면서 작성했다.

내가 작성한 lcd1602a_h68의 헤더파일 내용은 아래와 같다.

/*
 * lcd1602a_h68.h
 *
 * Created: 2020-06-25 오후 6:54:22
 *  Author: Yunseo Hwang
 */ 


#ifndef LCD1602A_H68_H_
#define LCD1602A_H68_H_

#define LCD_PIN_RS  0
#define LCD_PIN_RW  1
#define LCD_PIN_E   2

#define LCD_PIN_DB0 0
#define LCD_PIN_DB1 1
#define LCD_PIN_DB2 2
#define LCD_PIN_DB3 3

#define LCD_PIN_DB4 4
#define LCD_PIN_DB5 5
#define LCD_PIN_DB6 6
#define LCD_PIN_DB7 7

#define LCD_ROWS_MAX 16
#define LCD_COLS_MAX 2

#define LCD_SHIFT_RIGHT 1
#define LCD_SHIFT_LEFT  0

struct lcd1602a_port {
	volatile uint8_t *rsrwe_ddr;
	volatile uint8_t *data_ddr;
	volatile uint8_t *rsrwe_port;
	volatile uint8_t *data_port;
};

void set_lcd_port(struct lcd1602a_port port);
void set_lcd_bit(uint8_t bit);
uint8_t lcd_init(uint8_t rows, uint8_t cols);

uint8_t lcd_move(uint8_t x, uint8_t y);
uint8_t lcd_putc(const char c);
uint8_t lcd_puts(const char* str);
uint8_t lcd_clear();
uint8_t lcd_create_char(uint8_t location, uint8_t pattern[8]);
uint8_t lcd_display_shift_str(uint8_t direction, uint8_t str_size);
uint8_t lcd_display_shift(uint8_t direction);
#endif /* LCD1602A_H_ */

LCD_PIN_~으로 define 되있는 부분은 핀의 번호를 의미한다. LCD의 RS,RW,E 핀은 아무 포트나 핀 번호 0, 1, 2에 연결을 해주면 되고, 데이터핀인 DB0~7은 포트의 핀 번호 0~7에 각각 연결해주면 된다. 기본적으로 RS, RW, E핀과 데이터핀을 연결하는 포트는 서로 다르게 지정해야 하지만 4비트의 경우, 한 포트의 0, 1, 2 핀에는 RS, RW, E핀을, 4, 5, 6, 7 핀에는 데이터 핀을 연결하여 한 포트에서 LCD를 제어하는 것도 가능하다. 

lcd1602a_port는 이러한 RS, RW, E핀과 데이터핀을 연결할 포트 정보를 저장하기 위한 구조체이다. 여기서 rsrwe_ddr, rsrwe_port에는 RS, RW, E 핀을 연결할 DDRx 레지스터와 PORTx 레지스터의 주소값을 &연산자를 통해서 할당해주면 되고, data_ddr, data_port에는 데이터 핀들을 연결할 DDRx레지스터와 PORTx 레지스터의 주소값을 할당해주면 된다. lcd1602a_port의 초기화 예시는 아래에 있다.

// LCD를 8비트 모드에서 사용할 경우, RS, RW, E를 사용할 포트와 데이터 핀과 연결할 포트를 
// 다르게 지정해줘야 함. 왜냐하면 데이터 핀의 개수가 8개로 한 개 포트를 모두 사용하기 때문이다.
struct lcd1602a_port = {&DDRA, &DDRB, &PORTA, &PORTB};

// LCD를 4비트 모드에서 사용할 경우, 한 개의 포트만을 지정하는 것이 가능하다.
struct lcd1602a_port = {&DDRA, &DDRA, &PORTA, &PORTA};

이렇게 초기화해준 lcd1602a_port 구조체를  set_lcd_port 함수의 인자로 넘겨주면 LCD에 대한 포트 설정은 끝나게 된다.  그리고 set_lcd_bit 함수로 LCD를 몇 비트(4비트 or 8비트)로 사용할 것인지를 결정해준다.

set_lcd_bit(8); // or set_lcd_bit(4);
set_lcd_port(port);

set으로 시작하는 이 두 함수는 lcd1602a_h68의 다른 어떤 함수들보다도 먼저 실행되어야만 한다. lcd1602a_h68의 함수들은 이 두 함수로 설정한 값들을 기반으로 동작하고 있다. 여기서 이 두 함수 중 누가 먼저 호출되었는 지는 상관없다. 

그 다음으로 해야 할 작업은 lcd_init 함수를 호출하는 일이다. 이 함수는 위에서 설정한 값을 기반으로 LCD1602A 모듈을 초기화한다. 여기서 인자로 넘겨주는 rows와 cols는 LCD 상에서 커서가 이동할 수 있는 위치를 제한한다. 예를 들어 rows의 값이 2이고, cols의 값이 2라고 할 때, 첫 번째 줄에서 2개이상의 문자를 출력하게 되면 다음 줄로 넘어가게 된다.

LCD의 모든 칸을 사용하고자 한다면 미리 정의된 LCD_ROWS_MAX, LCD_COLS_MAX를 인자로 넘겨주거나 직접적으로 16과 2를 넘겨주면 된다.(LCD1602A의 경우 칸 수가 16x2이다). 

lcd_init(LCD_ROWS_MAX, LCD_COLS_MAX); // == lcd_init(16, 2);

여기까지가 LCD1602A 모듈을 lcd1602a_h68을 사용하기 위한 설정 부분이다. 이제는 lcd1602a_h68에서 제공하는 함수들을 조합해서 LCD에 원하는 글자들을 출력하면 된다. 각 함수에 대한 설명은 아래와 같다. 

lcd_move 함수 커서의 위치를 x와 y의 위치로 옮긴다.
lcd_putc 함수 LCD에 문자 하나를 출력하고 커서를 다음 위치로 옮긴다.
lcd_puts 함수 LCD에 문자열을 출력한다.
lcd_clear 함수 LCD의 모든 출력된 문자들을 지우고 커서를 처음 위치로 옮긴다. 
lcd_create_char 함수 커스텀 문자를 생성한다.
lcd_display_shift_str 함수 LCD의 display를 direction 방향에 따라 shift 시킨다.

여기서 lcd_move 함수의 인자인 x,y의 범위는 0 이상 lcd_init에서 설정한 값 미만이다. lcd_init(LCD_ROWS_MAX, LCD_COLS_MAX)로 LCD를 초기화했을 경우, x값은 0부터 15, y값은 0부터 1의 값을 지닐 수 있다. 

그러면 여기까지 lcd1602a_h68에 대한 설명을 마치고 이것을 활용한 예제 코드에 대해서 살펴보도록 하겠다. 먼저 git을 이용하여 lcd1602a_h68 라이브러리 repository를 clone한다.

git clone https://github.com/ga-mang/lcd1602a_h68.git

그런 다음 clone한 lcd1602a_h68에서 lib 폴더 안에 있는 파일들을 atmel studio 프로젝트에서 불러와 사용하면 된다.

example 폴더 안에는 main.c 파일이 있을 건데 이것의 내용은 아래와 같다.

// example/main.c
#define F_CPU 16000000UL
#include <avr/io.h>
#include <util/delay.h>
#include "lcd1602a_h68.h"

int main(void)
{
	struct lcd1602a_port port = {&DDRC, &DDRC, &PORTC, &PORTC};
    
	set_lcd_bit(4);
	set_lcd_port(port);
	lcd_init(LCD_ROWS_MAX - 6, LCD_COLS_MAX);
	
	while (1) 
	{
		lcd_puts("12345678901234567890");
		_delay_ms(500);
		lcd_clear();
		_delay_ms(500);
	}
}

여기서 lcd1602a_port 구조체 변수 port의 DDRx 레지스터와 PORTx 레지스터의 값을 스스로의 회로에 맞게 적절히 변경하고, set_lcd_bit 함수의 인자도 8비트 모드로 사용할 건지 4비트 모드로 사용할 건지에 따라 적절하게 넘겨준다.

그런 다음 atmel studio에서 위 코드를 실행시키게 되면 아래와 같이 1234567890이 2줄에 걸쳐 반복해서 출력되게 된다.

영상을 보면 오른쪽에 빈 공간이 생긴 것을 볼 수 있다. 이는 lcd_init 함수의 인자 값으로 "LCD_ROWS_MAX - 6"를 넘겨주어 발생한 것이다. 최대 공간(16)에서 6을 뺀 공간만큼에서만 LCD를 사용하겠다고 초기화하여 10칸을 넘어가는 출력(두 번째 1234567890)은 다음 줄에 출력되게 된 것이다. 

그 다음은 2020.06.28 기준으로 추가된 lcd_create_char 함수에 대한 예제(example2 폴더 안에 있는 main.c)를 살펴보도록 하겠다. 이 함수는 크기가 8인 8비트 배열과 CGRAM 상에서의 위치를 인자로 넘겨주면 CGRAM 상의 위치에 넘겨받은 배열의 데이터를 작성하여 커스텀 폰트를 사용할 수 있도록 한다. 

아래 예제는 최대 8개의 커스텀 문자 데이터를 저장할 수 있는 CGRAM 메모리에서 첫 번째 문자(symbol)을 저장할 수 있는 곳의 데이터를 바꿔가며 채워져 있는 하트와 채워져 있지 않은 하트를 반복해서 출력하는 예제이다. lcd_putc 함수는 DDRAM에 데이터를 써서 LCD에 문자를 출력하는 함수인데, 여기서 lcd_create_char 할 때 인자로 넘겨준 location(0~7까지의 범위를 지님)을 인자로 주게 되면 lcd_create_char로 생성한 패턴이 출력되게 된다.

// example2/main.c
#define F_CPU 16000000UL
#include <avr/io.h>
#include <util/delay.h>
#include "lcd1602a_h68.h"

int main(void)
{
	int i = 0, n = 0;
	struct lcd1602a_port port = { &DDRE, &DDRE, &PORTE, &PORTE };
	
	uint8_t heart[2][8] = {
		{0x00, 0x0A, 0x1F, 0x1F, 0x1F, 0x0E, 0x04,0x00 },
		{0x00, 0x0A, 0x15, 0x11, 0x11, 0x0A, 0x04,0x00 }
	};
	
	set_lcd_bit(4);
	set_lcd_port(port);
	lcd_init(LCD_ROWS_MAX, LCD_COLS_MAX);
	
	lcd_create_char(0, heart[n]);
	
	for(i = 0; i < 32; i++)
		lcd_putc(0x00);
	
	while (1)
	{
		lcd_create_char(0, heart[n]);
		_delay_ms(1000);
		
		n = !n;
	}
}

처음에는 채워져 있는 하트를 LCD에 출력한다. 그러다가 CGRAM 0번째 데이터를 lcd_create_char함수의 location 인자를 0으로 하여 채워져 있지 않은 하트로 덮어쓰기 하면서 기존에 출력된 CGRAM 0번 째 데이터가 모두 채워져 있지 않은 하트로 바뀐다. 그리고 다시 while문이 돌 때 채워져 있는 하트로 덮어쓰기 한다. 이것을 반복하면서 LCD는 채워져 있는 하트와 채워져 있지 않은 하트가 반복해서 출력되게 된다. 실행 결과는 아래와 같다.

 

이번에는 2020.06.30 기준으로 추가된 lcd_display_shift_str 함수와 lcd_display_shift 함수에 대해서 살펴보고자 한다. 우선 이 두 함수는 LCD의 display를 인자로 넘겨준 direction에 따라 결정된 방향으로 shift 시킨다. direction의 값은 0 또는 1이며, 이것은 각각 LCD_SHIFT_LEFT와 LCD_SHIFT_RIGHT로 lcd1602a_h68.h에 정의되어 있다. 여기서 모두 shift 되었을 경우, lcd_display_shift_str 함수는 인자로 넘겨준 str_size 만큼 처음 위치에서 인자로 넘겨준 direction 값의 반대 방향으로 이동하게 된다. 예를 들어 "Hello World"라는 문자열이 LCD에 출력되었고 이것을 계속 오른쪽으로 shift 하다가 lcd_init에서 결정한 최대 가로, 세로 길이에서 마지막으로 찍히고 shift 되어 사라졌을 때 str_size로 "Hello World"의 길이, 11을 넘겨주게 되면 처음 위치에서 11만큼 왼쪽으로 shift 된다 그리고 다시 오른쪽으로 한번 shift 했을 때 "d"가 나타나며 순차적으로 "Hello World"가 나타나게 된다. lcd_display_shift 함수는 내부적으로 lcd_display_shift_str 함수만을 호출하고 있으며 str_size에 0을 넘겨준다. 즉 lcd_display_shift 함수로 호출했을 때 lcd_init에서 결정한 최대 가로, 세로 길이를 넘어서 모두 shift 되면 처음 위치로 돌아가게 된다. 아래는 lcd_display_shift_str 함수를 사용하여 작성된 간단한 예제(example3/main.c)이다.

// example3/main.c
#define F_CPU 16000000UL
#include <avr/io.h>
#include <util/delay.h>
#include "lcd1602a_h68.h"

int main(void)
{
	struct lcd1602a_port port = {&DDRC, &DDRC, &PORTC, &PORTC};

	set_lcd_bit(4);
	set_lcd_port(port);
	lcd_init(LCD_ROWS_MAX, LCD_COLS_MAX);

	lcd_move(0, 0);
	lcd_puts("LCD1602A_H68");
	lcd_move(0, 1);
	lcd_puts("HELLO WORLD!");

	while (1)
	{
		lcd_display_shift_str(LCD_SHIFT_RIGHT, 12);
		_delay_ms(200);
	}
}

위 코드를 보면 LCD의 첫 번째 줄(y 좌표가 0), 첫 번째 자리(x 좌표가 0)부터 "LCD1602A_H68"을 출력하고 두 번째 줄(y자표가 1), 첫 번째 자리(x좌표가 0)부터 "HELLO WORLD!"를 출력한다. 그리고 while, 무한반복문 안에서 lcd_display_shift_str 함수를 이용하여 오른쪽으로 200ms마다 shift한다. 그리고 인자로 12("LCD1602A_H68"와 "HELLO WORLD!"의 최대 길이)를 줌으로써  모두 shift 되었을 때 처음보다 12만큼 왼쪽으로 더 shift되게 하여 문자열들을 안보이게 하고 다음 shift 때 문자열들이 나타나게 하였다. 아래 실행 영상에서 볼 수 있다시피 이 함수는 LCD1602a를 이용하여 작은 전광판 기능을 만들고자 할 때 등에 유용하게 사용할 수 있다.

 

이 라이브러리는 매우 유용하다고 생각하고 있다. 내가 만들었지만... 하지만 어느 라이브러리가 그렇듯 오류가 발생할 수 있고 정상적으로 동작하지 않을 수 있다. 만약 그렇다면 이 라이브러리의 github repository에서 issues를 생성해서 알려주거나 이 글의 댓글로 알려주면 된다. 그러면 확인 후에 문제를 해결해보도록 하겠다. 물론 직접 코드를 수정해서 기여를 하는 것도 좋다 :). 

만약 이 라이브러리를 유용하게 잘 사용하고 있다면 이 라이브러리의 github repository를 방문하여 star를 눌러주거나, 이 글에 댓글을 남겨주면 좋을 것 같다. 소소하지만 뿌듯함을 느끼게 해준다.

이 라이브러리의 코드는 여기에 있으며 MIT라이센스이기 때문에 누구나 자유롭게 사용할 수 있다.

반응형

댓글