From 6322bcce2a6136208b41e6a96efb498afe98df70 Mon Sep 17 00:00:00 2001 From: Philipp Oppermann Date: Wed, 31 Oct 2018 14:18:06 +0100 Subject: [PATCH 1/3] Use pc-keyboard crate for translating scancodes --- Cargo.lock | 34 +++++++++++++++++----------------- Cargo.toml | 1 + src/interrupts.rs | 31 +++++++++++++++---------------- src/lib.rs | 1 + 4 files changed, 34 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 05768770..e04d1eea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,11 +22,12 @@ version = "0.2.0" dependencies = [ "array-init 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "bootloader 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pc-keyboard 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "pic8259_simple 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "spin 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", + "spin 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", "uart_16550 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "volatile 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "volatile 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", "x86_64 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -78,11 +79,10 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "spin 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", - "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "spin 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -100,6 +100,11 @@ name = "os_bootinfo" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "pc-keyboard" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "pic8259_simple" version = "0.1.1" @@ -145,7 +150,7 @@ dependencies = [ [[package]] name = "spin" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -181,14 +186,9 @@ name = "ux" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "version_check" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "volatile" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -245,23 +245,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" "checksum getopts 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "0a7292d30132fb5424b354f5dc02512a86e4c516fe544bb7a25e7f266951b797" -"checksum lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca488b89a5657b0a2ecd45b95609b3e848cf1755da332a0da46e2b2b1cb371a7" +"checksum lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a374c89b9db55895453a74c1e38861d9deec0b01b405a82516e9d5de4820dea1" "checksum libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)" = "76e3a3ef172f1a0b9a9ff0dd1491ae5e6c948b94479a3021819ba7d860c8645d" "checksum nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "9a2228dca57108069a5262f2ed8bd2e82496d2e074a06d1ccc7ce1687b6ae0a2" "checksum os_bootinfo 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "66481dbeb5e773e7bd85b63cd6042c30786f834338288c5ec4f3742673db360a" +"checksum pc-keyboard 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fff50ab09ba31bcebc0669f4e64c0952fae1acdca9e6e0587e68e4e8443808ac" "checksum pic8259_simple 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dc64b2fd10828da8521b6cdabe0679385d7d2a3a6d4c336b819d1fa31ba35c72" "checksum pulldown-cmark 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8361e81576d2e02643b04950e487ec172b687180da65c731c03cf336784e6c07" "checksum rand 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8356f47b32624fef5b3301c1be97e5944ecdd595409cc5da11d05f211db6cfbd" "checksum remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3488ba1b9a2084d38645c4c08276a1752dcbf2c7130d74f1569681ad5d2799c5" "checksum skeptic 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "061203a849117b0f7090baf8157aa91dac30545208fbb85166ac58b4ca33d89c" -"checksum spin 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)" = "37b5646825922b96b5d7d676b5bb3458a54498e96ed7b0ce09dc43a07038fea4" +"checksum spin 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "ceac490aa12c567115b40b7b7fceca03a6c9d53d5defea066123debc83c5dc1f" "checksum tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" "checksum uart_16550 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "269f953d8de3226f7c065c589c7b4a3e83d10a419c7c3b5e2e0f197e6acc966e" "checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" "checksum usize_conversions 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f70329e2cbe45d6c97a5112daad40c34cd9a4e18edb5a2a18fefeb584d8d25e5" "checksum ux 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "53d8df5dd8d07fedccd202de1887d94481fadaea3db70479f459e8163a1fab41" -"checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" -"checksum volatile 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "54d4343a2df2d65144a874f95950754ee7b7e8594f6027aae8c7d0f4858a3fe8" +"checksum volatile 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d9ca391c55768e479d5c2f8beb40c136df09257292a809ea514e82cfdfc15d00" "checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index 43231692..730970f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ volatile = "0.2.3" uart_16550 = "0.1.0" x86_64 = "0.2.8" pic8259_simple = "0.1.1" +pc-keyboard = "0.3.1" [dependencies.lazy_static] version = "1.0" diff --git a/src/interrupts.rs b/src/interrupts.rs index 1c978ada..728e6853 100644 --- a/src/interrupts.rs +++ b/src/interrupts.rs @@ -61,26 +61,25 @@ extern "x86-interrupt" fn timer_interrupt_handler(_stack_frame: &mut ExceptionSt extern "x86-interrupt" fn keyboard_interrupt_handler(_stack_frame: &mut ExceptionStackFrame) { use x86_64::instructions::port::Port; + use pc_keyboard::{Keyboard, ScancodeSet1, DecodedKey, layouts}; + use spin::Mutex; + lazy_static! { + static ref KEYBOARD: Mutex> = + Mutex::new(Keyboard::new(layouts::Us104Key, ScancodeSet1)); + } + + let mut keyboard = KEYBOARD.lock(); let port = Port::new(0x60); let scancode: u8 = unsafe { port.read() }; - let key = match scancode { - 0x02 => Some('1'), - 0x03 => Some('2'), - 0x04 => Some('3'), - 0x05 => Some('4'), - 0x06 => Some('5'), - 0x07 => Some('6'), - 0x08 => Some('7'), - 0x09 => Some('8'), - 0x0a => Some('9'), - 0x0b => Some('0'), - _ => None, - }; - - if let Some(key) = key { - print!("{}", key); + if let Ok(Some(key_event)) = keyboard.add_byte(scancode) { + if let Some(key) = keyboard.process_keyevent(key_event) { + match key { + DecodedKey::Unicode(character) => print!("{}", character), + DecodedKey::RawKey(key) => print!("{:?}", key), + } + } } unsafe { PICS.lock().notify_end_of_interrupt(KEYBOARD_INTERRUPT_ID) } diff --git a/src/lib.rs b/src/lib.rs index accd97a4..31a702a6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ extern crate lazy_static; extern crate pic8259_simple; extern crate uart_16550; extern crate x86_64; +extern crate pc_keyboard; #[cfg(test)] extern crate array_init; From 264a32f747c2de66454fe7abb2b3fa457cdd2c11 Mon Sep 17 00:00:00 2001 From: Philipp Oppermann Date: Sat, 17 Nov 2018 16:59:27 +0100 Subject: [PATCH 2/3] Update post to use pc-keyboard for scancode translation --- .../posts/08-hardware-interrupts/index.md | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/blog/content/second-edition/posts/08-hardware-interrupts/index.md b/blog/content/second-edition/posts/08-hardware-interrupts/index.md index a2ac9e15..7dd7823b 100644 --- a/blog/content/second-edition/posts/08-hardware-interrupts/index.md +++ b/blog/content/second-edition/posts/08-hardware-interrupts/index.md @@ -532,7 +532,70 @@ The above code just translates keypresses of the number keys 0-9 and ignores all ![QEMU printing numbers to the screen](qemu-printing-numbers.gif) -Translating the other keys could work in the same way, probably with an enum for control keys such as escape or backspace. Such a translation function would be a good candidate for a small external crate, but I couldn't find one that works with scancode set 1. In case you'd like to write such a crate and need mentoring, just let us know, we're happy to help! +Translating the other keys works in the same way. Fortunately there is a crate named [`pc-keyboard`] for translating scancodes of scancode sets 1 and 2, so we don't have to implement this ourselves. To use the crate, we add it to our `Cargo.toml` and import it in our `lib.rs`: + +[`pc-keyboard`]: https://docs.rs/pc-keyboard/0.3.1/pc_keyboard/ + +```toml +# in Cargo.toml + +[dependencies] +pc-keyboard = "0.3.1" +``` + +```rust +// in src/lib.rs + +extern crate pc_keyboard; +``` + +Now we can use this crate to rewrite our `keyboard_interrupt_handler`: + +```rust +// in/src/interrupts.rs + +extern "x86-interrupt" fn keyboard_interrupt_handler( + _stack_frame: &mut ExceptionStackFrame) +{ + use x86_64::instructions::port::Port; + use pc_keyboard::{Keyboard, ScancodeSet1, DecodedKey, layouts}; + use spin::Mutex; + + lazy_static! { + static ref KEYBOARD: Mutex> = + Mutex::new(Keyboard::new(layouts::Us104Key, ScancodeSet1)); + } + + let mut keyboard = KEYBOARD.lock(); + let port = Port::new(0x60); + + let scancode: u8 = unsafe { port.read() }; + if let Ok(Some(key_event)) = keyboard.add_byte(scancode) { + if let Some(key) = keyboard.process_keyevent(key_event) { + match key { + DecodedKey::Unicode(character) => print!("{}", character), + DecodedKey::RawKey(key) => print!("{:?}", key), + } + } + } + + unsafe { PICS.lock().notify_end_of_interrupt(KEYBOARD_INTERRUPT_ID) } +} +``` + +We use the `lazy_static` macro to create a static [`Keyboard`] object protected by a Mutex. On each interrupt, we lock the Mutex, read the scancode from the keyboard controller and pass it to the [`add_byte`] method, which translates the scancode into an `Option`. The [`KeyEvent`] contains which key caused the event and whether it was a press or release event. + +[`Keyboard`]: https://docs.rs/pc-keyboard/0.3.1/pc_keyboard/struct.Keyboard.html +[`add_byte`]: https://docs.rs/pc-keyboard/0.3.1/pc_keyboard/struct.Keyboard.html#method.add_byte +[`KeyEvent`]: https://docs.rs/pc-keyboard/0.3.1/pc_keyboard/struct.KeyEvent.html + +To interpret this key event, we pass it to the [`process_keyevent`] method, which translates the key event to a character if possible. For example, translates a press event of the `A` key to either a lowercase `a` character or an uppercase `A` character, depending on whether the shift key was pressed. + +[`process_keyevent]: https://docs.rs/pc-keyboard/0.3.1/pc_keyboard/struct.Keyboard.html#method.process_keyevent + +With this modified interrupt handler we can now write text: + +TODO gif ### Configuring the Keyboard From 231888eb4c7af7810d7c1f897e96ec126f771b61 Mon Sep 17 00:00:00 2001 From: Philipp Oppermann Date: Sat, 17 Nov 2018 17:52:39 +0100 Subject: [PATCH 3/3] Add gif of me typing Hello World --- .../posts/08-hardware-interrupts/index.md | 2 +- .../posts/08-hardware-interrupts/qemu-typing.gif | Bin 0 -> 8344 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 blog/content/second-edition/posts/08-hardware-interrupts/qemu-typing.gif diff --git a/blog/content/second-edition/posts/08-hardware-interrupts/index.md b/blog/content/second-edition/posts/08-hardware-interrupts/index.md index 7dd7823b..bab486a8 100644 --- a/blog/content/second-edition/posts/08-hardware-interrupts/index.md +++ b/blog/content/second-edition/posts/08-hardware-interrupts/index.md @@ -595,7 +595,7 @@ To interpret this key event, we pass it to the [`process_keyevent`] method, whic With this modified interrupt handler we can now write text: -TODO gif +![Typing "Hello World" in QEMU](qemu-typing.gif) ### Configuring the Keyboard diff --git a/blog/content/second-edition/posts/08-hardware-interrupts/qemu-typing.gif b/blog/content/second-edition/posts/08-hardware-interrupts/qemu-typing.gif new file mode 100644 index 0000000000000000000000000000000000000000..7633d9ddcf3fd21712990abe14f23f8c3cc55d2a GIT binary patch literal 8344 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 L8j8{=tp#fU?}|13 literal 0 HcmV?d00001