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

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

 

 

[목적]

- TCS34725 센서를 통해 측정된 RGB값을 명도(Perceived Lightness)로 변환해 원두의 로스팅 정도를 파악

(변환 공식 : https://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color - Myndex)

- 실제 원두용 색도계는 근적외선 파장(NIR)을 측정

 

[실험 조건]

- 홀빈 상태와 분쇄 원두, 두 경우를 나누어 측정

- 홀빈, 분쇄 원두 표면과 센서와의 거리는 약 10mm

- A4 용지를 흰색 기준점으로 삼아 RGB 255,255,255 값을 갖는 최소한의 Timing , Gain 값으로 센서 레지스터 설정

- 아그트론 측정값을 갖는 원두를 테스트용으로 사용

- 홀빈 상태의 원두는 측정값 편차로 10회 측정 후 평균값 이용

- 분쇄 원두는 에스프레소 분쇄도, 측정 편차가 발생하지 않으므로 1회만 측정

 

[테스트 과정 중 문제점]

- 홀빈 상태의 경우, 일관된 RGB 값을 측정하는 것은 불가했음

(타원형인 원두의 모양으로 인해 센서와 측정되는 부분의 거리를 일정하게 유지할 수 없는 점과 원두 표면과 센터컷 사이의 색상 편차 때문으로 추정)

- 따라서, 측정 때마다 케이스에 담은 원두를 재정렬 및 평탄화 과정을 거친 뒤 10회 측정후 평균값을 구함

(아그트론 넘버를 사용하는 몇몇 원두용 색도계에서도 홀빈 상태에서의 측정값 편차가 발생한다고 함)

 

[테스트 대상]

- 측정 원두 : 아그트론 넘버 홀빈(59.64), 분쇄(61.02) (아그트론 넘버 낮을 수록 어두운 색상)

 

[실험 과정]

<A4 용지 색상 측정>

Timing 값 설정

- A4 용지를 기준으로 RGB 255, 255, 255 값을 갖는 최소한의 Timing, Gain 값을 찾아서 설정

 

<홀빈 색상 측정>

케이스에 최대한 평평하게 채운 뒤 측정

1회 2회 3회 4 5회 6회 7회 8회 9회 10회
37.46 37.73 37.55 37.55 37.95 37.64 37.63 37.64 37.84 37.84

(측정된 명도값은 소수점 세번째 자리에서 반올림)

- 평균 : 37.68

 

<분쇄 원두 색상 측정>

- 1회 측정값 : 37.77

 

[결론]

- 홀빈 색상 측정할 때, 센서와 측정되는 홀빈의 거리를 최대한 일정하게 유지해 측정값의 편차를 최소화하는게 중요

(최대한 홀빈을 평평하게 정렬)

- 홀빈 측정 결과들간의 오차가 있기 때문에 다회차 측정은 피할 수 없는 상황

- 홀빈 10회 측정 평균과 분쇄 원두 측정값의 차이는 0.09

- 너무 작은 차이로 인해 실제 홀빈 및 분쇄 원두 색상 측정용으로 사용하기에는 좀 어려워보이는데 확실한 결론을 위해선 더 다양한 데이터를 누적 시킬 필요가 보임

- 만약, 누적된 데이터를 통해 실제 사용이 유효하다 판단된다면

- 로스팅 단계에 따른 색상 기준점이 없기 때문에 아그트론 값을 가진 원두의 명도 측정값과 비교해 기준점을 만든다거나 누적된 데이터(로스팅 시간과 수분 증발율에 따른 명도값)를 기반으로 나름의 기준점 작성

- 홀빈과 분쇄 원두 사이의 명도 차이를 통해 언더 디벨롭 여부 파악 가능할듯

/** 20.06.09 수정

 * 1. tcs_read_all_reg_thread() 함수 수정 및 본문에 추가 (static 구조체 변수 -> pvPortMalloc 메모리 할당으로 변경)

 * 2. tcs34725_cmd_func() 함수 수정 (static 구조체 변수 -> pvPortMalloc 메모리 할당으로 변경 및 자잘한 수정)

 * 3. tcs34725_read_threshold() 함수 수정 (단순 LOW, HIGH 선택 방법에서 구조체 포인터 변수 활용 방식으로 변경)

 * 4. tcs34725_read_reg_cb(), tcs34725_read_thr_cb() 함수 수정 (vPortFree() 추가)

 */

 

[nRF52840, TWI를 사용한 TCS34725 제어]

https://rs29.tistory.com/18

 

nRF52 - TWI 이용 TCS34725 RGB 색상 검출 센서

- 사용 보드 : nRF52840-PDK - 사용 IDE : SEGGER Embedded Studio for ARM (노르딕 제품 사용하는 경우 무료 라이센스 이용 가능) - 작동 전압 : 2.7~3.6V (사용 모듈의 경우, 3.3~5.0V) - 검출 색상 : Red, Gre..

rs29.tistory.com

 

[개발 환경]

- 개발 보드 : nRF52840-PDK

- IDE : SEGGER Embedded Studio for ARM

- 센서 통신 : TWI (I2C) 사용

- 무선 통신 : BLE (UART 사용)

- SDK 예제 중 twi_master_using_nrf_twi_mngr(TWI 통신), ble_app_uart(BLE+UART 송수신), ble_app_hrs_freertos(FreeRTOS) 를 기반으로 작성

- BLE, UART의 설정 대부분은 ble_app_uart 예제 그대로 사용

- BLE 통신을 통해 명령어를 수신 및 실행하거나 RGBC 측정값과 레지스터 정보를 전송

- 참고한 사이트 : https://www.freertos.org/a00106.html


<기본 작동>

[RGBC 측정값 전송]

- RGBC 레지스터에 저장된 측정값을 읽어 오는 태스크가 vTaskDelay() 간격으로 상시 실행

- RGBC 측정값 수신 완료 후, 읽어 온 RGBC 값을 다룰 콜백 함수가 호출됨

- 콜백 함수에서 RGBC 값을 가공한 뒤에 큐에 등록하고 RGBC 값을 BLE를 통해 전송하는 태스크에 xTaskNotify() 함수를 사용해 알림 전달

- RGBC 데이터 BLE 전송 태스크에서 알림 수신 및 큐에 등록된 RGBC 데이터값을 받아와 BLE 통신을 통해 RGBC값을 전송

 

[명령어 실행]

- BLE 통신을 통해 데이터를 수신하면 명령어 부분과 데이터 부분으로 나눈 뒤에 큐에 등록 및 명령어를 처리하는 태스크에 알림 전달

- 명령어 처리 태스크는 알림 및 큐에 등록된 명령어 데이터를 받아와 명령어와 일치하는 TCS34725 레지스터에 액세스해 명령어 함께 수신된 데이터를 입력하고 다시 해당 레지스터에 액세스해 저장된 데이터를 읽어 옴 (데이터 입력 확인)

- 레지스터로부터 데이터를 읽어 온 직후, 콜백 함수가 호출되고 이 콜백 함수에서 데이터를 가공해 큐에 등록 및 BLE 통신을 통해 레지스터 데이터를 전송하는 태스크에 알림 전달

- 레지스터 데이터를 전송하는 태스크에서 알림 수신 및 큐에 등록된 데이터를 받아와 BLE 통신을 통해 전송


[코드]

 <TCS34725 RGBC Read 태스크>

static void tcs_read_rgbc_thread(void *arg)
{
    //스택 사이즈 확인용. 할당된 스택 크기에서 사용되고 남은 크기를 알려준다
    #ifdef STACK_SIZE_CHK
    configSTACK_DEPTH_TYPE uxHighWaterMark2;
    uxHighWaterMark2=uxTaskGetStackHighWaterMark(NULL);
    uint8_t stack_left=255;
    #endif

	//TCS34725로부터 읽어 온 RGBC 데이터를 저장할 구조체 변수
    tcs34725_rgbc_data_t tcs_rgbc_thread;
    
    while(1)
    {
    	//TCS34725 RGBC 레지스터에 접근해 저장된 측정값을 읽어 오는 함수 호출
        //측정값 수신 완료 후, 콜백 함수 호출
        tcs34725_read_rgbc(&tcs34725_instance,&tcs_rgbc_thread,tcs34725_rgbc_cb);
        
        vTaskDelay(200);

        //잔여 스택 크기를 지속적으로 갱신
        #ifdef STACK_SIZE_CHK
        uxHighWaterMark2=uxTaskGetStackHighWaterMark(NULL);
        if(uxHighWaterMark2<stack_left)
        {
            stack_left=uxHighWaterMark2;
            printf("Available stack size of RGBC read thread : %d\r\n",uxHighWaterMark2);
        }
        #endif
    }
}

<TCS34725 RGBC 콜백 함수>

void tcs34725_rgbc_cb(ret_code_t result, tcs34725_rgbc_data_t * p_raw_data)
{
    if(result!=NRF_SUCCESS)
    {
        NRF_LOG_INFO("tcs rgbc callback failed");
        return;
    }
    
    //가공한 RGB 데이터를 저장할 구조체 변수
    tcs34725_rgbc_data_t tcs_rgbc_cb_str;
    
    //읽어 온 RGB 데이터 가공 후, 선언한 구조체 변수에 저장
    tcs_rgbc_cb_str.clear=p_raw_data->clear;
    tcs_rgbc_cb_str.red=(int)((double)p_raw_data->red/p_raw_data->clear*255);
    tcs_rgbc_cb_str.green=(int)((double)p_raw_data->green/p_raw_data->clear*255);
    tcs_rgbc_cb_str.blue=(int)((double)p_raw_data->blue/p_raw_data->clear*255);

    //가공한 RGBC 데이터를 저장한 구조체 변수를 큐에 전송
    //큐에 남은 자리가 있다면 RGBC 구조체 변수 큐에 등록 후 BLE 전송 태스크에 알림 전달
    if(uxQueueSpacesAvailable(m_tcs_rgb_data_queue)!=0)
    {
        if(pdTRUE!=xQueueSend(m_tcs_rgb_data_queue, &tcs_rgbc_cb_str, 10))
        {
            NRF_LOG_INFO("TCS34725 RGBC CB : Queue send fail\r\n");
        }
        xTaskNotifyGive(m_ble_tcs_rgbc_send_thread);
    }
    //큐가 Full 이라면 큐에 RGBC 구조체 변수를 덮어쓰기 하고 알림 전달 X
    else
    {
        if(pdTRUE!=xQueueOverwrite(m_tcs_rgb_data_queue, &tcs_rgbc_cb_str))
        {
            NRF_LOG_INFO("TCS34725 RGBC CB : Queue overwrite fail\r\n");
        }
    }
    
    //BLE 통신을 통해 RGBC 데이터를 전송하는 태스크에 알림 전달
    xTaskNotifyGive(m_ble_tcs_rgbc_send_thread);
}

<TCS34725 RGBC BLE 전송 태스크>

static void ble_tcs_rgbc_send_thread(void *arg)
{
    ret_code_t err_code;
    
    //큐에 등록된 RGBC 구조체 변수 데이터를 받아 올 RGBC 구조체 변수 선언
    tcs34725_rgbc_data_t ble_tcs_send_rgb;
    
    //BLE 통신을 통해 전송될 문자열을 저장할 캐릭터 배열
    char data_array[18]={};
    //BLE 통신을 통해 전송될 문자열의 길이를 저장할 변수
    uint16_t length=sizeof(data_array);

    while(1)
    {
    	/*
    	전달 받은 알림이 있는지 확인 (알림이 없다면 0, 있다면 !=0) 
        && 큐에서 BLE 통신을 통해 전송할 RGBC 구조체 변수 읽어 옴
        */
        if((ulTaskNotifyTake(pdTRUE,10)!=0)&&
          (pdPASS==xQueueReceive(m_tcs_rgb_data_queue, &ble_tcs_send_rgb, 10)))
        {	
            //sprintf 함수를 통해 BLE 통신을 통해 전송될 문자열 생성
            //RGB((3)명령어)+Clear(5)+Red(3)+Green(3)+Blue(3)=17+"\n"
            sprintf(data_array,"RGB%5d%3d%3d%3d",ble_tcs_send_rgb.clear,ble_tcs_send_rgb.red,ble_tcs_send_rgb.green,
                    ble_tcs_send_rgb.blue);
            if(m_conn_handle!=BLE_CONN_HANDLE_INVALID)
            {
                err_code=ble_nus_data_send(&m_nus, data_array, &length, m_conn_handle);
                if ((err_code != NRF_ERROR_INVALID_STATE) &&
                    (err_code != NRF_ERROR_RESOURCES) &&
                    (err_code != NRF_ERROR_NOT_FOUND))
                {
                    APP_ERROR_CHECK(err_code);
                }
            }
        }
    }
}

<BLE 통신을 통해 수신한 UART 데이터 처리 함수>

/*
- BLE 통신을 통해 수신된 문자열 (명령어(3)+레지스터 데이터(3) or 쓰레스홀드 데이터(5))을
  저장하기 위한 구조체
*/
typedef struct{
    char cmd[4],data[6];
}tcs34725_cmd_t;

static void nus_data_handler(ble_nus_evt_t * p_evt)
{
    if (p_evt->type == BLE_NUS_EVT_RX_DATA)
    {
        uint32_t err_code;
        
        //수신한 문자열을 명령어, 데이터 부분으로 나누어 저장할 구조체 변수 선언
        tcs34725_cmd_t nus_cmd_str={0};

        NRF_LOG_DEBUG("Received data from BLE NUS. Writing data on UART.");
        NRF_LOG_HEXDUMP_DEBUG(p_evt->params.rx_data.p_data, p_evt->params.rx_data.length);

        //수신한 문자열 UART 출력
        for (uint32_t i = 0; i < p_evt->params.rx_data.length; i++)
        {
            do
            {
                err_code = app_uart_put(p_evt->params.rx_data.p_data[i]);

                if ((err_code != NRF_SUCCESS) && (err_code != NRF_ERROR_BUSY))
                {
                    NRF_LOG_ERROR("Failed receiving NUS message. Error 0x%x. ", err_code);
                    APP_ERROR_CHECK(err_code);
                }
            } while (err_code == NRF_ERROR_BUSY);
        }
        if (p_evt->params.rx_data.p_data[p_evt->params.rx_data.length - 1] == '\r')
        {
            while (app_uart_put('\n') == NRF_ERROR_BUSY);
        }

        //수신한 문자열의 1~3번째 문자를 구조체 변수의 명령어 부분에 저장
        memcpy(nus_cmd_str.cmd, p_evt->params.rx_data.p_data, 3);

        /*
        일반 레지스터 설정은 최대 255까지 입력 가능하므로 세 자리의 문자 길이,
        쓰레스홀드는 최대 65535까지 입력 가능하므로 다섯 자리의 문자 길이를 갖는다
        */
        
        /*
        - 일반 레지스터 설정
        수신한 문자열의 4~6번째 문자를 구조체 변수의 데이터 부분에 저장
        */
        if((4<p_evt->params.rx_data.length)&&(p_evt->params.rx_data.length<8))
        {
            memcpy(nus_cmd_str.data, &p_evt->params.rx_data.p_data[3], 3);
        }
        /*
        - 쓰레스홀드 설정
        수신한 문자열의 4~8번째 문자를 구조체 변수의 데이터 부분에 저장
        */
        else if(7<p_evt->params.rx_data.length)
        {
            memcpy(nus_cmd_str.data,&p_evt->params.rx_data.p_data[3],5);
        }

        //큐 잔여량을 확인해 잔여 상황에 따라 큐 전송을 다르게 처리함
        if(uxQueueSpacesAvailable(m_tcs_cmd_queue)!=0)
        {
            //큐에 남은 공간이 있을 경우, 구조체 변수를 큐에 등록 후 명령어 처리 태스크에 알림 전달
            if(pdPASS!=xQueueSend(m_tcs_cmd_queue,&nus_cmd_str,10))
            {
                NRF_LOG_INFO("NUS DATA HANLDER : Queue send fail");
            }
            xTaskNotifyGive(m_tcs_wr_reg_thread);
        }
        else
        {
            //큐에 남은 공간이 없는 경우, 덮어쓰기로 큐에 구조체 변수를 등록하고 알림은 전달하지 않음        
            if(pdPASS!=xQueueOverwrite(m_tcs_cmd_queue,&nus_cmd_str));
            {
                NRF_LOG_INFO("NUS DATA HANLDER : Queue overwrite fail");
            }
        }
    }
}

- SDK - BLE_APP_UART 예제에 나와있는 함수를 사용

- 큐에 남은 공간이 없을 때 구조체 변수를 덮어쓰기하면서 알림을 전달하지 않는 이유는 기존에 등록된 큐에 새로운 데이터를 덮어쓰는 행동은 처리해야 할 큐의 양을 증가 시키는 것이 아니기 때문

 

<수신 명령어 처리 태스크 및 함수>

static void tcs_wr_reg_thread(void *arg)
{
    ret_code_t err_code;
    tcs34725_cmd_t wr_cmd_str;

    while(1)
    {
    	/*
        전달된 알림이 있고 큐에 등록된 데이터를 성공적으로 읽어 왔다면 
        TCS34725 명령어 처리 함수에 읽어 온 명령어 구조체 변수 전달
        */
        if((ulTaskNotifyTake(pdTRUE,10)!=0)&&(pdPASS==xQueueReceive(m_tcs_cmd_queue,&wr_cmd_str,10)))
        {
            tcs34725_cmd_func(&wr_cmd_str);
        }
        vTaskDelay(100);
    }
}

/*
전달된 명령어에 따라 해당 레지스터에 액세스해 명령어와 함께 수신된 데이터를 입력하고
다시 해당 레지스터에 액세스해 설정된 데이터를 읽어 온 뒤 BLE 통신을 통해 전송
*/
void tcs34725_cmd_func(tcs34725_cmd_t *cmd_func_str)
{
	ret_code_t err_code;
	
    //수신한 명령어가 쓰레스홀드 명령어일 경우
    if((strcmp(cmd_func_str->cmd,"THL")==0)||(strcmp(cmd_func_str->cmd,"THH")==0))
    {
        //TCS34725로부터 쓰레스홀드 설정값을 수신할 쓰레스홀드 구조체 포인터 변수 메모리 할당
        tcs34725_threshold_data_t *tcs_cmd_thr=(tcs34725_threshold_data_t*)pvPortMalloc(sizeof(tcs34725_threshold_data_t));

        //쓰레스홀드 하한 설정
        if(strcmp(cmd_func_str->cmd,"THL")==0)
        {
            NRF_LOG_INFO("Set Threshold Low");
            err_code=tcs34725_set_threshold(&tcs34725_instance,TCS34725_THRESHOLD_LOW,chartoint(cmd_func_str->data,5));
            if(err_code!=NRF_SUCCESS)
            {
                NRF_LOG_INFO("Set Threshold Low fail");
                return;
            }
            tcs_cmd_thr->reg_addr=TCS34725_REG_THRESHOLD_LOW_L;
            err_code=tcs34725_read_threshold(&tcs34725_instance, tcs_cmd_thr, tcs34725_read_thr_cb);
            if(err_code!=NRF_SUCCESS)
            {
                NRF_LOG_INFO("Read Threshold Low fail");
                return;
            }
        }
        //쓰레스홀드 상한 설정
        else if(strcmp(cmd_func_str->cmd,"THH")==0)
        {
            NRF_LOG_INFO("Set Threshold High");
            err_code=tcs34725_set_threshold(&tcs34725_instance,TCS34725_THRESHOLD_HIGH,chartoint(cmd_func_str->data,5));
            if(err_code!=NRF_SUCCESS)
            {
                NRF_LOG_INFO("Set Threshold High fail");
                return;
            }
            tcs_cmd_thr->reg_addr=TCS34725_REG_THRESHOLD_HIGH_L;
            err_code=tcs34725_read_threshold(&tcs34725_instance, tcs_cmd_thr, tcs34725_read_thr_cb);
            if(err_code!=NRF_SUCCESS)
            {
                NRF_LOG_INFO("Read Threshold High fail");
                return;
            }
        }
    }
    //쓰레스홀드 외에 명령어일 때
    else
    {
        //TCS34725 레지스터값을 수신할 구조체 포이터 변수 메모리 할당
        tcs34725_reg_data_t *tcs_cmd_str=(tcs34725_reg_data_t*)pvPortMalloc(sizeof(tcs34725_reg_data_t));
        
        //TCS34725 각 레지스터에 설정된 데이터를 읽어 온 후 BLE 통신을 통해 전송하는 명령어
        if(strcmp(cmd_func_str->cmd,"RAR")==0)
        {
            /*    
            TCS34725의 각종 레지스터에 설정된 데이터를 읽어오는 태스크 생성 (ex:인터럽트 활성화 여부, 쓰레스홀드값 등등)
            읽어 온 데이터는 콜백 함수를 통해 가공된 후 BLE 통신을 통해 전송
            */
            if(pdPASS!=xTaskCreate(tcs_read_all_reg_thread, "TCS_READ_ALL_REG", configMINIMAL_STACK_SIZE+30,
                           NULL, 3, &m_tcs_reg_all_send_thread))
            {
                APP_ERROR_HANDLER(NRF_ERROR_NO_MEM);
            }
        }
        //타이밍값 설정
        else if(strcmp(cmd_func_str->cmd,"TIM")==0)
        {
            NRF_LOG_INFO("Set Timming");
            err_code=tcs34725_set_timing(&tcs34725_instance,chartoint(cmd_func_str->data,3));
            if(err_code!=NRF_SUCCESS)
            {
                NRF_LOG_INFO("Set timing fail");
                return;
            }
            tcs_cmd_str->reg_addr=TCS34725_REG_TIMING;
            err_code=tcs34725_read_reg(&tcs34725_instance,tcs_cmd_str,tcs34725_read_reg_cb);
            if(err_code!=NRF_SUCCESS)
            {
                NRF_LOG_INFO("Read timing fail");
                return;
            }
        }
        //대기 시간 설정
        else if(strcmp(cmd_func_str->cmd,"WAT")==0)
        {
            NRF_LOG_INFO("Set Wait Time");
            err_code=tcs34725_set_wait_time(&tcs34725_instance,chartoint(cmd_func_str->data,3));
            if(err_code!=NRF_SUCCESS)
            {
                NRF_LOG_INFO("Set wait time fail");
                return;
            }
            tcs_cmd_str->reg_addr=TCS34725_REG_WAIT_TIME;
            err_code=tcs34725_read_reg(&tcs34725_instance,tcs_cmd_str,tcs34725_read_reg_cb);
            if(err_code!=NRF_SUCCESS)
            {
                NRF_LOG_INFO("Read wait time fail");
                return;
            }
        }
        //게인값 설정
        else if(strcmp(cmd_func_str->cmd,"GIN")==0)
        {
            NRF_LOG_INFO("Set gain");
            err_code=tcs34725_set_gain(&tcs34725_instance,chartoint(cmd_func_str->data,3));
            if(err_code!=NRF_SUCCESS)
            {
                NRF_LOG_INFO("Set gain fail");
                return;
            }
            tcs_cmd_str->reg_addr=TCS34725_REG_CONTROL;
            err_code=tcs34725_read_reg(&tcs34725_instance,tcs_cmd_str,tcs34725_read_reg_cb);
            if(err_code!=NRF_SUCCESS)
            {
                NRF_LOG_INFO("Read gain fail");
                return;
            }
        }
        //인터럽트 활성화 설정
        else if(strcmp(cmd_func_str->cmd,"ENA")==0)
        {
            NRF_LOG_INFO("Set interrupt");
            err_code=tcs34725_set_interrupt(&tcs34725_instance,chartoint(cmd_func_str->data,3));
            if(err_code!=NRF_SUCCESS)
            {
                NRF_LOG_INFO("Set interrupt fail");
                return;
            }
            tcs_cmd_str->reg_addr=TCS34725_REG_ENABLE;
            err_code=tcs34725_read_reg(&tcs34725_instance,tcs_cmd_str,tcs34725_read_reg_cb);
            if(err_code!=NRF_SUCCESS)
            {
                NRF_LOG_INFO("Read interrupt fail");
                return;
            }
        }
        //Wait Long 활성화 설정 (활성화시 대기 시간이 설정된 대기 시간 x12가 된다)
        else if(strcmp(cmd_func_str->cmd,"WLO")==0)
        {
            NRF_LOG_INFO("Set wait long");
            err_code=tcs34725_set_wait_long(&tcs34725_instance,chartoint(cmd_func_str->data,3));
            if(err_code!=NRF_SUCCESS)
            {
                NRF_LOG_INFO("Set wait long fail");
                return;
            }
            tcs_cmd_str->reg_addr=TCS34725_REG_CONFIG;
            err_code=tcs34725_read_reg(&tcs34725_instance,tcs_cmd_str,tcs34725_read_reg_cb);
            if(err_code!=NRF_SUCCESS)
            {
                NRF_LOG_INFO("Read wait long fail");
                return;
            }
        }
        /*
        - 인터럽트가 발생하기까지 필요한 쓰레스홀드값을 벗어나는 Clear 값의 연속 측정 횟수 설정
        Persistence 값은 다른 레지스터 데이터와는 다르게 10진수값과 1:1 매칭이 되지 않는다
        따라서 설정값에 해당하는 2진수 데이터로 변환하는 과정이 필요하다
        */
        else if(strcmp(cmd_func_str->cmd,"PER")==0)
        {
            //2진수 데이터로 변환된 Persistence 값을 저장할 변수
            uint8_t persistence_val;
            
            NRF_LOG_INFO("Set Persistence");
            
            //BLE 통신을 통해 수신된 Persistence 데이터는 문자열이므로 상수로 변환
            persistence_val=chartoint(cmd_func_str->data,3);
            
            //설정값에 해당하는 2진수값으로 변환 (ex: 5 = 0100, 60 = 1111)
            persistence_val=tcs34725_per_dectobin(persistence_val);
            
            err_code=tcs34725_set_persistence(&tcs34725_instance,persistence_val);
            if(err_code!=NRF_SUCCESS)
            {
                NRF_LOG_INFO("Set Persistence fail");
                return;
            }
            tcs_cmd_str->reg_addr=TCS34725_REG_PERSISTENCE;
            err_code=tcs34725_read_reg(&tcs34725_instance,tcs_cmd_str,tcs34725_read_reg_cb);
            if(err_code!=NRF_SUCCESS)
            {
                NRF_LOG_INFO("Read Persistence fail");
                return;
            }
        }
        else
        {
            return;
        }
    }
}

- 명령어에 해당하는 레지스터에 데이터를 입력하고 다시 액세스해 저장된 데이터를 읽어 온 뒤 BLE 통신을 통해 전송함으로써 명령어와 데이터가 제대로 입력되었는지 확인

 

<TCS34725 레지스터 Read 콜백 함수>

/*
- BLE 전송 태스크에서 전송될 데이터를 저장하기 위한 구조체
- 명령어(3)+레지스터 데이터(3)
- or 명령어(3)+쓰레스홀드 레지스터 데이터(5)
- +'\n'
*/
typedef struct{
    char send_data[9];
}tcs34725_ble_reg_t;

void tcs34725_read_reg_cb(ret_code_t result, tcs34725_reg_data_t * p_raw_data)
{
    if(result!=NRF_SUCCESS)
    {
        NRF_LOG_INFO("TCS34725 register read fail");
        return;
    }
    
    //명령어 부분을 저장할 캐릭터 배열
    char read_reg_cb_cmd[]="CMD";
    
    //2진수 Persistence 값을 10진수로 변환하는데 사용할 변수
    uint8_t persistence_value;
    
    //Timing, Wait Time 레지스터 값이 0인 경우(설정값=256),
    //tcs34725_reg_data_t 구조체의 레지스터 값을 저장하는 변수 자료형은 uint8_t라 
    //오버플로우 발생하므로 uint16_t 변수를 이용해 따로 저장
    uint16_t reg_value;
    
    /*
    TCS34725 레지스터에 액세스하기 위해서 레지스터 주소=레지스터 주소+커맨드 레지스터(0x80)를 사용하므로
    레지스터 주소 & 0x1F 를 사용해 커맨드 레지스터 부분을 제거해 레지스터 주소만 남긴다
    */
    p_raw_data->reg_addr&=0x1F;
    
    //레지스터 주소에 해당하는 명령어를 앞서 선언한 캐릭터 배열에 저장
    switch(p_raw_data->reg_addr)
    {
    	//Enable 레지스터 (인터럽트 활성화 여부 확인)
        case TCS34725_REG_ENABLE :
            NRF_LOG_INFO("Enable register : %X",p_raw_data->reg_data);
            strcpy(read_reg_cb_cmd,"ENA");
            break;
        //타이밍 레지스터
        case TCS34725_REG_TIMING :
            NRF_LOG_INFO("Timing register : %X",p_raw_data->reg_data);
            strcpy(read_reg_cb_cmd,"TIM");
            reg_value=256-p_raw_data->reg_data;
            break;
        //Wait Time 레지스터(Wait Time 활성화 여부 확인)
        case TCS34725_REG_WAIT_TIME :
            NRF_LOG_INFO("Wait time register : %X",p_raw_data->reg_data);
            strcpy(read_reg_cb_cmd,"WAT");
            reg_value=256-p_raw_data->reg_data;
            break;
        /*
        - Persistence 레지스터
        Persistence 값은 다른 레지스터 데이터와는 다르게 10진수 값이 아닌 2진수값이므로 10진수로 변환
        */
        case TCS34725_REG_PERSISTENCE :
            NRF_LOG_INFO("Persistence register : %X",p_raw_data->reg_data);
            strcpy(read_reg_cb_cmd,"PER");
            persistence_value=p_raw_data->reg_data;
            persistence_value=tcs34725_per_bintodec(persistence_value);
            p_raw_data->reg_data=persistence_value;
            break;
        //Configuration 레지스터 (Wait Long 활성화 여부 확인)
        case TCS34725_REG_CONFIG :
            NRF_LOG_INFO("Configuration register : %X",p_raw_data->reg_data);
            strcpy(read_reg_cb_cmd,"WLO");
            break;
        //컨트롤 레지스터 (게인 설정)
        case TCS34725_REG_CONTROL :
            NRF_LOG_INFO("Control register : %X",p_raw_data->reg_data);
            strcpy(read_reg_cb_cmd,"GIN");
            break;
        // ID 레지스터 (센서 모델명 확인)
        case TCS34725_REG_ID :
            NRF_LOG_INFO("ID register : %X",p_raw_data->reg_data);
            strcpy(read_reg_cb_cmd,"IDR");
            break;
        //Status 레지스터 (인터럽트 활성화 여부 및 RGBC 유효한지 확인(integration cycle 완료 여부))
        case TCS34725_REG_STATUS :
            NRF_LOG_INFO("Status register : %X",p_raw_data->reg_data);
            strcpy(read_reg_cb_cmd,"STA");
            break;
        default :
            break;
    }

	//BLE 통신을 통해 레지스터 데이터를 전송하는 태스크에 전달될 구조체 변수 생성
    tcs34725_ble_reg_t tcs_ble_send_str;
    
    //구조체 변수에 명령어+데이터로 구성된 문자열 입력
    if((p_raw_data->reg_addr==TCS34725_REG_TIMING)||(p_raw_data->reg_addr==TCS34725_REG_WAIT_TIME))
    {
        sprintf(tcs_ble_send_str.send_data,"%s%3d",read_reg_cb_cmd,reg_value);
    }
    else
    {
        sprintf(tcs_ble_send_str.send_data,"%s%3d",read_reg_cb_cmd,p_raw_data->reg_data);
    }
  
    //콜백 함수가 호출됐다는 건, TCS34725 레지스터로부터 데이터 수신이 완료됐다는 뜻이므로
    //레지스터 Read에 사용했던 구조체 포인터 변수에 할당된 메모리를 해제
    vPortFree(p_raw_data);
    
  	//큐에 잔여 공간이 남아있다면 구조체 변수를 큐에 등록한 뒤 BLE 전송 태스크에 알림 전달
    if(uxQueueSpacesAvailable(m_tcs_reg_data_queue)!=0)
    {
        if(pdPASS!=xQueueSend(m_tcs_reg_data_queue,&tcs_ble_send_str,10))
        {
            NRF_LOG_INFO("TCS34725 READ REG CB : Queue send fail");
        }
        xTaskNotifyGive(m_ble_tcs_reg_send_thread);
    }
    //큐가 꽉찬 경우, 구조체 변수를 덮어쓰기로 큐에 등록하고 알림 전달은 하지 않음
    else 
    {
        if(pdPASS!=xQueueOverwrite(m_tcs_reg_data_queue,&tcs_ble_send_str))
        {
            NRF_LOG_INFO("TCS34725 READ REG CB : Queue overwrite fail");
        }
    }
}

- p_raw_data->reg_addr는 tsc34725_reg_read() 함수를 통해 전달된 구조체 변수의 레지스터 주소를 가리킨다

- 이 레지스터 주소와 Switch문을 통해 레지스터 주소에 해당하는 명령어를 선택한다

- 명령어+읽어 온 데이터를 큐에 등록한 뒤 BLE 전송 태스크에 알림을 전달

 

<TCS34725 레지스터 데이터 BLE 전송 태스크>

static void ble_tcs_reg_send_thread(void *arg)
{
    ret_code_t err_code;
    
    //큐에 등록된 명령어+레지스터 데이터로 구성된 데이터를 받아올 구조체 변수
    tcs34725_ble_reg_t ble_tcs_send_reg;
    
    //구조체 변수의 길이 (BLE 전송에 필요)
    uint16_t length=sizeof(ble_tcs_send_reg);

    while(1)
    {
    	/*
    	- 전달된 알림이 있는 경우, BLE 전송 시작
        - xTaskNotifyGive()는 해당 태스크에 알림값을 1씩 증가 시킨다
        (xTaskNotify는 32bit내 임의의 값을 전달 가능)
        - ulTaskNotifyTake() 매개변수
        - pdTRUE의 경우, 알림을 획득하면서 알림값을 초기화 시킨다
        - pdFALSE의 경우, 알림을 획득하면서 현재 누적된 알림값을 1만큼 감소 시킨다
        - 큐에 데이터를 등록하면서 xTaskNotifyGive()를 통해 알림값을 1씩 누적 시키므로
          큐에 등록된 데이터 숫자만큼 순차적으로 데이터를 전송한다
        */
        if((ulTaskNotifyTake(pdFALSE,10)!=0)&&
        (pdPASS==xQueueReceive(m_tcs_reg_data_queue, &ble_tcs_send_reg, 10)))
        {
            if(m_conn_handle!=BLE_CONN_HANDLE_INVALID)
            {
                err_code=ble_nus_data_send(&m_nus, ble_tcs_send_reg.send_data, &length, m_conn_handle);
                if ((err_code != NRF_ERROR_INVALID_STATE) &&
                    (err_code != NRF_ERROR_RESOURCES) &&
                    (err_code != NRF_ERROR_NOT_FOUND))
                {
                    APP_ERROR_CHECK(err_code);
                }
            }
        }
    }
}

- TCS34725 레지스터 Read 콜백 함수에서 BLE 통신을 통해 전송될 구조체 변수를 큐에 등록하면서 xTaskNotifyGive()를 통해 알림도 함께 전달하는데 이 알림값은 1씩 누적되는 값이다

- 그리고 ulTaskNotifyTake의 매개변수를 pdFALSE로 설정할 경우, 알림을 획득하면서 누적된 알림값을 1씩 감소 시키므로 누적된 알림값만큼 BLE 전송이 실행된다

- (pdTRUE의 경우, 알림 획득하며 알림값을 초기화하기 때문에 큐에 누적된 데이터를 처리하기에 부적합)

- 따라서 큐에 등록된 데이터를 처리하는 속도보다 큐에 등록되는 데이터의 속도가 더 빨라 큐가 Full이 되더라도 순차적으로 먼저 큐에 등록된 데이터부터 처리하며 큐를 비움

 

<쓰레스홀드 콜백 함수>

void tcs34725_read_thr_cb(ret_code_t result, tcs34725_threshold_data_t * p_reg_data)
{
    if(result!=NRF_SUCCESS)
    {
        NRF_LOG_INFO("TCS34725 Threshold register read fail");
        return;
    }

    char read_thr_cb_cmd[]="CMD";
    
    if(p_reg_data->reg_addr==TCS34725_REG_THRESHOLD_LOW_L)
    {
        NRF_LOG_INFO("Threshold Low value : %d",p_reg_data->threshold_data);
        strcpy(read_thr_cb_cmd,"THL");
    }
    else
    {
        NRF_LOG_INFO("Threshold High value : %d",p_reg_data->threshold_data);
        strcpy(read_thr_cb_cmd,"THH");
    }

    tcs34725_ble_reg_t tcs_ble_send_str;
    sprintf(tcs_ble_send_str.send_data,"%s%5d",read_thr_cb_cmd,p_reg_data->threshold_data);
    vPortFree(p_reg_data);

    if(uxQueueSpacesAvailable(m_tcs_reg_data_queue)!=0) //The number of free spaces available in the queue.
    {
        if(pdPASS!=xQueueSend(m_tcs_reg_data_queue,&tcs_ble_send_str,10))
        {
            NRF_LOG_INFO("TCS34725 THRESHOLD CB : Queue send fail");
        }
    }
    else
    {
        if(pdPASS!=xQueueOverwrite(m_tcs_reg_data_queue,&tcs_ble_send_str))
        {
            NRF_LOG_INFO("TCS34725 THRESHOLD CB : Queue overwrite fail");
        }
    }
    xTaskNotifyGive(m_ble_tcs_reg_send_thread);
}

- 쓰레스홀드 레지스터 데이터는 16bit로 구성되어 있으므로 레지스터 Read 콜백 함수에서 처리하는게 아니라 따로 쓰레스홀드 콜백 함수를 만들어 처리

- 데이터 길이가 5인 것을 제외하면 레지스터 Read 콜백 함수와 동일

- BLE 전송은 레지스터 Read 태스크와 동일하게 TCS34725 레지스터 데이터 BLE 전송 태스크 이용

 

<TCS34725 각종 설정값들을 전송하는 태스크 (RAR 명령어 수신했을 때)>

/* pvPortMalloc() 함수를 이용해 TCS34725 레지스터값을 수신할 구조체 포인터 변수들 생성
 * 수신 완료 후 호출되는 콜백 함수에서 vPortFree() 함수를 통해 메모리 할당 해제됨
 */
static void tcs_read_all_reg_thread(void *arg)
{
    tcs34725_reg_data_t *enable=(tcs34725_reg_data_t*)pvPortMalloc(sizeof(tcs34725_reg_data_t));
    tcs34725_reg_data_t *timing=(tcs34725_reg_data_t*)pvPortMalloc(sizeof(tcs34725_reg_data_t));
    tcs34725_reg_data_t *wait_time=(tcs34725_reg_data_t*)pvPortMalloc(sizeof(tcs34725_reg_data_t));
    tcs34725_reg_data_t *persistence=(tcs34725_reg_data_t*)pvPortMalloc(sizeof(tcs34725_reg_data_t));
    tcs34725_reg_data_t *config=(tcs34725_reg_data_t*)pvPortMalloc(sizeof(tcs34725_reg_data_t));
    tcs34725_reg_data_t *control=(tcs34725_reg_data_t*)pvPortMalloc(sizeof(tcs34725_reg_data_t));
    tcs34725_reg_data_t *id=(tcs34725_reg_data_t*)pvPortMalloc(sizeof(tcs34725_reg_data_t));
    tcs34725_reg_data_t *status=(tcs34725_reg_data_t*)pvPortMalloc(sizeof(tcs34725_reg_data_t));
    
    tcs34725_threshold_data_t *threshold_low=(tcs34725_threshold_data_t*)pvPortMalloc(sizeof(tcs34725_threshold_data_t));
    tcs34725_threshold_data_t *threshold_high=(tcs34725_threshold_data_t*)pvPortMalloc(sizeof(tcs34725_threshold_data_t));

    while(1)
    {
        enable->reg_addr=TCS34725_REG_ENABLE;
        tcs34725_read_reg(&tcs34725_instance, enable, tcs34725_read_reg_cb);

        timing->reg_addr=TCS34725_REG_TIMING;
        tcs34725_read_reg(&tcs34725_instance, timing, tcs34725_read_reg_cb);

        wait_time->reg_addr=TCS34725_REG_WAIT_TIME;
        tcs34725_read_reg(&tcs34725_instance, wait_time, tcs34725_read_reg_cb);
                
        persistence->reg_addr=TCS34725_REG_PERSISTENCE;
        tcs34725_read_reg(&tcs34725_instance, persistence, tcs34725_read_reg_cb);
        
        config->reg_addr=TCS34725_REG_CONFIG;
        tcs34725_read_reg(&tcs34725_instance, config, tcs34725_read_reg_cb);
        
        control->reg_addr=TCS34725_REG_CONTROL;
        tcs34725_read_reg(&tcs34725_instance, control, tcs34725_read_reg_cb);

        id->reg_addr=TCS34725_REG_ID;
        tcs34725_read_reg(&tcs34725_instance, id, tcs34725_read_reg_cb);

        status->reg_addr=TCS34725_REG_STATUS;
        tcs34725_read_reg(&tcs34725_instance, status, tcs34725_read_reg_cb);

        threshold_low->reg_addr=TCS34725_REG_THRESHOLD_LOW_L;
        tcs34725_read_threshold(&tcs34725_instance, threshold_low, tcs34725_read_thr_cb);

        threshold_high->reg_addr=TCS34725_REG_THRESHOLD_HIGH_L;
        tcs34725_read_threshold(&tcs34725_instance, threshold_high, tcs34725_read_thr_cb);
    }
}

 

<GPIO 핀 핸들러 함수 (TCS34725 인터럽트 처리)>

void in_pin_handler(nrf_drv_gpiote_pin_t pin, nrf_gpiote_polarity_t action)
{
    if(pin==TCS34725_INT_PIN)
    {
        ret_code_t err_code;
                
        //BLE 통신을 통해 전송될 구조체 변수 생성 및 명령어 부분을 인터럽트 발생 명령어로 설정
        static tcs34725_ble_reg_t tcs_int_alarm={0};
        strcpy(tcs_int_alarm.send_data,"INT");
        
        NRF_LOG_INFO("TCS34725 RGBC Interrupt occured");
        
        //인터럽트 클리어 (인터럽트 클리어 명령어를 사용하지 않을 경우, 더 이상의 인터럽트 발생 X)
        err_code=tcs34725_int_clear(&tcs34725_instance);
        APP_ERROR_CHECK(err_code);
        if(err_code==NRF_SUCCESS)
        {
            NRF_LOG_INFO("TCS34725 Clear channel interrupt clear");
        }
        
        //큐에 빈 자리가 있을 경우, 큐에 구조체 변수 등록 및 알림 전달
        if(uxQueueSpacesAvailable(m_tcs_reg_data_queue)!=0)
        {
            if(pdPASS!=xQueueSend(m_tcs_reg_data_queue,&tcs_int_alarm,10))
            {
                NRF_LOG_INFO("Interrupt queue send fail");
            }
            xTaskNotifyGive(m_ble_tcs_reg_send_thread);
        }
        //큐에 빈 자리가 없을 경우, 구조체 변수를 덮어쓰기로 큐에 등록하고 알림 전달 X
        else
        {
            if(pdPASS!=xQueueOverwrite(m_tcs_reg_data_queue,&tcs_int_alarm))
            {
                NRF_LOG_INFO("Interrupt queue overwrite fail");
            }
        }
    }
}

//GPIO 초기화
static void gpio_init(void)
{
    ret_code_t err_code;

    err_code = nrf_drv_gpiote_init();
    APP_ERROR_CHECK(err_code);

    //GPIO 핀이 HIGH -> LOW 로 전환되는 것을 감지
    nrf_drv_gpiote_in_config_t in_config = GPIOTE_CONFIG_IN_SENSE_HITOLO(true);
    
    //풀업 설정
    in_config.pull = NRF_GPIO_PIN_PULLUP;

    //TCS34725 인터럽트 핀에 연결된 GPIO 핀, 핀 환경설정, 핀 변화 감지시 호출될 핸들러 함수
    err_code = nrf_drv_gpiote_in_init(TCS34725_INT_PIN, &in_config, in_pin_handler);
    APP_ERROR_CHECK(err_code);

    //GPIO Input 핀 감지 활성화
    nrf_drv_gpiote_in_event_enable(TCS34725_INT_PIN, true);
}

- TCS34725 인터럽트 발생시 인터럽트 핀이 HIGH->LOW로 전환되므로 TCS34725 인터럽트 핀에 연결된 GPIO Input 핀이 HIGH->LOW로 전환될 때, 이벤트 핸들러가 호출되도록 설정

- 인터럽트 발생했을 경우, TCS34725 레지스터 데이터 BLE 전송 태스크를 통해 인터럽트 발생 명령어 전송

 

소스 : https://github.com/lee35210/nRF52840_BLE_FREERTOS_TCS34725

 

 

- 연결이 됨과 동시에 nRF52840 보드에 "RAR" 명령어를 보내 Timing, Gain, Wait Time, Wait Long, Interrupt, Persistence, Threshold Low, Threshold High 데이터를 읽어 온다

- 이후, RGBC Read 태스크가 vTaskDelay() (ms) 간격으로 실행되면서 측정한 RGBC 값을 BLE 통신을 통해 스마트폰에 전송

 

[타이밍과 게인 설정]

- 타이밍값과 게인값은 측정되는 RGBC 값에 영향을 준다

- (RGB값의 변화는 측정 색상에 따라 변화율이 다르고 Clear 값은 설정 값에 비례해서 변화한다)

- 대체로 두 설정 값이 낮아질 수록 측정되는 Clear 및 RGB값이 낮아지면서 측정 색상이 어두워지고

- 높은 갚을 가질 수록 측정되는 Clear 및 RGB 값이 높아지면서 측정 색상이 밝아지는 경향이 있다

- (측정 색상에 따라 두 설정값에 무관한 일관된 RGB 값을 갖는 경우도 존재한다 (Clear 값이 세자리수에서 65535까지 변화하는 상황에서도))

- Clear 값이 이미 최대치인 65535의 값을 갖는 상황에서도 타이밍 값이 높아질 수록 측정되는 RGB 값이 증가할 수 있다

- (RGBC 모든 색상은 0~65535의 값을 갖는다)

 

[RGBC 측정]

- RGBC 측정 주기는 {(256-타이밍 레지스터 데이터) + (256-Wait Time 레지스터 데이터)}*2.4ms 이다

- RGBC 값이 측정되는데 걸리는 시간은 2.4ms * (256-Timing 레지스터에 저장된 값) 

- 예) 타이밍 레지스터에 저장된 값이 246(0xF6)일 경우, RGBC 측정에 걸리는 시간은 (256-246) * 2.4ms = 24ms

