임베디드

임베디드 시스템 디바이스 제어 프로그래밍 (2)

잉읭응 2025. 6. 30. 17:10
반응형

GPIO pin의 역할

 

프로세서 원하는 논리 레벨을 출력하거나입력 상태를 읽을 수있는 pin

Gernal Purpose Input & Output

즉, 범용적으로 입력 혹은 출력으로 사용할 수 있는 개별 핀이다. 

 

port : 다수의 개별 GPIO핀을 그룹으로 관리

제조사 스펙마다 다르겠지만 STM은 포트별로 그룹에 속한 GPIO핀의 수는 최대 16개까지 존재

프로세서에서는 한 핀, 한 핀 개별적으로 on, off제어가 가능하다. 

 

GPIO는 하나의 용도로만 절대 사용하지 않는다. (핀이 늘어나니까)

ex) SPMI, Timer, UART, GPIO 등을 다 묶어서 하나를 선택해서 사용하게 한다. Boot0와 같은 중요한 핀만 전용 핀을 할당해주고 다른 것들은 다 묶여있다. 

 

그리고 GPIO 포트에서도 모드 설정을 해야함 

입력 모드: 버퍼로 구성. 읽는 순간 외부 핀의 Low, High 상태를 알 수 있다.

출력 모드: 1bit 메모리로 구성. 한번 값을 기록하면 계속 출력 값을 유지

특수 모드: 프로세서 pin 절약을 위해 특수모듈과 공통으로 사용. (uart, timerl, spmi) 

 

 

STM32의 입출력 pin구조

input : floating, pull-up, pull-down, analog

output : open-drain, push-pull

alternate function : push-pull, open-drain

 

Push-Pull은 가장 보편적인 출력형태로 항상 0 아니면 1의 상태로 출력된다.

Open-drain : p-ch를 제거한 형태라서 Floating, 0으로 생성 

즉, push-pull은 전류가 고정인데 open-drain은 외부 저항으로 조정할 수 있다.

만약에 push-pull 방식의 여러 출력을 하나로 묶으면, 출력이 상이한 경우 쇼트가 날 것이다.

 

IAP 프로그램

IAP: in application programming, 양산되어 필드에서 사용중인 장치의 rom에 프로그램 변경

ISP: rom이나 프로세서를 보드에 탑재하여 양산후 jtag장비 등으로 writing

주로 개발중엔 ISP가 사용되나 IAP도 종종 사용된다. 

 

IAP는 두가지 기능을 가진다.

 

app실행 : 플래시 메모리에 들어있는 응용프로그램을 직접 실행

app교체: 새로운 app을 sd, lan, 시리얼 포트등으로 다운받아 flash에 writing한다. 

 

 

 

Heap 메모리 할당 문제

 

표준 함수 사용을 위해서는 꼭 heap메모리도 할당해주어야 한다. 근데 이건 컴파일러에 종속적이라서 gcc 컴파일러 문서를 확인해서 할당 방법은 알아야 한다. 

 

어셈블리에서 ZI_LIMIT을 사용할 땐, 그냥 주소 참조를 하면 되지만, c에서는 변수로 지정해준 다음에 사용해야 된다고 함. 

char * _sbrk(int inc)
{
	extern unsigned char __ZI_LIMIT__;
	static char * heap = (char *)0;

	char * prevHeap;
	char * nextHeap;

	if(heap == (char *)0) heap = (char *)HEAP_BASE;

	prevHeap = heap;
	nextHeap = (char *)((((unsigned int)heap + inc) + 0x7) & ~0x7);

	if((unsigned int)nextHeap >= HEAP_LIMIT) return (char *)0;

	heap = nextHeap;
	return prevHeap;
}
#define HEAP_BASE   (((unsigned int)&__ZI_LIMIT__ + 0x7) & ~0x7)

 

위 처럼 0x7을 해주는 이유는 0x8 단위로 힙이나 스택을 지정해주어야 해서 ~0x7 = b1000 을 비트 and 옵션으로 넣어주고 있는 것이다.

 

 

컴파일러 마다 다르겠지만,,, 아래처럼 init.s에서 word로 첫번째 위치에 배치해주는 주소를 stack으로 인식하는 모양

	.syntax unified
	.thumb

	.text

	.word	0x20005000
	.word	__start

 

 

아래 처럼 지정해주고 있다고 했을 때,,,

#define SYSCLK	8000000
#define HCLK	SYSCLK
#define PCLK2	HCLK
#define PCLK1	HCLK

