ruby-adept 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. data/.gitignore +17 -0
  2. data/.travis.yml +6 -0
  3. data/Gemfile +10 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +29 -0
  6. data/Rakefile +16 -0
  7. data/adept.gemspec +33 -0
  8. data/autotest/discover.rb +2 -0
  9. data/bin/bprog +110 -0
  10. data/firmware/.gitignore +73 -0
  11. data/firmware/epp_stream/Basys2_100_250General.ucf +21 -0
  12. data/firmware/epp_stream/epp_controller.vhd +210 -0
  13. data/firmware/epp_stream/epp_stream.xise +355 -0
  14. data/firmware/epp_stream/fifo.vhd +178 -0
  15. data/firmware/epp_stream/tests/fifo_testbench.vhdl +164 -0
  16. data/lib/adept/boards/basys2.rb +84 -0
  17. data/lib/adept/boards.rb +2 -0
  18. data/lib/adept/connection_provider.rb +30 -0
  19. data/lib/adept/data_formats/bitstream.rb +116 -0
  20. data/lib/adept/data_formats/data_factories.rb +33 -0
  21. data/lib/adept/data_formats.rb +2 -0
  22. data/lib/adept/device.rb +127 -0
  23. data/lib/adept/error.rb +4 -0
  24. data/lib/adept/jtag/connection.rb +404 -0
  25. data/lib/adept/jtag/device.rb +178 -0
  26. data/lib/adept/jtag/devices/fpga.rb +162 -0
  27. data/lib/adept/jtag/devices/null.rb +0 -0
  28. data/lib/adept/jtag/devices/platform_flash.rb +23 -0
  29. data/lib/adept/jtag/devices.rb +2 -0
  30. data/lib/adept/jtag/error.rb +8 -0
  31. data/lib/adept/jtag/tap_state.rb +67 -0
  32. data/lib/adept/jtag/tap_states.rb +52 -0
  33. data/lib/adept/jtag.rb +11 -0
  34. data/lib/adept/low_level/connection.rb +59 -0
  35. data/lib/adept/low_level/device.rb +43 -0
  36. data/lib/adept/low_level/device_error.rb +22 -0
  37. data/lib/adept/low_level/device_manager.rb +142 -0
  38. data/lib/adept/low_level/enhanced_parallel.rb +151 -0
  39. data/lib/adept/low_level/error_handler.rb +60 -0
  40. data/lib/adept/low_level/jtag.rb +379 -0
  41. data/lib/adept/low_level/library.rb +173 -0
  42. data/lib/adept/low_level.rb +4 -0
  43. data/lib/adept/version.rb +3 -0
  44. data/lib/adept.rb +11 -0
  45. data/spec/firmware/epp_loopback.bit +0 -0
  46. data/spec/lib/adept/data_formats/bitstream_spec.rb +95 -0
  47. data/spec/lib/adept/data_formats/data_factories_spec.rb +42 -0
  48. data/spec/lib/adept/device_spec.rb +88 -0
  49. data/spec/lib/adept/jtag/connection_spec.rb +433 -0
  50. data/spec/lib/adept/jtag/device_spec.rb +107 -0
  51. data/spec/lib/adept/jtag/devices/fpga_spec.rb +71 -0
  52. data/spec/lib/adept/low_level/enhanced_parallel_spec.rb +72 -0
  53. data/spec/lib/adept/low_level/jtag_spec.rb +204 -0
  54. data/spec/spec_helpers.rb +25 -0
  55. metadata +240 -0
