임베디드 리눅스 프로그래밍 -...

121
임베디드 리눅스 프로그래밍 1 충남대학교 전기정보통신공학부 운영체제실습 실습교재 임베디드 리눅스 프로그래밍 (Imbedded Linux Programming) 2002. 3 충남대학교 정보통신공학부

Transcript of 임베디드 리눅스 프로그래밍 -...

임베디드 리눅스 프로그래밍 1

충남대학교 전기정보통신공학부 운영체제실습 실습교재

임베디드 리눅스 프로그래밍

(Imbedded Linux Programming)

2002. 3

충남대학교 정보통신공학부

임베디드 리눅스 프로그래밍 2

임베디드 리눅스 프로그래밍 3

목 차

1 리눅스(Linux) 개요 ............................................................................................7

1.1 기능의 특징 .........................................................................................7

1.2 커널 기능 구조 ....................................................................................7

1.3 소스 파일 구조 ....................................................................................9

1.4 Linux 의 활용 ....................................................................................13

2 임베디드 리눅스 개요 ......................................................................................14

2.1 임베디드 시스템과 리눅스....................................................................14

2.2 uClinux .............................................................................................15

3 임베디드 리눅스 실습환경 ................................................................................17

3.1 실습환경 개요 ....................................................................................17

3.2 개발 시스템용 리눅스 설치 ..................................................................19

3.2.1 커널 구성(configuration) ...............................................................21

3.2.2 커널 컴파일.................................................................................30

3.3 임베디드 시스템용 uClinux 환경 ...........................................................33

3.3.1 개발 과정....................................................................................33

3.3.2 개발 툴 ......................................................................................33

3.4 네트워킹 ...........................................................................................45

3.4.1 기본적인 네트워크........................................................................45

3.4.2 네트워크 디바이스 드라이버 ..........................................................45

3.4.3 네트워크 모듈..............................................................................45

3.4.4 네트워크 인터페이스 설정 .............................................................46

3.4.5 네트워크 라우팅...........................................................................46

3.4.6 주소 해석....................................................................................47

3.4.7 실습 환경....................................................................................47

4 uClinux 프로그래밍 .........................................................................................49

4.1 “Hello uClinux” ..................................................................................49

4.1.1 개발 시스템에서의 실행 ................................................................49

4.1.2 타겟시스템에서의 실행..................................................................49

4.2 uClinux 하드웨어 프로그래밍 ............................................................... 49

4.2.1 타겟시스템 하드웨어.....................................................................49

4.2.2 LED 제어 프로그래밍 ...................................................................52

4.3 uClinux 네트웍 프로그래밍 ..................................................................54

4.3.1 네트웍 상태.................................................................................54

4.3.2 서버 ...........................................................................................55

임베디드 리눅스 프로그래밍 4

4.3.3 클라이언트 ..................................................................................56

4.3.4 실습 ...........................................................................................57

5 uClinux 커널 프로그래밍 ..................................................................................61

5.1 커널 컴포넌트 ....................................................................................61

5.1.1 커널 컴포넌트..............................................................................61

5.1.2 커널 이미지 생성 .........................................................................67

5.1.3 커널 이미지 수정 .........................................................................69

5.2 디바이스 드라이버 ..............................................................................72

5.2.1 디바이스 드라이버의 기초 .............................................................72

5.2.2 디바이스 오퍼레이션 테이블 ..........................................................73

5.2.3 Open/Close 호출 .........................................................................75

5.2.4 Read/Write 호출 ..........................................................................77

5.2.5 Seek 호출...................................................................................79

5.2.6 IOCTL 인터페이스 ........................................................................80

5.2.7 Fops 테이블................................................................................84

5.2.8 리눅스 예제.................................................................................85

5.3 인터럽트 ...........................................................................................94

5.3.1 하드웨어 Base Vector 설정 ...........................................................94

5.3.2 인터럽트의 처리...........................................................................98

5.3.3 인터럽트 번호............................................................................ 102

5.4 타이머 ............................................................................................ 103

5.5 시스템 콜 ........................................................................................ 104

5.5.1 Syscall 추가하기 ........................................................................ 104

6 uClinux 웹 서버............................................................................................ 109

7 부록 ............................................................................................................ 110

7.1 Make 사용법.................................................................................... 110

7.1.1 간단한 Makefile ......................................................................... 110

7.1.2 Target 만들기............................................................................ 110

7.1.3 Target list ................................................................................. 110

7.1.4 Flags 와 switches ..................................................................... 111

7.1.5 조건문 ...................................................................................... 111

7.1.6 Shell interface ........................................................................... 112

7.2 Patch 사용법 ................................................................................... 112

7.2.1 Patch ....................................................................................... 112

7.2.2 Diff .......................................................................................... 112

7.2.3 예제 ......................................................................................... 114

임베디드 리눅스 프로그래밍 5

7.2.4 실제 사용 예제 .......................................................................... 114

7.2.5 문제점 ...................................................................................... 115

7.2.6 패치에 실패한 예 ....................................................................... 115

7.2.7 diff 예제 ................................................................................... 116

7.3 RPM............................................................................................... 117

7.3.1 Install ....................................................................................... 117

7.3.2 Remove.................................................................................... 118

7.3.3 Query....................................................................................... 118

7.3.4 Upgrade ................................................................................... 118

7.3.5 소스 RPM ................................................................................. 118

7.3.6 Package 재설치......................................................................... 118

7.3.7 RPM SPEC 파일 예제 ................................................................. 118

8 참고 문헌 .................................................................................................... 121

그림목차

그림 1.1 리눅스 커널의 기능 구조 ......................................................................8

그림 1.2 리눅스 커널의 소스파일들.....................................................................9

그림 3.1 임베디드 리눅스 실습환경...................................................................17

그림 3.2 ........................................................................................................20

그림 3.3 ........................................................................................................21

그림 3.4 ........................................................................................................22

그림 3.5 ........................................................................................................23

그림 3.6 ........................................................................................................24

그림 3.7 ........................................................................................................25

그림 3.8 ........................................................................................................26

그림 3.9 ........................................................................................................27

그림 3.10 ......................................................................................................28

그림 3.11 ......................................................................................................29

그림 3.12 ......................................................................................................30

그림 3.13 lilo.conf 파일 수정 ...........................................................................32

그림 4.1 uCsimm............................................................................................50

임베디드 리눅스 프로그래밍 6

임베디드 리눅스 프로그래밍 7

1 리눅스(Linux) 개요

리눅스(Linux)란 유닉스(Unix)에 뿌리를 둔 운영체제 이다. 헬싱키 대학생이던 리누스 토발

즈(Linus Torvalds)가 유닉스 커널에 기반하여 리눅스 커널을 개발을 시작한 후 인터넷에 소

스 코드(source code)를 공개함으로써 많은 사람들에 의해 개선, 보완되고 폭넓게 보급되어

가는 운영체제 이다. 리눅스는 GNU의 GPL(General Public License)을 따르는 무료 소프트

웨어이기 때문에, 연구, 교육용 뿐만 아니라 상업용으로도 빠르게 확산되어 가고 있다.

1.1 기능의 특징

리눅스는 유닉스와 유사한 기능을 가지는 멀티 유저(multi-user), 멀티 태스킹(multi-

tasking) 운영체제이다. 유닉스와 같이 커널(kernel)과 유틸리티(utility) 프로그램으로 구성되

며 다음과 같은 기능적 특징을 지닌다.

-다중 태스킹 (multi-tasking)

-다중 사용자 (multi-user) 접속 지원

-페이징 (paging) 방식의 가상 메모리

-페이지 결함을 이용한 수행 중 파일 적재(demand load executables)

-동적 캐쉬 (dynamic cache for hard disk)

-공유 라이브러리 (shared libraries)

-POSIX 1003.1 호환

-다양한 수행 이미지 (various formats for executable files)

-386 보호 모드 (protected mode)

-부동 소수점 연산 에뮬레이션 (emulation maths co-processor in the kernel)

-다중 키보드와 폰트 지원 (support for national keyboards and fonts)

-다양한 파일 시스템 (ext2, journaling, msdos..)

-다양한 통신 프로토콜 (TCP/IP, SLIP, PPP)

-BSD 소켓 (socket) 지원

-System V IPC 지원

-가상 콘솔 (Virtual Console)

-다중 프로세서 (multi-processor)

-다중 쓰레드 (multi-thread)

-다양한 CPU 지원 (80*86, sparc, arm, ppc, alpha, smp)

1.2 커널 기능 구조

임베디드 리눅스 프로그래밍 8

그림 1.1 리눅스 커널의 기능 구조

그림 1.1 은 리눅스 커널의 내부를 논리적인 구성 요소로 구분하여 표시한 것이다. 리눅스

커널은 크게 태스크 관리자, 메모리 관리자, 파일 시스템, 네트웍 관리자, 그리고 디바이스

드라이버의 5 가지 부분으로 구성된다.

태스크 관리자는 태스크의 생성, 실행, 상태 전이(state transition), 스케줄링, 시그널 처리,

프로세스간 통신 (inter process communication), 모듈 등의 서비스를 제공한다.

메모리 관리자는 가상 메모리, 주소 변환(address translation), 페이지 부재 결함 처리 등의

서비스를 제공한다.

파일 시스템은 파일의 생성, 접근 제어, inode 관리, 디렉토리 관리, 수퍼 블록 관리, 버퍼

캐쉬 관리 등의 서비스를 제공한다.

네트웍 관리자는 소켓(socket) 인터페이스, TCP/IP 같은 통신 프로토콜 등의 서비스를

제공한다.

디바이스 드라이버는 디스크나 터미널, CD, 네트웍 카드 등과 같은 주변 장치를 구동하는

드라이버들로 구성된다.

임베디드 리눅스 프로그래밍 9

시스템 호출(system call)은 커널이 사용자 수준 응용에게 서비스를 제공하는 인터페이스이

다. 태스크 관리자가 제공하는 서비스와 관련된 시스템 호출에는 fork(), execve(), getpid(),

signal() 등이 있으며, 파일 시스템이 제공하는 서비스와 관련된 시스템 호출에는 open(),

read(), write() 등이 있다. 메모리 관리자가 제공하는 시스템 호출에는 오직 하나 brk() 가

있으며 네트웍 관리 부분과 관련된 시스템 호출에는 socket(), bind(), connect() 등이 있다.

디바이스 드라이버가 제공하는 시스템 호출은 없다. 그 이유는 리눅스 커널에서 디바이스

드라이버는 사용자 수준 응용에 의해 직접 호출되지 않고 대신 파일 시스템을 거쳐 디바이

스 드라이버의 서비스가 호출되기 때문이다.

1.3 소스 파일 구조

/usr/src/linux

Doc

arch

includeinitfs

kernel ipc lib mm

net

scripts

driver

alpha

arm

m68k

mips

ppc

sparc

i386

boot

kernel

lib

math-emu

mm

coda

ext2

hpfs

msdos

nfs

isofs

...

minix

asm-alpha

asm-arm

asm-i386

...

linux

net

scsi

video

802

appletalk

decnet

ethernet

ipv4

unix

sunrpc

x25...

block

cdrom

char

net

pci

pnp

sbus

scsi

...

sound

video

/usr/src/linux

Doc

arch

includeinitfs

kernel ipc lib mm

net

scripts

driver

alpha

arm

m68k

mips

ppc

sparc

i386

boot

kernel

lib

math-emu

mm

coda

ext2

hpfs

msdos

nfs

isofs

...

minix

asm-alpha

asm-arm

asm-i386

...

linux

net

scsi

video

802

appletalk

decnet

ethernet

ipv4

unix

sunrpc

x25...

block

cdrom

char

net

pci

pnp

sbus

scsi

...

sound

video

그림 1.2 리눅스 커널의 소스파일들

위 그림은 리눅스 커널 버전 2.2.13 를 기반으로 /usr/src/linux 에 있는 리눅스 커널의 소스

트리 구조를 도시한 것이다.

arch

arch는 리눅스 커널 기능 중 하드웨어 종속적인 부분들을 구현한 소스 코드를 갖는 디렉토

리이다. 그래서 이름도 architecture를 의미하는 arch이다. 이 디렉토리는 CPU의 타입에 따

라 하위 디렉토리로 다시 구분된다. 대표적인 하위 디렉토리에는 인텔 프로세서를 구현한

arch/i386 디렉토리, 64비트 알파 AXP 아키텍처를 구현한 arch/alpha 디렉토리, 저전력 고

성능의 32비트 ARM(Advanced RISC Architecture) 프로세서를 구현한 arch/arm 디렉토리,

임베디드 리눅스 프로그래밍 10

모토로라 68000 계열 프로세서를 구현한 arch/m68k 디렉토리, Sun Sparc 프로세서를 구현

한 arch/sparc 디렉토리, Power PC 프로세서를 구현한 arch/ppc 디렉토리 등이 있다.

각 아키텍쳐 별로 다시 서브디렉토리들이 존재한다. 예를 들어 인텔 프로세서의 경우,

arch/i386 디렉토리는 5개의 하위 디렉토리로 구분되어 있다. arch/i386/boot 디렉토리에는

시스템의 초기화 때 사용하는 부트스트랩 코드가 구현되어 있으며, 커널 소스를 컴파일하면

그 결과로 만들어지는 리눅스 커널인 vmlinuz 파일도 여기에 생성된다. 한편

arch/i386/kernel에는 태스크 관리자 중에서 문맥 교환이나 쓰레드 관리 같은 하드웨어 종

속적인 부분이 구현되어 있으며, arch/i386/mm에는 메모리 관리자 중에서 페이지 부재 결

함 처리 같은 하드웨어 종속적인 부분이 구현되어 있다. arch/i386/lib에는 커널이 사용하는

라이브러리 함수가 구현되어 있으며, arch/i386/math-emu에는 FPU(Floating Point Unit)에

대한 에뮬레이터가 구현되어 있다.

init

init은 커널 초기화 부분, 즉 커널의 메인 시작 함수가 구현된 디렉토리이다. 시스템 초기화

과정을 간단하게 요약하면 다음과 같다. 시스템에 전원이 켜지면 CMOS가 수행된다. 이 루

틴은 하드 디스크의 구조(geometry)를 파악하고, arch/i386/boot/ 디렉토리의 부트스트랩

코드를 메모리에 적재하여 제어를 이곳으로 넘긴다. 그러면 부트스트랩 코드는 루트 파일

시스템의 구조를 확인하고 리눅스 커널을 메모리에 적재하여 제어를 이곳으로 넘긴다. 커널

의 시작 위치는 물리 메모리의 정해진 영역에 적재되도록 구현되었기 때문에 부트스트랩 코

드는 단지 약속된 그 주소로 분기하는 것 만으로 커널로 제어를 넘길 수 있다. 그러면

arch/i386/kernel/head.S 파일에 구현된 startup_32라는 이름의 함수가 수행된다. 이 함수는

메모리 초기화나 인터럽트 초기화, 그리고 SMP 관련 초기화 등 하드웨어 종속적인 초기화

를 수행한 후, 커널의 메인 시작 함수를 호출한다. 그러면 이 디렉토리에 구현된

start_kernel() 함수가 수행되는데 이 함수는 디바이스 드라이버 초기화, 커널 내부 자료 구

조 할당 및 초기화, 태스크 0 (리눅스에서 이 태스크는 idle 태스크이다)과 태스크 1(init, 초

기화 태스크) 생성 등을 수행한다. 이후 init 태스크가 초기화를 담당하며 kflushd 데몬,

syslogd 데몬, inetd 데몬 등의 데몬 태스크의 생성, 파일 시스템 마운트 및 초기화, 터미널

초기화, 네트웍 초기화, login 태스크 생성 등의 리눅스 시스템 초기화를 수행한다.

include

include 서브디렉토리는 커널 코드를 빌드하는데 필요한 모든 인클루드(include) 파 일들의

대부분을 가지고 있다. 여기에는 지원하는 아키텍쳐별로 하나씩 서브디렉토리가 있다.

/include/asm 서브디렉토리는 현재 아키텍쳐에 필요한 실제 디렉토리로 (예를 들어,

include/asm-i386) 소프트 링크되어 있다. 아키텍쳐를 다른 것으로 바꾸려면 커널 makefile

을 수정하고 리눅스 커널 환경설정 프로그램으로 돌아와야 한다. include는 리눅스 커널이

사용하는 헤더 파일들이 구현된 디렉토리이다. 헤더 파일 중에서 하드웨어 독립적인 부분은

임베디드 리눅스 프로그래밍 11

include/linux 하위 디렉토리에 구현되어 있으며, 하드웨어 종속적인 부분은 프로세서 이름

으로 구성된 하위 디렉토리에 구현되어 있다. 예를 들어 인텔 프로세서일 경우

include/asm-i386 라는 디렉토리에 헤더 파일들이 구현되어 있다.

mm

mm은 메모리 관리자가 구현된 디렉토리이다. 가상 메모리(virtual memory), 태스크마다 할

당되는 메모리 객체(mm struct, vm_area_struct, pgd) 관리, 커널 메모리 할당자(kernel

memory allocation) 등의 기능이 구현되어 있다. 페이지 부재 결함 처리(page fault handler)

나 주소 변환 같은 하드웨어 종속적인 메모리 관리 부분은 arch/i386/mm 디렉토리에 구현

되어 있다.

drivers

driver는 리눅스에서 지원하는 디바이스 드라이버가 구현된 디렉토리이다. 디바이스 드라이

버란 디스크, 터미널, 네트웍 카드 등 주변 장치를 추상화시키고 관리하는 커널 구성 요소

이다. 리눅스에서 디바이스 드라이버는 크게 블록(block) 디바이스 드라이버, 문자

(character) 디바이스 드라이버, 네트웍 디바이스 드라이버로 구분된다. 버퍼 캐쉬를 통해

접근되며 임의 접근(random access)이 가능한 블록 디바이스 드라이버는 driver/block이라

는 이름의 하위 디렉토리에 구현되어 있다. 예를 들어 대표적인 블록 디바이스 드라이버인

IDE 디스크의 경우 driver/block/hd.c 파일에 구현되어 있다. 반면에 순차적으로 접근되는

문자 디바이스 드라이버는 driver/char 하위 디렉토리에 구현되어 있다. 예를 들어 대표적인

문자 디바이스 드라이버인 터미널의 경우 driver/char/tty_io.c 파일에 구현되어 있다. 한편

3c509 같은 네트웍 카드를 위한 드라이버는 driver/net 하위 디렉토리에 구현되어 있다. 그

외 cdrom, sound, video 카드를 위한 드라이버는 각각 driver/cdrom, driver/sound,

driver/video라는 이름의 하위 디렉토리에 구현되어 있으며 driver/pnp에는 PnP(Plug and

Play)가 가능한 디바이스에 대한 드라이버가 구현되어 있다. 한편 PCI, SCSI, sbus 등 다양

한 버스 구조 및 통신 규약은 각각 driver/pci, driver/scsi, driver/sbus 등의 디렉토리에 구

현되어 있다.

ipc

ipc 는 리눅스 커널이 지원하는 프로세스간 통신(inter process communication) 기능이

구현된 디렉토리이다. 대표적인 프로세스간 통신에는 파이프, 시그널, SVR4 IPC, 소켓 등이

있으며, 이 디렉토리에는 SVR4 IPC 인 메시지 패싱(message passing), 공유 메모리(shared

memory), 그리고 세마포어(semaphore)가 구현되어 있다. 한편 파이프는 fs 디렉토리에,

시그널은 kernel 디렉토리에, 그리고 소켓은 net 디렉토리에 구현되어 있다.

modules

임베디드 리눅스 프로그래밍 12

이 디렉토리는 컴파일 된 모듈 함수들을 저장하기 위한 디렉토리이다.

fs

fs는 리눅스에서 지원하는 다양한 파일 시스템들과 open(), read(), write() 등의 시스템 호

출이 구현된 디렉토리이다. 현재 리눅스에는 15∼16개 정도의 파일 시스템이 구현되어 있

으며 계속 새로운 파일 시스템이 개발 중이다. 각 파일 시스템은 이 디렉토리의 하위 디렉

토리에 구현되어 있는데, 대표적인 파일 시스템으로는 ext2, nfs ,ufs, msdos, ntfs, proc,

coda 등이 있다. 한편 다양한 파일 시스템들을 사용자가 일관된 인터페이스로 접근할 수

있도록 하기 위하여 리눅스는 시스템 호출과 각 파일 시스템 사이에 추상화 된 한 층을 넣

었으며 이것이 VFS (Virtual File system)이다.

kernel

kernel은 태스크 관리자가 구현된 디렉토리이다. 태스크의 생성과 소멸, 프로그램의 실행,

스케줄링, 시그널 처리 등의 기능이 이 디렉토리에 구현되어 있다. 한편 문맥 교환(context

switch)과 같은 하드웨어 종속적인 태스크 관리 부분은 arch/i386/kernel 디렉토리에 구현되

어 있다.

net

net는 리눅스에서 지원하는 통신 프로토콜이 구현된 디렉토리이다. 현재 리눅스는 대표적인

통신 프로토콜인 TCP/IP 뿐만 아니라 UNIX 도메인 통신 프로토콜, 대표적인 WAN 통신 프

로토콜인 X.25, 802 통신 프로토콜, Novel에서 제안한 통신 프로토콜인 IPX, SUN

RPC(Remote Procedure Call), AppleTalk 등을 구현하였다. 각 통신 규약은 이 디렉토리의

하위 디렉토리에 구현되어 있다. 즉 위에서 기술한 통신 프로토콜들은 각각 net/ipv4 (or

ipv6), net/unix, net/x25, net/802, net/decnet, net/sunrpc, 그리고 net/appletalk 등의 하위

디렉토리에 구현되어 있다. 한편 다양한 통신 프로토콜의 추상화 층이며 사용자 인터페이스

를 제공하는 소켓(socket)은 net/ 디렉토리에 구현되어 있다. (LAN 통신 프로토콜로 예를

들어 802.2일 경우 CSMA/CD, 802.3일 경우 Token Bus 등을 의미한다.)

lib

이 디렉토리는 커널의 라이브러리 코드를 가지고 있다. 아키텍쳐 종속적인 라이브러리 코드

