여기서는 인터럽트를 사용하는 응용으로써 키패드 디바이스 드라이버를 설명한다. 키패드는 내부에 디코더를 가지고 있어 눌려진 키에 대한 4비트 BCD 데이터를 출력하며 이출력은 4비트 래치를 거쳐 프로세서의 데이터 버스에 입력된다. 4비트를 위해 사용되는 데이터 버스의 비트 위치는 비트 15 ~ 비트 12로 가정하였다.


키패드 회로에서는 주소 0x0C00 0000에서 래치 동작신호를 발생시키기 위하여 주소 디코더에 칩 선택 신호 CS3를 사용하고있다. 또 키패드에서 발생하는 인터럽트를 처리하기 위해 인터럽트 요청 신호가 PXA 프로세서의 GPIO핀에 연결되어 있다.


key-drv.h 소스 


#include "linux/kernel.h"
#include "linux/module.h"
#include "linux/init.h"
#include "linux/types.h"
#include "linux/ioport.h"
#inlcude "unistd.h"
#include "linux/slab.h"
#include "linux/mm.h"
......


#define KEY_ADDR 0xF3000000

#define KEY_IRQ IRQ_GPIO(19)

static int KEY_MAJOR = 67;

static DECLARE_WAIT_QUEUE_HEAD(key_queue);

key_drv.h의 처음 부분에는 디바이스 드라이버 구현에 필요한 각종 헤더파일들과 인터럽트 사용에 필요한 헤더파일들이 있다.


키패드 디바이스 파일의 주번호로 67이 정의되어 있다.


다음문에서는 인터럽트가 발생할 때까지 프로세스를 대기상태로 두는 대기큐를 선언한다.


DECLARE_WAIT_QUEUE_HEAD(key_queue);


다음 문은 리눅스 커널 소스에서 해당 임베디드 보드에 대한 물리주소와 가상주소를 서로 연결해주기 위한 것이다.


#define KEY_ADDR 0xF3000000



다음은 디바이스 드라이버 실제 구현 부분인 key-drv.c 부분이다.




#include "key-drv.h"

static unsigned short *key;
static int key_data =0;

static void key_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
  key_data = (*(key) & 0xf000) >> 12;

  printk("Interrupt Service : Input Data from Keypad : %d \n",key_data);

 wake_up_interruptible(&key_queue);
}

static ssize_t key_read(struct file *filp, char *buf,size_t count,loff_t *l)
{
  int tmp;

 tmp = key_data;

  intteruptible_sleep_on(&key_queue);

 copy_to_user(buf,&key_data,sizeof(tmp));
 return 0;
}

static int key_open(struct inode *inode, struct file *filp)
{
  MOD_INC_USE_COUNT;
  printk("keypad Device Open\n");
  return 0;
}

static int key_release(Struct inode *inode, struct file *filp)
{
 MOD_DEC_USE_COUNT;
 printk("Keypad Device Released\n");
 return 0;
}
 
static struct file_operations key_fops = {
 open : key_open,
 read : key_read,
 release : key_release,


};


static int __init key_init(void)
{
  int res, result;

   if((result = register_chrdev(KEY_MAJOR,"key",&key_fops))<0){
     printk(" register_chrdev() FAIL! \n");
     return result;
   }

   if(!check_region(KEY_ADDR,2))
    request_region(KEY_ADDR, 2, "key");

  key = (unsigned short *)KEY_ADDR;

  if((res = request_irq(KEY_IRQ, &key_interrupt, SA_INTERRUPT, "key", NULL))<0)
     printk(KERN_ERR, "request_irq() FAIL!\n");

  set_GPIO_IRQ_edge(IRQ_TO_GPIO_2_80(KEY_IRQ), GPIO_FALLING_EDGE);

  return 0;
}

void __exit key_exit(void)
{
   disable_irq(KEY_IRQ);
         free_irq(KEY_IRQ,NULL);

   release_region(KEY_ADDR, 2);
   unregister_chrdev(KEY_MAJOR,"key");
}

module_init(key_init);
module_exit(key_exit);

}


블로그 이미지

종환 Revolutionist-JongHwan

github.com/alciakng 항상 겸손하자.

 

* 인터럽트 제어레지스터

 

pxa 계열 프로세서의 인터럽트 제어 방식 및 관련 레지스터는 거의 유사하다. pxa계열 프로세서의 인터럽트 컨트롤러는 내부 유닛에서 발생하는 각종 인터럽트 및 GPIO 핀을 통한 외부 장치로 부터의 인터럽트 요청(IRQ)을 모두 처리한다.

 

PXA 계열 프로세서에서의 인터럽트는 상위레벨과 하위레벨의 두 단계 구조를 가진다.

상위레벨은 인터럽트를 발생하는 장치들이며 하위레벨은 각 장치 내부의 개별적인 인터럽트 소스들이다. 이들 내부 인터럽트 소스의 구분은 각 장치 내부의 상태 레지스터를 읽으므로써 알 수 있다.

 

 

예를 들어 DMA 컨트롤러는 상위레벨 인터럽트 발생 장치인데, DMA 컨트롤러 내부에는 16개의 채널이 있고 이들 개별 채널이 각각 하위레벨 인터럽트 소스가 된다. 따라서 DMA 컨트롤러는 16개의 하위레벨 인터럽트 소스를 가진다. 또 UART는 상위레벨 인터럽트 발생 장치인데, UART 내부에서는 송신 인터럽트, 수신 인터럽트, 에러 인터럽트 등 여러개의 하위레벨 인터럽트 소스를 가지는 것이다.

 

인터럽트 기능을 제어하는 레지스터들은 다음과 같은 것들이 있다

 

Interrupt Controller Pending Register(ICPR)

 

Interrupt Controller IRQ Pending Register(ICIP)

 

Interrupt Controller FIQ Pending Register(ICFP)

 

Interrupt Controller Mask Register(ICMR)

 

Interrupt Controller Level Register(ICLR)

 

Interrupt Controller Control Register(ICCR)

 

 

위의 인터럽트 컨트롤러 레지스터 중 ICPR 레지스터는 인터럽트 발생 시 해당 비트가 "1"로 된다.

 

인터럽트 소스로 IS 8 ~ IS 31 까지가 정의되어 있고 해당 인터럽트 장치(소스)에서 인터럽트 발생 시 해당 비트가  "1"로 되며 커널에서는 이를 보고 해당 장치의 인터럽트 핸들러를 찾을 수 있게 된다. 해당 장치의 인터럽트 핸들러에서는 해당 인터럽트 장치의 내부 레지스터를 접근해서 실제 인터럽트를 발생한 하위레벨 인터럽트 소스를 찾은 다음 여기에 대한 처리를 한다.

 

인터럽트 소스 IS 11은 USB 장치이며 하위레벨 인터럽트를 7개 가지는 것을 알 수 있고, 인터럽트 소스 IS 10 PXA 프로세서의 GPIO 핀 2~80으로부터의 외부 인터럽트이며 각 핀마다 인터럽트를 가지므로 총 79개의 하위레벨 인터럽트를 가짐을 알 수 있다. GPIO 총 79개 핀 중 하나라도 인터럽트가 발생하면 인터럽트 소스 IS 10은 "1"로 되며, 인터럽트 소스 IS 10에 대한 인터럽트 핸들러에서 GPIO 어느 핀에서 인터럽트가 발생하였는지 검색하여 여기에 대한 인터럽트 처리를 하는 것이다.

 

ICLR 레지스터는 발생한 인터럽트가 일반 인터럽트인 IRQ인지 고속 인터럽트인 FIQ인지 나타낸다. 해당 비트가 '0'이면 IRQ이고 "1"이면 FIQ 이다....

 

 

* 인터럽트 처리 및 등록

 

처리 : IRQ 인터럽트가 발생하면 현 프로그램 카운터 값을 링크 레지스터에 저장하고 CPSR(Current Processor Status Register) 값을 SPSR_irq(Saved Processor Status Register)에 각각 저장한다.

 

그리고 IRQ의 exception 벡터 주소로 점프하여 인터럽트 처리가 시작된다. 인터럽트 처리 완료 후에는 링크 레지스터에 저장된 복귀 주소로 리턴되고 SPSR_irq 레지스터 내용도 다시 CPSR로 복구하여 인터럽트 이전 상태로 돌아간다.

 

