이전 장에서 살펴본 바와 같이 GPIO를 제어할때 HAL Library를 사용하거나 레지스터를 직접 엑세스 해서 제어 할 수 있었다. 하지만 HAL Library내부로 들어가면 어차피 레지스터를 엑세스하는게 전부이므로 사실 둘다 동일한 방법이라고 볼 수 있다.
HAL Library 내부를 살펴볼때 확인했겠지만 각 레지스터는 대부분 비트단위로 제어를 해야 하는데 우리가 엑세스할 때는 uint32_t를 사용해 4바이트 단위로 엑세스를 해야하기 때문에 각종 Mask와 비트 연산자들을 사용해야 한다.
이 장에서 소개할 Bit-banding은 이런 접근을 좀더 편하게 할 수 있도록 해주는 방법이다. 즉 각 레지스터 하나의 비트를 남는 메모리에 4바이트 단위로 할당해서 각 비트를 엑세스 할 수 있도록 해주는 방식이다.
따라서 복잡한 Mask와 비트연산자 사용을 최소화 할 수 있는 방법인데 단순 공식으로 해당 레지스터의 특정 비트를 엑세스 할 수 있는 주소를 얻어 그곳에서 값을 읽거나 쓰면 된다.
|
1. 준비물
이전 장들에서 이미 받았겠지만 받지 않았다면 프로그래밍 매뉴얼과 레퍼런스 매뉴얼을 받자.
2. Bit-banding
Bit-banding은 프로그래밍 매뉴얼 32p에 소개되어 있다.
아래와 같이 SRAM과 Peripheral 메모리 두가지에 대해 각각 별도의 bit-band Alias 영역을 할당해 놓았다.
두 표에서 위에 나오는 xxx bit-band region이라고 되어 있는 영역이 원래의 SRAM과 Peripheral Register의 영역이고 아래쪽이 원래 영역에 대한 Alias로 지정된 영역이다.
예를들어 Peripheral 0x4000 0000의 2번 비트가 할당된 Alias영역은 0x4200 0000부터 시작해서 한 비트당 4바이트 영역을 가지므로 0x4200 0008 번지가 된다.
프로그래밍 매뉴얼에 이 주소를 구하는 공식이 아래와 같이 적혀 있다.
bit_word_offset = (byte_offset x 32) + (bit_number x 4)
bit_word_addr = bit_band_base + bit_word_offset
그리고 그 다음 페이지에 예제도 그림과 같이 나와 있으므로 참조하자.
여기서는 첫번째 예제만 살펴보자.
SRAM의 0x200F FFFF의 0번 비트의 Alias주소를 찾는다.
공식에 따라 SRAM의 시작주소인 0x2000 0000위치부터 해당 영역의 Offset값인 0x000F FFFF값을 32로 곱하고 있고 해당 비트인 0번 비트에 4를 곱하고 있다.
거기서 bit-band Alias의 시작주소인 0x2200 0000을 더해 최종 결과로 0x23FF FFE0를 구한다.(위의 예제에서 0x23FFFFED라고 되어 있는데 오타인듯 하다. 아래 그림에는 올바른 값이 표시되어 있다.)
공식 적용만 하면 되므로 어려울게 없다.
그러면 매뉴얼에 나오지 않는 GPIO Peripheral로 하나만 더 주소를 구해보자.
개발보드의 User Button은 PC13에 연결되어 있다. 이 값은 GPIOC_IDR의 13번 비트를 읽어서 입력값을 확인할 수 있는데 이 위치의 Alias값을 구해보자.
Peripheral Register의 Base주소는 0x4000 0000이다.
그리고 GPIOC 레지스터는 0x4002 0800부터 시작하고 IDR레지스터는 이 위치부터 0x10만큼 떨어진 위치에 있다.(레퍼런스 매뉴얼 286p)
따라서 위의 공식중 byte_offset에 해당하는 값은
byte_offset = 0x4002 0800 + 0x10 - 0x4000 0000 = 0x20810이다.
그래서 bit_word_offset값은
bit_word_offset = (0x20810 * 32) + (13 * 4) 이고
bit_word_addr = 0x4200 0000 + (0x20810 * 32) + (13 * 4) = 0x4241 0234 가 된다.
즉 이 주소에 있는 값을 읽으면 GPIOC_IDR 레지스터의 13번 비트를 읽는것과 동일한 값을 얻을 수 있다.
3. 소스에 적용하기
bit-banding을 소스로 구현해보자.
PC13에 연결된 User Button을 눌렀을 때 PB7에 연결된 LED Blue에 불이 들어오도록 하는게 목표다.
먼저 위의 공식을 일일이 계산하지 않기 위해 매크로 함수를 하나 만들고 GPIO레지스터의 각 Offset값들도 define을 하자.
(이 매크로에 사용된 PERIPH_BASE값은 stm32f429xx.h에 정의된 0x4000 0000UL이다.)
/* USER CODE BEGIN PTD */
#define PERIPH_BITBAND_BASE 0x42000000UL
#define GPIO_OFFSET_MODER 0x00
#define GPIO_OFFSET_OTYPER 0x04
#define GPIO_OFFSET_OSPEEDR 0x08
#define GPIO_OFFSET_PUPDR 0x0C
#define GPIO_OFFSET_IDR 0x10
#define GPIO_OFFSET_ODR 0x14
#define GPIO_OFFSET_BSRR 0x18
#define GPIO_OFFSET_LCKR 0x1C
#define GPIO_OFFSET_AFRL 0x20
#define GPIO_OFFSET_AFRH 0x24
#define BITBAND_ADDRESS(PERIPH_INDIVIDUAL_BASE, REG_OFFSET, BIT) (uint32_t*)(PERIPH_BITBAND_BASE + (((PERIPH_INDIVIDUAL_BASE + REG_OFFSET) - PERIPH_BASE) << 5) + (BIT << 2))
/* USER CODE END PTD */
그리고 main함수의 while문 안에 다음과 같이 코딩한다.
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
// 아래의 GPIOC_BASE, GPIOB_BASE는 stm32f429xx.h에 정의된 각 GPIO의 시작주소이다.
uint32_t *puiBBIn = BITBAND_ADDRESS(GPIOC_BASE, GPIO_OFFSET_IDR, 13);
GPIO_PinState state = *puiBBIn == 1 ? GPIO_PIN_SET : GPIO_PIN_RESET;
uint32_t *puiBBOut = BITBAND_ADDRESS(GPIOB_BASE, GPIO_OFFSET_ODR, 7);
*puiBBOut = state;
}
컴파일 하고 실행해보면 HAL_GPIO_ReadPin, HAL_GPIO_WritePin을 쓴것과 동일한 결과를 얻을 수 있다.
위의 구문은 쉽게 풀어쓰기 위해 단계별로 코딩한 것이고 다음과 같이 한줄로 쓸 수도 있다.
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
*BITBAND_ADDRESS(GPIOB_BASE, GPIO_OFFSET_ODR, 7) = *BITBAND_ADDRESS(GPIOC_BASE, GPIO_OFFSET_IDR, 13);
}
4. Performance
이전에 설명했다시피 일반적으로 프로그램 내에서 함수 호출은 오버헤드를 유발한다. 함수 호출과 리턴시 스택에 Push/Pop이 수반되기 때문이다.
그리고 속도가 빠르기는 하지만 Mask설정과 비트연산등도 클럭을 소모하기는 마찬가지다.
그에 반해 Bit-banding의 경우 이런 오버헤드를 최소화 할 수 있기 때문에 속도면에서 유리할 수 있다.
주소 계산에 드는 오버헤드가 있지 않냐고 반문할 수 있지만 매크로 함수로 구현해 놓으면 Precompiler에서 이미 주소를 계산해 리터럴 값으로 설정을 하므로 그런 걱정은 하지 않아도 된다.
다음 디버깅 결과를 보자.
소스에서 BITBAND_ADDRESS 매크로를 두번 사용해 한줄로 적은 코드의 어셈블리를 보면 Instruction이 4개밖에 안된다.
- 프로그램 코드가 위치한 주소의 0x0800 0504위치에서 주소값을 읽어 r3 레지스터 설정(PC13 bit-bading alias주소)
- 프로그램 코드가 위치한 주소의 0x0800 0508위치에서 주소값을 읽어 r2 레지스터 설정(PB7 bit-banding alias주소)
- r3레지스터가 가리키는 주소값에 들어있는 값으로 다시 r3값을 대체(PC13값을 읽어 r3에 저장)
- r2레지스터에 들어있는 주소 즉 PB7의 bit-banding alias주소가 가리키고 있는 위치에 r3값을 저장
이걸 C 구문으로 바꾸면
- uint32_t *r3 = 0x08000504;
- uint32_t *r2 = 0x08000508;
- r3 = *r3;
- *r2 = r3;
이런 형태가 될 것이다.
Memory Browser 그림과 같이 r2, r3에 저장하는 주소값이 들어있는 0x0800 0504, 0x0800 0508위치의 메모리 값을 보면 각각 0x4241 0234, 0x4240 829C이다.
이 값은 위의 공식에 의해 아래와 같이 계산된 값이 그대로 들어가 있다.
PC13 : 0x4241 0234 = 0x4200 0000 + 0x0041 0200 + 0x34
PB7 : 0x4240 829C = 0x4200 0000 + 0x0040 8280 + 0x1C
즉, 매크로 함수로 주소값을 계산 했으므로 실시간으로 주소를 계산하는게 아니라 이미 컴파일시 계산된 주소가 메모리의 코드 영역에 그대로 들어가 있는 것이다.
5. 마무리
이번장을 마지막으로 GPIO의 단순 bit I/O제어에 대해 모두 살펴 보았다.
사실 GPIO 의 bit I/O 제어는 어려울게 전혀 없지만 MCU의 제어를 기존 아두이노나 라즈베리파이등과 같이 제공하는 함수만 써서 코딩을 해왔다면 MCU의 동작방식을 이해하면서 코딩하는 게 조금 어려울 수도 있다.
하지만 이렇게 MCU의 레지스터를 다루는 방법은 비단 ARM Cortex시리즈뿐 아니라 다른 MCU에서도 유사하므로 한번 익혀두면 다른 MCU를 이해하는데 그대로 활용할 수 있다. (물론 레지스터 구조는 모두 다르기 때문에 매뉴얼을 찾아봐야 하겠지만...)
그리고 코딩을 할 때도 소개한 바와 같이 HAL Library에서 제공되는 함수를 쓰든 자신이 직접 레지스터를 제어하든 bit-banding을 사용해 제어를 하든 자신이 상황에 맞게 골라 쓰면 된다.
다만 어떤 방식을 사용하든 어떤 방식이 있고 그 방식의 장단점이 뭔지를 파악해서 적절하게 사용하는게 중요하다.
'임베디드 > STM32' 카테고리의 다른 글
11. [STM32] SWV/ITM 을 사용한 디버깅 (0) | 2024.09.23 |
---|---|
10. [STM32] GPIO예제(feat. Open Drain) (0) | 2024.09.23 |
8. [STM32] GPIO - Peripheral Register 2 (0) | 2024.08.13 |
7. [STM32] GPIO - Peripheral Register 1 (0) | 2024.07.26 |
6. [STM32] GPIO 기초 (0) | 2024.07.25 |