Nocuta NF-A8 5V PWM

- 보드 : P-NUCLEO-WB55

- 개발 툴 : STM32CubeMX, True Studio

- 팬 : 녹투아 NF-A8 5V PWM

- 팬 작동 전압 : 5V 

- 팬 소비 전력 : 0.75W

- 팬 Logic Level : PWM (Input) - Logic Low 최대 전압 0.8V / Logic High 최대 전압 5.25V

- 팬 PWM 주파수 : 25kHz (허용범위 21kHz ~ 28kHz)

- 팬 듀티 사이클 : 0 ~ 100%

- 팬 전원 : 18650 배터리

- 부스트 컨버터 : TI TPS61322 (18650 (2.7V~4.2V) -> TPS61322 (5V) -> NF-A8 5V PWM)

- 팬 제어 참고 자료

https://noctua.at/pub/media/wysiwyg/Noctua_PWM_specifications_white_paper.pdf

https://www.youtube.com/watch?v=gKHww3qJbs8


[연결]

[작동 방식]

<PWM>

Noctua PWM specifications white paper - PWM

- PWM을 이용해 팬 속도 조절

- 팬 속도는 듀티 사이클에 비례 (0~100%)

- 팬의 PWM 핀에 입력되는 PWM 신호가 0 V일 때 (듀티 사이클 0 %), 팬이 정지하고

- PWM 신호가 5 V일 때 (듀티 사이클 100%), 팬은 최고 속도로 회전한다

- 한 번의 PWM 신호 입력으로 팬 RPM이 고정되는 것이 아니라 지속적으로 PWM 신호를 입력해줘야 한다

 

<RPM>

Noctua PWM specifications white paper - RPM

- 팬의 Tachometer 출력 신호는 팬의 현재 RPM을 알아내는데 사용

- 출력 신호는 Hz 이고 팬 속도는 일반적으로 RPM (Revolutions Per Minute)으로 표기된다

- RPM으로 변환하기 위해 출력 신호를 60배 증가 시키고 팬이 회전당 두 개의 임펄스를 출력하므로 2로 나누어 준다

- fan speed [RPM] = frequency [Hz] * 60 / 2

- 86 [Hz] * 60 / 2 = 2580 [rpm]

Noctua PWM specifications white paper - Tachometer 핀 연결 설정

- 녹투아 팬에서 Tachometer 신호를 읽어들이기 위해선 위의 그림과 같은 연결이 필요하다

- 5V 팬을 사용하므로 Link에 5mA 미만의 전류가 흐르게 해야한다

- 그림과 다르게 Vcc 5V를 사용하고 1.2k 저항을 사용한 결과 정상 작동 확인


[CubeMX 설정]

<PWM 설정>

PWM 설정

- PWM 출력에 사용되는 클럭은 해당 타이머가 속하는 APB의 타이머 클럭을 기반으로 한다

- (위 설정의 경우, TIM2는 APB1에 속하고 APB1의 타이머 클럭은 32MHz 이다)

- PWM 주파수는 Timer Clock / (Prescaler+1)(Counter Period+1) 로 결정된다

- (Prescaler, Counter Period 는 실제 주파수 계산 때 +1이 되므로 원하는 값에 -1을 함)

- 따라서 위 설정에서 PWM 주파수는 32 Mhz / ((10-1)+1)((128-1)+1) = 25kHz 가 된다

- 듀티 사이클 (%) =  (사용자 입력값 / Counter Period) * 100

- Pulse : Counter Compare Register (CCR) 초기화 값

- Counter Mode : 카운트 증가 방향

- PWM mode 1 : 업카운팅 모드에서 period (CNT) < pulse (CCR)일 때, 타이머 채널 활성화 이외엔 비활성화

- 카운터 모드가 업이므로 timer counter register (CNT)는 1씩 증가하고 pulse (CCR) 값보다 작을 때는 타이머 채널이 활성화(CH Polarity : High 이므로 High 출력) pulse 값 이상일 땐 비활성화된다

- period (CNT)는 1씩 증가하다 Counter Period(AutoReload Register(ARR))+1 을 초과할 때 0이 된다

- (위의 설정에선 128을 넘어서면 다시 0부터 1씩 증가)

 

<Tachometer Input 핀 설정>

Tachometer 핀에 연결될 GPIO 핀 설정

- Tachometer 신호를 입력받는데 사용할 GPIO 핀을 GPIO_Input 으로 설정한다 (사용하지 않는 핀들 중에 선택)

 

<타이머 설정 (PWM 측정용)>

- PWM 실시간 측정용 타이머 설정

- 타이머 주기 = 32 MHz / {((32000-1)+1)*((1000-1)+1)} = 1 sec

- (사용 타이머가 속하는 APBx 타이머 클럭 (MHz) / {(Prescaler+1)(Counter Period+1)})

 

<버튼 설정 (필수 X, 듀티 사이클 조절하는데 사용)>

보드 탑재 버튼 설정

- 보드에 탑재되어있는 버튼들을 사용해 PWM 듀티 사이클 조절

- 사용할 버튼을 GPIO_EXTIx 로 변경해 인터럽트 모드 사용

- 풀업 설정 및 폴링 엣지에서 인터럽트가 발생하게 설정

- (내부 풀업으로 인해 버튼에 연결된 핀은 High 상태를 유지하다 버튼이 눌리는 순간 Low로 바뀐다)

 

<인터럽트 우선 순위 설정>

- SysTick 핸들러 내에서 증가하는 카운터에 의존하는 모든 딜레이와 타임아웃 HAL 함수들을 다른 인터럽트 내에서 사용할 경우, SysTick 인터럽트 우선 순위를 다른 인터럽트 함수보다 높게 설정해야 한다 (낮은 순자)

- (우선 순위 안 바꿀 경우, 버튼 및 타이머 인터럽트 콜백에서 HAL_GetTick() 정상 작동 X)

- 기본적으로 우선 순위가 0으로 설정되므로 다른 인터럽트들의 우선 순위를 낮춰서 사용 (숫자 ↑ 우선 순위 ↓)

