キーボードイベントのリアルタイム性を上げるには?


Kenny  2009-12-19 23:05:44  No: 36487

ストップウォッチソフトを作っています。ある程度できあがりテストをしていますが、
計測されたタイムが明らかに、粗いとびとびの数値になります。普通のストップウォッチを
使った計測では、光電管を使った公式計時と3/100秒以下程度の誤差ですが、10/100秒以上
誤差がある時もあります。
内部的にはTimeGetTime関数を使ってテストをして、精度は出ているようなのですが、
そもそも、キーボードイベントはRTCの精度の55ms以上の頻度では起こせない、という
問題(?)に行き着きました。計測の開始/ストップをキーボードで行っていますので。
この問題認識は正しいのでしょうか。また、何か多少でも回避できる策はあるでしょうか。
一応、Windowsヤメレとか、I/Oボード作れ、というのはなしと言うことで。


それは  2009-12-19 23:32:59  No: 36488

可能なのであれば、競技会等で専用HWを使用して計測する必要ないですよね。
特に1秒程度の精度ならともかく、1/100秒精度などは望めません。


monaa  2009-12-19 23:52:02  No: 36489

ちと気になったので、
>>キーボードイベントはRTCの精度の55ms以上の頻度では起こせない
これ初耳です、できれば情報源もしくはそう考えた理由があったらお願いします。
キーボードならどんなに粗悪品でも1/100秒は出せると思ってたので。
現時点では未確認なので私の主観です。


monaa  2009-12-19 23:58:02  No: 36490

あと、精度測定はどのように行ってますか?


monaa  2009-12-20 03:58:30  No: 36491

少し時間割いてみました。
キーを押しっぱなしにした時に送られてくるキーコードの時間間隔を測定しました。
私の所有しているキーボードはLogicool Classic Keyboard 200(1400円)
http://www.logitech.com/index.cfm/keyboards/keyboard/devices/188&cl=jp,ja?section=overview
CPUはCore2 Quad Q9550 2.83GHz
Delphi2009

結果、99回のKeyDownで
キー
間隔平均は0.033037秒、
標準偏差は0.000063秒でした。
最大誤差は0.0003秒(2%)
CPUクロック数から時間への変換はExcel使ってます。^^;
比較的綺麗な図が描けました。
ざっとしか検証してませんが、重い動作をさせなければ一発目のキーも恐らく0.0003秒程度の精度は出ていると思います。
ちなみにこれを測定した時も、Excel,Outlook,WinAmp,IE,notepad,Delphiが起動してましたし、デバッグモードです。

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs;

type
  TForm1 = class(TForm)
    procedure FormKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure FormCreate(Sender: TObject);
  private
    { Private 宣言 }
    Count : Integer;
    ClockData : array[0..100] of UInt64;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

function GetCPUClock():UInt64;
var
  Lo, Hi: DWORD;
begin
  asm
    PUSH edx
    PUSH eax
    rdtsc
    MOV  Lo, eax
    MOV  Hi, edx
    POP  eax
    POP  edx
  end;
  Result := Hi;
  Result := (Result shl 32) or Lo;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  Count := 0;
end;

procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word;  Shift: TShiftState);
begin
  if Count > 100 then begin beep; Exit; end;
  ClockData[Count] := GetCPUClock();
  inc(Count);
end;

procedure TForm1.FormKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
var
  i:Integer;
  strList : TStringList;
begin
  strList := TStringList.Create;
  for i := 1 to 100-1  do
  begin
    strList.Add(IntToStr(ClockData[i+1]-ClockData[i]));
  end;
  strList.SaveToFile('data.txt');
  ShowMessage(strList.Text);
  strList.Free;
  Count := 0;
end;

end.


  2009-12-20 06:39:30  No: 36492

http://members3.jcom.home.ne.jp/progstudio/win_tips16.html

使ったことはないのでどれほどの精度が出るか分かりませんが
QueryPerformanceCounter API
でしょうか。

多分Windowsを使う限りどれほど正確かは微妙です。

http://msdn.microsoft.com/ja-jp/library/cc410968.aspx
で判断してみては?


  2009-12-20 07:02:04  No: 36493

回答してみて見直したら