는 arch/*/lib/에 있다.

scripts

이 디렉토리는 커널을 설정하는데 사용되는 스크립트(예를 들어 awk나 tlk 스크립 트)를 가

지고 있다. 이 스크립트들은 커널 구성 및 컴파일 할 때 이용된다.

임베디드 리눅스 프로그래밍 13

doc

이 디렉토리에는 리눅스 커널 및 명령어들에 대한 자세한 문서 파일들이 존재한다.

1.4 Linux 의 활용

리눅스의 활용분야는 점점 확대되어 가고 있다. 소규모 워크스테이션 시스템으로부터 대형

데이터베이스 호스트, 메인 프레임 서버의 운영체제로 사용되다가 최근 개인용 컴퓨터나

휴대형 단말기와 같은 임베디드 시스템에 채택되고 있다. 리눅스를 지원하는 상용 프로그램

들의 수도 급격히 늘어나고 있는 추세에 있다. 지금까지는 운영체제와 그 외 주변 유틸리티

또는 GNU 프로그램, 공개 프로젝트 프로그램이 대부분이었지만 최근 수년간 많은 업체에서

나온 상용 소프트웨어들은 리눅스 버전을 가지고 있다.

한편 인터넷의 확신과 이동 통신의 발전으로 인터넷과 연동하는 지능형 정보가전 기기 그리

고 Handheld PC, PDA 와 같은 모바일 디바이스의 수요가 증가 추세에 있다. 이런 정보가

전 기기나 휴대형 디바이스는 메모리나 프로세서 등과 같은 컴퓨팅 하드웨어 용량이 제한적

이기 때문에, 운영체제도 불필요한 기능은 없애 버린 경량, 소형으로 설계, 구현되어야 한다.

임베디드 디바이스용으로 특수화된 리눅스를 임베디드 리눅스라고 부르며 최근 임베디드 리

눅스 개발이 많은 연구가 진행되고 있다.

본 교재에서도 리눅스 운영체제를 경량, 단순화한 임베디드 버전 리눅스를 대상으로 학생들

이 운영체제의 기능을 학습하고 내부 커널 코드를 이해하며, 임베디드 리눅스를 직접 수정

하고 대상 임베디드 시스템 하드웨어에 탑재하는 실습을 다룬다. 아울러 커널 상에서의 시

스템 프로그래밍을 실습하게 함으로써 리눅스에 대한 활용 능력을 키우게 한다.

임베디드 리눅스 프로그래밍 14

2 임베디드 리눅스 개요

2.1 임베디드 시스템과 리눅스

임베디드 시스템이란 우리 생활에서 쓰이는 각종 전자기기, 가전제품, 제어장치가 단순히

회로로만 구성된 것이 아니라 마이크로 프로세서가 내장되어 있고, 그 마이크로 시스템을

구동하여 특정한 기능을 수행하도록 프로그램이 내장되어 있는 시스템을 가리킨다. 임베디

드 시스템은 다양한 응용분야를 가지고 있다. 산업분야, 가전분야, 사무분야, 군사용 등 임

베디드 시스템은 다양한 응용분야에 적용되고 있다. 또한 구체적 적용 사례로는 핸드폰,

PDA, 사이버 아파트의 홈 관리 시스템, 홈 네트워크 게이트웨이 장치, 교통관리 시스템, 주

차 관리시스템, 홈 관리 시스템, 엘리베이터 시스템, 현금지급기(ATM), 항공 관제 시스템,

우주선 제어 장치, 군사용 제어 장치 등을 들 수 있다.

이러한 임베디드 시스템은 시간이 흐를수록 기능이 다양해지고 시스템의 크기가 날로 커져

가서 임베디드 시스템을 운용하기 위해서는 이를 적절히 통제할 만한 운영체제가 필요하게

되었으며, 운영체제의 성능에 따라 시스템의 성능 및 확장성 등에 지대한 영향을 미친다.

임베디드 시스템에서의 운영 체제는 시스템의 규모가 커짐에 따라서 멀티태스킹과 같은 복

잡한 기능을 요하며, TCP/IP, GUI, 오디오, 비디오 등 네트워크나 멀티미디어가 시스템의 기

본으로 자리 잡고 있다. 임베디드 시스템이 해야 할 일이 많아지고 복잡해짐으로 인해서 단

순히 순차적으로 운영되는 프로그램으로는 시스템을 운영하기가 어렵게 됨으로써 임베디드

시스템에도 운영체제의 개념이 필요하게 되었으며, 임베디드 시스템의 특성상 실시간이라는

요소 또한 중요한 요소로 자리잡고 있으며 이를 만족시켜야만 하는 시스템도 늘어나고 있는

추세이다.

임베디드 시스템을 운용하기 위해 WinCE, pSOS, VxWorks, Palm OS 등 다양한 운영체제가

사용되고 있다. 이러한 OS 중에 하나로 리눅스도 자리 매김을 하고 있다. 이제는 임베디드

리눅스가 학술 연구용의 범위를 넘어 산업체에서도 채택하는 실용 운영체제로서 폭넓게 그

리고 빠르게 확산되는 추세이다.

임베디드 시스템용 리눅스, 즉 임베디드 리눅스의 특징은 일반 리눅스의 특징과 같다. 앞에

서 소개한 리눅스의 특징, 즉

오픈 소스 -> 새 소프트웨어 연구, 개발을 촉진 -> 운용체제 확장 가능

로열티(royalty)가 없음 -> 가격 경쟁력

오류가 포함될 가능성이 적다 -> 안정적

다양한 서비스 제공 -> 품질 경쟁력

현존하는 거의 모든 프로세서와 시스템을 수용

임베디드 리눅스 프로그래밍 15

다양한 응용 -> 성장 가능성

점이 있다. 반면에 리눅스의 단점으로는

개발환경이 어렵고,                  .

완전히 검증되지 않았으며

특정 OS에 비해서 웹 브라우저와 멀티미디어 구현 능력이 떨어진다는 점이다.

앞에서 소개한 바와 같이 임베디드 리눅스는 리눅스 운영체제를 임베디드 시스템 하드웨어

에 맞게 특성화된 소형 리눅스이다. 범용 서버용이 아니기 때문에 일반적으로

멀티 유저 관리(multi-user management) 기능

메모리 관리(memory anagement) 기능

하드디스크 관리(hard disk management) 기능

protection 기능

등을 제외한 운영체제이지만, 일반적인 운영체제의 기능들 중 어느 것을 제외하는가는 임베

디드 시스템 용도나 하드웨어 특성에 따라 좌우된다. 임베디드 리눅스 커널의 구조는 마이

크로 커널(micro kernel) 구조를 따르는 것이 일반적이다.

2.2 uClinux

uClinux 는 리눅스 2.0 커널에 기반하여 마이크로컨트롤러(microcontroller) 용으로 개발된

임베디드 리눅스 커널이다. uClinux 는 메모리 관리 장치(MMU)를 가지지 않은 하드웨어 용

으로 개발된 것이다. uClinux가 MMU 프로세서를 지원하지 않기 때문에 멀티태스킹 실행에

는 적합하지 않다. 그러나 임베디드 시스템에서 실행되는 응용 프로그램들은 보통 멀티태스

킹 될 필요가 없는 단순한 것들이기 때문에 문제가 되지 않는다. 멀티태스킹을 지원하지 않

는 대신에, 커널 코드가 단순해 지는 장점이 있다. 따라서 uClinux는 일반 리눅스 커널보다

훨씬 크기가 적게 되지만 기타 리눅스의 장점, 즉 안정성, 뛰어난 네트웍 기능, 우수한 파일

시스템 지원 등의 장점을 가진다.

• 일반적인 Linux API

• uCkernel (uClinux의 커널)의 크기 < 512 kb

• uCkernel + tools < 900 kb

uClinux 는 TCP/IP 프로토콜 뿐만 아니라 기타 여러 네트웍 프로토콜도 스택을 가지고 있

다. 따라서 uClinux가 탑재될 임베디드 시스템은 인터넷을 액세스하는데 필요한 모든 기능

을 갖추고 있다. 또한 uClinux는 nfs, ext2, MS-DOS, 그리고 FAT16/32 를 지원한다.

uClinux의 공식 배포 CD에는 uClinux의 소스 코드와 바이너리 코드가 RPM(Redhat

임베디드 리눅스 프로그래밍 16

Package Manager) 형식으로 들어 있으며 쉽게 인스톨할 수 있다. 또한 소스 코드 tarballs

와 데모 버전 Eagle CAD, 성능 테스트 결과치, 그리고 SuSE 5.3 배포판 등 uClinux를 개발

시스템 워크스테이션에서 개발하는데 필요한 소프트웨어가 들어 있다.

임베디드 리눅스 프로그래밍 17

3 임베디드 리눅스 실습환경

3.1 실습환경 개요

임베디드 리눅스 실습환경은 아래 그림 3.1과 같이 세 부분으로 구성된다.

1. 개발시스템

일반적인 리눅스 워크스테이션, 또는 PC로서 시리얼 포트, 터미널 에뮬레이터(minicom),

이더넷카드, CD ROM 등을 가져야 한다.

2. 타겟시스템, 즉 임베디드 시스템

마이크로프로세서 모듈(uc68EZ328 Single Inline Microcontroller Module), 이더넷 컨트롤러,

시리얼 포트 등의 하드웨어에 OS로서 임베디드 리눅스인 uClinux가 탑재된다.

3. 네트웍

개발 시스템과 임베디드 시스템을 연결하기 위해 RS232 시리얼 연결이나 10Mbps 이더넷

으로 구성된다.

타겟시스템

Ethernet Switch

10 Mbps Ethernet 연결

개발시스템 Serial 연결

그림 3.1 임베디드 리눅스 실습환경

먼저 타겟시스템을 살펴보자. 본 교재에서 타겟시스템은 uCsimm인데 이것은 이더넷 컨트

롤러, 시리얼 포트를 가지고 있고 운영체제로 uClinux를 사용한다. uCsimm과 함께 사용되

는 것으로 uCgardener가 있는데 이것은 uCsimm을 위한 소켓을 제공하고 개발자가

uCsimm의 리소스를 사용할 수 있도록 다음과 같은 주변장치를 지원한다.

Simm socket

RJ45 ethernet jack

DB9 serial connector

Power jack, on-board voltage regulator

임베디드 리눅스 프로그래밍 18

개발 환경의 두번째 요소는 개발 시스템인 워크스테이션이다. 이것은 일반적인 리눅스 시스

템이면 되는데 다음과 같은

시리얼 포트

터미널 에뮬레이터(예: minicom : 대부분의 리눅스에 기본적으로 포함되어있다.)

이더넷 카드

CD ROM

기본적인 기능을 갖춘 리눅스가 개발 시스템이 된다. CD ROM은 uClinux System Builder Kit

을 설치하는데 필요하다. 이 CD에는 툴체인이라고 하는 개발툴이 포함되어있다. 일반적으

로 워크스테이션은 인텔 CPU를 사용하고 타켓시스템은 모토롤라의 DragonBallEZ MCU를

사용한다. 그렇기 때문에 툴체인은 크로스 컴파일 툴을 가지고 있다.

개발 환경의 세번째 요소는 시리얼 연결이다. 워크스테이션에서 실행되는 터미널 에뮬레이

터(minicom)는 타겟시스템의 터미널이 된다.

“minicom” 이라고 불리우는 터미널 이뮬레이터가 개발 시스템에서 실행되면, 개발 시스템

은 시리얼 연결을 통해 임베디드 시스템의 터미널 역할을 한다. 전원을 켜거나 uCgardener

의 reset 버튼을 누를 경우, uCsimm은 부트로더 프로그램을 실행시킨다. 부트로더는 다음

과 같은 커맨드들을 가진 시스템 모니터 프로그램이다.

① 새로운 uClinux 이미지를 개발 시스템에서 타겟시스템 RAM으로 업로드시키는

커맨드

② RAM 이미지를 Flash ROM으로 로드시키는 커맨드

③ Flash ROM에 저장된 uClinux 이미지로 타겟시스템을 부팅시키는 커맨드

④ RAM 에 저장된 uClinux 이미지로 타겟시스템을 부팅시키는 커맨드

uClinux 부팅이 끝나면 login prompt 가 나타난다. 이때부터 개발 시스템이 리눅스 터미널

로 동작하게 된다.

한편 uClinux 부팅과 함께 Ethernet 포트도 이용가능 상태가 된다. 개발 시스템의 한 디렉

토리를 타겟시스템 uClinux의 파일 시스템과 nfs 마운트시키면 개발 시스템의 그 디렉토리

를 마치 타겟시스템에 있는 작업공간처럼 이용할 수 있게 된다. 이렇게 하면 개발 시스템에

서 타겟시스템으로 새로운 uClinux 이미지를 보낼 때, 상대적으로 느린 serial 연결

(minicom)을 이용하지 않고, 보다 빠른 flashloader 커맨드를 이용할 수 있다. 이 커맨드는

마운트된 디렉토리에 있는 새로운 uClinux 이미지를 uCsimm 메모리로 Ethernet을 통해 신

속하게 이동시킨다. UCsimm 메모리로 새 이미지가 이동되면 Flash ROM에 로드시키고 다

시 부팅시키면 된다. 물론 uClinux 이미지 뿐만 아니라 기타 프로그램들도 마운트된 작업공

간늘 통해 신속히 타겟시스템으로 옮길 수 있기 때문에, 임베디드 프로그램 개발자들에게

매우 편리한 수단이다.

개발 시스템과 타겟시스템 간 Ethernet 연결이 매우 편리하기는 하지만 Ethernet 연결은

임베디드 리눅스 프로그래밍 19

uClinux가 타겟시스템에서 부팅된 후에야 쓸 수 있다는 점에 유의해야 한다. 새로 수정된

uClinux 이미지에 오류가 있어서 부팅이 실패할 경우에는 serial 연결이 유일한 통신수단이

된다. 이런 점에서 uClinux 이미지를 수정하고 타겟시스템에 옮겼을 때 무조건 Flash ROM

에 로드하고 ROM에서 부팅시키는 것(위 ③번 커맨드 이용) 보다는, 먼저 RAM 메모리 상에

서 부팅시켜 보는 것이 좋다(위 ④번 커맨드 이용). 이렇게 할 경우 만일 RAM에서 부팅된

uClinux 이미지로 부팅이 실패한다고 할 지라도 Flash ROM에 저장된 온전한 uClinux 이미

지는 영향을 받지 않기 때문에 ROM에 있는 이미지로 시스템을 부팅시킬 수 있어서 안전하

다.

3.2 개발 시스템용 리눅스 설치

개발 시스템용 리눅스는 어느 배포판에 관계없이 사용 가능하다. uClinux의 커널은 리눅스

커널 2.0.38을 기반으로 작성되었기 때문에 2.x 버전의 리눅스는 어느 것이나 좋다. 그러나

개발 시스템용 커널 버전이 uClinux 와 동일하면 좀 더 안정적이다. 본 교재에서는 레드햇

리눅스 6.2나 레드햇의 한글 배포판인 와우 리눅스 6.2에 맞춰서 진행해 나갈 것이다. 이

들 배포판은 무료로 제공되는 CD를 통해서 구할 수도 있고, 인터넷에 접속할 수 있다면

http://www.linux.co.kr에서 여러가지 종류의 배포판을 다운로드할 수 있다. 물론 배포판의

홈페이지에 가면 해당 배포판을 무료로 다운로드 할 수 있다.

레드햇 리눅스 6.2 CD를 이용해 리눅스 설치를 실습하자. 최근의 PC들은 기본적으로 CD

ROM 부팅이 가능하므로 이를 이용해서 설치하는 것이 가장 간편하다. 만일 여러분의 PC가

CD ROM 부팅을 지원하지 않는다면 플로피 디스켓으로 부팅한 후 CD ROM으로 설치가 가

능하다. 각각의 경우를 살펴보자.

1. CD ROM으로 부팅할 경우(권장)

A. 컴퓨터의 바이어스 설정에서 첫번째 부팅 장치를 CD ROM으로 변경

한다.

B. CD ROM에 레드햇 리눅스 CD를 넣고 컴퓨터를 재부팅한다.

C. 컴퓨터가 재부팅하고 그림 3.2과 같은 리눅스의 설치 화면이 뜨면 CD

로 설치할 수 있다.

임베디드 리눅스 프로그래밍 20

그림 3.2

2. 플로피 디스켓으로 설치하기

A. 레드햇 리눅스 CD를 탐색해 보면 ₩images 디렉토리가 보일 것이다.

여기에서 ‘boot.img’ 와 같은 여러가지 이미지 파일이 있는데 이 이미

지가 설치에 사용된다.

B. 같은 CD ROM에 ₩dosutils 디렉토리에 있는 ‘rawrite.exe’ 파일을 실

행한다. rawrite.exe 프로그램은 우선 사용할 디스크 이미지의 이름을

묻는다. 이때 ₩images₩boot.img라고 입력한다.

C. 그 다음에는 대상이 되는 플로피 드라이브를 묻는데 자신의 플로피 드

라이브 명을 적어준다. A: 또는 B:이다.

D. 컴퓨터의 바이어스 설정에서 첫번째 부팅 장치를 플로피 드라이브로

변경한다.

E. 플로피 드라이브에 위에서 만든 부팅 디스켓을 CD ROM에는 레드햇

리눅스 CD를 넣고 컴퓨터를 재부팅한다.

F. 컴퓨터가 재부팅하고 그림 3.2과 같은 리눅스의 설치 화면이 뜨면 이

제부터는 CD로 설치할 수 있다.

임베디드 리눅스 프로그래밍 21

그림 3.3

CD ROM으로 부팅하던지 플로피 디스켓으로 부팅하던지 일단 그림 3.2와 같은 화면과 같이

부팅하면 그 이후의 과정은 동일하다.

boot: 프롬프트에서는 커널에 대한 매개 변수를 지정할 수 있는데 설치 시에는 필요치 않으

므로 그냥 <Enter>키를 누르고 넘어가면 곧바로 설치가 시작된다.

3.2.1 커널 구성(configuration)

커널 구성이란 새로 만들어질 리눅스 커널에게 현재 시스템에 존재하는 하드웨어 특성, 커

널 구성 요소, 네트웍 특성 등의 정보를 알려주는 과정이며 이 전 절에서 설명한 바 있다.

보통 이 과정은 매우 복잡하며 자신이 가지고 있는 시스템의 하드웨어 정보들에 대한 사전

지식이 필요하다. 커널 구성은 “make config”라는 명령으로 수행될 수 있으며, 좀 더 편한

커널 구성을 위한 명령으로 “make menuconfig”나 “make xconfig” 등이 있다.

가장 먼저 묻는 질문은 설치하는 동안 사용할 언어인데 이때 한국어를 선택한다. 설치는 이

와 같은 간단한 질문을 통해서 이루어진다. 이와 같은 기본적인 질문에서는 디폴트 값을 사

용해도 큰 무리는 없다. 언어선택 다음으로는 키보드와 마우스를 선택하게 되는데 역시 디

폴트 값으로 한다.

임베디드 리눅스 프로그래밍 22

그림 3.4

설치유형으로 ‘Custom’을 선택한다. PC 전체를 리눅스로 사용한다면 다른 것을 선택할 수

도 있지만 ‘Custom’은 모든 것을 포함하기 때문에 이 경우에도 적합하다.

다음 질문은 파티션 프로그램을 선택하게 되는데 Fdisk와 디스크 드루이드(Disk Druid)이다.

Fdisk는 텍스트 기반 프로그램이기 때문에 직관적으로 사용하기 어렵지만 매우 안정적인 프

로그램이다. DOS나 Win98의 Fdisk와 사용법도 동일하다. 디스크 드루이드는 레드햇 리눅스

의 파티션 프로그램으로 그래픽 기반이기 때문에 사용하기 편리하다. 본 교재에서는 디스크

드루이드를 사용하며 Fdisk를 사용하고자 하는 독자는 별도의 리눅스 교재를 참고한다.

임베디드 리눅스 프로그래밍 23

그림 3.5

리눅스 시스템은 하드디스크의 여러 파티션으로 나누어 인스톨할 때 가장 잘 동작한다. 이

파티션들은 파일을 저장하거나 메모리 영역을 저장하는데 사용된다. 메모리를 저장하는데

사용하는 파티션은 스왑 파티션(swap partition)이라고 한다. 리눅스 시스템이 물리적 메모

리보다 많은 메모리를 사용할 때는 비활성화된 프로그램은 디스크에 저장하고 이 공간을 사

용한다.

디스크 분할의 궁극적 목표는 사용자 영역을 운영체제로부터 분리하고 로그파일, 런타임 데

이터와 스풀 파일들이 사용하는 영역이 기본 운영체제가 사용하는 영역과 분리되었음을 보

증하는 것이다.

디스크 분할의 가장 간단한 방법은 처음에 세 개의 파티션을 만드는 것이다. 몇 개의 파티

션을 나눌 것인가는 각 서버에 따라 다르다. Win98이나 Win2000을 사용하고 있다면 파티

션 정보에 해당하는 파티션이 있을 것이다. 어떤 경우에나 디스크의 남은 공간이 설치하는

양보다 많으면 이와 설정이 달라도 상관없다. 공간이 부족하면 다른 파티션을 지우거나 전

체 파티션을 다 지워서 리눅스용으로만 사용하는 수 밖에 없다. 공간이 충분하다고 가정하

고 아래 표대로 파티션을 분할해 보자.

임베디드 리눅스 프로그래밍 24

/boot 256M Boot Sector must be in the first 1024 cylinders

128M Swap partition

/ 2000M Main System Disk

/home 1000M User work space

이와 같이 디스크를 분할하기 위해서 그림 3.5에서 보이는 것처럼 디스크 드루이드에서 작

업한다. 그림에서는 2G(1998M) 바이트 크기에 아무것도 쓰여지지 않은 깨끗한 디스크이다.

우선 /boot 영역을 파티션으로 나누려면 그림 3.5에서 커서가 위치하고 있는 Add버튼을 누

른다. 그러면 분할을 위한 창이 하나 뜨는데 해당하는 내용을 적어주면 된다. 이 경우에

Mount Point는 /boot, 사이즈는 128, Partition Type은 Linux native로 선택한 후 OK버튼을

누른다. 나머지도 이와 동일한 과정으로 설정하면 된다. 주의 할 점은 스왑 영역을 만들

때 Partition Type을 Linux Swap으로 해주어야 한다. 만약 위와 같이 설정한 후에 디스크

공간이 남는다면 ‘/’ 나 ‘/home’과 같은 많은 공간을 필요로 하는 곳에 할당하면 된다.

다음으로 새로 만든 파티션을 포맷할 것인가를 선택하게 되는데 기본값으로 포맷을 선택한

다.

그림 3.6

시스템에서 사용할 부트 디스켓 만들기와 LILO(Linux Loader) 설정방법을 결정한다. 부트

임베디드 리눅스 프로그래밍 25

디스켓은 돌발 상황이 발생하였을 때 시스템을 부팅하여 손상된 시스템을 복구할 수 있다.

이를 위해서는 플로피 디스켓 1장이 필요하다. LILO는 리눅스 부트로더이다. 도스에서의

멀티부팅과 NT의 부트로더, 그리고 OS/2의 부트로더와 같이 여러 운영체제를 선택적으로

부팅할 수 있게 해준다. 리눅스의 LILO는 하드의 Master Boot Recorder (MBR)와 설치된

파티션의 첫 번째 섹터에 설치할 수 있지만 마스터 부트 레코드로 선택한다. 리눅스만을 사

용한다면 LILO를 설치하지 않아도 된다. 만일 Win98과 같은 다른 운영체제가 미리 설치되

어 있다면 그림 3.6와는 달리 다른 운영체제도 보일 것이다. LILO로 이들 운영체제를 선택

해 부팅할 수 있다.

다음은 네트웍을 설정하는데 설치 중에 모른다면 설치 후에 다시 변경할 수 있다. 그러나

미리 설정해 놓으면 추가적인 작업을 줄일 수 있다. 개발용 리눅스에서 네트웍은 필요하므

로 나중에라도 반드시 설정하도록 한다. 네트웍 설정은 각각의 PC마다 다르므로 각자의 환

경에 맞는 값을 설정하고 모르면 자신이 속한 네트웍의 관리자에게 문의하면 알 수 있다.

그림 3.7

다음은 시계와 시간대를 선택한다. 기본값으로 Asia/seoul이 선택되어 있으므로 이를 선택

한다.

다음은 루트(root) 패스워드를 설정한다. PC에 리눅스를 설치하고 있으므로 기본적으로 여

임베디드 리눅스 프로그래밍 26

러분은 시스템의 기본 관리자인 루트가 된것이다. 설치후 처음으로 리눅스 부팅을 하면 최

종 화면에서 login:프롬프트를 보게 되는데 반드시 로그인 과정을 거쳐 자신의 신분을 증명

해야 한다. 시스템 관리자 계정인 root의 패스워드를 입력한다. 확인을 위해 두 번 입력한

다.

그림 3.8

루트 계정은 반드시 만들어야 하지만 사용자 계정은 선택사항이다. 관리자 본인만 사용하는

시스템이라고 해도 일반 사용자 계정은 필요하니 자신의 계정을 따로 만들어 두는 것이 좋

다.

다음으로 인증방법을 설정한다.

MD5 패스워드 사용 : 리눅스 사용자가 패스워드를 저장할 때 좀 더 강력한 암호화

방식을 사용할 수 있도록 하는 옵션이다.

새도우 패스워드 사용 : 해커나 일반 시스템 사용자가 재미로 사용자 데이터베이스

를 훔쳐 시스템 관리 패스워드를 도용하지 못하도록 설계된 기술이다.

NIS 사용 : 유닉스(Unix) 네트웍를 기반으로 하는 서버가 있는 네트워크에서 가장

일반적으로 사용하는 네트웍 인증이다.

임베디드 리눅스 프로그래밍 27

그림 3.9

개인사용자는 디폴트 값으로 놔두고 다음 화면으로 넘어가도 된다.

다음은 패키지 그룹을 선택한다. 설치할 여러가지 패키지 가운데 개별적으로 선택할 수도

있다. 예를 들어 X 윈도우 시스템(X windows system) 부분을 체크를 해주면 X 윈도우 시스

템에 필요한 컴포넌트들이 자동으로 선택된다.

그러나 개별의 패키지를 선택하기 전에 각각의 패키지에 대해 알아야 하므로 그 역할이 무

엇인지를 알아야 한다. 그리고 본 교재에서 사용하는 패키지가 설치되지 않으면 이것을 설

치 하기 전에는 실습을 진행할 수 없다. 따라서 초보자의 경우에는 화면의 가장 밑으로 이

동해 ' Everything' 을 선택해 모두 설치하는 것이 좋다.

임베디드 리눅스 프로그래밍 28

그림 3.10

다음은 X 윈도우를 설정한다. 본 교재의 실습에서 X 윈도우가 반드시 필요하지는 않다.

오히려 사양이 낮은 PC에 설치하는 것이라면 리눅스의 가상 콘솔 기능을 사용하는 편이 좋

다. 그러나 웬만한 성능이 된다면 윈도우와 같은 운영체제에 익숙해져 있으므로 설치를 하

는 편이 본 교재의 실습 뿐 아니라 다른 리눅스의 기능을 사용할 수 있다.

보통 자동으로 그래픽 카드와 모니터의 종류가 검색되는데 Win98 설치와 비슷하게 자동

으로 검색되면 드라이버도 자동으로 설치한다. 만약 검색되지 않는다면 직접 그래픽 카드와

모니터의 모델을 선택해 주어야 한다. 사용자 정의를 선택한 후 적절한 것을 지정해 주면

된다. 또한 모니터를 설정한 후에는 현재 설정된 내용을 가지고 사용할 수 있는 해상도를

검색하는데 적절한 것을 선택한다. 그래픽 로그인을 체크하면 리눅스 부팅 후 X 윈도우로

바로 로그인 된다. X 윈도우 설정은 리눅스 설치 후에 다시 설정할 수 있다.

임베디드 리눅스 프로그래밍 29

그림 3.11

이제 설치에 필요한 설정은 모두 마치고 실제 하드디스크에 복사하는 일만 남았다. 이 과정

은 패키지를 얼마나 선택했느냐에 따라 다른데 보통 30분 ~ 1시간 정도의 시간이 걸린다.

복사가 완료되면 부팅디스켓을 만드는데 화면에 따라서 디스켓을 드라이브에 넣어주면 된다.

부팅디스켓 만들기를 생략했다면 이 과정 역시 생략된다. 그런 다음 부팅 디스켓과 설치

CD를 꺼내고 재부팅을 하면 새로 설치된 리눅스의 로그인 화면이 뜰 것이다. 앞에서 생성

한 계정으로 로그인하여 리눅스를 사용할 수 있다.

임베디드 리눅스 프로그래밍 30

그림 3.12

3.2.2 커널 컴파일

이미 리눅스를 사용하고 있는 독자 중에 본 교재의 uClinux와 호환되지 않거나 커널을 자신

이 직접 컴파일해서 업그레이드 하고자 하는 독자는 다음과 같이 커널을 직접 컴파일해서

사용할 수 있다.

본 절에서는 리눅스 커널을 어떻게 만드는 지를 설명한다. 일반 프로그램의 실행 파일인

a.out를 만들 때와 마찬가지로 커널 소스들을 gcc로 컴파일해서 리눅스 커널을 만들면 된

다. 사실 a.out이나 커널이나 만드는 방법은 같다. 단지 차이가 있다면 만들 때 커널은 상

당히 많은 파일들을 컴파일해야 한다는 것과 컴파일 결과가 a.out 이라는 이름이 아니라

bzImage 라는 것이 다를 뿐이다. 커널을 만들 때 많은 소스 파일들을 기반으로 컴파일해야

하기 때문에 보통 Makefile과 make 명령어를 이용하게 된다.

비록 a.out과 리눅스 커널이 만들 때에는 같은 방법으로 만들어지지만, 수행될 때에는 매우

다른 방식으로 동작한다. 우선 커널은 메모리에 상주하지만 사용자 수준 응용인 a.out은 필

요할 때 메모리에 적재된다. 한편 a.out은 수행 시 a.out은 사용자 권한(user level)으로 동

작하며, 커널은 사용자 권한보다는 더 강력한 커널 권한(kernel level)으로 동작한다.

구체적으로 리눅스 커널을 만드는 과정은 커널 구성(kernel configuration), 종속관계 확인

임베디드 리눅스 프로그래밍 31

(dependency check), 커널 컴파일(kernel compile), 모듈 인스톨(module install) 등의 과정

으로 구성된다. 커널 구성이 완료되면 커널 소스 파일간에 종속 관계를 확인하는 단계가 필

요하며, 그 다음 단계는 커널 컴파일이다. 이 단계는 커널 소스 파일을 이용해 실행 가능한

커널을 만드는 과정으로 “make bzImage”나 “make bzdisk”라는 명령으로 수행될 수 있다.

커널 컴파일이 끝나면 새로운 커널이 /usr/src/kernel/arch/i386/boot/ 디렉토리에

“bzImage”라는 이름으로 생성된다. 그 다음으로 모듈 인스톨을 수행해야 하는데, 이것은

모듈로 구성된 커널 내부 구성 요소를 알려주고, 이 후 그 구성 요소들이 사용될 때 자동으

로 커널에 적재될 수 있도록 설정하는 것이다.

커널 컴파일 예

/* 커널 소스의 상위 디렉토리로 이동 */