- (https://stackoverflow.com/questions/53899882/hal-delay-stuck-in-a-infinite-loop 참고해 설정)

 

[코드]

<듀티 사이클 조절 함수 (버튼 인터럽트 콜백 함수)>

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	//count : 버튼이 눌려있던 시간을 저장하는 변수
	//lower_limit : 버튼이 눌러졌다고 인식되기 위해 필요한 최소 시간(ms)
	int count=0, lower_limit=80;
    
	//타이머-채널에 설정된 듀티 사이클을 저장하는 변수 선언 및 현재 듀티 사이클 읽어와서 저장
	uint8_t current_dutyCycle=HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_1);

	//사용자가 누른 버튼이 1번 버튼일 때
	if(GPIO_Pin==B1_Pin)
	{
		//버튼 디바운스 함수를 통해 사용자가 버튼을 누르고 있던 시간을 측정    
		count=button_debounce(B1_GPIO_Port,B1_Pin);

		//하한을 넘어가는 시간동안 버튼을 누르고 있었다면 듀티 사이클 조절 실행
		if(lower_limit < count)
		{
			if(10<=current_dutyCycle)
			{
				//현재 설정된 듀티 사이클을 저장한 변수에 -10값을 새로운 듀티 사이클로 설정
				__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,dutyCycle-10);
				//위의 HAL 드라이버 대신 직접 타이머-채널 레지스터에 접근해 듀티 사이클 설정도 가능
//				TIM2->CCR1-=10;
			}
			else
			{
				__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,0);
//				TIM2->CCR1=0;
			}
		}
	}
	//사용자가 누른 버튼이 2번 버튼일 때
	else if(GPIO_Pin==B2_Pin)
	{
		count=button_debounce(B2_GPIO_Port,B2_Pin);

		if(lower_limit < count)
		{
			if(120<=current_dutyCycle)
			{
				__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,128);
//				TIM2->CCR1=128;
			}
			else
			{
				__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,dutyCycle+10);
//				TIM2->CCR1+=10;
			}
		}
	}
    //사용자가 누른 버튼이 3번 버튼일 때
	else if(GPIO_Pin==B3_Pin)
	{
		count=button_debounce(B3_GPIO_Port,B3_Pin);
		if(lower_limit < count)
		{
			//RPM 측정 함수 호출        
			rpm_calculation(&htim2,TIM_CHANNEL_1);
		}
	}
	else
	{

	}
	printf("PWM : %ld\r\n",HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_1));
//		printf("PWM : %ld\r\n",TIM2->CCR1);
}

- 버튼 입력을 통해 PWM 듀티 사이클을 조절하므로 HAL_GPIO_EXTI_Callback() (GPIO 인터럽트 콜백 함수) 사용

- HAL_TIM_ReadCapturedValue() 함수를 통해 변경하려는 PWM 채널의 현재 듀티 사이클 읽어옴

- ((&htim2, TIM_CHANNEL_1) : Timer 2의 채널 1)

- B1 (버튼1) : 듀티 사이클 감소, B2 (버튼2) : 듀티 사이클 증가, B2 (버튼3) : RPM 측정

- 콜백 함수가 호출되고 전달된 매개변수 GPIO_Pin을 통해 인터럽트가 발생한 GPIO 핀이 어떤 핀인지 확인

- if~else 문을 통해 핀에 따라 듀티 사이클 감소, 증가 or RPM 측정 실행

- button_debounce() 함수를 통해 버튼이 몇 ms동안 눌려졌는지 측정하고 그 값을 count 변수에 저장

- 설정한 하한(lower_limit 변수)을 넘어가는 시간만큼(ms, count변수) 버튼이 눌렸을 경우에만 듀티 사이클 조절 실행

- __HAL_TIM_SET_COMPARE() 함수를 통해 듀티 사이클을 변경하려는 타이머, 채널 및 듀티 사이클 값을 입력

- (HAL_TIM_ReadCapturedValue() 함수를 통해 읽어 온 현재 듀티 사이클 값을 기반으로 ±10 가감)

- 듀티 사이클 최대값은 CubeMX에서 설정한 Counter Period 값 +1 이다

- HAL_TIM_ReadCapturedValue(), __HAL_TIM_SET_COMPARE() 같은 HAL 드라이버를 사용하지 않고 TIMx->CCRn 로 직접 레지스터에 접근해 현재 설정된 듀티 사이클을 읽어오거나 설정할 수도 있다

 

<버튼 Debounce 함수>

/* @ 버튼 디바운스 함수
 *  gpio_port : 버튼과 연결된 GPIO 핀이 속한 GPIO Port
 *  gpio_pin : 버튼과 연결된 GPIO 핀
 */
int button_debounce(GPIO_TypeDef* gpio_port, uint16_t gpio_pin)
{
	//count : 버튼을 누르고 있던 시간을 저장할 변수
	//start_time, end_time : 버튼을 누르고 있던 시간을 측정하기 위해 사용할 변수
	int count, start_time, end_time;

	//버튼과 연결된 GPIO 핀 상태를 저장할 변수 선언 및 현재 핀 상태 저장
	GPIO_PinState pin_stat=HAL_GPIO_ReadPin(gpio_port,gpio_pin);

	//HAL_GetTick() 함수를 통해 측정 시작하는 시간을 start_time 변수에 저장
	start_time=HAL_GetTick();
	while(pin_stat==GPIO_PIN_RESET)
	{
		//버튼 GPIO 핀 상태 읽어와 저장    
		pin_stat=HAL_GPIO_ReadPin(gpio_port,gpio_pin);
	}
	//버튼에서 손을 떼 측정이 종료된 시간을 end_time 변수에 저장
	end_time=HAL_GetTick();
    
	//종료 시간-시작 시간으로 버튼이 눌려있던 시간을 계산해 저장 후 반환
	count=end_time-start_time;
	return count;
}

- 버튼 디바운스 함수

- Nordic사의 App button 에서 따옴

- HAL_GetTick() 함수를 사용해 버튼이 눌린 시각과 버튼에서 손을 뗀 시각을 저장한 후 이를 이용해 버튼이 눌려져있던 시간을 측정

- 버튼 인터럽트 발생 직후에 호출되어 버튼에 연결된 GPIO 핀이 RESET(Low) 상태일 동안 while()문이 반복

