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

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

 

 

사용한 MAX7219 7-sement 모듈

  • 통신 방법 : SPI (최대속도 : 10Mhz)
  • 작동 전압 : 4.0~5.5
  • 환경 : P-NUCLEO-WB55 개발 보드, Atollic TrueSTUDIO

[동작]

타이밍 다이어그램
데이터 포맷

  • 16 비트 데이터 포맷 사용 (D15~D12 사용 X)
  • D11~D8 ADDRESS(명령어 역할), D7~D0 DATA(MSB to LSB, 해당 명령어 설정값)
  • 작동 시작 됐을 때, 모든 컨트롤 레지스터는 리셋, 디스플레이는 비어있는 상태이고 MAX7219는 셧다운 모드에 진입한다
  • 디스플레이를 사용하기 전에 디스플레이 드라이버를 프로그래밍해야한다
  • 그렇지 않으면, 첫번째 자리를 스캔하도록 설정되고 데이터 레지스터의 데이터를 디코딩하지 않으며 밝기 레지스터는 최소값으로 설정된다

- 레지스터 어드레스 맵

  • MAX7219에 전달할 16비트 데이터 중 상위 8비트에 사용되는 어드레스 목록 (명령어 역할)

- 디코드 모드

디코드 모드 레지스터

  • 디코드 모드 레지스터는 BCD 코드 B(0-9, E, H, L, P, -)를 설정하거나 각 자리에 대해 디코드 미사용 설정
  • 레지스터의 각 비트는 디스플레이 숫자 자리에 해당 (D0=디스플레이 첫째 자리, D1=둘째 자리, ...)
  • 로직 하이(1)는 Code B 디코딩을 선택하고 로직 로우(0)는 디코더를 우회한다
  • 코드B 디코드 모드가 사용될 때, 디코더는 숫자 레지스터 데이터의 하위 니블(D3-D0)만을 보고 D4-D6 비트는 무시한다
  • 소수점을 설정하는 D7은 디코더 설정에 대해 독립적이고 로직 1일 때 소수점이 켜진다
  • 디코드 미사용 설정을 한 뒤, 해당 디스플레이 자리의 데이터 비트 D7-D0은 MAX7219의 세그먼트 라인에 해당
  • (ex : 0x0900 을 전송해 디스플레이 모든 자리를 디코드 미사용으로 설정하고 0x0130 (Digit 0+Segement Line B,C)을 전송하면 디스플레이 첫번째 자리에 세그먼트 라인 B, C가 켜진다)

숫자 레지스터 데이터 / 왼쪽 : 디코드 사용, 오른쪽 : 디코드 미사용
디코드 미사용 모드에서 세그먼트 라인 설정

- 밝기 조절

밝기 강도 레지스터

  • 디스플레이 밝기는 Intensity 레지스터를 통해 디지털 방식으로 조절할 수 있다
  • 디스플레이 밝기의 디지털 컨트롤은 intensity 레지스터의 하위 니블로 제어되는 내부 PWM을 통해 제공된다
  • 모듈레이터 스케일은 RSET에 의해 설정된 최대 전류의 최대 31/32에서 1/32까지 16단계로 평균 세그먼트 전류를 조정한다

- 스캔 리미트 레지스터

스캔 리미트 레지스터

  • 스캔 리미트 레지스터는 디스플레이의 1~8번째 자리 중 몇개의 자리를 사용할지를 설정한다
  • 스캔된 숫자의 갯수는 디스플레이 밝기에 영향을 주기 때문에 스캔 리미트 레지스터를 디스플레이의 여백 부분으로 사용해서는 안된다 (선두의 의미없는 0을 감추는 것처럼)
  • 표에서 보이는 것처럼 두번째 이상의 자리를 사용하면서 하위 자리의 디스플레이를 미사용하는 것은 불가능하다
  • (사용 설정을 한 뒤, 해당 자리를 공백으로 채우는 것과는 다름)

- 디스플레이 테스트 레지스터

디스플레이 테스트 레지스터

  • 디스플레이 테스트 레지스터는 테스트 모드, 노멀 모드 설정을 할 수 있다
  • 디스플레이 테스트 모드는 모든 컨트롤 및 자리 레지스터(셧다운 레지스터 포함)를 변경이 아닌 재정의 해서 모든 LED를 켠다
  • 디스플레이 테스트 모드에선 8자리가 스캔되고 31/32 듀티 사이클을 가진다
  • 디스플레이 테스트 레지스터가 노멀 동작으로 재설정되기 전까지는 테스트 모드 유지

- No-Op 레지스터

  • No-Op 레지스터는 MAX7219를 직렬로 연결할 때 사용
  • 모든 디바이스의 LOAD/CS 인풋을 묶어 연결하고 DOUT을 인접한 디바이스의 DIN에 연결한다
  • DOUT은 CMOS 로직 레벨 출력으로 연속적으로 직렬 연결된 파트의 DIN을 손쉽게 구동한다
  • 사용 예) 네 개의 MAX 7219가 직렬 연결되어있고 네 번째 칩에 데이터를 쓰기 위해 원하는 16비트 워드를 전송한 다음 3개의 비작동 코드를 전송한다
  • LOAD/CS 가 HIGH가 될 때, 모든 디바이스의 데이터가 잠긴다.
  • 처음 세 개의 칩은 No-Op 명령어를 수신하고 네번째 디바이스는 목표한 데이터를 수신한다

[CUBE MX 설정]

SPI 설정 / 데이터 사이즈 16 Bits, 보드 레이트 8.0Mbits/s, Hardware NSS Output Signal 사용
SPI 인터럽트 활성화

  • STM32WBXX 의 경우, SPI1 보드 레이트는 APB2 Peripheral Clock을 기준으로 정해진다
  • 디폴트 설정인 APB2 Peripheral clock 32Mhz, SPI1 Prescaler 4 의 값으로 SPI1 보드 레이트는 8.0Mbits/s
  • MAX7219는 데이터를 수신하는 역할만 하므로 Transmit Only Master 선택
  • NSS - CS 역할 (Master일 경우에만 Output 사용 가능, 사용 보드의 경우 NSS핀으로 PA4 자동 사용 설정)
  • (Hardware NSS를 사용하지 않고 GPIO를 소프트웨어 컨트롤해 CS 라인을 제어해도 된다)

[코드]

- 정의

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

#define max7219_SPI hspi1		//MAX7219 통신에 사용되는 SPI를 사용하기 편하게 재정의
//HAL_SPI_Transmit_IT(&max7219_SPI, ...) = HAL_SPI_Transmit_IT(&hspi1, ...) (SPI1 사용)

#define max7219_No_Op_Mode 0x00	//No-Op 모드

#define max7219_Decode_Mode 0x09	//디코드 모드

#define max7219_Intensity_Mode 0x0A	//디스플레이 밝기 모드

#define max7219_Scan_Limit_Mode 0x0B	//스캔 리미트 모드

#define max7219_Display_Test_Mode 0x0F	//디스플레이 테스트 모드
#define max7219_Display_Test_Enter 1	//테스트 모드 진입
#define max7219_Display_Test_Exit 0		//테스트 모드 종료, 노멀 모드로 전환

#define max7219_Shutdown_Mode 0x0C	//셧다운 모드
#define max7219_Shutdown_Enter 0		//셧다운 모드 진입
#define max7219_Shutdown_Exit 1		//셧다운 모드 종료, 노멀 모드로 전환

#define max7219_blank 0x0F			//디코드 모드에서 디스플레이 공백 표현에 사용
/* USER CODE END PD */

- 명령 함수

/*
명령어와 명령어에 따른 설정값을 받아와 전송하는 함수
*/
static void max7219_cmd(int cmd, int data)	//cmd=ADDRESS(명령어 역할), data=명령어 설정값
{
	uint16_t write_buffer=0;	//MAX7219에 전송할 16비트 변수
    
	write_buffer=(cmd<<8)|data;		
	//전달받은 명령어는 상위 8비트, 명령어에 따른 설정값은 하위 8비트에 저장
    
	HAL_SPI_Transmit_IT(&max7219_SPI,(uint8_t *)&write_buffer,1);	//MAX7219에 전송

	while(HAL_SPI_GetState(&max7219_SPI)==HAL_SPI_STATE_BUSY_TX)
	{
		//전송 완료까지 대기
	}
}
  • 16비트 포맷을 사용해야 하므로 MAX7219에 전송할 16비트 변수를 선언
  • 명령어와 명령어에 따른 설정값을 받아와서 앞서 선언한 16비트 변수의 상위 8비트에는 명령어(ADDRESS)를 하위 8비트에는 데이터(명령어 설정값)을 입력한다
  • MAX7219의 모든 기능이 동일한 데이터 수신 방법을 사용하므로 이 함수만으로도 모든 기능을 사용할 수 있다

- 숫자 입력 함수

/*
숫자를 입력할 자리와 입력할 데이터를 받아와 전송하는 함수
단순히 명령어를 입력하는 것과 디스플레이에 표현할 숫자를 입력하는 것을 구분 짓기 위해 만든 것으로
데이터 전달 방식에서 max7219_cmd() 함수와의 차이는 없다
*/
static void max7219_write_value(int digit, int value, int decimal_point)
//digit=숫자를 입력할 자리(1~8), value=입력할 데이터, 소수점 표현 (0=미사용, 1=사용)
{
	uint16_t write_buffer=0;	//MAX7219에 전달할 16비트 변수 선언
    
    /*
    디코드 모드를 사용할 때, (-, E, H, L, P, ' ')은 아스키 코드값으로 10이 넘는 인티저 값을 갖는 것을 이용
    (value!=0x20) 은 디코드 미사용할 때, 세그먼트 라인 B를 표기하는데 문제가 생기는 것을 막기 위함
    (' ' 과 동일한 값을 같기 때문에 사용자가 0x20을 입력하면 ' ' 취급되어 실제론 0x0F가 입력된다)
    */
	if((20<value)&&(value!=0x20))
	{
		char w_char=(char)value;
		switch(w_char)
		{
			case '-' :
				value=0x0A;	//디코드 모드에서 '-' 를 표현하기 위한 레지스터 데이터 값
				break;
			case 'e' :
			case 'E' :
				value=0x0B;
				break;
			case 'h' :
			case 'H' :
				value=0x0C;
				break;
			case 'l' :
			case 'L' :
				value=0x0D;
				break;
			case 'p' :
			case 'P' :
				value=0x0E;
				break;
			case ' ' :
				value=0x0F;
				break;
			default :
				break;
		}
	}
	write_buffer=((digit+1)<<8)|value;
	//디스플레이의 자릿수는 0x01부터 시작해 0x08까지 사용

	if(decimal_point==1)
	{
		write_buffer|=0x80;
	}
	//소수점을 사용할 경우, 7번째 비트를 High(1)로 설정

	HAL_SPI_Transmit_IT(&max7219_SPI,(uint8_t *)&write_buffer,1);
	while(HAL_SPI_GetState(&max7219_SPI)==HAL_SPI_STATE_BUSY_TX)
	{

	}
}
  • 단순히 명령어 전달과 디스플레이 표현을 구분지어 사용하기 위해 선언한 함수
  • 전달하는 데이터의 크기 및 데이터 구조는 명령 함수와 동일하다
  • 소수점 표현의 경우, 7번 비트를 로직 High(1)로 설정해주면 된다
  • ex ) 디스플레이 2번째 자리에 2. 을 표현하기 위해선 (디스플레이 첫번째 자리 = Digit 0 이라 가정)
  • 상위 8비트 : 디스플레이 4번째 자리 Digit 3 = 0x04 (Digit0=0x01 부터 시작 (0x00 (X))
  • 하위 8비트 : 숫자 2 = 0x02, 소수점 = 0x70 => 0x72

- 초기화 예시

int main(void)
{
  	...
  max7219_cmd(max7219_Shutdown_Mode,max7219_Shutdown_Exit);	//셧다운 모드 종료, 노멀 작동 시작
  max7219_cmd(max7219_Scan_Limit_Mode,0x07);	//스캔 리미트 모드, 0x07=디스플레이 8자리 모두 사용
  max7219_cmd(max7219_Decode_Mode, 0xFF);	//디코드 모드, 디스플레이 8자리 모두 디코드 모드로 설정
  max7219_cmd(max7219_Intensity_Mode, 0x08);	//디스플레이 밝기, 중간으로 설정 (0x00~0x0F 16단계)
  
}
  • 작동이 시작 됐을 때  MAX7219는 셧다운 모드에 진입한다고 되있었으므로 셧다운 모드 종료를 먼저 실행
  • 디스플레이를 사용하기 전에 프로그래밍을 하지 않을 경우, 첫째 자리를 스캔하도록 설정되고 데이터 레지스터의 데이터를 디코딩하지 않으며 밝기 레지스터는 최소값으로 설정되므로
  • 스캔 리미트 모드에서 디스플레이 8자리 모두 사용 설정 (0x00~0x07)
  • 디코드 모드에서 디스플레이 8자리 모두 디코드 모드 사용 설정 (데이터 비트=디스플레이 자릿수, 1=디코드 사용)
  • 디스플레이 밝기 모드에서 16단계 중 9단계로 설정

- 단순 사용 예시

int main(void)
{
	...
  /* USER CODE BEGIN 2 */
  
  max7219_cmd(max7219_Shutdown_Mode,max7219_Shutdown_Exit);
  max7219_cmd(max7219_Scan_Limit_Mode,0x07);
  max7219_cmd(max7219_Decode_Mode, 0x00);	//디스플레이 모든 자리 디코드 미사용
  max7219_cmd(max7219_Intensity_Mode, 0x08);
  
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    
	  max7219_cmd(max7219_Decode_Mode, 0);	//모든 자리 디코드 미사용
      
	  for(uint8_t i=0;i<8;i++)	//디스플레이 모든 자리 공백으로 초기화
	  {
	  	max7219_write_value(i,0,0);
	  	//디코드 모드 공백 : 0x0F, 디코드 미사용 공백 : 0x00
  	  }
  
 	  for(int i=0;i<8;i++)
  	  {
	  	  int k=0;
	  	  for(int j=0;j<8;j++)
	  	  {
		  	  max7219_write_value(i,k|=(0x01)<<j,0)	
		  	  //비트0 부터 MSB로 1비트씩 비트 시프트 및 OR 연산으로 해당 자리의 모든 세그먼트 라인을 켬
		  	  HAL_Delay(100);
	  	  }
	  }
      
      
	  max7219_cmd(max7219_Decode_Mode,0xFF);	//모든 자리 디코드 사용
      
	  for(uint8_t i=0;i<8;i++)
	  {
		  max7219_write_value(i,max7219_blank,0);	
		  //디스플레이 모든 자리 공백으로 초기화 (DATA : 0x0F)
	  }
      
	  for(uint8_t i=0;i<8;i++)
	  {
		  max7219_write_value(i,'-',1);	//디스플레이 -. (소수점 표기 사용)으로 채움
		  HAL_Delay(500);
	  }
      
	  for(uint8_t i=0;i<8;i++)
	  {
		  max7219_write_value(i,i,0);	//디스플레이 자릿수와 일치하는 숫자로 채움
		  HAL_Delay(500);
	  }
	  max7219_write_value(7,'H',0);
	  HAL_Delay(500);
	  max7219_write_value(6,'E',0);
	  HAL_Delay(500);
	  max7219_write_value(5,'L',0);
	  HAL_Delay(500);
	  max7219_write_value(4,'P',0);
	  HAL_Delay(500);
  }
  /* USER CODE END 3 */
}
  • 디스플레이 모든 자리 사용, 디코드 미사용, 밝기 9단계로 초기화
  • 디스플레이 모든 자리를 공백으로 초기화 (디코드 미사용일 때 공백을 표현하기 위한 데이터값은 0x00)
  • 디스플레이 0번부터 7번까지 순차적으로 세그먼트 라인을 하나씩 채워나감
  • 디스플레이 모든 자리를 디코드 사용으로 바꾼 뒤 공백으로 초기화 (디코드 모드 공백 데이터 : 0x0F)
  • (디코드 사용/미사용 전환을 해도 각 자리의 레지스터 데이터는 유지된 상태로 존재하기 때문에 디코드 사용/미사용에 따라 디스플레이 표현만 변한다)
  • 디스플레이 0번부터 7번까지 순차적으로 '-.' 문자를 표시
  • 디스플레이 자리와 동일한 숫자값을 디스플레이 0번부터 7번까지 순차적으로 표현
  • 디스플레이 7번부터 4번까지 순차적으로 H, E, L, P 문자 데이터 입력

디스플레이의 5번째 자리에 숫자 5를 입력했을 때

  • 디스플레이 자리는 0x01(=0번째)부터 시작하므로 상위 8비트 0x06은 Digit 5(다섯번째 자리)를 ADDRESS로 사용했음을 의미한다
  • 하위 8비트는 0x05이므로 디스플레이 다섯번째 자리에 숫자 5가 표현된다

MLX90614ESF-BCC 모델을 탑재한 모듈

  • 작동 전압 : 2.6~3.6V
  • 통신 방식 : PWM, I2C(10~100KHz)
  • 주변 온도 -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 사이의 방사율로 조정할 수 있다

[작동]

  • 측정된 온도값은 RAM내 지정된 주소에 16비트 16진수 데이터로 저장된다
  • 명령어를 통해 RAM, EEPROM에 저장된 각종 데이터들을 읽거나 필요에 따라 변경할 수 있다
  • 작동 시작 후 첫번째 데이터는 0.25초 뒤에 얻을 수 있다
  • 각 주소는 16bit의 데이터를 갖는다
  • 읽기/쓰기 둘 다 LSByte(16비트 데이터의 하위 8비트), MSByte(16비트 데이터의 상위 8비트) 순으로 데이터 전송이 진행된다

- EEPROM

EEPROM 주소표

  • EEPROM 내에 제한된 주소들만이 변경 가능하지만 전체 주소를 읽는 것은 가능하다
  • 원하는 주소에 값을 쓰기 전에 반드시 해당 주소에 0을 입력해 기존 데이터를 지워준 뒤 원하는 값을 입력해야 한다
  • 0x04 방사율의 경우, 0.1~1.0 까지 조정 가능하고 변경에 따른 재교정은 하지 않아도 된다
  • Factory calibration 을 유지하기 위해 특별한 도구 없이는 다음 비트와 레지스터를 변경해서는 안된다
  • Emissivity [15...0]; Config Register1 [14...11;7;3]; addresses 0x0F and 0x19

- RAM

RAM 주소표

  • 쓰기는 불가능하고 제한된 램 레지스터만 읽는 것이 가능하다
  • 0x06 - 주변 온도, 0x07 - 물체 온도1, 0x08 - 물체 온도2 가 저장된다

- Command

명령어표

  • RAM, EEPROM의 접근을 원하는 경우, x xxxx 부분에 Read/Write를 원하는 주소를 입력한다
  • (ex : RAM Obj1 온도 : 0x07, EEPROM Emissivity(방사율) : 0x24)

- Read

데이터 읽기 예) 램 0x07 주소에 저장된 Object1 온도 측정값을 읽어올 때

  • 센서 주소 : 0x5A (MSB부터 주소가 채워져야 하므로 MSB로 1비트 시프트 한 값이 0xB4)
  • 사용자가 수신할 데이터는 LSByte, MSByte, PEC 총 3 바이트
  • SDA 라인에서 첫번째 바이트는 0xB4(0x5A<<1+W(0))
  • 두번째 바이트는 사용자가 입력한 명령어
  • Repeated Start Condition 이후 세번째 바이트는 센서 주소+Read 비트=0xB5 (0x5A<<1+R(1))
  • 네번째 해당 주소의 LSByte, 다섯번째 해당 주소의 MSByte (LSB가 먼저 오기 때문에 이후에 순서를 바꿔줘야 한다)
  • 마지막 여섯번째 바이트는 0xB4(첫번째 바이트)~MSByte까지 총 5바이트를 CRC-8 계산을 통해 얻은 CRC값

-Write

데이터 쓰기 예) EEPROM 0x02 주소(PWMCTRL)에 데이터를 입력할 때

  • EEPROM에만 데이터 쓰기가 가능하다
  • 원하는 주소에 데이터를 쓰려면 반드시 해당 주소에 0x0000(LSByte(0x00)+MSByte(0x00))을 먼저 입력해 기존 16비트 데이터를 지운 뒤에 원하는 데이터를 입력해야 한다
  • 0x0000을 입력하고 최소 5ms(권장 10ms) 기다린 뒤 원하는 16비트 데이터를 입력하고 다시 5ms(권장 10ms) 기다린 후에 제대로 값이 입력됐는지 Read 명령어 통해 확인
  • 사용자가 전송할 데이터는 Command, LSByte, MSByte, PEC 총 4바이트
  • 첫번째 바이트는 센서 주소 0x5A<<1 + W(0) = 0xB4
  • 두번째 바이트는 명령어 (위의 경우 EEPROM 액세스 001x xxxx + PWMCTRL에 데이터를 쓸 것이므로 0x02 -> 0010 0000 + 0000 0010 = 0010 0010 = 0x22 가 된다)
  • 세번째 바이트는 입력할 16비트 데이터의 하위 8비트
  • 네번째 바이트는 입력할 16비트 데이터의 상위 8비트
  • 다섯번째 바이트는 첫번째 0xB4 바이트부터 네번째 MSByte까지 총 4바이트를 CRC-8 계산을 통해 얻은 CRC 값