$cd /usr/src/linux/

/* 커널 구성 (configuration) */

$make config (menuconfig, xconfig, oldconfig)

/* 커널 종속 관계 체크 (dependency check) */

$make dep

$make clean

/* 커널 컴파일 */

$make bzImage

/* 실제 커널 소스가 컴파일 됨 */

/* 인스톨 */

$make modules

$make modules_install

$ insmod (modprobe)

커널 컴파일이 완료되어 새로운 커널을 만들었다면, 이제는 새로 만들어진 커널로 시스템을

구성시켜 볼 차례이다. 이 단계는 커널을 루트 파일 시스템으로 복사하는 것과 LILO(Linux

Loader)에게 새로운 커널의 위치를 알리는 것 등으로 구성된다. LILO 에게 새로운 커널의

위치를 알리기 위해서는 새로 만들어진 커널을 루트 파일 시스템에 옮기고 그 위치를

/etc/lilo.conf 파일에 추가해야 한다 (커널을 만들 때 “make zlilo”를 사용하여 이러한

일들을 자동으로 할 수도 있다. 하지만 이 방법은 여러분이 직접 하는 것에 비해 덜

유연하다). 새로 컴파일 된 커널로 시스템 재구동 시키는 방법은 다음의 3 단계로 진행된다.

1. 커널 복사

$ cp /usr/src/linux/arch/i386/boot/bzImage /boot/newvmlinuz

임베디드 리눅스 프로그래밍 32

2. LILO 파일 편집

$ vi /etc/lilo.conf

3. 시스템 리부팅

시스템 부팅 시 LILO의 프롬프트에서 “linux”라고 입력하면 기존의 리눅스 커널인

/boot/vmlinuz로 부팅되고, “newlinux”라고 입력하면 새로 컴파일 된 리눅스 커널인

/boot/vmlinuz-new로 부팅

그림 3.13 는 /etc/lilo.conf 파일의 수정 예이다. 이 예에서는 새로 생성한 커널을 /boot 에

newvmlinuz 라는 이름으로 만든다고 가정하였다.

우선 “cp /usr/src/linux/arch/i386/boot/bzImage /boot/newvmlinuz" 명령으로 커널을 루트

파일 시스템에 복사하고, vi 같은 편집기를 이용해 /etc/lilo.conf 파일을 연다. 슬라이드의

왼쪽 그림은 이 파일의 내용을 도시한 것이다. (물론 시스템 구성에 따라 약간 다를 수도

있다.)

그럼 슬라이드의 오른쪽 그림과 같은 엔트리를 추가해 보자. 이 과정이 LILO에게 새로운 커

널의 위치를 알리는 과정이다. 위와 같이 추가하면 시스템 부팅 시 LILO의 프롬프트에서

“linux”라고 입력하면 기존의 리눅스 커널인 /boot/vmlinuz로 부팅되고, “newlinux”라고 입

력하면 여러분이 컴파일하여 생성한 새로운 리눅스 커널인 /boot/newvmlinuz로 부팅된다.

결국 /etc/lilo.conf 파일에서 ‘image’는 현재 설치된 커널들을 나타내며, ‘label’은 LILO가

어떤 커널을 부팅할 것인지를 결정하는데 사용한다. 그리고 ‘root’는 커널이 존재하는 디스

크의 파티션을 나타내며, 일반적으로 루트 파일 시스템이 존재하는 파티션을 가리키게 된다.

# original lilo.conf file

boot=/dev/hdatimeout=100..image = /boot/vmlinuz

label = linuxroot = /dev/hda1....

# original lilo.conf file

boot=/dev/hdatimeout=100..image = /boot/vmlinuz

label = linuxroot = /dev/hda1....

# modified lilo.conf file

boot=/dev/hdatimeout=100..image = /boot/vmlinuz

label = linuxroot = /dev/hda1....

/* 이 부분을 추가 */image = /boot/newvmlinuz

label = newlinuxroot = /dev/hda1....

# modified lilo.conf file

boot=/dev/hdatimeout=100..image = /boot/vmlinuz

label = linuxroot = /dev/hda1....

/* 이 부분을 추가 */image = /boot/newvmlinuz

label = newlinuxroot = /dev/hda1....

/etc/lilo.conf 파일 수정 예

그림 3.13 lilo.conf 파일 수정

임베디드 리눅스 프로그래밍 33

3.3 임베디드 시스템용 uClinux 환경

3.3.1 개발 과정

개발자는 임베디드 시스템용 응용 프로그램을 개발할 때 개발 시스템인 리눅스 워크스테이

션, PC 등을 사용한다. 리눅스 워크스테이션에서 사용가능한 X 윈도우 시스템이나 스크린

에디터 등 각종 유틸리티를 사용하여 코딩한다.

어플리케이션 소스 코드가 완성되면 타겟시스템에서 실행되는 형태의 바이너리 코드가 생성

되도록 크로스컴파일(cross compiler)한 후 uClinux 이미지 파일에 포함하여 시리얼 업로드

나 flashloader 명령어를 사용하여 uCsimm RAM에 전송한다.

uCsimm RAM에 전송된 이미지는 RAM 에서 실행(부팅)하거나 또는 Flash ROM에 로드한

후 실행할 수 있다. RAM 에서 실행되는 uClinux 이미지는 Flash ROM에 로드된 uClinux와

는 별개의 것이다. 즉, RAM에 들은 이미지로 새로 부팅을 하드라도 ROM에는 이전 버전이

그대로 남게된다.

이와 같이 개발자는 개발 시스템 운영체제에서 타겟시스템 운영체제로 오가며 프로그램을

개발하며 이때 동일한 키보드와 모니터를 사용하여 작업한다. 키보드와 모니터가 개발 시스

템인 리눅스 워크스테이션과 타겟시스템인 DragonBall MCU(Micro Controller Unit) uCsimm

을 동시에 서비스하기 때문이다.

3.3.2 개발 툴

본 교재는 uCsimm에서 동작하는 임베디드 시스템을 이용한 개발을 살펴보고 있다. 본 교

재의 uCsimm은 uClinux를 운영체제로 사용한다. uClinux (MicroController Linux)는 1998년

에 Dragonball 68k 시리즈에 처음으로 포팅되었으며, 그 이후로 수 많은 타겟 프로세서에

포팅이 되었다. 교재에서 사용하는 버전은 DragonBallEZ MCU 하드웨어에 포팅된 것을 사

용한다. 본 절에서는 3.1절에서 설명한 개발환경을 좀 더 자세히 살펴보기로 한다.

3.3.2.1 타겟시스템

uCsimm은 우리가 개발하는 환경인 인텔 x86 CPU와는 완전히 다른 CPU를 사용한다. CPU

가 다를 뿐만 아니라 다른 유닉스계열에 비해서 기본적인 기능도 부족하다. 2.2 절에서 설

명한 것과 같이 uCsimm은 메모리 관리 기능이 없다. 그렇기 때문에 프로그램과 이미지를

만들고 타겟시스템에 업로드하기 위해서는 크로스 개발 환경을 설정해야 한다. uClinux는

메모리 관리 시스템이 없는 시스템에서 매우 잘 동작하는 수정된 리눅스의 한가지 종류이다.

우리가 사용하고 있는 uCsimm은 모토롤라의 DragonBallEZ328 아키텍쳐 기반이다. 앞에서

간단히 살펴본 uCgardener은 uCsimm을 위한 소켓으로 사용되며 uCsimm의 리소스를 액세

스하는데 사용된다. uCsimm은 크게 세가지 부분으로 이루어져 있다.

임베디드 리눅스 프로그래밍 34

MCU Core

System Memory

Ethernet Controller

그리고 앞에서도 잠시 언급한 부트로더와 uClinux가 시스템 소프트웨어로 사용된다.

uCsimm 모듈의 특징은 다음과 같다.

2.7Mips EC000 CPU core

8Mb DRAM

2Mb FLASH ROM

30 pin SIMM form factor

100 mA current at 3.3V

Low power sleep mode

Real time clock

On board uClinux OS including TCP/IP

RS 232 serial Port up to 115200 bps

SPI serial master

QVGA LCD panel controller

Up to 19 Parallel I/O

10 Base T Ethernet

3.3.2.2 개발 시스템

개발 시스템은 앞절에서 설치한 일반적인 리눅스 워크스테이션을 말한다. 이제 일반적인 워

크스테이션을 타겟 임베디드 시스템을 개발하기 위한 워크스테이션으로 설정해보자.

(가) 툴체인 및 uClinux설치

툴체인은 uClinux System Builder Kit CD에 들어있다. CD는 다음과 같이 구성되어있다.

M68k 코드를 생성하기 위한 크로스 컴파일러, 어셈블러, 링커

uClinux 커널(2.038)

uClinux 라이브러리

유틸리티와 어플리케이션

루트 파일시스템 생성 도구

개발환경 설정 도구

uClinux/m68k 툴체인은 바이너리로 설치할 수도 있고 소스화일로부터 설치할 수도 있다.

소스화일로 설치하는 것보다 바이너리 설치가 간편하기 때문에 교재에서는 바이너리 인스톨

을 권장한다. 자신의 리눅스 워크스테이션에 설치된 공유 라이브러리의 타입에 따라 libc5

나 libc6 중에서 맞는 라이브러리를 설치하면 된다. 요즘 대부분의 리눅스는 libc6가 될 것

임베디드 리눅스 프로그래밍 35

이다. 그래도 혹시 여러분의 리눅스 워크스테이션에 설치된 공유 라이브러리를 살펴보려면

‘ldd /bin/ls’명령으로 알아볼 수 있다. 위 명령의 결과로 libc6인 경우는 아래와 같은 값을

출력한다.

libc.so.6 => /lib/libc.so.6(0x2aaab000)

/lib/ld-linux.so.2=>/lib/ld-linux.so.2(0x2aaab000)

libc5인 경우는 다음과 같다.

lib.so.5=>/lib/ld-linux.so.2(0x4000a000)

자신의 공유 라이브러리 타입을 알아냈으면 각 타입에 맞는 툴체인을 설치한다.

libc6인 경우에 우선 루트로 로그인한 다음

uClinux CD를 마운트한다. ( mount –t iso9660 /dev/cdrom /mnt/cdrom)

libc6 디렉토리로 이동한다. ( cd /mnt/cdrom/RPMS/libc6)

여기서 ‘make’ 명령을 치면 설치가 끝난다.

libc5인 경우에 우선 루트로 로그인한 다음

uClinux CD를 마운트한다. ( mount –t iso9660 /dev/cdrom /mnt/cdrom)

libc5 디렉토리로 이동한다. (cd /mnt/cdrom/RPMS/libc5)

여기서 ‘make’ 명령을 치면 설치가 끝난다.

툴체인 설치가 끝나면 /opt/uClinux 디렉토리로 가보면 다음과 같이 설치가 되었음을 확인

할 수 있다.

/bin dev/ info/ linux/ m68k-pic-coff/ romdisk/

deftemplate.sh* include/ lib/ m68k-coff/ man/ src/

리눅스 배포판에 따라 제대로 설치되지 않는 배포판이 있다. make 명령에서 의존성 문제가

발생하면 Makefile에서 의존성 검사를 제거하고 다시 설치해본다. 예를 들어

rpm –i uC-src-0.9.2-2.i386.rpm을

rpm –i –force –nodeps /mnt/cdrom/RPMS/libc6/uC-src-0.9.2-2.i386.rpm

으로 수정한다. CD에서는 수정이 안되므로 Makefile은 하드디스크로 복사해서 수정한다.

발생할 수 있는 다른 문제는 레드헷 계열의 배포판에서 genromfs가 /dev 디렉토리를 다룰

수 없는 문제가 있다. 이것은 uClinux가 부팅할 때 로그인 콘솔을 열 수 없게 되어 시스템

이 다운되게 한다. 해결 방법은 기존의 레드헷의 genromfs를 제거하고 uClinux CD에 들어

있는 genromfs를 설치하면 된다.

rpm –e genromfs

임베디드 리눅스 프로그래밍 36

실험해 본 결과 대부분의 레드헷 배포판에서 위와 같은 현상이 발생하기 때문에 CD에 있는

Makefile대로 하지 말고 위의 내용을 포함한 Makefile로 수정해서 툴체인을 설치하는게 좋

다.

(나) 작업환경 설정

이제 uClinux와 툴체인이 설치되었기 때문에 사용자는 uClinux를 사용할 수 있다. 루트 계

정이 아닌 자신의 일반 사용자 계정으로 로그인해서 자신의 환경을 설정한다.

여러분이 작업할 디렉토리를 만든다. (mkdir work; cd work)

사용자계정에 uClinux 개발환경을 생성한다. (buildenv)

Makefile을 실행한다. (make)

make명령에서 에러가 발생하지 않았다면 다음과 같은 파일과 디렉토리가 생성되었을 것이

다. 만일 이와 같지 않다면 앞의 과정에서 잘못된 부분이 있으므로 처음부터 다시 시도하기

바란다.

Makefile image.bin romdisk/ romdisk.map

각 파일에 대해서 알아보도록 하자.

① Makefile

이 파일은 buildenv 명령어를 실행하면 작업 리렉토리에 생성된다. 의존성과 작업 디렉토리

의 내용을 빌드하기 위한 make 명령어에 사용된다.

② deftemplate.sh

Makefile 은 이 템플릿 파일을 호출해서 여러분의 프로젝트를 위해 선택된 바이너리를 갖는

romdisk/ 디렉토리를 구성한다. 처음 시작할 때는 기본적인 템플릿으로서 괜찮지만 필요하

다면 자신만의 템플릿으로 대체할 수 있다. 예를 들어 여러분이 mytemplate.sh 라는 새로

운 템플릿을 만든다면 이것을 대신 사용할 수 있다. 커맨드 라인에서 다음과 같이 실행하면

템플릿이 대체된다.

make TEMPLATE=mytemplate.sh

이 스크립트는 특정 바이너리 파일을 /romdisk 디렉토리에 추가하는 역할을 하지만 기존에

디렉토리에 있는 바이너리 파일을 제거하지는 않는다는 것을 명심하자. 만약 이 스크립트에

있는 바이너리 파일을 제거하고 싶다면 스크립트에서 파일의 레퍼런스를 제거할 뿐만 아니

라 romdisk/ 에서도 실제 바이너리 파일을 제거해야만 한다.

③ image.bin

make 명령으로 우리가 최종적으로 만들고자 하는 파일이 이 바이너리 파일이다. 이 이미지

파일은 uCsimm FLASH ROM 이나 uClinux RAM에서 실행하는데 사용된다. 여러분의 임베

임베디드 리눅스 프로그래밍 37

디드 시스템용으로 개발한 코드는 이 이미지에 포함하여 실행하거나 nfs를 사용하여 워크스

테인션의 디렉토리에서도 실행할 수 있다. 이 이미지는 romdisk/ 의 내용과 파일 시스템이

함께 이미지로 만들어 지는 것을 명심하라. uCsimm에 전송하는 법은 뒷 절에서 살펴볼 것

이다.

④ linux/

이것은 uClinux의 소스 코드 디렉토리, 즉 /opt/uClinux/linux에 대한 링크다. m68k 명세가

있는 곳이기도 한데, linux/include/asm (asm은 링크이므로 어디를 링크하는지를 살펴보자)

과 linux/arch/m68knommu를 잘 살펴보라.

⑤ romdisk/

이 디렉토리는 ROMfs 이미지에 들어가는 루트 파일시스템을 위한 공간이다. 이 디렉토리의

계층적 구조는 uCsimm의 FLASH ROM에 그대로 전송된다. 그렇기 때문에 디렉토리의 내용

이 롬의 크기보다 커서는 안된다. 초과했을 때는 위의 deftemplate.sh의 설명을 보고 이미

지 내의 바이너리를 삭제하면 된다.

⑥ src/

이 디렉토리에는 바이너리 파일에 대한 소스코드가 들어 있다. uClinux를 처음 접하는 개발

자에게 좋은 예제가 될 것이다.

3.3.2.3 개발 시스템과 타켓 시스템 연결

지금까지 개발 시스템과 타켓 시스템 설정은 마쳤다. 이제 두 시스템간의 연결을 설정하면

임베디드 시스템 개발을 위한 환경이 갖춰지는 것이다.

(가) 시리얼 포트 설정

기본적으로 개발 시스템과 타겟시스템간의 연결은 시리얼 포트를 사용한다. 리눅스에서 시

리얼 포트는 /dev 에 위치해 있다. 이것은 일반적인 파일과 유사해 보인다. 자세히 살펴보

면 일반 텍스트 파일과는 차이가 있음을 알게 될 것이다. 시리얼 포트에 대한 파일 이름은

다음과 같다. (S가 대문자임을 유의하라.)

/dev/ttyS0 (시리얼 포트 1번)

/dev/ttyS1

본 교재에서는 기본적으로 시리얼 포트 1 번을 사용한 방법만을 설명하겠다. 물론 필요에

따라 시리얼 포트 2 번을 사용해도 무방하다. 먼저 ‘ls’명령으로 시리얼 포트를 살펴보자.

ls –l /dev/ttyS0

그러면 다음과 같은 결과를 얻을 것이다.

임베디드 리눅스 프로그래밍 38

crw------- 1 root tty 4, 64 10월 13 13:02 /dev/ttyS0

캐릭터 디바이스 장치를 의미하는 ‘c’가 맨 처음 나오고 루트만이 ‘rw’할 수 있게 되어있다.

본 교재의 실습에서는 일반사용자 계정으로 실습하기 때문에 권한을 일반사용자도 사용할

수 있도록 변경해야 한다. 다음과 같이 하자.

chmod 666 /dev/ttyS0

crw-rw-rw- 1 root tty 4, 64 10월 13 13:02 /dev/ttyS0

(나) 미니콤(minicom) 설치

리눅스에서 시리얼 포트를 사용하기 위한 가장 간편하고 좋은 방법은 ‘미니콤’이라고 하는

프로그램을 사용하는 것이다. 예전에 도스에서 통신용 터미널 에뮬레이터로 쓰이던 ‘이야기’

나 ‘새롬 데이터 맨’을 떠올리면 이해하기 쉬울 것이다. 일반적으로 미니콤은 기본적으로 설

치되어 있지만, 설치 옵션에 따라서 설치가 되어 있지 않을 수도 있기 때문에 명령라인에서

‘minicom’을 실행해서 이를 확인해야 한다. 자신의 컴퓨터에 미니콤이 설치되어 있다면 이

부분은 그냥 읽기 바란다.

자신이 설치한 리눅스 CD에 보면 minicom-1.xxxx.rpm이 들어 있을 것이다. 리눅스 배포판

에 따라 버전이 다르므로 각자마다 다른 이름으로 되어 있는데 앞부분이 minicom이면 대부

분 맞을 것이다. 레드헷 CD 기준으로 ‘/redhat/RPMS/’디렉토리에 위치해 있다. 이 디렉토

리로 이동한 후 다음과 같이 설치한다.

rpm –i minicom-1.xxxx(각자 조금씩 다르다).rpm

이렇게 하면 미니콤이 설치되는데 간혹 시스템에 따라 라이브러리 의존성 때문에 설치가 안

될 수도 있다. 이럴때는 소스 rpm 파일을 사용해 컴파일해서 설치하면 된다.

rpm –i –v minicom-1.xxxx.src.rpm

이렇게 하면 미니콤 소스파일이 /usr/src 디렉토리 밑에 설치된다. 소스로 설치할 때는 다

음의 과정이 더 필요하다.

rpm –ba /usr/src/xxxx/SPECS/minicom-1.xxxx.spec

rpm –ba /usr/src/xxxx/RPMS/minicom-1.xxxx.i386.rpm

(다) 미니콤 설정

uCsimm에 전원을 넣으면 uCsimm은 부트로더 프로그램을 실행시키고 이것의 디스플레이

정보를 터미널로 보내고 간단한 명령어를 입력받을 준비를 한다. 이때 터미널이 미니콤이다.

즉, 미니콤은 uCsimm의 디스플레이로 사용되는 것이다. 미니콤은 디스플레이 뿐만 아니라

시리얼 포트를 통해 이미지 전송도 한다. uCsimm은 시리얼 포트의 설정으로 9600bps, 8

data bits, no parity, one stop bit을 사용한다. uCsimm과 통신하기 위해서는 리눅스 워크스

테이션도 이를 같게 해주어야 한다. 미니콤도 이와 같이 설정하도록 하자.

리눅스 워크스테이션에서 최초로 미니콤을 설정하기 위해서는 설정 파일이 필요하다. 루트

계정으로 로그인한 후 명령라인에서 ‘minicom –s’를 실행해서 설정 파일을 작성해 보자. 그