등록 : 인터럽트를 사용하고자 하는 디바이스 드라이버는 인터럽트 서비스 루틴을 등록하고 이는 리눅스 커널의 전역변수 irq_desc에 등록된다. 인터럽트가 발생하면 커널은 ICPR 레지스터에서 "1"인 비트를 조사해 인터럽트를 발생한 장치 번호를 얻고, do_IRQ() 함수에서 전역변수 irq_desc를 조사해 해당 인터럽트 서비스 루틴을 찾는 방식으로 동작한다.

 

* GPIO 핀을 인터럽트 입력으로 설정하기

 

PXA 계열 프로세서의 GPIO 핀들은 외부장치로부터의 인터럽트 요청을 받는 입력신호로 사용할 수 있다. 이와 같이 설정하기 위해서 필요한 PXA 계열 프로세서의 GPIO 관련 레지스터들 및 그 기능은 다음과 같다.

 

인터럽트 입력으로 설정할 때

 

GPDR 레지스터 : 인터럽트 입력으로 사용할 핀의 동작 방향을 입력으로 설정한다.

 

GAFR 레지스터 : 인터럽트 입력으로 사용할 핀은  부가기능을 OFF 시켜야 하므로 이에 대응한 비트를 "00"으로 설정한다.

 

GRER 레지스터 : 인터럽트 입력으로 사용할 핀에서 신호가 상승 에지 시 인터럽트를 검출하고자 할 때 설정한다.

 

GFER 레지스터 : 인터럽트 입력으로 사용할 핀에서 신호가 하강 에지 시 인터럽트를 검출하고자 할 때 설정한다.

 

GEDR 레지스터 : 인터럽트 신호가 들어오면 해당 핀에 대응하는 비트가 "1"로 된다.

 

예를 들어 GPIO 40번 핀을 하강 에지로 설정하려면 위의 GFER1 레지스터의 비트 8을 "1"로 해주면 되고 GPIO 28핀을 상승 에지로 설정하려면 GRER레지스터의 28을 "1"로 해주면 된다. 이와 같은 기능을 대신해주는 함수로 set_GPIO_IRQ_edge()이 있다.

 

첫 번째 인수는 인터럽트 입력으로 설정하려는 GPIO 핀 번호이고 두 번째 인수는 플랙으로서 해당 핀을 상승에지로 설정하려면 GPIO_RISING_EDGE를 하강에지로 설정하려면 GPIO_FALLING_EDGE를 각각 넣으면 된다.

 

 

블로그 이미지

종환 Revolutionist-JongHwan

github.com/alciakng 항상 겸손하자.

문자 디바이스 드라이버 프로그래밍의 예로써 LED 장치 디바이스 드라이버를 설명한다.

 

 

#include "linux/kernel.h"
#include "LINUX/module.h"
#include "LINUX/init.h"
#include "LINUX/types.h"
#include "LINUX/ioport.h"
#include "UNISTD.H"
#include "LINUX/slab.h"
#include "LINUX/mm.h"
#include "ASM/hardware.h"
......

#define LED_ADDR0xF3000C00  // 베이스 주소+LED offset 값 0x0C00

static int led_major = 63;

 

 

 

led-drv.h에는 linux/kernel.h, linux/module.h 등 디바이스 드라이버 구현에 필요한 각종 헤더 파일들이 포함되어 있다. LED 디바이스 파일의 디바이스 주 번호로 63이 정의되어 있다.

 

참고로 linux/module.h는 커널 모듈 프로그래밍에서 필요한 정의, 함수 등이 선언되어 있다.

 

linux/init.h는 module_init()과 module_exit(), 매크로등이 선언되어 있다. 

 

 

디바이스 드라이버 실제 구현부분인 led-drv.c 의 소스는 다음과 같다

#include "led-drv.h"

int led_open(struct inode *inode, struct file *filp)
{
MOD_INC_USE_COUNT;
printk("LED device open \n");

return 0;
}

static ssize_t led_write(struct file *file, const char *buffer, size_t length, loff_t *offset){

unsigned char *led_port;
size_t len = length;
int value;

get_user(value, (int *)buffer);
printk("data from application program : %d\n",value);

led_port = (unsigned char *)(LED_ADDR);
*led_port = value;
return len;
}

int led_release(struct inode *inode, struct file *filp)
{
MOD_DEC_USE_COUNT;
return 0;
}

struct file_operations led_fops = {
open : led_open,
write : led_write,
release : led_release,
};

static int __init led_init(void)
{
int result;

result = register_chrdev(led_major,"led_device",&led_fops);
if(result<0)
{
printk(KERN_WARNING "register_chrdev() FAIL!\n");
return result;
}

if(!check_region(LED_ADDR,2))
  request_region(LED_ADDR,2,"led_device");
else
printk(KERN_WARNING "check_region() FAIL!\n");

return result;
}

static void __exit led_cleanup(void)
{
   release_region(LED_ADDR, 2);
if(unregister_chrdev(led_major, "led_device"))
printk(KERN_WARNING "unregister_chrdev() FAIL!\n");

}

module_init(led_init);
module_exit(led_cleanup);


 

 

 

 

 

 

위 소스에서 마지막 줄의 module_init(led_init)와 module_exit(led_cleanup) 문에서는 커널 모듈이 로드될 때 호출되는 함수가 led_init()이고 언로드 될 때 호출되는 함수가 led_cleanup()임을 나타낸다.

 

led_init 시에 check_region(LED_ADDR,2) 문을 사용해 LED_ADDR이 가리키는 영역이 현재 입출력(I/O) 포트로 사용할 수 있는 영역인지 확인한다. check_region() 함수의 포맷은 다음과 같다.

 

int check_region(unsigned int from,unsigned int extent)

 

위에서 from 인수는 입출력 포트에 해당하는 영역의 시작 주소이고, extent는 그영역의 범위를 나타낸다.

 

check_region() 함수가 성공하면, request_region(LED_ADDR, 2, "led_device") 문을 사용해 LED_ADDR이 가리키는 메모리 영역을 입출력 포트로 사용하기 위해 할당한다.

 

할당 후에는 LED_ADDR을 사용해 디바이스에 대한 접근을 할 수 있다.

 

 

request_region() 함수의 포맷은 다음과 같다.

 

void request_region(unsigned int from, unsigned int extent, const char *name);

 

위에서 from 인수는 입출력 포트에 해당하는 메모리 영역의 시작 주소이고, extent는 그 영역의 범위를 나타내며, name은 디바이스의 이름이다. 즉 request_region() 함수는 디바이스의 입출력 포트를 위해 특정 주소의 메모리 영역을 커널에서 확보하는데 사용한다.

 

led_write() 함수는 실제 LED로 출력을 하는데 다음 문에서는 get_user()함수를 이용해 사용자 메모리 영역(buffer)의 데이터를 커널 영역의 메모리(value)로 옮기고 있다. copy_from_user() 함수를 사용해도 동일한 작업을 할 수 있다.

 

get_user(value, (int *)buffer);

 

다음 문에서는 request_region() 함수로 확보한 메모리 영역에 접근하여 LED_ADDR을 사용하여 이를 통해 사용자로 부터 받은 값을 LED 디바이스로 직접 출력하고 있다.

 

led_ port = (unsigned char*)(LED_ADDR)

*led_port = value;

 

led_release() 함수는 사용자 응용 프로그램에서 close() 함수를 사용했을 때 호출되는 함수이며, 여기서는 단지 MOD_DEC_USE_COUNT 매크로를 이용하여 이 디바이스를 이용하는 프로그램 개수(usage count)를 줄이는 기능만 수행한다.

 

사용자 응용 프로그램 led-drv-app.c 소스는 다음과 같다

 

 

#include 
#include 
#include 

int main() {

int fd;
int value;

if((fd = open("/dev/led_device",O_RDWR))<0){
printf(" open() FAIL! \n");
exit(-1);
}

for(value=0; value<64; value++){
write(fd,&value,sizeof(int));
usleep(8000);
}

close(fd);
return 0;
}



 

 

 

 

 

