Parallel Port에 대해서
먼저 맨땅에 데이터시트를 읽으려니 이해가 힘들어서 Parallel port 프로토콜에 대해 대충 알아봤다. Parallel port란 물리적인 포트를 의미하기도 하지만, 이를 통해 이루어지는 통신 방식도 포함한다.
먼저 Parallel port는 보통 DB-25 커넥터를 사용하며 아래 핀 배열을 기반으로 한다.
핀 번호 | 신호 이름 | 설명 | 방향 |
1 | Strobe | 데이터 전송 시작 신호 | 출력 |
2-9 | Data0-Data7 | 8비트 데이터 라인 | 출력(기본)/입력 |
10 | Ack | 데이터 수신 확인 신호 | 입력 |
11 | Busy | 장치 사용 중 신호 | 입력 |
12 | Paper-Out | 용지 부족 신호 | 입력 |
13 | Select | 장치 선택 신호 | 입력 |
14 | Auto Feed | 자동 줄 바꿈 제어 | 출력 |
15 | Error | 장치 오류 신호 | 입력 |
16 | Initialize | 장치 초기화 제어 | 출력 |
17 | Select-In | 장치 선택 제어 | 출력 |
18-25 | Ground | 공통 접지 | - |
그리고 대표적인 Parallel port 통신 프로토콜로 SPP, EPP, ECP가 존재하며 주요 특징은 다음과 같다.
- SPP (Standard Parallel Port)
- 단방향 프로토콜로 데이터 핀(2-9)를 출력으로만 사용한다.
- EPP (Enhanced Parallel Port)
- 양방향 통신을 지원하며 14번 핀을 통해 데이터 핀(2-9)의 전송 방향을 제어한다.
- ECP (Extended Capability Port)
- 역시 양방향 통신이며 추가로 17번 핀을 활용해 DMA 기반 고속 데이터 전송이 가능하다.
CH382 데이터 시트 분석
그럼 페러럴 포트의 프린터 제어신호와 데이터 라인을 GPIO처럼 제어해보기 위해 데이터 시트를 분석해보자.
Parallel Port Register는 I/O base address + offset 0 에 위치하며 사이즈는 4바이트이다. 데이터시트에 따로 언급이 없어서 실험적으로 찾아보니 BAR2 영역을 쓰고있다. 그리고 페러럴포트의 통신모드는 SPP, EPP, ECP 세 가지 중에서 별도의 핸드셰이크나 데이터 래칭이 없는 SPP모드를 사용할거다.
여기서 다룰 레지스터는 PDR과 PSR이다.
Base + 0 (PDR) : D7-D0 출력을 제어한다.
Base + 1 (PSR) : 입력 신호를 읽어 장치의 동작 상태를 모니터링하는데 사용하는 레지스터다.
디바이스 드라이버 테스트를 위한 구성
DSUB25 F/M 케이블을 구입하여 점퍼 케이블로 필요한 입출력만 따로 뺐다.
디바이스 드라이버 작성
- my_ch382l_driver.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/pci.h> // PCI 장치 관련 함수와 구조체가 정의된 헤더
#include <linux/io.h> // I/O 메모리 매핑 관련 헤더
#include <linux/jiffies.h> // 시스템 타이머 인터럽트(1ms 간격)가 발생할 때마다 증가하는 전역 변수를 사용하기 위한 헤더
#include <linux/timer.h> // 커널 타이머 관련 헤더
#define CH382L_VENDOR_ID 0x1C00 // CH382L의 PCI 벤더 ID
#define CH382L_DEVICE_ID 0x3050 // CH382L의 PCI 디바이스 ID
#define BAR0 0 // Base Address Register 0
#define BAR1 1 // Base Address Register 1
#define BAR2 2 // Base Address Register 2
#define PDR_OFFSET 0x0 // PDR 레지스터의 오프셋
#define PSR_OFFSET 0x1 // PSR 레지스터의 오프셋
static struct pci_dev *pdev; // PCI 장치 구조체 포인터
static void __iomem *bar2_base; // BAR2 메모리를 맵핑할 주소
static struct timer_list my_timer; // 커널 전용 타이머 구조체
// 타이머 콜백 함수
void timer_callback(struct timer_list *data) {
u8 psr, pdr;
psr = ioread8(bar2_base + PSR_OFFSET); // PSR 읽기
// pdr = ioread8(bar2_base + PDR_OFFSET);
// printk("MY_CH382L_DRIVER: psr 0x%02X\n", psr);
// printk("MY_CH382L_DRIVER: pdr 0x%02X\n", pdr);
iowrite8(((psr >> 4) & 0x7) << 3, bar2_base + PDR_OFFSET); // PDR에 쓰기
mod_timer(&my_timer, jiffies + msecs_to_jiffies(100)); // 100ms 후 콜백되도록 타이머 재설정
}
// 모듈 초기화 함수
static int __init ch382l_init(void) {
int ret;
printk("MY_CH382L_DRIVER: Load module\n");
// VENDER ID와 DEVICE ID에 일치하는 첫 번째 PCI 장치를 가져오는 함수
pdev = pci_get_device(CH382L_VENDOR_ID, CH382L_DEVICE_ID, pdev);
if(pdev == NULL) {
printk("MY_CH382L_DRIVER: Failed to find PCI device\n");
return -ENOENT;
}
// 장치 활성화
ret = pci_enable_device(pdev);
if(ret) {
printk("MY_CH382L_DRIVER: Failed to enable PCI device\n");
return ret;
}
// BAR 영역을 안전하게 사용하도록 권한을 요청하는 함수
// 세번째 인자는 요청하는 영역에 대한 식별 문자열로 sudo cat /proc/ioports로 확인할 수 있다.
ret = pci_request_region(pdev, BAR2, "ch382l_bar2");
if(ret) {
printk("MY_CH382L_DRIVER: Failed to request BAR2 I/O region\n");
pci_disable_device(pdev);
return ret;
}
// BAR2 공간을 시스템 메모리 맵핑하는 함수
bar2_base = pci_iomap(pdev, BAR2, pci_resource_len(pdev, BAR2));
if(!bar2_base) {
printk("MY_CH382L_DRIVER: Failed to map BAR2\n");
pci_release_region(pdev, BAR2);
pci_disable_device(pdev);
return -ENOMEM;
}
// 100ms 주기로 타이머 설정
timer_setup(&my_timer, timer_callback, 0);
mod_timer(&my_timer, jiffies + msecs_to_jiffies(100));
pr_info("MY_CH382L_DRIVER: Module loaded successfully\n");
return 0;
}
// 모듈 종료 함수
static void __exit ch382l_exit(void) {
del_timer(&my_timer); // 타이머 삭제
if(bar2_base) pci_iounmap(pdev, bar2_base); // BAR2 메모리 매핑 해제
pci_release_region(pdev, BAR2); // BAR2 I/O 영역 해제
pci_disable_device(pdev); // 장치 비활성화
printk("MY_CH382L_DRIVER: Unload module\n");
}
// 모듈 초기화 및 종료 함수 등록
module_init(ch382l_init);
module_exit(ch382l_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("JSH");
MODULE_DESCRIPTION("A simple LKM for a PCI to parallel port adapter CH382L");
- Makefile
obj-m += my_ch382l_driver.o
all :
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
동작확인
먼저 lspci로 확인해봤을 때 해당 PCI장치가 다른 드라이버(parport_pc)에 의해 사용중인상태기 때문에 먼저 해당 드라이버를 언로드해야 PCI 장치를 사용할 수 있다.
다음 명령으로 04:00.0 장치에서 parport_pc를 언바인드한다.
echo -n "0000:04:00.0" | sudo tee /sys/bus/pci/drivers/parport_pc/unbind > /dev/null
- echo -n : 줄바꿈을 생략하고 출력한다.
- tee : 주로 파이프(|)와 함께 사용되어 표준입력으로 들어온 데이터를 파일에 저장하는 동시에 표준출력으로 출력한다.
- > /dev/null : 출력을 버린다.
참고로 위 명령은 1회성이라 재부팅시 매번 언바인드 해줘야 한다. 개발 시에는 위 방법으로 충분한 것 같고 운영 시에는 udev 규칙을 추가하여 장치를 관리할 수 있다. udev 규칙은 다음에 기회가 되면 정리해보도록 하겠다.
빌드 후 모듈 로드
$make
$sudo insmod my_ch382l_driver.ko
dmesg로 커널 메시지 확인 --follow 옵션을 사용하면 실시간으로 볼 수 있다.
$dmesg
$dmesg | tail -n 5
$dmesg --follow
$dmesg -w
모듈 해제
$sudo rmmod my_ch382l_driver
코드 분석
1. I/O Port, Memory Mapped I/O 접근
PCIe의 BAR 영역은 I/O Port랑 Memory Mapped I/O로 구분된다. 내가 제어한 CH382L의 BAR2는 I/O Port이고 고전적으로 I/O Port에 접근할 때는 inb, outb, inw, outw, inl, outl와 같은 CPU 어셈블리 명령어를 사용했다.
현재도 할당된 I/O Port 주소를 알면 아래와 같은 방식으로 제어할 수 있지만, 최근에는 잘 사용되지 않는다.
u8 value = inb(0xe100);
outb(0xFF, 0xe100);
대신 위에서 한 방식대로 __iomem 포인터를 선언하고 I/O 공간을 맵핑한 뒤 ioread8, iowrite8 함수로 읽고 쓴다.
이는 커널에서 하드웨어 장치 레지스터를 메모리 주소로 매핑하여 접근하는 방식으로 과거에는 Memory 공간에만 사용 했지만 요즘에는 I/O 공간과 Memory 공간 모두에 해당 방식을 사용한다.
static void __iomem *bar2_base;
// ...
bar2_base = pci_iomap(pdev, BAR2, pci_resource_len(pdev, BAR2));
// ...
if(bar2_base) pci_iounmap(pdev, bar2_base);
// ...
psr = ioread8(bar2_base + PSR_OFFSET);
// ...
iowrite8(((psr >> 4) & 0x7) << 3, bar2_base + PDR_OFFSET);
__iomem 메모리는 일반 CPU 메모리처럼 직접 접근할 수 없고 아래와 같은 I/O 메모리 접근 함수를 사용해야 한다.
- ioread8 / ioread16 / ioread32
- iowrite8 / iowrite16 / iowrite32
2. pci_driver 구조체 사용 방식 + 최신 리눅스 커널 방식으로 바꿔보기
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/pci.h>
#include <linux/io.h>
#include <linux/jiffies.h>
#include <linux/timer.h>
#undef pr_fmt
#define pr_fmt(fmt) KBUILD_MODNAME ": %s, " fmt, __func__
#define CH382L_VENDOR_ID 0x1C00
#define CH382L_DEVICE_ID 0x3050
#define BAR2 2
#define PDR_OFFSET 0x0
#define PSR_OFFSET 0x1
// 장치 데이터를 저장하기 위한 구조체
struct ch382l_data {
void __iomem *bar2_base;
struct timer_list timer;
};
// 타이머 콜백 함수
void timer_callback(struct timer_list *t) {
// container_of로 timer_list 구조체를 포함하는 ch382l_data 구조체에 접근
struct ch382l_data *data = container_of(t, struct ch382l_data, timer);
u8 psr;
psr = ioread8(data->bar2_base + PSR_OFFSET);
iowrite8(((psr >> 4) & 0x7) << 3, data->bar2_base + PDR_OFFSET);
mod_timer(&data->timer, jiffies + msecs_to_jiffies(100));
}
// PCI 프로브 함수
static int ch382l_probe(struct pci_dev *pdev, const struct pci_device_id *id) {
struct ch382l_data *data;
int ret;
pr_info("Probing device\n");
ret = pcim_enable_device(pdev);
if (ret) {
pr_info("Failed to enable PCI device\n");
return ret;
}
// BAR2 영역을 안전하게 사용하도록 매핑
ret = pcim_iomap_regions(pdev, BIT(BAR2), "ch382l_bar");
if (ret) {
pr_info("Failed to map BAR2 I/O region\n");
return ret;
}
// 장치 데이터를 저장할 메모리를 동적 할당
data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
if(!data) {
pr_info("Failed to allocate memory for device data\n");
return -ENOMEM;
}
// PCI 드라이버 데이터에 장치 데이터 구조체를 연결
pci_set_drvdata(pdev, data);
// BAR2의 매핑된 주소를 저장
data->bar2_base = pcim_iomap_table(pdev)[BAR2];
if(!data->bar2_base) {
pr_info("Failed to read mapped BAR2\n");
return -ENOMEM;
}
timer_setup(&data->timer, timer_callback, 0);
mod_timer(&data->timer, jiffies + msecs_to_jiffies(100));
pr_info("Device initialized successfully\n");
return 0;
}
// PCI 제거 함수
static void ch382l_remove(struct pci_dev *pdev) {
// PCI 드라이버 데이터에서 장치 데이터 구조체를 가져옴
struct ch382l_data *data = pci_get_drvdata(pdev);
// 타이머를 삭제하고 실행 중인 콜백을 기다림
del_timer_sync(&data->timer);
pr_info("Device removed\n");
}
// 지원하는 PCI 디바이스 ID 리스트
static const struct pci_device_id ch382l_pci_ids[] = {
{ PCI_DEVICE(CH382L_VENDOR_ID, CH382L_DEVICE_ID) },
{ 0, }
};
// 드라이버가 지원하는 장치 정보를 등록한다
MODULE_DEVICE_TABLE(pci, ch382l_pci_ids);
// PCI 드라이버 구조체
static struct pci_driver ch382l_driver = {
.name = "ch382l_driver",
.id_table = ch382l_pci_ids,
.probe = ch382l_probe,
.remove = ch382l_remove,
};
// PCI 드라이버 등록
module_pci_driver(ch382l_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("JSH");
MODULE_DESCRIPTION("A simple LKM for a PCI to parallel port adapter CH382L");
✓ pci_driver 구조체 사용
pci_driver는 PCI 장치 드라이버를 정의하는 핵심 구조체로 특정 VID/DID를 매칭하고 장치 탐지/제거 시 호출될 함수를 지정한다.
- name : 고유한 드라이버 이름
- id_table : 해당 드라이버가 지원하는 PCI 장치의 VID, DID 목록
- probe : 장치가 탐지되었을 때 호출되는 초기화 함수
- remove : 장치가 제거되었을 때 호출되는 종료 함수
pci_driver 구조체 사용 시의 특징은 커널의 PCI 서브시스템이 장치와 드라이버간 자동 매칭 및 관리를 수행한다는 점이다. id_table에 있는 장치가 탐지되면 커널은 자동으로 probe 함수를 호출하고, 반대로 장치가 제거되면 remove 함수를 호출한다. 때문에 이전 init 코드에서 사용했던 pci_get_device() 함수로 장치를 수동으로 검색하여 객체 포인터(struct pci_dev *pdev)를 가져오는 작업이 필요 없고, probe, remove 함수 호출시 전달받는다.
한편 pci_driver와 같은 고유 드라이버 구조체는 모든 서브시스템에 존재하는 것은 아니며, 버스 개념이 있는 인터페이스에만 존재한다. 대표적으로 i2c_driver, spi_driver, usb_driver 등이 있다.
이 외에 버스 개념이 없고 하드웨어 정보(Device Tree, x86의 경우 ACPI)를 기반으로 제어되는 장치의 경우 가상 플랫폼 버스 위에서 동작하는 platform_driver를 사용해 이와 비슷한 방식으로 드라이버를 구현 할 수 있다.
✓ module_pci_driver() 매크로 사용
또한 여기서는 module_pci_driver() 매크로를 사용하고 있는데 기존의 module_init(), module_exit() 매크로를 사용한다면 각각 __init, __exit 함수에서 pci_driver를 등록(pci_register_driver())하고 해제(pci_unregister_driver()) 해야 하는데, module_pci_driver() 매크로를 사용하면 이를 한 줄로 간소화 한다.
✓ 리소스 관리 자동화 및 안전성 강화
이전 코드와 달리 pcim_*, devm_ 등 리소스 관리가 자동으로 이루어지는 커널 함수들을 사용했다. 이렇게 뒤에 m 접미사가 붙은 함수들은 객체(장치)가 제거되거나 드라이버가 언로드될 때 커널이 자동으로 리소스를 정리(clean-up)하는 함수를 나타낸다.
1.pci_request_region(), pci_iomap() 대신 pcim_iomap_regions() 사용
pcim_iomap_regions() 함수는 BAR 리소스를 요청하고 이를 커널 가상 주소 공간에 매핑하는 역할을 한다. 이 함수로 매핑된 주소는 pcim_iomap_table() 함수가 반환하는 포인터 배열에서 접근할 수 있으며, 배열의 인덱스는 BAR 번호에 해당한다. pcim_iomap_regions()두 번째 인자는 맵핑할 BAR를 지정하는 비트마스크인데, BIT(n) 매크로 특정 BAR를 선택하거나, BIT(0) | BIT(1)로 여러 BAR를 동시에 선택할 수도 있다. 또한, 리소스가 자동으로 관리되는 PCI Managed API이므로 드라이버가 언로드될 때 pci_release_region이나 pci_iounmap을 호출할 필요가 없다.
2. 장치 관리용 데이터 캡슐화
struct ch382l_data 구조체를 사용하여 장치 관련 정보를 캡슐화하고, devm_kzalloc()을 통해 메모리를 할당 및 언로드 시 자동 해제되도록 한다. 그리고 이렇게 메모리가 할당된 구조체 포인터를 pci_set_drvdata()를 통해 PCI 장치와 연결한다. 정확히는 struct pci_dev 구조체 내부의 struct device 구조체에 포함된 void *driver_data와 연결되는데, 이후 pci_get_drvdata()를 데이터를 언제든지 불러올 수 있다.
또한 타이머 콜백 함수에서는 container_of 매크로를 통해 장치 데이터에 접근하고 있는데, 이 매크로는 특정 구조체 멤버의 주소를 기준으로 이를 포함하고 있는 부모 구조체에 안전하게 접근할 수 있도록 설계되었다. 인자로는 (구조체 멤버 포인터, 부모 구조체의 타입, 부모 구조체에서 멤버의 이름)을 받는다.
3. del_timer() vs del_timer_sync()
del_timer()는 활성화된 타이머를 바로 제거하지만 del_timer_sync()는 타이머 콜백 함수가 실행 중인 경우, 해당 함수의 실행이 완료될 때까지 대기했다가 삭제한다.
'임베디드 개발 > 리눅스 디바이스 드라이버' 카테고리의 다른 글
VSCode에서 리눅스 커널 모듈 개발시 Intellisense Error 없애기 (0) | 2024.12.29 |
---|---|
LDD ] PCIe 디바이스 드라이버 작성하기 - (1) (0) | 2024.12.25 |
LDD ] printk를 활용한 디버깅 (0) | 2024.10.06 |
LDD ] 리눅스 커널 모듈 In-tree building (0) | 2024.10.04 |
LDD ] Hello World 커널 모듈 + Makefile 작성법 (0) | 2024.10.02 |