MCU
STM32F746G DISCOVERY(ST)
IDE
STM32CubeIDE
목표
CAMERA 입력을 받아 LCD로 출력

 

1. DCMI(digital camera Interface)

- MCU에 내장되어 있는 디지털 카메라 인터페이스(DCMI)

- DCMI를 이용하여 DM-CAM130 카메라 테스트

 

1-1. DM-CAM130

DM-CAM130

- Dimensions: 47.8 mm * 37.6 mm * 6mm

- Signal System: CMOS 1.3 Mega Pixel

- Resolution: Up to 1280 * 1024

- Interface:30-pin FPC connector

- Power Supply:From STM32F4DIS-BB

- Operating Temp.:-10°C ~ 70°C

- Supports still photos

- Frame Rate:15 fps for SXGA, 30 fps for VGA and CI

 

1-2. Signal

Interfacing a camera module with an MCU

제어신호

- PIXCLK는 camer의 clock를 공급한다.

- VSYNC는 frame 동기화에 사용된다.

- HSYNC는 line 동기화에 사용된다.

 

Image Data 신호

- camer의 data가 전송되는데 사용되며 병렬 연결의 경우 8bit에서 12bit까지 사용된다.

 

Serial

- camera의 Cofiguration을 위해 사용되며 주로 I2C 버스이다.

 

1-3. DCMI peripheral

DCMI slave AHB2 peripheral in STM32F2x7 line smart architecture

위와 같이 Camera data는 AHB2 주변 장치로 입력되어 GP DMA2와 연결된다.

 

1-4. RGB565

RGB565 foramt은 아래와 같이 5bit의 red value가 msb로 배치되고, 중간에 6bit의 green value가 배치되고 마지작에 5bit의 blue value가 lsb로 배치된다.

LTDC의 Pixel Format도 RGB565로 설정하고 Camera의 DMA target 주소와 LCD의 buffer주소를 동일하게 설정하면 Camera data가 LCD로 출력된다.

 

1-5. YCbYCr

YCbYCr format은 위와 같이 8bit씩 배치된다. 즉 640X480 pixel이라면 절반은 Y data이고 ¼은 Cb ¼은 Cr data이다.

 

2. STM32F746 DISCOVERTY CAMERA 관련 회로도

MCU
JH55-A OV9655 Camera Module

 

3. DCMI 설정

3-1. DCMI : Slave 8 bits External Synchro

3-2. Parameter Settings :

 

3-3. I2C GPIO Settings

 

3-4. GPIO Settings :

 

3-5. DMA : DMA2 stream1

 

3-6. NVIC : EXTI enable

- GENERATE CODE


4. main.c 수정

- 해상도에 맞추어 Sensor에 I2C data를 전송

- HAL_DCMI_Start_DMA 함수를 호출

파라미터로 hdcmi 개체와 capture mode buffer 및 사이즈

- DMA Mode로 동작하므로 손실없이 Camera Image를 그대로 보여줌

#define CAM_DMA_SIZE     (240*320)

OV9655_ReadID(&ov9655_id);
OV9655_QVGAConfig();
HAL_DCMI_Start_DMA(&hdcmi, DCMI_MODE_CONTINUOUS, (uint32_t)pBuf, CAM_DMA_SIZE/2);
 

 

TM32F746 CAMERA 테스트

'ST > STM32F746DIS' 카테고리의 다른 글

STM32F746G DISCOVERY - LCD + TOUCH  (0) 2025.04.19
STM32F746G DISCOVERY - LCD I/F  (0) 2025.04.19
STM32F746G DISCOVERY - SDRAM  (0) 2025.04.19
STM32F746G DISCOVERY - TIM(IC, OC, PWM)  (0) 2025.04.19
STM32F746G DISCOVERY - TIMER  (0) 2025.04.19
 
MCU
STM32F746G DISCOVERY(ST)
IDE
STM32CubeIDE
목표
TOUCH SCREEN 입력을 받아 LCD 화면 전환

 

1. STM32F746 DISCOVERTY LCD + TOUCH 관련 회로도

Backlight driver & PFC of LCD panel

2. I2C 설정

2-1. I2C3 : I2C

 

2-2. I2C Parameter Settings : slave address 0x70

 

2-3. I2C GPIO Settings : PH7(I2C3_SCL), PH8(I2C3_SDA)로 변경

 

2-4. GPIO Settings : PI13(LCD_INT)로 추가

 

2-5. NVIC : EXTI enable

- GENERATE CODE


3. main.c 수정

- HAL_GPIO_EXTI_Callback() 작성

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	ts_trigger = 1;
}
 

 

- SDRAM 초기화 함수와 LTDC 프레임 버퍼 주소 지정, Touch Screen 초기화

	BSP_SDRAM_Initialization(REFRESH_COUNT);
	HAL_LTDC_SetAddress(&hltdc, pBuf, 0);
	status = BSP_TS_Init(480,272);
	if(status == 0) printf("Touch Init OK\r\n");
	else printf("Touch Init Error : %d\r\n", status);
 

 

- Touch 입력을 받아 화면 색을 바꾸는 함수 작성

	while (1)
	{
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
		if(ts_trigger == 1)
		{
			ts_trigger = 0;
			if(BSP_TS_GetState(&TS_State)==0){
				x=TS_State.touchX[0];
				y=TS_State.touchY[0];
				printf("TS_x = %d , TS_y=%d \r\n",x,y);
				uint32_t rng_val = HAL_RNG_GetRandomNumber(&hrng);
				if(x < 240)
				{
					if(y < 136) buf_rect(pBuf, 0,240,0,136,rng_val & 0xffff);
					else buf_rect(pBuf, 0,240,136,272,rng_val & 0xffff);
				}
				else
				{
					if(y < 136) buf_rect(pBuf, 240,480,0,136,rng_val & 0xffff);
					else buf_rect(pBuf, 240,480,136,272,rng_val & 0xffff);
				}
			}
			HAL_Delay(10);
		}
	}
	/* USER CODE END 3 */
 
