본문 바로가기
임베디드/STM32

8. [STM32] GPIO - Peripheral Register 2

by fuhehe 2024. 8. 13.

앞장에서 HAL Library의 소스를 통해 GPIO의 레지스터를 살펴보았다.

이번장에서는 앞에서 살펴보지 않은 GPIO초기화 관련 내용들을 살펴보자.

 

  • 본 블로그는 STM32를 소프트웨어 엔지니어 관점에서 바라본 블로그입니다. 따라서 회로등의 전자공학 관련 내용은 사실과 다를 수 있습니다.
  • 본 블로그에서 사용된 MCU 및 개발보드는 다음과 같습니다.
    1. NUCLEO-F429ZI (STM32F429ZIT LQFP144)
    2. NUCLEO-F439ZI (STM32F439ZIT LQFP144)

 

 

1. GPIO 초기화 함수

GPIO의 초기화 함수는 main함수에서 호출된다. 이 함수는 CubeMX가 만들어준 MX_GPIO_Init()이라는 함수이며 Project Manager에서 "Generate peripheral initialization as a pair of '.c/h' files per peripheral" 항목의 체크여부에 따라 main.c 또는 gpio.c에 구현되어 있다.

 

static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
/* USER CODE BEGIN MX_GPIO_Init_1 */
/* USER CODE END MX_GPIO_Init_1 */

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOC_CLK_ENABLE();
  __HAL_RCC_GPIOH_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();
  __HAL_RCC_GPIOD_CLK_ENABLE();
  __HAL_RCC_GPIOG_CLK_ENABLE();

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOB, LD1_Pin|LD3_Pin|LD2_Pin, GPIO_PIN_RESET);

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOG, OUT_OD_Pin|USB_PowerSwitchOn_Pin, GPIO_PIN_RESET);

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(EXT_LED1_GPIO_Port, EXT_LED1_Pin, GPIO_PIN_RESET);

  /*Configure GPIO pin : USER_BTN_Pin */
  GPIO_InitStruct.Pin = USER_BTN_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_PULLDOWN;
  HAL_GPIO_Init(USER_BTN_GPIO_Port, &GPIO_InitStruct);

  /*Configure GPIO pin : LD1_Pin */
  GPIO_InitStruct.Pin = LD1_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_PULLDOWN;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(LD1_GPIO_Port, &GPIO_InitStruct);

  /*Configure GPIO pins : LD3_Pin LD2_Pin */
  GPIO_InitStruct.Pin = LD3_Pin|LD2_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

  /*Configure GPIO pin : EXT_BTN2_Pin */
  GPIO_InitStruct.Pin = EXT_BTN2_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_PULLDOWN;
  HAL_GPIO_Init(EXT_BTN2_GPIO_Port, &GPIO_InitStruct);

  /*Configure GPIO pin : OUT_OD_Pin */
  GPIO_InitStruct.Pin = OUT_OD_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
  GPIO_InitStruct.Pull = GPIO_PULLUP;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(OUT_OD_GPIO_Port, &GPIO_InitStruct);

  /*Configure GPIO pin : USB_PowerSwitchOn_Pin */
  GPIO_InitStruct.Pin = USB_PowerSwitchOn_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(USB_PowerSwitchOn_GPIO_Port, &GPIO_InitStruct);

  /*Configure GPIO pin : USB_OverCurrent_Pin */
  GPIO_InitStruct.Pin = USB_OverCurrent_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(USB_OverCurrent_GPIO_Port, &GPIO_InitStruct);

  /*Configure GPIO pin : EXT_BTN1_Pin */
  GPIO_InitStruct.Pin = EXT_BTN1_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_PULLUP;
  HAL_GPIO_Init(EXT_BTN1_GPIO_Port, &GPIO_InitStruct);

  /*Configure GPIO pin : EXT_LED1_Pin */
  GPIO_InitStruct.Pin = EXT_LED1_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_PULLDOWN;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(EXT_LED1_GPIO_Port, &GPIO_InitStruct);

/* USER CODE BEGIN MX_GPIO_Init_2 */
/* USER CODE END MX_GPIO_Init_2 */
}

 

이 함수가 main.c에 정의되어 있으면 static형 함수로 gpio.c에 정의되어 있으면 일반 함수로 정의된다. static형 함수는 해당 파일(여기서는 main.c)에서만 호출가능한 함수라는걸 명시한다. gpio.c에 정의되어 있다면 main.c에서 호출해야 하므로 static이 빠진다.

 

함수 처음에 GPIO_InitTypeDef 구조체 형식의 GPIO_InitStruct 변수를 하나 초기화 한다. 

