[IT·HW.SW사용법]/H.W·S.W기술팁

[WindowsCE 6.0 특집 ④] USB CDC Driver를 이용한 Virtual Serial Driver

기쁨조미료25 2007. 11. 29. 01:38
[WindowsCE 6.0 특집 ④] USB CDC Driver를 이용한 Virtual Serial Driver의 구조 분석

김현수(SiRF Technology Senior Engineer)   2007/10/22
WindowsCE 6.0
embeddedce
Microsoft
DST
필자는 SiRF Technology의 FAE로서 활동하고 있으며, 윈도우임베디드커뮤니티인 WECOM의 MVP이기도 하다. 블로그 http://blog.naver.com/mclapd를 운영 중이다.

현재 임베디드 제품들은 PC와 같은 외부 단말기들에 다양한 형태의 통신 인터페이스를 제공하고 있다. 특히 최근에는 임베디드 제품에 블루투스, 무선모뎀(wireless modem), WiFi 등의 무선 인터페이스 지원에 대한 요구가 많아졌다.

일반적인 경우 CPU 내부에 이러한 인터페이스를 지원하는 컨트롤러를 내장하고 있지 않기 때문에 주변 디바이스(또는 모듈)에 의존하게 된다.

이런 주변 디바이스는 외부 단말기에 제공하는 인터페이스 외에 CPU와의 데이터 등의 처리를 시리얼(serial), USB, USP 등을 통해 하게 된다. 만약 OS 기반의 제품이라면 애플리케이션은 인터페이스의 프로토콜을 핸들링 할 수 있는 디바이스 드라이버가 필요하게 된다.

여기서 한 가지 상황을 가정해 보자. 어떤 회사의 엔지니어들이 많은 노력과 시간을 할애해서 serial 인터페이스를 갖고 있는 블루투스 모듈의 드라이버와 애플리케이션을 만들었지만 고객들은 더 빠른 통신 속도를 요구하였고 회사에서는 이에 대응하기로 결정했다.

다행히 시리얼 인터페이스를 USB로 대치한 디바이스가 나와서 이 디바이스로 다시 개발하기로 했다고 치자. 애플리케이션 엔지니어, 또는 디바이스 드라이버 엔지니어는 이미 만들어 놓은, 게다가 통신 속도 이외에는 동작에 아무런 문제가 없는 응용 프로그램과 디바이스 드라이버를 수정해야 하는 상황에 처하게 될 것이며 이는 많은 시간과 노력이 필요할 수도 있다.

이런 상황에서 애플리케이션이든지 디바이스 드라이버든지 어느 한 쪽만의 수정으로 문제가 해결된다면 최악의 상황에 놓이는 것은 피할 수 있겠지만 위와 같은 경우라면 두 부분에 모두 수정을 할 수 밖에 없게 된다. 그래서 이런 문제를 피하고자 한 가지 방법을 이 기고를 통해 제시하고자 한다.

물론 이 방법은 익히 널리 알려져 있고 쓰이고 있는 방법이기 때문에 특별히 새롭다거나 어렵지는 않다.

버추얼 시리얼 드라이버
앞서 언급한 문제에 대해서 필자가 제시하는 방법은 버추얼 시리얼 드라이버(virtual serial driver)의 사용이다. 그리고 아래는 버추얼 시리얼 드라이버 작성 개념에 대한 간단한 설명이다.

첫째, 애플리케이션은 주변 디바이스를 사용할 때 이 디바이스를 항상 시리얼 디바이스로 가정하고 사용하도록 작성한다.

즉, 주변 디바이스가 CPU와 USB로 연결되어 있든지, SPI로 연결되어 있든지 간에 모두 시리얼 장치로 보면 되는 것이다. 이로써 이 애플리케이션은 하드웨어가 무엇이든지, 또는 어떻게 바뀌든지에 관계없이 수정 없이(또는 아주 약간의 수정을 통해) 재사용할 수 있게 된다.

