module CrossOrigen
class IpXact < XMLDoc
AddressSpace = Struct.new(:name, :range, :width)
MemoryMaps = Struct.new(:name, :address_blocks)
AddressBlock = Struct.new(:name, :base_address, :range, :width)
# Create a shorthand way to reference Origen Core's Bit ACCESS_CODES
@@access_hash = Origen::Registers::Bit.const_get(:ACCESS_CODES)
# Import/reader that currently only supports creating registers and bit fields
def import(file, options = {}) # rubocop:disable CyclomaticComplexity
require 'kramdown'
filename = Pathname.new(file).basename('.*').to_s
unless options[:refresh] || CrossOrigen.refresh?
return if owner.import(filename, allow_missing: true)
end
model = CrossOrigen::Model.new
address_spaces = {}
doc(file, options) do |doc|
doc.xpath('//spirit:addressSpaces/spirit:addressSpace').each do |addr_space|
name = fetch addr_space.at_xpath('spirit:name'), downcase: true, to_sym: true, get_text: true
range = fetch addr_space.at_xpath('spirit:range'), get_text: true, to_dec: true
width = fetch addr_space.at_xpath('spirit:width'), get_text: true, to_i: true
address_spaces[name] = AddressSpace.new(name, range, width)
end
open_memory_map(doc) do |mem_map|
if mem_map
mem_map_name = fetch mem_map.at_xpath('spirit:name'), downcase: true, to_sym: true, get_text: true
if mem_map_name.to_s.empty?
mem_map_obj = model
else
model.sub_block mem_map_name
mem_map_obj = model.send(mem_map_name)
end
addr_blocks = mem_map.xpath('spirit:addressBlock')
else
mem_map_obj = model
addr_blocks = doc.xpath('//spirit:addressBlock')
end
addr_blocks.each do |addr_block|
name = fetch addr_block.at_xpath('spirit:name'), downcase: true, to_sym: true, get_text: true
base_address = fetch addr_block.at_xpath('spirit:baseAddress'), get_text: true, to_dec: true
range = fetch addr_block.at_xpath('spirit:range'), get_text: true, to_dec: true
width = fetch addr_block.at_xpath('spirit:width'), get_text: true, to_i: true
if name.to_s.empty?
addr_block_obj = mem_map_obj
else
mem_map_obj.sub_block name, base_address: base_address, range: range, lau: width
addr_block_obj = mem_map_obj.send(name)
end
addr_block.xpath('spirit:register').each do |register|
name = fetch register.at_xpath('spirit:name'), downcase: true, to_sym: true, get_text: true
size = fetch register.at_xpath('spirit:size'), get_text: true, to_i: true
addr_offset = fetch register.at_xpath('spirit:addressOffset'), get_text: true, to_dec: true
access = fetch register.at_xpath('spirit:access'), get_text: true
# Determine if a reset is defined for the register
if register.at_xpath('spirit:reset').nil?
# If a reset does not exist, need to set the reset_value to 0, as Origen does not (yet) have a concept
# of a register without a reset.
reset_value = 0
else
# If a reset exists, determine the reset_value (required) and reset_mask (if defined)
reset_value = fetch register.at_xpath('spirit:reset/spirit:value'), get_text: true, to_dec: true
reset_mask = fetch register.at_xpath('spirit:reset/spirit:mask'), get_text: true, to_dec: true
# Issue #8 fix - reset_mask is optional, keep reset value as imported when a mask is not defined.
# Only perform AND-ing if mask is defined. Only zero-out the reset_value if reset_value was nil.
if reset_value.nil?
# Set default for reset_value attribute if none was provided and issue a warning.
reset_value = 0
Origen.log.warning "Register #{name.upcase} was defined as having a reset, but did not have a defined reset value. This is not compliant with IP-XACT standard."
Origen.log.warning "The reset value for #{name.upcase} has been defined as 0x0 as a result."
elsif reset_mask.nil?
# If mask is undefined, leave reset_value alone.
else
# Do a logical bitwise AND with the reset value and mask
reset_value = reset_value & reset_mask
end
end
# Future expansion: pull in HDL path as abs_path in Origen.
addr_block_obj.reg name, addr_offset, size: size, access: access, description: reg_description(register) do |reg|
register.xpath('spirit:field').each do |field|
name = fetch field.at_xpath('spirit:name'), downcase: true, to_sym: true, get_text: true
bit_offset = fetch field.at_xpath('spirit:bitOffset'), get_text: true, to_i: true
bit_width = fetch field.at_xpath('spirit:bitWidth'), get_text: true, to_i: true
xml_access = fetch field.at_xpath('spirit:access'), get_text: true
# Newer IP-XACT standards list access as < read or write>-< descriptor >, such as
# "read-write", "read-only", or "read-writeOnce"
if xml_access =~ /\S+\-\S+/ || xml_access == 'writeOnce'
# This filter alone is not capable of interpreting the 1685-2009 (and 2014). Therefore
# must reverse-interpret the content of access_hash (see top of file).
#
# First get the base access type, ie: read-write, read-only, etc.
# base_access = fetch field.at_xpath('spirit:access'), get_text: true
base_access = xml_access
# Next grab any modified write values or read actions
mod_write = fetch field.at_xpath('spirit:modifiedWriteValue'), get_text: true
read_action = fetch field.at_xpath('spirit:readAction'), get_text: true
# Using base_access, mod_write, and read_action, look up the corresponding access
# acronym from access_hash, noting it is not possible to differentiate write-only
# from write-only, read zero and read-write from dc.
#
# Matched needs to be tracked, as there is no way to differentiate :rw and :dc in IP-XACT.
# Everything imported will default to :rw, never :dc.
matched = false
@@access_hash.each_key do |key|
if @@access_hash[key][:base] == base_access && @@access_hash[key][:write] == mod_write && @@access_hash[key][:read] == read_action && !matched
access = key.to_sym
matched = true
end
end
# Older IP-XACT standards appear to also accept short acronyms like "ro", "w1c", "rw",
# etc.
elsif xml_access =~ /\S+/
access = xml_access.downcase.to_sym
else
# default to read-write if access is not specified
access = :rw
end
range = nil
if bit_width == 1
range = bit_offset
else
range = (bit_offset + bit_width - 1)..bit_offset
end
reg.bit range, name, reset: reset_value[range], access: access, description: bit_description(field)
end
end
end
end
end
end
model.export(filename, include_timestamp: CrossOrigen.include_timestamp?)
owner.import(filename, options)
end
def doc(path, options = {})
# If a fragment of IP-XACT is given, then wrap it with a valid header and we will try our best
if options[:fragment]
require 'nokogiri'
content = %(
#{File.read(path)}
)
yield Nokogiri::XML(content)
else
super
end
end
# Returns a string representing the owner object in IP-XACT XML
# Usable / Available options:
# :vendor = Company name/web address, ex: 'nxp.com'
# :library = IP Library
# :schema = '1685-2009' or default of Spirit 1.4 (when no :schema option passed)
# :bus_interface = only 'AMBA3' supported at this time
# :mmap_name = Optionally set the memoryMap name to something other than the module name
# :mmap_ref = memoryMapRef name, ex: 'UserMap'
# :addr_block_name = addressBlock -> Name, ex: 'ATX'
def owner_to_xml(options = {})
require 'nokogiri'
options = {
include_bit_field_values: true
}.merge(options)
@format = options[:format]
# Compatible schemas: Spirit 1.4, 1685-2009
# Assume Spirit 1.4 if no schema provided
if options[:schema] == '1685-2009' # Magillem tool uses alternate schema
schemas = [
'http://www.spiritconsortium.org/XMLSchema/SPIRIT/1685-2009',
'http://www.spiritconsortium.org/XMLSchema/SPIRIT/1685-2009/index.xsd'
]
else # Assume Spirit 1.4 if not
schemas = [
'http://www.spiritconsortium.org/XMLSchema/SPIRIT/1.4',
'http://www.spiritconsortium.org/XMLSchema/SPIRIT/1.4/index.xsd'
]
end
if uvm? && !(options[:schema] == '1685-2009')
schemas << '$IREG_GEN/XMLSchema/SPIRIT/VendorExtensions.xsd'
end
if options[:schema] == '1685-2009' # Magillem tool uses alternate schema
headers = {
'xmlns:spirit' => 'http://www.spiritconsortium.org/XMLSchema/SPIRIT/1685-2009',
'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
'xsi:schemaLocation' => schemas.join(' ')
}
else # Assume Spirit 1.4 if not
headers = {
'xmlns:spirit' => 'http://www.spiritconsortium.org/XMLSchema/SPIRIT/1.4',
'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
'xsi:schemaLocation' => schemas.join(' ')
}
end
if uvm? && !(options[:schema] == '1685-2009')
headers['xmlns:vendorExtensions'] = '$IREG_GEN/XMLSchema/SPIRIT'
# Else:
# Do nothing ?
# headers['xmlns:vendorExtensions'] = '$UVM_RGM_HOME/builder/ipxact/schema'
end
builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
spirit = xml['spirit']
spirit.component(headers) do
spirit.vendor options[:vendor] || 'Origen'
spirit.library options[:library] || 'Origen'
# I guess this should really be the register owner's owner's name?
spirit.name try(:ip_name) || owner.class.to_s.split('::').last
spirit.version try(:ip_version, :version, :revision)
# The 1685-2009 schema allows for a bus interface. AMBA3 (slave) supported so far.
if options[:schema] == '1685-2009'
if options[:bus_interface] == 'AMBA3'
spirit.busInterfaces do
spirit.busInterface do
spirit.name 'Slave'
bustype_header = {
'spirit:vendor' => options[:vendor] || 'Origen',
'spirit:library' => 'amba3',
'spirit:name' => 'APB3',
'spirit:version' => '1.0'
}
xml['spirit'].busType bustype_header
spirit.slave do
mmapref_header = {
'spirit:memoryMapRef' => options[:mmap_ref] || 'APB'
}
xml['spirit'].memoryMapRef mmapref_header
end
end
end
end
end
spirit.memoryMaps do
memory_maps.each do |map_name, _map|
spirit.memoryMap do
# Optionally assign memory map name to something other than the module name in Ruby,
# default to 'RegisterMap'
spirit.name options[:mmap_name] || 'RegisterMap'
address_blocks do |domain_name, _domain, sub_block|
spirit.addressBlock do
# When registers reside at the top level, do not assign an address block name
if sub_block == owner
if options[:addr_block_name].nil?
spirit.name nil
else
spirit.name options[:addr_block_name]
end
else
spirit.name address_block_name(domain_name, sub_block)
end
spirit.baseAddress sub_block.base_address.to_hex
spirit.range range(sub_block)
spirit.width width(sub_block)
sub_block.regs.each do |name, reg|
# Required for now to ensure that the current value is the reset value
reg.reset
spirit.register do
spirit.name name
spirit.description try(reg, :name_full, :full_name)
spirit.addressOffset reg.offset.to_hex
spirit.size reg.size
if reg.bits.any?(&:writable?)
spirit.access 'read-write'
else
spirit.access 'read-only'
end
spirit.reset do
spirit.value reg.data.to_hex
spirit.mask mask(reg).to_hex
end
reg.named_bits do |name, bits|
spirit.field do
spirit.name name
spirit.description try(bits, :brief_description, :name_full, :full_name)
spirit.bitOffset bits.position
spirit.bitWidth bits.size
# When exporting to 1685-2009 schema, need to handle special cases (writeOnce),
# modifiedWriteValue, and readAction fields.
if options[:schema] == '1685-2009'
if bits.writable? && bits.readable?
if bits.access == :w1
spirit.access 'read-writeOnce'
else
spirit.access 'read-write'
end
elsif bits.writable?
if bits.access == :wo1
spirit.access 'writeOnce'
else
spirit.access 'write-only'
end
elsif bits.readable?
spirit.access 'read-only'
end
if bits.readable?
unless @@access_hash[bits.access][:read].nil?
spirit.readAction @@access_hash[bits.access][:read]
end
end
if bits.writable?
unless @@access_hash[bits.access][:write].nil?
spirit.modifiedWriteValue @@access_hash[bits.access][:write]
end
end
else # Assume Spirit 1.4 if not
spirit.access bits.access
end
# HDL paths provide hooks for a testbench to directly manipulate the
# registers without having to go through a bus interface or read/write
# protocol. Because the hierarchical path to a register block can vary
# greatly between devices, allow the user to provide an abs_path value
# and define "full_reg_path" to assist.
#
# When registers reside at the top level without a specified path, use 'top'.
if reg.owner.path.nil? || reg.owner.path.empty?
regpath = 'top'
else
regpath = reg.owner.path
end
# If :full_reg_path is defined, the :abs_path metadata for a register will
# be used for regpath. This can be assigned at an address block (sub-block)
# level.
unless options[:full_reg_path].nil? == true
regpath = reg.path
end
if options[:schema] == '1685-2009'
spirit.parameters do
spirit.parameter do
spirit.name '_hdlPath_'
# HDL path needs to be to the declared bit field name, NOT to the bus slice
# that Origen's "abs_path" will yield. Ex:
#
# ~~~ ruby
# reg :myreg, 0x0, size: 32 do |reg|
# bits 7..4, :bits_high
# bits 3..0, :bits_low
# end
# ~~~
#
# The abs_path to ...regs(:myreg).bits(:bits_low).abs_path will yield
# "myreg.myreg[3:0]", not "myreg.bits_low". This is not an understood path
# in Origen (myreg[3:0] does not exist in either myreg's RegCollection or BitCollection),
# and does not sync with how RTL would create bits_low[3:0].
# Therefore, use the path to "myreg"'s owner appended with bits.name (bits_low here).
#
# This can be done in a register or sub_blocks definition by defining register
# metadata for "abs_path". If the reg owner's path weren't used, but instead the
# reg's path, that would imply each register was a separate hierarchical path in
# RTL (ex: "top.myblock.regblock.myreg.myreg_bits"), which is normally not the case.
# The most likely path would be "top.myblock.regblock.myreg_bits.
spirit.value "#{regpath}.#{bits.name}"
end
end
end
# C. Hume - Unclear which vendorExtensions should be included by default, if any.
# Future improvment: Allow passing of vendorExtensions enable & value hash/string
# if options[:schema] == '1685-2009'
# spirit.vendorExtensions do
# vendorext = { 'xmlns:vendorExtensions' => '$UVM_RGM_HOME/builder/ipxact/schema' }
# xml['vendorExtensions'].hdl_path vendorext, "#{reg.path}.#{bits.name}"
# end
# end
# Allow optional inclusion of bit field values and descriptions
if options[:include_bit_field_values]
if bits.bit_value_descriptions[0]
bits.bit_value_descriptions.each do |val, desc|
spirit.values do
spirit.value val.to_hex
spirit.name "val_#{val.to_hex}"
spirit.description desc
end
end
end
end
if uvm? && !(options[:schema] == '1685-2009')
spirit.vendorExtensions do
xml['vendorExtensions'].hdl_path "#{regpath}.#{bits.name}"
end
end
end
end
end
end
# Unclear whether addressBlock vendor extensions are supported in Spirit 1.4
# if uvm?
# spirit.vendorExtensions do
# xml['vendorExtensions'].hdl_path sub_block.path(relative_to: owner)
# end
# end
end
end
# Assume byte addressing if not specified
if owner.methods.include?(:lau) == false
if methods.include?(:lau) == true
spirit.addressUnitBits lau
else
spirit.addressUnitBits 8
end
else
spirit.addressUnitBits owner.lau
end
end
end
end
end
end
# When testing with 'origen examples', travis_ci (bash) will end up with empty tags -
# '' that do not appear on some user's tshell environments. To
# prevent false errors for this issue, force Nokogiri to use self-closing tags
# (''), but keep the XML formatted for readability.
# All tags with no content will appear as ''.
#
builder.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::NO_EMPTY_TAGS |
Nokogiri::XML::Node::SaveOptions::FORMAT)
end
private
def open_memory_map(doc)
maps = doc.xpath('//spirit:memoryMaps/spirit:memoryMap')
maps = [nil] if maps.empty?
maps.each do |mem_map|
yield mem_map
end
end
def reg_description(register)
fetch register.at_xpath('spirit:description'), get_text: true, whitespace: true
end
def bit_description(bit)
desc = fetch(bit.at_xpath('spirit:description'), get_text: true, whitespace: true) || ''
bit_val_present = false
bit.xpath('spirit:values').each do |val|
unless bit_val_present
desc += "\n"
bit_val_present = true
end
value = extract(val, 'spirit:value', format: :integer, hex: true)
value_desc = extract val, 'spirit:description'
if value && value_desc
desc += "\n#{value.to_s(2)} | #{value_desc}"
end
end
desc
end
def mask(reg)
m = 0
reg.size.times do |i|
unless reg[i].reset_val == :undefined
m |= (1 << i)
end
end
m
end
def uvm?
@format == :uvm
end
def memory_maps
{ nil => {} }
end
def sub_blocks(domain_name)
owner.all_sub_blocks.select do |sub_block|
sub_block.owns_registers? &&
(sub_block.domains[domain_name] || domain_name == :default)
end
end
def address_blocks
domains = owner.register_domains
domains = { default: {} } if domains.empty?
domains.each do |domain_name, domain|
if owner.owns_registers?
yield domain_name, domain, owner
end
sub_blocks(domain_name).each do |sub_block|
yield domain_name, domain, sub_block
end
end
end
def address_block_name(domain_name, sub_block)
if domain_name == :default
sub_block.name.to_s
else
"#{domain_name}_#{sub_block.name}"
end
end
def width(sub_block)
sub_block.try(:width) || 32
end
def range(sub_block)
range = sub_block.try(:range) || begin
# This is to work around an Origen bug where max_address_reg_size is not updated in the case of
# only one register being present
# TODO: Fix in Origen
max_address_reg_size = sub_block.max_address_reg_size || sub_block.regs.first[1].size
(sub_block.max_reg_address + (max_address_reg_size / 8))
end
width_in_bytes = width(sub_block) / 8
if range % width_in_bytes != 0
range += (width_in_bytes - (range % width_in_bytes))
end
range
end
end
end