FPGA

愚直なRTLでソートを記述する

FPGAの部屋の記事、 RGB 24ビット・データ入出力対応のメディアン・フィルタをVitis HLS 2021.1で作成する1RGB 24ビット・データ入出力対応のメディアン・フィルタをVitis HLS 2021.1で作成する2にて、HLS記述にてバブルソートを記述し、Interval=1を得ている(1クロック毎にデータを入力できるパイプラインで動作)のを見て、 実はHLSではなく、愚直にRTLで書いてもInterval=1は達成できるのではないかと思い、実験してみました。

ソートをハードウェアで実装するための手法はよく研究されており、ソーティングネットワークと呼ばれているようです。

Wikipediaによれば、3x3(=9)画素のソートを行うためには、7段のソーティングネットワークが最小の段数であるそうです。

“sorting network generator"などと検索すると、ソーティングネットワークの自動生成ツールなどもあり、興味がある方には面白いかもしれません。

書いてみたRTL記述はこちら:

module sort (
    input            CLK,
    input [15:0]     in[0:8],
    output reg[15:0] out
);

    logic[15:0] tmp;
    logic[15:0] in_val[0:8];
    logic[15:0] result;

    int i,j;

    always @(*) begin
        in_val = in;
        for (i = 1; i < 9; i = i + 1) begin
            for (j = 0; j < 9 - i; j = j + 1) begin
                if (in_val[j] < in_val[j+1]) begin
                    tmp         = in_val[j];
                    in_val[j]   = in_val[j+1];
                    in_val[j+1] = tmp;
                end
            end
        end
        result = in_val[4];
    end

    always_ff @(posedge CLK) begin
        out <= result;
    end

endmodule

9個の数値(各16bit)からなる配列inを、上記記事ではC言語で書かれているのと同じ方法でソートしています。 このようなコードは、ある程度RTL記述に慣れた人の方が違和感を感じるかと思います。 ノンブロッキング代入(<=)ではなくブロッキング代入(=)を使っています。

FMステレオ復調を実装

やっとFMステレオ復調処理を実装しました。処理としては、 ディジタルFMステレオ・チューナの製作 ―― 雑誌の付属基板でここまでできる(ステレオ復調回路)にある通りです。

今回の実装について以下に説明しますが、勘違いしている可能性もありますので、 ご自分で確認してください。

DDSのIPを置いて、位相を可変とした19kHzのsin, cos信号を生成します。 cos信号とFM復調信号を掛け算し、その結果をLPFに通します。 LPFは、CICでfsを1/20にして(fs=500kHzからfs=25kHzに)、続いてFIRでfc=1kHzのフィルタを掛けました。 そして、この値に応じて、DDSで発振する19kHz信号の位相を進めたり遅らせたりします。

具体的には、パイロット信号をsin 19kHzとすると、三角関数の公式により、 sin x * cos y = 1/2 * {sin(x-y)+sin(x+y)} です。今回は、sin 19kHz * cos 19kHz = 1/2 * {sin(0) + sin(38kHz)} となります。LPFを通すことでsin(38kHz)は0になりますので、sin(0)だけが残ります。 つまり、パイロット信号と同調したcos信号が得られれば、sin(0)=0となります。 ただし、sinが0となるのは、0(x=yのとき)とπ(x=y+πのとき)がありますが、今回は0になるようにします。 それで、sinが正であれば、cosの位相(y)を増やし、sinが負であれば、 cosの位相を減らして、sin(x-y)のx-yが0となるように制御しました。

このようにsinのパイロット信号に対してcosの信号が得られたら、倍角の公式 sin 2x = 2 sin x * cos x により、DDSのsin, cos信号を掛け算することで、パイロット信号に同期した38kHzの信号が得られることになります。

あとは、この38kHzの信号をFM復調信号に掛け算し、LPFを通すことでL-Rの信号が得られます。

この状態で時報信号をキャプチャしたものが次になります。

“FM横浜時報信号”

ちょっと分かりにくいですが、上側のグラフは、左右の音声を別々の色で描画しています。 時報信号だと恐らく左右同じ音声信号になってしまっているので、ほとんど重なってしまい、 分かりにくいです。