- GPIO 핀이 SET(High) 상태가 되면 while()문 종료 및 눌린 상태로 존재하던 시간 계산 후 반환

 

<RPM 측정 함수>

void rpm_calculation(TIM_HandleTypeDef *tim_pwm, uint32_t tim_channel)
{
	//start_time : 측정 시작 시간 저장
	//end_time : 측정 종료 시간 저장
	int start_time=0, end_time=0, rpm=0;
	//측정된 주기를 바탕으로 계산한 주파수를 저장할 변수
	long double freq=0;

	//현재 듀티 사이클을 읽어와 저장
	uint8_t current_dutyCycle=HAL_TIM_ReadCapturedValue(tim_pwm, tim_channel);

	if(current_dutyCycle==0)
	{
		printf("RPM : 0\r\n");
		return;
	}

	GPIO_PinState pin_stat=HAL_GPIO_ReadPin(FAN_IN_GPIO_Port,FAN_IN_Pin);

	//현재 Tachometer 핀이 Low 상태일 때
	if(HAL_GPIO_ReadPin(FAN_IN_GPIO_Port,FAN_IN_Pin)==GPIO_PIN_RESET)
	{
		while(pin_stat==GPIO_PIN_RESET)
		{
			pin_stat=HAL_GPIO_ReadPin(FAN_IN_GPIO_Port,FAN_IN_Pin);
		}
		//측정 시작 시간 저장
		start_time=HAL_GetTick();
		while(pin_stat==GPIO_PIN_SET)
		{
			pin_stat=HAL_GPIO_ReadPin(FAN_IN_GPIO_Port,FAN_IN_Pin);
		}
		while(pin_stat==GPIO_PIN_RESET)
		{
			pin_stat=HAL_GPIO_ReadPin(FAN_IN_GPIO_Port,FAN_IN_Pin);
		}
		//측정 종료 시간 저장
		end_time=HAL_GetTick();
	}
	//현재 Tachometer 핀이 High 상태일 때
	else if(HAL_GPIO_ReadPin(FAN_IN_GPIO_Port,FAN_IN_Pin)==GPIO_PIN_SET)
	{
		while(pin_stat==GPIO_PIN_SET)
		{
			pin_stat=HAL_GPIO_ReadPin(FAN_IN_GPIO_Port,FAN_IN_Pin);
		}
		//측정 시작 시간 저장
		start_time=HAL_GetTick();
		while(pin_stat==GPIO_PIN_RESET)
		{
			pin_stat=HAL_GPIO_ReadPin(FAN_IN_GPIO_Port,FAN_IN_Pin);
		}
		while(pin_stat==GPIO_PIN_SET)
		{
			pin_stat=HAL_GPIO_ReadPin(FAN_IN_GPIO_Port,FAN_IN_Pin);
		}
		//측정 종료 시간 저장
		end_time=HAL_GetTick();
	}
	else
	{
    
	}
	//측정된 주기를 바탕으로 주파수 계산 (측정된 시간은 ms 단위이므로 *1000)
	freq=(1.0/(end_time-start_time))*1000;
	printf("Freq : %Lf\r\n",freq);
    
	//변환된 주파수를 주어진 RPM 계산 공식에 대입해 현재 RPM 계산
	rpm=(int)((freq*60.0/2.0));
	printf("RPM : %d\r\n",rpm);
}

- 팬의 Tachometer 핀에 연결된 GPIO 핀 (Input)의 상태를 측정해 RPM 계산

- 먼저 현재 듀티 사이클을 읽어와 0일 경우, 함수 종료 (RPM이 0일 때, Tachometer 핀은 Low 고정이므로)

- 팬 Tachometer 핀에 연결된 GPIO 핀 (Input)의 현재 상태를 측정한 뒤 두 가지 상황으로 나누어 진행

- 1. Tachometer 핀이 Reset (Low) 상태일 때

   - Tachometer 핀이 Low 상태 중간부터 측정이 시작됐다고 가정해 일단 Low 상태가 끝날 때까지 while()문으로 대기

   - 핀이 High로 전환되어 첫번째 while()문이 종료된 직후, HAL_GetTick() 함수를 사용해 Low 상태가 끝나고

     High 상태가 시작되는 시간을 start_time 변수에 저장 (ms 단위)

   - while()문을 통해 Tachometer 핀 High -> Low 한 주기가 지난 뒤, HAL_GetTick() 를 사용해 end_time 변수에

     한 주기가 끝나는 시간을 저장

- 2. Tachometer 핀이 Set (High) 상태일 때

   - Tachometer 핀이 High 상태 중간부터 측정이 시작됐다고 가정해 일단 High 상태가 끝날 때까지 while()문으로 대기

   - 핀이 Low로 전환되어 첫번째 while()문이 종료된 직후, HAL_GetTick() 함수를 사용해 High 상태가 끝나고

     Low 상태가 시작되는 시간을 start_time 변수에 저장 (ms 단위)

   - while()문을 통해 Tachometer 핀 Low -> High 한 주기가 지난 뒤, HAL_GetTick() 를 사용해 end_time 변수에

     한 주기가 끝나는 시간을 저장

- end_time-start_time을 통해 한 주기를 계산하고 이 값을 주파수로 변환한 뒤(f=1/T) 주어진 공식을 사용해 RPM 계산

 

<타이머 콜백 함수>

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	rpm_calculation(&htim2,TIM_CHANNEL_1);
}

- 타이머 인터럽트가 발생됐을 때 실행되는 콜백 함수

- rpm_calculation() 함수를 사용해 설정한 시간 간격(1초)마다 현재 팬의 RPM 측정

 

<main 함수>

int main(void)
{
  ...
  /* USER CODE BEGIN 2 */

  printf("Start\r\n");
  HAL_TIM_Base_Start_IT(&htim16);
  HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);

  /* USER CODE END 2 */
  ...
  while(1)
  {
  ...
  }
}

- 타이머 인터럽트(PWM 측정용으로 사용할) 시작 함수와 PWM 시작 함수 실행

 

<작동 결과>

- (세 번째 스크린샷의 dutycycle 표기는 오기입. 듀티사이클이 아닌 Counter Compare Register (CCR) 값)

[작동 테스트]

