본문 바로가기
DSP, MCU/STM32 (ARM Cortex-M)

STM32 ] RTC 와 LCD 모듈을 사용한 알람시계 구현 (1)

by eteo 2022. 6. 11.

 

 

 

 

 

처음 실행시 타임세팅 모드.

업/다운/라이트/레프트 버튼으로 시간 설정. 00분에서 다운버튼 누르면 59분, 59분에서 업버튼 누르면 00분으로 변함. 해당부분 할 드라이버 매크로함수 사용하였음

시간설정 후 유저버튼 한번 누르면 노멀모드 진입하고 시계 돌아감.

 

 

 

노멀 모드에서 유저버튼을 2초이상 누르고 있으면 바로 알람세팅 모드 진입

현재시간에서 커서가 깜빡거리고 버튼으로 알람시간을 설정한 후 유저버튼을 한번 누르면 다시 노멀모드로 돌아온다.

영상은 10초 뒤로 알람맞춘것. 알람설정 시간인 PM 02:25:26 가 되면 "삐삐삐삐" 하면서 알람이 울리는데 소리가 작아서 키워야 들린다. 알람울리는 중간에 mute interval을 주는 건 HAL_Delay를 사용하지 않았기 때문에 알람이 울리는 중간에도 시간은 계속 간다.

 

 

 

LCD모듈의 라이브러리 : https://github.com/afiskon/stm32-i2c-lcd-1602

 

 

lcm1602.h 파일과 lcm1602.c  파일로 분리해주고 함수 일부분을 지우고 매개변수, 내용 등을 수정하여 사용하였다. 그리고 

void lcd_put_cur(int row, int col)

라는 함수를 추가해 행, 열만 간단히 입력해도 해당 위치로 이동하게끔 했다.

lcm1602.h
0.00MB
lcm1602.c
0.00MB

 

 

디버그 하기 쉽게 하려고 모든 변수를 전역에 때려박았다. 나중에 수정할 예정이다.

 

/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "adc.h"
#include "i2c.h"
#include "rtc.h"
#include "tim.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <string.h>
#include <stdio.h>
#include "lcm1602.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
typedef enum{
	NONE,
	UP,
	DOWN,
	RIGHT,
	LEFT,
	UNKNOWN
}_Direction;
typedef enum{
	AMPM=0,
	HOUR_T,
	HOUR_O,
	MINUTE_T,
	MINUTE_O,
	SECOND_T,
	SECOND_O,
}_SetMode;
typedef enum{
	SETTING,
	NORMAL,
	ALARM
}_MODE;
/* USER CODE END PTD */

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

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */
RTC_TimeTypeDef sTime;
RTC_DateTypeDef sDate;

RTC_TimeTypeDef aTime;
RTC_DateTypeDef aDate;

_SetMode setmode = AMPM;
char tmpTime[100]= {0,};
char ampm[2][3] = {"AM", "PM"};
uint32_t adc_value=0;
_Direction direction;

_Direction button;
_Direction button_before=NONE;
uint8_t user_pressed_flag=0;
uint8_t user_pulled_flag=0;

_MODE mode=SETTING;

uint32_t old_tick=0;
uint32_t current_tick=0;

uint32_t old_alarm_tick=0;
uint32_t current_alarm_tick=0;

uint8_t alarm_on=0;

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
_Direction getButton();
void move_cur_time(RTC_TimeTypeDef *time, _Direction direction);
/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_I2C1_Init();
  MX_RTC_Init();
  MX_ADC1_Init();
  MX_USART3_UART_Init();
  MX_TIM2_Init();
  /* USER CODE BEGIN 2 */

  LCM1602_init();
  uint8_t toggle=0;

  HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
  HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);

  /* USER CODE END 2 */

 

원래는 CubeMX에서 RTC의 ALARM A기능을 on 시키고 RTC_AlarmTypeDef 구조체를 쓸 예정이었다. 이 구조체는 멤버변수로 RTC_TimeTypeDef 구조체를 가진다. 아무튼.. 세팅 하고 코드도 추가했는데 HAL_RTC_SetAlarm_IT 를 써도 알람 인터럽트가 트리거 되지 않는다..디버그해보니 아예 알람 레지스터에 세팅이 안되는거 같아서 직접 레지스터에 접근하는 방법을 쓰면 되지 않을까 했는데 굳이 싶어서 그냥 RTC_TimeTypeDef 의 aTime이라는 구조체를 하나 더 만들어서 알람시간을 저장하는 용도로 썼다.