STM32F746 LCD + TOUCH 테스트

 

'ST > STM32F746DIS' 카테고리의 다른 글

STM32F746G DISCOVERY - CAMERA I/F  (0) 2025.04.19
STM32F746G DISCOVERY - LCD I/F  (0) 2025.04.19
STM32F746G DISCOVERY - SDRAM  (0) 2025.04.19
STM32F746G DISCOVERY - TIM(IC, OC, PWM)  (0) 2025.04.19
STM32F746G DISCOVERY - TIMER  (0) 2025.04.19

 

MCU
STM32F746G DISCOVERY(ST)
IDE
STM32CubeIDE
목표
LCD I/F를 이용하여 TFT LCD 출력

 

- 이전 글과 달리 IDE를 ATOLLIC → CubeIDE로 변경

- ATOLLIC은 CubeMX와 ATOLLIC을 따로 설치해야 하나, CubeIDE = CubeMX + IDE 통합 환경

- 사용상의 큰 다른점은 없으나, ATOLLIC에서 생성한 프로젝트를 CubeIDE에서 바로 사용할 수 없고 Import 해서 사용해야 함


1. LTDC(LCD-TFT Display Controller)

STM32F746G DISCOVERY Board에는 480 x 272 해상도의 4.3인치 터치패널이 있는 TFT-LCD가 달려있다.

Red : Green : Blue = 5 : 6 : 5 = 16bit Color에 480x272 해상도의 한 화면을 그릴때 메모리를 계산해 보면,

- 480 x 272 x 2byte = 261,120byte = 261Kbytes

- STM32F746의 SRAM이 320Kbytes인데, 한 화면을 그리고 나면 메모리가 없다.

그래서 외부 SDRAM을 프레임 버퍼 메모리로 사용해야 하므로, LCD 설정에 앞서 SDRAM을 설정 해야 한다.


2. STM32F746 DISCOVERTY LCD + TOUCH 관련 회로도

 

MCU

 

 

Backlight driver & PFC of LCD panel

 

3. LTDC 설정

3-1. RGB565(16 bits)

 

3-2. Clock Configuration : LCD Clock 9.6MHz로 설정

 

3-3. LTDC Parameter Settings

3-4. LTDC Layer Settings : Layer 설정

3-5. LTDC GPIO Settings : Data Bus 결선 확인

아래 회로도를 확인하여, LTDC에서 기본으로 지정하는 포트가 아니고 DISCOVERY Board에서 지정하는 포트로 변경해야 한다.

MCU

 

 

3-6. LCD DSP와 Backlight Pin 설정

 

4. RNG 설정

- 화면에 색을 랜덤으로 바꾸기 위해 Random Number Generator 를 enable

- GENERATE CODE


5. main.c 수정

- 네모 표시함수를 작성

/* USER CODE BEGIN 0 */

void buf_rect(uint16_t  *pBuf, uint32_t start_x, uint32_t stop_x,
              uint32_t start_y,uint32_t stop_y,uint16_t color)
{
    for(uint32_t j=start_y; j<stop_y; j++)
    {
        for(uint32_t i=start_x; i<stop_x; i++)
        {
            *(pBuf + j*480 + i) = (uint16_t)color;
        }
    }
}
 

 

- pBuf 주소를 지정

/* USER CODE BEGIN 1 */
    uint16_t  *pBuf = (uint16_t *)0xC0000000;
/* USER CODE END 1 */
 

 

- SDRAM 초기화 함수와 LTDC 프레임 버퍼 주소 지정

/* USER CODE BEGIN 2 */
    BSP_SDRAM_Initialization_sequence(REFRESH_COUNT);
    HAL_LTDC_SetAddress(&hltdc, pBuf, 0);  // Reconfigure the frame buffer Address
/* USER CODE END 2 */
 

 

- random 함수를 발생시키는 함수를 추가하고 여기서 발생한 32bit 정수를 반씩 나누어 색으로 이용하는 함수를 작성

	while (1)
	{
	/* USER CODE END WHILE */
	
	/* USER CODE BEGIN 3 */
		uint32_t rng_val = HAL_RNG_GetRandomNumber(&hrng);
		printf("RNG_val = 0x%08lx \r\n",rng_val);
		buf_rect(pBuf, 0,240,0,136,rng_val & 0xffff);  	rng_val >>= 16;
		buf_rect(pBuf, 240,480,0,136,rng_val & 0xffff);
	
		rng_val = HAL_RNG_GetRandomNumber(&hrng);
		printf("RNG_val = 0x%08lx \r\n",rng_val);
		buf_rect(pBuf, 0,240,136,272,rng_val & 0xffff);  	rng_val >>= 16;
		buf_rect(pBuf, 240,480,136,272,rng_val & 0xffff);
		HAL_Delay(500);
	}
	/* USER CODE END 3 */
 
STM32F746 LCD 테스트

 

'ST > STM32F746DIS' 카테고리의 다른 글

STM32F746G DISCOVERY - CAMERA I/F  (0) 2025.04.19
STM32F746G DISCOVERY - LCD + TOUCH  (0) 2025.04.19
STM32F746G DISCOVERY - SDRAM  (0) 2025.04.19
STM32F746G DISCOVERY - TIM(IC, OC, PWM)  (0) 2025.04.19
STM32F746G DISCOVERY - TIMER  (0) 2025.04.19

 

MCU
STM32F746G DISCOVERY(ST)
IDE
Atollic(TrueSTUDIO), STM32CubeMX
목표
외부 SDRAM 추가

 

1. SDRAM External memory interface

1) STM32F746 DISCOVERY SDRAM Pin map(자세히는 MB1191 참고)

 

2) SDRAM signals