- 사용한 녹투아 NF-A8 5V PWM 의 경우, 듀티 사이클 0에서 정지, 듀티 사이클 6.25% (8/128)에서 팬 작동 시작

- 이때 측정되는 RPM은 192~200 RPM 사이, 듀티 사이클 10.15% (13/128)까지 동일 범위의 RPM 유지

 

<듀티 사이클 변화에 따른 RPM 변화 과정>

듀티 사이클 증가 직후, Tachometer 신호 변화 추이

- (위에서부터 각각 27.65ms / 26.75ms / 25.27ms 주기)
- 듀티 사이클 변경 직후, RPM이 해당 듀티 사이클에 해당하는 RPM으로 변화하기 전까지는 조금 시간이 걸린다

- (RPM 변경 완료까지 대략 18주기 정도 걸리는 것으로 측정)

 

<듀티 사이클 99%, 100%>

듀티 사이클 99.22% 신호 (127/128)
듀티 사이클 99.22% 에서의 Tachometer 신호
듀티 사이클 100%에서의 듀티 사이클 (128/128) 및 Tachometer 신호

- Counter Compare Register (CCR) 최대값(또한, Timer Counter Register(CNT)의 최대값)은 CubeMX 타이머 PWM에서 설정한 Counter Period (AutoReload Register) 값+1이다

- (Counter Period (AutoReload Register) = 128-1 로 설정된 상태)

- (위 두 그래프 CCR1=127, 마지막 그래프 CCR1=128인 상태)

 

<고정 듀티 사이클에서 RPM 변화>

- 듀티 사이클이 고정된 상태라도 RPM은 완벽히 고정되지 않고 일정 간격 내에서 계속 변화한다

사용한 ST7735 드라이버 탑재 80x160 0.96인치 LCD

(사용한 LCD 모듈 기준)

- 디스플레이 해상도 : 80x160 (드라이버 지원 해상도는 132x162, 128x160)

- 통신 : SPI (사용 모듈의 경우 4-line serial interface 고정 (C/S(Chip Select), SCL, MOSI, MISO))

- 작동 전압(VDD) : 2.6~3.6V

- I/O 전압 (GPIO) : 1.65~VDD

- 색심도(Color Depth) : 12-bit/pixel(RGB-444, 4k), 16-bit/pixer(RGB-565, 65k), 18-bit(RGB-666, 262k)


[작동 방식]

- SPI 통신 사용, 명령어를 전송해 디스플레이를 제어하거나 RGB 데이터를 전송해 디스플레이 표현

4-line serial interface 에서 사용되는 데이터 전송 흐름

- Clock Polarity : 0 (SCL 비활성화 상태에서 LOW 유지)

- Clock Phase : 0 (SCL 첫번째 엣지에서 데이터 캡쳐, 두번째 엣지에서 출력)

- CPOL=0, CPHA=0 이므로 SCL의 Rising Edge에서 SDA 라인의 데이터가 캡쳐되고 Falling Edge에서 데이터 출력

- D/C : LOW일 때 전송되는 데이터는 명령어,

          HIGH일 때 전송되는 데이터는 명령어 레지스터에 저장될 매개변수 (명령어와 연이어 전송될 경우)

          or 디스플레이 데이터 램에 저장될 RGB 데이터 배열

- 데이터 8 비트씩, MSB부터 전송

- 전송할 데이터의 종류에 따라(명령어=0, 디스플레이 데이터/명령어 매개변수=1) D/C 라인을 Low or High로 설정한다

- C/S 라인이 LOW 상태가 된 후에 데이터 전송이 이루어져야만 한다

- 데이터 전송이 완료된 이후엔 C/S 라인을 다시 High로 설정한다

 

타이밍

- ST7735에 데이터를 쓸 때 : SCL 주기 최소 66ns (15.15Mhz 이하)

- ST7735로부터 데이터를 읽을 때  SCL 주기 최소 150ns (6.67Mhz 이하)

 

<Color Depth>

- ST7735는 디스플레이를 표현하는데 있어서 4k (RGB 4-4-4 bit), 65k (RGB 5-6-5 bit), 262k (6-6-6 bit) 총 세가지 방식의 컬러 심도를 가지고 있다

- 전송한 RGB 데이터는 ST7735 디스플레이 데이터 램(132x162x18-bit 그래픽 타입 static 램)에 저장된다

- 전원 인가 직후 기본 값은 262k (18 bit per pixel)

12 bit, 16 bit 픽셀 포맷

- 기본 컬러 심도 설정은 18 bit이므로 12, 16bit 컬러 심도를 사용하기 위해선 먼저 컬러 심도 변경에 필요한 명령어와 명령어 매개변수를 전송해 컬러 심도 설정을 변경해야한다 (3Ah(명령어(COLMOD)) + D2~D0(매개변수) 총 2byte)

- 12 bit, 16 bit 포맷 둘 다 RGB 데이터 비트가 빈틈없이 연속적으로 이어진다

- 12 bit 포맷은 RGB(4-4-4)의 형태이므로 3byte가 두 픽셀의 RGB값을 가진다

- 16 bit 포맷은 RGB(5-6-5)의 형태이므로 2byte가 한 픽셀의 RGB값을 가진다

18bit RGB 6-6-6 포맷

- 전원 인가 직후, 하드웨어 리셋시 기본적으로 설정되어 있는 컬러 심도는 18 bit RGB 6-6-6 형태

- 12 bit, 16 bit 컬러 심도와는 다르게 RGB 비트가 빈틈없이 연이어 전송되는 형태가 아니고 6bit 색상 데이터(R/G/B)+2bit로 구성된 1 byte 데이터 세 개(R+G+B)가 하나의 픽셀을 나타낸다

- 전송되는 RGB 각 1 byte의 MSB인 D7(Bit 7)부터 D2(Bit 2)까지 색상값을 입력하면 된다 (D1, D0 사용 X, 전송 O)

 

<어드레스 카운터>

어드레스 카운터 표

- 어드레스 카운터는 읽기, 쓰기를 위한 디스플레이 데이터 램의 주소를 설정한다

- 데이터는 드라이버의 램 행렬(132x162x18-bit)에 픽셀 단위로 쓰여진다

- 램의 위치는 어드레스 포인터에 의해 지정된다