그리고 나중에는 RTC 시간을 백업 레지스터 저장하는 함수의 사용방법을 익혀 한번 사용해보고 싶다.

 

 

 

while문은 크게 SETTING 모드 NORMAL 모드 ALARM 모드로 나뉜다.

 

중복되는 부분, 함수화할 수 있는 부분이 많은데 일단은 생각나는대로 줄줄 썼다. 나중에 수정할 예정이다.

 

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
  // 세팅모드
	  if(mode==SETTING){
		  toggle^=1;

		  lcd_put_cur(0,0);
		  LCD_SendString("Time Setting   ");
		// ADC
		  HAL_ADC_Start(&hadc1);
		  HAL_ADC_PollForConversion(&hadc1, 10);
		  adc_value = HAL_ADC_GetValue(&hadc1);
		// 버튼 읽기
		  button = getButton();
		// 버튼 달라졌을때의 처리
		  if(button!=button_before){
			  move_cur_time(&sTime, button);
			  HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
			  button_before=button;
		  }
		// 커서 깜빡이는 부분
		  if(toggle){
			  sprintf(tmpTime,"%s %02d:%02d:%02d", ampm[sTime.TimeFormat], sTime.Hours, sTime.Minutes, sTime.Seconds);
		  }else{
			  if(setmode==AMPM){
				  sprintf(tmpTime,"   %02d:%02d:%02d", sTime.Hours, sTime.Minutes, sTime.Seconds);
			  }else if(setmode==HOUR_T){
				  sprintf(tmpTime,"%s  %d:%02d:%02d", ampm[sTime.TimeFormat], sTime.Hours%10, sTime.Minutes, sTime.Seconds);
			  }else if(setmode==HOUR_O){
				  sprintf(tmpTime,"%s %d :%02d:%02d", ampm[sTime.TimeFormat], sTime.Hours/10, sTime.Minutes, sTime.Seconds);
			  }else if(setmode==MINUTE_T){
				  sprintf(tmpTime,"%s %02d: %d:%02d", ampm[sTime.TimeFormat], sTime.Hours, sTime.Minutes%10, sTime.Seconds);
			  }else if(setmode==MINUTE_O){
				  sprintf(tmpTime,"%s %02d:%d :%02d", ampm[sTime.TimeFormat], sTime.Hours, sTime.Minutes/10, sTime.Seconds);
			  }else if(setmode==SECOND_T){
				  sprintf(tmpTime,"%s %02d:%02d: %d", ampm[sTime.TimeFormat], sTime.Hours, sTime.Minutes, sTime.Seconds%10);
			  }else if(setmode==SECOND_O){
				  sprintf(tmpTime,"%s %02d:%02d:%d ", ampm[sTime.TimeFormat], sTime.Hours, sTime.Minutes, sTime.Seconds/10);
			  }
		  }

		  lcd_put_cur(1,0);
		  LCD_SendString(tmpTime);
	// 모드 변경 버튼 확인
		  if(HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13)){
			  HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
			  mode=NORMAL;

		  }

	// 노말모드
	  }else if(mode==NORMAL){

		  HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
		  HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
		  sprintf(tmpTime,"%s %02d:%02d:%02d    ", ampm[sTime.TimeFormat], sTime.Hours, sTime.Minutes, sTime.Seconds);
		  lcd_put_cur(0,0);
		  LCD_SendString("Current Time   ");
		  lcd_put_cur(1,0);
		  LCD_SendString(tmpTime);


		  if(sTime.TimeFormat==aTime.TimeFormat
				  && sTime.Hours==aTime.Hours
				  && sTime.Minutes==aTime.Minutes
				  && sTime.Seconds==aTime.Seconds){
			  alarm_on=1;
		  }


		  if(alarm_on==1){
			  toggle^=1;
			  current_alarm_tick=HAL_GetTick();
			  if(toggle==1){
				  HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_1);
			  }else{
				  HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
			  }
			  if(current_alarm_tick-old_alarm_tick > 3000){
				  old_alarm_tick=current_alarm_tick;
				  alarm_on=0;
				  HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_1);
			  }
		  }


		  if(user_pressed_flag==1){
			  current_tick=HAL_GetTick();
			  if(current_tick-old_tick > 2000){
				  old_tick=current_tick;
				  user_pressed_flag=0;
				  mode=ALARM;
				  HAL_RTC_GetTime(&hrtc, &aTime, RTC_FORMAT_BIN);
				  HAL_RTC_GetDate(&hrtc, &aDate, RTC_FORMAT_BIN);

			  }
		  }


	// 알람모드
	  }else if(mode==ALARM){

		  toggle^=1;

		  lcd_put_cur(0,0);
		  LCD_SendString("Alarm Setting");

		  HAL_ADC_Start(&hadc1);
		  HAL_ADC_PollForConversion(&hadc1, 10);
		  adc_value = HAL_ADC_GetValue(&hadc1);

		  button = getButton();

		  if(button!=button_before){
			  move_cur_time(&aTime, button);
			  button_before=button;
		  }


		  if(toggle){
			  sprintf(tmpTime,"%s %02d:%02d:%02d  AL", ampm[aTime.TimeFormat], aTime.Hours, aTime.Minutes, aTime.Seconds);
		  }else{
			  if(setmode==AMPM){
				  sprintf(tmpTime,"   %02d:%02d:%02d  AL", aTime.Hours, aTime.Minutes, aTime.Seconds);
			  }else if(setmode==HOUR_T){
				  sprintf(tmpTime,"%s  %d:%02d:%02d  AL", ampm[aTime.TimeFormat], aTime.Hours%10, aTime.Minutes, aTime.Seconds);
			  }else if(setmode==HOUR_O){
				  sprintf(tmpTime,"%s %d :%02d:%02d  AL", ampm[aTime.TimeFormat], aTime.Hours/10, aTime.Minutes, aTime.Seconds);
			  }else if(setmode==MINUTE_T){
				  sprintf(tmpTime,"%s %02d: %d:%02d  AL", ampm[aTime.TimeFormat], aTime.Hours, aTime.Minutes%10, aTime.Seconds);
			  }else if(setmode==MINUTE_O){
				  sprintf(tmpTime,"%s %02d:%d :%02d  AL", ampm[aTime.TimeFormat], aTime.Hours, aTime.Minutes/10, aTime.Seconds);
			  }else if(setmode==SECOND_T){
				  sprintf(tmpTime,"%s %02d:%02d: %d  AL", ampm[aTime.TimeFormat], aTime.Hours, aTime.Minutes, aTime.Seconds%10);
			  }else if(setmode==SECOND_O){
				  sprintf(tmpTime,"%s %02d:%02d:%d   AL", ampm[aTime.TimeFormat], aTime.Hours, aTime.Minutes, aTime.Seconds/10);
			  }
		  }


		  lcd_put_cur(1,0);
		  LCD_SendString(tmpTime);



		  if(user_pressed_flag==1){
			  current_tick=HAL_GetTick();
			  if(current_tick-old_tick > 1){
				  old_tick=current_tick;
				  user_pressed_flag=0;
				  mode=NORMAL;
			  }
		  }
	  }

    /* USER CODE END WHILE */

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

 