둘째, 디바이스 드라이버를 MDD와 PDD 형태로 나누고 MDD는 시리얼 디바이스 드라이버로 작성하고 나머지 PDD는 주변 디바이스의 인터페이스에 알맞은 디바이스 드라이버의 형태로 만들자. 물론 이 MDD와 PDD의 내부는 상황에 맞게 더 다양한 계층구조로 세분화 될 수도 있을 것이다.

USB CDC 클라이언트 드라이버
본 기고에서는 USB CDC 클라이언트 드라이버를 버추얼 시리얼 드라이버의 한 예로 들 것이며 유감스럽게도 지면의 제약상 위에서 제시한 내용 중 시리얼 디바이스 드라이버와 USB CDC 클라이언트 드라이버가 어떻게 연결되는가에 대한 연결 구조와 operation에 대한 procedure만을 다루려고 한다.

앞으로 언급할 CDC는 커뮤니케이션 디바이스를 위한 USB 서브 클래스로, 본 기고에서 다룰 USB CDC 클라이언트 드라이버는 스마트 폰 등에 사용되는 퀄컴 등의 무선 모뎀 디바이스를 위한 드라이버이다. 여기서 CDC specification에 준하기 위한 조건 중 하나인 flow control 등의 기능 등은 논외로 하겠다. 참고로 필자는 USB Host controller가 CPU 내부에 포함되어있지 않아 외장 USB Host 디바이스를 사용하였다.

한 가지 드라이버에 대한 내용이라도 상당히 많은 부분을 다루어야 하기 때문에 다음과 같이 몇 가지 전제조건이 필요할 것으로 본다.

첫째, 독자는 WinCE의 커널(Kernel), 디바이스 드라이버에 대한 지식을 가지고 있다.
둘째, 특히 시리얼 드라이버의 전체적인 메커니즘을 이해하고 있다.
셋째, USB Specification을 읽어 본 적이 있고 어느 정도의 USB 프로토콜, 하드웨어적인 지식, 그리고 USB 드라이버에 대한 지식을 가지고 있다.

차후 기회가 주어진다면 하드웨어에서부터 애플리케이션까지 전체적인 흐름과 구현 방법에 대해 다시 기고하기로 하겠다.

USB CDC 클라이언트 드라이버의 구조
그림 1 은 애플리케이션, USB CDC 클라이언트 드라이버, USBD, HCD의 계층간의 연결 구조를 보여주고 있다.

CDC 클라이언트 드라이버를 사용하는 애플리케이션의 목적은 모뎀 디바이스를 통해 데이터를 송신하거나 수신하는 것일 것이다.

이러한 동작을 단순화시켜 본다면 Read, Write, CDC Specification에 준하기 위한 기타 GPIO 제어 정도가 될 것 같다. 이 애플리케이션은 시리얼 디바이스 드라이버를 사용할 때와 마찬가지로 USB CDC 클라이언트 드라이버를 CreateFile()등의 API 함수를 통해 serial port를 open하여 위에서 말한 read, write 같은 동작을 사용하면 된다.

애플리케이션의 하위 계층인 USB CDC 클라이언트 드라이버의 시리얼 MDD 계층은 일반 시리얼 드라이버의 MDD2와 거의 같은 형태로 구성 되어있기 때문에 애플리케이션에게 대부분의 시리얼 드라이버 인터페이스를 제공한다.

따라서, 시리얼 드라이버를 사용하는 애플리케이션이라면 실제로 시리얼 인터페이스가 아니더라도 아무런 문제가 없게 된다.

예로 현재 대부분 GPS 모듈이 시리얼 인터페이스를 통해 위치 데이터를 넘겨주고 있는데 향후 USB 인터페이스를 제공하는 모듈을 사용하게 된다고 가정했을 때 맵(map) SW를 만드는 업체들은 이에 대한 아무런 수정을 하지 않아도 된다는 것이다.

다음 계층인 시리얼 PDD 계층은 상위 계층의 시리얼 드라이버(MDD)와 하위 계층인 USB 클라이언트 드라이버 간의 인터페이스 역할을 해주고 있다. 시리얼 MDD 계층에서 요구된 operation들을 시리얼 드라이버의 형태에서 USB 인터페이스 함수(주로 API)로 구현해 주게 된다.