SDRAM signal
Port
I/O
type
Description
Alternate function
SDCLK
PG8
O
SDRAM clock
-
SDCKE[1:0]
PC3
O
SDCKE0 : SDRAM Bank 1 Clock Enable
SDCKE1 : SDRAM Bank 2 Clock Enable
-
SDNE[1:0]
PH3
O
SDNE0: SDRAM Bank 1 Chip Enable
SDNE1: SDRAM Bank 2 Chip Enable
-
A[12:0]
A0~A11
O
Address
FMC_A[12:0]
D[31:0]
D0~D15
I/O
Bidirectional data bus
FMC_D[31:0]
BA[1:0]
PG5,PG4
O
Bank Address
FMC_A[15:14]
NRAS
PF11
O
Row Address Strobe
-
NCAS
PG15
O
Column Address Strobe
-
SDNWE
PH5
O
Write Enable
-
NBL[3:0]
PE1,PE0
O
Output Byte Mask for write accesses
(memory signal name: DQM[3:0])
FMC_NBL[3:0]

 

2. STM32CubeMX : SDRAM 추가

- 'Clock Configuration' Tab으로 이동

1) HCKL = 216MHz

 

- 'Pinout & Configuration' Tab으로 이동

1) Connectivity - FMC 선택

 

2) SDRAM1 선택

3) Clock and chip enable : SDCKE0 + SDNE0(PC3, PH3)

SDCKE0 기본 할당 PH2 → PC3으로 변경

4) Internal bank number : 4 banks(BA0, BA1)

5) Address : 12 bits(A0 ~ A11 ; 최대 13 bits)

6) Data : 16 bits

7) Byte enable : 16-bit byte enable

 

2. SDNRAS,SDNCAS,SDCLK는 회로도와 동일하게 자동 할당

1) column addr : 8bit(A0 ~A7)

2) row addr : 12bit(A0 ~A11)

3) CAS latency : 3 memory cycles(100MHz 이상 동작시)

4) write protection : disabled

5) SDRAM CLK = HCLK/2=108MHz(max 143MHz)

6) burst read : Disabled

7) read pipe delay : 1 HCLK clock cycle

 

8) Load mode register to active daly : 2

9) Exit self-refresh delay : 7

10) Self-refresh time : 4

11) SDRAM common row cycle delay : 7

12) write recovery time : 3

13) SDRAM common row precharge delay : 2

14) Row to column delay : 2

- GENERATE CODE 클릭


- FMC만 설정한다고 SDRAM을 바로 쓸수 있는게 아니고 SDRAM을 초기화 해줘야 하는데 여기서는 bsp sdram library를 사용함

- BSP library를 프로젝트 폴더에 넣고 시작할것

 

 

3. ATOLLIC 실행

1) C/C++ General > Paths and Symbols > Includes > GNU C에 BSP 폴더 추가

 

2) C/C++ General > Paths and Symbols > Source Location에 BSP 폴더 추가

 

3) BSP_SDRAM_Initialization() 수정

: Command.CommandTarget = FMC_SDRAM_CMD_TARGET_BANK1으로 되어있는지 확인

void BSP_SDRAM_Initialization(uint32_t RefreshCount)
{
  __IO uint32_t tmpmrd =0;

  /* Step 1:  Configure a clock configuration enable command */
  Command.CommandMode             = FMC_SDRAM_CMD_CLK_ENABLE;
  Command.CommandTarget           = FMC_SDRAM_CMD_TARGET_BANK1;
  Command.AutoRefreshNumber       = 1;
  Command.ModeRegisterDefinition  = 0;

  /* Send the command */
  HAL_SDRAM_SendCommand(&hsdram1, &Command, SDRAM_TIMEOUT);

  /* Step 2: Insert 100 us minimum delay */ 
  /* Inserted delay is equal to 1 ms due to systick time base unit (ms) */
  HAL_Delay(1);

  /* Step 3: Configure a PALL (precharge all) command */ 
  Command.CommandMode             = FMC_SDRAM_CMD_PALL;
  Command.CommandTarget           = FMC_SDRAM_CMD_TARGET_BANK1;
  Command.AutoRefreshNumber       = 1;
  Command.ModeRegisterDefinition  = 0;

  /* Send the command */
  HAL_SDRAM_SendCommand(&hsdram1, &Command, SDRAM_TIMEOUT);  
  
  /* Step 4: Configure an Auto Refresh command */ 
  Command.CommandMode             = FMC_SDRAM_CMD_AUTOREFRESH_MODE;
  Command.CommandTarget           = FMC_SDRAM_CMD_TARGET_BANK1;
  Command.AutoRefreshNumber       = 4;
  Command.ModeRegisterDefinition  = 0;

  /* Send the command */
  HAL_SDRAM_SendCommand(&hsdram1, &Command, SDRAM_TIMEOUT);
  
  /* Step 5: Program the external memory mode register */
  tmpmrd = (uint32_t)SDRAM_MODEREG_BURST_LENGTH_1          |
                     SDRAM_MODEREG_BURST_TYPE_SEQUENTIAL   |
                     SDRAM_MODEREG_CAS_LATENCY_3           |
                     SDRAM_MODEREG_OPERATING_MODE_STANDARD |
                     SDRAM_MODEREG_WRITEBURST_MODE_SINGLE;
  
  Command.CommandMode             = FMC_SDRAM_CMD_LOAD_MODE;
  Command.CommandTarget           = FMC_SDRAM_CMD_TARGET_BANK1;
  Command.AutoRefreshNumber       = 1;
  Command.ModeRegisterDefinition  = tmpmrd;

  /* Send the command */
  HAL_SDRAM_SendCommand(&hsdram1, &Command, SDRAM_TIMEOUT);
  
  /* Step 6: Set the refresh rate counter */
  /* Set the device refresh rate */
  HAL_SDRAM_ProgramRefreshRate(&hsdram1, RefreshCount); 
}
 

 

4) main.c 수정

: BSP_SDRAM_Initialization(REFRESH_COUNT); 호출

/* USER CODE BEGIN PV */
/* Private variables ---------------------------------------------------------*/
#define SDRAM_ADDRESS 0xC0000000

