본문 바로가기

MCU/ESP32

ESP32 키보드: usb 키 전송이 매우 느린 문제 # polling rate # 6-key Rollover # N-Key Rollover (무한 입력)

728x90
반응형

6-key 롤 오버를 하더라도, 폴링 주기 동안 눌린 키를 보낸다고 하면, 지금처럼 usb를 통한 키 전송 방식에서 엄청 느리진 않을거다.

지금 내가 구현한 방식은 키 입력이 발생하면 바로 report를 보내는 방식이다.

 

이렇게 하는 것이 아니라, 폴링 주기 동안 눌려진 키를 어레이에 담아서, 그 어레이를 폴링 주기가 도래했을 때 모아서 report를 해야 한다. 그래야 씹히는 키가 발생하지 않게 된다.

 

이렇게 하려면, 폴링주기를 잡고, 그 폴링주기 동안 어레이에 키 입력을 순차적으로 저장하고 폴링 주기가 왔을 때 report를 해야한다.

 

우선 위 방식대로 코드를 수정하도록 하자.

 

 

----

무한 입력은 정말 짧은 시간내에 많은 키가 입력돼도 모두 report가 되도록 해야하는데, 이는 descriptor를 커스텀해서 6개의 키입력을 담아서 보내는게 아니라 30개, 50개 등 내가 원하는 키 입력갯수를 커스텀해서 보내는게 가능하다고 한다.

이를 위해서는 descriptor를 커스텀하는게 필수라고 한다.

 

우선 무한 입력을 하기 전에, 6kro를 폴링주기에 맞춰서 제대로 응용해보도록 하자.

폴링주기는 임의로 잡는 것 말고, row, column의 스캔이 모두 끝났을 때 보내는 걸로 해보자.

 

아래는 ChatGPT 대화내용

 

ESP32-S3 를 가지고 키보드를 만들거야. 일반적인 키보드 들에서, 무한 입력이라고 들어봤어? 10개나 20개나 몇개를 누르든 고스트키 현상 없이, 다 눌리도록 하는 거야. 이를 위해서 하드웨어적으로는 매트릭스 키보드 구성하면서 다이오드를 연결하는 것은 알아. 내가 궁금한 것은 일반적으로 소프트웨어는 어떻게 구성하는지야
 

ChatGPT의 말:

ChatGPT
메모리 업데이트됨

무한 입력(N-Key Rollover, NKRO)을 소프트웨어적으로 구현할 때는 몇 가지 중요한 개념이 있어요. 일반적인 매트릭스 키보드에서 소프트웨어 구성 방식은 대략 다음과 같습니다.

  1. 스캔 루프:
    • 키 매트릭스의 각 행을 순차적으로 활성화하고, 그 행에 연결된 열을 읽어 현재 눌린 키를 감지합니다. 이 과정을 매우 빠르게 반복하면서 모든 키 입력을 동시에 처리할 수 있습니다.
  2. 디바운싱:
    • 스위치의 물리적인 특성으로 인해 눌릴 때와 뗄 때 전기적 노이즈(바운스)가 발생합니다. 디바운싱 코드를 통해 스위치가 실제 눌렸는지 안정적으로 확인합니다. 디바운싱을 하지 않으면 키 입력이 여러 번 인식될 수 있어요.
  3. 다중 키 입력(NKRO) 처리:
    • 키를 누를 때마다 스캔 루프가 활성화된 행과 열을 조합하여 매트릭스에서 각 키의 상태를 기록합니다. 이를 통해 여러 개의 키 입력이 동시에 감지되면, 소프트웨어는 각 키를 구분하여 처리합니다.
    • 이와 함께, 무한 입력 시 발생할 수 있는 "고스트 키(Ghost Key)"를 방지하기 위해 다이오드를 사용하는 것이 중요합니다. 고스트 키가 없다면 소프트웨어 측에서는 별다른 고스트 키 방지 코드가 필요 없을 수 있습니다.
  4. 키 매핑 및 보고:
    • 매트릭스의 키 위치를 실제 키보드 레이아웃에 맞게 매핑합니다. ESP32와 같은 마이크로컨트롤러에서는 매핑된 키 상태를 HID(Human Interface Device) 프로토콜에 맞춰 PC에 전송할 수 있습니다.
    • 다수의 키 입력을 처리하는 과정에서 모든 키 상태를 유지하고, 새로운 입력이 발생할 때마다 이를 업데이트해 전송하도록 합니다.
  5. 타이머 기반 폴링:
    • 스캔 루프와 디바운싱을 효율적으로 처리하려면 타이머를 기반으로 폴링 주기를 설정하는 것이 좋습니다. 폴링 주기가 지나치게 짧으면 CPU 부하가 커지고, 너무 길면 반응 속도가 느려질 수 있습니다.