임베디드 리눅스 프로그래밍 39

러면 설정을 위한 메뉴가 뜨는데 제일 먼저 ‘serial port setup’을 선택하고 각 항목에 대해

서 설정한다.

Serial Device : /dev/ttySo

Bps/Par/Bits : 9600 8N1

Hardware Flow Control : no

Software Flow Control : no

한단계 상위 메뉴에서 Modem and dialing 을 선택해서 역시 설정한다.

Init String 의 모든 내용을 지운다.

Reset String 의 모든 내용을 지운다.

설정이 끝나면 Save seup as df1.. 으로 한번 저장해 준다. 이렇게 하면 기본적인 설정화일

이 만들어 진다. 그리고 나서 Save setup as ... 를 선택해서 자신이 사용할 설정을 만들도

록 하자. 다시 말하면 명령라인에서 ‘minicom’을 실행시키면 기본설정이 로드되고

‘minicom 자신의 설정화일’을 실행하면 자신의 설정이 로드된다.

마지막으로 미니콤이 xmodem 프로토콜을 사용하기 때문에 이 프로토콜 또한 리눅스에 설

치되어 있어야 한다. 대부분 기본적으로 설치되어 있지만 혹시 모르니 조사해 보기 바란다.

‘/usr/bin/rx’ ‘/usr/bin/sx’가 있는지 조사한다. 만약 이 파일들이 없으면 다음과 같이 링크

를 만들어 주어야 한다.

ln –sf /usr/bin/rz /usr/bin/rx

ln –sf /usr/bin/sz /usr/bin/sx

rz나 sz도 없다면 xmodem 프로토콜이 설치되지 않은것이니 자신의 리눅스 CD에서 ‘lrzsz’

라는 패키지를 설치하고 CD에도 없다면 ‘http://rpmfind.net/linux/RPM/’에서 다운로드 받고

설치하도록 하자.

미니콤이 제대로 실행되지 않으면 다음을 확인해보자.

하나 이상의 미니콤이 실행되는지 검사한다. ps –ax | grep minicom

만일 있다면 실행되는 미니콤 프로세스 죽인다. kill –9 <id from above>

다음으로 lock 파일이 존재하는지 검사한다. 있으면 삭제한다.

rm /var/lock/LCK..ttyS0

미니콤에 대해서 더 자세히 알고 싶은 독자는 리눅스의 man minicom 명령으로 알아 볼 수

있다.

(라) uClinux 부팅

임베디드 리눅스 프로그래밍 40

이제 임베디드 시스템을 개발하기 위한 초기 설정은 모두 끝났다. 그러면 처음으로 uClinux

를 부팅해보자. 물론 uCsimm은 전원이 공급되고 워크스테이션과 시리얼 케이블과 이더넷

케이블로 연결되어 있어야 한다. 워크스테이션의 미니콤을 실행한 후 uCsimm의 초록색 리

셋 버튼을 눌러 초기화 시킨다. 그러면 다음의 화면을 볼 수 있을 것이다.

위의 화면은 uCsimm의 부트로더의 프롬프트를 보여주는 것이다. 미니콤에서 보여지는 것

은 uCsimm의 디스플레이 임을 다시 한번 명심하라. 즉, 위의 프롬프트에서 실행을 하면

uCsimm에서 실행하는 것이다. uCsimm 킷트를 처음 개봉하면 기본 세팅을 가진 이미지가

들어 있다. 우선 이 이미지를 가지고 부팅을 해보자. 미니콤의 명령 프롬프트에서 ‘go’를

치기만 하면 uClinux를 부팅할 수 있다. 만일 이더넷 케이블을 연결하지 않았다면 연결하자.

기본값으로 이더넷 케이블이 없으면 부팅조차 할 수 없기 때문이다.

부팅을 하기 시작하면 화면의 내용을 살펴보자. 일반적인 리눅스의 부팅과정과 유사하다.

사실 uClinux는 크기만 작을 뿐 리눅스 커널이므로 당연하다. 부팅과정을 마치고 나면 로그

인 화면이 뜨는데 이때 기본값으로 아이디는 ‘root’이고 패스워드는 ‘uClinux’ 이다. 로그인

한 후 일반적인 리눅스 명령어를 실행해보자. ‘ls’나 ‘vi’를 실행해 보면 일반적인 리눅스와

같이 동작한다.

(마) uClinux 부팅과정

부팅과정을 알아보려면 먼저 부트로더를 알아보아야 한다. 부트로더는 uClinux를 부팅할 때

사용하는 프로그램이다. 부트로더는 플래시 롬에 있으며 크기는 64kb이다. 이것은 하드웨어

를 정해진 상태로 초기화하고 하드웨어를 테스트 한 후 사용자에게 부트로더 쉘을 보여준다.

부트로더에 사용할 수 있는 명령어는 다음과 같다.

sh: invoke a new shell

exit: exit from the current shell

help: display this command list

printenv: display the contents or environment var

setenv: set or replace an environment var

eraseenv: erase all environment vars

pmask: set the protection mask

임베디드 리눅스 프로그래밍 41

rx: receive XMODEM into the RAM buffer

program: erase OS, then burn OS from RAM buffer

verify: compare OS FLASH image to the RAM buffer

go: execute an image in ram

fast: change to 115200bps

slow: change to 9600bps

md: dump memory

mm: memory modify

envmm: memory modify from environment vars

(바) uClinux 이미지 수정

여러분은 이제 uClinux를 실행하는데 필요한 설정은 모두 마치고 부팅까지 해보았다. 이번

에는 uClinux 이미지 파일을 직접 수정해서 컴파일 하고 uCsimm에 업로드해서 새로운 이

미지로 부팅하는 법을 배울 것이다. 크게 어려운 점은 없으며 오히려 이제부터 점점 흥미로

운 실습이 시작된다고 할 수 있다.

우리가 가장 먼저 수정할 곳은 romdisk/etc/rc 파일이다. 리눅스가 부팅할 때 /sbin/init를

실행하는데 이것은 /etc/rc파일의 쉘 스크립트를 차례로 실행한다. 이 파일의 내용은 다음

과 같다.

호스트 이름 설정

네트워크 인터페이스 설정

RAM디스크 확장

/dev/ram0, proc, nfs를 마운트

인터넷 데몬 시작

/sbin/init 프로그램은 다음으로 /etc/inittab 파일에 있는 내용을 실행한다. 초기 설정에서

inittab은 터미널을 위한 시리얼 라인을 설정한다.

앞의 내용을 정리해 보자. 커널이 초기화하고 부팅을 완료하면 리눅스는 /sbin/init를 실행하

는데 이것은 /etc/rc파일을 실행한다. 그렇기 때문에 네트워크의 설정을 변경하려면 /etc/rc

파일을 수정하면 된다. 자신에게 할당된 ip, gateway, subnet mask등을 변경하면 된다. 초

기값으로 외부 인터넷에 연결되지 않은 시스템을 위한 설정을 가지고 있다. 즉, 로컬 네트

워크로 누구나 사용할 수 있는 192.168.x.x대의 주소를 가지고 있다. 사실 외부 인터넷을

사용하지 않는다면 초기 설정으로도 얼마든지 실습을 할 수 있다. 그러나 이 시스템을 다른

용도로도 사용하려면 인터넷을 연결할 수 있는 설정으로 바꾸는 것이 좋다. 다음은 기본값

으로 설정된 /etc/rc파일의 내용이다.

#!/bin/sh

임베디드 리눅스 프로그래밍 42

#

# system startup.

# set up the hostname

/bin/hostname uCsimm

# attach the interfaces

/sbin/ifattach

/sbin/ifattach \

--addr 192.168.1.200 \

--mask 255.255.255.0 \

--net 192.168.1.0 \

--gw 192.168.1.100 eth0

# expand the ramdisk

/sbin/expand /ramfs.img /dev/ram0

# mount ramdisk, proc and nfs

/bin/mount -t ext2 /dev/ram0 /var

/bin/mount -t proc proc /proc

/bin/mount -t nfs 192.168.1.11:/home/kim/kit /usr

# start up the internet superserver

/sbin/inetd &

# that's it... sucess

exit 0

일단 본 교재에서는 위의 예제대로 설명을 할것이다. 이것은 예제의 하나이므로 얼마든지

변경이 가능하다. 위의 설정에서 ip, network mask, network address, gateway 등을 설정하

는 것은 타겟시스템인 uCsimm의 주소를 설정하는 부분이다. 개발시스템인 워크스테이션에

대한 것이 아니라는 점을 명심하자. 워크스테이션의 주소는 /bin/mount –t nfs

192.168.1.11:/home/kim/kit /usr 에 나와있는 192.168.1.11이 된다.

정리를 해보면

리눅스 워크스테이션의 IP는 192.168.1.11

uCsimm의 IP는 192.168.1.200

임베디드 리눅스 프로그래밍 43

리눅스 워크스테이션의 작업 디렉토리는 /home/kim/kit

nfs 디렉토리를 설정할 부분은 uCsimm 의 /usr이 된다.

이렇게 하면 uCsimm은 자신만의 네트워크 주소를 가지므로 이제 인터넷도 연결할 수 있고

이더넷으로 리눅스 워크스테이션과 접근도 가능하다. /romdisk/etc/rc 파일을 저장하고 작업

디렉토리로 이동해서 변경한 /romdisk의 내용을 이미지 화일로 만들어 보자.

make image.bin

이렇게 하면 image.bin이 우리가 변경한 새로운 이미지로 변경된다. 생성된 파일 날짜를 확

인해서 방금 생성한 이미지인가를 확인한다. 이것을 확인하지 않고 uCsimm에 업로드했다

가 변경 내용이 반영되지 않은 옛 이미지를 업로드하여 시간을 허비하는 경우가 자주 있다.

여러번 하다보면 실수할 때가 있으니 새로운 이미지를 만들때 마다 확인하는 습관을 가지도

록 노력하자.

(사) 이미지 파일 업로드하기

앞에서 생성한 이미지를 uCsimm에 전송해서 실행해 보자. 먼저 자신의 아이디로 로그인한

후 작업디렉토리에서 미니콤을 실행한다. 그리고 uCsimm에 전원을 넣고 리셋 버튼을 누르

면 uCsuimm이 초기화하고 명령을 입력받게 되는데 이곳에서 이미지 파일을 전송하면 된다.

시리얼 전송에는 두가지 방법이 있는데 ‘slow’와 ‘fast’ 모드가 있다. 먼저 ‘slow’ 모드를 살

펴보자.

B$ 프롬프트에서 rx 명령을 입력한다. rx는 부트로더가 xmodem 프로토콜의 수신

모드로 동작함을 의미한다. 프롬프트에서의 입력은 uCsimm쪽의 명령임을 명심하

자.

uCsimm이 수신모드로 되어 있기 때문에 워크스테이션에서는 전송을 해야한다. 미

니콤에서 전송하기 위해서는 ctrl-a 키를 동시에 누르고 z 키를 다음에 입력하면

미니콤의 명령어를 선택할 수 있다. 여기서 Send files를 선택하자. 그리고 업로드

프로토콜 선택메뉴에서 xmodem을 선택한다. 그런 후 앞서 만든 새로운 이미지 파

일을 선택해서 전송하면 된다. 익숙해 지면 ctrl-a 키를 누르고 곧바로 s 키를 누르

면 곧장 전송할 수 있다. 프로그램이 불안정하여 한번에 전송이 성공하는 경우는

거의 없고 같은 과정을 두세 번 반복해야 전송이 된다. ‘slow’ 모드에서 이미지 파

일을 전송하는데는 보통 40-50분 정도의 긴 시간이 필요하다. 전송이 끝나면 경쾌

한 멜로디가 울려서 전송이 완료되었음을 알려준다.

다음으로 ‘fast’ 모드를 살펴보자. ‘slow’ 모드와 대부분의 과정이 동일하고 약간의 추가적인

명령이 필요하다.

B$ 프롬프트에서 fast 명령을 입력한다.

Ctrl-a 를 동시에 누르고 다음으로 p를 누르면 전송속도를 선택하는 메뉴가 뜨는데

임베디드 리눅스 프로그래밍 44

이때 속도를 115200bps로 설정한다.

‘slow’ 모드와 마찬가지로 uCsimm을 수신모드로 놓자. rx명령으로 수신 모드로 놓

는다.

미니콤에서 ctrl-a 를 동시에 누르고 s키를 누른다. 그리고 xmodem을 선택하고 이

미지 파일을 전송한다. ‘fast’ 모드는 약 5분정도의 시간이 소요된다. 역시 느리긴

하지만 ‘slow’ 모드에 비해서는 매우 빠른 것을 알 수 있을 것이다.

이제 uCsimm의 RAM으로 새로운 이미지가 전송되었다. 이미지가 전송되었다고 곧바로 실

행할 수 있는 것은 아니다. RAM의 내용을 플래시 롬으로 다시 옯겨야 한다. ‘program’명령

이 이 역할을 한다. 이 명령은 플래시 롬의 내용을 지우고 새로운 이미지를 롬에 기록한다.

여기까지 하면 새로운 이미지로 부팅준비가 완료된 것이다. 앞절에서 했듯이 ‘go’명령을 실

행해서 uClinux를 부팅해 보자. 부팅하고 로그인한 후 인터넷을 테스트해 보면 인터넷이 가

능함을 알 수 있다. 이때 인터넷이 안되면 역시 앞의 내용을 확인해서 다시 컴파일하고 업

로드해야 한다. 특히 /etc/rc파일에서 오타가 있으면 아무런 메시지도 없이 연결이 되지 않

는다. 자신이 인터넷이 안된다면 거의 대부분이 오타에 의한 것이다. 자신이 알고 있는 주

소로 ping 테스트를 해보자. 인터넷이 되면 거의 대부분 성공한 것이다. 그러나 아직은

/etc/rc파일에서 설정한 nfs는 동작하지 않을 것이다. 왜냐하면 nfs의 서버가 되는 워크스테

이션을 설정하지 않았기 때문이다.

리눅스 워크스테이션에서 nfs설정을 해보자. 먼저 자신의 작업 디렉토리를 리눅스 워크스테

이션의 /etc/exports 파일에 기록한다.

/home/kim/kit (ro)

를 파일에 기록하고 저장한다. 이렇게 하면 nfs에 등록이 되는데 이때 반드시 nfs서버를 재

시작해야 등록된 파일이 적용된다. nfs서버의 재시작은 /etc/rc.d/init.d/디렉토리에 있는 nfs

명령을 사용한다.

/etc/rc.d/init.d/nfs stop

/etc/rc.d/init.d/nfs start

로 재시작한다.

nfs 서버를 재시작 했으면 다시 미니콤으로 돌아와서 uClinux를 재부팅해보자. 이번에는

nfs가 제대로 동작하는 지를 확인하는 것이다. uCsimm의 리셋키를 누르고 ‘go’명령으로 부

팅한다. 로그인 후 nfs로 마운트한 /usr의 내용을 확인해 보자. 리눅스 워크스테이션의

/etc/exports파일에서 설정한대로 작업 디렉토리의 내용이 /usr에 나타나면 nfs 설정은 성

공한 것이다. 이 과정은 uClinux 어플리케이션을 개발할 때 거의 필수적이므로 반드시 nfs

설정을 성공하고 다음 과정으로 넘어가도록 하자.

uClinux가 부팅해서 nfs를 성공적으로 마운트하면 앞에서 이미지 파일을 전송할 때 사용한

시리얼 포트를 더 이상 사용하지 않아도 된다. nfs에 마운트된 /usr파일에서 image.bin을

임베디드 리눅스 프로그래밍 45

직접 로드하고 부팅할 수 있기 때문이다. 시리얼 포트의 ‘fast’ 모드와 비교해도 훨씬 빠르

다. 약 10초대로 파일 전송을 완료하고 부팅까지 할 수 있다. 시리얼 포트 전송으로 어플리

케이션 개발을 상상해 보라. 한번 수정할 때마다 몇십분씩 기다려야 한다면 생각만 해도 끔

찍하다. nfs를 이용한 이미지 전송명령은 다음과 같다.

flashloader /usr/image.bin

3.4 네트워킹

본 절에서는 이 책에서 실습하는데 필요한 간단한 네트워크 설정방법을 소개할 것이다. 네

트워크의 범위가 너무 광범위하기 때문에 자세한 내용은 해당 서적을 참고하기 바란다. 따

라서 본 교재의 실습에 관련된 리눅스의 네트워크에 대해서만 살펴 볼 것이다.

3.4.1 기본적인 네트워크

네트워크 하드웨어 중 LAN 을 구축할 때 거의 절대적으로 사용되고 있는 이더넷 카드의 설

치 방법에 대하여 알아보겠다. 네트워크 인터페이스는 주요한 세가지 요소를 가지고 있다.

네트워크 디바이스 드라이버

인터페이스 설정

라우팅과 주소해석

디바이스 드라이버는 적재가능한 모듈로서 이것이 적재될 때 네트워크 디바이스를 생성하게

된다. 리눅스에서 ifconfig 라는 명령어는 디바이스를 시스템에 올리고 인터페이스에 필요한

설정을 할 수 있게 해준다. 라우팅은 route 명령어를 사용해 설정하게 된다. 이 명령들은

/sbin 디렉토리에 위치해 있다.

주요 네트워크 인터페이스 설정은 스크립트 파일인 /etc/rc.d/init.d/network 에 저장되어 있

다.

3.4.2 네트워크 디바이스 드라이버

디바이스 드라이버는 커널에 컴파일되어 들어가 있을 수 있다. 만약 그렇다면 디바이스 드

라이버에 의해 생성된 네트워크 디바이스 [eth0….x]는 부팅할 때 감지될 것이다.

/etc/rc.d/init.d/network 스크립트는 /etc/sysconfig/network-scripts/ifcfg-eth[0…x] 의 정

보를 사용해서 디바이스에 대한 설정정보를 적재할 것이다. 이것은 각각 설정이 다른 하나

이상의 이더넷 디바이스를 시스템에 사용할 수 있게 해준다. 디바이스 번호는 PCI 슬롯 순

서로 정해진다.

3.4.3 네트워크 모듈

임베디드 리눅스 프로그래밍 46

네트워크가 적재 가능한 모듈을 사용해서 설정된다면 /etc/modules.conf 에 있는 정보가

특정 이더넷 디바이스에 맞는 alias 를 할당하는데 사용된다. Alias 명령어는 네트워크 인터

페이스를 설정하려고 할 때 어느 모듈이 사용될지를 시스템에게 알려준다. 어떤 디바이스

드라이버는 irq, ioport 같은 옵션을 요구하기도 한다. 이런 옵션들도 마찬가지로

/etc/modules.conf 에 정의된다.

alias eth0 tulip

3.4.4 네트워크 인터페이스 설정

일반적으로 ifconfig 명령어를 사용한다.

ifconfig - 모든 네트워크 디바이스의 현재 설정을 보여준다.

ifconfig eth0 - 특정 디바이스에 대한 설정을 보여준다.

ifconfig eth0 [up|down] - 디바이스를 켜거나 끈다.

Ifconfig eth0 netmask 255.255.255.0 broadcast 192.168.2.255

192.168.2.234 - 일반적인 완전한 사용법

IP alias 는 하나의 네트워크 디바이스에 다른 네트워크를 추가할 때 유용하다. 기존 시스템

의 설정을 변경하지 않고 타겟시스템으로의 사설 네트워크를 사용하고자 할 때가 하나의 경

우이다. 또 테스트 목적으로 IP 어드레스가 필요할 경우에도 유용하다. IP alias 옵션은 리눅

스 커널에 컴파일되어 들어가거나 모듈 형태로 선택적으로 이용되도록 설정될 수 있다. IP

alias 로 다음과 같이 사용할 수 있다.

ifconfig eth0:1 netmask 255.255.255.0 broadcast 192.168.1.255

192.168.1.100 – 일반적인 완전한 사용법

ifconfig eth0:1 192.168.1.100 - 간단한 사용법

이 예는 서로 다른 클래스 C 서브네트워크를 eth0 디바이스에 alias 시킨 것이다.

3.4.5 네트워크 라우팅

네트워크가 동작하기 위해서는 시스템이 다양한 IP 어드레스가 어느 방향에 있는지를 알 필

요가 있다. Route 명령어는 시스템의 라우팅 테이블을 설정하는데 사용된다. 아무런 옵션없

이 route 명령어만을 사용하면 라우팅 테이블의 내용을 볼 수 있다.

가끔 DNS의 문제로 라우팅 명령이 동작하지 않을 수도 있다. 이때는 IP어드레스를 사용하

도록 하는 route –n 옵션으로 해결할 수 있다.

임베디드 리눅스 프로그래밍 47

[kim@linux kim]$ /sbin/route

Kernel IP routing table

Destination Gateway Genmask Flags Metric Ref Use Iface

localhost.local * 255.255.255.255 UH 0 0 0 eth0

168.188.46.0 * 255.255.255.128 U 0 0 0 eth0

127.0.0.0 * 255.0.0.0 U 0 0 0 lo

default 168.188.46.1 0.0.0.0 UG 0 0 0 eth0

라우팅 테이블의 내용은 런타임에도 추가하거나 삭제할 수 있다.

route add –net 127.0.0.0 : 루프백을 추가한다.

route add –net 192.56.76.0 netmask 255.255.255.0 dev eth0 : eth0

을 통해서 192.56.76.x 로 가는 경로를 추가한다.

route add default gw some-addr : 기본 경로를 추가한다.

3.4.6 주소 해석

/etc/resolv.conf 파일은 네임서버 주소의 리스트를 가지고 있다. 네임 서버로의 루트가 반

드시 존재해야 하고 디폴트 루트가 지정될 수도 있다.

nslookup 명령어는 주소 해석 기능을 테스트 하는데 사용된다. 이 명령어는 도메인을 IP 주

소로 알려준다.

/

[kim@linux kim]$ nslookup yahoo.com

Server: dns.comeng.chungnam.ac.kr

Address: 168.188.44.10

Non-authoritative answer:

Name: yahoo.com

Addresses: 216.115.109.6, 216.115.109.7

etc/hosts 파일은 IP 주소와 호스트 이름의 쌍들을 담고 있는데 이것은 DNS가 없는 작은

트워크에서 유용하다.

3.4.7 실습 환경

임베디드 리눅스 프로그래밍 48

실습을 위해서 앞절에서 설명한 것을 정리해 보겠다.

ping 명령어를 사용해서 네트워크를 테스트한다.

alias를 설정한다.

파일 시스템을 익스포트(export)한다.

익스포트된 파일 시스템을 마운트한다.

예 Comments

네트워크 테스트 ping 10.0.1.xx

alias 설정 /sbin/ifconfig eth0:1

192.168.2.xxx

/etc/exports 파일 수

/home/test/(rw,

noroot_squash)를 /etc/exports

에 추가

(mkdir

/home/test)

파일 시스템 리로드 /usr/sbin/exportfs –av 실행

익스포트된 파일시스템 마

운트

Mount –t nfs

192.168.2.xxx:/home/test

/mnt/test

(mkdir

/mnt/test

first)

임베디드 리눅스 프로그래밍 49

4 uClinux 프로그래밍

4.1 “Hello uClinux”

“Hello” 프로그램은 아래와 같이 간단한 프로그램인데 일반적으로 시스템이 제대로 동

작하는 지를 시험하는데 쓰인다.

/* hello.c */

#include <sdtio.h>

int main()

{

printf(“hello uClinux\n”);

}

이 프로그램은 개발 시스템과 타겟시스템에서 모두 실행되어야 한다. 먼저 개발 시스템

에서 실행시켜 보자.

4.1.1 개발 시스템에서의 실행

(1) Test 디렉토리를 만든다. $ mkdir /home/kim/test

(2) Test 디렉토리(/home/kim/test)로 이동한다. $ cd /home/kim/test

(3) 스크린 에디터를 사용하여 hello.c 코드를 작성한다.

(4) hello.c를 컴파일 한다. $ cc –o hello hello.c

(5) 실행한다. $ ./hello

4.1.2 타겟시스템에서의 실행

개발 시스템 용으로 만들어진 hello.c 프로그램을 m68k 프로세서 용으로 컴파일한다.

($ m68k-pic-coff-gcc –o hellouc hello.c) 이때 만들어진 hellouc를 개발 시스템에

서 실행시켜 보자. m68k 하드웨어가 아니기 때문에 실행이 되지 않는다. 따라서 hellouc는

타겟시스템으로 옮긴 후 실행한다.

4.2 uClinux 하드웨어 프로그래밍

4.2.1 타겟시스템 하드웨어

uCsimm 하드웨어 모듈은 그림 4.1에 보인 것과 같이, 높이가 3 cm, 길이 8 cm 정도

임베디드 리눅스 프로그래밍 50