/* USER CODE BEGIN 2 */
BSP_SDRAM_Initialization(REFRESH_COUNT);
 

 

5) Linker Script 수정

: SDRAM 추가

- SDRAM 사용 예제를 찾아보면 SDRAM 주소에 직접 데이터를 쓰는 경우를 봤는데, 변수 한두개도 아니고 64Mbit 용량을 주소에다가 직접 사용하는건 비효율적이라고 본다.

MEMORY
{
RAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 320K
FLASH (rx)      : ORIGIN = 0x8000000, LENGTH = 1024K
SDRAM (xrw)      : ORIGIN = 0xC0000000, LENGTH = 8M
}
 

 

Compile시에 아래와 같이 SDRAM이 할당된 것을 확인 할 수 있다.

 

6) 실제로 SDRAM에 변수를 사용하기 위해서 .bss를 SDRAM에 할당하자.

  /* Uninitialized data section */
  . = ALIGN(4);
  .bss :
  {
    /* This is used by the startup in order to initialize the .bss secion */
    _sbss = .;         /* define a global symbol at bss start */
    __bss_start__ = _sbss;
    *(.bss)
    *(.bss*)
    *(COMMON)

    . = ALIGN(4);
    _ebss = .;         /* define a global symbol at bss end */
    __bss_end__ = _ebss;
  } >SDRAM
 

이게 잘 될까? 결론부터 얘기하면 동작하지 않는다.

SDRAM 초기화에 사용하는 변수중에 .bss로 할당 되는 변수가 있는데, SDRAM에 변수를 할당해야 하는데 SDRAM은 초기화가 되지 않았다? 아직 집을 짓지도 않았는데, 그 집주소에 택배를 보내는 꼴이다.

 

그러면 어떻게 해야 하나?

1. Bootloader로 SDRAM을 살리고 본 프로그램에서 변수를 SDRAM에 할당

2. 내가 쓰고자 하는 소스가 있는 폴더만 SDRAM에 할당 하도록 함

 

여기서는 2번 방법에 대해 설명한다. 아래와 같이 링커 스트립트를 수정하면, USER 폴더 안에 있는 소스의 모든 변수는 SDRAM에 할당 된다.

  .bss_sdram :
  {
  . = ALIGN(8);
  
  	_sbss_sdram = .; 
  	*USER* (.bss .bss* COMMON)
  	
  	. = ALIGN(8);
  } >SDRAM
  
  /* Uninitialized data section into "RAM" Ram type memory */
  . = ALIGN(4);
  .bss :
  {
    /* This is used by the startup in order to initialize the .bss section */
    _sbss = .;         /* define a global symbol at bss start */
    __bss_start__ = _sbss;
    *(.bss)
    *(.bss*)
    *(COMMON)

    . = ALIGN(4);
    _ebss = .;         /* define a global symbol at bss end */
    __bss_end__ = _ebss;
  } >RAM
 

 

Debug 폴더 안의 map 파일을 열어보면 USER 폴더안의 소스의 변수 'flag_filter'가 SDRAM에 할당 된것을 확인 할 수 있다.

 

.bss_sdram      0x0c010000        0x4
                0x0c010000                _sbss_sdram = .
 *USER*(.bss .bss* COMMON)
 .bss.flag_filter
                0x0c010000        0x4 USER\temp.o
                0x0c010000                flag_filter
                0x0c010004                _ebss_sdram = .
                0x0c010004                . = ALIGN (0x4)

.bss            0x20000074      0x1fc
                0x20000074                _sbss = .
                0x20000074                __bss_start__ = _sbss
 *(.bss)
 

'ST > STM32F746DIS' 카테고리의 다른 글

STM32F746G DISCOVERY - LCD + TOUCH  (0) 2025.04.19
STM32F746G DISCOVERY - LCD I/F  (0) 2025.04.19
STM32F746G DISCOVERY - TIM(IC, OC, PWM)  (0) 2025.04.19
STM32F746G DISCOVERY - TIMER  (0) 2025.04.19
STM32F746G DISCOVERY - UART(DMA)  (0) 2025.04.19
 
MCU
STM32F746G DISCOVERY(ST)
IDE
Atollic(TrueSTUDIO), STM32CubeMX
목표
HAL driver를 사용하여 TIMER 다양한 mode를 사용하여 LED를 1Hz(1초에 한번 켜졌다 꺼졌다)로 ON-OFF 한다.

 

TIM2를 사용하는데 TIM2는 APB1 Timer clock = 100MHz

 

1. STM32CubeMX : TIMER Output Compare 설정

- 'Pinout & Configuration' Tab으로 이동

1) Timers - TIM2 선택

2) Channel1 : Output Compare No Output 선택

3) Prescaler : 50000-1

- 100MHz / 50,000 = 2,000 = 2KHz

4) Counter Period : 1000-1

- 2,000 / 1,000 = 2Hz, 0.5s 간격으로 TIM2 Interrupt 발생

5) Output Compare : 100

- OC count가 500이 될때 마다 TIM2 Output Compare Interrupt 발생

6) NVIC : TIM2 global interrupt Enabled

- GENERATE CODE 클릭


- ATOLLIC : main.c 수정

- HAL_TIM_Base_Start_IT, HAL_TIM_OC_Start_IT ; 인터럽트 시작을 위해 추가

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_GFXSIMULATOR_Init();
  MX_USART1_UART_Init();
  MX_RTC_Init();
  MX_TIM6_Init();
  MX_TIM2_Init();
  /* USER CODE BEGIN 2 */
  HAL_TIM_Base_Start_IT(&htim2);
  HAL_TIM_OC_Start_IT(&htim2, TIM_CHANNEL_1);
  /* USER CODE END 2 */
 

- HAL_TIM_PeriodElapsedCallback(), HAL_TIM_OC_DelayElapsedCallback() 작성

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance == htim2.Instance)
	{
		HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
	}
}

void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance == htim2.Instance)
	{
		HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
	}
}
 

- 0.5s 마다 duty 10%로 LED 점멸