ESP32에서는 이 모든 작업을 빠르게 수행할 수 있는 충분한 성능이 있어, 매트릭스 스캔 루프와 디바운싱을 타이머와 함께 효과적으로 사용할 수 있을 겁니다.

 
4o

나의 말:

타이머 기반 폴링에서, 내가 궁금한 것은 아래와 같아. 일반적으로 usb를 통해 키 입력을 보고할 때, 하나의 키 입력만 전송하지 않고, 6개의 키 입력을 담아서 보낼 수 있게 어레이에 담아서 보내잖아. 맞지?
 

ChatGPT의 말:

ChatGPT

맞아요! USB 키보드의 HID (Human Interface Device) 프로토콜에서는 일반적으로 최대 6개의 키 입력을 동시에 전송할 수 있도록 배열로 보내게 되어 있어요. 이 방식은 **부트 모드(boot mode)**에서 사용되는 표준 방식으로, 6개의 키와 함께 모디파이어 키(Shift, Ctrl 등) 상태를 전송할 수 있습니다.

이 방식은 **6-Key Rollover (6KRO)**라고 하며, USB HID 프로토콜의 기본 동작 방식입니다. 하지만 만약 무한 입력(NKRO)이 필요한 경우에는, 프로토콜을 다르게 처리하거나 별도의 소프트웨어 프로토콜을 사용해 HID 보고서의 형식을 확장해야 합니다.

간단히 정리하면:

  1. 6-Key Rollover 방식:
    • USB HID 표준에서는 6개의 키 코드만을 보고서에 담을 수 있습니다.
    • 각 보고서에는 모디파이어(Shift, Ctrl 등)와 최대 6개의 키 코드 배열이 포함됩니다.
  2. N-Key Rollover (NKRO) 처리:
    • 키보드가 무한 입력을 지원하도록 구성할 때는 USB HID 표준을 확장하거나 커스텀 HID 보고서를 구성하여, 더 많은 키 입력을 담을 수 있도록 구현하는 방식이 필요합니다.

ESP32에서도 이 방식을 사용해 최대 6개 키 입력까지는 일반 HID 규격을 따를 수 있으며, 무한 입력을 위해서는 추가적인 커스텀 처리가 필요할 수 있습니다.

 
4o

나의 말:

그러면 6-Key Rollover 방식을 사용하면서 타이머 기반 폴링을 이용하면, 폴링 주기가 오기 전에 여러 키가 눌리면 (예를 들어 a, b, c), 이 여러키가 6 개의 키 코드 어레이에 담겨서 보고되는거야?
 

ChatGPT의 말:

ChatGPT

네, 맞습니다! 타이머 기반 폴링 주기 내에서 여러 키가 눌리면, 이 모든 입력이 6개의 키 코드 배열에 담겨서 보고됩니다.