이며 하나의 표준 30핀 SIMM 기판에 칩들이 꽂힌 것으로서 메모리 모듈과 유사하게 생겼

다.

uCsimm 은 uClinux 운영체제를 위한 마이크로컨트롤러 모듈이다. 단일 프로세서 기능,

이더넷 기능을 내장한 하나의 완전한 마이크로컨트롤러인데 웹서버에서

PLC(Programmable Logic Controller)까지 널리 사용될 수 있다.

그림 4.1 uCsimm

uCsimm 구성요소는 다음과 같다.

16MHz 68EZ328 DragonBall 마이크로컨트롤러 1개

2 MB FLASH ROM

8 MB DRAM

21 다목적 입출력(I/O) 핀

디스플레이드라이버(단색 LCD, 해상도 QVGA(640x480)) 1개

10Base-T 이더넷 제어칩

RS-232 시리얼 포트

고속의 (1Mbit/sec) I2C 또는 SPI 3 직렬 케이블

3.3 볼트 소모(idle 상태일때 단지 수 마이크로암페어 정도 소모)

uCsimm 모듈을 연결하도록 SIMM 소켓을 가지고 있는 uCgardener라고 불리는 PCB 기판

은, RJ45 이더넷 jack(female) 1개, DB9 직렬 접속기 1개, power jack과 on board voltage

regulator 가 달려 있다.

uCsimm 에 들어 있는 하드웨어 구성요소에 대해 보다 자세히 알아보자.

모든 장치의 소프트웨어 맵은 include 파일에 있다.

/opt/uClinux/linux/include/asm/MC68EZ328.h

4.2.1.1 메모리

임베디드 리눅스 프로그래밍 51

주소 범위 기 능

0x00000000 0x0001FFFF Bootloader System RAM

0x00020000 0x007EFFFF Operation System RAM

0x007F0000 0x007FFFFF Bootloader Stack RAM

0x10000300 0x10000310 CS8900A Ethernet

0x10C00000 0x10C0FFFF Bootloader FLASH Image

0x10C10000 0x10DFFFFF OS FLASH Image

0xFFFFF000 0xFFFFFDFF DragonBallEZ Registers

0xFFFFFE00 0xFFFFFFFF DragonBallEZ Boot microcode

4.2.1.2 이더넷

CS8900A 이더넷 제어기는 0x10000300의 위치에 저장되어 있다.

MAC 주소는 환경변수 HWADDR0에 저장되는데 bootloader에 의해서 변경 가능하다.

4.2.1.3 Uart

다양한 인터럽트를 제공하고 16 문자까지 저장하는 FIFO 장치이다.

4.2.1.4 PWM

일정한 간격으로 발생하는 펄스에 변화를 주도록 프로그램될 수 있는 장치이다.

PWMC_CLKSEL_MASK 0x0003 /* Clock Selection */

PWMC_CLKSEL_SHIFT 0

PWMC_REPEAT_MASK 0x000c /* Sample Repeats */

PWMC_REPEAT_SHIFT 2

PWMC_EN 0x0010 /* Enable PWM */

PWMC_FIFOAV 0x0020 /* FIFO Available */

PWMC_IRQEN 0x0040 /* Interrupt Request Enable */

PWMC_IRQ 0x0080 /* Interrupt Request (FIFO empty) */

PWMC_PRESCALER_MASK 0x7f00 /* Incoming Clock prescaler */

PWMC_PRESCALER_SHIFT 8

PWMC_CLKSRC 0x8000 /* Clock Source Select */

4.2.1.5 LCD 제어기

LCD 제어기는 병렬 포트 C를 사용하고, 30 – 23 핀은 PPC의 출력에 사용된다.

임베디드 리눅스 프로그래밍 52

4.2.1.6 병렬 포트

다음과 같은 병렬 포트가 사용가능하다.

port a

PADIR - data direction register

PADATA - data register

PAPUEN - pull up enable

port b - rts/cts 및 timer outputs 용

PBDIR - data direction register

PBDATA - data register

PBPUEN - pull up enable

PBSEL - select register

port c - LCD 용으로 사용

PCDIR - data direction register

PCDATA - data register

PCPUEN - pull up enable

PCSEL - select register

port d - gpio 용으로 사용

PDDIR - data direction register

PDDATA - data register

PDPUEN - pull up enable

PDSEL - select register

Port e - gpio 용으로 사용

PEDIR - data direction register

PEDATA - data register

PEPUEN - pull up enable

PESEL - select register

4.2.2 LED 제어 프로그래밍

임베디드 리눅스 프로그래밍 53

본 절에서는 uCsimm의 하드웨어 중 LED를 액세스하고 제어하는 실습을 수행한다.

(1) Test 디렉토리로 이동한다. $ cd /home/jeff/test

(2) 스크린 에디터를 사용하여 아래 led.c 코드를 작성한다.

(3) m68k 용으로 컴파일 한다. $ m68k-pic-coff-gcc –o led led.c

(4) 타겟시스템으로 옮겨 실행한다. $ ./led

하드웨어 컴포넌트에 대한 define은 아래 include file에 기록한다. init_portd()

function에서 원하는 포트를 지정하고 write_ouput_portd() function에서 선택한 LED

비트를 셋한다.

/* led.c */

#include <asm/MC68EZ328.h>

void init_portd(void)

{

/*select port D bits for I/O */

PDSEL=0xFF;

PDDIR=0xFF;

/* init to off */

PDDATA = 0xFF;

}

int write_ouput_portd(unsigned int which_bit)

{

unsigned char temp;

if(which_bit > 7)return –1;

temp = 0x01 << which_bit;

PDDATA = PDDATA & ~temp;

if (temp ==0) return 0;

else return 1;

}

int main(void)

{

unsigned int out_led;

int result;

init_portd();

임베디드 리눅스 프로그래밍 54

while(1){

printf(“\nWhich led [range 0->7] shall we turn on?\n”);

scanf(“%d”,&out_led);

result = write_output_portd(out_led);

if(result < 0){

printf(“Invalid led number\n”);

continue;

}

}

}

4.3 uClinux 네트웍 프로그래밍

본 절에서는 uCsimm 네트웍 장치를 사용하는 것을 실습한다. TCP/IP 네트워킹을 위해서는

서버와 클라이언트가 필요하다.

서버 프로그램의 listen() function은 socket을 만들어서 노드 번호를 할당한다.

그러면 클라이언트는 그 노드 번호를 통해 서버에게 데이터를 넘긴다.

4.3.1 네트웍 상태

일반적으로 리눅스 시스템에는 각기 다른 포트 번호 상에서 실행되는 서버 프로세스가 여러

개 있는데, netstat –atn 명령어는 시스템에 현재 열려있는 포트 번호의 목록을 보여준다.

[root@bierk philw]# netstat –atn

Active Internet connections (severs and established)

Proto Recv-Q Send-Q Local Address Foreign Address State

tcp 1 0 10.0.1.14:2774 209.155.42.198:80 CLOSE_WAIT

tcp 0 0 10.0.1.14:53 0.0.0.0:* LISTEN

tcp 0 0 0.0.0.0:6000 0.0.0.0:* LISTEN

tcp 0 0 0.0.0.0:119 0.0.0.0:* LISTEN

tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN

tcp 0 0 0.0.0.0:25 0.0.0.0:* LISTEN

tcp 0 0 0.0.0.0:1023 0.0.0.0:* LISTEN

tcp 0 0 0.0.0.0:515 0.0.0.0:* LISTEN

tcp 0 0 127.0.0.1:53 0.0.0.0:* LISTEN

tcp 0 0 0.0.0.0:98 0.0.0.0:* LISTEN

임베디드 리눅스 프로그래밍 55

tcp 0 0 0.0.0.0:113 0.0.0.0:* LISTEN

tcp 0 0 0.0.0.0:79 0.0.0.0:* LISTEN

tcp 0 0 0.0.0.0:513 0.0.0.0:* LISTEN

tcp 0 0 0.0.0.0:514 0.0.0.0:* LISTEN

tcp 0 0 0.0.0.0:23 0.0.0.0:* LISTEN

tcp 0 0 0.0.0.0:21 0.0.0.0:* LISTEN

tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN

4.3.2 서버

이 코드는 클라이언트 프로그램이 연결하도록 소켓을 생성하는 역할을 하는 서버 프로그램

으로서, 서버에 소켓 하나를 만들고 하나의 포트를 할당 받아 bind한 후 listening하는 코

드이다.

char * node_addr = “192.168.1.101”;

int port_num = 4242;

serv_sock_desc = socket(AF_INET, SOCK_STREAM,0);

serv_addr.sin_family = AF_INET;

result = inet_aton(node_addr, &serv_addr.sin_addr);

serv_addr.sin_port = htons(port_num);

serv_len = sizeof(serv_addr);

bind(serv_sock_desc, (struct sockaddr *) & serv_addr, serv_len);

listen(serv_sock_desc,1);

접속을 요청 받으면 서버는 접속을 받아들인 후 데이터를 받는다.

위의 경우에 하나의 문자를 읽고서 받은 문자를 보여준다.

cli_len = sizeof(cli_addr);

cli_sock_desc = accept(serv_sock_desc, (struct sockaddr* ) &cli_addr,

&cli_len);

read(cli_sock_desc, &ch, 1);

write(cli_sock_desc, &ch, 1);

임베디드 리눅스 프로그래밍 56

작업이 끝나면 소켓을 닫고 종료한다.

close(cli_sock_desc);

close(serv_sock_desc);

4.3.3 클라이언트

클라이언트는 서버가 접속준비 상태이어야만 연결이 가능하고, 연결이 된 상태에서만 데이

터를 보낼 수 있다.

클라이언트 소켓은 서버와 비슷한 방법으로 만들 수 있다.

클라이언트는 서버의 주소와 포트 번호를 알고 있어야 한다. 아래 코드에서 IP 주소

“192.168.1.101” 자리에 서버 프로그램이 실행될 호스트의 IP 주소를 적는다. 포트 번

호는 그대로 둔다.

char * node_addr = “192.168.1.101”;

int port_num = 4242;

char ch = ‘3’;

serv_sock_desc = socket(AF_INET, SOCK_STREAM, 0);

serv_addr.sin_family = AF_INET;

result = inet_aton(node_addr, &serv_addr.sin_addr);

클라이언트는 서버 소켓과 연결한다.

client_len = sizeof(client_addr);

result = connect (client_sock_desc,

(struct sockaddr *) &client_addr, client_len);

그 후 클라이언트는 데이터를 보내고 응답을 기다린다.

write(client_sock_desc, &ch, 1);

printf(“Client sends <%c> to server”, ch);

임베디드 리눅스 프로그래밍 57

read(client_sock_desc, &ch, 1);

printf(“Server sends <%c> to client”,ch);

작업을 마친 후 클라이언트도 접속을 끊는다.

close(client_sock_desc);

exit(0);

4.3.4 실습

node_addr은 서버 프로그램을 실행시킬 시스템의 주소이다. 서버 코드는 개발 시스템 워크

스테이션에서 실행할 것이다. 클라이언트 코드는 workstation에서 실행할 수도 있고,

uCSimm에서 컴파일 하면,uCSimm에서도 실행 할 수 있다. workstation 에서 실행되는 서

버 코드에서 node_addr 을 바꾸고 서버를 uCSimm 에서 실행시킨다.

전체 코드

/* wserver.c */

#include <sys/types.h>

#include <sys/socket.h>

#include <stdio.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <unistd.h>

char * node_addr = “192.168.1.101”;

int port_num = 4242;

int main(void)

{

int serv_sock_desc, serv_len;

struct sockaddr_in serv_addr;

int cli_sock_desc, cli_len;

struct sockaddr_in cli_addr;

char ch;

int result;

임베디드 리눅스 프로그래밍 58

serv_sock_desc = socket(AF_INET, SOCK_STREAM, 0);

serv_addr.sin_family = AF_INET;

result = inet_aton(node_addr, &serv_addr.sin_addr);

if( result ==0 ) {

perror(“inet_aton error”);

exit(0);

}

serv_addr.sin_port = htons(port_num);

serv_len = sizeof(serv_addr);

bind(serv_sock_desc,

(struct sockaddr*) &serv_addr, serv_len);

listen(serv_sock_desc, 1);

printf(“ server awaiting client request \n”);

cli_len = sizeof(cli_addr);

cli_sock_desc = accept(serv_sock_desc,

(struct sockaddr *) &cli_addr, &cli_len);

read(cli_sock_desc, &ch, 1);

write(cli_sock_desc, &ch, 1);

close(cli_sock_desc);

close(sev_sock_desc);

}

/* 컴파일 방법

workstation gcc –O2 –o wserver wserver.c

uCsimm m68k-pic-coff-gcc –O2 –o wserveruc wserver.c

*/

/* wclient.c */

#include <sys/types.h>

#include <sys/socket.h>

#include <stdio.h>

임베디드 리눅스 프로그래밍 59

#include <netinet/in.h>

#include <arpa/inet.h>

#include <unistd.h>

char * node_addr = “192.168.1.101”;

int port_num = 4242;

int main(void)

{

int client_sock_desc, client_len;

struct sockaddr_in client_addr;

char ch = ‘3’;

int result;

client_sock_desc = socket(AF_INET, SOCK_STREAM, 0);

client_addr.sin_family = AF_INET;

result = inet_aton(node_addr, &client_addr.sin_addr);

if( result == 0){

perror(“inet_aton error”);

exit(1);

}

printf(“ Client socket set up \n”);

client_addr.sin_port = htons(port_num);

client_len = sizeof(client_addr);

result = connect(client_sock_desc,

(struct sockaddr *) &client_addr, client_len);

printf(“ client connected \n);

if( result == -1 ){

perror(“Client cannot connect \n”);

exit(1);

}

write(client_sock_desc, &ch, 1);

임베디드 리눅스 프로그래밍 60

printf(“Client sends <%c> to Server”, ch);

ch = ‘5’;

read(client_sock_desc, &ch, 1);

printf(“Server sends <%c> to Client”, ch);

close(client_sock_desc);

exit(0);

}

/* 컴파일 방법

workstation gcc –O2 –o wclientr wclient.c

uCsimm m68k-pic-coff-gcc –O2 –o wclientuc wclient.c

*/

임베디드 리눅스 프로그래밍 61

5 uClinux 커널 프로그래밍

5.1 커널 컴포넌트

5.1.1 커널 컴포넌트

1.1.2절에서 설명한 것과 같이 리눅스 커널은 여러 모듈로 구성된다. 각 모듈의 기능을

살펴 보자.

5.1.1.1 시스템 초기화

시스템 초기화는 리눅스가 부팅된 후 리눅스 커널이 처음 수행하는 작업이다. 초기화 파일

./arch/m68knommu/platform/68EZ328/ucsimm/crt0_rom.S

은 모든 cs레지스터들을 셋팅하고, RAM 공간에 커널을 상주시킨 후 아래 명령어를 실행

한다.

jsr start_kernel

jmp lp

5.1.1.2 커널 시작

init/main.c 의 코드를 살펴보자.

setup_arch(&command_line, &memory_start, &memory_end);

memory_start = pagin_init(memory_start, memory_end);

trap_init();

init_IRQ();

sched_init();

time_init();

parse_optins(command_line);

memory_start = console_init(memory_start, memory_end);

memory_start = kmalloc_init(memory_start, memory_end);

sti();

calibrate_delay();

memory_start = inode_init(memory_start, memory_end);

memory_start = file_table_init(memory_start, memory_end);

memory_start = name_cache_init(memory_start, memory_end);

mem_init(memory_start, memory_end);

buffer_init();

임베디드 리눅스 프로그래밍 62

sock_init();

ipc_init();

dquot_init();

arch_syms_export();

sti();

check_bugs();

printk(linux_banner);

sysctl_init();

/* We count on the initial thread going ok

Like idlers init is an unlocked kernel thread, which will

Make syscalls ( and thus be locked)

*/

kernel_thread(init, NULL, 0);

각 초기화 시퀀스는 초기화 함수가 사용할 메모리를 할당하게 하기 위해서 메모리 위치를

가리키는 포인터(memory_start pointer)를 변경하게 하는 옵션을 가지고 있다.

5.1.1.3 메모리 관리(Memory Management)

MMU가 없는 시스템의 경우 메모리 관리는 아주 단순하다. swap file과 page table이 필요

없고 응용 프로그램용 메모리를 할당받는 데에도 mmap 호출 대신 kmalloc 호출을 쓴다.

그러나 단순한 이면에 다음과 같은 단점들이 있다.

일반적인 fork를 사용할 수 없다. vfork를 대신 사용한다.

vfork 후에 별도의 stack 컨텍스트(context)를 얻기 위해 execve을 실행하거나 또는

exit해야 한다.

동적 스택이 제공되지 않는다.

메모리 영역 보호 메커니즘이 거의 존재하지 않는다.

5.1.1.4 인터럽트(Interrupts)

리눅스에서 각 인터럽트는 숫자로 정의 되는데, 이 숫자로 인터럽트를 식별하는데 사용한다.

uCsimm에서의 인터럽트의 mask와 set 함수는 다음 파일에 정의되어 있다.

임베디드 리눅스 프로그래밍 63

include/asm-m68knommu/irq.h

인터럽트 번호와 처리루틴은 아래 파일에 정의된다.

arch/m68knommu/platform/68EZ328/entry.S

인터럽트 종류에는 fast interrupt와 slow interrupt 가 있다. fast interrupt는 키보드 인터럽트

처럼 신속히 처리해야 하는 종류인데 다른 인터럽트들을 disable 시킨 상태에서 실행된다.

slow interrupt 는 다른 인터럽트들이 enalble된 보통 인터럽트이다. 특정 하드웨어가 fast

와 slow 중 어떤 인터럽트 방식으로 처리해야 하는지는 IRQ 요청 호출(request_irq) 파라미

터 중의 하나인 FLAG으로 결정된다. fast interrupt는 장치를 리셋하고 인터럽트 처리 작

업의 나머지 부분을 실행하기 위해 bottom half 를 스케줄 한다. bottom half는 인터럽트가

발생했을 때 수행해야 할 작업들 중 덜 시급한 것들을 정의한 함수이다. 이 bottom half는

스케줄러에 의해 나중에 실행된다.

5.1.1.5 스케줄러(Scheduler)

스케줄러는 메인 프로세서 제어기이다.

(1) 스케줄러

스케줄러는 타이머 인터럽트가 걸릴 때마다 동작한다. 각 프로세스는 타이머 인터럽트가 실

행될 때 마다 감소되는 카운터를 가진다. 스케줄러가 호출될 때 uClinux에서는 다음의 오퍼

레이션들이 수행된다.

* 인터럽트 내부에서 스케줄링을 하는 것이 발견되면 경고를 보낸다.

* 스케줄된 bottom half 를 실행한다. 스케줄러가 재진입(reentrant)되지 않아야 한다.

* 스케줄러 작업 대기 행렬에 준비된 작업을 실행한다.

* 현재 작업 스케줄이 RR(Round Robin) 방식이면 카운트 값을 조사하여 0일 경우

작업을 끝낸다.

* 현재 작업이 인터럽트 당할 수 있는 것인데 타임아웃이 일어났거나 또는 신호

(signal)를 가지고 있다면, 이 작업을 실행가능 상태로 설정한다.

* 각 태스크는 goodness 값이 주어진다.

* 가장 우선순위가 높은 작업이 실행된다. 태스크를 교체할 경우 타이머 타임아웃

값을 새로 설정한다.

(2) Wait Queues

wait queues는 호출되기를 기다리는 작업대기행렬 리스트이다.

wakeup 또는 wakeup_interruptable 이벤트가 발생하면 wait 큐에 들어있던 태스크의 상태

가 TASK_RUNNING 으로 바뀌고 실행(run) 큐로 옮겨진다.

임베디드 리눅스 프로그래밍 64

(3) Mutexes

중요한 함수를 보호하기 위해 사용된다.

/* ./drivers/block/blkmem.c 사용 예 */

static struct semaphore spare_lock = MUTEX;

/* Mutex all access to FLASH */

down(&spare_lock);

#ifdef DEBUG

printk(“rsld\n”);

#endif

/* Just copy the data into target buffer */

memcpy( buffer, (void*)(a->address+pos), length);

/* release MUTEX */

up(&spare_lock);

(4) 타이머(Timer)

미래 어느 시점에 어느 함수가 실행되게 할 때 사용된다.

#include <timer.h> // Set up a timer

samp_enable_timer(struct timer_list * timer)

{

init_timer(timer);

timer->function = samp_rx_fifo_reset;

timer->data = (unsigned long) timer;

timer->expires = jiffies +10;

add_timer(timer);

}

// disable a timer

samp_disable_timer(struct timer_list *timer)

{

del_timer(timer);

임베디드 리눅스 프로그래밍 65

}

//timeout routine

samp_rx_fifo_reset(unsigned long foo)

{

struct timer_list * timer = (struct timer_list *) foo;

if(still working)

.. do something

else

samp_enable_timer(timer);

}

(5) Task Queues

uClinux는 다음 세가지 태스크 큐를 운영한다.

*Scheduler queue

*Timer queue

*Immediate queue

이들 외에도 사용자 자신의 태스크 큐를 만들어 운영하는 것도 가능하다.

#include <tqueue.h>

struct tq_struct foo_task;

foo_task.routine = foo_fun;

foo_task.data = (void*) &foo_data;

….

queue_task(&foo_task,&tq_scheduler( or &tq_timer));

// the next time the schedule

5.1.1.6 신호(Signals)

신호는 어떤 이벤트가 발생했을 때 당시 실행하던 작업을 인터럽트한다. 예를 들어, 터미널

에서 ^C(ctl+C)키를 눌렀을 경우 실행되던 작업으로 신호가 보내진다. 보통 이동작은 작업

을 멈추게 하는 것이다.

신호는 받지 않도록 막을 수도 있고 받게 할 수 있으며, 또한 신호가 왔을 때 처리 루틴을

가진다.

임베디드 리눅스 프로그래밍 66

5.1.1.7 장치 드라이버(Device Driver)

(1) 블록 디바이스(Block Devices)

문자 디바이스(Char Deveice)와 다른 점은 데이터를 블록단위로 임시 저장(buffer)하고, 그

임시 저장된 데이터를 랜덤하게 접근할 수 있다는 것이다.

Block Devices는 만들기 어렵고, 다양한 오퍼레이션을 적용할 수 있으며 분할(patition)될

수 있고, mount될 수 있다. 접근 속도를 높이기 위해 블록 디바이스는 버퍼 캐쉬를 사용한

다.

버퍼 캐쉬 메커니즘(buffer cache mechnism)이 사용된 장소는 Flash Device 또는 RAM 메

모리이다.

(2) 문자 디바이스(Char Devices)

블록 디바이스 보다 간단하며, 어떤 순간 한 바이트씩 입출력 되는 병렬 포트나 직렬 포트

를 위해 사용한다. 간단한 문자 디바이스를 만들어 보자.

먼저 장치를 등록하는 방법을 살펴보자. 등록할 드라이버 용으로 file operation table을 만

든다. 우선 이번 예에서는 이 드라이버가 아무 일도 하지 않기 때문에 이 테이블의 내용은

모두 NULL이다. 등록 후 그 디바이스용으로 MAJOR 번호를 할당한다. 번호를 할당함으로

써 커널에게 장치 드라이버의 존재를 알리게 된다.

/proc/devices 파일을 살펴보면 장치가 등록된 것을 확인할 수 있다. 등록된 드라이버와 장

치번호에 대한 리스트는 Documentation/devices.txt에 있다.

/* 장치는 등록되지만, 아무것도 하지 않는 장치 드라이버 */

/* An example of a very simple driver */

/* mydrv.c */

#include < linux/fs.h>

#define MY_MAJOR 123

static struct file_operations my_fops = {

NULL, /* my_lseek */

NULL, /* my_read */

NULL, /* my_write */

NULL, /* my_readdir */

NULL /* my_select */

NULL, /* my_ioctl */

NULL, /.* my_mmap */

임베디드 리눅스 프로그래밍 67

NULL, /* my_open */

NULL /* my_releas */

};

int init_mydrv(void) {

if(register_chrdev(MY_MAJOR, “mydrv”,&my_fops)){

printk(“mydrv : unable to get major %d\n”, MY_MAJOR);

return –EIO;

}

return 0;

}

5.1.1.8 네트웍

네트웍 드라이버는 블록 드라이버(block driver)와 비슷하지만, 차이점은 파일 시스템에서 생

기는 것이 아니라는 것이다. 네트웍 드라이버 또한 디스크 드라이버와는 달리 커널에 데이

터를 저장한다.

네트웍 인터페이스는 리눅스의 장점 중의 하나이다. 이 네트웍 모듈은 잘 짜여지고 프로토