- 대기 시간(Wait Time)은 RGBC 측정 사이에 센서가 대기 상태로 존재할 시간을 의미한다

- 대기 시간 계산은 타이밍 계산 방법과 동일 (256-Wait Time 레지스터 데이터)*2.4ms

- 따라서 측정되는 RGBC 값이 빠르게 갱신되길 원한다면 대기 시간 값을 낮춰야한다

(타이밍 레지스터 값을 낮추는 것도 한 방법일 수 있으나 타이밍 레지스터 데이터는 측정되는 RGBC값에 영향을 준다)

- Wait Long을 활성화할 경우, 대기 시간 = (256-Wait Time 레지스터 데이터)*2.4ms*12 가 된다

- 코드는 (256-입력 데이터)가 해당 레지스터에 저장되도록 설계됐다

 

[인터럽트]

- 인터럽트 활성화 전제

- Persistence 값이 0일 경우, 쓰레스홀드 값과는 상관없이 RGBC 측정 때마다 인터럽트가 발생한다

- Persistence 값은 인터럽트가 발생하기까지 필요한 쓰레스홀드 범위를 벗어나는 Clear 값의 연속 측정 횟수를 의미

- 따라서 인터럽트가 발생하는데 걸리는 시간은 {(타이밍 값 + 대기 시간 값)*2.4ms}*Persistence 값이 된다

