임베디드 시스템에 필요한 기능을 탑제. 마이크로프로세서와 입출력 모듈을 하나의 칩으로 micro controller unit

임베디드 시스템에 필요한 기능이 들어가있는 MCU
AHB/APB Bus
advanced High-performance Bus
고속의 데이터 전송을 위한 통로, CPU<->메모리 연결
advanced peripheral bus
저속의 데이터 전송을 위한 통로. 온칩 디바이스 레지스터 <-> 주변장치
APB에 연결된 장치들은 AHB/APB Bridge를 통해 프로세서와 통신한다.
General Purpose Input Output . 범용 I/O
GPIO를 어떻게 제어, 즉 어떤 레지스터를 통해 핀의 동작을 설정하고 데이터를 읽고 쓰는지
포트 A-J는 16비트이다. 즉 포트마다 16개의 핀이 존재
포트 K는 8비트. 즉 8개의 핀만 있다. GPIOK0 - GPIOK7
최대 16개의 핀(I/O)를 한 번에 설정할 수 있다.
메모리 맵 : 특정 하드웨어가 CPU의 어떤 메모리 주소에 연결되어 있는지
STM32의 경우, 각 GPIO포트 (A-K)가 아래 사진과 같이 특정 메모리 주소에 할당되어 있다.

VDD : 양의 전압을 의미한다.
VSS : 접지, 0V를 의미한다.
따라서, GPIO 사용 전에 위에 해당하는 레지스터 비트를 원하는 입출력 구성에 맞게 설정해야 한다.

| MODER(i) [1:0] | OTYPER(i) | OSPEEDR(i) [B:A] | PUPDR(i) [1:0] | I/O configuration | 설명 |
|---|---|---|---|---|---|
| 10 | 0 | SPEED | 00 | AF, PP | 대체기능(AF), 푸시풀 출력(Push-Pull), 풀업/풀다운 없음 |
| 10 | 0 | SPEED | 01 | AF, PP + PU | 대체기능, 푸시풀 출력, 내부 풀업 저항 연결 |
| 10 | 0 | SPEED | 10 | AF, PP + PD | 대체기능, 푸시풀 출력, 내부 풀다운 저항 연결 |
| 10 | 0 | SPEED | 11 | Reserved | 예약(사용 금지) |
| 10 | 1 | SPEED | 00 | AF, OD | 대체기능, 오픈드레인 출력(Open Drain), 풀업/풀다운 없음 |
| 10 | 1 | SPEED | 01 | AF, OD + PU | 대체기능, 오픈드레인 출력, 내부 풀업 저항 연결 |
| 10 | 1 | SPEED | 10 | AF, OD + PD | 대체기능, 오픈드레인 출력, 내부 풀다운 저항 연결 |
| 10 | 1 | SPEED | 11 | Reserved | 예약(사용 금지) |
| 00 | x | x | 00 | Input, Floating | 입력 모드, 풀업/풀다운 없음(플로팅 입력) |
| 00 | x | x | 01 | Input, PU | 입력 모드, 내부 풀업 저항 연결 |
| 00 | x | x | 10 | Input, PD | 입력 모드, 내부 풀다운 저항 연결 |
| 00 | x | x | 11 | Reserved | 예약(플로팅 입력, 사용 금지) |
| 11 | x | x | xx | Input/output, Analog | 아날로그 모드(ADC 등 아날로그 입력/출력에 사용) |
| xx | x | x | 11 | Reserved | 예약(사용 금지) |
MODER(i) 1:0
00 입력
01 일반 출력
10 대체 기능
11 아날로그
OTYPER(i)
0 : push pull
1 : Open drain .. 등

