tkata117’s blog

tkata117’s blog

備忘録

Vivado シミュレータ (XSIM) が UVM をサポート

Vivado のダウンロードページ を見ると、Vivado Design Suite 2019.2 から シミュレータ (XSIM) が UVM をサポートとのことなので、UVM を使ってHello Worldを表示できるか試してみた。

ug900 Appendix C を読むと、 xvlog と xelab 実行時に -L uvm オプションをつければ UVM 1.2 が使えるとのこと。
(UVM 1.1 を使いたい場合は、 -uvm_version 1.1 のようにバージョンを指定可能)

準備

Makefile と tb_top.sv を用意。

Makefile

all: comp sim

comp:
    xvlog \
        -sv tb_top.sv \
        -L uvm
    xelab tb_top \
        -timescale 1ns/100ps \
        -override_timeunit \
        -override_timeprecision \
        -L uvm

sim:
    xsim tb_top -R

tb_top.sv

module tb_top;
`include "uvm_macros.svh"
    import uvm_pkg::*;

    initial begin
        `uvm_info("info", "Hello World!", UVM_LOW)
    end

endmodule // tb_top                                                          

Sim実行

$ make 

コンパイル・Simを実行したところ、以下のように無事に Hello World が表示された。

UVM_INFO /home/tkata117/work/uvm/uvm_study/hello_world/tb_top.sv(6) @ 0: reporter [info] Hello World!
exit
INFO: [Common 17-206] Exiting xsim at Sun Nov  3 16:29:33 2019...

無料のEDAツールでUVMがお手軽に使えるようになったのはうれしい。
次は、下記のTutorial や 書籍のサンプルを実行してみようと思う。

Ruby gem コードリーディング 02 : I18n (2)

Ruby gem コードリーディング 02 : I18n (1) の続き。

I18n.tranlsate() の詳細を読み解くために、処理の流れをシーケンス図に書いてみた。
なお、図中の処理(3)~(8) は一度だけ実行される (ただし、I18n.reload! を実行すると、次の tanslation処理時に再度実行される)。

I18n.translate処理のシーケンス図
I18n.translate処理のシーケンス図

locale の確認

1 I18n.translate(:test) を呼び出すと、I18nlocale_available?メソッド内の処理で、 Configに対して 2 available_locales_set() を要求する。

Config の available_locales_set メソッドは以下のようになっており、@@available_locales がnil だったら、Backend に対して 3 available_locales() を要求している。available_locales が取得できたら、 Set を使って重複を排除し、available_locales_set を返す。

    def available_locales
      @@available_locales ||= nil
      @@available_locales || backend.available_locales
    end
                                                                      
    def available_locales_set                                                                                                                        
      @@available_locales_set ||= available_locales.inject(Set.new) do |set, locale|
        set << locale.to_s << locale.to_sym
      end
    end

変換対象の辞書データのロード

3 available_locales() がコールされると、初期化が完了していない場合は、Backend(Simple) は init_translation() を実行して、辞書データのロードを実行する。すでに初期化が完了している場合は、ロード済みの辞書データから有効なlocaleを返す。

init_translation メソッドから load_translationsメソッドが呼ばれ、各辞書データのロードが実行される。

  • load_translationsメソッドは、各辞書データ毎に load_fileメソッド を呼ぶ
    • load_fileメソッド から load_yml メソッドを呼んで、辞書データから Hashを取得
      • load_ymlメソッドは、locale 毎の Hash を store_translations メソッドに渡す
        • store_translations メソッドは、 @translations Hash を更新する
# Backend::Base
      def load_translations(*filenames)
        filenames = I18n.load_path if filenames.empty?
        filenames.flatten.each { |filename| load_file(filename) }
      end

      def load_file(filename)
        type = File.extname(filename).tr('.', '').downcase
        raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}", true)
        data = send(:"load_#{type}", filename)
        unless data.is_a?(Hash)
          raise InvalidLocaleData.new(filename, 'expects it to return a hash, but does not')
        end
        data.each { |locale, d| store_translations(locale, d || {}) }
      end

      def load_yml(filename)
        begin
          YAML.load_file(filename)
        rescue TypeError, ScriptError, StandardError => e
          raise InvalidLocaleData.new(filename, e.inspect)
        end
      end
