들어가며
완벽한 세상이라면 USB 장치와 허브는 문제없이 동작하고, 발생하는 어떤 문제든 스스로 해결할 수 있을 것입니다. 현실에서는 온갖 이상한 일이 일어나므로 뭔가 조치를 취해야 합니다. 가장 흔한 해결책은 USB 플러그를 뽑았다가 다시 꽂는 것입니다. 그런데 이런 동작을 자동으로 해야 한다면 어떨까요?
가장 강력한 해결책은 다른 웹 페이지에 소개되어 있습니다. 이 방법은 USB 허브의 드라이버를 종료했다가 다시 시작하게 만듭니다. 그 효과는 컴퓨터에 연결된 모든 USB 장치를 뽑았다가 다시 꽂는 것과 거의 같습니다. 가장 무거운 망치 같은 방법이지만, 때로는 필요합니다.
이 페이지에서는 특정 USB 장치 하나를 리셋하는 두 가지 방법을 소개합니다. 먼저 각 방법을 최소한의 설명과 함께 보여드리고, 그다음에 기술적인 세부 사항을 자세히 다루겠습니다.
여기서 다루는 내용은 리눅스에 한정됩니다. 두 번째 방법은 다른 플랫폼에서도 가능할 수 있겠지만, 이 글에서는 리눅스를 기준으로 설명합니다.
이 글의 많은 내용은 Universal Serial Bus Specification Revision 2.0(USB 2.0 규격)에서 가져왔습니다. 이 문서의 파일명은 보통 usb_20.pdf 입니다.
방법 #1: 리눅스 커널에 장치 리셋 요청하기
이 작업을 수행할 수 있는 도구는 여러 가지입니다. 가장 발전된 도구는 usbutils 패키지에 들어 있습니다. usbreset.c 파일을 내려받아 gcc 컴파일러로 컴파일하면 됩니다.
$ gcc -O3 -Wall -g usbreset.c -o usbreset
그리고 다음과 같이 실행합니다.
$ ./usbreset Usage: usbreset PPPP:VVVV - reset by product and vendor id usbreset BBB/DDD - reset by bus and device number usbreset "Product" - reset by product name Devices: Number 001/004 ID 045e:07b2 Microsoft® Nano Transceiver v1.0 Number 001/002 ID 04f3:0103 $ ./usbreset 001/004 Resetting Microsoft® Nano Transceiver v1.0 ... can't open [Permission denied] $ sudo ./usbreset 001/004 Resetting Microsoft® Nano Transceiver v1.0 ... ok $ sudo ./usbreset 045e:07b2 Resetting Microsoft® Nano Transceiver v1.0 ... ok
리셋 요청에 대한 응답으로 커널 로그에는 다음처럼 나타납니다.
usb 1-6: reset full-speed USB device number 4 using xhci_hcd
위 실행 결과에서 알 수 있는 점은 다음과 같습니다.
- 인자 없이 usbreset 명령을 실행하면 USB 장치 목록이 출력됩니다. lsusb 명령으로도 같은 정보를 얻을 수 있습니다.
- 실제로 장치를 리셋하려면 root 권한이 있어야 합니다(그래서 sudo 명령을 사용했습니다).
- 장치는 버스 주소(버스 번호와 장치 번호)로 선택할 수 있습니다. 버스 주소는 장치가 연결될 때마다 바뀝니다.
- Vendor ID(제조사 ID) 또는 Product ID(제품 ID)를 사용해 장치를 선택할 수도 있습니다. 같은 종류의 USB 장치가 하나뿐일 때는 이 방법이 더 좋습니다.
- 리셋 후에도 장치의 USB 버스 주소는 바뀌지 않았습니다(리셋 때문에 재열거되지 않았습니다).
그렇다면 usbreset 명령은 어떤 일을 할까요? USB 장치의 디바이스 파일에 대해 다음 명령을 실행하는 것입니다.
ioctl(fd, USBDEVFS_RESET, 0)
결국 커널의 usb_reset_device() 함수를 호출하는 셈입니다. 이 함수는 단순히 장치를 리셋하는 것보다 훨씬 많은 일을 합니다. 전체 과정을 매끄럽게 만들기 위한 것입니다. 이 함수는 요청이 있으면 리셋 전후에 장치의 드라이버에게 리셋 사실을 알립니다. 리셋 전에 드라이버를 바인딩 해제(unbind)하고, 리셋 후에는 다시 바인딩합니다. 리셋 후에는 장치의 구성도 다시 로드합니다. 이 과정이 없다면 장치는 자신의 버스 주소를 알지도 못하고 데이터 교환 준비도 되지 않을 것입니다.
따라서 장치를 다시 연결하는 것과 거의 같지만, 장치가 자신을 식별하도록 요구하지 않습니다(이미 정보를 알고 있으니까요). 또한 새 주소를 할당하지도 않습니다.
USB 3.x의 경우 이 리셋은 핫 리셋(hot reset)이며 효율이 떨어지는 유형입니다. 핫 리셋에 대한 자세한 내용은 아래에서 설명합니다.
방법 #2: USB 포트의 전원 상태 토글하기
이 목적에도 여러 도구가 있습니다. 여기서는 hubpower 도구로 설명하겠습니다. hubpower.c 파일을 내려받아 컴파일합니다.
$ gcc -O3 -Wall -g hubpower.c -o hubpower
이 도구는 사용법이 조금 더 까다롭습니다. 명령이 USB 장치 자체로 전송되지 않기 때문입니다. 요청은 USB 장치가 연결된 USB 허브로 전송됩니다.
따라서 첫 번째 단계는 어떤 허브인지 알아내는 것입니다. 먼저 USB 장치의 주소를 찾아보겠습니다.
$ lsusb Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub Bus 001 Device 004: ID 045e:07b2 Microsoft Corp. Bus 001 Device 002: ID 04f3:0103 Elan Microelectronics Corp. ActiveJet K-2024 Multimedia Keyboard Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
USB 장치는 버스 1번, 장치 4번에 있습니다. 장치 번호는 USB 장치가 열거될 때마다 바뀝니다.
그렇다면 USB 장치는 어떤 허브의 어떤 포트에 연결되어 있을까요?
$ lsusb -t /: Bus 02.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/6p, 5000M /: Bus 01.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/12p, 480M |__ Port 6: Dev 4, If 0, Class=Human Interface Device, Driver=usbhid, 12M |__ Port 6: Dev 4, If 1, Class=Human Interface Device, Driver=usbhid, 12M |__ Port 6: Dev 4, If 2, Class=Human Interface Device, Driver=usbhid, 12M |__ Port 11: Dev 2, If 0, Class=Human Interface Device, Driver=usbhid, 1.5M |__ Port 11: Dev 2, If 1, Class=Human Interface Device, Driver=usbhid, 1.5M
장치 4번은 포트 6번에 연결되어 있습니다. 허브의 버스 번호는 1, 장치 번호는 1입니다(이 허브는 루트 허브(root hub)로 동작하는 메인보드의 USB 컨트롤러입니다).
hubpower 도구가 이 상황을 어떻게 보여주는지 확인해 보겠습니다.
$ sudo ./hubpower 1:1 status
Port 1 status: 0100 Power-On
Port 2 status: 0100 Power-On
Port 3 status: 0100 Power-On
Port 4 status: 0100 Power-On
Port 5 status: 0100 Power-On
Port 6 status: 0103 Power-On Enabled Connected
Port 7 status: 0100 Power-On
Port 8 status: 0100 Power-On
Port 9 status: 0100 Power-On
Port 10 status: 0100 Power-On
Port 11 status: 0303 Low-Speed Power-On Enabled Connected
Port 12 status: 0100 Power-On
여기서 "1:1" 부분은 허브의 버스 주소입니다. 외장 허브라면 이 주소는 허브를 컴퓨터에 연결할 때마다 바뀝니다.
포트 번호는 절대 바뀌지 않습니다(장치가 같은 물리적 포트에 연결되어 있는 한).
이제 장치가 연결된 포트 번호를 알았으니 리셋해 보겠습니다.
$ sudo ./hubpower 1:1 power 6 off Port 6 status: 0000 Power-Off $ sudo ./hubpower 1:1 power 6 on Port 6 status: 0100 Power-On
첫 번째 명령을 실행하면 커널 로그에 장치 연결 해제가 기록됩니다.
usb 1-6: USB disconnect, device number 4
두 번째 명령을 실행하면 컴퓨터는 장치가 물리적으로 연결된 것처럼 동작합니다.
usb 1-6: new full-speed USB device number 5 using xhci_hcd
usb 1-6: New USB device found, idVendor=045e, idProduct=07b2, bcd Device= 7.04
usb 1-6: New USB device strings: Mfr=1, Product=2, SerialNumber=0
usb 1-6: Product: Microsoft® Nano Transceiver v1.0
usb 1-6: Manufacturer: Microsoft
[ ... ]
재열거 때문에 장치에 새 버스 주소가 할당됩니다.
$ lsusb
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 005: ID 045e:07b2 Microsoft Corp.
Bus 001 Device 002: ID 04f3:0103 Elan Microelectronics Corp. ActiveJet K-2024 Multimedia Keyboard
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
그럼 이 방법은 USB 장치를 물리적으로 분리했다가 다시 연결하는 것과 같을까요? 대답은 "아마도"입니다. 대부분의 경우 hubpower 명령은 실제로 USB 장치의 전원 공급을 차단하지 않습니다. 따라서 장치는 계속 VBUS 전압(5V)을 공급받습니다. 실제로 전원을 차단하는 USB 허브는 극히 드뭅니다. 이에 대해서는 아래에서 자세히 설명합니다.
대다수의 허브에서 첫 번째 명령("power off")은 포트를 장치를 무시하는 상태로 만들 뿐입니다. 두 번째 명령("power on")은 포트를 원래 상태로 되돌립니다. 그 결과 장치가 감지되고 초기화 절차가 시작됩니다. 그 과정에서 장치는 리셋되고 열거되며, 드라이버가 장치를 초기화합니다.
따라서 보통은 물리적으로 분리하는 것이 이러한 명령보다 더 효과적입니다. 특히 장치가 USB 플러그에서 전원을 공급받는 경우라면 더욱 그렇습니다. 하지만 허브가 실제로 VBUS를 차단한다면 hubpower 역시 그만큼 효과적입니다.
이 예에서는 명령이 메인보드의 USB 컨트롤러(루트 허브)로 전송되었습니다. USB 장치가 컴퓨터에 직접 연결되어 있었기 때문입니다. 외장 허브를 통해 연결되어 있다면 그 외장 허브로 명령을 보내야 합니다. 두 경우 모두 hubpower 사용법은 동일합니다.
아쉽게도 hubpower 도구는 USB 3.0(SuperSpeed)을 지원하지 않습니다. 자세한 내용은 아래에서 다룹니다.
두 방법의 차이점
그렇다면 두 번째 방법(hubpower)은 첫 번째 방법(usbreset)과 어떻게 다를까요? 장치 문제 해결이라는 목적에서는 두 방법 모두 본질적으로 같습니다. 리셋 명령을 보낸다는 점입니다. 동작 방식은 매우 다르지만, 문제가 해결된다면 그건 어쨌든 이 리셋 명령 덕분일 것입니다.
하지만 몇 가지 중요한 차이점이 있습니다.
- hubpower 명령은 장치가 버스에서 인식되지 않을 때도 동작합니다. 예를 들어 컴퓨터가 장치 열거를 여러 번 시도하다가 포기한 경우입니다. 이때 장치는 물리적으로 연결되어 있을 수 있지만, 버스 주소가 없으므로 usbreset 명령은 쓸모가 없습니다.
- USB 포트가 비활성화된 경우에도 hubpower 명령만이 도움이 될 수 있습니다. 컴퓨터가 특정 USB 포트에서 여러 번 오류를 겪고 나면 그 포트를 완전히 무시하는 경우가 있습니다. hubpower 명령으로 해결할 수 있습니다.
- hubpower 도구는 SuperSpeed(USB 3.x)에서는 동작하지 않습니다. 아마 지원을 추가하면 되는 문제일 것입니다. SuperSpeed 관련 자세한 내용은 아래에서 설명합니다.
- hubpower 도구는 USB 장치의 전원 공급도 제어할 수 있습니다(하지만 보통 실제로 제어되지는 않습니다). 문제 해결을 위해 전원을 완전히 차단했다가 다시 공급해야 하는 장치라면 이는 장점이 됩니다.
- hubpower 명령은 다루기가 더 번거롭습니다. USB 장치가 허브의 어느 포트에 연결되어 있는지, 그리고 허브의 버스 주소도 알아내야 하기 때문입니다.
전기 장비 제어(?)
이 페이지의 주제는 USB 장치 문제 해결이지만, 흥미로운 부수 효과도 하나 있습니다. USB 포트의 5V 전원 공급을 제어할 수 있는 경우가 있다는 것입니다. 다시 말해 간단하고 값싼 USB 허브 하나로 2.5W 전원 공급을 켜고 끌 수 있습니다.
이 정도면 전자기 릴레이(relay)를 제어하기에 충분합니다. 따라서 110V/220V 전원으로 동작하는 전기 제품을 제어하려면 부품 하나만 추가하면 됩니다. 물론 USB 허브가 손상되지 않도록 간단한 다이오드도 하나 추가하는 것이 좋습니다. 그리고 그것으로 끝입니다.
안타깝게도 전원 공급 제어 기능은 선택 사항입니다. USB 2.0 규격 11.11절에 따르면, 허브는 포트가 Powered-Off 상태일 때 해당 포트의 5V 전원 공급을 차단하는 전원 스위치를 가질 수 있습니다. 또는 하나의 전원 스위치로 여러 포트의 전원을 묶어서 제어하는 방식("ganged power switching")을 쓸 수도 있습니다. 전원 공급을 제어하는 주된 목적은 과전류를 소모하는 USB 장치를 차단해 나머지 포트가 계속 정상 동작할 수 있게 하는 것입니다.
하지만 앞서 말했듯이 물리적인 전원 제어는 선택 사항입니다. hubpower 명령이 실제로 하는 일은 포트의 PORT_POWER 속성(USB 규격에서는 "feature"라고 부릅니다) 값을 바꾸는 것입니다. 이 변경은 허브 포트의 서로 다른 두 측면과 관련됩니다.
- 필수 사항: 포트의 논리적 상태입니다. PORT_POWER 값이 0이면 포트는 Powered-Off 상태(또는 Not Configured 상태)만 될 수 있습니다. 다시 말해 허브는 포트에 전압이 없는 것처럼 동작하며, 장치가 연결되어 있어도 무시해야 합니다.
- 선택 사항: 포트의 전원 공급 배선에 VBUS(5V 전압)가 실제로 존재하는지 여부입니다. 허브가 이 기능을 지원하지 않으면 PORT_POWER 값이 0이어도 전압이 공급될 수 있습니다.
PORT_POWER 속성에 대한 자세한 설명은 아래에 있습니다.
내 허브가 전압을 실제로 차단할까?
내 허브가 실제로 전압을 차단하는지 어떻게 알 수 있을까요? 확실한 방법은 직접 테스트해 보는 것입니다. USB 장치는 아니지만 USB 포트에서 전력을 소모하는 무엇이든 연결해 보세요. 실제 USB 장치를 사용하면 혼란스러울 수 있습니다. 예를 들어 광 마우스는 5V 전압이 계속 공급되더라도 power-off 명령에 반응해 LED를 끄는 경우가 보통입니다.
각 허브는 "lsusb -v"로 확인할 수 있는 정보(USB 2.0 규격 11.23.2.1절에 정의된 Hub Descriptor)를 통해 전압 제어 지원 여부와 제어 단위를 선언합니다. 허브는 전압을 제어하지 않는다고 선언하거나, 포트 그룹("gang") 단위로 제어한다고 선언하거나, 포트마다 개별적으로 제어한다고 선언할 수 있습니다. USB 2.0 규격 11.11절에 따르면, "전원 스위치가 있는 허브는 모든 포트를 하나의 그룹(gang)으로 묶어 전원을 차단하거나, 각 포트를 개별적으로 제어하거나, 하나 이상의 포트로 구성된 임의 개수의 갱을 가질 수 있습니다."
하지만 이 정보는 신뢰할 수 없습니다. 전압을 제어한다고 선언한 허브를 여러 번 본 적이 있는데, 실제로 제어하는 허브는 하나도 없었습니다.
예를 들어, "lsusb -v" 출력의 예는 다음과 같습니다.
Bus 001 Device 073: ID 0bda:5411 Realtek Semiconductor Corp. Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.10 bDeviceClass 9 Hub bDeviceSubClass 0 Unused bDeviceProtocol 2 TT per port bMaxPacketSize0 64 idVendor 0x0bda Realtek Semiconductor Corp. idProduct 0x5411 bcdDevice 1.23 iManufacturer 1 Generic iProduct 2 4-Port USB 2.0 Hub iSerial 0 bNumConfigurations 1 Configuration Descriptor: [ ... ] Hub Descriptor: bLength 9 bDescriptorType 41 nNbrPorts 4 wHubCharacteristic 0x00a9 Per-port power switching Per-port overcurrent protection TT think time 16 FS bits Port indicators bPwrOn2PwrGood 0 * 2 milli seconds bHubContrCurrent 100 milli Ampere DeviceRemovable 0x00 PortPwrCtrlMask 0xff Hub Port Status: Port 1: 0000.0503 highspeed power enable connect Port 2: 0000.0503 highspeed power enable connect Port 3: 0000.0100 power Port 4: 0000.0100 power
그럴듯해 보이지요? 실제로 이 허브는 전압을 전혀 제어하지 못합니다.
반면, 다음은 평범한 메인보드 내장 허브의 출력입니다.
Bus 005 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 9 Hub bDeviceSubClass 0 Unused bDeviceProtocol 1 Single TT bMaxPacketSize0 64 idVendor 0x1d6b Linux Foundation idProduct 0x0002 2.0 root hub bcdDevice 4.15 iManufacturer 3 Linux 4.15.0-20-generic xhci-hcd iProduct 2 xHCI Host Controller iSerial 1 0000:06:00.0 bNumConfigurations 1 Configuration Descriptor: [ ... ] Hub Descriptor: bLength 9 bDescriptorType 41 nNbrPorts 2 wHubCharacteristic 0x000a No power switching (usb 1.0) Per-port overcurrent protection TT think time 8 FS bits bPwrOn2PwrGood 10 * 2 milli seconds bHubContrCurrent 0 milli Ampere DeviceRemovable 0x00 PortPwrCtrlMask 0xff Hub Port Status: Port 1: 0000.0100 power Port 2: 0000.0100 power Device Status: 0x0001 Self Powered
이 허브는 전압 제어를 지원하지 않는다고 스스로 밝히고 있습니다.
전원 스위칭 정보 외에도 "Hub Port Status"로 각 포트의 상태 정보가 표시됩니다. 이 상태 비트들은 USB 2.0 규격 11.24.2.7.1절의 표 11-21에 정의되어 있습니다. hubpower 명령이 가져오는 정보도 바로 이것입니다.
uhubctl 도구
USB 허브로 릴레이를 제어한다는 아이디어는 많은 프로젝트의 동기가 되었습니다. 이와 관련해서는 uhubctl 프로젝트를 살펴볼 만합니다. 이 프로젝트가 전압을 제어하는 USB 허브 목록을 관리하기 때문입니다.
이 도구는 분명히 전압 제어만을 위한 것이며, USB 장치 문제 해결용이 아닙니다. 예를 들어 uhubctl 도구는 기본적으로 각 포트의 전압을 개별적으로 제어할 수 있다고 선언하지 않은 허브를 무시합니다.
다음 명령은 위의 hubpower 명령과 동등합니다.
$ sudo ./uhubctl -f -l 1 -p 6 -a 0 Current status for hub 1 [1d6b:0002 Linux 5.16.0 xhci-hcd xHCI Host Controller 0000:00:14.0, USB 2.00, 12 ports, nops] Port 6: 0103 power enable connect [045e:07b2 Microsoft Microsoft? Nano Transceiver v1.0] Sent power off request New status for hub 1 [1d6b:0002 Linux 5.16.0 xhci-hcd xHCI Host Controller 0000:00:14.0, USB 2.00, 12 ports, nops] Port 6: 0000 off $ sudo ./uhubctl -f -l 1 -p 6 -a 1 Current status for hub 1 [1d6b:0002 Linux 5.16.0 xhci-hcd xHCI Host Controller 0000:00:14.0, USB 2.00, 12 ports, nops] Port 6: 0000 off Sent power on request New status for hub 1 [1d6b:0002 Linux 5.16.0 xhci-hcd xHCI Host Controller 0000:00:14.0, USB 2.00, 12 ports, nops] Port 6: 0100 power
명령 구문은 그다지 편리하지 않습니다. 아래에서 간단히 설명합니다.
uhubctl 도구는 libusb 기반이라 다른 운영 체제에서도 동작합니다. 반면 hubpower 도구는 /dev/bus/usb/(또는 /proc/bus/usb/)의 허브 디바이스 파일에 직접 접근하므로 리눅스에서만 동작합니다.
uhubctl 도구는 USB 3.x 규격도 지원합니다(git 커밋과 후속 커밋 참고). 하지만 실제 USB 장치가 허브에 연결되어 있을 때 어떤 일이 생기는지에는 신경을 쓴 사람이 없는 것 같습니다. 제가 SuperSpeed 장치를 리셋하려고 시도했을 때 이상한 결과가 나왔습니다. 전원이 복구된 뒤 장치가 Polling 상태에 머물러 열거되지 않았습니다. 또 전원이 꺼져 있는 동안에는 "lsusb -v"가 멈추기도 했습니다. 어쨌든 뭔가 잘못된 일이 벌어졌습니다.
다음은 SuperSpeed 포트의 전원을 껐다가 다시 켜는 명령입니다. SuperSpeed 관련 내용은 아래에 더 자세히 나옵니다.
# ./uhubctl -f -e -l 2 -p 4 -a 0 # ./uhubctl -f -e -l 2 -p 4 -a 1
- -f 옵션은 허브가 각 포트의 전압을 개별적으로 제어할 수 없다고 보고해도 해당 허브에 대해 동작하라는 뜻입니다.
- -e 옵션은 "exact position(정확한 위치)"을 뜻하며, SuperSpeed 포트를 허브 두 개로 계산하라는 의미입니다.
- -l 2 는 두 번째 허브를 의미합니다(첫 번째 허브는 제 컴퓨터의 USB 2.0 허브였습니다).
- -p 4 는 포트 번호 4를 의미합니다.
- -a 0 은 전원을 끄라는 뜻입니다(액션 #0).
- -a 1 은 전원을 켜라는 뜻입니다(액션 #1).
PORT_POWER 자세히 알아보기
호스트에서 PORT_POWER 값을 0으로 바꾸라는 명령이 도착하면 포트는 무조건 Powered-off 상태가 됩니다. 허브가 계속 장치에 5V 전원을 공급하더라도 마찬가지입니다. PORT_POWER 값이 0이 되는 다른 이유도 있습니다. 특히 포트에 과전류(overcurrent) 상태, 즉 장치가 너무 많은 전류를 소모하는 경우입니다.
PORT_POWER 값을 1로 바꿀 수 있는 유일한 방법은 호스트의 명령입니다. 이 명령은 포트를 Disconnected 상태로 만듭니다. 아무것도 연결되지 않은 USB 포트는 보통 이 상태에 있습니다. 포트에서 장치가 감지되면 잠시 후 상태가 Disabled로 바뀝니다. 장치가 이미 연결된 상태에서 PORT_POWER 값이 1로 바뀌면 이 전환은 즉시 일어납니다.
이 상태에서 장치를 활성화하는 유일한 경로는 포트를 리셋하는 것입니다(PORT_RESET 사용, 아래 참고). 이 작업은 오직 호스트만 할 수 있습니다. 따라서 허브가 할 수 있는 일은 주의가 필요한 장치가 연결되었음을 호스트에 알리는 것뿐입니다.
이때 포트 상태 비트 중 하나인 PORT_CONNECTION 상태 비트가 등장합니다. 이 비트는 포트가 Powered-off 상태이거나 Disconnected 상태일 때 0이어야 합니다. 포트가 Disconnected 상태에서 Disabled 상태로 전환되면 PORT_CONNECTION 값은 1로 바뀝니다. PORT_CONNECTION 값의 변화는 허브 이벤트를 발생시켜 드라이버에 통지됩니다(즉, 커널 hub.c 파일의 port_event() 함수가 USB_PORT_FEAT_C_CONNECTION 을 활성화한 상태로 호출됩니다). 드라이버는 이에 응답해 포트의 PORT_RESET 비트를 1로 바꿔 포트를 리셋하고 연결된 장치를 열거합니다.
이 모든 내용은 USB 2.0에 관한 것입니다. USB 2.0 규격의 그림 11-10 을 보면 허브 포트의 상태 변화가 나와 있습니다.
PORT_RESET 및 PORT_ENABLE 직접 제어
hubpower 도구는 두 가지 다른 속성(PORT_RESET, PORT_ENABLE)도 직접 변경할 수 있습니다. 실제로 저는 이 기능을 제가 포크(fork)한 버전에 추가했습니다. 다만 이 기능으로 할 수 있는 일은 USB 장치를 일부러 오류 상태로 만드는 것뿐임을 유의하세요(그러면 컴퓨터가 장치를 리셋해 문제를 해결하게 됩니다). 이제 자세히 설명하겠습니다.
USB 2.0 규격 11.5.1.5절에 따르면 포트 리셋을 시작하려면 호스트가 PORT_RESET 비트를 1로 설정해야 합니다. 허브는 리셋을 완료한 후 PORT_RESET 값을 다시 0으로 바꿉니다. 허브는 스스로 포트 리셋을 시작하지 않으며, 호스트도 이 속성에 0을 쓸 수 없습니다(11.24.2.7.1.5절).
허브는 포트가 Powered-off 또는 Disconnected 상태이면 PORT_RESET 요청을 무시합니다.
PORT_ENABLE 속성에 대해 설명하겠습니다. 이 속성이 0으로 바뀌면 포트는 Disabled 상태로 전환됩니다. 이는 호스트의 요청, USB 장치 연결 해제, 포트의 전원 꺼짐 상태, 리셋 과정 중 오류 등으로 발생할 수 있습니다.
PORT_ENABLE 값은 오직 호스트의 포트 리셋 요청 결과로만 1로 바뀔 수 있습니다(USB 2.0 규격 11.24.2.7.1.2절).
그러므로 호스트는 PORT_RESET 값을 0으로 바꾸거나 PORT_ENABLE 값을 1로 바꿀 수 없습니다. 그렇게 시도하면 허브가 오류 응답을 반환합니다(관련 ioctl() 명령이 오류 상태를 반환합니다).
hubpower 명령은 포트의 PORT_RESET 값을 1로 변경해 리셋을 직접 요청할 수 있습니다. 이렇게 하면 USB 장치가 리셋되고, 그 결과 장치는 자신의 버스 주소를 잊어버립니다. 따라서 장치에 더 이상 접근할 수 없게 됩니다.
이 명령은 허브로 직접 전송되므로 리눅스 커널의 허브 드라이버는 이런 일이 발생했는지 알지 못합니다. 사실 컴퓨터는 USB 장치에 접근하려고 시도하기 전까지는 어떤 변화도 알아차리지 못합니다. 그다음에 어떤 일이 벌어질지는 장치 드라이버에 달려 있습니다. 장치는 일종의 하드웨어 오류가 발생한 것처럼 취급되고, 그에 따라 어떤 오류 수정 조치가 취해질 것입니다. 대부분 그 조치에는 리셋이 포함됩니다.
따라서 hubpower 명령으로 리셋을 직접 발생시키면 원하는 결과를 얻을 수는 있겠지만 불필요한 소란이 따릅니다. PORT_POWER 값을 바꾸는 쪽이 더 깔끔합니다. PORT_RESET 비트를 직접 사용할 때의 유일한 장점은 드라이버가 "이 장치에 뭔가 정말 문제가 있으니 확실한 조치를 취하자"고 판단할 수 있다는 점입니다. 그런 판단이 도움이 될 수도 있습니다.
PORT_ENABLE 속성을 조작할 때도 같은 일이 벌어집니다. 장치가 갑자기 사라집니다. 이 경우에도 컴퓨터는 무슨 일이 일어났는지 즉시 알지 못합니다. USB 2.0 규격 11.24.2.7.2.2절에 따르면, PORT_ENABLE 값의 변화 알림(C_PORT_ENABLE)은 링크 오류 때문에 포트가 비활성화된 경우에만 발생합니다. 규격은 다른 이유로는 이 알림이 발생하지 않는다고 명시합니다.
따라서 PORT_ENABLE 값을 0으로 바꾸는 것은 PORT_RESET 비트를 직접 바꾸는 것과 거의 같은 효과를 냅니다. 단점이 하나 있다면, USB 3.0 규격 10.14.2.6.1절에 따르면 PORT_ENABLE 는 "SuperSpeed 허브에서 지원되지 않습니다"라는 점입니다.
SuperSpeed(USB 3.x)
무엇보다 먼저: USB 3.x 장치에 문제가 생겨 이 글을 읽고 있다면, USB 3.x가 제공하는 데이터 전송 속도가 정말 필요한지 스스로에게 물어보세요. 그럴 필요가 없다면 USB 3.x를 지원하지 않는 USB 허브(또는 짧은 USB 2.0 케이블)를 통해 장치를 컴퓨터에 연결해 보세요. 이것만으로 문제가 해결될 수도 있습니다.
SuperSpeed USB(USB 3.x와 같은 의미)는 USB 2.0과 병렬로 공존합니다. 모든 SuperSpeed 장치는 실제로 두 개의 장치로 구성됩니다. SuperSpeed 전용 장치 하나와 USB 2.0 전용 장치 하나입니다. 이 두 USB 버전은 각각 USB 케이블의 서로 다른 전선을 사용하며, 전기적으로도 개념적으로도 서로 독립적입니다.
USB 규격은 모든 SuperSpeed 장치를 이렇게 두 개의 장치로 구성된 것으로 간주하지만, 실제로 그렇게 구현할 필요는 없습니다. 다시 말해 USB 2.0을 지원하지 않는 SuperSpeed 장치도 SuperSpeed 포트에 연결하면 정상적으로 동작할 수 있습니다.
SuperSpeed 장치를 SuperSpeed 포트에 연결하면 먼저 SuperSpeed 인터페이스로 연결을 시도합니다. 실패하면 USB 2.0으로 연결을 시도합니다. 실제로는(규격에 따라서도) USB 장치가 두 버전으로 동시에 연결되는 일은 없습니다. 하지만 가능은 하며, 그렇게 되면 USB 장치는 마치 두 개의 별도 장치처럼 동작하게 됩니다.
SuperSpeed 허브는 USB 2.0용 허브와 SuperSpeed용 허브 두 개가 병렬로 결합된 것입니다. 외장 SuperSpeed USB 허브를 컴퓨터에 연결하면 시스템에 허브 두 개가 추가됩니다. 두 개의 별도 장치로 나타납니다. 일반 USB 장치는 두 버전을 병렬로 사용할 수 없지만, 허브는 그래야 합니다.
SuperSpeed 허브를 USB 2.0 포트에 연결하면 일반 USB 2.0 허브처럼 동작합니다.
일반적으로 이 두 허브는 서로 독립적으로 동작합니다. 각 허브는 자신의 포트를 가지며, 각 포트도 독립적으로 동작합니다. 특히 병렬 허브 중 하나에서 포트 파라미터를 변경해도 다른 허브의 포트에는 영향을 주지 않습니다.
여기서 한 가지 더 알 수 있는 사실은 "lsusb -t" 출력에서 장치가 SuperSpeed 루트 허브를 통해 컴퓨터에 연결된 것으로 표시되면 그 장치는 SuperSpeed 장치로 동작한다는 것입니다. 즉 데이터 전송 속도가 5Gbit/s 이상입니다. 마찬가지로 USB 2.0 루트 허브를 통해 연결되어 있다면 데이터 전송 속도는 480Mbit/s 이하입니다.
SuperSpeed 및 PORT_POWER
앞서 hubpower 명령이 실제로 하는 일은 포트의 PORT_POWER 속성 값을 바꾸는 것이라고 했습니다.
그런데 SuperSpeed 허브는 병렬로 연결된 두 개의 허브로 구성됩니다. 이 두 허브는 각 포트에 대해 저마다 독립적인 PORT_POWER 속성을 가집니다. 그렇다면 허브는 언제 VBUS 전원을 차단해야 할까요? 각 물리적 전원 스위치는 병렬 허브 각각에서 오는 두 개의 PORT_POWER 속성 값에 의존합니다.
USB 3.0 규격의 표 10-2에는 허브가 전원 공급을 활성화할지 여부에 대한 진리표(truth table)가 나와 있습니다. 요약하면 다음과 같습니다. 허브가 USB 2.0 허브로만 동작하는 경우(예: SuperSpeed를 지원하지 않는 컴퓨터에 연결된 경우)에는 USB 2.0 쪽 PORT_POWER 값을 따릅니다. SuperSpeed 허브로 연결된 경우(또는 병렬 허브 둘 다 연결된 경우)에는 두 PORT_POWER 값이 모두 0일 때만 VBUS를 차단합니다.
복잡하게 들렸다면, 그건 사실 쉬운 부분이었습니다. SuperSpeed 허브 부분은 내부 상태 머신(state machine)이 다릅니다. 링크 트레이닝(link training) 방식이 다르기 때문에 당연한 일입니다. 이 상태 머신은 PORT_POWER 값이 0일 때 들어갈 수 있는 상태가 USB 2.0처럼 하나가 아니라 세 가지입니다.
- DSPORT.Powered-off: 포트가 완전히 비활성 상태임을 의미합니다.
- DSPORT.Powered-off-detect: 포트가 SuperSpeed 링크 상대(link partner)를 감지하려 시도하는 상태입니다.
- DSPORT.Powered-off-reset: 포트가 링크 상대에게 웜 리셋(warm reset)을 수행하는 상태입니다(자세한 내용은 아래에서 설명).
마지막 두 상태의 목적은 자체 전원을 가진 SuperSpeed 장치가 포트에 연결되었을 때 연결이 USB 2.0으로 폴백(fallback)하지 않도록 하는 것입니다. 장치가 자신이 SuperSpeed 포트에 연결되었는지 알 방법이 없다면 USB 2.0으로 폴백될 수 있기 때문입니다.
그럼 여기서 무엇을 배울 수 있을까요? 주로 SuperSpeed 허브에서 PORT_POWER 값을 바꿔 장치를 리셋하는 것은 USB 2.0 허브에서처럼 간단하지 않다는 점입니다.
SuperSpeed: 웜 리셋과 핫 리셋
USB 2.0 에는 장치를 리셋하는 간단한 방법이 하나 있습니다. 허브가 두 신호선을 10 ms 동안 접지(ground)에 연결하는 방식(SE0)입니다. 반면 SuperSpeed 장치는 리셋 방법이 두 가지입니다. 웜 리셋(warm reset)과 핫 리셋(hot reset)입니다. 이것들을 리셋의 원인을 규정하는 용어인 PowerOn Reset 또는 Inband Reset과 혼동해서는 안 됩니다. Inband Reset은 호스트의 요청 때문에 리셋이 발생한다는 뜻입니다. 요청 유형에 따라 웜 리셋이나 핫 리셋이 될 수 있습니다(자세한 내용은 바로 아래에서 설명).
웜 리셋과 핫 리셋을 구분하는 것이 중요합니다. 특히 웜 리셋은 데이터 스트림을 중단하고 처음부터 다시 올리는 과정을 포함합니다. 핫 리셋은 데이터 스트림 자체에 실려 전송되며 데이터 스트림을 계속 유지합니다. 따라서 핫 리셋이 훨씬 빠르지만, 데이터 스트림을 재시작하면 해결되는 문제라면 웜 리셋이 필요합니다. 그러한 문제의 예로 물리 계층의 비트 오류(bit error)가 있습니다. 비트스트림(bitstream)을 내렸다가 다시 올리면 문제가 해결될 수 있습니다. 아마 이퀄라이저(equalizer)의 차선의 튜닝을 바로잡을 수 있기 때문일 것입니다.
앞서 usbreset 명령이 USBDEVFS_RESET ioctl() 호출을 수행한다고 했습니다. 그 과정에서 일어나는 여러 일 중에는 장치의 USB 버전과 관계없이 PORT_RESET 값을 0으로 바꾸는 일도 포함됩니다(리눅스 커널 v5.16 기준).
USB 3.0 규격 7.4.2절에 따르면 PORT_RESET 요청은 핫 리셋을 발생시킵니다. 즉 데이터 스트림이 활성 상태라면 그것을 끊지 않고, 데이터 스트림 상에서 리셋 명령을 보냅니다. 이 규격에는 BH_PORT_RESET(기능 번호 28)도 정의되어 있습니다. 이 기능은(포트가 비활성화되지 않은 한) 웜 리셋을 강제합니다. 이 리셋은 더 근본적입니다. 데이터 스트림을 내리고, 다시 올리는 절차(LFPS 시그널링을 통해)를 처음부터 시작합니다. 중요한 차이는 데이터 스트림의 재시작이 필요할 때 BH_PORT_RESET 기능은 해내지만 PORT_RESET 기능은 해내지 못한다는 점입니다.
새 SuperSpeed 장치의 연결에는 웜 리셋이 수반됩니다. 그런데 SuperSpeed 포트의 PORT_POWER 값을 바꿔 그런 효과를 낼 수 있는 도구가 없는 것이 아쉽습니다. 앞서 언급했듯이 uhubctl 도구는 기술적으로 가능하지만, 장치가 지저분한 상태에 빠집니다.
잡다한 메모
유용할 수는 있지만 딱히 넣을 곳이 없었던 정보를 모아 두었습니다.
- PORT_POWER 같은 명령의 코드는 USB 2.0 규격의 표 11-17 및 USB 3.0 규격의 표 10-8에 허브 클래스 기능 선택자(Hub class feature selector)로 나와 있습니다.
- 커널에서 PORT_RESET 요청을 활성화하는 함수는 hub_port_reset() 입니다. 하지만 포트를 실제로 다시 초기화하려면 usb_reset_device() 함수가 있습니다. 이 함수는 리셋을 소유 드라이버에 준비시키고 나서 usb_reset_and_verify_device() 함수를 호출합니다. 이 함수들은 모두 drivers/usb/core/hub.c 파일에 정의되어 있고, usb_reset_device() 함수만 외부로 내보내집니다(export).
- drivers/usb/core/devio.c 파일에서 usb_reset_device() 함수는 proc_resetdevice() 함수에 의해 호출됩니다. proc_resetdevice() 함수는 해당 디바이스 파일에 대한 USBDEVFS_RESET ioctl() 호출로 트리거됩니다. 이것이 usbreset 명령이 하는 일입니다(위 참고).
- 이 동작은 linux_usbfs.c 파일에 있는 libusb의 op_reset_device() 함수와 동등합니다. 거기서는 대신 IOCTL_USBFS_RESET 호출을 사용하지만, 이것은 USBDEVFS_RESET 값과 같습니다. 둘 다 값이 20입니다(커널의 include/uapi/linux/usbdevice_fs.h 파일과 비교하세요). libusb의 함수는 인터페이스의 클레임(claim)도 해제합니다. proc_resetdevice() 함수는 장치의 인터페이스 중 하나라도 클레임되어 있으면 정중하게 거부합니다.
- hub.c 파일에서 port_event() 함수는 hub_event() 함수에 의해 호출됩니다. 전자가 포트 비트의 변화를 감지합니다. hub_event() 함수는 작업 항목(work item)이며, kick_hub_wq() 함수에 의해 시작됩니다. 특히 hub_irq() 함수가 그런 역할을 하는데, 이 함수는 이 목적으로 지정된 허브의 IN 엔드포인트로 도착하는 상태 변경 알림에 응답하여 "포트 상태 변화와 각종 오류 발생 시" 실행됩니다.