만약 엔지니어가 원한다면 이 같은 동작을 USB 클라이언트 드라이버 쪽으로 옮겨도 문제는 없다. 이는 엔지니어의 선택사항으로 구현을 USB 클라이언트 드라이버에서 하는 것도 나쁘지는 않지만, 상위 계층과 하위 계층을 서로 연결해주는 역할을 하기 때문에 이 계층에서 하는 것이 더 좋지 않을까 생각한다.

위의 그림1에서 보라색 부분이 USB CDC 클라이언트 드라이버 부분이며 노란색 부분은 하드웨어가 바뀔 경우 또는 인터페이스가 변경되어 새로운 드라이버가 필요할 경우 교체될 부분을 나타내고 있다. 주변 디바이스에 대한 드라이버는 디바이스 제조사에서 제공하는 경우가 많기 때문에 엔지니어는 실제로 시리얼 PDD 부분을 주로 수정하게 될 것이다.

USB CDC 클라이언트 드라이버의 동작 과정
USB 클라이언트 드라이버의 전체적인 동작 과정을 보여주면 그림 2와 같다.

먼저 USB 클라이언트 드라이버를 빌트인 드라이버로 만들었기 때문에 시스템이 부팅될 때 Deivce.exe에 의해 드라이버는 초기화 되고 애플리케이션 등의 사용에 대기하게 된다.

무선 모뎀을 사용 하고자 하는 전화기 또는 웹 브라우저 같은 애플리케이션이 드라이버를 오픈하는 과정에서 CPU의 GPIO에 의해 물리적으로 끊어져 있던 USB 인터페이스 라인(D+, D-)이 연결되어 USB 호스트 디바이스와 모뎀 디바이스가 물리적으로 연결되게 되고, 이 연결로 인하여 모뎀 디바이스를 인식한 호스트 디바이스는 상위 계층으로 모뎀 디바이스의 인식을 알리게 된다.

이때 USBDeviceAttach() 함수가 호출되어 인터페이스에 관련된 USB pipe, event, descriptor 등이 처리되게 된다.

이러한 COM_Open() 함수의 수행으로 인터페이스가 사용 가능하게 되며 이때부터 애플리케이션은 궁극적으로 하고자 하는 동작인 read, write, 기타 포트 제어 등을 수행할 수 있게 된다.

마지막으로 모든 작업을 마친 애플리케이션은 COM_Close() 함수를 호출하고 시리얼 드라이버 부분과 USB 드라이버 부분에 대한 정리 작업을 수행하게 된다. 그리고 COM_Deinit()은 일반적인 빌트인 드라이버의 경우와 마찬가지로 호출되지 않는다. 지금까지 USB CDC client 드라이버의 초기화부터 close 되는 과정을 전체적으로 살펴보았다.

그럼 각 단계를 세분화하여 알아보도록 하자.

COM_Init
그림 3은 CDC 클라이언트 드라이버가 초기화되는 과정이다. 시리얼 MDD 계층으로부터 하위 계층의 초기화 함수를 통해 단계적으로 초기화 하는 것을 알 수 있다.

이 초기화 과정에서 시리얼 MDD 계층에서는 USB 인터페이스에 관련된 코드는 필요치 않다. 이 MDD 계층에서 호출한 시리얼 PDD 계층의 초기화 함수 PDD_SerInit()는 일반적인 시리얼 드라이버의 초기화 과정과 USB 인터페이스에 대한 초기화 작업을 수행하게 된다. 이 USB 인터페이스 초기화 작업을 설명하자면 다음과 같다.

USB 호스트 디바이스와 모뎀 디바이스간의 USB 인터페이스인 D+/D- 라인은 평상시 끊어져 있는 상태로 유지되며 애플리케이션에 의해 USB CDC 클라이언트 드라이버가 모뎀 디바이스를 사용하려고 할 때 연결된다.

이는 CPU의 GPIO에 의해 제어된다. 따라서, 이 Initialize 과정에서는 제어를 담당하는 GPIO에 대한 현재 상태를 체크하여 만약 연결되어 있다면 연결이 끊어진 상태로 유지시키는 역할만 한다.