[CUBE MX 설정]

  • 센서와의 통신에 사용할 I2C 설정 및 데이터 검증에 CRC-8 사용하므로 CRC 사용 설정

 Standard Mode(100KHz) 사용
인터럽트 활성화
CRC 설정 (8-bit, X2+X1+X0, Init Value=0)

[코드]

- 사용에 필요한 함수들 정의

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include <math.h>	//pow 함수 사용
/* USER CODE END Includes */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define mlx90614xCx	//MLX90614xCx 타입을 사용하지 않는 경우 마스크 처리 필요
#define MLX90614_addr 0x5A	// MLX90614 Slave Address
#define mlx_cmd_pointer 1	// 수신, 전송 버퍼내 명령어(CMD)를 입력할 배열 위치
#define mlx_cmd_amb 0x06	// Ta(Ambient Temperature) Address (RAM)
#define mlx_cmd_obj_1 0x07	// Tobj1(Object1 Temperature) Address (RAM)
#define mlx_cmd_obj_2 0x08	// Tobj2(Object1 Temperature) Address (RAM)
#define mlx_cmd_emissivity 0x24	// Emissivity Address (EEPROM)
#define mlx_cmd_emissivity_2 0x2F	// MLX90614xCx 버전에 한해 방사율을 변경했을 때 함께 조정해줘야 하는 레지스터 (EEPROM)
/* USER CODE END PD */

- 전역 변수 선언

/* USER CODE BEGIN PV */
uint8_t mlx_read_buffer[6]={0xB4,0x00,0xB5,0};	//ADDR((0x5A<<1)+W(0)), CMD, ADDR((0x5A<<1)+R(1)), LSByte, MSByte, PEC
uint8_t mlx_write_buffer[5]={0xB4,0};	//ADDR((0x5A<<1)+W(0)), CMD, LSByte, MSByte, PEC
uint8_t mlx_write_zero[5]={0xB4,0};	//ADDR((0x5A<<1)+W(0)), CMD, LSByte, MSByte, PEC
/* USER CODE END PV */
  • Read/Write에 사용할 전역 변수 배열 선언
  • 용이한 PEC 계산을 위해 Read/Write에서 PEC에 사용되는 데이터의 갯수만큼 배열 선언
  • Read의 경우 첫번째, 세번째 배열은 0xB4, 0xB5의 값을 사용하므로 그에 맞춰 배열 선언 및 초기화
  • Write의 경우 첫번째 바이트만 0xB4로 고정되므로 그에 맞춰 배열 선언 및 초기화

- 측정된 온도를 읽어오는 함수

void mlx_read_temperature(void)
{
	double temp_obj_1, temp_obj_2, temp_amb; //물체1, 물체2, 주변 온도를 저장할 변수
	uint8_t crc_obj_1,crc_obj_2,crc_amb;	 //물체1, 물체2, 주변 온도 CRC값 저장할 변수

	/*
	Ambient temperature(센서 주변 온도), RAM-0x06
	*/
	mlx_read_buffer[mlx_cmd_pointer]=mlx_cmd_amb;
	//데이터 버퍼의 CMD 부분에 데이터 수신에 사용할 명령어 입력 (단순 PEC계산용)
    
	HAL_I2C_Mem_Read_IT(&hi2c1,(uint16_t)MLX90614_addr<<1,mlx_cmd_amb,I2C_MEMADD_SIZE_8BIT,
			(uint8_t *)&mlx_read_buffer[3],3);
	/*
	인터럽트 사용해 데이터 수신, 슬레이브의 특정 메모리 주소로부터 데이터를 읽어오는 함수
	사용할 I2C, 슬레이브 주소(MSB로 1비트 시프트 필요), 접근할 내부 메모리 주소(CMD), 내부 메모리 주소 크기, 
	수신할 데이터 저장할 포인터 주소(mlx_read_buffer[3]), 수신할 데이터 갯수(mlx_read_buffer[3]~[5])
	*/
    
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	//수신 완료(I2C Ready 상태)까지 대기
    
	temp_amb=(mlx_read_buffer[4]<<8|mlx_read_buffer[3]);
	/*
	16비트로 구성된 센서 데이터가 하위 8비트->상위 8비트 순으로 수신되므로 mlx_read_buffer[4]에
	저장된 MSByte를 MSB로 8비트 시프트 한 뒤 LSByte(mlx_read_buffer[3])와 합친다
	*/
    
	temp_amb=(temp_amb*0.02)-273.15;
	//10진수로 변환한 온도 측정값을 섭씨 온도로 변환하는 과정이 필요하다
	//Ta[°C] = (Tareg*0.02)-273.15	//(Ta register*0.02)=켈빈온도
    
	crc_amb=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_read_buffer,5);
	//mlx_read_buffer에 저장된 0xB5, 0x07(CMD), 0xB4, LSByte, MSByte를 CUBE MX를 통해 설정한
	//값들을 기준으로 CRC값을 계산한다
    
	if(crc_amb==mlx_read_buffer[5])
	{
		printf("Ambient Temperature : %.2lf\n",temp_amb);
        //센서로부터 수신한 PEC(Packet Error Code, mlx_read_buffer[5])와 계산한 CRC값이 일치하는 경우 출력
	}
	else
	{
    		//PEC값과 CRC계산값이 일치하지 않는 경우
	}
	
	/*
	Object 1 temperature, RAM-0x07
	*/
	mlx_read_buffer[mlx_cmd_pointer]=mlx_cmd_obj_1;
	HAL_I2C_Mem_Read_IT(&hi2c1,(uint16_t)MLX90614_addr<<1,mlx_cmd_obj_1,I2C_MEMADD_SIZE_8BIT,
    	(uint8_t *)&mlx_read_buffer[3],3);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	temp_obj_1=(mlx_read_buffer[4]<<8|mlx_read_buffer[3]);
	temp_obj_1=(temp_obj_1*0.02)-273.15;
	crc_obj_1=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_read_buffer,5);

	if(crc_obj_1==mlx_read_buffer[5])
	{
		printf("Obj 1 Temperature : %.2lf\n",temp_obj_1);
	}
	else
	{
    		//PEC값과 CRC계산값이 일치하지 않는 경우
	}

	/*
	Object 2 temperature, RAM-0x07
	*/
	mlx_read_buffer[mlx_cmd_pointer]=mlx_cmd_obj_2;
	HAL_I2C_Mem_Read_IT(&hi2c1,(uint16_t)MLX90614_addr<<1,mlx_cmd_obj_2,I2C_MEMADD_SIZE_8BIT,
    	(uint8_t *)&mlx_read_buffer[3],3);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	temp_obj_2=(mlx_read_buffer[4]<<8|mlx_read_buffer[3]);
	temp_obj_2=(temp_obj_2*0.02)-273.15;
	crc_obj_2=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_read_buffer,5);

	if(crc_obj_2==mlx_read_buffer[5])
	{
		printf("Obj 2 Temperature : %.2lf\n",temp_obj_2);
	}
	else
	{
    		//PEC값과 CRC계산값이 일치하지 않는 경우
	}
}

 

  • 센서로부터 온도값을 읽어오는 함수
  • 값을 읽어오는데 사용할 데이터 버퍼의 형태는
  • {0xB4, 0x00, 0xB5, 0x00, 0x00, 0x00} //(센서주소<<1+W(0), CMD, 센서주소<<1+R(1), LSByte, MSByte, PEC)
  • 용이한 CRC 계산을 위해 첫번째 0xB4, 세번째 0xB5는 값을 고정한 상태로 두고 두번째 0x00(CMD)는 사용할 명령어에 따라 변경. 그리고 나머지 배열 세칸에 센서로부터 수신한 데이터를 저장한 뒤 [0]~[4]까지 CRC 계산
  • RAM에 저장된 Ambient Temperature, Object1 Temperature, Object2 Temperature 데이터의 수신 및 처리 방식은 명령어(CMD)를 제외하고는 전부 동일

- 방사율(Emissivity) 변경 함수

void mlx_change_emissivity(float new_emissivity)
{
	/*
	방사율(Emissivity) 변경은 EEPROM 내 0x04 번지의 16비트 데이터를 변경하는 것으로 이뤄질 수 있다
	(xCx 모델의 경우 0x0F 번지의 데이터 변경도 필요하다)
	*/
    
	uint8_t crc_emissivity, crc_emissivity_2;	
	/*
	MLX90614xCx 센서의 경우, 방사율을 변경할 때 EEPROM - 0x04(Emissivity)에 더해
	0x0F 번지에 저장된 데이터값도 변경해야 되기 때문에 0x04, 0x0F CRC 값을 저장하는 변수를 두개 생성
	*/
    
	uint16_t new_04, new_0f;//변경할 방사율에 따른 계산값을 0x04, 0x0F 번지에 저장하는데 사용할 변수
	double old_04,old_0f;	//기존에 저장된 0x04, 0x0F 주소의 데이터를 저장할 변수 (xCx 타입에만 필요한 부분)

	printf("New E : %.2lf\r\n",new_emissivity);	//사용자가 입력한 새 방사율값 출력
	new_04=round(65536*(new_emissivity)-1.0);	
	//데이터 시트에 나와있는 공식을 사용해 EEPROM-0x04 번지에 저장할 새 방사율값 계산
	printf("New E (Calculated) : %d, %X\r\n",new_04,new_04);	//계산된 새 방사율값 출력

/*
MLX90614xCx 타입을 사용하는 경우 0x0F 번지의 데이터 변경도 필요하므로 아래의 과정들이 필요하다
xAx, xBx 타입을 사용하는 경우 사용할 필요가 없는 부분
*/
#ifdef mlx90614xCx
	//EEPROM - 0x04 번지에 저장된 현재 방사율값을 읽어온다 (새 방사율값 계산에 필요, xCx 모델만 필요한 부분)
	mlx_read_buffer[mlx_cmd_pointer]=mlx_cmd_emissivity;
	HAL_I2C_Mem_Read_IT(&hi2c1,(uint16_t)MLX90614_addr<<1,mlx_cmd_emissivity,I2C_MEMADD_SIZE_8BIT,
    	(uint8_t *)&mlx_read_buffer[3],3);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	crc_emissivity=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_read_buffer,5);
	old_04=((mlx_read_buffer[4]<<8)|mlx_read_buffer[3]);
	if(mlx_read_buffer[5]==crc_emissivity)	//CRC 계산값과 수신한 PEC값 비교
	{
		printf("Old 0x04 : %d, %X\r\n",(int)old_04,(int)old_04);	
		//계산된 CRC값과 수신한 PEC값이 일치, 현재 0x04 번지에 저장된 방사율 출력
	}
	else	
	{
    		//계산된 CRC값과 수신한 PEC값이 일치하지 않는 경우
	}

	//EEPROM - 0x0F 번지에 저장된 새로운 방사율값을 계산하는데 필요한 데이터를 읽어온다 (xCx 모델만 필요한 부분)
	mlx_read_buffer[mlx_cmd_pointer]=mlx_cmd_emissivity_2;
	HAL_I2C_Mem_Read_IT(&hi2c1,(uint16_t)MLX90614_addr<<1,mlx_cmd_emissivity_2,I2C_MEMADD_SIZE_8BIT,
    	(uint8_t *)&mlx_read_buffer[3],3);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	crc_emissivity_2=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_read_buffer,5);
	old_0f=((mlx_read_buffer[4]<<8)|mlx_read_buffer[3]);
	if(mlx_read_buffer[5]==crc_emissivity_2)	//CRC 계산값과 수신한 PEC값 비교
	{
		printf("Old 0x0F : %d, %X\r\n",(int)old_0f,(int)old_0f);
		//계산된 CRC값과 수신한 PEC값이 일치, 현재 0x0F 번지에 저장된 데이터 출력
	}
	else	
	{
    		//계산된 CRC값과 수신한 PEC값이 일치하지 않는 경우
	}
	
	new_0f=round((old_04/new_04)*old_0f);	//xCx 타입만 해당 (xAx, xBx는 이 변수 자체가 필요 없음)
	//제조사에서 제공하는 데이터 시트에 나와있는 식을 기반으로 EEPROM-0x0F 번지에 저장할 값 계산
    printf("New 0x0F : %d, %X\r\n",new_0f,new_0f);
    
#endif

	/*
	EEPROM에 데이터를 쓰기 전에 우선적으로 데이터를 쓸 주소에 0x000 값을 입력해
	저장된 16비트 데이터를 지워야 할 필요가 있다
	mlx_write_zero 배열의 첫번째 바이트만 0xB4 (센서 주소0x5A<<1+W(0), PEC 계산에 사용)로 고정해 두고
	mlx_write_zero[1] - CMD(필요에 따라 변경, PEC 계산뿐만이 아니라 실제 전송에서 CMD로 사용된다)
	mlx_write_zero[2], [3] - LSByte, MSByte (0x0000을 입력해야되므로 둘 다 0x00의 값을 가진다)
	mlx_write_zero[4] - PEC (0xB4, CMD를 CRC 계산한 결과)
	mlx_write_zero[1]~mlx_write_zero[4]까지의 배열이 센서로 전송된다
	*/
	mlx_write_zero[1]=mlx_cmd_emissivity;	//Write CMD, 0x24(0010 0000 + 0x04)
	mlx_write_zero[4]=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_write_zero,4);  //[0]~[3] CRC 계산 결과
    
	/*
	-센서에 전송될 데이터 버퍼 설정, mlx_write_buffer[0] - 0xB4로 고정 (단순 PEC 계산용)
	mlx_write_buffer[1] - CMD 입력 (실제 전송에서 CMD로 사용 및 PEC 계산에 사용됨) 
	mlx_write_buffer[2] - 새로 계산한 방사율의 LSByte 저장
	mlx_write_buffer[3] - 새로 계산한 방사율의 MSByte 저장
	mlx_write_buffer[4] - mlx_write_buffer[0]~[3]까지의 CRC 계산값 저장
	mlx_write_buffer[1]~mlx_write_buffer[4]까지의 배열이 센서로 전송된다
	*/
	mlx_write_buffer[1]=mlx_cmd_emissivity;	//0x24(0010 0000 + 0x04) (EEPROM - 0x04 (Emissivity)
	mlx_write_buffer[2]=new_04&0xff;	//LSByte
	mlx_write_buffer[3]=(new_04>>8)&0xff;	//MSByte
	mlx_write_buffer[4]=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_write_buffer,4);

	printf("New_04 : %d, %X, %X, %X	crc : %X\r\n",new_04,new_04,mlx_write_buffer[2],mlx_write_buffer[3],mlx_write_buffer[4]);
    
	HAL_I2C_Master_Transmit_IT(&hi2c1,MLX90614_addr<<1,(uint8_t *)&mlx_write_zero[1],4);
	//쓰기를 원하는 주소에 먼저 0x0000 16비트 데이터를 써서 현재 저장된 값을 지운다
	//값을 지울 때도 PEC를 같이 전송해야 한다 (CMD, 0x00, 0x00, PEC)
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	//I2C Ready 상태까지 대기
	HAL_Delay(10);
	//딜레이 사용해 쓰기 완료 후 10ms 대기 (최소 5ms, 권장 10ms)
    
	HAL_I2C_Master_Transmit_IT(&hi2c1,MLX90614_addr<<1,(uint8_t *)&mlx_write_buffer[1],4);
	//CMD, 계산된 방사율을 상/하 8비트씩 나눈 값과 계산된 PEC값을 전송
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	HAL_Delay(10);
	//딜레이 사용해 쓰기 완료 후 10ms 대기 (최소 5ms, 권장 10ms)

	/*
	EEPROM - 0x0F 주소에 데이터 전송
	xCx 타입만 해당되는 사항으로 new_0f=round((old_04/new_04)*old_0f); 를 통해 얻은 16비트 데이터를
	0x0F 주소에 전송한다. 전송 순서 및 방법은 0x04에 데이터 쓸 때와 동일
	*/
    
//MLX90614xCx 타입을 사용할 경우, 0x0F 번지에 위에서 계산한 새로운 데이터를 입력
//과정은 위의 0x04 번지에 데이터를 쓸 때와 CMD가 다른 것 빼고는 동일
#ifdef mlx90614xCx
	mlx_write_zero[1]=mlx_cmd_emissivity_2;	//0x2F (001x xxxx + 0x0F)
	mlx_write_zero[4]=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_write_zero,4);
	mlx_write_buffer[1]=mlx_cmd_emissivity_2;	//0x2F (001x xxxx + 0x0F)
	mlx_write_buffer[2]=new_0f&0xff;
	mlx_write_buffer[3]=(new_0f>>8)&0xff;
	mlx_write_buffer[4]=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_write_buffer,4);
    
	HAL_I2C_Master_Transmit_IT(&hi2c1,MLX90614_addr<<1,(uint8_t *)&mlx_write_zero[1],4);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	HAL_Delay(10);

	HAL_I2C_Master_Transmit_IT(&hi2c1,MLX90614_addr<<1,(uint8_t *)&mlx_write_buffer[1],4);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	HAL_Delay(10);
#endif

	//EEPROM-0x04 번지에 값이 변경됐는지 확인
	mlx_read_buffer[mlx_cmd_pointer]=mlx_cmd_emissivity;
	HAL_I2C_Mem_Read_IT(&hi2c1,(uint16_t)MLX90614_addr<<1,mlx_cmd_emissivity,I2C_MEMADD_SIZE_8BIT,
    	(uint8_t *)&mlx_read_buffer[3],3);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	crc_emissivity=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_read_buffer,5);
	if((mlx_read_buffer[3]==(new_04&0xff))&&(mlx_read_buffer[4]==((new_04>>8)&0xff)))
	{
		printf("0x04 : Successfully Changed\r\n");	
		//0x04번지에 입력한 새 방사율 데이터와 읽어 온 0x04번지의 방사율 데이터가 동일할 때
	}
	else
	{	
		//0x04번지에 저장된 방사율이 사용자가 입력한 방사율과 다를 때
	}

#ifdef mlx90614xCx
	//EEPROM-0x0F 번지에 값이 변경됐는지 확인
	mlx_read_buffer[mlx_cmd_pointer]=mlx_cmd_emissivity_2;
	HAL_I2C_Mem_Read_IT(&hi2c1,(uint16_t)MLX90614_addr<<1,mlx_cmd_emissivity_2,I2C_MEMADD_SIZE_8BIT,
    	(uint8_t *)&mlx_read_buffer[3],3);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	crc_emissivity=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_read_buffer,5);
	if((mlx_read_buffer[3]==(new_0f&0xff))&&(mlx_read_buffer[4]==((new_0f>>8)&0xff)))
	{
		printf("0x0F : Successfully Changed\r\n");
		//0x0F번지에 입력한 새 방사율 데이터와 읽어 온 0x0F번지의 방사율 데이터가 동일할 때
	}
	else
	{
		//0x0F 번지에 저장된 데이터가 사용자가 입력한 데이터와 다른 경우
	}
#endif
}
  • Emissivity(방사율) 변경에 사용할 함수
  • 0.1~1.0까지 변경 가능하고 방사율 변경에 따른 온도 측정값 교정은 필요하지 않다
  • EEPROM에 데이터를 쓰기 전에 반드시 해당 주소에 0x0000을 전송해 데이터를 지운 뒤 새롭게 입력할 데이터를 전송해야 한다
  • MLX90614xAx, xBx 타입은 EEPROM - 0x04(Emissivity) 번지의 값만을 주어진 계산식을 통해 얻은 새 방사율값으로 변경하면 되지만 xCx 타입은 0x04 및 0x0F 번지에 현재 저장되어있는 데이터를 기반으로 하는 공식(0x04와는 다른)을 활용해 값을 산출한 뒤 이를 0x0F 번지에 추가로 전송해줘야 한다
  • xCx 타입을 사용할 경우, 혹시 모를 상황을 대비해 EEPROM-0x0F 번지에 저장되어있는 데이터를 따로 기록해 두는 것을 권장 (0x0F 번지에 저장되는 값은 0x04, 0x0F 번지의 데이터를 기반으로 계산된 값이라 한번 잘못 저장하면 돌이킬 수 없다)
  • EEPROM에 데이터를 쓴 직후와 그 다음 쓰기 or 읽기 사이에 적어도 5ms(10ms-안전)의 대기 시간이 필요하다
  • Read 함수와는 달리 센서 내 특정 메모리에 접근해서 데이터를 쓰는 방식이 아니기 때문에 CMD도 전송 데이터 버퍼 안에 포함되어 있어야 한다. (CMD, LSByte, MSByte, PEC 순으로 총 4개의 배열을 전송)
  • 센서로부터 데이터를 읽어 올 때와 동일하게 LSByte->MSByte 순으로 전송이 진행된다
  • 쉬운 CRC 계산을 위해 첫번째 배열([0],0x5A<<1)을 고정값으로 둔 채 두번째 배열([1])부터 마지막 배열까지만을 센서에 데이터를 쓰는데 사용
  • 방사율을 변경한 이후, 전원을 껏다 켜서 변경한 방사율값이 유지되는지 확인한다
  • (방사율을 변경하고 전원을 재인가 하지 않았을 때 온도 측정이 부정확해지는 현상 겪음, 재인가 후 온도 측정 정상 및 변경된 방사율 유지 확인)