다음 Makefile은 위에서 설명한 디바이스 드라이버 소스 led-drv.c와 사용자 응용 프로그램 led-drv-app.c를 각각 컴파일 하여 커널 모듈인 led-drv.o와 사용자 실행 프로그램 led-drv-app를 생성하기 위한 것이다.

CC = arm-linux-gcc
STRIP = arm-linux-strip

INCLUDEDIR = /src/linux-2.4.21/include

CFLAGS = -D__KERNEL__ -DMODULE -02 -Wall -I$(INCLUDEDIR)

EXECS = led-drv-app
MODULE_OBJ = led-drv.o
SRC = led-drv.c
ARRP_SRC = led-drv-app.c
HDR = led-drv.h

all : $(MODULE_OBJ) $(EXECS)

$(MODULE_OBJ) : $(SRC) $(HDR)
 $(CC) $(CFLAGS)  -c $(SRC)

$(EXECS) : $(APP_SRC)
$(CC)  -o $@   $(APP_SRC)
$(STRIP) $@

clean : 
rm -f *.o  $(EXECS)

 

 

 

 

위에서 CFLAGS는 커널 모듈을 컴파일 하기 위해 필요한 컴파일 옵션들은 선언하고 있다. 여기서 옵션 -Wall은 'Warning All'의 의미로 모든 경고 메시지를 표시하라는 것으로 이는 커널 모듈은 커널 레벨에서 동작하므로 응용 레벨 프로그램과는 달리 에러 발생 시 시스템 전체에 영향을 미칠 수 있는 상황을 막기 위한 보호 장치가 없기 때문에 조심하기 위한 것이다.

 

컴파일 옵션 -O2는 최적화 레벨을 나타낸다. 이 옵션은 컴파일 시 최적화하지 않는 경우 인라인 함수를 외부로 공개하는 심벌로 간주해 에러가 발생할 수 있기 때문에 사용한다. 옵션 -O를 사용해도 된다.

 

컴파일 옵션 -D__KERNEL__은 코드가 커널 모드에서 실행됨을 알려주며, 옵션 -DMODULE은 헤더파일에게 소스가 모듈임을 알려 준다. INCLUDEDIR은 커널 모듈을 생성하기 위해서 필요로 하는 헤더 파일 위치를 나타낸다.

 

커널 모듈 컴파일 시는 링크 과정이 없으므로 옵션 -c를 사용하였고, 사용자 프로그램 컴파일 시는 링크 과정을 통해 실행파일이 생성되어야 하므로 옵션 -o가 사용되었다. 위 Makefile 파일에 대해 make 명령을 실행하면 커널 모듈 led-drv.o와 사용자 실행프로그램 led-drv-app가 생성된다.

 

디바이스 드라이버 실행 준비를 위해 먼저 위에서 얻은 디바이스 드라이버 파일 led-drv.o와 실행파일 led-drv-app을 임베디드 보드로 전송해야 한다. 이를 위해서 이 책에서는 임베디드 보드가 부팅 과정에서 NFS를 사용하여 마운트하는 루트파일 시스템이 호스트 컴퓨터의 /test/nfs 디렉터리라고 설정하였으므로 다음과 같이 cp 명령을 사용하여 임베디드 보드의 '/root' 디렉터리, 즉 root 계정의 홈디렉터리로 보낸다.

 

cp led-drv.o /test/nfs/root

cp led-drv-app /test/nfs/root

 

이후 호스트에서 시리얼 통신 프로그램을 실행시켜 임베디드 보드와 연결 후 사용자 계정 root로 임베디드 보드에 로그인 하면 위에서 cp명령으로 전송했던 파일이 root의 홈 디렉터리에 있는 것이 보인다.

  LED 디바이스 드라이버 실행을 위해 먼저 LED 디바이스 파일을 만들어야 한다. 이를 위해 다음 명령을 수행하여 디바이스 주(major) 번호 63으로 LED디바이스 파일을 생성한다.

 

mknod /dev/led_device c 63 0

 

다음 명령을 수행하여 LED 디바이스 드라이버 모듈인 led-drv.o를 메모리에 로드한다.

 

insmod led-drv.o

 

insmod 명령을 사용하여 LED 디바이스 드라이버를 메모리에 로드한 후에는 다음과 같이 하여 사용자 응용 프로그램 led-drv-app을 실행할 수 있다.

 

/led-drv-app

 

실행 결과로 임베디드 보드의 8개 LED에 0~64에 해당 하는 2진수 값이 차례로 표시된다.

 

 

블로그 이미지

종환 Revolutionist-JongHwan

github.com/alciakng 항상 겸손하자.

사용자 응용프로그램에서 디바이스 드라이버와 서로 데이터를 주고받기 위해서는 보통 read()나 write()  함수를 사용한다.

반면 응용프로그램에서 ioctl() 함수는 복잡한 기능의 디바이스를 제어하기 위해 사용한다.

 

응용 프로그램에서 호출하는 ioctl 함수 포맷 : int ioctl(int d, int request, ....)

 

여기서 ioctl 함수의 첫 번째 인수는 open 함수로 오픈된 디바이스 파일을 가리키는 파일 디스크립터이다. 두 번째 인수는 디바이스 드라이버 내에서 사용하기 위해 정의된 명령어 번호이다. ioctl 함수의 세번째 인수가 '....'인 것은 세번째 인수가 char 타입, long타입, pointer 타입 등 여러가지 데이터 타입을 가질 수 있어 컴파일 시 인수 타입 불일치로 인한 에러 발생을 막기 위한 것이다.

 

위의 ioctl() 함수의 두 번째 인수인 명령어 번호는 보통 디바이스 드라이버 개발자가 할당한다. 리눅스 커널에서는 이 명령어 번호를 쉽게 관리할 수 있는 매크로를 제공한다.

 

 

매크로 

기능 

_IO(type,nr) 

ioctl() 함수의 세 번째 인수를 사용하지 않을 때 사용 

_IOR(type, nr, size) 

ioctl() 함수의 세 번째 인수가 디바이스 드라이버에서 read될 때 사용 

)IOW(type, nr, size) 

ioctl() 함수의 세 번째 인수가 디바이스 드라이버로 write될 때 사용 

_IOWR(type, nr, size)

ioctl() 함수의 세 번째 인수가 읽기/쓰기/동작에 모두 사용될 때 사용 

 

 

아래에 LCD 디스플레이 디바이스 드라이버에서 매직넘버 및 ioctl() 함수의 명령어를 정의하는 예를 보였다.

 

#define LCD_DEV_MAGIC 'Y'

 

#define LCD_INIT _IO(LCD_DEV_MAGIC,0)

#define LCD_CMD _IOW(LCD_DEV_MAGIC, 1, unsigned char)

#define LCD_FILL _IOW(LCD_DEV_MAGIC, 2, unsigned char)

 

위 예에서 ioctl() 함수의 LCD_INIT 명령은 사용자 응용 프로그램과 디바이스 드라이버 사이에서 데이터 전달이 필요 없는 명령이어서 _IO 매크로를 사용하였다. LCD_CMD, LCD_FILL 명령은 사용자 응용 프로그램에서 디바이스 드라이버로 데이터를 보내는(write) 기능이므로 _IOW() 매크로를 이용하였다. 이때 전달하는 데이터는 1바이트 문자로 하였다. 

 

사용자 실행 프로그램에서 위와 같이 ioctl() 함수를 호출하면 해당 디바이스 드라이버에서 file_operations 구조체가 가리키는 ioctl 함수가 호출된다

 

file_operations->(*ioctl) (struct inode*, struct file *, unsigned int, unsigned long)

 

 

 

블로그 이미지

종환 Revolutionist-JongHwan

github.com/alciakng 항상 겸손하자.

static int __init chrdev_init(void)
{
int result;
result = register_chrdev(63, "chrdev_dd",&chrdev_fops);
if(result<0)
{
printk("chrdev_dd: 에러!\");
return result;
}
return result;
}

int chrdev_open(struct inode *inode, struct file *filp){
 MOD_INC_USE_COUNT;
 return 0;
}

static ssize_t chrdev_read(struct file * file, char *buffer, size_t, count, loff_t *offset){
 chrdev_buff = *chrdev_addr;
 copy_to_user(buffer, &chrdev_buff, size);
}

static ssize_t chrdev_write(struct file *file, const char *buffer, size_t length, loff_t *offset){
  int value, *keyReg;
  ......생략...........
  get_user(value,(int*)buffer);
  *chardev_wr_addr = ~value;
 .......생략..........
}

int chrdev_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg)
{
    return 0;
}

