| |
5장에서는 우리가 실습에 사용할 Mini2440 개발보드를 가지고 지금까지 이론으로만 공부했던 어셈블리어와 C언어를 이용해서 주변 장치들을 제어하는 실습을 해보도록 하겠습니다. 실제 타켓보드에 내가 작성한 프로그램을 다운로드하여 동작을 확인하는 일은 항상 가슴이 설레입니다.
5. S3C2440 개발보드 실습
5.1 S3C2440 Startup 코드 분석
ARM개발보드의 부트코드(Startup)에는 지금까지 이론으로 배웠던 내용들이 거의 모두 포함이 되어 있습니다. 부트 코드만 잘 분석해도 CPU의 50% 이상은 알고 있다고 해도 과언이 아닙니다. 부트코드 기능을 간략하게 요약해 보면 아래와 같습니다.
– Clock & Power Initialization
– Setup each exception handler
– Memory (SDRAM) Initialization
– Peripheral Initialization
– Stack Initialization for each Processor Mode
– Interrupt Handler Setup
– Segment Initialization
– Jump to User Application
이제부터 부팅이 되는 순서대로 실제 코드를 분석해 보도록 하겠습니다.
(1) Exception Vector Table
__program_start
b ResetHandler
b HandlerUndef ;handler for Undefined mode
b HandlerSWI ;handler for SWI interrupt
b HandlerPabort ;handler for PAbort
b HandlerDabort ;handler for DAbort
b . ;reserved
b HandlerIRQ ;handler for IRQ interrupt
b HandlerFIQ ;handler for FIQ interrupt
|
CPU에 전원이 인가되면 처음으로 시작되는 Vector Table(0x0000 0000) 입니다.
(2) Watchdog Disable
Watchdog 가 무엇일까요? 직역을 하면 "지키는개" 이런 뜻이네요. Watchdog는 보통 S/W 적으로 설정한 시간동안 Kick(집지키는 개를 한번씩 차주어야 잠을 자지 않겠죠 ... ^) 을 해주지 않으면 CPU를 Reset 시키는 기능으로 주로 사용합니다. 왜 이런 기능이 필요한 걸까요 ? 우리가 자주 사용하는 스마트폰을 예로 들어 보도록 하겠습니다. 스마트폰 사용중에 어떤 App을 실행시켰는데 그 이후로 스마트폰이 그 App때문에 터치도 되지않고, 전원 버튼도 입력이 되지 않게 먹통이 되었다고 가정을 하면 베터리를 분리시킨 후 다시 연결하는 방법이외에는 방법이 없습니다. 이때 만약 Watchdog가 활성화 되어 있다면 스마트폰이 먹통이 되는 순간 S/W 적으로 설정한 시간동안 Kick이 없으면 스마트폰이 Reset(재 부팅)이 되어 다시 사용할 수 있는 상태가 될것입니다. 베터리를 분리하는거소다는 낫겠죠..
그리고 부트코드에서 Watchdog Disable 하는것은 부팅이 완료되기도 전에 Watchdog에 의해서 CPU가 Reset이 되는것을 방지하기 위해서 하는것 입니다.

0x5300 0000 번지의 SFR 레지스터를 제어하면 WTCON을 설정할 수 있습니다. 실제로는 5번 비트만 "0 = Disable" 해도 됩니다.
ResetHandler
ldr r0,=WTCON ;watch dog disable
ldr r1,=0x0
str r1,[r0]
|
(3) Interrupt Disable
부팅중에 예측하지 못하는 인터럽트가 발생하지 않도록 Disable 하는것이 안전 합니다.
- Core Level Disable: CPSR 레지스터의 I, F 필드를 "1" 로 Mask합니다.
CPSR.I (1), CPSR.F(1) : 부팅시에 "1" 로 마스킹되어 있습니다.
- S3C2440 CPU Level 의 Interrupt Controller 를 Disable 합니다.
Interrupt Mask Register
Interrupt SubMask Register
아래 그림은 S3C2440 CPU의 인터럽트 컨트롤러 블럭도 입니다. " S3C2440 CPU Level 의 Interrupt Controller 를 Disable" 한다는 것은 아래 블럭도에서 바로 "SUBMASK", "MASK" 를 Disable("1" 로 Mask) 한다는 것입니다.

ldr r0,=INTMSK
ldr r1,=0xffffffff ;all interrupt disable
str r1,[r0]
ldr r0,=INTSUBMSK
ldr r1,=0x3ff ;all sub interrupt disable
str r1,[r0]
|
(4) PLL 설정
우리가 사용하는 S3C2440 Mini 개발보드는 외부 Crystal로 12MHz를 사용 합니다.
Input Frequency 가 12MHz 일때, FCLK:HCLK:PCLK = 400MHz : 100MHz : 40MHz, 즉 1:4:8 비율로 분주가 되도록 설정 합니다.
(4.1) Clock Divider Control Register(CLKDIVIN) 설정

CLKDIV_VAL값이 "b0101" 으로 세팅이 되어 있어서 FCLK:HCLK:PCLK = 1:4:8 비율로 분주가 되도록 설정이 됩니다.
최종적으로는 12MHz의 입력주파수를 받아서 동작 주파수가 400MHz가 되도록 설정 합니다.
(4.2) UPLL Control Register(USB CLK) 설정

Upll = (m * Fin) / (p * 2s)
m = (MDIV + 8), p = (PDIV + 2), s = SDIV
Fin = FCLK입력으로 들어오는 Crystal 주파수 12MHz
UPLL = ((56+8)*12) / ((2+2)*2*2) = 48MHz