- 현재 센서에 설정된 방사율을 읽어오는 함수

void mlx_read_emissivity()
{
	uint8_t crc_emissivity;		//CRC 계산값 저장에 사용
	uint16_t read_emissivity;	//EEPROM-0x04 번지로부터 읽어 방사율 데이터를 저장할 변수
	float cur_emissivity;		//읽어 온 방사율 데이터를 실제 방사율로 변환한 값을 저장할 변수
    
	mlx_read_buffer[mlx_cmd_pointer]=mlx_cmd_emissivity;	//EEPROM-0x04
	HAL_I2C_Mem_Read_IT(&hi2c1,(uint16_t)MLX90614_addr<<1,mlx_cmd_emissivity,I2C_MEMADD_SIZE_8BIT,
    	(uint8_t *)&mlx_read_buffer[3],3);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
	crc_emissivity=HAL_CRC_Calculate(&hcrc,(uint32_t *)mlx_read_buffer,5);	//CRC 계산
	
	if(crc_emissivity==mlx_read_buffer[5])	//계산한 CRC값과 읽어 온 PEC 값이 일치할 때
	{
		read_emissivity=(mlx_read_buffer[4]<<8|mlx_read_buffer[3]);	
		//8비트씩 나누어진 방사율 데이터를 비트 시프트와 OR을 사용해 하나로 합침
        
		cur_emissivity=(read_emissivity+1)/65536.0;	
		//식을 통해 실제 방사율 값으로 변환
        
		printf("Current Emissivity : %.2f\r\n",cur_emissivity);	
	}
	else
	{
    		//읽어 온 PEC값과 계산한 CRC값이 일치하지 않을 때
	}
}

- 슬립 모드

슬립모드 예시

  • 슬립 모드에 진입하기 위해선 0xFF 명령어와 0xB4+0xFF를 CRC 계산한 결과값인 0xE8만 센서에 전송하면 된다
  • 전류 소비를 2.5μA로 제한하기 위해서 슬립 상태를 유지하는 동안 SCL핀을 Low로 유지하는 것을 권장. SCL 핀에 연결된 내부 통합 제너 다이오를 통한 누설 전류가 있기 때문. SCL 핀을 Push-Pull로 설정하고 SCL 핀에 Pull-Up 레지스터가 연결되어있지 않은 상태여야 한다.
  • 슬립 모드를 종료하기 위해선 SCL이 High인 상태에서 SDA를 최소 33ms 동안 Low로 유지하거나
  • or POR (Power on Reset)
void mlx_sleep()
{
	uint8_t enter_sleep[2]={0xFF,0xE8};	//0xFF-Sleep CMD, 0xE8-PEC (0xB4+0xFF)
	HAL_I2C_Master_Transmit_IT(&hi2c1,(uint16_t)MLX90614_addr<<1,(uint8_t *)&enter_sleep,2);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
	}
}

void mlx_wakeup()
{
	/*
	MLX90614와의 통신에 사용하는 I2C를 해제한 뒤, SDA로 사용하던 GPIO 핀을 새로운 설정으로 초기화
	SDA핀이 Low를 출력하도록 설정한 뒤 딜레이 함수를 이용해 33ms를 대기함으로써 슬립 모드에서 벗어난다
	이후 SDA핀에 대한 GPIO설정을 해제한 뒤 MLX90614를 사용하기 위해 I2C를 다시 초기화한다
	*/
	HAL_I2C_DeInit(&hi2c1);	//MLX90614와의 통신에 사용하는 I2C 해제
    
	/*
	SCL로 사용되던 GPIO 핀을 새롭게 설정
	*/
	GPIO_InitTypeDef GPIO_InitStruct = {0};
	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_RESET);	//통신에 사용하던 I2C SDA 핀과 핀이 속한 포트
	GPIO_InitStruct.Pin = GPIO_PIN_9;	//SDA로 사용하던 핀 번호
	GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;	//Output Mode
	GPIO_InitStruct.Pull = GPIO_NOPULL;	//No Pull-Up
	HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);	//GPIO 초기화 시작
	HAL_Delay(33);	//33ms 대기 (SDA핀은 0인 상태)
	HAL_GPIO_DeInit(GPIOB,GPIO_PIN_9);	//GPIO 설정 해제
    
	HAL_I2C_Init(&hi2c1);	//MLX90614 통신에 사용하던 I2C 초기화 시작
	HAL_Delay(250);	
	//센서가 정상 작동을 시작하고 0.25초 후에 유효한 첫 데이터를 얻을 수 있으므로 250ms 동안 대기
}
  • I2C 통신을 통해 슬립 모드를 벗어나는 방법은 찾지 못했기 때문에 I2C 설정을 해제하는 방법으로 진행
  • MLX90614와의 통신에 사용하던 I2C를 해제한 뒤 I2C SDA핀으로 사용되던 GPIO핀을 LOW 출력 설정
  • (SCL핀의 경우 High 상태를 유지하는 것을 확인했으므로 SDA핀만 재설정)
  • 딜레이 함수를 통해 슬립 모드에서 벗어나기 위한 최소 시간인 33ms 동안 대기 (SDA핀 LOW 상태)
  • 이후 GPIO핀 설정을 해제하고 센서와의 통신을 위해 I2C를 다시 초기화한다

mlx_sleep() 함수 실행 이후 바로 wakeup 함수 호출한 상황
wake up 이후 0.25초 대기
대기 후 온도 mlx_read_temperature() 함수 시작

- 예시

void main(void)
{
	...
	/* USER CODE BEGIN 2 */
	HAL_Delay(1000);
	mlx_read_emissivity();	//EEPROM-0x04 번지에 현재 설정되어있는 방사율 읽어옴
	mlx_change_emissivity(1.0);	//방사율 1.0으로 변경
	printf("\n");
  	mlx_read_emissivity();	//현재 설정되어있는 방사율 읽기 (설정 변경 확인용)
	mlx_change_emissivity(0.9);	//방사율 0.9로 변경
	printf("\n");
	mlx_read_emissivity();	//현재 설정되어있는 방사율 읽기 (설정 변경 확인용)
	mlx_sleep();	//슬립 모드 진입
	printf("Sleep\n");
	mlx_wakeup();	//슬립 모드에서 빠져나옴
	printf("Wake up\n");
	mlx_read_emissivity();	//현재 설정되어있는 방사율 읽기 (설정 변경 확인용)
  	/* USER CODE END 2 */

  	/* Infinite loop */
  	/* USER CODE BEGIN WHILE */
  	while (1)
  	{
  	/* USER CODE END WHILE */

  	/* USER CODE BEGIN 3 */

  		mlx_read_temperature();
  		HAL_Delay(1000);
  	}
  	/* USER CODE END 3 */
}
  • 방사율 1.0 설정 후, 0.9로 재변경

출력 결과

- xCx 타입의 경우

  • 딜레이가 없는 경우, 디버그가 실행 직후부터 main() 함수가 재실행되어 mlx_change_emissivity() 함수가 호출 및 실행되다가 디버그 창이 뜨면서 도중에 중단되는 상황이 발생
  • 이로인해, EEPROM 내에 데이터를 쓰던 중에 통신이 중단되면서 내부 데이터가 기존 데이터+새로운 데이터가 혼재된 상태가 되었다
  • EEPROM-0x04 번지의 방사율은 고정된 값과 주어진 방사율을 기반으로 계산된 값이 들어가기 때문에 이에따른 문제가 없었지만 0x0F 번지의 데이터는 기존에 저장된 0x04, 0x0F 번지의 데이터를 기반으로 새로운 값이 만들어지기에 비정상적인 값이 저장되는 현상이 발생했다
  • 때문에 딜레이 함수를 사용해 mlx_change_emissivity() 함수가 실행되는 시기를 늦춤으로써 디버그 준비 직후부터 실행 직전까지의 mlx_change_emissivity() 함수 호출이 안되게끔 했다

 

/*@ 20/05/26

 * - CUBEMX 설정 Active, Passive 모드 두 가지 상황으로 분리

 * - [동작] 부분 액티브, 패시브 모드에서의 동작 상황 설명 추가

 * - 액티브 모드 코드 및 설명 재수정 (타이머 인터럽트 제거)

 * - 패시브 모드 코드 및 설명 수정 (타이머 인터럽트 추가)

 *

 *@ 20/05/21 

 * - CUBE MX 타이머 인터럽트 설정 추가

 * - 타이머 인터럽트를 사용한 PMS7003 데이터 수신으로 변경

 * - 수신 완료 콜백 함수 코드 수정

 * - 타이머 부분 추가

 * - 액티브 모드 부분 코드 및 설명 수정

 * - 코드 링크 추가

 */

 

PMS7003 먼지센서

  • VCC : 5V (4.5~5.5V 내부의 팬이 5V로 동작되어야 하기 때문)
  • 통신 방식 : UART
  • DATA : 3.3V (MCU가 5V로 작동한다면 R/X, T/X핀에 Level Shifter 연결 필요 (5V->3.3V))
  • 인터페이스 레벨 : L<0.8, H>2.7 (3.3V)
  • 측정 범위 : 0.3~1.0 / 1.0~2.5 / 2.5~10 (㎛)
  • 검출 효율 : 50% (0.3㎛), 98%(>=0.5㎛)
  • 유효 범위 (pm2.5 기준) : 0~500 ㎍/㎥
  • 최대 범위 (pm2.5 기준) : >=1000 ㎍/㎥
  • 해상도 : 1 ㎍/㎥

[하드웨어 연결]

핀 설정
어댑터 보드
1.27mm / 2.00mm / 2.54mm 보드

주의점

  • SET, RESET 핀은 내부 풀업 되있으므로 사용하지 않는다면 연결해선 안된다
  • PIN7, 8은 연결해선 안된다

연결

  • 손쉬운 연결을 위해 PMS7003 어댑터 보드, 1.27mm-2.00mm-2.54mm 보드 및 1.25mm 커넥터를 사용
  • UART TX -> PMS7003 RX, UART RX -> PMS7003 TX 에 연결
  • 데이터 라인의 전압이 3.3V을 필요로 하므로 5V로 작동하는 MCU를 사용할 경우, TX, RX 라인에 Level Shifter 를 사용해 데이터 라인의 전압을 변환시킬 필요가 있다
  • ex) MCU UART TX(5V) -> 5V to 3.3V Level Shifter -> PMS7003 RX(3.3V)
  •     PMS7003 TX(3.3V) -> 3.3V to 5V Level Shifter -> MCU UART RX

[CUBE MX 설정]

- Active, Passive 모드에 따라 UART DMA, Timer 다르게 설정

 

<공통>

- UART 설정

  • 보드 레이트 : 9600 bps
  • 데이터 길이 : 8 비트
  • 패리티 비트 : X
  • 스탑 비트 : 1
  • 인터럽트 활성화

UART 설정
인터럽트 설정

<Active Mode>

- UART DMA - Rx Circular 모드로 설정

- 타이머 인터럽트 사용 X

UART DMA Rx - Circular 모드로 설정 (Tx - Normal)

<Passive Mode>

- DMA 미사용, 인터럽트를 통해 송수신 (DMA 사용 설정 되있어도 상관 X)

타이머 인터럽트 설정

 

- 타이머 인터럽트 설정

  • 타이머 주기 : 해당 타이머 인터럽트가 속하는 APBx 타이머 클럭 속도 / (Prescaler + 1) + (Counter Period + 1)
  • WB55 : TIM16 - APB2 (Timer Clock=32Mhz)
  •           32,000,000 / {((32,000 -1) + 1)) + ((5,000 - 1) + 1)} = 0.2 Hz = 5 Sec (T=1/f)
  • 5초 주기로 타이머 인터럽트 작동

[동작]

- 두개의 디지털 출력 옵션 : 액티브(디폴트 설정), 패시브 모드

- 액티브 모드 : PMS7003 센서는 지속적으로 측정한 센서값을 호스트로 자동 전송하고 호스트는 DMA Rx를 Circular 모드로 설정함으로써 PMS7003 데이터 수신 마지막에 자동으로 transfer count를 리셋하고 소스 버퍼의 첫번째 바이트부터 다시 수신을 시작한다

- 패시브 모드 : PMS7003에 Change Mode 명령어+패시브 모드 명령어를 전송해 PMS7003을 패시브 모드로 변경하고 데이터를 읽어올 땐 Passive Read 명령어를 보낸 뒤 UART 수신 명령어를 사용해 센서 데이터를 수신한다

- 커맨드 명령어를 통해 슬립, 웨이크업 기능을 사용할 수 있다

- 팬 성능으로 인해 센서가 슬립 모드에서 깨어나고 적어도 30초 지난 뒤에야 안정된 데이터를 얻는게 가능

 

<액티브 모드>

  • 액티브 모드에서 센서는 자동으로 시리얼 데이터를 호스트에 보냄
  • 액티브 모드는 두개의 서브 모드로 분리 : Stable 모드, Fast 모드
  • 농도 변화가 작으면 센서는 2.3s 간격의 스테이블 모드로 작동
  • 농도 변화가 크다면 센서는 200~800ms 간격의 패스트 모드로 자동 변경 (농도가 높을 수록 더 짧은 간격을 가짐)
  • 호스트(MCU)로 전송하는 데이터의 길이 : 32Bytes (각 8bit의 크기를 가진 32개의 배열)

(00~31 중 사용할 부분만 정리)

00 : 스타트 캐릭터1 : 0x42 고정 

01 : 스타트 캐릭터2 : 0x4d 고정

(고정값이므로 UART 통신이 문제없이 이뤄지고 있는지 확인하는데 사용할 수 있다)

 

10 : 데이터4 상위 8비트 : PM1.0에서의 먼지 농도 ㎍/㎥(대기 환경 하)

11 : 데이터4 하위 8비트

12 : 데이터5 상위 8비트 : PM2.5에서의 먼지 농도 ㎍/㎥(대기 환경 하)

13 : 데이터5 하위 8비트

14 : 데이터6 상위 8비트 : PM10에서의 먼지 농도 ㎍/㎥(대기 환경 하)

15 : 데이터6 하위 8비트

 

16 : 데이터7 상위 8비트 : 0.1L의 공기 중 지름 0.3um 이상의 입자의 갯수를 나타냄

17 : 데이터7 하위 8비트

18 : 데이터8 상위 8비트 : 0.1L의 공기 중 지름 0.5um 이상의 입자의 갯수를 나타냄

19 : 데이터8 하위 8비트

20 : 데이터9 상위 8비트 : 0.1L의 공기 중 지름 1.0um 이상의 입자의 갯수를 나타냄

21 : 데이터9 하위 8비트

22 : 데이터10 상위 8비트 : 0.1L의 공기 중 지름 2.5um 이상의 입자의 갯수를 나타냄

23 : 데이터10 하위 8비트

24 : 데이터11 상위 8비트 : 0.1L 공기 중 지름 5.0um 이상을 갖는 입자의 갯수를 나타냄

25 : 데이터11 하위 8비트

26 : 데이터12 상위 8비트 : 0.1L 공기 중 지름 10um 이상을 갖는 입자의 갯수를 나타냄

27 : 데이터12 하위 8비트

 

30 : 데이터와 체크 상위 8비트 : check code = 스타트 캐릭터1+스타트 캐릭터2+...+데이터13 하위 8비트

31 : 데이터와 체크 하위 8비트 

 

*데이터1~데이터3의 먼지 농도는 공장 환경 하에서 사용이 권장되기에 제외

 

<패시브 모드>

  • 디폴트 보드 레이트 : 9600bps, 체크 비트 : none, 스탑 비트 : 1 비트
  • 각 8비트(1바이트)의 값을 가지는 총 7개의 배열을 전송해야 한다
  • 1,2번 배열의 값은 고정인 상태로 명령어(CMD)와 그 명령어에 따른 필요 데이터를 데이터1,2 배열에 입력하고 Verify Byte 1,2에 Start Byte1~Data2 까지의 배열의 합을 상위, 하위 각 8비트 나누어 저장한 뒤 센서에 전송
  • 패시브 모드에서 센서로부터 읽어오는 데이터의 크기와 내용은 액티브 모드와 동일하다

스타트 바이트 1 : 0x42 (고정)

스타트 바이트 2 : 0x4d (고정)

명령어 : CMD

데이터1 : 데이터 H

데이터2 : 데이터 L

확인 바이트1 : LRC H 확인 바이트를 제외한 모든 바이트 합 (상위8비트)

확인 바이트2 : LRC L (하위 8비트)

 

명령어

0xe2 : 패시브 모드에서 읽기, 32바이트 응답

0xe1 : 00 - Passive 모드로 변경

       : 01 - Active 모드로 변경

0xe4 : 00 - Sleep 설정

       : 01 - Wakeup 설정

 

[코드]

<Active, Passive 모드 공통>

- 전역변수

/* USER CODE BEGIN PV */
uint8_t pms7003_Buffer[32];	//PMS7003 센서로부터 받아 올 32바이트 데이터를 저장하는 배열
uint8_t pms7003_send_buffer[7]={0x42,0x4d,0};	//센서에 명령어를 보내는데 사용될 데이터 배열
/* USER CODE END PV */
  • 측정값을 수신할 데이터 배열, 명령어를 전송할 배열 및 사용에 필요한 변수 선언

- 센서로부터 읽어 온 데이터를 출력하는 함수

void print_PMS7003(void)
{
	/*
	combine_value : 8비트씩 나눠진 데이터를 하나로 합치는데 사용할 변수
	check_byte_receive : 센서로부터 수신 받은 체크 코드(30,31)를 하나로 합치는데 사용할 변수
	check_byte_calculate : 센서로부터 수신 받은 0~29 까지의 데이터를 합산해 체크 코드와 비교
	*/
	uint16_t combine_value, check_byte_receive, check_byte_calculate=0;
    
	check_byte_receive=pms7003_Buffer[30]<<8|pms7003_Buffer[31];
	for(uint8_t i=0;i<30;i++)
	{
		check_byte_calculate+=pms7003_Buffer[i];
	}
    
    	//센서로부터 수신 받은 체크 코드(30,31)와 수신 받은 0~29까지의 데이터 합산이 일치할 때만 출력
	if(check_byte_receive==check_byte_calculate)
	{
		printf("PM1.0 : %d	",(combine_value=(pms7003_Buffer[10]<<8)|pms7003_Buffer[11]));
		printf("PM2.5 : %d	",(combine_value=(pms7003_Buffer[12]<<8)|pms7003_Buffer[13]));
		printf("PM10 : %d	",(combine_value=(pms7003_Buffer[14]<<8)|pms7003_Buffer[15]));
		printf("0.3um : %d	",(combine_value=(pms7003_Buffer[16]<<8)|pms7003_Buffer[17]));
		printf("0.5um : %d	",(combine_value=(pms7003_Buffer[18]<<8)|pms7003_Buffer[19]));
		printf("1.0um : %d	",(combine_value=(pms7003_Buffer[20]<<8)|pms7003_Buffer[21]));
		printf("2.5um : %d	",(combine_value=(pms7003_Buffer[22]<<8)|pms7003_Buffer[23]));
		printf("5.0um : %d	",(combine_value=(pms7003_Buffer[24]<<8)|pms7003_Buffer[25]));
		printf("10.0um : %d\n",(combine_value=(pms7003_Buffer[26]<<8)|pms7003_Buffer[27]));
	}
	else	//체크 코드가 일치하지 않는 경우
	{
	}
}
  • 상, 하나로 나뉘어 저장된 데이터(각 8bit)를 하나로 합친 뒤(16bit) 출력

- 명령어 전송 함수