int chrdev_release(struct inode *inode,struct file *filp){
MODE_DEC_USE_COUNT;
return 0;
}

struct file_operations chrdev_fops = {
open : chrdev_open;
read : chrdev_read;
write : chrdev_write,
ioctl : chrdev_ioctl,
release : chrdev_release,
};

module_init(chrdev_init)l
module_exit(chrdev_cleanup);


 

 

 

 

 

위에서 module_init(chrdev)init);문에 의해 디바이스 드라이버가 메모리에 로드될 때 chrdev_init()함수가 실행되며 이때 등록함수 register_chrdev()가 호출된다. 함수에서 인수로 디바이스 주번호 63 디바이스명은 chrdev_dd file_operations 구조체 포인터는 chrdev_fops를 사용한다. 커널 레벨에서의 문자 출력 함수인 printk()문은 등록 실패시 실행된다.

 

 

응용프로그램에서 open("/dev/chrdev_dd", O_RDWR) 식으로 open()함수를 호출하면, 커널 내부에서는 /dev 디렉터의 디바이스 파일 chrdev_dd에 할당된 디바이스 주(major) 번호를 가지고 chardevs[] 배열에서 해당하는 문자 디바이스 드라이버의 file_operations 구조체 멤버 (*open)이 가리키는 chrdev_open()함수를 호출한다.

 

디바이스 파일 chrdev_dd의 오픈에 성공하면 파일 디스크립터를 얻게 되고 이후 응용프로그램에서는 이 파일 디스크립터를 사용해 해당 디바이스에 접근(read/write)할 수 잇다. 이 open()함수는 디바이스를 초기화하고 필요한 자원을 할당받으며 MOD_INT_USE_COUNT 매크로를 이용해서 카운트 값을 1 증가시켜 디바이스 드라이버가 사용중임을 표시한다.

 

 

여기서 file_operations 구조체 chrdev_fops에는 응용프로그램의 open(), read(), write(), ioctl(), release()에 대응하는 디바이스 드라이버 내부 함수 chrdev_open(),chrdev_read(), chrdev_write, chrdev_ioctl(), chrdev_release()를 각각 정의하고 있다.

 

chrdev_read() 함수에서는 실제 디바이스 주소로부터 데이터를 읽어온 후 이를 copy_to_user 매크로를 사용해 to포인터가 가리키는 사용자 메모리로 from 포인터가 가리키는 커널 메모리에 있는 내용을 복사한다.

 

디바이스에 데이터를 출력하기 위해 응용프로그램에서 write() 함수를 호출하면 이때 문자 디바이스 드라이버 내의 함수 chrdev_write()가 실행된다. chrdev_write()에서는 디바이스에 데이터를 쓰기 전에 copy_from_user(to, from, n) 매크로를 사용해 from 포인터가 가리키는 사용자 메모리 영역의 내용을 to 포인터가 가리키는 커널 메모리 영역으로 복사한다.

 

응용프로그램에서 ioctl() 함수를 호출하면 디바이스 드라이버 내의 함수 chrdev_ioctl()가 실행된다. 응용프로그램에서 read()나 write() 함수는 디바이스와 데이터를 주고 받는데 주로 사용하며, 이를 통해 디바이스의 기능 자체를 제어하기는 적합치 않은 경우가 있다. 이런 경우 디바이스 자체를 제어하는 특수 기능은 ioctl()을 통해서 한다.

블로그 이미지

종환 Revolutionist-JongHwan

github.com/alciakng 항상 겸손하자.


* 리눅스 디바이스 드라이버


 리눅스 디바이스 드라이버는 리눅스 커널의 일부분으로 포함되어 컴퓨터나 임베디드 시스템의 하드웨어를 제어하는 기능 및 이에 대한 인터페이스를 제공한다.

임베디드 시스템에서 주변장치를 제어하고자 하는 응용프로그램은 리눅스 디바이스 드라이버가 제공하는 인터페이스를 통하여 해당 장치를 제어할 수 있다. 


 리눅스 디바이스 드라이버는 리눅스 커널 자체에서 이미 지원하는 것도 있고 임베디드 시스템 개발 과정에서 개발자가 직접 작성해야 하는 경우도 있다. 


- 문자 디바이스 


 문자 디바이스는 바이트 단위로 동작하며 파일처럼 바이트 스트림으로 읽거나 쓸 수 있는 디바이스이며, 블록 디바이스는 블록 단위로 동작하며 하드디스크처럼 내부에 파일시스템을 가질 수 있는 디바이스이다. 


- 블록 디바이스 


 블록 디바이스는 하드디스크나 USB 메모리 처럼 내부에 파일시스템을 가질 수 있는 디바이스다


* 문자 디바이스 드라이버


 디바이스 드라이버도 디바이스 종류와 마찬가지로 문자 디바이스 드라이버, 블록 디바이스 드라이버 및 네트워크 디바이스 드라이버로 분류할 수 있다.. 임베디드 시스템 개발 시 가장 많이 사용하는 문자 디바이스 드라이버의 작성 시에는 응용 프로그램에서 이 디바이스를 사용할 수 있도록 하기 위해 open(), close(), read(), write(), ioctl() 같은 시스템 호출 함수들을 구현해야 한다. 



* 리눅스 커널 모듈 


 리눅스 시스템에서 디바이스 드라이버는 커널 모듈 형태로 만들어진다. 커널 모듈은 커널의 재컴파일 및 설치가 필요 없이 동적으로 리눅스 커널에 모듈 형태로 커널 기능의 추가나 제거를 가능하게 해준다. 따라서 리눅스 부팅 이후 커널이 실행중인 동안에도 필요 시에 커널에 기능을 추가할 수 있다.


커널 모듈 코드에는 init-module() 함수와 cleanup_module() 함수가 반드시 포함되는데, init_module() 함수는 커널 모듈이 메모리에 적재될 때 호출되며 cleanup_module()은 커널 모듈이 제거될 때 호출된다. 커널 모듈은 실행파일이 아닌 오브젝트 파일 형태이므로 커널 모듈을 컴파일할 때는 C 컴파일러에게 오브젝트 코드만을 생성하도록 하는 '-c' 옵션을 주면 된다. 


커널 모듈이 실제로 링크되는 시점은 메모리에 적재될 때이다. 커널 모듈을 메모리에 적재할 때는 insmod명령을 사용하고, 제거할 때에는 rmmod 명령을 사용한다.

커널 모듈소스에는 헤더 파일로서 linux/module.h가 포함되어야 한다. 




간단한 커널 모듈프로그램

#define MODULE

#include "linux/module.h"
#include "linux/kernel.h"

int init_module(void)
{
printk(" Linux Kernel Module Loading ... \n");
return 0;
}

void cleanup_module(void)
{
printk(" Linux Kernel Module Unloading ...\n");
}



커널 모듈이 메모리에 적재될 때(insmod 명령 사용) init_module()함수에 의해 적재된 커널 모듈이 초기화되면 자신을 현재 실행중인 커널에 등록한다. 이때 등록에 사용하는 함수 이름은 문자 디바이스의 경우 register_chrdev(), 블록 디바이스의 경우 register_blkdev() 이다.


rmmod 명령에 의해서 커널 모듈이 제거될 때에는 cleanup_module()함수가 호출되어 init_module() 함수에서 할당받은 자원을 반환하고, 문자 디바이스의 경우 unregister_chrdev() 함수를 사용하여 커널 모듈의 등록을 해제한다. 블록 디바이스의 경우 등록 해제에 unregister_blkdev()함수를 사용한다.



* file_operations 구조체 


file_operations 구조체는 사용하려는 응용 프로그램과 디바이스 드라이버를 서로 연결해 주는 기능을 하며, 디바이스 드라이버 내에서 구현된 함수들에 대한 포인터들로 구성되어 있다.  file_operations 구조체는 include/linux/fs.h 헤더 파일에 정의되어 있으며 다음에 이 내용의 일부를 보였다.