ちょ
>monaaさん
ソースにアセンブラ入ってるw
凄い人はやっぱり凄い


Kenny  2009-12-20 08:54:30  No: 36494

皆さん、アドバイスありがとうございます。
それは さん>
真剣にやるなら専用HWというのは理解しているのです。ディスクアクセスと重なると、入力を
受け付けないような感じになったりするときもありますし。

monaaさん>
キーボードイベントの最小間隔ですが、私もネット上をさまよって「どうやらそうらしい」という
結論に至っただけで、自信はありません。もっと細かくキーボードイベントが取れるという情報が
あれば歓迎なのですが。
測定精度の測定は、コードでsleep()させてその時間を比較したりしていますが、それよりも
ストップウォッチ計時でMax 3/100位しか狂わないのが、平気で0.1秒以上狂ってくれますから、
もう少し何とかしたいなぁと。ピット前で計測していて0.1秒の狂いだと、5mぐらい押し損ねた
感じになりますから、全然なんです。
キーボードリピートを使った最小イベント間隔は、こちらの計測でも0.03秒という結果ですが、
この間隔はキーボードリピートの間隔であって、キーボード割り込みを伴うキーボードイベントの
間隔とは違う、と素人は考えたのですが、どうなのでしょう。
キーボード自体が信号を送っても、それで割り込みがかかるのが55ms(?)ごとなのでWindowsが
キーコードを受け取るのがその後、という事のようです。伝統的な「割り込み」が重要らしく、
レジストリでIRQ8のプライオリティを上げたりしていますが、大ボケする回数が減ったぐらいです。

と さん>
QueryPerformanceCounterはプロトタイプの段階で、sleep()なんかを使ってTimeGetTimeと
比較しました。キーボードではなくて、コードでイベントを起こせばどちらも十分に精度が
あるようでしたが、QueryPerformanceCounterはHWによっては対応していないとか、動的に
クロックが変化するCPUでは使えないという情報があって、今回は没にしました。


Kenny  2009-12-20 09:11:53  No: 36495

連続ですみません。
monaaさんのコードを拝見すると私の考えは間違っていて、キーボードリピートがかかるときは
キーボードからずらずらキーコードが送られてくるんですよね。Windowsが内部処理をして
キーコードを量産しているのじゃ無くって。
ということは少なくとも33msで割り込みがかかっている訳ですよねぇ。
sleep()も9x系統で最小55ms、NT3.1(だったかな?)で16ms、それ以降10msということですから、
ハードやOSで最小間隔が変わってくるのでしょうか。私がキーボードリピートの間隔をテストした
ノートブックの場合、本体キーボードよりも外付けUSBキーボードの方が間隔が長くはなりました
けど。


monaa  2009-12-20 10:26:56  No: 36496

寝る前だってのに…
先ず、USBキーボードが押されてからアプリケーションが押されたと識別するまでの流れ
1.キーを押す
2.キーボード内のICが短絡を検知
3.キーコードをUSB経由で送信
4.Windowsがキーボード信号を検知
5.Windowsがフォーカスアプリケーションに向けてWM_KEYDOWNを送信
6.アプリがWM_KEYDOWNを受信
今回問題にしているのは「1−6迄大体何秒の誤差がでるか?」ですよね?
スタート、ストップが共にキー入力で行われるわけなので、
1−6迄の経過時間は開始と終了で相殺されますもんね。

ではこれをどうやって調べれば知ることが出来るでしょうか?
結果として相手は0.1msオーダー以下の挙動なので難問です。
その計測を安価な既知の信号を用いて検証しているのが私の上記ソースです。

先ず、既知の一定間隔のキーボードからの信号をWindowsに送ります。
この信号は厳密に一定でなくてはなりません、
ですが、一般家庭にそんなにしっかりした振動体を用意することなんて現実的ではないので、
今回はあえて、キーを押しっぱなしにしたときの信号を一定と仮定して使用してます。
送信信号が一定であれば受信も一定であるのが本来安定したシステムです。
ですが、Windowsと言うOSはマルチタスク、イベントの割り込みなんてのは日常茶飯事。
そのため送信信号が一定であっても受信が一定になること理論上あり得ません。

