STM32F103

ST7565P GLCD 제어하기(제2편) - 제어함수 만들기 및 사용하기

엠쿠스 2018. 11. 28. 12:22
이글의 전부 또는 일부, 사진, 소스프로그램 등은 저작자의 동의 없이는 상업적인 사용을 금지합니다. 또한, 비상업적인 목적이라하더라도 출처를 밝히지 않고 게시하는 것은 금지합니다.


 

 

앞의 글 RA8835 GLCD 다루기에서 하드웨어 의존적 함수들은 모두 WS320240Display.c 파일 안에 만들고 GlcdDisplay.c와 Graphic.c는 가급적 하드웨어 독립적으로 만들었습니다. 이글에서도 이와 비슷하게 구성하여 GlcdDisplay.c와 Graphic.c 파일에 수정을 최소화하여 재활용하도록 하겠습니다.

True Studio 프로젝트의 [Inc] 폴더에 M19264Display.h 파일을 추가하고 다음과 같이 매크로 및 전역 변수, 함수 원형을 선언합니다.

#ifndef M19264DISPLAY_H_
#define M19264DISPLAY_H_


#define M19264DATAPORT DB0_GPIO_Port
#define M19264CTRLPORT E_RD_GPIO_Port

#define	ST7565P_DISPLAY_ON          0xAF
#define	ST7565P_DISPLAY_OFF         0xAE
#define	ST7565P_STARTLINE_CMD       0x40          // or line
#define	ST7565P_PAGE_CMD            0xB0         // or PAGE
#define	ST7565P_COLUMN_CMD          0x10         // or COLUMN
#define	ST7565P_SEGMODE_NORMAL      0xA0
#define	ST7565P_SEGMODE_REVERSE     0xA1
#define	ST7565P_LCD_BIAS_9          0xA2
#define	ST7565P_LCD_BIAS_7          0xA3
#define	ST7565P_DISPLAY_ALL_NORMAL  0xA4
#define	ST7565P_DISPLAY_ALL_POINT   0xA5
#define	ST7565P_DISPLAY_NORMAL      0xA6
#define	ST7565P_DISPLAY_REVERSE     0xA7
#define	ST7565P_COMMODE_NORMAL      0xC0
#define	ST7565P_COMMODE_REVERSE     0xC8
#define	ST7565P_SETEV_CMD           0x81
#define	ST7565P_PWR_CTRL_SET        0x28
#define	ST7565P_BOOSTER_ON          0x04
#define	ST7565P_BOOSTER_OFF         0x00
#define	ST7565P_REGULATOR_ON        0x02
#define	ST7565P_REGULATOR_OFF       0x00
#define	ST7565P_FOLLOW_CIRCUIT_ON   0x01
#define	ST7565P_FOLLOW_CIRCUIT_OFF  0x00
#define	ST7565P_REGULATE_CMD        0x20
#define	ST7565P_MODIFY              0xE0
#define	ST7565P_END                 0xEE

#define	ST7565P_COLUMNS             132
#define	M19264_RESOLUTION_X         192
#define	M19264_RESOLUTION_Y         64
#define	M19264_PAGES                (64 / BITSPERBYTE)
#define	M19264_COLUMNS              (M19264_RESOLUTION_X / 2)
#define	M19264_START_COLUMN         ((ST7565P_COLUMNS - M19264_COLUMNS) / 2)

#define	BITSPERBYTE                 8
#define	ASCII_FONTSIZE_X            8
#define	ASCII_FONTSIZE_Y            16
#define	ASCII_FONTSIZE	            (ASCII_FONTSIZE_X * ASCII_FONTSIZE_Y / BITSPERBYTE)
#define	HANGEUL_FONTSIZE_X          (ASCII_FONTSIZE_X * 2)
#define	HANGEUL_FONTSIZE_Y          ASCII_FONTSIZE_Y
#define	HANGEUL_FONTSIZE            (HANGEUL_FONTSIZE_X * HANGEUL_FONTSIZE_Y / BITSPERBYTE)