블로그 이미지

종환 Revolutionist-JongHwan

github.com/alciakng 항상 겸손하자.

임베디드 보드에 부착된 각종 주변 장치에 대한 제어 프로그램 작성을 위해서는 먼저 이들 장치에 대한 주소를 알아야 한다. 임베디드 보드에서의 LED 장치는 프로세서의 주소버스를 디코딩하여 주소가 결정된다.


LED 장치 주소가 0x0C00 0C00번지라고 가정하여 제어 프로그램을 작성해보자 



led.h소스 



#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define CS3_BASE 0x0c000000
#define LED_OFFSET 0x0C00

unsigned char *CS3_base;
unsigned char *led;

static int fd;

led.h 소스에는 기본 헤더 파일들과 임베디드 보드에서 설정된 LED를 가리키기 위해 프로세서의 chip select 신호의 기본 주소인 0x0C00 0000을 CS3_BASE로 정의하고 있다. 또 이 기본 주소에서 LED 장치 주소와의 차이값인 0x0C00을 LED_OFFSET에 정의하고 있다. CS3_base는 chip select 신호의 기본주소를 가리키는 포인터 변수이고, led는 LED 장치 주소를 가리키는 포인터 변수이다.

led.c 소스

#include "led.h"

int memopen(void)
{
   fd = open("/dev/mem",0_RDWR);
   if(fd<0){
       perror("/dev/mem FAIL!\n");
       exit(1);
  }

return 0;
}

int ledaddr(void)
{
  CS3_base = (unsigned char *)mmap(NULL,1024,PROT_READ|PROT_WRITE,MAP_SHARED,fd,CS3_BASE);
    if((int)CS3_base == -1){
            perror("mmap FAIL! \n");
            return -1;
   }
   led = CS3_base + LED_OFFSET;
   return 0;
}

void memrelease(void)
{
  munmap((unsigned char * )CS3_base,1024);
}

void outled(int sel)
{
 switch(sel){
      case 0:
          {
               printf("LED data = 0xFF\n");
               ledaddr();
               *led = 0xff;
               memrelease();
               break;
          } 
       case 1:
   ........생략.......

       case 2:
  ......
   }
 }

int main(void)
{ 
  int i;
  memopen();

for(i =0; i<10;i++){
  outled(0);
  sleep(1);
  outled(1);
  sleep(1);
  outled(2);
}
  close(fd);
}

위의 led.c 소스에서 fd = open("/dev/mem",0_RDWR); 는 /dev/mem 파일을 열어 파일 기술자를 반환한다. /dev/mem 파일은 O_RDWR 모드로 열리며 이 파일은 메모리 입/출력을 하기 위한 문자 디바이스 파일이다. 

open() 함수에 의해서 반환된 파일 기술자는 변수 fd에 할당되고, 이 값을 아래 mmap() 함수의 파라미터로 사용해 CS3_BASE 주소에 해당하는 현 프로세스 메모리 주소를 구한다.


이 mmap() 함수는 CS3_BASE주소를 가리키는 현 프로세스 주소 공간에서의 주소값을 변수 CS3_base에 리턴한다 

led_addr = CS3_base + LED_OFFSET; 여기서 베이스 주소와 LED 장치와의 오프셋 값인 LED_offset을 베이스 주소에 더하여 변수 led에 LED 장치의 주소를 얻게 된다. 


 *led = oxff; 여기서 실제 LED 장치로 값을 출력한다.. 


* mmap함수


 리눅스/유닉스에서는 프로세스와 프로세스 사이의 보호를 위해 각 프로세스는 서로 별도의 주소 공간을 가지게 된다. 이를 위해서 CPU가 하드웨어적으로 지원하는 MMU(Memory Management Unit)가 필요하며 32비트급 임베디드 시스템용 프로세서는 대부분 MMU를 가지고 있다. 각 프로세스가 별도의 주소 공간을 가지면 한쪽 프로세스에서의 잘못된 동작이 다른 쪽 프로세스에 영향을 미치지 않으므로 프로세스의 보호는 잘 이루어지지만 프로세스와 프로세스 사이에 데이터를 전달하거나 공유할 경우 별도의 대책이 필요하다.


 각 프로세스가 별도의 주소 공간을 가지면 한쪽 프로세스에서의 잘못된 동작이 다른 쪽 프로세스에 영향을 미치지 않으므로 프로세스의 보호는 잘 이루어지지만 프로세스와 프로세스 사이에 데이터를 전달하거나 공유할 경우 별도의 대책이 필요하다. LED 장치의 주소는 물리주소 공간에서 0x0C00 0C00으로 되어 있으나 현 프로세스 주소 공간에서의 주소 0x0C00 0C00는 서로의 주소공간이 다르므로 실제 LED 장치를 가르키는 것이 아니다. 이를 위한 여러 방법 중 하나가 mmap()함수를 사용하는 것이다. mmap() 함수는 현 프로세스의 메모리 공간을 파일이나 공유 메모리 특정영역으로 맵핑시킨다. 파일은 운영체제에서 글로벌 자원이므로 다른 프로세스와 공유가 가능하다. 따라서 파일로 맵핑된 메모리 영역은 프로세스 사이의 데이터 교환에 사용될 수 있다.




블로그 이미지

종환 Revolutionist-JongHwan

github.com/alciakng 항상 겸손하자.

ARM 아키텍처는 임베디드 기기에 많이 사용되는 RISC 프로세서이다. 저전력을 사용하도록 설계하여 ARM CPU는 모바일 시장에서 뚜렷한 강세를 보인다.


ARM 프로세서 명령어 세트 


데이터 처리 명령어 세트


ex) ADD {condition} {S} Rd, Rn, shifter_operand 


덧셈 명령으로써 두 개의 오퍼랜드(Rn, shifter_operand)를 서로 더해 결과를 목적지 레지스터 Rd에 저장한다. 조건(condition) 부분은 플랙에 따른 명령어 실행 조건을 의미한다.


Load/Store 명령어 세트


Branch 명령어 세트


기타 명령어 세트


PXA 시리즈 프로세서


pxa 시리즈 프로세서는 인텔 32비트 Xscale RiSC 코어를 기반으로 하고 여러 주변 장치를 탑재한 임베디드 시스템용 프로세서이다. CPU 부분인 Xscale은 ARM 버전 5구조를 기반으로 하며 비교적 저전력에 고성능을 제공한다. PXA2xx 시리즈와 PXA3xx 시리즈가 있다.


(학교 임베디드 시스템 수업 PXA255 사용함)


PXA 255는 주변 장치 인터페이스를 위해서 시리얼 통신을 위한 UART 포트, MMC 카드 기능, USB 1.1 클라이언트 기능, 적외선 시리얼 통신을 위한 IrDA, 오디오 코텍, 시리얼 통신 기능인 I2C 등을 가지고 있다. UART는 표준 UART, FF UART, 블루투스 UART를 모두 지원한다. 


GPIO


PXA255는 모두 85개의 GPIO 핀 신호를 가지며 각 핀은 입력이나 출력으로 설정될 수 있고 인터럽트 요청 라인이나 UART의 신호들, LCD 디스플레이를 위한 데이터 라인 등 각종 외부 장치와의 연결이나 인터페이스로 사용한다. 대부분의 GPIO는 단순히 디지털 입출력뿐만 아니라 부가적인 기능을 추가로 가진다. GPIO 핀의 각 기능을 설정하고 사용하기 위한 GPIO 레지스터들의 이름과 기능은 다음 표와 같다. 


GPDR(GPIO PIN Direction Register)  - 입출력 방향 설정 레지스터


이 레지스터는 핀 방향, 즉 입출력 방향 설정 레지스터로서 GPIO 핀을 입력 또는 출력으로 설정할 것인지를 결정한다. 0이면 입력으로 설정되어 GPLR을 통해서 GPIO 핀 상태를 읽을 수 있고 1이면 출력으로 설정되어 GPSR이나 GPCR을 사용해 '1'이나 '0'을 출력할 수 있다. 


GAFR(GPIO alternate function register) - Alternate 기능 설정 레지스터


GPIO 부가기능을 설정한다. 


GRER() - 상승 에지 검출 레지스터


