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();
   }
}

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

一方、ブラウザ側は次のようになりました。Chart.jsのメモリリークの対応など、 こちらもそれなりに苦労しました。一旦Chartが生成されたら、データの更新はupdateを呼び出すことで行います。

decode_valuesは、8bytes(64bits)に12bitのADCデータ5個が詰めてあるのを分解しています。 データは2047から-2048の範囲となります。

<html>
    <head>
        <script
            src="https://code.jquery.com/jquery-3.2.1.js"
            integrity="sha256-DZAnKJ/6XZ9si04Hgrsxu/8s717jcIzLy3oi35EouyE="
            crossorigin="anonymous"></script>
        <script type="text/javascript" src="fft.js"></script>
        <script type="text/javascript" src="windowing.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.bundle.js"></script>
    </head>
    <body>
        <p><div id="time">Time[ms]</div></p>
        <button id="start">Start</button>
        <canvas id="chart" width="1280" height="600"></canvas>
        <script type="text/javascript">
            $(function() {
                const FFT_POINTS = 8192*2;

                var ctx = $('#chart')[0].getContext('2d');
                var chart = null;

                function decode_values(buf, len){
                    var i,j;
                    var data = new Array(len+4);

                    for (i = 0, j = 0; i < len; i += 5, j++){
                        var b0 = buf[j*8+0];
                        var b1 = buf[j*8+1];
                        var b2 = buf[j*8+2];
                        var b3 = buf[j*8+3];
                        var b4 = buf[j*8+4];
                        var b5 = buf[j*8+5];
                        var b6 = buf[j*8+6];
                        var b7 = buf[j*8+7];
                        data[i] = (b0 | ((b1 & 0xF) << 8));
                        data[i + 1] = (((b1 & 0xF0) >> 4) | (b2 << 4));
                        data[i + 2] = (b3 | ((b4 & 0xF) << 8));
                        data[i + 3] = (((b4 & 0xF0) >> 4) | (b5 << 4));
                        data[i + 4] = (b6 | ((b7 & 0xF) << 8));
                    }
                    // convert to signed
                    for (i = 0; i < len; i++){
                        if ((data[i] & 0x800) != 0){    // negative
                            data[i] = data[i] - 0x1000;
                        }
                    }
                    return data;
                }

                function convert_to_complex(data, imag, window, len){
                    for (var i = 0; i < FFT_POINTS; i++){
                        data[i] = data[i] / (1.0 * (1<<(12-1))); // ToDo: use window
                        imag[i] = 0.0;
                    }
                    data.length = imag.length;  // drop trailing data
                }

                function draw_charts(real, imag, scale, sample_freq){
                    var dat = new Array(real.length/2);
                    var s = sample_freq / real.length;
                    var labels = new Array(real.length/2);
                    for (var i = 0; i < real.length/2; i++){
                        real[i] *= scale;
                        imag[i] *= scale;
                        var val = Math.sqrt(real[i]*real[i]+imag[i]*imag[i]);
                        dat[i] = 20.0*Math.log10(val);
                        labels[i] = i*s;
                    }

                    if (chart == null){
                        chart = new Chart(ctx, {
                            type: 'line',
                            data: {
                                labels: labels,
                                datasets: [{
                                    label: 'FFT',
                                    data: dat,
                                    pointRadius: 0.0,
                                    fill: false,
                                    borderColor: "rgba(153,255,51,0.8)",
                                }]
                            },
                            options: {
                                legend: {
                                    display: false,
                                },
                                title: {
                                    display: true,
                                    text: 'FFT',
                                },
                                scales: {
                                    yAxes: [
                                        {
                                            scaleLabel: {
                                                display: true,
                                                labelString: '[dB]'
                                            }
                                        }
                                    ],
                                    xAxes: [
                                        {
                                            scaleLabel: {
                                                display: true,
                                                labelString: 'freq'
                                            }
                                        }
                                        ],
                                },
                                layout: {
                                    padding: {
                                        left: 20,
                                        right: 20,
                                    }
                                },
                                animation: {
                                    duration: 0,
                                },
                                elements: {
                                    line: {
                                        tension: 0, // disable bezier curves
                                    }
                                }
                            }
                        });
                    }else{  // already chart is populated
                        chart.data.datasets[0].data = dat;
                        chart.update();
                    }
                }

                var fft = new FFTNayuki(FFT_POINTS);
                var scale = 1.0 / Math.sqrt(FFT_POINTS);

                $('#start').click(function(){
                    var tm = 0;

                    var start_ms = performance.now();
                    var elapsed_ms;
                    var imag = new Array(FFT_POINTS);
                    var cn = new WebSocket('ws://192.168.0.90:3012/');

                    cn.addEventListener('message', function(ev){
                        var reader = new FileReader();
                        reader.addEventListener("loadend", function(){
                            var ary = new Uint8Array(reader.result);
                            var current = performance.now();

                            var data = decode_values(ary, FFT_POINTS);
                            convert_to_complex(data, imag, null, FFT_POINTS);
                            fft.forward(data, imag);

                            var current2 = performance.now();
                            draw_charts(data, imag, scale, 40.0);

                            var current3 = performance.now();

                            if (tm % 60 == 0){
                                var current = performance.now();
                                elapsed_ms = current - start_ms;
                                $("#time").text(elapsed_ms/60.0 + "[ms]");
                                start_ms = current;
                            }
                            getData();
                        });
                        reader.readAsArrayBuffer(ev.data);
                    });
                    cn.addEventListener('error', function(ev){
                        console.log(ev);
                    });

                    var getData = function(){
                        var abuf = new ArrayBuffer(1);
                        var view = new Uint8Array(abuf);
                        view[0] = tm & 0xFF;
                        cn.send(abuf);
                        tm++;
                    }
                    cn.addEventListener('open', function(){
                        getData();
                    });
                });
            });
        </script>
    </body>