WM_KEYDOWNを受け取った時刻を100回記録して、それぞれの間のブレを測定すれば、
そのブレこそが精度の限界値というわけです。

なので私のPCでは
間隔平均は0.033037秒、  ってのは確認で
標準偏差は0.000063秒    これが大体信頼の置ける精度
最大誤差は0.0003秒(2%)  これが大外れを起こしたときのズレ(大体2%の確立で起こる。)
と言う意味。

もちろんソフトウェア制御なので、この精度は使っているハードウエア構成や、CPU使用率に大きく影響します。
また、WindowsAPIのシステムタイマは最小1msですから今回の計測には使えません。
Sleep関数も最小1msですから使ってはいけません。
WM_KEYDOWNはアプリケーションが受信を行わないとOSが溜め込む働きを持っています。
まーですが、今回は0.033秒以内に受信が完了しているのでそれはたいした問題ではありません。
あと、キー入力はOSの中でかなり優先度の高い信号なので、比較的優先されてOS内で処理されます。

でわ寝るぽ。


Kenny  2009-12-20 14:41:10  No: 36497

monaaさん、ありがとうございます。睡眠時間を奪った罰として、寝ないで考えています。

本来精度はあるけど、誤差が出る要素は色々ある、というのは今まで以上に良く理解できました。
そして、それに基づくと、疑問の持ち方を変えた方が良いと思いますので、簡単なサンプル
コードを作ってみました。イベントはこれだけなのですが、

TForm1
  private
    StartTime: DWORD;
    { Private 宣言 }

procedure TForm1.FormCreate(Sender: TObject);
begin
  timeBeginPeriod(1);
  StartTime:=timeGetTime;
  timeEndPeriod(1);
end;

procedure TForm1.FormKeyPress(Sender: TObject; var Key: Char);
var
  EndTime: DWORD;
begin
  timeBeginPeriod(1);
  EndTime:=timeGetTime;
  timeEndPeriod(1);
  Memo1.Lines.Add(inttostr(EndTime-StartTime));
  StartTime:=EndTime
end;

これを実行し、大体2秒間隔前後でいい加減にキーボードを押すと、以下の結果になりした。

2704,1983,1853,2023,1923,1792,1953,1672,2083,1973
1743,1672,2073,1783,1852,1933,2033,2093,2243,1893
1883,1862,1883,1672,1843,1632,1673,1833,1962,1693
1792,1733,1943,1772,1753,1932,2514,2484,2143,2163
2283,2574,2113,2383,2574,2123,2273,2173,2414,1873

50回分しかコピーしませんが、1/1000秒台が正確に3前後に揃ってしまうんです。100回やっても
200回やっても同じです。また、1/1000秒台の数字は、1秒台の数字*1.5の位になるようで、
1秒ちょい間隔で押すと末尾は2に、3秒ちょいで押すと末尾は5が多くなるかんじです。
こういった法則から、サンプリングレートの粗さを感じて、今回の質問となったのです。
もちろん手動計時ですから1/1000秒台の正確性は不要ですし、他の問題が多いことも理解して
いますが、この正確に誤差が出る原因が分かれば、何かやれることがあるのかな、と思ったのです。
TimeGetTimeの使い方とか、何か大きな勘違いをしているのでしょうか。


UUU  2009-12-20 15:43:21  No: 36498

キーボードはUSBですか?
USBは事象が発生してからすぐにホストへ送ることはありません。
基本はホストからのポーリングです。
ポーリング間隔以上の精度はでません。
ですから無理があるのでは?
PS/2インターフェースならまだましだと思いますが。
内部処理の遅れは色々なキーボードで一定しているわけではありません。
リピート間隔は「Typematic Rate/Delay」で変更できますが、精度を期待するのは無理だと思います。
(ここは素直に外部H/Wを検討すべきでは?)


Kenny  2009-12-20 18:37:37  No: 36499