이 레지스터의 입력 핀 상태가 LOW에서 HIGH로 되면, GEDR의 해당 비트가 "1"로 된다. 단 GPDR의 해당 비트가 "0"으로 되어 있어 입력 상태여야 하고, GAFR의 해당비트가 "0"으로 되어 있어 부가기능을 사용하지 않는 상태이어야 한다.



GFER(GPIO Falling edge detect register) - 하강 에지 검출 레지스터 


이 레지스터의 입력 핀 상태가 high에서 low로 되면 gedr의 해당 비트가 '1'로 된다. 단 gpdr의 해당 비트가 '0'로 되어 있어 입력 상태이어야 하고, GAFR의 해당 비트가 0으로 되어 있어 부가기능을 사용하지 않는 상태이어야 한다.



GPSR(GPIO Pin output set register) - 출력 '1' 설정 레지스터


이 레지스터에 "1"을 설정하면 해당 핀은 HIGH 상태로 되고 "0"을 설정하면 해당 핀은 변화 없이 이전 상태를 유지한다. 


GPCR(GPIO Pin output clear register) - 출력 '0' 설정 레지스터


이 레지스터에 "1"을 설정하면 해당 핀은 LOW 상태가 되고, "0"을 설정하면 해당 핀은 변화 없이 이전 상태를 유지한다. 


GPLR(GPIO PIN-LEVEL REGISTER) - 입력 레벨 검출 레지스터


이 레지스터는 GPIO의 핀에 인가된 전압 레발 값을 가지므로 현재의 입력핀의 상태를 읽을 수 있다. 전압 레벨이 LOW이면 0으로 HIGH 이면 "1"로 읽힌다. 


GEDR(GPIO edge detect status register) - 입력 에지 검출 레지스터


GPDR해당 비트가 "0"으로 되어 있어 입력 상태이어야 하고, GAFR의 해당 비트가 0으로 되어 있어 부가기능을 사용하지 않는 상태에서 GRER이나 GFER의 해당 비트가 설정되어 있을 경우 상승/하강 에지가 발생하면 해당 비트가 "1"로 되고 이때 인터럽트를 발생시킬 수도 있다. 

블로그 이미지

종환 Revolutionist-JongHwan

github.com/alciakng 항상 겸손하자.

* 크로스 개발환경 및 툴체인 


임베디드 시스템의 개발을 위해서는 호스트 컴퓨터에 타깃시스템 용의 프로그램 작성을 위한 각종 프로그램 개발 관련 도구가 설치되어야 한다. 이와 같은 타깃시스템 프로그램 개발을 위해 호스트에 설치되는 각종 패키지를 크로스개발환경이라고 한다.



크로스 개발환경에서 호스트와 임베디드 시스템의 기본 연결이다.


시리얼 연결은 호스트와 타깃 시스템 사이에서 가장 기본적인 저속 통신 기능을 제공한다. 개발 과정에서 호스트 측에서 임베디드 시스템에 대한 부팅 명령이나 리눅스 명령을 주거나 임베디드 시스템에서 발생한 출력 메시지를 호스트 측에서 보는데 주로 사용된다. 임베디드 시스템은 개발 단계에서 기본 통신을 위해 보통 시리얼 포트를 갖추고 있다. 이를 호스트의 시리얼 포트와 서로 연결한다.


호스트가 pc인 경우 뒷면에 시리얼 포트가 하나 또는 두개 있다.


이후 minicom과 같은 시리얼 통신프로그램을 이용해 타깃시스템과 서로 통신한다.


개발자가 임베디드 시스템에 주는 각종 명령 및 임베디드 시스템에서 출력하는 각종 실행 결과 및 커널 메시지들은 모두 시리얼 포트를 통해서 이루어짐.


이더넷 연결은 100/1000mbps의 고속 전송이 가능하므로 개발단계에서는 주로 호스트와 임베디드 시스템사이의 파일 전송에 사용


jtag(joint test action group)은 하드웨어 수준의 디버깅 및 호스트에서 작성된 부트로더를 임베디드 시스템의 플래시메모리에 쓰는 역할을 한다. 



* 툴체인


호스트의 프로세서와 개발 대상인 임베디드 시스템의 프로세서는 대부분 다른 경우가 많다. 호스트의 프로세서는 대부분 인텔 80 * 86 계열인데 이 프로세서가 임베디드 시스템에 사용되는 경우는 거의 없다. 


임베디드 시스템용 프로세서로는 스마트폰이나 고성능 제어 기기에 많이 사용되는 32/64비트 ARM 계열과 저전력 센서 네트워크 등에 많이 사용되는 마이크로컨트롤러인 AVR 시리즈 등이 있다. 이들을 위한 크로스 어셈블러 및 크로스 컴파일러는 모두 공개되어 있어 쉽게 구할 수 있다. 


rpm 패키지를 구해 설치하는 경우는 간단히 rpm -ivh 패키지 파일명 식으로 rpm 명령을 사용하면 된다. rpm 패키지 설치 시는 패키지 파일에 따라 실제로 설치되는 디렉터리가 달라진다. 예로 /opt/cdt/xscale/bin 디렉터리에 설치되었다면 이 디렉터리에 다음 파일들이 생성된다.


arm-linux-as : ARM 프로세서용 크로스어셈블러

arm-linux-gcc : 크로스컴파일러

arm-linux-ld : 링커

arm-linux-strip : 실행파일에서 심벌 등을 제거하여 실행파일 크기를 줄이는 프로그램


툴체인 설치를 모두 마치면 실행파일이 들어 있는 디렉터리의 위치를 리눅스 셸에 알려주어야 한다. 리눅스 기본 셸은 bash이므로 bash의 환경변수 PATH에 "/opt/cdt/xscale/bin"을 추가하면 된다. 이렇게 하기 위해서 자신의 홈디렉터리에서 .bash_profile 파일을 편집기로 열어 PATH 라인 뒤에 ";/opt/cdt/xscale/bin" 라고 추가하면 된다. 


* 부트로더 


부트로더는 파워 온 시 임베디드 시스템의 각종 하드웨어를 초기화하고 기본적인 통신 기능을 제공하며 플래시메모리나 ROM에 들어 있는 운영체제 커널 부분을 주 메모리(RAM)로 이동시켜 운영체제의 부팅을 시작하는 기능을 한다. 


부트로더 주요기능


타겟 하드웨어 초기화, 운영체제 부팅, 파일이동, 통신기능, 플래시메모리관리


* JTAG


임베디드 시스템 개발 초기에는 부트로더 자체가 없으므로 이를 임베디드 시스템 플래시메모리에 넣어주기 위한 방법이 필요한데, 보통 JTAG 인터페이스를 주로 사용한다. 


호스트 PC에서 개발한 부트로더를 임베디드 시스템으로 전송하기 위해서는 먼저 임베디드 시스템의 JTAG 커넥터와 호스트 PC의 프린터 포트를 JTAG 시리얼 케이블로 연결한다. 다음 호스트 PC에서 부트로더 전송 기능을 하는 명령어에 전송할 부트로더 파일명을 입력하면 된다. 



*부트로더에서의 TFTP 사용


부트로더는 네트워크 인터페이스를 통해 호스트에서 커널 이미지나 파일을 가져오는 기능을 제공한다. 이때 FTP기능을 축소한 간단한 파일 전송프로그램인 TFTP를 사용한다. TFTP는 FTP에 비해 사용자 인증과정이 없고 접속된 서버 내에서 디렉터리를 이동할 수 없고 서버에서 미리 설정한 디렉터리에만 연결된다. 또 서버 디렉터리 내의 파일명을 볼 수도 없다. TFTP는 실행 코드가 작아 부트로더에 넣기 쉽고 실행을 위한 명령어는 보통 'tftp'이다.



* NFS 


NFS 는 네트워크에 연결된 다른 컴퓨터의 하드디스크 일부(파일 및 디렉터리)를 자신의 로컬 하드디스크처럼 사용할 수 있게 한다. NFS는 유닉스/리눅스에서 기본적으로 지원하는 기능이다.  



* 부트로더에 의한 부팅 과정 