포트 x의 각 핀별 Mode -> 입력, 출력, AF, Analog 설정
핀마다 2비트씩 할당하여 포트마다 총 32비트를 사용..
그러니까, 지금 STM32 MCU 레지스터 구조를 보여주는거임
하나의 포트 예를 들어 GPIO_A ~ K 에 있는 16개 또는 K의 경우 8개 ㅇㅇ핀 각각의 동작 모드를 설정하는 데 사용된다.
핀 하나당 2비트씩 (2자리수) 사용되므로, 포트 하나당 총 32비트 사용
MODER 1:0은 0번 핀의 모드를 설정한다.
핀의 모드라고 하면 위에서 말한것처럼 00 01 10 11 등 존재
rw? read write
즉 입력포트로 설정 시, 출력 회로는 꺼지고, 입력 회로가 켜짐. 외부 신호가 들어오면 그 값이 Input Data Register에 저장하고 CPU가 해당 레지스터의 값을 읽어서 핀 값을 확인하게 된다.
clock : MCU가 어느정도의 빠르기로 동작하는지 정해주는 박자 역할
따라서, 프로세서를 구동하기 위해 여러 종류의 클럭을 생성해야 한다.
다양한 클럭 소스를 조합하여 필요 없는 클럭은 끄고 ㅇㅇ 그렇게 사용하믄댐
MCU 안에 있는 GPIO, 타이머, UART 등 .. 기능을 설정하려면 레지스터를 바꿔야됨
모든 주변장치의 초기화 작업은, 주변장치의 레지스터들을 설정하여 수행
주변장치 레지스터 -> 메모리 주소에 매핑되어 있음. 즉 포인터를 이용하여 접근
ex) GPIO 설정 - 헤더파일의 레지스터 주소 정의
헤더파일에는 이런 주소들이 이름으로서 정리되어 있다.
*(unsigned int*)0x40020014 = 0x01; // PA0 핀을 HIGH로 만든다
GPIOA의 ODR 주소가 0x40020014라면 해당 값을 1로 바꾸어 핀에 전기가 나가게 한다.
헤더파일에는 아래처럼 define되어 있으므로,
#define GPIOA_ODR (*(volatile unsigned int*)0x40020014)
아래처럼 쉽게 접근할 수 있다.
GPIOA_ODR = 0x01;
위의 설명처럼 #define을 이용하여 주변장치 레지스터에 접근해보자.

struct 구조체의 원소들의 순서는 GPIO 제어를 위한 메모리 맵 상의 레지스터들의 오프셋 주소와 일치하게 된다. 이때 각 원소들의 주소는 레지스터 길이만큼씩 offset 차이가 난다.
offset? 기준점에서 얼마나 떨어져 있는지. 즉 거리
GPIO_LED는 GPIOx 포트의 레지스터들의 시작주소로 설정한다. 포트x의 레지스터들은 C언어의 struct 접근방식으로 read write 가능
Enum 타입으로 아래와 같이 활용 가능

중간 정리
외부와 소통하는 GPIO에서, LED는 특정 포트와 연결되어 있다. 소프트웨어는 해당 핀의 모드와 상태를 제어해야 하는데 이때 LED를 enum 타입으로 정의하여 유지보수 쉽게 ㅇㅇ
MCU내부의 GPIO (입출력 포트) 를 통하여, LED를 끄고 키는 것을 통제할 수 있는데,
GPIOA 라고 쓰는것만으로도 미리 define 즉 메모리 주소와 매핑시켜놓자
LED1을 켜라 -> GPIOA의 특정 비트에 1을 써라!
버튼을 읽어라 -> GPIOB의 입력값을 읽어라 등
┌───────────────────────────────────────────────┐
│ MCU(마이크로컨트롤러) │
│ │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ CPU │ │ 메모리 │ │
│ │ (두뇌) │ │ (RAM/ROM) │ │
│ └─────────────┘ └──────────────┘ │
│ │ │ │
│ │ │ │
│ │ │ │
│ ┌─────────────────────────────────────┐ │
│ │ 주변장치(Peripherals) │ │
│ │ ┌────────────┐ ┌───────────────┐ │ │
│ │ │ GPIOA │ │ 타이머 │ │ ... │
│ │ │ (입출력) │ │ (Timer) │ │ │
│ │ └────────────┘ └───────────────┘ │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ │ GPIOA의 특정 핀(예: PA5) │
│ │ │
│ ┌───────────────────────┐ │
│ │ LED │ ←←←←←←←←←←←←←←←←─┘
│ │ (불빛 부품) │
│ └───────────────────────┘
└───────────────────────────────────────────────┘
1. **CPU(두뇌)**가
2. **메모리 주소(0x40010800 등)**를 통해
3. **GPIOA(입출력 회로)**의
4. **특정 핀(PA5)**에 신호를 보내서
5. 외부의 LED를 켜거나 끈다
헤더 파일에 GPIO 메모리 맵을 define으로 정의