콜에 독립적이다. 따라서 동일한 네트웍 configuration 툴이 다른 종류의 네트웍 장치들에

대해 설정하는데 사용될 수 있다.

5.1.2 커널 이미지 생성

복잡하지만 편리한 Make 파일을 사용하여 커널 이미지를 생성한다. Make 파일은, 코드 컴

포넌트가 선택적으로 다른 include file를 지정하거나 또는 프로그램 실행에 쓰이는 입력을

서로 다른 값을 줄 수 있게 하는 선택 구성 체계(option configuration scheme)를 제공한다.

선택된 옵션은 configuration 파일( .config )에 저장된다. 이 파일은 종속성(dependency)

을 설정하고, 커널 구성(configuration) 당시 만들어진 선택사항에 따라 main makefile 작업

을 조절한다. 기본 사항은 main makefile의 첫 부분에 정의된다.

VERSION =2

PATCHLEVEL =0

SUBLEVEL = 38

UCRELEASE = ipre10

ARCH = m68knommu

임베디드 리눅스 프로그래밍 68

Cross compile 시스템에 대한 사항은 몇 라인 뒤에 정의된다.

CROSS_COMPILE = m68k-coff

AS =$(CROSS_COMPILE) as

LD =$(CROSS_COMPILE) ld

CC =$(CROSS_COMPILE) gcc –g –D__KERNEL__ -I$ (HPATH)

CPP =$(CC) –E

AR =$(CROSS_COMPILE)ar

NM =$(CROSS_COMPILE)nm

STRIP =$(CROSS_COMPILE)strip

MAKE =make

보통 커널 생성절차와 다른 점은 커널 make 내에 RAM디스크 생성하는 부분이 추가된다는

것이다.

linux.data : linux

$(CROSS_COMPILE) objcopy -O binary --remove-section=.romvec \

--remove-section=.text --remove-section=.ramvec \

--remove-section=.bss --remove-section=.eram linux linux.data

linux.text : linux

$(CROSS_COMPILE)objcopy -O binary --remove-section=.ramvec \

--remove-section=.bss --remove-section=.data --remove-section=.eram \

--set-section-flags=.romvec=CONTENTS,ALLOC,LOAD,READONLY,CODE \

linux linux.text

linux.bin : linux.text linux.data romfs.img

if [ -f romfs.img ] ; then \

cat linux.text linux.data romfs.img > linux.bin;\

else \

cat linux.text linux.data > linux.bin; \

fi

romfs.img가 커널생성 부분 바깥에서 생성된다는 것에 주목하라. 이 프로세스는 romfs.img

파일을 생성하는데 genromfs를 사용하게 될 것이다.

실행되는 코드의 첫 부분은 crt0라고 불린다. 이것은 다음에 정의 되어 있다.

임베디드 리눅스 프로그래밍 69

arch/m68knommu/platform/68EZ328/ucsimm/crt0_[rom|ram].S

코드가 RAM에서 실행될지 ROM에서 실행될지 결정하는데 사용되는 초기 파일에는 두 가지

파일이 있다. 이 파일들에 있는 코드는 기본적인 칩 선택과 메모리 접근 정보를 설정한 후,

초기에 설정된 코드를 끝낸다.

lp :

jsr start_kernel

jmp lp

start_kernel 코드는 linux/init/main.c 에 존재한다.

5.1.3 커널 이미지 수정

커널에 새로운 장치 드라이버를 추가시켜 보자. 커널 소스 트리는 새로운 드라이버를 추가

할 때 수정이 필요하다. 그림 1.2 에 있는 커널 소스 트리를 참조한다.

1. 우선, samp_misc.c 를 커널트리의 samp_misc.c로 복사한다.

2. linux/drivers/char/Makefile 에 CONFIG_SAMP_MISC 를 추가한다.

3. linux/arch/m68knommu/config.in에

bool ‘uCLinux Special Systems Driver’ CONFIG_SAMP_MISC 를 추가한다.

4. linux/arch/m68knommu/defconfig에 CONFIG_SAMP_MISC=y 를 추가한다.

5. linux/drivers/char/mem.c에 samp_misc_init(void); 를 추가한다.

6. 테스트 한다.

5.1.3.1 세부 절차

(1) 우선, samp_misc.c 를 커널트리의 samp_misc.c로 복사한다.

cp /mnt/train/code/v20/samp_misc.c

/home/train/work/linux/drivers/char/samp_misc.c

(2) /home/train/work/linux/drivers/char/Makefile 에 아래 코드를 추가한다.

#ifdef CONFIG_SAMP_MISC

L_OBJS += samp_misc.o

임베디드 리눅스 프로그래밍 70

endif

(3) /home/train/work/linux/arch/m68knommu/config.in 파일의

bool ‘uCsimm moule support’ CONFIG_UCSIMM

라인 바로 뒤에 아래 코드를 추가한다.

if [ =$CONFIG_UCSIMM” = “y” ]; then

bool ‘UCLinux Special Systems Driver’ CONFIG_SAMP_MISC

fi

(4) /home/train/work/linux/arch/m68knommu/defconfig 에서

CONFIG_UCSIMM=y 뒤에 아래 코드를 추가한다.

CONFIG_SAMP_MISC=y

(5) /home/train/work/linux/drivers/char/mem.c 에서 처음 부분에 아래 코드를

추가하고,

#ifdef CONFIG_SAMP_MTSC

extern int samp_misc_init(void);

#endif

끝 부분에 아래 코드를 추가한다.

#ifdef CONFIG_SAMP_MISC

samp_misc_init();

#endif

또한 마지막 라인에 아래 코드를 추가한다.

return 0;

}

(6) 테스트 한다.

cd /home/train/work/linux

임베디드 리눅스 프로그래밍 71

make mrproper

make menuconfig

make dep

make clean

make linux.bin

다음, 아래 과정을 꼭 실행한다.

cd ..

make

그 후 타겟시스템에서

flashloader /usr/image.bin

하고, 테스트 중인 uCsimm에서 cat /proc/devices 실행한다.

# cat /proc/devices

Character devices:

1 mem

2 pty

3 ttyp

4 ttys

5 cua

64 samp

Block devices :

1 ramdisk

31 Blkmem

디버깅할 때에는 /home/train/work/linux에서

make linux.bin | grep samp

하면 도움이 된다. 또한 다음과 같은 에러 메시지를 발생하면

target archclean : is missing

linux/kernel/arch/m68knommu/Makefile 에 다음을 추가한다.

archclean : after the line archdep

임베디드 리눅스 프로그래밍 72

(7) 개발 시스템에서 디바이스를 만든다.

/opt/uclinux/romdisk/@dev/ 에 다음 코드를 추가한다.

mknod --mode=0600 /opt/uClinux/romdisk/@dev/samp0 c 64 0

mknod --mode=0600 /opt/uClinux/romdisk/@dev/samp1 c 64 1

mknod --mode=0600 /opt/uClinux/romdisk/@dev/samp2 c 64 2

mknod --mode=0600 /opt/uClinux/romdisk/@dev/samp3 c 64 3

그리고 재생성한다.

cd /home/train/work

make

그 후 타겟시스템에 이미지를 로드한다.

flashloader /usr/image.bin

(8) kernel/linux/Documentation/Configure.help 에 적절한 설명문을 추가한다.

5.2 디바이스 드라이버

5.2.1 디바이스 드라이버의 기초

디바이스에는 문자 디바이스와 블록 디바이스가 있다. 이들을 위한 드라이버 파일은 보통

/dev 디렉토리에 있지만 어느 곳에 위치해도 무방하다. 가상(pseudo) 파일들은 드라이버

인터페이스를 위해 사용되는 Major number와 Minor number를 정한다.

[root@bierk opt]# ls –l /dev/rtc

crw-rw-r-- 1 root root 10, 135 May 5 1998 /dev/rtc

주장치번호(major number)는 커널이 디바이스 액세스 함수를 처리할 때 사용하는 디바이스

타입을 정의한 것이다. 부장치번호(minor numer)는 해당 타입의 디바이스 중 특정 디바이스

를 지정하기 위해 사용된다.

10 = Major (first) 135 = Minor

사용 가능한 드라이버의 목록은 /proc/devices 에 기록된다.

임베디드 리눅스 프로그래밍 73

[root@bierk opt]# cat /proc/devices

Character devices;

1 mem

2 pty

3 ttyp

4 ttys

5 cua

7 vcs

10 misc

14 sound

36 netlink

128 ptm

136 pts

254 pcmcia

Block devices :

1 ramdisk

2 fd

3 ide0

9 md

22 idel

특수 파일이나 노드는 mknod 명령으로 만들어 진다. 이 명령은 노드 정보에 Major 와

Minor 번호를 추가시킨다. 노드를 만드는 방법은 다음과 같다.

[root@bierk opt]# mknod /dev/samptest0 c 64 0

[root@bierk opt]# ls –l /dev/samptest0

crw-r--r-- 1 root root 64, 0 Sep 6 18:08 /dev/samptest0

5.2.2 디바이스 오퍼레이션 테이블

이 테이블은 커널 level 호출이 해당 디바이스 용으로 준비된 디바이스 드라이버 코드 중의

특정 함수를 호출하게 한다.

* open

int samp_open ( struct inode *inode, struct file *filp )

임베디드 리눅스 프로그래밍 74

* close(release)

void samp_release ( struct inode *inode, struct file *filp )

* read

read_write_t samp_read ( struct inode *inode, struct file * filp,

const char *buf, count_t count )

* write

read_write_t samp_write ( struct inode *inode, struct file *filp,

cont char *buf, count_t count )

* seek

int samp_lseek ( struct inode *inode, struct file *filp,

off_t off, int whence )

* ioctl … the catch all

int samp_ioctl (struct inode *inode, struct file *filp,

unsigned int cmd, unsigned long arg )

드라이버 코드는 디바이스 오퍼레이션 테이블을 생성한다. 이 테이블은 처음에는 NULL로

설정되어서 default 로 정해진 오퍼레이션을 하도록 정의되지만 그 후 특정 드라이버용 코

드로 대체된다.

Defined in linux/include/linux/fs.h

Struct file_operations samp_fops = {

NULL,

};

samp_fops.lseek = samp_lseek;

samp_fops.read = samp_read;

samp_fops.write = samp_write;

samp_fops.ioctl = samp.ioctl;

samp_fops.open = samp.open;

samp_fops.release = samp.release;

파일 오퍼레이션 테이블(file operations table)은 Major number 별로 등록된다. 문자열

“samp”는 /proc/devices 디스플레이에서 사용하는 문자와 같은 이름의 장치를 가져다 준

다.

result = register_chrdev(samp_major, “samp”, &samp_fops);

임베디드 리눅스 프로그래밍 75

clean up 동안 디바이스의 등록은 삭제될 수 있다. 이것은 커널의 요구에 따라 load되고

unlode되는 일시적인 드라이버들을 위한 것이다. loadable 모듈을 사용하지 않는 한 실행되

고 있는 커널에서 드라이버를 쉽게 지울 수 없다.

unregister_chdev(samp_major, “samp”);

5.2.3 Open/Close 호출

디바이스 작업들을 설정하고 해제하는데 사용된다.

Open() 호출은 사용자 프로그램이 디바이스를 액세스하는 첫 단계에서 호출하는 함수인데

아래 오퍼레이션을 수행한다.

* 관련 커널 자료구조 초기화

* 컴퓨터 시스템에 통보

* 디바이스에 대한 독점적 사용권한 보장, 사용정도 측정(usage count)

Close() 호출은 디바이스에 대한 오퍼레이션을 더 이상하지 않을 때 사용된다.

* 관련 커널 자료구조 해제

* 컴퓨터 시스템에 통보

* 디바이스에 대한 독점적 사용권한 보장, 사용정도 측정(usage count)

이들 함수를 사용하는 간단한 예를 제시한다.

static int samp_open ( struct inode *inode, struct file *filep)

{

/* determine Major / Minor Number */

struct inode *inode = filep->f_dentry->d_inode;

unsigned int minor = MINOR(inode->I_rdev);

//unsigned int majar = MAJOR(inode->I_rdev);

/* pointer to private data structure */

void * dev;

/* check minor */

if( minor > MAX_MINOR ){

return –ENODEV;

}

/* FS TABLE OPTION

임베디드 리눅스 프로그래밍 76

* reconfigure the device table if required

* This is done if the Minor number requires a different set of drivers.

* This saves a switch being done in the driver for each minor device.

*/

/*

filep->f_op = file_op_array[minor];

*/

/* set file position to 0 */

filep->f_pos = 0;

/* set up the private data struct one for all */

dev = ( void * ) samp_data;

/* alternate data structure one for each minor number

dev = ( void * )&samp_data[minor];

*/

/*alternate data structure create a new one each time

you open the device

dev = (void *) kmalloc(sizeof(struct samp_data),GFP_KERNEL);

*/

filled->private_data = dev;

/* do selected open if we have one */

/* Note this will only be done if we substituted an extra open

function when we changed the file operations table due to

the minor number in the FS TABLE OPTION above.

*/

if( filep->f_op->open)

return filep->f_op->open(inode,filep);

else

return 0; /* success !!!*/

}

Static void samp_release ( struct inode *inode, struct file *filep)

{

/*determine Major / Minor Number */

struct inode *inode = filep->f_dentry->d_inode;

임베디드 리눅스 프로그래밍 77

//unsigned int minor = MINOR(inode->i_rdev);

//unsigned int major = MAJOR(inode->I_rdev);

/* pointer to private data structure */

void * dev;

dev = (void *) filep->private_data;

/* alt free dev if we created it as a private data structure

*/

return;

}

5.2.4 Read/Write 호출

디바이스에서 데이터를 읽고 쓰는데 사용된다. 장치에서 버퍼로 count 만큼의 문자들을 읽

어오는 예제를 제시한다.

#define COPYTOUSER memcpy_tofs

#define DATA_SIZE 32

static char mydata[DATA_SIZE];

ssize_t samp_read (struct inode * inode, struct file *filep,

char * buf, int count)

{

int pos;

char * data;

pos = (int) filep->f_pos;

if( pos > DATA_SIZE-1) {

pos = DATA_SIZE-1;

}

if( pos + count > DATA_SIZE-1) {

count = DATA_SIZE - 1 - pos;

}

임베디드 리눅스 프로그래밍 78

data = &mydata[pos];

filep->f_pos = pos + count;

COPYTOUSER(buf,data,count);

return count; /* success !!! */

}

다음은 버퍼에서 디바이스로 count 만큼의 문자를 쓰는 예제이다.

#define COPYFROMUSER memcopy_fromfs

ssize_t samp_write(struct inode * inode, struct file *filep,

char *buf, int count)

{

unsigned int minor = MINOR(inode->i_rdev);

unsigned int major = MAJOR(inode->i_rdev);

/* position */

int pos;

/* pointer to private data structure */

char * dev;

char * data;

dev = (char *) filep->private_data;

pos = (int) filep->f_pos;

if( pos >= MAX_DATA )

return –ENOMEM;

if( pos + count >= MAX_DATA )

return –ENOMEM;

data = &dev[pos];

COPYFROMUSER(data, buf, count);

return count; /* success !!! */

임베디드 리눅스 프로그래밍 79

}

5.2.5 Seek 호출

Seek 함수는 디바이스 내의 특정 위치를 찾는 함수인데 파일 포인터를 사용한다. 위치를

정하기 위해 아래와 같은 옵션을 사용한다.

* SEEK_SET : 주어진 값으로 offset 값 변경

* SEEK_CUR : offset 값을 현재 위치 값으로 설정

* SEEK_END : offset 값을 마지막 위치 값으로 설정

간단한 예제를 제시한다.

int samp_seek ( struct inode * inode, struct file * filep,

off_t offset, int origin);

#define MAX_DATA 64

#define SEEK_SET 0

/* seek function */

int samp_seek( struct inode * inode, struct file *filep,

off_t offset, int origin );

{

/* determine Major / Minor Number if we need them */

unsigned int minor = MINOR(inode->i_rdev);

unsigned int major = MAJOR(inode->i_rdev);

int pos;

dev = (char *) filep->private_data;

pos = offset;

if(pos >= MAX_DATA) {

return –EINVAL;

}

if(pos + count >= MAX_DATA){

return –EINVAL;

임베디드 리눅스 프로그래밍 80

}

if(origin <> SEEK_SET) {

return –EINVAL;

}

filep->f_pos = pos;

return 0; /* success !!! */

}

5.2.6 IOCTL 인터페이스

디바이스 드라이버 IOCTL은 커널 메모리 영역 내에 있는 디바이스 관련 자료구조에 액세스

하는 다양한 커널 인터페이스 옵션을 제공한다.

5.2.6.1 사용자 입장에서의 IOCTL

사용자는 다음의 함수를 사용하여 IOCTL을 다룬다.

int ioctl(int fd, int cmd, var * data)

‘data’ 인자는 존재하지 않을 수도 있는데, 존재할 경우 데이터 값 또는 메모리 포인터가

될 수도 있다.

일반적으로 이 호출을 사용하는 때는 터미널 속성(attribute)을 읽거나 설정 하는 경우이다.

/* Initial termio settings : 8-bit characters, raw-mode, blocking i/o

*/

termio_init(tp, speed, op)

struct termio *tp;

int spped;

struct options *op;

{

int fd;

fd = 0;

tp->c_cflag = CS8 | HUPCL | CREAD | speed;

if( op->flags & F_LOCAL){

tp->c_cflag |= CLOCAL;

}

임베디드 리눅스 프로그래밍 81

tp->c_iflag = tp->c_lflag = tp->c_oflag = tp->c_line = 0;

tp->c_cc[VMIN] = 1;

tp->c_cc[VTIME] = 0;

/* optionally enable hardware flow control */

#ifdef CRTCTS

if( op->flags & F_RTSCTS)

tp->c_cflag |= CRTSCTS;

#endif

(void) ioctl(fd,TCSETA,tp);

}

5.2.6.2 커널에서의 IOCTL

커널에서는 IOCTL 함수가 아래와 같이 switch statement로 구현되어 있다.

static int tty_ioctl(struct inode * inode, struct file * file,

unsigned int cmd, unsigned long arg)

{

int retval;

struct tty_struct * tty;

struct tty_struct *real_tty;

struct winsize tmp_ws;

pid_t pgrp;

unsigned char ch;

char mbs = 0;

struct termios tmp_termios;

tty = (struct tty_struct *) file->private_data;

if( tty_paranoia_check(tty, inode->I_rdev, “tty_ioctl”))

return –EINVAL;

if(tty->driver.type == TTY_DRIVER_TYPE_PTY &&

tty->driver.subtype == TTY_TYPE_MASTER)

real_tty = tty->link;

else

real_tty = tty;

임베디드 리눅스 프로그래밍 82

switch(cmd){

……생략

case TCSETA :

retval = verify_area(VERIFY_READ, (void *) arg,

sizeof(struct termios));

if(retval)

return retval;

memcpy_fromfs(&tmp_termios, (struct termios *) arg,

sizeof(struct termios);

return 0;

…… 생략

}

}

5.2.6.3 IOCTL 명령

IOCTL 함수를 부르는데 사용하는 명령어들은 명확하게 정의되고, 커널코드와 사용자가 잘

이해할 수 있어야 한다. IOCTL 명령들에 대한 정의는 include 파일 (include/asm-

m68knommu/ioctls.h)에 저장된다. 명령들의 정의를 서술하는 방법에는 여러가지 표준이

있다. 그 중 전통적인 방법은 8 비트 “magic number” 를 상위 byte로 놓고 하위 byte에

자신의 명령어를 포함시키는 것이다. 사용자로부터 또는 사용자에게의 파라미터 데이터 타

입과 데이터 흐름의 방향을 정의하기 위해 이보다 더 복잡한 표준도 있다. 보다 최근 방법

은 IOCTL 명령어에서 4byte로 구성하는 것이다.

* type – 8 비트 magic number

* number – 8 비트 command number

* direction – 2 비트 (Read, Write, Read와 Write)

* size – read/write 할 바이트 수(프로세서 종류에 따라 8 비트 또는 14 비트)

다음과 같은 매크로들이 include/asm/ioctl.h 에 설정되어 있다.

#define _IOC_NRBITS 8

#define _IOC_TYPEBITS 8

#define _IOC_SIZEBITS 14

#define _IOC_DIRBITS 2

#define _IOC_NRMASK ((1 << _IOC_NRBITS)-1)

임베디드 리눅스 프로그래밍 83

#define _IOC_TYPEMASK ((1 << _IOC_TYPEBITS)-1)

#define _IOC_SIZEMASK ((1 << _IOC_SIZEBITS)-1)

#define _IOC_DIRMASK ((1 << _IOC_DIRBITS)-1)

#define _IOC_NRSHIFT 0

#define _IOC_TYPESHIFT (_IOC_NRSHIFT + _IOC_NRBITS)

#define _IOC_SIZESHIFT (_IOC_TYPESHIFT + _IOC_TYPEBITS)

#define _IOC_DIRSHIFT (_IOC_SIZESHIFT + _IOC_SIZEBITS)

#define _IOC(dir,type,nr,size) \

(((dir) << _IOC_DIRSHIFT) | \

((type) << _IOC_TYPESHIFT) | \

((nr) << _IOC_NRSHIFT) | \

((size) << _IOC_SIZESHIFT))

/*used to create numbers */

#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)

#define _IOR(type,nr,size)

_IOC(_IOC_READ,(type),(nr), sizeof(size))

#define _IOW(type,nr,size)

_IOC(_IOC_WRITE,(type),(nr), sizeof(size))

#define _IOWR(type,nr,size)

_IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))

여러분의 시스템을 위해 여러분 자신의 헤더 파일을 만든다.

#include <iocltl.h>

#define SAMP_MAGIC ‘s’

#define SAMP_IOCGETD _IOR(SAMP_MAGIC, 1, samp_data)

#get driver data

#define SAMP_IOCSETD _IOW(SAMP_MAGIC, 2, samp_data)

#set driver data

#define SAMP_IOCXCHD _IORW(SAMP_MAGIC, 3, samp_data)

#exchange driver data

임베디드 리눅스 프로그래밍 84

5.2.6.4 사용자의 데이터 처리

사용자 프로그램의 데이터 주소는 커널 주소로 변화되어야 한다. 또한 이 사용자 데이터 주

소는 정확해야 하고 사용자 프로세스 메모리 공간의 한 부분이어야 한다. uClinux 는 메모

리 관리 모듈이 없기 때문에 주소 점검(address checking)이 일반 리눅스와 같을 필요가 없

지만 호환성을 유지하기 위해 사용되고 있다.

verify_area(VERIFY_READ,(void*) arg, sizeof(struct termios));

사용자 데이터 주소 점검 시 위 함수가 사용된다. 위의 함수는 mmnommu/memory.c에 정

의 되어 있다.

int verify_area(int type, const void * addr, unsigned long size)

{

if((unsigned long)addr > 0x10f00000) {

printk(“Bad verify_area in process %d: %lx\n”,

current->pid, (unsigned long)addr);

return –EFAULT;

}

return 0;

}

5.2.7 Fops 테이블

이 테이블은 디바이스를 위한 파일 오퍼레이션(file operation)을 설정하는데 사용한다. fops

테이블은 커널 내에서 디바이스 드라이버에 대한 가장 주요한 정의들로서 주장치 (MAJOR)

번호와 디바이스 이름으로 등록된다. 디바이스 이름은 단지 식별(identification)을 위한 것

이고, 어느 드라이버를 사용할 것인가는 주장치 번호로 결정된다.

/* define the structure */

static struct file_operations samp_fops = { NULL, };

/* now fill it out */

void fill_samp_fops(void)

{

samp_fops.lseek=samp_seek;

임베디드 리눅스 프로그래밍 85

samp_fops.read=samp_read;

samp_fops.write=samp_write;

samp_fops.ioctl=samp_ioctl;

samp_fops.open=samp_open;

samp_fops.release=samp_release;

return;

}

5.2.8 리눅스 예제

지금까지 디바이스 드라이버에 대해 필요한 대부분을 배웠다. 이제 그 내용을 실습해 보자.

5.2.8.1 드라이버 소스 만들기

먼저 드라이버가 사용할 local 데이터 구조를 결정해야 한다. 그리고, 시스템에 등록하고 초

기화 하여야한다. 마지막으로, makefile을 만들어야 한다. 먼저 소스는

① 다음의 커널 디렉토리로 이동한다.

/home/train/work/linux/kernel/drivers/char

② /mnt/train/code/v20/samp.c 를 samp_misc.c 으로 복사한다.

③ /home/train/work/test 디렉토리로 이동한다.

(존재하지 않을시 mkdir 명령으로 디렉토리를 생성한다.)

④ /mnt/train/code/v20/stest.c 에서 stest.c로 복사한다.