- 주소 범위는 X=0~131, Y=0~161 이고 범위 밖에 주소는 허용되지 않는다

- 램에 디스플레이 데이터를 쓰기 전에 데이터가 기록될 창(window)이 정의되어야 한다

- 창은 시작 주소를 지정하는 XS, YS와 마지막 주소를 지정하는 XE, YE 커맨드 레지스터를 통해 프로그래밍 할 수 있다

- 예를 들어, 전체 디스플레이 컨텐츠가 기록된다면 윈도우는 다음 값으로 정의 할 수 있다

   XS=0, YS=0, XE=127, YE=161 (128x162)

- 수직 어드레싱 모드(MV=1), Y-주소는 각 바이트 이후에 증가하고 마지막 Y-주소(Y=YE) 이후에 Y는 YS로 랩 어라운드(어드레스 최대 번지 다음에 제로 번지가 연속함)하고, X는 다음 열을 어드레싱하기 위해 증가

- 수평 어드레싱 모드(V=0), X-주소는 각 바이트 이후에 증가하고 마지막 X-주소(X=XE) 이후에 X는 XS로 랩 어라운드하고 Y는 다음 행을 어드레싱하기 위해 증가한다

- 모든 마지막 주소(X=XE, Y=YE) 이후에 어드레스 포인터는 X=XS, Y=YS 주소로 랩 어라운드 한다

- 다양한 디스플레이 아키텍처를 다루기 위한 유연성을 위해 "CASET, RASET, MADCTL" 명령어는 X-주소, Y-주소의 반전을 허용하는 MX, MY 플래그를 정의한다

- MX, MY 그리고 MV(열<->행 교환)가 변경되면 데이터 버스트가 디스플레이 램에 다시 쓰여진다

MADCTL(Memory Data Access Control) 설정에 따른 프레임 데이터 쓰기 방향


[CUBEMX 설정]

SPI 설정
사용할 SPI가 속하는 APB의 클럭 설정

- ST7735에 데이터를 전송하기만 하므로 Trasmit Only Master 로 설정 (Master MOSI -> ST7735 SDA)

- 데이터는 8 bit씩 MSB부터 전송

- 데이터시트 상 SCL Write 주기 최소값은 66ns (15.15Mhz 이하)

- 사용하는 SPI가 속하는 APB의 클럭 속도를 설정한 뒤, SPI 프리스케일러 설정을 통해 SCL 설정

- 16Mhz 설정의 경우, 62.5ns로 최소값 보다는 낮지만 정상 작동 확인

- CPOL=Low(=0), CPHA=1 Edge(=0)

- SCL은 비활성화시 Low를 유지하고 통신 시작시 SCL 첫번째 엣지(Rising)에서 SDA 라인의 데이터가 캡쳐되고 SCL의 두번째 엣지(Falling)에서 캡쳐된 데이터가 출력된다

- 인터럽트와 DMA를 활용해 데이터 전송

- C/S (Chip Select, =SS (Slave Select), D/C (Data/Command), Reset 핀 할당 및 Output 으로 설정

(사용하지 않는 GPIO 핀들 사용)

- C/S 핀은 SPI 통신 비활성화 상태에선 High 상태를 유지하고 통신 때만 Low로 전환해 사용

- D/C 핀은 명령어를 전송할 땐 Low 상태여야하고 명령어 매개변수 or 디스플레이 데이터를 전송할 땐 High 상태여야 한다

- Reset 핀은 H/W 리셋에 사용되는 핀으로 기본적으론 High 상태를 유지하고 있어야 한다


[코드]

(Write 기능들만 구현)

<초기화>

 *기본 설정

전원 인가/하드웨어 리셋/소프트웨어 리셋 직후 디스플레이 기본 설정

- 전원 인가시 디스플레이는

  - Sleep In 상태이므로 Sleep Out 명령어를 전송해 Sleep 모드에서 빠져나온다

  - Display Off 상태이므로 Display On 명령어 전송해 디스플레이를 활성화 시킨다

- 행렬 시작 주소 (0, 0), 마지막 주소 (161, 131) 이므로 디스플레이 창은 (0~161, 0~131)이 된다

 

 *화면 반전

- 사용한 LCD의 경우, 위 사진에서 (0, 0)으로 표시한 부분이 어드레스 포인터의 시작점이다 (행,열은 사진 방향과 동일)

- 80x160 해상도에서 80에 해당하는 부분은 26~105까지의 주소를 사용한다 (반전 설정 상관없이 26~105)

- 160에 해당하는 부분은 1~160까지의 주소를 사용한다 (디스플레이 해상도가 128x160이 아닌 132x162로 추정)

(왼쪽) 기본 설정 (MY=0, MX=0, MV=0) (오른쪽) MY=0, MX=1, MV=1
Memory Data Access Control 레지스터 MV=1, MX=1, MY=0 (행열 교환, 열 주소 순서 반전)

- 행과 열의 방향은 사진과 동일하고 RGB 데이터는 열부터 순차적으로 쓰여진다

- 어드레스 포인터는 열의 마지막 부분에서 열의 시작 지점으로 돌아가고 행 주소는 1 증가한다

- 오른쪽 사진처럼 사용하기 위해 Memory Data Access Control (MADCTL) 레지스터에서 MV 비트를 1로 설정해 행과 열을 서로 바꾸고 MX 비트를 1로 설정해 열 주소 순서를 반전시켰다

 

 *RGB 출력 반전

Memory Data Access Control (MADCTL) 레지스터에서 RGB BIT의 역할
RGB 픽셀 0xFC, 0x00, 0x00 전송 / (왼쪽) MADCTL RGB BIT=0 (오른쪽) MADCTL RGB BIT=1

- 사용한 LCD 모듈의 경우, Memory Data Access Control(MADCTL) 레지스터의 RGB 비트 설정이 0임에도 불구하고 BGR 순으로 반대로 디스플레이에 데이터가 표현됨. 따라서 MADCTL 레지스터의 RGB 순서 설정 비트를 1로 변경함으로 전송한 RGB 데이터 순서대로 디스플레이 출력이 되게끔 설정.

- RGB BIT 설정으로 인해 변경된 RGB 출력 순서는 좌우, 상하 반전을 하더라도 변하지 않는다

 

*디스플레이 반전

Display Inversion On 명령어
RGB 픽셀 0xFC, 0x00, 0x00 전송, MADCTL RGB BIT=1 / (왼쪽) Inversion Off (오른쪽) Inversion On

- 사용한 LCD 모듈의 경우, Inversion Off 일 때 RGB값 0x00에서 최대 밝기, 0xFC에서 최소 밝기가 된다

- 예) R=0xFC, G=0x00, B=0x00 이라면 해당 픽셀은 Red=off / Green, Blue=최대 밝기가 된다