UPLL은 USB컨트롤러에서 사용할 CLK 으로 결국은 12MHz Crystal 입력을 받아서 48MHz를 만들어서 사용하고 있습니다.
(4.3) MPLL Control Register(Main CLK) 설정
Mpll = (2 * m * Fin) / (p * 2s)
m = (MDIV + 8), p = (PDIV + 2), s = SDIV
MPLL = (2*(92+8)*12) / ((1+2)*2*1) = 400MHz
MPLL은 결국은 12MHz Crystal 입력을 받아서 FCLK = 400MHz,
HCLK = 100MHz,
PCLK = 50MHz 를 만들어서 사용하고 있습니다. HCLK과 PCLK의 계산은
CLKDIV 레지스터 설정을 FCLK:HCLK:PCLK = 1:4:8 비율로 분주비를 설정 했기 때문에 자동으로 계산이 됩니다.
- 참조로 PLL 계산 방식은 S3C2440 Datasheet 를 참조하시기 바랍니다.
ldr r0,=CLKDIVN ; 0x4C000014
ldr r1,=CLKDIV_VAL ; CLKDIV_VAL=5 ---> 1:4:8
str r1,[r0]
;Configure UPLL
ldr r0,=UPLLCON ; 0x4C000008
ldr r1,=((U_MDIV<<12)+(U_PDIV<<4)+U_SDIV) ; U_MDIV=56, U_PDIV=2, U_SDIV=2
str r1,[r0]
;Configure MPLL
ldr r0,=MPLLCON
ldr r1,=((M_MDIV<<12)+(M_PDIV<<4)+M_SDIV)
str r1,[r0]
|
(5) Internal Bus Mode
- Synchronous : Core Clock System Clock(HCLK) 에 동기화 되어 사용
- Asynchronous : System Clock(HCLK)과 관계없이 Free Running Clock(FCLK) 을 이용
우리는 FCLK을 이용할 것이기 때문에 MMU_SetAsyncBusMode 함수를 호출 하였습니다.
(6) Memory System 초기화
시스템에 연결되 FLASH, SDRAM, I/O Device 등과 같은 장치들을 제어하기 위해서 Memory Controller를 초기화 해야 합니다.
– Access Timing
– Data Bus Width
– Wait Cycle
– Refresh Rate
– Bank Memory Size
소스 코드들이 꽤 알아먹기 힘든 코드들이네요. 복잡해 보이지만 하는 일은 BWSCON주소의 SFR 에 SMRDATA 의 4Byte(32-bit) 데이터들을 루프를 돌면서 Write 하는 것입니다.
- SMRDATA : 메모리 컨트롤러 SFR에 기록할 내용들을 4Byte 길이로 해서 테이블 형태로 데이터를 순차적으로 가지고 있습니다.
- BWSCON : S3C2440 메모리 컨트롤러의 SFR 주소 입니다.
소스 파일의 memcfg.inc 에 다음과 같이 정의 되어 있습니다.
;BWSCON
DW8 EQU (0x0)
DW16 EQU (0x1)
DW32 EQU (0x2)
WAIT EQU (0x1<<2)
UBLB EQU (0x1<<3)
B1_BWSCON EQU (DW32)
B2_BWSCON EQU (DW16)
B3_BWSCON EQU (DW16+WAIT+UBLB)
B4_BWSCON EQU (DW16) ; N.C.
B5_BWSCON EQU (DW16) ; N.C.
B6_BWSCON EQU (DW32) ; MINI2440 SDRAM(K4S281632C)-2M*16bit*4Bank*2, SDRAM(K4S561632C) 32MBx2, 32-bit
B7_BWSCON EQU (DW32) ; N.C.
SMRDATA의 맨첫번째 데이터들이 무엇을 의미하는것일까요 ?
(0+(B1_BWSCON<<4)+(B2_BWSCON<<8)+(B3_BWSCON<<12)+(B4_BWSCON<<16)+(B5_BWSCON<<20)+(B6_BWSCON<<24)+(B7_BWSCON<<28))
-->
(0+(0x2<<4)+(0x1<<8)+(0xD<<12)+(0x1<<16)+(0x1<<20)+(0x2<<24)+(0x2<<28))
상위 비트부터 살펴보도록 하겠습니다.
- (0x2<<28) = DW7[29:28] = Bank7에는 어떤 Memory 디바이스도 연결되어 있지 않습니다. 사실은 무의미한 코드 입니다.

- (0x2<<24) = DW6[25:24] = Bank6은 SDRAM이 연결되어 있는 메모리 뱅크 입니다. 데이터 라인은 32bit(2b10) 입니다.

- (0x1<<20) = DW5[21:20] = N.C(Not connected)

- (0x1<<16) = DW4[17:16] = N.C(Not connected)

- (0xD<<12) = ST3[15], WS3[14], DW3[13:12] = 16-bit DM9000 Ehternet 컨트롤러 설정 입니다.

- (0x1<<8) = DW2[9:8] = N.C(Not connected)

- (0x2<<4) = DW1[5:4] = N.C(Not connected)

- DW0[2:1] = Read only 영역으로 OM[1:0] 핀에 의해서 결정 됩니다 . 우리가 사용하는 개발보드는 16bit Data width의 NOR Flash 입니다.

