diff --git a/decoders/ps2/__init__.py b/decoders/ps2/__init__.py old mode 100644 new mode 100755 index 75c47687..d6194dd8 --- a/decoders/ps2/__init__.py +++ b/decoders/ps2/__init__.py @@ -2,6 +2,7 @@ ## This file is part of the libsigrokdecode project. ## ## Copyright (C) 2016 Daniel Schulte +## Copyright (C) 2023 Marshal Horn ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by @@ -18,9 +19,11 @@ ## ''' -This protocol decoder can decode PS/2 device -> host communication. +This protocol decoder can decode PS/2 device -> host communication \ +and host -> device communication. -Host -> device communication is currently not supported. +To interpret the data, please stack the appropriate keyboard or mouse \ +decoder ''' from .pd import Decoder diff --git a/decoders/ps2/pd.py b/decoders/ps2/pd.py old mode 100644 new mode 100755 index a9d0a986..c1ac6246 --- a/decoders/ps2/pd.py +++ b/decoders/ps2/pd.py @@ -2,6 +2,7 @@ ## This file is part of the libsigrokdecode project. ## ## Copyright (C) 2016 Daniel Schulte +## Copyright (C) 2023 Marshal Horn ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by @@ -18,22 +19,33 @@ ## import sigrokdecode as srd -from collections import namedtuple class Ann: - BIT, START, STOP, PARITY_OK, PARITY_ERR, DATA, WORD = range(7) + BIT, START, WORD, PARITY_OK, PARITY_ERR, STOP, ACK, NACK, HREQ, HSTART, HWORD, HPARITY_OK, HPARITY_ERR, HSTOP = range(14) + +class Bit: + def __init__(self, val, ss, es): + self.val = val + self.ss = ss + self.es = es + +class Ps2Packet: + def __init__(self, val, host=False, pok=False, ack=False): + self.val = val #byte value + self.host = host #Host transmissions + self.pok = pok #Parity ok + self.ack = ack #Acknowlege ok for host transmission. -Bit = namedtuple('Bit', 'val ss es') class Decoder(srd.Decoder): api_version = 3 id = 'ps2' name = 'PS/2' longname = 'PS/2' - desc = 'PS/2 keyboard/mouse interface.' + desc = 'PS/2 packet interface used by PC keyboards and mice' license = 'gplv2+' inputs = ['logic'] - outputs = [] + outputs = ['ps2'] tags = ['PC'] channels = ( {'id': 'clk', 'name': 'Clock', 'desc': 'Clock line'}, @@ -42,19 +54,28 @@ class Decoder(srd.Decoder): annotations = ( ('bit', 'Bit'), ('start-bit', 'Start bit'), - ('stop-bit', 'Stop bit'), + ('word', 'Word'), ('parity-ok', 'Parity OK bit'), ('parity-err', 'Parity error bit'), - ('data-bit', 'Data bit'), + ('stop-bit', 'Stop bit'), + ('ack', 'Acknowledge'), + ('nack', 'Not Acknowledge'), + ('req', 'Host request to send'), + ('start-bit', 'Start bit'), ('word', 'Word'), + ('parity-ok', 'Parity OK bit'), + ('parity-err', 'Parity error bit'), + ('stop-bit', 'Stop bit'), ) annotation_rows = ( ('bits', 'Bits', (0,)), - ('fields', 'Fields', (1, 2, 3, 4, 5, 6)), + ('fields', 'Device', (1,2,3,4,5,6,7,)), + ('host', 'Host', (8,9,10,11,12,13)), ) def __init__(self): self.reset() + self.min_clk_hz = 9e3 #Minimum clock rate for PS/2 is 10kHz, but I want to be lenient def reset(self): self.bits = [] @@ -62,65 +83,102 @@ def reset(self): def start(self): self.out_ann = self.register(srd.OUTPUT_ANN) - - def putb(self, bit, ann_idx): - b = self.bits[bit] - self.put(b.ss, b.es, self.out_ann, [ann_idx, [str(b.val)]]) + self.out_py = self.register(srd.OUTPUT_PYTHON) + + def metadata(self,key,value): + if key == srd.SRD_CONF_SAMPLERATE: + self.samplerate = value + + def get_bits(self, n, edge:'r'): + _, dat = self.wait([{0:edge},{1:'l'}]) #No timeout for start bit + if not self.matched[1]: + return #No start bit + self.bits.append(Bit(dat, self.samplenum, self.samplenum+self.max_period)) + if not self.matched[0]: + self.wait({0:'f'}) #Wait for clock edge from device + for i in range(1,n): + _, dat = self.wait([{0:edge},{'skip':self.max_period}]) + if not self.matched[0]: + break #Timed out + self.bits.append(Bit(dat, self.samplenum, self.samplenum+self.max_period)) + #Fix the ending period + self.bits[i-1].es = self.samplenum + if len(self.bits) == n: + self.wait([{0:'r'},{'skip':self.max_period}]) + self.bits[-1].es = self.samplenum + self.bitcount = len(self.bits) def putx(self, bit, ann): self.put(self.bits[bit].ss, self.bits[bit].es, self.out_ann, ann) - def handle_bits(self, datapin): - # Ignore non start condition bits (useful during keyboard init). - if self.bitcount == 0 and datapin == 1: - return - - # Store individual bits and their start/end samplenumbers. - self.bits.append(Bit(datapin, self.samplenum, self.samplenum)) - - # Fix up end sample numbers of the bits. - if self.bitcount > 0: - b = self.bits[self.bitcount - 1] - self.bits[self.bitcount - 1] = Bit(b.val, b.ss, self.samplenum) - if self.bitcount == 11: - self.bitwidth = self.bits[1].es - self.bits[2].es - b = self.bits[-1] - self.bits[-1] = Bit(b.val, b.ss, b.es + self.bitwidth) - - # Find all 11 bits. Start + 8 data + odd parity + stop. - if self.bitcount < 11: - self.bitcount += 1 - return - - # Extract data word. - word = 0 - for i in range(8): - word |= (self.bits[i + 1].val << i) + def handle_bits(self, host=False): + packet = None + if self.bitcount > 8: + # Annotate individual bits + for b in self.bits: + self.put(b.ss, b.es, self.out_ann, [Ann.BIT, [str(b.val)]]) + # Annotate start bit + self.putx(0, [Ann.HSTART if host else Ann.START, ['Start bit', 'Start', 'S']]) + # Annotate the data word + word = 0 + for i in range(8): + word |= (self.bits[i + 1].val << i) + self.put(self.bits[1].ss, self.bits[8].es, self.out_ann, + [Ann.HWORD if host else Ann.WORD, + ['Data: %02x' % word, 'D: %02x' % word, '%02x' % word]]) + packet = Ps2Packet(val = word, host = host) # Calculate parity. - parity_ok = (bin(word).count('1') + self.bits[9].val) % 2 == 1 - - # Emit annotations. - for i in range(11): - self.putb(i, Ann.BIT) - self.putx(0, [Ann.START, ['Start bit', 'Start', 'S']]) - self.put(self.bits[1].ss, self.bits[8].es, self.out_ann, [Ann.WORD, - ['Data: %02x' % word, 'D: %02x' % word, '%02x' % word]]) - if parity_ok: - self.putx(9, [Ann.PARITY_OK, ['Parity OK', 'Par OK', 'P']]) - else: - self.putx(9, [Ann.PARITY_ERR, ['Parity error', 'Par err', 'PE']]) - self.putx(10, [Ann.STOP, ['Stop bit', 'Stop', 'St', 'T']]) + if self.bitcount > 9: + parity_ok = 0 + for bit in self.bits[1:10]: + parity_ok ^= bit.val + if bool(parity_ok): + self.putx(9, [Ann.HPARITY_OK if host else Ann.PARITY_OK, ['Parity OK', 'Par OK', 'P']]) + packet.pok = True #Defaults to false in case packet was interrupted + else: + self.putx(9, [Ann.HPARITY_ERR if host else Ann.PARITY_ERR, ['Parity error', 'Par err', 'PE']]) + + # Annotate stop bit + if self.bitcount > 10: + self.putx(10, [Ann.HSTOP if host else Ann.STOP, ['Stop bit', 'Stop', 'St', 'T']]) + # Annotate ACK + if host and self.bitcount > 11: + if self.bits[11].val == 0: + self.putx(11, [Ann.ACK, ['Acknowledge', 'Ack', 'A']]) + else: + self.putx(11, [Ann.NACK, ['Not Acknowledge', 'Nack', 'N']]) + packet.ack = not bool(self.bits[11].val) + + if(packet): + self.put(self.bits[0].ss, self.bits[-1].ss, self.out_py,packet) + self.reset() - self.bits, self.bitcount = [], 0 def decode(self): + if self.samplerate: + self.max_period = int(self.samplerate / self.min_clk_hz)+1 + else: + raise SamplerateError("Cannot decode without samplerate") while True: - # Sample data bits on the falling clock edge (assume the device - # is the transmitter). Expect the data byte transmission to end - # at the rising clock edge. Cope with the absence of host activity. - _, data_pin = self.wait({0: 'f'}) - self.handle_bits(data_pin) - if self.bitcount == 1 + 8 + 1 + 1: - _, data_pin = self.wait({0: 'r'}) - self.handle_bits(data_pin) + # Falling edge of data indicates start condition + # Clock held for 100us indicates host "request to send" + self.wait([{1: 'f'},{0:'l'}]) + ss = self.samplenum + host = self.matched[1] + if host: + # Make sure the clock is held low for at least 100 microseconds before data is pulled down + self.wait([{0:'h'},{'skip': self.max_period},{1:'l'}]) + if self.matched[0]: + continue #Probably the trailing edge of a transfer + elif self.matched[2]: #Probably a bus error + self.wait({1:'h'}) #Wait for data to be released + continue + # Host emits bits on rising clk edge + self.get_bits(12, 'r') + if self.bitcount > 0: + self.put(ss,self.bits[0].ss,self.out_ann, [Ann.HREQ,['Host RTS', 'HRTS', 'H']]) + else: + # Client emits data on falling edge + self.get_bits(11, 'f') + self.handle_bits(host=host) diff --git a/decoders/ps2_keyboard/__init__.py b/decoders/ps2_keyboard/__init__.py new file mode 100755 index 00000000..a9d499a6 --- /dev/null +++ b/decoders/ps2_keyboard/__init__.py @@ -0,0 +1,25 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2023 Marshal Horn +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program. If not, see . +## + +''' +This protocol decoder can decode PS/2 keyboard commands. +It should be stacked on the PS/2 packet decoder. +''' + +from .pd import Decoder diff --git a/decoders/ps2_keyboard/pd.py b/decoders/ps2_keyboard/pd.py new file mode 100755 index 00000000..70cae724 --- /dev/null +++ b/decoders/ps2_keyboard/pd.py @@ -0,0 +1,93 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2023 Marshal Horn +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, see . +## + +import sigrokdecode as srd +from .sc import key_decode + +class Ps2Packet: + def __init__(self, val, host=False, pok=False, ack=False): + self.val = val #byte value + self.host = host #Host transmissions + self.pok = pok #Parity ok + self.ack = ack #Acknowlege ok for host transmission. + +class Ann: + PRESS,RELEASE,ACK = range(3) + +class Decoder(srd.Decoder): + api_version = 3 + id = 'ps2_keyboard' + name = 'PS/2 Keyboard' + longname = 'PS/2 Keyboard' + desc = 'PS/2 keyboard interface.' + license = 'gplv2+' + inputs = ['ps2'] + outputs = [] + tags = ['PC'] + binary = ( + ('Keys', 'Key presses'), + ) + annotations = ( + ('Press', 'Key pressed'), + ('Release', 'Key released'), + ('Ack', 'Acknowledge'), + ) + annotation_rows = ( + ('keys', 'key presses and releases',(0,1,2)), + ) + + def __init__(self): + self.reset() + + def reset(self): + self.sw = 0 #for switch statement + self.ann = Ann.PRESS #defualt to keypress + self.extended = False + + def start(self): + self.out_binary = self.register(srd.OUTPUT_BINARY) + self.out_ann = self.register(srd.OUTPUT_ANN) + + def decode(self,startsample,endsample,data): + if data.host: + # Ignore host commands or interrupted keycodes + self.reset() + return + if self.sw < 1: + self.ss = startsample + self.sw = 1 + if self.sw < 2: + if data.val == 0xF0: #Break code + self.ann = Ann.RELEASE + return + elif data.val == 0xE0: #Extended character + self.extended = True + return + elif data.val == 0xFA: #Acknowledge code + c = ['Acknowledge','ACK'] + self.ann = Ann.ACK + self.sw = 4 + if self.sw < 3: + c = key_decode(data.val, self.extended) + + self.put(self.ss,endsample,self.out_ann,[self.ann,c]) + if self.ann == Ann.PRESS: + self.put(self.ss,endsample,self.out_binary,[0,c[-1].encode('UTF-8')]) + self.reset() + diff --git a/decoders/ps2_keyboard/sc.py b/decoders/ps2_keyboard/sc.py new file mode 100644 index 00000000..96f76c16 --- /dev/null +++ b/decoders/ps2_keyboard/sc.py @@ -0,0 +1,99 @@ +#A list of scancodes for the PS2 keyboard +ext = { + 0x1F:'L Sup', + 0x14:'R Ctrl', + 0x27:'R Sup', + 0x11:'R Alt', + 0x2F:'Menu', + 0x12:'PrtScr', + 0x7C:'SysRq', + 0x70:'Insert', + 0x6C:'Home', + 0x7D:'Pg Up', + 0x71:'Delete', + 0x69:'End', + 0x7A:'Pg Dn', + 0x75:['Up arrow','^'], + 0x6B:['Left arrow','Left','<-'], + 0x74:['Right arrow','Right','->'], + 0x72:['Down arrow','Down','v'], + 0x4A:['KP /','/'], + 0x5A:['KP Ent','\n'], +} + +std = { + 0x1C:'A', + 0x32:'B', + 0x21:'C', + 0x23:'D', + 0x24:'E', + 0x2B:'F', + 0x34:'G', + 0x33:'H', + 0x43:'I', + 0x3B:'J', + 0x42:'K', + 0x4B:'L', + 0x3A:'M', + 0x31:'N', + 0x44:'O', + 0x4D:'P', + 0x15:'Q', + 0x2D:'R', + 0x1B:'S', + 0x2C:'T', + 0x3C:'U', + 0x2A:'V', + 0x1D:'W', + 0x22:'X', + 0x35:'Y', + 0x1A:'Z', + 0x45:'0)', + 0x16:'1!', + 0x1E:'2@', + 0x26:'3#', + 0x25:'4$', + 0x2E:'5%', + 0x36:'6^', + 0x3D:'7&', + 0x3E:'8*', + 0x46:'9(', + 0x0E:'`~', + 0x4E:'-_', + 0x55:'=+', + 0x5D:'\|', + 0x66:'Backsp', + 0x29:['Space',' '], + 0x0D:['Tab','\t'], + 0x58:'CapsLk', + 0x12:'L Shft', + 0x14:'L Ctrl', + 0x11:'L Alt', + 0x59:'R Shft', + 0x5A:['Enter','\n'], + 0x76:'Esc', + 0x5:'F1', + 0x6:'F2', + 0x4:'F3', + 0x0C:'F4', + 0x3:'F5', + 0x0B:'F6', + 0x83:'F7', + 0x0A:'F8', + 0x1:'F9', + 0x9:'F10', + 0x78:'F11', + 0x7:'F12', + 0x7E:'ScrLck', +} + + +def key_decode(code, extended): + try: + c = ext[code] if extended else std[code] + except KeyError: + fs = '[E0%0X]' if extended else '[%0X]' + c = fs % code + if not isinstance(0,list): + c = [c] + return c diff --git a/decoders/ps2_mouse/__init__.py b/decoders/ps2_mouse/__init__.py new file mode 100755 index 00000000..b43a1684 --- /dev/null +++ b/decoders/ps2_mouse/__init__.py @@ -0,0 +1,25 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2023 Marshal Horn +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program. If not, see . +## + +''' +This protocol decoder can decode PS/2 mouse commands. +It should be stacked on the PS/2 packet decoder. +''' + +from .pd import Decoder diff --git a/decoders/ps2_mouse/pd.py b/decoders/ps2_mouse/pd.py new file mode 100755 index 00000000..0101deab --- /dev/null +++ b/decoders/ps2_mouse/pd.py @@ -0,0 +1,124 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2023 Marshal Horn +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, see . +## + +import sigrokdecode as srd + +class Ps2Packet: + def __init__(self, val, host=False, pok=False, ack=False): + self.val = val #byte value + self.host = host #Host transmissions + self.pok = pok #Parity ok + self.ack = ack #Acknowlege ok for host transmission. + +class Decoder(srd.Decoder): + api_version = 3 + id = 'ps2_mouse' + name = 'PS/2 Mouse' + longname = 'PS/2 Mouse' + desc = 'PS/2 mouse interface.' + license = 'gplv2+' + inputs = ['ps2'] + outputs = [] + tags = ['PC'] + binary = ( + ('bytes', 'Bytes without explanation'), + ('movement', 'Explanation of mouse movement and clicks'), + ) + annotations = ( + ('Movement', 'Mouse movement packets'), + ) + annotation_rows = ( + ('mov', 'Mouse Movement',(0,)), + ) + + def __init__(self): + self.reset() + + def reset(self): + self.packets = [] + self.es = 0 + self.ss = 0 + + def start(self): + self.out_binary = self.register(srd.OUTPUT_BINARY) + self.out_ann = self.register(srd.OUTPUT_ANN) + + def metadata(self,key,value): + if key == srd.SRD_CONF_SAMPLERATE: + self.samplerate = value + + def mouse_movement(self): + if len(self.packets) >= 3: + if not self.packets[0].host: + msg = '' + [flags,x,y] = [p.val for p in self.packets[:3]] + if flags & 1: + msg += 'L' + if flags & 2: + msg += 'M' + if flags & 4: + msg += 'R' + if flags & 0x10: + x = x-256 + if flags & 0x20: + y = y-256 + if x != 0: + msg += ' X%+d' % x + if flags & 0x40: + msg += '!!' + if y != 0: + msg += ' Y%+d' % y + if flags & 0x80: + msg += '!!' + if msg == '': + msg = 'No Movement' + ustring = ('\n' + msg).encode('UTF-8') + self.put(self.ss,self.es,self.out_binary, [1,ustring] ) + self.put(self.ss,self.es,self.out_ann, [0,[msg]] ) + + + def print_packets(self): + self.mouse_movement() + tag = "Host: " if self.packets[-1].host else "Mouse:" + octets = ' '.join(["%02X" % x.val for x in self.packets]) + unicode_string = ("\n"+tag+" "+octets).encode('UTF-8') + self.put(self.ss,self.es,self.out_binary, [0,unicode_string] ) + self.reset() + + def mouse_ack(self,ss,es): + self.put(ss,es,self.out_binary, [0,b' ACK'] ) + + def decode(self,startsample,endsample,data): + if len(self.packets) == 0: + self.ss = startsample + elif data.host != self.packets[-1].host: + self.print_packets() + self.ss = startsample #Packets were cleared, need to set startsample again + if data.val == 0xFA and not data.host: + #Special case: acknowledge byte from mouse + self.mouse_ack(startsample,endsample) + self.reset() + return + + self.packets.append(data) + self.es = endsample + #Mouse streaming packets are in 3s + #Timing is not guaranteed because host can hold the clock at any point + if len(self.packets)>2: + self.print_packets()