# Backend::Simple::Implementation
      def store_translations(locale, data, options = EMPTY_HASH)
        if I18n.enforce_available_locales &&
          I18n.available_locales_initialized? &&
          !I18n.available_locales.include?(locale.to_sym) &&
          !I18n.available_locales.include?(locale.to_s)
          return data
        end
        locale = locale.to_sym
        translations[locale] ||= {}
        data = data.deep_symbolize_keys
        translations[locale].deep_merge!(data)
      end

[辞書ファイルの拡張子に応じた処理]
load_file メソッド内の以下の記述で、辞書データの拡張子を抽出し、その拡張子に対応するメソッドが定義されているかを確認している。この記述方法なら、新たな拡張子の辞書データに対応する際に、load_file メソッドの記述を修正する必要がない。便利な記述だと思うので参考にしたい。

        type = File.extname(filename).tr('.', '').downcase
        raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}", true)
        data = send(:"load_#{type}", filename)

[デザインパターン]
Backend::Base のメソッド定義を見ると、 Templateパターンが使われている。 Backend::Base で定義されている available_locales メソッド , store_translations メソッド, lookupメソッドは、以下のように NoImplementedError を raise するようになっている。これらのメソッドは、このモジュールを include したクラス で実装が必須となっている 。

      def store_translations(locale, data, options = EMPTY_HASH)
        raise NotImplementedError
      end

Backend の translate 処理

Backend の translate メソッドで、lookupメソッドを呼んでいる。

# Backend::Simple::Implementation
        def lookup(locale, key, scope = [], options = EMPTY_HASH)
          init_translations unless initialized?
          keys = I18n.normalize_keys(locale, key, scope, options[:separator])

          keys.inject(translations) do |result, _key|
            return nil unless result.is_a?(Hash)
            unless result.has_key?(_key)
              _key = _key.to_s.to_sym
              return nil unless result.has_key?(_key)
            end
            result = result[_key]
            result = resolve(locale, _key, result, options.merge(:scope => nil)) if result.is_a?(Symbol)
            result
          end
        end

lookup メソッドでは、

  • I18n.normalize_keys メソッドを使って keys 配列を作成
    • I18n.translate(:test) を実行すると、 keys = [:en, :test] となる
  • keys.inject で、変換処理を行う
    • 1回目のループ処理で、 key = :en の変換が行われる
      • 変換前: result = {:en=>{:test=>"This is a test"}, :ja=>{:test=>"これはテストです。"}}
      • 変換後: result = {:test=>"This is a test"}
    • 2回目のループ処理で、 key = :test の変換が行われる
      • 変換前: result = {:test=>"This is a test"}
      • 変換後: result = "This is a test"

これで、変換処理が完了。

resolve メソッドは、変換結果が Symbol or Proc のときに、更なる変換処理を行う模様(今回のtranslate処理では不要なので、読んでない)。


I18n.translate() の基本的な流れは追えたので、次は簡易版のI18nを自分で書いてみる予定。

Ruby gem コードリーディング : 目次ページ

Ruby の勉強をするために、気になった Ruby gem のコードを少しずつ読んでいる。

勉強ログの目次

Ruby gem コードリーディング 02 : I18n (1)

今回は I18n を読んでみる。

I18n は、 Rails でも使われている 国際化・多言語化 対応を行う gem であり(参考url)、指定された locale (英語 とか 日本語) に対応した辞書からデータを引っ張ってくるものという理解。

I18n の README を読むと色々機能が書かれているが、今回は指定された locale のデータをyaml 形式のファイルから引っ張ってくるところを読む予定。 この範囲の機能ならコード量もそれほど多くないと予想。

まずは、動作を確認するために、yaml 形式のファイルを test/test_data ディレクトリの下に用意して、以下のコードを実行。

require 'i18n'

I18n.load_path << Dir[File.expand_path("test/test_data") + "/*.yml"]

puts I18n.translate(:test) # => "This is a test"
puts I18n.translate(:test, locale: :ja) # => "これはテストです。"