나머지 데이터 설정값들도 Datasheet를 참조해서 분석해 보시기 바랍니다.
;Set memory control registers
ldr r0,=SMRDATA
ldr r1,=BWSCON ; 0x48000000
add r2, r0, #52 ;End address of SMRDATA
L5
ldr r3, [r0], #4
str r3, [r1], #4
cmp r2, r0
bne L5
.
.
.
LTORG
SMRDATA DATA
DCD (0+(B1_BWSCON<<4)+(B2_BWSCON<<8)+(B3_BWSCON<<12)+(B4_BWSCON<<16)+(B5_BWSCON<<20)+(B6_BWSCON<<24)+(B7_BWSCON<<28))
DCD ((B0_Tacs<<13)+(B0_Tcos<<11)+(B0_Tacc<<8)+(B0_Tcoh<<6)+(B0_Tah<<4)+(B0_Tacp<<2)+(B0_PMC)) ;GCS0
DCD ((B1_Tacs<<13)+(B1_Tcos<<11)+(B1_Tacc<<8)+(B1_Tcoh<<6)+(B1_Tah<<4)+(B1_Tacp<<2)+(B1_PMC)) ;GCS1
DCD ((B2_Tacs<<13)+(B2_Tcos<<11)+(B2_Tacc<<8)+(B2_Tcoh<<6)+(B2_Tah<<4)+(B2_Tacp<<2)+(B2_PMC)) ;GCS2
DCD ((B3_Tacs<<13)+(B3_Tcos<<11)+(B3_Tacc<<8)+(B3_Tcoh<<6)+(B3_Tah<<4)+(B3_Tacp<<2)+(B3_PMC)) ;GCS3
DCD ((B4_Tacs<<13)+(B4_Tcos<<11)+(B4_Tacc<<8)+(B4_Tcoh<<6)+(B4_Tah<<4)+(B4_Tacp<<2)+(B4_PMC)) ;GCS4
DCD ((B5_Tacs<<13)+(B5_Tcos<<11)+(B5_Tacc<<8)+(B5_Tcoh<<6)+(B5_Tah<<4)+(B5_Tacp<<2)+(B5_PMC)) ;GCS5
DCD ((B6_MT<<15)+(B6_Trcd<<2)+(B6_SCAN)) ;GCS6
DCD ((B7_MT<<15)+(B7_Trcd<<2)+(B7_SCAN)) ;GCS7
DCD ((REFEN<<23)+(TREFMD<<22)+(Trp<<20)+(Trc<<18)+(Tchr<<16)+REFCNT)
DCD 0x32 ;SCLK power saving mode, BANKSIZE 128M/128M
DCD 0x30 ;MRSR6 CL=3clk
DCD 0x30 ;MRSR7 CL=3clk
DATA
|
(7) Stack Pointer 초기화
각 프로세서 모드별(7개 동작모드)로 Processor Mode를 전환하면서 Stack을 초기화 해야 합니다. 이전 강좌인 ARM Architecture의 ARM Register 부분을 다시 보시면 R13(SP) 는 각 동작 모드별로 뱅크되 되어 있는 레지스터임을 확인 할 수 있습니다. 여기서 주의해야 할 점은 User Mode Stack은 제일 마지막에 초기화 해야 합니다. 왜 글럴까요 ? User Mode 는 비 특권 모드이므로 한번 User Mode로 진입을 하면 SWI 명령등을 사용하지 않으면 다시 특권 모드로 진입을 할 수 없어서 다른 Processor Mode의 Stack Pointer를 초기화 할 수가 없습니다. 이런 이유로 해서 User Mode 의 Stack Pointer를 제일 마지막에 초기화 하도록 해야 합니다.
;function initializing stacks
InitStacks
;Don't use DRAM,such as stmfd,ldmfd......
;SVCstack is initialized before
;Under toolkit ver 2.5, 'msr cpsr,r1' can be used instead of 'msr cpsr_cxsf,r1'
mrs r0,cpsr
bic r0,r0,#MODEMASK
orr r1,r0,#UNDEFMODE|NOINT
msr cpsr_cxsf,r1 ;UndefMode
ldr sp,=UndefStack
orr r1,r0,#ABORTMODE|NOINT
msr cpsr_cxsf,r1
;AbortMode
ldr sp,=AbortStack
orr r1,r0,#IRQMODE|NOINT
msr cpsr_cxsf,r1 ;IRQMode
ldr sp,=IRQStack
orr r1,r0,#FIQMODE|NOINT
msr cpsr_cxsf,r1 ;FIQMode
ldr sp,=FIQStack
;// 아래(System Mode Stack) 부분 추가 함.
bic r0,r0,#MODEMASK|NOINT
orr r1,r0,#SYSMODE
msr cpsr_cxsf,r1 ;SYSMode
ldr sp,=SYSStack
bic r0,r0,#MODEMASK|NOINT
orr r1,r0,#SVCMODE
msr cpsr_cxsf,r1 ;SVCMode
ldr sp,=SVCStack
;USER mode has not be initialized. We use always SVC mode.
mov pc,lr
;The LR register won't be valid if the current mode is not SVC mode.
|
(8) Segment Initialization
ROM Binary가 만들어 지고 나서 프로그램이 실행되기 위해서는 반드시 RAM 이 있어야 합니다. Segment Initialization 은 RAM에서 프로그램이 올바른 데이터를 가지고 실행 될수 있도록 RAM 영역에 전역변수의 초기값들을 저장하는 일을 합니다. 좀더 자세한 사항은 이전 강좌인 ARM Architecture 4.4 Linker 부분을 참조하시기 바랍니다.


위의 그림에서 왼쪽이 ROM Binary 이고 오른쪽이 RAM 영역 입니다.
- .data
: C언어 등에서 선언한 전역변수들의 초기값이 저장되어 있는 영역 입니다.
- .bss : 0 으로 초기화 되는 영역 입니다. 초기값을 지정하지 않은 전역변수등이 여기에 해당 합니다.
- .textrw : 컴파일러에의해 생성된 RAM에서 실행되는 함수들 입니다.
#pragma segment = ".bss"
#pragma segment = ".data"
#pragma segment = ".data_init"
#pragma segment = ".rodata"
#pragma segment = ".textrw"
#pragma segment = ".textrw_init"
#pragma segment = "CSTACK"
void
InitSegment()
{
unsigned int k, n;
unsigned char *pdst, *psrc;
//
// initialize zero-initialized segment
//
n = (unsigned int)__section_end(".bss") - (unsigned int)__section_begin(".bss");
pdst = (unsigned char *)__section_begin(".bss");
for(k=0; k<n; k++)
{
*(pdst + k) = 0;
}
//
// initialize non-zero-initialized segment
//
n = (unsigned int)__section_end(".data_init") - (unsigned int)__section_begin(".data_init");
pdst = (unsigned char *)__section_begin(".data");
psrc = (unsigned char *)__section_begin(".data_init");
for(k=0; k<n; k++)
{
*(pdst + k) = *(psrc + k);
}
//
// initialize segment for ram-function
//
n = (unsigned int)__section_end(".textrw_init") - (unsignedint)__section_begin(".textrw_init");
pdst = (unsigned char *)__section_begin(".textrw");
psrc = (unsigned char *)__section_begin(".textrw_init");
for(k=0; k<n; k++)
{
*(pdst + k) = *(psrc + k);
}
return;
} |
(9) main 함수로 이동
EXTERN main
ldr pc, =main
드디어 모든 Stack초기화와 Segment 초기화 과정을 끝내고 C 함수를 호출할 수 있게 되었습니다. 이제 부터는 C 코드를 이용해서 각 디바이스들을 테스트 할수 있게 되었네요.
5.2 GPIO Output( LED On/Off )
LED를 컨트롤 하기 위해서는 포트를 Output으로 설정한 후에 GPIO(General Purpose Input Output) 포트에 Low or High 를 출력하면 됩니다. 아래 회로도는 우리가 실습에 사용하고 있는 Mini2440의 LED 회로도 입니다. 4개의 LED가 있는데 각 LED는 GPB5 ~ 8 에 연결이 되어 있습니다. 그렇다면 LED1을 켜기 위해서 GPB5 포트에 Low(0) 로 해야 할까요 ? 아니면 High(1) 로 해야 할까요 ? 정답은 LED의 한쪽 끝이 VDD33V 에 연결이 되어 있기 때문에 Low로 설정 해야 전류의 흐름이 발생하여 LED가 켜지게 됩니다.

