본문 바로가기
프로젝트(Project) 모음

STM32 프로젝트 , 팩맨 게임 ( ADC in DMA mode 로 조이스틱 이용한 방향제어 , I2C LCD , Timer 인터럽트 및 PWM 사용 )

by eteo 2022. 5. 31.

개인 프로젝트

제작기간 : 3일

보드 : STM32F429ZI

Tool : STM32CubeIDE

 

I2C LCD 모듈, 조이스틱, 수동부저 사용

 

 

https://github.com/joeteo/Pacman

 

GitHub - joeteo/Pacman: STM32 Pacman game with LCD and Joystick

STM32 Pacman game with LCD and Joystick. Contribute to joeteo/Pacman development by creating an account on GitHub.

github.com

 

 

 

 

 

 

 

 

회로도

핀 연결과 설정은 아래표를 참고해주세요.

 

I2C interface 16x2 LCD
GND GND
VCC 5V
SCL SCL (PB8)
SDA SDA (PB9)

 

 

 

 

 

Dual axis Joystick
GND GND
+5V 3.3V
VRX PA3 (ADC1-Channel3)
VRY PC0 (ADC1-Channel10)
SW PC3 (GPIO input)

 

 

 

 

 

 

Passive Buzzer
+ PB0 (TIM3-Channel3)
- GND

 

 

Clock 설정

 

 

 

 

 

 

 

 

 

1. I2C LCD

 

내가 사용한 1602 LCD 모듈은 굉장히 많은 핀이 달려있는데 이를 SCL, SDA 두 개의 선 연결만으로 가능하게 해주는 I2C Interface adapter 인 PCF8574 라는 부품이 뒤에 달려있다.

 

 

datasheet를 확인하면 PCF8574의 기본주소는 아래와 같다. 사진과 같이 A2, A1, A0 가 모두 open된 상태에선 디폴트 슬레이브 주소가 0100111 이다. 8비트가 아니라 7비트인 이유는 마지막 한비트가 RW(Read/Write)를 결정하는 역할을 담당하기 때문이다. 실제 통신을 할 때는 7비트 슬레이브주소+마지막 비트가 0인 경우 write를 하고 1인 경우 read를 하겠다는 표현이 된다. 그래서 이 기본 주소를 얘기할 때 RW비트 포함 0x4E 라고 하는 사람도 있고 RW비트를 제외한 앞자리를 LSB로 보고 0x27이라고 하는 사람도 있어서 확인이 필요하다.

 

 

그리고 아래의 I2C Address set을 점퍼로 연결하면 내가 임의로 슬레이브 주소값을 바꿀 수 있어서 최대 2^3 인 8대의 LCD 모듈을 같은 라인에서 동시에 사용 가능케한다. 예를들어 A0를 점퍼나 납땜으로 단락시키면 주소가 0x4C (또는 0x26)이 된다.

 

 

 

 

 

 

그리고 나는 아래 유튜버가 공유한 i2c-lcd 라이브러리를 프로젝트 폴더에 머지하여 사용하였다.

 

 

 

 

 

 

기본 주소값은 0x4E로 표현하고 있고 아래와 같은 함수를 포함하고 있다.

 

#define SLAVE_ADDRESS_LCD 0x4E

void lcd_init (void);   // initialize lcd
void lcd_send_cmd (char cmd);  // send command to the lcd
void lcd_send_data (char data);  // send data to the lcd
void lcd_send_string (char *str);  // send string to the lcd
void lcd_put_cur(int row, int col);  // put cursor at the entered position row (0 or 1), col (0-15);
void lcd_clear (void);

 

 

그리고 LCD 모듈의 datasheet를 확인하면 CGROM에 기본 제공하는 문자들이 있으며 이걸 가져다 쓸 수 있다. 일본에서 만든거라 일본글자가 보인다.

 

 

예를 들면 팩맨의 먹이를 표현하는데 하이라이트한 문자를 사용했는데 코드는 아래와 같이 0xa5로 접근해 사용하면 된다.

 

lcd_put_cur(i,j);
lcd_send_data(0xa5);

 

 

 

 

그리고 아래 사이트에서 커스텀 캐릭터를 헥사코드로 만들어 LCD 모듈의 CGRAM에 올려 사용할 수도 있다.