- 예) 타이밍 150, 대기 시간 50, Persistence 10일 경우

- (150*2.4 + 50*2.4)*10 = 4,800ms  (Clear 값이 10 연속으로 쓰레스홀드 범위를 벗어나는 상황을 전제)

- 설정된 Persistence 횟수에 도달하기 전에 Clear 값이 쓰레스홀드 범위 내로 측정된다면 Persistence 횟수는 초기화된다

 

[기타]

- 센서와 측정 물체간의 거리가 멀어질 수록 측정을 위해 사용하는 광원의 세기가 강해져야한다. 그렇지 않을경우, 부정확한(원래의 색상보다 어두운) 값이 측정된다

/** 20/05/10 - <TCS34725 레지스터 Read 콜백 함수> Timing, Wait Time 설명 추가

 *

 ** 20/06/09

 * 1. tcs34725_read_threshold() 함수 수정

 * 2. tcs34725_read_all_config() 함수 삭제

 * 3. timer_handler() 함수내 tcs34725_read_rgbc() 함수에서 사용되는 구조체 변수를 전역 변수에서

 *    malloc() 함수를 사용한 메모리 할당 방식으로 변경

 */

 

사용한 TCS34725 모듈

- 사용 보드 : nRF52840-PDK

- 사용 IDE : SEGGER Embedded Studio for ARM (노르딕 제품 사용하는 경우 무료 라이센스 이용 가능)

- 작동 전압 : 2.7~3.6V (사용 모듈의 경우, 3.3~5.0V)

- 검출 색상 : Red, Green, Blue, Clear (센서에 감지되는 빛의 세기)

- 통신 : TWI (I2C), 최대 속도 400kbps,

          SCL, SDA - Input Low Voltage 최대 0.3 VDD, Input High Voltage 최소 0.7 VDD

- SDK 내 예제 프로젝트를 기반으로 작성

(TWI 통신, APP TIMER : examples\peripheral\twi_master_using_nrf_twi_mngr)

(GPIO 인터럽트 : examples\peripheral\pin_change_int)

(APP BUTTON : examples\ble_peripheral\ble_app_blinky)

(SDK 다운로드 : https://developer.nordicsemi.com/)

- 제조사 제품 페이지 : https://ams.com/ko/tcs34725 (데이터시트 다운 가능)

 

[작동 방식]

<기본 작동>

TCS34725 데이터 시트내 시스템 상태 다이어그램

- 전원 인가시, 내부 power-on-reset이 디바이스를 초기화하고 low-power Sleep 상태로 만듦

- I2C 버스에서 스타트 컨디션이 감지되면 디바이스는 아이들 상태로 전환되어 Enable 레지스터(0x00)의 PON 비트(Power ON)를 체크함

- PON 비트가 비활성화 되어 있다면 전력 감소를 위해 슬립 상태로 전환

- 활성화되어 있다면 RGBC 기능(AEN 비트)이 활성화 되기 전까지 아이들 상태를 유지한다

- RGBC 기능이 활성화되면 디바이스는 대기 및 RGBC 감지를 순서대로 실행한다

- (대기 시간(Wait Time)이 길어질 수록 색상 감지 간격이 길어지므로 빠른 색상 감지를 위해서는 낮은 대기 시간을 설정할 필요가 있다)

- 완료되면 아이들 상태로 돌아오고 PON, AEN 비트가 활성화 되어있는 한 디바이스는 자동으로 Wait-RGBC 주기를 반복한다

 

<RGBC 작동>

- RGBC 엔진은 RGBC 게인 컨트롤(AGAIN)과 RGBC 포토 다이오드를 위한 네 개의 통합 ADC를 갖는다

- RGBC 통합 시간(ATIME)은 RGBC의 해상도 및 감도에 영향을 준다

- 네 개 채널의 통합이 동시에 이뤄지며 변환이 완료되면 결과값은 컬러 데이터 레지스터로 전송된다

- 전송 중 인식 불가능한 데이터가 읽히는 것을 막기 위해 더블 버퍼링 된다

- 전송 이후, 디바이스는 설정된 상태 머신에 따라 다음 상태로 자동으로 이동한다

 

<인터럽트>

- 인터럽트 기능은 사용자가 설정한 범위를 벗어나는 빛의 세기에 대해 센서를 폴링할 필요를 없앰으로써 시스템을 간소화하고 효율을 상승시킨다.

- 인터럽트 기능이 활성화 되어있는 동안 스테이터스 레지스터(0x13)에서 인터럽트 상태를 확인 가능하다

- 인터럽트 아웃풋 상태는 Enable 레지스터(0x00)의 RGBC 인터럽트 활성화(AIEN) 영역을 사용해 활성화 할 수 있다

- 사용자는 두개의 16비트 인터럽트 쓰레스홀드 레지스터를 통해 원하는 빛의 세기의 하한 및 상한 제한을 설정할 수 있다

- 인터럽트는 클리어 데이터(CDATA)가 클리어 인터럽트 로우 쓰레스홀드(AILTx) 보다 낮거나 클리어 인터럽트 하이 쓰레스홀드(AITHx) 보다 클 때 발생된다

- 쓰레스홀드는 로우 쓰레스홀드, 하이 쓰레스홀드순으로 비교되므로 만약, 로우 쓰레스홀드가 하이 쓰레스홀드 보다 높은 쓰레스홀드 값이 설정되있다면 하이 쓰레스홀드는 무시되고 로우 쓰레스홀드만이 비교된다

- 인터럽트 발생 시기를 추가 제어하기 위해 디바이스는 지속성 필터를 제공한다

- 지속성 필터를 통해 사용자는 인터럽트 발생까지의 클리어 쓰레스홀드 범위를 벗어나는 측정값의 연속 발생 횟수를 지정 할 수 있다

- 지속성 필터 레지스터(0x0C)를 통해 사용자는 Clear 지속성 필터(APERS) 값을 지정할 수 있다

- 지속성 필터가 인터럽트를 발생시킨 이후, 스페셜 펑션 인터럽트 클리어 커맨드를 받기 전까지는 인터럽트 핀의 LOW 상태가 유지된다 (인터럽트 핀이 HIGH->LOW 로 전환되며 인터럽트 발생)

 

<시스템 타이밍>

TCS34725 데이터 시트내 다이어그램

- 시스템 상태 머신은 상태 개요 및 시스템의 디바이스 컨트롤을 제공하는 상태로의 전환을 제공한다

- 전원 관리 기능(WEN(Wait time Enable))이 활성화 되어있을 때, 상태 머신은 대기 상태로 전환된다

- 대기 시간은 WTIME과 대기 시간을 12배로 연장하는 WLONG에 의해 결정된다

- RGBC(AEN) 기능이 활성화 되어있을 때, 상태 머신은 RGBC 초기화 및 RGBC ADC 상태를 통해 전환된다

- RGBC 초기화 상태는 2.4ms 가 걸리고 RGBC ADC 시간은 통합 시간(ATIME)에 의존한다

- RGBC 주기의 결과로 인터럽트가 발생하면 RGBC ADC 끝에 실행된다

 

<전원 관리>

데이터시트 내 전류 소비 예시

- 전력 소비는 대기 상태를 통해 관리 가능

- 대기 상태는 일반적으로 64uA 를 소모

- 최악의 전류 소모는 대기 상태를 사용하지 않는 것

 

[TWI 통신]

데이터시트 내 I2C(TWI) 통신 프로토콜 예시

- Write 가능한 모든 레지스터(커맨드 레지스터 제외)는 전원 재인가시 설정값이 초기화된다

- Wait Time 레지스터만 0xFF의 초기값을 갖고 나머지 레지스터는 0x00의 초기값을 갖는다

 

<커맨드 레지스터 (레지스터 주소 X)>

데이터시트 - 커맨드 레지스터

- 읽거나 쓸 타겟 레지스터 주소를 특정화하는데 사용됨

- 타겟 레지스터 주소 + CMD 명령어가 장치 주소를 제외한 첫번째 바이트에 와야 한다

- 7번 비트 CMD는 특정 레지스터 접근이던 인터럽트 클리어를 위한 목적이던 1이여야 한다

- 5, 6번 비트는 레지스터 주소 접근 방식 및 인터럽트 클리어 명령어를 사용하는데 쓰이는 비트

  00일 경우 : 지정한 레지스터만 반복해서 읽거나 쓴다

  (예 : 0x00 번지로부터 3바이트 Read를 실행한 경우, 0x00 번지만 세 번 읽는다)

  01일 경우 : 지정한 레지스터 주소 사용 이후, 자동으로 레지스터 주소값이 1 증가된다

  (예 : 0x00 번지로부터 3바이트 Read를 실행한 경우, 0x00, 0x01, 0x02 레지스터 주소에 저장된 데이터를 읽어 온다)

  11일 경우 : 4~0 번 비트에 00110를 함께 설정 및 전송해 인터럽트 클리어를 실행한다

  (인터럽트 후 인터럽트 핀은 LOW 상태가 되는데 클리어를 실행하면 다시 HIGH로 전환되어 인터럽트 재실행 가능)

 

<Enable 레지스터 0x00>

데이터시트 - 인에이블 레지스터

- 주로 TCS34725 전원 ON, OFF 에 사용되는 레지스터

- BIT 4 (AIEN) : 인터럽트 활성화 비트

- BIT 3 (WEN) : 대기 활성화 비트

- BIT 1 (AEN) : RGBC 활성화 비트. 2채널 ADC를 활성화 한다

- BIT 0 (PON) : 타이머와 ADC채널이 작동되도록 내부 오실레이터를 활성화하는 비트

 

<타이밍 레지스터 0x01>

데이터 시트 - 타이밍 레지스터

- RGBC 클리어와 IR 채널 ADC들의 내부 통합 시간을 2.4ms 단위로 조절

- 최대 RGBC 카운트 = (256-ATIME)*1024 (최대 65535)

- 카운트가 늘어날 수록 RGBC 검출 시간이 증가하지만 해상도 및 감도도 증가해 더 정확한 색상 검출이 가능해진다

- 설정값은 1~256을 갖는데 실제 레지스터에 입력되는 값은 256-설정값이 되어야 한다

 

<대기 시간 레지스터 0x03>

데이터 시트 - Wait Time 레지스터

- 대기 시간을 12배로 늘리는 WLONG 비트가 활성화되지 않는한 2.4ms 단위로 대기 시간을 설정한다

- WTIME은 2의 보수로 프로그램된다

- 설정값은 1~256을 갖는데 실제 레지스터에 입력되는 값은 256-설정값이 되어야 한다

 

<RGBC 인터럽트 쓰레스홀드 레지스터 0x04~0x07>

데이터시트 - 쓰레스홀드 레지스터

- 측정되는 빛의 세기 (Clear)의 상한 및 하한을 설정하는 레지스터

- 하한 및 상한은 각각 2바이트로 별도의 값 변환없이 10진수 입력 (0~65535까지 설정 가능)

- 레지스터 주소는 하위 8비트, 상위 8비트 순으로 입력할 16비트 데이터를 8비트씩 나누어 하위 8비트 먼저 전송

- 하한 보다 더 낮은 값, 상한 보다 더 높은 값이 측정됐을 때만 인터럽트가 실행된다

  (인터럽트 발생 시기는 지속 레지스터(0x0C)에 설정된 값에 영향을 받는다)

- 인터럽트 핀은 기본적으로 HIGH 상태를 유지하다 인터럽트가 발생하며 LOW로 전환된다

- 인터럽트 핀은 LOW상태를 유지하므로 인터럽트 클리어를 실행하기 전까진 더 이상의 인터럽트가 발생하지 않는다

 

<지속 레지스터 0x0C>

 

데이터시트 - 지속 레지스터

- 디바이스의 필터링 인터럽트 기능을 제어하는데 사용되는 레지스터

- 각 통합 주기마다 인터럽트를 발생시킬지 아니면 쓰레스홀드 레지스터에 지정된 값을 벗어나는 결과를 지정한 횟수만큼 측정한 이후에 인터럽트를 발생시킬지를 결정

- 예) 쓰레스홀드 Low=10,000 / High=50,000 이고 APERS=1111일 때,

  60번 연속으로 0~9,999 / 50,001~65535 범위의 Clear 값이 측정된다면 인터럽트가 발생된다

  APERS=0000 이라면 매 RGBC 측정 때마다 인터럽트가 발생한다

 

<환경설정 레지스터 0x0D>

데이터시트 - 환경설정 레지스터

- Wait long 타임 설정 레지스터

- 2번 비트인 WLONG을 활성화할 경우, 대기 시간이 프로그램된 WTIME 시간의 12배가 된다

 

<컨트롤 레지스터 0x0F>

데이터시트 - 컨트롤 레지스터

- RGBC 측정값 증폭에 사용되는 레지스터

- 측정값은 게인값에 정비례하지 않는다 (1x gain*4 != 4x gain)

 

<ID 레지스터 0x12>

데이터시트 - ID 레지스터