STM32F746 TIMER OC 테스트(LED 점멸)

2. STM32CubeMX : TIMER PWM 설정

- 'Pinout & Configuration' Tab으로 이동

1) TIM2 선택

2) Channel1 : PWM Generation No Output 선택

3) Prescaler : 50000-1

- 100MHz / 50,000 = 2,000 = 2KHz

4) Counter Period : 1000-1

- 2,000 / 1,000 = 2Hz, 0.5s 간격으로 TIM2 Interrupt 발생

5) Output Compare : 900

- Duty 90%

6) NVIC : TIM2 global interrupt Enabled

- GENERATE CODE 클릭

 

- ATOLLIC : main.c 수정

- HAL_TIM_PWM_Start ; 인터럽트 시작을 위해 추가

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_GFXSIMULATOR_Init();
  MX_USART1_UART_Init();
  MX_RTC_Init();
  MX_TIM6_Init();
  MX_TIM2_Init();
  /* USER CODE BEGIN 2 */
  HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_1);
  HAL_TIM_PWM_Start_IT(&htim2, TIM_CHANNEL_1);
  /* USER CODE END 2 */
 

- HAL_TIM_PWM_PulseFinishedCallback() 작성

- 0.5s 마다 인터럽트가 걸리는데, 인터럽트 걸릴때 마다 토글하게 되있으므로 1s 마다 LED 점멸

void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
	  if(htim->Instance == htim2.Instance)
	  {
		  HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
	  }
}
 

 

- 소스를 약간 수정해 본다.

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_GFXSIMULATOR_Init();
  MX_USART1_UART_Init();
  MX_RTC_Init();
  MX_TIM6_Init();
  MX_TIM2_Init();
  /* USER CODE BEGIN 2 */
  HAL_TIM_Base_Start_IT(&htim2);
  HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_1);
  HAL_TIM_PWM_Start_IT(&htim2, TIM_CHANNEL_1);
  /* USER CODE END 2 */
 
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance == htim2.Instance)
	{
		HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
	}
}
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
	  if(htim->Instance == htim2.Instance)
	  {
		  HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
	  }
}
 

- 0.5s 마다 duty 90%로 LED 점멸

STM32F746 TIMER PWM 테스트(LED 점멸)

 

'ST > STM32F746DIS' 카테고리의 다른 글

STM32F746G DISCOVERY - LCD I/F  (0) 2025.04.19
STM32F746G DISCOVERY - SDRAM  (0) 2025.04.19
STM32F746G DISCOVERY - TIMER  (0) 2025.04.19
STM32F746G DISCOVERY - UART(DMA)  (0) 2025.04.19
STM32F746G DISCOVERY - UART(Polling, Interrupt)  (0) 2025.04.19

 

MCU
STM32F746G DISCOVERY(ST)
IDE
Atollic(TrueSTUDIO), STM32CubeMX
목표
HAL driver를 사용하여 TIMER로 LED를 1Hz(1초에 한번 켜졌다 꺼졌다)로 ON-OFF 한다.

 

TIMER6은 외부 출력이 없는 내부 타이머. 사용에 앞서 TIM6의 기본 clock이 얼마인지 확인해야 한다. TIMER6은 APB1에 연결 되어 있음을 확인 할 수 있다.

Bus
Boundary address
Peripheral

APB1
0x4000 8000 - 0x4000 FFFF
Reserved
0x4000 7C00 - 0x4000 7FFF
UART8
0x4000 7800 - 0x4000 7BFF
UART7
0x4000 7400 - 0x4000 77FF
DAC
0x4000 7000 - 0x4000 73FF
PWR
0x4000 6C00 - 0x4000 6FFF
HDMI-CEC
0x4000 6800 - 0x4000 6BFF
CAN2
0x4000 6400 - 0x4000 67FF
CAN1
0x4000 6000 - 0x4000 63FF
I2C4
0x4000 5C00 - 0x4000 5FFF
I2C3
0x4000 5800 - 0x4000 5BFF
I2C2
0x4000 5400 - 0x4000 57FF
I2C1
0x4000 5000 - 0x4000 53FF
UART5
0x4000 4C00 - 0x4000 4FFF
UART4
0x4000 4800 - 0x4000 4BFF
USART3
0x4000 4400 - 0x4000 47FF
USART2
0x4000 4000 - 0x4000 43FF
SPDIFRX
0x4000 3C00 - 0x4000 3FFF
SPI3 / I2S3
0x4000 3800 - 0x4000 3BFF
SPI2 / I2S2
0x4000 3400 - 0x4000 37FF
Reserved
0x4000 3000 - 0x4000 33FF
IWDG
0x4000 2C00 - 0x4000 2FFF
WWDG
0x4000 2800 - 0x4000 2BFF
RTC & BKP Registers
0x4000 2400 - 0x4000 27FF
LPTIM1
0x4000 2000 - 0x4000 23FF
TIM14
0x4000 1C00 - 0x4000 1FFF
TIM13
0x4000 1800 - 0x4000 1BFF
TIM12
0x4000 1400 - 0x4000 17FF
TIM7
0x4000 1000 - 0x4000 13FF
TIM6
0x4000 0C00 - 0x4000 0FFF
TIM5
0x4000 0800 - 0x4000 0BFF
TIM4
0x4000 0400 - 0x4000 07FF
TIM3
0x4000 0000 - 0x4000 03FF
TIM2

 

​APB1 Timer clock = 100MHz

1. STM32CubeMX : TIMER 설정

- 'Pinout & Configuration' Tab으로 이동

1) TIM6 선택

2) Activated에 체크

3) Prescaler : 10000-1

- 100MHz / 5,000 = 20,000 = 20KHz

4) Counter Period : 10000-1

- 1/20,000 * 10,000 = 0.5s 마다 인터럽트 발생

5) TIM6 global interrupt : Enbled

- GENERATE CODE 클릭


- ATOLLIC : main.c