1. 임베디드 시스템의 부트로더는 리눅스 커널을 호스트에 요청한다. 이때 호스트는 압축된 리눅스 커널 이미지를 TFTP를 사용해 임베디드 시스템에 전송하고 수신된 리눅스 커널 이미지는 임베디드 시스템의 RAM 영역에 저장된다.

2. 부트로더는 RAM에 로드된 커널로 실행권을 넘겨 커널의 압축이 해제되고 리눅스 부팅과정이 시작된다.

3. 부팅 과정 중 NFS 기능을 사용하여 루트파일시스템을 마운트하여 부팅이 계속된다.

4. 부팅 완료후 호스트에서 시리얼 통신 프로그램(minicom)을 통해 임베디드 시스템에 로그인한다(로그인 이후 자기 것으로 보이는 루트 파일시스템은 실제는 호스트에서 NFS를 통하여 제공하는 것이다)



* 루트 파일시스템 


루트 파일시스템은 리눅스에서 최상위 파일시스템이며 리눅스 자체의 동작을 위해서는 반드시 필요하다. 임베디드 시스템 개발 과정에서 루트 파일시스템을 만드는 과정은 반드시 필요하다. 루트 파일시스템은 개발과정에서는 편의상 앞에서 설명한 호스트의 NFS 디렉터리에 두는 경우가 많지만 개발이 완료되면 반드시 임베디드 시스템 내부의 플래시 메모리로 옮겨야 한다.


우선 호스트 컴퓨터의 /test/nfs 디렉터리에 임베디드 시스템의 루트 파일시스템을 생성하고 이 디렉터리를 NFS를 사용하여 임베디드 시스템에 마운트시켜 동작하는 것으로 가정하였다. 머저 루트 파일시스템을 구축할 /test/nfs 디렉터리로 가서 리눅스에서 사용되는 필수 디렉터리를 생성한다.


이상의 과정에서 루트 파일시스템의 필수 디렉터리가 생성되었으면 다음 단계로 시스템 초기화를 수행하는 init 프로그램을 설치한다.


* busybox 


 루트 파일시스템 구축의 마지막 단계로 BusyBox를 사용하여 임베디드 시스템에 필요한 각종 리눅스 유틸리티 및 응용프로그램들을 설치하는 과정을 설명한다. 



* 램디스크 


 램 디스크(Ramdisk)란 메모리에 일반 하드디스크에서와 같은 파일시스템을 구축한 것이다. 

램 디스크를 이용하면 메모리에 파일들을 둘 수 있으므로 메모리를 하드디스크처럼 사용 할 수 있다. 물론 RAM상에 존재하므로 전원 OFF 시에는 내용이 소멸되나 속도가 매우 빠르며 임베디드 시스템에서는 플래시 메모리와 함께 사용되어 서로 보완하는 효과가 있다.


램디스크는 하드디스크가 없는 임베디드 시스템에서 루트파일시스템을 구축하는 데 필요하며 루트 파일시스템만이 아니라 일반 파일시스템도 여기에 둘 수 있다.


* JFFS


 JFFS는 전원이 갑자기 OFF될 경우에도 데이터를 안정적으로 보호할 수 있는 기능을 가진 플래시 메모리에 구축된 파일시스템이다. 임베디드 시스템에서의 파일시스템은 대부분 JFFS(JFFS2)를 사용한다. 저널링 파일시스템은 동작 시 파일시스템의 변화 내용을 별도의 로그 파일에 기록하며 따라서 갑작스런 전원 OFF등의 이상 발생 시 복구가 가능하도록 하는 기능을 제공한다.











블로그 이미지

종환 Revolutionist-JongHwan

github.com/alciakng 항상 겸손하자.

* 임베디드 시스템


임베디드 시스템은 내부에 CPU, 메모리, 입출력 포트를 가지고 있어 특정한 기능을 수행하는 시스템이다.


임베디드 시스템의 핵심인 프로세서는 단순한 기능을 가지는 경우 8비트 이하의 저기능 프로세서가 주로 사용되고 고급 기능의 임베디드 시스템 경우는 32비트 이상의 고기능 프로세서가 주로 사용된다. 


32비트급 임베디드 시스템용 프로세서는 ARM 계열 프로세서가 대표적이고 8비트급 프로세서로는 ATmel AVR 시리즈가 대표적이다.


AVR 시리즈는 칩 내부에 플래시 메모리가 있어 PC에서 작성한 프로그램 코드를 쉽게 칩 내부로 보내어 실행할 수 있다는 편리한 점이 있다.



* 임베디드 시스템용 운영체제 


실기간성이 필요한 임베디드 시스템의 경우 실시간 운영체제(Real time OS)가 사용되어야 하는데, 이는 정해진 시간 내에 주어진 작업을 처리하여 시스템이 결과를 출력 할 수 있도록 하는 운영체제 이다. 즉 주어진 작업을 단순히 빨리 처리하기 보다는 정해진 시간 안에 처리를 완료한다는 의미이다. 이를 위해 실시간 운영체제는 시스템 응답속도, 인터럽트 처리속도가 빠르고 선점형 멀티태스킹 및 스케줄링 등이 지원되어야 한다. 


* 리눅스 디렉터리 구조 





/ 최상위디렉터리


/bin 기본 명령어들이 위치하는 디렉터리.. 실행 명령어(예: cp, mv, vi)들이 이 디렉터리에 포함되어 있다.


/sbin 리눅스 시스템 관리에 필요한 명령어가 위치하는 디렉터리이다.


/dev 각 디바이스에 대한 디바이스 파일이 위치하는 디렉터리이다.


/etc 리눅스 환경 설정에 필요한 파일들이 위치하는 디렉터리이다.


/lib 리눅스 기본 라이브러리(커널 모듈)가 위치하는 디렉터리이다.


/opt 사용자가 나중에 추가로 설치하는 패키지가 위치하는 디렉터리이다.


/boot 부팅에 필요한 파일들이 위치하는 디렉터리이다.


/usr 리눅스 응용프로그램, 시스템 파일, 각종 패키지 및 이들을 위한 라이브러리 파일 등이 위치하는 디렉터리이다. 



* 리눅스 주요 명령어

 

cat : 텍스트 파일 내용을 화면에 출력한다. 


cat test.c -> test.c 파일 내용을 화면에 출력


cd : 디렉터리를 이동한다.


cd /usr/bin -> 현 디렉터리에서 /usr/bin 디렉터리로 이동


chmod :  파일이나 디렉터리의 퍼미션 설정(change mode) 이때 퍼미션은 소유자(owner), 그룹(group), 그 외 사용자(others) 


chmod 666 test.dat -> 파일 test.dat에 대해 퍼미션을 666으로 설정한다.(즉 모든 사용자가 해당 파일을 읽기/쓰기 할 수 있다)


chmod 700 exam2 -> 파일 exam2에 대해 퍼미션을 700으로 설정한다.


chgrp : 파일이나 디렉터리의 그룹을 변경한다.


chgrp project2 exam4 -> 파일 exam4의 그룹을 project2로 변경한다.


chown : 파일이나 디렉터리의 소유자를 변경한다,


chown gildong exam5 -> 파일 exam5의 소유자를 gildong으로 변경한다.


cp : 파일이나 디렉터리를 복사한다. 


ifconfig : 네트워크 인터페이스의 각 항목을 설정하거나 설정된 상태를 보여준다.


more : 텍스트 내용을 한 화면씩 출력한다.


mount : mount 명령은 하드디스크, CD-ROM, USB 메모리 등 블록 디바이스에 있는 파일시스템을 리눅스 디렉터리에 연결해 사용자가 이에 접근할 수 있도록 해준다. 마운트 시킨 디바이스와의 연결을 끊을 때는 언마운트 동작 명령인 umount한다.


ping : 목적지까지의 TCP/IP 네트워크 연결 상태를 테스트하는 데 사용한다 일정 개수의 패킷을 보내어 돌아오는 시간, 손실된 패킷 수 등을 표시한다.


ps : 현재 프로세스의 상태를 보여준다.


rpm : RPM(RedHat Package Manager)형태의 파일을 풀어 설치하고 관리 할 수 있게 해주는 패키지 관리 프로그램이다. 레드햇 계열 리눅스에서 주로 사용한다. 이를 사용하면 rpm 형태의 파일로 묶여 있는 프로그램을 쉽게 설치/업데이트/삭제할 수 있다.