UUUさん>
キーボードはノートブックPCですので、どういう接続か確認はできませんが、デバイスマネージャ
ではPS/2接続で表示されます。
精度を期待するなら専用H/Wというのは重々承知しているのですが、フリーで配布することを
考えていますので、「何とかならないかな?」程度の要求ではあるのです。
また、どちらかというと今はmonaaさんに教えて頂いたことと、自分の回りにある数台のPCで
計れる時間の食い違いの「なぜ」に興味が移っている、ということもあります。
プログラム上でこれ以上仕方がないということができているのか、恥ずかしい間違いをして
いるのか、ということも大きいですね。
専用H/Wを使わないという間違いは既に犯しているのですが。


さな  2009-12-20 19:26:47  No: 36500

フリーで配布することを考えたら、精度を突き詰めても無駄ではないかと思うのですが…
環境ごとに出せる精度に差があるでしょうし。

monnaさんの
>1−6迄の経過時間は開始と終了で相殺されますもんね。
というのは毎回OSとHWの状態が一致していればそうだとおもいますが、
そんなことはなくOSの状況は毎回違うはずです。
したがって、開始と終了で処理時間が一致するという保証ができないと思うのですが、
そのへんはどうなんでしょう?


Kenny  2009-12-20 20:07:55  No: 36501

さなさん>
>フリーで配布することを考えたら、精度を突き詰めても無駄
全くその通りで、プログラミング上の興味、という側面が強いです。

で、環境依存を確認するために、これまでノートPCでしか行っていなかったテストを、デスクトップ
PCでも行いに出かけました。
結論から言うと、デスクトップ機(古いCeleron機)では私のサンプルコードでも末尾が揃うことは
なく、monaaさんのコードはきれいに揃った数字を返しました。
しかし、ノートPCでは私のコードは末尾が揃い、monaaさんのコードは平均値付近が7割ぐらいで、
時々まとまって平均値の3倍ぐらいのクロック数が入り、更に時々半分以下のクロック数が入る、
という結果になりました。テストできたノート機はcentrino機ばかりなので、そのせいかも知れないですね。
OSはXPで統一ですから、XPのH/W依存部分と、H/Wそのものの違いでしょうか。
ただそれが特定できれば、Readmeにその旨書けるから、ある意味目的達成なんですけどね。


monaa  2009-12-21 00:09:01  No: 36502

USBキーボードのレポートレートは標準で125Hzつまり
0.008000秒(8ミリ秒)以上の精度は出ませんね…
正直もう少し高いと思ってました。最初に書いた経験則の1/100秒は意外と妥当だったんですね。
私が上で算出した値はあくまで理論値ですが、そのレポートレートまで同期させた時のゆらぎです。(押しっぱなし一定間隔の為)
そのブレが0.000063秒だったということですね。
ですので、完全に一定間隔でない場合、Kennyさんの最後の計測結果の1桁目はUSBキーボードの精度未満の話なので、切り捨てるのが妥当だと思います。

どーしても気になるというのであれば、ソフトウエア側の精度をめいいっぱい考慮したタイマーをこしらえてみたのでこれを使ってみてください。
USBレポートレートの精度を大幅に超えてるはずです。
USB 125Hzを使っている時点で全くの無価値ですが。USBはもっと周波数を上げることが可能です。

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs;

type
  TForm1 = class(TForm)
    procedure FormCreate(Sender: TObject);
    procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
  private
    { Private 宣言 }
    fStartClock : UInt64;
    fStopClock  : Uint64;
    fCPUClock   : UInt64;
    fCounting   : Boolean;
  public
    { Public 宣言 }
    procedure ShowResult();
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//CPUクロック数取得
function GetCPUClock():UInt64;
var
  Lo, Hi: DWORD;
begin
  asm
    PUSH edx
    PUSH eax
    // RDTSC Delphi5では使えないらしい
    // http://rakasaka.fc2web.com/delphi/cpuclock.html
    //DW   $310F //私は未確認なので保証しません。
    rdtsc
    MOV  Lo, eax
    MOV  Hi, edx
    POP  eax
    POP  edx
  end;
  Result := Hi;
  Result := (Result shl 32) or Lo;
end;

//1秒当たりのCPU周Clock数取得
//1秒の取得にはWindowsAPIを使用するしかない
function GetCPUClockFrequency(): UInt64;
var
  aStartClock    : UInt64;
  aAPIFreq       : Int64;
  aAPIClockStart : Int64;
  aAPIClockStop  : Int64;
  aAPIClockNow   : Int64;