void write_PMS7003(char* cmd)
{
	//1~5번째까지의 배열의 합을 필요로 하는 6, 7번째 배열인 verify byte1, 2에 값을 저장하기 위해 사용할 변수
	uint16_t verify_byte=0;	

	//사용자가 입력한 명령어(문자열)을 비교하여 해당하는 명령어에 따라
	//센서로 전송할 데이터 배열을 채운다
	if(strcmp(cmd,"Read")==0)
	{
		pms7003_send_buffer[2]=0xe2;
		pms7003_send_buffer[3]=0x00;
		pms7003_send_buffer[4]=0x00;
	}
	else if(strcmp(cmd,"Passive")==0)
	{
		pms7003_send_buffer[2]=0xe1;
		pms7003_send_buffer[3]=0x00;
		pms7003_send_buffer[4]=0x00;
	}
	else if(strcmp(cmd,"Active")==0)
	{
		pms7003_send_buffer[2]=0xe1;
		pms7003_send_buffer[3]=0x00;
		pms7003_send_buffer[4]=0x01;

	}
	else if(strcmp(cmd,"Sleep")==0)
	{
		pms7003_send_buffer[2]=0xe4;
		pms7003_send_buffer[3]=0x00;
		pms7003_send_buffer[4]=0x00;
	}
	else if(strcmp(cmd,"WakeUp")==0)
	{
		pms7003_send_buffer[2]=0xe4;
		pms7003_send_buffer[3]=0x00;
		pms7003_send_buffer[4]=0x01;
	}
	for(uint8_t i=0;i<5;i++)
	{
		verify_byte+=pms7003_send_buffer[i];	//verify byte1,2=배열 1~5(buffer[0]~[4])까지의 합
	}
	pms7003_send_buffer[5]=verify_byte>>8;	//배열 1~5까지의 합을 상하로 나누어 저장
	pms7003_send_buffer[6]=verify_byte;

	//센서와의 통신에 사용할 UART 라인이 준비 상태가 될 때까지 대기
	while(HAL_UART_GetState(&hlpuart1)!=HAL_UART_STATE_READY)
	{

	}
	//인터럽트를 이용해 센서에 명령어 전송
	if(HAL_UART_Transmit_IT(&hlpuart1,(uint8_t*)pms7003_send_buffer,7)!=HAL_OK)
	{

	}

	//만약 사용자가 입력한 명령어가 Read라면 센서로부터 측정 데이터를 받아오는 함수를 실행한다
	if(strcmp(cmd,"Read")==0)
	{
		//UART가 준비 상태가 될 때까지 대기
		while(HAL_UART_GetState(&hlpuart1)!=HAL_UART_STATE_READY)
		{

		}
		//인터럽트를 통해 센서로부터 데이터 수신
		if(HAL_UART_Receive_IT(&hlpuart1,pms7003_Buffer, 32)!=HAL_OK)
		{

		}
	}
}
  • 센서에 명령을 내리기 위한 데이터 배열을 전송하는 함수
  • 1,2번째 배열은 각 0x42, 0x4d로 고정값을 갖고 6,7번째 배열엔 1~4번째 배열의 합이 상,하 8비트로 나뉘어 저장되어야 한다
  • 명령어가 "Read"일 경우, 마지막에 인터럽트를 이용한 UART 수신을 통해 센서로부터 데이터를 받아온다 (패시브 모드에서 수신하는 데이터도 액티브 모드와 동일한 데이터 순서, 길이를 갖는다)

<Active Mode>

- 메인 함수

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

  if(HAL_UART_Receive_DMA(&hlpuart1,pms7003_Buffer,32)!=HAL_OK)
  {
	  //Execution fail
  }

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}
  • DMA를 통한 UART 수신 함수 실행
  • UART DMA - Rx를 Circular 모드로 설정했음으로 센서가 데이터를 전송할 때마다 수신이 자동 시작된다

- 수신 완료 후 호출되는 UART 콜백 함수

/* USER CODE BEGIN 4 */

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	if(huart->Instance==LPUART1)
	{
		print_PMS7003();
	}
}
  • USER CODE 4 부분은 main.c 내 하단에 위치
  • UART 데이터 수신이 완료되면 HAL_UART_RxCpltCallback() 함수가 자동으로 호출된다
  • 콜백 함수는 자동 생성되지 않으므로 main.c 내에 직접 작성할 필요가 있다

- 결과

사용 코드 : https://github.com/lee35210/STM32WB_PMS7003/tree/active

 

<Passive Mode>

- 타이머 초기화 함수

static void MX_TIM16_Init(void)
{

  /* USER CODE BEGIN TIM16_Init 0 */

  /* USER CODE END TIM16_Init 0 */

  /* USER CODE BEGIN TIM16_Init 1 */

  /* USER CODE END TIM16_Init 1 */
  htim16.Instance = TIM16;
  htim16.Init.Prescaler = 32000-1;
  htim16.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim16.Init.Period = 5000-1;
  htim16.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim16.Init.RepetitionCounter = 0;
  htim16.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  if (HAL_TIM_Base_Init(&htim16) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN TIM16_Init 2 */

  HAL_TIM_Base_Start_IT(&htim16);

  /* USER CODE END TIM16_Init 2 */

}
  • CUBEMX main.c 하단에 자동 생성되는 타이머 초기화 함수
  • 함수 내 하단 사용자 코드란에 HAL_TIM_Base_Start_IT(&타이머 이름) 을 입력해야 타이머 인터럽트가 시작된다

- 메인 함수

int main(void)
{
	...
  /* USER CODE BEGIN 2 */
  
  //PMS7003 Passive Mode로 설정
  write_PMS7003("Passive");

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}
/* USER CODE BEGIN 4 */

/**
  * @brief  Tx Transfer completed callback
  * @param  huart: UART handle.
  * @note   This example shows a simple way to report end of DMA Tx transfer, and
  *         you can add your own implementation.
  * @retval None
  */
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
	if(huart->Instance==LPUART1)
	{

	}
}

/**
  * @brief  Rx Transfer completed callback
  * @param  UartHandle: UART handle
  * @note   This example shows a simple way to report end of IT Rx transfer, and
  *         you can add your own implementation.
  * @retval None
  */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	if(huart->Instance==LPUART1)
	{
		print_PMS7003();
	}
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance==TIM16)
	{
		write_PMS7003("Read");
	}
}
  • 설정한 타이머 주기마다 HAL_TIM_PeriodElapsedCallback() 함수가 호출되고 PMS7003에 Read 명령어를 전송한다
  • 각종 명령어를 전송한 직후에 HAL_UART_TxCpltCallback() 함수가 호출된다
  • PMS7003으로부터 데이터 수신 완료 후, HAL_UART_RxCpltCallback() 함수가 호출되고 수신한 데이터를 출력한다

- 결과

출력 결과

사용 코드 : https://github.com/lee35210/STM32WB_PMS7003/tree/passive

 

<Sleep, Wakeup>

  • 슬립 모드에선 팬의 작동이 정지되고 센서로부터의 데이터 수신이 불가능해진다
  • 예시
int main(void)
{
	...
    /* USER CODE BEGIN 2 */
  uint8_t sleep_mode=0; (슬립 모드 확인하는데 사용할 변수, 1=슬립 모드 상태)
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
	  if((receive_complete==0)&&(sleep_mode==0))	//슬립 모드 상태일 때는 데이터 수신 실행 X
	  {
		  while(HAL_UART_GetState(&hlpuart1)!=HAL_UART_STATE_READY)
		  {
		  }
		  HAL_UART_Receive_IT(&hlpuart1,(uint8_t *)pms7003_Buffer,32);
	  }
	  if(receive_complete==1)
	  {
		  print_PMS7003();
	  }

	  if(30<i&&i<40)
	  {
		  if(sleep_mode==0)
		  {
			  printf("Sleep\r\n");
			  write_PMS7003("Sleep");
			  sleep_mode=1;
		  }
	  }
	  else if(40<=i)
	  {
		  printf("WakeUp\r\n");
		  write_PMS7003("WakeUp");
		  sleep_mode=0;
		  i=0;
	  }
	  i++;
	  HAL_Delay(1000);
  }
  /* USER CODE END 3 */
}
  • 주기적으로 슬립, 웨이크업 반복

출력 결과

  • 슬립 모드 직후, 센서로부터 받아 온 데이터에는 비어있는 부분들이 존재하지만 센서 작동 시간이 늘어감에 따라 측정값들이 채워지고 안정화되어 간다
  • STM32WB55 SoC에서 BLE, Thread, MAC 802.15.4 를 사용하기 위해선 M0+ CPU에 사용할 통신 방법에 맞는 Wireless Stack 펌웨어가 업로드 되어 있어야 한다 (Nordic nRF 칩셋에서 SoftDevice 같은 역할)
  • 펌웨어 업로드 방법 및 펌웨어에 대한 설명은 STM32Cube의 Repository 폴더내에 저장되어있다
  • 기본 설치 위치
  • C:/Users/사용자명/STM32Cube/Repository/STM32Cube_FW_WB_V1.2.0/Projects/STM32WB_Copro_Wireless_Binaries/Release_Notes.html
  • STM32Cube_FW_WB_V1.2.0 - 펌웨어 버전에 따라 V.1.2.0이 아닌 다른 표기가 올 수 있음 (19/10/16 기준 1.2.0)

[펌웨어 업로드 과정]

  • STM32Cube\Repository\STM32Cube_FW_WB_V1.2.0\Projects\P-NUCLEO-WB55.Nucleo\Applications\BLE\BLE_Proximity 예제 사용을 전제로 이에 필요한 stm32wb5x_BLE_Stack_fw.bin 펌웨어를 업로드하는 과정

1. STM32CubeProgrammer 프로그램 설치

 - https://www.st.com/content/st_com/en/products/development-tools/software-development-tools/stm32-software-development-tools/stm32-programmers/stm32cubeprog.html

 

STM32CubeProg - STMicroelectronics

STM32CubeProg - STM32CubeProgrammer software for programming STM32 products, STM32CubeProg, STMicroelectronics

www.st.com

2. Command Line Interface (CLI) mode 사용하기 위한 설정

(명령 프롬프트에서 STM32_Programmer_CLI.exe 사용하기 위한 과정)

- STM32CubeProgrammer 기본 설치 위치

C:\Program Files\STMicroelectronics\STM32Cube\STM32CubeProgrammer\

- STM32CubeProgrammer 폴더 내에 STM32_Programmer_CLI.exe 가 위치해있는 bin 폴더를 추가

 

3. 부트로더 USB 인터페이스 액세스

* Nucleo 보드

- Boot0(CN7.7) 핀을 VDD(CN7.5)에 연결하는 것으로 부트모드 설정

- BOOT0과 VDD 핀을 점퍼 케이블 등을 이용해 연결

- JP1 5V soruces의 점퍼핀을 USB MCU에 꽂고 컴퓨터와 연결된 USB 케이블을 USB_USER 커넥터에 연결

* USB-Dongle

- 스위치를 BOOT0 방향으로 이동

 

4. 현재 M0+ CPU에 저장되어 있는 Wireless Stack 펌웨어 삭제

- 명령 프롬프트 or 윈도우 파워쉘 실행

 

* 명령 프롬프트 실행

- 시작버튼 우클릭 > 실행 (or 윈도우 펑션키 + r)

명령 프롬프트 실행
명령 프롬프트 실행 결과

- CD 명령어 + 무선 펌웨어가 저장되어 있는 폴더 경로를 복사 붙여넣기한 후 실행해 폴더 위치 이동


* 윈도우 파워쉘 실행

- Wireless 펌웨어가 저장되어있는 폴더로 이동 (STM32Cube\Repository\STM32Cube_FW_WB_V1.2.0\Projects\STM32WB_Copro_Wireless_Binaries)

- 좌측 상단 파일 > Windows PowerShell 열기 > Windows PowerShell 열기 클릭

펌웨어 폴더로 이동
윈도우 파워쉘 실행
파워쉘 실행 결과

- STM32_Programmer_CLI.exe -c port=usb1 -fwdelete 명령어 실행해 현재 저장되어있는 펌웨어 삭제


5. FUS(Firmware Upgrade Service) 버전 읽기, 업그레이드

- STM32_Programmer_CLI.exe -c port=usb1 -r32 0x20030030 1

0x20030030 : 00050300 : FUSv0.5.3 => 새로운 FUS 펌웨어 업그레이드 필요 

0x20030030 : 01000100 or 01000200 : FUSv1.0.x => 업데이트 된 상태. 바로 M0+ CPU에 Wireless 펌웨어 다운 가능

 

5-2. FUS 업그레이드

- STM32_Programmer_CLI.exe -c port=usb1 -fwupgrade [FUS_Binary] [Install@] firstinstall=0

- STM32_Programmer_CLI.exe -c port=usb1 -fwupgrade stm32wb5x_FUS_fw.bin 0x080EC000 firstinstall=0

- [FUS_Binary] : FUS 펌웨어 파일명 (확장자 포함)

- [Install@] : 인스톨 주소

- Release_Notes.html 하단에 나와있는 표를 참고로 인스톨 주소 입력 (SoC 플래시 메모리 용량에 따라 달라진다)

(P-NUCLEO-WB55 개발킷의 경우 누클레오 보드, 동글 둘 다 플래시 메모리가 1MB 이므로  0x080EC000 사용)

*Error : Firmware not authentic! 에러가 발생하는 경우

- 우선적으로 STM32_Programmer_CLI.exe -c port=usb1 -r32 0x20030030 1 명령어 사용한 뒤 결과를 확인해 업그레이드가 됐는지 안됐는지를 확인

- 안됐을 경우 아래 방법 시도

https://community.st.com/s/question/0D50X0000Ap44FlSQI/restore-stm32wb55-fus-firmware

 

ST Community

 

community.st.com

- 위의 링크 마지막 방법으로 해결 (보드 연결 방식 ST-LINK가 아닌 USB)

  • STM32Programmer 실행 후, 왼쪽 상단 세 줄 아이콘 클릭 후 Erasing & programming 메뉴 선택

  • 우측 상단에서 USB 선택 후 Connect 버튼 클릭

  • 체크 박스 해제 후, 재클릭으로 전체 섹터 선택한 다음 Erase selected sectors 버튼 클릭하여 선택된 섹터 삭제 시작

  • 뒷부분에 보호된 섹터를 제외한 나머지 섹터가 지워진다
  • 이후 5-2. FUS 업그레이드 과정 재시도

섹터 지우기 후, FUS 확인 결과

  • FUS 재업그레이드 과정 중 에러가 발생했지만 FUS가 제대로 업그레이드 된 것을 확인 가능

6. Wireless Stack 펌웨어 M0+ CPU에 다운로드

- STM32_Programmer_CLI.exe -c port=usb1 -fwupgrade [Wireless_Coprocessor_Binary] [Install@] firstinstall=1

- STM32_Programmer_CLI.exe -c port=usb1 -fwupgrade stm32wb5x_BLE_Stack_fw.bin 0x080CC000 firstinstall=1

- [Wireless_Coprocessor_Binary] : M0+ CPU에 다운로드 할 Wireless Stack 펌웨어

- 필요에 따라 STM32WB_Copro_Wireless_Binaries 폴더 내에 다른 Wireless Stack 펌웨어를 선택하면 된다

- [Install@] : 인스톨 주소 (Wireless 펌웨어에 따라 달라진다. Release_Notes.html 하단 표에 주소 표기되어 있음)

stm32wb5x_BLE_Stack_fw.bin 를 다운로드 하는 과정

7. 노말 모드로 복귀

- BOOT0 핀과 VDD 핀의 연결 해제

- JP1의 점퍼핀을 USB STL에 꽂고 컴퓨터와 연결된 USB 케이블을 ST-LINK 커넥터에 연결

8. BLE_Proximity Example 프로젝트 업로드 후, 확인

- 업로드 이후, 블루투스 목록에 보이지 않는다면 USB 케이블 재연결로 보드 다시 부팅

- ST사에서 제공하는 ST BLE Profile 앱으로 연결 및 작동이 정상적으로 되는지 확인

  • 작동 전압 : 4.5~5.25V
  • 통신 방법 : UART, Modbus
  • 보드 레이트 : 9600bps
  • 측정 범위 : 400~2000ppm (PWM). 0~10000ppm (UART)
  • 정확도 : ±40ppm ±3%
  • 측정 간격 : 4초
  • 소비 전력 : 최대 300mA, 평균 18mA 이하
  • 마스터에서만 트랜잭션 시작 가능 (마스터에서 슬레이브로 명령어를 전송해야 슬레이브가 응답하는 방식)
  • 센서가 결함이 감지되지 않고 작동 중인지 확인하기 위해 주기적으로 센서의 상태를 체크해야 할 필요가 있음
  • ABC(Automatic Baseline Correction) 알고리즘 내장으로 일반적인 환경 하에선 유지보수로부터 자유로움
  • 미리 설정된 시간 간격동안 최하 판독값을 지속적으로 추적하고 예상된 신선한 공기에서의 CO2 값인 400ppm과 비교해 감지된 장기간 흐름을 천천히 수정함

 

[UART 설정]

  • 데이터 비트 길이 : 8비트 (패리티 비트 사용 X)
  • 스탑 비트 길이는 1, 2 비트 둘 다 사용 가능

  • 인터럽트 활용해 데이터 전송 및 수신

 

[데이터 전송 방법]

  • 기본적으로 마스터에서 센서로 데이터 버퍼를 보내면 센서가 이에 응답하는 방식
  • 센서의 각 레지스터는 16비트로 구성되어 있다
  • 센서로 보내는 데이터 버퍼는
  • 센서 주소(1) + 명령어(1) + 레지스터 시작 주소(2) + 읽어들일 레지스터 갯수(2) + CRC(2) 형태를 갖춘다
  • (괄호 안은 데이터의 크기 (바이트 기준))
  • 레지스터 시작 주소, 읽어들일 레지스터의 갯수, CRC는 상위1+하위1 총 2바이트로 나누어 전송해야 한다
  • 센서에서 마스터로 전송되는 데이터 버퍼는
  • 센서 주소(1) + 명령어(1) + 수신될 데이터의 바이트 크기(1) + 레지스터 데이터(레지스터 갯수*2) + CRC(2)
  • (읽어들일 레지스터 갯수에 맞춰 버퍼의 크기를 설정해야 한다)
  • CRC 값은 해당 데이터 버퍼의 0~CRC 직전까지의 데이터를 계산한 결과이므로 센서 응답의 CRC값은 유동적이다

1. CO2 값만을 읽어 올 때

  • 다음의 8바이트를 센서로 전송
  • <FE><04><00><03><00><01><C5><D5>
  • 센서 주소 : 0xFE
  • 명령어 : 0x04 (CO2 값이 저장된 인풋 레지스터를 읽어오는데 사용하는 명령어)
  • 레지스터 시작 주소 : 0x0003 (CO2 값이 저장된 인풋 레지스터3(0x03))
  • 레지스터 갯수 : 1 (0x0001)
  • CRC : D5C5 (하위 바이트인 C5 먼저 전송)
  • 예) 인풋 레지스터4(0x03)에 저장된 CO2 값(ppm 400(0x190h)이라 가정)을 읽어올 때
  • <FE><04><00><03><00><01><C5><D5> -총 8바이트를 전송
  • 센서는 이에 대한 응답으로
  • <FE><04><02><01><90><C5><D5> - 총 7바이트를 마스터로 전송
  • 센서 주소 + 명령어 + 전송되는 바이트 수(레지스터 갯수*2) + CO2 값 Hi + CO2 값 Lo + CRC Hi + CRC Lo

 

2. 센서 상태만을 읽어 올 때

  • 다음의 8바이트를 센서로 전송
  • <FE><04><00><00><00><01><25><C5>
  • 센서 상태는 인풋 레지스터1(0x00)에 저장되어 있으므로 인풋 레지스터를 읽는데 사용되는 0x04 명령어와 시작 주소 0x0000를 2바이트로 나누어 전송
  • 인풋 레지스터1에만 센서 상태가 저장되어 있으므로 읽어 올 레지스터의 갯수는 1 (0x0001)
  • CRC값 C525는 하위 25부터 전송
  • 센서는 다음과 같은 7바이트 데이터로 응답한다
  • <FE><04><02><00><00><AD><24>
  • 02는 전송되는 바이트의 수(레지스터 갯수*2)
  • 4,5번째의 <00><00>은 현재 센서의 에러 상태를 나타낸다

 