* 실험예제
LED1, LED2 는 On을 하고 LED3, LED4는 Off 시켜 봅니다.
(1) GPBCON 레지스터에 GPB5 ~ GPB8 을 Output으로 설정 합니다.


// GPB5 Output
(*(volatile unsigned *)0x56000010) = (*(volatile unsigned *)0x56000010) & ~(0x3 << 10);
(*(volatile unsigned *)0x56000010) = (*(volatile unsigned *)0x56000010) | (0x1 << 10);
// GPB6 Output
rGPBCON = rGPBCON & ~(0x3 << 12);
rGPBCON = rGPBCON | (0x1 << 12);
// GPB7 Output
rGPBCON = rGPBCON & ~(0x3 << 14);
rGPBCON = rGPBCON | (0x1 << 14);
// GPB8 Output
rGPBCON = rGPBCON & ~(0x3 << 16);
rGPBCON = rGPBCON | (0x1 << 16);
(3) GPBDAT 레지스터의 GPB5, GPB6 을 Low 로, GPB7, GPB8 을 High로 세팅 합니다.
// LED1 On
rGPBDAT = rGPBDAT & ~(0x1 << 5);
// LED2 On
rGPBDAT = rGPBDAT & ~(0x1 << 6);
//rGPBDAT = rGPBDAT | (0x1 << 6);
// LED3 Off
rGPBDAT = rGPBDAT | (0x1 << 7);
// LED4 Off
rGPBDAT = rGPBDAT | (0x1 << 8);
diag.c - led_test()
// GPB5 Output
(*(volatile unsigned *)0x56000010) = (*(volatile unsigned *)0x56000010) & ~(0x3 << 10);
(*(volatile unsigned *)0x56000010) = (*(volatile unsigned *)0x56000010) | (0x1 << 10);
// GPB6 Output
rGPBCON = rGPBCON & ~(0x3 << 12);
rGPBCON = rGPBCON | (0x1 << 12);
// GPB7 Output
rGPBCON = rGPBCON & ~(0x3 << 14);
rGPBCON = rGPBCON | (0x1 << 14);
// GPB8 Output
rGPBCON = rGPBCON & ~(0x3 << 16);
rGPBCON = rGPBCON | (0x1 << 16);
// LED1 On
rGPBDAT = rGPBDAT & ~(0x1 << 5);
// LED2 On
rGPBDAT = rGPBDAT & ~(0x1 << 6);
// LED3 Off
rGPBDAT = rGPBDAT | (0x1 << 7);
// LED4 Off
rGPBDAT = rGPBDAT | (0x1 << 8);
|
* 소스코드 분석
(*(volatile unsigned *)0x56000010) = (*(volatile unsigned *)0x56000010) & ~(0x3 << 10);
GPBCON 레지스터의 주소가 0x50000010 입니다. 0x3(b11) 을 왼쪽으로 10번 쉬프트를 하면 b110000000000 이고 이것을 "~" (Bit clear) 시키면 GPB5[11:10] 부분이 "00" 으로 Clear 됩니다.
(*(volatile unsigned *)0x56000010) = (*(volatile unsigned *)0x56000010) | (0x1 << 10);
0x1(b01) 을 왼쪽으로 10번 쉬프트를 하면 b010000000000 이고 이것을 "|" (OR) 시키면 GPB5[11:10] 부분이 "01" 으로 Output으로 설정 됩니다.
소스 코드중에 volatile 이라는 것을 사용하고 있습니다. 이것은 컴파일러 최적화에서 제외되는 효과가 있습니다.
컴파일러 최적화 전
|
컴파일러 최적화 후 |
int a = 0; // 전역 변수
int b = 0; // 전역 변수
int i; // 로컬 변수
for(i=0;i<100;i++)
b = b + a * 100; |
int a = 0; // 전역 변수
int b = 0; // 전역 변수
int i; // 로컬 변수
for(i=0;i<100;i++)
b = b + 0; // a * 100; |
위의 코드에서 오르쪽 코드처럼 개발자가 의도하지 않게 컴파일러에 의해 최적화가 되어 "a*100" 부분을 최적화 하여 b 변수에 항상 0으로 저장이 되도록 할 수도 있습니다. 물론 일반적인 상황에서는 아무 문제가 되지 않지만 for 루프 수행중에 외부 인터럽트가 발생하여 인터럽트 서비스 루틴에서 a 의 값을 0이 아닌 다른 값으로 변하게 한 후 다시 for 루프를 수행 하면 개발자는 b에 0이 아닌 다른값이 저장되기를 기대하고 있겠지만 최적화된 코드에서는 b 에 항상 0 이 저장이 되어 있을것입니다. 이것은 개발자가 의도한 결과가 아닙니다. 이런 현상을 방지하기 위해서는 변수 a 를 volatile 로 선언을 하면 됩니다. 특히나 SFR 레지스터등에 값을 세팅하는 작업을 한다면 항상 volatile 로 선언을 해서 사용하는 하는것이 좋습니다.
5.3 GPIO Input( KEY Input) - Polling
LED를 켰을때와 반대로 이번에는 GPIO포트를 이용해서 입력을 받아 보도록 하겠습니다. 포트에서 입력을 받기 위해서는 포트를 Input 으로 설정한 후에 GPIO(General Purpose Input Output) 포트를 읽으면 됩니다. 6개의 KEY가 있는데 우리는 INT11, INT13 에 연결되어 있는 K2, K3 에 대해서 폴링방식으로 입력을 감지해 보도록 하겠습니다. 참고로 폴링 방식이란 코드에서 무한 루프를 돌면서 특정 행위를 하는것을 말합니다. 이번 경우에는 Key가 눌려졌는지를 감시하는 것이겠지요. 폴링과 다른 방식으로는 인터럽트 방식이 있습니다. 다음절에서 공부하게될 내용입니다.

