
리눅스 디바이스 드라이버를 깊이 이해하기 위해서는
"파일 시스템(User) - 커널(Kernel) - 하드웨어(HW)"가 어떻게 유기적으로 연결되는지, 그리고 이 과정에서 포팅(Porting)이 어떤 의미를 갖는지 명확히 알아야 합니다.
리눅스에서 /dev 디렉터리에 있는 파일들은 실제 데이터를 저장하는 파일이 아닙니다. 이들은 디바이스 노드(Device Node)라고 불리며, 유저 영역의 프로그램이 커널 드라이버에 접속하기 위한 '진입점(Entry Point)' 역할을 합니다.
✅ 동작 원리 (i-node의 비밀):
우리가 ls -l /dev/ttyS0를 치면 c 4 64 ... 같은 숫자가 보입니다. 여기서 c는 문자 디바이스(Character)를 뜻하고, 4는 주번호(Major), 64는 부번호(Minor)입니다.
파일 시스템의 내부 구조인 inode에는 이 파일이 일반 파일인지 디바이스 파일인지 표시되어 있습니다.
연결 고리: 유저가 이 파일을 open()하면, 커널은 이 파일의 주번호(Major Number)를 보고 커널 내부에 등록된 드라이버 목록(cdev_map)에서 해당 번호를 가진 드라이버를 찾아 연결해 줍니다.
✅ 자동 생성 (udev/mdev):
과거에는 개발자가 mknod 명령어로 직접 이 파일을 만들었습니다.
요즘 리눅스(임베디드 포함)는 시스템 부팅 시 커널이 하드웨어를 감지하면, udev 데몬이 자동으로 /dev 밑에 적절한 디바이스 파일을 생성해 줍니다.
디바이스 드라이버 코드는 커널 공간(Kernel Space)에 존재합니다. 여기서 가장 중요한 개념은 유저 공간과 커널 공간의 엄격한 분리입니다.
✅ 보호 모드와 권한:
유저 애플리케이션은 하드웨어 주소(레지스터)에 직접 접근할 권한이 없습니다. (Segmentation Fault 발생)
드라이버(커널)만이 실제 물리 메모리와 하드웨어 레지스터를 건드릴 수 있습니다.
✅ 데이터 이동 (copy_to/from_user):
유저 프로그램이 보낸 데이터를 드라이버가 읽거나, 드라이버가 하드웨어에서 읽은 데이터를 유저에게 보낼 때, 단순한 포인터 참조나 memcpy를 사용하면 안 됩니다.
반드시 copy_from_user() (유저 -> 커널)와 copy_to_user() (커널 -> 유저)라는 특수한 커널 함수를 써야 합니다. 이 함수들은 유효하지 않은 메모리 접근을 체크하여 시스템 붕괴를 막아줍니다.
✅ File Operations (fops):
드라이버 코드의 핵심입니다. 유저가 open, read, write를 호출했을 때, 커널 내부에서 실제로 수행할 함수들을 함수 포인터로 매핑해놓은 구조체입니다.
예: "유저가 read를 요청하면, 내 드라이버의 my_sensor_read 함수를 실행해라"라고 커널에 등록하는 것입니다.
리눅스 커널 소스 코드는 범용적입니다. 하지만 임베디드 보드는 제조사마다, 모델마다 하드웨어 구성(CPU 핀맵, 메모리 주소, 주변 장치 등)이 다릅니다. 이 범용 커널을 내 보드에서 돌아가게 만드는 과정을 포팅이라고 합니다.
✅ BSP (Board Support Package):
✅ 디바이스 트리 (Device Tree, DTS):
과거(ARM 리눅스 초기)에는 드라이버 코드 안에 "LED는 핀 5번에 연결됨"이라는 정보를 C언어로 하드코딩했습니다. 하드웨어가 조금만 바뀌어도 커널을 다시 컴파일해야 했습니다.
현재의 포팅: 드라이버 소스 코드(로직)와 하드웨어 정보(주소, 핀 번호)를 분리합니다. 하드웨어 정보는 DTS(Device Tree Source)라는 텍스트 파일에 따로 적습니다.
예시: "내 드라이버 코드는 그대로 두고, DTS 파일만 수정해서 핀 번호를 5번에서 10번으로 바꾼다." -> 이것이 현대적인 드라이버 포팅 및 개발 방식입니다.
부트로더가 부팅 시 이 DTS 정보를 바이너리(.dtb)로 만들어 커널에게 넘겨주면, 커널 드라이버가 이를 읽어서 하드웨어를 제어합니다.
이 모든 개념을 합치면 다음과 같은 흐름이 완성됩니다.
(1) 개발 (Porting) : 개발자가 하드웨어 스펙에 맞춰 DTS를 작성하고, 이를 처리할 드라이버(.ko)를 빌드하여 커널에 적재(insmod)합니다.
(2) 등록 : 드라이버가 로드되면서 주번호(Major No)를 할당받고 커널에 자신을 등록합니다.
(3) 인터페이스 생성 : udev가 드라이버 정보를 보고 /dev/my_device 파일을 생성합니다.
(4) 사용 :
write(fd, "Data", ...) 호출./dev/my_device의 주번호를 보고 해당 드라이버의 write 함수 호출.copy_from_user()로 데이터 가져옴 -> 하드웨어 레지스터 제어 -> 하드웨어 동작.