:test に対応する 英語、日本語の文をそれぞれ表示できた。


読んでみる

I18n.translate の処理を追ってみる。

    def translate(key = nil, *, throw: false, raise: false, locale: nil, **options) # TODO deprecate :raise                                                                         
      locale ||= config.locale
      raise Disabled.new('t') if locale == false
      enforce_available_locales!(locale)

      backend = config.backend

      result = catch(:exception) do
        if key.is_a?(Array)
          key.map { |k| backend.translate(locale, k, options) }
        else
          backend.translate(locale, key, options)
        end
      end

      if result.is_a?(MissingTranslation)
        handle_exception((throw && :throw || raise && :raise), result, locale, key, options)
      else
        result
      end
    end
  • config.locale を取得
    • I18n::Config の locale
  • 有効な locale が指定されているかをチェック
    • I18n::Config にチェック処理を委譲
      • さらに、 I18n::Backend にチェック処理を委譲
  • backend.translate で、keyに対応するデータを取得
    • catch(:exception) で括られているので、 backend は変換に失敗したら throw :exception でここまで抜けてこれるようにしている模様

I18n::Config, I18n::Backend にほとんどの処理を任せているようなので、この2つのコードをそれぞれ追っていく。

I18n::Config

ざっと眺めた感じだと、Config の仕事は以下の模様。

  • 辞書データのファイルのPath を保持
    • ユーザが、 I18n.load_path に辞書ファイルの path をセットする
    • I18n に以下の記述があり、 load_path などの変数へのアクセスが I18n::Config に委譲されている
    # Write methods which delegates to the configuration object                                                                          
    %w(locale backend default_locale available_locales default_separator                                                                 
      exception_handler load_path enforce_available_locales).each do |method|
      module_eval <<-DELEGATORS, __FILE__, __LINE__ + 1                                                                                  
        def #{method}                                                                                                                    
          config.#{method}                                                                                                               
        end                                                                                                                              
                                                                                                                                         
        def #{method}=(value)                                                                                                            
          config.#{method} = (value)                                                                                                     
        end                                                                                                                              
      DELEGATORS                                                                                                                         
    end
  • 設定された locale の保持
    • default は :en
    • この変数だけが、インスタンス変数になっている (path 等はクラス変数)。
  • backend の保持
    • default だと、 I18n::Backend::Simple が new される
  • 辞書データ中の有効な locale を保持
    • backend で辞書データを解析し、backend から有効な locale の array をもらっている

I18n::Backend

I18n::Backend::Simple を読んでいく。

Simple クラス内で Implement というモジュールを定義し、Simpleクラスは Implement モジュールを include している。 Simple クラスの全処理が Implement モジュールに記述されているのだが、なぜ Simpleクラスにメソッドを直接定義していないのか疑問に感じたので、ここを少し見ていく。

wiki にこの実装の理由が書かれていた。Simple クラスを継承せずにmodule を include することで、継承を使わずに各メソッド処理を上書きできるようにしている模様。 include で I18n::Backend::Pluralization を取り込む前後の Simple クラスのクラス・モジュール階層を irb で確認してみる。

>> I18n::Backend::Simple.ancestors
=> [I18n::Backend::Simple, I18n::Backend::Simple::Implementation, I18n::Backend::Base, I18n::Backend::Transliterator, Object, JSON::Ext::Generator::GeneratorMethods::Object, Kernel, BasicObject]

>> I18n::Backend::Simple.include(I18n::Backend::Pluralization)
>> I18n::Backend::Simple.ancestors
=> [I18n::Backend::Simple, I18n::Backend::Pluralization, I18n::Backend::Simple::Implementation, I18n::Backend::Base, I18n::Backend::Transliterator, Object, JSON::Ext::Generator::GeneratorMethods::Object, Kernel, BasicObject]

include により、I18n::Backend::Simple::Implementation の下の階層(こども側) に Pluralization を取り込めている。Implementation モジュールでなく、Simpleクラスに実装を書いてしまうと、Pluralization の機能が Simpleクラスの上の階層(親側)に入ってしまうので、include でメソッドの上書きができない ということだと思われる。