</html>

まだ機能検証レベルのプログラムで、これからいろいろ調整が必要ですが、 とりあえずブラウザで目的が達成できそうだということが分かりました。

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には、

[dependencies]
ws = "*"

を追加します。

$ cargo build --target armv7-unknown-linux-gnueabihf --release

とすることで、x86環境のUbuntuでarmv7ターゲットのバイナリができます。

ちなみに、Rustはdebugビルドとreleaseビルドでの実行速度の差がかなり大きいようです。 自分の印象だと、よほどカリカリに数値計算などを行っていなければ、 C/C++などの言語では、体感2,3倍程度の速度差ですが、Rustは10倍位に感じます (あくまで印象です)。

$ cp target/armv7-unknown-linux-gnueabihf/debug/ws-rust /srv/root/

出来上がったバイナリを、NFSマウントされた場所にコピーし、Zynq上で実行しました。

HTMLは少し修正して、次のようにしました。

<html>
    <body>
        <script
            src="https://code.jquery.com/jquery-3.2.1.js"
            integrity="sha256-DZAnKJ/6XZ9si04Hgrsxu/8s717jcIzLy3oi35EouyE="
            crossorigin="anonymous"></script>
        <p><div id="time">Time[ms]</div></p>
        <canvas id="canvas" width="1920" height="1080"></canvas>
        <script type="text/javascript">
            $(window).on('load', function(){
                var tm = 0;
                var canvas = $('#canvas').get(0);
                var width = canvas.width;
                var height = canvas.height;
                var ctx = canvas.getContext('2d');
                var imageData = ctx.getImageData(0, 0, width, height);

                var buf = new ArrayBuffer(imageData.data.length);
                var buf8 = new Uint8ClampedArray(buf);
                var data = new Uint32Array(buf);
                var start_ms = performance.now();
                var elapsed_ms;
                var cn = new WebSocket('ws://192.168.0.90:3012/');

                cn.addEventListener('message', function(ev){
                    var reader = new FileReader();
                    reader.addEventListener("loadend", function(){
                        var ary = new Uint8Array(reader.result);
                        for (var j = 0; j < height; j++){
                            for (var i = 0; i < width; i++){
                                var val = ary[j*width+i];
                                data[j*width+i] = (255<<24)| (val<<16) | (val<<8) | val;
                            }
                        }
                        imageData.data.set(buf8);
                        ctx.putImageData(imageData, 0, 0);
                        if (tm % 60 == 0){
                            var current = performance.now();
                            elapsed_ms = current - start_ms;
                            $("#time").text(elapsed_ms/60.0 + "[ms]");
                            start_ms = current;
                        }
                    });
                    reader.readAsArrayBuffer(ev.data);
                    getData();
                });
                cn.addEventListener('error', function(ev){
                    console.log(ev);
                });

                var getData = function(){
                    var abuf = new ArrayBuffer(1);
                    var view = new Uint8Array(abuf);
                    view[0] = tm & 0xFF;
                    cn.send(abuf);
                    tm++;
                }
                cn.addEventListener('open', function(){
                    getData();
                });
            });
        </script>
    </body>
</html>

こちらをFirefoxで開くと、データ転送が始まって、画像が表示されました。

"動作画面"

200Mbps(25MB/s)程度のデータレートで転送されています。まぁまぁでしょうか。

これでWebSocketのサーバは簡単に作成できることが分かりましたので、 後はmmapやuioの制御のためのreadなどのsyscallがRustで実行できれば、 ADCのデータをブラウザまで送り込むことができそうです。