- HAL_TIM_Base_Start_IT (&htim6); 인터럽트 시작을 위해 추가

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_RTC_Init();
  MX_GFXSIMULATOR_Init();
  MX_USART1_UART_Init();
  MX_TIM6_Init();
  /* USER CODE BEGIN 2 */
  HAL_TIM_Base_Start_IT (&htim6);
  /* USER CODE END 2 */
 

- ATOLLIC : stm32f1xx_it.c 수정

- void TIM6_IRQHandler(void) 수정

- CubeMX 설정에서 0.5초마다 인터럽트가 걸리게 설정하였고 인터럽트 핸들러에서 매 인터럽트 마다 포트를 토글 하므로 1초마다(1Hz) LED가 깜박임.

void TIM6_DAC_IRQHandler(void)
{
  /* USER CODE BEGIN TIM6_DAC_IRQn 0 */

  /* USER CODE END TIM6_DAC_IRQn 0 */
  HAL_TIM_IRQHandler(&htim6);
  /* USER CODE BEGIN TIM6_DAC_IRQn 1 */
  HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
  /* USER CODE END TIM6_DAC_IRQn 1 */
}
 

직접 인터럽트 핸들러를 수정하는 방법도 있고 callback 함수를 추가하는 방법도 있으나, 여기서는 TIM6가 내부 인터럽트만 존재하므로(한가지) 인터럽트 핸들러를 수정하였음.

STM32F746 TIMER 테스트(LED 점멸)

 

 

MCU
STM32F746G DISCOVERY(ST)
IDE
Atollic(TrueSTUDIO), STM32CubeMX
목표
HAL driver를 사용하여 UART DMA mode 입출력 Test(Receive Overrun 방지)

 

- UART Firmware 작성시 아래와 같이 크게 세가지 모드로 구분 할 수 있음

- 각 mode별로 입출력 Test

1. Polling

2. Interrupt

3. DMA

이번 페이지에서는 DMA mode 사용


3. STM32CubeMX : UART-DMA 설정

- Configuration은 STM32F746 DISCOVERY - printf()를 이용하여 UART로 문자열 출력 참고

 

- DMA Settings 확인

1) DMA 선택

2) Add 클릭

3) USART1_RX : Mode : Circular, Increment : Memory

- GENERATE CODE 클릭


- ATOLLIC : main.c

- 터미널에서 Keyboard 입력을 받아 다시 Terminal로 출력 코드 작성

- 필요한 변수 선언

#define RX_MAX    1

uint8_t rcv_data[RX_MAX];
 

- while() 작성

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  HAL_UART_Receive_DMA(&huart1, rcv_data, RX_MAX);

  while (1)
  {
	  HAL_Delay(50);

	  if(rcv_data[0])
	  {
  		  printf("Rx OK = %c\r\n", rcv_data[0]);
  		  rcv_data[0] = 0x00;
  	  }

  	  if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_ORE))
  	  {
  		  printf("UART1 Overrun Error occurred.\r\n");
  		  __HAL_UART_CLEAR_IT(&huart1,UART_CLEAR_OREF);
  	  }
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
 

 

MCU
STM32F746G DISCOVERY(ST)
IDE
Atollic(TrueSTUDIO), STM32CubeMX
목표
HAL driver를 사용하여 UART각 Polling, Interrupt mode 입출력 Test(Receive Overrun 방지)

 

- UART Firmware 작성시 아래와 같이 크게 세가지 모드로 구분 할 수 있음

- 각 mode별로 입출력 Test

1. Polling

2. Interrupt

3. DMA

이번 페이지에서는 Polling, Interrupt mode 사용


1. STM32CubeMX : UART-Polling 설정

- Configuration은 STM32F746 DISCOVERY - printf()를 이용하여 UART로 문자열 출력 참고

 

- ATOLLIC : main.c

- 터미널에서 Keyboard 입력을 받아 다시 Terminal로 출력 코드 작성

- 필요한 변수 선언

#define RX_MAX    1

uint8_t rcv_data[RX_MAX];
 

- while() 작성

  while (1)
  {
	HAL_UART_Receive(&huart1, rcv_data, RX_MAX, 1000);

    if(rcv_data[0])   {
        printf("Rx OK = %c\r\n", rcv_data[0]);
        rcv_data[0] = 0x00;
    }

    if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_ORE))   {
        printf("UART1 Overrun Error occurred.\r\n");
        __HAL_UART_CLEAR_IT(&huart1,UART_CLEAR_OREF);
    }
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
 

- 위와 같이 작성하였지만 Keyboard 입력이 빠르지 않기 때문에 Overrun Error가 발생하지 않는다.

때문에 HAL_Delay만 추가 해서 Test

  while (1)
  {
	HAL_Delay(50);
	HAL_UART_Receive(&huart1, rcv_data, RX_MAX, 1000);

    if(rcv_data[0])   {
        printf("Rx OK = %c\r\n", rcv_data[0]);
        rcv_data[0] = 0x00;
    }

    if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_ORE))   {
        printf("UART1 Overrun Error occurred.\r\n");
        __HAL_UART_CLEAR_IT(&huart1,UART_CLEAR_OREF);
    }
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
 

- Delay 하나로, Keyboard 입력이 쌓임 - 바로 처리 하지 못하고 Delay - Overrun Error 발생함을 확인


2-1. STM32CubeMX : UART-Interrupt(1) 설정

- Configuration은 STM32F746 DISCOVERY - printf()를 이용하여 UART로 문자열 출력 참고

- USART1 NVIC enable 할 것

 

- ATOLLIC : main.c

- 터미널에서 Keyboard 입력을 받아 다시 Terminal로 출력 코드 작성

- 필요한 변수 및 callback() 작성

#define RX_MAX    1

uint8_t rcv_data[RX_MAX];

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)  {
	if(huart->Instance == USART1)
	{
		HAL_UART_Receive_IT(&huart1, rcv_data, RX_MAX);
	}
}
 