実際に聞いてみて、確かにこれまでより奥行きが感じられるようになりました。 ただ、この実装がどれほど正しいかは、上のグラフだけでは分かりません。 本来であれば左だけ、右だけの音声を復調してみて、それが正確に再生されるか確認する必要があります。 ただ、手ごろなFMトランスミッタを持ち合わせていないので、このような確認まではできていません。

Zynqで動作するWebSocketサーバから受信したデータをブラウザにてFFT表示

色々試行錯誤することで、ようやく目的の動作に少し近づきました。

Zynq上でWebSocketサーバを動作させ、ADCからキャプチャしたデータをブラウザに送り込み、 ブラウザにてFFTを行いリアルタイムに表示します。

WebSocketサーバはRustで作成しました。

まだFFTのWindow処理は行っていません。1回描画するために256kBの生データをブラウザに送っています。

FFT自体はこちらを使わせてもらっています(参考: FFTs in JavaScript)。

グラフ表示は、Chart.jsを使用しています。

まず、Zynqで動作するプログラムです。

次の記事がUIO経由のレジスタアクセスの参考になりました: 【Rust】Raspberry Pi 3でGPIOのレジスタを叩いてLチカ

read, mmapなどのシステムコール、メモリアドレスからVecへの変換、 データ取り込みスレッドとサーバスレッドとの同期方法など、 色々と苦労したノウハウが詰まっています。

/dev/memをmmapすることで、Z-turn boardに搭載されている1GBのDDRの後半512MBを仮想アドレス空間にマップしています。 PLから当該領域にAXIバス経由でADCデータは転送されます。転送を行うためのIPの設定を、 /dev/uio0にマップされた制御レジスタを通して行います。/dev/uio0にreadを行うことで、割り込み待ちも行えます。

ただし、まだデータ取り込みとブラウザへのデータ転送はオーバーラップしていません。 シーケンシャルに取り込み->転送と動作しています。

extern crate ws;
extern crate libc;

use ws::{listen, Handler, Result, Message, CloseCode, Handshake};
use ws::Message::Text;
use ws::Message::Binary;
use ws::util::Token;
use std::fs::{OpenOptions, File};
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::{AsRawFd};
use std::io;
use std::io::{Write, Read, Cursor};
use std::ptr::{self, read_volatile, write_volatile};
use std::thread;
use std::sync::mpsc::channel;
use std::sync::mpsc::{Sender, Receiver};
use std::sync::Arc;
use std::slice;
use std::vec::Vec;
use std::mem;
use std::boxed::Box;

struct Server {
    out: ws::Sender,
    tx: Arc<Sender<i32>>,
    rx: Arc<Receiver<u32>>
}

const KB : libc::size_t = 1024;
const MB : libc::size_t = 1024*1024;
const MEM_SIZE : libc::size_t = 512*MB;
const MEM_OFFSET : libc::off_t = 512*MB as libc::off_t;
const ZERO_OFFSET : libc::off_t = 0;
const PAGE_SIZE : libc::size_t = 4096;

fn memmap() -> io::Result<*mut u32> {
    let mem_file = OpenOptions::new()
                    .read(true)
                    .write(true)
                    .custom_flags(libc::O_SYNC)
                    .open("/dev/mem")
                    .expect("can't open /dev/mem");
    unsafe {
        let ptr = libc::mmap(ptr::null_mut(),
                            MEM_SIZE, libc::PROT_READ | libc::PROT_WRITE,
                            libc::MAP_SHARED,
                            mem_file.as_raw_fd(),
                            MEM_OFFSET);        // second 512MB of the total of 1GB of DDR memory
        if ptr == libc::MAP_FAILED {
            Err(io::Error::last_os_error())
        }
        else
        {
            Ok(ptr as *mut u32)
        }
    }
}