PERIPH_BASE는 주변장치 영역이 시작되는 주소, 즉 0x40000000 번지부터 각종 주변장치가 배치되어 있다.
각 포트의 메모리 주소를 정할 수 있다.
위에서 이해한 바와 같이, 코드를 읽기 쉽게 하기 위하여 GPIO 관련 핀번호를 레지스터에서 관련된 위치값들로 미리 정의해둔다.


GPIO mode 설정값들을 모드 이름으로 정의해준다.

매크로를 이용하여, 간단한 체크를 함수처럼 표현한다. 마찬가지로 프로그램이 읽기 쉬워짐
매크로 == 자동치환
ARM 계열 CPU의 헤더파일에는 주변장치의 레지스터를 가리키는 자료형인 volatile을 정의한 _ _ IO 유형이 들어있다.
레지스터 관련 변수들은 volatile로 선언하여, 단순 변수가 아니라는 것을 컴파일러에게 지시해야 한다.
volatile로 선언하지 않은 변수들은 컴파일러가 메모리로 인식하여 코드를 변환하거나 최적화로 삭제할 수 있다.
레지스터 관련 변수의 값을 항상 메모리에서 직접 읽고 쓸 수 있게 한다. 컴파일러가 변수의 값을 메모리로 인식하여 코드를 변환하거나 최적화하여 삭제/변경할 수 있기 때문이다.