- 사용하는 센서가 TCS34725인지 아니면 TCS34727인지를 알려주는 레지스터

- 읽기 전용

 

<스테이터스 레지스터 0x13>

데이터시트 - 스테이터스 레지스터

- 디바이스 내부 상태를 제공해주는 레지스터

- BIT 5 (AINT) : 인터럽트 활성화 여부 (1-인터럽트 활성화)

- BIT 0 (AVALID) : RGBC 활성화 여부

 

<RGBC 채널 데이터 레지스터 0x14~0x1B>

데이터시트 - RGBC 데이터 레지스터

- 측정된 RGBC 데이터를 저장하는 레지스터

- 각 색상은 16비트 데이터로 상하 각 8비트로 나누어 저장된다

- 값을 읽어올 때 추가로 10진수 변환할 필요는 없다

- 연속으로 데이터를 읽어와야 하므로 커맨드 레지스터의 값이 0xA0 (CMD : 1, TYPE : 01 (자동 주소 증가))여야 한다

- 예) 0x14 번지부터 0x1B까지 8바이트를 읽어와야 한다면 슬레이드 주소를 제외한 첫 전송 바이트는 (0x14 | 0xA0)가 되어야 한다

- 하위 데이터 레지스터를 읽을 때, 상위 8비트는 섀도우 레지스터에 저장되어 연속으로 읽어진다

- 따라서 하위 바이트와 상위 바이트를 읽는 사이에 추가적인 ADC 통합 주기가 끝나더라도 옳바른 값을 읽어 온다

 

[코드]

<매크로>

/*
main.c
*/
//TCS34725 센서 주소
#define TCS34725_ADDR 0x29

//TWI PIN
#define TCS34725_SDA_PIN 28
#define TCS34725_SCL_PIN 29

//TSC34725와의 통신에 사용할 TWI 인스턴스 ID
#define TWI_INSTANCE_ID 0

//TWI trasaction manager 인스턴스 Queue 최대 크기 (대기 전송 갯수)
#define MAX_PENDING_TRANSACTIONS 20

/*
- TWI transaction manager 인스턴스 정의 매크로
생성할 TWI transaction manager 인스턴스 이름, 트랜잭션 Queue 크기, 사용할 TWI 인스턴스
*/
NRF_TWI_MNGR_DEF(m_nrf_twi_mngr, MAX_PENDING_TRANSACTIONS, TWI_INSTANCE_ID);

/*
- 공통 TWI 센서 인스턴스 생성 매크로
공통 센서 인스턴스 이름, 사용할 TWI transaction manager 인스턴스, 버퍼 크기
(버퍼 크기의 경우 TWI manager Queue 크기보다 작거나 같아야 한다)
*/
NRF_TWI_SENSOR_DEF(sensor_instance, &m_nrf_twi_mngr, MAX_PENDING_TRANSACTIONS);

/*
- 센서 인스턴스 생성 매크로
센서 인스턴스 이름, 사용할 공통 TWI 센서 인스턴스, 센서 주소
*/
TCS34725_INSTANCE_DEF(tcs34725_instance, &sensor_instance, TCS34725_ADDR);

 

<TWI 설정>

static void twi_config(void)
{
    uint32_t err_code;
    
    /*
    -TCS34725와의 TWI 통신 설정
    -SDA, SCL핀, 통신속도, 인터럽트 우선순위 설정 뒤 twi 초기화
    */
    nrf_drv_twi_config_t const config={
      .scl=TCS34725_SCL_PIN,
      .sda=TCS34725_SDA_PIN,
      .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);
}

 

<TCS34725 레지스터 구조체 및 Read 함수>

/*
-TCS34725 레지스터로부터 데이터를 읽어오거나 데이터를 쓸 때 사용할 구조체
reg_data : TCS34752 레지스터로부터 읽어 온 데이터 저장 or 레지스터에 쓸 데이터 저장
reg_addr : 액세스 할 레지스터 주소
*/
typedef struct
{
    uint8_t reg_data;
    uint8_t reg_addr;
} tcs34725_reg_data_t;

/*
-TCS34725의 특정 레지스터로부터 데이터를 읽어 오는 함수
-TCS34725_INSTANCE_DEF을 통해 생성한 TCS34725 센서 인스턴스
-p_reg_data : 액세스 할 레지스터 주소와 읽어 온 데이터를 저장하는데 사용할 구조체 변수
-user_cb : TCS34725로부터 데이터를 수신한 직후 호출된 콜백 함수
 (TWI Manager에 큐를 등록한 직후가 아님)
*/
ret_code_t tcs34725_read_reg(tcs34725_instance_t const * p_instance,
                              tcs34725_reg_data_t *      p_reg_data,
                              tcs34725_data_callback_t   user_cb
                             )
{
    /*
    p_instance->p_sensor_data : NRF_TWI_SENSOR_DEF을 통해 설정된 공통 센서 인스턴스
    p_instance->sensor_addr : TCS34725 센서 인스턴스에 설정된 TCS34725 센서 주소
    p_reg_data->reg_addr|0x80 : 전달받은 구조체에 저장된 레지스터 주소+커맨드 레지스터
    				해당 레지스터 주소에 접근해 데이터를 읽어온다
    user_cb : 데이터 수신 완료 후 호출 할 콜백 함수
    p_reg_data : 읽어 온 레지스터 데이터를 저장할 포인터 변수
    TCS34725_REGISTER_SIZE : 읽어 올 데이터의 바이트 크기 (레지스터 하나의 크기는 8비트=1바이트)
    */
    return nrf_twi_sensor_reg_read(p_instance->p_sensor_data,
                                   p_instance->sensor_addr,
                                   p_reg_data->reg_addr|0x80,
                                   (nrf_twi_sensor_reg_cb_t) user_cb,
                                   (uint8_t *) p_reg_data,
                                   TCS34725_REGISTER_SIZE);
}

- 이 함수를 통해 TCS34725 레지스터로부터 데이터를 읽어 온다

- p_reg_data->reg_addr | 0x80 : 레지스터 주소 + 커맨드 레지스터 (CMD : 1, TYPE : 00 (Repeated byte)=0x80)

- 읽어 올 데이터 크기를 1바이트로 설정했으므로 TYPE : 01 (Auto-increment) 로 설정해도 상관없다

 

<TCS34725 Write 함수>

ret_code_t tcs34725_write_reg(tcs34725_instance_t const * p_instance,
                              tcs34725_reg_data_t *       p_reg_data)
{
	//&p_reg_data->reg_data : 레지스터에 입력할 데이터가 저장된 구조체 변수 주소
    return nrf_twi_sensor_reg_write(p_instance->p_sensor_data,
                                    p_instance->sensor_addr,
                                    p_reg_data->reg_addr|0x80,
                                    (uint8_t *)&p_reg_data->reg_data,
                                    TCS34725_REGISTER_SIZE);
}

- 아래에 이어질 레지스터 설정 함수들이 이 함수를 통해 TCS34725 레지스터에 설정값을 전송한다

 

<TCS34725 초기화 함수>

/*
-TCS34752 초기화 함수
-인에이블 레지스터(0x00)에 설정 전송
-인터럽트 활성화, 대기 시간 활성화, RGBC 활성화, POWER ON/OFF 설정
*/
ret_code_t tcs34725_init(tcs34725_instance_t const * p_instance)
{
    ret_code_t err_code;
    //레지스터 데이터 구조체 선언 (레지스터 주소 및 인에이블 레지스터에 입력할 데이터 저장)
    tcs34725_reg_data_t enable_reg;
	enable_reg.reg_addr=TCS34725_REG_ENABLE;
    
    //인에이블 설정 저장할 구조체 선언 및 각종 기능 활성화 설정
    tcs34725_config_enable_t init_config;
    init_config.rgbc_interrupt=false;	//인터럽트 활성화
    init_config.wait_enable=true;	//대기 시간 활성화
    init_config.rgbc_enable=true;	//RGBC 측정 활성화
    init_config.power_on=true;		//Power ON/OFF

    //설정값들 해당 위치로 비트 시프트
    enable_reg.reg_data=(init_config.rgbc_interrupt << TCS34725_INT_POS)|
                        (init_config.wait_enable << TCS34725_WAIT_POS)|
                        (init_config.rgbc_enable << TCS34725_RGBC_ENABLE_POS)|
                        (init_config.power_on << TCS34725_POWER_ON_POS);

    //TCS34725 인에이블 레지스터(0x00)로 데이터 전송
    err_code=tcs34725_write_reg(&tcs34725_instance, &enable_reg);
    return err_code;
}

<TCS34725 레지스터 Read 콜백 함수>

/*
- 레지스터 Read 콜백 함수
- 구조체 포인터 변수에 저장된 레지스터 주소를 스위치 문을 통해 확인 후 로그 출력
*/
void tcs34725_read_reg_cb(ret_code_t result, tcs34725_reg_data_t * p_raw_data)
{    
    if(result!=NRF_SUCCESS)
    {
        NRF_LOG_INFO("TCS34725 register read fail");
        return;
    }
    p_raw_data->reg_addr&=0x1F;

    switch(p_raw_data->reg_addr)
    {
        case TCS34725_REG_ENABLE :
            NRF_LOG_INFO("Enable register : %X",p_raw_data->reg_data);
            break;
        case TCS34725_REG_TIMING :
            NRF_LOG_INFO("Timing register : %X",p_raw_data->reg_data);
            break;
        case TCS34725_REG_WAIT_TIME :
            NRF_LOG_INFO("Wait time register : %X",p_raw_data->reg_data);
            break;
        case TCS34725_REG_PERSISTENCE :
            NRF_LOG_INFO("Persistence register : %X",p_raw_data->reg_data);
            break;
        case TCS34725_REG_CONFIG :
            NRF_LOG_INFO("Configuration register : %X",p_raw_data->reg_data);
            break;
        case TCS34725_REG_CONTROL :
            NRF_LOG_INFO("Control register : %X",p_raw_data->reg_data);
            break;
        case TCS34725_REG_ID :
            NRF_LOG_INFO("ID register : %X",p_raw_data->reg_data);
            break;
        case TCS34725_REG_STATUS :
            NRF_LOG_INFO("Status register : %X",p_raw_data->reg_data);
            break;
        default :
            break;
    }
    //레지스터 데이터 수신에 사용된 구조체 포인터 변수 메모리 할당 해제
    free(p_raw_data);
}

- Timing, Wait Time의 경우, 1~256 사이의 설정값을 갖는데 실제 레지스터에 설정값을 입력할 땐 [256-사용자 설정값]이 입력되어야 한다

- 예) Timing값을 1로 설정할 경우, 256-1=255의 값이 실제 레지스터에 입력되는 값이 되어야 한다

- 따라서 콜백 함수에서 Timing, Wait Time 설정값을 확인하려면 [256 - p_raw_data->reg_data = 설정값] 이 되어야 한다

 

<TCS34725 타이밍 설정 함수>

//타이밍 설정 함수 (2.4ms 단위)
//atime : RGBC 통합 주기
ret_code_t tcs34725_set_timing(tcs34725_instance_t const * p_instance,
                                uint16_t atime)
{
    ret_code_t err_code;
    //통합 시간이 최소 1*2.4ms 를 갖는다는 것을 명확히 하기 위해 1~256의 값을 입력해야하도록 설정
    if((atime==0)||(256 < atime))
    {
        err_code=NRF_ERROR_INVALID_DATA;
        return err_code;
    }

    //타이밍 레지스터에 전달될 타이밍 레지스터 데이터 구조체 선언 후 레지스터 주소 및 통합 시간 설정
    tcs34725_reg_data_t timing_str;
    timing_str.reg_addr=TCS34725_REG_TIMING;
    //통합 시간은 2.4ms*(256-ATIME) 으로 결정된다. 즉, 레지스터에 입력되는 실제값이 낮을 수록 통합 시간 ↑
    timing_str.reg_data=(256-atime);
    
    //설정한 레지스터 구조체 변수 타이밍 레지스터로 전송
    err_code=tcs34725_write_reg(p_instance,&timing_str);
    return err_code;
}

- Timing 설정값은 1~256 사이의 값을 갖고 레지스터엔 [256-설정값]이 입력되어야 한다

 

<TCS34725 대기 시간 설정 함수>

//대기 시간 설정 함수 (2.4ms 단위)
//wait_val : 설정할 대기 시간 (wait_val * 2.4ms 가 대기 시간이 된다)
ret_code_t tcs34725_set_wait_time(tcs34725_instance_t const * p_instance,
                                   uint8_t wait_val)
{
    ret_code_t err_code;
    //타이밍 설정 함수와 마찬가지로 최소 대기시간이 1*2.4ms임을 명확히 하기 위해 사용
    if((wait_val==0)||(256 < wait_val))
    {
        err_code=NRF_ERROR_INVALID_DATA;
        return err_code;
    }

	//대기 시간 레지스터에 입력될 구조체 변수 선언 후 대기 시간 레지스터 주소 및 데이터 입력
    tcs34725_reg_data_t wait_time_str;
    wait_time_str.reg_addr=TCS34725_REG_WAIT_TIME
    
    //wait_val*2.4ms가 대기 시간이 된다 (WLONG=1일 경우, *12 추가)
    //실제 레지스터 입력값은 (256 - WAIT TIME)이 되어야한다
    wait_time_str.reg_data=(256-wait_val);
    
    //대기 시간 레지스터(0x03)에 설정한 대기 시간 전송
    err_code=tcs34725_write_reg(p_instance,&wait_time_str);
    return err_code;
}

- Wait Time 설정값은 1~256 사이의 값을 갖고 레지스터엔 [256-설정값]이 입력되어야 한다.

 

<TCS34725 지속 레지스터 설정 함수>

//지속 설정 레지스터 설정 함수
//out_of_range_val : 인터럽트를 발생시키기 위해 필요한 쓰레스홀드 범위를 벗어나는 클리어 값 연속 측정 횟수
ret_code_t tcs34725_set_persistence(tcs34725_instance_t const * p_instance,
                                    tcs34725_persistence_t out_of_range_val)
{
    ret_code_t err_code;
    
    //지속 레지스터에 전송할 구조체 변수 선언 후 지속 레지스터 주소 및 데이터 설정
    tcs34725_reg_data_t persistence;
    persistence.reg_addr=TCS34725_REG_PERSISTENCE;
    persistence.reg_data=out_of_range_val;
    err_code=tcs34725_write_reg(p_instance, &persistence);
    return err_code;
}

<TCS34725 환경설정(Wait long) 레지스터 설정 함수>

//환경 설정 레지스터 설정 함수 (Wait long 외에 설정 가능한 데이터가 없다)
//wait_long_val : WLONG 비트 활성화 여부 결정
ret_code_t tcs34725_set_wait_long(tcs34725_instance_t const * p_instance,
                                   tcs34725_wait_long_t wait_long_val)
{
    ret_code_t err_code;
    tcs34725_reg_data_t wait_long;
    wait_long.reg_addr=TCS34725_REG_CONFIG;
    //WLONG 비트는 1번 비트에 위치하므로 비트 시프트 사용
    wait_long.reg_data=wait_long_val << TCS34725_WAIT_LONG_POS;
    err_code=tcs34725_write_reg(p_instance, &wait_long);
    return err_code;
}

<TCS34725 컨트롤(게인) 레지스터 설정 함수>

//컨트롤 레지스터 설정 함수 (게인값 외에 설정 가능한 데이터 없음)
//gain_val : 설정할 게인값. 0~1번 비트를 사용해 게인값을 1,4,16,60x 설정 가능
ret_code_t tcs34725_set_gain(tcs34725_instance_t const * p_instance,
                                 tcs34725_gain_t gain_val)
{
    ret_code_t err_code;
    tcs34725_reg_data_t gain;
    gain.reg_addr=TCS34725_REG_CONTROL;
    gain.reg_data=gain_val;
    err_code=tcs34725_write_reg(p_instance, &gain);
    return err_code;
}

<TCS34725 쓰레스홀드 설정 함수>

/*
- 쓰레스홀드 데이터 구조체
- threshold_data : 전송 or 수신할 16비트 쓰레스홀드 데이터 (하위 8비트 먼저 전송 or 수신됨)
- reg_addr : 쓰레스홀드 레지스터 주소 (하한 : 0x04~0x05, 상한 0x06~0x07)
*/
typedef struct
{
    uint16_t threshold_data;
    uint8_t reg_addr;
} tcs34725_threshold_data_t;

/*
- 쓰레스홀드 설정 함수
- tcs34725_threshold_lh_t : 설정할 쓰레스홀드값이 쓰레스홀드 하한인지 상한인지 판단하는데 사용
- threshold_val : 설정할 쓰레스홀드 값
*/
ret_code_t tcs34725_set_threshold(tcs34725_instance_t const * p_instance,
                                  tcs34725_threshold_lh_t threshold_low_high,
                                  uint16_t threshold_val)
{
    ret_code_t err_code;
    
    //전송할 데이터를 저장할 쓰레스홀드 구조체 변수 선언 및 설정할 쓰레스홀드값 입력
    tcs34725_threshold_data_t threshold_str;
    threshold_str.threshold_data=threshold_val;

    /*	
    - 설정할 쓰레스홀드값이 상한인지 하한인지에 따라 앞서 선언한 쓰레스홀드 구조체 변수의
    레지스터 주소에 해당 레지스터 주소 입력
    */
    if(threshold_low_high==TCS34725_THRESHOLD_LOW)
    {
        threshold_str.reg_addr=TCS34725_REG_THRESHOLD_LOW_L;	//쓰레스홀드 Low - Low byte
    }
    else if(threshold_low_high==TCS34725_THRESHOLD_HIGH)
    {
        threshold_str.reg_addr=TCS34725_REG_THRESHOLD_HIGH_L;	//쓰레스홀드 High - Low byte
    }
    else
    {
        err_code=NRF_ERROR_INVALID_ADDR;
        return err_code;
    }
    
    /*
    - threshold_str.reg_addr|0xA0 : 각 쓰레스홀드값은 하위 8비트+상위 8비트=16비트이므로
    				커맨드 레지스터 0xA0을 사용해 연속된 레지스터 주소에 데이터를 입력해야 한다
    - TCS34725_THRESHOLD_BYTES : 각 쓰레스홀드 데이터는 2바이트로 구성되므로 2
    */
    err_code=nrf_twi_sensor_reg_write(p_instance->p_sensor_data,
                                      p_instance->sensor_addr,
                                      threshold_str.reg_addr|0xA0,
                                      (uint8_t *)&threshold_str,
                                      TCS34725_THRESHOLD_BYTES);
}

<TCS34725 쓰레스홀드 Read 함수>

/*
- 쓰레스홀드 Read 함수
- thr_data_str : 읽어 온 쓰레스홀드 데이터가 저장될 구조체 포인터 변수
- user_cb : 쓰레스홀드 데이터 수신 완료 후 호출할 콜백 함수
*/
ret_code_t tcs34725_read_threshold(tcs34725_instance_t const * p_instance, 
                                   tcs34725_threshold_data_t * thr_data_str,
                                   tcs34725_threshold_callback_t user_cb)
{
    ret_code_t err_code;

    //thr_data_str.reg_addr|0xA0 : 연이은 레지스터 주소에 접근해 데이터를 읽어야 하므로 
    //레지스터 주소에 0xA0 OR 연산 추가
    err_code=nrf_twi_sensor_reg_read(p_instance->p_sensor_data,
                                     p_instance->sensor_addr,
                                     thr_data_str->reg_addr|0xA0,
                                     (nrf_twi_sensor_reg_cb_t) user_cb,
                                     (uint8_t *) thr_data_str,
                                     TCS34725_THRESHOLD_BYTES);
    return err_code;
}

<TCS34725 쓰레스홀드 콜백 함수>

void tcs34725_read_thr_cb(ret_code_t result,
                          tcs34725_threshold_data_t * p_reg_data)
{
    if(result!=NRF_SUCCESS)
    {
        NRF_LOG_INFO("Reading threshold regiseter is failed");
        return;
    }
    //쓰레스홀드 Low 데이터인지 High 데이터인지 구조체 포인터 변수에 저장된 레지스터 주소로 판단 후 출력
    if(p_reg_data->reg_addr==TCS34725_REG_THRESHOLD_LOW_L)
    {
        NRF_LOG_INFO("Threshold Low value : %d",p_reg_data->threshold_data);
    }
    else
    {
        NRF_LOG_INFO("Threshold High value : %d",p_reg_data->threshold_data);
    }
    //쓰레스홀드 레지스터 데이터 수신에 사용된 구조체 포인터 변수 메모리 할당 해제
    free(p_reg_data);
}