/mnt/train/code/v20/Makefile을 Makefile로 복사한다. Makefile에 mknod 명령을 추가하고

make를 실행한다.

* add nodes : target

* <tab>mknod /opt/uClinux/romdisk/@dev/samp0 c 64 0

* <tab>mknod /opt/uClinux/romdisk/@dev/samp1 c 64 1

* <tab>mknod /opt/uClinux/romdisk/@dev/samp2 c 64 2

* <tab>mknod /opt/uClinux/romdisk/@dev/samp3 c 64 3

make에 설치 명령들을 추가한다.

* romdisk/bin 에 stest를 둔다.

새로운 이미지가 생기면 타겟시스템으로 로드한다.

make ( make image.bin)

flashloader /user/image.bin

임베디드 리눅스 프로그래밍 86

다음 ./stest 명령으로 드라이버를 테스트 한다.

5.2.8.2 드라이버 코드 예

/** Create samp_misc.c from /mnt/train/code/v20/samp_misc.c */

/* headers for example driver code */

#include <linux/types.h>

#include <linux/errno.h>

#include <linux/fs.h>

#include <linux/malloc.h>

#include <linux/ioport.h>

#include <linux/fcntl.h>

#ifdef CONFIG_PROC_FS

#undef CONFIG_PROC_FS

#endif

#ifdef CONFIG_PROC_FS

#include <linux/stat.h>

#include <linux/proc_fs.h>

#endif

#include <linux/kernel.h>

#include <linux/module.h>

#include <linux/version.h>

#include <asm/segment.h>

#define SAMP_MAJOR 64

#define SAMP_NAME “samp”

#define MAX_DATA 32

#degine MAX_MINOR 1

#define SEEK_SET 0/* small hack */

임베디드 리눅스 프로그래밍 87

/* local data for example driver code */

/* default data */

static unsigned char samp_data[MAX_DATA];

/* init data */

void samp_init_data(void)

{

int i;

for( i=0; i<4 ; i++){

samp_data[i] = i + 6;

printk(“ index %d data %x\n”,i,samp_data[i]);

}

}

/* open / release code */

static int samp_open(struct inode *inode, struct file *filep)

{

/* determine Major/Minor Number */

unsigned int minor = MINOR(inode->i_rdev);

// unsigned int major = MAJOR(inode->i_rdev);

/* pointer to private data structure */

void * dev;

/* check minor */

if( minor > MAX_MINOR){

return -ENODEV;

}

/* set file position to 0 */

filep->f_pos =0;

/* set up the private data struct one for all */

/* minor =0 actual data */

/* minor=1 data pointers */

dev = (void *) &samp_data[0];

임베디드 리눅스 프로그래밍 88

filep->private_data = dev;

return 0; /* success !! */

}

static void samp_release(struct inode *inode,

struct file *filep)

{

/* determine Major/Minor Number */

// unsigned int minor = MINOR(inode->i_rdev);

// unsigned int major = MAJOR(inode->i_rdev);

/* pointer to private data structure */

void * dev;

dev = (void*) filep->private_data;

return;

}

/* read/write code */

#define COPYTOUSER memcpy_tofs

int samp_read(struct inode *inode, struct file *filep,

char *buf, int count)

{

/* determine Major/Minor Number if we need them */

// unsigned int minor = MINOR(inode->i_rdev);

// unsigned int major = MAJOR(inode->i_rdev);

/* position */

int pos;

/* pointer to private data structure */

char * dev;

char * data;

임베디드 리눅스 프로그래밍 89

dev = (char*) filep->private_data;

pos = (int) filep->f_pos;

if(pos >= MAX_DATA){

return -ENOMEM;

}

if(pos + count >= MAX_DATA){

return -ENOMEM;

}

data = &dev[pos];

COPYTOUSER(buf,data,count);

Return count; /* success !! */

}

#define COPYFROMUSER memcpy_fromfs

int samp_write(struct inode * inode, struct file *filep,

const char *buf, int count)

{

/* determine Major/Minor Number if we need them */

// unsigned int minor = MINOR(inode->i_rdev);

// unsigned int major = MAJOR(inode->i_rdev);

/* position */

int pos;

/* pointer to private data structure */

char * dev;

char * data;

dev = (char *) filep->private_data;

pos = (int) filep->f_pos;

if(pos >= MAX_DATA) {

return -ENOMEM;

임베디드 리눅스 프로그래밍 90

}

if(pos + count >= MAX_DATA){

return -ENOMEM;

}

data = &dev[pos];

COPYFROMUSER(data,buf,count);

return count; /* success !!! */

}

/* seek code */

static int samp_seek(struct inode * inod, struct file *filep,

off_t offset, int origin)

{

/* determine Major/Minor Number if we need them */

// unsigned int minor = MINOR(inode->i_rdev);

// unsigned int major = MAJOR(inode->i_rdev);

int pos;

char *dev;

pos = offset;

if(pos >= MAX_DATA){

return -EINVAL;

}

if(origin != SEEK_SET){

return -EINVAL;

}

filep->f_pos = pos;

return 0; /* success !! */

임베디드 리눅스 프로그래밍 91

}

/* extra ioctl */

int samp_ioctl(struct inode* inode, struct file * filep,

unsigned int ioint, unsigned long iolong)

{

return 0;

}

/* handle registration */

static struct file_operations samp_fops = { NULL, };

/* init fops structure */

static void samp_register(void)

{

int res;

samp_fops.lseek=samp_seek;

samp_fops.read=samp_read;

samp_fops.write=samp_write;

samp_fops.ioctl=samp_ioctl;

samp_fops.open=samp_open;

samp_fops.release=samp_release;

res=register_chrdev(SAMP_MAJOR, SAMP_NAME, &samp_fops);

#ifdef CONFIG_PROC_FS

proc_register_dynamic(&proc_root, &samp_proc_entry);

#endif

samp_init_data();

return;

}

/* unregister */

static void samp_unregister(void)

{

임베디드 리눅스 프로그래밍 92

#ifdef CONFIG_PROC_FS

proc_unregister(&proc_root, samp_proc_entry.low_ino);

#endif

unregister_chrdev(SAMP_MAJOR, SAMP_NAME);

return;

}

/* kernel interface */

int samp_misc_init(void){

printk(“ Setting up sample misc device…”);

samp_register();

printk(“...done\n”);

return 0;

}

/* driver module code */

#ifdef MODULE

int init_module()

{

samp_register();

return 0;

}

void cleanup_module(void)

{

int i;

samp_unregister();

/* use print k to seek our data */

for(i=0; i<4; i++){

printk(“index %d data %d \n”, i,(int)samp_data[i]);

}

return;

}

#endif

임베디드 리눅스 프로그래밍 93

5.2.8.3 시험용 사용자 코드 예

/** Sample test code /mnt/train/code/v20/stest.c

#include <stdio.h>

#include <stdlib.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

int main(int argc, char * argv[])

{

int fdr0, fdw;

char buf0[8];

int i;

fdr0 = open(“/dev/samp0”,O_RDONLY);

printf(“ fdr0 = %d \n”,fdr0);

if(fdr0 <= 0) exit(1);

read(fdr0, &buf0[0],4);

printf(“ we are all ok \n”);

for( i=0; i<4; i++){

printf(“samp0 index %d data %x\n”,i,buf0[i]);

buf0[i]++;

}

fdw = open(“/dev/samp0”,O_WRONLY);

printf(“fdw = %d \n”,fdw);

if(fdw <= 0) exit(1);

write( fdw, &buf0[0],4);

close(fdr0);

close(fdw);

}

임베디드 리눅스 프로그래밍 94

5.2.8.4 Makefile

TARGDIR = /home/test/bin

CROSS = m68k-pic-coff-

CC = $(CROSS)gcc

AR = $(CROSS)ar

LD = $(CROSS)ld

NM = $(CROSS)nm

RANLIB = $(CROSS)ranlib

CFLAGS = -O2 -g -fomit-frame-pointer

all: stest samp_misc.o

stest: stest.c

$(CC) $(CFLAGS) -o stest stest.c

clean:

rm -f stest.coff stest

5.3 인터럽트

uClinux에서 인터럽트와 인터럽트 벡터를 사용하는 실습을 하자.

5.3.1 하드웨어 Base Vector 설정

인터럽트가 일어나게 하려면 초기화 코드에서 벡터테이블을 설정하도록 해야 한다. 먼저 전

체적인 과정을 설명한다. Vector Base Register(VBR)는 리눅스에서 _ramvec 이라고 불리는

/opt/uClinux/arch/m68knommu/platform/68EZ328/ints.c 의 M68EZ328_init_IRQ

함수안에 설정되어 있다. 그 데이터는 link time에 해당 장소로 보내진다.

/opt/uClinux/linux/arch/m68knommu/platform/68EZ328/ucsimm/r[oa].ld

xxxx_init_IRQ 함수는

임베디드 리눅스 프로그래밍 95

/opt/uClinux/linux/arch/m68knommu/platform/68EZ328/ints.c

안의 mach_init_IRQ에 매핑된다.

mach_init_IRQ는 init_IRQ의 한 부분으로서

/opt/uClinux/linux/arch/m68knommu/platform/68EZ328/traps.c

안에서 호출되는데 /opt/uClinux/init/main.c 가 호출하며, 결국 다음과 같이 시작한다.

/opt/uClinux/linux/arch/m68knommu/platform/68EZ328/ucsimm/crt0_r[ad]m.S:

jsr start_kernel

이상의 과정을 다시 상세히 살펴보자.

/opt/uClinux/linux/arch/m68knommu/platform/68EZ328/ucsimm/r[oa]m.ld 를 살펴보면 링

크될 때 데이터가 해당한 메모리 공간으로 보내진다.

MEMORY

{

romvec : ORINGIN = 0x04020000, LENGTH = 0x00400

flash : ORINGIN = 0x04020400, LENGTH = 0xDFC00

eflash : ORINGIN = 0x04100000, LENGTH = 1

ramvec : ORINGIN = 0x00000000, LENGTH = 0x00400

/* ram : ORINGIN = 0x00020000, LENGTH = 0x0000FFFF-0x20000 */

ram : ORINGIN = 0x00000400, LENGTH = 0x0000FFFF - 0x400

eram : ORINGIN = 0x00100000, LENGTH = 1

}

SECTIONS

{

.romvec :

{

_romvec = . ;

__rom_start = . ;

} > romvec

.text :

{

text_start = . ;

*(.text)

_etext = . ;

임베디드 리눅스 프로그래밍 96

__data_rom_start = ALIGN(4);

} > flash

.eflash :

{

_flashend = . ;

_romdisk_start = . ;

} > eflash

.ramvec :

{

__ram_start = . ;

_ramvec = . ;

} > ramvec

.data :

{

_sdata = . ;

__data_start = . ;

*(.data)

_edata = . ;

edata = ALIGN(0x10);

} > ram

.bss :

{

_sbss = ALIGN(0x10);

__bss_start = ALIGN(0x10);

__data_end = ALIGN(0x10);

*(.bss)

*(COMMON)

_ebss = . ;

__bss_end = . ;

end = ALIGN(0x10);

_end = ALIGN(0x10);

} > ram

.eram :

{

_ramend = . ;

} > eram

임베디드 리눅스 프로그래밍 97

}

함수 xxxx_init_IRQ 는

/opt/uClinux/linux/arch/m68knommu/platform/68EZ328/ints.c

안의 mach_init_IRQ에 맵핑되어 있다. mach_init_IRQ 함수는

/opt/uClinux/linux/arch/m68knommu/platform/68EZ328/traps.c

의 init_IRQ 함수가 호출한다.

void init_IRQ(void)

{

int i;

for( i=0; i < SYS_IRQS; i++) {

if(mach_default_handler)

irq_list[i].handler = (*mach_default_handler)[i];

else

irq_list[i].handler = NULL;

irq_list[i].falgs = IRQ_FLG_STD;

irq_list[i].dev_id = NULL;

irq_list[i].devname = default_names[i];

}

for( i=0; i < NUM_IRQ_NODES; i++)

nodes[i].handler = NULL;

mach_init_IRQ();

}

위 init_IRQ 함수는 /opt/uClinux/init/main.c에서 호출된다.

asmlinkage void start_kernel(void)