- Inversion On 명령어를 전송해 전송하는 R/G/B 값의 크기가 디스플레이 픽셀 밝기에 그대로 반영되게 한다

 

 *초기화 코드

//Send Command
void st7735_write_cmd(uint8_t cmd)
{
	while(HAL_SPI_GetState(&hspi1)!=HAL_SPI_STATE_READY)
	{

	}

	HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, RESET);

	//Send Command
	HAL_GPIO_WritePin(SPI1_DC_GPIO_Port, SPI1_DC_Pin, RESET);
	if(HAL_SPI_Transmit_IT(&hspi1,&cmd,1)!=HAL_OK)
	{

	}
	while(HAL_SPI_GetState(&hspi1)!=HAL_SPI_STATE_READY)
	{

	}

	HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, SET);
}

//Send Command, Command Parameter, Parameter Size
void st7735_write_cmd_param(uint8_t cmd, uint8_t *parameter, uint8_t param_size)
{
	while(HAL_SPI_GetState(&hspi1)!=HAL_SPI_STATE_READY)
	{

	}

	HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, RESET);

	//Send Command
	HAL_GPIO_WritePin(SPI1_DC_GPIO_Port, SPI1_DC_Pin, RESET);
	if(HAL_SPI_Transmit_IT(&hspi1,&cmd,1)!=HAL_OK)
	{
		//fail
	}
	while(HAL_SPI_GetState(&hspi1)!=HAL_SPI_STATE_READY)
	{

	}

	//Send Command Parameter
	HAL_GPIO_WritePin(SPI1_DC_GPIO_Port, SPI1_DC_Pin, SET);
	if(HAL_SPI_Transmit_IT(&hspi1,parameter,param_size)!=HAL_OK)
	{
		//fail
	}
	while(HAL_SPI_GetState(&hspi1)!=HAL_SPI_STATE_READY)
	{

	}

	HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, SET);
}

//Set Addresss Pointer
void st7735_set_address(uint8_t col_start, uint8_t col_end, uint8_t row_start, uint8_t row_end)
{
	uint8_t addr_pointer[4]={0};

/** Column Address Set
 * 80x160 해상도에서 80인 부분을 열로 사용한다면 열 주소는 26~105까지
 * 				  160인 부분을 열로 사용한다면 열 주소는 1~160까지
 */
	addr_pointer[1]=col_start;
	addr_pointer[3]=col_end;
	st7735_write_cmd_param(0x2A,addr_pointer,4);

/** Row Address Set
 * 80x160 해상도에서 80인 부분을 행으로 사용한다면 행 주소는 26~105까지
 * 				  160인 부분을 행으로 사용한다면 행 주소는 1~160까지
 */
	addr_pointer[1]=row_start;
	addr_pointer[3]=row_end;
	st7735_write_cmd_param(0x2B,addr_pointer,4);
}

//Initialize ST7735 Driver
void st7735_init(void)
{
	uint8_t cmd_param;

//Sleep Out
	st7735_write_cmd(0x11);
	HAL_Delay(120);

//Display On
	st7735_write_cmd(0x29);

/** MADCTL : Memory Data Access Control
 * D7 D6 D5 D4 D3  D2
 * MY MX MV ML RGB MH
 */
 	//Exchange Column, Row + Mirror Column Address Order + Mirror RGB Order
	cmd_param=0x68;
	st7735_write_cmd_param(0x36,&cmd_param,1);

/** Addresss Set
 * Column Start, Column End, Row Start, Row End
 */
	st7735_set_address(1,160,26,105);

//Display Inversion On
	st7735_write_cmd(0x21);
}

- 인터럽트를 이용해 데이터 전송

- st7735_write_cmd(), st7735_write_cmd_param() 함수를 ST7735에 명령어와 명령어+매개변수를 전송하는 기본 툴로 사용

- Sleep Out -> Display On -> Memory Data Access Control (행열 교환 및 열 방향 반전) -> Address Pointer 설정 (Window) -> Display Inversion On 순서로 ST7735 드라이버 초기화 (데이터 시트에 초기화 과정 없기에 임의 설정)

- 열과 행의 주소는 각각 2 byte의 길이를 가지는데 열과 행의 주소 둘 다 255를 넘어가지 않으므로 명령어 매개변수의 두번째, 네번째 배열에 원하는 주소를 입력해 전송

- st7735_write_cmd(), st7735_write_cmd_param() 함수는 우선적으로 SPI 사용 가능 때까지 while()문을 통해 대기

- SPI가 Ready 상태가 되면 CS(Low, SPI 통신 활성화) -> D/C (Data/Command) -> IT 통해 SPI 통신 개시 -> 통신 완료까지 while()문 사용해 대기 -> CS(High, SPI 통신 비활성화)

<폰트>

- https://github.com/ayoy/fontedit 프로그램을 이용해 폰트 헤더 파일 생성

FontEdit 프로그램 설정 (consolas 10pt 16x26)

 *문자 주소 지정 함수

void st7735_set_address_char(uint8_t col_start, uint8_t row_start)
{
	uint8_t command, addr_pointer[4]={0};

	command=0x2A;
	addr_pointer[1]=1+col_start;
	addr_pointer[3]=col_start+FONT_WIDTH;
	st7735_write_cmd_param(command,addr_pointer,4);

	command=0x2B;
	addr_pointer[1]=row_start+26;
	addr_pointer[3]=row_start+26+FONT_HEIGHT+1;
	st7735_write_cmd_param(command,addr_pointer,4);
}

- 문자 배열이 입력될 Window 주소를 전송하는 함수

- ST7735에 전송할 주소 설정 명령어 매개변수 배열 생성([0],[1]=시작 주소, [2],[3]=마지막 주소)