구체적으로 설명하자면:

  1. 폴링 주기 동안 여러 키가 눌렸다면, 이때 감지된 키들은 배열에 순차적으로 기록됩니다. 예를 들어, a, b, c를 누른 경우, 키보드는 각 키의 키 코드를 보고서의 배열에 담습니다.
  2. 보고서 생성:
    • 폴링 주기마다 현재 키 상태를 확인하고, 배열에 담긴 키 코드들을 USB HID 보고서에 포함해 전송합니다.
    • 배열은 6개의 슬롯을 가지고 있으므로, 6개 이하의 키는 모두 담기고, 그보다 많은 입력이 있으면 일부 키는 전송되지 않게 됩니다 (6키까지만 전송).
  3. 타이머 기반 폴링의 장점:
    • 폴링 주기 내에서 발생한 모든 입력을 수집하여 한 번에 보고할 수 있어, 한 키마다 별도로 보고하는 것보다 효율적입니다.
    • 입력 속도에 따라 주기를 적절히 설정해 빠르고 일관된 응답성을 유지할 수 있습니다.

그래서 6-Key Rollover 방식과 타이머 기반 폴링이 결합되면, 매 주기마다 최대 6개의 키 입력을 안정적으로 수집하고, 이 상태를 보고할 수 있게 됩니다.

 
4o

나의 말:

근데 보통 6 key rollover로 보내면, modifier는 어디에 담아서 보내? 몇번째 순서에?
 

ChatGPT의 말:

ChatGPT

USB HID 키보드 보고서에서 모디파이어 키는 일반 키 코드 배열과는 별도의 필드에 담겨서 전송됩니다. 일반적인 USB HID 키보드 보고서의 구조는 다음과 같습니다:

  1. 모디파이어 바이트 (1바이트):
    • 보고서의 첫 번째 바이트에 모디파이어 키 상태가 담깁니다.
    • 각 비트가 하나의 모디파이어 키에 해당하며, 이 비트가 1로 설정되면 해당 모디파이어 키가 눌린 상태임을 의미합니다.
    • 예를 들어, Shift, Ctrl, Alt, GUI(윈도우 키) 같은 키가 모디파이어 바이트의 각 비트에 할당됩니다.
  2. 예약된 바이트 (1바이트):
    • 두 번째 바이트는 보통 예약되어 있으며, 대부분의 경우 사용되지 않습니다.
  3. 키 코드 배열 (6바이트):
    • 뒤의 6바이트가 일반 키 코드를 담는 배열입니다. 여기에는 눌린 일반 키 코드들이 최대 6개까지 들어갑니다.

간단히 정리하면:

  • 보고서 구조: [모디파이어(1바이트), 예약(1바이트), 키 코드 배열(6바이트)]
  • 모디파이어 위치: 첫 번째 바이트
  • 일반 키 위치: 세 번째 바이트부터 시작하는 6개의 바이트 배열

따라서 모디파이어 키는 일반 키 코드 배열과는 별도로 첫 번째 바이트에 비트로 관리되므로, 6-Key Rollover 방식을 유지하면서도 모디파이어 상태를 쉽게 전송할 수 있습니다.

 
4o

나의 말:

그러면, 무한키 입력 방식은 어떤 방식으로 만들 수 있어?
 

ChatGPT의 말:

ChatGPT

무한키 입력(N-Key Rollover, NKRO)을 USB HID로 구현하려면, 표준 6키 제한을 넘는 추가적인 키 입력을 처리할 수 있도록 커스텀 HID 보고서를 만들어야 합니다. 여러 가지 방법이 있는데, 주요 방식은 다음과 같습니다:

1. 커스텀 HID 보고서 정의

  • USB HID 표준을 확장하여, 일반적인 8바이트(1바이트 모디파이어 + 1바이트 예약 + 6바이트 키 배열) 대신 더 많은 바이트로 구성된 커스텀 보고서를 정의할 수 있습니다.
  • 예를 들어, 16바이트로 정의된 보고서를 사용할 경우, 첫 1바이트를 모디파이어로, 나머지 15바이트를 키 코드로 사용할 수 있습니다.
  • 이 방식은 HID 디스크립터에서 정의해줘야 하며, 이를 지원하는 소프트웨어가 필요할 수 있습니다. 일부 운영 체제나 소프트웨어에서는 기본적인 HID 디스크립터만 인식하기 때문에, 이를 지원하는 펌웨어와 드라이버가 필요할 수 있습니다.