그리고 __HAL_RCC_GPIOx_CLK_ENABLE() 매크로 함수가 포트갯수만큼 나온다. 이름을 보면 GPIO관련 Clock Enable에 관련된 매크로라는 걸 유추할 수 있다. STM32F429/439의 경우 GPIO 포트가 A~G까지 이므로 포트 각각 Clock관련 초기화를 하고 있다.

 

그 다음으로 HAL_GPIO_WritePin()함수로 출력으로 정의된 핀에 대해 각각 Reset값을 설정한다.

 

그 밑으로는 우리가 CubeMX에서 정의한 GPIO갯수대로 각각 초기화를 수행한다.(LD2_Pin,과 LD3_Pin은 OR에 의해 한번에 초기화 진행)

[CubeMX의 GPIO설정]

 

이 초기화 부분을 보면 GPIO_InitStruct.Mode가 GPIO_MODE_INPUT, GPIO_MODE_OUTPUT_PP, GPIO_MODE_OUTPUT_OD로 정의되어 있고 GPIO_InitStruct.Pull은 GPIO_PULLDOWN, GPIO_PULLUP, GPIO_NOPULL등으로 정의되어 있는게 보인다.

  • GPIO_InitTypeDef.Mode : GPIO_MODE_INPUT, GPIO_MODE_OUTPUT_PP, GPIO_MODE_OUTPUT_OD
  • GPIO_InitTypeDef.Pull : GPIO_PULLDOWN, GPIP_PULLUP, GPIO_NOPULL
  • GPIO_InitTypeDef.Speed : GPIO_SPEED_FREQ_LOW, GPIO_SPEED_FREQ_MEDIUM, GPIO_SPEED_FREQ_HIGH, GPIO_SPEED_FREQ_VERY_HIGH
  • GPIO_InitTypeDef.Alternate : GPIO_AFn_X...

우리가 GPIOTest 프로젝트에서 CubeMX로 설정한 각 핀의 내용과 동일하다.

(Alternate의 경우 일반 Bit I/O에서는 설정하지 않으며 Alternate Function, 즉 UART, I²2 등의 통신 설정시 사용한다.)

 

소스 중간의 HAL_GPIO_WritePin은 앞장에서 살펴보았으므로 Clock초기화 부분과 각 핀의 초기화부분만 구분해서 살펴보기로 하자.

 

2. GPIO Clock Enable

해당 GPIO 포트의 Clock초기화는 __HAL_RCC_GPIOx_CLK_ENABLE()이라는 매크로 함수에 의해 수행된다.

이 이름에서 RCC는 Reset and Clock Control이라는 의미이다. 따라서 이 레지스터 집합에는 MCU Reset이나 Clock관련 설정 기능이 포함되어 있다.

Power Reset은 아직 살펴보지 않았으므로 다른 장에서 보기로 하고 여기서는 Clock에 대한 부분만 살펴볼 것이다.

먼저 __HAL_RCC_GPIOx_CLK_ENABLE() 매크로 함수를 보자.

#define __HAL_RCC_GPIOC_CLK_ENABLE()  do { \
            __IO uint32_t tmpreg = 0x00U; \
            SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);\
            /* Delay after an RCC peripheral clock enabling */ \
            tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);\
            UNUSED(tmpreg); \
              } while(0U)

뭔가 복잡해 보인다. 하나씩 살펴보자.

제일 바깥으로 do-while문이 나온다. 하지만 이 do-while문은 조건이 while(0U)이어서 단 한번만 실행되므로 실제 Loop를 수행하지는 않는다. 이렇게 쓴 이유를 설명하자면 조금 길므로 밑에 "매크로 함수의 do-while문"에 별도로 설명하기로 하고 흐름을 끊지 않기 위해 지금은 매크로 함수 내용 자체에 집중하자. 현재는 do-while문이 하나의 블럭역할만 한다고 알고 있으면 된다.

 

우선 volatile uint32_t형식의 tmpreg변수를 하나 선언한다.

그리고 SET_BIT()매크로 함수로 RCC->AHB1ENR 레지스터를 설정하는 구문이 나온다.

SET_BIT()매크로는 다음과 같이 정의되어 있다.

#define SET_BIT(REG, BIT)     ((REG) |= (BIT))
#define READ_BIT(REG, BIT)    ((REG) & (BIT))

첫번째 파라미터로 주어진 레지스터중 두번째 파라미터의 BIT위치의 값을 설정하는 매크로이다.

그 밑에 READ_BIT는 마찬가지로 레지스터의 현재 특정 BIT값을 읽는다.

 

그 다음 SET_BIT()매크로의 첫번째 파라미터로 전달된 RCC->AHB1ENR  레지스터를 보자.

GPIO 레지스터와 같이 RCC레지스터의 define을 따라가 보면 아래와 같다.