<TCS34725 RGBC Read 함수>

/*
- TCS34725 RGBC 측정값을 저장할 구조체
- 각 색상은 하위 8비트 + 상위 8비트 = 총 16비트 (0~65535)
- TWI 통신을 통해 데이터를 읽어 올 때, 각 색상 변수의 하위 8비트->상위 8비트 순으로 데이터가 채워짐
*/
typedef struct
{
    uint16_t clear;
    uint16_t red;
    uint16_t green;
    uint16_t blue;
} tcs34725_color_data_t;

/*
- TCS34725 RGBC Read 함수
- tcs34725_color_data_t : TCS34725 RGBC 구조체
- tcs34725_rgbc_callback_t : TCS34725 RGBC 콜백 함수, RGBC 값 수신 완료 후 호출
*/
ret_code_t tcs34725_read_rgbc(tcs34725_instance_t const * p_instance,
                               tcs34725_color_data_t *     rgbc_str,
                               tcs34725_rgbc_callback_t    user_cb)
{
    ret_code_t err_code;
    
    /*
    - TCS34725_REG_CLEAR | 0xA0 : Clear data low byte(0x14)부터 Blue data high byte(0x1B)까지
    			연속으로 8바이트를 읽어와야 하므로 0xA0 (CMD :1, TYPE : 01 (Auto-increment)) 사용
    - rgbc_str : 읽어온 RGBC 데이터를 저장할 구조체 포인터 변수
    - TCS34725_RGBC_BYTES : 읽어 올 데이터는 총 8바이트이므로 8
    */
    err_code=nrf_twi_sensor_reg_read(p_instance->p_sensor_data,
                           p_instance->sensor_addr,
                           (TCS34725_REG_CLEAR|0xA0),
                           (nrf_twi_sensor_reg_cb_t) user_cb,
                           (uint8_t *) rgbc_str,
                           TCS34725_RGBC_BYTES);
    return err_code;
}

<TCS34725 RGBC Read 콜백 및 RGBC 수신 데이터 출력 함수>

//RGBC 콜백 함수, TCS34725로부터 RGBC 데이터 수신 완료 직후 호출됨
void tcs34725_rgbc_callback(ret_code_t result, tcs34725_color_data_t * p_raw_data)
{
    if(result!=NRF_SUCCESS)
    {
        NRF_LOG_INFO("Reading RGBC registers is failed");
        return;
    }
    //RGBC 출력 함수 호출
    tcs34725_rgbc_print(p_raw_data);
    //RGBC 데이터 수신에 사용된 구조체 포인터 변수 메모리 할당 해제
    free(p_raw_data);
}

/*
- RGBC 측정값 출력 함수
- 구조체 포인터 변수에 저장된 RGB 데이터를 Clear 값으로 나누고 255를 곱한 뒤 출력
  (RGB 값을 0~255 사이로 맞추기 위함)
*/
void tcs34725_rgbc_print(tcs34725_color_data_t * color_str)
{
    uint16_t c_red,c_green,c_blue;

    c_red=(int)((double)color_str->red/color_str->clear*255);
    c_green=(int)((double)color_str->green/color_str->clear*255);
    c_blue=(int)((double)color_str->blue/color_str->clear*255);
    
    NRF_LOG_INFO("Clear : %d",color_str->clear);
    NRF_LOG_INFO("Red   : %d",c_red);
    NRF_LOG_INFO("Green : %d",c_green);
    NRF_LOG_INFO("Blue  : %d",c_blue);
}

- RGB 변환은 https://forums.adafruit.com/viewtopic.php?f=8&t=67795&start=15 참조해서 작성

 

<TCS34725 인터럽트 설정 함수>

/*
- 인터럽트 설정(활성화) 함수
- 먼저 현재 TCS34725 Enable 레지스터에 저장된 데이터를 읽어 온다
  그리고 그 데이터에 int_enable 값에 따라 BIT4 AIEN (인터럽트 인에이블)을 0 or 1로 설정
- 인터럽트 활성화 여부를 추가한 설정 데이터를 TCS34725 인에이블 레지스터에 다시 전송
- int_enable : 인터럽트 활성화 여부
*/
ret_code_t tcs34725_set_interrupt(tcs34725_instance_t const * p_instance,
                                  tcs34725_int_enable_t int_enable)
{
    ret_code_t err_code;
    
    //TCS34725 인에이블 레지스터로부터 읽어 온 데이터를 저장하고 재전송할 구조체 변수 선언
    tcs34725_reg_data_t enable_reg_str;
    enable_reg_str.reg_addr=TCS34725_REG_ENABLE;
    
    //인에이블 레지스터에 저장된 데이터를 읽어 와 구조체 변수에 저장
    tcs34725_read_reg(p_instance,&enable_reg_str,NULL);
    
    //TWI 매니저 큐에 대기된 전송이 없을 때까지(=인에이블 레지스터로부터 데이터 수신이 완료될 때까지)
    //do~while문 반복
    do
    {
        nrf_delay_us(10);
    }while(nrf_twi_mngr_is_idle(&m_nrf_twi_mngr)!=true);

	//읽어 온 인에이블 레지스터 데이터에 int_enable 값에 따라 BIT4 AIEN에 0 or 1 입력
    if(int_enable==TCS34725_INTERRUPT_ENABLE)
    {
        enable_reg_str.reg_data=(enable_reg_str.reg_data|(TCS34725_INT_MASK));
    }
    else if(int_enable==TCS34725_INTERRUPT_DISABLE)
    {
        enable_reg_str.reg_data=(enable_reg_str.reg_data&~(TCS34725_INT_MASK));
    }
    else
    {
        err_code=NRF_ERROR_INVALID_PARAM;
        return err_code;
    }
    //수정한 레지스터 설정값 전송
    tcs34725_write_reg(p_instance, &enable_reg_str);
}

 

<TCS34725 인터럽트 클리어 함수>

/*
- 인터럽트 발생시 호출되는 핸들러
- Clear 쓰레스홀드값을 벗어나는 측정값들로 인해 인터럽트가 발생했음을 알리고
  TCS34725에 인터럽트 클리어 명령을 전송해 인터럽트가 계속해서 발생할 수 있게 함
*/
void in_pin_handler(nrf_drv_gpiote_pin_t pin, nrf_gpiote_polarity_t action)
{
    if(pin==TCS34725_INT_PIN)
    {
        ret_code_t err_code;
        NRF_LOG_INFO("TCS34725 RGBC Interrupt occured");
        
        //인터럽트 클리어 명령 전송
        err_code=tcs34725_int_clear(&tcs34725_instance);
        APP_ERROR_CHECK(err_code);
        
        //성공적으로 전송 완료시 출력
        if(err_code==NRF_SUCCESS)
        {
            NRF_LOG_INFO("TCS34725 Clear channel interrupt clear");
        }

    }
}

/*
- 인터럽트 클리어 함수
- 인터럽트 핀이 High->Low로 전환되며 인터럽트가 발생하는데 TCS34725에 인터럽트 클리어 명령어를 
  보내지 않는다면 인터럽트 핀이 Low 상태를 유지하므로 더 이상의 인터럽트가 발생하지 않는다
*/
ret_code_t tcs34725_int_clear(tcs34725_instance_t const * p_instance)
{
    ret_code_t err_code;
    
    /*
    - 커맨드 레지스터 사용
    - 0x66 : TYPE=11 (Special Function), ADDR/SF : 00110 (Clear channel interrupt clear)
    - 0x80 : CMD=1
    */
    uint8_t interrupt_cmd=0x66|0x80;
    err_code=nrf_twi_sensor_write(p_instance->p_sensor_data, p_instance->sensor_addr, &interrupt_cmd, 
                                  TCS34725_REGISTER_SIZE, true);
    return err_code;
}

 

https://github.com/lee35210/nRF52840_TWI_TCS34725

 

lee35210/nRF52840_TWI_TCS34725

nRF52840_TWI_TCS34725. Contribute to lee35210/nRF52840_TWI_TCS34725 development by creating an account on GitHub.

github.com

- nRF52840-DK 1번 버튼 : LED 토글, 2번 버튼 : 쓰레스홀드 인터럽트 활성화, 3번 버튼 : 쓰레스홀드 인터럽트 비활성화

- 핀 설정 : TWI SDA 28, SCL 29 / TCS34725 모듈 LED 제어 30 / TCS34725 인터럽트 감지 31

- APP TIMER에 설정한 시간마다 TCS34725 RGBC 레지스터에 액세스해 측정된 RGBC값 읽어 옴

[nRF52840, TWI를 사용한 MLX90614 제어]

https://rs29.tistory.com/16

 

nRF52 - TWI 이용 MLX90614 적외선 비접촉 온도 측정 센서

사용 보드 : nRF52840-PDK 사용 IDE : SEGGER Embedded Studio for ARM (노르딕 제품 사용하는 경우 무료 라이센스 이용 가능) 작동 전압 : 2.6~3.6V (MLX90614Bxx, MLX90614Dxx 모델 기준, MLX90614Axx은 4.5~5...

rs29.tistory.com

[개발 환경]

- 개발 보드 : nRF52840-PDK

- IDE : SEGGER Embedded Studio for ARM

- 센서 : MLX90614BCC, 통신 TWI (I2C) 사용

- 무선 통신 : BLE (UART 사용)

- SDK 예제 중 twi_master_using_nrf_twi_mngr(TWI 통신), ble_app_uart(BLE+UART 송수신), ble_app_hrs_freertos(FreeRTOS) 를 기반으로 작성

- 대부분의 코드(BLE, UART 등의 설정)는 예제 그대로 사용

- BLE 통신을 통해 명령어를 수신 및 실행하거나 온도, 방사율 값을 전송함

- 참고한 사이트 : https://www.freertos.org/a00106.html

 

[작동]

- 상시 작동할 태스크는 3개 (보드 0번 LED 토글, MLX90614 온도 측정, 측정된 온도 BLE 통신 전송)

- 각 태스크 후미에 사용한 vTaskDelay()에 입력한 시간 간격마다 LED 토글 및 MLX90614 Object 1 Temperature 레지스터 Read 함수 호출

- MLX90614로부터 온도 데이터를 수신한 직후 호출되는 콜백 함수에서 측정한 온도값을 전송할 BLE 전송 태스크에 알림 및 읽어 온 온도 데이터 전달 (BLE 전송 태스크는 if문을 사용해 알림을 수신하기 전까지는 BLE 전송이 실행되지 않음)

- BLE 전송 태스크에서 알림 및 데이터를 획득한 뒤, 온도 데이터를 섭씨 온도로 변환 및 BLE 통해 전송

 

- BLE 통신을 통해 MLX90614를 제어하는데 사용할 각종 명령어 및 명령어 데이터 수신

- 방사율을 전송할 경우, 그에 필요한 태스크들을 생성한 뒤 삭제 (지속적으로 갱신할 필요가 없는 데이터이기 때문)

 

[코드]

<FreeRTOS 태스크 핸들 생성>

static TaskHandle_t m_led_toggle_thread;		//nRF52840-DK LED0 토글
static TaskHandle_t m_mlx_rd_temp_thread;	//MLX90614 온도 Read
static TaskHandle_t m_mlx_rd_emi_thread;		//MLX90614 방사율 Read
static TaskHandle_t m_ble_mlx_temp_thread;	//읽어 온 온도를 BLE 통신을 사용해 전송
static TaskHandle_t m_ble_mlx_emi_thread;	//읽어 온 방사율을 BLE 통신을 사용해 전송

#if NRF_LOG_ENABLED
static TaskHandle_t m_logger_thread;	//Log 확인
#endif

- 태스크 핸들 생성 (태스크 생성, 중단, 삭제 등 API 호출 시 태스크를 참조하는 데 사용)

- sdk_config.h 에서 NRF_LOG_ENABLED 1 로 설정했을 경우, nrf_log module configuration 내에 활성화한 각종 드라이버 및 라이브러리들의 로그가 출력됨 (J-Link RTT Viewer 사용해 확인 가능)

 

<태스크 생성 함수>

static void task_create_func(void)
{
    if (pdPASS != xTaskCreate(mlx_rd_temp_thread, "MLX90614_RD_TEMP", configMINIMAL_STACK_SIZE+30, NULL, 2, &m_mlx_rd_temp_thread))
    {
        APP_ERROR_HANDLER(NRF_ERROR_NO_MEM);
    }

    if (pdPASS!=xTaskCreate(led_toggle_thread, "LED0", configMINIMAL_STACK_SIZE + 10, NULL, 2, &m_led_toggle_thread))
    {
        APP_ERROR_HANDLER(NRF_ERROR_NO_MEM);
    }

    if (pdPASS != xTaskCreate(ble_mlx_temp_thread, "BLE_MLX_TEMP", configMINIMAL_STACK_SIZE+30, NULL, 2, &m_ble_mlx_temp_thread))
    {
        APP_ERROR_HANDLER(NRF_ERROR_NO_MEM);
    }

//sdkconfig.h - NRF_LOG_ENABLED 1 로 활성화했을 때, Log 출력 태스크 생성
#if NRF_LOG_ENABLED
    if (pdPASS!=xTaskCreate(logger_thread, "LOGGER", 256, NULL, 1, &m_logger_thread))
    {
        APP_ERROR_HANDLER(NRF_ERROR_NO_MEM);
    }
#endif
}

태스크 생성 예)

- mlx_rd_temp_thread : 태스크를 실행할 함수명

- MLX90614_RD_TEMP : 태스크 설명 이름 (디버깅용)

- configMINIMAL_STACK_SIZE+30 : 태스크에 할당할 스택 사이즈

  (configMINIMAL_STACK_SIZE=60)

  (byte가 아닌 word 기준 (32bit MCU의 경우, 1 word=32bit), 스택 사이즈 90=360 bytes)

- NULL : 생성한 태스크에 전달 될 매개변수

- 2 : 태스크 우선 순위

- &m_mlx_rd_temp_thread : 태스크 핸들

 

<MLX90614 온도 Read 태스크>

static void mlx_rd_temp_thread(void *arg)
{
    mlx90614_reg_data_t obj_1_temp_struct={0,0,MLX90614_REG_OBJECT_1_TEMP};
    
    while(1)
    {
        mlx90614_reg_read(&mlx90614_instance,&obj_1_temp_struct,mlx90614_read_temp_cb);
        vTaskDelay(1000);
    }
}

- 센서로부터 온도 데이터를 읽어 오는데 사용할 구조체 변수 선언 및 Object 1 Temperature 레지스터 주소 설정

- MLX90614 레지스터 Read 함수 호출

- vTaskDelay(1000) : 1000ms 마다 태스크 실행

 

<MLX90614 온도 콜백 함수>

//MLX90614 - Object 1 레지스터 데이터 수신 완료 직후에 호출되는 함수
void mlx90614_read_temp_cb(ret_code_t result, mlx90614_reg_data_t * p_raw_data)
{
    uint8_t crc_value;
    
    if (result != NRF_SUCCESS)
    {
        NRF_LOG_WARNING("Read Temperature Callback Error : %d", (int)result);
        return;
    }

    if(p_raw_data->pec!=mlx90614_crc_cal(p_raw_data,MLX90614_Read))
    {
        NRF_LOG_WARNING("MLX90614 Read temperature CRC doesn't match");
        NRF_LOG_INFO("PEC : %X  CRC : %X",p_raw_data->pec,mlx90614_crc_cal(p_raw_data,MLX90614_Read));
    }
    else
    {
    	/*
    	TWI 통신을 통해 성공적으로 Object 1 레지스터에 저장된 온도 데이터를 수신 완료하고
        CRC 계산을 통과하면 BLE 통신을 통해 온도값을 전송하는 태스크에 알림 및 읽어 온 온도 데이터 전송
        m_ble_mlx_temp_thread : 알림을 수신할 태스크 생성에 사용된 태스크 핸들
        p_raw_data->reg_data : 전달할 데이터 (최대 32bit까지만 가능)
        eSetValueWithOverwrite : 전달될 데이터의 행동 설정 (무조건적으로 p_raw_data->reg_data 사용 설정)
        */
        xTaskNotify(m_ble_mlx_temp_thread, p_raw_data->reg_data, eSetValueWithOverwrite);
    }
}

- MLX90614의 온도 레지스터로부터 데이터를 읽어 온 직후 호출되는 함수

- 데이터 수신에 성공 및 CRC 계산값이 일치한다면 BLE 통신을 통해 온도값을 전송하는 태스크에 알림을 전송

- xTaskNotify의 경우, 최대 32bit의 데이터를 알림과 함께 전달 가능하므로 온도 데이터값을 가지고 있는 p_raw_data->reg_data의 값을 알림과 함께 전달

 

<BLE - 온도 전송 태스크>

/*
- xTaskNotify()를 통해 알림이 전달되었을 경우에만 전달된 데이터를 온도값으로 변환한 뒤
  BLE 통신을 통해 온도값을 전송하는 태스크
*/
static void ble_mlx_temp_thread(void *arg)
{
    ret_code_t err_code;
    char data_array[6]={};	//변환된 온도 데이터를 BLE 통신을 통해 전송할 때 사용할 배열
    				//정수 세 자리 + '.'+ 소수점 두자리 = 6
    double temperature;		//변환한 온도를 저장할 변수
    uint16_t temp;		//xTaskNotify를 통해 알림과 같이 전달된 온도 데이터(p_raw_data->reg_data)를 저장할 변수
    uint16_t length=sizeof(data_array);	//BLE 통신을 통해 전송할 데이터의 길이

    while(1)
    {
    	/*
    	- xTaskNotify()를 통해 전달된 알림이 있을 경우에만 ulTaskNotifyTake()를 통해 획득한
          데이터가 0이 아닌 값을 갖는다
        - 알림을 수신할 때까지 최대 10ms동안 Blocked 상태로 대기
          (Blocked 상태일 때 CPU 시간 소모 X)
        */
        if((temp=ulTaskNotifyTake(pdTRUE,10))!=0)
        {
            temperature=mlx90614_temp_conversion(temp);
            //UART 통신을 사용하므로 변환된 온도 데이터를 sprintf()함수를 이용해 문자열로 저장 후 전송
            sprintf(data_array,"%3.2f",temperature);
            
            //BLE 통신 연결이 된 상태 & MLX90614로부터 읽어 온 데이터가 유효할 때 온도값 전송
            //(MLX90614가 유효한 데이터를 측정하기 전에 데이터를 읽어 올 경우 온도 데이터=-273.15)
            if((m_conn_handle!=BLE_CONN_HANDLE_INVALID)&&(temperature!= -273.15))
            {
                err_code=ble_nus_data_send(&m_nus, data_array, &length, m_conn_handle);
                if ((err_code != NRF_ERROR_INVALID_STATE) &&
                    (err_code != NRF_ERROR_RESOURCES) &&
                    (err_code != NRF_ERROR_NOT_FOUND))
                {
                    APP_ERROR_CHECK(err_code);
                }
            }
        }
    }
}

 

<MLX90614 방사율 Read 태스크>

static void mlx90614_rd_emi_thread(void *arg)
{
    mlx90614_reg_data_t emissivity_struct={0,0,MLX90614_REG_EMISSIVITY_1};
    while(1)
    { 
        mlx90614_reg_read(&mlx90614_instance, &emissivity_struct, mlx90614_read_emissivity_cb);
        vTaskDelete(m_mlx_rd_emi_thread);
    }
}

- 방사율을 읽어 오는데 사용할 구조체 변수 선언 및 Emissivity 1 레지스터 주소 설정

- MLX90614 레지스터 Read 함수를 호출하고 태스크 삭제

- 방사율의 경우, 주기적으로 읽어 올 필요가 없기 때문에 필요한 경우에만 태스크를 생성 및 삭제

 

<MLX90614 방사율 콜백 함수>

void mlx90614_read_emissivity_cb(ret_code_t result, mlx90614_reg_data_t * p_raw_data)
{
    uint8_t crc_value;

    if (result != NRF_SUCCESS)
    {
        NRF_LOG_WARNING("Read Emissivity Callback Error : %d", (int)result);
        return;
    }    

    if(p_raw_data->pec!=mlx90614_crc_cal(p_raw_data,MLX90614_Read))
    {
        NRF_LOG_WARNING("MLX90614 Read emissivity CRC doesn't match");
        NRF_LOG_INFO("PEC : %X  CRC : %X",p_raw_data->pec,mlx90614_crc_cal(p_raw_data,MLX90614_Read));
    }
    else
    {   
        xTaskNotify(m_ble_mlx_emi_thread, p_raw_data->reg_data, eSetValueWithOverwrite);
    }
}

