Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 1 | ## |
| 2 | ## This file is part of the libsigrokdecode project. |
| 3 | ## |
| 4 | ## Copyright (C) 2014 Jens Steinhauser <jens.steinhauser@gmail.com> |
| 5 | ## |
| 6 | ## This program is free software; you can redistribute it and/or modify |
| 7 | ## it under the terms of the GNU General Public License as published by |
| 8 | ## the Free Software Foundation; either version 2 of the License, or |
| 9 | ## (at your option) any later version. |
| 10 | ## |
| 11 | ## This program is distributed in the hope that it will be useful, |
| 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 14 | ## GNU General Public License for more details. |
| 15 | ## |
| 16 | ## You should have received a copy of the GNU General Public License |
| 17 | ## along with this program; if not, write to the Free Software |
| 18 | ## Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
| 19 | ## |
| 20 | |
| 21 | import sigrokdecode as srd |
| 22 | |
Uwe Hermann | f04964c | 2014-08-10 09:22:29 +0200 | [diff] [blame^] | 23 | class ChannelError(Exception): |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 24 | pass |
| 25 | |
| 26 | regs = { |
| 27 | # addr: ('name', size) |
| 28 | 0x00: ('CONFIG', 1), |
| 29 | 0x01: ('EN_AA', 1), |
| 30 | 0x02: ('EN_RXADDR', 1), |
| 31 | 0x03: ('SETUP_AW', 1), |
| 32 | 0x04: ('SETUP_RETR', 1), |
| 33 | 0x05: ('RF_CH', 1), |
| 34 | 0x06: ('RF_SETUP', 1), |
| 35 | 0x07: ('STATUS', 1), |
| 36 | 0x08: ('OBSERVE_TX', 1), |
| 37 | 0x09: ('RPD', 1), |
| 38 | 0x0a: ('RX_ADDR_P0', 5), |
| 39 | 0x0b: ('RX_ADDR_P1', 5), |
| 40 | 0x0c: ('RX_ADDR_P2', 1), |
| 41 | 0x0d: ('RX_ADDR_P3', 1), |
| 42 | 0x0e: ('RX_ADDR_P4', 1), |
| 43 | 0x0f: ('RX_ADDR_P5', 1), |
| 44 | 0x10: ('TX_ADDR', 5), |
| 45 | 0x11: ('RX_PW_P0', 1), |
| 46 | 0x12: ('RX_PW_P1', 1), |
| 47 | 0x13: ('RX_PW_P2', 1), |
| 48 | 0x14: ('RX_PW_P3', 1), |
| 49 | 0x15: ('RX_PW_P4', 1), |
| 50 | 0x16: ('RX_PW_P5', 1), |
| 51 | 0x17: ('FIFO_STATUS', 1), |
| 52 | 0x1c: ('DYNPD', 1), |
Uwe Hermann | 35b380b | 2014-07-15 22:49:43 +0200 | [diff] [blame] | 53 | 0x1d: ('FEATURE', 1), |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 54 | } |
| 55 | |
| 56 | class Decoder(srd.Decoder): |
| 57 | api_version = 2 |
| 58 | id = 'nrf24l01' |
Uwe Hermann | 35b380b | 2014-07-15 22:49:43 +0200 | [diff] [blame] | 59 | name = 'nRF24L01(+)' |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 60 | longname = 'Nordic Semiconductor nRF24L01/nRF24L01+' |
Uwe Hermann | 35b380b | 2014-07-15 22:49:43 +0200 | [diff] [blame] | 61 | desc = '2.4GHz transceiver chip.' |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 62 | license = 'gplv2+' |
| 63 | inputs = ['spi'] |
| 64 | outputs = ['nrf24l01'] |
| 65 | annotations = ( |
Uwe Hermann | 35b380b | 2014-07-15 22:49:43 +0200 | [diff] [blame] | 66 | # Sent from the host to the chip. |
| 67 | ('cmd', 'Commands sent to the device'), |
| 68 | ('tx-data', 'Payload sent to the device'), |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 69 | |
Uwe Hermann | 35b380b | 2014-07-15 22:49:43 +0200 | [diff] [blame] | 70 | # Returned by the chip. |
| 71 | ('register', 'Registers read from the device'), |
| 72 | ('rx-data', 'Payload read from the device'), |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 73 | |
| 74 | ('warning', 'Warnings'), |
| 75 | ) |
| 76 | ann_cmd = 0 |
| 77 | ann_tx = 1 |
| 78 | ann_reg = 2 |
| 79 | ann_rx = 3 |
| 80 | ann_warn = 4 |
| 81 | annotation_rows = ( |
| 82 | ('commands', 'Commands', (ann_cmd, ann_tx)), |
| 83 | ('responses', 'Responses', (ann_reg, ann_rx)), |
| 84 | ('warnings', 'Warnings', (ann_warn,)), |
| 85 | ) |
| 86 | |
| 87 | def __init__(self, **kwargs): |
| 88 | self.next() |
| 89 | |
| 90 | def start(self): |
| 91 | self.out_ann = self.register(srd.OUTPUT_ANN) |
| 92 | |
| 93 | def warn(self, pos, msg): |
| 94 | '''Put a warning message 'msg' at 'pos'.''' |
| 95 | self.put(pos[0], pos[1], self.out_ann, [self.ann_warn, [msg]]) |
| 96 | |
| 97 | def putp(self, pos, ann, msg): |
| 98 | '''Put an annotation message 'msg' at 'pos'.''' |
| 99 | self.put(pos[0], pos[1], self.out_ann, [ann, [msg]]) |
| 100 | |
| 101 | def next(self): |
| 102 | '''Resets the decoder after a complete command was decoded.''' |
Uwe Hermann | 35b380b | 2014-07-15 22:49:43 +0200 | [diff] [blame] | 103 | # 'True' for the first byte after CS went low. |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 104 | self.first = True |
| 105 | |
Uwe Hermann | 35b380b | 2014-07-15 22:49:43 +0200 | [diff] [blame] | 106 | # The current command, and the minimum and maximum number |
| 107 | # of data bytes to follow. |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 108 | self.cmd = None |
| 109 | self.min = 0 |
| 110 | self.max = 0 |
| 111 | |
Uwe Hermann | 35b380b | 2014-07-15 22:49:43 +0200 | [diff] [blame] | 112 | # Used to collect the bytes after the command byte |
| 113 | # (and the start/end sample number). |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 114 | self.mb = [] |
| 115 | self.mb_s = -1 |
| 116 | self.mb_e = -1 |
| 117 | |
| 118 | def mosi_bytes(self): |
| 119 | '''Returns the collected MOSI bytes of a multi byte command.''' |
| 120 | return [b[0] for b in self.mb] |
| 121 | |
| 122 | def miso_bytes(self): |
| 123 | '''Returns the collected MISO bytes of a multi byte command.''' |
| 124 | return [b[1] for b in self.mb] |
| 125 | |
| 126 | def decode_command(self, pos, b): |
| 127 | '''Decodes the command byte 'b' at position 'pos' and prepares |
| 128 | the decoding of the following data bytes.''' |
| 129 | c = self.parse_command(b) |
Uwe Hermann | 35b380b | 2014-07-15 22:49:43 +0200 | [diff] [blame] | 130 | if c is None: |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 131 | self.warn(pos, 'unknown command') |
| 132 | return |
| 133 | |
| 134 | self.cmd, self.dat, self.min, self.max = c |
| 135 | |
| 136 | if self.cmd in ('W_REGISTER', 'ACTIVATE'): |
Uwe Hermann | 35b380b | 2014-07-15 22:49:43 +0200 | [diff] [blame] | 137 | # Don't output anything now, the command is merged with |
| 138 | # the data bytes following it. |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 139 | self.mb_s = pos[0] |
| 140 | else: |
| 141 | self.putp(pos, self.ann_cmd, self.format_command()) |
| 142 | |
| 143 | def format_command(self): |
| 144 | '''Returns the label for the current command.''' |
| 145 | if self.cmd == 'R_REGISTER': |
| 146 | reg = regs[self.dat][0] if self.dat in regs else 'unknown register' |
Jens Steinhauser | 8aeedf9 | 2014-08-03 23:30:42 +0200 | [diff] [blame] | 147 | return 'Cmd R_REGISTER "{}"'.format(reg) |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 148 | else: |
Jens Steinhauser | 8aeedf9 | 2014-08-03 23:30:42 +0200 | [diff] [blame] | 149 | return 'Cmd {}'.format(self.cmd) |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 150 | |
| 151 | def parse_command(self, b): |
| 152 | '''Parses the command byte. |
| 153 | |
| 154 | Returns a tuple consisting of: |
| 155 | - the name of the command |
| 156 | - additional data needed to dissect the following bytes |
| 157 | - minimum number of following bytes |
| 158 | - maximum number of following bytes |
| 159 | ''' |
| 160 | |
| 161 | if (b & 0xe0) in (0b00000000, 0b00100000): |
| 162 | c = 'R_REGISTER' if not (b & 0xe0) else 'W_REGISTER' |
| 163 | d = b & 0x1f |
| 164 | m = regs[d][1] if d in regs else 1 |
| 165 | return (c, d, 1, m) |
| 166 | if b == 0b01010000: |
| 167 | # nRF24L01 only |
| 168 | return ('ACTIVATE', None, 1, 1) |
| 169 | if b == 0b01100001: |
| 170 | return ('R_RX_PAYLOAD', None, 1, 32) |
| 171 | if b == 0b01100000: |
| 172 | return ('R_RX_PL_WID', None, 1, 1) |
| 173 | if b == 0b10100000: |
| 174 | return ('W_TX_PAYLOAD', None, 1, 32) |
| 175 | if b == 0b10110000: |
| 176 | return ('W_TX_PAYLOAD_NOACK', None, 1, 32) |
| 177 | if (b & 0xf8) == 0b10101000: |
| 178 | return ('W_ACK_PAYLOAD', b & 0x07, 1, 32) |
| 179 | if b == 0b11100001: |
| 180 | return ('FLUSH_TX', None, 0, 0) |
| 181 | if b == 0b11100010: |
| 182 | return ('FLUSH_RX', None, 0, 0) |
| 183 | if b == 0b11100011: |
| 184 | return ('REUSE_TX_PL', None, 0, 0) |
| 185 | if b == 0b11111111: |
| 186 | return ('NOP', None, 0, 0) |
| 187 | |
| 188 | def decode_register(self, pos, ann, regid, data): |
| 189 | '''Decodes a register. |
| 190 | |
| 191 | pos -- start and end sample numbers of the register |
| 192 | ann -- is the annotation number that is used to output the register. |
| 193 | regid -- may be either an integer used as a key for the 'regs' |
| 194 | dictionary, or a string directly containing a register name.' |
| 195 | data -- is the register content. |
| 196 | ''' |
| 197 | |
| 198 | if type(regid) == int: |
Uwe Hermann | 35b380b | 2014-07-15 22:49:43 +0200 | [diff] [blame] | 199 | # Get the name of the register. |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 200 | if regid not in regs: |
| 201 | self.warn(pos, 'unknown register') |
| 202 | return |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 203 | name = regs[regid][0] |
| 204 | else: |
| 205 | name = regid |
| 206 | |
Uwe Hermann | 35b380b | 2014-07-15 22:49:43 +0200 | [diff] [blame] | 207 | # Multi byte register come LSByte first. |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 208 | data = reversed(data) |
| 209 | |
| 210 | if self.cmd == 'W_REGISTER' and ann == self.ann_cmd: |
Uwe Hermann | 35b380b | 2014-07-15 22:49:43 +0200 | [diff] [blame] | 211 | # The 'W_REGISTER' command is merged with the following byte(s). |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 212 | label = '{}: {}'.format(self.format_command(), name) |
| 213 | else: |
Jens Steinhauser | 8aeedf9 | 2014-08-03 23:30:42 +0200 | [diff] [blame] | 214 | label = 'Reg {}'.format(name) |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 215 | |
Jens Steinhauser | 8aeedf9 | 2014-08-03 23:30:42 +0200 | [diff] [blame] | 216 | self.decode_mb_data(pos, ann, data, label, True) |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 217 | |
Jens Steinhauser | 8aeedf9 | 2014-08-03 23:30:42 +0200 | [diff] [blame] | 218 | def decode_mb_data(self, pos, ann, data, label, always_hex): |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 219 | '''Decodes the data bytes 'data' of a multibyte command at position |
Jens Steinhauser | 8aeedf9 | 2014-08-03 23:30:42 +0200 | [diff] [blame] | 220 | 'pos'. The decoded data is prefixed with 'label'. If 'always_hex' is |
| 221 | True, all bytes are decoded as hex codes, otherwise only non |
| 222 | printable characters are escaped.''' |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 223 | |
Jens Steinhauser | 8aeedf9 | 2014-08-03 23:30:42 +0200 | [diff] [blame] | 224 | if always_hex: |
| 225 | def escape(b): |
| 226 | return '{:02X}'.format(b) |
| 227 | else: |
| 228 | def escape(b): |
| 229 | c = chr(b) |
| 230 | if not str.isprintable(c): |
| 231 | return '\\x{:02X}'.format(b) |
| 232 | return c |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 233 | |
| 234 | data = ''.join([escape(b) for b in data]) |
| 235 | text = '{} = "{}"'.format(label, data) |
| 236 | self.putp(pos, ann, text) |
| 237 | |
| 238 | def finish_command(self, pos): |
| 239 | '''Decodes the remaining data bytes at position 'pos'.''' |
| 240 | |
| 241 | if self.cmd == 'R_REGISTER': |
| 242 | self.decode_register(pos, self.ann_reg, |
Uwe Hermann | 35b380b | 2014-07-15 22:49:43 +0200 | [diff] [blame] | 243 | self.dat, self.miso_bytes()) |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 244 | elif self.cmd == 'W_REGISTER': |
| 245 | self.decode_register(pos, self.ann_cmd, |
Uwe Hermann | 35b380b | 2014-07-15 22:49:43 +0200 | [diff] [blame] | 246 | self.dat, self.mosi_bytes()) |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 247 | elif self.cmd == 'R_RX_PAYLOAD': |
| 248 | self.decode_mb_data(pos, self.ann_rx, |
| 249 | self.miso_bytes(), 'RX payload', False) |
| 250 | elif (self.cmd == 'W_TX_PAYLOAD' or |
| 251 | self.cmd == 'W_TX_PAYLOAD_NOACK'): |
| 252 | self.decode_mb_data(pos, self.ann_tx, |
| 253 | self.mosi_bytes(), 'TX payload', False) |
| 254 | elif self.cmd == 'W_ACK_PAYLOAD': |
| 255 | lbl = 'ACK payload for pipe {}'.format(self.dat) |
| 256 | self.decode_mb_data(pos, self.ann_tx, |
| 257 | self.mosi_bytes(), lbl, False) |
| 258 | elif self.cmd == 'R_RX_PL_WID': |
| 259 | msg = 'Payload width = {}'.format(self.mb[0][1]) |
| 260 | self.putp(pos, self.ann_reg, msg) |
| 261 | elif self.cmd == 'ACTIVATE': |
| 262 | self.putp(pos, self.ann_cmd, self.format_command()) |
| 263 | if self.mosi_bytes()[0] != 0x73: |
| 264 | self.warn(pos, 'wrong data for "ACTIVATE" command') |
| 265 | |
| 266 | def decode(self, ss, es, data): |
| 267 | ptype, data1, data2 = data |
| 268 | |
| 269 | if ptype == 'CS-CHANGE': |
| 270 | if data1 == 0 and data2 == 1: |
Uwe Hermann | 35b380b | 2014-07-15 22:49:43 +0200 | [diff] [blame] | 271 | # Rising edge, the complete command is transmitted, process |
| 272 | # the bytes that were send after the command byte. |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 273 | if self.cmd: |
Uwe Hermann | 35b380b | 2014-07-15 22:49:43 +0200 | [diff] [blame] | 274 | # Check if we got the minimum number of data bytes |
| 275 | # after the command byte. |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 276 | if len(self.mb) < self.min: |
| 277 | self.warn((ss, ss), 'missing data bytes') |
| 278 | elif self.mb: |
| 279 | self.finish_command((self.mb_s, self.mb_e)) |
| 280 | |
| 281 | self.next() |
| 282 | elif ptype == 'DATA': |
Uwe Hermann | 35b380b | 2014-07-15 22:49:43 +0200 | [diff] [blame] | 283 | mosi, miso = data1, data2 |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 284 | pos = (ss, es) |
| 285 | |
Uwe Hermann | 35b380b | 2014-07-15 22:49:43 +0200 | [diff] [blame] | 286 | if miso is None or mosi is None: |
Uwe Hermann | f04964c | 2014-08-10 09:22:29 +0200 | [diff] [blame^] | 287 | raise ChannelError('Both MISO and MOSI pins required.') |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 288 | |
| 289 | if self.first: |
| 290 | self.first = False |
Uwe Hermann | 35b380b | 2014-07-15 22:49:43 +0200 | [diff] [blame] | 291 | # First MOSI byte is always the command. |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 292 | self.decode_command(pos, mosi) |
Uwe Hermann | 35b380b | 2014-07-15 22:49:43 +0200 | [diff] [blame] | 293 | # First MISO byte is always the status register. |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 294 | self.decode_register(pos, self.ann_reg, 'STATUS', [miso]) |
| 295 | else: |
| 296 | if not self.cmd or len(self.mb) >= self.max: |
| 297 | self.warn(pos, 'excess byte') |
| 298 | else: |
Uwe Hermann | 35b380b | 2014-07-15 22:49:43 +0200 | [diff] [blame] | 299 | # Collect the bytes after the command byte. |
Jens Steinhauser | 0e501c7 | 2014-07-08 22:15:30 +0200 | [diff] [blame] | 300 | if self.mb_s == -1: |
| 301 | self.mb_s = ss |
| 302 | self.mb_e = es |
| 303 | self.mb.append((mosi, miso)) |