fn uiomap() -> (*mut u32, File) {
    let uio_file = OpenOptions::new()
                    .read(true)
                    .write(true)
                    .custom_flags(libc::O_SYNC)
                    .open("/dev/uio0")
                    .expect("can't open /dev/uio0");
    unsafe {
        let ptr = libc::mmap(ptr::null_mut(),
                            PAGE_SIZE, libc::PROT_READ | libc::PROT_WRITE,
                            libc::MAP_SHARED,
                            uio_file.as_raw_fd(),
                            ZERO_OFFSET);
        if ptr == libc::MAP_FAILED {
            panic!("{}", io::Error::last_os_error())
        }
        (ptr as *mut u32, uio_file)
    }    
}

fn set_axi_dma_reg(reg_adr : *mut u32, size : u32, offset : u32) {
    let status_reg : *mut u32 = unsafe { reg_adr.offset(0) };
    let ien_reg : *mut u32 = unsafe { reg_adr.offset(1) };
    let done_reg : *mut u32 = unsafe { reg_adr.offset(2) };
    let offset_reg : *mut u32 = unsafe { reg_adr.offset(4) };
    let len_reg : *mut u32 = unsafe { reg_adr.offset(6) };

    unsafe {
        let status = read_volatile(status_reg); // clear ap_done
        write_volatile(ien_reg, 1);     // enable
        write_volatile(done_reg, 1);    // IP Interrupt enable reg
        write_volatile(offset_reg, offset/8);
        write_volatile(len_reg, size/8);
    }
}

fn start_axi_dma(reg_adr : *mut u32){
    let start_reg : *mut u32 = unsafe { reg_adr.offset(0) };
    unsafe {
        write_volatile(start_reg, 1);
    }
}

fn clear_interrupt(reg_adr : *mut u32){
    let intcl_reg : *mut u32 = unsafe { reg_adr.offset(3) };
    unsafe {
        write_volatile(intcl_reg, 1);
    }
}

fn wait_interrupt(uio_file : &mut File) -> (){
   let mut tmp = [0; 4];
   if 4 != uio_file.read(&mut tmp).unwrap() {    // wait interrupt
        panic!("failed read");
   }
}

impl Handler for Server {
    fn on_open(&mut self, _shake: Handshake) -> Result<()> {
        println!("on_open");
        Ok(())
    }
    fn on_timeout(&mut self, event: Token) -> Result<()> {
        println!("on_timeout");
        Ok(())
    }
    fn on_message(&mut self, msg: Message) -> Result<()> {
        match msg {
            Text(_) => {
                let vec: Vec<u8> = vec![0; 1920*1080];
                self.out.send(vec)
            },
            Binary(v) => {
                let adr = self.rx.recv().unwrap();
                let mut vec : Vec<u8> = Vec::new();
                let ary : &'static [u8] = unsafe { slice::from_raw_parts(adr as *const u8, 256*KB) };
                vec.write(ary).expect("unable to write");
                let r = self.out.send(Binary(vec));
                self.tx.send(1);
                r
            }
        }
    }
    fn on_close(&mut self, _code: CloseCode, _reason: &str) {
    }
}

fn main() {
   let mapped_ptr : *mut u32 = memmap().expect("failed mmap");
   let (uio_ptr, mut uio_file) = uiomap();
   let one : [u8; 4] = [1, 0, 0, 0];

   let (tx_web, rx_cap) = channel();
   let (tx_cap, rx_web) = channel();

   let hdl = thread::spawn(move || {
       let tx = Arc::new(tx_web);
       let rx = Arc::new(rx_web);
       listen("192.168.0.90:3012",
                |out| Server { out: out, tx: Arc::clone(&tx), rx: Arc::clone(&rx) })
                .unwrap();
   });
   loop {
        set_axi_dma_reg(uio_ptr, 256*KB as u32, 512*MB as u32);
        uio_file.write(&one).expect("write");            // enable interrupt
        start_axi_dma(uio_ptr);
        wait_interrupt(&mut uio_file);
        clear_interrupt(uio_ptr);
        tx_cap.send(mapped_ptr as u32);    // send start address
        let v = rx_cap.recv().unwrap();
   }
}

ビルド/デプロイ方法は、一つ前の記事を参考にしてください。

RustでWebSocketサーバをクロスコンパイルしてZynqで動かした

以前に、 HaskellでWebSocketのサーバを動作させ、バイナリデータをブラウザで受け取ってCanvasで描画する実験を行いました。