#define RAM_START	0x20000000
#define RAM_END		0x20004FFF
#define HEAP_BASE	(((unsigned int)&__ZI_LIMIT__ + 0x7) & ~0x7)
#define HEAP_SIZE	(4*1024)
#define HEAP_LIMIT	(HEAP_BASE + HEAP_SIZE)
#define STACK_LIMIT	(HEAP_LIMIT + 8)
#define STACK_BASE	(RAM_END + 1)
#define STACK_SIZE	(STACK_BASE - STACK_LIMIT)

 

RUNTIME ENVIRONMENT TEST
RO_BASE => 0X08009500
RO_LIMIT => 0X08009828
RW_BASE => 0X20000000
RW_LIMIT => 0X20000580
ZI_BASE => 0X20000580
ZI_LIMIT => 0X200005C8
HEAP_BASE => 0X200005C8
HEAP_LIMIT => 0X200015C8
HEAP_SIZE => 0X00001000
STACK_LIMIT => 0X200015D0
STACK_BASE => 0X20005000
STACK_SIZE => 0X00003A30
p=0X200005D0
q=0X200005E0
[001]q=0X200005D0
[002]q=0X20000658
[003]q=0X200006E0
[004]q=0X20000768
[005]q=0X200007F0
[006]q=0X20000878
[007]q=0X20000900
[008]q=0X20000988
[009]q=0X20000A10
[010]q=0X20000A98
[011]q=0X20000B20
[012]q=0X20000BA8
[013]q=0X20000C30
[014]q=0X20000CB8
[015]q=0X20000D40
[016]q=0X20000DC8
[017]q=0X20000E50
[018]q=0X20000ED8
[019]q=0X20000F60
[020]q=00000000
Heap Over

p와q가 16byte만큼 할당되는 이유

8의 배수로 할당되어야 하기 때문인데, heap이라는 영역은 할당, 할당 해제(free)도 진행되어야 하기 떄문에 8 + 8byte = 16byte 만큼 할당 되어진다고 한다. 즉 8바이트 관리 테이블, 4바이트 메모리 공간, 4바이트 reserved 이렇게 하나씩 할당해주면서 추가가 되고 있는 것이다. 관리테이블에 대한 관리가 수월하지 않고, 메모리 누수가 발생하는 형태이다.

 

임베디드에서는 malloc, calloc 등은 위 이유처럼 메모리 누수가 많이 발생해서 사용 안하는 경우도 많다.

 

 

Volatile의 필요성

 

아래는 gpio toggling 함수이다.

void Main(void)
{
	volatile int i;
	volatile int j;
	Uart_Init(115200);
	Uart_Printf("LED Toggling Test #1\n");
	RCC_APB2ENR |= (1<<3); //b port clock on

	GPIOB_CRH = 0x66 << 0;

	for(;;j++)
	{
		GPIOB_ODR = 0x0 << 8;
		for(i=0; i<0x40000; i++);
		GPIOB_ODR = 0x3 << 8;
		for(i=0; i<0x40000; i++);
	}
}

 

 

CFLAGS          = -mcpu=cortex-m3 -c -g -O3 -Wall -mthumb -msoft-float -fno-builtin -funsigned-char

 

만약에 volatile이 아니라면,

최적화 레벨에 따라서 위 코드가 정상 동작하지 않을 수 있다. 기본적으로 컴파일러는 본인이 직접 최적화를 통해서 효율적인 바이너리를 생성한다. 

이때 volatile을 지정해주면 매번 매모리에 접근해서 그대로 동작을 수행하라고 요청하는 것이다. 

 

만약 최적화 레벨을 낮춰주고 싶으면 -O0와 같은 값으로 변경해주면 된다. 

하지만 최적화를 해주지 않으면 바이너리의 크기가 커질 수 있다. 그래서 위 처럼 필요한 경우에는 volatile 형태로 변경해주는 과정이 필요하다. 

 

 

define에 사용되는 매크로들도 volatile 붙여주는 것을 잊지말자.. 당장은 될지 몰라도 컴파일러, 상황에 따라서 비정상 동작을 야기할 수 있다. 무조건 메모리 맵 I/O는 무조건 volatile을 사용하자! 

#define RCC_APB2ENR   (*(volatile unsigned long*)0x40021018)

#define GPIOB_CRH      (*(volatile unsigned long*)0x40010C04)
#define GPIOB_ODR      (*(volatile unsigned long*)0x40010C0C)

 

 

volatile main()으로 선언하면 안되나요? 

answer : 함수앞에 volatile은 의미 없는 행동이다. 만약 함수앞에 붙이면 함수를 호출할 때, 의미한다. 함수내부에서의 최적화는 그대로 진행된다.

 

아래는 최적화 레벨에 대한 정리