- 온도 콜백 함수와 작동 방식 동일

 

<BLE - 방사율 전송 태스크>

static void ble_mlx90614_emi_thread(void *arg)
{
    ret_code_t err_code;
    static char data_array[4]={NULL};	//정수 한자리 + '.' + 소수점 두자리 = 4
    static float emissivity;
    static uint16_t temp;
    static uint16_t length=sizeof(data_array);

    while(1)
    {
        if((temp=ulTaskNotifyTake(pdTRUE,(TickType_t)10))!=0)
        {
            emissivity=mlx90614_emissivity_conversion(temp);
            sprintf(data_array,"%.2f",emissivity);

            if((m_conn_handle!=BLE_CONN_HANDLE_INVALID))
            {
                err_code=ble_nus_data_send(&m_nus, data_array, &length, m_conn_handle);
                if ((err_code != NRF_ERROR_INVALID_STATE) &&
                    (err_code != NRF_ERROR_RESOURCES) &&
                    (err_code != NRF_ERROR_NOT_FOUND))
                {
                    APP_ERROR_CHECK(err_code);
                }
            }
            vTaskDelete(m_ble_mlx_emi_thread);
        }
    }
}

- 마지막에 vTaskDelete() 함수로 전송 완료 후 방사율 전송 태스크 삭제

- 나머지 부분은 온도 전송 태스크와 동일

 

<BLE를 통한 UART 송수신에 사용되는 함수>

- SDK 예제 중 ble_app_uart의 코드 활용

static void nus_data_handler(ble_nus_evt_t * p_evt)
{
    char str_cmd[]={"000"};	//수신한 명령어 저장
    char str_data[]={"000"};	//수신한 데이터 저장 (명령어+명령어에 필요한 데이터 조합)

    //BLE UART를 통해 데이터를 수신했을 때
    if (p_evt->type == BLE_NUS_EVT_RX_DATA)
    {
        uint32_t err_code;

        NRF_LOG_DEBUG("Received data from BLE NUS. Writing data on UART.");
        NRF_LOG_HEXDUMP_DEBUG(p_evt->params.rx_data.p_data, p_evt->params.rx_data.length);

        for (uint32_t i = 0; i < p_evt->params.rx_data.length; i++)
        {
            do
            {
                err_code = app_uart_put(p_evt->params.rx_data.p_data[i]);
                if ((err_code != NRF_SUCCESS) && (err_code != NRF_ERROR_BUSY))
                {
                    NRF_LOG_ERROR("Failed receiving NUS message. Error 0x%x. ", err_code);
                    APP_ERROR_CHECK(err_code);
                }
            } while (err_code == NRF_ERROR_BUSY);
        }
        if (p_evt->params.rx_data.p_data[p_evt->params.rx_data.length - 1] == '\r')
        {
            while (app_uart_put('\n') == NRF_ERROR_BUSY);
        }
        //수신한 데이터의 1~3번째 바이트를 명령어 배열(str_cmd)에 저장
        memcpy(str_cmd,p_evt->params.rx_data.p_data,3);
        //수신한 데이터의 4~6번째 바이트를 데이터 배열(str_data)에 저장
        if(3<p_evt->params.rx_data.length)
        {
            memcpy(str_data,&p_evt->params.rx_data.p_data[3],3);
        }
    }
    
    /*
    - 수신한 명령어가 있을 때, MLX90614 명령어 실행 함수 호출
    BLE 데이터 송신으로 핸들러 함수가 호출되었을 때는 str_cmd 배열의 값이 "000"을 유지하므로
    MLX90614에 명령어를 실행하는 함수가 실행되지 않는다
     */
    if(strcmp(str_cmd,"000")!=0)
    {
        mlx90614_cmd_func(str_cmd, str_data);
    }
}

- BLE를 통해 UART 방식의 통신을 전송, 수신했을 때 호출되는 핸들러 함수

- MLX90614를 제어하는데 사용할 각종 명령어 및 데이터를 수신 및 배열에 저장한 뒤 제어 함수 호출

 

<MLX90614 명령어 실행 함수>

void mlx90614_cmd_func(char *str_cmd, char *str_data)
{
    if(strcmp(str_cmd,"REM")==0)   //Read Emissivity
    {
        if (pdPASS != xTaskCreate(mlx90614_rd_emi_thread, "MLX90614_RD_EMI", configMINIMAL_STACK_SIZE+10, NULL, 2, &m_mlx_rd_emi_thread))
        {
            APP_ERROR_HANDLER(NRF_ERROR_NO_MEM);
        }
        if (pdPASS != xTaskCreate(ble_mlx90614_emi_thread, "MLX90614_BLE_RD_EMI", configMINIMAL_STACK_SIZE+30, NULL, 2, &m_ble_mlx_emi_thread))
        {
            APP_ERROR_HANDLER(NRF_ERROR_NO_MEM);
        }
    }
    else
    {
        
    }
}

- "REM" 문자열을 수신한 경우, MLX90614로부터 방사율을 읽어오는 태스크와 읽어 온 방사율 데이터를 BLE 통신을 통해 전송하는 태스크를 생성

 

<Led 토글 태스크>

static void led_toggle_thread(void * pvParameter)
{
    UNUSED_PARAMETER(pvParameter);
    while (true)
    {
        bsp_board_led_invert(BSP_BOARD_LED_0);
        vTaskDelay(1000);
    }
}

- SDK-blinky_freertos 예제에 있는 태스크

- vTaskDelay에 입력한 시간(ms) 간격으로 nRF52840-DK 보드의 0번 LED를 토글

 

<스택 크기 측정>

-예) MLX90614 Object 1 Register Read 태스크

static void mlx_rd_temp_thread(void *arg)
{
    mlx90614_reg_data_t obj_1_temp_struct={0,0,MLX90614_REG_OBJECT_1_TEMP};

    UBaseType_t uxHighWaterMark;
    uxHighWaterMark=uxTaskGetStackHighWaterMark(NULL);
    
    while(1)
    {
        mlx90614_reg_read(&mlx90614_instance,&obj_1_temp_struct,mlx90614_read_temp_cb);
        vTaskDelay(1000);
        uxHighWaterMark=uxTaskGetStackHighWaterMark(NULL);
        printf("mlx_rd_temp_thread remaining stack space : %d\r\n",uxHighWaterMark);
    }
}

- FreeRTOSConfig.h 에서 INCLUDE_uxTaskGetStackHighWaterMark 1 로 설정 필요

- 태스크를 생성하며 할당한 스택 크기 중 태스크가 실행된 이후 측정된 사용 가능 스택 잔량의 최소 크기를 반환

- 반환값이 0에 근접할 수록 스택 오버 플로우에 가까워짐

 

<스마트폰을 통한 온도 및 방사율값 수신>

- nRF Toolbox 애플리케이션 사용

- nRF52840-DK와 연결된 직후부터 BLE 통신을 통해 온도값이 수신되기 시작

- 온도 측정 중 "REM"(방사율 측정 및 수신) 명령어를 전송해 MLX90614에 현재 설정된 방사율을 BLE 통신을 통해 수신

- MLX90614로부터 방사율을 읽어오는 TWI 통신은 TWI transaction manager 라이브러리의 nrf_twi_mngr_schedule() 함수를 사용해 TWI 전송 큐에 등록되므로 온도값을 읽어 오는 중간에 사용되어도 온도값 수신에 영향을 미치지 않는다

- MLX90614로부터 방사율을 읽어오는데 사용되는 mlx90614_rd_emi_thread() 태스크는 mlx90614_reg_read() 함수를 호출한 이후에 삭제되므로 MLX90614로부터 저장된 방사율 데이터를 읽어오는 TWI 통신에 영향을 미치지 않는다

- BLE 방사율 전송 태스크 또한 알림을 획득하고 BLE 전송을 완료한 이후에만 태스크 삭제가 실행되므로 BLE 통신을 통한 방사율값 전송에 영향을 미치지 않는다

 

https://github.com/lee35210/nRF52840_BLE_FREERTOS_MLX90614

사용한 MLX90614ESF-BCC 모델 탑재 모듈

  • 사용 보드 : nRF52840-PDK
  • 사용 IDE : SEGGER Embedded Studio for ARM (노르딕 제품 사용하는 경우 무료 라이센스 이용 가능)
  • 작동 전압 : 2.6~3.6V (MLX90614Bxx, MLX90614Dxx 모델 기준, MLX90614Axx은 4.5~5.5V)
  • (사용 모듈의 경우, Low Dropout Voltage 레귤레이터 탑재로 3~5V에서 작동)
  • 로직 레벨 : Input High (Min : (VDD-0.1)V), Input Low (Max : 0.6V) (MLX90614Bxx, Dxx 기준)
  • 통신 방식 : PWM, SMBus Compatible 2-Wire Interface (10~100KHz)
  • Slave Address : 0x5A
  • SDK 내 예제 프로젝트를 기반으로 작성 (\examples\peripheral\twi_master_using_nrf_twi_mngr)
  • (SDK 다운로드 : https://developer.nordicsemi.com/)

[센서]

  • Power On Reset 이후, 0.25초 지난 후부터 유효 출력 (RAM내 데이터)
  • 주변 온도 -40°C~+125°C, 물체 온도 -70°C~+380°C 까지 측정 가능
  • 측정 정확도 : 0.5°C
  • 측정 해상도 : 0.02°C
  • PWM 출력의 경우, 0.14°C 해상도로 -20~120°C까지 온도 측정 가능
  • Factory calibrated 된 출력을 갖는다
  • 측정값은 FOV 내 모든 물체의 평균 온도값
  • 센서 장착 주변부에 센서를 뜨겁게 or 차갑게 만드는 요소가 있어선 안되고 뜨겁거나 차가운 측정 물체에 너무 가깝게 근접 시켜서도 안된다
  • xCx(ex:MLX90614BCC) 버전의 경우 내부적으로 측정된 열경사도와 측정된 온도를 보상에 사용한다
  • 이 경우 xCx버전은 열경사도에 덜 민감해지지만 영향이 완전히 없어진 상태는 아니다
  • 따라서 열경사도의 원인을 가능한 피하거나 원인으로부터 센서를 보호해주는 것이 중요하다
  • 물체 방사율 1에 대해 교정(Calibration)되어 있지만 재교정없이 0.1~1.0 사이의 방사율로 조정할 수 있다

[작동 방식]

 

STM32 - I2C 통신 MLX90614 적외선 비접촉 온도 측정 센서

작동 전압 : 2.6~3.6V 통신 방식 : PWM, I2C(10~100KHz) 주변 온도 -40°C~+125°C, 물체 온도 -70°C~+380°C 까지 측정 가능 측정 정확도 : 0.5°C 측정 해상도 : 0.02°C PWM 출력의 경우,..

rs29.tistory.com

 

[사전 설정]

- TWI 통신에 사용할 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

 

[코드]

- SDK 폴더 내 components\drivers_ext 에 위치한 CCS811, LPS22HB 드라이버 등을 바탕으로 작성됨

- mlx90614.c, mlx90614.h, mlx90614_internal.h 하나의 소스 파일, 두 개의 헤더 파일로 구성

 

- 매크로 선언

/*
main.c
*/

//MLX90614와의 통신에 사용할 TWI 인스턴스 ID
#define TWI_INSTANCE_ID_1 1

//TWI trasaction manager 인스턴스 Queue 최대 크기 (대기 전송 갯수)
#define MAX_PENDING_TRANSACTIONS 255

/*
- TWI transaction manager 인스턴스 정의 매크로
생성할 TWI transaction manager 인스턴스 이름, 트랜잭션 Queue 크기, 사용할 TWI 인스턴스
*/
NRF_TWI_MNGR_DEF(m_nrf_twi_mngr_1, MAX_PENDING_TRANSACTIONS, TWI_INSTANCE_ID_1);

/*
- 공통 TWI 센서 인스턴스 생성 매크로
공통 센서 인스턴스 이름, 사용할 TWI transaction manager 인스턴스, 버퍼 크기
(버퍼 크기의 경우 TWI manager Queue 크기보다 작거나 같아야 한다)
*/
NRF_TWI_SENSOR_DEF(sensor_instance_1, &m_nrf_twi_mngr_1, MAX_PENDING_TRANSACTIONS);

/*
- 센서 인스턴스 생성 매크로
센서 인스턴스 이름, 사용할 공통 TWI 센서 인스턴스, 센서 주소
*/
MLX90614_INSTANCE_DEF(mlx90614_instance, &sensor_instance_1, MLX90614_ADDR);
/*
mlx90614.h
*/

//MLX90614xCx 모델 사용하는 경우 주석 해제
//#define MLX90614xCx

//CRC-8 계산에 이용
#define MLX90614_Write  0
#define MLX90614_Read   1

//센서 주소
#define MLX90614_ADDR 0x5A

//통신에 사용할 TWI manager instance 에 요구하는 최소 Queue 크기
#define MLX90614_MIN_QUEUE_SIZE 4

//MLX90614 통신에 사용할 SDA, SCL PIN
#define MLX90614_SDA_PIN 28
#define MLX90614_SCL_PIN 29

//데이터 콜백 프로토타입
typedef void (* mlx90614_data_callback_t)(ret_code_t result, mlx90614_reg_data_t * p_raw_data);

//MLX90614 인스턴스 정의 매크로
#define MLX90614_INSTANCE_DEF(_mlx90614_inst_name, _p_twi_sensor, _sensor_address)                \
    MLX90614_INTERNAL_INSTANCE_DEF(_mlx90614_inst_name, _p_twi_sensor, _sensor_address)
/*
mlx90614_internal.h
*/

//전송할 데이터 바이트 크기 (송수신 동일. LSB+MSB+PEC)
#define MLX90614_REGISTER_BYTES 3

//MLX90614 RAM 및 EEPROM Register 주소
#define MLX90614_REG_AMBIENT_TEMP   0x06	//주변 온도
#define MLX90614_REG_OBJECT_1_TEMP  0x07	//물체 온도 1
#define MLX90614_REG_OBJECT_2_TEMP  0x08	//물체 온도 2
#define MLX90614_REG_EMISSIVITY_1 0x24	//방사율 주소
#define MLX90614_REG_EMISSIVITY_2 0x2F	//MLX90614xCx 모델을 사용할 경우에만 이용할 추가 방사율 주소
#define MLX90614_REG_SLEEP  0xFF	//슬립 모드 진입에 사용

/*
MLX90614 인스턴스 구조체
p_sensor_data : NRF_TWI_SENSOR_DEF을 통해 생성한 common twi sensor instance
sensor_addr : MLX90614_INSTANCE_DEF를 통해 MLX90614 인스턴스를 생성할 때 입력한 센서 주소
*/
typedef struct
{
    nrf_twi_sensor_t * const p_sensor_data;
    uint8_t const            sensor_addr;
} mlx90614_instance_t;


/*
센서 인스턴스 생성 매크로
*/
#define MLX90614_INTERNAL_INSTANCE_DEF(_mlx90614_inst_name, _p_twi_sensor, _sensor_address)\
    static mlx90614_instance_t _mlx90614_inst_name =                                     \
    {                                                                                    \
        .p_sensor_data = _p_twi_sensor,                                                  \
        .sensor_addr   = _sensor_address,                                                \
    }

- 구조체

/*
mlx90614.h
*/

/*
reg_data : MLX90614로부터 읽어 온 데이터를 저장하거나 MLX90614 EEPROM 레지스터에 입력할 데이터를 저장
	RAM, EEPROM 의 각 레지스터는 16bit 크기를 갖는다
pec : MLX90614로부터 읽어 온 데이터가 제대로 전송이 됐는지 확인하기 위한 Packet Error Code 
	CRC-8 계산 필요
reg_addr : 해당 변수 구조체가 접근할 MLX90614의 레지스터 주소를 저장하는 변수
	PEC 계산에도 이용된다 (단순 레지스터 주소 저장용일뿐. 데이터 전송에는 이용되지 않음)
구조체 변수를 통해 레지스터 데이터를 읽거나 쓸 때, reg_data의 LSB -> MSB -> pec 순으로 송수신 된다
MLX90614의 데이터 송수신은 LSB부터 시작되므로 reg_data 의 MSB, LSB 스왑은 필요없다
*/
typedef struct
{
    uint16_t reg_data;
    uint8_t pec;
    uint8_t reg_addr;
} mlx90614_reg_data_t;

- TWI 설정 및 초기화 (main.c)

//MLX90614와의 통신에 사용할 TWI 통신 설정
static void twi_config(void)
{
    uint32_t err_code;
    
    //통신에 사용할 핀, 속도, 인터럽트 우선 순위 설정
    nrf_drv_twi_config_t const config={
      .scl=29,
      .sda=28,
      .frequency=NRF_DRV_TWI_FREQ_100K,
      .interrupt_priority=APP_IRQ_PRIORITY_MID,
      };
    
    //앞서 생성한 설정을 바탕으로 TWI transaction manager instance 초기화
    err_code=nrf_twi_mngr_init(&m_nrf_twi_mngr_1,&config);
    APP_ERROR_CHECK(err_code);
}

- Register Data Read 함수

/*
mlx90614_instance_t : MLX90614_INSTANCE_DEF을 통해 생성한 MLX90614 센서 인스턴스
mlx90614_reg_data_t : 읽어 온 데이터를 저장할 센서 데이터 구조체
mlx90614_data_callback_t : 데이터 수신 완료 후 호출될 콜백 함수
*/
ret_code_t mlx90614_reg_read(mlx90614_instance_t const * p_instance,
                             mlx90614_reg_data_t *       p_reg_data,
                             mlx90614_data_callback_t    user_cb
                            )
{
    ASSERT(p_instance != NULL);
    ASSERT(p_reg_data != NULL);

    /*
    p_instance->p_sensor_data : NRF_TWI_SENSOR_DEF을 통해 설정된 공통 센서 인스턴스
    p_instance->sensor_addr : MLX90614 센서 인스턴스에 설정된 MLX90614 센서 주소
    p_reg_data->reg_addr : 전달받은 구조체에 저장된 레지스터 주소
    			해당 레지스터 주소에 접근해 데이터를 읽어온다
    nrf_twi_sensor_reg_cb_t : 데이터 수신 완료 후 호출 할 콜백 함수
    MLX90614_REGISTER_BYTES : 읽어 올 데이터의 바이트 크기 (uint8_t 기준)
    			(Data 16bit(LSB(8)+MSB(8)) + PEC(8bit)=24bit)                
    */
    return nrf_twi_sensor_reg_read(p_instance->p_sensor_data,
                                   p_instance->sensor_addr,
                                   p_reg_data->reg_addr,
                                   (nrf_twi_sensor_reg_cb_t) user_cb,
                                   (uint8_t *) p_reg_data,
                                   MLX90614_REGISTER_BYTES);
}
  • MLX90614 RAM, EEPROM의 레지스터 주소에 액세스해 레지스터에 저장된 16bit 데이터를 읽어오는 함수
  • nrf_twi_sensor_reg_read 함수는 nrf_twi_mngr_schedule 함수를 이용하므로 수신이 완료될 때까지 대기하는게 아니라 TWI transaction manager instance의 transaction queue에 데이터 수신을 대기시킨다
  • transaction queue에 대기해있던 데이터 수신이 완료된 이후에 콜백 함수가 호출된다
  • (콜백 함수 호출이 필요없다면 NULL 값을 입력)

- 온도 데이터, 방사율 데이터 변환 함수

//읽어 온 16bit 방사율 데이터를 데이터 시트에 나와있는 식을 통해 실제 방사율값으로 변환
inline float mlx90614_emissivity_conversion(uint16_t emissivity_val)
{
    return (emissivity_val+1)/65536.0;
}

//읽어 온 16bit 온도 데이터를 데이터 시트에 나와있는 식을 통해 실제 온도값으로 변환 (섭씨 기준)
inline double mlx90614_temp_conversion(uint16_t temperature_val)
{
    return (temperature_val*0.02)-273.15;
}
  • 콜백 함수에서 사용

- CRC-8 테이블 및 계산 함수

/*
속도면에서 필요할 때마다 CRC 값을 계산하는 방식보다 계산된 값이 저장된 테이블을 통해 
CRC 계산값을 찾는 것이 더 유리해 테이블 사용이 권장되므로 테이블 사용
*/
static const uint8_t crc8_table[256]=
{
    0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D,
    0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D,
    0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD,
    0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD,
    0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA,
    0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A,
    0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A,
    0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A,
    0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4,
    0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4,
    0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44,
    0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34,
    0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63,
    0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13,
    0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,
    0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3
};

/*
mlx90614_reg_data_t : 전달받은 구조체의 데이터값을 기반으로 PEC 계산
write_read : 읽어 온 데이터의 PEC 계산을 할 것인지 
		MLX90614에 입력할 PEC값을 계산할 것인지를 결정
*/
int mlx90614_crc_cal(mlx90614_reg_data_t * p_raw_data, uint8_t write_read)
{
    //CRC-8 계산 결과값을 저장할 변수
    uint8_t sum_val=0;

    /*
    읽어 온 데이터에 대한 CRC-8 계산을 순차적으로 실행하면서 sum_val 변수에 저장
    */
    if(write_read==MLX90614_Read)
    {
        uint8_t pec_array[5]={(MLX90614_ADDR<<1), p_raw_data->reg_addr, ((MLX90614_ADDR<<1)|1),
                            LSB_16(p_raw_data->reg_data), MSB_16(p_raw_data->reg_data)};
        for(int i=0; i<5; i++)
        {
            sum_val=crc8_table[(sum_val^pec_array[i])];
        }        
    }
    /*
    레지스터에 입력할 PEC값을 순차적으로 CRC-8 계산하면서 sum_val 변수에 저장
    */
    else if(write_read==MLX90614_Write)
    {
        uint8_t pec_array[4]={(MLX90614_ADDR<<1), p_raw_data->reg_addr, 
                            LSB_16(p_raw_data->reg_data), MSB_16(p_raw_data->reg_data)};
        for(int i=0; i<4; i++)
        {
            sum_val=crc8_table[(sum_val^pec_array[i])];
        }
    }
    //CRC-8 계산 결과 반환
    return sum_val;
}
  • nRF5 SDK는 CRC-16, CRC-32 계산 라이브러리만 제공
  • CRC-8 테이블은 http://www.sunshine2k.de/coding/javascript/crc/crc_js.html 사이트를 참고해 작성
  • MLX90614로부터 읽어 온 PEC는
  • (MLX90614 Address<<1+W(0)) + Register Address(Command) + (MLX90614 Address<<1+R(1)) + Register Data (LSB) + Register Data (MSB) 의 CRC-8 계산
  • MLX90614에 데이터를 쓸 때 함께 입력할 PEC는
  • (MLX90614 Address<<1+W(0)) + Register Address(Command) + Register Data (LSB) + Register Data (MSB) 의 CRC-8 계산

- 방사율 변경 함수

  • EEPROM으로부터 데이터를 읽어오거나 데이터를 쓸 경우엔 데이터 시트의 EEPROM 테이블에 나와있는 레지스터 주소에서 0x20 의 값을 더해 준 값을 사용해야 한다
  • ex) EEPROM 방사율 주소 = 0x04 번지, 실제 사용은 0x04 | 0x20 = 0x24 (CMD) 이용
  • MLX90614xCx 모델의 경우, EEPROM 0x0F 번지의 데이터도 변경해줘야 온도가 제대로 측정되는데 이 데이터는 0x04 번지의 데이터를 계산할 때와는 다르게 기존 0x0F 번지에 저장되어있던 값을 이용해 계산되므로 우선적으로 0x0F에 저장된 값을 따로 기록해 둘 것을 권장 (사용 모듈의 경우, 0x0F = 0x079A 인 상태였음)
  • EEPROM에 새로운 데이터를 입력하기 위해선 우선 해당 레지스터 주소에 0x0000 데이터를 입력해 기존 데이터를 지워줘야 한다