最終的には、ADCのデータをブラウザに送って、ブラウザ上で各種処理を行うことを目論んでいます。

現在はADCのデータは次のように取得しています。

Cで作成したTCPのサーバプログラムをZynq上で動作させ、ADCのデータをmmap等により取得し、TCP経由で送信します。 クライアント側は、C#で作成したプログラムでデータを受信して、各種処理を行っています。

これだと、どうしてもWindows専用のプログラムになってしまいます(Monoを使ってLinuxでも動くのかも知れませんが)。 近頃はかなりの事がブラウザでできるようになっていますし、 そうすればクロスプラットフォームで使えますので、そちらの方が望ましいと思います。

それで上記のような実験を行っていました。 ただ、Haskellでarmv7にクロスコンパイルするのは少し面倒そうでした。

Rustは、かなり簡単にクロスコンパイルが行えるということが分かり、 上記の目的が達成できるか、少しずつ試しています。

Rustでクロスコンパイル

上記の記事に従うことで、簡単にクロスコンパイル環境が構築できました。

次に、前回Haskellで作成したプログラムとほぼ同機能のプログラムを作りました。


extern crate ws;

use ws::{listen, Sender, Handler, Result, Message, CloseCode, Handshake};
use ws::Message::Text;
use ws::Message::Binary;
use ws::util::Token;

struct Server {
    out: Sender,
}

impl Handler for Server {
    fn on_open(&mut self, _shake: Handshake) -> Result<()> {
        println!("on_open");
        Ok(())
    }
    fn on_message(&mut self, msg: Message) -> Result<()> {
        let mut vec: Vec = vec![0; 1920*1080];
        match msg {
            Text(_) => {
                let vec: Vec = vec![0; 1920*1080];
                self.out.send(vec)
            },
            Binary(v) => {
                for j in 0..1080 {
                    for i in 0..1920 {
                        vec[j*1920+i] = (i+j+v[0] as usize) as u8;
                    }
                }
                self.out.send(Binary(vec))
            }
        }
    }
}

fn main() {
   listen("192.168.0.90:3012", |out| Server { out: out }).unwrap();
}

Cargo.tomlには、

画像処理回路の基礎(ラインメモリの構成法)

FPGAで各種画像フィルタを実装するというのは良く行われます。 ちょっと検索した範囲では、フィルタに必要な3x3のデータ等を作るのに必要な回路の構成法について、 詳しく記載されているページを見つけることができませんでした。 FPGA使いには当たり前なのかもしれませんが、あまり画像処理回路に詳しくない場合、 その方法が分かりにくいことも考えられます。

それで、参考までに資料を作成しました。

とにかく、Block RAMの使い方とレイテンシ調整がキモになります。

7シリーズFPGAでの非同期クロック切り替え

注文済みの基板の到着を待つ間に、FPGAデザインの修正を行いました。

ADCにつながる信号線のピン配置を変更しても良かったのですが、今回の基板は、前回とは異なるコネクタに装着することになります。 つまり、新旧二つの基板を、同時に一つのZ-turn boardに装着することができます。 それで、Z-turn board上に実装されているディップスイッチで、どちらの基板からの信号を処理するかを変更できるようにしました。

FPGAの大きさとしては、両方の基板からのADCデータを同時に処理することも可能です。 でも、そこまでする必要性を今は感じません。それで、FM処理回路自体は1つだけで、当該回路にどちらの基板から信号を供給するかを、スイッチで切り替えるという算段です。

それで、ひとまず想定した実装を行って、既存基板で動作することを確認しようとしたら、どうにも動作しませんでした。 調べてみたところ、MMCMのlocked信号がHighになっていない。つまり、基板からの40MHzのクロックがちゃんと切り替えられていない。

結果として、次のようにすることで解決できました。