그런데 일반적인 rpm 패키지라면 yum 명령을 사용하는 것이 더 편리하다. yum도 rpm과 마찬가지로 레드햇 계열 리눅스에서 사용하는 패키지 자동 설치 및 업데이트 명령이다.


yum install '패키지 이름'


yum으로 설치가 안되는 패키지도 있으며 이런 경우는 직접 rpm 파일을 구해서 rpm으로 설치해야 한다.


shutdown : 리눅스 시스템을 종료한다.


su : subsitute user 의미로서 다른 사용자로 바꾸어 쉘을 새롭게 시작한다.


uname : 시스템 관련 정보를 화면에 출력한다.


vi : 리눅스 기본 텍스트 편집기 


tar : 여러 파일과 디렉터리를 하나의 파일로 묶는 용도 


tar cf test_dir.tar test_dir  -> test_dir의 모든 내용을 test_dir.tar 라는 파일로 묶어준다.


gzip과 gunzip : 파일 압축(gzip)과 해제(gunzip) 기능을 가지며 압축 효율이 초기 유닉스 압축 프로그램인 compress보다 높다.


* 리눅스 시리얼 통신 기능


임베디드 시스템 개발 시 사용하는 리눅스의 대표적인 시리얼 통신 프로그램으로 미니컴이 있다. 대부분의 리눅스 버전에 포함되어 있으며 없는 경우 추가로 설치하면 된다. 이 시리얼 통신 프로그램이 정상적으로 동작하려면 호스트와 임베디드 시스템 사이에 시리얼 포트끼리 서로 연결되어 있어야 하고, 통신 속도(bps), 포트 번호, 제어 방법 등 시리얼 통신을 위한 파라미터도 서로 일치해야 한다.


minicom -s configuration 옵션 메뉴가 나온다.


여기서 상하 화살표 키를 사용해 "serial port setup" 항목을 선택하면 시리얼 포트, 전송속도, 흐름제어(flow control) 등을 설정하는 화면이 나온다.


이 화면에서 먼저 'A'를 사용해 사용할 호스트의 시리얼 포트를 정해준다. 보통 /dev/ttyS1, 또는 /dev/ttyS0 로 준다. 'E' 키를 누르면 시리얼 통신 파라미터를 설정하는 화면이 나온다. 이 화면에서 Current : 38400 8N1 부분은 현재 전송속도가 38400 bps이고 데이터 전송 단위가 8비트씩이며 패리티비트는 사용하지 않고 스톱비트로 1트를 사용한다는 의미이다. 


* 리눅스 make 기능


리눅스 make 기능은 프로그램 개발 과정에서 수시로 수정되는 여러 소스 파일들의 연관관계를 파악하여 자동으로 컴파일 및 실행파일을 만들어 준다. 


Makefile은 make가 이해할 수 있는 일종의 쉘 스크립트 언어이며 기본 구조는 다음과 같이 target, dependency, command의 세 항목으로 이루어진다.


target : dependency

[tab] command

[tab] command


여기서 target 항목은 command가 수행된 이후 최종적으로 생성되는 결과 파일을 지정하며, command 항목이 컴파일 명령이라면 오브젝트 파일이나 실행파일이 target의 이름이 된다. dependency 항목에는 서로 연관관계가 있는 파일 이름들이 나열된다.


command 항목에는 일반 리눅스 명령들이 들어가고 제일 앞에 tab 문자로 시작해야 한다. dependency 항목의 파일들이 수정되었거나, 현 디렉터리에 target 항목이 가리키는 파일이 없을 때 command 항목의 명령들이 차례대로 수행된다.


ex : ex1.o ex2.o ex3.o

  gcc -o ex ex1.o ex2.o ex3.o

ex1.o : ex.h ex1.c

  gcc -c ex1.c

ex2.o : ex.h ex2.c

  gcc -c ex2.c

ex3.o : ex.h ex3.c

  gcc -c ex3.c


 위는 3개의 파일(ex1.c, ex2.c, ex3.c)을 컴파일하여 실행파일을 얻는 예이다. 여기서 target 항목에 지정된 실행파일 "ex"가 생성되기 위해서는 ex1.o, ex2.o, ex3.o 파일이 필요함을 보여준다. 따라서 최초 make 명령을 주면, 컴파일 명령인 gcc -c ex1.c, gcc -c ex2.c, gcc -c ex3.c가 차례로 수행되어 오브젝트 파일 ex1.o, ex2.o, ex3.o 가 차례로 생성되고 마지막으로 링크 동작을 위해 gcc -o ex ex1.o ex2.o ex3.o 명령이 수행되어 실행파일 ex가 생성된다.


이후 ex2.c 와 ex3.c 파일이 수정되었다면 다시 make 라고 치면 자동으로 gcc -c ex2.c 와 gcc -c ex3.c 가 실행되고 이어 gcc -o ex ex1.o ex2.o ex3.o 가 실행되어 수정된 내용이 반영된 실행파일이 만들어진다. 


리눅스 make에서는 매크로를 사용할 수 있는데 이를 사용하면 정의된 문구를 대치하므로 긴 문구나 반복되는 문구를 대신하면 편리하다. 매크로 정의는 매크로이름 = 문구 식으로 하고 사용 시에는 $(매크로 이름) 식으로 한다. 


EXOBJ = ex1.o ex2.o ex3.o

EXHDR = ex1.h ex2.h ex3.h 

ex : $(EXOBJ)

 gcc -o ex $(EXOBJ)

ex1.o : $(EXHDR) ex1.c

 gcc -c ex1.c

ex2.o : $(EXHDR) ex2.c

 gcc -c ex2.c

ex3.o : $(EXHDR) ex3.c

 gcc -c ex3.c 


이외에 predefined 매크로도 있다.


리눅스 make에서는 레이블을 사용할 수 있다. Makefile 파일에서 target 부분 이후에 dependency 항목이 오지 않을 경우 해당 target은 레이블로 인식되며 이때 이 레이블은 make 명령의 파라미터로 사용될 수 있다.


delete : 

   rm $(EXOBJ)


위에서는 "delete" 다음에 dependency 항목이 없으므로 레이블이고 


make delete 와 같이 입력하면 rm $(EXOBJ) 명령이 실행되어 매크로에 의해 정의된 파일들이 삭제된다.



* 리눅스 커널 및 설치


리눅스의 핵심 기능을 제공하는 커널은 최초 버전 1.0부터 시작하여 현재 버전 3이 주로 사용된다.


리눅스 커널이란, 응용 프로그램을 대신하여 하드웨어를 제어하는 편리한 기능을 제공하며, 응용 프로그램의 처리를 요청받아 동작하는 이벤트 중심의 프로그램이다. 응용 프로그램으로 부터 시스템 콜이 요청되거나 하드웨어로 부터 인터럽트 요청이 들어오면, 그 요청에 대응하는 처리를 수행합니다. 다시 말해서 리눅스 커널은 자기 혼자서 동작하지 않는다.


커널 소스 주요 디렉터리별 기능들은 다음과 같다.


kernel 디렉터리 - 커널의 중심이 되는 소스(스케줄링, 프로세스 관리, 시그널 처리 등) 부분이 들어 있으나 일부 프로세서에 종속적인 커널 코드는 디렉터리 arch/'프로세서명'/kernel에 있다.


arm 프로세서 경우는 arch/arm/kernel에 있다. 이 디렉터리에는 커널의 기능별로 소스 파일이 분리되어 있고 예로 module.c, timer.c, signal.c, sched.c 등이 있다.


arch 디렉터리 - 프로세서 유형별로 서브디렉터리를 가지고 있으며 프로세서에 종속적인 커널 코드를 포함하고 있다. 


drivers 디렉터리 - 디바이스 드라이버 코드를 가지고 있으며 각 디바이스 드라이버에 대한 코드가 들어 있는 서비디렉터리를 가진다. 블록 디바이스 드라이버 코드가 들어 있는 block, 문자 디바이스 코드가 들어 있는 char, 네트워크 장치 디바이스 드라이버 코드가 들어 있는 net.. 







블로그 이미지

종환 Revolutionist-JongHwan

github.com/alciakng 항상 겸손하자.