#define PERIPH_BASE           0x40000000UL /*!< Peripheral base address in the alias region                                */
#define AHB1PERIPH_BASE       (PERIPH_BASE + 0x00020000UL)
#define RCC_BASE              (AHB1PERIPH_BASE + 0x3800UL)
#define RCC                 ((RCC_TypeDef *) RCC_BASE)

0x4002 3800 주소값을 RCC_TypeDef 구조체 포인터로 형변환 하고 있다. GPIO와 유사한 구조이다.

RCC_TypeDef구조체는 RCC 관련 레지스터를 그대로 멤버로 선언해 놓았고 이 레지스터는 Reference Manual의 152p부터 나와 있다. 현재 우리가 보고자 하는 AHB1ENR 레지스터는 182p에 기술되어 있다.

앞장에서 설명했다시피 GPIO는 AHB1버스에 연결되어 있으므로 AHB1ENR레지스터에 GPIO의 Clock설정이 존재한다.

우리가 보고 있는 GPIO는 GPIOC이므로 이 레지스터의 2번 비트를 1로 설정하고 있다.

[RCC_AHB1ENR 레지스터]

SET_BIT매크로의 두번째 값 RCC_AHB1ENR_GPIOCEN은 아래와 같이 define되어 있다.

#define RCC_AHB1ENR_GPIOCEN_Pos            (2U)
#define RCC_AHB1ENR_GPIOCEN_Msk            (0x1UL << RCC_AHB1ENR_GPIOCEN_Pos)   /*!< 0x00000004 */
#define RCC_AHB1ENR_GPIOCEN                RCC_AHB1ENR_GPIOCEN_Msk

1을 왼쪽으로 2bit만큼 Shift했으므로 이 값은 이진수로 0b0000 0000 0000 0100 이 된다. 따라서 2번 비트를 1로 설정하는 것이다. 이 명령에 의해 지금부터는 GPIOC 포트의 Clock이 Enable상태가 된다.

 

다시 __HAL_RCC_GPIOx_CLK_ENABLE() 매크로로 돌아오자.

SET_BIT() 이후 READ_BIT() 매크로로 동일 레지스터의 동일 Bit값을 읽어서 tmpreg 변수에 넣는다.

그리고 UNUSED(tmpreg)로 tmpreg변수를 void로 형변환한다. 

UNUSED()매크로는 소스의 주석을 보면 /*To avoid gcc/g++ warnings */ 라고 되어 있는데 변수에 값을 설정하고 사용하지 않으면 컴파일러에서 경고 메시지(variable 'xxx' set buf not used)를 출력하기 때문에 이를 방지하기 위해서이다.

 

그런데 여기서 중요한건 쓰지도 않는 tmpreg에 왜 해당 Bit를 읽어서 값을 설정했느냐이다.

일단 __HAL_RCC_GPIOx_CLK_ENABLE()안의 주석을 보면 /* Delay after an RCC peripheral clock enabling */ 이라고 나오는데 이 내용대로라면 단순히 지연의 용도로 사용되는 더미코드이다.

이렇게 지연을 시키는 이유를 구글에서 찾다보니 ST사의 <STM32F405/407xx and STM32F415/417xx Description of device errata>라는 정오표에서 이 내용을 확인할 수 있었다. 이 문서의 12p에 보면 위의 주석과 동일한 내용의 오류를 볼 수 있다.

즉 MCU오류로 인해 AHB 버스에 연결된 Peripheral의 경우 Clock Enable후 두번의 AHB Cycle만큼의 지연이 있다는 것이다.

즉, 이 지연시간을 위해 의미없는 더미코드를 삽입한 것이다.

단, 이 문서는 STM32F405/407/415/417을 위한 것인데 우리가 사용하는 STM32F429/439F도 동일한 오류가 있는지는 확인하지 못했다.

(위의 Reference Manual상의 RCC_AHB1ENR 설명에는 "no wait state"로 나와 있는데 이 문서는 STM32F429/439뿐 아니라 STM32F405/417의 내용도 포함하고 있다. 

따라서 개발시 __HAL_RCC_GPIOx_CLK_ENABLE() 매크로를 사용하지 않고 별도의 초기화 함수를 작성해 사용한다면 429/439도 이런 문제점을 내포하고 있다고 가정하고 이를 적용하는게 안전할 거 같다.)

 

 

3. 핀 초기화

모든 GPIO포트의 Clock Enable설정이 끝나면 CubeMX에서 설정한 각 GPIO별 설정 부분이 나온다.

소스를 보면 CubeMX에서 설정한 모든 GPIO에 대해 GPIO_InitTypeDef라는 구조체를 설정한 뒤 HAL_GPIO_Init()함수로 초기화 작업을 진행하고 있다.