{

char * command_line;

/*

* This little check will move.

*/

#ifdef __SMP__

임베디드 리눅스 프로그래밍 98

static int first_cpu =1;

if( !first_cpu)

start_secondary();

first_cpu=0;

#endif

/*

* Interrupts are still disabled. Do necessary setups,

* then enable them

*/

setup_arch(&command_line, &memory_start, &memory_end);

memory_start = paging_init(memory_start,memory_end);

trap_init();

init_IRQ();

sched_init();

이상의 일련의 함수 호출은 아래의 명령으로 시작된다.

/opt/uClinux/linux/arch/m68knommu/platform/68EZ328/ucsimm/crt0_rom.S:

jsr start_kernel

5.3.2 인터럽트의 처리

벡터들의 내용이 /opt/uClinux/linux/arch/m68knommu/platform/68EZ328/entry.S 안에 정

의 되어 있다. entry.S 는 주 인터럽트 루틴인 process_int를 호출한다. process_int는

/opt/uClinux/linux/arch/m68knommu/platform/68EZ328/traps.c 에 정의 되어 있다.

traps.c 는 mach_process_int 를 찾는다.

/opt/uClinux/linux/arch/m68knommu/platform/68EZ328/inits.c

:mach_process_int = M68EZ328_do_irq;

결국 IRQ 서비스 루틴은

int_irq_list[irq]->handler 안의 일반적인 함수 형태로 정의되고 호출된다.

상세한 과정을 살펴보자.

벡터들이 설정된 entry.S를 살펴보자.

/opt/uClinux/linux/arch/m68knommu/platform/68EZ328/entry.S

임베디드 리눅스 프로그래밍 99

/*

** This is the main interrupt handler, responsible for calling

process_int()

*/

SYMBOL_NAME_LABEL(inthandler1)

SAVE_ALL

oriw #0x700,%sr

moveq #-1,%d0

movel %d0, %sp@(LORIG_D0)

/* a -1 in the ORIGD0 field */

/* signifies that the stack frame */

/* is NOT for syscall */

addq1 #1,SYMBOL_NAME(intr_count)

/* put exception # in d0 */

/* movel %sp@(LFORMATVEC),%d0 */

/* lsr #4, %d0 */

movew %sp@(LFORMATVEC), %d0 /* LFORMATVEC는 인터럽트 벡터 */

and #0x3ff, %d0

/* bfextu %sp@(LFORMATVEC){#4,#10},%d0 */

movel %sp, %sp@-

movel #65, %sp@- /* put vector # on stack (was %d0) */

jbsr SYMBOL_NAME(process_int) /* process the IRQ */

addql #8, %sp /* pop parameters off stack */

bra ret_from_interrupt

/* this was fallthrough */

이 함수는 주 인터럽트 루틴인 process_int를 호출한다. process_int 함수는

/opt/uClinux/linux/arch/m68knommu/platform/68EZ328/traps.c

에 정의 되어 있다.

asmlinkage void process_int(unsigned ling vec, struct pt_regs *fp)

임베디드 리눅스 프로그래밍 100

{

/* give the machine specific code a crack at it first */

if(mach_process_int)

if(!mach_process_int(vec,fp))

return;

if(vec < VEC_SPUR || vec > VEC_INT7)

panic(“No interrupt handler for vector %ld\n”,vec);

vec -= VEC_SPUR;

kstat.interrupts[vec]++;

if(irq_list[vec].handler)

irq_list[vec].handler(vec,irq_list[vec].dev_id, fp);

else

panic(“No interrupt handler for autovector %ld\n”, vec);

}

이 함수는 mach_process_int 값을 이용하는데 이것은

/opt/uClinux/linux/arch/m68knommu/platform/68EZ328/ints.c 에서

mach_process_int = M68EZ328_do_irq;

와 같이 설정된다. 이 파일에는 다음의 코드도 가지고 있다.

/* The 68k family did not have a good way to determine the source of

interrupts until later in the family. The EC000 core does not provide

the vector number on the stack, we vector everything into one vector

and look in the blasted mask register...

This code is designed to be fast, almost constant time, not clean! */

inline int M68EZ328_do_irq(int vec, struct pt_regs *fp)

{

int irq;

int mask;

unsigned long pend =*(volatile unsigned long*) 0xfffff30c;

while(pend){

if (pend & 0x0000ffff){

if(pend & 0x000000ff){

임베디드 리눅스 프로그래밍 101

if(pend & 0x0000000f){

mask = 0x00000001;

irq = 0;

}else{

mask = 0x00000010;

irq = 4;

}

}else{

if(pend & 0x00000f00){

mask = 0x00000100;

irq = 8;

}else{

mask = 0x00001000;

irq = 12;

}

}

}else{

if(pend & 0x00ff0000){

if(pend & 0x000f0000){

mask = 0x00010000;

irq = 16;

}else{

mask = 0x00100000;

irq = 20;

}

}else{

if(pend & 0x0f000000){

mask = 0x01000000;

irq = 24;

}else{

mask = 0x1000000;

irq = 28;

}

}

}

while(!(mask & pend)){

임베디드 리눅스 프로그래밍 102

mask <<=1;

irq++;

}

if(int_irq_list[irq] && int_irq_list[irq]->handler){

int_irq_list[irq]->handler(irq|IRQ_MACHSPEC,

int_irq_list[irq]->dev_id, fp);

int_irq_count[irq]++;

}else{

printk(“unregistered interrupt %d!\n”, irq);

printk(“Turning it off in the IMR... \n”);

*(volatile unsigned long*)0xfffff304 |= mask;

}

pend &= ~mask;

}

return 0;

}

실제 IRQ 서비스루틴은 int_irq_list[irq]->handler 함수에서 끝나게 된다.

5.3.3 인터럽트 번호

인터럽트는 고유한 번호를 가진다. 인터럽트 번호(Number)는 단지 mask 레지스터에 액세

스하면 알 수 있으며 irq 의 비트 번호에 의해 지정된다.

*(volatile unsigned long*)0xfffff304 |= mask

따라서 인터럽트를 활성화/비활성화 방법은 아래 코드와 같다. 이 코드에서 printk는 시

스템을 다운시킬 수 있음을 유의하자.

void M68EZ328_disable_irq(unsigned int irq)

{

ir(irq >= INTERNAL_IRQS){

printk(“%s: Unknown IRQ %d\n”, __FUNCTION__,irq);

return;

}

if(int_irq_ablecount[irq]++)

retrun;

임베디드 리눅스 프로그래밍 103

/* disable the interrupt */

*(volatile unsigned long*)0xfffff304 |= 1<<irq;

}

5.4 타이머

리눅스 커널은 타이머 함수를 가지고 있다. 이것은 커널 기능이 어떤 시간 지연 후에 실행

되게 만들 수 있다. 리눅스의 기본 시간 단위는 JIFFIES 카운트이다. 이것은 부팅 후에 진

행되는 카운터이고, 거의 모든 하드웨어가 10밀리초(millisecond) 마다 한번씩 증가하도록

만들어져 있다. 타이머 액세스하는 인터페이스는 다음과 같다.

* 타이머를 관리하는 함수.

init_timer(&testtimer);

add_timer(&testtimer);

del_timer(&testtimer);

* 타이머 함수 설정

testtimer.function

* 함수 데이터 설정

testimer.function = timerfun;

* 타이머 지연 설정

Testtimer.expires(in jiffies or 10mS time slots)

testtimer.expires = jiffies +2;

이 함수들은 아래 타이머 데이터 구조를 이용한다.

struct timer_list{

struct timer_list *next;

struct timer_list *prev;

unsigned long expires;

unsigned long data;

void (*function) (unsigned long);

};

아래 코드는 현재 CPU시간에 기초해 타이머 종료 시간을 타이머 함수에 설정한다.

struct timer_list testtimer;

init_timer(&testtimer);

임베디드 리눅스 프로그래밍 104

testtimer.expires = jiffies +2;

testtimer.data = &testtimer;

testtimer.function = timerfun;

add_timer(&testtimer);

반복적으로 타이머를 설정해야 하는 경우, 타이머 서비스 루틴은 매번 타이머 큐에 넣어야

한다.

static void timerfun(unsigned long tdata)

{

struct timer_list *h = (struct timer_list*) tdata;

h->expires = jiffies +2;

add_timer(h);

misc_flags++;

}

5.5 시스템 콜

커널 위에서 수행되는 응용프로그램에서 리눅스 커널과 인터페이스하는 수단이 시스템 콜

(system call) 이다. 시스템 콜은 소프트웨어 인터럽트로서, 사용자 프로그램을 실행하다가

커널모드로 변환하게 하고 사용자 요청에 해당하는 커널코드를 실행하게 한다. 메모리 관리

기능이 있는 운영체제에서 시스템 콜은 사용자 주소공간에서 커널 주소공간으로 이동하게

한다.

unistd.h라 불리는 파일은 주어진 시스템콜에 대한 주요 매핑 관계를 알려준다. 일반적으로

운영체제 시스템에 새로운 시스템콜을 추가하는 것은 쉬운 작업이다. 본 절에서 그 과정을

실습하자.

5.5.1 Syscall 추가하기

기초적인 단계는 다음과 같다.

① 새로운 시스템 호출 함수를 커널에 구현한다.

시스템 호출 처리 함수들 중에서 태스크 관리자와 관련된 함수들은 리눅스 커널 소스 트리

중에서 kernel/ 디렉토리에, 파일 시스템과 관련된 함수들은 fs/ 디렉토리에 구현되는 것이

일반적이다. 이 예에서 구현한 함수는 kernel/ 디렉토리에 newcall.c 라는 이름의 파일이다.

아래의 함수를 구현해 보자.

임베디드 리눅스 프로그래밍 105

/* /opt/uClinux/linux/kernel/newcall.c */

#include <linux/unistd.h>

int sys_myhello(int i)

{

printk(“Hello World!, I am in Kernel.\n”);

return 0;

}

위 그림에서 구현한 새로운 시스템 호출 처리 함수의 이름이 sys_myhello()임에 주의하라.

리눅스 커널은 시스템 호출을 처리하는 함수의 경우 sys_라는 접두어를 붙이는 전통이 있

으며, 이 예에서도 그 전통을 따른 것이다. 그리고 sys_myhello()의 반환 값(return value)이

정수이기 때문에 int 키워드가 함수 이름 앞에 붙어 있다 (시스템 호출 처리 함수 대부분의

반환 값은 정수이다).

sys_ myhello() 함수가 호출되면, 이 함수는 단지 문자열을 터미널에 출력하는 일만 수행한

다. 이때 사용한 함수 이름이 printk() 임에 주의하라. sys_ myhello()은 커널 수준에서 수행

되는 함수이므로 사용자 수준에서 수행되는 표준 C 라이브러리를 사용할 수 없다. 따라서

리눅스 커널은 printf()와 비슷한 일을 수행하는 printk()라는 커널 라이브러리를 제공하며,

우리가 구현한 sys_ myhello()은 이 함수를 사용한 것이다.

② sys_exit을 변환한다. (선택사항)

/opt/uClinux/linux/kernel/exit.c

NORET_TYPE void do_exit(long code)

.

.

current -> exit_code = code;

exit_notify();

#ifdef DEBUG_PROC_TREE

audit_ptree();

#endif

----- here perhaps

.

.

③ linux/unistd.h 에 추가할 호출에 대한 고유번호(call number)를 지정한다.

임베디드 리눅스 프로그래밍 106

리눅스 커널이 제공하는 모든 시스템 호출은 각각 고유한 번호를 갖는다. 그리고 이 정보는

include/asm/unistd.h 파일에 구현되어 있으며 그 내용은 아래와 같다.

exit() 시스템 호출은 번호 1에, fork() 시스템 호출은 번호 2에, 그리고 read() 시스템 호출

은 번호 3에 대응됨을 알 수 있다. 또한 mremap()라는 시스템 호출은 번호 163번에 대응

됨을 알 수 있다. (리눅스에서는 시스템 호출 번호 앞에 __NR_라는 접두어가 붙는다). 리눅

스에서는 최대 256개까지 시스템 호출을 지원할 수 있으므로 현재 정의된 호출 번호 다음

부터 255번까지는 미래에 구현될 새로운 시스템 호출을 위해 유지하고 있는 번호이다.

따라서 우리가 만들 새로운 시스템 호출인 newsyscall()은 164번부터 255번까지 임의의 번

호를 사용할 수 있으며, 이 예에서는 164번을 사용하겠다.

/opt/uClinux/linux/include/asm/unistd.h 파일의 내용

#define __NR_exit 1

#define __NR_fork 2

#define __NR_read 3

...

#define __NR_nanosleep 162

#define __NR_mremap 163

/* 수정된 include/asm_i386/unistd.h 파일의 내용 */

#define __NR_exit 1

#define __NR_fork 2

#define __NR_read 3

...

# define __NR_mremap 163

#define __NR_myhello 164 /* 추가된 내용 */

④ sys_call_table에 새로운 엔트리 하나를 추가한다.

그 다음 해야 할 일은 시스템 호출 테이블에 새로운 시스템 호출 처리 함수를 등록하는

것이다. 리눅스 커널이 제공하는 모든 시스템 호출 처리 함수는 sys_call_table 이라는

테이블에 등록이 되어야 한다. sys_call_table 정보는

arch/m68knommu/platform/68EZ328/entry.S 파일에 구현되어 있으며 그 내용은 아래

그림과 같다. arch/m68knommu/platform/68EZ328/entry.S 파일에 구현된 내용을 기반으로

그림 우측에 보이는 시스템 호출 테이블(sys_call_table)을 그릴 수 있다. 각 테이블의

엔트리에는 시스템 호출 처리 함수 시작점 주소가 들어있고, 각 엔트리는 각 시스템 호출

번호를 인덱스로 접근된다. 예를 들어 fork()라는 시스템 호출은 커널 내에서 sys_fork()라는

임베디드 리눅스 프로그래밍 107

이름의 함수로 구현되어 있으며, 번호 2 을 인덱스로 접근할 수 있는 엔트리에 이 함수가

등록되어 있다. (fork() 시스템 호출의 번호가 2 번이었음을 상기하라)

/opt/uClinux/linux/arch/m68knommu/platform/68EZ328/entry.S

.long SYMBOL_NAME(sys_mremap)

.long SYMBOL_NAME(sys_mremap) /* 추가된 내용 */

.space (NR_syscalls-164)*4

sys_myhello()….

sys_call_tablesys_ni_syscall()sys_exit()sys_fork()sys_read()sys_write()…..

0

164

255

/* arch/…../entry.S 파일의 내용 */

#define __NR_exit 1ENTRY(sys_call_table).long SYMBOL_NAME(sys_ni_syscall) /* 0 */.long SYMBOL_NAME(sys_exit) /* 1 */.long SYMBOL_NAME(sys_fork) /* 2 */.long SYMBOL_NAME(sys_read) /* 3 */…..long SYMBOL_NAME(sys_myhello) /* 164 */.space (NR_syscalls-164)*4

/* arch/…../entry.S 파일의 내용 */

#define __NR_exit 1ENTRY(sys_call_table).long SYMBOL_NAME(sys_ni_syscall) /* 0 */.long SYMBOL_NAME(sys_exit) /* 1 */.long SYMBOL_NAME(sys_fork) /* 2 */.long SYMBOL_NAME(sys_read) /* 3 */…..long SYMBOL_NAME(sys_myhello) /* 164 */.space (NR_syscalls-164)*4

sys_myhello()….

sys_call_tablesys_ni_syscall()sys_exit()sys_fork()sys_read()sys_write()…..

0

164

255

/* arch/…../entry.S 파일의 내용 */

#define __NR_exit 1ENTRY(sys_call_table).long SYMBOL_NAME(sys_ni_syscall) /* 0 */.long SYMBOL_NAME(sys_exit) /* 1 */.long SYMBOL_NAME(sys_fork) /* 2 */.long SYMBOL_NAME(sys_read) /* 3 */…..long SYMBOL_NAME(sys_myhello) /* 164 */.space (NR_syscalls-164)*4

/* arch/…../entry.S 파일의 내용 */

#define __NR_exit 1ENTRY(sys_call_table).long SYMBOL_NAME(sys_ni_syscall) /* 0 */.long SYMBOL_NAME(sys_exit) /* 1 */.long SYMBOL_NAME(sys_fork) /* 2 */.long SYMBOL_NAME(sys_read) /* 3 */…..long SYMBOL_NAME(sys_myhello) /* 164 */.space (NR_syscalls-164)*4

⑤ 커널 Makefile을 추가하고 커널을 컴파일한다.

커널을 새로 컴파일한 후 새로 생성된 image.bin을 flashloader로 uCsimm으로 옮긴다.

/opt/uClinux/linux/kernel/Makefile

OBJS=sched.o dma.o fork.o exec_domain.o panic.o printk.o \

sys.o module.o exit.o signal.o itimer.o info.o time.o \

softirq.o resource.o sysctl.o newcall.o

⑥ unistd.h에 정의를 추가한다.

/opt/uClinux/linux/include/asm/unistd.h

static inline_syscall1(int, myhello, int, exitcode)

inline_systemcall1에서 “call” 뒤에 “1”의 의미는 파라미터가 하나인 시스템 콜이란 뜻이다.

임베디드 리눅스 프로그래밍 108

앞에서 1번 과정에서 sys_myhello를 정의할 때 int 타입의 파라미터가 하나 있었음을 기억

하자. inline_systemcall1의 파라미터 중 첫 int 는 sys_myhello의 타입이고 세번째 파라미터

의 int는 sys_myhello의 파라미터의 타입이다.

지금까지 새로운 시스템 호출을 위한 커널 수정에 대하여 설명하였다. 그럼 이제부터

새로운 시스템 호출을 이용하는 사용자 수준 응용을 구현해 보자. 사용자 수준의 응용을

구현하는 방법은 라이브러리를 이용하지 않는 방법과 새로운 라이브러리를 이용하는

방법으로 구분할 수 있다. 여기에서는 라이브러리를 이용하지 않는 방법을 설명한다. vi

같은 편집기를 이용해 아래와 같은 응용 프로그램을 만들어 보자.

#include </opt/uClinux/linux/include/asm/unistd.h>

_syscall1(int, myhello, int, exitcode);

int main(int argc, char * argv [])

{

myhello(23);

return 0;

}

위 예제에 구현된 프로그램은 시스템 호출을 부르게 위해 _syscall1() 이라는 매크로를 사용

한다. 한편, 이 매크로는 include/asm/unistd.h에 구현되어 있기 때문에 프로그램에서 이

헤더 파일을 삽입하였다. 이 매크로는 파라미터가 하나 있기 때문에 그 이름이 _syscall1()

이다. 파라미터가 0개 또는 2개일 경우에는 _syscall0() 또는 _syscall2() 이라는 매크로를

사용해야 한다. 이 매크로는 트랩 처리 메커니즘을 이용해 커널 수준으로 진입한다

이제 이 프로그램을 “gcc"를 이용해 a.out으로 만들어 실행시켜 보자. 콘솔에 ‘Hello World!,

I am in Kernel’ 이라는 메시지가 출력되면 지금까지 작성한 새로운 시스템 호출 구현이 성

공한 것이다.

임베디드 리눅스 프로그래밍 109

6 uClinux 웹 서버

uCsimm에서 간단한 웹서버로 동작하도록 httpd가 제공된다. 이 코드는

/home/train/work/src/httpd 에 들어 있으며 디렉토리들, HTML 파일들, JPEG 파일들, GIF

파일들 및 텍스트 파일들을 다룰 수 있다. 428 라인의 짧은 프로그램이지만 제대로 구현되

어 있고 쉽게 확장 가능한 프로그램이다.

httpd의 구성과 기능을 살펴보자.

프린트 헤더 : Content Type 메시지를 제공한다.

DoJpeg, DoGif, DoHTML, DoText : 해당 헤더를 제공하며 파일을 출력장치로 복

사한다.

DoDir : 디렉토리를 HTML 페이지로 parse 한다.

ParseReq : request에서 파일명과 하나의 argument를 추출한다. 만일 파일명이

주어지지 않은 경우 request를 디렉토리로 간주한다. Jpeg, Gif, Text 또는 HTML

에 대한 request를 알아낸다.

HandleConnect : 요청한 클라이언트로부터 데이터를 얻어 content 길이와

Referrer를 디코드한 후 ParseReq를 호출한다.

Main : inetd call 인지 독자적인 것인지를 알아낸다. 그 후 root를

HTTPD_DOCUMENT_ROOT로 바꾼 다음 필요한 경우(inetd 기반이 아닌 경우)

socket 연결을 설정한다. 그 후 HandleConnect를 부른다.

이 httpd 프로그램을 이해한 후 타겟시스템에서 실행시켜 보자.

1. 포트 8000번에서 실행되도록 myhttpd 프로세스를 생성한다.

2. 타겟시스템에서 데몬 프로세스를 테스트한다.

3. debug 메시지를 프린트한다.

임베디드 리눅스 프로그래밍 110

7 부록

7.1 Make 사용법

이 프로그램은 많은 unix 응용프로그램을 간편하게 컴파일시키는 방법이다. 간단한

Makefile을 만들어 보자.

7.1.1 간단한 Makefile

현재 디렉토리에 “Makefile” 이라는 이름의 파일을 생성한다. 그리고 에디터로 아래 내용을

입력하자.

foo.o:foo.c

이것은 foo.c가 갱신되었을 경우만 컴파일 되어 foo.o가 생성된다는 것을 나타낸다. foo.o

가 존재하지 않을 때는 foo.o를 생성한다.

echo “foo.o:foo.c” > Makefile

touch foo.c foo.c 파일 생성하거나 갱신

make cc –c –o foo.o foo.c 실행됨.

7.1.2 Target 만들기

Make는 일반적으로 여러 개의 target들을 지정한다. 위의 간단한 예제에서 foo.o는 target

이다. 그리고 그것은 또한 make를 실행시킬 때 사용되는 디폴트 target이다.

이제 다른 target을 추가해 보자.

Makefile 파일에 다음의 라인을 추가한다.

clean :

rm foo.o # note the it won’t work without

이때 rm 전에 꼭 tab키 사용해야 한다. 다음 이것을 실행시킨다.

make clean clean : 부분의 rm foo.o 가 실행된다.

make 첫 target 즉 cc –c –o foo.o foo.c 실행된다.

7.1.3 Target list

임베디드 리눅스 프로그래밍 111

Makefile 내에서 target이 여러 파일일 경우

OBJS = file1.o file2.o file3.o file4.o

같은 방법으로 여러 파일들을 한 기호에 연관시키면 된다. 위 라인을 Makefile 맨위에 추가

해 보자. 그러면 위 OBJS는 다음과 같이 사용할수 있다.

clean :

rm $(OBJS)

7.1.4 Flags 와 switches

컴파일러들의 기본적인 특성은 flag과 switch들에 의해 설정될 수 있다는 것이다.

CFLAGS := -g

위의 라인을 Makefile 맨 위에 추가해보자.

1. make clean을 실행시킨후, make foo를 실행시켜보자

2. make clean을 실행시킨후, make foo CFLAGS=-O2 를 실행시켜보자

그리고 두 결과를 비교해 본다.

7.1.5 조건문

Make는 또한 조건문(conditionnal statement)들을 사용할 수 있다.

linux:

ifeq ($(KERNEL),src)

echo Making a local copy of the kernel source

(cd /opt/xxlinux; tar –cpf – linux) | tar –xpf –

rm –f .kernel_stamp

else

echo Setting up a link to the kernel binaries

ln –sf /opt/xxlinux/linux linux

touch .kernel_stamp

endif

이것은 변수 KERNEL 의 값들을 테스트하고 결과에 따라 여러가지 실행을 할 것이다.

make KERNEL=src 하면 위 작업이 수행된다.

임베디드 리눅스 프로그래밍 112

7.1.6 Shell interface

Shell은 make 프로세스를 위한 정보를 모으는데 사용한다.

RTSYS = $(shell test –f /usr/src/rtai/rtai; echo $$? )

ifeq($(RTSYS),0)

# add these to RTFLAGS if you have rtai-0.9 :

# -DRTAI_PROC –DRTAI_CONIO

RTFLAGS = -DRTAI

CFLAGS = -Wall –O2 –D__SMP__ -fno-schedule-insns2

RTINC = -I/usr/src/rtai/include

else

RTFLAGS = -D_RTL_

RTDIR = /usr/src/rtlinux/rtl

CFLAGS = -Wall –O2 –fno –schedule –insns2

RTINC = -I/usr/src/rtlinux/rtl/include

endif

7.2 Patch 사용법

이 프로그램들은 소스 파일들 패키지를 효과적으로 관리하고 배포하는데 쓰인다. diff 프로

그램을 이용하여 두 소스 프로그램들의 트리들의 차이점을 검사하고 한 트리를 다른 트리로

변환하는데 이용할 “patches”의 셋을 생성한다. patch 프로그램은 소스 트리에 변경 사항을

적용해서 새로운 트리를 생성할 수 있다. 그래서 리눅스 소스는 일반적으로 patch를 통해

변경된다.

patch를 하는데 사용되는 리눅스의 두가지 유틸리티를 살펴보자.

7.2.1 Patch

patch는 어떤 소스 트리에 patch 내용을 적용시켜 새 소스 트리를 만들 때 이용한다.

patch는 원래의 트리 혹은 패치된 트리에 적용될 수 있다.

7.2.2 Diff

diff는 패치파일을 생성하는데 사용된다.

임베디드 리눅스 프로그래밍 113

mkdir old

mkdir new

echo “ this is the old file” > old/file

echo “ this is the new file” > new/file

[philw@refsys2 patch]$ diff –urN old new

diff –urN old/file new/file

--- old/file Thu Aug 31 10:10:54 2000

+++ new/file Thu Aug 31 10:11:00 2000

@@ -1 +1 @@

- this is the old file

+ this is the new file

위 내용은 모니터로 출력되는데 파일로 출력하려면 리디렉션(“>”)을 사용한다.

[philw@refsys2 patch]$ diff –urN old new > patch.diff

패치 파일이 만들어졌으므로 예전 파일에서 새 파일을 또는 새 파일에서 예전 파일을 얻을

수 있다.

우선 new 파일을 지운다.

[philw@refsys2 patch]$ rm –rf new

예전파일에서 새파일을 생성한다.

[philw@refsys2 patch]$ patch old/file < patch.diff

patching file ‘old/file’

[philw@refsys2 patch]$ more old/file

this is the new file

반대의 경우를 실행해보자.

[philw@refsys2 patch]$ pathch old/file < patch.diff

patching file ‘old/file’

Reversed (or previously applied) patch detected! Assume–R?[n] y

임베디드 리눅스 프로그래밍 114

[philw@refsys2 patch]$more old/file

this is old file

7.2.3 예제

보다 복잡한 예제를 실습하자. 이 예제에서 수행할 내용과 실제 커널에서의 패치와 다른점

은 단지 어디에서부터 패치를 적용하냐는 것이다. 일반적으로 patch는 패치된 트리의 디렉

토리 안에서 적용된다. 다음을 실행해보자.

[philw@refsys2 patch]$ cd old

[philw@refsys2 patch]$ more file

this is the old file

-p1 옵션을 사용해 보자. 이것은 첫번째 디렉토리 레벨을 건너뛴다는 것(old 디렉토리로 한

레벨 내려왔으므로)을 의미한다.

[philw@refsys2 old]$ patch –p1 < ../patch.diff

pathcing file ‘file’

[philw@refsys2 old]$ more file

this is the new file

다시한번 반복해보자.

[philw@refsys2 old]$ patch –p1 < ../patch.diff

pathcing file ‘file’

Reversed (or previously applied) patch detected! Assume–R? [n]y

[philw@refsys2 old]$ more file

this is the old file

7.2.4 실제 사용 예제

소스 디렉토리를 마운트(mount) 해보자.

mount –t nfs 10.0.1.100: /home/test /mnt/test

/mnt/redhat/SOURCES로부터 minicom 소스를 가져오자.

Action Command

Move to home directory cd

Make a test dir mkdir minicom

Move in to the test dir cd minicom

Unpack Minicom tar -xvzf /mnt/redhat/SOURCES/minicom-

임베디드 리눅스 프로그래밍 115

1.78.src.tar.gz

Move into the working dir cd minicom-1.78

Patching Minicom config patch –p1 < /mnt/redhat/SOURCES/minicom-

1.78-config.patch

Patching Minicom make patch –p1< /mnt/redhat/SOURCES/minicom-1.78-

makefile.patch

7.2.5 문제점

패치 프로그램을 사용하면서 발생할 수 있는 에러를 맞게 된다면 그것은 패치 프로그램이

적용할 패치를 위한 키(key)들을 찾지 못하는 경우가 일반적일 것이다. 패치가 실패 되면

원래의 파일과 reject 파일을 저장한다. 이 두 파일에서 올바른 파일을 생성하는 것 사용자

에게 달렸다.

7.2.6 패치에 실패한 예

[philw@refsys2 patch]$ patch –p1 < ../patch2.diff

patching file ‘memory.c’

Hunk #1 FAILED at 9.

1 out of 1 hunk FAILED – saving rejects to memory.c.rej

This thime the patch failed and we registered a failure

Not all is lost here the patch program has tried to help us out

[philw@refsys2 patch]$ ls –l

total 12

-rw-r--r-- 1 philw philw 346 Aug 31 12:15 memory.c

-rw-r--r-- 1 philw philw 346 Aug 31 12:15 memory.c.orig

-rw-r--r-- 1 philw philw 298 Aug 31 12:15 memory.c.reg

We still have our original memory.c saved as memory.c.orig

We also have a reject file memory.c.rej

만약 reject이 소스 트리 안의 파일들에 대해 전반적으로 많이 일어난다면 그것은 패치하려

는 트리에 무언가 잘못이 있다는 것을 의미한다. 아마 잘못된 트리거나 잘못된 버전일지 모

른다.

임베디드 리눅스 프로그래밍 116

에러를 찾기 위해 “.rej” 스트링을 찾아 grep하면 된다.

patch –p1 <

/mnt/redhat/SOURCES/minicom-1.78-makefile.patch | grep .rej

7.2.7 diff 예제

이제 패치 프로세스를 관찰하는 예제를 제시한다. old 디렉토리와 new 디렉토리를 생성하고

각 디렉토리에 파일을 생성하자.

mkdir old

mkdir new

echo “ this is the old file” > old/file

echo “ this is the new file” > new/file

다음 패치 파일 patch.diff를 생성한다.

[philw@refsys2 patch]$ diff –urN old new

diff –urN old/file new/file

--- old/file Thu Aug 31 10:10:54 2000

+++ new/file Thu Aug 31 10:11:00 2000

@@ -1 +1 @@

- this is the old file

+ this is the new file

[philw@refsys2 patch]$ diff –urN old new > patch.diff

이제 old 파일에서 new 파일을, 또 역으로 new 파일에서 old 파일을 만들어 가며 디스플

레이되는 메시지와 결과를 관찰하자.

new 파일을 지운다.

[philw@refsys2 patch]$ rm –rf new

old 파일에서 new파일 생성

[philw@refsys2 patch]$ patch old/file < patch.diff

patching file ‘old/file’

임베디드 리눅스 프로그래밍 117

새로 만들어진 파일의 내용을 살펴보자.

[philw@refsys2 patch]$ more old/file

this is the new file

역작업을 수행한다.

[philw@refsys2 patch]$ patch old/file < patch.diff

patching file ‘old/file’

Reversed (or previously applied) patch detected! Assume –R? [n]y

[philw@refsys2 patch]$ more old/file

this is the old file

reject를 잡기 위해 아래 방법을 수행해 보자.

[philw@refsys2 linux-2.0.38.1pre1]$ patch –p1

< ../uClinux-2.0.38.1pre1.diff | grep rej

7.3 RPM

리눅스 소프트웨어 파일들을 버전 번호에 따라 패키지 형태로 관리하고, 배포 및 설치가

편리하도록 지원하는 기능이다. RPM 명령 사용법을 보려면 –vv 옵션을 사용하는데, 일반적

인 사용법은 아래와 같다.

rpm [option] filename or package

7.3.1 Install

rpm 파일을 설치하려면 아래와 같이 하며 이때 rpm 내 파일들 간 의존관계나 충돌관계를

조사한다.

rpm –i –vv package.xxx.i386.rpm

--force : 강제로 설치하게 된다. 보통 패키지 설치시에 현재의 패키지에 포함된

파일이 이미 다른 패키지에 의해 설치되 있을 때 충돌을 한다며 에러

가 나는데 이 옵션으로 계속 진행하게 할 수 있다. 이 옵션은 이미 있

는 파일은 덮어 쓰지 않는다. 이미 있는 파일마져 덮어 쓸려면 --

replcaefiles 를 사용하면 된다.

--vv : 인스톨에 대한 정보를 보여준다.

임베디드 리눅스 프로그래밍 118

7.3.2 Remove

설치된 패키지를 삭제한다.

rpm –e package(원하는 package 이름)

7.3.3 Query

버전 등과 같은 rpm 패키지 정보(찾는 rpm이 설치되지 않았다면 메시지 표시)를 표시한다.

rpm –q [option] package

7.3.4 Upgrade

현재 설치된 패키지보다 새로운 것으로 업데이트 할 때 사용한다.

rpm –u [option] package

7.3.5 소스 RPM

소스 RPM은 재설치 시 필요한 정보를 가지고 있다. 이 정보들은 SPEC 파일이나 하나 또

는 그 이상의 SOURCE 파일에 들어있다.

RPM SPEC 파일에는 SOURCE 파일들을 결합해서 이진(binary) 이미지로 만드는 방법에 대

한 명령들이 들어있다. 또 특별한 인스톨 명령어가 포함되어 있을 수도 있다. SOURCE 파

일과 함께 SPEC 파일은 이진 RPM 을 만드는데 필요하다.

RPM SOURCE 파일에는 텍스트 파일, tarball, 패치 파일등이 있다.

7.3.6 Package 재설치

rpm -–rebuild xxx.src.rpm

7.3.7 RPM SPEC 파일 예제

summary : TTY mode communication package ala Telix

Name : minicom

Version : 1.78

Release : 1

Copyright : GPL

Group : Applications/Communications

Source : ftp://sunsite.unc.edu/pub/Linux/apps/serialcomm

/dialout/minicom-1.78.src.t

Source1 : minicom.wmconfig

Patch0 : minicom-1.78-config.patch

Patch1 : minicom-1.78-makefile.patch

임베디드 리눅스 프로그래밍 119

BuildRoot : /tmp/minicom-%{PACKAGE_VERSION}-root

%changelog

* Sat Feb 21 1998 Marek Habersack

- updated to version 1.78

- added BuildRoot support

- added the termcap/ and terminfo/ dirs to docs

* Wed Oct 29 1997 Otto Hammersmith

- added wmconfig entries

* Tue Oct 21 1997 Otto Hammersmith

- fixed source url

* Thu Jul 10 1997 Erik Troan

- built against glibc

% description

Minicom is a communications program that resembles the

MSDOS Telix somewhat. It has a dialing directory, color,

Full ANSI and VT100 emulation, an (external) scripting

Language and more.

%prep

%setup

%patch0

%patch1

%build

cd src

make RPM_OPT_FLAGS = “$RPM_OPT_FLAGS”

%install

cd src

install –d –m 755 $RPM_BUILD_ROOT/etc

install –d –m 755 $RPM_BUILD_ROOT/usr/bin

install –d –m 755 $RPM_BUILD_ROOT/usr/man/man1

임베디드 리눅스 프로그래밍 120

install –d –m 755 $RPM_BUILD_ROOT/usr/doc/examples/minicom

make install ROOTDIR=”$RPM_BUILD_ROOT”

strip $RPM_BUILD_ROOT/usr/bin/minicom

$RPM_BUILD_ROOT/usr/bin/runscript

echo “FIX ME !!!!!!” >&2

chown root.uucp $RPM_BUILD_ROOT/usr/bin/minicom

chmod 2755 $RPM_BUILD_ROOT/usr/bin/minicom

install –d $RPM_BUILD_ROOT/etc/X11/wmconfig

install –m 644 –o root –g root

$RPM_SOURCE_DIR/minicom.wmconfig $RPM_BUILD_ROOT/etc

%files

%doc demos doc termcat terminfo

%config /etc/minicom.users

/usr/bin/minicom

/usr/bin/runscript

/usr/bin/xminicom

/usr/bin/ascii-xfr

/usr/man/man1/minicom.1.gz

/usr/man/man1/runscript.1.gz

/usr/man/man1/ascii–xfr.1.gz

/etc/X11/wmconfig/minicom

임베디드 리눅스 프로그래밍 121

8 참고 문헌

1. 안성진,정진욱,박진호,안용학, Linux 프로그래밍 기술, 양서각, 2001.

2. 이만용, 리눅스 알짜 레드햇 5.2 바이블, 정보문화사, 1999

3. M. Beck and et al., Linux Kernel Internals, 2nd Ed. Addison-Wesley, 1998.

4. LINEO Academix, uClinux Coursebook, LINEO Korea, 2001.

5. LINEO Academix, An Introduction to Embedded Programming in a Linux

Environment, LINEO Inc., 2000.

6. D. P. Bovet and M. Cesati, Understanding the Linux Kernel, O’Reilly, 2001.

7. LINUX.CO.KR, http://www.linux.co.kr