@@ -0,0 +1,404 @@
1
+
2
+ require 'adept'
3
+ require 'adept/low_level'
4
+ require 'adept/jtag'
5
+
6
+ module Adept
7
+ module JTAG
8
+
9
+ #
10
+ # Represents a connection to a JTAG device.
11
+ #
12
+ class Connection
13
+ extend ConnectionProvider
14
+
15
+ attr_reader :tap_state
16
+
17
+ #
18
+ # Sets up a new JTAG connection.
19
+ #
20
+ def initialize(device, port_number=0)
21
+
22
+ #Store the information regarding the owning device...
23
+ @device = device
24
+
25
+ #Initialize the chain to zero until enumeration occurs.
26
+ @chain_length = 0
27
+ @devices_in_chain = 0
28
+
29
+ #... open a JTAG connection.
30
+ LowLevel::JTAG::EnableEx(@device.handle, port_number)
31
+
32
+ end
33
+
34
+ #
35
+ # Closes the given JTAG connection.
36
+ #
37
+ def close
38
+ LowLevel::JTAG::Disable(@device.handle)
39
+ end
40
+
41
+ #
42
+ # Returns a list of the JTAG IDCodes for all connected devices.
43
+ #
44
+ def connected_devices
45
+
46
+ #Reset all targets' TAPs; this will automatically load the IDCODE instruction into the
47
+ #instruction register
48
+ reset_target
49
+
50
+ devices = []
51
+ chain_length = 0
52
+ devices_in_chain = 0
53
+
54
+ #Loop until we've enumerated all devices in the JTAG chain.
55
+ loop do
56
+
57
+ #Recieve a single 32-bit JTAG ID code, LSB first.
58
+ idcode = receive_data(32, true)
59
+
60
+ #If we've recieved the special "null" IDcode, we've finished enumerating.
61
+ #(In this case, we'll choose to accept the technically-valid all-ones IDcode as null,
62
+ #as it is returned by most system boards when their power is turned off, and isn't
63
+ #otherwise supported.)
64
+ break if idcode == "\x00\x00\x00\x00" or idcode == "\xFF\xFF\xFF\xFF"
65
+
66
+ #Otherwise, add this idcode to the list...
67
+ devices << JTAG::Device.from_idcode(idcode.reverse, self, devices_in_chain, chain_length)
68
+
69
+ #... add its width the the known scan-chain length
70
+ chain_length += devices.last.instruction_width
71
+ devices_in_chain += 1
72
+
73
+ end
74
+
75
+ #Update the internal chain-length.
76
+ @chain_length = chain_length
77
+ @devices_in_chain = devices_in_chain
78
+
79
+ #Return the list of IDCodes.
80
+ devices.reverse
81
+
82
+ end
83
+
84
+ #
85
+ # Sets the state of the target's Test Access Port.
86
+ #
87
+ def tap_state=(new_state)
88
+
89
+ #If we're trying to enter the reset state, force a reset of the test hardware.
90
+ #(This ensure that we can reset the test hardware even if a communications (or target) error
91
+ # causes improper behavior.)
92
+ reset_target if new_state == JTAG::TAPStates::Reset
93
+
94
+ #If we're already in the desired state, abort.
95
+ return if new_state == @tap_state
96
+
97
+ #Find the correct sequence of TMS values to reach the desired state...
98
+ path = path_to_state(new_state).reverse
99
+ tms_values = [path.to_i(2)]
100
+
101
+ #... and apply them.
102
+ LowLevel::JTAG::transmit(@device.handle, tms_values, false, path.length, false)
103
+
104
+ #Update the internal record of the TAP state.
105
+ @tap_state = new_state
106
+
107
+ end
108
+
109
+ #
110
+ # Transmit an instruction over the JTAG test access lines, to be placed into
111
+ # the JTAG instruction register.
112
+ #
113
+ # bytes: A byte-string which contains the instruction to be transmitted.
114
+ # bit_count: The total amount of bits to be transmitted from the byte string.
115
+ #
116
+ # pad_to_chain_length:
117
+ # If set, the transmitted data will be suffixed with logic '1's until the chain length has been met.
118
+ # This allows the transmitter to easily put devices to the "left" of afttarget device into bypass.
119
+ #
120
+ # prefix_with_ones:
121
+ # Prefixes the transmitted data with the specified amount of logic '1's. Prefixing is skipped if this parameter
122
+ # is not provided, or is set to zero. This allows the transmitter to easily put devices to the "right" of a
123
+ # target device into bypass.
124
+ #
125
+ # do_not_finish:
126
+ # If set, the device will be left in the ShiftIR state, so additional instructions data be transmitted.
127
+ #
128
+ def transmit_instruction(bytes, bit_count, pad_to_chain_length=false, prefix_with_ones=0, do_not_finish=false)
129
+
130
+ #If the pad-to-chain length option is selected, compute the total amount of padding required.
131
+ #Otherwise, set the required padding to zero.
132
+ padding_after = pad_to_chain_length ? [@chain_length - prefix_with_ones - bit_count, 0].max : 0
133
+
134
+ #Move to the Exit1IR state after transmission, allowing the recieved data to be processed,
135
+ #unless the do_not_finish value is set.
136
+ state_after = do_not_finish ? nil : TAPStates::Exit1IR
137
+
138
+ #Transmit the actual instruction.
139
+ transmit_in_state(TAPStates::ShiftIR, bytes, bit_count, state_after, true, prefix_with_ones, padding_after)
140
+
141
+ end
142
+
143
+ #
144
+ # Transmit data over the JTAG test access lines, to be placed into
145
+ # the JTAG data register.
146
+ #
147
+ # bytes: A byte-string which contains the instruction to be transmitted.
148
+ # bit_count: The total amount of bits to be transmitted from the byte string.
149
+ #
150
+ # pad_to_chain_length:
151
+ # If set, the transmitted data will be suffixed with logic '1's until the chain length has been met,
152
+ # *assuming that all devices other than the single target device are in bypass*.
153
+ # This allows the transmitter to easily fill the bypass registers of all additional devices with zeroes.
154
+ #
155
+ # prefix_with_zeroes:
156
+ # Prefixes the transmitted data with the specified amount of logic '0's. Prefixing is skipped if this parameter
157
+ # is not provided, or is set to zero.
158
+ #
159
+ # do_not_finish:
160
+ # If set, the device will be left in the ShiftIR state, so additional instructions data be transmitted.
161
+ #
162
+ def transmit_data(bytes, bit_count, pad_to_chain_length=false, prefix_with_zeroes=0, do_not_finish=false)
163
+
164
+ #If the pad-to-chain length option is selected, compute the total amount of padding required.
165
+ #Otherwise, set the required padding to zero.
166
+ padding_after = pad_to_chain_length ? [@devices_in_chain - prefix_with_zeroes - 1, 0].max : 0
167
+
168
+ #Move to the Exit1IR state after transmission, allowing the recieved data to be processed,
169
+ #unless the do_not_finish value is set.
170
+ state_after = do_not_finish ? nil : TAPStates::Exit1DR
171
+
172
+ #Transmit the actual instruction.
173
+ transmit_in_state(TAPStates::ShiftDR, bytes, bit_count, state_after, false, prefix_with_zeroes, padding_after)
174
+
175
+ end
176
+
177
+
178
+ #
179
+ # Recieve data from the JTAG data register.
180
+ #
181
+ # bit_count: The amount of bits to receive.
182
+ # do_not_finish: If set, the transmission will be "left open" so additional data can be received.
183
+ #
184
+ def receive_data(bit_count, do_not_finish=false, overlap=false)
185
+
186
+ #Put the device into the desired state.
187
+ self.tap_state = JTAG::TAPStates::ShiftDR
188
+
189
+ #Transmit the data, and recieve the accompanying response.
190
+ response = LowLevel::JTAG::receive(@device.handle, false, false, bit_count, overlap)
191
+
192
+ #If a state_after was provided, place the device into that state.
193
+ self.tap_state = JTAG::TAPStates::Exit1DR unless do_not_finish
194
+
195
+ #Return the received response.
196
+ response
197
+
198
+ end
199
+
200
+ #
201
+ # Switches to run/test mode, and holds that state for the desired amount of clock ticks.
202
+ #
203
+ def run_test(clock_ticks)
204
+
205
+ #Put the target into the Run-Test-Idle state.
206
+ self.tap_state = JTAG::TAPStates::Idle
207
+
208
+ #And "tick" the test clock for the desired amount of cycles.
209
+ LowLevel::JTAG::tick(@device.handle, false, false, clock_ticks, false)
210
+
211
+ end
212
+
213
+ #
214
+ # Force-resets the target device.
215
+ #
216
+ def reset_target
217
+
218
+ #Reset the target device's JTAG controller by sending five bits of TMS='1'.
219
+ LowLevel::JTAG::tick(@device.handle, true, false, 5)
220
+
221
+ #Set the internal TAP state to reset.
222
+ @tap_state = JTAG::TAPStates::Reset
223
+
224
+ end
225
+
226
+ #
227
+ # Registers a device type to be handled by JTAG connections, allowing JTAGDevice
228
+ # instances to be automatically created upon device enumeration.
229
+ #
230
+ # Device types typically are classes which include the JTAGDevice mixin,
231
+ #
232
+ #
233
+ def self.register_device_type(type)
234
+ @device_types << type
235
+ end
236
+
237
+ #
238
+ # Determines if the given device can serve as the host for a JTAG connection.
239
+ #
240
+ def self.supported_by?(device)
241
+ LowLevel::JTAG::supported?(device)
242
+ end
243
+
244
+ private
245
+
246
+ #
247
+ # Transmits a sequence of data while in a given state.
248
+ #
249
+ # bytes: A byte-string which contains the instruction to be transmitted;
250
+ # or a boolean value to send a single bit repeatedly.
251
+ # bit_count: The total amount of bits to be transmitted.
252
+ #
253
+ #
254
+ def transmit_in_state(state_before, value, bit_count, state_after=nil, pad_with=false, pad_before=0, pad_after=0)
255
+
256
+ #Put the device into the desired state.
257
+ self.tap_state = state_before
258
+
259
+ #If we've been instructed to pad before the transmission, do so.
260
+ LowLevel::JTAG::transmit(@device.handle, false, pad_with, pad_before) unless pad_before.zero?
261
+
262
+ #Transmit the data, and receive the accompanying response.
263
+ response = transmit_and_advance(false, value, bit_count, pad_after.zero?() ? state_after : nil)
264
+
265
+ #If we've been instructed to pad after the transmission, do so.
266
+ transmit_and_advance(false, pad_with, pad_after, state_after) unless pad_after.zero?
267
+
268
+ #If a state_after was provided, place the device into that state.
269
+ self.tap_state = state_after unless state_after.nil?
270
+
271
+ #Return the received response.
272
+ response
273
+
274
+ end
275
+
276
+ #
277
+ # Performs a data transmission, and advances TMS.
278
+ #
279
+ # The final TMS value is overlapped with the last bit, so the final shift and
280
+ # state change occur on the same clock edge.
281
+ #
282
+ # tms: The TMS value to use for all but the last bit of the transmission.
283
+ # tdi: The TDI values to transmit, in the same format accepted by the other transmit functions.
284
+ # bit_count: The total amount of bits to transmit.
285
+ # advance_towards:
286
+ # The state to advance towards after the transmission is complete. Used to
287
+ # determine the last value of TMS. If advance_towards is nil, the TAP state
288
+ # will not be advanced- this is useful for conditionally advancing the state.
289
+ #
290
+ def transmit_and_advance(tms, tdi, bit_count, advance_towards)
291
+
292
+ #If we don't have a state to advance towards, perform a normal transmit, and return.
293
+ unless advance_towards
294
+ return LowLevel::JTAG::transmit(@device.handle, tms, tdi, bit_count, false)
295
+ end
296
+
297
+ #If we were passed a byte array, pack it into a string
298
+ tdi = tdi.pack("C*") if tdi.respond_to?(:pack)
299
+
300
+ #Transmit all but the last bit of the TDI data.
301
+ main_response = LowLevel::JTAG::transmit(@device.handle, tms, tdi, bit_count - 1, false)
302
+
303
+ #Figure out what TMS should be during the last transmission by checking the path
304
+ #to the state we want to advance towards.
305
+ last_tms, next_state = @tap_state.next_hop_towards(advance_towards)
306
+
307
+ #Transmit the last bit of the TDI data, with the final TMS value.
308
+ last_tdi = bit_of_message(tdi, bit_count - 1)
309
+ last_response = LowLevel::JTAG::transmit(@device.handle, last_tms == 1, last_tdi, 1, false)
310
+
311
+ #Update the internal TAP fsm.
312
+ @tap_state = next_state
313
+
314
+ #Compose a single byte-string response by merging the response from the
315
+ #first and second transmissions.
316
+ return add_bit_to_message(main_response, bit_count - 1, last_response)
317
+
318
+ end
319
+
320
+ #
321
+ # Returns the appropriate bit of a given message.
322
+ #
323
+ def bit_of_message(string, bit_number)
324
+
325
+ #If were passed a non-unpackable element, return it directly.
326
+ return string unless string.respond_to?(:unpack)
327
+
328
+ #Break the message down into an array of bytes
329
+ bytes = string.unpack("B*").first
330
+
331
+ #... and return the requested bit, as a boolean.
332
+ bytes[-bit_number - 1] == "1"
333
+
334
+ end
335
+
336
+ #
337
+ # Appends a given bit to a message.
338
+ #
339
+ # message: The message to which the bit is to be appended, as a byte-string.
340
+ # message_length: The message's length, in bits.
341
+ # bit: The bit to be appended, as a boolean.
342
+ #
343
+ def add_bit_to_message(message, message_length, bit)
344
+
345
+ #If we don't have a message, return the bit as a string.
346
+ return bit ? "\x01" : "\x00" if message_length.zero?
347
+
348
+ #If we have something other than an integer, consider its truthiness
349
+ #in the same way that C would.
350
+ bit = bit.unpack("C*").first.nonzero? if bit.respond_to?(:unpack)
351
+
352
+ #Convert the message into a sequence of binary bits.
353
+ message = message.unpack("B*").first
354
+
355
+ #Ensure that the message is zero-padded to be at least the message length.
356
+ message = message.rjust(message_length, '0')
357
+
358
+ #Extract all of the bits up to the bit count, and add the new bit.
359
+ message = (bit ? '1' : '0') + message[-message_length..-1]
360
+
361
+ #Convert the message back into a packed string, and return it.
362
+ [message].pack("B*")
363
+
364
+ end
365
+
366
+ #
367
+ # Find the shortest "path" (sequence of most-select values) which will
368
+ # put the JTAG TAP FSM into the desired state.
369
+ #
370
+ # Note that the next-hop-towards algorithms do not consider "impossible"
371
+ # combinations, such as a jump from EXIT1DR to EXIT2DR; these may cause
372
+ # an infinite loop.
373
+ #
374
+ def path_to_state(destination, start=nil)
375
+
376
+ #Create a "state pointer", which will be used to trace the FSM in order to
377
+ #find a path to the destination state. If no start was provided, use the
378
+ #current TAP state.
379
+ state = start || @tap_state
380
+
381
+ path = ""
382
+
383
+ #Traverse the FSM until we reach our destination.
384
+ until state == destination
385
+
386
+ #Find the next hop on the path to the destination...
387
+ next_hop, _ = state.next_hop_towards(destination)
388
+
389
+ #Move the "state pointer" to the next state, simulating a traversal
390
+ #of the Finite State Machine.
391
+ state = state.next_state(next_hop)
392
+
393
+ #And add the hop to the path
394
+ path << next_hop.to_s
395
+
396
+ end
397
+
398
+ #And return the computed path.
399
+ path
400
+
401
+ end
402
+ end
403
+ end
404
+ end
@@ -0,0 +1,178 @@
1
+
2
+ module Adept
3
+ module JTAG
4
+
5
+ #
6
+ # Generic JTAG Device.
7
+ #
8
+ # This class primarily exists to serve as a base for custom JTAG devices,
9
+ # but also can be used to represent an unknown JTAG device.
10
+ #
11
+ class Device
12
+
13
+ attr_accessor :idcode
14
+
15
+ #Assume an instruction width of 4; the minimum possible insruction width.
16
+ #This should be re-defined in each inheriting class.
17
+ InstructionWidth = 4
18
+
19
+ # An internal list of device types which can be recognized
20
+ # on a JTAG bus.
21
+ @device_types = []
22
+
23
+ #
24
+ # Hook which is executed when a class inherits from the JTAG Device
25
+ # class. Registers the class as a Device Type provider, and sets up
26
+ # the device's basic metaprogramming abilities.
27
+ #
28
+ def self.inherited(klass)
29
+
30
+ #Register the class as device-type provider...
31
+ @device_types << klass
32
+
33
+ #And set up the "supports_idcode" metaprogramming facility.
34
+ klass.instance_variable_set(:@supported_idcodes, [])
35
+
36
+ end
37
+
38
+ #
39
+ # Factory method which creates a new Device whose type is determined
40
+ # by the provided IDCode.
41
+ #
42
+ # idcode: The IDCode of the new JTAG device.
43
+ # position_in_chain:
44
+ # The device's position in the chain. The first device to recieve data will have
45
+ # the highest device number. In a two device chain, Device 1 will recieve data before
46
+ # Device 0.
47
+ # chain_offset:
48
+ # The amount of bits which must be transmitted to other devices before an instruction
49
+ # can be transmitted to this device- equal to the amount of bits to the _right_ of the
50
+ # active device on the scan chain.
51
+ #
52
+ def self.from_idcode(idcode, connection, position_in_chain, chain_offset)
53
+
54
+ #Find the first device type which supports the IDCode.
55
+ device_type = @device_types.find { |type| type.supports?(idcode) }
56
+
57
+ #If we weren't able to find a device, use this class as a generic wrapper.
58
+ device_type ||= self
59
+
60
+ #Otherwise, instantiate tha new device from the device type.
61
+ device_type.new(idcode, connection, position_in_chain, chain_offset)
62
+
63
+ end
64
+
65
+ #
66
+ # Default implementation for detection of IDCode support.
67
+ # Checks to see if any of the IDcode matches any of this class's masks.
68
+ #
69
+ def self.supports?(idcode)
70
+ @supported_idcodes.any? { |mask| idcode_matches_mask(mask, idcode) }
71
+ end
72
+
73
+
74
+ #
75
+ # Returns the expected instruction width of the JTAG device.
76
+ #
77
+ # In this case, we don't know what the instruction width will be,
78
+ # so we'll assume the minimum possible width of four bits.
79
+ #
80
+ def instruction_width
81
+ return self.class::InstructionWidth
82
+ end
83
+
84
+ #
85
+ # idcode: The IDCode of the new JTAG device.
86
+ # scan_offset:
87
+ # The amount of bits which must be transmitted to other devices before an instruction
88
+ # can be transmitted to this device- equal to the amount of bits to the _right_ of the
89
+ # active device on the scan chain.
90
+ #
91
+ #
92
+ def initialize(idcode, connection, position_in_chain, chain_offset)
93
+ @idcode = idcode
94
+ @connection = connection
95
+ @position_in_chain = position_in_chain
96
+ @chain_offset = chain_offset
97
+ end
98
+
99
+ #
100
+ # Activate the device, and set its current operating instruction.
101
+ # All other devices in the scan chain are placed into BYPASS.
102
+ #
103
+ def instruction=(instruction)
104
+
105
+ #If we were provided an instruction name, look up the corresponding instruction.
106
+ instruction = self.class::Instructions[instruction] if instruction.kind_of?(Symbol)
107
+
108
+ #If we have a packable number, pack it into a byte-string.
109
+ instruction = [instruction].pack("C*") if instruction.kind_of?(Numeric)
110
+
111
+ #Transmit the instruction itself.
112
+ @connection.transmit_instruction(instruction, instruction_width, true, @chain_offset)
113
+ end
114
+
115
+ #
116
+ # TODO: Handle instruction readback, by rotating instructions through the device.?
117
+ #
118
+
119
+ #
120
+ # Send data directly to (and receive data directly from) the given device.
121
+ # Assumes the current device is active, and all other devices are in bypass.
122
+ #
123
+ def transmit_data(data, bit_count=data.length * 8)
124
+ @connection.transmit_data(data, bit_count, true, @position_in_chain)
125
+ end
126
+
127
+ #
128
+ # Recieves data directly from the given device by sending the device an
129
+ # appropriately-sized string of zeroes.
130
+ # Assumes the current device is active, and all other devices are in bypass.
131
+ #
132
+ #
133
+ def receive_data(bit_count)
134
+ @connection.transmit_data(false, bit_count, true, @position_in_chain, true)
135
+ end
136
+
137
+ #
138
+ # Allows the device to run its test operation for a certain amount of TCK cycles.
139
+ # (Delegates the run_test operation to the JTAG connection object, which is in charge
140
+ # of the TAP state.)
141
+ #
142
+ def run_test(cycles)
143
+ @connection.run_test(cycles)
144
+ end
145
+
146
+ private
147
+
148
+ #
149
+ # Metaprogramming routine which indicates that the class being defined
150
+ # supports an IDcode mask.
151
+ #
152
+ def self.supports_idcode(*idcodes)
153
+
154
+ #And merge them with the known supported IDcodes.
155
+ @supported_idcodes |= idcodes
156
+
157
+ end
158
+
159
+ #
160
+ # Determines if a given IDCode matches a hex mask.
161
+ #
162
+ def self.idcode_matches_mask(mask, idcode)
163
+
164
+ #Convert the IDcode into a string, for comparison.
165
+ idcode = idcode.unpack("H*").first.downcase
166
+
167
+ #Get a set of pairs containing the characters with the same position in each string.
168
+ character_pairs = mask.downcase.chars.zip(idcode.chars)
169
+
170
+ #And verify that each character is either a match, or a Don't Care.
171
+ character_pairs.all? { |m, i| m == 'x' || m == i }
172
+
173
+ end
174
+
175
+ end
176
+
177
+ end
178
+ end