3. 센서와 CO2값을 한번에 읽어 올 때

  • 센서 상태는 인풋 레지스터1, CO2값은 인풋 레지스터4에 저장되므로 0x00~0x03까지 4개의 레지스터를 읽어와야 한다
  • <FE><04><00><00><00><04><E5><C6>
  • <FE>는 센서 주소(254), <04>는 인풋 레지스터 읽기 명령어인 0x04
  • 3,4 번째 <00><00>은 시작 주소 인풋 레지스터1 주소인 0x0000을 나타냄
  • 5,6 번째 <00><04>는 읽어 올 레지스터의 갯수인 4를 나타냄
  • <E5><C6>은 CRC 값
  • 센서는 다음과 같은 13바이트 데이터로 응답
  • <FE><04><08><00><00><00><00><00><00><01><90><16><E6>
  • 3번째 <08>은 전송될 데이터의 바이트 크기가 8바이트임을 의미 (레지스터 갯수 4*2)
  • 4,5번째 <00><00>은 인풋 레지스터1에 저장된 센서 상태를 나타냄
  • 6~7, 8~9는 각각 인풋 레지스터2(알람 상태), 3(아웃풋 상태)을 나타냄
  • 10,11번째 <01><90>은 측정된 CO2값을 나타냄 (0x190=400ppm)
  • <16><E6>은 CRC값

[코드]

- STM32Cube - Repository 내 Examples 중 UART_TwoBoards_ComIT 를 참조

- 센서의 CO2 측정 간격은 4초이므로 CO2 값을 4초 미만의 간격으로 읽어 올 필요는 없다

 

- CO2값을 읽어 올 때

//CO2 값을 읽기 위해 S8 센서로 보내는 데이터 버퍼
uint8_t send_Buffer[8]={0xFE, 0x04, 0x00, 0x03, 0x00, 0x01, 0xD5, 0xC5};

//S8 센서로부터 받아오는 데이터를 저장하기 위한 13바이트 데이터 버퍼
uint8_t receive_Buffer[7]={0};	

//상,하 1바이트씩 나뉘어 수신되는 CO2 값을 하나로 합치는데 사용
uint16_t co2_Value;	

//CO2 값 읽어오기 위한 데이터 버퍼를 센서로 전송
//데이터 전송에 사용할 UART, 전송할 버퍼, 전송할 데이터의 바이트 크기
if(HAL_UART_Transmit_IT(&hlpuart1,(uint8_t*)send_Buffer,8)!=HAL_OK) 
{
	Error_Handler();
}
//버퍼 전송 완료되기 전까지 대기
while(HAL_UART_GetState(&hlpuart1)==HAL_UART_STATE_BUSY_TX)
{
}
//버퍼 전송 완료 후, 센서 응답을 수신하기 위해 사용 중인 UART를 수신 상태로 둠
//데이터 수신에 사용할 UART, 전송된 데이터를 저장할 버퍼, 수신할 데이터의 바이트 크기
if(HAL_UART_Receive_IT(&hlpuart1,(uint8_t*)receive_Buffer,7)!=HAL_OK)
{
	Error_Handler();
}
//센서 응답 수신 완료까지 대기
while(HAL_UART_GetState(&hlpuart1)==HAL_UART_STATE_BUSY_RX)
{
}

//센서로부터 전달 받은 CO2 값의 상위 1바이트는 receive_Buffer[3], 하위 1바이트는 receive_Buffer[4]에 저장됨
//비트시프트를 사용하여 16비트 변수 co2_Value에 CO2 값을 하나로 합침
co2_Value=(receive_Buffer[3]<<8)|(receive_Buffer[4]);

CO2 측정 결과

 

-센서 상태를 읽어 올 때

//센서 상태를 읽기 위해 S8 센서로 보내는 데이터 버퍼
uint8_t send_Buffer[8]={0xFE, 0x04, 0x00, 0x00, 0x00, 0x01, 0x25, 0xC5};

//S8 센서로부터 받아오는 데이터를 저장하기 위한 7바이트 데이터 버퍼
uint8_t receive_Buffer[7]={0};	

//센서 상태 값 읽어오기 위한 데이터 버퍼를 센서로 전송
//데이터 전송에 사용할 UART, 전송할 버퍼, 전송할 데이터의 바이트 크기
if(HAL_UART_Transmit_IT(&hlpuart1,(uint8_t*)send_Buffer,8)!=HAL_OK) 
{
	Error_Handler();
}
//버퍼 전송 완료되기 전까지 대기
while(HAL_UART_GetState(&hlpuart1)==HAL_UART_STATE_BUSY_TX)
{
}
//버퍼 전송 완료 후, 센서 응답을 수신하기 위해 사용 중인 UART를 수신 상태로 둠
//데이터 수신에 사용할 UART, 전송된 데이터를 저장할 버퍼, 수신할 데이터의 바이트 크기
if(HAL_UART_Receive_IT(&hlpuart1,(uint8_t*)receive_Buffer,7)!=HAL_OK)
{
	Error_Handler();
}
//센서 응답 수신 완료까지 대기
while(HAL_UART_GetState(&hlpuart1)==HAL_UART_STATE_BUSY_RX)
{
}

//receive_Buffer[4]에 저장된 0~6 비트를 데이터 시트를 참고해 어떤 종류의 에러인지를 판단
if((receive_Buffer[4]&&1)==1)
{
	//Fatal error
}
else if((receive_Buffer[4]>>1)&&1==1)
{
	//Offset regulation error
}
else if((receive_Buffer[4]>>2)&&1==1)
{
	//Algorithm error
}
else if((receive_Buffer[4]>>3)&&1==1)
{
	//Output error
}    
else if((receive_Buffer[4]>>4)&&1==1)
{
	//Self diagnostics error
}
else if((receive_Buffer[4]>>5)&&1==1)
{
	//Out of Range
}
else if((receive_Buffer[4]>>6)&&1==1)
{
	//Memory error
}
else
{
	//No error
}

S8 센서 상태 확인 결과

SSD1306 드라이버 칩셋을 사용한 128x64 OLED 모듈

  • 작동 전압 : 3.3~5V
  • 통신 방식 : I2C (칩셋이 I2C, SPI를 포함한 5개의 통신 방식을 지원하지만 사용 모듈은 I2C 전용으로 설계됨)
  • 세로 16(Yellow) + 48(Blue) 줄로 구성된 모듈 사용

 

[기본 구동 방식]

Write Mode - 데이터 구성

  • Control Byte+Data Byte/Command Byte를 전송해 OLED 기능 또는 디스플레이에 표현될 데이터를 설정
  • Slave Address : 0x3C (011 110+SA0) (D/C 핀 HIGH, LOW 설정에 따라 SA0 비트를 0 or 1로 설정해 주소를 정할 수 있는데 사용 모듈의 경우 0으로 고정되어 있음)
  • 첫 바이트는 다른 I2C 통신과 동일하게 Slave Address + R/W bit (8 bit)로 구성
  • Slave Address 다음에 오는 Control byte는 뒤이어 전송될 데이터가 Command(명령어) / Data Byte일지를 결정
  • Control Byte는 Co(0) + D/C#(Command=0, Data=1) + 00000 로 구성된다
  • 명령어 전송 : Control Byte = Co(0) + D/C#(0) + 00000로 구성 (0x00)
  • GDDRAM(그래픽 표현)에 저장될 데이터 전송 : Control Byte는 Co(0)+D/C#(1)+00000 로 구성 (0x40)

[Graphic Display Data RAM (GDDRAM) - 그래픽 표현]

GDDRAM 구성표

  • GDDRAM은 디스플레이에 표현될 그래픽 데이터들을 저장하는데 사용된다
  • RAM의 크기는 128x64 bits이며 128x64 흑백 도트 매트릭스에 사용될 0~7페이지 (세로 8등분)로 나뉘어져있다
  • 가로 SEG0~SEG127, 세로 COM0~COM63 (1 페이지 = 8 줄로 구성)로 구성되어 있다
  • (페이지 및 세그먼트 순서는 리맵핑 명령어를 통해 변경할 수 있음)

GDDRAM 구성표 (2)

  • 각 페이지는 127개의 세그먼트로 구성되어 있고 각 세그먼트는 8 bit (1 Byte) 데이터를 가지고 있다
  • 세그먼트 1bit는 1 픽셀의 디스플레이 이미지를 표현하고 기본적으로 0 bit일 때는 OFF, 1 bit일 때는 ON 된다
  • (RGB가 아닌 단 하나의 색상만을 가지고 있기 때문에 단순히 0 or 1로만 표현된다)
  • 세그먼트의 MSB는 각 페이지의 최하단, LSB는 최상단이다
  • 예) 1100 1001 (0xC9)가 입력될 경우, 하단부터 1,2,5,8번째 픽셀이 켜진다

SEG0에 1100 1001 (0xC9)가 입력된 경우

 

[메모리 어드레싱 모드]

  • Page, Horizontal, Vertical 세 개의 메모리 주소 지정 모드 존재
  • 세 모드 중 어떤 모드를 사용하더라도 세로열이 0~7 페이지로 구분되어진다는 점은 변함이 없다
  • Page / Horizontal, Vertial 모드는 서로 다른 포인터 주소 지정 방법을 갖는다

1. Page Addressing Mode

Page Addressing Mode 에서 어드레스 포인터 이동

  • 디스플레이 RAM을 읽거나 쓴 뒤에는 Column address pointer가 자동으로 1 증가
  • 열 주소 포인터가 열 마지막 주소에 이르면 열 포인터 주소는 시작 주소로 리셋되고 페이지 어드레스 포인터는 변화하지 않는다
  • RAM access pointer 시작 위치를 지정하기 위해선 다음 과정이 필요
    1. B0~B7 명령어를 사용해 타겟 페이지를 설정
    2. 포인터의 하위 시작 열 주소를 00~0F 명령어 사용해 설정
    3. 포인터의 상위 시작 열 주소를 10~1F 명령어 사용해 설정 
      • ex) 83열(0x53)을 시작 주소로 설정한다고 했을 때 상위 4 bit를 0x1F, 하위 4bit를 0x0F의 하위 4bit 에 나누어 저장 후 전송한다고 생각하면 된다
      • 0x53 & 0x0F = 0x03 (하위 4bit)
      • 0x10 | ((0x53>>4)&0x0F) = 0x15 (상위 4bit)
  • ex) 2 페이지, 3열을 시작 주소로 지정했을 때 (0xB2, 0x03, 0x10 명령어를 전송한 뒤 0xFF 1바이트 데이터만 전송했을 때)

 

2. Horizontal Addressing Mode

Horizontal Mode에서 어드레스 포인터 이동

  • 디스플레이 RAM을 읽거나 쓴 뒤에는 Column address pointer가 자동으로 1 증가
  • 만약 열 주소 포인터가 열 마지막 주소에 이르면 열 주소 포인터는 열 시작 주소로 리셋 되고 페이지 주소 포인터는 1 증가됨
  • 열, 페이지 주소 포인터 둘 다 마지막 주소에 이르면 포인터들은 시작 주소로 리셋됨
  • 0x21, 0x22 명령어를 통해 페이지 및 열의 시작, 끝 주소를 지정할 수 있음

3. Vertical Addressing Mode

  • 위의 두 모드와는 다르게 열이 아닌 행이 증가되는 모드 (1행이 아닌 1페이지 단위로 증가)
  • 페이지 주소 포인터가 페이지 끝 주소에 이르면 페이지 주소 포인터는 페이지 시작 주소로 리셋되고 열 주소 포인터는 1증가
  • 열, 페이지 주소 포인터 둘 다 마지막 주소에 이르면 포인터들은 시작 주소로 리셋됨
  • 0x21, 0x22 명령어를 통해 페이지 및 열의 시작, 끝 주소를 지정할 수 있음

(*) Horizontal, Vertical Addressing Mode 에서의 포인터 주소 지정 방법

  • 0x21 명령어 + 시작 열 주소(0~127) + 마지막 열 주소(0~127) 를 전송해 그래픽 데이터를 저장할 열 범위를 지정
  • 0x22 명령어 + 시작 페이지 주소(0~7) + 마지막 페이지 주소(0~7) 를 전송해 그래픽 데이터를 저장할 페이지 범위 지정

Horizontal 모드에서 열 2~125, 페이지 1~6으로 설정된 경우

  • 페이지 및 열의 범위가 제한되어 있다고 하더라도 포인터 주소가 이동하는 방식은 원래의 모드와 동일하다
  • 시작 주소인 2 열에서 포인터가 시작되어 읽기/쓰기 후 자동으로 1열씩 주소가 증가하고 설정된 열의 끝 주소인 125 열에 도달했을 때 자동으로 페이지는 +1, 열은 시작 열로 리셋된다
  • 열 및 페이지의 마지막 주소인 6페이지, 125열에 도달했을 경우엔 시작 주소인 1페이지, 2열로 리셋된다

[CUBE MX 설정]

  • Clock Configuration 설정없이 Pin & Out Configuration > Connectivity 란에서 사용할 I2C 선택한 후 설정
  • I2C Speed Mode의 경우, 데이터 시트에 맞춰 400KHz의 속도를 갖는 Fast Mode를 선택했으나 1000KHz의 속도를 갖는(1us) Fast Mode Plus로 설정해도 정상적으로 작동하는 것을 확인

[초기화 설정]

SSD1306 데이터 시트에 나와있는 초기화 예
Sequential COM 핀 배열

void ssd1306_W_Command(uint8_t cmd)
{
	uint8_t buffer[2]={0};		//Control Byte + Command Byte
	buffer[0]=(0<<7)|(0<<6);	//Co=0 , D/C=0
	buffer[1]=cmd;

	if(HAL_I2C_Master_Transmit_DMA(&ssd1306_I2C_PORT,(uint16_t)(ssd1306_Address)<<1,(uint8_t*)buffer,2)!= HAL_OK)
	{
		Error_Handler();
	}
	while (HAL_I2C_GetState(&ssd1306_I2C_PORT) != HAL_I2C_STATE_READY)
	{
	}
}

void ssd1306_Init(void)
{
	ssd1306_W_Command(0xA8);	//Set Mux Ratio
	ssd1306_W_Command(0x3F);	//64MUX

	ssd1306_W_Command(0xD3);	//Set Display Offset
	ssd1306_W_Command(0x00);	//COM0

	ssd1306_W_Command(0x40);	//Set Display Start Line

	ssd1306_W_Command(0xA1);	//Set Segment re-map, Default 0xA0
					//column address 127 is mapped to SEG0 (좌우 반전)

	ssd1306_W_Command(0xC8);	//Set COM Output Scan Direction, default 0xC0
					//remapped mode. Scan from COM[N-1] to COM0 (상하 반전)

	ssd1306_W_Command(0xDA);	//Set COM Pins hardware configuration
	ssd1306_W_Command(0x12);

	ssd1306_W_Command(0x20);	//Set Memory Addressing Mode
	ssd1306_W_Command(0x02);	//Page Addressing Mode

	ssd1306_W_Command(0x81);	//Set Contrast Control
	ssd1306_W_Command(0x7F);	//1~256

	ssd1306_W_Command(0xA4);	//Disable Entire Display On

	ssd1306_W_Command(0xA6);	//Set Normal Display

	ssd1306_W_Command(0xD5);	//Set Osc Frequency
	ssd1306_W_Command(0x80);

	ssd1306_W_Command(0x8D);	//Enable charge pump regulator
	ssd1306_W_Command(0x14);

	ssd1306_W_Command(0xAF);	//Display ON
}
  • 명령어 입력을 위해 전송할 배열의 첫번째 칸은 0x00 (Co=0, D/C=0)으로 고정시키고 두번째 칸엔 전달 받은 명령어를 저장한 뒤 배열 전송
  • 초기화의 경우, 데이터 시트에 나와있는 예를 참고해 코드 작성
  • 초기화 코드를 그대로 사용할 경우, 노란색 영역이 하단에 위치 (Page 6,7)
  • 노란색 영역을 상단으로 사용하기 위해 0xC8 명령어를 사용해 COM 출력 스캔 방향을 COM63 to COM0 으로 바꿔 상하 반전을 시키고 0xA1 명령어를 통해 127 열을 SEG0 으로 리맵핑 하여 좌우를 반전 시켜줌
  • Set COM Pins hardware configuration의 경우, COM0~COM63이 순차적으로 화면 최상단에서 최하단까지 이어지는 Sequential 핀 배열과 COM0~COM31, COM32~COM63 이 교차되어 나열되는 (COM0, COM31, COM2... / COM31, COM0, COM32...) Alternative 핀 배열의 두 방법이 있다
  • Sequential 배열을 사용하기 위해 데이터 시트를 참고해 0xDA+0x02 명령어를 사용했지만 실제 결과는 Alternative 배열처럼 COM0~COM31/COM32~COM63이 서로 교차되어 표시되는 결과를 얻음
  • 0xDA+0x12(Alternative COM 핀 배열) 명령어를 사용한 결과, COM0~COM63 까지 화면에 순차적으로 표시됨

초기화 이후의 화면 상태

 

[화면에 표시될 데이터 전송 (GDDRAM에 저장될 데이터)]

  • GDDRAM에 저장된 데이터 형태 그대로 디스플레이와 1:1 매칭이 된다고 생각하면 편하다
  • GDDRAM의 형태는 크게 128x64 화면의 세로열(64열)을 8등분하여 각각 8개의 행을 가진 페이지로 구성되어 있으며 (순서는 화면 상단(0)->하단(7)) 각 페이지는 128개의 1바이트 크기 세로열을 가지고 있다
void ssd1306_W_Data(uint8_t* data_buffer, uint16_t buffer_size)
{
	if(HAL_I2C_Mem_Write_DMA(&ssd1306_I2C_PORT,(uint16_t)(ssd1306_Address<<1),0x40,1,data_buffer,buffer_size)!= HAL_OK)
	{
	}
	while(HAL_I2C_GetState(&ssd1306_I2C_PORT) != HAL_I2C_STATE_READY)
	{
	}
}
  • GDDRAM에 저장될 데이터를 전송하기 위해선 전송되는 첫번째 바이트가 0x40 (Co=0,D/C=1)이 되어 이후 전송될 데이터가 명령어가 아닌 GDDRAM에 저장될 데이터임을 나타내야 하고 그 뒤에 데이터 배열이 오는 형태가 되게끔 하면 된다
  • 데이터 전송 전, 후로 화면을 갱신하는데 필요한 명령어는 따로 없고 0x40+데이터 배열이 전송되는데로 포인터 주소가 위치한 곳부터 전송된 데이터 크기만큼 화면이 갱신된다
  • 예) 초기화 이후 화면 전체를 검은색(0x00, 픽셀 OFF)으로 전환
void ssd1306_Clear(void)
{
	//1열은 1바이트로 구성되어 있고 1페이지는 128개의 열이 존재하므로
	//1바이트 크기의 원소 128개를 갖는 배열 선언
	uint8_t buffer[128]={0};	
    
	//0번째 열부터 갱신해야 하므로 0x00, 0x10을 명령어 함수를 통해 전송해
	//포인터 주소를 0열로 지정
	ssd1306_W_Command(0x00);	
	ssd1306_W_Command(0x10);

	for(uint8_t i=0;i<8;i++)
	{
    	//Page Addressing Mode의 경우 포인터 주소가 열의 끝부분에 도달했을 때
        //페이지 주소는 그대로 유지된채 열 주소만 리셋되므로 
        //전체 페이지를 갱신하기 위해선 포인터가 위치한 페이지 주소를 1씩 증가시켜줘야 한다
		ssd1306_W_Command(0xB0+i);
        
        //1바이트 크기의 0x00값을 가진 128 배열을 전송하므로 한 페이지 전체가 0으로 갱신된다
		ssd1306_W_Data(buffer,128);	
	}
}

 

[페이지와 열 포인터 시작 주소 지정]

void ssd1306_Set_Coord(uint8_t page, uint8_t col)
{
	uint8_t col_low=0x0F,col_high=0x1F;
	col_low=(col&0x0F);
	col_high=0x10|((col>>4)&0x0F);
	ssd1306_W_Command(0xB0+page);
	ssd1306_W_Command(col_low);
	ssd1306_W_Command(col_high);
}
  • Page Addressing Mode의 경우 Horizontal, Vertical 모드와는 달리 페이지 및 열의 마지막 입력이 필요치 않으므로 시작 지점만을 입력한다
  • 페이지 시작 주소 설정은 0xB0 + 원하는 페이지의 값(0~7)을 더해 전송하면 된다
  • 열 시작 주소는 전달받은 열을 상위 4bit, 하위 4bit로 나누어 하위 4bit는 0x0F, 상위 4bit는 0x1F에 결합하여 전송
  • 이후 데이터를 전송하면 지정된 시작 지점부터 화면 데이터가 갱신된다

[폰트]

 

The Dot Factory: An LCD Font and Image Generator