begin
  QueryPerformanceFrequency(aAPIFreq);
  aStartClock := GetCPUClock();
  QueryPerformanceCounter(aAPIClockStart);
  aAPIClockStop := aAPIClockStart + aAPIFreq;
  repeat
    QueryPerformanceCounter(aAPIClockNow);
  until aAPIClockStop <= aAPIClockNow;
  Result := GetCPUClock()-aStartClock;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  //最初の2回は時間がかかるらしい
  //http://www.02.246.ne.jp/~torutk/cxx/clock/cpucounter.html
  GetCPUClock();
  GetCPUClock();
  fCPUClock := GetCPUClockFrequency();
  fCounting  := False;
end;

procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  if fCounting = False then
    fStartClock := GetCPUClock() else
  begin
    fStopClock  := GetCPUClock();
    ShowResult;
  end;
  fCounting := not fCounting;
end;

procedure TForm1.ShowResult();
var
  str:string;
  aTime:Extended;
begin
  aTime := (fStopClock - fStartClock)/fCPUClock;
  str   := FloatToStr(aTime) + ' sec';
  Canvas.TextOut(0,0,str);
  Beep;
end;

end.

さらにソフトウエア側の精度を上げるにはVCLとの決別が必要かと。


monaa  2009-12-21 00:39:49  No: 36503

確認しましたが、1msの値が大体同じ値になりますね。
8ms(125Hz)の周波数だと0,8,6,4,2で5通りになるのが正しいハズなのですが。
うーん、多分正しくないんですね。
もう少しいじってみます。


Kenny  2009-12-21 01:25:07  No: 36504

monaaさん>
度重なるアドバイスありがとうございます。
早速コードを実験させて頂きました。不正確な(?)ノートPCでも精度が上がりそうな感触が
あります。ただどこかの関数の戻り値がおかしいようで、10秒回すと4秒程度の答えが出ます
から、monaaさんのコードをよくよく研究して、トライを重ねてみたいと思います。
もちろん、だからといって全ての問題が解決する訳ではないことは承知していますが、工夫の
余地を教えて頂いて、感謝しています。
なおDelphiのバージョンは7です。
あと、一応キーボードはPCIバス上のIntel82801 DBM LPC. Interface Controllerというのに
繋がっていて、USB接続ではないと表示されています。


monaa  2009-12-21 02:04:04  No: 36505

さっきからずっとやってますが、
私の環境では100Hzが限界みたいです。
つまり1msの値が大体近似します。
125Hzは出せませんでした。(1msの値が5種類取得できない。)
VCL未使用最小構成Windowアプリ、コンソールアプリでも試しましたが、
ほとんど同じ結果となりました。
なんだか残念な結果ですが、これがソフトからできる限界でしょう。
でもまぁ限界が確認できたってだけで、少しすっきりしました。

Kennyさん
私の環境だと時計見ながら10秒で押せば大体10秒って出ます。
D7でも今確認しました。CPUが違うのかな??
QueryPerformanceCounter
QueryPerformanceFrequency
だけ使えば精度は1/1000以下になりますが、今回の動作には支障ありません。
CPUに依存しないし公開するならこっちの方がいいかもしれません。


Kenny  2009-12-21 03:55:26  No: 36506

色々テストを繰り返していますが、私のシステムではtimeGetTimeを使うよりも、QueryPerformanceFrequency/
QueryPerformanceCounterを使う方が精度良くキーボードイベントが起きた時刻を取れる
ようです。(RDTSCは使っていません。)なぜだか分かりませんが、コードでイベントを起こした
ときには同じような精度と思えまたが、キーボードでイベントを起こすと違う結果になりますね。
いずれにせよ、大体5/100秒以下の精度があればよい、そもそもWindowsだし、時々起こる大外しも
許容しないといけないということからすれば、これで納得するべきなのでしょう。
ただ、QueryPerformanceFrequencyは、システムによってはRDTSCから取った数値を返したり、
マルチコアで特別な配慮が必要なようなので、もう少し考えてみます。


※返信する前に利用規約をご確認ください。

※Google reCAPTCHA認証からCloudflare Turnstile認証へ変更しました。






  このエントリーをはてなブックマークに追加