https://maxpromer.github.io/LCD-Character-Creator/

 

 

datasheet를 확인하면 5x8 도트의 최대 8개의 커스텀 문자 생성이 가능하다. 한줄에 1바이트씩, 한 문자에는 8바이트가 쓰이고 최대 64바이트의 CGRAM을 사용할 수 있는 것이다.

 

 

 

 

 

나같은 경우 팩맨이 오른쪽 보는 버전(입벌린것/입닫은것), 왼쪽보는 버전(입벌린것/입닫은것), 적으로 사용할 문어 해서 총 5개의 커스텀 캐릭터를 만들었다.

 

 

 

 

아래 코드가 CGRAM에 커스텀 캐릭터를 쓰는 부분이다. lcd_send_cmd 함수로 CGRAM 위치를 지정하고 lcd_send_data로 함수로 데이터를 보낸다.

 

  // write to CGRAM
  char pac1[] = {0x07, 0x0F, 0x1E, 0x1C, 0x1C, 0x1E, 0x0F, 0x07};
  char pac2[] = {0x07, 0x0F, 0x1E, 0x1C, 0x1E, 0x1F, 0x0F, 0x00};

  char pac3[] = {0x1C, 0x1E, 0x0F, 0x07, 0x07, 0x0F, 0x1E, 0x1C};
  char pac4[] = {0x1C, 0x1E, 0x0F, 0x07, 0x0F, 0x1F, 0x1E, 0x00};

  char enemy[] = {0x1F, 0x15, 0x1F, 0x1F, 0x0E, 0x0A, 0x0A, 0x1B};

  lcd_send_cmd(0x40);
  for(int i=0; i<8; i++) lcd_send_data(pac1[i]);

  lcd_send_cmd(0x40+8);
  for(int i=0; i<8; i++) lcd_send_data(pac2[i]);

  lcd_send_cmd(0x40+16);
  for (int i=0; i<8; i++) lcd_send_data(pac3[i]);

  lcd_send_cmd(0x40+24);
  for (int i=0; i<8; i++) lcd_send_data(pac4[i]);

  lcd_send_cmd(0x40+32);
  for (int i=0; i<8; i++) lcd_send_data(enemy[i]);

 

 

 

다음 커스텀 캐릭터를 사용할 때는 lcd_put_cur 함수로 화면상의 커서 위치를 지정하고 lcd_send_data()의 인수로 0~7 까지의 숫자를 넣어 불러쓰면 된다.

 

lcd_put_cur(0,0);
lcd_send_data(0);
lcd_send_data(1);
lcd_send_data(2);
lcd_send_data(3);
lcd_send_data(4);

 

 

 

lcd_put_cur 함수는 실제 데이터를 화면에 표시하는 DDRAM에 접근한다.

 

 

 

 

 

Cube MX에서는 I2C를 Enable 해준다.

 

 

 

그리고 I2C의 핀은 풀업으로 설정해 평소 high상태를 유지해야 한다.

나중에 기회가 되면 I2C통신에 대해 정리글을 써보도록 하겠다.

https://www.realdigital.org/doc/6da47099f4ada78618d85b3d68613ac8

 

 

 

 

 

2. ADC (in DMA mode)

 

조이스틱에는 x축, y축의 두 개의 포텐셔미터가 달려있다. 그리고 STM32는 최대 12비트 해상도의 ADC를 사용할 수 있으므로 조이스틱을 어느쪽으로 돌리느냐(CW/CCW)에 따라서 0부터 4095사이의 값으로 전압레벨을 읽을 수 있다.

 

 

 

 

 

처음엔 인터럽트를 쓰려고 했는데 아래 영상을 참고해 DMA 방식을 사용했다.

 

 

 

 

DMA란 Direct Memory Access의 약자로 주변장치(Peripheral)가 CPU와 독립적으로 메모리에 직접접근할 수 있게 해주는 기능이다. DMA 컨트롤러에 AD 컨버젼이 완료되면 자동으로 사용자가 지정한 배열에 저장한다.

 

 

핀은 보드매뉴얼을 보고 고르고 ADC1의 해당 채널을 켠다.

 

 

먼저 ADC 세팅은 따로 코드 추가없이 계속 Enable된 상태로 두기위해 아래와 같이 체크한다.

