libfst 0.1.0
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.
- checksums.yaml +7 -0
- data/.yardopts +11 -0
- data/LICENSE +674 -0
- data/README.md +133 -0
- data/ext/extconf.rb +5 -0
- data/ext/fastlz.c +549 -0
- data/ext/fastlz.h +109 -0
- data/ext/fst_config.h +11 -0
- data/ext/fst_win_unistd.h +52 -0
- data/ext/fstapi.c +7204 -0
- data/ext/fstapi.h +466 -0
- data/ext/libfst_rb.c +2525 -0
- data/ext/lz4.c +2789 -0
- data/ext/lz4.h +868 -0
- data/lib/libfst/reader.rb +345 -0
- data/lib/libfst/tfp.rb +112 -0
- data/lib/libfst/vcd.rb +597 -0
- data/lib/libfst/version.rb +4 -0
- data/lib/libfst/writer.rb +50 -0
- data/lib/libfst.rb +6 -0
- data/libfst.gemspec +50 -0
- data/samples/create_file.rb +69 -0
- data/samples/gtkwave.png +0 -0
- data/samples/out.gtkw +46 -0
- data/samples/read2.rb +8 -0
- data/samples/read_file.rb +8 -0
- data/samples/skinny_rand.fst +0 -0
- data/samples/transaction_filter_process/full_boot.fst +0 -0
- data/samples/transaction_filter_process/full_boot.gtkw +39 -0
- data/samples/transaction_filter_process/sdcard.rb +793 -0
- data/samples/transaction_filter_process/uart.rb +141 -0
- data/samples/vcd/skinny_rand.vcd.xz +0 -0
- data/samples/vcd/vcd_read.rb +34 -0
- metadata +72 -0
data/lib/libfst/vcd.rb
ADDED
@@ -0,0 +1,597 @@
|
|
1
|
+
# Copyright (C) 2024 Théotime Bollengier <theotime.bollengier@ensta-bretagne.fr>
|
2
|
+
#
|
3
|
+
# This file is part of libfst.rb <https://gitlab.ensta-bretagne.fr/bollenth/libfst.rb>
|
4
|
+
#
|
5
|
+
# libfst.rb is free software: you can redistribute it and/or modify it
|
6
|
+
# under the terms of the GNU General Public License as published
|
7
|
+
# by the Free Software Foundation, either version 3 of the License,
|
8
|
+
# or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# libfst.rb is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
13
|
+
# See the GNU General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU General Public License
|
16
|
+
# along with libfst.rb. If not, see <https://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
|
19
|
+
# Handle [Value Change Dump](https://en.wikipedia.org/wiki/Value_change_dump) files.
|
20
|
+
module VCD
|
21
|
+
# A Scope corresponds to a design element
|
22
|
+
class Scope
|
23
|
+
attr_reader :name # @return [String]
|
24
|
+
attr_reader :type # @return [Symbol] begin, fork, function, module or task
|
25
|
+
attr_reader :variables # @return [Array<Variable>]
|
26
|
+
attr_reader :children # @return [Array<Scope>]
|
27
|
+
attr_reader :parent # @return [Scope,nil]
|
28
|
+
|
29
|
+
# @!visibility private
|
30
|
+
def initialize(name, type, parent)
|
31
|
+
@name = name.to_s
|
32
|
+
@type = type.to_s.to_sym
|
33
|
+
@variables = []
|
34
|
+
@children = []
|
35
|
+
@parent = parent
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [String]
|
39
|
+
def to_s
|
40
|
+
"<#{self.class} @name=#{@name.inspect}, @type=#{@type.inspect}>"
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [String]
|
44
|
+
def path
|
45
|
+
return '/' if @parent.nil? # root
|
46
|
+
"#{@parent.path}#{@name}/"
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [String]
|
50
|
+
# @overload tree
|
51
|
+
def tree(prefix = '', last: true, first: true)
|
52
|
+
return @children.first.tree(prefix, last: last, first: first) if @parent.nil? && @variables.empty? && @children.length == 1
|
53
|
+
str = prefix + (first ? '' : (last ? '└─ ' : '├─ '))
|
54
|
+
str += "\e[34;1m#{@name}\e[0m (#{@type})\n"
|
55
|
+
prefix = prefix + (first ? '' : (last ? ' ' : '│ '))
|
56
|
+
mxvarnamelen = ([0] + @variables.collect{|v| v.name.length}).max
|
57
|
+
mxlenlen = ([0] + @variables.collect{|v| v.width == 0 ? 0 : v.width.to_s.length}).max
|
58
|
+
@variables.each_with_index do |v, i|
|
59
|
+
l = (@children.empty? and (i+1 == @variables.length))
|
60
|
+
str += prefix + (l ? '└─ ' : '├─ ') + v.name + ' '*(mxvarnamelen+1-v.name.length)
|
61
|
+
str += (v.width > 0 ? v.width.to_s : '').rjust(mxlenlen)
|
62
|
+
str += " #{v.type}\n"
|
63
|
+
end
|
64
|
+
@children.each_with_index do |c, i|
|
65
|
+
str += c.tree(prefix, last: (i+1 == @children.length), first: false)
|
66
|
+
end
|
67
|
+
str
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
# A Variable corresponds to a signal in the hierarchy of the design.
|
73
|
+
# It points to a {Trace}.
|
74
|
+
# Different variables can point to the same {Trace} : for example the clock signals from different modules
|
75
|
+
# are actualy the same physical signal.
|
76
|
+
class Variable
|
77
|
+
attr_reader :name # @return [String]
|
78
|
+
attr_reader :type # @return [Symbol] event, integer, parameter, real, realtime, reg, supply0, supply1, time, tri, triand, trior, trireg, tri0, tri1, wand, wire or wor
|
79
|
+
attr_reader :width # @return [Integer]
|
80
|
+
attr_reader :id # @return [String]
|
81
|
+
attr_reader :parent # @return [Scope]
|
82
|
+
attr_reader :trace # @return [Trace]
|
83
|
+
|
84
|
+
# @!visibility private
|
85
|
+
def initialize(name, type, width, id, parent)
|
86
|
+
@name = name.to_s
|
87
|
+
@type = type.to_s.to_sym
|
88
|
+
@width = width.to_i
|
89
|
+
@id = id.to_s
|
90
|
+
@parent = parent
|
91
|
+
end
|
92
|
+
|
93
|
+
# @return [String]
|
94
|
+
def to_s
|
95
|
+
"<#{self.class} @name=#{@name.inspect}, @type=#{@type.inspect}, @width=#{@width.inspect}, @id=#{@id.inspect}>"
|
96
|
+
end
|
97
|
+
|
98
|
+
# @return [String]
|
99
|
+
def path
|
100
|
+
"#{@parent.path}#{@name}"
|
101
|
+
end
|
102
|
+
|
103
|
+
# @!visibility private
|
104
|
+
def set_trace(trace)
|
105
|
+
raise "Trace allready set for variable #{path}" if @trace
|
106
|
+
@trace = trace
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
class Trace
|
112
|
+
attr_reader :type # @return [Symbol] event, integer, parameter, real, realtime, reg, supply0, supply1, time, tri, triand, trior, trireg, tri0, tri1, wand, wire or wor
|
113
|
+
attr_reader :width # @return [Integer]
|
114
|
+
attr_reader :variables # @return [Array<Variable>]
|
115
|
+
attr_reader :id # @return [String]
|
116
|
+
|
117
|
+
# @!visibility private
|
118
|
+
def initialize(type, width, variables, id)
|
119
|
+
@type = type.to_sym
|
120
|
+
@width = width.to_i
|
121
|
+
@variables = variables.freeze
|
122
|
+
@id = id.to_s
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
class Reader
|
128
|
+
attr_reader :date # @return [String,nil]
|
129
|
+
attr_reader :version # @return [String,nil]
|
130
|
+
attr_reader :comments # @return [Array<String>]
|
131
|
+
attr_reader :timescale # @return [Float]
|
132
|
+
attr_reader :root # @return [Scope]
|
133
|
+
attr_reader :traces # @return [Hash{String => Trace}] Traces stored by ID
|
134
|
+
|
135
|
+
|
136
|
+
# @param io [String,IO] either a file name or an opened IO.
|
137
|
+
def initialize(io)
|
138
|
+
@io_is_file = false
|
139
|
+
if io.is_a?(String) then
|
140
|
+
@io_is_file = true
|
141
|
+
@io = File.open(io)
|
142
|
+
else
|
143
|
+
@io = io
|
144
|
+
end
|
145
|
+
@buf = []
|
146
|
+
@root = Scope.new('root', :module, nil)
|
147
|
+
@scope_stack = [@root]
|
148
|
+
@state = :process_definitions
|
149
|
+
@comments = []
|
150
|
+
@stop_parsing = false
|
151
|
+
@io.each_line do |line|
|
152
|
+
process_line(line.chomp)
|
153
|
+
break if @stop_parsing
|
154
|
+
end
|
155
|
+
if @io_is_file then
|
156
|
+
@io.close
|
157
|
+
@io = io
|
158
|
+
end
|
159
|
+
|
160
|
+
remove_instance_variable :@stop_parsing
|
161
|
+
remove_instance_variable :@state
|
162
|
+
remove_instance_variable :@buf
|
163
|
+
remove_instance_variable :@scope_stack
|
164
|
+
|
165
|
+
@on_comment = nil
|
166
|
+
@on_time_change = nil
|
167
|
+
end
|
168
|
+
|
169
|
+
|
170
|
+
# Find a {Variable} by its name or path
|
171
|
+
# @param name [String] name or path to look for
|
172
|
+
# @return [Variable,nil]
|
173
|
+
def variable(name)
|
174
|
+
@traces.each_value do |t|
|
175
|
+
t.variables.each do |v|
|
176
|
+
return v if v.name == name or v.path == name
|
177
|
+
end
|
178
|
+
end
|
179
|
+
nil
|
180
|
+
end
|
181
|
+
|
182
|
+
|
183
|
+
# Returns the scope or variable corresponding to the specified path
|
184
|
+
# @param path [String]
|
185
|
+
# @return [Scope,Variable,nil]
|
186
|
+
def [](path)
|
187
|
+
return @root if path == '/'
|
188
|
+
raise ArgumentError, "expecting a String, not a #{path.class}" unless path.is_a?(String)
|
189
|
+
a = path.split('/').reject(&:empty?)
|
190
|
+
return nil if a.empty?
|
191
|
+
scope = @root
|
192
|
+
var = nil
|
193
|
+
a.each do |w|
|
194
|
+
var = scope.variables.select{ |v| v.name == w }.first
|
195
|
+
return var if var
|
196
|
+
s = scope.children.select{ |v| v.name == w }.first
|
197
|
+
return nil if s.nil?
|
198
|
+
scope = s
|
199
|
+
end
|
200
|
+
scope
|
201
|
+
end
|
202
|
+
|
203
|
+
|
204
|
+
# Set the block of code to be executed when a comment is found while reading the value change section of the file
|
205
|
+
# @return [self]
|
206
|
+
# @yieldparam comment [String]
|
207
|
+
# @yieldparam time [Integer] this integer is to be multiplied with {#timescale} the get the real time
|
208
|
+
# @yieldparam vcd [self]
|
209
|
+
def on_comment(&block)
|
210
|
+
@on_comment = block
|
211
|
+
self
|
212
|
+
end
|
213
|
+
|
214
|
+
|
215
|
+
# Set the block of code to be called when a time change is encountered.
|
216
|
+
# @return [self]
|
217
|
+
# @yieldparam time [Integer] this integer is to be multiplied with {#timescale} the get the real time
|
218
|
+
# @yieldparam vcd [self]
|
219
|
+
def on_time_change(&block)
|
220
|
+
@on_time_change = block
|
221
|
+
self
|
222
|
+
end
|
223
|
+
|
224
|
+
|
225
|
+
# Stop the parsing of the file.
|
226
|
+
# This method can be called from an {on_comment} or {read} block.
|
227
|
+
# @return [self]
|
228
|
+
def stop_parsing
|
229
|
+
@stop_parsing = true
|
230
|
+
self
|
231
|
+
end
|
232
|
+
|
233
|
+
|
234
|
+
# Read the value change section of the VCD file.
|
235
|
+
# @return [self]
|
236
|
+
# @param signals [Trace,Variable,Scope,String] if signals are specified, only those will trigger the block.
|
237
|
+
# A path can be provided as a string, which correspond to a {Variable} or a {Scope}.
|
238
|
+
# In the case of a {Scope}, all its variables will be added (NOT recursively).
|
239
|
+
# @param start_time [Integer,Float,String,nil] start time from which to trigger the block
|
240
|
+
# @param end_time [Integer,Float,String,nil] end time until which to trigger the block
|
241
|
+
# @yieldparam trace [Trace]
|
242
|
+
# @yieldparam value [String,Float]
|
243
|
+
# @yieldparam time [Integer] this integer is to be multiplied with {#timescale} the get the real time
|
244
|
+
# @yieldparam vcd [self]
|
245
|
+
def read(*signals, start_time: nil, end_time: nil, &block)
|
246
|
+
@time_win_start = nil
|
247
|
+
@time_win_end = nil
|
248
|
+
@trace_mask = nil
|
249
|
+
|
250
|
+
@time_win_start = time_to_itime(start_time, false) if start_time
|
251
|
+
@time_win_end = time_to_itime(end_time, true) if end_time
|
252
|
+
|
253
|
+
unless signals.empty? then
|
254
|
+
@trace_mask = {}
|
255
|
+
signals.each do |sig|
|
256
|
+
if sig.is_a?(String) then
|
257
|
+
s = self[sig]
|
258
|
+
raise "Cannot find #{sig.inspect} in the hierarchy" if s.nil?
|
259
|
+
sig = s
|
260
|
+
end
|
261
|
+
case sig
|
262
|
+
when Variable
|
263
|
+
@trace_mask[sig.trace.id] = true
|
264
|
+
when Trace
|
265
|
+
@trace_mask[sig.id] = true
|
266
|
+
when Scope
|
267
|
+
sig.variables.each do |var|
|
268
|
+
@trace_mask[var.trace.id] = true
|
269
|
+
end
|
270
|
+
else
|
271
|
+
raise TypeError, "Expecting a path, a #{Variable}, a #{Trace} or a #{Scope}, not a #{sig.class}"
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
@on_read = block
|
277
|
+
|
278
|
+
if @io_is_file then
|
279
|
+
io = @io
|
280
|
+
@io = File.open(io)
|
281
|
+
@io.seek(@value_change_pos, IO::SEEK_SET)
|
282
|
+
end
|
283
|
+
|
284
|
+
@stop_parsing = false
|
285
|
+
@state = nil
|
286
|
+
@buf = nil
|
287
|
+
@time = 0
|
288
|
+
|
289
|
+
@io.each_line do |line|
|
290
|
+
process_line(line.chomp)
|
291
|
+
break if @stop_parsing
|
292
|
+
end
|
293
|
+
|
294
|
+
remove_instance_variable :@time_win_start
|
295
|
+
remove_instance_variable :@time_win_end
|
296
|
+
remove_instance_variable :@trace_mask
|
297
|
+
remove_instance_variable :@on_read
|
298
|
+
remove_instance_variable :@stop_parsing
|
299
|
+
remove_instance_variable :@state
|
300
|
+
remove_instance_variable :@buf
|
301
|
+
|
302
|
+
if @io_is_file then
|
303
|
+
@io.close
|
304
|
+
@io = io
|
305
|
+
end
|
306
|
+
|
307
|
+
@time = nil
|
308
|
+
end
|
309
|
+
|
310
|
+
|
311
|
+
# When called in one of the callbacks, returns the current time as an integer, `nil` otherwise.
|
312
|
+
# @return [Integer,nil] this value is to be multiplied with {#timescale} the get the real time
|
313
|
+
def time
|
314
|
+
@time
|
315
|
+
end
|
316
|
+
|
317
|
+
|
318
|
+
# When called in one of the callbacks, returns the current time in seconds, `nil` otherwise.
|
319
|
+
# @return [Float,nil]
|
320
|
+
def real_time
|
321
|
+
return nil unless @time
|
322
|
+
@time*@timescale
|
323
|
+
end
|
324
|
+
|
325
|
+
|
326
|
+
# When called in one of the callbacks, returns the current time as a string, `nil` otherwise.
|
327
|
+
# @param t [Integer,nil]
|
328
|
+
# @return [String,nil] example: `"2.57 µs"`
|
329
|
+
def pretty_time(t = nil)
|
330
|
+
t ||= @time
|
331
|
+
return nil unless t
|
332
|
+
return '0 s' if t == 0
|
333
|
+
e = Math.log10(@timescale).round
|
334
|
+
while (-e % 3) != 0 do
|
335
|
+
t *= 10
|
336
|
+
e -= 1
|
337
|
+
end
|
338
|
+
while e < -15 do
|
339
|
+
t /= 10
|
340
|
+
e += 1
|
341
|
+
end
|
342
|
+
|
343
|
+
while e < 0 and (t % 1000) == 0 do
|
344
|
+
t /= 1000
|
345
|
+
e += 3
|
346
|
+
end
|
347
|
+
|
348
|
+
ent = t
|
349
|
+
qe = e
|
350
|
+
mf = 1
|
351
|
+
while qe < 0 and (ent / 1000) != 0 do
|
352
|
+
ent /= 1000
|
353
|
+
qe += 3
|
354
|
+
mf *= 1000
|
355
|
+
end
|
356
|
+
|
357
|
+
rem = t - mf*ent
|
358
|
+
while rem != 0 and (rem % 10) == 0 do
|
359
|
+
rem /= 10
|
360
|
+
e += 1
|
361
|
+
end
|
362
|
+
|
363
|
+
s = ent.to_s
|
364
|
+
if rem != 0 then
|
365
|
+
s += '.' + rem.to_s.rjust(qe - e, '0')
|
366
|
+
end
|
367
|
+
s += case qe
|
368
|
+
when 0; ' s'
|
369
|
+
when -3; ' ms'
|
370
|
+
when -6; ' µs'
|
371
|
+
when -9; ' ns'
|
372
|
+
when -12; ' ps'
|
373
|
+
when -15; ' fs'
|
374
|
+
else
|
375
|
+
raise "unknown unit for exponent #{qe}"
|
376
|
+
end
|
377
|
+
s
|
378
|
+
end
|
379
|
+
|
380
|
+
|
381
|
+
# @return [String]
|
382
|
+
def to_s
|
383
|
+
[
|
384
|
+
"Date: #{@date.inspect}",
|
385
|
+
"Version: #{@version.inspect}",
|
386
|
+
"Timescale: #{@timescale}",
|
387
|
+
"Comments:",
|
388
|
+
@comments.collect{|s| " " + s.inspect},
|
389
|
+
@root.tree
|
390
|
+
].flatten.join("\n")
|
391
|
+
end
|
392
|
+
|
393
|
+
|
394
|
+
private
|
395
|
+
|
396
|
+
|
397
|
+
def process_line(line)
|
398
|
+
line.split(/\s+/).reject{|s| s.empty?}.each do |word|
|
399
|
+
process_word(word)
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
|
404
|
+
def process_word(word)
|
405
|
+
case @state
|
406
|
+
when :process_definitions
|
407
|
+
@buf << word
|
408
|
+
process_buffer if word == '$end'
|
409
|
+
when :get_bin_vector_vc
|
410
|
+
on_value_change_callback(word, @buf)
|
411
|
+
@state = nil
|
412
|
+
when :get_real_vc
|
413
|
+
on_value_change_callback(word, @buf)
|
414
|
+
@state = nil
|
415
|
+
when :get_string_vc
|
416
|
+
on_value_change_callback(word, @buf)
|
417
|
+
@state = nil
|
418
|
+
when :in_comment
|
419
|
+
if word == '$end' then
|
420
|
+
cmnt = @buf.join(' ')
|
421
|
+
@comments.push(cmnt)
|
422
|
+
@state = nil
|
423
|
+
on_comment_callback(cmnt)
|
424
|
+
else
|
425
|
+
@buf.push(word)
|
426
|
+
end
|
427
|
+
else
|
428
|
+
case word
|
429
|
+
when /^#(\d+)$/ # simulation time
|
430
|
+
@time = $1.to_i
|
431
|
+
on_time_change_callback
|
432
|
+
when /^([01xXzZuUwWlLhH-])([!-~]+)$/ # scalar value change
|
433
|
+
on_value_change_callback($2, $1)
|
434
|
+
when /^[bB]([01xXzZuUwWlLhH-]+)$/ # binary vector value change
|
435
|
+
@buf = $1
|
436
|
+
@state = :get_bin_vector_vc
|
437
|
+
when /^[sS](.+)$/
|
438
|
+
@buf = $1
|
439
|
+
@state = :get_string_vc
|
440
|
+
when /^[rR]([0-9.+eE-]+)$/ # real value change
|
441
|
+
@state = :get_real_vc
|
442
|
+
@buf = $1.to_f
|
443
|
+
when '$comment'
|
444
|
+
@buf = []
|
445
|
+
@state = :in_comment
|
446
|
+
when '$end'
|
447
|
+
@state = nil
|
448
|
+
when '$dumpall'
|
449
|
+
raise 'intricated simulation commands' if @state
|
450
|
+
@state = :in_dumpall
|
451
|
+
when '$dumpoff'
|
452
|
+
raise 'intricated simulation commands' if @state
|
453
|
+
@state = :in_dumpoff
|
454
|
+
when '$dumpon'
|
455
|
+
raise 'intricated simulation commands' if @state
|
456
|
+
@state = :in_dumpon
|
457
|
+
when '$dumpvars'
|
458
|
+
raise 'intricated simulation commands' if @state
|
459
|
+
@state = :in_dumpvars
|
460
|
+
else
|
461
|
+
raise "unexpected \"#{word}\""
|
462
|
+
end
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
|
467
|
+
def process_buffer
|
468
|
+
case @buf.first
|
469
|
+
when '$scope'
|
470
|
+
scope = Scope.new(@buf[2], @buf[1], @scope_stack.last)
|
471
|
+
@scope_stack.last.children.push(scope)
|
472
|
+
@scope_stack.push(scope)
|
473
|
+
when '$var'
|
474
|
+
var = Variable.new(@buf[4], @buf[1], @buf[2], @buf[3], @scope_stack.last)
|
475
|
+
@scope_stack.last.variables.push(var)
|
476
|
+
when '$upscope'
|
477
|
+
raise 'VCD hierarchy error: poping root scope with $upscope' if @scope_stack.pop == @root
|
478
|
+
when '$date'
|
479
|
+
if @date.nil? then
|
480
|
+
@date = ''
|
481
|
+
else
|
482
|
+
@date += ' '
|
483
|
+
end
|
484
|
+
@date += @buf[1...-1].join(' ')
|
485
|
+
when '$version'
|
486
|
+
if @version.nil? then
|
487
|
+
@version = ''
|
488
|
+
else
|
489
|
+
@version += ' '
|
490
|
+
end
|
491
|
+
@version += @buf[1...-1].join(' ')
|
492
|
+
when '$comment'
|
493
|
+
@comments.push(@buf[1...-1].join(' '))
|
494
|
+
when '$attrbegin'
|
495
|
+
## Ignore $attrbegin generated by nvc
|
496
|
+
when '$timescale'
|
497
|
+
s = @buf[1...-1].join
|
498
|
+
m = s.match(/^(\d+)([munpf]?)s$/)
|
499
|
+
raise "Cannot parse timescale: \"#{s}\"" unless m
|
500
|
+
@timescale = m[1].to_i * { '' => 1.0, 'm' => 1e-3, 'u' => 1e-6, 'n' => 1e-9, 'p' => 1e-12, 'f' => 1e-15 }[m[2]]
|
501
|
+
when '$enddefinitions'
|
502
|
+
@state = nil
|
503
|
+
gather_signals
|
504
|
+
@value_change_pos = @io.tell if @io_is_file
|
505
|
+
@stop_parsing = true
|
506
|
+
else
|
507
|
+
raise "VCD parsing ERROR: unexpected tocken: \"#{@buf.first}\""
|
508
|
+
end
|
509
|
+
@buf = []
|
510
|
+
end
|
511
|
+
|
512
|
+
|
513
|
+
def gather_variables(id_vararr, scope)
|
514
|
+
scope.variables.each do |var|
|
515
|
+
arr = id_vararr[var.id]
|
516
|
+
unless arr then
|
517
|
+
arr = []
|
518
|
+
id_vararr[var.id] = arr
|
519
|
+
end
|
520
|
+
arr.push(var)
|
521
|
+
end
|
522
|
+
scope.children.each do |subscope|
|
523
|
+
gather_variables(id_vararr, subscope)
|
524
|
+
end
|
525
|
+
end
|
526
|
+
|
527
|
+
|
528
|
+
def gather_signals
|
529
|
+
@traces = {}
|
530
|
+
variables = {}
|
531
|
+
gather_variables(variables, @root)
|
532
|
+
variables.each do |k, v|
|
533
|
+
type = v.first.type
|
534
|
+
width = v.first.width
|
535
|
+
trace = Trace.new(type, width, v, k)
|
536
|
+
@traces[k] = trace
|
537
|
+
v.each do |var|
|
538
|
+
raise "trace with variables of different types (#{type}, #{var.type})" unless var.type == type
|
539
|
+
raise "trace with variables of different width (#{width}, #{var.width})" unless var.width == width
|
540
|
+
var.set_trace(trace)
|
541
|
+
end
|
542
|
+
end
|
543
|
+
end
|
544
|
+
|
545
|
+
|
546
|
+
def on_time_change_callback
|
547
|
+
return unless @on_time_change
|
548
|
+
return if @time_win_start && @time < @time_win_start
|
549
|
+
return if @time_win_end && @time > @time_win_end
|
550
|
+
@on_time_change.call(@time, self)
|
551
|
+
end
|
552
|
+
|
553
|
+
|
554
|
+
def on_comment_callback(cmnt)
|
555
|
+
return unless @on_comment
|
556
|
+
return if @time_win_start && @time < @time_win_start
|
557
|
+
return if @time_win_end && @time > @time_win_end
|
558
|
+
@on_comment.call(cmnt, @time, self)
|
559
|
+
end
|
560
|
+
|
561
|
+
|
562
|
+
def on_value_change_callback(id, value)
|
563
|
+
return unless @on_read
|
564
|
+
return if @time_win_start && @time < @time_win_start
|
565
|
+
return if @time_win_end && @time > @time_win_end
|
566
|
+
return if @trace_mask && @trace_mask[id].nil?
|
567
|
+
@on_read.call(@traces[id], value, @time, self)
|
568
|
+
end
|
569
|
+
|
570
|
+
|
571
|
+
def time_to_itime(t, up = false)
|
572
|
+
u = case t
|
573
|
+
when Integer
|
574
|
+
t
|
575
|
+
when Float
|
576
|
+
if up then
|
577
|
+
(t / @timescale).ceil
|
578
|
+
else
|
579
|
+
(t / @timescale).floor
|
580
|
+
end
|
581
|
+
when String
|
582
|
+
m = t.match(/^([\d.eE+-]+)\s*([muµnpf]?)s?$/)
|
583
|
+
raise "cannot parse \"#{t}\" as a time" unless m
|
584
|
+
t = m[1].to_f
|
585
|
+
e = { '' => 1.0, 'm' => 1e-3, 'u' => 1e-6, 'µ' => 1e-6, 'n' => 1e-9, 'p' => 1e-12, 'f' => 1e-15 }[m[2]]
|
586
|
+
t *= e
|
587
|
+
t /= @timescale
|
588
|
+
up ? t.ceil : t.floor
|
589
|
+
else
|
590
|
+
raise "expecting an integer, a float or a string, not a #{t.class}"
|
591
|
+
end
|
592
|
+
[0, u].max
|
593
|
+
end
|
594
|
+
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# Copyright (C) 2024 Théotime Bollengier <theotime.bollengier@ensta-bretagne.fr>
|
2
|
+
#
|
3
|
+
# This file is part of libfst.rb <https://gitlab.ensta-bretagne.fr/bollenth/libfst.rb>
|
4
|
+
#
|
5
|
+
# libfst.rb is free software: you can redistribute it and/or modify it
|
6
|
+
# under the terms of the GNU General Public License as published
|
7
|
+
# by the Free Software Foundation, either version 3 of the License,
|
8
|
+
# or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# libfst.rb is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
13
|
+
# See the GNU General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU General Public License
|
16
|
+
# along with libfst.rb. If not, see <https://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
|
19
|
+
module LibFST
|
20
|
+
class Writer
|
21
|
+
# Helper to create a variable of type `:vcd_real`
|
22
|
+
# @see create_variable
|
23
|
+
# @return [Variable]
|
24
|
+
def create_float_variable(name, direction: :implicit)
|
25
|
+
create_variable(name, type: :vcd_real, direction: direction)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Helper to create a 32-bit variable of type `:vcd_integer`
|
29
|
+
# @see create_variable
|
30
|
+
# @return [Variable]
|
31
|
+
def create_integer_variable(name, direction: :implicit)
|
32
|
+
create_variable(name, type: :vcd_integer, direction: direction, length: 32)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Helper to create a 64-bit variable of type `:vcd_integer`
|
36
|
+
# @see create_variable
|
37
|
+
# @return [Variable]
|
38
|
+
def create_integer64_variable(name, direction: :implicit)
|
39
|
+
create_variable(name, type: :vcd_integer, direction: direction, length: 64)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Helper to create a variable of type `:sv_logic`
|
43
|
+
# @see create_variable
|
44
|
+
# @return [Variable]
|
45
|
+
def create_logic_variable(name , direction: :implicit, length: 1)
|
46
|
+
create_variable(name, type: :sv_logic, direction: direction, length: length)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
data/lib/libfst.rb
ADDED
data/libfst.gemspec
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require File.expand_path('../lib/libfst/version.rb', __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'libfst'
|
5
|
+
s.version = LibFST::VERSION
|
6
|
+
s.date = Time.now.strftime '%Y-%m-%d'
|
7
|
+
s.summary = 'Fast Signal Trace bindings for Ruby'
|
8
|
+
s.description = 'fstlib allows to read and write FST files (Fast Signal Trace) from Ruby'
|
9
|
+
s.license = 'GPL-3.0-or-later'
|
10
|
+
s.authors = ['Théotime Bollengier']
|
11
|
+
s.email = 'theotime.bollengier@ensta-bretagne.fr'
|
12
|
+
s.homepage = 'https://gitlab.ensta-bretagne.fr/bollenth/fstlib.rb'
|
13
|
+
s.extensions = ['ext/extconf.rb']
|
14
|
+
s.files = [
|
15
|
+
'LICENSE',
|
16
|
+
'README.md',
|
17
|
+
'libfst.gemspec',
|
18
|
+
'lib/libfst.rb',
|
19
|
+
'lib/libfst/version.rb',
|
20
|
+
'lib/libfst/reader.rb',
|
21
|
+
'lib/libfst/writer.rb',
|
22
|
+
'lib/libfst/vcd.rb',
|
23
|
+
'lib/libfst/tfp.rb',
|
24
|
+
'ext/extconf.rb',
|
25
|
+
'ext/libfst_rb.c',
|
26
|
+
'ext/fstapi.c',
|
27
|
+
'ext/fastlz.c',
|
28
|
+
'ext/lz4.c',
|
29
|
+
'ext/fst_config.h',
|
30
|
+
'ext/lz4.h',
|
31
|
+
'ext/fstapi.h',
|
32
|
+
'ext/fastlz.h',
|
33
|
+
'ext/fst_win_unistd.h',
|
34
|
+
'samples/create_file.rb',
|
35
|
+
'samples/read_file.rb',
|
36
|
+
'samples/read2.rb',
|
37
|
+
'samples/skinny_rand.fst',
|
38
|
+
'samples/gtkwave.png',
|
39
|
+
'samples/out.gtkw',
|
40
|
+
'samples/vcd/skinny_rand.vcd.xz',
|
41
|
+
'samples/vcd/vcd_read.rb',
|
42
|
+
'samples/transaction_filter_process/full_boot.fst',
|
43
|
+
'samples/transaction_filter_process/full_boot.gtkw',
|
44
|
+
'samples/transaction_filter_process/uart.rb',
|
45
|
+
'samples/transaction_filter_process/sdcard.rb',
|
46
|
+
'.yardopts'
|
47
|
+
]
|
48
|
+
s.required_ruby_version = '>= 2.7'
|
49
|
+
end
|
50
|
+
|