*
mlx90614_instance_t : MLX90614_INSTANCE_DEF을 통해 생성한 MLX90614 센서 인스턴스
new_emissivity : EEPROM에 입력할 새 방사율
*/
ret_code_t mlx90614_emissivity_write(mlx90614_instance_t const * p_instance,
                                    float new_emissivity
                                    )
{
    ASSERT(p_instance != NULL);

    //방사율은 0.1~1.0까지만 설정 가능
    if(new_emissivity<0.10 || 1.0<new_emissivity)
    {
        NRF_LOG_WARNING("Invaild Emissivity Value");
        return NRF_ERROR_INVALID_DATA;
    }

    //TWI transaction manager instance의 Queue 상태를 확인하기 위한 부울 변수
    bool idle_chk=false;

    //에러 코드 저장
    ret_code_t err_code;
    
    //레지스터에 새 방사율 데이터가 제대로 써졌는지 확인용
    uint16_t emissivity_chk_data;
    
    /*
    새 방사율값을 저장할 구조체
    reg_addr : 계산된 새 방사율값을 저장할 EEPROM 레지스터 주소 (0x04 | 0x20 = 0x24)
    reg_data : 데이터 시트에 나와있는 식을 통해 계산된 16비트 데이터
    pec : 데이터에 이어서 전송될 PEC 데이터
    	만약 제대로 계산된 PEC값이 전송되지 않는다면 EEPROM은 기존 데이터를 유지한다
    */
    mlx90614_reg_data_t new_emissivity_1;
    new_emissivity_1.reg_addr=MLX90614_REG_EMISSIVITY_1;
    new_emissivity_1.reg_data=(int)(round(65536*(new_emissivity)-1.0));
    new_emissivity_1.pec=mlx90614_crc_cal(&new_emissivity_1,MLX90614_Write);

    //0x04 번지에 새 방사율 데이터가 제대로 저장이 됐는지 확인하기 위해 계산된 새 방사율 데이터 저장
    emissivity_chk_data=new_emissivity_1.reg_data;

    /*
    EEPROM에 데이터를 쓰기 위해선 우선적으로 해당 레지스터에 0x0000 값을 갖는 16비트 데이터를 입력해야 한다
    0x0000 의 값을 전송할 구조체 선언 후 레지스터 주소 입력, PEC 계산값 저장
    */
    mlx90614_reg_data_t emissivity_zero;
    emissivity_zero.reg_addr=MLX90614_REG_EMISSIVITY_1;
    emissivity_zero.pec=mlx90614_crc_cal(&emissivity_zero,MLX90614_Write);

    /*
    - MLX90614xCx 모델을 사용하는 경우에만 실행되는 부분
    현재 EEPROM 0x04 번지에 입력되어있는 방사율 데이터가 필요하다
    */
    #ifdef MLX90614xCx
    
    //현재 EEPROM에 저장되어있는 0x04 번지의 방사율 데이터를 저장할 구조체 변수 선언 및 레지스터 주소 입력
    mlx90614_reg_data_t old_emissivity_1;
    old_emissivity_1.reg_addr=MLX90614_REG_EMISSIVITY_1;

    //old_emissivity_1 구조체에 EEPROM 0x04 (CMD 0x24) 번지에 저장된 현재 방사율값 읽어와 저장
    err_code=nrf_twi_sensor_reg_read(p_instance->p_sensor_data,
                           p_instance->sensor_addr,
                           MLX90614_REG_EMISSIVITY_1,
                           (nrf_twi_sensor_reg_cb_t) NULL,
                           (uint8_t *) &old_emissivity_1,
                           MLX90614_REGISTER_BYTES);

    //nrf_twi_sensor_reg_read 함수 실행 에러 발생시
    if (err_code != NRF_SUCCESS)
    {
        NRF_LOG_WARNING("MLX90614xCx : Old emissivity 1 read failed.");
        return err_code;
    }

    /*
    데이터 송수신에 사용하는 TWI Sensor module 라이브러리는 
    TWI trasaction manager 라이브러리의 nrf_twi_mngr_schedule 함수를 이용해 데이터를 전송한다
    때문에 old_emissivity_1 구조체에 레지스터로부터 읽어 온 데이터가 저장되기까지 대기를 한 후
    (=TWI transaction manager queue==idle 상태)
    CRC 값을 계산 및 읽어 온 PEC값과 비교한다
    */
    do
    {
        idle_chk=nrf_twi_mngr_is_idle(p_instance->p_sensor_data->p_twi_mngr);
        nrf_delay_us(10);
    }while(idle_chk!=true);
    
    //수신이 제대로 됐는지 확인을 위해 읽어 온 PEC 값과 CRC-8 계산 결과를 비교
    if(old_emissivity_1.pec!=mlx90614_crc_cal(&old_emissivity_1,MLX90614_Read))
    {
        NRF_LOG_WARNING("MLX90614xCx : Old emissivity 1 CRC doesn't match");
        return NRF_ERROR_INVALID_DATA;
    }
    #endif

	//모델 상관없이 공통적으로 진행 시작
    
    //0x0000의 데이터값을 먼저 전송해 방사율 레지스터 주소(0x04)의 데이터를 지운다
    err_code=nrf_twi_sensor_reg_write(p_instance->p_sensor_data,
                               p_instance->sensor_addr,
                               MLX90614_REG_EMISSIVITY_1,
                               (uint8_t *)&emissivity_zero,
                               sizeof(emissivity_zero)/sizeof(uint8_t)
                               );
    //nrf_twi_sensor_reg_write 함수 실행 에러 발생시
    if (err_code != NRF_SUCCESS)
    {
        NRF_LOG_WARNING("MLX90614  : Emissivity 1 erase failed");
        return err_code;
    }

    /*
    0x0000 데이터 입력 후, 새 방사율 데이터를 전송하기 위해선 최소 5ms, 권장 10ms의
    여유 시간이 필요하므로 0x0000 데이터 전송 완료까지 기다린 뒤에 딜레이 함수를 이용해 10ms 대기
    */
    do
    {
        idle_chk=nrf_twi_mngr_is_idle(p_instance->p_sensor_data->p_twi_mngr);
        nrf_delay_us(10);
    }while(idle_chk!=true);
    nrf_delay_ms(10);

    //데이터 시트에 나와있는 식을 통해 계산된 새 방사율값 EEPROM 0x04 번지에 전송
    err_code=nrf_twi_sensor_reg_write(p_instance->p_sensor_data,
                           p_instance->sensor_addr,
                           MLX90614_REG_EMISSIVITY_1,
                           (uint8_t*)&new_emissivity_1,
                           sizeof(new_emissivity_1)/sizeof(uint8_t)
                           );
    if (err_code != NRF_SUCCESS)
    {
        NRF_LOG_WARNING("MLX90614 : New emissivity 1 write failed");
        return err_code;
    }
    do
    {
        idle_chk=nrf_twi_mngr_is_idle(p_instance->p_sensor_data->p_twi_mngr);
        nrf_delay_us(10);
    }while(idle_chk!=true);
    
    //새 방사율 데이터를 레지스터에 쓴 이후에도 똑같이 최소 5ms, 권장 10ms의 대기가 필요하다
    nrf_delay_ms(10);

    /*
    EEPROM 0x04 번지에 저장된 방사율 데이터를 읽어와 앞서 emissivity_chk_data에 저장한 
    새 방사율 데이터와 비교해 EEPROM에 새 방사율 데이터가 제대로 입력됐는지 확인
    */
    err_code=nrf_twi_sensor_reg_read(p_instance->p_sensor_data,
                                   p_instance->sensor_addr,
                                   MLX90614_REG_EMISSIVITY_1,
                                   (nrf_twi_sensor_reg_cb_t) NULL,
                                   (uint8_t *) &new_emissivity_1,
                                   MLX90614_REGISTER_BYTES);
    if (err_code != NRF_SUCCESS)
    {
        NRF_LOG_WARNING("MLX90614 : New emissivity 1 read failed", err_code);
        return err_code;
    }
    /*
    TWI transaction manager instance의 queue에 데이터 수신이 대기된 상태로 존재하기 때문에
    수신 완료까지(Queue==0) 대기한 후에 데이터 비교를 진행한다
    */
    do
    {
        idle_chk=nrf_twi_mngr_is_idle(p_instance->p_sensor_data->p_twi_mngr);
        nrf_delay_us(10);
    }while(idle_chk!=true);
    
    //EEPROM 0x04 번지에 새 방사율 데이터가 제대로 입력됐는지 확인
    if(new_emissivity_1.reg_data!=emissivity_chk_data)
    {
        NRF_LOG_INFO("New Emissivity 1 data doesn't match");
        return NRF_ERROR_INVALID_DATA;
    }

    /*
    - MLX90614xCx 모델을 사용하는 경우에만 실행되는 부분
    MLX90614xCx 모델의 경우, 방사율 데이터를 변경할 때 EEPROM 0x04 번지의 데이터만 바꾸는 것이 아니라 
    0x0F 번지에 저장되어 있는 값도 데이터 시트에 나와있는 식을 통해 얻은 값으로 변경해야 
    방사율에 따른 옳바른 온도값이 측정된다
    */
    #ifdef MLX90614xCx
    
    /*
    앞서 0x04 번지의 데이터를 지우기 위해 선언 및 사용한 0x0000 데이터를 가진 구조체 변수에 
    0x0F 주소 입력 및 PEC값 재계산해 저장
    */
    emissivity_zero.reg_addr=MLX90614_REG_EMISSIVITY_2;
    emissivity_zero.pec=mlx90614_crc_cal(&emissivity_zero, MLX90614_Write);

    /*
    new_emissivity_2 : EEPROM 0x0F 번지에 쓸 새 방사율 데이터를 저장할 구조체 변수
    old_emissivity_2 : 현재 EEPROM 0x0F 번지에 저장된 방사율 데이터를 읽어와 저장할 구조체 변수
    */
    mlx90614_reg_data_t new_emissivity_2, old_emissivity_2;
    
    //EEPROM 0x0F 번지 주소 입력 (0x0F | 0x20 = 0x2F)
    new_emissivity_2.reg_addr=MLX90614_REG_EMISSIVITY_2;
    old_emissivity_2.reg_addr=MLX90614_REG_EMISSIVITY_2;

    //현재 EEPROM 0x0F 번지에 입력되어있는 방사율 데이터를 읽어와 old_emissivity_2 구조체 변수에 저장
    err_code=nrf_twi_sensor_reg_read(p_instance->p_sensor_data,
                           p_instance->sensor_addr,
                           MLX90614_REG_EMISSIVITY_2,
                           (nrf_twi_sensor_reg_cb_t) NULL,
                           (uint8_t *) &old_emissivity_2,
                           MLX90614_REGISTER_BYTES);
    if (err_code != NRF_SUCCESS)
    {
        NRF_LOG_WARNING("MLX90614xCx : Old emissivity 2 read failed");
        return err_code;
    }
    do
    {
        idle_chk=nrf_twi_mngr_is_idle(p_instance->p_sensor_data->p_twi_mngr);
        nrf_delay_us(10);
    }while(idle_chk!=true);
    if(old_emissivity_2.pec!=mlx90614_crc_cal(&old_emissivity_2,MLX90614_Read))
    {
        NRF_LOG_WARNING("Old emissivity 2 CRC doesn't match");
        return NRF_ERROR_INVALID_DATA;
    }

    /*
    기존에 저장되어있는 0x04 번지의 방사율 데이터(old_emissivity.reg_data)와 앞서 0x04 번지에 입력한
    새로운 방사율 데이터(new_emissivity_1.reg_data) 그리고 0x0F 번지에 저장되어있던 데이터
    (old_emissivity_2.reg_data)를 이용해 0x0F 번지에 입력될 새로운 데이터 값을 구한다
    */
    //0x0F 번지에 입력할 새로운 데이터값 계산 후 저장
    new_emissivity_2.reg_data=
	(round(((double)old_emissivity_1.reg_data/new_emissivity_1.reg_data)*old_emissivity_2.reg_data));
        
    //0x0F 번지에 전송될 PEC값을 CRC-8 계산 후 저장
    new_emissivity_2.pec=mlx90614_crc_cal(&new_emissivity_2,MLX90614_Write);
    
    //위에서 계산된 0x0F에 입력할 새 방사율 데이터를 데이터 확인용 변수에 저장
    emissivity_chk_data=new_emissivity_2.reg_data;
 
    //0x000 데이터를 먼저 전송해 0x0F 번지 데이터 지움
    err_code=nrf_twi_sensor_reg_write(p_instance->p_sensor_data,
                               p_instance->sensor_addr,
                               MLX90614_REG_EMISSIVITY_2,
                               (uint8_t *)&emissivity_zero,
                               sizeof(emissivity_zero)/sizeof(uint8_t)
                               );
    if (err_code != NRF_SUCCESS)
    {
        NRF_LOG_WARNING("MLX90614xCx : Emissivity 2 erase failed", err_code);
        return err_code;
    }
    do
    {
        idle_chk=nrf_twi_mngr_is_idle(p_instance->p_sensor_data->p_twi_mngr);
        nrf_delay_us(10);
    }while(idle_chk!=true);
    nrf_delay_ms(10);

    //0x0F 번지에 새롭게 계산된 방사율 데이터를 전송
    err_code=nrf_twi_sensor_reg_write(p_instance->p_sensor_data,
                           p_instance->sensor_addr,
                           MLX90614_REG_EMISSIVITY_2,
                           (uint8_t *)&new_emissivity_2,
                           sizeof(new_emissivity_2)/sizeof(uint8_t)
                           );
    if (err_code != NRF_SUCCESS)
    {
        NRF_LOG_WARNING("MLX90614xCx : New emissivity 2 write failed");
        return err_code;
    }
    do
    {
        idle_chk=nrf_twi_mngr_is_idle(p_instance->p_sensor_data->p_twi_mngr);
        nrf_delay_us(10);
    }while(idle_chk!=true);
    nrf_delay_ms(10);
	
    //새로운 방사율 데이터가 제대로 써졌는지 확인하기 위해 0x0F 번지에 저장된 데이터를 읽어 옴
    err_code=nrf_twi_sensor_reg_read(p_instance->p_sensor_data,
                           p_instance->sensor_addr,
                           MLX90614_REG_EMISSIVITY_2,
                           (nrf_twi_sensor_reg_cb_t) NULL,
                           (uint8_t *) &new_emissivity_2,
                           MLX90614_REGISTER_BYTES);
    if (err_code != NRF_SUCCESS)
    {
        NRF_LOG_WARNING("MLX90614xCx : New emissivity 2 read failed");
        return err_code;
    }
    do
    {
        idle_chk=nrf_twi_mngr_is_idle(p_instance->p_sensor_data->p_twi_mngr);
        nrf_delay_us(10);
    }while(idle_chk!=true);
	
    //0x0F 번지에 새 방사율 데이터가 제대로 써졌는지 확인
    if(new_emissivity_2.reg_data=!emissivity_chk_data)
    {
        NRF_LOG_INFO("New emissivity 2 data doesn't match");
        return NRF_ERROR_INVALID_DATA;
    }
    #endif
    
    return NRF_SUCCESS;
}

- Sleep 함수

/*
슬립 모드에 진입하기 위해선 Command=0xFF, PEC=0xE8 2바이트를 전송하면 된다
Command는 nrf_twi_sensor_reg_write의 세번째 인자로 쓰인 MLX90614_REG_SLEEP가 사용되므로
실제 전송할 바이트 크기는 PEC 1바이트가 된다
*/
ret_code_t mlx90614_sleep_enter(mlx90614_instance_t const * p_instance)
{
    uint8_t sleep_pec=0xE8;

    return nrf_twi_sensor_reg_write(p_instance->p_sensor_data,
                       p_instance->sensor_addr,
                       MLX90614_REG_SLEEP,
                       (uint8_t*)&sleep_pec,
                       sizeof(sleep_pec)/sizeof(uint8_t)
                       );
}

/*
슬립 모드에서 빠져나오기 위해선 SDA 핀을 33ms를 초과하는 시간동안 LOW로 유지해야 한다
이를 위해 MLX90614 와의 통신에 사용하던 TWI를 해제하고 SDA핀을 LOW 출력 설정으로 바꾼 후
딜레이 함수를 통해 34ms 동안 LOW 상태를 유지함으로 슬립 모드에서 탈출
이후에 다시 twi_config() 함수를 실행해 통신에 사용할 TWI를 재초기화해줘야 한다
하지 않을 경우, 센서와의 통신 불가
*/
void mlx90614_sleep_exit(mlx90614_instance_t const * p_instance, uint8_t sda_pin)
{
    bool idle_chk;
    
    //현재 TWI manager instance Queue에 대기해있는 전송이 완료될 때까지 대기
    do
    {
        idle_chk=nrf_twi_mngr_is_idle(p_instance->p_sensor_data->p_twi_mngr);
        nrf_delay_us(10);
    }while(idle_chk!=true);

	//TWI manager instance 해제
    nrf_twi_mngr_uninit(p_instance->p_sensor_data->p_twi_mngr);
    
    //SDA 핀 Output 설정 및 LOW 출력으로 설정
    nrf_gpio_cfg_output(sda_pin);
    nrf_gpio_pin_write(sda_pin,0);
    
    //34ms 동안 SDA=LOW 유지
    nrf_delay_ms(34);
}