(DMA Continuous Requests는 DMA를 먼저 켜줘야 Enabled로 설정 가능하다.)

 

 

 

 

 

멀티채널이기 때문에 아래와 같이 설정해주어야 한다.

 

채널의 샘플링타임은 위의 있는 ADC Clock cycles보다는 커야하는데 여기선 84 Cycles로 설정해주었다. (참고로 ADC는 APB2 bus를 사용한다.)

 

아날로그신호의 샘플링 간격을 줄이면 디지털 컨버젼 과정에서의 정보의 왜곡을 줄일 수 있지만 그만큼 데이터양도 많아진다.

 

랭크를 눌러 채널마다 설정한다.

 

다음 DMA Setting을 눌러 아래와 같이 설정해주었다.

ADC 데이터를 저장할 변수는 배열로 선언해줄거니까 메모리의 연속된 공간에 있어서 Increment Address를 체크해주고 Peripheral은 떨어진 핀 두 개를 설정했으므로 체크 해제한다.

Circular 모드로 하고 Data Width는 Word로 하였다.

DMA Request가 Normal 모드일 때 DMA컨트롤러는 모든 데이터 전송을 완료한 이후에 중지하고 Circular 모드인 경우엔 모든 데이터 전송을 완료한 뒤 다시 메모리의 시작주소로 이동하여 전송을 계속한다.

 

 

다음과 같이 전역변수로 배열을 만든 후 

uint32_t XY[2];

 

 

USER CODE BEGIN 2 부분에 아래와 같이 HAL_ADC_Start_DMA 를 쓴다. 

HAL_ADC_Start_DMA(&hadc1, XY, 2);	// start ADC in DMA mode for Multi channel

세번째 매개변수는 length인데 word 단위로 길이가 2인 배열이니까 위와같이 한다.

 

 

 

디버그모드 Live Expressions에서 실시간으로 배열에 저장되는 XY 값을 확인할 수 있다.

 

 

주의.

ADC_Init() 이 되기 전에 DMA_Init() 이 먼저 되어야 한다. Generate Code를 너무 성급하게 눌러서 ADC_Init이 DMA_Init보다 앞에오면 정상작동하지 않는다.

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

 

 

 

 

 

 

 

3. 타이머 설정

 

TIM2 설정

 

 

타이머2는 주인공 캐릭터의 적인 문어의 움직임을 조절하기 위한 인터럽트 용도로 썼다. 

APB1 공급클락은 90MHz이고 PSC와 ARR은 위와 같이 설정해 초기값은 1Hz로 하고 게임 중간에 레벨에 따라 PSC를 조절해 1.33Hz, 2Hz로 바꿀것이다.

 

인터럽트로 쓸거니까 NVIC 세팅도 켜준다.

 

 

 

 

TIM3 설정

 

TIM3은 수동부저를 제어할 PWM 모드로 사용했다. TIM3의 클락도 APB1에서 공급된다. 초기값은 391Hz정도로 해주었고 PWM의 펄스폭은 소스코드에서 제어할거니까 초기값은 0으로 했다.

 

수동부저의 +극에 연결할 핀은 PB0이다.

 

 

 

 

4. 소스코드

 

 

 

 

/* USER CODE BEGIN Includes */
#include "i2c-lcd.h"
#include <string.h>
#include <stdlib.h>
/* USER CODE END Includes */

 

 

 

게임에 필요한 구조체를 정의하고 전역변수 등을 선언한 부분

/* USER CODE BEGIN PV */
typedef enum{
	ING,
	WIN,
	OVER
}_Game_status;
_Game_status game_status=ING;
typedef enum{
	LEVEL1=1,
	LEVEL2,
	LEVEL3
}_Level;
_Level level=LEVEL1;
typedef enum{
	UP,
	DOWN,
	RIGHT,
	LEFT,
	NONE
}_Direction;
typedef struct{
	int row;
	int col;
	int image_num;
	int past_position[2][16];
	uint8_t prey;
}_Character;

typedef struct{
	int row;
	int col;
	int image_num;
	uint8_t clock_before;
}_Enemy;

//char uart_buf[30];
uint32_t XY[2];
uint8_t ClockFlag = 0;

/* USER CODE END PV */

 

 

 

함수의 원형 선언 부분

/* USER CODE BEGIN PFP */