The Dot Factory is a small open source tool (MIT licensed) intended to generate the required C language information to store many fonts and images, as efficiently as possible, on a microcontroller. These fonts are then uploaded via the LCD driver (see the

www.eran.io

  • 링크의 프로그램을 이용해 폰트 배열을 생성한 뒤 사용
  • 시스템 내에 설치된 폰트를 바탕으로 입력된 문자(특문,대문자,소문자) 및 폰트 크기 설정에 따라 자동으로 배열을 생성해줌
  • 한 페이지당 한줄씩 문자열을 표시하려고 했으나 디스플레이 크기 자체가 너무 작아 보기가 힘들어져 2페이지 당 한줄씩 표현하기로 결정
  • Sequential 핀 배열을 설정했으므로 연속된 두 페이지(0,1/2,3/...)에 폰트를 상하로 1Byte씩 나눠서 저장함
  • 두 페이지를 한 줄 기준으로 삼았기 때문에 변환된 폰트의 높이가 16bit 이하여야 한다
  • 예) 12x16 bit 크기로 변환된 A를 지정된 좌표에 입력
  • (같은 폰트 내에서도 각 글자마다 크기가 서로 다르므로 12x16 bit 내에서 차지하는 높이나 너비가 다르다)

/* @792 'A' (12 pixels wide) */
//
//      #
//     # #
//     # #
//     # #
//    #   #
//    #   #
//    #####
//   #     #
//   #     #
//   #     #
//  #       #
//
//
//
//
0x00, 0x00, 0x00, 0xE0, 0x9C, 0x82, 0x9C, 0xE0, 0x00, 0x00, 0x00, 0x00, //상단 페이지에 저장
0x00, 0x08, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x08, 0x00, 0x00, //하단 페이지에 저장

void ssd1306_W_Char(uint8_t character_Code, uint8_t page, uint16_t column)
{
	uint8_t char_Buffer[font_width*2]={0};

	for(uint8_t i=0;i<font_width*2;i++)
	{
		char_Buffer[i]=ssd1306_Fonts[(character_Code-32)*(font_width*2)+i];
	}

	for(uint8_t i=0;i<2;i++)
	{
		ssd1306_Set_Coord(page+i,column);
		ssd1306_W_Data(&char_Buffer[i*font_width],font_width);
	}
}

ssd1306_W_Char('A',0,0)
  1. 변환된 폰트의 너비*2만큼의 원소 개수를 갖는 배열 선언 (*2는 글자를 상/하 페이지로 나누어 저장하기 위함)
  2. 입력된 문자를 int 변수로 받아 ASCII 코드값을 기반으로 저장한 폰트 배열에서 매칭되는 문자를 찾는다
    • 폰트 배열의 경우 아스키 코드 32번 space부터 시작하여 126번 ~ 까지 순서대로 변환, 저장해서 사용했다
    • space의 아스키 코드값(32)과 폰트 배열 0번의 차인 32가 폰트 배열에서 입력된 문자를 찾는 기준점이 된다
    • 하나의 문자에서 다음 문자로 넘어가는데는 문자 너비*2 만큼의 수가 필요
    • (전달된 문자의 아스키 코드값-32)*(폰트 너비*2)를 하면 문자와 매칭되는 폰트 배열의 시작점이 나온다
  3. 해당 문자가 저장된 폰트 배열 시작점에서부터 순서대로 앞서 생성한 char_Buffer 배열에 폰트 데이터를 저장한다
  4. 페이지 및 열 포인터 시작 주소를 지정하고 우선 폰트 너비만큼의 데이터를 전송해 폰트의 상단 부분을 표시
  5. 페이지 주소 1 증가 및 열 시작 주소를 재지정한 뒤 남은 배열을 전송해 폰트 하단 부분을 완성

 

[문자 전송 예시]

void ssd1306_W_String(char *str, uint8_t page, uint8_t col)
{
	while(*str)
	{
		if((127<col+font_width))
		{
			if(page==6)
			{
				break;
			}
			page+=2;
			col=0;
		}
		ssd1306_W_Char(*str,page,col);

		col+=font_width;
		str++;
	}
}

int main(void)
{
	...
    
    ssd1306_W_String("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz{|}~",0,0);
    /*
    변수 사용 예시
    char print_buff[10];
    int print_num=123456;
    sprintf(print_buff,"%d",print_num);
    ssd1306_W_String(print_buff,0,0);
    */
    while(1)
    {
		
    }
}
  • 전달된 문자열의 주소를 1씩 증가시키며 ssd1306_W_Char 함수를 통해 문자 전송
  • 만약 페이지에 문자를 쓸 공간이 부족할 경우 다음 페이지(문자 한 줄을 표현하는데 두 페이지를 사용하므로 현재 페이지+2)로 이동하고 세로열 시작을 0으로 지정한 후 남은 문자 순서대로 전송
  • 마지막 페이지+문자를 쓸 공간이 없을 경우 전송 중단

사용한 DS3231 모듈

  • 작동 전압 : 2.3~5.5V
  • 통신 : I2C
  • Fast(400kHz) 모드 지원
  • 배터리 용량이 부족할 경우, 시간 흐름이 부정확해짐
  • (사용 모듈의 경우 INT/SQW 핀에 풀업 레지스터 구성되어 있음)

[내부 레지스터 주소 및 데이터]

DS3231이 제공하는 데이터들 (총 19 바이트)

  • 0x00~0x06 : 현재 설정되어 있는 시간
  • 0x07~0x0A : 현재 설정되어 있는 알람1 시간
  • 0x0B~0x0D : 현재 설정되어 있는 알람2 시간
  • 0x0E~0x10 : 제어 레지스터
  • 0x11~0x12 : 온도 (0x11 : 정수, 0x12 : 소수)
  • 0x02 시간(Hours) 레지스터에서 6비트를 HIGH로 설정하면 12시간, LOW라면 24시간 기준으로 시간을 표시
    • 12시간 모드에선 오후 시간일 때(PM) 5비트가 HIGH 상태가 된다
    • 24시간 모드에선 5비트는 20~23시를 표시하는데 사용된다

[Read]

MASTER가 DS3231로부터 데이터를 읽어올 때
레지스터 포인터에 읽어 올 주소를 Write 후 Read 시작
Read 모드 사용시 레지스터 포인터에 대한 주의사항

  • 데이터를 읽어 올 때, SDA 라인의 첫 1바이트는 MSB부터 7 비트는 DS3231(Slave) 주소 + Read(1) 비트로 구성된다
  • Write 모드 사용시에만 레지스터 포인터가 설정되는데 이는 DS3231로부터 데이터를 읽어오는 시작 위치가 된다
  • ex) Write 모드 마지막으로 분이 설정된 경우 레지스터 포인터엔 0x01 주소가 설정되고 DS3231로부터 데이터를 읽어올 때 0x00부터 읽어오는게 아니라 레지스터 포인터에 저장된 0x01부터 설정된 크기만큼의 데이터를 읽어온다
  • 따라서 필요에 따라 레지스터 포인터를 원하는 주소로 재설정한 후 Read 를 시작해야 한다

[Write]

Master로부터 DS3231로의 데이터 전송

  • Write 모드일 때, SDA 라인의 첫번째 바이트는 7 비트 Slave 주소 + Write(0) 비트로 구성
  • 그 이후엔 DS3231내의 데이터를 쓰고자 하는 주소(WORD ADDRESS) ((예)초:0x00, 분:0x01, 시:0x02, ...), 그리고 그 다음엔 해당 주소에 입력할 값이 와야 한다
  • WORD ADDRESS 다음에 바로 이어지는 1 바이트 데이터는 WORD ADDRESS 주소에 입력되고 그 이후에 이어지는 데이터는 순서대로 WORD ADDRESS+1, WORD ADDRESS+2, ..., WORD ADDRESS+X 의 주소에 저장된다
  • ex) 3바이트 크기의 배열 : [0]seconds 주소(0x00), [1]seconds, [2]minutes 의 배열을 전송하는 경우, [0]부터 순서대로 DS3231에 전송되어 0x00 주소에 seconds가 입력되고 그 다음으로 주소가 +1되어 0x01에 minutes가 전송됨
  • Write 모드를 사용할 때 마지막으로 사용된 DS3231 내부 주소(WORD ADDRESS)가 레지스터 포인터에 설정되어 이후 Read 모드 사용시 데이터를 읽어오기 시작하는 시작점으로 사용된다

[CubeMX 설정]

I2C 설정

  • Fast Mode 설정, 7-bit 주소 설정, 인터럽트 활성화

[코드]

- 정의

DS3231 타임 레지스터 구성 및 주소

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/*
DS3231 주소 
실제 통신에선 원 주소인 0x68을 MSB 방향으로 1비트 시프트한 값에 Read or Write 비트가 합해져 1바이트가 된다
*/
#define ds3231_addr 0x68<<1	
#define ds3231_sec_addr 0x00		//DS3231 내 초(sec) 레지스터 주소
#define ds3231_min_addr 0x01	//분(min) 레지스터 주소
#define ds3231_hour_addr 0x02	//시간
#define ds3231_day_addr 0x03	//요일
#define ds3231_date_addr 0x04	//날짜
#define ds3231_month_addr 0x05	//달
#define ds3231_year_addr 0x06	//년도
/*
DS3231 시간 레지스터(0x02)의 6번 비트는 12/24 시간 설정, 
5번 비트는 AM/PM or 24 시간 표시를 사용할 때 2x 시간일 경우 사용됨
*/
#define ds3231_AM 0		//시간 구조체의 시간 부분을 AM으로 설정할 때 사용
#define ds3231_PM 1		//시간 구조체의 시간 부분을 PM으로 설정할 때 사용
#define ds3231_24_hour 2	//DS3231에 쓸 구조체의 시간 부분을 24시간으로 설정할 때 사용
#define ds3231_day 1	//시간 알람 구조체의 날짜/요일 설정 부분을 요일로 설정할 때 사용
#define ds3231_date 0	//시간 알람 구조체의 날짜/요일 설정 부분을 날짜로 설정할 때 사용
#define ds3231_alarm1_addr 0x07	//DS3231 알람1 레지스터 주소
#define ds3231_alarm2_addr 0x0B	//DS3231 알람2 레지스터 주소
#define ds3231_ctrl_addr 0x0E	//DS3231 컨트롤 레지스터 주소
#define ds3231_temp_addr 0x11
/* USER CODE END PD */
  • DS3231 주소 및 DS3231 내의 레지스터 주소들 정의

- 구조체 

/*
시간 레지스터에서 AM/PM or 24시간 표기 설정에 사용
*/
typedef struct
{
	//12시간 표기를 사용할 경우엔 ds3231_AM or define ds3231_PM, 24시간 표기일 경우엔 ds3231_24_hour 입력
	uint8_t am_pm_24;	//AM or PM or 24시간 표기 사용 설정
	uint8_t hour;	//12, 24시간 표기에 상관없이 입력할 시간(Hour) 설정
}am_pm_24;

/*
DS3231로부터 시간을 읽어오거나 시간 데이터를 쓸 때 사용할 시간 구조체
*/
typedef struct
{
	uint8_t	sec;
	uint8_t min;
	am_pm_24 hour_select;
	uint8_t day;
	uint8_t date;
	uint8_t month;
	uint8_t year;
}ds3231_time;

/*
알람에서 요일 or 날짜를 쓸건지 구분 지을 구조체
*/
typedef struct
{
	uint8_t day_or_date;	//ds3231_day or ds3231_day 를 입력해 날짜 or 요일 설정
	uint8_t value;	//날짜 or 요일값 설정
}day_date;

/*
알람1을 읽어오거나 전송할 때 사용할 구조체
*/
typedef struct
{
	uint8_t sec;
	uint8_t min;
	am_pm_24 hour_select;
	day_date day_date_select;
}ds3231_Alarm1;

- 10진수, 2진수 변환 함수

/* USER CODE BEGIN PFP */

int decTobcd(uint8_t dec)
{
	return ((dec/10)*16)+(dec%10);
}
int bcdTodec(uint8_t bcd)
{
	return (bcd/16*10)+(bcd%16);
}
  • DS3231은 2진수를 사용하므로 10진수 사용을 위해선 DS3231로부터 읽어들인 데이터는 10진수로 변환해야 하고 Master 장치로부터 DS3231로 나가는 데이터는 2진화 해야 한다

- 시간 출력 함수, Read 함수 

/*
DS3231로부터 읽어 온 시간 데이터를 출력하는 함수
시간 영역에 저장된 값(시간 구조체->hour_select->am_pm_24)을 판단해
AM, PM, 24시간 표기로 구분한 뒤 출력
*/
void ds3231_print_time(ds3231_time *current_time)
{
	printf("year : %d   month : %d   date : %d   day : %d   ",current_time->year,current_time->month
			,current_time->date,current_time->day);
	switch (current_time->hour_select.am_pm_24)
	{
		//12시간 표기의 경우 0~4번 비트까지 유효한 시간 데이터, 5~6번 비트는 시간 표기 설정 비트
		case ds3231_AM :
			printf("AM : %d   ",(current_time->hour_select.hour)&0x1F);	
			break;
		case ds3231_PM :
			printf("PM : %d   ",(current_time->hour_select.hour)&0x1F);
			break;
		case ds3231_24_hour :
			printf("24 : %d   ",current_time->hour_select.hour);
			break;
		default :
			break;
	}
	printf("min : %d   sec : %d\r\n",current_time->min,current_time->sec);
}

/*
전달받은 시간 구조체 주소에 DS3231로부터 읽어 온 시간 데이터를 저장하는 함수
*/
void ds3231_read_time(ds3231_time *current_time)
{
	uint8_t ds3231_read_time_buff[7];	
	//DS3231로부터 읽어 올 데이터를 저장할 데이터 버퍼 (초,분,시간,요일,날짜,달,년도-총 7개)
    
	/*
	특정 메모리 주소에 액세스해서 원하는 갯수만큼 데이터를 읽어와서 데이터 버퍼에 저장
	*/
	HAL_I2C_Mem_Read_IT(&hi2c1,ds3231_addr,ds3231_sec_addr,I2C_MEMADD_SIZE_8BIT,ds3231_read_time_buff,7);
	//사용할 I2C, DS3231 주소, 접근할 DS3231 레지스터 주소, 메모리 블럭 크기, 읽어 온 데이터를 저장할 버퍼,
	//읽어 올 데이터 갯수
    
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
		//수신 완료 때까지 대기
	}

	/*
	DS3231로부터 읽어 온 데이터를 10진수로 변환한 뒤 전달받은 구조체에 입력
	*/
	current_time->sec=bcdTodec(ds3231_read_time_buff[0]);
	current_time->min=bcdTodec(ds3231_read_time_buff[1]);

	/*
	시간 레지스터(0x02)로부터 읽어 온 데이터의 6번 비트(0x40), 5번 비트(0x20)을 확인해 
	High(1)로 설정되있을 경우 구조체 변수에 AM, PM 설정을 한다 (24시간 표기일 경우 6번 비트 Low(0))
	*/
	if((ds3231_read_time_buff[2]&0x40)!=0)	//BIT6!=0 이므로 12시간 표기
	{
		if((ds3231_read_time_buff[2]&0x20)==0)	//BIT5==0 이므로 AM
		{
			current_time->hour_select.am_pm_24=ds3231_AM;
		}
		else	//BIT5!=0 이므로 PM
		{
			current_time->hour_select.am_pm_24=ds3231_PM;
		}
	}
	else
	{
		//24시간 표시 설정일 경우 (6번 비트 0, 5번 비트 2x 시간일 때만 1)
	}
	current_time->hour_select.hour=bcdTodec(ds3231_read_time_buff[2]);
	current_time->day=bcdTodec(ds3231_read_time_buff[3]);
	current_time->date=bcdTodec(ds3231_read_time_buff[4]);
	current_time->month=bcdTodec(ds3231_read_time_buff[5]);
	current_time->year=bcdTodec(ds3231_read_time_buff[6]);

	ds3231_print_time(current_time);	//터미널에 출력할 함수 호출
}
  • 사용자가 지정한 메모리 주소(레지스터 주소)로부터 데이터를 읽어오는 함수 사용 (HAL_I2C_Mem_Read_IT)
  • 레지스터 주소를 지정해서 데이터를 읽어오기 때문에 DS3231에 설정된 레지스터 포인터 위치를 신경 쓸 필요가 없다
  • 시간 레지스터(0x02) 자체에 AM/PM or 24시간 표기 설정을 저장할 수 있으므로 필요에 따라 6, 5번 비트를 설정해준다 (비트6 HIGH-12시간 표기, LOW-24시간 표기 /  비트5 LOW-AM, HIGH-PM (24시간 표기 사용할 경우 따로 설정할 필요는 없다. 시간이 2x시간 이상일 때 사용됨))

- Read 예시

DS3231로부터 데이터를 읽어올 때 (0x00(초)~0x04(날짜))
Start + DS3231 주소(7) + Read(1) + ACK bit(1) + Seconds(8) + ACK(1) + Minutes(8)
Minutes (8) + ACK bit (1) + Hours (8) + ACK bit (1)
Temp_MSB(8) + Temp_LSB(8) + NACK bit + Stop

  • SCL 라인이 HIGH일 때, SDA 라인이 HIGH에서 LOW로 이동하는 것을 스타트 상태로 규정
  • I2C 통신의 첫 데이터는 Slave 장치 주소가 온다
  • DS3231의 경우 1101000(0x68)의 주소를 갖는데 MSB부터 채워져야 하므로 0xD0 or 0x68<<1을 사용해야 한다
  • 7-bit 주소를 사용한다면 MSB부터 주소가 채워지고 마지막 0 bit 자리에 Read(1) 또는 Write(0) 비트가 채워진다
  • 전송이 중단되기 전까지는 SCL 라인의 9번째 비트에 상응하는 SDA 라인의 9번째 비트(Acknowledge bit)가 LOW를 유지
  • 마지막 Temp(LSB) 데이터 전송 후, SCL 라인의 9번째 신호에서 SDA 라인이 HIGH를 유지하며 Not Acknowledge 상태를 나타내고 Master가 전송을 중단하기 위해 STOP 상태 발생 시킴

해당 데이터값 (날짜/요일/시간/분/초)

- Write 함수

/*
전달받은 구조체 주소를 통해 DS3231에 전송할 시간 데이터 버퍼를 채움
*/
void ds3231_write_time(ds3231_time *ds3231_write_time_struct)
{
	uint8_t write_buf[7];	//전송에 사용할 데이터 버퍼 배열 선언
	
	//입력한 10진수 시간 데이터를 2진수화
	write_buf[0]=decTobcd(ds3231_write_time_struct->sec);
	write_buf[1]=decTobcd(ds3231_write_time_struct->min);
	write_buf[2]=decTobcd(ds3231_write_time_struct->hour_select.hour);
	write_buf[3]=decTobcd(ds3231_write_time_struct->day);
	write_buf[4]=decTobcd(ds3231_write_time_struct->date);
	write_buf[5]=decTobcd(ds3231_write_time_struct->month);
	write_buf[6]=decTobcd(ds3231_write_time_struct->year);

	/*
	24시간 표기가 아닌 12시간 표기(AM/PM)을 사용하는 경우엔 
	DS3231 시간(Hour) 레지스터(0x02)에 씌여질 데이터에 추가로 6, 5번 비트를 설정해줘야 한다
	*/
	switch(ds3231_write_time_struct->hour_select.am_pm_24)
	{
		case ds3231_AM :	//AM인 경우 6번 비트(12/24)만 High로 설정해주면 된다
			write_buf[2]|=0x40;
			break;
		case ds3231_PM :	//PM인 경우 6, 5번 비트를 High로 설정해줘야 한다
			write_buf[2]|=0x60;
			break;
		case ds3231_24_hour :
			break;
		default :
			break;
	}

	/*
	DS3231 초(Sec) 레지스터(0x00)부터 7개의 8비트 데이터 배열을 입력
	각 타임 레지스터는 8비트의 크기를 가지므로 0x00부터 0x06까지 순차적으로 8비트 데이터 7개가 입력된다
	*/
	HAL_I2C_Mem_Write_IT(&hi2c1,ds3231_addr,ds3231_sec_addr,I2C_MEMADD_SIZE_8BIT,
			write_buf,7);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
		//입력 완료까지 대기
	}
}

int main(void)
{
	...
  ds3231_time ds_time_default;	//구조체 변수 선언

  //구조체 변수 데이터 입력
  ds_time_default.sec=0;
  ds_time_default.min=43;
  ds_time_default.hour_select.am_pm_24=ds3231_PM;
  ds_time_default.hour_select.hour=7;
  ds_time_default.day=6;
  ds_time_default.date=21;
  ds_time_default.month=12;
  ds_time_default.year=19;

  ds3231_write_time(&ds_time_default);	//원하는 시간으로 설정한 구조체 변수 DS3231에 전송
}
  • 사용자가 지정한 시간 위치를 사용자가 입력한 데이터로 변경
  • 0x00 번지에 액세스해 8비트 크기의 배열 7개를 입력한다 (레지스터 주소는 8비트 입력 후 자동으로 1씩 증가한다)

- 예시