최적화 옵션:
-O0: 최적화를 수행하지 않습니다. 디버깅에 유용합니다.
-O1: 기본적인 최적화를 수행합니다.
-O2: -O1보다 더 많은 최적화를 수행합니다.
-O3: 가능한 가장 높은 수준의 최적화를 수행합니다. 
-Os: 코드 크기를 최소화하는 최적화를 수행합니다.
-Og: 디버깅에 유용한 최적화를 수행합니다. 
-Ofast: 더 공격적인 최적화를 수행하며, 일부 IEEE 표준을 위반할 수 있습니다. 

 

하지만 컴파일러 마다 다를테니 꼭 공식 문서를 보고 확인해야 한다.

 

 

volatile 붙여야 할 때

1. memory mapped I/O

2. DMA에 의한 전송 (cpu 간섭없이 dma ip를 사용해서 고속 이동하는 방법인데, 말그대로 cpu는 메모리 변화를 알지 못하니까, 무조건 volatile을 지정해주어야 한다.) 

3. 인터럽트 처리 루틴 (인터럽트 처리루틴과 Main루틴의 공유 변수, 컴파일러는 인터럽트 처리함수를 CPU가 호출하는 함수 인지 알지 못함 )

4. 멀티프로세스 또는 멀티 프로세싱에 의한 메모리 공유하는 경우

 

ARM CMSIS 함수 : 제조사 무관하게 호환성을 위한 공통의 함수 제공 목적으로 만들어진 함수

core_cm3.h, core_cm3.c를 참고한다. 

 

아래 같은 함수들이 포함된다. ex) __func 형태

uint32_t __get_PSP(void) __attribute__( ( naked ) );
uint32_t __get_PSP(void)
{
  uint32_t result=0;

  __ASM volatile ("MRS %0, psp\n\t" 
                  "MOV r0, %0 \n\t"
                  "BX  lr     \n\t"  : "=r" (result) );
  return(result);
}

uint32_t __get_MSP(void) __attribute__( ( naked ) );
uint32_t __get_MSP(void)
{
  uint32_t result=0;

  __ASM volatile ("MRS %0, msp\n\t" 
                  "MOV r0, %0 \n\t"
                  "BX  lr     \n\t"  : "=r" (result) );
  return(result);
}

 

구조체 형식으로 접근 할 수 있게 구현해주었고, 이 스타일 대로 제조사에서도 본인들 함수를 같은 형태로 개발하는 추세이다.

 

typedef struct
{
  __IO uint32_t ISER[8];                      /*!< Offset: 0x000  Interrupt Set Enable Register           */
       uint32_t RESERVED0[24];                                   
  __IO uint32_t ICER[8];                      /*!< Offset: 0x080  Interrupt Clear Enable Register         */
       uint32_t RSERVED1[24];                                    
  __IO uint32_t ISPR[8];                      /*!< Offset: 0x100  Interrupt Set Pending Register          */
       uint32_t RESERVED2[24];                                   
  __IO uint32_t ICPR[8];                      /*!< Offset: 0x180  Interrupt Clear Pending Register        */
       uint32_t RESERVED3[24];                                   
  __IO uint32_t IABR[8];                      /*!< Offset: 0x200  Interrupt Active bit Register           */
       uint32_t RESERVED4[56];                                   
  __IO uint8_t  IP[240];                      /*!< Offset: 0x300  Interrupt Priority Register (8Bit wide) */
       uint32_t RESERVED5[644];                                  
  __O  uint32_t STIR;                         /*!< Offset: 0xE00  Software Trigger Interrupt Register     */
}  NVIC_Type;
#define NVIC                ((NVIC_Type *)          NVIC_BASE)        /*!< NVIC configuration struct         */

 

구조체 형태라서 가독성이 좋다는 것도 장점이 될 수 있다. 

 

 

 

비트 마스킹 

& : bit단위 and 연산. 0일 때 clear, 1일 때 유지 -> 원하는 비트를 clear할 때 사용함

| : bit단위 or 연산. 0일 때 유지, 1일 때 set -> 원하는 비트를 set할 때 사용함 

^ : bit단위 exclusive or 연산. 같을 땐 0, 다르면 1이됨 -> 원하는 비트 invert할 때 사용함

~ : 1의 보수 (전체 반전)

<<, >> : 비트 시프트 연산. >> 는 signed와 unsigned 동작이 다름. signed는 부호비트가 생기고, unsigned는 0으로 채워짐 

 

ex) i |= ((1<<0) | (1<<6) | (3<< 24)) :  기존 값 읽어서 추가로 원하는 비트 값을 더 추가해줌. 꼭 복합대입 |= 을 해줘야함. 컴파일 에러도 안남 

i &= ~((1<<0) | (1<<6) | (3<< 24)) : 반전으로 바꿔주고 and 연산 해줘야 clear 되기 때문에 이게 좀 헷갈릴 수 있다. 이렇게 사용되는 걸로 외우고 넘어가자.

 

 

 

 

반응형