이 구조체는 다음과 같이 정의되어 있는데 CubeMX의 GPIO설정 부분에서 본 내용과 동일하다.

/** 
  * @brief GPIO Init structure definition  
  */ 
typedef struct
{
  uint32_t Pin;       /*!< Specifies the GPIO pins to be configured.
                           This parameter can be any value of @ref GPIO_pins_define */

  uint32_t Mode;      /*!< Specifies the operating mode for the selected pins.
                           This parameter can be a value of @ref GPIO_mode_define */

  uint32_t Pull;      /*!< Specifies the Pull-up or Pull-Down activation for the selected pins.
                           This parameter can be a value of @ref GPIO_pull_define */

  uint32_t Speed;     /*!< Specifies the speed for the selected pins.
                           This parameter can be a value of @ref GPIO_speed_define */

  uint32_t Alternate;  /*!< Peripheral to be connected to the selected pins. 
                            This parameter can be a value of @ref GPIO_Alternate_function_selection */
}GPIO_InitTypeDef;

 

  • Pin : 핀번호이고 define으로 정의된 GPIO_PIN_n 값을 지정하거나 CubeMX의 Label에 입력한 내용대로 main.h파일에 define되어 있는 값을 사용한다.
  • Mode : 해당 핀의 동작 모드를 설정한다. CubeMX의 "GPIO mode"에 설정한 내용이 들어간다. stm32f4xx_hal_gpio.h파일에 GPIO_MODE_INPUT(입력모드), GPIO_MODE_OUTPUT_PP(출력모드, Push/Pull), GPIO_MODE_OUTPUT_OD(출력모드 Open Drain), GPIO_MODE_AF_PP(Alternate모드 Push/Pull), GPIO_MODE_AF_OD(Alternate모드 Open Drain), GPIO_MODE_ANALOG(아날로그) 등으로 각각 정의되어 있다.
  • Pull : 해당 핀이 Pull-up인지 Pull-down인지를 설정한다. CubeMX의 "GPIO Pull-up/Pull-down"에 설정한 내용이 들어간다. stm32f4xx_hal_gpio.h파일에 GPIO_NOPULL, GPIO_PULLUP, GPIO_PULLDOWN으로 정의되어 있다.
  • Speed : 해당 핀의 출력 속도를 설정한다. CubeMX의 "Maximum output speed"에 설정한 내용이 들어간다. stm32f4xx_hal_gpio.h파일에 GPIO_SPEED_FREQ_LOW, GPIO_SPEED_FREQ_MEDIUM, GPIO_SPEED_FREQ_HIGH, GPIO_SPEED_FREQ_VERY_HIGH 로 정의되어 있다.
  • Alternate : 해당 핀이 Alternate Function Mode로 사용될 경우 설정되는 값이다. 값은 stm32f4xx_hal_gpio_ex.h 파일에 GPOI_AFn_XXX등으로 정의되어 있다.

위의 설명에서 Alternate Function Mode는 해당핀이 UART, I²C, SPI, CAN통신등 정형화된 신호용으로 사용하는것을 말한다.

GPIO 설정에서는 이 값을 사용하지 않지만 UART등에서는 이 값을 설정해야 한다.

main함수안에 MX_USART3_UART_Init()함수를 따라 들어가 보면 내부에서 HAL_UART_Init()->HAL_UART_MspInit()과 같이 함수 호출을 하고 있다.

HAL_UART_MspInit()함수는  원래 stm32f4xx_hal_uart.c에 __weak형식으로 정의가 되어 있지만 이 함수는 CubeMX의 Project Manager에서 Peripheral별로 파일을 쪼갤지 여부에 따라 쪼갰다면 usart.c에 쪼개지 않았다면 stm32f4xx_hal_msp.c파일에 재정의 되어 있다.

이 함수에 보면 GPIO_InitTypeDef구조체의 Alternate Function을 설정하는 걸 볼 수 있다.

void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{

  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(uartHandle->Instance==USART3)
  {
  /* USER CODE BEGIN USART3_MspInit 0 */

  /* USER CODE END USART3_MspInit 0 */
    /* USART3 clock enable */
    __HAL_RCC_USART3_CLK_ENABLE();

    __HAL_RCC_GPIOD_CLK_ENABLE();
    /**USART3 GPIO Configuration
    PD8     ------> USART3_TX
    PD9     ------> USART3_RX
    */
    GPIO_InitStruct.Pin = STLK_RX_Pin|STLK_TX_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF7_USART3;
    HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);

  /* USER CODE BEGIN USART3_MspInit 1 */

  /* USER CODE END USART3_MspInit 1 */
  }
}

 

다시 MX_GPIO_Init()함수로 돌아오자.

