- TI bq21040 Single-Input, Single Cell Li-Ion and Li-Pol Battery Charger

- 4.2V 리튬-이온, 리튬-폴리머 배터리 차저

- 작동 전압 : 4.45~6.45

- 외부 저항 통해 최대 800mA까지 충전 전류 설정 가능 (최소 50mA)

- 패지키 : SOT-23

- Micro USB 커넥터 사용

- 정션 온도 초과를 방지하기 위해 방열판 사용

- 참고

  - bq21040 데이터 시트 : 

  https://www.ti.com/lit/ds/symlink/bq21040.pdf?ts=1596254009516&ref_url=https%253A%252F%252Fwww.google.com%252F

- CNC 활용 보드 제작 (알리익스프레스에서 판매 중인 CNC3018 사용)

- 사용 프로그램 : KiCAD (회로 및 아트웍), FlatCAM (G-Code 생성), bCNC (CNC 제어)


[최대 정격 절대값]

- 입력 전압 : -0.3~30V

- 출력 전압 : -0.3~7V

- 입력 최대 전류 : 1.25A

- 출력 최대 전류 : 1.25A

- 출력 싱크 전류 : 15mA (CHG에 연결된 LED에 흐르는 전류)

- 정션 온도 : -40~150℃
- 보관 온도 : -65~150

- 최대 정격 절대값 이상의 스트레스는 장치에 영구적인 데미지를 줄 수 있음

 

[권장 작동 조건]

 

- 작동 전압 : VDPM(Adaptor low input voltage protection), VOVP(Input Overvoltage Protection)으로 인해 4.45~6.45V

- 입력 및 출력 최대 전류 : 0.8A

- 정션 온도 : 0~125℃ 

- Fast-charge 전류 프로그래밍 레지스터 : 0.675~10.8k (배터리 충전 전류(IOUT) 설정에 사용)

- TS 사용 안 할 경우, TS핀과 VSS 사이에 10k 이상의 레지스터 연결

 

[온도 정보]

 

- RθJA : 전력 손실 1W 당 정션 온도 130.8℃ 상승

- 전력 손실 P = [V(IN) – V(OUT)] × I(OUT) + [V(OUT) – V(BAT)] × I(BAT)  (데이터시트 p.23)

- 예) VIN=5V, VOUT=4.2 IOUT=500mA, IBAT= 498.48mA (I OUT(500mA) - I TS(0.42mA) - I CHG(1.1mA)), 배터리 3.4V

  - [5 - 4.2] x 0.5 + [4.2 - 3.4] x 0.499 = 0.799W 

  - 104.51℃ 증가, 주변 온도 30℃라면 30+104.51=134.51℃ (정션 온도)

- (R TS - 10k, R CHG - 1K, LED - 3.1V)

- SMD 타입의 경우, 다양한 열 전도 경로와 PCB 설계와의 관계가 더 높아서 저항 매개변수를 사용해 정션 온도를 측정하면 잘못된 결과를 초래할 수 있다

- 보드 유형, 크기, 층, 동박 두께, 비아 수 등과 같은 PCB 설계 차이가 최종 열 성능에 큰 영향을 미친다

- 참고 : https://www.ti.com/lit/an/slua844b/slua844b.pdf?ts=1596342730994&ref_url=https%253A%252F%252Fwww.google.com%252F

 

[작동]

TI bq21040 Charging Profile With Thermal Regulation (데이터시트 p.10)

- 차저는 세가지 충전 단계를 갖는다
- 완방된 배터리를 복구하기 위한 프리차지, 벅 차지를 안전하게 공급하는 패스트차지 정전류, 

  안전하게 풀 용량에 도달하기 위한 볼티지 레귤레이션

- 차저는 온도 센싱 스탠다드, 오버 볼티지 프로텍션(OVP), DPM-IN, 세이프티 타이머, ISET 쇼트 보호 등의 안전 기능 세트를 가지고 있다
- 만약 배터리 전압이 LOWV 쓰레스홀드(2.5V) 보다 낮다면 배터리는 방전된 것으로 여겨지고 프리컨디셔닝 사이클이 시작된다. 이 페이즈에서 배터리로 흘러가는 전류량은 프리 차지 전류라고 불리고 패스트 차지 전류의 20%로 고정되어 있다
- 배터리 전압이 VLOWV 쓰레스홀드까지 충전되면 패스트차지가 시작되고 패스트차지 전류가 적용된다

- 패스트차지 정전류는 ISET 터미널을 사용해 프로그램된다
- 충전의 대부분은 정전류에 의해 이뤄진다
- IC내의 전력 소모는 배터리 전압이 최저일 때 극대가 된다
- 만약 IC가 125℃에 도달해 IC가 써멀 레귤레이션에 들어가면 타이머 클럭이 절반으로 느려지고 온도를 더 이상 상승시키지 않기 위해 필요한만큼 충전 전류를 줄인다
- 배터리 셀이 레귤레이션 전압만큼 충전되면 전압 루프 제어를 취하고 전류가 종단 쓰레스홀드에 가까워질 때까지 배터리를 레귤레이션 전압으로 유지한다
- 종단 전류는 패스트차지 전류의 10%로 설정된다
- CHG 터미널은 첫번째 충전 사이클일 때만 LOW (LED ON)이고 충전 전류에 대한 종단이 활성, 비활성이든 관계없이 종단 쓰레스홀드에 도달하면 턴 오프된다
- (데이터시트 p.8 내용 발췌)

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 간격, 빨간색 폰트로 연속 출력

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

 

 

+ Recent posts