- 전달받은 열 시작 주소 + 폰트 너비를 열의 마지막 주소

- 전달받은 행 시작 주소 + 폰트 높이를 행의 마지막 주소로 설정

- 열 마지막 주소를 시작 주소 + 폰트 너비로 설정할 경우, 열의 마지막 주소 이후에 어드레스 포인터가 다음 행, 열의 시작 주소로 자동 이동되므로 사용하는 폰트 형태 그대로 디스플레이에 표현됨

- 열 시작 주소의 +1은 MADCTL 명령어를 통해 설정된 화면의 열이 1~160까지의 주소를 갖기 때문이고 행 시작 주소의 +26은 화면의 행이 26~105까지의 주소를 갖기 때문

- st7735_write_cmd_param() 함수를 통해 열 주소 설정 명령어+명령어 매개변수(시작,끝 주소), 행 주소 설정 명령어+명령어 매개변수(시작, 끝 주소)를 전송

 

 *Char, String 전송 함수

#include <malloc.h>
#include "fonts.h"

#define ST7735_WIDTH 160
#define ST7735_HEIGHT 80
#define FONT_WIDTH 16
#define FONT_HEIGHT 26

#define ST7735_COLOR_RED 0xFC0000
#define ST7735_COLOR_GREEN 0x00FC00
#define ST7735_COLOR_BLUE 0x0000FC
#define ST7735_COLOR_BLACK 0x000000

void st7735_write_char(uint8_t chr_ascii, uint32_t color, uint8_t col, uint8_t row)
{
	uint8_t *char_array;
	char_array=malloc(sizeof(uint8_t)*(3*(FONT_WIDTH*FONT_HEIGHT)));
	memset(char_array,0,sizeof(uint8_t)*(3*(FONT_WIDTH*FONT_HEIGHT)));
	uint8_t red,green,blue;

	red=color>>16;
	green=color>>8;
	blue=color;

	for(int j=0;j<(FONT_WIDTH*FONT_HEIGHT/8);j++)
	{
		for(int i=0;i<8;i++)
		{
			if(((consolas_10pt[(chr_ascii-32)*(FONT_WIDTH*FONT_HEIGHT/8)+j])>>(7-i))&0x01)
			{
				char_array[j*24+i*3]=red;
				char_array[j*24+i*3+1]=green;
				char_array[j*24+i*3+2]=blue;
			}
		}
	}

	st7735_set_address_char(col, row);

	//Memory Write
	st7735_write_cmd(0x2C);

	//Send RGB Pixel Data
	HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, RESET);
	HAL_GPIO_WritePin(SPI1_DC_GPIO_Port, SPI1_DC_Pin, SET);
	if(HAL_SPI_Transmit_DMA(&hspi1,char_array,3*(FONT_WIDTH*FONT_HEIGHT))!=HAL_OK)
	{

	}
}

- 문자 아스키 코드값, 폰트 색상, 열 시작 주소, 행 시작 주소를 전달 받아 사용

- 하나의 문자를 표현하는데 필요한 픽셀 갯수(폰트 가로*세로) *3 (RGB)만큼 malloc 함수를 사용해 메모리 할당

- memset() 함수를 사용해 할당된 메모리를 0으로 초기화

- DMA를 이용해 디스플레이 데이터를 전송하는데 지역 변수를 사용할 경우, DMA를 통한 디스플레이 데이터 전송 도중에 st7735_write_char()가 종료되면서 지역 변수가 소멸되어 쓰레기값이 전송된다

- 따라서 malloc() 함수를 통해 전송할 디스플레이 데이터만큼의 메모리를 할당하고 DMA 전송이 완료된 이후에 free() 함수를 사용해 할당된 메모리를 해제한다

- 사용한 폰트 데이터는 하나의 픽셀이 0,1의 값만을 갖고 있는 형태이므로 한 픽셀에 RGB 3바이트 데이터가 들어있는 형태로 변환해야 한다

- (전달받은 아스키 코드값 - 32) * (폰트 가로 너비 * 폰트 세로 높이 / 8) 식을 통해 폰트 배열의 시작점을 찾는다

(/8은 하나의 폰트는 폰트 가로 너비*폰트 세로 높이 만큼의 픽셀을 가지고 있으면서 1 바이트 단위로 구성된 배열을 가지고 있기 때문 (1pixel=1bit), ex)16*26 픽셀을 사용하는 폰트는 416 픽셀을 사용하고 이는 52byte 데이터로 저장된다)

- 정의된 매크로 색상은 24bit로 MSB부터 8bit씩 Red, Green, Blue 색상 데이터를 나타낸다

- 전달받은 색상을 비트 시프트를 이용해 uint8_t red, green, blue 변수에 각각 저장한 뒤 픽셀의 RGB값을 입력하는데 사용한다

- 폰트 배열을 1 byte 단위로 MSB부터 비트 시프트를 사용해 픽셀이 유효한지 확인하고 유효한 데이터(1)을 가지고 있을 경우 전달 받은 색상을 앞서 malloc() 함수를 통해 할당된 메모리에 RGB 각 1바이트씩 순차적으로 저장해 나간다

(하나의 픽셀이 RGB 3byte 데이터를 가지므로 폰트 1픽셀 데이터 -> ST7735 1픽셀 RGB 3byte 데이터)

- 1 byte에 대한 분석이 끝나면 폰트 배열 주소를 1(1byte) 증가 시킨 뒤 분석 및 RGB 변환 저장 과정 반복

- RGB 데이터 저장 완료 이후에 전달받은 col, row 값을 사용해 문자가 입력될 주소 지정

- ST7735 디스플레이 데이터 램에 데이터를 전송하기 위해 Memory Write (0x2C) 명령어를 전송 (필수)

- CS(Low), D/C(High(Data))로 설정한 뒤 DMA를 이용해 디스플레이 데이터 전송

(마지막에 CS(High) 설정 코드를 넣을 경우, DMA 전송 중에 CS핀이 High가 되어 통신이 중단된다)

 

 *문자열 전송 함수