- while() 작성

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  HAL_UART_Receive_IT(&huart1, rcv_data, RX_MAX);
  
  while (1)
  {
	  if(rcv_data[0])
	  {
		  printf("Rx OK = %c\r\n", rcv_data[0]);
		  rcv_data[0] = 0x00;
	  }
	  
	  if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_ORE))
	  {
		  printf("UART1 Overrun Error occurred.\r\n");
		  __HAL_UART_CLEAR_IT(&huart1,UART_CLEAR_OREF);
	  }
	/* USER CODE END WHILE */
	
	/* USER CODE BEGIN 3 */
	}
 

- 위와 같이 작성하였지만 Keyboard 입력이 빠르지 않기 때문에 Overrun Error가 발생하지 않는다.

때문에 HAL_Delay만 추가 해서 Test

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  HAL_UART_Receive_IT(&huart1, rcv_data, RX_MAX);
  
  while (1)
  {
	  HAL_Delay(50);

	  if(rcv_data[0])
	  {
		  printf("Rx OK = %c\r\n", rcv_data[0]);
		  rcv_data[0] = 0x00;
	  }
	  
	  if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_ORE))
	  {
		  printf("UART1 Overrun Error occurred.\r\n");
		  __HAL_UART_CLEAR_IT(&huart1,UART_CLEAR_OREF);
	  }
	/* USER CODE END WHILE */
	
	/* USER CODE BEGIN 3 */
	}
 

- HAL_UART_Receive_IT()를 사용하였음에도 Overrun Error 발생

 

Q. printf()가 시간이 오래 걸리는데 printf()를 사용해서 문제가 발생하지 않았나? Delay가 너무 길다.

A. printf()를 사용하지 않아 문제가 발생하지 않았다 하더라도, Polling 방식과 마찬가지로, 내가 컨트롤 할 수 없는 Delay가 발생할때 Overrun Error 발생될 수 있음(데이터가 들어오면 처리 해야 하는데, delay 때문에 처리는 못하고 데이터가 또 들어오면 Overrun)

 

Q. HAL_UART_Receive_IT 버퍼 사이즈를 키우면 되지 않나?

A. 수신 사이즈 만큼의 데이터가 들어와야 인터럽트 발생되며 사이즈를 무한정 키울수도 없음.

수신 사이즈를 1000으로 키웠는데 999개가 들어온다면? 1001개가 들어온다면?

 

아래에 Overrun Error를 방지하기 위한 예제를 설명합니다.


2-2. STM32CubeMX : UART-Interrupt(2) 설정

- Configuration은 STM32F746 DISCOVERY - printf()를 이용하여 UART로 문자열 출력 참고

- USART1 NVIC enable 할 것

 

- ATOLLIC : main.c

- 터미널에서 Keyboard 입력을 받아 다시 Terminal로 출력 코드 작성

- main.c : main() 수정

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  HAL_UART_Receive_IT(&huart1, rcv_data, RX_MAX);
  
  while (1)
  {
	  HAL_Delay(50);
	  
	  if(rcv_data[0])
	  {
  		  printf("Rx OK = %c\r\n", rcv_data[0]);
  		  rcv_data[0] = 0x00;
  	  }

  	  if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_ORE))
  	  {
  		  printf("UART1 Overrun Error occurred.\r\n");
  		  __HAL_UART_CLEAR_IT(&huart1,UART_CLEAR_OREF);
  	  }
  	/* USER CODE END WHILE */

  	/* USER CODE BEGIN 3 */
  }
 

- stm32f7xx_it.c : USART1_IRQHandler 생성됨

void USART1_IRQHandler(void)
{
	/* USER CODE BEGIN USART1_IRQn 0 */

	/* USER CODE END USART1_IRQn 0 */
	HAL_UART_IRQHandler(&huart1);
	/* USER CODE BEGIN USART1_IRQn 1 */

	/* USER CODE END USART1_IRQn 1 */
}
 

- stm32f7xx_it.c : USART1_IRQHandler 수정

void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */
	if((huart1.Instance->ISR & UART_FLAG_RXNE) != RESET)
	{
		rcv_data[0] =  huart1.Instance->RDR;
	}

  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */

  /* USER CODE END USART1_IRQn 1 */
}
 

위와 같이 rx 데이터가 들어올 때 바로 읽어야 Overrun Error 발생 하지 않음

 

※ UART Interrupt mode 추가 설명(꼭 읽고 가면 좋은)

- HAL_UART_Receive_IT()를 호출하면 UART 수신 인터럽트에 관한 설정을 자동으로 해주며, UART 수신 인터럽트 대기 상태가 됨(while()이나 다른 Timer 인터럽트에서 호출 하면 데이터를 잃어 버릴수 있음)

 

- HAL_UART_Receive_IT()에서 데이터 size가 고정되어 있는것이 아니라면 한 바이트씩 수신 추천

 

- UART 수신 인터럽트가 발생(데이터가 들어오면)하면 USARTx_IRQHandler() 실행됨

 

2-1. STM32CubeMX : UART-Interrupt(1) 추가 설명

- USARTx_IRQHandler()HAL_UART_IQRHandler() 호출하여 인터럽트 관련 처리를 수행함

(STM32CubeMX에서 USART1 NVIC enable 하면 자동 생성됨)

 

- HAL_UART_IQRHandler()의 수신 인터럽트에서 HAL_UART_RxCpltCallback() 호출

 

- HAL_UART_RxCpltCallback()는 weak로 선언 되어 있으며 사용자가 원하는대로 작성, 마지막에 HAL_UART_Receive_IT()를 호출하여 다시 UART 수신 인터럽트 대기 상태로 만들 것

 

2-2. STM32CubeMX : UART-Interrupt(2) 추가 설명

- 위 USARTx_IRQHandler()의 huart1.Instance->RDR 넣는 어레이는 링버퍼로 작성 권장(데이터 잃어 버리고 싶지 않으면)

 