* 실험예제
K2, K3 를 읽어서 KEY가 눌려져 있으면 LED2, LED3 를 On으로 하고 눌려져 있지 않으면 Off 로 설정해 봅시다.
(1) GPGCON 레지스터에 GPG3, GPG5 를 Input으로 설정 합니다.


// KEY3, GPG5 Input
rGPGCON = rGPGCON & ~(0x3 << 10);
// KEY2, GPG3 Input
rGPGCON = rGPGCON & ~(0x3 << 6);
(2) GPGDAT 레지스터의 GPG3, GPG5 를 읽어서 "0" 이면 KEY가 눌린 상태이고, "1" 이면 KEY 눌려지지 않은 상태 입니다.
diag.c - key_test()
// KEY3, GPG5 Input
rGPGCON = rGPGCON & ~(0x3 << 10);
// KEY2, GPG3 Input
rGPGCON = rGPGCON & ~(0x3 << 6);
while(1)
{
if( (rGPGDAT & (0x1 << 3)) == 0 ) // KEY2 pressed
// LED2 On
rGPBDAT = rGPBDAT & ~(0x1 << 6);
else
// LED2 Off
rGPBDAT = rGPBDAT | (0x1 << 6);
if( (rGPGDAT & (0x1 << 5)) == 0 ) // KEY3 pressed
// LED3 On
rGPBDAT = rGPBDAT & ~(0x1 << 7);
else
// LED3 Off
rGPBDAT = rGPBDAT | (0x1 << 7);
Delay(80);
};
|
5.4 GPIO Input( KEY Input) - Interrupt
이번에는 인터럽트 방식으로 K2, K3 입력을 감지해 보도록 하겠습니다. 인터럽트 방식은 폴링방식보다 효율적이기는 하지만 좀더 복잡합니다. 여기서 효율적이라고 하는것은 CPU사용을 효과적으로 한다는것이지 폴링방식보다 빠르다는 말은 아닙니다. 인터럽트방식은 인터럽트요청이 왔을때 인터럽트 서비스 루틴으로 분기할때까지 지연시간이 있어 폴링 방식보다 느릴수 있습니다. 하지만 폴링방식처럼 계속 루프를 돌면서 대기하지 않아도 되기 때문에 CPU활용면에서는 효율적입니다.
* 실험예제
K2, K3 를 읽어서 KEY가 눌려져 있으면 LED2, LED3 를 On으로 하고 눌려져 있지 않으면 Off 로 설정해 봅시다.
(1) GPGCON 레지스터에 GPG3, GPG5 를 EINT(0x2) 으로 설정 합니다.

// KEY3, GPG5 EINT13
rGPGCON = rGPGCON & ~(0x3 << 10);
rGPGCON = rGPGCON | (0x2 << 10);
// KEY2, GPG3 EINT11
rGPGCON = rGPGCON & ~(0x3 << 6);
rGPGCON = rGPGCON | (0x2 << 6);
(2) EXTINT1 레지스터에 EINT11, EINT13을 Falling Edge로 설정 합니다.


// set eint13 falling edge trigger
rEXTINT1 &= ~(0x7 << 20);
rEXTINT1 |= (0x2 << 20);
// set eint11 falling edge trigger
rEXTINT1 &= ~(0x7 << 12);
rEXTINT1 |= (0x2 << 12);
(3) EINTPEND 레지스터의 Pending bit 를 Clear 합니다.


// clear eint 11,13
rEINTPEND |= (1 << 11)|(1 << 13);
"It is cleard by writing 1" 이라는 문구가 보이는데요. 무슨 의미 일까요 ? 일반적으로 우리가 SFR 레티스터를 설정 할때에 아래와 같이 먼저 Bit를 Clear 시키고 Set 하는 2단계 과정을 거치게 됩니다.
rEXTINT1 &= ~(0x7 << 20);
rEXTINT1 |= (0x2 << 20);
하지만 인터럽트 서비스 루틴 안에서는 이것 조차도 비효율적일수 있고 Atmoic Operation 이 아니기 때문에 Pending Clear를 하는 과정에서 새로운 인터럽트가 발생할 경우에 새로운 인터럽트가 지연될수도 있습니다.
rEINTPEND |= (1 << 11)|(1 << 13);
이러한 이유로 위와 같이 "1" 을 써넣어서 바로 Pending을 Clear 할수 있도록한 것입니다.
(4) EINTMASK 레지스터에서 Interrupt를 Enable 합니다.

// enable eint 11,13
rEINTMASK &= ~((1 << 11)|(1 << 13));
(5)
SRCPND 레지스터의 Pending bit 를 Clear 합니다.

rSRCPND = (0x1<<5); // Clear pending bit
(6) INTPND 레지스터의 Pending bit 를 Clear 합니다.


rINTPND = (0x1<<5); // Clear pending bit
(7) EINT 인터럽트 서비스 루틴의 함수 포인터를 설정 합니다.
pISR_EINT8_23 = (U32)isr_eint_8_23;
(8) INTMSK 에서 EINT8_23 의 인터럽트를
Enable 합니다.