그리고, USB 호스트 디바이스를 suspend 상태로 진입시켜 전원에 대한 소비를 최소화 한다. 아직 USB 장치가 동작되고 있는 상태가 아니기 때문에 USB 동작에 관련된 pipe나 descriptor 그리고 각종 event에 대한 생성 및 초기화는 하지 않고 단순히 USB 인터페이스 라인과 기타 관련 디바이스들에 대한 초기화만 처리한다.

COM_Open
그림 4는 애플리케이션이 드라이버를 오픈했을 때의 과정을 나타내고 있다.

시리얼 MDD 계층의 COM_Open() 함수에서 일반 시리얼 드라이버와 마찬가지로 드라이버의 open operation을 수행하기 위해 시리얼 PDD의 PDD_SerOpen() 함수를 호출 한다.

이 PDD_SerOpen() 함수에서 다시 USB_SetPortControl()를 호출하여 USB 호스트 디바이스에 전원을 인가하거나 USB 인터페이스 D+/D- 라인을 제어하여 USB 호스트 디바이스와 모뎀 디바이스를 연결시켜주는 등의 동작을 수행하고 이 동작에 대한 결과를 USBDeviceAttach() 함수로부터 기다리게 된다.

이때 USB 호스트 디바이스는 모뎀 디바이스가 자신의 허브 포트에 꽂힌 하나의 디바이스 장치로 인식하게 되고 적합한 클라이언트 드라이버를 로딩하는 과정이 일어나게 된다. 이러한 일련의 과정에서 USBDeviceAttach()가 호출되게 된다.

USBDeviceAttach() 함수는 USB transfer에서 필요한 Bulk와 Interrupt에 대한 pipe, 여러 dispatch event 대한 생성, descriptor 처리 등을 하며 모든 동작이 완료되었을 때 이 사실을 event로 signal하여 장치가 잘 인식되었음을 알리게 된다.

결과를 대기 중인 SetPortControl() 함수는 signal을 받았을 경우 드라이버의 오픈 과정을 완료하게 되고 timeout 등의 원인으로 동작의 결과가 실패라면 이에 대한 에러 처리를 하게 된다.

몇 가지 이유로 위에서 말한 event를 받지 못해 timeout이 발생 했을 때는 다시 USB 호스트 디바이스나 USB 인터페이스 라인을 끊었다가 붙여주는 등의 동작을 여러 번 시도해 보는 루틴을 넣는 것이 좋다.

COM_Read
그림 5는 Read 동작에 대한 과정을 나타내고 있다.

먼저 소스 1을 보면 USB_BulkInThread()에서 lpIssueBulkTransfer() API 함수로 Bulk In pipe로 read transfer를 수행하며 이때 USB_BulkInTransferComplete()함수를 두 번째 인자로 넘겨주어 transfer가 완료되었을 때 이 사실을 signal 할 callback 함수로 등록한다.

실제로 Read transfer가 문제 없이 진행되었으면 얻은 데이터를 read queue로 옮기기 위한 함수인 PutToRxQueue()를 호출한다.

PutToRxQueue() 함수에서 인자로 넘겨받은 buffer의 내용 중 일부 또는 전부를 read queue에 카피하고 카피한 byte만큼 이 queue의 write index를 증가시켜 업데이트한다. 그리고 PDD_SerialDispatchThread()에 데이터를 수신했다는 event를 signal해 receive interrupt handler인 PDD_SerRxIntr() 함수가 이를 처리하도록 한다.

이 receive interrupt handler에서 read queue의 내용을 buffer로 다시 옮기고 읽어간 byte만큼 read queue의 read index를 조정한다. 마지막으로 COM_Read() 함수와 수신을 대기중인 애플리케이션에게 데이터를 가져갈 것을 event로 알려준다. 이러한 과정은 USB read transfer로 얻어온 데이터를 모두 전달할 때까지 반복된다.


COM_Write
그림 6은 Write에 대한 전체적인 과정을 보여주고 있다.