각 핀별로 HAL_GPIO_Init()함수를 호출하는데 첫번째 파라미터는 GPIO 포트, 두번째 파라미터는 위에서 살펴본 GPIO_InitTypeDef구조체이다.

그러면 HAL_GPIO_Init()함수를 살펴보기로 하자.

함수 내용이 조금 길지만 GPIO 모드별로 블럭화 되어 있어 그렇게 복잡하지는 않다. 전체소스는 직접 살펴보면 될것이고 여기서는  이 코드를 단순화 시킨 Pseudo코드로 설명을 진행하겠다.

void HAL_GPIO_Init(GPIO_TypeDef  *GPIOx, GPIO_InitTypeDef *GPIO_Init)
{
	uint32_t ioposition = 0x0U;
    
	for(position : 0~GPIO핀갯수(16개))
	{
		ioposition = 0x01U << position;
		iocurrent = (uint32_t)(GPIO_Init->Pin) & ioposition;
        
		if(position == iocurrent)
		{
			if(모드가 MODE OUTPUT, MODE_AF)
				OSPEEDR, OTYPER 레지스터 설정;
			
			if(모드가 ANALOG가 아니면)
				PUPDR레지스터 설정;	// Pull-up/Pull-down
	
			if(모드가 MODE_AF)
				AFR레지스터 설정;	// Alternate function

			MODER레지스터 설정;

			if(모드가 인터럽트 모드)
			{
				EXTICR레지스터 설정;
				
				인터럽트 모드에서 TRIGGER_RISING, TRIGGER_FALLING, EXTI_EVT, EXTI_IT구분에 따라 각각
				RTSR, FTSR, EMR, IMR 레지스터 설정;
			}
		}
	}

}

 

먼저 제일 바깥으로 for문이 나온다. 단순히 GPIO_NUMBER define문에 따라 16번의 루프를 진행한다.

그 밑으로 ioposition, iocurrent를 설정하는 실제 소스를 그대로 적어놨는데 현재 루프의 비트 순번과 입력된 핀의 비트 순번을 구해 그 아래 if문의 조건으로 사용하는 루틴이다.

 

그런데 여기서 왜 Loop를 돌리고 if문으로 조건을 걸어서 처리를 하고 있을까?

그냥 GPIO_Init->Pin 에 현재 비트 순번이 들어가 있으므로 루프없이 이 값을 그대로 사용하면 되지 않을까?

이유는 GPIO_Init->Pin에 OR연산으로 여러개의 비트를 한번에 전달할 수 있기 때문이다.

이 HAL_GPIO_Init()함수를 호출하는 MX_GPIO_Init()함수를 보면 현재 LD3_Pin과 LD2_Pin의 경우 OR연산으로 한번의 HAL_GPIO_Init()만 호출하고 있다.

이렇게 동일 조건의 핀을 묶어 한번의 호출만으로 처리할 수 있도록 하기 위해 for문과 if문이 사용된 것이다.

 

다음으로 처음 나오는 if문 루틴에서는 모드가 MODE_OUTPUT, MODE_AF인 경우 OSPEEDR, OTYPER 레지스터를 세팅한다.

OSPEEDR레지스터는 다음과 같다.

[GPIOx_OSPEEDR]

이름에서 보듯 이 레지스터는 해당 핀의 속도를 결정하는 레지스터이고 이 값은 CubeMX에서 설정한 "Maxium output speed"의 값이다.

이 레지스터에 들어가는 값은 Low, Medium, High, Very High 총 4가지가 될 수 있으므로 각 핀당 2비트가 할당되어 있다.

temp = GPIOx->OSPEEDR; 
temp &= ~(GPIO_OSPEEDER_OSPEEDR0 << (position * 2U));
temp |= (GPIO_Init->Speed << (position * 2U));
GPIOx->OSPEEDR = temp;

따라서 위 소스의 2~3라인과 같이 해당 핀의 비트위치에 2를 곱하고 있다.

 

그SPEEDR 레지스터 세팅 다음으로 OTYPER 레지스터를 세팅하는데 이 레지스터는 출력 타입을 Output push-pull로 할지 Open-drain으로 할지를 설정하는 레지스터이다. 이것 역시 CubeMX에서 설정한 내용 그대로 레지스터를 세팅한다.

[GPIOx_OTYPER]

 

그 다음 if루틴에서는 해당 핀이 아날로그 입출력이 아닐 경우 PUPDR 레지스터를 세팅하고 있다.

이 레지스터는 CubeMX에서 해당 핀의 GPIO Pull-up/Pull-down에 설정한 값을 적용하는 레지스터이다.

[GPIOx_PUPDR]

이 레지스터 역시 각 핀의 상태가 3가지이므로 각각 2비트를 사용해야 한다. 따라서 소스상에서 위치를 설정하기 위해 position값에 2를 곱하고 있다.

 