int main(void)
{
	...
  ds3231_time ds_time_default;	//구조체 변수 선언
  ds3231_read_time(&ds_time_default);	
  //현재 DS3231에 저장되어있는 시간 데이터를 읽어와 선언한 구조체 변수에 저장

  //DS3231에 새롭게 입력할 시간 데이터들을 설정 (위에서 선언한 변수 사용)
  ds_time_default.sec=0;
  ds_time_default.min=43;
  ds_time_default.hour_select.am_pm_24=ds3231_PM;
  ds_time_default.hour_select.hour=7;
  ds_time_default.day=6;
  ds_time_default.date=21;
  ds_time_default.month=12;
  ds_time_default.year=19;

  ds3231_write_time(&ds_time_default);	//구조체를 이용해 DS3231에 시간 데이터 입력
  ds3231_read_time(&ds_time_default);	
  //시간이 변경됐는지 확인하기 위해 타임 레지스터에 저장되어있는 시간 데이터 읽어옴
	...
}

첫줄 : 기존에 저장되어있던 시간 / 두번째 줄 : 변경 직후. 이후부턴 새로 설정된 시간에 기반해 작동

[알람]

알람 레지스터 테이블

  • DS3231엔 두 개의 알람이 존재한다
  • 알람1 - 초, 분, 시간, 요일 or 날짜. 총 4개의 데이터
  • 알람2 - 분, 시간, 요일 or 날짜. 총 3개의 데이터
  • 설정에 따라 타임 키핑 레지스터(DS3231에서 진행 중인 시간)값이 알람 시간과 일치하면 INT/SQW 핀이 High 에서 Low로 전환되면서 인터럽트가 발생한다
  • INT/SQW핀을 사용하려면 INT/SQW핀에 외부 풀업 레지스터를 연결해야 한다

인터럽트 감지에 사용될 GPIO 설정 (PINB4, 인터럽트 모드(Falling edge 감지), 풀업/풀다운 X)

- 알람 1

알람1 레지스터 테이블
알람1 마스크 비트 설정에 따른 알람 매칭

  • 알람1은 DS3231내 0x07 부터 0x0A까지 총 4바이트의 주소를 사용한다
  • 0x09에 저장되는 시간(Hour) 데이터의 6번 및 5번 비트 설정에 따라 12시간/24시간 표기 및 AM, PM이 설정된다
  • 0x0A에 저장되는 데이터의 6번 비트 설정에 따라 날짜를 기준으로 알람을 설정할지 아니면 요일을 기준으로 설정할지가 결정된다
  • 각 알람1 레지스터의 7번 비트 설정에 따라 (테이블 순서대로)
  • 1. 매 초마다 알람
  • 2. 타임 키핑 레지스터에 저장되어있는 시간과 알람1에 저장된 초가 일치할 때 알람
  • 3. 타임 키핑 레지스터에 저장되어있는 시간과 알람1에 저장된 분, 초가 일치할 때 알람
  • 4. 타임 키핑 레지스터에 저장되어있는 시간과 알람1에 저장된 시간, 분, 초가 일치할 때 알람
  • 5. 타임 키핑 레지스터에 저장되어있는 시간과 알람1에 저장된 날짜, 시간, 분, 초가 일치할 때 알람
  • 6. 타임 키핑 레지스터에 저장되어있는 시간과 알람1에 저장된 요일, 시간, 분, 초가 일치할 때 알람

- 알람1 설정 함수

void ds3231_set_alarm1(ds3231_Alarm1 *alarm1_data)
{
	uint8_t alarm1_buff[4];	//DS3231에 전송할 알람1 데이터 버퍼

	alarm1_buff[0]=decTobcd(alarm1_data->sec);	//초
	alarm1_buff[1]=decTobcd(alarm1_data->min);	//분
	alarm1_buff[2]=decTobcd(alarm1_data->hour_select.hour);	//시간
	alarm1_buff[3]=decTobcd(alarm1_data->day_date_select.value);	//날짜 or 요일값

	/*
	12/24 시간 및 AM, PM 설정
	*/
	switch(alarm1_data->hour_select.am_pm_24)
	{
		case ds3231_AM :	//전달된 구조체의 알람 시간이 AM일 경우
			alarm1_buff[2]|=0x40;	//BIT6 Logic High=12시간 표기, BIT5 Logic Low=AM
			break;
		case ds3231_PM :	//전달된 구조체의 알람 시간이 PM일 경우
			alarm1_buff[2]|=0x60;	//BIT6 Logic High=12시간 표기, BIT5 Logic High=PM
			break;
		case ds3231_24_hour :	//전달된 구조체의 알람 시간이 24시간 표기일 경우 (별도 설정 필요 X)
			break;
		default :
			break;
	}

	/*
	알람 기준을 요일로 할지 아니면 날짜로 할지를 설정
	*/
	switch(alarm1_data->day_date_select.day_or_date)
	{
		case ds3231_date :	//날짜 설정, BIT6 - Low
			break;
		case ds3231_day :
			alarm1_buff[3]|=0x40;	//요일 설정, BIT6 - High
			break;
		default :
			break;
	}

	HAL_I2C_Mem_Write_IT(&hi2c1,(uint16_t)ds3231_addr,(uint16_t)ds3231_alarm1_addr,I2C_MEMADD_SIZE_8BIT,
			(uint8_t*)alarm1_buff,4);
	//ds3231_alarm1_addr (0x07)에 액세스해 각 1바이트 크기를 가진 알람1 시간 데이터 4개를 전송
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
		//전송 완료까지 대기
	}
}

- DS3231에 저장된 알람1 데이터를 읽어오는 함수와 알람1 구조체 출력 함수

/*
전달받은 구조체에 저장된 알람1 시간을 출력하는 함수
*/
void ds3231_print_alarm1(ds3231_Alarm1 *current_alarm1)
{
	switch (current_alarm1->day_date_select.day_or_date)	//0x0A - Alarm1 DAY/DATE Register
	{
		case ds3231_date :	//BIT6 Low - Date
			printf("Date : %d   ",(current_alarm1->day_date_select.value)&0x3F);
			break;
		case ds3231_day :	//BIT6 High - Day
			printf("Day : %d   ",(current_alarm1->day_date_select.value)&0x0F);
			break;
		default :
			break;
	}

	switch (current_alarm1->hour_select.am_pm_24)	//0x09 - Alarm1 Hour Register
	{
		case ds3231_AM :	//BIT6 - High, BIT5 - Low
			printf("AM : %d   ",current_alarm1->hour_select.hour);
			break;
		case ds3231_PM :	//BIT6 - High, BIT5 - High
			printf("PM : %d   ",current_alarm1->hour_select.hour);
			break;
		case ds3231_24_hour :	//BIT6 - Low, BIT5 - 2x 시간일 때 High
			printf("24 : %d   ",current_alarm1->hour_select.hour);
			break;
		default :
			break;
	}
	printf("min : %d   sec : %d\r\n",current_alarm1->min,current_alarm1->sec);
}

/*
DS3231로부터 읽어 온 알람1 시간을 전달받은 구조체에 입력하는 함수
*/
void ds3231_read_alarm1(ds3231_Alarm1 *current_alarm1)
{
	uint8_t read_alarm1_buff[4];	//DS3231로부터 읽어 온 알람1 시간을 저장할 데이터 버퍼
    
	HAL_I2C_Mem_Read_IT(&hi2c1,ds3231_addr,ds3231_alarm1_addr,I2C_MEMADD_SIZE_8BIT,read_alarm1_buff,4);
	//ds3231_alarm1_addr (0x07)에 액세스해 0x0A까지 1바이트 크기를 가진 4개의 알람1 데이터를 읽어 옴
    
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
		//수신 완료까지 대기
	}
    
	/*
	수신한 데이터를 10진수로 변환 후 전달받은 구조체에 입력
	*/
	current_alarm1->sec=bcdTodec(read_alarm1_buff[0]);
	current_alarm1->min=bcdTodec(read_alarm1_buff[1]);

	if((read_alarm1_buff[2]&0x40)!=0)	//12시간 표기를 사용할 경우
	{
		if((read_alarm1_buff[2]&0x20)==0)	//알람 설정 시간이 AM일 경우
		{
			current_alarm1->hour_select.am_pm_24=ds3231_AM;
			current_alarm1->hour_select.hour=bcdTodec(read_alarm1_buff[2]&0x1F);
			//12시간 표기를 사용할 경우, 시간 레지스터에서 유효한 시간 데이터는 BIT 0 ~ BIT 4 까지
		}
		else	//알람 설정 시간이 PM일 경우
		{
			current_alarm1->hour_select.am_pm_24=ds3231_PM;
			current_alarm1->hour_select.hour=bcdTodec(read_alarm1_buff[2]&0x1F);
		}
	}
	else	//24시간 표기
	{
		current_alarm1->hour_select.hour=bcdTodec(read_alarm1_buff[2]);	
	}

	if((read_alarm1_buff[3]&0x40)!=0)	//Day일 경우 (BIT 6 - High)
	{
		current_alarm1->day_date_select.day_or_date=ds3231_day;
		current_alarm1->day_date_select.value=bcdTodec(read_alarm1_buff[3]&0x0F);
		//Day 사용할 경우, 유효한 날짜 데이터는 BIT 0 ~ BIT 3까지
	}
	else	//Date일 경우 (BIT 6 - Low)
	{
		current_alarm1->day_date_select.day_or_date=ds3231_date;
		current_alarm1->day_date_select.value=bcdTodec(read_alarm1_buff[3]&0x3F);
		//Date 사용할 경우, 유효한 날짜 데이터는 BIT 0 ~ BIT 5까지
	}

	ds3231_print_alarm1(current_alarm1);	//읽어 온 알람1 데이터를 출력하기 위한 함수 호출
}

- 알람1 활성화 함수

/*
알람1을 활성화 or 비활성화하는 함수
Control(0x0E), Control/Status(0x0F)에 현재 설정되어있는 값을 읽어 온 뒤
이 값에 활성화 또는 비활성화에 필요한 설정값만을 수정&입력하고 이 값을 DS3231에 재전송
*/
void ds3231_enable_alarm1(int enable)
{
	uint8_t ctrl_stat_buff[2];	//읽어 온 Control(0x0E), Contorl/Status(0x0F) 값을 저장할 데이터 버퍼 배열
    
	HAL_I2C_Mem_Read_IT(&hi2c1,ds3231_addr,ds3231_ctrl_addr,I2C_MEMADD_SIZE_8BIT,ctrl_stat_buff,2);
	//컨트롤 레지스터(0x0E)에 액세스해 컨트롤/스테이터스(0x0F) 레지스터까지 2바이트 데이터를 읽어온다
    
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
		//수신 완료까지 대기
	}

	if(enable==0)	//알람1 비활성화
	{
		ctrl_stat_buff[0]&=~(0x01);	//컨트롤 레지스터 비트0 A1IE 비활성화
	}
	else	//알람1 활성화
	{
		ctrl_stat_buff[0]|=(0x05);	//컨트롤 레지스터 비트3 INTCN, 비트0 A1IE 활성화
		ctrl_stat_buff[1]&=~(0x03);	//컨트롤/스테이터스 레지스터 비트1(A2F), 비트0(A1F) Clear
	}
    
	HAL_I2C_Mem_Write_IT(&hi2c1,ds3231_addr,ds3231_ctrl_addr,I2C_MEMADD_SIZE_8BIT,ctrl_stat_buff,2);
	//컨트롤 레지스터(0x0E)에 액세스해 필요에 따라 수정된 컨트롤, 컨트롤/스테이터스 레지스터 값을 전송
    
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{
		//전송 완료까지 대기
	}
}

컨트롤 레지스터 (0x0E)
스테이터스 레지스터 (0x0F)

  • 알람1을 사용하기 위해선 컨트롤 레지스터(0x0E)에 A1IE(Alarm 1 Interrupt Enable) 비트와 INTCN (Interrupt Control) 비트를 활성화(Logic 1) 해야 한다
  • 위 설정 상태에서 타임 키핑 레지스터에 저장된 값과 알람1에 저장된 값이 일치할 때, 스테이터스 레지스터(0x0F)의 A1F (Alarm 1 Flag)비트가 Logic 1로 변경되고 High 상태를 유지하던 INT/SQW 핀이 Low로 전환되면서 인터럽트가 발생한다
  • (A1F=1 일 때, SQW-HIGH / A1F=0 일 때, SQW-LOW)
  • 알람을 재활성화 하기 위해선 A1F 비트를 0으로 재설정해야 한다

- 예시

int main(void)
{
	...
  ds3231_time ds_time_default;	//시간 구조체 변수 선언
  
  //DS3231에 입력할 구조체 변수 시간 설정
  ds_time_default.sec=50;
  ds_time_default.min=43;
  ds_time_default.hour_select.am_pm_24=ds3231_PM;
  ds_time_default.hour_select.hour=7;
  ds_time_default.day=6;
  ds_time_default.date=21;
  ds_time_default.month=12;
  ds_time_default.year=19;
  
  ds3231_write_time(&ds_time_default);	//DS3231에 입력한 구조체 변수 값들을 전송
  
  
  ds3231_Alarm1 alarm1_default;	//알람1 구조체 변수 선언
  
  ds3231_read_alarm1(&alarm1_default);	//DS3231에 현재 설정된 알람1 데이터 읽어오는 함수 호출
  
  //DS3231에 입력할 알람1 구조체 변수 시간 설정
  alarm1_default.sec=0;
  alarm1_default.min=44;
  alarm1_default.hour_select.am_pm_24=ds3231_PM;
  alarm1_default.hour_select.hour=7;
  alarm1_default.day_date_select.value=6;
  alarm1_default.day_date_select.day_or_date=ds3231_day;
  
  ds3231_set_alarm1(&alarm1_default);	//DS3231에 입력한 알람1 구조체 변수 값들 전송
  ds3231_read_alarm1(&alarm1_default);	//DS3231에 설정된 알람1 값 읽어옴

  ds3231_enable_alarm1(1);	//알람1 활성화 (0 입력시 비활성화)
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
	  ds3231_read_time(&ds_time_default);
	  HAL_Delay(1000);
  }
}

/* USER CODE BEGIN 4 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	printf("external interrupt occurred\r\n");
	if (GPIO_Pin == Alarm_Interrupt_Line_Pin)
	{
		printf("Alarm interrupt occurred\r\n");
		HAL_GPIO_TogglePin(LD1_GPIO_Port,LD1_Pin);
	}
}

알람1으로 설정한 Day 6, PM 7, 44분, 0초에 인터럽트가 발생
INT/SQW 핀이 HIGH 상태를 유지하다 타임 키핑 레지스터에 저장된 시간과 알람1에 설정된 시간이 일치하면서 LOW로 전환되어 인터럽트 발생

  • A1F를 0으로 재설정해주지 않는다면 INT/SQW 핀은 LOW 상태를 계속 유지한다
  • 따라서 알람을 재활성화 하기 위해선 A1F 비트를 0으로 재설정해줘야 한다

- 알람2

알람2 레지스터 테이블
알람2 마스크 비트 테이블

  • 0x0B~0x0D까지 1바이트씩 총 3바이트의 레지스터 주소를 사용
  • 알람1과의 차이점은 초를 설정할 수 없다는 점이다 (분, 시간, 요일 or 날짜. 세 개만 설정 가능)
  • 그외에 컨트롤 레지스터에서 A2IE 비트, 스테이터스 비트에서 A2F 비트를 설정해줘야 한다는 것과
  • 마스크 비트 설정에 따른 알람 차이점이 존재한다 (마스크 비트 테이블 순서대로)
  • 1. 매 분마다 알람 발생 (매 분 00초)
  • 2. 타임 키핑 레지스터에 저장되어있는 시간과 알람2에 저장된 분이 일치할 때 알람
  • 3. 타임 키핑 레지스터에 저장되어있는 시간과 알람2에 저장된 시간, 분이 일치할 때 알람
  • 4. 타임 키핑 레지스터에 저장되어있는 시간과 알람2에 저장된 날짜, 시간, 분이 일치할 때 알람
  • 5. 타임 키핑 레지스터에 저장되어있는 시간과 알람2에 저장된 요일, 시간, 분이 일치할 때 알람

- 알람2 코드 (알람1에서 초를 설정하는 부분이 빠진 것과 알람2 설정에 따른 레지스터 주소 변경 외엔 차이점이 없다)

void ds3231_print_alarm2(ds3231_Alarm2 *current_alarm2)
{
	switch (current_alarm2->day_date_select.day_or_date)
	{
		case ds3231_date :	//bit6 Low
			printf("Date : %d   ",(current_alarm2->day_date_select.value)&0x3F);
			break;
		case ds3231_day :	//bit6 High
			printf("Day : %d   ",(current_alarm2->day_date_select.value)&0x0F);
			break;
		default :
			break;
	}

	switch (current_alarm2->hour_select.am_pm_24)
	{
		case ds3231_AM :
			printf("AM : %d   ",current_alarm2->hour_select.hour);
			break;
		case ds3231_PM :
			printf("PM : %d   ",current_alarm2->hour_select.hour);
			break;
		case ds3231_24_hour :
			printf("24 : %d   ",current_alarm2->hour_select.hour);
			break;
		default :
			break;
	}
	printf("min : %d\r\n",current_alarm2->min);
}

void ds3231_read_alarm2(ds3231_Alarm2 *current_alarm2)
{
	uint8_t read_alarm2_buff[3];
	HAL_I2C_Mem_Read_IT(&hi2c1,ds3231_addr,ds3231_alarm2_addr,I2C_MEMADD_SIZE_8BIT,read_alarm2_buff,3);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{

	}
	current_alarm2->min=bcdTodec(read_alarm2_buff[0]);

	if((read_alarm2_buff[1]&0x40)!=0)
	{
		if((read_alarm2_buff[1]&0x20)==0)
		{
			current_alarm2->hour_select.am_pm_24=ds3231_AM;
			current_alarm2->hour_select.hour=bcdTodec(read_alarm2_buff[1]&0x1F);
		}
		else
		{
			current_alarm2->hour_select.am_pm_24=ds3231_PM;
			current_alarm2->hour_select.hour=bcdTodec(read_alarm2_buff[1]&0x1F);
		}
	}
	else
	{
		current_alarm2->hour_select.hour=bcdTodec(read_alarm2_buff[2]);	//24시간 표기
	}

	if((read_alarm2_buff[2]&0x40)!=0)	//Day or Date
	{
		current_alarm2->day_date_select.day_or_date=ds3231_day;	//Day=BIT6 High
		current_alarm2->day_date_select.value=bcdTodec(read_alarm2_buff[2]&0x0F);
	}
	else
	{
		current_alarm2->day_date_select.day_or_date=ds3231_date;	//Date=BIT6 Low
		current_alarm2->day_date_select.value=bcdTodec(read_alarm2_buff[2]&0x3F);
	}

	ds3231_print_alarm2(current_alarm2);
}

void ds3231_set_alarm2(ds3231_Alarm2 *alarm2_data)
{
	uint8_t alarm2_buff[4];

	alarm2_buff[0]=decTobcd(alarm2_data->min);
	alarm2_buff[1]=decTobcd(alarm2_data->hour_select.hour);
	alarm2_buff[2]=decTobcd(alarm2_data->day_date_select.value);

	switch(alarm2_data->hour_select.am_pm_24)
	{
		case ds3231_AM :
			alarm2_buff[1]|=0x40;	//BIT5 AM/PM, Logic High=PM
			break;
		case ds3231_PM :
			alarm2_buff[1]|=0x60;	//BIT6 12/24, Logic High=12
			break;
		case ds3231_24_hour :
			break;
		default :
			break;
	}

	switch(alarm2_data->day_date_select.day_or_date)
	{
		case ds3231_date :
			break;
		case ds3231_day :
			alarm2_buff[2]|=0x40;	//day-BIT6 High
			break;
		default :
			break;
	}

	HAL_I2C_Mem_Write_IT(&hi2c1,(uint16_t)ds3231_addr,(uint16_t)ds3231_alarm2_addr,I2C_MEMADD_SIZE_8BIT,
			(uint8_t*)alarm2_buff,3);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{

	}
}

void ds3231_enable_alarm2(int enable)
{
	uint8_t ctrl_stat_buff[2];
	HAL_I2C_Mem_Read_IT(&hi2c1,ds3231_addr,ds3231_ctrl_addr,I2C_MEMADD_SIZE_8BIT,ctrl_stat_buff,2);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{

	}

	if(enable==0)
	{
		ctrl_stat_buff[0]&=~(0x02);	//A2IE disable
	}
	else
	{
		ctrl_stat_buff[0]|=(0x06);	//INTCN, A2IE enable
		ctrl_stat_buff[1]&=~(0x03);	//A1F, A2F Clear
	}
	HAL_I2C_Mem_Write_IT(&hi2c1,ds3231_addr,ds3231_ctrl_addr,I2C_MEMADD_SIZE_8BIT,ctrl_stat_buff,2);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{

	}
}

- 알람2 예시

int main(void)
{
	...
  ds3231_time ds_time_default;
  ds_time_default.sec=50;
  ds_time_default.min=29;
  ds_time_default.hour_select.am_pm_24=ds3231_AM;
  ds_time_default.hour_select.hour=10;
  ds_time_default.day=6;
  ds_time_default.date=21;
  ds_time_default.month=12;
  ds_time_default.year=19;
  ds3231_write_time(&ds_time_default);

  /*
  알람2 구조체 변수 선언 및 설정 후 DS3231에 전송
  */
  ds3231_Alarm2 alarm2_default;
  alarm2_default.min=30;
  alarm2_default.hour_select.am_pm_24=ds3231_AM;
  alarm2_default.hour_select.hour=10;
  alarm2_default.day_date_select.day_or_date=ds3231_date;
  alarm2_default.day_date_select.value=21;
  ds3231_set_alarm2(&alarm2_default);
  ds3231_read_alarm2(&alarm2_default);
  ds3231_enable_alarm2(1);
  
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
	  ds3231_read_time(&ds_time_default);
	  HAL_Delay(1000);
  }
}