_Direction readJoystick();
void MoveCharacter(_Character *character, _Direction direction);
void DisplayLCD(_Character *character);
void DisplayEnemy(_Enemy enemy);
_Game_status GameCHK(_Character *character, _Enemy *enemy);
void MoveEnemy(_Enemy *enemy, _Character character, uint8_t pulse);
void StartSound();
void LoseSound();
void WinSound();
void LevelupInit(_Character *character, _Enemy *enemy);

/* USER CODE END PFP */

 

 

 

 

USER CODE BEGIN 2  부분

  /* USER CODE BEGIN 2 */

  HAL_ADC_Start_DMA(&hadc1, XY, 2);	// start ADC in DMA mode for Multi channel

  lcd_init();

  // write to CGRAM
  char pac1[] = {0x07, 0x0F, 0x1E, 0x1C, 0x1C, 0x1E, 0x0F, 0x07};
  char pac2[] = {0x07, 0x0F, 0x1E, 0x1C, 0x1E, 0x1F, 0x0F, 0x00};

  char pac3[] = {0x1C, 0x1E, 0x0F, 0x07, 0x07, 0x0F, 0x1E, 0x1C};
  char pac4[] = {0x1C, 0x1E, 0x0F, 0x07, 0x0F, 0x1F, 0x1E, 0x00};

  char enemy[] = {0x1F, 0x15, 0x1F, 0x1F, 0x0E, 0x0A, 0x0A, 0x1B};

  lcd_send_cmd(0x40);
  for(int i=0; i<8; i++) lcd_send_data(pac1[i]);	// 0

  lcd_send_cmd(0x40+8);
  for(int i=0; i<8; i++) lcd_send_data(pac2[i]);	// 1

  lcd_send_cmd(0x40+16);
  for (int i=0; i<8; i++) lcd_send_data(pac3[i]);	// 2

  lcd_send_cmd(0x40+24);
  for (int i=0; i<8; i++) lcd_send_data(pac4[i]);	// 3

  lcd_send_cmd(0x40+32);
  for (int i=0; i<8; i++) lcd_send_data(enemy[i]);	// 4

  lcd_put_cur(0, 0);
  lcd_send_string("press the button");

  lcd_put_cur(1, 4);
  lcd_send_string("to start");

  while(HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_3)){
	  HAL_Delay(100);
  }

  HAL_TIM_Base_Start_IT(&htim2);

  HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3);

  StartSound();

  lcd_clear();
  lcd_put_cur(0, 1);
  lcd_send_string("LEVEL 1");
  HAL_Delay(500);

  lcd_put_cur(0, 9);
  lcd_send_string("start!");
  HAL_Delay(800);

  // init _Character
  _Character pacman;
  memset(&pacman, 0, sizeof(pacman));
  pacman.prey=31;
  _Enemy octopus;

  // init _Enemy
  memset(&pacman, 0, sizeof(octopus));
  octopus.image_num=4;
  octopus.row=1;
  octopus.col=8;

  lcd_clear();
  lcd_put_cur(pacman.row, pacman.col);
  lcd_send_data(pacman.image_num);

  /* USER CODE END 2 */

 

LCD CGRAM에 커스텀 캐릭터를 올린다. 0~3이 팩맨 오른쪽/왼쪽, 입벌린/입닫은 캐릭터이고, 4가 문어 캐릭터이다.

 

화면에 "Press the button to start" 문자열을 출력한 후,

 

while() HAL_Delay(100)의 무한루프를 걸어 input 풀업모드로 설정해둔 조이스틱의 스위치가 눌려야만(LOW) 다음문장으로 진행되게끔 했다.

 

 

 

타이머 시작하고 (TIM2는 인터럽트, TIM3의 채널3dms PWM 모드) 캐릭터 팩맨과 Enemy 문어의 구조체변수 선언하고 memset으로 초기화한뒤 0이 아니어야하는 값을 따로 설정해준다.

 

팩맨은 LCD 0,0 좌표에서 시작하고 팩맨의 먹이는 시작위치를 제외하고 2행16열이니까 31개가 된다. 

 

문어의 image.num은 4로 하고 1행, 8열에서 시작하게 했다.

 

 

 

 

 

 

 