ADC는 폴링모드를 썼다. 알람 음계를 출력하는데 TIM2를 쓰고 음 사이의 공백은 다른 타이머를 쓰려고 생각하고있다.

 

 

LCD 모듈의 버튼은 getButton 함수 내에서도 이전 상태랑 비교해서 아무것도 안누른 상태(NONE)에서 버튼을 누른게 감지된 순간의 것만 인정했고 while 문 내에서도 button_before 변수에 버튼상태를 저장하게끔 하여 이전버튼이랑 겹치지 않을때만 누른 것으로 인정하게끔 했다.

 

부저에 알람소리를 내게 하는건 PA0 번 핀 TIM2 CH1 을 사용했고 Prescaler는 1000-1, ARR은 180-1해서 주파수는 500Hz로 맞춰놓고, 펄스폭 설정은 ARR의 절반인 90으로했다. 지금은 3초동안 단순히 "삐삐삐삐삐" 하며 소리를 냈다 안냈다 반복하는 거라서 그냥 toggle 변수를 사용해 TIM_PWM_Start와 Stop을 반복하지만 나중에 노래를 출력하게 하려면 다른 타이머를 같이 사용해야 할것 같다.

 

 

/* USER CODE BEGIN 4 */

_Direction getButton(){

	if(adc_value > 3700){
		return NONE;
	}else if(adc_value < 20 && button_before==NONE){
		return UP;
	}else if(adc_value > 800 && adc_value < 900 && button_before==NONE){
		return DOWN;
	}else if(adc_value > 1700 && adc_value < 2100 && button_before==NONE){
		return LEFT;
	}else if(adc_value > 2700 && adc_value < 3100 && button_before==NONE){
		return RIGHT;
	}else
		return UNKNOWN;
}