2. 멀티 리포트 방식

  • NKRO를 위해 HID 디스크립터에서 여러 개의 리포트(Report ID)를 생성하여, 각 리포트마다 다른 키 조합을 보내도록 할 수도 있습니다.
  • 예를 들어, 첫 번째 리포트는 6개의 키만 보내고, 추가 리포트는 다음 6개의 키를 보내는 식으로 구성할 수 있습니다.
  • 이러한 멀티 리포트 방식은 하나의 HID 디바이스에서 여러 개의 보고서를 동시에 전송하는 방식이므로, 일부 시스템에서 인식이 어려울 수 있습니다.

3. 신호 방식을 활용한 특수 보고서

  • USB 인터페이스 외에 BLE(Bluetooth Low Energy)와 같은 무선 프로토콜을 활용하는 경우, 여러 개의 키 입력을 한번에 전송할 수 있는 방식으로 커스텀 프로토콜을 구축할 수 있습니다.
  • ESP32는 BLE를 지원하므로, 무선으로 NKRO를 구현할 때 BLE 프로토콜에 맞춰 확장된 HID 보고서를 정의하여 무한 키 입력을 처리하는 것도 가능합니다.

4. 타임슬롯을 활용한 순차적 보고

  • 커스텀 펌웨어를 작성하여 빠르게 반복 폴링하면서 모든 키 입력을 차례대로 전송하는 방법도 있습니다.
  • 예를 들어, 무한 입력 상태가 발생할 경우 빠르게 여러 HID 보고서를 연속으로 전송해 모든 키 입력이 다 전달되도록 합니다.
  • 이를 사용하면 어느 정도는 무한 입력에 가까운 성능을 낼 수 있지만, USB 폴링 주기나 전송 주기 제한으로 인해 완전한 무한 입력을 보장하지는 않습니다.

실용적인 선택

ESP32-S3로 구현할 때는 커스텀 HID 보고서 정의 방식이나 BLE를 통한 커스텀 NKRO 방식이 적합할 수 있습니다.

  • USB로 구현할 경우, 커스텀 HID 보고서를 설정해 추가적인 키 입력을 전송하는 방법이 일반적입니다.
  • 무선 BLE로 구현한다면 HID 보고서의 길이를 확장하여 NKRO를 위한 추가적인 키를 전송할 수 있습니다.

이 방식을 통해 무한 키 입력을 구현할 수 있으며, 운영체제가 지원하는 한 무한 키 입력에 가까운 성능을 확보할 수 있습니다.

 
4o

나의 말:

6 key rollover 방식이나 NKRO 방식 모두 폴링 주기 동안에 눌려진 키들을 어레이에 담아서 보내는 건 같아? 그 수만 다른거고?
 

ChatGPT의 말:

ChatGPT

네, 맞습니다! 6-Key Rollover 방식이나 NKRO 방식 모두 폴링 주기 동안에 감지된 키 입력들을 배열에 담아서 보내는 방식은 동일하며, 차이점은 한번에 담을 수 있는 키 수의 제한에 있습니다.

공통점: 폴링 주기 동안 감지된 키를 배열에 담아 전송

  • 폴링 주기 내에 눌린 키들은 배열에 차곡차곡 추가됩니다. 이 배열은 각 키 입력을 키 코드로 표현하여 순서대로 담습니다.
  • 폴링 주기가 끝날 때마다 현재 배열에 담긴 키 코드들을 USB HID 보고서로 전송합니다.

차이점: 배열에 담을 수 있는 키 개수의 차이

  • 6-Key Rollover 방식에서는 표준 USB HID 보고서가 제공하는 6개 키 슬롯 제한으로 인해, 최대 6개까지만 배열에 담아 전송할 수 있습니다. 6개 이상 키가 눌리면, 나머지 키는 해당 보고서에서 전송되지 않습니다.
  • NKRO 방식에서는 HID 보고서의 배열 크기를 확장하거나 커스텀 방식으로 여러 키를 보낼 수 있게 하여, 실제로 눌린 모든 키를 배열에 담아 전송할 수 있습니다. 이때 배열 크기는 설정에 따라 달라질 수 있으며, 원하는 만큼 확장할 수 있습니다.