uint8_t a = 255; // 8비트 부호 없는 정수
int16_t b = -12345; // 16비트 부호 있는 정수
uint32_t c = 100000; // 32비트 부호 없는 정수
C언어에서 기본 자료형은 컴퓨터나 컴파일러에 의해 크키가 달라질 수 있다. 하지만 임베디드 시스템에서는 정확한 사이즈의 자료형이 필요하다. 따라서 위의 코드를 작성하여 항상 같은 크기로 변수를 쓸 수 있게 한다.
다른 프로세서로 소스코드를 포팅할 때, 해당 파일을 수정하면 변경이 쉬워진다.
-> 포터블 코딩 기법
// STM32에서 LED가 연결된 GPIO 핀을 초기화하는 함수 예시
void BSP_LED_Init(Led_TypeDef Led)
{
GPIO_InitTypeDef GPIO_InitStruct; // 1. GPIO 설정값을 담을 구조체 선언
// 2. 해당 LED가 연결된 GPIO 포트의 클럭(전원 공급)을 켠다.
// (GPIO를 사용하려면 반드시 클럭을 먼저 활성화해야 함)
LEDx_GPIO_CLK_ENABLE(Led);
// 3. GPIO 핀 설정값 지정
GPIO_InitStruct.Pin = GPIO_PIN[Led]; // (3-1) 사용할 핀 번호 지정 (예: PA5)
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // (3-2) 출력 모드로 설정 (Push-Pull)
GPIO_InitStruct.Pull = GPIO_PULLUP; // (3-3) 풀업 저항 활성화 (핀을 기본적으로 1로 유지)
GPIO_InitStruct.Speed = GPIO_SPEED_FAST; // (3-4) 빠른 신호 전환 속도 설정
// 4. 설정한 값으로 실제 GPIO 하드웨어를 초기화
// (구조체에 담긴 값대로 레지스터에 적용)
HAL_GPIO_Init(GPIO_PORT[Led], &GPIO_InitStruct);
// 5. LED 핀의 초기 출력값을 설정 (여기서는 High로 설정)
// (LED 회로에 따라 High가 켜짐 또는 꺼짐이 될 수 있음)
HAL_GPIO_WritePin(GPIO_PORT[Led], GPIO_PIN[Led], GPIO_PIN_SET);
}
/*
* STM32 HAL 라이브러리에서 GPIO 핀을 Alternate Function(대체 기능, 예: UART, SPI, I2C 등)으로 설정
*/
/* 만약 GPIO 핀의 모드가 Alternate Function(Push-Pull 또는 Open-Drain)이라면 */
if ((GPIO_Init->Mode == GPIO_MODE_AF_PP) ||
(GPIO_Init->Mode == GPIO_MODE_AF_OD))
{
/* 1. Alternate function 값이 올바른지 검사 (디버깅용, 잘못된 값이면 assert 에러) */
assert_param(IS_GPIO_AF(GPIO_Init->Alternate));
/* 2. 현재 설정하려는 핀(position)에 해당하는 AFR(Alternate Function Register)의 값을 읽어온다.
* - STM32는 한 포트에 16개 핀이 있고, AFR 레지스터는 2개(각각 8핀씩 담당, AFR[0], AFR[1])
* - position >> 3: position(0~15)을 8로 나눠서 0이면 AFR[0], 1이면 AFR[1]을 선택
*/
temp = GPIOx->AFR[position >> 3];
/* 3. 해당 핀의 4비트(Alternate Function 설정 영역)를 먼저 0으로 클리어한다.
* - (position & 0x07): 0~7 중 몇 번째 핀인지
* - * 4: 4비트씩 이동 (각 핀마다 4비트 자리)
* - 0xF << (위치): 해당 4비트만 1로 만든 마스크
* - ~: 반전해서 해당 4비트만 0으로 만들기
*/
temp &= ~((uint32_t)0xF << ((uint32_t)(position & (uint32_t)0x07) * 4));
/* 4. 원하는 Alternate Function 값을 해당 위치에 넣는다.
* - GPIO_Init->Alternate: 설정하려는 Alternate Function 값(0~15)
* - (위치): 해당 핀의 4비트 자리로 이동
* - |=: 기존 값에 새 값을 더해 넣기
*/
temp |= ((uint32_t)(GPIO_Init->Alternate)
<< (((uint32_t)position & (uint32_t)0x07) * 4));
/* 5. 변경한 값을 AFR 레지스터에 다시 저장한다.
* - 이렇게 하면 해당 핀이 원하는 Alternate Function(예: UART, SPI 등)으로 동작하게 됨
*/
GPIOx->AFR[position >> 3] = temp;
}
함수의 입력값이 올바른지 자동으로 검사해주는 디버깅용 매크로
#ifdef USE_FULL_ASSERT
#define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__))
void assert_failed(uint8_t* file, uint32_t line);
#else
#define assert_param(expr) ((void)0)
#endif
USE_FULL_ASSERT가 정의되어 있고
expr이 참이면 -> 아무일도 하지 않음
expr이 거짓이면 -> assert_failed() 호출. 문제 발생 위치를 알려줌
USE_FULL_ASSERT가 정의되어 있지 않으면,
void 0 즉 아무 동작도 하지 않음
해당 함수를 실행시키려면
define USE_FULL_ASSERT 1 의 주석을 제거하고 프로젝트를 반드시 리빌드 해야 한다.
void assert_failed(uint8_t* file, uint32_t line) {
printf("Assert fail at File %s Line %d", file, (int)line);
while(1); // 무한루프에 빠져서 프로그램이 여기서 멈춤 (개발자가 원하면 다른 동작도 가능)
}
테스트 후에는 USE_FULL_ASSERT를 0으로 하여 동작하지 않게 해야함.