void move_cur_time(RTC_TimeTypeDef *Time, _Direction direction){
	switch(direction){
	case RIGHT:
		setmode++;
		if(setmode > SECOND_O) setmode = SECOND_O;
		break;
	case LEFT:
		if(setmode > AMPM) setmode--;
		break;
	case UP:
		if(setmode==AMPM){
			Time->TimeFormat ^= 1;
		}else if(setmode==HOUR_T){
			Time->Hours+=10;
			if(!IS_RTC_HOUR12(Time->Hours)) Time->Hours = 1;
		}else if(setmode==HOUR_O){
			Time->Hours++;
			if(!IS_RTC_HOUR12(Time->Hours)) Time->Hours = 1;
		}else if(setmode==MINUTE_T){
			Time->Minutes+=10;
			if(!IS_RTC_MINUTES(Time->Minutes)) Time->Minutes = 0;
		}else if(setmode==MINUTE_O){
			Time->Minutes++;
			if(!IS_RTC_MINUTES(Time->Minutes)) Time->Minutes = 0;
		}else if(setmode==SECOND_T){
			Time->Seconds+=10;
			if(!IS_RTC_SECONDS(Time->Seconds)) Time->Seconds = 0;
		}else if(setmode==SECOND_O){
			Time->Seconds++;
			if(!IS_RTC_SECONDS(Time->Seconds)) Time->Seconds = 0;
		}
		break;
	case DOWN:
		if(setmode==AMPM){
			Time->TimeFormat ^= 1;
		}else if(setmode==HOUR_T){
			Time->Hours-=10;
			if(!IS_RTC_HOUR12(Time->Hours)) Time->Hours = 12;
		}else if(setmode==HOUR_O){
			Time->Hours--;
			if(!IS_RTC_HOUR12(Time->Hours)) Time->Hours = 12;
		}else if(setmode==MINUTE_T){
			Time->Minutes-=10;
			if(!IS_RTC_MINUTES(Time->Minutes)) Time->Minutes = 59;
		}else if(setmode==MINUTE_O){
			Time->Minutes--;
			if(!IS_RTC_MINUTES(Time->Minutes)) Time->Minutes = 59;
		}else if(setmode==SECOND_T){
			Time->Seconds-=10;
			if(!IS_RTC_SECONDS(Time->Seconds)) Time->Seconds = 59;
		}else if(setmode==SECOND_O){
			Time->Seconds--;
			if(!IS_RTC_SECONDS(Time->Seconds)) Time->Seconds = 59;
		}
		break;
	case NONE:
		break;
	case UNKNOWN:
		break;
	}


}

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	if(GPIO_Pin == GPIO_PIN_13){

		if(HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13)){
			user_pulled_flag=0;
			user_pressed_flag=1;
			old_tick=HAL_GetTick();
			current_tick=HAL_GetTick();
		}else {
			user_pulled_flag=1;
			user_pressed_flag=0;
		}

	}
}



/* USER CODE END 4 */

 

커서가 위치할 수 있는 곳은 typedef enum으로 정의해서 총 7곳인데 나중에 늘리려고 생각하고 있다.

 

유저버튼은 외부인터럽트로 핀설정을 했고 rising edge/falling edge 모두 트리거 되게 한 뒤에 콜백함수 내에서 readpin 함수로 현재 상태를 읽어 이게 rising edge에서 들어온건지 falling edge에서 들어온건지 구분하고 다른 행동을 취하게 끔했다. 버튼이 눌린상태일땐 pressed_flag 버튼에서 손 뗀 상태일땐 pulled_flag가 반전으로 켜지고, 버튼이 눌렸을 때 old_tick과 current_tick 변수를 같은값으로 초기화 한 후 while문 내에서 current_tick을 계속 갱신한다. 이를 통해 한번 클릭과 2초 이상 클릭된 것을 구분한다.. 나중엔 더블클릭도 구별할 수 있게 수정해보려고한다.