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>

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

comments powered by Disqus