따라서, 폴링 주기 동안 키를 배열에 담고 전송하는 구조는 동일하지만, 한 번에 담을 수 있는 키 수의 차이가 6-Key Rollover와 NKRO 방식의 주요 차이점입니다.

 
4o

나의 말:

폴링 주기는 보통 어떻게 잡아야돼? 모든 로우와 컬럼이 스캔이 완료되는 시점으로 해?
 

ChatGPT의 말:

ChatGPT

맞습니다! 일반적으로 폴링 주기는 모든 행(row)과 열(column)이 스캔을 완료하는 시점으로 설정하는 것이 기본입니다.

다만, 반응 속도와 정확도를 최적화하기 위해 몇 가지 기준을 고려하여 폴링 주기를 조정할 수 있습니다:

1. 폴링 주기 기준

  • 타이밍 기준: 키보드에서는 일반적으로 1ms에서 10ms 사이의 폴링 주기를 사용합니다. 게이밍 키보드나 반응성이 중요한 경우에는 1ms 폴링 주기를 선호하고, 일반적인 경우 5~10ms도 무리가 없습니다.
  • 스캔 루프 완료 시점: 모든 행과 열이 한 번씩 스캔되어 상태가 확인된 시점을 기준으로 폴링 주기를 설정합니다. 예를 들어, 매트릭스에 여러 행과 열이 있다면, 한 번의 루프가 끝나는 시점까지의 시간을 측정하여, 그 시간보다 약간 여유 있는 주기를 설정하는 것이 일반적입니다.

2. 최적 폴링 주기 설정을 위한 고려 사항

  • 반응 속도: 짧은 폴링 주기는 빠른 반응을 제공하지만, 너무 짧게 설정하면 CPU 부하가 늘어날 수 있습니다.
  • 디바운싱: 스위치 바운스를 안정적으로 처리할 수 있는 주기를 선택하는 것이 중요합니다. 디바운싱은 보통 5ms~10ms 내외로 충분하며, 폴링 주기를 너무 짧게 하면 디바운싱 과정에서 노이즈를 키 입력으로 오인할 수 있습니다.
  • CPU 성능: ESP32 같은 마이크로컨트롤러에서는 빠른 폴링 주기도 무리 없이 지원할 수 있으므로, 키보드 반응성을 위해 1ms~5ms 정도의 주기를 설정해도 좋습니다.

3. ESP32에서의 폴링 주기 추천

  • ESP32의 성능을 고려했을 때 5ms 이하의 폴링 주기에서도 충분히 스캔이 가능합니다. 게이밍처럼 반응 속도가 중요하다면 1ms, 일반적인 키보드 용도로는 5ms 정도를 추천합니다.
  • 폴링 주기를 설정할 때, 타이머를 사용해 주기를 일정하게 유지하고, 각 스캔 루프에서 모든 행과 열이 완료되었는지 확인하는 방식이 효과적입니다.

정리하자면, 모든 행과 열이 스캔되는 완료 시점을 기준으로 하되, 1ms~5ms 범위 내에서 실험하며 최적의 반응성과 안정성을 얻을 수 있는 주기를 찾는 것이 좋습니다.

 
4o

나의 말:

폴링 주기를 직접 설정하는 것 말고, 키 스캔이 모두 완료 됐을 때 한번 보고를 하고, 또 키스캔이 모두 완료됐을 때 한번 보고를 하는 식으로 하면 어떤식으로 코드를 구성해야돼?\
 

ChatGPT의 말:

ChatGPT