prepend を使えば、Implementationモジュールを挿まなくても同じことができるのでは?と思ったのだが、prepend は Ruby 2.0 から追加された機能なので、昔はこのように実装するしかなかったのかなと予想(この理解は間違っているかも)。
このあたりは、メタプログラミングRuby を読んでもう少し勉強しようと思う。

I18n::Backend::Simple の処理は、明日から読んでいこうと思う。

Ruby gem コードリーディング 01 : Faker (2)

Ruby gem コードリーディング 01 : Faker (1) の続き。

Faker gem の概要は追えたので、簡易Faker を自分で書いてみた。 → faker_study

Faker::Name.name の動作に最低限必要な記述を Faker から抜き出して、少し書き換えてみた程度。

  • my_faker.rb
    • MyFaker::Config : locale を保持するアクセサのみ
    • MyFaker::Baser : parse, fetch, sample メソッドのみ(簡略版)
  • name.rb
  • locals
    • en/name.yml : 架空の名前の辞書(英語)
    • jp/name.yml : 架空の名前の辞書(日本語)

テストを書いてみる

本家 Faker の test を参考に、 MyFaker のテストを書いてみた。
本家もそうだが、かなり簡易的なテスト。乱数が絡む処理のテストってどう書くべきなのだろう?

  • test
    • MyFaker::Name の各メソッドのテスト (:en, :ja locale のテスト)

bundle exec rake test を実行し、Green であることを確認。

method_missing を活用してみる

Faker では、以下の様に method_missing を override している。

      # You can add whatever you want to the locale file, and it will get caught here.           
      # E.g., in your locale file, create a                                                      
      #   name:                                                                                  
      #     girls_name: ["Alice", "Cheryl", "Tatiana"]                                           
      # Then you can call Faker::Name.girls_name and it will act like #first_name                
      def method_missing(mth, *args, &block)
        super unless @flexible_key

    if (translation = translate("faker.#{@flexible_key}.#{mth}"))
          sample(translation)
        else
          super
        end
      end

locale file に key を追加した際に、generator 側に メソッドを追加定義しなくても、key 名のメソッドを呼び出せる様にしている模様。
これを真似して、 name.rb をいじらずに locale file に girls_name という key を追加しただけで、MyFaker::Name.girls_name を実行できるようにしてみる。

まずは、 以下の様に MyFaker::Name.girsls_nametest を書いて、RED になることを確認する。

  def test_girls_name
    assert @tester.girls_name.match(/\w+/)
  end

結果