BUFGCTRL bufg_inst (  // use as asynchronous MUX
    .IGNORE0    (1'b1),
    .IGNORE1    (1'b1),
    .CE0        (1'b1),
    .CE1        (1'b1),
    .S0         (!SW[2]),
    .S1         (SW[2]),
    .I1         (w_adc_ck_bufg),
    .I0         (w_adc_ck_b_bufg),
    .O          (w_adck)
);

最初はBUFGMUX_CTRLを使用していましたが、それが原因でした。UG472をよく読んでみると、 BUFGMUX_CTRLは基本的には、どちらの入力クロックも常時トグルしている場合に使用することが前提となることが分かりました。

それだと、場合によって(必要な要件によって)は当然困った事態になりますので、ちゃんと回避方法が準備されています。 それが、BUFGCTRLのIGNOREピンです。IGNORE0をHighにすると、I0ピンのクロックからI1ピンへのクロックの切り替えが(クロックの変化を待たず)即時に行われます。IGNORE1をHighにすると、I1ピンからI0ピンへのクロック切り替えが即時に行われます。

というわけで、上記のように記述することで、現在のように片方の基板が接続されていない(したがってI0がずっとLow)場合でも、 SW[2]の切り替えに応じてクロック選択が可能になりました。

7シリーズFPGAのBUFMRを有効活用する

Xilinxの7シリーズFPGAを使用していると、Implementation時にCLOCK_DEDICATED_ROUTE=BACKBONEを設定しないと他のクロックリージョンのCMTを駆動できないよ、といったエラーが出ることがあります。

多くの場合、下図(Kintex-7 160T)を例とすると、Bank15とBank14のCMTを同一のクロックで駆動しようとしたときに、このエラーが発生します。

“Clock Region”

このような場合、もちろんメッセージに従ってCLOCK_DEDICATED_ROUTE=BACKBONEを指定することも可能ですが、 “その場合は最適ではないよ(sub-optimal)“といった怪しい内容を承諾することになります。

でも実は、それ以外にもあまり知られていない(かもしれない)逃げ道があるのです。

それはBUFMRを使用することです。BUFMRは、入力されたバンクの上下も駆動できます。 例えば、上図でBank15(のMRCCピン)に入力されたクロックは、Bank16, Bank14のCMTを駆動することができます。

それで、MRCC pin->BUFMR->BUFR->CMTというように接続すれば、Bank15に入力されたクロックでBank14-16のCMTをエラー無く駆動することができます。

SRCCにクロックが入力されていた場合は… 残念ですが、CLOCK_DEDICATED_ROUTE=BACKBONEを指定してください。

ADC基板の電源測定

オシロスコープを入手したので、早速電源のノイズをみてみました。

ノイズ測定をするにあたって、正確な測定のためには、GNDクリップは使用しないで、 直近のGNDで測定しないと意味がないようですが、ノイズの大きさの傾向だけはつかめるかと思いました。

Z-turn boardから送られてくる3.3V電源。これはスイッチング電源の出力です。

"+3.3V"

上記電源がFBを通過した後の様子

"+3.3V"

相当ノイズは減っているように見受けられます。

次に、LDO出力の3.0V(上の二枚と異なり、垂直軸が10mVレンジなのに注意)

"+3.0V"

上記がFBを通過した後の様子

"+3.0V"

スイッチング電源ではないのでFBはいらなかったかもしれません。

いずれにしても、LDO出力のノイズが大きすぎるような気がします。 全くノイズが減少しているように見受けられません。

急ぎで製作してしまったので、LDOの推奨レイアウトのようにベタにしなかったのがまずかったのか、 それともそのほかの理由があるのか検討しないといけませんね。

2017/7/28 追記: 今になって分かったのですが、LDOは一般的に数百kHz以上のノイズについてはほとんどフィルタの役割を果たしません。そういった周波数帯のノイズを除去したいのであれば、ちゃんとしたフィルタを通す必要があります。 上記の結果はそれを確認していることになります。

また、LDOでできるだけノイズを除去したいのであれば、ドロップアウト電圧も1V以上とることが推奨されるようです。 あまりにドロップアウト電圧が小さいと、ノイズ除去能力も低減します。

上記のような内容は、LDOのデータシートをよーく読めばわかります。また、LDOに使用されているオペアンプの特性からしても、 高周波のノイズ除去はできないのは明らかそうです。

SiglentのDSOを購入

FMステレオ復調のところは、時間が無くて取り組めていません。

その間に、基板の性能の方を調べて見たくなり、いろいろとオシロスコープを物色しました。

最初はTektronixの中古をヤフオクなどで落とそうかと思ったのですが、最終的には新品を購入してしまいました。

大体100MHzの帯域が測定できる必要があります。それと、FFTも欲しいです。 安くて確実そうなのはRIGOLのDS1054Zかと思いましたが、 最終候補になったのは、HantekのDSO5102Bと、 SiglentのSDS1202X-Eでした。 前者は大体339USD, 後者は379USD程度です。

SDS1202X-Eは、I2C, SPI, UART, CANなどの信号のデコード機能も持っているのが優位点でした。 また、SDS1202X-Eの方が新しいです。こちらによると、Zynqの7020を使用しているようです。

結局、SDS1202X-Eを購入しました。最初はAmazon.comから買おうかと思いましたが、 調べてみたらAliExpressにて送料無料で購入できることがわかり、 そちらに出店していたSains Electronix Storeから購入しました。 6/15(木)の夜に注文して、6/19(月)には到着しました。早いです。

(2019/1/12現在、日本のAmazonからも購入できるみたいです)

注文したら、すぐにお店から、“USコードを選択していたけど、EUコードしか在庫がないんだよ。 USコードは2週間待ちなんだけど、EUコードで送っても良いかい? (意訳)“という内容のメッセージが届きました。 “いやいや、待っても良いからUSコードでお願い。でもできるだけ早くね! (意訳)“と返しました。 そうしたら翌日6/16(金)には発送のお知らせが来たので、まさかEUコードが添付されているのではないか、 とはらはらしましたが、到着した商品を確認したら、ちゃんとUSコードが添付されていました。

うっかりしていたのは、このコードは3ピンタイプだったので、家にある電源プラグに接続するために変換プラグを購入する必要がありました。こういうやつですね。 あるいは3ピン2ピンケーブルでも良いですね。

AliExpressは初めて使いましたが、楽天のように店舗によって対応がまちまちのようです。 今回使用した店舗は迅速で満足でした。Web上での評価も高いようです。梱包も問題ありませんでした。

“同梱物”

当たり前ですが、プローブが2本ついています。写っていませんが、USBケーブルも添付されていました。

“SDS1202X-E”

オシロ本体です。とりあず、電源を入れてプローブ補正だけしました。

早速ADC基板を少し測定してみました。 基板に電源は投入せずに、FMアンテナを接続して、BPFの後の個所にプローブを当て、FFTをしてみた結果が次になります。 ちなみに、GNDは基板上に立てたピンでとっているので、あまり良くないかもしれません。

“BPF後のFFT”

そういえば、画面キャプチャも本体でできるはずですね。わざわざ撮影する必要は無かったですね。

それは置いておいて、上の画面では84.7MHz近辺を拡大しています。Y軸の単位はdBVrmsになっています。 大体20dB位は信号が出ているようですね。

問題は、電源を投入した時の様子です。順次調べていこうと思います。

Artix-7でのMIGエラー

とある基板を製作するにあたり、Artix-7にてDDR3のピン配置がどうなるか確認したいと 思い、MIG(Memory Interface Generator)にてDDR3コントローラを作ってみました。 IP IntegratorにてAdd IPにてMIGを追加し、ダブルクリックして所望の設定をしていきました。 最後にGenerateボタンで完了!

…と思ったら、Artix-7 MIG DDR3 fails to generate custom UI と同じ症状が発生して、生成に失敗します。

リンク先の情報によれば、Windows8、またVivado 2015.3では修正予定とあります。 質問した方はWindows8はやめてWindows7で動かすことにしているようです。

私が問題に遭遇した環境は、Windows10 Proです。しかもVivado 2016.4 / 2017.1どちらでも同じエラーが出てしまいます。 実際、リンク先の最後(2017/2/1付)の投稿を見ると、Windows10/Vivado 2016.4で同じエラーが出ている方がいるようです。

Xilinxのツールの質の低さには辟易しますね。デバイスの特性はIntel(旧Altera)より良いと思っているのですが、 これではぶち壊しです。