extern uint16_t CurrentCS;
extern uint8_t  CurrentX, CurrentY;

void GlcdWriteCommand(uint8_t cmd);
void GlcdWriteData(uint8_t data);
uint8_t GlcdReadData(void);

void GlcdInitialize(void);
void GlcdWriteCurrentCS(uint16_t cs);
void GlcdWriteCurrentXY(uint8_t x, uint8_t y);
void GlcdGraphicClear(void);
void GlcdGraphicClearCS(uint8_t cs);
void GlcdGraphicGotoxy(uint16_t x, uint16_t y);
void GlcdGraphicPut16byteVert(uint8_t *p);
void GlcdGraphicPut16byteVertGotoxy(uint16_t x, uint16_t y, uint8_t *ptr);
void GlcdGraphicPut16x2byteVert(uint8_t *ptr);
void GlcdGraphicPut16x2byteVertGotoxy(uint16_t x, uint16_t y, uint8_t *ptr);
void GlcdSetPixel(uint16_t x,uint16_t y, int color);

#endif /* M19264DISPLAY_H_ */


ST7565로 시작하는 매크로들은 그래픽 lcd 컨트롤러인 ST7565P 제어에 사용할 명령어나 상수들입니다. 제어 명령 및 초기화 함수에 관한 설명은 차후에 추가하도록 하겠습니다.
함수 원형들은 이전 글 RA8835 GLCD 제어하기 제5편제6편에 첨부한 소스 프로그램의 WS320240Display.h에 포함된 함수들과 같습니다. 다만 RA8835는 자체 문자 폰트를 가지고 있어서 자체 문자 폰트로 출력하던 함수(ex: GlcdPuts() 등)들을 만들어서 사용했지만, ST7565P에는 문자 폰트가 포함되어 있지 않으므로 GlcdPuts() 등과 같은 몇몇의 함수들은 만들 필요가 없습니다.

True Studio 프로젝트의 [Src] 폴더에 M19264Display.c 파일을 추가하고 STM32F103으로부터 그래픽 lcd로 명령을 전달하는 함수 GlcdWriteCommand() 함수와 GlcdWriteData() 함수를 만듭니다.

#include "stm32f1xx_hal.h"
#include "M19264Display.h"

uint16_t CurrentCS;
uint8_t  CurrentX, CurrentY;

void GlcdWriteCommand(uint8_t cmd)
{
	M19264DATAPORT->ODR = cmd;
 	M19264CTRLPORT->BRR = CurrentCS | RS_Pin | RW_WR_Pin;
	M19264CTRLPORT->BSRR = CurrentCS | RS_Pin | RW_WR_Pin;
}

void GlcdWriteData(uint8_t data)
{
	M19264DATAPORT->ODR = data;
	M19264CTRLPORT->BRR = CurrentCS | RW_WR_Pin;
	M19264CTRLPORT->BSRR = CurrentCS | RW_WR_Pin;
}

uint8_t GlcdReadData(void)
{
	uint8_t data;

	M19264DATAPORT->CRL = 0x44444444;	     //SetDataportInput
	M19264CTRLPORT->BRR = CurrentCS | E_RD_Pin;
	data = M19264DATAPORT->IDR;
	M19264CTRLPORT->BSRR = CurrentCS | E_RD_Pin;
	M19264DATAPORT->CRL = 0x22222222;         //SetDataportOutput
	return data;
}


ST7565P는 6800 방식으로 제어할 수도 있고, 8080 방식으로 제어할 수도 있으며, SPI 방식으로도 제어할 수 있습니다. 예전에 avr로 제어했던 글 ST7565P GLCD 다루기 - MGG19264(2편)에서는 6800방식으로 제어해도 되고, 8080방식으로 제어해도 되었던 것 같은데, STM32로 제어하다보니 6800방식으로는 올바른 동작을 하지 않습니다. 요즈음 만지지 않았던 avr을 꺼내서 다시 확인해봐아 할 것 같습니다. 위의 GlcdWriteCommand() 함수와 GlcWriteData() 함수, GlcdReadData() 함수들은 8080 방식으로 ST7565P 컨트롤러를 제어합니다.