그 다음 if문 루틴은 Alternate Function에 관련된 레지스터 설정이 나온다.

이 레지스터는 상태값이 AF0~AF15까지 총 16개의 상태를 설정할 수 있기 때문에 한 핀당 4비트를 사용해야 한다. 따라서 32bit레지스터 하나만으로는 16개 핀을 할당할 수 없으므로 GPIOx_AFRL, GPIOx_AFRH 두개의 레지스터를 사용한다.

소스에서는 AFR[2] 와 같이 배열로 선언해서 사용하고 있다.

따라서 소스에서는 position >> 3U와 같이 배열 첨자를 계산해서 0~7이면 0, 8~15이면 1번 첨자를 참조하고 있다.

 

다음으로 별도 조건없이 GPIOx_MODER 레지스터를 세팅한다.

이 레지스터는 해당 핀의 용도를 설정하는 레지스터인데 해당핀을 Input, Output, Alternate, Analog중 어느 용도로 사용할지를 결정하는 레지스터이다.

[GPIOx_MODER]

이 레지스터 역시 설정할 수 있는 값이 4개이므로 한 핀당 각각 2bit가 할당되어 있다.

그런데 MX_GPIO_Init()함수에서 HAL_GPIO_Init()함수를 호출할 때 넘기는 GPIO_InitTypeDef.Mode값 설정을 보면 위 레지스터 설명과 달리 값이 좀 더 세분화 되어 있다.

사용되는 MODE의 define문을 stm32f4xx_hal_gpio.h에서 찾아보면 

#define  GPIO_MODE_INPUT                        MODE_INPUT                                                  /*!< Input Floating Mode                   */
#define  GPIO_MODE_OUTPUT_PP                    (MODE_OUTPUT | OUTPUT_PP)                                   /*!< Output Push Pull Mode                 */
#define  GPIO_MODE_OUTPUT_OD                    (MODE_OUTPUT | OUTPUT_OD)                                   /*!< Output Open Drain Mode                */
#define  GPIO_MODE_AF_PP                        (MODE_AF | OUTPUT_PP)                                       /*!< Alternate Function Push Pull Mode     */
#define  GPIO_MODE_AF_OD                        (MODE_AF | OUTPUT_OD)                                       /*!< Alternate Function Open Drain Mode    */

#define  GPIO_MODE_ANALOG                       MODE_ANALOG                                                 /*!< Analog Mode  */
    
#define  GPIO_MODE_IT_RISING                    (MODE_INPUT | EXTI_IT | TRIGGER_RISING)                     /*!< External Interrupt Mode with Rising edge trigger detection          */
#define  GPIO_MODE_IT_FALLING                   (MODE_INPUT | EXTI_IT | TRIGGER_FALLING)                    /*!< External Interrupt Mode with Falling edge trigger detection         */
#define  GPIO_MODE_IT_RISING_FALLING            (MODE_INPUT | EXTI_IT | TRIGGER_RISING | TRIGGER_FALLING)   /*!< External Interrupt Mode with Rising/Falling edge trigger detection  */
 
#define  GPIO_MODE_EVT_RISING                   (MODE_INPUT | EXTI_EVT | TRIGGER_RISING)                     /*!< External Event Mode with Rising edge trigger detection             */
#define  GPIO_MODE_EVT_FALLING                  (MODE_INPUT | EXTI_EVT | TRIGGER_FALLING)                    /*!< External Event Mode with Falling edge trigger detection            */
#define  GPIO_MODE_EVT_RISING_FALLING           (MODE_INPUT | EXTI_EVT | TRIGGER_RISING | TRIGGER_FALLING)   /*!< External Event Mode with Rising/Falling edge trigger detection     */

와 같이 많은 양이 정의되어 있다.

 

이는 전달된 GPIO_InitTypeDef.Mode값에 실제 MODER레지스터에 설정할 값 뿐 아니라 다른 레지스터에 적용할 값을 모두 포함하기 위해서 각각 비트를 별도 설정해서 전달하기 때문이다. 그리고 각 if문의 조건에도 이 값을 Masking해서 비교하고 있다.

위의 다른 레지스터 설명에서 다루지 않았지만 지금 살펴보기로 하자.

OTYPER레지스터의 경우 해당 핀이 Output Push/Pull이냐 Output Open-Drain이냐를 설정하는 레지스터이다.

이 레지스터는 첫번째 if문 블럭안에서 설정을 하는데

/* Configure the IO Output Type */
temp = GPIOx->OTYPER;
temp &= ~(GPIO_OTYPER_OT_0 << position) ;
temp |= (((GPIO_Init->Mode & OUTPUT_TYPE) >> OUTPUT_TYPE_Pos) << position);
GPIOx->OTYPER = temp;