Error: test_girls_name(TestEnFakerName): NoMethodError: undefined method `girls_name' for MyFaker::Name:Class

意図通り Error となった。

次に、locale file に girls_name という 辞書データを追加し、 MyFaker::Name の method_missing メソッドを Overrideしてみる。

      def method_missing(meth, *args, &block)
        fetch("#{key_path}.#{meth}")
      end
name:                                                                                  
  girls_name: ["Alice", "Cheryl", "Tatiana"]     

irb でうまく動いていることと、 test が Green になることを確認。

>> MyFaker::Name.girls_name
=> "Tatiana"
>> MyFaker::Name.girls_name
=> "Alice"

まとめ

  • 簡易的ではあるが、Faker もどきを作成できた
  • テスト駆動っぽく機能を追加してみた
    • method_missing の雰囲気を掴めた

Ruby gem コードリーディング 01 : Faker (1)

Ruby の勉強をするために、気になった Ruby gem のコードを少しずつ読んでいこうと思う。

Rails Tutorial でも 使われている Faker を読んでみる。

Faker は、指定した generator に応じた架空のデータを作成してくれる gem である。
Rails Tutorial では、 Name generator で架空のユーザ名をランダムに生成し、 Database に架空のユーザ登録する という使い方をしている。

>> require 'faker'
>> Faker::Name.name
=> "Isobel Gerhold V"
>> Faker::Name.name
=> "Glen Eichmann"
>> Faker::Name.name
=> "Ivah Lemke Jr."

Name 以外にもgenerator が用意されており、色々な種類の架空のデータを生成できる。また、自分で新たな generator や 架空のデータを追加することも可能な模様。

コードを読んで、以下を理解したい。

  • generator をどのように実装しているのか (どうすれば複数の generator を効率よく実装できる?)
  • 架空のデータはどのように登録しておくのか

読んでみる

Faker::Name.name の動作から追って行こうと思い、 lib/faker/default/name.rb を覗いてみる。

module Faker
  class Name < Base
    flexible :name

    class << self
      extend Gem::Deprecate

      def name
        parse('name.name')
      end

Faker::Name は Faker::Base を継承している。
Faker::Name.name は、 Faker::Base から継承した parseメソッド に 'name.name' という文字列を渡しているだけみたいなので、Faker::Base を読めば動作を追えそう。
Faker::Name 以外の generator も少し見てみたところ、基本的には Faker::Base から継承している parse メソッドか fetch メソッド に文字列を渡しているだけ。Faker::Base が generator のほぼ全ての処理を持っている模様。

ということで、 lib/faker.rb の Faker::Base を読んでいくことにする。

Faker::Base.parse は、クラスメソッドとして以下の様に定義されている。

      def parse(key)
        fetched = fetch(key)
        parts = fetched.scan(/(\(?)#\{([A-Za-z]+\.)?([^\}]+)\}([^#]+)?/).map do |prefix, kls, meth, etc|
          cls = kls ? Faker.const_get(kls.chop) : self

          text = prefix
          text += if cls.respond_to?(meth)
                    cls.send(meth)
                  else
                    # Do just enough snake casing to convert PhoneNumber to phone_number
                    key_path = cls.to_s.split('::').last.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
                    fetch("#{key_path}.#{meth.downcase}")
                  end

          text + etc.to_s
        end
        parts.any? ? parts.join : numerify(fetched)
      end

Faker::Name.name は、 parseメソッドに 'name.name' を渡している。parseメソッドの処理の流れは以下の通り。

  1. fetch('name.name') で何か文字列を取ってくる
  2. fetchで取得してきた文字列を scan でサーチし、何か変換処理を行う。文字列の配列を作っている模様
  3. 文字列の配列を結合する

上記1, 2の処理をそれぞれ見てみる。

"1. fetch('name.name') で何か文字列を取ってくる" は何をしている?

fetch メソッドは以下のコード。

      def fetch(key)
        fetched = sample(translate("faker.#{key}"))
        if fetched&.match(%r{^\/}) && fetched&.match(%r{\/$}) # A regex
          regexify(fetched)
        else
          fetched
        end
      end

まず、 translate("faker.name.name") を追ってみると、 I18n.translate("faker.name.name", opts) の実行結果を返している。 I18nRails で使われている 国際化・多言語化 対応を行う gem であり(参考url)、指定された locale (英語 とか 日本語) に対応した辞書からデータを引っ張ってくるものという理解。
lib/locales を見ると、色々な locale の辞書が置かれているので、 Faker はここに定義された辞書データを引っ張ってくることで、架空のデータを生成している模様。 lib/locales/en/name.yml を見たら、 'faker.name.name' には以下の定義があった。

  name:
    - "#{prefix} #{first_name} #{last_name}"
    - "#{first_name} #{last_name} #{suffix}"
    - "#{first_name} #{last_name}"
    - "#{first_name} #{last_name}"
    - "#{first_name} #{last_name}"
    - "#{first_name} #{last_name}"

fetch("faker.name.name) は、 上記のいずれかの文字列を返すことがわかった。 "#{first_name} #{last_name}" という同じ文字列が 4 個も定義されているが、これは Randomで選択される割合を多くするためだろうか?( 4/6 の確率で選択されるはず)

"2. fetchで取得してきた文字列を scan でサーチし、何か変換処理を行う" は何をしている?

fetch("faker.name.name") で取得してきた文字列を以下の scan にかけて、ヒットした文字列ごとに処理を行っている。

parts = fetched.scan(/(\(?)#\{([A-Za-z]+\.)?([^\}]+)\}([^#]+)?/).map do |prefix, kls, meth, etc|

fetched = '#{first_name} #{last_name}' とした場合、scan 結果の配列は、
[ ["", nil, "first_name", " "], ["", nil, "last_name", nil]]
となる。
そうすると、map 処理の結果は、
parts[0] = self.send("first_name") + " ".to_s
parts[1] = self.send("last_name") + nil.to_s
となる。

self は Faker::Name なので、 Faker::Name.first_name, Faker::Name.last_name が実行されることになる。

Faker::Name.first_name は、 parse("name.first_name") を実行しているので、再帰的に parse 処理を行っていることがわかる。
"first_name" は "#{male_first_name}" or "#{female_first_name}" に変換され、Faker::Name.male_first_name or Faker::Name.female_first_name が実行される。辞書データを見ると、"male_first_name", "female_first_name" には、"#{}"形式ではなく名前のリストが書かれている。この名前のリストから一つがランダムに選ばれ、再帰処理が完了している。

Faker::Name.last_name は、辞書中の last_name からランダムに1つの名前が fetchで選ばれ、変換処理が完了している。

つまり、例として"Aaron Abbott" という架空のデータが生成される場合を考えると、変換の流れは以下の様になる。

name                               // Faker::Name.name
-> #{first_name} #{last_name}      // Faker::Name.first_name, parseメソッドで再帰処理
-> #{male_first_name} #{last_name} // Faker::Name.male_first_name , fetchメソッドでランダムな first_name に置換
-> Aaron #{last_name}              // Faker::Name.last_name, fetch メソッドでランダムな last_name に置換
-> Aaron Abbott

Faker は、辞書データ中の "#{hoge}" という文字列を hogeメソッド として処理し、再帰的に辞書データを変換して架空のデータを生成していることがわかった。

まとめ

  • 各 generator は、Baseクラス の parse, fetch を呼び出すだけという設計
  • Baseクラスは、I18n を使って、辞書データから架空のデータを生成している
    • 辞書データ中の "#{hoge}" という文字列は hoge メソッドとして処理され、再帰的に辞書データへの変換が行われている
  • 自分で generator を追加したいときは、
    • 辞書データを作成する(再帰処理が必要な文字列は "#{hoge}"と記述)
    • Baseクラスを継承したgeneratorクラスを作り、parse, fetch で辞書のkey を指定する

ZYBOで遊ぶ02:AXI CDMA IPを使ってみた(2)

ZYBOで遊んだメモ

ZYBOで遊ぶ02:AXI CDMA IPを使ってみた(1) - ThuruThuruToru’s blog の続き


ZYBOで遊ぶ02:AXI CDMA IPを使ってみた(1) - ThuruThuruToru’s blog
ZYBOで遊ぶ02:AXI CDMA IPを使ってみた(2) - ThuruThuruToru’s blog



2. AXI CDMAの最低限の仕様を確認

Xilinx SDKでSWを書き始める前に,AXI CDMAの動かし方を調べる.

XilinxのWebページ XilinxのAXI CDMA のPG034 - AXI Central Direct Memory Access v4.1 Product Guide に仕様書が置かれている.

Register Details, Programming Sequence の節を読めば動かし方がなんとなくわかる.

2.1. 制御レジスタ

今回はScatter/Gatherはなしなので,以下の制御レジスタを使うだけで動かせる.

  • CDMACR
    • Err_IrqEn
      • 1にセットでエラー時の割り込みを有効化
    • IOC_IrqEn
      • 1にセットで転送完了割り込みを有効化
  • CDMASR
    • Err_Irq
      • エラー発生時に1がセットされる
      • 1をWriteするとクリア
    • IOC_Irq
      • 転送完了時に1がセットされる
      • 1をWriteするとクリア
    • DMADecErr, DMASlvErr, DMAIntErr
      • エラー情報が読める
    • Idle
      • Idle状態なら1が読める
  • SA
    • Sourceアドレスを指定する
  • DA
  • BTT
    • 転送するバイト数を指定する
    • このレジスタへのWriteでDMA転送がキックされる

2.2. 制御の流れ

SWは以下の処理を行えば良い

  1. 割り込みハンドラの設定
  2. CDMASR.IDLE=1を確認
  3. CDMACR.IOC_IrqEnとCDMACR.IrqEnに1をセット
  4. SAに転送元のアドレスをセット
  5. DAに転送先のアドレスをセット
  6. BTTに転送するバイト数をセットし,DMA転送を開始させる
  7. 割り込みが上がるのを待つ
  8. 割り込み処理を行う
    • CDMASR.Err_Irq, CDMASR.IOC_Irqを確認し,割り込み要因をクリアする
  9. 上記2に戻る

3. Xilinx SDK で DMACを操作するSWを書く

2つバッファを確保し,DMAでデータを転送するSWを書く. 今回は勉強のため, AXI CDMA用の関数(xaxicdma.h内で宣言)は使わず,制御レジスタを自分で操作する.

コード全部を載せると長くなるので完成したコードは ZYBOで遊ぶ02 · GitHub に置いた.

DMACキック後は割り込みが上がるのを待つようにし,割り込みハンドラでは割り込み要因のクリアを行った後,転送先のバッファのデータをUARTを使ったPC側のターミナルに表示させる.

3.1. プロジェクト作成

File->New->Application Projectをクリック.
Project nameを埋めて,Nextボタンをクリック.
TemplatesにHello Worldを指定してFinishボタンをクリック.

3.2. 制御レジスタ関連の定数の定義

xaxicdma.h にCDMAを操作するための関数,構造体,定数が宣言されているが,今回はこれらを使わずに自分で定数の定義をおこない,制御レジスタをひとつひとつ読み書きして操作する.

#define CDMA_BASE_ADDR XPAR_AXI_CDMA_0_BASEADDR
#define CDMA_CR_ADDR  CDMA_BASE_ADDR+0x00
#define CDMA_SR_ADDR  CDMA_BASE_ADDR+0x04
#define CDMA_SA_ADDR  CDMA_BASE_ADDR+0x18
#define CDMA_DA_ADDR  CDMA_BASE_ADDR+0x20
#define CDMA_BTT_ADDR CDMA_BASE_ADDR+0x28

#define CDMA_CR_ERR_IRQ_MASK 0x00004000
#define CDMA_CR_IOC_IRQ_MASK 0x00001000

#define CDMA_SR_ERR_IRQ_MASK 0x00004000
#define CDMA_SR_IOC_IRQ_MASK 0x00001000
#define CDMA_SR_DEC_ERR_MASK 0x00000040
#define CDMA_SR_SLV_ERR_MASK 0x00000020
#define CDMA_SR_INT_ERR_MASK 0x00000010
#define CDMA_SR_IDLE_MASK    0x00000002

3.3. バッファの確保

転送元のバッファと転送先のバッファを以下の様に静的に確保する.

volatile u8 src_buf[BUF_BYTE_NUM] __attribute__ ((aligned(32)));
volatile u8 dst_buf[BUF_BYTE_NUM] __attribute__ ((aligned(32)));

バス幅が64bitなので8Byteアライン指定で良いかと思ったが,8Byteアラインとするとキャッシュのinvalidateが一部うまくいかなかった(Xil_DCacheInvalidateRange関数の説明を読めば謎が解けるはずなので読む).
キャッシュラインサイズに合わせておけばキャッシュ操作もうまくいくと考え,Cortex-A9のキャッシュラインサイズは32Byteのようなので32Byteアラインとした. BUF_BYTE_NUMは自分で定義した定数で,今回は128Byteとした.

3.4. バッファに初期値をセットする

以下のset_data関数で転送元のバッファには0, 1, 2,・・・という様なインクリメントした値を順にセットし, 転送先のバッファにはall 0 データをセットする.

void set_data(u8 *buf_ptr, u32 num, int mode/* 0:incr/1:zero*/)
{
    int i;

    print("\n[set_data]\n\r");
    for (i = 0; i < num; i++) {
        buf_ptr[i] = (mode) ? 0 : i;
    }

    // flush data
    Xil_DCacheFlushRange((u32)buf_ptr, num);
}

重要なのが最後の Xil_DCacheFlushRange関数の実行.
この関数で明示的にキャッシュフラッシュをしないと,転送元のバッファにセットしたデータがDRAMまで書き込まれず,DMA転送実行後の結果が意図した通りにならない.
最初はキャッシュの存在を意識していなかったため,データが化ける?????としばらく悩んだが,キャッシュをフラッシュすることで解決.

3.5. DMAキック前のバッファ内のデータ確認

転送元と転送先のバッファ内のデータをprint_buf関数で,PCのターミナルに表示させ,値を確認する.

3.6. 割り込み設定

SetupIntrSystem関数に記述.
基本的には ZYBOで遊ぶ01:簡易タイマーIPの自作(4) - ThuruThuruToru’s blog と同じだが,

typedef struct {
  u8 *src_buf_ptr;
  u8 *dst_buf_ptr;
} BufSt;

という転送元,転送先バッファのポインタをまとめた構造体を定義し,割り込みハンドラにこの構造体のポインタを渡すようにしている.

3.7. 割り込みハンドラ

cdma_int_handler関数に記述.
CDMAが割り込みをあげる度にこの関数が読み出される.
引数には,割り込み設定で指定した BufSt構造体のポインタが渡される.

void cdma_int_handler(void *callback)
{
  u32 data32;
  char str[STR_BYTE_NUM];

  print("\n[cdma_int_handler]\n\r");

  // check SR
  data32 = Xil_In32(CDMA_SR_ADDR);
  snprintf(str, BUF_BYTE_NUM, "CDMA_SR = %lx\n\r", data32);
  print(str);
  if (data32 & CDMA_SR_ERR_IRQ_MASK)
    print("ERR_IRQ\n\r");
  if (data32 & CDMA_SR_IOC_IRQ_MASK)
    print("IOC_IRQ\n\r");
  if (data32 & CDMA_SR_DEC_ERR_MASK)
    print("DEC_ERR\n\r");
  if (data32 & CDMA_SR_SLV_ERR_MASK)
    print("SLV_ERR\n\r");
  if (data32 & CDMA_SR_INT_ERR_MASK)
    print("INT_ERR\n\r");
  if (data32 & CDMA_SR_IDLE_MASK)
    print("IDLE\n\r");

  // clear interrupt
  print("clear interrupt\n\r");
  data32 = CDMA_SR_ERR_IRQ_MASK | CDMA_SR_IOC_IRQ_MASK;
  Xil_Out32(CDMA_SR_ADDR, data32);

  // check SR
  data32 = Xil_In32(CDMA_SR_ADDR);
  snprintf(str, BUF_BYTE_NUM, "CDMA_SR = %lx\n\r", data32);
  print(str);

  // dcache invalidate
  print("dcache invalidate\n\r");
  Xil_DCacheInvalidateRange((unsigned int)((BufSt *)callback)->dst_buf_ptr, BUF_BYTE_NUM);

  // print dst buffer
  print_buf(((BufSt *)callback)->dst_buf_ptr, BUF_BYTE_NUM);
}

処理としては,割り込みクリア,データキャッシュinvalidate,転送先バッファ内のデータの表示をしている.

初期データの確認を3.5で行ったので,転送先バッファのデータはデータキャッシュに格納された状態となっている.そのため,データキャッシュのinvalidateをしないと,DMACがDRAMに転送したデータではなく,キャッシュにヒットした古いデータを読み出してしまう.
ここもはまった.

3.8. CDMA初期化

init_cdma関数に以下の処理を記述.

  • CDMASR, CDMACR を読んでIDLEであることを確認(IDLEになるまでポーリング)
  • CDMACR.IOC_IrqEnとCDMACR.IrqEnに1をセット

3.9. DMACキック

trans_data関数内に,CDMAの制御レジスタへ転送元,転送先アドレスと転送バイト数を書き込む処理を記述.

3.10. 動かす

転送先のバッファに転送元のバッファのデータが書き込まれることが確認できるはず.

qと打つとプログラムが終了するようにしている.

まとめ

AXI CDMA を使って DRAM<->DRAM のデータ転送を行ってみた.
次は AXI VDMA(AXI4 <-> AXI4-Stream)を使ってみる予定.