GlcdWriteCommand() 함수와 GlcdWriteData() 함수는는 단 3줄로 그 기능을 다 수행합니다. 최적화시키면 다른 코드가 나올 수도 있겠지만, 현재 상태로 디스어셈블리 코드를 보니까 다음과 같습니다.

 20       	M19264DATAPORT->ODR = cmd;
          GlcdWriteCommand:
08000c8c:   ldr     r3, [pc, #20]   ; (0x8000ca4 < GLCDWRITECOMMAND+24>)
 21        	M19264CTRLPORT->BRR = CurrentCS | RS_Pin | RW_WR_Pin;
08000c8e:   ldr     r2, [pc, #24]   ; (0x8000ca8 < GLCDWRITECOMMAND+28>)
 20       	M19264DATAPORT->ODR = cmd;
08000c90:   str     r0, [r3, #12]
 21        	M19264CTRLPORT->BRR = CurrentCS | RS_Pin | RW_WR_Pin;
08000c92:   ldr     r3, [pc, #24]   ; (0x8000cac < GLCDWRITECOMMAND+32>)
08000c94:   ldrh    r3, [r3, #0]
08000c96:   orr.w   r3, r3, #96     ; 0x60
08000c9a:   uxth    r3, r3
08000c9c:   str     r3, [r2, #20]
 22       	M19264CTRLPORT->BSRR = CurrentCS | RS_Pin | RW_WR_Pin;
08000c9e:   str     r3, [r2, #16]
08000ca0:   bx      lr
08000ca2:   nop     
08000ca4:   lsrs    r0, r0, #32
08000ca6:   ands    r1, r0
08000ca8:   lsrs    r0, r0, #16
08000caa:   ands    r1, r0
08000cac:   movs    r6, r5
08000cae:   movs    r0, #0


어셈블리 코드를 위와 같이 생성하기 때문에 avr에서 어셈블리어로 함수를 만들 때처럼 "NOP" 명령을 군데군데 넣어서 시간을 지연시켜 타이밍을 맞추려는 노력은 하지 않아도 됩니다. 어셈블리어의 위력이라고 할까? C언어의 간편함이라고 할까? 하여간 STM32의 어셈블리어는 탐구해 본적도 없고 당분간 그럴 계획도 없습니다만, 프로그램은 간결하고 빠르게 돌리려면 아무리 C언어라고해도 어셈블리어에게는 대적이 안된다는 생각이 듭니다.

M19264Display.c 파일에 위 세 함수 이외에 필요한 다른 함수들을 다음과 같이 만듭니다.

void GlcdInitialize(void)
{
	GlcdWriteCurrentXY(0,0);
	GlcdWriteCurrentCS(CS1_Pin | CS2_Pin);
	GlcdWriteCommand(ST7565P_LCD_BIAS_9);
	GlcdWriteCommand(ST7565P_SEGMODE_NORMAL);
	GlcdWriteCommand(ST7565P_COMMODE_REVERSE);
	GlcdWriteCommand(ST7565P_REGULATE_CMD | 0x03);
	GlcdWriteCommand(ST7565P_PWR_CTRL_SET | ST7565P_BOOSTER_ON | ST7565P_REGULATOR_ON | ST7565P_FOLLOW_CIRCUIT_ON);
	GlcdWriteCommand(ST7565P_SETEV_CMD);
	GlcdWriteCommand(0x34);
	GlcdWriteCommand(ST7565P_STARTLINE_CMD);
	GlcdWriteCommand(ST7565P_DISPLAY_ON);
	GlcdGraphicClear();
}

void GlcdWriteCurrentCS(uint16_t cs)
{
	CurrentCS = cs;
}

void GlcdWriteCurrentXY(uint8_t x, uint8_t y)
{
	CurrentX = x;
	CurrentY = y;
	uint8_t ch = (x % M19264_COLUMNS) + M19264_START_COLUMN;
	GlcdWriteCurrentCS((x < M19264_COLUMNS) ? CS1_Pin : CS2_Pin);
	GlcdWriteCommand(ST7565P_PAGE_CMD + y / BITSPERBYTE);
	GlcdWriteCommand(ST7565P_COLUMN_CMD | (ch >> 4));
	GlcdWriteCommand(ch & 0x0F);
}

void GlcdGraphicClear(void)
{
	GlcdGraphicClearCS(CS2_Pin);
	GlcdGraphicClearCS(CS1_Pin);
}

void GlcdGraphicClearCS(uint8_t cs)
{
	uint8_t i,j;

	for(i = 0;i < M19264_PAGES;i++) {
		GlcdGraphicGotoxy(cs == CS1_Pin ? 0 : M19264_COLUMNS, i * BITSPERBYTE);
		for(j = 0; j < M19264_COLUMNS;j++)
			GlcdWriteData(0);
	}
	GlcdGraphicGotoxy(cs == CS1_Pin ? 0 : M19264_COLUMNS, 0);
}

void GlcdGraphicGotoxy(uint16_t x, uint16_t y)
{
	if(x >= M19264_RESOLUTION_X) {
		x = 0;
		y += ASCII_FONTSIZE_Y;
	}
	if(y >= M19264_RESOLUTION_Y) y = 0;
	GlcdWriteCurrentXY(x,y);
}

void GlcdGraphicPut16byteVert(uint8_t *p)
{
	uint8_t i;

	for(i = 0;i < ASCII_FONTSIZE_X;i++)
		GlcdWriteData(*(p + i));
	GlcdGraphicGotoxy(CurrentX, CurrentY + BITSPERBYTE);
	for(;i < ASCII_FONTSIZE_X * 2;i++)
		GlcdWriteData(*(p + i));
	GlcdGraphicGotoxy(CurrentX + 8, CurrentY - BITSPERBYTE);
}

void GlcdGraphicPut16byteVertGotoxy(uint16_t x, uint16_t y, uint8_t *p)
{
	GlcdGraphicGotoxy(x, y);
	GlcdGraphicPut16byteVert(p);
}

void GlcdGraphicPut16x2byteVert(uint8_t *p)
{
	uint8_t i;

	for(i = 0;i < HANGEUL_FONTSIZE_X;i++)
		GlcdWriteData(*(p + i));
	GlcdGraphicGotoxy(CurrentX, CurrentY + BITSPERBYTE);
	for(;i < HANGEUL_FONTSIZE_X * 2;i++)
		GlcdWriteData(*(p + i));
	GlcdGraphicGotoxy(CurrentX + HANGEUL_FONTSIZE_X, CurrentY - BITSPERBYTE);
}

void GlcdGraphicPut16x2byteVertGotoxy(uint16_t x, uint16_t y, uint8_t *p)
{
	GlcdGraphicGotoxy(x, y);
	GlcdGraphicPut16x2byteVert(p);
}

void GlcdSetPixel(uint16_t x,uint16_t y, int color)
{
	uint8_t i;

	GlcdGraphicGotoxy(x, y);
	GlcdReadData();			// dummy read
	i = GlcdReadData();
	GlcdGraphicGotoxy(x, y);
	if(color) GlcdWriteData(i | (0x01 << (y % BITSPERBYTE)));
	else GlcdWriteData(i &= ~(0x01 << (y % BITSPERBYTE)));
}


앞의 글 RA8835 GLCD 제어하기에서 만들었던 함수와 같은 이름으로 GlcdGraphicPut16byteVert() 함수와 GlcdGraphicPut16x2byteVert() 함수를 만들었습니다. 이 두 함수들은 각각 8행 16열로 16바이트, 16열 16행으로 32바이트 출력하는 함수입니다. GlcdGraphicPut16byteVert() 함수는 ASCII 문자 한 문자, GlcdGraphicPut16x2byteVert() 함수는 한글 한 글자를 출력합니다. 이 두 함수는 실행 후에 다음 문자를 출력할 위치로 이동하도록 프로그램을 작성했습니다. 각각의 함수명 끝에 Gotoxy가 붙은 함수는 GlcdGraphicGotoxy() 함수를 호출한 후에 각각에 해당하는 함수를 호출합니다.

이 그래픽 lcd는 컨트롤러 ST7565P를 두 개 가지고 있습니다. 화면상의 좌표로 보면 열0부터 열95까지는 첫번째의 ST7565P가 담당하고 열96부터 열 191까지는 두번째의 ST7565P가 담당합니다. 데이터를 출력하려는 열의 위치에 따라 첫째 ST7565P를 사용할 것인지 두번째 ST7565P를 사용할 것인지를 결정해야 합니다. 간혹 그래픽 lcd 초기화나 화면 지우기 등과 같이 두 개의 ST7565P를 동시에 다루는 것이 더 효율적인 경우도 있습니다. GlcdWriteCommand(), GlcdWriteData(), GlcdReadData() 등의 함수를 호출하면 전역 변수 CurrentCS에 들어 있는 값에 따라 ST7565P를 선택하여 제어합니다. 전역 변수 CurrentCS에는 STM32CubeMX에서 지정한 GPIO 핀명인 CS1_Pin, CS2_Pin 또는 CS1_Pin | CS2_Pin 등의 값을 넣으면 됩니다.
GlcdWriteCurrentCS()를 만들고, 전역 변수 CurrentCS에 값을 써 넣을 때에는 반드시 이 함수를 호출하도록 프로그램을 작성합니다. 이렇게 함으로써 디버그할 때에 이 함수만 감시하면 어디서 값이 잘못되는지를 쉽게 파악할 수 있습니다.

RA8835와 달리 ST7565P에는 현재 커서의 값을 읽어 오는 기능이 없는 듯 합니다. 그래서 현재 커서의 위치를 기억시키기 위해서 CurrentX와 CurrentY를 전역 변수로 선언하여 사용합니다. CurrentX와 CurrentY의 값을 변경할 때에도 CurrnetCS의 경우와 마찬가지로, GlcdWriteCurrentXY() 함수를 만들고 반드시 이 함수를 호출하여 CurrentX와 CurrentY의 값을 변화시키도록 프로그램을 작성합니다.

CurrentX의 값에 따라 CurrentCS의 값이 정해져야하므로 GlcdWriteCurrentXY() 함수 내에서 CurrentCS의 값을 파악하여 GlcdWriteCurrentCS() 함수를 호출하도록 했습니다.

ST7565P의 구조상 한 바이트를 화면에 출력하면 Y축 방향으로 8개의 픽셀에 데이터가 출력됩니다. 이때문에 y값을 완전히 픽셀 단위로 사용하려면 영문자나 한글 등 문자를 출력할 때에 비트 단위의 연산을 많이 해야 합니다. 번거로움에 비하여 실익이 별로 없으므로, y값은 픽셀 단위로 지정하되 실제로는 8행 단위로 지정합니다. 즉 GlcdWriteCurrentXY(0, 7);은 결과적으로 GlcdWriteCurrentXY(0,0);과 같습니다.

위 코드 중 함수 GlcdWriteCurrentXY() 안의 코드 GlcdWriteCommand(ST7565P_PAGE_CMD + y / BITSPERBYTE);가 Y축 방향으로 8행 단위로 계산하여 출력하도록 지정하는 기능을 합니다.
데이터를 출력할 화면 상의 위치를 지정하는 함수로 GlcdGraphicGotoxy() 함수를 만들었습니다. GlcdGraphicGotoxy() 함수는 내부적으로 GlcdWriteCurrentXY() 함수를 호출하여 출력 위치를 지정합니다.

X축은 픽셀 단위로 좌표를 지정할 수 있습니다. 다만 제어칩이 CS1에서 CS2로 바뀌어야 하는데, 한 바이트 출력할 때마다 CurrentCS를 설정하지 않으므로 하나의 문자가 CS1과 CS2에 걸쳐서 출력되지 않게 주의해야합니다. 물론 GlcdGraphicPut16byteVert() 함수와 GlcdGraphicPut16x2byteVert() 함수 내에서 한 바이트 출력할 때마다 GlcdWriteCurrentXY() 함수를 호출하도록 약간만 수정하면 간단히 해결할 수 있습니다만, 실행 속도 등 효율성 측면에서 그리 큰 실익이 없다고 판단하여 적용하지 않았습니다.

GlcdSetPixel() 함수는 화면에 한 점을 출력하는 함수입니다. 한 점을 출력하기 위해서는 기존에 화면의 내용을 읽어온 다음에 출력할 내용을 OR 연산 처리한 다음 화면으로 출력합니다. 기존 화면의 내용을 읽어 오기 위해서 연속적으로 두 번 GlcdReadData() 함수를 호출하고 있습니다. ST7565P의 매뉴얼에 의하면 주소를 설정하거나 쓰기 작업을 한 후에 데이터를 읽어 오려면 dummy read를 한 번 해야 합니다. GlcdSetPixel() 함수에서 GlcdGraphicGotoxy()를 호출하는데, GlcdGraphicGotoxy() 함수에서 주소를 기록하는 작업을 하기때문에 데이터를 읽기 전에 dummy read를 한 번 해야 합니다. 이런 이유로 GlcdReadData() 함수를 두 번 호출하고 두번째 값을 읽은 값으로 사용합니다.

앞의 글 RA8835 GLCD 제어하기에서 사용했던 GraphicDisplay.h와 Graphic.h를 그대로 지금의 프로젝트 [Inc] 파일에 추가합니다. GraphicDisplay.c 파일과 Graphic.c 파일은 포함하는 헤더 파일명만 아래과 같이 수정하여 [Src] 폴더에 추가합니다.

GraphicDisplay.c

#include "stm32f1xx_hal.h"
#include "stm32f1xx.h"
#include "M19264Display.h"
#include "ST7565Font.h"
#include "GlcdDisplay.h"


GraphicDisplay.
#include "stm32f1xx_hal.h"
#include "M19264Display.h"
#include "Graphic.h"

 



RA8835와 ST7565P는 메모리와 화면 출력 매칭 패턴이 다르므로 폰트를 그대로 사용할 수 없습니다. RA8835에 사용했던 그래픽 ASCII 문자 폰트와 한글 폰트들을 ST7565P에 맞게 변환한 ST7565Font.c를 사용해야 합니다.

.

main.c에 다음과 같이 파일을 포함시키고, 예제 함수를 실행시켜 봅니다.

/* USER CODE BEGIN Includes */
#include "M19264Display.h"
#include "GlcdDisplay.h"
#include "Graphic.h"
/* USER CODE END Includes */

  /* USER CODE BEGIN 1 */
  uint8_t* pMsg = (uint8_t *)"(C)mcus.tistory.com 한글";
  /* USER CODE END 1 */

  /* USER CODE BEGIN 2 */
  HAL_Delay(100);
  GlcdInitialize();
  GlcdGraphicPutsGotoxy(0, 0, pMsg);
  GlcdLine(0, 0, 191, 63);
  GlcdCircle(96, 31, 31);
  GlcdLine(191, 0, 0, 63);
  /* USER CODE END 2 */

 



동작하는 화면 사진입니다.

 

 

 

소스 프로그램을 압축한 파일을 첨부합니다.

 

 

STM32F103M19264.zip



 

STM32F103C8T6 보드용 보드를 만들었으니, 여기에 여러가지 부품을 달아가면서 STM32F의 기능들을 익혀갈까 합니다.