와 같이 코딩되어 있다.

원래 OTYPER 레지스터를 읽고 지금 설정하려는 핀의 bit위치를 0으로 리셋한 뒤 주어진 파라미터로 값을 설정하는 구문이다.

이때 GPIO_Init->Mode를 OUTPUT_TYPE과 AND연산후 OUTPUT_TYPE_Pos만큼 오른쪽 쉬프트를 해서 값을 설정하는데 이 구문의 OUTPUT_TYPE을 보면

#define OUTPUT_TYPE_Pos                         4U
#define OUTPUT_TYPE                             (0x1UL << OUTPUT_TYPE_Pos)

와 같이 정의되어 있다.

즉, 1을 왼쪽으로 4bit쉬프트 시켜 4번 bit값을 설정한 것이므로 GPIO_Init->Mode 로 넘어온 4번비트를 값으로 사용하겠다는 것이다.

 

현재 LED1에 할당된 PB0의 Mode설정을 보면 GPIO_MODE_OUTPUT_PP로 설정되어 있다. 이 정의는 위의 각 MODE정의를 보면

#define  GPIO_MODE_OUTPUT_PP                    (MODE_OUTPUT | OUTPUT_PP)

이렇게 되어 있는데 (MODE_OUTPUT | OUTPUT_PP)는

#define GPIO_MODE_Pos                           0U
#define GPIO_MODE                               (0x3UL << GPIO_MODE_Pos)
#define MODE_INPUT                              (0x0UL << GPIO_MODE_Pos)
#define MODE_OUTPUT                             (0x1UL << GPIO_MODE_Pos)
#define MODE_AF                                 (0x2UL << GPIO_MODE_Pos)
#define MODE_ANALOG                             (0x3UL << GPIO_MODE_Pos)
#define OUTPUT_TYPE_Pos                         4U
#define OUTPUT_TYPE                             (0x1UL << OUTPUT_TYPE_Pos)
#define OUTPUT_PP                               (0x0UL << OUTPUT_TYPE_Pos)
#define OUTPUT_OD                               (0x1UL << OUTPUT_TYPE_Pos)
#define EXTI_MODE_Pos                           16U
#define EXTI_MODE                               (0x3UL << EXTI_MODE_Pos)
#define EXTI_IT                                 (0x1UL << EXTI_MODE_Pos)
#define EXTI_EVT                                (0x2UL << EXTI_MODE_Pos)
#define TRIGGER_MODE_Pos                         20U
#define TRIGGER_MODE                            (0x7UL << TRIGGER_MODE_Pos)
#define TRIGGER_RISING                          (0x1UL << TRIGGER_MODE_Pos)
#define TRIGGER_FALLING                         (0x2UL << TRIGGER_MODE_Pos)

이렇게 각각 비트를 할당한 정의가 사용된다.

즉, 실제 MODER에 사용할 비트는 [1,0]비트, OTYPER에 사용할 비트는 [4]비트, 인터럽트 설정의 IMR레지스터 설정등은 [18:16]비트를 사용한다.

 

따라서 GPIO_MODE_OUTPUT_PP는

#define GPIO_MODE_Pos                           0U
#define MODE_OUTPUT                             (0x1UL << GPIO_MODE_Pos)

#define OUTPUT_TYPE_Pos                         4U
#define OUTPUT_PP                               (0x0UL << OUTPUT_TYPE_Pos)


#define  GPIO_MODE_OUTPUT_PP                    (MODE_OUTPUT | OUTPUT_PP)

이므로 이 값을 하위 16비트 이진수로 표시하면

0b0000 0000 0000 0001 이 된다.

따라서 [1,0]비트값 0b01를 MODER 레지스터에, [4]비트값 0을 OTYPER 레지스터에 각각 세팅한다.

 

MODER레지스터 설정이 끝나면 인터럽트 관련 레지스터 설정이 나온다. 아직 인터럽트는 다루지 않았으므로 이번장에서는 생략하겠지만 설명된 내용대로 각자 매뉴얼에서 레지스터 정의를 찾아 살펴보면 될것이다.

 

 

4. 매크로 함수의 do-while문

매크로 함수에 do-while문을 쓰는 이유는 몇가지가 있다.

우선 do-while문은 {}로 쌓여 있기 때문에 블럭화가 가능하다. 이 구문 안에 변수 선언을 해서 사용해야 할 경우 이 변수를 블럭 외부로 노출하지 않고 내부에서만 사용하도록 할 수가 있기 때문이다.

따라서 __HAL_RCC_GPIOx_CLK_ENABLE() 내부에 tmpreg라는 변수를 선언하고 이를 블럭밖으로는 노출하지 않기 위해 do-while문을 사용한다.