void st7735_write_string(char *string, uint32_t font_color, uint8_t col_start, uint8_t row_start)
{
	while(*string)
	{
		st7735_write_char(*string,font_color,col_start,row_start);

		if(col_start < 160-FONT_WIDTH)
		{
			col_start+=FONT_WIDTH;
			string++;
		}
		else
		{
			break;
		}
	}
}

- 전달받은 문자열의 첫번째 문자부터 순차적으로 하나씩 st7735_write_char() 함수에 전달

- 매 전송 이후, 폰트 가로 너비만큼 열 주소를 증가

- 열의 남은 공간이 폰트의 가로 너비 보다 작을 경우 전송 중단

(필요한 경우 else 란에 row_start+폰트 높이, col_start=0 을 입력해 다음 행의 0번째 열부터 나머지 문자가 출력되게끔 설정)

 

 *stm32wbxx_it.c

stm32wbxx_it.c 위치 (CUBEMX 기준)

void DMA1_Channel1_IRQHandler(void)
{
  /* USER CODE BEGIN DMA1_Channel1_IRQn 0 */

  /* USER CODE END DMA1_Channel1_IRQn 0 */
  HAL_DMA_IRQHandler(&hdma_spi1_tx);
  /* USER CODE BEGIN DMA1_Channel1_IRQn 1 */
  
  if(HAL_DMA_GetState(&hdma_spi1_tx)==HAL_DMA_STATE_READY)
  {
	  HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, SET);
	  free(hspi1.pTxBuffPtr);
  }
  
  /* USER CODE END DMA1_Channel1_IRQn 1 */
}

- stm32wbxx_it.c 소스 파일에 위치한 DMA 인터럽트 핸들러

- 사용한 SPI의 TX에 할당한 DMA 채널을 찾아 코드 입력

- DMA를 통한 전송이 완료되어 DMA 채널이 Ready 상태라면 CS 핀을 High (SPI 통신 비활성화)로 설정하고 전송에 사용된 디스플레이 데이터의 메모리 할당을 해제함

 

 *활용 예

void st7735_fill_screen(uint32_t color)
{
	while(HAL_SPI_GetState(&hspi1)!=HAL_SPI_STATE_READY)
	{

	}
	uint8_t *display_buffer;
	display_buffer=malloc(sizeof(uint8_t)*(3*ST7735_WIDTH*ST7735_HEIGHT));
	memset(display_buffer,0,sizeof(uint8_t)*(3*ST7735_WIDTH*ST7735_HEIGHT));

	for(int i=0; i<ST7735_WIDTH*ST7735_HEIGHT; i++)
	{
		display_buffer[i*3]=color>>16;
		display_buffer[i*3+1]=color>>8;
		display_buffer[i*3+2]=color;
	}

	st7735_set_address(1,160,26,105);

	//Memory Write
	st7735_write_cmd(0x2C);

	//Display data transfer
	HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, RESET);
	HAL_GPIO_WritePin(SPI1_DC_GPIO_Port, SPI1_DC_Pin, SET);

	if(HAL_SPI_Transmit_DMA(&hspi1,display_buffer,3*ST7735_WIDTH*ST7735_HEIGHT)!=HAL_OK)
	{

	}
}

void st7735_example(void)
{
	uint8_t st_col=0, st_row=0;

	st7735_fill_screen(ST7735_COLOR_BLACK);
	HAL_Delay(1000);
	st7735_fill_screen(ST7735_COLOR_RED);
	HAL_Delay(1000);
	st7735_fill_screen(ST7735_COLOR_GREEN);
	HAL_Delay(1000);
	st7735_fill_screen(ST7735_COLOR_BLUE);
	HAL_Delay(1000);
	st7735_fill_screen(ST7735_COLOR_BLACK);
	HAL_Delay(1000);

	st7735_write_string("Hello",ST7735_COLOR_RED,0,0);
	HAL_Delay(1000);
	st7735_write_string("Hello",ST7735_COLOR_GREEN,0,FONT_HEIGHT);
	HAL_Delay(1000);
	st7735_write_string("Hello",ST7735_COLOR_BLUE,0,FONT_HEIGHT*2);
	HAL_Delay(1000);
    
	char font_test=' ';
	for(int i=' ';i<='~';i++)
	{
	  st7735_write_char(font_test,ST7735_COLOR_RED,st_col,st_row);
	  font_test++;
	  HAL_Delay(100);
	  st_col+=FONT_WIDTH;
	  if(160<=st_col)
	  {
		  st_col=0;
		  if(st_row<52)
		  {
			  st_row+=FONT_HEIGHT;
		  }
		  else
		  {
			  st_row=0;
		  }
	  }
	}
	font_test=' ';
	for(int i=' ';i<='~';i++)
	{
	  st7735_write_char(font_test,ST7735_COLOR_GREEN,st_col,st_row);
	  font_test++;
	  HAL_Delay(100);
	  st_col+=FONT_WIDTH;
	  if(160<=st_col)
	  {
		  st_col=0;
		  if(st_row<52)
		  {
			  st_row+=FONT_HEIGHT;
		  }
		  else
		  {
			  st_row=0;
		  }
	  }
	}
	font_test=' ';
	for(int i=' ';i<='~';i++)
	{
	  st7735_write_char(font_test,ST7735_COLOR_BLUE,st_col,st_row);
	  font_test++;
	  HAL_Delay(100);
	  st_col+=FONT_WIDTH;
	  if(160<=st_col)
	  {
		  st_col=0;
		  if(st_row<52)
		  {
			  st_row+=FONT_HEIGHT;
		  }
		  else
		  {
			  st_row=0;
		  }
	  }
	}
}

- 1초 간격으로 화면 전체를 검은색 -> 빨간색 -> 초록색 -> 파란색 -> 검은색으로 채움

- (0,0)부터 "Hello" 문자열 빨간색 폰트 출력

- 1초 딜레이 이후, (0, 폰트 높이 (=26))부터 "Hello" 문자열 초록색으로 출력

- 1초 딜레이 이후, (0, 폰트 높이*2 (=52))부터 "Hello" 문자열 초록색으로 출력

- 화면의 (0, 0)부터 32번째 아스키 코드인 ' '(Space)부터 126번째 '~'까지 100ms 간격, 빨간색 폰트로 연속 출력

- 이후 초록색, 파란색 순서로 위의 과정 반복

 

 

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