Brian Silverman | 4787a6e | 2018-10-06 16:00:54 -0700 | [diff] [blame] | 1 | #!/usr/bin/python3 |
| 2 | |
| 3 | # This is a program to parse output from the ITM and DWT. |
| 4 | # The "Debug ITM and DWT Packet Protocol" section of the ARMv7-M Architecture |
| 5 | # Reference Manual is a good reference. |
| 6 | # |
| 7 | # This seems like it might be a poster child for using coroutines, but those |
| 8 | # look scary so we're going to stick with generators. |
| 9 | |
| 10 | import io |
| 11 | import os |
| 12 | import sys |
| 13 | |
Ravago Jones | 5127ccc | 2022-07-31 16:32:45 -0700 | [diff] [blame] | 14 | |
Brian Silverman | 4787a6e | 2018-10-06 16:00:54 -0700 | [diff] [blame] | 15 | def open_file_for_bytes(path): |
Ravago Jones | 5127ccc | 2022-07-31 16:32:45 -0700 | [diff] [blame] | 16 | '''Returns a file-like object which reads bytes without buffering.''' |
| 17 | # Not using `open` because it's unclear from the docs how (if it's possible at |
| 18 | # all) to get something that will only do one read call and return what that |
| 19 | # gets on a fifo. |
Brian Silverman | 4787a6e | 2018-10-06 16:00:54 -0700 | [diff] [blame] | 20 | try: |
Ravago Jones | 5127ccc | 2022-07-31 16:32:45 -0700 | [diff] [blame] | 21 | return io.FileIO(path, 'r') |
| 22 | except FileNotFoundError: |
| 23 | # If it wasn't found, try (once) to create it and then open again. |
| 24 | try: |
| 25 | os.mkfifo(path) |
| 26 | except FileExistsError: |
| 27 | pass |
| 28 | return io.FileIO(path, 'r') |
| 29 | |
Brian Silverman | 4787a6e | 2018-10-06 16:00:54 -0700 | [diff] [blame] | 30 | |
| 31 | def read_bytes(path): |
Ravago Jones | 5127ccc | 2022-07-31 16:32:45 -0700 | [diff] [blame] | 32 | '''Reads bytes from a file. This is appropriate both for regular files and |
Brian Silverman | 4787a6e | 2018-10-06 16:00:54 -0700 | [diff] [blame] | 33 | fifos. |
| 34 | Args: |
| 35 | path: A path-like object to open. |
| 36 | Yields: |
| 37 | Individual bytes from the file, until hitting EOF. |
| 38 | ''' |
Ravago Jones | 5127ccc | 2022-07-31 16:32:45 -0700 | [diff] [blame] | 39 | with open_file_for_bytes(path) as f: |
| 40 | while True: |
| 41 | buf = f.read(1024) |
| 42 | if not buf: |
| 43 | return |
| 44 | for byte in buf: |
| 45 | yield byte |
| 46 | |
Brian Silverman | 4787a6e | 2018-10-06 16:00:54 -0700 | [diff] [blame] | 47 | |
| 48 | def parse_packets(source): |
Ravago Jones | 5127ccc | 2022-07-31 16:32:45 -0700 | [diff] [blame] | 49 | '''Parses a stream of bytes into packets. |
Brian Silverman | 4787a6e | 2018-10-06 16:00:54 -0700 | [diff] [blame] | 50 | Args: |
| 51 | source: A generator of individual bytes. |
| 52 | Generates: |
| 53 | Packets as bytes objects. |
| 54 | ''' |
Ravago Jones | 5127ccc | 2022-07-31 16:32:45 -0700 | [diff] [blame] | 55 | try: |
| 56 | while True: |
| 57 | header = next(source) |
| 58 | if header == 0: |
| 59 | # Synchronization packets consist of a bunch of 0 bits (not necessarily |
| 60 | # a whole number of bytes), followed by a 128 byte. This is for hardware |
| 61 | # to synchronize on, but we're not in a position to do that, so |
| 62 | # presumably those should get filtered out before getting here? |
| 63 | raise 'Not sure how to handle synchronization packets' |
| 64 | packet = bytearray() |
| 65 | packet.append(header) |
| 66 | header_size = header & 3 |
| 67 | if header_size == 0: |
| 68 | while packet[-1] & 128 and len(packet) < 7: |
| 69 | packet.append(next(source)) |
| 70 | else: |
| 71 | if header_size == 3: |
| 72 | header_size = 4 |
| 73 | for _ in range(header_size): |
| 74 | packet.append(next(source)) |
| 75 | yield bytes(packet) |
| 76 | except StopIteration: |
| 77 | return |
| 78 | |
Brian Silverman | 4787a6e | 2018-10-06 16:00:54 -0700 | [diff] [blame] | 79 | |
| 80 | class PacketParser(object): |
Brian Silverman | 4787a6e | 2018-10-06 16:00:54 -0700 | [diff] [blame] | 81 | |
Ravago Jones | 5127ccc | 2022-07-31 16:32:45 -0700 | [diff] [blame] | 82 | def __init__(self): |
| 83 | self.stimulus_handlers = {} |
Brian Silverman | 4787a6e | 2018-10-06 16:00:54 -0700 | [diff] [blame] | 84 | |
Ravago Jones | 5127ccc | 2022-07-31 16:32:45 -0700 | [diff] [blame] | 85 | def register_stimulus_handler(self, port_number, handler): |
| 86 | '''Registers a function to call on packets to the specified port.''' |
| 87 | self.stimulus_handlers[port_number] = handler |
| 88 | |
| 89 | def process(self, path): |
| 90 | for packet in parse_packets(read_bytes(path)): |
| 91 | header = packet[0] |
| 92 | header_size = header & 3 |
| 93 | if header_size == 0: |
| 94 | # TODO(Brian): At least handle overflow packets here. |
| 95 | pass |
| 96 | else: |
| 97 | port_number = header >> 3 |
| 98 | if port_number in self.stimulus_handlers: |
| 99 | self.stimulus_handlers[port_number](packet[1:]) |
| 100 | else: |
| 101 | print('Warning: unhandled stimulus port %d' % port_number, |
| 102 | file=sys.stderr) |
| 103 | self.stimulus_handlers[port_number] = lambda _: None |
| 104 | |
Brian Silverman | 4787a6e | 2018-10-06 16:00:54 -0700 | [diff] [blame] | 105 | |
| 106 | if __name__ == '__main__': |
Ravago Jones | 5127ccc | 2022-07-31 16:32:45 -0700 | [diff] [blame] | 107 | parser = PacketParser() |
Brian Silverman | 4787a6e | 2018-10-06 16:00:54 -0700 | [diff] [blame] | 108 | |
Ravago Jones | 5127ccc | 2022-07-31 16:32:45 -0700 | [diff] [blame] | 109 | def print_byte(payload): |
| 110 | sys.stdout.write(payload.decode('ascii')) |
| 111 | |
| 112 | parser.register_stimulus_handler(0, print_byte) |
| 113 | |
| 114 | for path in sys.argv[1:]: |
| 115 | parser.process(path) |