그러나 이 목적만이라면 do-while을 쓰지않고 단순히 

#define __HAL_RCC_GPIOC_CLK_ENABLE()	{\
					_IO uint32_t tmpreg = 0x00U;\
   					...\
					}

이렇게 써도 무방하다.

 

하지만 매크로가 if문내에서 사용된다면 문제가 생길 수 있다.

#define DUMMY_MACRO()	{ \
			__IO uint32_t tmpreg = 0x00U; \
			SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);\
			tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);\
			UNUSED(tmpreg); \
			}

위와 같이 매크로를 정의했다고 해보자.

이 매크로를

if(어떤 조건)
	DUMMY_MACRO();

이렇게 쓰면 Precompiler는 아래와 같이 변경할 것이다.

if(어떤 조건)
	{ 
		__IO uint32_t tmpreg = 0x00U; 
		SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);
		tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);
		UNUSED(tmpreg); 
	};

별 문제가 없어 보인다. if문 블럭괄호 끝에 세미콜론이 있다는것 외에는...

이 세미콜론은 실제 적을 필요가 없지만 컴파일상으로는 아무런 문제가 되지 않는다.

하지만 if문에 else가 추가되면 문제가 달라진다.

if(어떤 조건)
	DUMMY_MACRO();
else
	assert_param(..);

이 구문은 Precompiler에 의해 다음과 같이 변경된다.

if(어떤 조건)
	{ 
		__IO uint32_t tmpreg = 0x00U; 
		SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);
		tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);
		UNUSED(tmpreg); 
	};
else
	((void)0U);

 

이렇게 되면 매크로에 의한 블럭문 뒤의 세미콜론 때문에 문법 오류가 발생한다.

그런데 이 매크로를 단순 블럭대신 do-while을 사용하면 이 문제가 해결된다. 왜냐하면 do-while문의 while문 끝에는 세미콜론을 붙여야 하기 때문이다.

if(어떤 조건)
	do { 
		__IO uint32_t tmpreg = 0x00U; 
		SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);
		tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);
		UNUSED(tmpreg); 
	} while(0U);
else
	((void)0U);

 

그래서 HAL라이브러리 개발자가 매크로에 do-while문을 사용했다는건 이 매크로 함수가 if-else 구문에도 사용될 가능성에 대비를 했다는 말이다.

 

 

5. 마무리

이전 7장과 이번장에서는 ARM Cortex-M4의 GPIO 레지스터에 관해 알아보았다. 레지스터를 확인하기 위해 HAL Library가 어떻게 코딩되어 있는지도 더불어 살펴보았다. 한개의 장으로 설명하기에는 너무길어 부득이 두개의 장으로 나눠 설명했다.

 

사실 HAL Library에서 해당 기능을 지원하면 레지스터를 직접 조작하지 않고 제공되는 함수를 사용하면 된다. 코딩도 쉬울뿐더러 코드 가독성도 높기 때문이다. HAL_GPIO_ReadPin() 이런 함수는 모르는 사람이 봐도 Pin상태를 읽는 기능을 하는 함수라는 걸 유추할 수 있다.

하지만 HAL Library가 모든 기능을 제공하는건 아니다. 그리고 프로젝트별로 HAL Library와는 다르게 레지스터를 엑세스해야 하는 경우도 발생한다.

이런 경우는 어쩔수없이 Reference Manual을 뒤져가면서 직접 레지스터를 엑세스 하는 수 밖에 없다.

또한 MCU의 몇 틱이라도 아껴서 써야 하는 빠른 처리가 필요한 경우 구문에 함수호출없이 바로 레지스터 엑세스 구문을 넣어야 하는 경우도 있다.(특히 Loop문안에서 반복되는 경우는 더 그렇다.)

함수의 경우 어셈블리어로 보면 함수 호출과 리턴시 스택에 Push/Pop하는등의 오버헤드가 발생하기 때문이다.

 

따라서 두가지 방법 모두 알고 있어야 하며 적절하게 선택해서 사용해야 한다.

앞으로 나오는 장에서는 필요한 경우이외에는 레지스터를 직접 다루는 설명은 하지 않겠지만 이 장에서 익힌 내용을 토대로 하면 관련 내용을 찾아서 사용할 수 있을 것이다.

'임베디드 > STM32' 카테고리의 다른 글

10. [STM32] GPIO예제(feat. Open Drain)  (0) 2024.09.23
9. [STM32] Bit-banding  (0) 2024.09.10
7. [STM32] GPIO - Peripheral Register 1  (0) 2024.07.26
6. [STM32] GPIO 기초  (0) 2024.07.25
5. [STM32] NUCLEO F429/439 보드 Pinmap  (0) 2024.07.22