MCU
STM32F746G DISCOVERY(ST)
IDE
Atollic(TrueSTUDIO), STM32CubeMX
목표
printf()를 이용하여 UART로 문자열 출력

 

- STM32F746G DISCOVERY에서는 Uart1과 Uart6을 이용 가능

 

- Uart1은 디버거인 STM103과 연결되어 디버거의 VCP(Virtual Com Port)를 이용하여 별도 장비 없이 Uart를 이용 가능

- Uart6은 Arduino D0/D1 핀과 연결

 

1. STM32F746 DISCOVERTY Uart1 관련 회로도

 

2. STM32CubeMX : UART-Polling 설정

- 'Pinout & Configuration' Tab으로 이동

1) Connectivity 선택

2) USART1 항목 선택

- Mode : Asynchronous, Synchronous 등

- Hardware Flow Control :Disable

- PB6(USART1_TX), PB7(USART1_RX)가 자동 할당(녹색 표시)됨

PB6(USART1_TX) → PA9(USART1_TX)로 변경

 

- Parameter Settings 확인

1) Baud Rate : 115200 Bits/s

2) Word Length : 8 Bits

3) Parity : None

4) Stop Bits : 1

 

- NVIC Settings 확인

1) NVIC 선택

2) USART1 global interrupt : Enabled에 체크

- GENERATE CODE 클릭

 

- STM32CubeMX Version 5.1.0에서 아래와 같은 에러가 뜨는데 그냥 무시하고 써도 된다는 의견이 있다.

Is there any way to disable GFXSimulator in CubeMx 5.1? For STM32F429 CubeMx often displays warnings that some simulation settings are incorrect even for projects which have nothing to do with graphics.

Cookie Notice Cookies and similar technologies enable us to provide you with an optimized user experience and functionality of our website. They also help us to monitor its performance and to make our advertising and marketing relevant to you. You have full control over which cookies are set by clic...

community.st.com

- 무시하고 진행


3. ATOLLIC 실행

- USART1_IRQHandler 생성됨

void USART1_IRQHandler(void)
{
	/* USER CODE BEGIN USART1_IRQn 0 */

	/* USER CODE END USART1_IRQn 0 */
	HAL_UART_IRQHandler(&huart1);
	/* USER CODE BEGIN USART1_IRQn 1 */

	/* USER CODE END USART1_IRQn 1 */
}
 

- 표준 출력 변경 : 표준 출력이 Terminal I/O에 연결 되어 있기 때문에 아래를 추가하여 표준 출력을 변경 해야 함

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
int _write(int FD, char *buffer, int len)
{
	HAL_UART_Transmit (&huart1, (uint8_t*)buffer, len, 500);
	return len;
}
/* USER CODE END 0 */
 

- printf() : 사용 하여 메시지 출력

  /* USER CODE BEGIN 2 */
  printf("STM32F746 DISCOVERTY printf() TEST.\r\n");
  /* USER CODE END 2 */

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

 

- USART1은 디버거인 STM103과 연결되어 디버거의 VCP(Virtual Com Port)를 이용하여 별도 장비 없이 Uart를 이용할수 있다. 내컴퓨터 - 장치 관리자로 들어가 몇번 Com Port에 할당 되었는지 확인

- ATOLLIC - Debug로 들어가서 새 Terminal 시작

- 위에서 확인한 COM Port와 Setting값 설정

- Terminal에서 'STM32F746 DISCOVERTY printf() TEST.' 문구 확인 가능

 
MCU
STM32F746G DISCOVERY(ST)
IDE
Atollic(TrueSTUDIO), STM32CubeMX
목표
HAL driver를 사용하여 GPIO에 Switch를 연결해 외부 인터럽트 모드로 입력을 받아 LED를 ON-OFF 한다.

 

1. STM32CubeMX : GPIO OUTPUT 설정

- 'Pinout & Configuration' Tab으로 이동

1) Pinout View 로 마우스 이동

2) PI1을 Mouse 왼쪽 버튼 클릭하여 GPIO_Output 선택

3) Mouse 오른쪽 버튼 클릭하여 Enter User Label 선택 : GPIO_Output → LED 으로 변경

 

- GPIO Configuration 확인

1) GPIO 선택

2) PI1 선택

3) GPIO output level : Low(LED ON), HIGH(LED OFF) ; 초기 상태

4) GPIO mode : Output Push Pull, Output Open Drain

5) Maximum output speed : Low, Medium, High

6) User Label : LED

2. STM32CubeMX : GPIO EXTI 설정

- 'Pinout & Configuration' Tab으로 이동

1) Pinout View 로 마우스 이동

2) PI11을 Mouse 왼쪽 버튼 클릭하여 GPIO_EXTI1 선택

3) Mouse 오른쪽 버튼 클릭하여 Enter User Label 선택 : GPIO_EXTI1 → SW1 으로 변경

- GPIO Configuration 확인

1) GPIO 선택

2) PI11 선택

3) GPIO mode : External Interrupt Mode with Rising edge trigger detection

※ Rising edge로 선택 하였으나 회로 구성에 따라 스위치를 누를때 or 뗄때 Interrupt 발생

4) GPIO Pull-up/Pull-down : No pull-up and No pull-down, Pull-up, Pull-down

5) User Label : SW1

 

- NVIC 설정

1) NVIC 선택

2) EXTI line1 interrupt : Enabled에 체크

- GENERATE CODE 클릭


3. ATOLLIC : main.c 수정

- SW1 입력이 들어올 때마다 LED toggle 되는 source code

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	if(GPIO_Pin == SW1_Pin)
	{
		HAL_GPIO_TogglePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin);
	}
}
/* USER CODE END 0 */
 

- 총 16개의 외부 GPIO source는 EXTI line 0부터 15까지로 제공

※ PXn 형태의 GPIO pin들 중 n이 중복되는 핀의 EXTI 동시 사용 불가

ex) PA0, PB0, PC0, … 등의 GPIO들은 모두 EXTI0 line에 연결되므로 중복 사용 불가

 

+ Recent posts