- Sleep 실행 예시

1. 방사율, Object 1 온도 읽음

2. 슬립 모드 진입

3. 다섯 번의 Object 1 Read 실행

4. 슬립 모드 탈출

5. twi_config() 실행

6. Object 1 Read 시작

슬립 모드 진입 후 다섯번의 Object 1 Temperature 레지스터 접근 시도가 있었지만 실패
35ms 동안 SDA 핀 LOW 유지해 슬립 모드 탈출

소스 코드 : https://github.com/lee35210/nRF52840-MLX90614

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

 

 

SK6812 RGBW LED 스트립

  • 사용 보드 : nRF52840-PDK
  • 사용 IDE : SEGGER Embedded Studio for ARM (노르딕 제품 사용하는 경우 무료 라이센스 이용 가능)
  • 작동 전압 : 4.5~5.5V
  • 로직 인풋 레벨 (Din에 연결할) : -0.5~VDD+0.5
  • (3V 작동 전압 + 3V 로직 레벨로도 작동이 가능하다)
  • nRF52840-PDK 보드의 VDD(3V)를 통해 SK6812 전압 공급
  • 신호 전송에 사용될 GPIO Output high voltage = VDD 이므로 문제없이 구동된다
  • 특별히 사용 지정된 통신 방법은 없음
  • SDK 내 I2S 예제 프로젝트를 기반으로 작성 (\examples\peripheral\i2s)
  • (SDK 다운로드 : https://developer.nordicsemi.com/)

[동작]

<SPI를 통한 제어 시도>

<I2S를 통한 SK6812 RGBW LED 구동>

- SCK 설정

  • SK6812 모듈에 유효한 데이터 전송을 위해서는 0.3us, 0.6us, 0.9us (허용 오차 ±0.15us)의 크기를 갖는 데이터 블럭을 전송해야 한다
  • 데이터 1비트를 전송하는데 필요한 클럭(SCK)을 설정하기 위해선 다음과 같은 과정이 필요하다

nRF I2S

  1. 주어진 클럭(32Mhz)을 8~125 사이의 값으로 나눠서 Master Clock를 구함
    • MCK = 32Mhz / 8~125 (MCK : Master Clock)
  2. MCK를 32~512 사이의 값으로 나눠서 LRCK를 구함
    • LRCK = MCK / CONFIG.RATIO (32~512)
  3. 위의 과정을 통해 계산된 LRCK 값에 Sample Width 및 2를 곱한 값이 Serial Clock 값이 된다
    1. SCK = 2 * LRCK * CONFIG.SWIDTH (Sample Width)
  • 실제 사용 예시)
  • config.mck_setup = NRF_I2S_MCK_32MDIV10;
  • config.ratio = NRF_I2S_RATIO_32X;
  • config.sample_width = NRF_I2S_SWIDTH_16BIT;
    • MCK = 32 Mhz / 10 = 3.2 Mhz
    • LRCK = 3.2 / 32 = 0.1 Mhz
    • SCK = 2 * 0.1 * 16 = 3.2 Mhz
  • 따라서 1비트를 전송하는데는 0.3125us 시간이 걸린다

- 전송 데이터 구성

SK6812 데이터 인식 순서 (실제로는 RGBW가 아닌 GRBW 순으로 인식)

  • LED 각 색깔은 8개의 데이터(0~255 표현)로 구성되어있고 RGBW 네 개의 색을 가지고 있으므로 총 32개의 데이터가 전송되어야 한다
  • 하나의 데이터는 0 or 1로 표현할 수 있고 이 데이터를 표현하기 위해선 4개의 비트가 필요한다
  • 예) R0=0 이라면 1000 (0x8), R0=1이라면 1100 (0xC)로 구성됨

SK6812를 작동 시키는데 필요한 데이터 타이밍

  • 데이터의 0을 표현하기 위해선 0.3us 시간 동안 HIGH + 0.9 us 시간 동안 LOW 의 신호를 가져야 한다 (0x8, 1000)
  • 데이터의 1을 표현하기 위해선 0.6us 시간 동안 HIGH + 0.6 us 시간 동안 LOW 의 신호를 가져야 한다 (0xC, 1100)
  • 그러므로 하나의 색깔에 대한 전체 데이터는 4 bits * 8 = 32 bits 를 가지고 하나의 모듈을 구동하는데 필요한 데이터는 32 * 4(RGBW) = 128 bits 가 필요하다
  • 예) 빨간색 126 단계(0~255)를 표현하기 위해선 1000 0000 (R7 -> R0)의 데이터가 필요하고 이 데이터를 표현하기 위해서 0xC + 0x8 + 0x8 + ... + 0x8 (0xC + 0x8*7) 총 32비트를 사용

- I2S 전송 순서

CONFIG.FORMAT = Aligned, CONFIG.SWIDTH = 8Bit, CONFIG.CHANNELS = Stereo

  • L, R 데이터가 번갈아가며 전송된다 (그래프의 경우엔 SWIDTH=8 Bit로 L, R 각각은 8비트 데이터를 갖는다)
  • (SK6812에 전송할 신호 설정은 위 그래프 설정에서 SWIDTH만 16 Bit로 바꾼 상태)
  • 위의 설정(그래프 설정에서 SWIDTH만 16비트로 변경)에서 32 비트 신호를 전송하는 경우 15~0 비트가 LEFT, 31~16 비트가 RIGHT 채널에 해당한다
  • 예를 들어, Red=97을 설정하는 경우에 0110 0001 의 데이터가 필요하고 이 데이터는 0x8CC8 888C 로 구성된다
  • 그리고 이 데이터를 888C 8CC8 순서로 전송해야 제대로 된 밝기 단계가 설정된다
  • (만약 순서를 바꾸지 않고 전송한다면 R=22 로 설정된다)

- SK6812 내에서 데이터 처리

SK6812 LED에서 데이터를 받아들이는 방법

  • 그래프에서처럼 3개의 SK6812 LED에 데이터를 전송하는 경우, 첫 32개의 데이터는 SDOUT이 연결된 첫번째 LED에 그 다음 32개의 데이터는 두번째 LED, 마지막 32개 데이터는 세번째 LED에 전송된다
  • (LED에 전송된 데이터의 MSB부터 32개의 데이터(128Bit)만 남기고 나머지 부분만 다음 LED에 전송)
  • 때문에 특정 위치의 LED 색상을 바꾸기 위해 필요한 만큼의 데이터를 전송하는 것은 불가능하다
  • SK6812에 LED 데이터를 전송한 후, 다음 데이터를 전송하기 위해선 80us 시간 이상의 리셋 시간이 필요

[코드]

- 전역변수 및 정의

#define sk6812_led_num 1	//SK6812 LED 갯수 설정
#define sk6812_rst_buffer 8	//리셋, 32bit*8= 256bit = 0.3125us*256 = 80us
#define sk6812_buf_len sk6812_led_num*4+8	//리셋을 포함한 전송할 LED 데이터 총 길이(32비트 기준)

/*
하나의 색을 표현하는데 32비트 데이터가 필요하므로 unsigned 32bit integer 배열 선언
[0][0]=Green, [0][1]=Red, [0][2]=Blue, [0][3]=White 의 색상 정보를 저장
이중배열이지만 첫번째 행(sk6812_buffer[0][x])만 사용
*/
static uint32_t sk6812_buffer[2][sk6812_buf_len]={0};

static bool i2s_running=false;	//데이터 중복 전송을 피하는데 사용

- 구조체

/*
LED 색상 정보를 저장하고 버퍼에 반영하는데 사용
*/
typedef struct sk6812_rgbw{
  uint8_t red;
  uint8_t green;
  uint8_t blue;
  uint8_t white;
}sk6812_rgbw_struct;

- I2S 데이터 핸들러

/*
I2S 초기화와 함께 설정된 데이터 핸들러는 전송 직후 및 전송 완료 후 호출된다
*/
static void i2s_data_handler(nrf_drv_i2s_buffers_t const * p_released,
                         uint32_t                      status)
{
    static uint8_t transfer_chk=0;

    /*
    p_released - 이전에 드라이버에 전달된 버퍼에 대한 포인터
    전송이 처음 실행되어 호출됐을 땐, 전송된 데이터가 없기 때문에 p_released->p_tx_buffer=0이다
    전송 이후엔 p_released->p_tx_buffer엔 마지막으로 전송된 데이터 버퍼가 존재
    */
    if(p_released->p_tx_buffer!=0)	//전송이 이뤄진 뒤엔 p_tx_buffer엔 데이터가 존재하므로 실행됨
    {
      transfer_chk++;	//데이터가 전송됐다면 값 증가해 데이터 전송이 일어났음을 표시
    }

    if(transfer_chk!=0)	//데이터 전송이 일어난 이후
    {
      nrf_drv_i2s_stop();	//I2S 전송 중단
      i2s_running=false;	//I2S 전송 중이 아님을 표시
      transfer_chk=0;		//데이터 전송 여부 
    }
}

- I2S 데이터 전송 함수

/*
I2S를 통한 데이터 전송에 사용되는 함수. 데이터는 DMA를 통해 전송된다
*/
void sk6812_send_buffer()
{
  uint32_t err_code = NRF_SUCCESS;
  
  //I2S 구조체 변수 선언 및 전송 버퍼에 전송할 데이터가 있는 SK6812 버퍼 주소 입력
  nrf_drv_i2s_buffers_t const i2s_buffers = {
      .p_tx_buffer=sk6812_buffer[0],
  };

  //I2S 통신이 사용 중이 아니라면 전송 실행
  if(i2s_running==false)
  {
    err_code = nrf_drv_i2s_start(&i2s_buffers, sk6812_buf_len, 0);
    //sk6812_buffer[0]부터 sk6812_buf_len 길이만큼 전송 (32bit 기준)
    
    APP_ERROR_CHECK(err_code);
    i2s_running==true;	//중복 전송이 일어나지 않게 하기 위해 전송 중임을 나타내는 true로 변경
  }
}

- SK6812 색상 설정 함수

/*
전달받은 색상 구조체 변수와 LED 위치(index)를 기반으로 
버퍼의 원하는 부분에 LED 색상 데이터를 설정함
*/
void sk6812_set_color(sk6812_rgbw_struct* set_color, uint8_t index)
{
  uint32_t temp_g=0, temp_r=0, temp_b=0, temp_w=0;
  //전달받은 구조체 색상값을 SK6812에서 인식하는 데이터로 변환 후 저장하는데 사용할 32비트 변수
  
  /*
  8비트 색상값의 MSB부터 0비트 자리로 비트 시프트 한 뒤에 0x1을 AND 연산
  나온 값이 1이면 0xC, 0이면 0x8을 32비트 temp_ 변수의 MSB부터 차례대로 입력
  */
  for(uint8_t i=0;i<8;i++)	//Green
  {
    if((set_color->green>>(7-i))&0x1==1)
    {
      temp_g|=0xC<<(7-i)*4;
    }
    else
    {
      temp_g|=0x8<<(7-i)*4;
    }

    if((set_color->red>>(7-i))&0x1==1)	//Red
    {
      temp_r|=0xC<<(7-i)*4;
    }
    else
    {
      temp_r|=0x8<<(7-i)*4;
    }

    if((set_color->blue>>(7-i))&0x1==1)	//Blue
    {
      temp_b|=0xC<<(7-i)*4;
    }
    else
    {
      temp_b|=0x8<<(7-i)*4;
    }

    if((set_color->white>>(7-i))&0x1==1)	//White
    {
      temp_w|=0xC<<(7-i)*4;
    }
    else
    {
      temp_w|=0x8<<(7-i)*4;
    }
  }
  
  /*
  I2S 통신은 L, R이 번갈아가면서 전송되고 L, R 각 크기는 SWIDTH 설정에 의해 결정됨
  SWIDTH=16 Bit 로 설정해 사용하므로 16비트 간격으로 L, R이 나뉘어 전송된다
  LEFT는 15~0비트, RIGHT는 31~16비트를 차지하는데 LEFT 먼저 전송이 시작되므로
  제대로 된 전송을 위해선 LEFT<->RIGHT 비트 위치 전환을 해야 한다 (MSB->LSB 순으로 전송)
  */
  sk6812_buffer[0][index*4]=((temp_g&0x0000FFFF)<<16)|((temp_g&0xFFFF0000)>>16);
  sk6812_buffer[0][index*4+1]=((temp_r&0x0000FFFF)<<16)|((temp_r&0xFFFF0000)>>16);
  sk6812_buffer[0][index*4+2]=((temp_b&0x0000FFFF)<<16)|((temp_b&0xFFFF0000)>>16);
  sk6812_buffer[0][index*4+3]=((temp_w&0x0000FFFF)<<16)|((temp_w&0xFFFF0000)>>16);

  sk6812_send_buffer();	//SK6812 버퍼 전송 함수 호출
  nrf_delay_ms(1);
}

- 메인

/*
SDK Example 폴더 내 I2S 프로젝트 기반
*/
int main(void)
{
    uint32_t err_code = NRF_SUCCESS;
    APP_ERROR_CHECK(err_code);

    //I2S 설정
    nrf_drv_i2s_config_t config = NRF_DRV_I2S_DEFAULT_CONFIG;	//I2S 환경설정 구조체 변수 선언
    config.sdout_pin = I2S_SDOUT_PIN;	//SDOUT PIN 설정 (예제 기준 P0.27)
    config.mck_setup = NRF_I2S_MCK_32MDIV10;	(MasterClock = 32Mhz / 10)
    config.ratio     = NRF_I2S_RATIO_32X;
    config.sample_width=NRF_I2S_SWIDTH_16BIT;	(L, R 각 채널이 갖는 데이터 비트 크기)
    config.channels  = NRF_I2S_CHANNELS_STEREO;	(스테레오 설정)
    
    //I2S 환경설정 변수와 데이터 핸들러를 기반으로 I2S 통신 초기화
    err_code = nrf_drv_i2s_init(&config, i2s_data_handler);	
    APP_ERROR_CHECK(err_code);

    while(true)
    {  

    }
}

- 단순 사용 예시) 8개의 LED를 R->G->B->W->R 순서로 색상 변환 (각 색상의 밝기는 128 단계로 설정)

#define sk6812_led_num 8

int main(void)
{
    uint32_t err_code = NRF_SUCCESS;
    APP_ERROR_CHECK(err_code);

    //I2S 설정
    nrf_drv_i2s_config_t config = NRF_DRV_I2S_DEFAULT_CONFIG;	//I2S 환경설정 구조체 변수 선언
    config.sdout_pin = I2S_SDOUT_PIN;	//SDOUT PIN 설정 (예제 기준 P0.27)
    config.mck_setup = NRF_I2S_MCK_32MDIV10;	(MasterClock = 32Mhz / 10)
    config.ratio     = NRF_I2S_RATIO_32X;
    config.sample_width=NRF_I2S_SWIDTH_16BIT;	(L, R 각 채널이 갖는 데이터 비트 크기)
    config.channels  = NRF_I2S_CHANNELS_STEREO;	(스테레오 설정)
    
    //I2S 환경설정 변수와 데이터 핸들러를 기반으로 I2S 통신 초기화
    err_code = nrf_drv_i2s_init(&config, i2s_data_handler);	
    APP_ERROR_CHECK(err_code);

    //SK6812 색상 설정에 사용할 색상 구조체 변수 선언
    sk6812_rgbw_struct temp_rgbw;
    
    //모든 색상에 대해 0의 값을 설정
    temp_rgbw.red=0;
    temp_rgbw.green=0;
    temp_rgbw.blue=0;
    temp_rgbw.white=0;
    
    //모든 LED 버퍼에 0의 색상값 입력 후 전송, i=LED 순서(SDOUT이 연결된 LED가 시작점)
    for(uint8_t i=0;i<sk6812_led_num;i++)
    {
      sk6812_set_color(&temp_rgbw,i);
    }

    int PAUSE_TIME=1000;
    
    /*
    모든 LED에 동일한 색상을 지정한 뒤 전송하고 PAUSE_TIME 시간동안 정지된 후 
    다음 색상으로 변환 (빨강 -> 초록 -> 파랑 -> 흰색 순으로 무한 루프)
    */
    while(true)
    {  
        temp_rgbw.red=128;
        temp_rgbw.green=0;
        temp_rgbw.blue=0;
        temp_rgbw.white=0;
        for(uint8_t i=0;i<sk6812_led_num;i++)
        {
          sk6812_set_color(&temp_rgbw,i);
        }
        nrf_delay_ms(PAUSE_TIME);

        temp_rgbw.red=0;
        temp_rgbw.green=128;
        temp_rgbw.blue=0;
        temp_rgbw.white=0;
        for(uint8_t i=0;i<sk6812_led_num;i++)
        {
          sk6812_set_color(&temp_rgbw,i);
        }
        nrf_delay_ms(PAUSE_TIME);

        temp_rgbw.red=0;
        temp_rgbw.green=0;
        temp_rgbw.blue=128;
        temp_rgbw.white=0;
        for(uint8_t i=0;i<sk6812_led_num;i++)
        {
          sk6812_set_color(&temp_rgbw,i);
        }
        nrf_delay_ms(PAUSE_TIME);

        temp_rgbw.red=0;
        temp_rgbw.green=0;
        temp_rgbw.blue=0;
        temp_rgbw.white=128;
        for(uint8_t i=0;i<sk6812_led_num;i++)
        {
          sk6812_set_color(&temp_rgbw,i);
        }
        nrf_delay_ms(PAUSE_TIME);
    }
}

FOR 문 하나에 해당하는 데이터 전송 신호 (8번의 LED 전체 색상 데이터 전송)
8개의 LED 색상 데이터에 해당하는 데이터 신호 (32*4*8=1024 Bit)
LED 8개의 색상 데이터 전송 중 첫번째 LED에 Blue=128가 입력

  • I2S 전송 함수(sk6812_send_buffer())를 색상 설정 함수(sk6812_set_color) 내부에 넣었기 때문에 하나의 FOR문이 실행될 때 LED 갯수인 8개만큼의 전체 버퍼 전송이 이루어진다
  • 단순히 사용의 편의를 위해 I2S 전송 함수를 색상 설정 함수 내부에 넣은 것이므로 전송 함수를 색상 설정 함수에서 제외하고 LED 색상 버퍼의 데이터를 전부 바꾼 뒤에 따로 전송 함수를 호출하는 식으로 사용해도 된다
/*
사용 LED 4개, sk6812_set_color() 함수 내의 I2S 전송 함수 제외한 뒤 따로 사용 예
SK6812에 전송할 데이터 버퍼의 색상 정보 전체를 원하는 값으로 바꾼 후에 I2S 전송 함수 호출
첫번째 LED : Red 128, 두번째 LED : Green 128, 세번째 LED : Blue 128, 네번째 LED : White 128 의 값을 가짐
*/
temp_rgbw.red=128;
temp_rgbw.green=0;
temp_rgbw.blue=0;
temp_rgbw.white=0;
sk6812_set_color(&temp_rgbw,0);	//첫번째 LED 색상 버퍼 변경

temp_rgbw.red=0;
temp_rgbw.green=128;
temp_rgbw.blue=0;
temp_rgbw.white=0;
sk6812_set_color(&temp_rgbw,1);	//두번째 LED 색상 버퍼 변경

temp_rgbw.red=0;
temp_rgbw.green=0;
temp_rgbw.blue=128;
temp_rgbw.white=0;
sk6812_set_color(&temp_rgbw,2);	//세번째 LED 색상 버퍼 변경

temp_rgbw.red=0;
temp_rgbw.green=0;
temp_rgbw.blue=0;
temp_rgbw.white=128;
sk6812_set_color(&temp_rgbw,3);	//네번째 LED 색상 버퍼 변경

sk6812_send_buffer();

 

+ Recent posts