while문 내

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

	  if(game_status==ING){
		  MoveCharacter(&pacman, readJoystick());
		  DisplayLCD(&pacman);
		  MoveEnemy(&octopus, pacman, ClockFlag);
		  DisplayEnemy(octopus);

		  game_status=GameCHK(&pacman, &octopus);

		  HAL_Delay(100);
		  TIM3->CCR3 = 0;

	  }else if(game_status==WIN){
		  lcd_put_cur(0,4);
		  lcd_send_string("YOU WIN");
		  lcd_put_cur(1,0);
		  lcd_send_string("Congratulations!");
	  }else if(game_status==OVER){
		  lcd_put_cur(0,4);
		  lcd_send_string("YOU LOSE");
	  }

    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }

 

대부분 함수화 했기때문에 간략하다.

 

enum type 전역변수인 game_status가 ING일 때는 readJoystick의 반환값을 매개변수로 MoveCharacter를 하고 DisplayLCD 함수로 팩맨과 먹이를 같이 그린다.

 

MoveEnemy 함수로 문어의 움직임을 결정하는데 타이머2 인터럽트에 의해 토글되는 클락플래그를 변수로 받는다. DisplayEnemy 한 뒤 게임진행상황을 체크하는 GameCHK 함수의 반환값을 다시 game_status 에 써서 WIN이나 OVER인 경우 else if문으로 넘어가게 한다.

 

HAL_Delay(100)은 팩맨의 움직임 속도를 고정한 것이고, 그 아래 TIM3->CCR3 = 0; 은 팩맨이 먹이를 먹고 난 뒤 '띵' 하고 울리는 소리를 딱 100ms만 유지하고 멈추도록 그 밑에 쓴 것이다.

 

이기거나 진 경우 효과음을 내는 부분은 GameCHK 함수 안에 있고 game_status 가 바뀌면 LCD에 문자열 출력하는 부분만 나와서 계속해 출력된다.

 

 

 

 

 

 

 

조이스틱 ADC값으로 방향 읽는 함수

_Direction readJoystick(){
	  if(XY[0]>4080){
		  return RIGHT;
	  }else if(XY[0]<20){
		  return LEFT;
	  }else if(XY[1]<50){
		  return UP;
	  }else if(XY[1]>4050){
		  return DOWN;
	  }else
		  return NONE;
}

 

반환값이 enum type _Direction이고 이도저도 아닌상황에는 NONE을 리턴하게 했다.

 

 

 

 

캐릭터 칸 이동시키는 함수

void MoveCharacter(_Character *character, _Direction direction){

	switch(direction){
	case RIGHT:
		character->col++;
		if(character->col > 15) character->col = 15;
		character->image_num &= ~(0x2);
		character->image_num ^= 1;
		break;
	case LEFT:
		character->col--;
		if(character->col < 0) character->col = 0;
		character->image_num |= 0x2;
		character->image_num ^= 1;
		break;
	case UP:
		character->row--;
		if(character->row < 0) character->row = 0;
		break;
	case DOWN:
		character->row++;
		if(character->row > 1) character->row = 1;
		break;
	default :
		break;
	}
	character->past_position[character->row][character->col]=1;
}

 

위의 함수의 반환값을 바로 두번째 매개변수로 받는다. 행과 열의 최소/최대 움직임은 0-1, 0-15로 제한했고, 오른쪽/왼쪽으로 갈때마다 입벌리고 닫은 캐릭터가 토글되서 출력되게 했다. 오른쪽방향을 보는 이미지가 0,1 이고 왼쪽방향을 보는 이미지가 2,3 이다.

 

그 아래 past_position 이차원 배열의 현재 위치를 1로 만드는 것은 캐릭터가 먹이를 얼마나 먹었는지 확인하기 위한 방법으로 만든것이다. 캐릭터는 0,0 위치에서 시작하는데 모든 위치에 한번씩 다 가서 이차원배열이 전부 1로 채워진다면 먹이를 다 먹었다는 뜻이 된다.

 

 

 

 

 

화면에 출력하는 함수

void DisplayLCD(_Character *character){
	uint8_t chk=0;
	lcd_clear();
	lcd_put_cur(character->row, character->col);
	lcd_send_data(character->image_num);
	for(int i=0;i<=1;i++){
		for(int j=0;j<16;j++){
			if(character->past_position[i][j]!=1){
				lcd_put_cur(i,j);
				lcd_send_data(0xa5);
				chk++;
			}
		}
	}
	if(chk < character->prey){
		TIM3->CCR3 = TIM3->ARR /2;
		character->prey=chk;
	}
}

 