키 스캔이 완료될 때마다 자동으로 보고하는 방식으로 코드를 구성하려면, 스캔 루프가 끝날 때 보고(전송) 함수를 호출하도록 하여 스캔이 완료되는 타이밍에 맞춰 전송하게끔 설정할 수 있습니다. 이렇게 하면 폴링 주기를 타이머로 고정하지 않고, 스캔 완료 시점마다 보고하도록 동작을 최적화할 수 있습니다.

코드 구성 방식

이 방식의 핵심은 스캔 루프와 보고 루프의 분리입니다. 스캔 루프가 각 키의 상태를 매트릭스에서 확인하고 모든 행과 열을 확인했을 때, 그 상태를 보고서에 담아 한 번에 전송하는 구조로 구성합니다.

1. 키 스캔 함수 작성

  • 각 행을 활성화하고, 열을 읽어 키 상태를 확인합니다.
  • 이 과정이 끝나면 키 상태를 배열에 저장해둡니다.

2. 상태 변화 감지 및 보고 함수

  • 스캔 루프가 끝날 때마다 현재 키 상태 배열과 이전 키 상태 배열을 비교해 변경 사항이 있으면 이를 보고서로 만들어 전송합니다.
  • 키 상태가 변경되었을 때에만 보고를 하도록 하면, 불필요한 보고를 줄일 수 있습니다.

3. 루프 구성 예시

  • 아래는 스캔이 완료될 때마다 키 상태를 보고하는 코드의 예시입니다:
c
코드 복사
// 필요한 헤더 포함 #include <stdio.h> // 키 매트릭스 행과 열 크기 정의 #define NUM_ROWS 5 #define NUM_COLS 14 // 현재 키 상태와 이전 키 상태를 저장할 배열 정의 uint8_t keyState[NUM_ROWS][NUM_COLS]; uint8_t prevKeyState[NUM_ROWS][NUM_COLS]; // 키 스캔 함수 void scanKeys() { for (int row = 0; row < NUM_ROWS; row++) { activateRow(row); // 현재 행 활성화 for (int col = 0; col < NUM_COLS; col++) { keyState[row][col] = readColumn(col); // 열 값 읽기 } deactivateRow(row); // 현재 행 비활성화 } } // 보고 함수 void reportKeys() { // 키 상태가 변경되었는지 확인 bool stateChanged = false; for (int row = 0; row < NUM_ROWS; row++) { for (int col = 0; col < NUM_COLS; col++) { if (keyState[row][col] != prevKeyState[row][col]) { stateChanged = true; break; } } if (stateChanged) break; } // 상태가 변경된 경우에만 보고서 전송 if (stateChanged) { sendHIDReport(keyState); // 현재 키 상태를 HID 보고서로 전송 // 현재 상태를 이전 상태로 저장하여 다음 스캔에서 비교 for (int row = 0; row < NUM_ROWS; row++) { for (int col = 0; col < NUM_COLS; col++) { prevKeyState[row][col] = keyState[row][col]; } } } } // 메인 루프 void loop() { // 키 스캔 scanKeys(); // 보고 조건 확인 및 보고 수행 reportKeys(); }

설명

  1. scanKeys() 함수:
    • 키 매트릭스의 각 행을 순차적으로 활성화하고, 그에 연결된 열 상태를 확인해 keyState 배열에 기록합니다.
  2. reportKeys() 함수:
    • keyState와 prevKeyState를 비교하여 키 상태가 변경되었는지 확인합니다.
    • 변경이 있으면 sendHIDReport() 함수를 호출하여 키 상태를 USB HID 보고서로 전송합니다.
    • 전송 후 keyState 값을 prevKeyState에 복사하여 다음 루프에서 변경 사항을 추적할 수 있도록 합니다.
  3. 메인 루프(loop):
    • 스캔과 보고를 반복하여 스캔 완료 시마다 보고가 이루어지도록 합니다.

이렇게 하면 각 스캔 루프가 끝날 때마다 변경 사항을 즉시 보고할 수 있어 폴링 주기를 별도로 고정할 필요 없이 동작할 수 있습니다.

728x90
반응형