/* USER CODE BEGIN 4 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	printf("external interrupt occurred\r\n");
	if (GPIO_Pin == Alarm_Interrupt_Line_Pin)
	{
		printf("Alarm interrupt occurred\r\n");
		HAL_GPIO_TogglePin(LD1_GPIO_Port,LD1_Pin);
	}
}

설정된 알람2 값이 타임 키핑 레지스터에 저장된 값과 일치하면서 인터럽트 발생

  • 알람2도 알람1과 같이 인터럽트 발생 후 스테이터스 레지스터의 A2F 비트를 0으로 재설정해줘야 알람이 재활성화된다

- 마스크 비트 단순 예시 (매 초마다 알람 인터럽트가 발생하도록 설정했을 때)

int alarm_every_sec=0;	//알람 인터럽트 발생 후 알람 재활성화 하기 위해 사용할 전역 변수

/*
매 초마다 알람을 발생 시키려면 알람1의 모든 시간 레지스터들의 7번 비트를 1로 설정해줘야 한다
*/
void ds3231_alarm_every_sec()
{
	uint8_t alarm_buff[4]={0x80,0x80,0x80,0x80};	//매 초마다 알람 발생 시키기 위한 마스크 비트 설정
	HAL_I2C_Mem_Write_IT(&hi2c1,ds3231_addr,ds3231_alarm1_addr,I2C_MEMADD_SIZE_8BIT,alarm_buff,4);
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{

	}
}

int main(void)
{
	...
    ds3231_enable_alarm1(1);	//매 초마다 알람을 발생 시킬 수 있는 건 알람1 뿐이다 (알람2는 매 분마다)
    ds3231_alarm_every_sec();
    
    while(1)
    {
      if((HAL_I2C_GetState(&hi2c1)==HAL_I2C_STATE_READY)&&(alarm_every_sec==0))
	  {
		  ds3231_read_time(&ds_time_default);
          /*
          알람 발생 이후 A1F=1, INT/SQW핀=LOW 상태를 유지해 INT/SQW핀을 통한 새 인터럽트가 발생하지 
          못하므로 A1F를 0으로 재설정해 INT/SQW핀을 다시 HIGH 상태로 만들어 준다
          */
		  ds3231_enable_alarm1(1); 
		  alarm_every_sec=1;
	  }
    }
}

/* USER CODE BEGIN 4 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	printf("external interrupt occurred\r\n");
	if (GPIO_Pin == Alarm_Interrupt_Line_Pin)
	{
		printf("Alarm interrupt occurred\r\n");
		alarm_every_sec=0;
	}
}

매 초마다 알람 인터럽트가 발생하도록 설정한 결과
알람 인터럽트 발생 후, 스테이터스 레지스터의 A1F 비트 값을 0으로 재설정해 INT/SQW 핀을 다시 HIGH로 만들어 다음 인터럽트가 발생할 수 있게끔 만들어준다

- 온도

온도 레지스터

  • DS3231의 온도 레지스터엔 64초를 주기로 갱신되는 0.25℃ 해상도를 가진 온도값이 저장된다
  • 정수 부분(레지스터 0x11), 소수 부분(0x12) 두 부분으로 나뉘어 온도값이 저장된다
  • 소수 부분은 상위 2비트(비트6, 비트7)만 사용된다
float ds3231_temperature;	//읽어 온 온도값을 저장할 float 전역 변수

void ds3231_read_temperature()
{
	uint8_t ds3231_temp_buff[2];	//상위 8비트, 하위 8비트를 읽어와야 하므로 2개의 배열을 가진 변수 생성
	
	HAL_I2C_Mem_Read_IT(&hi2c1,ds3231_addr,ds3231_temp_addr,I2C_MEMADD_SIZE_8BIT,
			ds3231_temp_buff,2);
	//ds3231_temp_addr(0x11)에 액세스해 정수(0x11), 소수(0x12) 레지스터를 읽어온다
    
	while(HAL_I2C_GetState(&hi2c1)!=HAL_I2C_STATE_READY)
	{

	}
	ds3231_temperature=ds3231_temp_buff[0]+((ds3231_temp_buff[1]>>6)*0.25);
	//온도값을 저장하기 위해 선언한 float 전역 변수에 정수 부분과 소수 부분을 합산해 저장한다
	//소수 부분은 상위 2비트에 소수 온도 데이터가 저장되므로 이를 LSB 방향으로 6비트 비트 시프트 한 뒤 계산
	//00=0.00, 01=0.25, 10=0.5, 11=0.75
    
	printf("%.2f\r\n",ds3231_temperature);
}

SK6812 RGBW 모델

  • RED, GREEN, BLUE + WHITE 네 부분으로 구성된 RGBW 모듈
  • 작동 전압 5V
  • RGBW 각각의 값은 0~255 사이의 8비트 데이터로 구성됨
  • VDD, DIN, DOUT, GND 네 부분으로 구성되며 DIN에 신호를 보내 컨트롤
  • DIN에만 제어 신호를 전달하면 되므로 SPI통신을 이용할 경우 MOSI를 DIN에 연결하여 사용

RGBW 각각의 비트에 0, 1을 구현하기 위한 방법
SK6812 RGBW LED가 필요로 하는 데이터 구조 및 인식하는 순서 (위의 표와 다르게 실제로는 GRBW 순으로 인식함)

  • RGBW 각각은 8비트 데이터로 구성되고 각각의 비트는 T0H, T0L, T1H, T1L 조합으로 0과 1을 표현함
  • 0의 경우 : 0.3us(HIGH)+0.9us(LOW) 로 구성 (T0H + T0L + T0L +T0L)
  • 1의 경우 : 0.6us(HIGH)+0.6us(LOW) 로 구성 (T1H + T1H + T1L + T1L)
  • 총 길이 : 1.2us

구동 조건

  • 여러 개의 모듈을 구동하기 위해선 32bit * 구동할 LED의 갯수만큼의 데이터를 한번에 전송해야 함
  • LED 데이터 갱신이 필요한 경우, 이전 전송 이후 80us 이상의 리셋(LOW) 상태를 유지한 뒤에 데이터 재전송해야 함
  • 데이터 라인에 첫번째로 연결된 LED가 첫 32비트(first)를 받고 나머지 값은 그 다음 LED로 넘김

구동 방식

  • DIN으로 들어간 신호가 32비트 데이터(RGBW값)를 해당 순서의 LED에 전달된 뒤 나머지 값들은 DOUT으로 나가면서 계속 이어져가는 방식

[CUBE MX 설정]

데이터 사이즈 : 8 Mbits, 보드 레이트 3.0Mbits/s, NSSP Mode : 미사용

  • 3.0Mbit/s 의 경우, 하나의 신호당 요구되는 0.3us +-0.15 오차 안에 포함되는 0.333us의 길이를 갖음
  • 이 경우 LED 데이터의 0 or 1의 비트를 표현하기 위해 4개의 비트를 필요로 함 (0.333us*4)
  • 예)RED=1의 경우, 0000 0001 -> 0=0.333us(HIGH*1)+0.999us(LOW*3) -> 1000 *7(7~1비트)
  •                                       -> 1=0.666us(HIGH*2)+0.666us(LOW*2) -> 1100 *1(0비트)
  • 총 32(R8+G8+B8+W8)*4(0과 1 구현 위한 LOW와 HIGH 조합)*구동하고자 하는 LED의 갯수만큼의 비트가 필요
  • 예) LED 하나 사용하는 경우, 32*4*1=128비트로 총 16바이트이며 각 색상별로 4바이트를 사용
  • (4.0Mbits/s, 8.0Mbits/s, 16.0Mbits/s을 사용했을 때, 출력 형태가 오차 범위내에 포함 or 걸치는 형태였으나 정상  작동 X (ex: 0=0.25us+0.75us, 1=0.5us+0.5us))

  • STM32WB에서 SPI1의 경우 APB2에 속해있으므로 APB2의 클럭을 조절해줘야 함
  • 0.333us의 신호 길이를 갖기 위해 peripheral 클럭을 24MHz로 설정한 뒤, prescaler 값을 8로 설정 (1s/3.0M=0.333us)

  • 그 외, SPI1 Interrupt 체크 및 DMA 설정

[코드]

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

#define led_num 4
//led 하나당 16바이트 사용 (4bit*8(=하나의 색)*RGBW=16바이트),+4는 리셋을 위함
#define led_spi_buffer_bytes led_num*4*4+4

/* USER CODE END PD */
  • 사용할 LED의 갯수와 그에 따른 버퍼의 크기를 define
  • MOSI의 신호가 데이터 전송 후 LOW로 떨어지지 않고 HIGH를 유지하여 LED 작동에 문제를 가져다주는 문제를 해결하기 위해 4바이트의 LOW신호를 LED 데이터 마지막에 이어서 전송

추가 LOW 신호가 없을 때, MOSI는 데이터 전송 이후 HIGH를 유지 (LED 3개 사용으로 총 48바이트의 길이)
추가로 2바이트 길이의 LOW 신호를 줬을 때, 2바이트 길이의 LOW신호 이후 MOSI 신호는 다시 HIGH로 바뀜
추가로 4바이트 길이의 LOW 신호를 줬을 때, 4바이트 길이의 LOW 신호 이후에도 LOW 상태를 유지 (정상 작동)

  • 데이터 시트의 80us 이상의 Reset(LOW) 신호를 준수하기 위해서는 4바이트가 아닌 30바이트 정도의 추가 LOW 신호를 사용 (80us/0.333us=240bits=30bytes)
  • Reset에 필요한 30바이트가 아닌 4바이트만을 사용한 이유는 단순히 개인적으로 80us 이하로 잦은 LED 색상 변경을 사용하는 경우가 없어 MOSI 신호를 LOW로 유지 시켜주는 최소한의 추가 바이트만 사용해도 괜찮겠다 생각했기 때문
/* USER CODE BEGIN PV */

struct sk_RGBW
{
	uint8_t red;
	uint8_t green;
	uint8_t blue;
	uint8_t white;
};
struct sk_RGBW sk6812_led[led_num]={0};		
//사용할 LED 갯수만큼 구조체 변수 생성
//ex) sk6812_led[0]의 경우, 첫번째 순서의 LED가 가질 색상들을 의미
uint8_t led_buffer[led_spi_buffer_bytes]={0};	//SPI를 이용해 LED에 전달될 데이터 배열

/* USER CODE END PV */
  • LED 값을 저장할 구조체와 배열을 생성
/* USER CODE BEGIN PFP */
void set_color(struct sk_RGBW *led_arr, uint8_t led_index)
{
	uint8_t i;
	uint32_t led_temp_buff[4]={0};
    //하나의 배열당 하나의 색을 담당 ex)led_temp_buff[0]=Green, led_temp_buff[1]=Red
    //LED 데이터 하나의 비트를 표현하기 위해 4개의 H, L 비트가 필요하고 총 8자리의 LED 데이터가 필요하므로
    //unsigned int 32bits 배열 사용 (4*8=32)
    
    //LED 비트 0을 표현하기 위해선 1000=0x8, 1을 표현하기 위해선 1100=0xc
    //예) Red=128일 때(1000 0000), led_temp_buff[1]=1100 1000 1000 ... 1000
	
    /*
    MSB부터 출력되므로 led_temp_buff의 MSB부터 전달받은 색 데이터의 0, 1을 구분한 뒤 채워나감
    전달받은 포인터 구조체 주소를 참조해 LED 배열의 MSB부터 데이터를 채워나감
    예) led_arr->green=1100 1000일 때 (Green=200), 
    첫번째(i=0) 루프에서 0만큼 MSB 방향으로 전체 비트를 시프트하고
    1000 0000을 곱한 뒤, 나온 값이 0이냐 1이냐에 따라 0x8(0일 때) or 0xC(1일 때)를
    (7-0)*4만큼 MSB 방향으로 시프트 시킨 뒤 32비트 배열에 저장
    i=0일 때, led_temp_buff[0]=(31,MSB) 1100 0000 0000 .... 0000 (0,LSB)
    i=1일 때, led_temp_buff[0]=(31,MSB) 1100 1100 0000 .... 0000 (0,LSB)
    i=7일 때, led_temp_buff[0]=(31,MSB) 1100 1100 1000 1000 1100 1000 1000 1000 (0,LSB)
    
    SK6812는 데이터를 RGBW순이 아닌 GRBW순으로 인식함
    */
	for(i=0;i<8;i++)
	{
		if((led_arr->green<<i)&0x80)	//Green
			led_temp_buff[0]+=(0xc<<(7-i)*4);
		else
			led_temp_buff[0]+=(0x8<<(7-i)*4);
		if((led_arr->red<<i)&0x80)	//Red
			led_temp_buff[1]+=(0xc<<(7-i)*4);
		else
			led_temp_buff[1]+=(0x8<<(7-i)*4);
		if((led_arr->blue<<i)&0x80)	//Blue
			led_temp_buff[2]+=(0xc<<(7-i)*4);
		else
			led_temp_buff[2]+=(0x8<<(7-i)*4);
		if((led_arr->white<<i)&0x80)	//WHITE
			led_temp_buff[3]+=(0xc<<(7-i)*4);
		else
			led_temp_buff[3]+=(0x8<<(7-i)*4);
	}


	/*
    led_index는 실제 LED의 순서를 의미
    ex) led_index=0 : MOSI에 연결된 첫번째 LED
    
    led_index*16에서 *16은 하나의 LED당 16바이트의 색상 데이터를 사용하기 때문
    ex) led_index=2일 경우(실제로는 세번째에 위치한 LED),
    전역변수로 선언된 led_buffer[] 배열 값 중에서 led_index[32]~led_index[47]까지의 배열만 값이 바뀌고
    나머지 배열들의 값은 유지된 채로 출력됨
    
	led_buffer 배열에 LED 데이터를 입력한 후, DMA를 이용해 LED에 출력
    각 색깔별로 4(LOW+HIGH)*8(LED 데이터(=LED 강도))=32bits=4bytes의 데이터 크기를 갖음
    ex) GREEN=200=1100 1000=1100 1100 1000 1000 ... 1000 일 때,
    led_buffer[0]=led_temp_buff[0] 31~24 비트값 1100 1100
    led_buffer[1]=led_temp_buff[0] 24~16 비트값 1000 1000
    led_buffer[2]=led_temp_buff[0] 15~8 비트값 1100 1000
    led_buffer[3]=led_temp_buff[0] 7~0 비트값 1000 1000
    (MSB부터 출력되므로 MSB를 [0]번 배열에 저장)
    
    led_buffer[4]~[7]=RED, led_buffer[8]~[11]=BLUE, led_buffer[12]~[15]=WHITE
    */
    
	for(i=0;i<4;i++)
	{
		led_buffer[(i+led_index*16)]=(led_temp_buff[0]>>(3-i)*8);	//GREEN
		led_buffer[(i+led_index*16+4)]=(led_temp_buff[1]>>(3-i)*8);	//RED
		led_buffer[(i+led_index*16+8)]=(led_temp_buff[2]>>(3-i)*8);	//BLUE
		led_buffer[(i+led_index*16+12)]=(led_temp_buff[3]>>(3-i)*8);	//WHITE
	}
	HAL_SPI_Transmit_DMA(&hspi1,led_buffer,led_spi_buffer_bytes);
    //DMA를 이용해 데이터 보냄, SPI1 사용, led_buffer 보낼 데이터, led_spi_buffer_bytes 전송할 byte의 크기
}
/* USER CODE END PFP */
  • SPI 통신을 사용하고 DMA를 이용해 RGBW 데이터를 출력하는 함수
void set_rgb_value(struct sk_RGBW *current_rgb,uint8_t red,uint8_t green,uint8_t blue,uint8_t white)
{
	current_rgb->red=red;
	current_rgb->green=green;
	current_rgb->blue=blue;
	current_rgb->white=white;
}
  • 구조체 배열 변수 주소를 전달받아 해당 배열의 RGBW색상을 설정하는 함수
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    
	  set_rgb_value(&sk6812_led[0],0,0,0,0);	//구조체 배열 변수(sk6812_led[0])에 RGBW값 설정
	  for(i=0;i<led_num;i++)
	  {
		  set_color(&sk6812_led[0],i);	//설정된 LED 갯수만큼 sk6812_led[0]에 저장된 RGBW값을 출력
	  }
	  HAL_Delay(1000);

	  set_rgb_value(&sk6812_led[0],255,0,0,0);	//RED=255, 나머지는 0으로 설정
	  for(i=0;i<led_num;i++)
	  {
		  set_color(&sk6812_led[0],i);
          //ex) LED 갯수 3, sk6812_led[0]=RED 255를 제외한 나머지 GBW값이 0일 때, 
          //	세 개의 LED가 동일한 RED=255만큼의 빛 세기를 가진채로 켜짐
	  }
	  HAL_Delay(1000);
      
	  set_rgb_value(&sk6812_led[1],0,255,0,0);	//sk6812_led[1]에 GREEN=255, 나머지=0 입력
	  set_color(&sk6812_led[1],1);	
      /*
      sk6812_led[1]에 저장된 RGBW값을 2번째 LED에 전달 
      led_index값이 0부터 시작되므로 led_index=1은 두번째 LED를 의미
      sk6812_led[1]의 값을 두번째 LED에 할당한 이유는 단순히 
      sk6812_led[0]은 첫번째 LED, sk6812_led[1]은 두번째 LED의 값을 갖는다는 순서를 맞춰준것일뿐
      중요한 부분은 set_color(&sk6812_led[1],1); 에서 LED 순서를 의미하는 led_index=1
      */
	  HAL_Delay(1000);
   }
  • 사용할 LED의 갯수를 네 개라 가정 (led_num=4)
  • 처음엔 네 개의 LED가 꺼진 상태로 시작 (RGBW=0,0,0,0)
  • 1초의 딜레이 후, 출력에 이용될 LED 배열에 RGBW 값을 입력 (RED=255, GBW=0)
  • FOR문이 네 차례 반복되며 SK6812 RGBW LED에 led_buffer[0]~led_buffer[68]까지의 버퍼를 출력
  • i=0일 때, led_buffer 중 led_buffer[0]~led_buffer[15]만 값을 가진채로 출력되므로 첫번째 LED만 켜짐 (나머지 배열은 초기화 값인 0 유지)
  • i=1일 때, led_buffer 중 led_buffer[16]~led_buffer[31]의 값이 입력되어 led_buffer[0]~led_buffer[31]까지 유효한 LED 데이터 값을 가지므로 두 번째 LED까지 켜짐
  • i=2,3 도 위의 상황이 반복되므로 결국 네 개의 LED가 켜짐
  • 출력에 사용될 RGBW 배열을 sk6812_led[0]으로 고정시켰기 때문에 네 개의 LED가 동일하게 RED=255 값의 밝기를 가진다
  • 1초의 딜레이 후, sk6812_led[1]의 값을 GREEN=255, RBW=0으로 변경
  • sk6812_led[1]의 값을 두 번째 LED(led_index=1)에 출력하기 위해 출력 함수인 set_color 사용
  • led_buffer 배열 중 두 번째 LED값에 해당하는 led_buffer[16]~led_buffer[31]의 값만이 sk6812_led[1]의 값으로 치환되고 HAL_SPI_Transmit_DMA 함수를 통해 led_buffer[0]~led_buffer[64] 네 개의 LED 전체 RGBW값을 출력함
  • 첫번째와 세번째, 네번째 LED의 값은 RED=255의 값을 유지하고 두 번째 LED만이 GREEN=255, RBW=0의 값으로 변한다

+ Recent posts