LCD 화면을 지우고 커서를 위에서 변경시킨 캐릭터의 행, 열로 옮긴 뒤 이미지를 출력한다. 그리고 팩맨의 먹이를 출력하는데 팩맨이 지나가지 않은곳(character->past_position[i][j]!=1)에만 먹이를 그린다.

 

그걸 또 chk++로 세서, 초기값31인 Character->prey가 chk 개수보다 크다면 원래 0이었던 TIM3->CCR3의 펄스폭을 ARR의 50%비율로 해서 부저를 울린다. 함수밖의 HAL_Delay(100)에 걸려서 100ms 동안만 울릴것이다. 그리고 character->prey에 남은 먹이 개수인 chk를 다시 집어넣는다.

 

 

 

 

HAL_TIM_PeriodElapsedCallback 함수를 통해 전역변수인 ClockFlag를 인터럽트 발생시 토글한다.(초기값 1Hz) 

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	ClockFlag ^= 0x01;
}

 

 

 

 

 

 

문어를 움직이는 함수

void MoveEnemy(_Enemy *enemy, _Character character, uint8_t pulse){
	uint8_t move = rand()%2;

	if(pulse==1 && enemy->clock_before==0){
		if(move==0){
			if(enemy->row!=character.row) enemy->row=character.row;
		}else if(move==1) {
			if(enemy->col > character.col){
				enemy->col--;
				if(enemy->col<0) enemy->col=0;
			}else if(enemy->col < character.col){
				enemy->col++;
				if(enemy->col>15) enemy->col=15;
			}
		}
	}

	enemy->clock_before=pulse;
}

void DisplayEnemy(_Enemy enemy){
	lcd_put_cur(enemy.row, enemy.col);
	lcd_send_data(enemy.image_num);
}

 

문어는 위치를 옮겨야되니까 구조체 포인터 매개변수로 받고 주인공의 값은 읽기만 하면 되서 걍 구조체 변수로 받았고 3번째 매개변수로 위에서 토글되는 클락펄스를 받는다.

 

이 함수에서 문어는 포인터라 멤버변수에 ->로 접근하고 주인공은 그냥 구조체 변수라 멤버에 .으로 접근한다. 

 

그리고 그 클락펄스가 상승엣지일 때만 if(pulse==1 && enemy->clock_before==0)) 문어가 움직인다.

 

문어랑 주인공이 다른 행일 때는 문어가 주인공이 있는 행으로 가고, 문어와 주인공의 열은 대소비교해서 문어가 주인공 있는 열쪽으로 한칸 가게끔 했다. 처음엔 다 if else 문으로 이어서 만들었더니 문어의 움직임이 단조로워서 위의 move 변수에 rand()%2로 옵션을 줘서 랜덤으로 행 또는 열로 움직이게 했다.

 

마지막으로 문어 구조체의 멤버변수인 clock_before에 지금 pulse를 기억시키고, LCD 화면에 출력한다.

 

 

 

 

현재 게임상황 체크하는 함수

_Game_status GameCHK(_Character *character, _Enemy *enemy){
	uint8_t chk=0;
	for(int i=0;i<=1;i++){
		for(int j=0;j<16;j++){
			if(character->past_position[i][j]==1) chk++;
		}
	}
	if(character->row==enemy->row && character->col==enemy->col){
		LoseSound();
		return OVER;
	}
	else if(chk==32 && level==1){
		WinSound();
		lcd_clear();
		lcd_put_cur(0, 1);
		lcd_send_string("LEVEL 2");
		HAL_Delay(500);
		lcd_put_cur(0, 9);
		lcd_send_string("start!");
		HAL_Delay(800);
		LevelupInit(character, enemy);
		TIM2->PSC = 6750;
		return game_status;
	}else if(chk==32 && level==2){
		WinSound();
		lcd_clear();
		lcd_put_cur(0, 1);
		lcd_send_string("LEVEL 3");
		HAL_Delay(500);
		lcd_put_cur(0, 9);
		lcd_send_string("start!");
		HAL_Delay(800);
		LevelupInit(character, enemy);
		TIM2->PSC = 4500;
		return game_status;
	}else if(chk==32 && level==3){
		WinSound();
		return WIN;
	}
	else return game_status;
}

 