rINTMSK &= ~((0x1<<5));
(9) Startup 코드의 IRQ Exception 에서 인터럽트 번호를 확인하고 인터럽트 서비스 루틴으로 분기 합니다.
Startup 코드 설명할때 이부분은 설명을 하지 않고 그냥 넘어갔습니다. 어떤 마법의 코드로 인해서 수많은 인터럽트 핸들러 함수가 1개의 루틴으로 해결이 될까요 ? 이번에 상세하게 설명 하도록 하겠습니다.
IsrIRQ
sub sp,sp,#4 ;reserved for PC --> 1
stmfd sp!,{r8-r9} ; --> 2
ldr r9,=INTOFFSET ; --> 3
ldr r9,[r9] ; --> 4
ldr r8,=HandleEINT0 ; --> 5
add r8,r8,r9,lsl #2 ; --> 6
ldr r8,[r8] ; --> 7
str r8,[sp,#8] ; --> 8
ldmfd sp!,{r8-r9,pc} ; --> 9
|
--> 1 : 스택포인터를 1 감소하여 빈 공간을 만듭니다. 인터럽트 핸들러 함수의 주소값이 저장될 공간 입니다.
--> 2 : r8, r9 를 사용하고 복원해야 하므로 stack에 저장 합니다. stmfd는 먼저 어드레스를 감소하고 저장하므로 1 에서 만들어진 빈간공은 그대로 남아 있습니다.
--> 3, 4 : INTOFFSET 을 r9 레 로드 합니다.
--> 5 :
HandleEINT0 를 r8 에 로드 합니다. HandleEINT0는 Startup 코드의 아래 부분에 정의되어 있습니다.
--> 6,7 : HandleEINT0 + INTOFFSET*4 의 수식이 됩니다. INTOFFSET 레지스터에는 발생한 인터럽트 소스의 Offset 값이 들어있는 레지스터 입니다.
우리는 EINT8_23 을 이용할 것이기 때문에 INTOFFSET 레지스터에는 5가 저장되어 있을것입니다. 그러므로 HandleEINT0 + 5*4 즉 HandleEINT8_23의 주소가 됩니다.
소스코드에서 key_test_eint() 함수에서 pISR_EINT8_23 = (U32)isr_eint_8_23; 의 코드를 삽입하면 EINT8_23 인터럽트가 발생하면 isr_eint_8_23 함수로 분기를 하게 됩니다.

--> 8 :
str r8,[sp,#8] 에 의해서 맨 처음에 Stack의 빈간공을 만들어 두었던 곳으로 인터럽트 핸들러 함수의 포인터주소값을 저장 합니다.
--> 9
: r8, r9 를 Stack 에서 복원하고 pc 에 인터럽트 핸들러 함수의 주소가 저장이 되어 실제로 인터럽트 핸들러 함수가 실행이 됩니다.
(10) 인터럽트 서비스 루틴에서 GPGDAT 레지스터의 GPG3, GPG5 를 읽어서 "0" 이면 KEY가 눌린 상태이고, "1" 이면 KEY 눌려지지 않은 상태 입니다.
지금까지 설명한 인터럽트 관련 설정들을 S3C2440 인터럽트 컨트롤러 블럭도와 비교해서 보시기 바랍니다.

Interrupt MODE도 IRQ/FIQ 설정을 해야 하지만 S3C2440의 기본 인터럽트 설정 값이 IRQ 이기 때문에 생략 하였습니다.
외부 인터럽트 1개를 서비스 받기 위해서 정말 많은 과정이 필요 하네요. ARM Applications 이후에 하게될 Cortex-M3 Architecture 부분에서도 인터럽트 서비스에 대해서 설명을 할텐데, 그때 Cortex-M3 가 얼마나 간결하고 효율적인지 비교해 보시기 바랍니다.
diag.c - key_test_eint()
__irq __arm void isr_eint_8_23(void)
{
if(rINTPND==BIT_EINT8_23)
{
ClearPending(BIT_EINT8_23);
if(rEINTPEND&(1<<11))
{
rEINTPEND |= 1 << 11;
rGPBDAT = rGPBDAT ^ (0x1 << 6); // toggle
}
if(rEINTPEND&(1<<13))
{
rEINTPEND |= 1 << 13;
rGPBDAT = rGPBDAT ^ (0x1 << 7); // toggle
}
}
}
void key_test_eint(void)
{
// KEY3, GPG5 EINT13
rGPGCON = rGPGCON & ~(0x3 << 10);
rGPGCON = rGPGCON | (0x2 << 10);
// KEY2, GPG3 EINT11
rGPGCON = rGPGCON & ~(0x3 << 6);
rGPGCON = rGPGCON | (0x2 << 6);
// set eint13 falling edge trigger
rEXTINT1 &= ~(0x7 << 20);
rEXTINT1 |= (0x2 << 20);
// set eint11 falling edge trigger
rEXTINT1 &= ~(0x7 << 12);
rEXTINT1 |= (0x2 << 12);
// clear eint 11,13 --> clear by writing "1" test
rEINTPEND |= (1 << 11)|(1 << 13);
// enable eint 11,13
rEINTMASK &= ~((1 << 11)|(1 << 13));
rSRCPND = (0x1<<5); // Clear pending bit --> clear by writing "1" test
rINTPND = (0x1<<5); // Clear pending bit --> clear by writing "1" test
pISR_EINT8_23 = (U32)isr_eint_8_23;
// Enable EINT8_23 Interrupt
rINTMSK &= ~((0x1<<5));
}
|
5.5 Timer
임베디드 시스템에서 Timer는 가장 중요하고 필수적인 요소중의 하나로 OS에서 Task 스케줄링을 위해서 사용되기도 합니다. 전자액자, 차량용 블랙박스 등의 응용 어플리케이션에서도 특정 시간 이후에 인터럽트를 발생시켜 정해진 일을 수행하는 경우등 응용분야는 무수히 많습니다.
시간 이야기가 나와서 이야기 하는데 CPU의 클럭 속도가 1Hz 라는 것은 무엇을 의미하는 것일까요 ? 이것은 1초에 1번의 Tick 이 발생한다는 것입니다. 아주 정확한 것은 아니지만 캐시가 있는 시스템에서 캐시 Hit(캐시에서 데이터 혹은 명령어를 가지고 올 경우)일 경우에 1초에 1개의 명령어를 수행한다는 것과 같습니다. S3C2440이 400MHz 로 동작 한다면 1초에 4억번의 Tick이 발생하는 것입니다. 정말 어마어마 하지요. 상상해 보세요. 요즘 최신 스마트폰들은 2GHz CPU이면서 그것도 듀얼 코어 입니다. 아래는 시간과 주파수 사이의 관계 입니다. Timer 설정하는데 이정도는 알고 있어야 합니다. 잘 기억 하시기 바랍니다.
- 1sec = 1,000ms = 1,000,000us = 1,000,000,000ns
- 1Hz = 1KHz = 1MHz = 1GHz
* 실험예제
S3C2440 CPU의 내장된 Timer4를 이용해서 1초에 한번씩 Timer 인터럽트를 발생시켜 LDE2를 Blink(On/Off) 하는 실험을 해보도록 하겠습니다.
(1) TCFG0 레지스터의 Timer4 Prescaler를 15 로 설정 합니다.
// Timer4 Prescaler = 15
rTCFG0 = 0x0;
rTCFG0 = (15 << 8);
(2) TCFG1 레지스터의 Timer4 Divider를 1/2 로 설정 합니다.

// Timer4 Divider = 2
rTCFG1 = 0x0;
rTCFG1 = (0x0 << 16);
위와 같이 설정하면 Datasheet 에 있는 아래 공식에 의해서
Timer input clock Frequency = PCLK / {prescaler value+1} / {divider value}
Timer input clock Frequency = 50000000/(15+1)/2 = 1562500
위의 계산식의 의미는 Timer4의 클럭이 1초에 1562500 번 발생 한다는 의미 입니다. 그러므로 Timer4 buffer count(TCNTB4) 를 15625 로 설정해 주면 10ms 마다 Auto Reload(Timer Overflow) 가 되어 인터럽트가 발생하게 됩니다. 물론 간단하게 TCNTB4를 1562500로 설정해 주면 1초에 한번 발생하는 인터럽트를 만들수 있겠지만, 안타깝게도 S3C2440의 타이머는 16비트 타이머 이므로 최대 값으로 0xFFFF(65535) 이상을 사용할수가 없습니다.
(3) TCNTB4 레지스터에 Timer4 buffer count 를 15625로 설정 합니다.
// buffer count
rTCNTB4 = 15625; // interrupt resolution 10msec
(4) TCON 레지스터에 Update TCNTB4와 Auto Reload를 설정 합니다.

rTCON = rTCON & ~(0xffffff) | 0x1<<21 ; // Manual update : Update TCNTB4, TIMER Stop
rTCON = rTCON | (1 << 22); // Auto reload
(5) TCON 레지스터를 설정하여 Timer를 Start 시킵니다.
// Start Timer 4
rTCON |= (1<<20);
(6) TCON
에서 Manual Update를 해제하고 INTMSK 에서 인터럽트 마스크를 Clear 시킵니다.
// clean manual update
rTCON &= ~(1<<21);
// Enable interrupt
rINTMSK &= ~(0x1<<14);
이렇게 하면 Timer4 인터럽트가 10msec 마다 한번씩 발생하게 됩니다.

diag.c - timer4_test()
__irq __arm void isr_timer4(void)
{
rSRCPND = BIT_TIMER4; //Clear pending bit
rINTPND = BIT_TIMER4;
// Timer가 10msec 마다 발생하므로 1초에 한번씩 toggle 시키기 위해서
if( ++one_seconds_var == 100 )
{
rGPBDAT = rGPBDAT ^ (0x1 << 6); // toggle LED2
one_seconds_var = 0;
}
void timer4_test(void)
{
/*
Timer input clock Frequency = PCLK / {prescaler value+1} / {divider value}
{prescaler value} = 0~255
{divider value} = 2, 4, 8, 16
period = (prescaler value + 1) * (divider value) * buffer count / PCLK = 10 ms
e.g.; PCLK = 50 Mhz
10 ms = (15 + 1) * 2 * 15625 / (50000 * 1000)
15626 = 10 ms * (50000 * 1000) / 2 / (15 + 1)
*/
// Timer4 Prescaler = 15
rTCFG0 = 0x0;
rTCFG0 = (15 << 8);
// Timer4 Divider = 2
rTCFG1 = 0x0;
rTCFG1 = (0x0 << 16);
// Timer input clock Frequency = PCLK / {prescaler value+1} / {divider value}
// 50000000/(15+1)/2 = 1562500 --> 1초에 발생하는 Timer Tick
// buffer count
rTCNTB4 = 15625; // interrupt resolution 10msec
rTCON = rTCON & ~(0xffffff) | 0x1<<21 ; // Manual update : Update TCNTB4, TIMER Stop
rTCON = rTCON | (1 << 22); // Auto reload
pISR_TIMER4 = (int)isr_timer4;
// Start Timer 4
rTCON |= (1<<20);
// clean manual update
rTCON &= ~(1<<21);
// Enable interrupt
rINTMSK &= ~(0x1<<14);
}
|
여기서 잠깐 일반함수와 "__irq __arm" 가 있는 함수의 차이점은 무엇일까요 ?
* 일반 함수의 Return 부분

* ISR 함수의 Return 부분

ISR 함수는 ARM Architecture 에서 배운데로 Return을 SUBS PC,LR,#4 로 하고 있습니다. 잘 기억이 나지 않으시면 ARM Architecture 자료에서 Pipeline과 Interrupt 부분을 다시 보시기 바랍니다.
5.6 PWM Buzzer
Buzzer, LED 등의 밝기 조정등에 응용을 할 수 있습니다.
* 실험예제
Buzzer을 울리도록 하고 외부 인터럽트 방식으로 Key K2를 누르면 주파수를 10Hz 올리고 K3를 누르면 10Hz 주파수가 내려가도록 해봅시다.

(1)
GPBCON 레지스터에서 GPB0를 TOUT0(Timer0 Output) 으로 설정 합니다.


rGPBCON &= ~0x3; // set GPB0 as tout0, pwm output
rGPBCON |= 0x2;
(2) TCFG0 레지스터의 Timer0 Prescaler를 15 로 설정 합니다.
rTCFG0 &= ~0xff;
rTCFG0 |= 15; // prescaler = 15+1
(3) Timer0의 Divider 를 1/8 로 설정 합니다.

![]()
rTCFG1 &= ~0xf;
rTCFG1 |= 2; // mux = 1/8
(4)
Timer0 의 Count buffer register, compare buffer register 를 설정 합니다.

rTCNTB0 = (Pclk>>7)/buzzer_freq;
rTCMPB0 = rTCNTB0>>1; // rTCNTB0/2
여기까지 설정을 하면 아래와 같이 1KHz의 주파수가 만들어 짐.
Timer input clock Frequency = PCLK / {prescaler value+1} / {divider value}
Timer input clock Frequency = 50000000/(15+1)/8 = 390625 --> 1초에 발생하는 Timer Tick
50000000Hz(PCLK) >> 7 = 390625 --> 결국은 1초에 1번 1Hz 값,
buzzer_freq(1000) 으로 나누어 주면 TCNTBn은 1KHz 가 됩니다.
추가로 TCMPBn 은 TCNTBn/2 로 하면 최종적으로 1KHz인 Timer0(TOUT0) 으로 1KHz인 PWM 파형이 출력 됩니다.

(5) Timer0 를 Start 시킵니다.

rTCON &= ~0x1f;
rTCON |= 0xb; //disable deadzone, auto-reload, inv-off, update TCNTB0&TCMPB0, start timer 0
rTCON &= ~2; //clear manual update bit
diag.c - pwm_buzzer_test()
__irq __arm void isr_eint_8_23(void)
{
if(rINTPND==BIT_EINT8_23)
{
ClearPending(BIT_EINT8_23);
if(rEINTPEND&(1<<11)) // KEY2
{
rEINTPEND |= 1 << 11;
rGPBDAT = rGPBDAT ^ (0x1 << 6); // LED2 toggle
buzzer_freq += 100;
pwm_buzzer_test();
}
if(rEINTPEND&(1<<13)) // KEY3
{
rEINTPEND |= 1 << 13;
rGPBDAT = rGPBDAT ^ (0x1 << 7); // LED3 toggle
buzzer_freq -= 100;
pwm_buzzer_test();
}
}
}
void pwm_buzzer_test(void)
{
rGPBCON &= ~0x3; // set GPB0 as tout0, pwm output
rGPBCON |= 0x2;
rTCFG0 &= ~0xff;
rTCFG0 |= 15; // prescaler = 15+1
rTCFG1 &= ~0xf;
rTCFG1 |= 2; // mux = 1/8
// Timer input clock Frequency = PCLK / {prescaler value+1} / {divider value}
// 50000000/(15+1)/8 = 390625 --> 1초에 발생하는 Timer Tick
// PCLK = 50000000Hz >> 7 = 390625 --> 결국은 1초에 1번 1Hz 값이다.
// buzzer_freq(1000) 으로 나누어 주면 1KHz가 된다.
rTCNTB0 = (PCLK>>7)/buzzer_freq;
rTCMPB0 = rTCNTB0>>1; // rTCNTB0/2
rTCON &= ~0x1f;
rTCON |= 0xb; //disable deadzone, auto-reload, inv-off, update TCNTB0&TCMPB0, start timer 0
rTCON &= ~2; //clear manual update bit
}
|
5.7 UART
임베디드 프로그램에서 Timer와 함께 UART도 필수적인 인터페이스 입니다. 개발초기에 디버그모니터용으로 활용을 하기도 하고 개발이 완료된 이후에는 제품의 Setting, Firmware 업그레이드 등에도 활용을 합니다.
 
* 실험예제
UART0를 PC와 115200bps Baudrate로 통신(RX, TX)을 하는 Echo server로 만들어 봅시다.
(1) GPH2를 TXD로 GPH3을 RXD로 설정하고, GPH의 Pullup을 Disable 합니다.
rGPHCON &= ~( (0x3 << 6) | (0x3 << 4) );
rGPHCON |= ( (0x2 << 6) | (0x2 << 4) );
rGPHUP = 0x7ff; // The pull up function is disabled GPH[10:0]
(2) UART0의 FIFO Buffer를 Disable 합니다.

rUFCON0 = 0x0; //UART channel 0 FIFO control register, FIFO disable
(3) UART0의 Auto Flow Control을 Disable 합니다.

rUMCON0 = 0x0; //UART chaneel 0 MODEM control register, AFC disable
(4) UART Line Control Regier 설정(Normal Mode, No Parity, One Stop Bit, Word Length = 8)


rULCON0 = 0x3; //Line control register : Normal,No parity,1 stop,8 bits
(5) UART Control Regier 설정




rUCON0 = (0x0 << 10) | (0x1 << 6) | (0x0 << 5) | (0x0 << 4) | (0x1 << 2) | (0x1 << 0);
(6) UART Baudrate Divisor Register 설정

rUBRDIV0 = ( (int)(Pclk/16./baud+0.5) -1 ); //Baud rate divisior register 0 value = 26
UBRDIV = (int)( UART clock / ( buad rate x 16) ) –1
( UART clock : PCLK, FCLK/n or UEXTCLK )
우리는 UART Clock으로 PCLK 50MHz(50000000)을 사용하고 있으므로 계산식은 다음과 같습니다.
UBRDIV =
( PCLK / (115200*16) ) - 1 = 26
diag.c - uart0_test()
void uart0_send_byte(int data)
{
while(!(rUTRSTAT0 & 0x2)); //Wait until THR is empty.
Delay(10);
WrUTXH0(data);
}
void uart0_test(unsigned int baud)
{
rGPHCON &= ~( (0x3 << 6) | (0x3 << 4) );
rGPHCON |= ( (0x2 << 6) | (0x2 << 4) );
rGPHUP = 0x7ff; // The pull up function is disabled GPH[10:0]
rUFCON0 = 0x0; //UART channel 0 FIFO control register, FIFO disable
rUMCON0 = 0x0; //UART chaneel 0 MODEM control register, AFC disable
//UART0
rULCON0 = 0x3; //Line control register : Normal,No parity,1 stop,8 bits
rUCON0 = (0x0 << 10) | (0x1 << 6) | (0x0 << 5) | (0x0 << 4) | (0x1 << 2) | (0x1 << 0);
rUBRDIV0 = ( (int)(Pclk/16./baud+0.5) -1 ); //Baud rate divisior register 0 value = 26
while(!(rUTRSTAT0 & 0x4)); //Wait until tx shifter is empty.
uart0_send_byte('E');
uart0_send_byte('c');
uart0_send_byte('h');
uart0_send_byte('o');
uart0_send_byte('\r');
uart0_send_byte('\n');
while(1)
{
Delay(10);
while(!(rUTRSTAT0 & 0x1)); //Receive data ready
uart0_send_byte((*(volatile unsigned char *)0x50000024));
}
}
|
위의 코드는 터미널창에 "Echo" 를 Display 하고 터미널에서 입력한 문자를 바로 Echo 하여 다시 터미널 창에 표시하게 됩니다.
|