시리얼 MDD의 COM_Write() 함수에서 같은 계층에 있는 DoTxData()를 호출한다. 이것은 일반적인 시리얼 드라이버의 sequence와 같은 것이다.

이 함수에서는 다시 하위 계층인 Serial PDD의 transmit interrupt handler인 PDD_SerTxIntrEx() 함수를 호출한다. 이 transmit interrupt handler에서는 소스 2와 같이 lpIssueBulkTransfer() API 함수를 통해 Bulk Out pipe로 전송을 시도한다.


이때 함수의 인자 중 두 번째 인자인 callback 함수, USB_BulkOutTransferComplete()를 등록하고 전송에 대한 결과를 기다리게 된다. 전송이 이상 없이 끝났음을 callback 함수로부터 signal 받으면 PDD_SerialDispatchThread()에서 다시 DoTxData() 함수가 호출된다. 이 과정은 모든 데이터가 Bulk Out pipe를 통해 전송될 때까지 반복되게 된다.

위에서 read transfer에서도 사용한 lpIssueBulkTransfer() 함수를 대신해서 IssueBulkTransfer() 함수를 사용해도 무관하지만,IssueBulkTransfer() 함수도 결과적으로는 lpIssueBulkTransfer()를 호출하게 되므로 직접 사용하였다.

COM_Close
그림 7은 클로즈(Close)에 대한 과정을 나타내고 있다. 오픈하는 과정과 반대되는 동작이라 당연한 것일지는 모르겠지만 오픈의 과정과 많은 부분이 sequence적으로 유사하다.

Serial MDD 계층의 COM_Close() 함수는 하위 계층의 PDD_SerClose() 함수를 호출하게 된다. 이 함수에서는 USB 인터페이스에 사용하던 thread나 event등을 정리하고 하위 계층의 DeviceNotify() 함수에 event를 signal해서 생성해 놓은 USB pipe 나 관련 transfer에 관련된 event등을 삭제하도록 한다.

동시에 같은 하위계층의 USB_SetPortControl()을 호출해서 USB 인터페이스 라인은 끊도록 한다.

USB_SetPortControl() 함수에서 USB 인터페이스 라인을 끊은 후, DeviceNotify()로부터 모든 USB 드라이버의 object들이 제거되었는지를 확인하기 위해 대기한다. 이런 정리과정에 문제가 없을 경우 USB_close() 함수를 호출해서 USB host 디바이스를 다시 suspend 상태로 유지되도록 만든다.

마치며
지금까지 버추얼 시리얼 드라이버의 필요성과 장점, 그리고 버추얼 시리얼 드라이버의 예로 설명한 USB CDC 클라이언트 드라이버의 전체적인 동작과 연결 구조에 대해서 살펴 보았다.

전체적인 부분을 구체적으로 다루지 못하고 일부분만 다루었기 때문에 기고 자체가 독자 분들의 이해에 오히려 혼란을 줄까 염려된다.

이러한 이유로 완벽하게 제공치 못하는 소스 코드의 참조를 자제했으며 대신 블록 다이어그램으로 대치했다. 개인적인 바람이지만 비슷한 종류의 드라이버를 개발할 때 약간이나마 참고가 되길 바란다.

엔지니어에게는 역시 소스 코드를 하나하나 분석하는 것이 제일 명확한 방법이기 때문에 인터넷상에 공개되어있는 비슷한 종류의 드라이버를 구해서 궁금했던 부분을 살펴보는 것이 좋을 것 같다. @

참고 자료
Company and Organization
-USB Implementers Forum : http://www.usb.org
-LeCroy : http://www.lecroy.com

Tools
-LeCroy Chief : Protocol analyzer
-USBView : 현재 PC에 접속된 USB 장치의 Descriptor를 출력한다.
-Snoopy : USBView의 기능에 USB 장치의 load/unload 기능이 추가되어 있다.

Windows CE Serial driver and CPU vendor’s BSP/CSP

Books
-USB Complete ? Axelson(lakeview research)
-USB Guide ? 김형훈(Ohm)