반환이 enum _Game_status 타입이다. game_status는 전역변수로 만들어서 매개변수로 받지 않아도 접근할 수 있다.

 

먼저 캐릭터의 past_position 이차원 배열이 얼마나 1로 채워져 있는지 센다.

 

만약 캐릭터의 행과 열이 문어의 행과 열과 같으면 효과음 출력하고 OVER를 리턴한다.

 

레벨이 1인 상태에서 먹이를 다 먹었으면 TIM2->PSC를 줄여 문어의 속도를 빠르게 조절하고 현재 game_status인 ING를 리턴하고 레벨이 2일 때도 마찬가지이다.

레벨이 3인 상태에서 이기면 효과음 출력 후 WIN을 리턴한다.

 

위의 모든 경우에 해당하지 않은경우 그냥 현재 game_status를 리턴한다.

 

 

 

 

 

레벨업 시 enum type 전역변수인 level++ 하고, 캐릭터 위치랑 먹이 갯수, past_position 배열, 문어 위치 등 초기화 해줄게 많아서 함수화했다.

void LevelupInit(_Character *character, _Enemy *enemy){
	level++;
	character->row=0;
	character->col=0;
	character->prey=31;
	for(int i=0;i<=1;i++){
		for(int j=0;j<16;j++){
			character->past_position[i][j]=0;
		}
	}
	enemy->row=1;
	enemy->col=8;
}

 

 

 

 

 

 

효과음 출력하는 함수들

void StartSound(){
	TIM3->ARR = 156;
	TIM3->CCR3 = TIM3->ARR / 2;
	HAL_Delay(100);
	TIM3->ARR = 111;
	TIM3->CCR3 = TIM3->ARR / 2;
	HAL_Delay(100);
	// setting for prey eating sound
	TIM3->ARR = 1060;
	TIM3->CCR3 = 0;
}

void LoseSound(){
	TIM3->ARR = 290;
	TIM3->CCR3 = TIM3->ARR / 2;
	HAL_Delay(80);
	TIM3->ARR = 391;
	TIM3->CCR3 = TIM3->ARR / 2;
	HAL_Delay(80);
	TIM3->ARR = 290;
	TIM3->CCR3 = TIM3->ARR / 2;
	HAL_Delay(80);
	TIM3->ARR = 391;
	TIM3->CCR3 = TIM3->ARR / 2;
	HAL_Delay(80);
}

void WinSound(){
	TIM3->ARR = 593;
	TIM3->CCR3 = TIM3->ARR / 2;
	HAL_Delay(100);
	TIM3->CCR3 = 0;
	HAL_Delay(10);
	TIM3->CCR3 = TIM3->ARR / 2;
	HAL_Delay(100);
	TIM3->CCR3 = 0;
	HAL_Delay(10);
	TIM3->CCR3 = TIM3->ARR / 2;
	HAL_Delay(100);
	TIM3->CCR3 = 0;
	HAL_Delay(10);
	TIM3->ARR = 767;
	TIM3->CCR3 = TIM3->ARR / 2;
	HAL_Delay(100);
	TIM3->ARR = 593;
	TIM3->CCR3 = TIM3->ARR / 2;
	HAL_Delay(300);
	TIM3->ARR = 508;
	TIM3->CCR3 = TIM3->ARR / 2;
	HAL_Delay(300);
	TIM3->ARR = 1029;
	TIM3->CCR3 = TIM3->ARR / 2;
	HAL_Delay(300);
	TIM3->CCR3 = 0;
}

효과음 음계는 이 유튜버의 코드를 참고했다. https://www.youtube.com/watch?v=HU-3VD1_Pgg 

 

TIM3 CH3은 while문 이전에 PWM start 해주고 ARR로 주파수를 조절해 음계 결정하고 CCR3으로 음량 결정해 출력한다.

 

 

 

 

+ 해당 프로젝트는 깃허브에 전체공개 해놨고 라이센스도 명시 안해놓았으니 자유롭게 참고하셔도 됩니다만, 프로젝트를 통째로 자신의 것으로 둔갑시켜 과제로 제출하거나 하는일은 없었으면 하네요.