diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 diff --git a/.travis.yml b/.travis.yml old mode 100755 new mode 100644 index b699c5f..59f7b3c --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,10 @@ language: python python: - 2.7 - + - 3.7 + sudo: required -dist: trusty +dist: xenial before_install: - sudo add-apt-repository -y ppa:team-electronics/ppa @@ -11,26 +12,33 @@ before_install: install: - sudo apt-get install -qq libhdf5-serial-dev - - #- sudo apt-get -y install iverilog-daily + + #- sudo apt-get -y install iverilog-daily - sudo apt-get install gperf - git clone https://github.com/steveicarus/iverilog.git - cd iverilog && autoconf && ./configure && make && sudo make install && cd .. - + # Install conda - - wget http://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh + - wget http://repo.continuum.io/miniconda/Miniconda2-latest-Linux-x86_64.sh -O miniconda.sh - bash miniconda.sh -b -p $HOME/miniconda - export PATH=$HOME/miniconda/bin:$PATH - conda update --yes conda - - conda install --yes numpy bitarray pytest pyyaml pytables - - pip install cocotb - - # Install basil - - git clone -b v2.4.10 https://github.com/SiLab-Bonn/basil; cd basil; python setup.py develop; cd ..; + - conda create --yes -n test-environment python=$TRAVIS_PYTHON_VERSION pytest numpy psutil qtpy pyqt pyyaml pyzmq pytables + - source activate test-environment + - pip install cocotb==1.0.dev3 + - pip install xvfbwrapper # fake x server for Qt gui tests + + # Install basil + #- git clone -b v3.0.0 https://github.com/SiLab-Bonn/basil; cd basil; python setup.py develop; cd ..; + - pip install basil-daq>=3.0.0 - # Install tlu + # Install pytlu - python setup.py develop - -script: - - cd tests; py.test -s + # Install EUDAQ + - source tests/setup_eudaq.sh + +script: + - cd tests + - python --version + - pytest -s -v diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..574102d --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2015-2018 SiLab, Institute of Physics, University of Bonn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..6dc4c14 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include README.* VERSION requirements.txt +include pytlu/*.yaml +recursive-include pytlu/firmware * +recursive-include firmware * diff --git a/README.md b/README.md index b6b873a..66091dd 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,71 @@ -# pytlu +# pytlu [![Build Status](https://travis-ci.org/SiLab-Bonn/pytlu.svg?branch=master)](https://travis-ci.org/SiLab-Bonn/pytlu) -DAQ software and firmware for the [Trigger Logic Unit (TLU)](https://twiki.cern.ch/twiki/bin/view/MimosaTelescope/TLU). +DAQ software and firmware for the EUDAQ [Trigger Logic Unit (TLU)](https://twiki.cern.ch/twiki/bin/view/MimosaTelescope/TLU). ## Description The main features of the DAQ software and FPGA firmware are: -- protocol compatible with original firmware -- integrated TDC (1.5625ns resolution, 640 MHz) -- configurable input inversion -- configurable input acceptance based on the pulse width -- trigger acceptance based on input rising distance -- continuous data storage of accepted triggers (trigger id, timestamp, TDC) -- testbench for software and firmware -- example fpga receiver module : https://github.com/SiLab-Bonn/basil/tree/master/firmware/modules/tlu +- Protocol compatible with original EUDAQ TLU firmware +- Integrated TDC (1.5625ns resolution, 640 MHz) +- Configurable input inversion +- Configurable input acceptance based on the pulse width +- Trigger acceptance based on the distance of the input pulse leading edge +- Continuous data storage of accepted triggers (trigger ID, timestamp, TDC) +- Testbench for software and firmware +- Example FPGA module provided by basil: [TLU/trigger FSM](https://github.com/SiLab-Bonn/basil/tree/master/firmware/modules/tlu) +- Data monitoring provided by the [online_monitor](https://github.com/SiLab-Bonn/online_monitor) package -The data of all accepted triggers will be stored in a .h5 file. It contains the following data: +The data of all accepted triggers will be stored in a HDF5/PyTables file. It contains the following data: -- timestamp of the trigger (64 bit number, 40 MHz) -- trigger id (32 bit number) -- distance between leading edge of input pulse and generation of trigger signal for each input channel (each of them 8 bit numbers) +- Timestamp of the trigger (64-bit number, 40 MHz) +- Trigger ID (32-bit number) +- Distance between leading edge of input pulse and generation of trigger signal for each input channel (each of them 8-bit numbers) + + +## Online Monitor + +The pytlu online monitor displays the trigger rate vs. time. +![Pytlu online monitor](online_monitor.png) ## Installation -Install [conda](http://conda.pydata.org). +Installation of [Anaconda Python](https://www.anaconda.com/download) or [Miniconda Python](https://conda.io/miniconda.html) is recommended. -Install required packages: +Install dependencies: ```bash -conda install numpy bitarray pyyaml pytables +conda install numpy psutil qtpy pyqt pyyaml pyzmq pytables +pip install pyusb pySiLibUSB +pip install basil-daq>=3.0.0 ``` -Install pytlu via: +Install pytlu from PyPI: ```bash pip install pytlu ``` +or install pytlu from sources: +```bash +python setup.py develop +``` + For development/testing see [.travis.yml](https://github.com/SiLab-Bonn/pytlu/blob/master/.travis.yml) for details. +### USB Driver -If you use the TLU for the first time, you need to add a udev rule in order to set the correct permissions. Create the file `/etc/udev/rules.d/54-tlu.rules` and add the following lines: +Install libusb library by following the pySiLibUSB [installation guide](https://github.com/SiLab-Bonn/pySiLibUSB/wiki). +If you are using the TLU for the first time, you need to add a permanent udev rule in order to access the TLU. Create the file `/etc/udev/rules.d/tlu.rules` and add the following lines. +For a RedHat-based distribution (e.g., SL7/Centos 7) use: ``` -# for Red Hat, e.g. SL5 -SYSFS{idVendor}=="165d", SYSFS{idProduct}=="0001", GROUP="NOROOTUSB", ←- -MODE="0666" +SUBSYSTEM=="usb", ATTR{idVendor}=="165d", ATTR{idProduct}=="0001", GROUP="NOROOTUSB", MODE="0666" ``` -if you are using a Red Hat-based distribution or: +OR for a Debian-based distribution (e.g., Ubuntu) use: ``` -# for Debian -ACTION=="add", DRIVERS=="?*", ATTR{idVendor}=="165d", -ATTR{idProduct}=="0001", MODE="0666" +SUBSYSTEM=="usb", ATTR{idVendor}=="165d", ATTR{idProduct}=="0001", MODE="0666" ``` -in case you are using a debian-based distribution. ## Usage @@ -62,7 +74,99 @@ In order to get a description of the possible input arguments run: pytlu -h ``` +In order to start pytlu online monitor run: +```bash +pytlu_monitor +``` + Example: ```bash pytlu -t 10000 -c 10000 -oe CH1 --timeout 2 ``` + +## EUDAQ integration + +Pytlu can connect to the data acquisition framework [EUDAQ 1](https://github.com/eudaq/eudaq/tree/v1.x-dev), which is the common run control software used at *pixel test beams*. For the installation of EUDAQ 1.x please follow this [wiki](https://telescopes.desy.de/EUDAQ). To use the EUDAQ libraries within pytlu a [python wrapper](https://github.com/eudaq/eudaq/blob/v1.x-dev/python/PyEUDAQWrapper.py) is used. This wrapper is not build with default settings, thus the following cmake option must be specified when building EUDAQ `-DBUILD_python=ON`. + +### Example minimal installation +The following commands setup EUDAQ 1.x development version for pytlu with minimum requirements (no [ROOT](https://root.cern.ch/), no [Qt](https://www.qt.io/)) and are tested on *Ubuntu 14.04 LTS*. This installation is sufficient to use and test the pytlu eudaq producer. The paths `/home/user/git` have to be adjusted to your system, of course. +#### Install dependencies +EUDAQ needs a recent [cmake3](https://cmake.org/download/) version that might not be shipped with your distribution. This is a [known issue](https://github.com/eudaq/eudaq/issues/466). To install a recent version under Ubuntu do +```bash +wget https://cmake.org/files/v3.11/cmake-3.11.1.tar.gz +tar xf cmake-3.11.1.tar.gz +cd cmake-3.11.1 +./configure +make -j 4 +sudo apt-get install checkinstall +sudo checkinstall +``` +#### Install eudaq +```bash +git clone -b v1.x-dev https://github.com/eudaq/eudaq +cd eudaq/build +cmake -DBUILD_python=ON -DBUILD_gui=OFF -DBUILD_onlinemon=OFF -DBUILD_runsplitter=OFF -DUSE_ROOT=OFF .. +make install -j 4 +``` +The producer has to know the installation path of EUDAQ. One way is to specify `PYTHONPATH` to include the `python` folder in the EUDAQ directory, e.g.: +```bash +export PYTHONPATH="${PYTHONPATH}:/home/user/git/eudaq/python" +``` +This is not needed when you mention the path of the eudaq installation for every call to `pytlu_eudaq` (see below). + +**Run control GUI** + +When you want to have the run control GUI for more convenient testing change the CMAKE option to: +``` +-DBUILD_gui=ON +``` +Warning: you have to avoid calling `cmake` within the anaconda environment, since the anaconda QT5 version cannot easily be used to build code. + +### Usage with Pytlu +A simple command line interface is provided to start the pytlu producer: +```bash +pytlu_eudaq --help +``` +Please read the help output for program parameters. +Since there are configuration parameters which exist within the [EUDAQ TLU controller](https://github.com/eudaq/eudaq/blob/v1.x-dev/producers/tlu/src/TLUController.cc) but not within the +pytlu producer and vice versa, only the following configuration parameters can be set using the EUDAQ config file: + + - `TriggerInterval` + - `AndMask` + - `DutMask` + +These are mapped properly to the format needed by pytlu. Of course, the usual parameters supplied by pytlu can be specified, e.g.: + +``` +pytlu_eudaq --timeout 5 -f /path/to/data/ +``` + +Please note that the settings specified in the EUDAQ configuration file will overwrite the settings specified by the command line interface. + + +If you did not add the EUDAQ directory to the `PYTHONPATH` explicitly after installation (see above) you can give the path when running `pytlu_eudaq`, e.g.: + +``` +pytlu_eudaq --path /home/user/git/eudaq +``` + +### Debugging and testing +#### Replay feature +It is possible to replay a recorded pytlu raw data file with correct timing to test the system. This allows development and debugging without hardware. To replay a [pytlu raw data file](https://github.com/SiLab-Bonn/pytlu/blob/development/data/tlu_example_data.h5) one has to type: + +``` +pytlu_eudaq --replay /home/user/git/pytlu/data/tlu_example_data.h5 +``` + +Sometimes it is not needed to replay the data in real time. You can delay the data sending for every read out by an arbitrary time by specifying a `delay` parameter. For example to add a delay of one second you can type: + +``` +pytlu_eudaq --replay tlu_example_data.h5 --delay 1 +``` + +#### Unit test +A unit test is also available to test the complete chain: pytlu producer + DataConverter + Run Control. To test if everything is setup correctly open a console and make sure you added eudaq to you python path (see above). Then go to the [pytlu/tests](https://github.com/SiLab-Bonn/pytlu/tree/development/tests) folder and type +``` +python test_eudaq.py +``` +This test succeeds if everything is setup correctly. diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..3eefcb9 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0.0 diff --git a/data/tlu_example_data.h5 b/data/tlu_example_data.h5 new file mode 100644 index 0000000..8b6a3ab Binary files /dev/null and b/data/tlu_example_data.h5 differ diff --git a/firmware/ise/tlu.xise b/firmware/ise/tlu.xise old mode 100755 new mode 100644 diff --git a/firmware/src/ZestSC1.ucf b/firmware/src/ZestSC1.ucf old mode 100755 new mode 100644 diff --git a/firmware/src/stream_fifo/stream_fifo.v b/firmware/src/stream_fifo/stream_fifo.v old mode 100755 new mode 100644 index 852556a..cbb0d21 --- a/firmware/src/stream_fifo/stream_fifo.v +++ b/firmware/src/stream_fifo/stream_fifo.v @@ -1,9 +1,10 @@ /** * ------------------------------------------------------------ - * Copyright (c) All rights reserved + * Copyright (c) All rights reserved * SiLab, Institute of Physics, University of Bonn * ------------------------------------------------------------ */ + `timescale 1ps/1ps `default_nettype none @@ -27,7 +28,7 @@ module stream_fifo #( output wire [1:0] SRAM_BW_N, output wire SRAM_OE_N, output wire SRAM_WE_N, - + output wire FIFO_READ_NEXT_OUT, input wire FIFO_EMPTY_IN, input wire [15:0] FIFO_DATA, @@ -36,8 +37,6 @@ module stream_fifo #( input wire STREAM_READY, output wire STREAM_WRITE_N, output wire [15:0] STREAM_DATA - - ); wire IP_RD, IP_WR; @@ -61,8 +60,8 @@ bus_to_ip #( .IP_DATA_IN(IP_DATA_IN), .IP_DATA_OUT(IP_DATA_OUT) ); - - + + stream_fifo_core #( .ABUSWIDTH(ABUSWIDTH) ) stream_fifo_core ( @@ -81,16 +80,16 @@ stream_fifo_core #( .SRAM_BW_N(SRAM_BW_N), .SRAM_OE_N(SRAM_OE_N), .SRAM_WE_N(SRAM_WE_N), - + .FIFO_READ_NEXT_OUT(FIFO_READ_NEXT_OUT), .FIFO_EMPTY_IN(FIFO_EMPTY_IN), .FIFO_DATA(FIFO_DATA), - + .USB_STREAM_CLK(STREAM_CLK), .STREAM_READY(STREAM_READY), .STREAM_WRITE_N(STREAM_WRITE_N), .STREAM_DATA(STREAM_DATA) - + ); endmodule diff --git a/firmware/src/stream_fifo/stream_fifo_core.v b/firmware/src/stream_fifo/stream_fifo_core.v old mode 100755 new mode 100644 index 07aea31..d2579f7 --- a/firmware/src/stream_fifo/stream_fifo_core.v +++ b/firmware/src/stream_fifo/stream_fifo_core.v @@ -1,9 +1,10 @@ /** * ------------------------------------------------------------ - * Copyright (c) All rights reserved + * Copyright (c) All rights reserved * SiLab, Institute of Physics, University of Bonn * ------------------------------------------------------------ */ + `timescale 1ps/1ps `default_nettype none @@ -18,7 +19,7 @@ module stream_fifo_core input wire BUS_RST, input wire BUS_WR, input wire BUS_RD, - + output wire SRAM_CLK, output wire [22:0] SRAM_ADD, inout wire [17:0] SRAM_DATA, @@ -26,7 +27,7 @@ module stream_fifo_core output wire [1:0] SRAM_BW_N, output wire SRAM_OE_N, output wire SRAM_WE_N, - + output wire FIFO_READ_NEXT_OUT, input wire FIFO_EMPTY_IN, input wire [15:0] FIFO_DATA, @@ -35,24 +36,24 @@ module stream_fifo_core input wire STREAM_READY, output reg STREAM_WRITE_N, output reg [15:0] STREAM_DATA - + ); -localparam VERSION = 1; +localparam VERSION = 2; wire SOFT_RST, LOCKED; -assign SOFT_RST = (BUS_ADD==0 && BUS_WR); +assign SOFT_RST = (BUS_ADD==0 && BUS_WR); wire RST; -assign RST = BUS_RST | SOFT_RST | !LOCKED; +assign RST = BUS_RST | SOFT_RST | !LOCKED; -wire [23:0] CONF_SIZE_BYTE; // write data count in units of bytes -reg [22:0] CONF_SIZE; // in units of 2 bytes (16 bit) +wire [19:0] CONF_SIZE_BYTE; // write data count in units of bytes +reg [18:0] CONF_SIZE; // in units of 2 bytes (16 bit) assign CONF_SIZE_BYTE = CONF_SIZE * 2; -reg [23:0] CONF_SIZE_BYTE_BUF; - -reg [7:0] status_regs[8:0]; - +reg [11:0] CONF_SIZE_BYTE_BUF; + +reg [7:0] status_regs[3:0]; + always @(posedge BUS_CLK) begin if(RST) begin status_regs[0] <= 8'b0; @@ -69,7 +70,7 @@ assign CONF_READ_COUNT = {status_regs[3], status_regs[2], status_regs[1]}; reg [1:0] start_count_ff; always @(posedge BUS_CLK) - start_count_ff <= {start_count_ff[0], (BUS_ADD==3 && BUS_WR)}; + start_count_ff <= {start_count_ff[0], (BUS_ADD==3 && BUS_WR)}; wire START_COUNT = start_count_ff[1]==0 & start_count_ff[0]==1; @@ -83,14 +84,12 @@ always @ (posedge BUS_CLK) begin //(*) begin BUS_DATA_OUT <= CONF_READ_COUNT[15:8]; else if(BUS_ADD == 3) BUS_DATA_OUT <= CONF_READ_COUNT[23:16]; - else if(BUS_ADD == 4) BUS_DATA_OUT <= CONF_SIZE_BYTE[7:0]; // in units of bytes else if(BUS_ADD == 5) - BUS_DATA_OUT <= CONF_SIZE_BYTE_BUF[15:8]; + BUS_DATA_OUT <= CONF_SIZE_BYTE_BUF[7:0]; else if(BUS_ADD == 6) - BUS_DATA_OUT <= CONF_SIZE_BYTE_BUF[22:16]; - + BUS_DATA_OUT <= {4'b0, CONF_SIZE_BYTE_BUF[11:8]}; else BUS_DATA_OUT <= 8'b0; end @@ -98,8 +97,10 @@ end always @ (posedge BUS_CLK) begin - if (BUS_ADD == 4 && BUS_RD) - CONF_SIZE_BYTE_BUF <= CONF_SIZE_BYTE; + if (RST) + CONF_SIZE_BYTE_BUF <= 0; + else if (BUS_ADD == 4 && BUS_RD) + CONF_SIZE_BYTE_BUF <= CONF_SIZE_BYTE[19:8]; end wire U1_CLK0; @@ -107,48 +108,49 @@ wire STREAM_CLK, STREAM_CLK_FB; wire U1_CLK2X; DCM #( - .CLKDV_DIVIDE(6), - .CLKFX_DIVIDE(3), - .CLKFX_MULTIPLY(10), - .CLKIN_DIVIDE_BY_2("FALSE"), - .CLKIN_PERIOD(20.833), - .CLKOUT_PHASE_SHIFT("NONE"), - .CLK_FEEDBACK("1X"), - .DESKEW_ADJUST("SYSTEM_SYNCHRONOUS"), - .DFS_FREQUENCY_MODE("LOW"), - .DLL_FREQUENCY_MODE("LOW"), - .DUTY_CYCLE_CORRECTION("TRUE"), - .FACTORY_JF(16'h8080), - .PHASE_SHIFT(0), - .STARTUP_WAIT("TRUE") - ) DCM_BUS ( - .CLKFB(STREAM_CLK_FB), - .CLKIN(USB_STREAM_CLK), - .DSSEN(1'b0), - .PSCLK(1'b0), - .PSEN(1'b0), - .PSINCDEC(1'b0), - .RST(1'b0), - .CLKDV(), - .CLKFX(), - .CLKFX180(), - .CLK0(U1_CLK0), - .CLK2X(U1_CLK2X), - .CLK2X180(), - .CLK90(), - .CLK180(), - .CLK270(), - .LOCKED(LOCKED), - .PSDONE(), - .STATUS()); - - + .CLKDV_DIVIDE(6), + .CLKFX_DIVIDE(3), + .CLKFX_MULTIPLY(10), + .CLKIN_DIVIDE_BY_2("FALSE"), + .CLKIN_PERIOD(20.833), + .CLKOUT_PHASE_SHIFT("NONE"), + .CLK_FEEDBACK("1X"), + .DESKEW_ADJUST("SYSTEM_SYNCHRONOUS"), + .DFS_FREQUENCY_MODE("LOW"), + .DLL_FREQUENCY_MODE("LOW"), + .DUTY_CYCLE_CORRECTION("TRUE"), + .FACTORY_JF(16'h8080), + .PHASE_SHIFT(0), + .STARTUP_WAIT("TRUE") +) DCM_BUS ( + .CLKFB(STREAM_CLK_FB), + .CLKIN(USB_STREAM_CLK), + .DSSEN(1'b0), + .PSCLK(1'b0), + .PSEN(1'b0), + .PSINCDEC(1'b0), + .RST(1'b0), + .CLKDV(), + .CLKFX(), + .CLKFX180(), + .CLK0(U1_CLK0), + .CLK2X(U1_CLK2X), + .CLK2X180(), + .CLK90(), + .CLK180(), + .CLK270(), + .LOCKED(LOCKED), + .PSDONE(), + .STATUS() +); + + wire STREAM_CLK2X; BUFG CLK_STREAM_BUFG_INST (.I(U1_CLK0), .O(STREAM_CLK_FB)); assign STREAM_CLK = STREAM_CLK_FB; - + BUFG CLK_STREAM2X_BUFG_INST (.I(U1_CLK2X), .O(STREAM_CLK2X)); - + wire STREAM_RST; cdc_reset_sync rst_pulse_sync (.clk_in(BUS_CLK), .pulse_in(RST), .clk_out(STREAM_CLK), .pulse_out(STREAM_RST)); @@ -171,19 +173,19 @@ reg sram_read; wire sram_full, sram_write, sram_empty; reg [23:0] count; -always @(posedge STREAM_CLK) +always @(posedge STREAM_CLK) if(STREAM_RST) count <= 0; else if(!cdc_count_empty) count <= cdc_data_count/2; else if(sram_read & count!=0) count <= count - 1; - + reg [18:0] sram_rd_end; always @(posedge STREAM_CLK) if(STREAM_RST) sram_rd_end <= 0; - else if(!cdc_count_empty) + else if(!cdc_count_empty) sram_rd_end <= {sram_addr_wr[18:3], 3'b000}; reg sram_rd_addr_inc; @@ -234,9 +236,23 @@ reg [18:0] sram_size_stream; always @ (posedge STREAM_CLK) sram_size_stream <= sram_addr_wr - sram_addr_rd; -//TODO:This should be synchronized clock-domain-crossing, IT IS WRONG LIKE THIS -always @ (posedge BUS_CLK) - CONF_SIZE <= sram_size_stream; +reg [18:0] sram_size_gray; +always@(posedge STREAM_CLK) + sram_size_gray <= (sram_size_stream>>1) ^ sram_size_stream; + +reg [18:0] sram_size_gray_cdc0, sram_size_gray_cdc1; +always@(posedge BUS_CLK) begin + sram_size_gray_cdc0 <= sram_size_gray; + sram_size_gray_cdc1 <= sram_size_gray_cdc0; +end + +integer gbi; +always@(*) begin + CONF_SIZE[18] = sram_size_gray_cdc1[18]; + for(gbi = 17; gbi >= 0; gbi = gbi - 1) begin + CONF_SIZE[gbi] = sram_size_gray_cdc1[gbi] ^ CONF_SIZE[gbi + 1]; + end +end wire stream_data_valid; wire [15:0] sram_data_out; @@ -261,19 +277,19 @@ zbt_sram_ctl zbt_sram_ctl( .SRAM_BW_N(SRAM_BW_N), .SRAM_OE_N(SRAM_OE_N), .SRAM_WE_N(SRAM_WE_N) - ); +); reg is_data_out; -always @(negedge STREAM_CLK) +always @(negedge STREAM_CLK) if(STREAM_RST) is_data_out <= 0; else if(sram_read) is_data_out <= sram_rd_addr_inc; -always @(negedge STREAM_CLK) +always @(negedge STREAM_CLK) STREAM_DATA <= is_data_out ? sram_data_out : 16'b0; - -always @(negedge STREAM_CLK) + +always @(negedge STREAM_CLK) STREAM_WRITE_N <= !stream_data_valid; //This is one big HACK! @@ -285,7 +301,7 @@ always@(posedge STREAM_CLK) fsm <= fsm + 1; else if(STREAM_READY & count!=0) fsm <= 1; - + reg stream_ready_ff; always@(posedge STREAM_CLK) stream_ready_ff <= STREAM_READY; @@ -294,16 +310,16 @@ always@(posedge STREAM_CLK) sram_read <= fsm == 2 & stream_ready_ff; /* -`ifndef COCOTB_SIM +`ifndef COCOTB_SIM wire [35:0] control_bus; chipscope_icon ichipscope_icon ( .CONTROL0(control_bus) ); -chipscope_ila ichipscope_ila +chipscope_ila ichipscope_ila ( .CONTROL(control_bus), - .CLK(STREAM_CLK), + .CLK(STREAM_CLK), .TRIG0({count, stream_data_valid, sram_read, cdc_count_empty, stream_ready_ff}) ); diff --git a/firmware/src/stream_fifo/zbt_sram_ctr.v b/firmware/src/stream_fifo/zbt_sram_ctr.v old mode 100755 new mode 100644 index d520352..009fb50 --- a/firmware/src/stream_fifo/zbt_sram_ctr.v +++ b/firmware/src/stream_fifo/zbt_sram_ctr.v @@ -1,3 +1,12 @@ +/** + * ------------------------------------------------------------ + * Copyright (c) All rights reserved + * SiLab, Institute of Physics, University of Bonn + * ------------------------------------------------------------ + */ + +`timescale 1ps/1ps +`default_nettype none module zbt_sram_ctl( input wire CLK, CLK2X, @@ -25,7 +34,7 @@ reg [22:0] write_addr_q [3:0]; reg [3:0] we_q, re_q; ODDR sram_clk_oddr( - .D1(1'b0), .D2(1'b1), + .D1(1'b0), .D2(1'b1), .C(CLK2X), .CE(1'b1), .R(1'b0), .S(1'b0), .Q(SRAM_CLK) ); @@ -45,13 +54,13 @@ always @ (posedge CLK) begin end -always @ (posedge CLK) +always @ (posedge CLK) if(RESET) re_q <= 0; else re_q <= {re_q[2:0], RD}; - -always @ (posedge CLK) + +always @ (posedge CLK) if(RESET) we_q <= 0; else @@ -64,13 +73,13 @@ always @(posedge CLK) DATA_OUT_VALID <= re_q[0]; ODDR sram_we( - .D1(~we_q[0]), .D2(1'b1), + .D1(~we_q[0]), .D2(1'b1), .C(CLK), .CE(1'b1), .R(1'b0), .S(1'b0), .Q(SRAM_WE_N) ); ODDR sram_oe( - .D1(1'b1), .D2(1'b0), + .D1(1'b1), .D2(1'b0), .C(CLK), .CE(1'b1), .R(1'b0), .S(1'b0), .Q(SRAM_OE_N) ); @@ -80,7 +89,7 @@ generate for (ch = 0; ch < 23; ch = ch + 1) begin: addr_ch ODDR sram_addr( - .D1(write_addr_q[0][ch]), .D2(ADDR_RD[ch]), + .D1(write_addr_q[0][ch]), .D2(ADDR_RD[ch]), .C(CLK), .CE(1'b1), .R(1'b0), .S(1'b0), .Q(SRAM_ADD[ch]) ); @@ -88,7 +97,7 @@ end endgenerate reg [17:0] sram_data_out; -always @(posedge CLK2X) +always @(posedge CLK2X) sram_data_out <= {2'b00, write_data_q[1]}; reg out_en_pre = 0; @@ -98,7 +107,7 @@ always @(posedge CLK2X) else if(we_q[1]) out_en_pre <= 1; -reg out_en; +reg out_en; always @(posedge CLK2X) out_en <= out_en_pre; @@ -109,14 +118,14 @@ genvar ch_d; generate for (ch_d = 0; ch_d < 18; ch_d = ch_d + 1) begin: data_ch IDDR sram_data( - .Q1(), .Q2(DATA_2X[ch_d]), + .Q1(), .Q2(DATA_2X[ch_d]), .C(CLK2X), .CE(1'b1), .R(1'b0), .S(1'b0), .D(SRAM_DATA[ch_d]) ); end endgenerate -always @(posedge CLK) +always @(posedge CLK) DATA_OUT <= DATA_2X[15:0]; endmodule diff --git a/firmware/src/tlu.v b/firmware/src/tlu.v old mode 100755 new mode 100644 index a48177b..a460d71 --- a/firmware/src/tlu.v +++ b/firmware/src/tlu.v @@ -1,22 +1,26 @@ /** * ------------------------------------------------------------ - * Copyright (c) SILAB , Physics Institute of Bonn University + * Copyright (c) All rights reserved + * SiLab, Institute of Physics, University of Bonn * ------------------------------------------------------------ */ - + +`timescale 1ps/1ps +`default_nettype none + `include "utils/reset_gen.v" -`include "utils/bus_to_ip.v" +`include "utils/bus_to_ip.v" `include "utils/clock_divider.v" `include "utils/cdc_reset_sync.v" `include "utils/ddr_des.v" -`include "utils/cdc_syncfifo.v" -`include "utils/generic_fifo.v" +`include "utils/cdc_syncfifo.v" +`include "utils/generic_fifo.v" + +`include "gpio/gpio.v" -`include "gpio/gpio.v" - -`include "i2c/i2c.v" -`include "i2c/i2c_core.v" -`include "utils/cdc_pulse_sync.v" +`include "i2c/i2c.v" +`include "i2c/i2c_core.v" +`include "utils/cdc_pulse_sync.v" `include "tlu_clk_gen.v" @@ -33,79 +37,78 @@ `include "stream_fifo/zbt_sram_ctr.v" `ifdef COCOTB_SIM //for simulation - `include "utils/BUFG_sim.v" + `include "utils/BUFG_sim.v" `include "utils/IDDR_sim.v" `include "utils/DCM_sim.v" `include "utils/ODDR_sim.v" `else `include "utils/IDDR_s3.v" - `include "utils/ODDR_s3.v" + `include "utils/ODDR_s3.v" `endif - + module tlu ( - input wire BUS_CLK_IN, - input wire [15:0] USB_BUS_ADD, - inout wire [7:0] BUS_DATA, - input wire BUS_OE_N, - input wire BUS_RD_N, - input wire BUS_WR_N, - input wire BUS_CS_N, - - input wire USB_STREAM_CLK, - output wire [1:0] USB_STREAM_FIFOADDR, - output wire USB_STREAM_PKTEND_N, - input wire [2:0] USB_STREAM_FLAGS_N, - output wire USB_STREAM_SLOE_n, - output wire USB_STREAM_SLRD_n, - (* IOB="TRUE" *) - output wire USB_STREAM_SLWR_n, - (* IOB="TRUE" *) - output wire [15:0] USB_STREAM_DATA, - input wire USB_STREAM_FX2RDY, - - //IO - inout wire I2C_SCL_OUT, I2C_SDA_OUT, - input wire I2C_SCL_IN, I2C_SDA_IN, - output wire [1:0] I2C_SEL, - - output wire [5:0] DUT_TRIGGER, DUT_RESET, - input wire [5:0] DUT_BUSY, DUT_CLOCK, - input wire [3:0] BEAM_TRIGGER, - - output wire SRAM_CLK, - output wire [22:0] SRAM_ADD, - output wire SRAM_ADV_LD_N, - output wire [1:0] SRAM_BW_N, - inout wire [17:0] SRAM_DATA, - output wire SRAM_OE_N, - output wire SRAM_WE_N, - - output wire [3:0] DEBUG - + input wire BUS_CLK_IN, + input wire [15:0] USB_BUS_ADD, + inout wire [7:0] BUS_DATA, + input wire BUS_OE_N, + input wire BUS_RD_N, + input wire BUS_WR_N, + input wire BUS_CS_N, + + input wire USB_STREAM_CLK, + output wire [1:0] USB_STREAM_FIFOADDR, + output wire USB_STREAM_PKTEND_N, + input wire [2:0] USB_STREAM_FLAGS_N, + output wire USB_STREAM_SLOE_n, + output wire USB_STREAM_SLRD_n, + (* IOB="TRUE" *) + output wire USB_STREAM_SLWR_n, + (* IOB="TRUE" *) + output wire [15:0] USB_STREAM_DATA, + input wire USB_STREAM_FX2RDY, + + //IO + inout wire I2C_SCL_OUT, I2C_SDA_OUT, + input wire I2C_SCL_IN, I2C_SDA_IN, + output wire [1:0] I2C_SEL, + + output wire [5:0] DUT_TRIGGER, DUT_RESET, + input wire [5:0] DUT_BUSY, DUT_CLOCK, + input wire [3:0] BEAM_TRIGGER, + + output wire SRAM_CLK, + output wire [22:0] SRAM_ADD, + output wire SRAM_ADV_LD_N, + output wire [1:0] SRAM_BW_N, + inout wire [17:0] SRAM_DATA, + output wire SRAM_OE_N, + output wire SRAM_WE_N, + + output wire [3:0] DEBUG ); //assign DUT_TRIGGER = {BEAM_TRIGGER, BEAM_TRIGGER[1:0]}; //assign DUT_RESET = DUT_BUSY; -(* KEEP = "{TRUE}" *) -wire CLK320; -(* KEEP = "{TRUE}" *) +(* KEEP = "{TRUE}" *) +wire CLK320; +(* KEEP = "{TRUE}" *) wire CLK160; -(* KEEP = "{TRUE}" *) +(* KEEP = "{TRUE}" *) wire CLK40; -(* KEEP = "{TRUE}" *) +(* KEEP = "{TRUE}" *) wire CLK16; -(* KEEP = "{TRUE}" *) +(* KEEP = "{TRUE}" *) wire BUS_CLK; -(* KEEP = "{TRUE}" *) +(* KEEP = "{TRUE}" *) wire CLK8; -(* KEEP = "{TRUE}" *) +(* KEEP = "{TRUE}" *) wire I2C_CLK; wire CLK_LOCKED; - - tlu_clk_gen tlu_clk_gen( + +tlu_clk_gen tlu_clk_gen( .CLKIN(BUS_CLK_IN), .BUS_CLK(BUS_CLK), .U1_CLK8(CLK8), @@ -115,56 +118,53 @@ wire CLK_LOCKED; .U2_CLK320(CLK320), .U2_LOCKED(CLK_LOCKED) ); - + wire BUS_RST; reset_gen ireset_gen(.CLK(BUS_CLK), .RST(BUS_RST)); -// ------- BUS SYGNALING ------- // +// ------- BUS SIGNALING ------- // wire BUS_RD, BUS_WR; -wire [15:0] BUS_ADD; -assign BUS_RD = !BUS_RD_N && !BUS_CS_N && !BUS_OE_N; -assign BUS_WR = !BUS_WR_N && !BUS_CS_N; -assign BUS_ADD = USB_BUS_ADD - 16'h2000; +wire [15:0] BUS_ADD; +assign BUS_RD = !BUS_RD_N && !BUS_CS_N && !BUS_OE_N; +assign BUS_WR = !BUS_WR_N && !BUS_CS_N; +assign BUS_ADD = USB_BUS_ADD - 16'h2000; -// ------- MODULE ADREESSES ------- // -localparam VERSION = 8'h04; +// ------- MODULE ADDRESSES ------- // +localparam VERSION = 8'h05; localparam GPIO_BASEADDR = 16'h3000; -localparam GPIO_HIGHADDR = 16'h4000-1; - +localparam GPIO_HIGHADDR = 16'h4000-1; + localparam I2C_BASEADDR = 16'h4000; localparam I2C_HIGHADDR = 16'h5000-1; localparam TLU_MASTER_BASEADDR = 16'h5000; localparam TLU_MASTER_HIGHADDR = 16'h6000 - 1; - + localparam PULSE_TEST_BASEADDR = 16'h6000; localparam PULSE_TEST_HIGHADDR = 16'h7000 - 1; - + localparam FIFO_BASEADDR = 16'h7000; localparam FIFO_HIGHADDR = 16'h8000 - 1; - - -// ------- MODULES ------- // - -reg RD_VERSION; -always@(posedge BUS_CLK) - if(BUS_ADD == 16'h2000 && BUS_RD) - RD_VERSION <= 1; - else - RD_VERSION <= 0; - -assign BUS_DATA = (RD_VERSION) ? VERSION : 8'bz; + + +// ------- MODULES ------- // +reg RD_VERSION; +always@(posedge BUS_CLK) + if(BUS_ADD == 16'h2000 && BUS_RD) + RD_VERSION <= 1; + else + RD_VERSION <= 0; + +assign BUS_DATA = (RD_VERSION) ? VERSION : 8'bz; wire [7:0] GPIO; -gpio -#( - .BASEADDR(GPIO_BASEADDR), +gpio #( + .BASEADDR(GPIO_BASEADDR), .HIGHADDR(GPIO_HIGHADDR), .IO_WIDTH(8), .IO_DIRECTION(8'hff) -) i_gpio -( +) i_gpio ( .BUS_CLK(BUS_CLK), .BUS_RST(BUS_RST), .BUS_ADD(BUS_ADD), @@ -172,59 +172,53 @@ gpio .BUS_RD(BUS_RD), .BUS_WR(BUS_WR), .IO(GPIO[7:0]) -); +); +assign I2C_SEL = GPIO[1:0]; - -assign I2C_SEL = GPIO[1:0]; - -wire I2C_CLK_PRE; -clock_divider #( .DIVISOR(480) ) i2c_clkdev ( .CLK(BUS_CLK), .RESET(BUS_RST), .CE(), .CLOCK(I2C_CLK_PRE) ); +wire I2C_CLK_PRE; +clock_divider #( .DIVISOR(480) ) i2c_clkdev ( .CLK(BUS_CLK), .RESET(BUS_RST), .CE(), .CLOCK(I2C_CLK_PRE) ); `ifdef COCOTB_SIM //for simulation - BUFG BUFG_I2C ( .O(I2C_CLK), .I(BUS_CLK) ); + BUFG BUFG_I2C ( .O(I2C_CLK), .I(BUS_CLK) ); `else BUFG BUFG_I2C ( .O(I2C_CLK), .I(I2C_CLK_PRE) ); `endif - -i2c -#( - .BASEADDR(I2C_BASEADDR), - .HIGHADDR(I2C_HIGHADDR), - .MEM_BYTES(8), - .IGNORE_ACK(1) -) i_i2c_out -( - .BUS_CLK(BUS_CLK), - .BUS_RST(BUS_RST), - .BUS_ADD(BUS_ADD), - .BUS_DATA(BUS_DATA), - .BUS_RD(BUS_RD), - .BUS_WR(BUS_WR), - - .I2C_CLK(I2C_CLK), - .I2C_SDA(I2C_SDA_OUT), - .I2C_SCL(I2C_SCL_OUT) -); - -//THIS CANOT BE DONE BCAUSE THE PCB DESIGN IS WRONG!!! -//assign I2C_SDA_OUT = I2C_SDA_IN ? 1'bz : 1'b0; -//assign I2C_SCL_OUT = I2C_SCL_IN ? 1'bz : 1'b0; - -`ifndef COCOTB_SIM //for simulation - PULLUP isda (.O(I2C_SDA_OUT)); - PULLUP iscl (.O(I2C_SCL_OUT)); -`else - pullup isda (I2C_SDA_OUT); - pullup iscl (I2C_SCL_OUT); + +i2c #( + .BASEADDR(I2C_BASEADDR), + .HIGHADDR(I2C_HIGHADDR), + .MEM_BYTES(8), + .IGNORE_ACK(1) +) i_i2c_out ( + .BUS_CLK(BUS_CLK), + .BUS_RST(BUS_RST), + .BUS_ADD(BUS_ADD), + .BUS_DATA(BUS_DATA), + .BUS_RD(BUS_RD), + .BUS_WR(BUS_WR), + + .I2C_CLK(I2C_CLK), + .I2C_SDA(I2C_SDA_OUT), + .I2C_SCL(I2C_SCL_OUT) +); + +//THIS CANOT BE DONE BCAUSE THE PCB DESIGN IS WRONG!!! +//assign I2C_SDA_OUT = I2C_SDA_IN ? 1'bz : 1'b0; +//assign I2C_SCL_OUT = I2C_SCL_IN ? 1'bz : 1'b0; + +`ifndef COCOTB_SIM //for simulation + PULLUP isda (.O(I2C_SDA_OUT)); + PULLUP iscl (.O(I2C_SCL_OUT)); +`else + pullup isda (I2C_SDA_OUT); + pullup iscl (I2C_SCL_OUT); `endif wire TEST_PULSE; -pulse_gen -#( - .BASEADDR(PULSE_TEST_BASEADDR), +pulse_gen #( + .BASEADDR(PULSE_TEST_BASEADDR), .HIGHADDR(PULSE_TEST_HIGHADDR) -) i_pulse_gen -( +) i_pulse_gen ( .BUS_CLK(BUS_CLK), .BUS_RST(BUS_RST), .BUS_ADD(BUS_ADD), @@ -236,7 +230,7 @@ pulse_gen .EXT_START(1'b0), .PULSE(TEST_PULSE) ); - + wire TDC_MASTER_FIFO_READ, TDC_MASTER_FIFO_EMPTY; wire [15:0] TDC_MASTER_FIFO_DATA; @@ -247,9 +241,9 @@ tlu_master #( .CLK320(CLK320), .CLK160(CLK160), .CLK40(CLK40), - + .TEST_PULSE(TEST_PULSE), - .DUT_TRIGGER(DUT_TRIGGER), .DUT_RESET(DUT_RESET), + .DUT_TRIGGER(DUT_TRIGGER), .DUT_RESET(DUT_RESET), .DUT_BUSY(DUT_BUSY), .DUT_CLOCK(DUT_CLOCK), .BEAM_TRIGGER(BEAM_TRIGGER), @@ -266,13 +260,11 @@ tlu_master #( ); wire STREAM_WRITE_N; -wire STREAM_READY; -stream_fifo -#( - .BASEADDR(FIFO_BASEADDR), +wire STREAM_READY; +stream_fifo #( + .BASEADDR(FIFO_BASEADDR), .HIGHADDR(FIFO_HIGHADDR) ) stream_fifo ( - .BUS_CLK(BUS_CLK), .BUS_RST(BUS_RST), .BUS_ADD(BUS_ADD), @@ -291,13 +283,12 @@ stream_fifo .SRAM_BW_N(SRAM_BW_N), .SRAM_OE_N(SRAM_OE_N), .SRAM_WE_N(SRAM_WE_N), - + .STREAM_CLK(USB_STREAM_CLK), .STREAM_READY(STREAM_READY), .STREAM_WRITE_N(STREAM_WRITE_N), .STREAM_DATA(USB_STREAM_DATA) - - ); +); assign USB_STREAM_PKTEND_N = 1'b1; @@ -305,28 +296,28 @@ assign USB_STREAM_FIFOADDR = 2'b10; assign USB_STREAM_SLRD_n = 1'b1; assign USB_STREAM_SLOE_n = 1'b1; assign USB_STREAM_SLWR_n = STREAM_WRITE_N; -assign STREAM_READY = (USB_STREAM_FLAGS_N[1] == 1'b1) & (USB_STREAM_FX2RDY == 1'b1); +assign STREAM_READY = (USB_STREAM_FLAGS_N[1] == 1'b1) & (USB_STREAM_FX2RDY == 1'b1); -/* +/* wire [35:0] control_bus; chipscope_icon ichipscope_icon ( .CONTROL0(control_bus) ); -chipscope_ila ichipscope_ila +chipscope_ila ichipscope_ila ( .CONTROL(control_bus), - .CLK(USB_STREAM_CLK), + .CLK(USB_STREAM_CLK), .TRIG0({USB_STREAM_DATA, USB_STREAM_FLAGS_N,USB_STREAM_FX2RDY, USB_STREAM_SLWR_n, USB_STREAM_CLK, BUS_CLK}) -); +); +*/ + +/* +assign DEBUG[0] = STREAM_WRITE; +assign DEBUG[1] = STREAM_READY; +assign DEBUG[2] = USB_STREAM_CLK; +assign DEBUG[3] = BUS_CLK; */ - -/* -assign DEBUG[0] = STREAM_WRITE; -assign DEBUG[1] = STREAM_READY; -assign DEBUG[2] = USB_STREAM_CLK; -assign DEBUG[3] = BUS_CLK; -*/ -//assign DEBUG = 0; +//assign DEBUG = 0; endmodule diff --git a/firmware/src/tlu_clk_gen.v b/firmware/src/tlu_clk_gen.v old mode 100755 new mode 100644 index d38afad..7fb8f59 --- a/firmware/src/tlu_clk_gen.v +++ b/firmware/src/tlu_clk_gen.v @@ -1,13 +1,12 @@ /** * ------------------------------------------------------------ - * Copyright (c) All rights reserved + * Copyright (c) All rights reserved * SiLab, Institute of Physics, University of Bonn * ------------------------------------------------------------ */ - `timescale 1ns / 1ps - + module tlu_clk_gen( input wire CLKIN, output wire BUS_CLK, @@ -17,7 +16,7 @@ module tlu_clk_gen( output wire U2_CLK160, output wire U2_CLK320, output wire U2_LOCKED - ); +); wire U1_CLK0, U1_CLK0_BUF, U1_CLKDV, U1_CLKDV_BUF; wire U2_CLKDV_BUF, U2_CLK0_BUF, U2_CLK2X_BUF, U2_CLKFX_BUF; @@ -28,7 +27,7 @@ module tlu_clk_gen( assign U2_CLK160 = U2_CLK0_BUF; assign U2_CLK320 = U2_CLK2X_BUF; assign U1_CLK8 = U1_CLKDV_BUF; - + BUFG CLKFB_BUFG_INST (.I(U1_CLK0), .O(U1_CLK0_BUF)); BUFG CLKDV_BUFG_INST (.I(U1_CLKDV), .O(U1_CLKDV_BUF)); @@ -53,26 +52,26 @@ module tlu_clk_gen( .PHASE_SHIFT(0), // Amount of fixed phase shift from -255 to 255 .STARTUP_WAIT("TRUE") // Delay configuration DONE until DCM_SP LOCK, TRUE/FALSE ) DCM_BUS ( - .CLKFB(U1_CLK0_BUF), - .CLKIN(CLKIN), - .DSSEN(1'b0), - .PSCLK(1'b0), - .PSEN(1'b0), - .PSINCDEC(1'b0), + .CLKFB(U1_CLK0_BUF), + .CLKIN(CLKIN), + .DSSEN(1'b0), + .PSCLK(1'b0), + .PSEN(1'b0), + .PSINCDEC(1'b0), .RST(1'b0), .CLKDV(U1_CLKDV), - .CLKFX(U1_CLKFX), - .CLKFX180(), - .CLK0(U1_CLK0), - .CLK2X(), - .CLK2X180(), - .CLK90(), - .CLK180(), - .CLK270(), - .LOCKED(U1_LOCKED), - .PSDONE(), + .CLKFX(U1_CLKFX), + .CLKFX180(), + .CLK0(U1_CLK0), + .CLK2X(), + .CLK2X180(), + .CLK90(), + .CLK180(), + .CLK270(), + .LOCKED(U1_LOCKED), + .PSDONE(), .STATUS()); - + wire U1_CLKFX_BUF, U2_CLKDV; wire U2_CLK0, U2_CLKFX, U2_CLK2X; //BUFG CLKFX_2_BUFG_INST (.I(U1_CLKFX), .O(U1_CLKFX_BUF)); @@ -81,7 +80,7 @@ module tlu_clk_gen( BUFG CLKFB_2_BUFG_INST (.I(U2_CLK0), .O(U2_CLK0_BUF)); BUFG CLKFX2_2_BUFG_INST (.I(U2_CLKFX), .O(U2_CLKFX_BUF)); BUFG U2_CLK2X_INST (.I(U2_CLK2X), .O(U2_CLK2X_BUF)); - + DCM #( .CLKDV_DIVIDE(10), // Divide by: 1.5,2.0,2.5,3.0,3.5,4.0,4.5,5.0,5.5,6.0,6.5 // 7.0,7.5,8.0,9.0,10.0,11.0,12.0,13.0,14.0,15.0 or 16.0 @@ -100,7 +99,7 @@ module tlu_clk_gen( .PHASE_SHIFT(0), // Amount of fixed phase shift from -255 to 255 .STARTUP_WAIT("FALSE") // Delay configuration DONE until DCM_SP LOCK, TRUE/FALSE ) DCM_U2 ( - .DSSEN(1'b0), + .DSSEN(1'b0), .CLK0(U2_CLK0), // 0 degree DCM_SP CLK output .CLK180(), // 180 degree DCM_SP CLK output .CLK270(), // 270 degree DCM_SP CLK output @@ -120,6 +119,6 @@ module tlu_clk_gen( .PSINCDEC(1'b0), // Dynamic phase adjust increment/decrement .RST(!U1_LOCKED)// // DCM_SP asynchronous reset input ); - + endmodule diff --git a/firmware/src/tlu_master/tlu_ch_rx.v b/firmware/src/tlu_master/tlu_ch_rx.v old mode 100755 new mode 100644 index d750149..763707d --- a/firmware/src/tlu_master/tlu_ch_rx.v +++ b/firmware/src/tlu_master/tlu_ch_rx.v @@ -124,4 +124,4 @@ always@(posedge CLK40) LAST_RISING_REL <= LAST_RISING_RELATIVE; -endmodule +endmodule \ No newline at end of file diff --git a/firmware/src/tlu_master/tlu_master.v b/firmware/src/tlu_master/tlu_master.v old mode 100755 new mode 100644 diff --git a/firmware/src/tlu_master/tlu_master_core.v b/firmware/src/tlu_master/tlu_master_core.v old mode 100755 new mode 100644 index 50d9074..21d9207 --- a/firmware/src/tlu_master/tlu_master_core.v +++ b/firmware/src/tlu_master/tlu_master_core.v @@ -1,6 +1,6 @@ /** * ------------------------------------------------------------ - * Copyright (c) All rights reserved + * Copyright (c) All rights reserved * SiLab, Institute of Physics, University of Bonn * ------------------------------------------------------------ */ @@ -14,9 +14,9 @@ module tlu_master_core input wire CLK320, input wire CLK160, input wire CLK40, - + input wire TEST_PULSE, - output wire [5:0] DUT_TRIGGER, DUT_RESET, + output wire [5:0] DUT_TRIGGER, DUT_RESET, input wire [5:0] DUT_BUSY, DUT_CLOCK, input wire [3:0] BEAM_TRIGGER, @@ -31,26 +31,25 @@ module tlu_master_core input wire BUS_RST, input wire BUS_WR, input wire BUS_RD - + ); -localparam VERSION = 1; +localparam VERSION = 3; wire SOFT_RST, START; -assign SOFT_RST = (BUS_ADD==0 && BUS_WR); +assign SOFT_RST = (BUS_ADD==0 && BUS_WR); assign START = (BUS_ADD==1 && BUS_WR); - + wire RST; -assign RST = BUS_RST | SOFT_RST; +assign RST = BUS_RST | SOFT_RST; reg [7:0] status_regs[9:0]; -wire CONF_DONE; wire [3:0] CONF_EN_INPUT; assign CONF_EN_INPUT = status_regs[3][3:0]; wire [3:0] CONF_INPUT_INVERT; assign CONF_INPUT_INVERT = status_regs[3][7:4]; - + wire [4:0] CONF_MAX_LE_DISTANCE; assign CONF_MAX_LE_DISTANCE = status_regs[4][4:0]; wire [4:0] CONF_DIG_TH_INPUT; @@ -59,19 +58,22 @@ wire [4:0] CONF_N_BITS_TRIGGER_ID; assign CONF_N_BITS_TRIGGER_ID = status_regs[9][4:0]; wire [5:0] CONF_EN_OUTPUT; assign CONF_EN_OUTPUT = status_regs[6][5:0]; -reg [7:0] SKIP_TRIG_COUNTER; +reg [31:0] SKIP_TRIG_COUNTER, SKIP_TRIG_COUNTER_SYNC; reg [7:0] TIMEOUT_COUNTER; wire [15:0] CONF_TIME_OUT; assign CONF_TIME_OUT = {status_regs[8], status_regs[7]}; - -reg [63:0] TIME_STAMP; -reg [31:0] TRIG_ID; - -reg [7:0] LOST_DATA_CNT; -reg [63:0] TIME_STAMP_BUF; -reg [31:0] TRIG_ID_BUF; - + +wire [2:0] TX_STATE[5:0]; + +reg [63:0] TIME_STAMP, TIME_STAMP_SYNC; +reg [31:0] TRIG_ID, TRIG_ID_SYNC; + +reg [7:0] LOST_DATA_CNT; +reg [63-8:0] TIME_STAMP_BUF; +reg [31-8:0] TRIG_ID_BUF; +reg [31-8:0] SKIP_TRIG_COUNTER_BUF; + always @(posedge BUS_CLK) begin if(RST) begin status_regs[0] <= 8'b0; @@ -83,7 +85,7 @@ always @(posedge BUS_CLK) begin status_regs[6] <= 8'b0; status_regs[7] <= 8'hff; //TIMEOUT status_regs[8] <= 8'hff; - status_regs[9] <= 8'b0; //N_BITS_TRIGGER_ID + status_regs[9] <= 8'd15; //N_BITS_TRIGGER_ID default is 15 end else if(BUS_WR && BUS_ADD < 10) status_regs[BUS_ADD[3:0]] <= BUS_DATA_IN; @@ -94,9 +96,9 @@ always @(posedge BUS_CLK) begin if (BUS_ADD == 0) BUS_DATA_OUT <= VERSION; else if(BUS_ADD == 1) - BUS_DATA_OUT <= {7'b0, CONF_DONE}; + BUS_DATA_OUT <= {8'b0}; // start else if(BUS_ADD == 2) - BUS_DATA_OUT <= {8'b0}; //TODO: MODE; + BUS_DATA_OUT <= {8'b0}; // not used, TODO: MODE else if(BUS_ADD == 3) BUS_DATA_OUT <= {CONF_INPUT_INVERT, CONF_EN_INPUT}; else if(BUS_ADD == 4) @@ -111,56 +113,133 @@ always @(posedge BUS_CLK) begin BUS_DATA_OUT <= CONF_TIME_OUT[15:8]; else if(BUS_ADD == 9) BUS_DATA_OUT <= {3'b0, CONF_N_BITS_TRIGGER_ID}; - else if(BUS_ADD == 16) BUS_DATA_OUT <= TIME_STAMP[7:0]; else if(BUS_ADD == 17) - BUS_DATA_OUT <= TIME_STAMP_BUF[15:8]; + BUS_DATA_OUT <= TIME_STAMP_BUF[7:0]; else if(BUS_ADD == 18) - BUS_DATA_OUT <= TIME_STAMP_BUF[23:16]; + BUS_DATA_OUT <= TIME_STAMP_BUF[15:8]; else if(BUS_ADD == 19) - BUS_DATA_OUT <= TIME_STAMP_BUF[31:24]; + BUS_DATA_OUT <= TIME_STAMP_BUF[23:16]; else if(BUS_ADD == 20) - BUS_DATA_OUT <= TIME_STAMP_BUF[39:32]; + BUS_DATA_OUT <= TIME_STAMP_BUF[31:24]; else if(BUS_ADD == 21) - BUS_DATA_OUT <= TIME_STAMP_BUF[47:40]; + BUS_DATA_OUT <= TIME_STAMP_BUF[39:32]; else if(BUS_ADD == 22) - BUS_DATA_OUT <= TIME_STAMP_BUF[55:48]; + BUS_DATA_OUT <= TIME_STAMP_BUF[47:40]; else if(BUS_ADD == 23) - BUS_DATA_OUT <= TIME_STAMP_BUF[63:56]; + BUS_DATA_OUT <= TIME_STAMP_BUF[55:48]; else if(BUS_ADD == 24) BUS_DATA_OUT <= TRIG_ID[7:0]; else if(BUS_ADD == 25) - BUS_DATA_OUT <= TRIG_ID_BUF[15:8]; + BUS_DATA_OUT <= TRIG_ID_BUF[7:0]; else if(BUS_ADD == 26) - BUS_DATA_OUT <= TRIG_ID_BUF[23:16]; + BUS_DATA_OUT <= TRIG_ID_BUF[15:8]; else if(BUS_ADD == 27) - BUS_DATA_OUT <= TRIG_ID_BUF[31:24]; + BUS_DATA_OUT <= TRIG_ID_BUF[23:16]; else if(BUS_ADD == 28) - BUS_DATA_OUT <= SKIP_TRIG_COUNTER; + BUS_DATA_OUT <= SKIP_TRIG_COUNTER[7:0]; else if(BUS_ADD == 29) - BUS_DATA_OUT <= TIMEOUT_COUNTER; + BUS_DATA_OUT <= SKIP_TRIG_COUNTER_BUF[7:0]; else if(BUS_ADD == 30) + BUS_DATA_OUT <= SKIP_TRIG_COUNTER_BUF[15:8]; + else if(BUS_ADD == 31) + BUS_DATA_OUT <= SKIP_TRIG_COUNTER_BUF[23:16]; + else if(BUS_ADD == 32) + BUS_DATA_OUT <= TIMEOUT_COUNTER; + else if(BUS_ADD == 33) BUS_DATA_OUT <= LOST_DATA_CNT; + else if(BUS_ADD == 34) + BUS_DATA_OUT <= {1'b0,TX_STATE[1],1'b0,TX_STATE[0]}; + else if(BUS_ADD == 35) + BUS_DATA_OUT <= {1'b0,TX_STATE[3],1'b0,TX_STATE[2]}; + else if(BUS_ADD == 36) + BUS_DATA_OUT <= {1'b0,TX_STATE[5],1'b0,TX_STATE[4]}; else BUS_DATA_OUT <= 0; end end -//TODO: THIS SHULD BE GRAY CODED ETC.... for CDC -always @ (posedge BUS_CLK) begin +// Gray-code for CDC +always @ (posedge BUS_CLK) +begin if (RST) - TIME_STAMP_BUF <= 32'b0; + TIME_STAMP_BUF <= 0; else if (BUS_ADD == 16 && BUS_RD) - TIME_STAMP_BUF <= TIME_STAMP; + TIME_STAMP_BUF <= TIME_STAMP[63:8]; end -always @ (posedge BUS_CLK) begin + +reg [63:0] time_stamp_gray; +always@(posedge CLK40) + time_stamp_gray <= (TIME_STAMP_SYNC>>1) ^ TIME_STAMP_SYNC; + +reg [63:0] time_stamp_gray_cdc0, time_stamp_gray_cdc1; +always@(posedge BUS_CLK) begin + time_stamp_gray_cdc0 <= time_stamp_gray; + time_stamp_gray_cdc1 <= time_stamp_gray_cdc0; +end + +integer gbi_ts; +always@(*) begin + TIME_STAMP[63] = time_stamp_gray_cdc1[63]; + for(gbi_ts = 62; gbi_ts >= 0; gbi_ts = gbi_ts - 1) begin + TIME_STAMP[gbi_ts] = time_stamp_gray_cdc1[gbi_ts] ^ TIME_STAMP[gbi_ts + 1]; + end +end + +always @ (posedge BUS_CLK) +begin if (RST) - TRIG_ID_BUF <= 32'b0; + TRIG_ID_BUF <= 0; else if (BUS_ADD == 24 && BUS_RD) - TRIG_ID_BUF <= TRIG_ID; + TRIG_ID_BUF <= TRIG_ID[31:8]; +end + +reg [31:0] trigger_id_gray; +always@(posedge CLK40) + trigger_id_gray <= (TRIG_ID_SYNC>>1) ^ TRIG_ID_SYNC; + +reg [31:0] trigger_id_gray_cdc0, trigger_id_gray_cdc1; +always@(posedge BUS_CLK) begin + trigger_id_gray_cdc0 <= trigger_id_gray; + trigger_id_gray_cdc1 <= trigger_id_gray_cdc0; +end + +integer gbi_id; +always@(*) begin + TRIG_ID[31] = trigger_id_gray_cdc1[31]; + for(gbi_id = 30; gbi_id >= 0; gbi_id = gbi_id - 1) begin + TRIG_ID[gbi_id] = trigger_id_gray_cdc1[gbi_id] ^ TRIG_ID[gbi_id + 1]; + end +end + +always @ (posedge BUS_CLK) +begin + if (RST) + SKIP_TRIG_COUNTER_BUF <= 0; + else if (BUS_ADD == 28 && BUS_RD) + SKIP_TRIG_COUNTER_BUF <= SKIP_TRIG_COUNTER[31:8]; end +reg [31:0] skip_trigger_gray; +always@(posedge CLK40) + skip_trigger_gray <= (SKIP_TRIG_COUNTER_SYNC>>1) ^ SKIP_TRIG_COUNTER_SYNC; + +reg [31:0] skip_trigger_gray_cdc0, skip_trigger_gray_cdc1; +always@(posedge BUS_CLK) begin + skip_trigger_gray_cdc0 <= skip_trigger_gray; + skip_trigger_gray_cdc1 <= skip_trigger_gray_cdc0; +end + +integer gbi_skip; +always@(*) begin + SKIP_TRIG_COUNTER[31] = skip_trigger_gray_cdc1[31]; + for(gbi_skip = 30; gbi_skip >= 0; gbi_skip = gbi_skip - 1) begin + SKIP_TRIG_COUNTER[gbi_skip] = skip_trigger_gray_cdc1[gbi_skip] ^ SKIP_TRIG_COUNTER[gbi_skip + 1]; + end +end + + wire RST_SYNC; cdc_reset_sync rst_pulse_sync (.clk_in(BUS_CLK), .pulse_in(RST), .clk_out(CLK40), .pulse_out(RST_SYNC)); @@ -168,42 +247,42 @@ wire START_SYNC; cdc_pulse_sync start_pulse_sync (.clk_in(BUS_CLK), .pulse_in(START), .clk_out(CLK40), .pulse_out(START_SYNC)); -wire [7:0] LAST_RISING_REL [3:0]; +wire [7:0] LAST_RISING_REL [3:0]; wire [3:0] VALID; - + always@(posedge CLK40) if(RST_SYNC || START_SYNC) - TIME_STAMP <= 1; - else if(TIME_STAMP != 64'hffffffff_ffffffff) - TIME_STAMP <= TIME_STAMP + 1; + TIME_STAMP_SYNC <= 1; + else if(TIME_STAMP_SYNC != 64'hffffffff_ffffffff) + TIME_STAMP_SYNC <= TIME_STAMP_SYNC + 1; genvar ch; generate for (ch = 0; ch < 4; ch = ch + 1) begin: tlu_ch - + tlu_ch_rx tlu_ch_rx ( .RST(RST_SYNC), - + .CLK320(CLK320), .CLK160(CLK160), .CLK40(CLK40), - .TIME_STAMP(TIME_STAMP[3:0]), - + .TIME_STAMP(TIME_STAMP_SYNC[3:0]), + .EN_INVERT(CONF_INPUT_INVERT[ch]), .TLU_IN(BEAM_TRIGGER[ch]), - + .DIG_TH(CONF_DIG_TH_INPUT), .EN(CONF_EN_INPUT[ch]), - + .VALID(VALID[ch]), - .LAST_RISING(), - .LAST_FALLING(), + .LAST_RISING(), + .LAST_FALLING(), .LAST_TOT(), .LAST_RISING_REL(LAST_RISING_REL[ch]) ); end -endgenerate - +endgenerate + reg [7:0] MIN_LE; integer imin; @@ -245,62 +324,59 @@ wire SKIP_TRIGGER = TRIG_PULSE & !GEN_TRIG_PULSE; always@(posedge CLK40) if(RST_SYNC | START_SYNC) - SKIP_TRIG_COUNTER <= 0; - else if(SKIP_TRIGGER & SKIP_TRIG_COUNTER!=8'hff ) - SKIP_TRIG_COUNTER <= SKIP_TRIG_COUNTER + 1; + SKIP_TRIG_COUNTER_SYNC <= 0; + else if(SKIP_TRIGGER) // let overflow.. & SKIP_TRIG_COUNTER_SYNC!=32'hffffffff ) + SKIP_TRIG_COUNTER_SYNC <= SKIP_TRIG_COUNTER_SYNC + 1; always@(posedge CLK40) if(RST_SYNC | START_SYNC) - TRIG_ID <= 0; //32'h3fff-10; + TRIG_ID_SYNC <= 0; //32'h3fff-10; else if(GEN_TRIG_PULSE) - TRIG_ID <= TRIG_ID + 1; + TRIG_ID_SYNC <= TRIG_ID_SYNC + 1; -reg [31:0] TRIG_ID_FF; +reg [31:0] TRIG_ID_SYNC_FF; always@(posedge CLK40) - TRIG_ID_FF <= TRIG_ID; - + TRIG_ID_SYNC_FF <= TRIG_ID_SYNC; + localparam INV_OUT = 6'b101010; wire [5:0] TIME_OUT; genvar dut_ch; generate for (dut_ch = 0; dut_ch < 6; dut_ch = dut_ch + 1) begin: dut_ch_tx - tlu_tx - #( - .INV_OUT(INV_OUT[dut_ch]) - ) - tlu_tx( - .SYS_CLK(CLK40), - .CLK320(CLK320), - .CLK160(CLK160), - - .TRIG_LE(MAX_LE[3:0]), - .SYS_RST(RST_SYNC), - .ENABLE(CONF_EN_OUTPUT[dut_ch]), - .TRIG(GEN_TRIG_PULSE), - .TRIG_ID(TRIG_ID_FF[30:0]), - .N_BITS_TRIGGER_ID(CONF_N_BITS_TRIGGER_ID), - .READY(READY[dut_ch]), - .CONF_TIME_OUT(CONF_TIME_OUT), - .TIME_OUT(TIME_OUT[dut_ch]), - - .TLU_CLOCK(DUT_CLOCK[dut_ch]), .TLU_BUSY(DUT_BUSY[dut_ch]), - .TLU_TRIGGER(DUT_TRIGGER[dut_ch]), .TLU_RESET(DUT_RESET[dut_ch]) - ); + tlu_tx #( + .INV_OUT(INV_OUT[dut_ch]) + ) tlu_tx ( + .SYS_CLK(CLK40), + .CLK320(CLK320), + .CLK160(CLK160), + .TRIG_LE(MAX_LE[3:0]), + .SYS_RST(RST_SYNC), + .ENABLE(CONF_EN_OUTPUT[dut_ch]), + .TRIG(GEN_TRIG_PULSE), + .TRIG_ID(TRIG_ID_SYNC_FF[30:0]), + .N_BITS_TRIGGER_ID(CONF_N_BITS_TRIGGER_ID), + .READY(READY[dut_ch]), + .CONF_TIME_OUT(CONF_TIME_OUT), + .TIME_OUT(TIME_OUT[dut_ch]), + .STATE_OUT(TX_STATE[dut_ch]), + .TLU_CLOCK(DUT_CLOCK[dut_ch]), .TLU_BUSY(DUT_BUSY[dut_ch]), + .TLU_TRIGGER(DUT_TRIGGER[dut_ch]), .TLU_RESET(DUT_RESET[dut_ch]) + ); end -endgenerate +endgenerate always@(posedge CLK40) if(RST_SYNC | START_SYNC) TIMEOUT_COUNTER <= 0; else if( (|TIME_OUT) & TIMEOUT_COUNTER!=8'hff ) TIMEOUT_COUNTER <= TIMEOUT_COUNTER + 1; - + wire cdc_wfull; wire [127:0] cdc_data; wire fifo_full, cdc_fifo_empty; wire cdc_fifo_write; - + always@(posedge CLK40) begin if(RST_SYNC) LOST_DATA_CNT <= 0; @@ -316,7 +392,7 @@ assign LE[2] = CONF_EN_INPUT[2] ? LAST_RISING_REL[2] + 8'd43 : 0; assign LE[3] = CONF_EN_INPUT[3] ? LAST_RISING_REL[3] + 8'd43 : 0; ///TODO: add some status? Lost count? Skipped triggers? -assign cdc_data = {TRIG_ID, TIME_STAMP, LE[3], LE[2], LE[1], LE[0]}; +assign cdc_data = {TRIG_ID_SYNC, TIME_STAMP_SYNC, LE[3], LE[2], LE[1], LE[0]}; assign cdc_fifo_write = GEN_TRIG_PULSE; wire [127:0] cdc_data_out; @@ -334,7 +410,7 @@ wire out_fifo_read; wire [127:0] out_fifo_data_out; wire out_fifo_empty; gerneric_fifo #(.DATA_SIZE(128), .DEPTH(64)) gerneric_fifo -( +( .clk(BUS_CLK), .reset(RST), .write(!cdc_fifo_empty), .read(out_fifo_read), @@ -352,22 +428,22 @@ always@(posedge BUS_CLK) out_word_cnt <= 0; else if (FIFO_READ) out_word_cnt <= out_word_cnt + 1; - + reg [127:0] fifo_data_out_buf; always@(posedge BUS_CLK) if(out_fifo_read) fifo_data_out_buf <= out_fifo_data_out; - + wire [15:0] fifo_data_out_word [7:0]; - - + + genvar iw; generate assign fifo_data_out_word[0] = out_fifo_data_out[15:0]; for (iw = 1; iw < 8; iw = iw + 1) begin: gen_out assign fifo_data_out_word[iw] = fifo_data_out_buf[(iw+1)*16-1:iw*16]; end -endgenerate +endgenerate assign FIFO_DATA = fifo_data_out_word[out_word_cnt]; assign FIFO_EMPTY = out_word_cnt==0 & out_fifo_empty; diff --git a/firmware/src/tlu_master/tlu_tx.v b/firmware/src/tlu_master/tlu_tx.v index 8581a30..f0b288a 100644 --- a/firmware/src/tlu_master/tlu_tx.v +++ b/firmware/src/tlu_master/tlu_tx.v @@ -30,6 +30,7 @@ module tlu_tx input wire [3:0] TRIG_LE, input wire [15:0] CONF_TIME_OUT, output wire TIME_OUT, + output wire [2:0]STATE_OUT, input wire TLU_CLOCK, TLU_BUSY, output wire TLU_TRIGGER, TLU_RESET @@ -81,6 +82,9 @@ always@(posedge CLK160) assign TLU_CLOCK_REAL = TLU_CLOCK_FF[4:3] == 2'b00 && TLU_CLOCK_FF[2:1] == 2'b11; //This is bad but seem to help for some cross-talk +wire TLU_CLOCK_HIGH; +assign TLU_CLOCK_HIGH = TLU_CLOCK_FF[4:1] == 4'b1111; + reg [31:0] TRIG_ID_SR; initial TRIG_ID_SR = 0; //always@(posedge TLU_CLOCK_REAL or posedge TRIG_FF) @@ -88,7 +92,7 @@ integer n; always@(posedge CLK160) if(TRIG_FF) TRIG_ID_SR <= {TRIG_ID, 1'b0}; - else if (TLU_CLOCK_REAL) + else if (TLU_CLOCK_REAL & BUSY_REAL) // This loop basically does: TRIG_ID_SR <= {(32 - N_BITS_TRIGGER_ID)'b0, TRIG_ID_SR[N_BITS_TRIGGER_ID:1]} for (n = 0; n < 31; n = n + 1) if (n < N_BITS_TRIGGER_ID) @@ -114,9 +118,10 @@ assign TLU_RESET = INV_OUT ? 1'b0 : 1'b0; reg TLU_CLOCK_VETO; always@(posedge SYS_CLK) - TLU_CLOCK_VETO <= TLU_CLOCK_REAL; + TLU_CLOCK_VETO <= TLU_CLOCK_HIGH; assign READY = (state == WAIT_STATE && TLU_CLOCK_VETO == 0 && WAIT_CNT==0) | !ENABLE; +assign STATE_OUT = ENABLE? state: 3'b0; reg [15:0] TIME_OUT_CNT; always@(posedge SYS_CLK) begin @@ -178,4 +183,4 @@ oddr_reg oddr_reg( .OUT(TLU_TRIGGER) ); -endmodule +endmodule \ No newline at end of file diff --git a/online_monitor.png b/online_monitor.png new file mode 100644 index 0000000..eafd62f Binary files /dev/null and b/online_monitor.png differ diff --git a/pytlu/ZestSC1.py b/pytlu/ZestSC1.py old mode 100755 new mode 100644 index b89334a..5737bed --- a/pytlu/ZestSC1.py +++ b/pytlu/ZestSC1.py @@ -1,29 +1,31 @@ +import logging import struct import array -import logging from threading import RLock as Lock +import numpy as np +import usb.core + + logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) -import numpy as np -import usb.core ID_VENDOR = 0x165d ID_PRODUCT = 0x0001 BITFILE = {'name': 0x61, 'part': 0x62, 'date': 0x63, 'time': 0x64, - 'image': 0x65} + 'image': 0x65} REQUEST = {'write_register': 0xd0, 'read_register': 0xd1, - 'write_config': 0xd2, 'read_config': 0xd3, - 'write_eeprom': 0xd7, 'read_eeprom': 0xd8, - 'firmware': 0xdc, 'reset_8051': 0xa0, - 'set_signal': 0xd5, 'get_signal': 0xd6, - 'signal_direction': 0xd4} + 'write_config': 0xd2, 'read_config': 0xd3, + 'write_eeprom': 0xd7, 'read_eeprom': 0xd8, + 'firmware': 0xdc, 'reset_8051': 0xa0, + 'set_signal': 0xd5, 'get_signal': 0xd6, + 'signal_direction': 0xd4} EEPROM = {'fpga_type': 0xfffa, 'card_id': 0xfffb, - 'serial_number': 0xfffc, 'memory_size': 0xfff6} + 'serial_number': 0xfffc, 'memory_size': 0xfff6} ENDPOINT = {'write_ctrl': 0x0040, 'read_ctrl': 0x00c0, 'write_data': 0x0002, 'read_data': 0x0086, @@ -32,6 +34,8 @@ VALUE_8051 = 0xe600 # convert array [AB, CD, ...] to ABCD... (in hex) + + def byteshift(array): shifted_sum = 0 for i in range(len(array)): @@ -40,12 +44,16 @@ def byteshift(array): return shifted_sum # the length of the section is saved in len_bytes + + def read_bitfile_section(f, len_bytes): length = [struct.unpack('B', f.read(1))[0] for i in range(len_bytes)] length = byteshift(length) return length, f.read(length) - + # 16 bytes per row, similar to wireshark capture + + def print_bitfile_to_file(bitfile, length): f_out = open('f_out.txt', 'w') for i in range(0, length, 16): @@ -60,6 +68,8 @@ def print_bitfile_to_file(bitfile, length): # "while byte:" loops over the bitfile until the end is reached # struct.unpack converts byte to a readable format + + def open_bitfile(path_to_file): ret = {} @@ -82,28 +92,78 @@ def open_bitfile(path_to_file): byte = f.read(1) # self.print_bitfile_to_file(ret['image'][1], ret['image'][0]) - + return ret # weird size modification, no idea why it is necessary + + def modify_bitfile_image(bitfile): image_size = bitfile['image'][0] length = (image_size + 511 + 512) & ~511 ret = array.array('B', bitfile['image'][1]) - ret += array.array('B', [0] * (length - len(ret)) ) + ret += array.array('B', [0] * (length - len(ret))) return ret -class Board: -# device is not None if usb.core.find() does not find any boards + +class TluDevice: + # device is not None if usb.core.find() does not find any boards def __init__(self, device=None): - self.dev = device self._lock = Lock() - - device.set_configuration() + self.dev = device + self.dev.set_configuration() + + @classmethod + def from_board_sn(cls, board_sn): + usb_devices = usb.core.find(find_all=True, idVendor=ID_VENDOR, idProduct=ID_PRODUCT) + if usb_devices is None: + raise ValueError('No device found') + + boards = [] + for usb_device in usb_devices: + try: + tlu_device = cls(device=usb_device) + except usb.core.USBError: + pass + else: + try: + curr_board_sn = tlu_device.board_sn + except usb.core.USBError: + pass + else: + if curr_board_sn == board_sn: + usb_device.append(usb_device) + usb_device.dispose() + if not boards: + raise ValueError('No device found with SN %d' % board_sn) + elif len(boards) > 1: + raise ValueError('Found %d devices with SN %d' % (len(boards), board_sn)) + else: + return boards[0] + + @property + def fpga_type(self): + return self.get_fpga_type() + + @property + def card_id(self): + return self.get_card_id() + + @property + def board_sn(self): + return self.get_serial_number() + + @property + def memory_size(self): + return self.get_memory_size() + + @property + def fw_version(self): + return self.get_firmware_version() def read_eeprom(self, address): return self.dev.ctrl_transfer(ENDPOINT['read_ctrl'], - REQUEST['read_eeprom'], address, 0, 3, timeout=1000) + REQUEST['read_eeprom'], address, 0, 3, timeout=1000) def get_fpga_type(self): return self.read_eeprom(EEPROM['fpga_type'])[2] @@ -112,83 +172,82 @@ def get_card_id(self): return self.read_eeprom(EEPROM['card_id'])[2] def get_serial_number(self): - return struct.unpack('>I', ''.join([chr(self.read_eeprom(EEPROM['serial_number'] + i)[2]) for i in range(4)]))[0] + return struct.unpack('>I', bytearray(''.join([chr(self.read_eeprom(EEPROM['serial_number'] + i)[2]) for i in range(4)]), 'utf-8'))[0] def get_memory_size(self): - return struct.unpack('>I', ''.join([chr(self.read_eeprom(EEPROM['memory_size'] + i)[2]) for i in range(4)]))[0] + return struct.unpack('>I', bytearray(''.join([chr(self.read_eeprom(EEPROM['memory_size'] + i)[2]) for i in range(4)]), 'utf-8'))[0] def get_firmware_version(self): return np.array(self.dev.ctrl_transfer(ENDPOINT['read_ctrl'], - REQUEST['firmware'], 0, 0, 3)[0:3:], timeout=1000) + REQUEST['firmware'], 0, 0, 3)[0:3:], timeout=1000) def __str__(self): return str({'card_id': '{}'.format(self.get_card_id()), - 'fpga_type': '{}'.format(self.get_fpga_type()), - 'serial_number':' {}'.format(self.get_serial_number()), - 'memory_size': '{}'.format(self.get_memory_size()), - #'firmware_version': '{}?'.format(self.get_firmware_version()) - }) + 'fpga_type': '{}'.format(self.get_fpga_type()), + 'serial_number': ' {}'.format(self.get_serial_number()), + 'memory_size': '{}'.format(self.get_memory_size()), + # 'firmware_version': '{}?'.format(self.get_firmware_version()) + }) def _reset_8051(self): ret = np.array([0, 0]) ret[0] = self.dev.ctrl_transfer(ENDPOINT['write_ctrl'], - REQUEST['reset_8051'], VALUE_8051, 0, [1], timeout=1000) + REQUEST['reset_8051'], VALUE_8051, 0, [1], timeout=1000) ret[1] = self.dev.ctrl_transfer(ENDPOINT['write_ctrl'], - REQUEST['reset_8051'], VALUE_8051, 0, [0], timeout=1000) -# print('reset_8051: {}'.format(ret)) + REQUEST['reset_8051'], VALUE_8051, 0, [0], timeout=1000) # Not sure why endpoint is 'read_ctrl' and not 'write_ctrl' def write_register(self, index, data): with self._lock: - for i,d in enumerate(data): + for i, d in enumerate(data): ret = self.dev.ctrl_transfer(ENDPOINT['read_ctrl'], - REQUEST['write_register'], wValue=index+i, wIndex=d, - data_or_wLength=1, timeout=1000) + REQUEST['write_register'], wValue=index + i, wIndex=d, + data_or_wLength=1, timeout=1000) logger.debug('write_register: {}'.format(ret)) def read_register(self, index, length): - ret = array.array('B', '\x00' * length) - with self._lock: + ret = array.array('B', ('\x00' * length).encode('utf-8')) + with self._lock: for i in range(length): ctrl_ret = self.dev.ctrl_transfer(ENDPOINT['read_ctrl'], - REQUEST['read_register'], wValue=index+i, wIndex=0, - data_or_wLength=2, timeout=1000) + REQUEST['read_register'], wValue=index + i, wIndex=0, + data_or_wLength=2, timeout=1000) ret[i] = ctrl_ret[1] - + logger.debug('read_register: {}'.format(ret)) return ret # Not sure if timeout=1000 is necessary def write_data(self, data): - with self._lock: + with self._lock: assert self.dev.write(ENDPOINT['write_data'], data) == len(data) def read_data(self, length): - with self._lock: + with self._lock: ret = self.dev.read(ENDPOINT['read_data'], length, timeout=1000) - + logger.debug('read_data: {}'.format(ret)) return ret def set_signal_direction(self, direction): with self._lock: ret = self.dev.ctrl_transfer(ENDPOINT['read_ctrl'], - REQUEST['signal_direction'], wValue=direction, wIndex=0, - data_or_wLength=1, timeout=1000) + REQUEST['signal_direction'], wValue=direction, wIndex=0, + data_or_wLength=1, timeout=1000) logger.debug('set_signal_direction: {}'.format(ret)) def set_signal(self, signal): with self._lock: ret = self.dev.ctrl_transfer(ENDPOINT['read_ctrl'], - REQUEST['set_signal'], wValue=signal, wIndex=0, - data_or_wLength=1, timeout=1000) + REQUEST['set_signal'], wValue=signal, wIndex=0, + data_or_wLength=1, timeout=1000) logger.debug('set_signal: {}'.format(ret)) def get_signal(self): with self._lock: ret = self.dev.ctrl_transfer(ENDPOINT['read_ctrl'], - REQUEST['get_signal'], wValue=0, wIndex=0, - data_or_wLength=2, timeout=1000) + REQUEST['get_signal'], wValue=0, wIndex=0, + data_or_wLength=2, timeout=1000) logger.debug('get_signal: {}'.format(ret)) return ret @@ -204,62 +263,64 @@ def read_int(self, length): def open_card(self): with self._lock: self._reset_8051() - + ret = self.dev.ctrl_transfer(ENDPOINT['read_ctrl'], - REQUEST['write_config'], wValue=4096, wIndex=4096, - data_or_wLength=array.array('B', [0, 0]), timeout=1000) + REQUEST['write_config'], wValue=4096, wIndex=4096, + data_or_wLength=array.array('B', [0, 0]), timeout=1000) logger.debug('ctrl_transfer: {}'.format(ret)) - + Buffer = np.full(4096, 0, dtype=np.uint16) Buffer = array.array('B', Buffer) - + ret = self.dev.write(ENDPOINT['write_data'], Buffer, timeout=1000) logger.debug('bulk_write: {}'.format(ret)) - + self._reset_8051() def load_bitarray_to_board(self, bitarray): with self._lock: self._reset_8051() - + length = len(bitarray) - wValue = (length>>16)&0xffff - wIndex = length&0xffff + wValue = (length >> 16) & 0xffff + wIndex = length & 0xffff ret = self.dev.ctrl_transfer(ENDPOINT['read_ctrl'], - REQUEST['write_config'], wValue=wValue, wIndex=wIndex, - data_or_wLength=array.array('B', [0, 0]), timeout=1000) + REQUEST['write_config'], wValue=wValue, wIndex=wIndex, + data_or_wLength=array.array('B', [0, 0]), timeout=1000) logger.debug('ctrl_transfer: {}'.format(ret)) - + ret = self.dev.write(ENDPOINT['write_data'], bitarray) logger.debug('bulk_write: {}'.format(ret)) - + ret = self.dev.ctrl_transfer(ENDPOINT['read_ctrl'], - REQUEST['read_config'], wValue=0, wIndex=0, - data_or_wLength=array.array('B', [0, 0, 0]), timeout=1000) + REQUEST['read_config'], wValue=0, wIndex=0, + data_or_wLength=array.array('B', [0, 0, 0]), timeout=1000) logger.debug('ctrl_transfer: {}'.format(ret)) - def close_board(self): with self._lock: ret = self.dev.ctrl_transfer(ENDPOINT['read_ctrl'], - REQUEST['write_config'], wValue=4096, wIndex=4096, - data_or_wLength=array.array('B', [0, 0]), timeout=1000) + REQUEST['write_config'], wValue=4096, wIndex=4096, + data_or_wLength=array.array('B', [0, 0]), timeout=1000) logger.debug('ctrl_transfer: {}'.format(ret)) - + self._reset_8051() # find_all=True: devs is not None if no boards are found, so it's pointless to # check if any boards were found -# find_all=False: dev is None if no board is found +# find_all=False: dev is None if no TLU is found # the usb backend can be changed if required -def find_boards(): -# backend = usb.backend.libusb1.get_backend(find_library=lambda x: -# "/usr/lib/libusb-1.0.so") -# devs = usb.core.find(find_all=True, idVendor=VENDOR_ID, -# idProduct=PRODUCT_ID, backend=backend) - - devs = usb.core.find(find_all=True, idVendor=ID_VENDOR, - idProduct=ID_PRODUCT) - - return [Board(device=dev) for dev in devs] +def find_tlu_devices(): + # backend = usb.backend.libusb1.get_backend(find_library=lambda x: + # "/usr/lib/libusb-1.0.so") + # devs = usb.core.find(find_all=True, + # idVendor=ID_VENDOR, + # idProduct=ID_PRODUCT, + # backend=backend) + + devs = usb.core.find(find_all=True, + idVendor=ID_VENDOR, + idProduct=ID_PRODUCT) + + return [TluDevice(device=dev) for dev in devs] diff --git a/pytlu/ZestSC1TL.py b/pytlu/ZestSC1TL.py old mode 100755 new mode 100644 index 8e02d3f..529da44 --- a/pytlu/ZestSC1TL.py +++ b/pytlu/ZestSC1TL.py @@ -8,13 +8,14 @@ import logging import os -import ZestSC1 as zest +from pytlu.ZestSC1 import TluDevice, find_tlu_devices, open_bitfile, modify_bitfile_image + from basil.TL.SiTransferLayer import SiTransferLayer class ZestSC1Usb(SiTransferLayer): '''SiLab USB device - ''' + ''' BASE_ADDRESS_EXTERNAL = 0x00000 HIGH_ADDRESS_EXTERNAL = BASE_ADDRESS_EXTERNAL + 0x10000 @@ -27,29 +28,33 @@ def __init__(self, conf): self._dev = None def init(self): - super(ZestSC1Usb, self).init() - - devices = zest.find_boards() - logging.info('Found USB board(s): {}'.format(', '.join(('%s with ID %s (Serial no. %s)' % ('ZestSC1', device.get_card_id(), device.get_serial_number())) for device in devices))) - - - self._dev = devices[0] - logging.info('Usinng USB board: {}'.format(str(self._dev))) - self._dev.open_card() + super(ZestSC1Usb, self).init() + self._init.setdefault('board_sn', None) + if self._init['board_sn'] and self._init['board_sn'] >= 0: + self._dev = TluDevice.from_board_sn(self._init['board_sn']) + else: + # search for any available device + devices = find_tlu_devices() + if not devices: + raise IOError('Can\'t find TLU. Connect or reset TLU!') + else: + logging.info('Found TLU(s): {}'.format(', '.join(('%s with ID %s (Serial no. %s)' % ('ZestSC1', device.get_card_id(), device.get_serial_number())) for device in devices))) + if len(devices) > 1: + raise ValueError('Found %d TLUs. Please specify "board_sn"' % len(devices)) + self._dev = devices[0] - - - if 'bit_file' in self._init.keys(): + logging.info('Using TLU: {}'.format(str(self._dev))) + self._dev.open_card() + if 'bit_file' in self._init.keys(): if os.path.exists(self._init['bit_file']): bit_file = self._init['bit_file'] elif os.path.exists(os.path.join(os.path.dirname(self.parent.conf_path), self._init['bit_file'])): bit_file = os.path.join(os.path.dirname(self.parent.conf_path), self._init['bit_file']) else: - raise ValueError('No such bit file: %s' % self._init['bit_file']) - - logging.info("Programming FPGA: %s..." % (self._init['bit_file'])) - bitfile = zest.open_bitfile(bit_file) - bitarray = zest.modify_bitfile_image(bitfile) + raise ValueError('No such bit file: %s' % self._init['bit_file']) + logging.info("Programming FPGA: %s..." % (self._init['bit_file'])) + bitfile = open_bitfile(bit_file) + bitarray = modify_bitfile_image(bitfile) self._dev.load_bitarray_to_board(bitarray) def write(self, addr, data): diff --git a/pytlu/__init__.py b/pytlu/__init__.py old mode 100755 new mode 100644 diff --git a/pytlu/fifo_readout.py b/pytlu/fifo_readout.py old mode 100755 new mode 100644 index e866e58..e8cf3a3 --- a/pytlu/fifo_readout.py +++ b/pytlu/fifo_readout.py @@ -2,12 +2,16 @@ from time import sleep, time, mktime from threading import Thread, Event, Lock from collections import deque -from Queue import Queue, Empty +try: + from queue import Queue, Empty # Python3 +except ImportError: + from Queue import Queue, Empty # Python2 import sys import datetime data_iterable = ("data", "timestamp_start", "timestamp_stop", "error") + class FifoError(Exception): pass @@ -43,8 +47,8 @@ def __init__(self, dut): self._is_running = False self.reset_sram_fifo() self._record_count_lock = Lock() - self.set_record_count(0,reset=True) - + self.set_record_count(0, reset=True) + @property def is_running(self): return self._is_running @@ -68,7 +72,7 @@ def data_words_per_second(self): self._result.get() self._calculate.set() try: - result = self._result.get(timeout=2 * self.readout_interval) + result = self._result.get(timeout=self.readout_interval) except Empty: self._calculate.clear() return None @@ -137,7 +141,6 @@ def stop(self, timeout=10.0): def print_readout_status(self): tlu_lost_count = self.get_data_tlu_fifo_lost_count() - logging.info('Received words: %d', self._record_count) logging.info('Data queue size: %d', len(self._data_deque)) logging.info('SRAM FIFO size: %d', self.dut['stream_fifo']['SIZE']) @@ -162,7 +165,9 @@ def readout(self, no_data_timeout=None): raise NoDataTimeout('Received no data for %0.1f second(s)' % no_data_timeout) data = self.read_data() self._record_count += len(data) + # print self._record_count except Exception: + logging.warn('Exception occured %s', sys.exc_info()[2]) no_data_timeout = None # raise exception only once if self.errback: self.errback(sys.exc_info()) @@ -175,10 +180,11 @@ def readout(self, no_data_timeout=None): if data_words > 0: last_time, curr_time = self.update_timestamp() status = 0 + skip_triggers = self.get_data_tlu_skipped_trigger_count() if self.callback: - self._data_deque.append((data, last_time, curr_time, status)) + self._data_deque.append((data, last_time, curr_time, status, skip_triggers)) if self.fill_buffer: - self._data_buffer.append((data, last_time, curr_time, status)) + self._data_buffer.append((data, last_time, curr_time, status, skip_triggers)) self._words_per_read.append(data_words) elif self.stop_readout.is_set(): break @@ -201,7 +207,7 @@ def worker(self): try: data = self._data_deque.popleft() except IndexError: - self.stop_readout.wait(self.readout_interval) # sleep a little bit, reducing CPU usage + self.stop_readout.wait(self.readout_interval / 2.0) # sleep a little bit, reducing CPU usage else: if data is None: # if None then exit break @@ -217,7 +223,6 @@ def watchdog(self): logging.debug('Starting %s', self.watchdog_thread.name) while True: try: - if self.get_data_tlu_fifo_lost_count(): raise FifoError('TLU FIFO lost data error(s) detected') except Exception: @@ -237,19 +242,19 @@ def update_timestamp(self): def read_status(self): raise NotImplementedError() - + def get_record_count(self): self._record_count_lock.acquire() - cnt=self._record_count + cnt = self._record_count self._record_count_lock.release() return cnt - - def set_record_count(self,cnt,reset=False): + + def set_record_count(self, cnt, reset=False): self._record_count_lock.acquire() if reset: - self._record_count=cnt + self._record_count = cnt else: - self._record_count=self._record_count+cnt + self._record_count = self._record_count + cnt self._record_count_lock.release() def reset_sram_fifo(self): @@ -262,15 +267,15 @@ def reset_sram_fifo(self): if fifo_size != 0: logging.warning('SRAM FIFO not empty after reset: size = %i', fifo_size) - def get_data_tlu_fifo_lost_count(self, channels=None): return self.dut['tlu_master'].LOST_DATA_CNT - + + def get_data_tlu_skipped_trigger_count(self): + return self.dut['tlu_master'].SKIP_TRIG_COUNTER + def get_float_time(self): '''returns time as double precision floats - Time64 in pytables - mapping to and from python datetime's - ''' t1 = time() t2 = datetime.datetime.fromtimestamp(t1) return mktime(t2.timetuple()) + 1e-6 * t2.microsecond - diff --git a/pytlu/firmware/tlu.bit b/pytlu/firmware/tlu.bit new file mode 100755 index 0000000..926e00b Binary files /dev/null and b/pytlu/firmware/tlu.bit differ diff --git a/pytlu/online_monitor/configuration.yaml b/pytlu/online_monitor/configuration.yaml new file mode 100644 index 0000000..3b1b924 --- /dev/null +++ b/pytlu/online_monitor/configuration.yaml @@ -0,0 +1,17 @@ +#producer_sim : +# TLU_Producer : +# backend : tcp://127.0.0.1:8600 +# delay : 0.1 +# kind : pytlu_producer_sim +# data_file: /home/silab/git/pytlu/data/tlu_example_data.h5 + +converter : + TLU_Converter : + kind : pytlu_converter + frontend : tcp://127.0.0.1:8600 + backend : tcp://127.0.0.1:8700 + +receiver : + TLU : + kind : pytlu_receiver + frontend : tcp://127.0.0.1:8700 diff --git a/pytlu/online_monitor/pytlu_converter.py b/pytlu/online_monitor/pytlu_converter.py new file mode 100644 index 0000000..cdca0db --- /dev/null +++ b/pytlu/online_monitor/pytlu_converter.py @@ -0,0 +1,135 @@ +import numpy as np + +from zmq.utils import jsonapi +from online_monitor.converter.transceiver import Transceiver +from online_monitor.utils import utils + + +class PyTLU(Transceiver): + def setup_transceiver(self): + self.set_bidirectional_communication() # We want to be able to change the histogrammmer settings + + def setup_interpretation(self): + # array for simulated status data + self.status_data = np.zeros(shape=1, dtype=[('trigger_rate_acc', 'f4'), ('trigger_rate_real', 'f4')]) + # set array size; must be shape=(2, x); increase x to plot longer time span + self.array_size = (2, 1600) + + # add arrays for plots; array[0] is time axis + self.trigger_rate_acc_array = np.zeros(shape=self.array_size) + self.trigger_rate_real_array = np.zeros(shape=self.array_size) + + # add dicts for individual handling of each parameter + # Using structured np.arrays produces weird VisibleDeprecationWarning + # dict with all data arrays + self.all_arrays = {'trigger_rate_acc': self.trigger_rate_acc_array, + 'trigger_rate_real': self.trigger_rate_real_array} + + # dict with set of current data indices + self.array_indices = {'trigger_rate_acc': 0, + 'trigger_rate_real': 0} + + # dict with set of start times of each key since last shifted through + self.shift_cycle_times = {'trigger_rate_acc': 0, + 'trigger_rate_real': 0} + + # dict with set of current time indices + self.update_time_indices = {'trigger_rate_acc': 0, + 'trigger_rate_real': 0} + + # dict with set of current times corresponding current data + self.now = {'trigger_rate_acc': 0, + 'trigger_rate_real': 0} + + self.updateTime = 0 + self.fps = 0 + self.readout = 0 + self.n_readouts = 0 + self.skipped_trigger_counter_old = 0 # variable needed to calcualte actual trigger counter + + def deserialize_data(self, data): + try: + self.meta_data = jsonapi.loads(data) + except ValueError: + try: + dtype = self.meta_data.pop('dtype') + shape = self.meta_data.pop('shape') + if self.meta_data: + try: + raw_data_array = np.frombuffer(data, dtype=dtype).reshape(shape) + return raw_data_array + except (KeyError, ValueError, TypeError): # KeyError happens if meta data read is omitted; ValueError if np.frombuffer fails due to wrong sha + return None + except AttributeError: # Happens if first data is not meta data + return None + return {'meta_data': self.meta_data} + + def interpret_data(self, data): + # add function to fill arrays with data and shift through + def fill_arrays(array, data, time, time_index): + array[0][time_index] = time + array[1] = np.roll(array[1], 1) + array[1][0] = data + return array + + if data[0][1] is not None: + meta_data = data[0][1]['meta_data'] + data_length = meta_data['data_length'] + timestamp_start = meta_data['timestamp_start'] + timestamp_stop = meta_data['timestamp_stop'] + skipped_triggers = meta_data['skipped_triggers'] + actual_skipped_triggers = skipped_triggers - self.skipped_trigger_counter_old + self.status_data['trigger_rate_acc'] = data_length / (timestamp_stop - timestamp_start) / 1e3 # acc trigger rate in kHz + self.status_data['trigger_rate_real'] = (data_length + actual_skipped_triggers) / (timestamp_stop - timestamp_start) / 1e3 # real trigger rate in kHz + self.skipped_trigger_counter_old = skipped_triggers + + # fill time and data axes, here only one key (trigger rate) up to now + for key in self.all_arrays: + # update starting time (self.shift_cycle_time) if we just started or once shifted through the data array + if self.array_indices[key] == 0 or self.array_indices[key] % self.array_size[1] == 0: + + self.shift_cycle_times[key] = meta_data['timestamp_start'] + self.update_time_indices[key] = 0 + + # time since we started or last shifted through + self.now[key] = self.shift_cycle_times[key] - meta_data['timestamp_start'] + + self.all_arrays[key] = fill_arrays(self.all_arrays[key], self.status_data[key][0], self.now[key], self.update_time_indices[key]) + + # increase indices for timing and data + self.update_time_indices[key] += 1 + self.array_indices[key] += 1 + + now = float(meta_data['timestamp_stop']) + recent_fps = 1.0 / (now - self.updateTime) # calculate FPS + self.updateTime = now + self.fps = self.fps * 0.7 + recent_fps * 0.3 + + return [{'tlu': self.all_arrays, 'indices': self.array_indices, 'fps': self.fps, 'timestamp_stop': now}] + + self.readout += 1 + + if self.n_readouts != 0: # = 0 for infinite integration + if self.readout % self.n_readouts == 0: + self.trigger_rate_acc_array = np.zeros_like(self.trigger_rate_acc_array) + self.trigger_rate_real_array = np.zeros_like(self.trigger_rate_real_array) + self.array_indices['trigger_rate_acc'] = 0 + self.array_indices['trigger_rate_real'] = 0 + self.update_time_indices['trigger_rate_acc'] = 0 + self.update_time_indices['trigger_rate_real'] = 0 + self.readouts = 0 + + def serialize_data(self, data): + return utils.simple_enc(None, data) + + def handle_command(self, command): + # received signal is 'ACTIVETAB tab' where tab is the name (str) of the selected tab in online monitor + if command[0] == 'RESET': + self.trigger_rate_acc_array = np.zeros_like(self.trigger_rate_acc_array) + self.trigger_rate_real_array = np.zeros_like(self.trigger_rate_real_array) + self.array_indices['trigger_rate_acc'] = 0 + self.array_indices['trigger_rate_real'] = 0 + self.update_time_indices['trigger_rate_acc'] = 0 + self.update_time_indices['trigger_rate_real'] = 0 + else: + self.n_readouts = int(command[0]) diff --git a/pytlu/online_monitor/pytlu_producer_sim.py b/pytlu/online_monitor/pytlu_producer_sim.py new file mode 100644 index 0000000..56bc16c --- /dev/null +++ b/pytlu/online_monitor/pytlu_producer_sim.py @@ -0,0 +1,94 @@ +''' This is a producer faking data coming from pytlu by taking real data and sending these in chunks''' + +import time +import numpy as np +import tables as tb +import zmq +import logging + +from online_monitor.utils.producer_sim import ProducerSim + + +class PyTLU(ProducerSim): + + def setup_producer_device(self): + ProducerSim.setup_producer_device(self) + self.in_file_h5 = tb.open_file(self.config['data_file'], mode="r") + self.meta_data = self.in_file_h5.root.meta_data[:] + self.raw_data = self.in_file_h5.root.raw_data + self.n_readouts = self.meta_data.shape[0] + self.total_data = 0 # amount of replayed data in MB + self.time_start = time.time() # calculate duration of replay + self.time_end = 0 # calculate duration of replay + + try: + self.scan_parameter_name = self.in_file_h5.root.scan_parameters.dtype.names + self.scan_parameters = self.in_file_h5.root.scan_parameters[:] + except tb.NoSuchNodeError: + self.scan_parameter_name = 'No parameter' + self.scan_parameters = None + + self.readout_word_indeces = np.column_stack((self.meta_data['index_start'], self.meta_data['index_stop'])) + self.actual_readout = 0 + self.last_readout_time = None + + def get_data(self): # Return the data of one readout + if self.actual_readout < self.n_readouts: + index_start, index_stop = self.readout_word_indeces[self.actual_readout] + data = [] + data.append(self.raw_data[index_start:index_stop]) + data.extend((float(self.meta_data[self.actual_readout]['timestamp_start']), + float(self.meta_data[self.actual_readout]['timestamp_stop']), + int(self.meta_data[self.actual_readout]['error']), + int(self.meta_data[self.actual_readout]['skipped_triggers']))) + + # FIXME: Simple syncronization to replay with similar timing, does not really work + now = time.time() + + if self.last_readout_time is not None: + delay = now - self.last_readout_time + additional_delay = self.meta_data[self.actual_readout]['timestamp_stop'] - self.meta_data[self.actual_readout]['timestamp_start'] - delay + if additional_delay > 0: + time.sleep(additional_delay) + self.last_readout_time = now + + if self.scan_parameters is not None: + return data, {str(self.scan_parameter_name): int(self.scan_parameters[self.actual_readout][0])} + else: + return data, {'No parameter': 0} + + def send_data(self): + '''Sends the data of every read out (raw data and meta data) via ZeroMQ to a specified socket + ''' + time.sleep(float(self.config['delay'])) # Delay is given in seconds + + try: + data, scan_parameters = self.get_data() # Get data of actual readout + except TypeError: # Data is fully replayes + self.time_end = time.time() + logging.warning('%s producer: No data to replay anymore! Data send in %.2f s is %.2f MB' % (self.name, self.time_end - self.time_start, self.total_data / (1024.0 ** 2))) # show amount of sent data after replay ended + time.sleep(10) + return + + self.actual_readout += 1 + + data_meta_data = dict( + name='ReadoutData', + dtype=str(data[0].dtype), + shape=data[0].shape, + data_length=data[0].shape[0], # data length + timestamp_start=data[1], # float + timestamp_stop=data[2], # float + readout_error=data[3], # int + skipped_triggers=data[4], # skipped trigger counter + scan_parameters=scan_parameters # dict + ) + try: + self.total_data += data[0].nbytes # sum up sent data packages + self.sender.send_json(data_meta_data, flags=zmq.SNDMORE | zmq.NOBLOCK) + self.sender.send(data[0], flags=zmq.NOBLOCK) # PyZMQ supports sending numpy arrays without copying any data + except zmq.Again: + pass + + def __del__(self): + self.in_file_h5.close() diff --git a/pytlu/online_monitor/pytlu_receiver.py b/pytlu/online_monitor/pytlu_receiver.py new file mode 100644 index 0000000..5bdcc6e --- /dev/null +++ b/pytlu/online_monitor/pytlu_receiver.py @@ -0,0 +1,109 @@ +import time + +from PyQt5 import Qt +import pyqtgraph as pg +from pyqtgraph.Qt import QtGui +from pyqtgraph.dockarea import DockArea, Dock +import pyqtgraph.ptime as ptime + +from online_monitor.utils import utils +from online_monitor.receiver.receiver import Receiver +from zmq.utils import jsonapi + + +class PyTLU(Receiver): + + def setup_receiver(self): + self.set_bidirectional_communication() # We want to change converter settings + + def setup_widgets(self, parent, name): + dock_area = DockArea() + parent.addTab(dock_area, name) + # Docks + dock_rate = Dock("Particle rate (Trigger rate)", size=(400, 400)) + dock_status = Dock("Status", size=(800, 40)) + dock_area.addDock(dock_rate, 'above') + dock_area.addDock(dock_status, 'top') + + # Status dock on top + cw = QtGui.QWidget() + cw.setStyleSheet("QWidget {background-color:white}") + layout = QtGui.QGridLayout() + cw.setLayout(layout) + self.rate_label = QtGui.QLabel("Readout Rate\n0 Hz") + self.timestamp_label = QtGui.QLabel("Data Timestamp\n") + self.plot_delay_label = QtGui.QLabel("Plot Delay\n") + self.spin_box = Qt.QSpinBox(value=0) + self.spin_box.setMaximum(1000000) + self.spin_box.setSuffix(" Readouts") + self.reset_button = QtGui.QPushButton('Reset') + layout.addWidget(self.timestamp_label, 0, 0, 0, 1) + layout.addWidget(self.plot_delay_label, 0, 1, 0, 1) + layout.addWidget(self.rate_label, 0, 2, 0, 1) + layout.addWidget(self.spin_box, 0, 6, 0, 1) + layout.addWidget(self.reset_button, 0, 7, 0, 1) + dock_status.addWidget(cw) + + # Connect widgets + self.reset_button.clicked.connect(lambda: self.send_command('RESET')) + self.spin_box.valueChanged.connect(lambda value: self.send_command(str(value))) + + # particle rate dock + trigger_rate_graphics = pg.GraphicsLayoutWidget() + trigger_rate_graphics.show() + plot_trigger_rate = pg.PlotItem(labels={'left': 'Trigger Rate / kHz', 'bottom': 'Time / s'}) + self.trigger_rate_acc_curve = pg.PlotCurveItem(pen='#B00B13') + self.trigger_rate_real_curve = pg.PlotCurveItem(pen='#228B22') + + # add legend + legend_acc = pg.LegendItem(offset=(80, 10)) + legend_acc.setParentItem(plot_trigger_rate) + legend_acc.addItem(self.trigger_rate_acc_curve, 'Accepted Trigger Rate') + legend_real = pg.LegendItem(offset=(80, 50)) + legend_real.setParentItem(plot_trigger_rate) + legend_real.addItem(self.trigger_rate_real_curve, 'Real Trigger Rate') + + # add items to plots and customize plots viewboxes + plot_trigger_rate.addItem(self.trigger_rate_acc_curve) + plot_trigger_rate.addItem(self.trigger_rate_real_curve) + plot_trigger_rate.vb.setBackgroundColor('#E6E5F4') + plot_trigger_rate.setXRange(-60, 0) + plot_trigger_rate.getAxis('left').setZValue(0) + plot_trigger_rate.getAxis('left').setGrid(155) + + # add plots to graphicslayout and layout to dock + trigger_rate_graphics.addItem(plot_trigger_rate, row=0, col=1, rowspan=1, colspan=2) + dock_rate.addWidget(trigger_rate_graphics) + + # add dict of all used plotcurveitems for individual handling of each plot + self.plots = {'trigger_rate_acc': self.trigger_rate_acc_curve, + 'trigger_rate_real': self.trigger_rate_real_curve} + self.plot_delay = 0 + + def deserialize_data(self, data): + datar, meta = utils.simple_dec(data) + return meta + + def handle_data_if_active(self, data): + # look for TLU data in data stream + if 'tlu' in data: + # fill plots + for key in data['tlu']: + # if array not full, plot data only up to current array_index, 'indices' is keyword + if data['indices'][key] < data['tlu'][key].shape[1]: + # set the plot data to the corresponding arrays where only the the values up to self.array_index are plotted + self.plots[key].setData(data['tlu'][key][0][:data['indices'][key]], + data['tlu'][key][1][:data['indices'][key]], autoDownsample=True) + + # if array full, plot entire array + elif data['indices'][key] >= data['tlu'][key].shape[1]: + # set the plot data to the corresponding arrays + self.plots[key].setData(data['tlu'][key][0], + data['tlu'][key][1], autoDownsample=True) + + # set timestamp, plot delay and readour rate + self.rate_label.setText("Readout Rate\n%d Hz" % data['fps']) + self.timestamp_label.setText("Data Timestamp\n%s" % time.asctime(time.localtime(data['timestamp_stop']))) + now = ptime.time() + self.plot_delay = self.plot_delay * 0.9 + (now - data['timestamp_stop']) * 0.1 + self.plot_delay_label.setText("Plot Delay\n%s" % 'not realtime' if abs(self.plot_delay) > 5 else "%1.2f ms" % (self.plot_delay * 1.e3)) diff --git a/pytlu/online_monitor/sender.py b/pytlu/online_monitor/pytlu_sender.py similarity index 74% rename from pytlu/online_monitor/sender.py rename to pytlu/online_monitor/pytlu_sender.py index c2efc40..c48c8a9 100644 --- a/pytlu/online_monitor/sender.py +++ b/pytlu/online_monitor/pytlu_sender.py @@ -1,24 +1,20 @@ import logging -import glob -from threading import RLock -import os.path -from os import remove -from operator import itemgetter - -import tables as tb import zmq + def init(socket_address="tcp://127.0.0.1:5500"): logging.info('Creating ZMQ context') context = zmq.Context() logging.info('Creating socket connection to server %s', socket_address) socket = context.socket(zmq.PUB) # publisher socket socket.bind(socket_address) - send_meta_data(socket, None, name='Reset') # send reset to indicate a new scan + # send reset to indicate a new scan + send_meta_data(socket, None, name='Reset') return socket -def send_meta_data(socket,conf, name): + +def send_meta_data(socket, conf, name): '''Sends the config via ZeroMQ to a specified socket. Is called at the beginning of a run and when the config changes. Conf can be any config dictionary. ''' meta_data = dict( @@ -30,7 +26,8 @@ def send_meta_data(socket,conf, name): except zmq.Again: pass -def send_data(socket, data, scan_parameters={}, name='ReadoutData'): + +def send_data(socket, data, len_raw_data, scan_parameters={}, name='ReadoutData'): '''Sends the data of every read out (raw data and meta data) via ZeroMQ to a specified socket ''' if not scan_parameters: @@ -39,18 +36,22 @@ def send_data(socket, data, scan_parameters={}, name='ReadoutData'): name=name, dtype=str(data[0].dtype), shape=data[0].shape, + data_length=len_raw_data, # data length timestamp_start=data[1], # float timestamp_stop=data[2], # float readout_error=data[3], # int + skipped_triggers=data[4], # skipped trigger counter scan_parameters=scan_parameters # dict ) try: socket.send_json(data_meta_data, flags=zmq.SNDMORE | zmq.NOBLOCK) - socket.send(data[0], flags=zmq.NOBLOCK) # PyZMQ supports sending numpy arrays without copying any data + # PyZMQ supports sending numpy arrays without copying any data + socket.send(data[0], flags=zmq.NOBLOCK) except zmq.Again: pass + def close(socket): - if socket!=None: + if socket is not None: logging.info('Closing socket connection') socket.close() # close here, do not wait for garbage collector diff --git a/pytlu/online_monitor/start_pytlu_online_monitor.py b/pytlu/online_monitor/start_pytlu_online_monitor.py new file mode 100644 index 0000000..048cc7c --- /dev/null +++ b/pytlu/online_monitor/start_pytlu_online_monitor.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +''' Entry point to simplify the usage from command line for +the online_monitor with pytlu plugins. Not really needed +start_online_monitor config.yaml would also work... +''' +import logging +import sys +import os +import subprocess + +import psutil +from PyQt5 import Qt + +from online_monitor.OnlineMonitor import OnlineMonitorApplication +from online_monitor.utils import utils + + +def kill(proc): + ''' Kill process by id, including subprocesses. + + Works for Linux and Windows + ''' + process = psutil.Process(proc.pid) + for child_proc in process.children(recursive=True): + child_proc.kill() + process.kill() + + +def run_script_in_shell(script, arguments, command=None): + if os.name == 'nt': + creationflags = subprocess.CREATE_NEW_PROCESS_GROUP + else: + creationflags = 0 + return subprocess.Popen("%s %s %s" % ('python' if not command else command, + script, arguments), shell=True, + creationflags=creationflags) + + +def main(): + if sys.argv[1:]: + args = utils.parse_arguments() + else: # no config yaml provided -> start online monitor with std. settings + class Dummy(object): + def __init__(self): + folder = os.path.dirname(os.path.realpath(__file__)) + self.config_file = os.path.join(folder, r'configuration.yaml') + self.log = 'INFO' + args = Dummy() + logging.info('No configuration file provided! Use std. settings!') + + utils.setup_logging(args.log) + + # Start the producer + producer_manager_process = run_script_in_shell('', + args.config_file, + 'start_producer_sim') + # Start the converter + converter_manager_process = run_script_in_shell('', + args.config_file, + 'start_converter') + +# Helper function to run code after OnlineMonitor Application exit + def appExec(): + app.exec_() + # Stop other processes + try: + kill(converter_manager_process) + kill(producer_manager_process) + # If the process was never started it cannot be killed + except psutil.NoSuchProcess: + pass + # Start the online monitor + app = Qt.QApplication(sys.argv) + win = OnlineMonitorApplication(args.config_file) + win.show() + sys.exit(appExec()) + + +if __name__ == '__main__': + main() diff --git a/pytlu/stream_fifo.py b/pytlu/stream_fifo.py old mode 100755 new mode 100644 index 39b1577..d20808b --- a/pytlu/stream_fifo.py +++ b/pytlu/stream_fifo.py @@ -1,28 +1,26 @@ -# -# ------------------------------------------------------------ -# Copyright (c) All rights reserved -# SiLab, Institute of Physics, University of Bonn -# ------------------------------------------------------------ -# - -from basil.HL.RegisterHardwareLayer import RegisterHardwareLayer - - -class stream_fifo(RegisterHardwareLayer): - ''' - ''' - _registers = {'RESET': {'descr': {'addr': 0, 'size': 8, 'properties': ['writeonly']}}, - 'VERSION': {'descr': {'addr': 0, 'size': 8, 'properties': ['ro']}}, - 'SET_COUNT': {'descr': {'addr': 1, 'size': 24}}, - 'SIZE': {'descr': {'addr': 4, 'size': 24}}, - } - - _require_version = "==1" - - def __init__(self, intf, conf): - super(stream_fifo, self).__init__(intf, conf) - - def reset(self): - '''Soft reset the module.''' - self.RESET = 0 - +# ------------------------------------------------------------ +# Copyright (c) All rights reserved +# SiLab, Institute of Physics, University of Bonn +# ------------------------------------------------------------ +# + +from basil.HL.RegisterHardwareLayer import RegisterHardwareLayer + + +class stream_fifo(RegisterHardwareLayer): + ''' Stream FIFO + ''' + _registers = {'RESET': {'descr': {'addr': 0, 'size': 8, 'properties': ['writeonly']}}, + 'VERSION': {'descr': {'addr': 0, 'size': 8, 'properties': ['ro']}}, + 'SET_COUNT': {'descr': {'addr': 1, 'size': 24}}, + 'SIZE': {'descr': {'addr': 4, 'size': 24}}, + } + + _require_version = "==2" + + def __init__(self, intf, conf): + super(stream_fifo, self).__init__(intf, conf) + + def reset(self): + '''Soft reset the module.''' + self.RESET = 0 diff --git a/pytlu/tlu.bit b/pytlu/tlu.bit deleted file mode 100644 index f6e092c..0000000 Binary files a/pytlu/tlu.bit and /dev/null differ diff --git a/pytlu/tlu.py b/pytlu/tlu.py old mode 100755 new mode 100644 index a1e9472..ec8e5b2 --- a/pytlu/tlu.py +++ b/pytlu/tlu.py @@ -1,374 +1,448 @@ -# -# ------------------------------------------------------------ -# Copyright (c) All rights reserved -# SiLab, Institute of Physics, University of Bonn -# ------------------------------------------------------------ -# - -import yaml -import basil -from basil.dut import Dut -import logging -import os -import time -import argparse -import signal -import tables as tb -import numpy as np - -from fifo_readout import FifoReadout -from contextlib import contextmanager - -signal.signal(signal.SIGINT, signal.default_int_handler) -logging.getLogger().setLevel(logging.DEBUG) - - -class Tlu(Dut): - - VERSION = 4 - I2C_MUX = {'DISPLAY': 0, 'LEMO': 1, 'HDMI': 2, 'MB': 3} - I2C_ADDR = {'LED': 0x40, 'TRIGGER_EN': 0x42, 'RESET_EN': 0x44, 'IPSEL': 0x46} - PCA9555 = {'DIR': 6, 'OUT': 2} - IP_SEL = {'RJ45': 0b11, 'LEMO': 0b10} - - def __init__(self, conf=None, log_file=None, data_file=None, monitor_addr=None): - - cnfg = conf - logging.info("Loading configuration file from %s" % conf) - if conf is None: - conf = os.path.dirname(os.path.abspath(__file__)) + os.sep + "tlu.yaml" - - self.data_dtype = np.dtype([('le0', 'u1'), ('le1', 'u1'), ('le2', 'u1'), - ('le3', 'u1'), ('time_stamp', 'u8'), ('trigger_id', 'u4')]) - self.meta_data_dtype = np.dtype([('index_start', 'u4'), ('index_stop', 'u4'), ('data_length', 'u4'), - ('timestamp_start', 'f8'), ('timestamp_stop', 'f8'), ('error', 'u4')]) - - self.run_name = time.strftime("tlu_%Y%m%d_%H%M%S") - self.output_filename = self.run_name - self._first_read = False - - self.log_file = self.output_filename + '.log' - if log_file: - self.log_file = log_file - - self.fh = logging.FileHandler(self.log_file) - self.fh.setFormatter(logging.Formatter("%(asctime)s [%(levelname)-5.5s] %(message)s")) - self.fh.setLevel(logging.DEBUG) - self.logger = logging.getLogger() - self.logger.addHandler(self.fh) - logging.info('Initializing %s', self.__class__.__name__) - - self.data_file = self.output_filename + '.h5' - if data_file: - self.data_file = data_file - - logging.info('Data file name: %s', self.data_file) - - ### open socket for monitor - if (monitor_addr==None): - self.socket=None - else: - try: - self.sender = __import__('pytlu.online_monitor.sender') - self.socket = self.sender.init(monitor_addr) - self.logger.info('Inintialiying online_monitor: connected=%s'%monitor_addr) - except: - self.logger.warn('Inintialiying online_monitor: failed addr=%s'%monitor_addr) - self.socket=None - - super(Tlu, self).__init__(conf) - - def init(self): - super(Tlu, self).init() - - fw_version = self['intf'].read(0x2000, 1)[0] - logging.info("TLU firmware version: %s" % (fw_version)) - if fw_version != self.VERSION: - raise Exception("TLU firmware version does not satisfy version requirements (read: %s, require: %s)" % (fw_version, self.VERSION)) - - # Who know why this is needed but other way first bytes are mising - # every secount time? - self['stream_fifo'].SET_COUNT = 8 * 512 - self['intf'].read(0x0001000000000000, 8 * 512) - - self.write_i2c_config() - - def write_i2c_config(self): - self.write_rj45_leds() - self.write_lemo_leds() - self.write_trigger_en() - self.write_reset_en() - self.write_ip_sel() - - def write_rj45_leds(self): - self['I2C_MUX']['SEL'] = self.I2C_MUX['MB'] - self['I2C_MUX'].write() - - self['i2c'].write(self.I2C_ADDR['LED'], [self.PCA9555['DIR'], 0x00, 0x00]) - - val = self['I2C_LED_CNT'].tobytes().tolist() - self['i2c'].write(self.I2C_ADDR['LED'], [self.PCA9555['OUT'], ~val[0] & 0xff, ~val[1] & 0xff]) - - def write_lemo_leds(self): - self['I2C_MUX']['SEL'] = self.I2C_MUX['LEMO'] - self['I2C_MUX'].write() - - self['i2c'].write(self.I2C_ADDR['LED'], [self.PCA9555['DIR'], 0x00, 0x00]) - - val = self['I2C_LEMO_LEDS'].tobytes().tolist() - self['i2c'].write(self.I2C_ADDR['LED'], [self.PCA9555['OUT'], ~val[0] & 0xff, val[1] & 0xff]) - - def write_trigger_en(self): - self['I2C_MUX']['SEL'] = self.I2C_MUX['MB'] - self['I2C_MUX'].write() - - self['i2c'].write(self.I2C_ADDR['TRIGGER_EN'], [self.PCA9555['DIR'], 0x00, 0x00]) - val = self['I2C_TRIGGER_EN'].tobytes().tolist() - self['i2c'].write(self.I2C_ADDR['TRIGGER_EN'], [self.PCA9555['OUT'], val[0] & 0xff, val[1] & 0xff]) - - def write_reset_en(self): - self['I2C_MUX']['SEL'] = self.I2C_MUX['MB'] - self['I2C_MUX'].write() - - self['i2c'].write(self.I2C_ADDR['RESET_EN'], [self.PCA9555['DIR'], 0x00, 0x00]) - val = self['I2C_RESET_EN'].tobytes().tolist() - self['i2c'].write(self.I2C_ADDR['RESET_EN'], [self.PCA9555['OUT'], val[0] & 0xff, val[1] & 0xff]) - - def write_ip_sel(self): - self['I2C_MUX']['SEL'] = self.I2C_MUX['MB'] - self['I2C_MUX'].write() - - self['i2c'].write(self.I2C_ADDR['IPSEL'], [self.PCA9555['DIR'], 0x00, 0x00]) - val = self['I2C_IP_SEL'].tobytes().tolist() - self['i2c'].write(self.I2C_ADDR['IPSEL'], [self.PCA9555['OUT'], val[0] & 0xff, val[1] & 0xff]) - - def get_fifo_data(self): - stream_fifo_size = self['stream_fifo'].SIZE - - if stream_fifo_size != 0: - how_much_read = (stream_fifo_size / 512 + 1) * 512 - - self['stream_fifo'].SET_COUNT = how_much_read - ret = self['intf'].read(0x0001000000000000, how_much_read) - - retint = np.frombuffer(ret, dtype=self.data_dtype) - retint = retint[retint['time_stamp'] > 0] - - return retint - else: - return np.array([], dtype=self.data_dtype) - - @contextmanager - def readout(self, *args, **kwargs): - if not self._first_read: - time.sleep(0.1) - - self.filter_data = tb.Filters(complib='blosc', complevel=5) - self.filter_tables = tb.Filters(complib='zlib', complevel=5) - self.h5_file = tb.open_file(self.data_file, mode='w', title='TLU') - self.data_table = self.h5_file.create_table(self.h5_file.root, name='raw_data', description=self.data_dtype, title='data', filters=self.filter_data) - self.meta_data_table = self.h5_file.create_table(self.h5_file.root, name='meta_data', description=self.meta_data_dtype, title='meta_data', filters=self.filter_tables) - - self.fifo_readout = FifoReadout(self) - self.fifo_readout.print_readout_status() - self._first_read = True - - self.fifo_readout.start( - callback=self.handle_data, errback=self.handle_err) - yield - self.fifo_readout.stop() - self.fifo_readout.print_readout_status() - - def close(self): - ### close socket - if self.socket!=None: - try: - self.sender.close(self.socket) - except: - pass - - super(Tlu, self).close() - - def handle_data(self, data_tuple): - '''Handling of the data. - ''' - - total_words = self.data_table.nrows - - #print data_tuple[0] - - self.data_table.append(data_tuple[0]) - self.data_table.flush() - - len_raw_data = data_tuple[0].shape[0] - self.meta_data_table.row['timestamp_start'] = data_tuple[1] - self.meta_data_table.row['timestamp_stop'] = data_tuple[2] - self.meta_data_table.row['error'] = data_tuple[3] - self.meta_data_table.row['data_length'] = len_raw_data - self.meta_data_table.row['index_start'] = total_words - total_words += len_raw_data - self.meta_data_table.row['index_stop'] = total_words - self.meta_data_table.row.append() - self.meta_data_table.flush() - - - ##### sending data to online monitor - if self.socket!=None: - try: - self.sender.send_data(self.socket,data_tuple) - except: - self.logger.warn('ScanBase.hadle_data:sender.send_data failed') - try: - self.sender.close(self.socket) - except: - pass - self.socket=None - - def handle_err(self, exc): - pass - - -def main(): - - input_ch = ['CH0', 'CH1', 'CH2', 'CH3'] - output_ch = ['CH0', 'CH1', 'CH2', 'CH3', 'CH4', 'CH5', 'LEMO0', 'LEMO1', 'LEMO2', 'LEMO3'] - - def th_type(x): - if int(x) > 31 or int(x) < 0: - raise argparse.ArgumentTypeError("Threshold is 0 to 31") - return int(x) - - parser = argparse.ArgumentParser( - description='TLU DAQ \n example: sitlu -ie CH0 -oe CH0', formatter_class=argparse.RawTextHelpFormatter) - - parser.add_argument('-ie', '--input_enable', nargs='+', type=str, choices=input_ch, default=[], - help='Enable input channels. Allowed values are ' + ', '.join(input_ch), metavar='CHx') - parser.add_argument('-oe', '--output_enable', nargs='+', type=str, choices=output_ch, required=True, - help='Enable ouput channels. CHx and LEM0x are exclusive. Allowed values are ' + ', '.join(output_ch), metavar='CHx/LEMOx') - parser.add_argument('-th', '--threshold', type=th_type, default=0, - help="Digital threshold for input (in units of 1.5625ns). Default=0", metavar='0...31') - parser.add_argument('-b', '--n_bits', type=th_type, default=16, - help="Number of bits for trigger ID. Should correspond to TLU_TRIGGER_MAX_CLOCK_CYCLES - 1 which is set for TLU module. Default=0", metavar='0...31') - parser.add_argument('-ds', '--distance', type=th_type, default=31, - help="Maximum distance between inputs rise time (in units of 1.5625ns). Default=31, 0=disabled", metavar='0...31') - parser.add_argument('-t', '--test', type=int, - help="Generate triggers with given distance (in units of 25 ns).", metavar='1...n') - parser.add_argument('-c', '--count', type=int, default=0, - help="Number of generated triggers. 0=infinite (default) ", metavar='0...n') - parser.add_argument('--timeout', type=int, default=0xffff, - help="Timeout to wait for DUT. Default=65535, 0=disabled", metavar='0...65535') - parser.add_argument('-inv', '--input_invert', nargs='+', type=str, choices=input_ch, default=[], - help='Invert input. Allowed values are ' + ', '.join(input_ch), metavar='CHx') - parser.add_argument('-l', '--log', type=str, - default=None, help='Name of log file') - parser.add_argument('-d', '--data', type=str, - default=None, help='Name of data file') - parser.add_argument('--monitor_addr', type=str, default=None, - help="Address for online monitor wait for DUT. Default=disabled, Example=tcp://127.0.0.1:5550") - - args = parser.parse_args() - - chip = Tlu(log_file=args.log, data_file=args.data,monitor_addr=args.monitor_addr) - chip.init() - - ch_no = [int(x[-1]) for x in args.output_enable] - for i in range(4): - if ch_no.count(i) > 1: - raise argparse.ArgumentTypeError( - "Output channels. CHx and LEM0x are exclusive") - - for oe in args.output_enable: - if oe[0] == 'C': - chip['I2C_LED_CNT'][oe] = 3 - else: # LEMO - chip['I2C_LEMO_LEDS']['BUSY' + oe[-1]] = 1 - chip['I2C_LEMO_LEDS']['TRIG' + oe[-1]] = 1 - chip['I2C_LEMO_LEDS']['RST' + oe[-1]] = 1 - - for oe in args.output_enable: - no = oe[-1] - if no < 4: - chip['I2C_IP_SEL'][no] = chip.IP_SEL['RJ45'] if oe[0] == 'C' else chip.IP_SEL['LEMO'] - - chip.write_i2c_config() - - chip['tlu_master'].MAX_DISTANCE = args.distance - chip['tlu_master'].THRESHOLD = args.threshold - chip['tlu_master'].TIMEOUT = args.timeout - chip['tlu_master'].N_BITS_TRIGGER_ID = args.n_bits - - in_en = 0 - for ie in args.input_enable: - in_en = in_en | (0x01 << int(ie[-1])) - - out_en = 0 - for oe in args.output_enable: - out_en = out_en | (0x01 << int(oe[-1])) - - chip['tlu_master'].EN_OUTPUT = out_en - - in_inv = 0 - for ie in args.input_invert: - in_inv = in_inv | (0x01 << int(ie[-1])) - - chip['tlu_master'].INVERT_INPUT = in_inv - - def print_log(freq=None): - if freq is not None: - logging.info("Time: %.2f TriggerId: %8d TimeStamp: %16d Skipped: %2d Timeout: %2d Av. Rate: %.2f Hz" % (time.time() - start_time, - chip['tlu_master'].TRIGGER_ID, chip['tlu_master'].TIME_STAMP, chip['tlu_master'].SKIP_TRIGGER_COUNT, chip['tlu_master'].TIMEOUT_COUNTER, freq)) - else: - logging.info("Time: %.2f TriggerId: %8d TimeStamp: %16d Skipped: %2d Timeout: %2d" % (time.time() - start_time, - chip['tlu_master'].TRIGGER_ID, chip['tlu_master'].TIME_STAMP, chip['tlu_master'].SKIP_TRIGGER_COUNT, chip['tlu_master'].TIMEOUT_COUNTER)) - - - if args.test: - logging.info("Starting test...") - with chip.readout(): - chip['test_pulser'].DELAY = args.test - chip['test_pulser'].WIDTH = 1 - chip['test_pulser'].REPEAT = args.count - chip['test_pulser'].START - - start_time = time.time() - time_2 = 0 - trigger_id_2 = 0 - - while(not chip['test_pulser'].is_ready): - time_1 = time.time() - trigger_id_1 = chip['tlu_master'].TRIGGER_ID - freq = (trigger_id_1 - trigger_id_2) / (time_1 - time_2) - print_log(freq=freq) - time_2 = time.time() - trigger_id_2 = chip['tlu_master'].TRIGGER_ID - time.sleep(1) - print_log() - return - - logging.info("Starting ... Ctrl+C to exit") - start_time = time.time() - time_2 = 0 - trigger_id_2 = 0 - stop = False - with chip.readout(): - chip['tlu_master'].EN_INPUT = in_en - print in_en, 'INPUT ENABLE' - while not stop: - try: - time_1 = time.time() - trigger_id_1 = chip['tlu_master'].TRIGGER_ID - freq = (trigger_id_1 - trigger_id_2) / (time_1 - time_2) - print_log(freq=freq) - time_2 = time.time() - trigger_id_2 = chip['tlu_master'].TRIGGER_ID - time.sleep(1) - except KeyboardInterrupt: - chip['tlu_master'].EN_INPUT = 0 - chip['tlu_master'].EN_OUTPUT = 0 - stop = True - print_log() - chip.close() - - -if __name__ == '__main__': - main() +# +# ------------------------------------------------------------ +# Copyright (c) All rights reserved +# SiLab, Institute of Physics, University of Bonn +# ------------------------------------------------------------ +# + +import logging +import os +import sys +import time +import argparse +import signal +from contextlib import contextmanager + +import yaml +import tables as tb +import numpy as np + +from basil.dut import Dut + +from pytlu.fifo_readout import FifoReadout +from pytlu.online_monitor import pytlu_sender + +root_logger = logging.getLogger() +root_logger.setLevel(logging.DEBUG) +root_logger.handlers[0].setFormatter(logging.Formatter("%(asctime)s [%(levelname)-3.3s] %(message)s")) + +stop_run = False + +input_ch = ['CH0', 'CH1', 'CH2', 'CH3'] +output_ch = ['CH0', 'CH1', 'CH2', 'CH3', 'CH4', 'CH5', 'LEMO0', 'LEMO1', 'LEMO2', 'LEMO3'] + + +def handle_sig(signum, frame): + logging.info('Pressed Ctrl-C') + signal.signal(signal.SIGINT, signal.SIG_DFL) + global stop_run + stop_run = True + + +def parse_arguments(eudaq=False): + def th_type(x): + if int(x) > 31 or int(x) < 0: + raise argparse.ArgumentTypeError("Threshold is 0 to 31") + return int(x) + + parser = argparse.ArgumentParser(usage="pytlu_eudaq" if eudaq else "pytlu -ie CH0 -oe CH0", + description='TLU DAQ\n TX_STATE: 0= DISABLED 1=WAIT 2=TRIGGERED (wait for busy HIGH) 4=READ_TRIG (wait for busy LOW) LBS is CH0', formatter_class=argparse.RawTextHelpFormatter) + + parser.add_argument('-ie', '--input_enable', nargs='+', type=str, choices=input_ch, default=[], + help='Enable input channels. Allowed values are ' + ', '.join(input_ch), metavar='CHx') + parser.add_argument('-oe', '--output_enable', nargs='+', type=str, choices=output_ch, default=[], + help='Enable ouput channels. CHx and LEM0x are exclusive. Allowed values are ' + ', '.join(output_ch), metavar='CHx/LEMOx') + parser.add_argument('-th', '--threshold', type=th_type, default=0, + help="Digital threshold for input (in units of 1.5625ns). Default=0", metavar='0...31') + parser.add_argument('-b', '--n_bits_trig_id', type=th_type, default=16, + help="Number of bits for trigger ID. Should correspond to TLU_TRIGGER_MAX_CLOCK_CYCLES - 1 which is set for TLU module. Default=0", metavar='0...31') + parser.add_argument('-ds', '--coincidence_window', type=th_type, default=31, + help="Maximum distance between inputs rise time (in units of 1.5625ns). Default=31, 0=disabled", metavar='0...31') + parser.add_argument('-t', '--test', type=int, + help="Generate triggers with given distance (in units of 25 ns).", metavar='1...n') + parser.add_argument('-c', '--count', type=int, default=0, + help="Number of generated triggers. 0=infinite (default) ", metavar='0...n') + parser.add_argument('--timeout', type=int, default=0x0000, + help="Timeout to wait for DUT. Default=0, 0=disabled. If you need to be synchronous with multiple DUTs choose timeout = 0.", metavar='0...65535') + parser.add_argument('-inv', '--input_invert', nargs='+', type=str, choices=input_ch, default=[], + help='Invert input and detect positive edges. Allowed values are ' + ', '.join(input_ch), metavar='CHx') + parser.add_argument('-f', '--output_folder', type=str, + default=None, help='Output folder of data and log file. Default: /pytlu/output_data') + parser.add_argument('-l', '--log_file', type=str, + default=None, help='Name of log file') + parser.add_argument('-d', '--data_file', type=str, + default=None, help='Name of data file') + parser.add_argument('--monitor_addr', type=str, default=None, + help="Address for online monitor wait for DUT. Default=disabled, Example=tcp://127.0.0.1:5550") + parser.add_argument('--scan_time', type=int, default=0, + help="Scan time in seconds. Default=disabled, disable=0") + + if eudaq: + # additional EUDAQ related arguments + parser.add_argument('address', metavar='address', + help='Destination address', + default='tcp://localhost:44000', + nargs='?') + parser.add_argument('--path', type=str, + help='Absolute path of your eudaq installation') + parser.add_argument('--replay', type=str, + help='Raw data file to replay for testing') + parser.add_argument('--delay', type=float, + help='Additional delay when replaying data in seconds') + + args = parser.parse_args() + + return args + + +def print_log(trg_rate, trg_rate_acc, trg_number, skipped_trigger, timeout_counter, tx_state): + ''' + Print logging message. + + Parameters: + ----------- + trg_rate: float + Trigger rate on scintillator inputs + trg_rate_acc: float + Real trigger rate (rate of triggers accepted by DUTs) + ''' + logging.info("Trigger: %8d | Skip: %8d | Timeout: %2d | Rate: %.2f (%.2f) Hz | TxState: %06x" % ( + trg_number, skipped_trigger, timeout_counter, trg_rate_acc, trg_rate, tx_state)) + + +def create_configuration(args): + config = {} + for arg in vars(args): + config[arg] = getattr(args, arg) + return config + + +class Tlu(Dut): + I2C_MUX = {'DISPLAY': 0, 'LEMO': 1, 'HDMI': 2, 'MB': 3} + I2C_ADDR = {'LED': 0x40, 'TRIGGER_EN': 0x42, 'RESET_EN': 0x44, 'IPSEL': 0x46} + PCA9555 = {'DIR': 6, 'OUT': 2} + IP_SEL = {'RJ45': 0b11, 'LEMO': 0b10} + + def __init__(self, conf=None, output_folder=None, log_file=None, data_file=None, monitor_addr=None): + if conf is None: + conf = os.path.dirname(os.path.abspath(__file__)) + os.sep + "tlu.yaml" + logging.info("Loading configuration file from %s" % conf) + + self.data_dtype = np.dtype([('le0', 'u1'), ('le1', 'u1'), ('le2', 'u1'), + ('le3', 'u1'), ('time_stamp', 'u8'), ('trigger_id', 'u4')]) + self.meta_data_dtype = np.dtype([('index_start', 'u4'), ('index_stop', 'u4'), ('data_length', 'u4'), + ('timestamp_start', 'f8'), ('timestamp_stop', 'f8'), ('error', 'u4'), + ('skipped_triggers', 'u8')]) + + self.run_name = time.strftime("%Y%m%d_%H%M%S_tlu") + self.output_filename = self.run_name + self._first_read = False + + if output_folder: + self.output_folder = output_folder + else: + self.output_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'output_data') + if not os.path.exists(self.output_folder): + os.makedirs(self.output_folder) + + if log_file: + self.log_file = os.path.join(self.output_folder, log_file + ".log") + else: + self.log_file = os.path.join(self.output_folder, self.output_filename + ".log") + + if data_file: + self.data_file = os.path.join(self.output_folder, data_file + ".h5") + else: + self.data_file = os.path.join(self.output_folder, self.output_filename + ".h5") + + logging.info('Log file name: %s', self.log_file) + logging.info('Data file name: %s', self.data_file) + + self.fh = logging.FileHandler(self.log_file) + self.fh.setFormatter(logging.Formatter("%(asctime)s [%(levelname)-5.5s] %(message)s")) + self.fh.setLevel(logging.DEBUG) + self.logger = logging.getLogger() + self.logger.addHandler(self.fh) + logging.info('Initializing %s', self.__class__.__name__) + + # open socket for monitor + if monitor_addr is None: + self.socket = None + else: + try: + self.socket = pytlu_sender.init(monitor_addr) + self.logger.info('Initializing online_monitor: connected to %s' % monitor_addr) + except Exception: + self.logger.warning('Initializing online_monitor: failed to connect to %s' % monitor_addr) + self.socket = None + + super(Tlu, self).__init__(conf) + + def init(self): + super(Tlu, self).init() + + fw_version = self['intf'].read(0x2000, 1)[0] + logging.info("TLU firmware version: %s" % (fw_version)) + if int(self.version) != fw_version: + raise Exception("TLU firmware version does not match DUT configuration file (read: %s, require: %s)" % (fw_version, int(self.version))) + + # Who know why this is needed but other way first bytes are mising + # every secount time? + self['stream_fifo'].SET_COUNT = 8 * 512 + self['intf'].read(0x0001000000000000, 8 * 512) + self['tlu_master'].get_configuration() + + self.write_i2c_config() + + def write_i2c_config(self): + self.write_rj45_leds() + self.write_lemo_leds() + self.write_trigger_en() + self.write_reset_en() + self.write_ip_sel() + + def write_rj45_leds(self): + self['I2C_MUX']['SEL'] = self.I2C_MUX['MB'] + self['I2C_MUX'].write() + + self['i2c'].write(self.I2C_ADDR['LED'], [self.PCA9555['DIR'], 0x00, 0x00]) + + val = self['I2C_LED_CNT'].tobytes().tolist() + self['i2c'].write(self.I2C_ADDR['LED'], [self.PCA9555['OUT'], ~val[0] & 0xff, ~val[1] & 0xff]) + + def write_lemo_leds(self): + self['I2C_MUX']['SEL'] = self.I2C_MUX['LEMO'] + self['I2C_MUX'].write() + + self['i2c'].write(self.I2C_ADDR['LED'], [self.PCA9555['DIR'], 0x00, 0x00]) + + val = self['I2C_LEMO_LEDS'].tobytes().tolist() + self['i2c'].write(self.I2C_ADDR['LED'], [self.PCA9555['OUT'], ~val[0] & 0xff, val[1] & 0xff]) + + def write_trigger_en(self): + self['I2C_MUX']['SEL'] = self.I2C_MUX['MB'] + self['I2C_MUX'].write() + + self['i2c'].write(self.I2C_ADDR['TRIGGER_EN'], [self.PCA9555['DIR'], 0x00, 0x00]) + val = self['I2C_TRIGGER_EN'].tobytes().tolist() + self['i2c'].write(self.I2C_ADDR['TRIGGER_EN'], [self.PCA9555['OUT'], val[0] & 0xff, val[1] & 0xff]) + + def write_reset_en(self): + self['I2C_MUX']['SEL'] = self.I2C_MUX['MB'] + self['I2C_MUX'].write() + + self['i2c'].write(self.I2C_ADDR['RESET_EN'], [self.PCA9555['DIR'], 0x00, 0x00]) + val = self['I2C_RESET_EN'].tobytes().tolist() + self['i2c'].write(self.I2C_ADDR['RESET_EN'], [self.PCA9555['OUT'], val[0] & 0xff, val[1] & 0xff]) + + def write_ip_sel(self): + self['I2C_MUX']['SEL'] = self.I2C_MUX['MB'] + self['I2C_MUX'].write() + + self['i2c'].write(self.I2C_ADDR['IPSEL'], [self.PCA9555['DIR'], 0x00, 0x00]) + val = self['I2C_IP_SEL'].tobytes().tolist() + self['i2c'].write(self.I2C_ADDR['IPSEL'], [self.PCA9555['OUT'], val[0] & 0xff, val[1] & 0xff]) + + def get_fifo_data(self): + stream_fifo_size = self['stream_fifo'].SIZE + if stream_fifo_size >= 16: + how_much_read = (stream_fifo_size // 512 + 1) * 512 + self['stream_fifo'].SET_COUNT = how_much_read + ret = self['intf'].read(0x0001000000000000, how_much_read) + retint = np.frombuffer(ret, dtype=self.data_dtype) + retint = retint[retint['time_stamp'] > 0] + return retint + # return ret + else: + return np.array([], dtype=self.data_dtype) + # return np.empty([], dtype=np.uint8) + + @contextmanager + def readout(self, *args, **kwargs): + if not self._first_read: + self.filter_data = tb.Filters(complib='blosc', complevel=5) + self.filter_tables = tb.Filters(complib='zlib', complevel=5) + self.h5_file = tb.open_file(self.data_file, mode='w', title='TLU') + self.data_table = self.h5_file.create_table(self.h5_file.root, name='raw_data', description=self.data_dtype, title='data', filters=self.filter_data) + self.meta_data_table = self.h5_file.create_table(self.h5_file.root, name='meta_data', description=self.meta_data_dtype, title='meta_data', filters=self.filter_tables) + self.meta_data_table.attrs.kwargs = yaml.dump(kwargs) + + self.fifo_readout = FifoReadout(self) + self.fifo_readout.print_readout_status() + self._first_read = True + + self.fifo_readout.start(callback=self.handle_data, + errback=self.handle_err) + try: + yield + finally: + try: + self.fifo_readout.stop() + except Exception: + self.fifo_readout.stop(timeout=0.0) + self.fifo_readout.print_readout_status() + self.meta_data_table.attrs.config = yaml.dump(self.get_configuration()) + + def close(self): + try: + self.h5_file.close() + except Exception: + pass + # close socket + if self.socket is not None: + try: + pytlu_sender.close(self.socket) + except Exception: + pass + super(Tlu, self).close() + + def handle_data(self, data_tuple): + '''Handling of the data. + ''' + + total_words = self.data_table.nrows + self.data_table.append(data_tuple[0]) + self.data_table.flush() + + len_raw_data = data_tuple[0].shape[0] + self.meta_data_table.row['timestamp_start'] = data_tuple[1] + self.meta_data_table.row['timestamp_stop'] = data_tuple[2] + self.meta_data_table.row['error'] = data_tuple[3] + self.meta_data_table.row['skipped_triggers'] = data_tuple[4] + self.meta_data_table.row['data_length'] = len_raw_data + self.meta_data_table.row['index_start'] = total_words + total_words += len_raw_data + self.meta_data_table.row['index_stop'] = total_words + self.meta_data_table.row.append() + self.meta_data_table.flush() + + # sending data to online monitor + if self.socket is not None: + try: + pytlu_sender.send_data(self.socket, data_tuple, len_raw_data) + except Exception: + self.logger.warning('online_monitor.pytlu_sender.send_data failed %s' % str(sys.exc_info())) + try: + pytlu_sender.close(self.socket) + except Exception: + pass + self.socket = None + + def handle_err(self, exc): + self.logger.warning(exc[1].__class__.__name__ + ": " + str(exc[1])) + + def configure(self, config): + ''' Configure TLU. + ''' + ch_no = [int(x[-1]) for x in config['output_enable']] + for i in range(4): + if ch_no.count(i) > 1: + raise argparse.ArgumentTypeError("Output channels. CHx and LEM0x are exclusive") + + for oe in config['output_enable']: + if oe[0] == 'C': + self['I2C_LED_CNT'][oe] = 3 + else: # LEMO + self['I2C_LEMO_LEDS']['BUSY' + oe[-1]] = 1 + self['I2C_LEMO_LEDS']['TRIG' + oe[-1]] = 1 + self['I2C_LEMO_LEDS']['RST' + oe[-1]] = 1 + + for oe in config['output_enable']: + no = oe[-1] + if oe in output_ch[:4]: # TODO: why is this needed + self['I2C_IP_SEL'][no] = self.IP_SEL['RJ45'] if oe[0] == 'C' else self.IP_SEL['LEMO'] + + self.write_i2c_config() + + self['tlu_master'].MAX_DISTANCE = config['coincidence_window'] + self['tlu_master'].THRESHOLD = config['threshold'] + self['tlu_master'].TIMEOUT = config['timeout'] + self['tlu_master'].N_BITS_TRIGGER_ID = config['n_bits_trig_id'] + + in_en = 0 + for ie in config['input_enable']: + in_en = in_en | (0x01 << int(ie[-1])) + + out_en = 0 + for oe in config['output_enable']: + out_en = out_en | (0x01 << int(oe[-1])) + self['tlu_master'].EN_OUTPUT = out_en + + in_inv = 0 + for ie in config['input_invert']: + in_inv = in_inv | (0x01 << int(ie[-1])) + self['tlu_master'].INVERT_INPUT = in_inv + + if config['test']: + self['test_pulser'].DELAY = config['test'] + self['test_pulser'].WIDTH = 1 + self['test_pulser'].REPEAT = config['count'] + + return in_en, out_en + + +def main(): + # Parse arguments + args = parse_arguments() + + # Create configuration dict + config = create_configuration(args) + + chip = Tlu(output_folder=config['output_folder'], log_file=config['log_file'], data_file=config['data_file'], monitor_addr=config['monitor_addr']) + chip.init() + + in_en, _ = chip.configure(config) + + start_time = 0 + trigger_id_start = 0 + skipped_triggers_start = 0 + + logging.info("Starting... Press Ctrl+C to exit...") + signal.signal(signal.SIGINT, handle_sig) + if config['test']: + logging.info("Starting internal trigger generation...") + with chip.readout(): + chip['test_pulser'].START # Start test pulser + while not chip['test_pulser'].is_ready and not stop_run: + # Calculate parameter for logging output + actual_time = time.time() + actual_trigger_id = chip['tlu_master'].TRIGGER_ID + actual_skipped_triggers = chip['tlu_master'].SKIP_TRIG_COUNTER + trg_rate_acc = (actual_trigger_id - trigger_id_start) / (actual_time - start_time) + trg_rate = trg_rate_acc + (actual_skipped_triggers - skipped_triggers_start) / (actual_time - start_time) + timeout_counter = chip['tlu_master'].TIMEOUT_COUNTER + tx_state = chip['tlu_master'].TX_STATE + start_time = actual_time + trigger_id_start = actual_trigger_id + skipped_triggers_start = actual_skipped_triggers + print_log(trg_rate, trg_rate_acc, actual_trigger_id, actual_skipped_triggers, timeout_counter, tx_state) + time.sleep(1) + # reset pulser in case of abort + chip['test_pulser'].RESET + else: + logging.info("Triggering on scintillator inputs: {0}".format(config['input_enable'])) + with chip.readout(): + chip['tlu_master'].EN_INPUT = in_en # Enable inputs + while not stop_run: + # Calculate parameter for logging output + actual_time = time.time() + actual_trigger_id = chip['tlu_master'].TRIGGER_ID + actual_skipped_triggers = chip['tlu_master'].SKIP_TRIG_COUNTER + trg_rate_acc = (actual_trigger_id - trigger_id_start) / (actual_time - start_time) + trg_rate = trg_rate_acc + (actual_skipped_triggers - skipped_triggers_start) / (actual_time - start_time) + timeout_counter = chip['tlu_master'].TIMEOUT_COUNTER + tx_state = chip['tlu_master'].TX_STATE + start_time = actual_time + trigger_id_start = actual_trigger_id + skipped_triggers_start = actual_skipped_triggers + print_log(trg_rate, trg_rate_acc, actual_trigger_id, actual_skipped_triggers, timeout_counter, tx_state) + time.sleep(1) + + # close and disable inputs and outputs + chip['tlu_master'].EN_INPUT = 0 + chip['tlu_master'].EN_OUTPUT = 0 + chip.close() + + +if __name__ == '__main__': + main() diff --git a/pytlu/tlu.yaml b/pytlu/tlu.yaml old mode 100755 new mode 100644 index 1d0f862..2df4525 --- a/pytlu/tlu.yaml +++ b/pytlu/tlu.yaml @@ -5,38 +5,43 @@ # ------------------------------------------------------------ # +name : tlu_v0.x +version : 5 + transfer_layer: - name : intf - type : pytlu.ZestSC1TL init: - bit_file : tlu.bit + type : pytlu.ZestSC1TL + init: + bit_file : firmware/tlu.bit + hw_drivers: - name : gpio type : gpio interface : intf base_addr : 0x3000 - size : 8 + size : 8 - name : i2c type : i2c interface : intf base_addr : 0x4000 - + - name : tlu_master type : pytlu.tlu_master interface : intf base_addr : 0x5000 - + - name : test_pulser type : pulse_gen interface : intf base_addr : 0x6000 - + - name : stream_fifo type : pytlu.stream_fifo interface : intf base_addr : 0x7000 - - + + registers: - name : I2C_MUX type : StdRegister @@ -70,7 +75,7 @@ registers: - name : CH5 size : 2 offset : 3 - + - name : I2C_LEMO_LEDS type : StdRegister size : 16 @@ -112,7 +117,7 @@ registers: - name : BUSY3 size : 1 offset : 15 - + - name : I2C_TRIGGER_EN type : StdRegister size : 16 @@ -127,7 +132,7 @@ registers: - name : RJ45 size : 4 offset : 3 - + - name : I2C_RESET_EN type : StdRegister size : 16 @@ -142,7 +147,7 @@ registers: - name : RJ45 size : 4 offset : 3 - + - name : I2C_IP_SEL type : StdRegister size : 16 @@ -165,4 +170,3 @@ registers: - name : '3' size : 2 offset : 9 - \ No newline at end of file diff --git a/pytlu/tlu_eudaq.py b/pytlu/tlu_eudaq.py new file mode 100644 index 0000000..17b39fb --- /dev/null +++ b/pytlu/tlu_eudaq.py @@ -0,0 +1,342 @@ +# +# ------------------------------------------------------------ +# Copyright (c) All rights reserved +# SiLab, Institute of Physics, University of Bonn +# ------------------------------------------------------------ +# + +''' + This script connects pytlu to the EUDAQ 1.x data acquisition system. +''' + +import os +import time +import sys + +import numpy as np +import tables as tb +from tqdm import tqdm +import yaml +import logging +from pytlu.tlu import Tlu +from pytlu import tlu + +root_logger = logging.getLogger() +root_logger.setLevel(logging.DEBUG) +root_logger.handlers[0].setFormatter(logging.Formatter("%(asctime)s [%(levelname)-3.3s] %(message)s")) + + +class EudaqScan(Tlu): + def set_callback(self, fun): + ''' + Set function to be called for each raw data chunk of one trigger + ''' + + self.callback = fun + self.last_trigger_number = -1 + self.event_counter = 0 # FIXME: Start at 0 or 1? + + def handle_data(self, data_tuple): + ''' + Called on every readout (a few Hz) + Sends data per event by checking for the trigger word that comes first. + ''' + + super(EudaqScan, self).handle_data(data_tuple) + skipped_triggers = data_tuple[4] + raw_data = data_tuple[0] + n_triggers = raw_data.shape[0] + for i in range(n_triggers): + # Split can return empty data, thus do not return send empty data + # Otherwise fragile EUDAQ will fail. It is based on very simple event counting only + if np.any(raw_data['trigger_id']): + actual_trigger_number = raw_data[i]['trigger_id'] + data = raw_data[i] + # Check for jumps in trigger number + if actual_trigger_number != self.last_trigger_number + 1: + logging.warning('Expected != Measured trigger number: %d != %d', self.last_trigger_number + 1, actual_trigger_number) + self.last_trigger_number = actual_trigger_number + self.callback(data=data, skipped_triggers=skipped_triggers, event_counter=self.event_counter) + self.event_counter += 1 + + +def replay_tlu_data(data_file, real_time=True): + ''' + Replay data from file. + + Parameters + ---------- + real_time: boolean + Delays return if replay is too fast to keep + replay speed at original data taking speed. + ''' + + with tb.open_file(data_file, mode="r") as in_file_h5: + meta_data = in_file_h5.root.meta_data[:] + raw_data = in_file_h5.root.raw_data + n_readouts = meta_data.shape[0] + + last_readout_time = time.time() + + last_trigger_number = -1 + + for i in tqdm(range(n_readouts)): + # Raw data indeces of readout + i_start = meta_data['index_start'][i] + i_stop = meta_data['index_stop'][i] + + t_start = meta_data[i]['timestamp_start'] + + # Determine replay delays + if i == 0: # Initialize on first readout + last_timestamp_start = t_start + now = time.time() + delay = now - last_readout_time + additional_delay = t_start - last_timestamp_start - delay + if real_time and additional_delay > 0: + # Wait if send too fast, especially needed when readout was + # stopped during data taking (e.g. for mask shifting) + time.sleep(additional_delay) + last_readout_time = time.time() + last_timestamp_start = t_start + + actual_data = raw_data[i_start:i_stop] + + skipped_triggers = meta_data[i]['skipped_triggers'] + for dat in actual_data: + actual_trigger_number = dat['trigger_id'] + trg_number, trg_timestamp = dat['trigger_id'], dat['time_stamp'] + # Check for jumps in trigger number + if actual_trigger_number != last_trigger_number + 1: + logging.warning('Expected != Measured trigger number: %d != %d', last_trigger_number + 1, actual_trigger_number) + last_trigger_number = actual_trigger_number + + yield trg_number, trg_timestamp, skipped_triggers + + +def main(): + chip = None + + args = tlu.parse_arguments(eudaq=True) + + # Create configuration dict + config = tlu.create_configuration(args) + + # Import EUDAQ python wrapper with error handling + try: + from PyEUDAQWrapper import PyTluProducer + except ImportError: + if not config['path']: + logging.error('Cannot find PyEUDAQWrapper! Please specify the path of your EUDAQ installation!') + return + else: + wrapper_path = os.path.join(config['path'], 'python/') + sys.path.append(os.path.join(config['path'], 'python/')) + try: + from PyEUDAQWrapper import PyTluProducer + except ImportError: + logging.error('Cannot find PyEUDAQWrapper in %s', wrapper_path) + return + + logging.info('Connect to %s', config['address']) + + if config['replay']: + if os.path.isfile(config['replay']): + logging.info('Replay %s', config['replay']) + else: + logging.error('Cannot open %s for replay!', config['replay']) + delay = config['delay'] if config['delay'] else 0. + + pp = PyTluProducer(config['address']) + + def get_dut_status(): + ''' Get DUT status and convert into EUDAQ format. + For details see EUDAQ user manual. + ''' + if chip is not None: + tx_state = chip['tlu_master'].TX_STATE + else: + return + tx_state_str = [] + for i in range(6): + if (out_en >> i) & 0x01: + dut_state = (tx_state >> (4 * i)) & 0xF + if dut_state in [1, 2]: # convert to ugly EUDAQ format + dut_state -= 1 + tx_state_str.append("-%i," % dut_state) + else: + tx_state_str.append("--,") + tlu_status = ''.join(tx_state_str) + tlu_status = tlu_status + ' (-,-)' # no veto and DMA state availabe + return tlu_status + + def send_data_to_eudaq(data, skipped_triggers, event_counter): + ''' + Send data to EUDAQ. + + Parameters: + ------------ + data: tuple + Tuple containing (trigger number, trigger timestamp, skipped triggers) of actual readout data. + event_counter: int + Event number (number of received triggers) + ''' + trg_number, trg_timestamp = data['trigger_id'], data['time_stamp'] + # According to EUDAQ nomenclature + particles = trg_number + skipped_triggers # amount of possible triggers (accepted + skipped) + status = get_dut_status() # TLU status according to EUDAQ format + scalers = '-, -, -, -' # input triggers on each scinitllator input (TODO: not yet implemented) + pp.SendEventExtraInfo((event_counter, trg_timestamp, trg_number), particles, status, scalers) # Send data to EUDAQ + + # Start state mashine, keep connection until termination of euRun + while not pp.Error and not pp.Terminating: + # Wait for configure cmd from RunControl + while not pp.Configuring and not pp.Terminating: + if pp.StartingRun: + break + time.sleep(0.1) + + # Check if configuration received + if pp.Configuring: + logging.info('Configuring...') + if not config['replay']: # Only need configure step if not replaying data + if chip is None: # Init TLU + chip = EudaqScan(output_folder=config['output_folder'], log_file=config['log_file'], data_file=config['data_file'], monitor_addr=config['monitor_addr']) + chip.init() + chip.set_callback(send_data_to_eudaq) # Set callback function in order to send data to EUDAQ + + # Read configuration file, map to pytlu format and update already existing config + trigger_interval = float(pp.GetConfigParameter(item="TriggerInterval", default=False)) # (auto) trigger interval in units of 1 ms + config["test"] = int(trigger_interval * 1e-3 / (25 * 1e-9)) # map to pytlu format (in units of 25 ns) + + and_mask = int(pp.GetConfigParameter(item="AndMask", default=0)) # bitmask for scintillator input channels + and_mask_list = [] + if and_mask > 0: + for i in range(6): + if and_mask & (0b1 << i): + and_mask_list.append('CH%i' % i) + config['input_enable'] = and_mask_list + else: + config['input_enable'] = [] + + dut_mask = int(pp.GetConfigParameter(item="DutMask", default=0)) # bitmask for DUT channels + dut_mask_list = [] + if dut_mask > 0: + for i in range(6): + if dut_mask & (0b1 << i): + dut_mask_list.append('CH%i' % i) + config['output_enable'] = dut_mask_list + else: + config['output_enable'] = [] + + # Configure TLU + in_en, out_en = chip.configure(config) + pp.Configuring = True + + # Check for start of run cmd from RunControl + while not pp.StartingRun and not pp.Terminating: + if pp.Configuring: + break + time.sleep(0.1) + + # Check if we are starting: + if pp.StartingRun: + logging.info('Starting run...') + + start_time = 0 + trigger_id_start = 0 + skipped_triggers_start = 0 + + if not config['replay']: + # Start pytlu + pp.StartingRun = True # set status and send BORE + with chip.readout(): + if config["test"] > 0: + logging.info("Starting internal trigger generation...") + # Start test pulser + # FIXME: This is bad since belongs to configure step, but pulser cannot simply be stopped w/o reset? + chip['test_pulser'].DELAY = config["test"] + chip['test_pulser'].WIDTH = 1 + chip['test_pulser'].REPEAT = config["count"] + chip['test_pulser'].START + else: + logging.info("Triggering on scintillator inputs: {0}".format(config['input_enable'])) + # Enable inputs + chip['tlu_master'].EN_INPUT = in_en + stop_run = False + while not stop_run: + if pp.Error or pp.Terminating: + if pp.Error: + logging.warning('Received error') + logging.info('Stopping run...') + # Disable output + if config["test"]: + # FIXME: pusler cannot simply be stopped. Has to be resetted such that configuration is lost. + chip['test_pulser'].RESET + else: + chip['tlu_master'].EN_INPUT = 0 + # FIXME: using not thread safe variable + stop_run = True + break + if pp.StoppingRun: + logging.info('Stopping run...') + # Disable output + if config["test"]: + # FIXME: pulser cannot simply be stopped. Has to be resetted such that configuration is lost. + chip['test_pulser'].RESET + else: + chip['tlu_master'].EN_INPUT = 0 + # FIXME: using not thread safe variable + stop_run = True + break + # Calculate parameter for logging output + actual_time = time.time() + actual_trigger_id = chip['tlu_master'].TRIGGER_ID + actual_skipped_triggers = chip['tlu_master'].SKIP_TRIG_COUNTER + trg_rate_acc = (actual_trigger_id - trigger_id_start) / (actual_time - start_time) + trg_rate = trg_rate_acc + (actual_skipped_triggers - skipped_triggers_start) / (actual_time - start_time) + timeout_counter = chip['tlu_master'].TIMEOUT_COUNTER + tx_state = chip['tlu_master'].TX_STATE + start_time = actual_time + trigger_id_start = actual_trigger_id + skipped_triggers_start = actual_skipped_triggers + tlu.print_log(trg_rate, trg_rate_acc, actual_trigger_id, actual_skipped_triggers, timeout_counter, tx_state) + time.sleep(1) + else: + logging.info("Replaying data...") + pp.StartingRun = True # set status and send BORE + for event_counter, data in enumerate(replay_tlu_data(data_file=config['replay'])): + trg_number, trg_timestamp, skipped_triggers = data + # According to EUDAQ nomenclature + particles = trg_number + skipped_triggers # amount of possible triggers (accepted + skipped) + status = get_dut_status() # TLU status (TX state) + scalers = '-, -, -, -' # input triggers on each scinitllator input (TODO: not yet implemented) + pp.SendEventExtraInfo((event_counter, trg_timestamp, trg_number), particles, status, scalers) # Send data to EUDAQ + if pp.Error or pp.Terminating: + break + if pp.StoppingRun: + break + time.sleep(delay) + + # Abort conditions + if pp.Error or pp.Terminating: + pp.StoppingRun = False # Set status and send EORE + # Check if the run is stopping regularly + if pp.StoppingRun: + pp.StoppingRun = True # Set status and send EORE + + # Back to check for configured + start run state + time.sleep(0.1) + + # Proper close and disable inputs and outputs in case of termination or error + logging.info('Closing TLU...') + chip['tlu_master'].EN_INPUT = 0 + chip['tlu_master'].EN_OUTPUT = 0 + chip['test_pulser'].RESET + chip.close() + + +if __name__ == "__main__": + # When run in development environment, eudaq path can be added with: + sys.path.append('/home/user/git/eudaq/python/') + main() diff --git a/pytlu/tlu_master.py b/pytlu/tlu_master.py index a560d0f..7624b9b 100644 --- a/pytlu/tlu_master.py +++ b/pytlu/tlu_master.py @@ -1,46 +1,45 @@ -# -# ------------------------------------------------------------ -# Copyright (c) All rights reserved -# SiLab, Institute of Physics, University of Bonn -# ------------------------------------------------------------ -# - -from basil.HL.RegisterHardwareLayer import RegisterHardwareLayer - - -class tlu_master(RegisterHardwareLayer): - ''' - ''' - _registers = {'RESET': {'descr': {'addr': 0, 'size': 8, 'properties': ['writeonly']}}, - 'VERSION': {'descr': {'addr': 0, 'size': 8, 'properties': ['ro']}}, - 'START': {'descr': {'addr': 1, 'size': 8, 'properties': ['writeonly']}}, - 'READY': {'descr': {'addr': 1, 'size': 1, 'properties': ['ro']}}, - - 'EN_INPUT': {'descr': {'addr': 3, 'size': 4, 'offset': 0}}, - 'INVERT_INPUT': {'descr': {'addr': 3, 'size': 4, 'offset': 4}}, - - 'MAX_DISTANCE': {'descr': {'addr': 4, 'size': 5, 'offset': 0}}, - 'THRESHOLD': {'descr': {'addr': 5, 'size': 5, 'offset': 0}}, - 'EN_OUTPUT': {'descr': {'addr': 6, 'size': 6, 'offset': 0}}, - - 'TIMEOUT': {'descr': {'addr': 7, 'size': 16, 'offset': 0}}, - - 'N_BITS_TRIGGER_ID': {'descr': {'addr': 9, 'size': 5, 'offset': 0}}, - - 'TIME_STAMP': {'descr': {'addr': 16, 'size': 64, 'properties': ['ro']}}, - 'TRIGGER_ID': {'descr': {'addr': 24, 'size': 32, 'properties': ['ro']}}, - 'SKIP_TRIGGER_COUNT': {'descr': {'addr': 28, 'size': 8, 'properties': ['ro']}}, - 'TIMEOUT_COUNTER': {'descr': {'addr': 29, 'size': 8, 'properties': ['ro']}}, - 'LOST_DATA_CNT': {'descr': {'addr': 30, 'size': 8, 'properties': ['ro']}}, - - } - - _require_version = "==1" - - def __init__(self, intf, conf): - super(tlu_master, self).__init__(intf, conf) - - def reset(self): - '''Soft reset the module.''' - self.RESET = 0 - +# +# ------------------------------------------------------------ +# Copyright (c) All rights reserved +# SiLab, Institute of Physics, University of Bonn +# ------------------------------------------------------------ +# + +from basil.HL.RegisterHardwareLayer import RegisterHardwareLayer + + +class tlu_master(RegisterHardwareLayer): + ''' TLU FSM + ''' + _registers = {'RESET': {'descr': {'addr': 0, 'size': 8, 'properties': ['writeonly']}}, + 'VERSION': {'descr': {'addr': 0, 'size': 8, 'properties': ['ro']}}, + 'START': {'descr': {'addr': 1, 'size': 8, 'properties': ['writeonly']}}, + 'READY': {'descr': {'addr': 1, 'size': 1, 'properties': ['ro']}}, + + 'EN_INPUT': {'descr': {'addr': 3, 'size': 4, 'offset': 0}}, + 'INVERT_INPUT': {'descr': {'addr': 3, 'size': 4, 'offset': 4}}, + + 'MAX_DISTANCE': {'descr': {'addr': 4, 'size': 5, 'offset': 0}}, + 'THRESHOLD': {'descr': {'addr': 5, 'size': 5, 'offset': 0}}, + 'EN_OUTPUT': {'descr': {'addr': 6, 'size': 6, 'offset': 0}}, + + 'TIMEOUT': {'descr': {'addr': 7, 'size': 16, 'offset': 0}}, + + 'N_BITS_TRIGGER_ID': {'descr': {'addr': 9, 'size': 5, 'offset': 0}}, + + 'TIME_STAMP': {'descr': {'addr': 16, 'size': 64, 'properties': ['ro']}}, + 'TRIGGER_ID': {'descr': {'addr': 24, 'size': 32, 'properties': ['ro']}}, + 'SKIP_TRIG_COUNTER': {'descr': {'addr': 28, 'size': 32, 'properties': ['ro']}}, + 'TIMEOUT_COUNTER': {'descr': {'addr': 32, 'size': 8, 'properties': ['ro']}}, + 'LOST_DATA_CNT': {'descr': {'addr': 33, 'size': 8, 'properties': ['ro']}}, + 'TX_STATE': {'descr': {'addr': 34, 'size': 24, 'properties': ['ro']}}, + } + + _require_version = "==3" + + def __init__(self, intf, conf): + super(tlu_master, self).__init__(intf, conf) + + def reset(self): + '''Soft reset the module.''' + self.RESET = 0 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..86d6867 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +basil_daq>=3.0.0 +online_monitor>=0.4.1<0.5 +numpy +psutil +qtpy +pyusb +pyyaml +pyzmq +tables +tqdm diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index 66fb270..c4d8ab1 --- a/setup.py +++ b/setup.py @@ -2,32 +2,54 @@ from setuptools import setup from setuptools import find_packages -from platform import system -version = '0.1.0' +import pytlu + +with open('VERSION') as version_file: + version = version_file.read().strip() + +with open('requirements.txt') as f: + install_requires = f.read().splitlines() setup( name='pytlu', version=version, - description='DAQ for TLU', + description='DAQ for EUDAQ TLU', url='https://github.com/SiLab-Bonn/pytlu', license='', long_description='', - author='Janek Fleper, Tomasz Hemperek, Yannick Dieter', + author='Janek Fleper, Tomasz Hemperek, Yannick Dieter, Jens Janssen', maintainer='Tomasz Hemperek', - author_email='j.fleper@gmx.de,hemeprek@uni-bonn.de,dieter@physik.uni-bonn.de', + author_email='j.fleper@gmx.de, hemeprek@uni-bonn.de, dieter@physik.uni-bonn.de, janssen@physik.uni-bonn.de', maintainer_email='hemeprek@uni-bonn.de', packages=find_packages(), - include_package_data=True, - package_data={'': ['README.*'], 'pytlu': ['*.yaml', '*.bit']}, + include_package_data=True, + install_requires=install_requires, + setup_requires=['online_monitor>=0.4.1<0.5'], entry_points={ 'console_scripts': [ 'pytlu = pytlu.tlu:main', + 'pytlu_eudaq = pytlu.tlu_eudaq:main', + 'pytlu_monitor = pytlu.online_monitor.start_pytlu_online_monitor:main', ] }, - install_requires=[ - 'basil_daq == 2.4.10', - 'tables' - ], platforms='any' ) + + +# FIXME: bad practice to put code into setup.py +# Add the online_monitor pytlu plugins +try: + import os + from online_monitor.utils import settings + # Get the absoulte path of this package + package_path = os.path.dirname(pytlu.__file__) + # Add online_monitor plugin folder to entity search paths + settings.add_producer_sim_path(os.path.join(package_path, + 'online_monitor')) + settings.add_converter_path(os.path.join(package_path, + 'online_monitor')) + settings.add_receiver_path(os.path.join(package_path, + 'online_monitor')) +except ImportError: + pass diff --git a/tests/StreamDriver.py b/tests/StreamDriver.py old mode 100755 new mode 100644 index d88ad01..994a4f6 --- a/tests/StreamDriver.py +++ b/tests/StreamDriver.py @@ -21,7 +21,7 @@ class StreamDriver(BusDriver): """Abastract away interactions with the control bus. """ - _signals = ["BUS_CLK", "BUS_RST", "BUS_DATA", "BUS_ADD", "BUS_RD", "BUS_WR", "STREAM_READY", "STREAM_WRITE", "STREAM_DATA"] + _signals = ["BUS_CLK", "BUS_RST", "BUS_DATA", "BUS_ADD", "BUS_RD", "BUS_WR", "STREAM_READY", "STREAM_WRITE", "STREAM_DATA"] _optional_signals = ["BUS_BYTE_ACCESS"] def __init__(self, entity): @@ -38,7 +38,7 @@ def __init__(self, entity): self._has_byte_acces = False self.BASE_ADDRESS_STREAM = 0x0001000000000000 - + # Kick off a clock generator cocotb.fork(Clock(self.clock, 20830).start()) @@ -51,7 +51,7 @@ def init(self): self.bus.BUS_ADD <= self._x self.bus.BUS_DATA <= self._high_impedence self.bus.STREAM_READY <= 0 - + for _ in range(8): yield RisingEdge(self.clock) @@ -71,27 +71,27 @@ def init(self): @cocotb.coroutine def read(self, address, size): result = [] - + if address >= self.BASE_ADDRESS_STREAM: - result = yield self.read_stream(address, size) + result = yield self.read_stream(address, size) else: self.bus.BUS_DATA <= self._high_impedence self.bus.BUS_ADD <= self._x self.bus.BUS_RD <= 0 - + yield RisingEdge(self.clock) - + byte = 0 while(byte <= size): if(byte == size): self.bus.BUS_RD <= 0 else: self.bus.BUS_RD <= 1 - + self.bus.BUS_ADD <= address + byte - + yield RisingEdge(self.clock) - + if(byte != 0): if(self._has_byte_acces and self.bus.BUS_BYTE_ACCESS.value.integer == 0): result.append(self.bus.BUS_DATA.value.integer & 0x000000ff) @@ -99,22 +99,20 @@ def read(self, address, size): result.append((self.bus.BUS_DATA.value.integer & 0x00ff0000) >> 16) result.append((self.bus.BUS_DATA.value.integer & 0xff000000) >> 24) else: - # result.append(self.bus.BUS_DATA.value[24:31].integer & 0xff) + # result.append(self.bus.BUS_DATA.value[24:31].integer & 0xff) if len(self.bus.BUS_DATA.value) == 8: result.append(self.bus.BUS_DATA.value.integer & 0xff) else: - #value = self.bus.BUS_DATA.value[24:31].integer & 0xff - - #workaround for cocotb https://github.com/potentialventures/cocotb/pull/459 + # value = self.bus.BUS_DATA.value[24:31].integer & 0xff + # workaround for cocotb https://github.com/potentialventures/cocotb/pull/459 value = BinaryValue(self.bus.BUS_DATA.value.binstr[24:32]) - result.append(value.integer) - + if(self._has_byte_acces and self.bus.BUS_BYTE_ACCESS.value.integer == 0): byte += 4 else: byte += 1 - + self.bus.BUS_ADD <= self._x self.bus.BUS_DATA <= self._high_impedence yield RisingEdge(self.clock) @@ -149,31 +147,30 @@ def write(self, address, data): self.bus.BUS_WR <= 0 yield RisingEdge(self.clock) - - + @cocotb.coroutine def read_stream(self, address, size): result = [] - + yield RisingEdge(self.clock) self.bus.STREAM_READY <= 1 - - for _ in range(size/2): - + + for _ in range(size // 2): + yield RisingEdge(self.clock) - + while self.bus.STREAM_WRITE.value.integer == 0: yield RisingEdge(self.clock) - + result.append(self.bus.STREAM_DATA.value.integer & 0xff) result.append((self.bus.STREAM_DATA.value.integer >> 8) & 0xff) - + yield RisingEdge(self.clock) yield RisingEdge(self.clock) yield RisingEdge(self.clock) yield RisingEdge(self.clock) self.bus.STREAM_READY <= 0 yield RisingEdge(self.clock) - + raise ReturnValue(result) diff --git a/tests/TestConfig.conf b/tests/TestConfig.conf new file mode 100644 index 0000000..03c8d02 --- /dev/null +++ b/tests/TestConfig.conf @@ -0,0 +1,17 @@ +# This is an example config file, you can adapt it to your needs. +# All text following a # character is treated as comments + +[RunControl] +RunSizeLimit = 1000000000 + +[DataCollector] +FilePattern = "../data/run$6R$X" + +[LogCollector] +SaveLevel = EXTRA +PrintLevel = INFO + +[Producer.TLU] +ConfParameter = 123 + + diff --git a/tests/example.py b/tests/example.py old mode 100755 new mode 100644 index c47e71b..b95f778 --- a/tests/example.py +++ b/tests/example.py @@ -4,67 +4,60 @@ # SiLab, Institute of Physics, University of Bonn # ------------------------------------------------------------ # - + +''' First test/example of pytlu. Generates one million triggers and checks data. +''' + from pytlu.tlu import Tlu -#from importlib import import_module import numpy as np - -import time + +import time if __name__ == '__main__': - #mod = import_module('tlu.ZestSC1Usb') chip = Tlu() - chip.init() - - - np.set_printoptions( linewidth=120 ) - - #raw_input("Press Enter to continue...") - - #size = 512*8 - #chip['stream_fifo'].SET_COUNT = size - #ret = chip['intf'].read(0x0001000000000000, size) - #print size, len(ret), ret - - #chip['stream_fifo'].SET_COUNT = size - #ret = chip['intf'].read(0x0001000000000000, size) - - #for k in range(2): + chip.init() + + np.set_printoptions(linewidth=120) + + # size = 512*8 + # chip['stream_fifo'].SET_COUNT = size + # ret = chip['intf'].read(0x0001000000000000, size) + # print size, len(ret), ret + + # chip['stream_fifo'].SET_COUNT = size + # ret = chip['intf'].read(0x0001000000000000, size) + chip['tlu_master'].EN_INPUT = 0 chip['tlu_master'].MAX_DISTANCE = 31 chip['tlu_master'].THRESHOLD = 10 chip['tlu_master'].EN_OUTPUT = 0 chip['tlu_master'].TIMEOUT = 20 - - how_many = 10*10**6 - chip['test_pulser'].DELAY = 200 -5 + how_many = 10**6 + chip['test_pulser'].DELAY = 200 - 5 chip['test_pulser'].WIDTH = 5 chip['test_pulser'].REPEAT = how_many chip['test_pulser'].START - #time.sleep(0.1) - def get_data(tid): retint = chip.get_fifo_data() - + if len(retint): - ok = np.array_equal(retint['trigger_id'], np.arange(tid, tid + len(retint))) - #print retint['trigger_id'] - print 'ok:', ok, retint['trigger_id'][0], '-', retint['trigger_id'][-1] , float(retint['time_stamp'][-1]) / (40*(10**6)), retint['trigger_id'][-1] - retint['trigger_id'][0], chip['tlu_master'].LOST_DATA_CNT - + ok = np.array_equal(retint['trigger_id'], + np.arange(tid, tid + len(retint))) + print('ok:', ok, retint['trigger_id'][0], '-', retint['trigger_id'][-1], float(retint['time_stamp'][-1]) / (40 * (10**6)), retint['trigger_id'][-1] - retint['trigger_id'][0], chip['tlu_master'].LOST_DATA_CNT) + time.sleep(0.1) - + return len(retint) - + tid = 0 while(not chip['test_pulser'].is_ready): tid += get_data(tid) - + for _ in range(3): tid += get_data(tid) - - print 'TRIGGER_ID', chip['tlu_master'].TRIGGER_ID - print 'LOST_DATA_CNT', chip['tlu_master'].LOST_DATA_CNT - print 'TIMEOUT_COUNTER', chip['tlu_master'].TIME_STAMP, chip['tlu_master'].TIME_STAMP/(40*(10**6)) - \ No newline at end of file + + print('TRIGGER_ID', chip['tlu_master'].TRIGGER_ID) + print('LOST_DATA_CNT', chip['tlu_master'].LOST_DATA_CNT) + print('TIMEOUT_COUNTER', chip['tlu_master'].TIME_STAMP, chip['tlu_master'].TIME_STAMP / (40 * (10**6))) diff --git a/tests/setup_eudaq.sh b/tests/setup_eudaq.sh new file mode 100644 index 0000000..8dddeae --- /dev/null +++ b/tests/setup_eudaq.sh @@ -0,0 +1,7 @@ +git clone -b v1.x-dev https://github.com/eudaq/eudaq # FIXME: silab repo for now +cd eudaq/build +cmake -DBUILD_python=ON -DBUILD_gui=OFF -DBUILD_onlinemon=OFF -DBUILD_runsplitter=OFF -DUSE_ROOT=OFF .. +make install -j 4 +cd ${PWD}/../python/ +export PYTHONPATH="${PYTHONPATH}:${PWD}" +cd ../.. \ No newline at end of file diff --git a/tests/tb.v b/tests/tb.v old mode 100755 new mode 100644 index fc4456f..dc08ab9 --- a/tests/tb.v +++ b/tests/tb.v @@ -117,7 +117,10 @@ localparam FIFO_HIGHADDR = 32'hf200-1; localparam TDC_BASEADDR = 32'hf200; localparam TDC_HIGHADDR = 32'hf300 - 1; - + +localparam PULSE_VETO_BASEADDR = 32'hf300; +localparam PULSE_VETO_HIGHADDR = 32'hf400 - 1; + localparam FIFO_BASEADDR_DATA = 32'h8000_0000; localparam FIFO_HIGHADDR_DATA = 32'h9000_0000; @@ -179,7 +182,7 @@ tlu_controller #( .FIFO_PREEMPT_REQ(), .TRIGGER({7'b0, DUT_TRIGGER[0]}), - .TRIGGER_VETO({7'b0, 1'b1}), + .TRIGGER_VETO({7'b0, VETO_PULSE}), .TLU_TRIGGER(DUT_TRIGGER[0]), .TLU_RESET(DUT_RESET[0]), @@ -282,6 +285,26 @@ bram_fifo ); +// ----- Veto signal pulser ----- // +wire VETO_PULSE; +pulse_gen +#( + .BASEADDR(PULSE_VETO_BASEADDR), + .HIGHADDR(PULSE_VETO_HIGHADDR) +) i_pulse_gen +( + .BUS_CLK(BUS_CLK), + .BUS_RST(BUS_RST), + .BUS_ADD(BUS_ADD), + .BUS_DATA(BUS_DATA), + .BUS_RD(BUS_RD), + .BUS_WR(BUS_WR), + + .PULSE_CLK(dut.CLK40), + .EXT_START(1'b0), + .PULSE(VETO_PULSE) + ); + //cy1472 cy1472( .d(SRAM_DATA), .clk(SRAM_CLK), .a(SRAM_ADD[21:0]), .bws(SRAM_BW_N), // .we_b(SRAM_WE_N), .adv_lb(SRAM_ADV_LD_N), .ce1b(1'b0), .ce2(1'b1), // .ce3b(1'b0), .oeb(SRAM_OE_N), .cenb(1'b0), .mode(1'b0)); diff --git a/tests/test_Sim.py b/tests/test_Sim.py old mode 100755 new mode 100644 index 36ad1c1..8c3f772 --- a/tests/test_Sim.py +++ b/tests/test_Sim.py @@ -4,45 +4,42 @@ # SiLab, Institute of Physics, University of Bonn # ------------------------------------------------------------ # - -import unittest import os -from basil.utils.sim.utils import cocotb_compile_and_run, cocotb_compile_clean -import sys -import yaml import time +import unittest +import yaml + import numpy as np +from basil.utils.sim.utils import cocotb_compile_and_run, cocotb_compile_clean + from pytlu.tlu import Tlu -class TestSim(unittest.TestCase): +class TestSim(unittest.TestCase): def setUp(self): - - root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) #../ - print root_dir + root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) cocotb_compile_and_run( - sim_bus ="StreamDriver", - sim_files = [root_dir + '/tests/tb.v'], - include_dirs = (root_dir, root_dir + "/firmware/src", root_dir + "/tests") - ) - + sim_bus="StreamDriver", + sim_files=[root_dir + '/tests/tb.v'], + include_dirs=(root_dir, root_dir + "/firmware/src", root_dir + "/tests")) + with open(root_dir + '/pytlu/tlu.yaml', 'r') as f: cnfg = yaml.load(f) cnfg['transfer_layer'][0]['type'] = 'SiSim' - cnfg['hw_drivers'].append({'name' : 'SEQ_GEN_TB', 'type' : 'seq_gen', 'interface': 'intf', 'base_addr': 0xc000}) - cnfg['hw_drivers'].append({'name' : 'TLU_TB', 'type' : 'tlu', 'interface': 'intf', 'base_addr': 0xf000}) - cnfg['hw_drivers'].append({'name' : 'FIFO_TB', 'type' : 'bram_fifo', 'interface': 'intf', 'base_addr': 0xf100, 'base_data_addr' : 0x80000000}) - cnfg['hw_drivers'].append({'name' : 'TDC_TB', 'type' : 'tdc_s3', 'interface': 'intf', 'base_addr': 0xf200}) - - seq_tracks = [{'name': 'T0', 'position': 0},{'name': 'T1', 'position': 1},{'name': 'T2', 'position': 2},{'name': 'T3', 'position': 3}] - cnfg['registers'].append({'name' : 'SEQ_TB', 'type' : 'TrackRegister', 'hw_driver': 'SEQ_GEN_TB', 'seq_width': 8, 'seq_size': 8*1024, 'tracks': seq_tracks}) + cnfg['hw_drivers'].append({'name': 'SEQ_GEN_TB', 'type': 'seq_gen', 'interface': 'intf', 'base_addr': 0xc000}) + cnfg['hw_drivers'].append({'name': 'TLU_TB', 'type': 'tlu', 'interface': 'intf', 'base_addr': 0xf000}) + cnfg['hw_drivers'].append({'name': 'FIFO_TB', 'type': 'bram_fifo', 'interface': 'intf', 'base_addr': 0xf100, 'base_data_addr': 0x80000000}) + cnfg['hw_drivers'].append({'name': 'TDC_TB', 'type': 'tdc_s3', 'interface': 'intf', 'base_addr': 0xf200}) + cnfg['hw_drivers'].append({'name': 'VETO_PULSER_TB', 'type': 'pulse_gen', 'interface': 'intf', 'base_addr': 0xf300}) + + seq_tracks = [{'name': 'T0', 'position': 0}, {'name': 'T1', 'position': 1}, {'name': 'T2', 'position': 2}, {'name': 'T3', 'position': 3}] + cnfg['registers'].append({'name': 'SEQ_TB', 'type': 'TrackRegister', 'hw_driver': 'SEQ_GEN_TB', 'seq_width': 8, 'seq_size': 8 * 1024, 'tracks': seq_tracks}) self.dut = Tlu(conf=cnfg) self.dut.init() - def test_single_simle_mode(self): - + def test_single_simple_mode(self): self.dut['TLU_TB'].TRIGGER_COUNTER = 0 self.dut['TLU_TB'].TRIGGER_MODE = 2 self.dut['TLU_TB'].TRIGGER_SELECT = 1 @@ -51,7 +48,7 @@ def test_single_simle_mode(self): self.dut['TDC_TB'].EN_TRIGGER_DIST = 1 self.dut['TDC_TB'].ENABLE = 1 - + self.dut['tlu_master'].EN_INPUT = 1 self.dut['tlu_master'].MAX_DISTANCE = 10 self.dut['tlu_master'].THRESHOLD = 5 @@ -59,29 +56,28 @@ def test_single_simle_mode(self): how_many = 25 self.dut['SEQ_TB'].set_repeat(how_many) - + self.dut['SEQ_TB']['T0'][:] = 0 - self.dut['SEQ_TB']['T0'][16*40:16*44] = 1 - - self.dut['SEQ_TB'].set_size(45*16+1) - self.dut['SEQ_TB'].write(46*16) + self.dut['SEQ_TB']['T0'][16 * 40:16 * 44] = 1 + + self.dut['SEQ_TB'].set_size(45 * 16 + 1) + self.dut['SEQ_TB'].write(46 * 16) self.dut['SEQ_TB'].START - - while(not self.dut['SEQ_TB'].is_ready): + + while not self.dut['SEQ_TB'].is_ready: pass - + self.check_data(how_many, tdc_en=True) - + def test_single_full_mode(self): - self.dut['TLU_TB'].TRIGGER_COUNTER = 0 self.dut['TLU_TB'].TRIGGER_MODE = 3 self.dut['TLU_TB'].TRIGGER_SELECT = 1 self.dut['TLU_TB'].TRIGGER_VETO_SELECT = 2 self.dut['TLU_TB'].TRIGGER_ENABLE = 1 self.dut['TLU_TB'].TRIGGER_DATA_DELAY = 2 - + self.dut['tlu_master'].EN_INPUT = 1 self.dut['tlu_master'].MAX_DISTANCE = 10 self.dut['tlu_master'].THRESHOLD = 0 @@ -90,20 +86,20 @@ def test_single_full_mode(self): self.dut['SEQ_TB'].set_repeat(1) self.dut['SEQ_TB']['T0'][:] = 0 - self.dut['SEQ_TB'].write(40*40) - self.dut['SEQ_TB'].set_size(40*40) - + self.dut['SEQ_TB'].write(40 * 40) + self.dut['SEQ_TB'].set_size(40 * 40) + how_many = 25 for i in range(how_many): self.dut['SEQ_TB']['T0'][:] = 0 - self.dut['SEQ_TB']['T0'][0:i+1] = 1 + self.dut['SEQ_TB']['T0'][0:i + 1] = 1 self.dut['SEQ_TB'].write(32) - + self.dut['SEQ_TB'].START - - while(not self.dut['SEQ_TB'].is_ready): + + while not self.dut['SEQ_TB'].is_ready: pass - + self.check_data(how_many) def test_injection_test(self): @@ -113,44 +109,42 @@ def test_injection_test(self): self.dut['TLU_TB'].TRIGGER_VETO_SELECT = 2 self.dut['TLU_TB'].TRIGGER_ENABLE = 1 self.dut['TLU_TB'].TRIGGER_DATA_DELAY = 2 - + self.dut['tlu_master'].EN_INPUT = 1 self.dut['tlu_master'].MAX_DISTANCE = 10 self.dut['tlu_master'].THRESHOLD = 0 self.dut['tlu_master'].EN_OUTPUT = 1 self.dut['tlu_master'].N_BITS_TRIGGER_ID = 15 - #self.dut['tlu_master'].TIMEOUT = 3 - + # self.dut['tlu_master'].TIMEOUT = 3 + how_many = 50 self.dut['test_pulser'].DELAY = 200 self.dut['test_pulser'].WIDTH = 10 self.dut['test_pulser'].REPEAT = how_many self.dut['test_pulser'].START - - while(not self.dut['test_pulser'].is_ready): + + while not self.dut['test_pulser'].is_ready: pass - + self.check_data(how_many) - + def test_digital_threshold(self): - self.dut['TLU_TB'].TRIGGER_COUNTER = 0 self.dut['TLU_TB'].TRIGGER_MODE = 2 self.dut['TLU_TB'].TRIGGER_SELECT = 1 self.dut['TLU_TB'].TRIGGER_VETO_SELECT = 2 self.dut['TLU_TB'].TRIGGER_ENABLE = 1 - - + self.dut['TDC_TB'].EN_TRIGGER_DIST = 1 self.dut['TDC_TB'].ENABLE = 1 - + self.dut['tlu_master'].EN_INPUT = 1 self.dut['tlu_master'].EN_OUTPUT = 1 self.dut['tlu_master'].MAX_DISTANCE = 10 self.dut['tlu_master'].THRESHOLD = 0 self.dut['SEQ_TB'].set_repeat(1) - + self.dut['SEQ_TB']['T0'][:] = 0 self.dut['SEQ_TB']['T0'][0:1] = 1 self.dut['SEQ_TB']['T0'][1000:1002] = 1 @@ -160,66 +154,63 @@ def test_digital_threshold(self): self.dut['SEQ_TB']['T0'][5000:5031] = 1 self.dut['SEQ_TB']['T0'][6000:6032] = 1 self.dut['SEQ_TB']['T0'][7000:7033] = 1 - + self.dut['SEQ_TB'].set_size(7100) self.dut['SEQ_TB'].write(7100) - + start = 0 exp = [8, 7, 6, 5, 4, 3, 2] - for i, th in enumerate([0,1,2,3,10,30,31]): - + for i, th in enumerate([0, 1, 2, 3, 10, 30, 31]): + self.dut['tlu_master'].THRESHOLD = th self.dut['SEQ_TB'].START - while(not self.dut['SEQ_TB'].is_ready): + while not self.dut['SEQ_TB'].is_ready: pass - - + ret = self.dut['FIFO_TB'].get_data() - - #for k,w in enumerate(ret): - # print k, hex(w) - - self.assertEqual(ret.size, exp[i]*2) - + + # for k,w in enumerate(ret): + # print(k, hex(w)) + + self.assertEqual(ret.size, exp[i] * 2) + tlu_word = ret >> 31 == 1 exp_tlu = np.arange(0x80000000 + start, 0x80000000 + start + exp[i], dtype=np.uint32) self.assertEqual(np.array_equal(ret[tlu_word], exp_tlu), True) - - exp_tdc = np.array([134]*exp[i], dtype=np.uint32) - #tdc referencee to previus + + exp_tdc = np.array([134] * exp[i], dtype=np.uint32) + # tdc referencee to previus if i != 0: exp_tdc[0] = 0xff ret_tdc = ret[~tlu_word] >> 20 - + self.assertFalse(np.any(np.abs(ret_tdc - exp_tdc) > 1)) - + self.assertEqual(self.dut['tlu_master'].TRIGGER_ID, start + exp[i]) - + start += exp[i] - - - def check_data(self, how_many, tdc_en = False, start = 0): - - for i in range(20): + + def check_data(self, how_many, tdc_en=False): + for _ in range(20): self.dut['SEQ_TB'].is_ready - + ret = self.dut['FIFO_TB'].get_data() - - #for k,w in enumerate(ret): - # print k, hex(w) - - self.assertEqual(ret.size, how_many * (tdc_en+1)) - + + # check number of received triggers + self.assertEqual(ret.size, how_many * (tdc_en + 1)) + tlu_word = ret >> 31 == 1 exp_tlu = np.arange(0x80000000, 0x80000000 + how_many, dtype=np.uint32) + + # check if received trigger words from FIFO are correct self.assertEqual(ret[tlu_word].tolist(), exp_tlu.tolist()) - - #distance is 0x71 + + # distance is 0x71 if tdc_en: - exp_tdc = np.array([135]*how_many, dtype=np.uint32) + exp_tdc = np.array([135] * how_many, dtype=np.uint32) ret_tdc = ret[~tlu_word] >> 20 - self.assertEqual((ret_tdc/2).tolist(), (exp_tdc/2).tolist()) - + self.assertEqual((ret_tdc // 2).tolist(), (exp_tdc // 2).tolist()) + self.assertEqual(self.dut['tlu_master'].TRIGGER_ID, how_many) self.assertEqual(self.dut['tlu_master'].TIMEOUT_COUNTER, 0) return ret @@ -230,10 +221,10 @@ def test_timeout(self): self.dut['tlu_master'].THRESHOLD = 0 self.dut['tlu_master'].EN_OUTPUT = 1 self.dut['tlu_master'].TIMEOUT = 5 - + self.dut['TDC_TB'].EN_TRIGGER_DIST = 1 self.dut['TDC_TB'].ENABLE = 1 - + how_many = 300 distance = 20 self.dut['test_pulser'].DELAY = distance - 1 @@ -241,19 +232,19 @@ def test_timeout(self): self.dut['test_pulser'].REPEAT = how_many self.dut['test_pulser'].START - while(not self.dut['test_pulser'].is_ready): + while not self.dut['test_pulser'].is_ready: pass ret = self.dut['FIFO_TB'].get_data() self.assertEqual(ret.size, how_many) - - ret_fifo = self.dut.get_fifo_data() + + ret_fifo = self.dut.get_fifo_data() self.assertEqual(ret.size, how_many) - self.assertEqual(range(how_many), ret_fifo['trigger_id'].tolist()) - self.assertEqual(range(ret_fifo['time_stamp'][0],int(ret_fifo['time_stamp'][0])+how_many*distance, distance), ret_fifo['time_stamp'].tolist()) - + self.assertEqual(list(range(how_many)), ret_fifo['trigger_id'].tolist()) + self.assertEqual(list(range(ret_fifo['time_stamp'][0], int(ret_fifo['time_stamp'][0]) + how_many * distance, distance)), ret_fifo['time_stamp'].tolist()) + self.assertEqual(self.dut['tlu_master'].TRIGGER_ID, how_many) - self.assertEqual(self.dut['tlu_master'].TIMEOUT_COUNTER, 255 if how_many > 255 else how_many) + self.assertEqual(self.dut['tlu_master'].TIMEOUT_COUNTER, 255 if how_many > 255 else how_many) def test_multi_input_distance(self): self.dut['TLU_TB'].TRIGGER_COUNTER = 0 @@ -325,19 +316,55 @@ def test_fifo_readout(self): self.dut['tlu_master'].THRESHOLD = 10 self.dut['tlu_master'].EN_OUTPUT = 0 self.dut['tlu_master'].TIMEOUT = 20 - + how_many = 100 - self.dut['test_pulser'].DELAY = 200 -5 + self.dut['test_pulser'].DELAY = 200 - 5 self.dut['test_pulser'].WIDTH = 5 self.dut['test_pulser'].REPEAT = how_many - + with self.dut.readout(): self.dut['test_pulser'].START - while(not self.dut['test_pulser'].is_ready): + while not self.dut['test_pulser'].is_ready: pass self.assertEqual(self.dut.fifo_readout.get_record_count(), how_many) - + + def test_tlu_veto(self): + self.dut['TLU_TB'].TRIGGER_COUNTER = 0 + self.dut['TLU_TB'].TRIGGER_MODE = 3 + self.dut['TLU_TB'].TRIGGER_SELECT = 0 + self.dut['TLU_TB'].TRIGGER_VETO_SELECT = 1 # veto on veto pulse + self.dut['TLU_TB'].EN_TLU_VETO = 1 # enable TLU veto + self.dut['TLU_TB'].TRIGGER_ENABLE = 1 + self.dut['TLU_TB'].TRIGGER_DATA_DELAY = 2 + self.dut['TLU_TB'].HANDSHAKE_BUSY_VETO_WAIT_CYCLES = 0 # be directly after de-asserting BUSY ready for incoming vetos + self.dut['TLU_TB'].TRIGGER_HANDSHAKE_ACCEPT_WAIT_CYCLES = 1 + + self.dut['tlu_master'].EN_INPUT = 1 + self.dut['tlu_master'].MAX_DISTANCE = 10 + self.dut['tlu_master'].THRESHOLD = 1 + self.dut['tlu_master'].EN_OUTPUT = 1 + + # generate veto signals + how_many_vetos = 80 + distance_veto = 80 + self.dut['VETO_PULSER_TB'].DELAY = distance_veto - 1 + self.dut['VETO_PULSER_TB'].WIDTH = 100 + self.dut['VETO_PULSER_TB'].REPEAT = how_many_vetos + self.dut['VETO_PULSER_TB'].START + + how_many_triggers = 50 + self.dut['test_pulser'].DELAY = 200 + self.dut['test_pulser'].WIDTH = 1 + self.dut['test_pulser'].REPEAT = how_many_triggers + self.dut['test_pulser'].START + + while not self.dut['test_pulser'].is_ready: + pass + + expected_vetoed_triggers = 29 # 29 triggers will not be accepted due to veto signal + self.check_data(how_many_triggers - expected_vetoed_triggers) + def tearDown(self): self.dut.close() time.sleep(5) diff --git a/tests/test_eudaq.py b/tests/test_eudaq.py new file mode 100644 index 0000000..7c9321e --- /dev/null +++ b/tests/test_eudaq.py @@ -0,0 +1,250 @@ +# +# ------------------------------------------------------------ +# Copyright (c) All rights reserved +# SiLab, Institute of Physics, University of Bonn +# ------------------------------------------------------------ +# + +''' +This test case needs a working eudaq 1.x-dev installation and the +eudaq/python folder added to the python path, see: +https://github.com/SiLab-Bonn/pytlu/blob/development/README.md + +The installation has to done at std. location (bin/lib in +eudaq folder) + +Otherwise the unit tests are all skipped. +''' + +import os +import time +import unittest +import threading +import subprocess +import sys +try: + from queue import Queue, Empty # Python3 +except ImportError: + from Queue import Queue, Empty # Python2 + +import tables as tb +import psutil +import numpy as np + +from pytlu import tlu_eudaq +import utils + +pytlu_path = os.path.dirname(tlu_eudaq.__file__) +data_folder = os.path.abspath(os.path.join(pytlu_path, '..', 'data')) +dir_path = os.path.dirname(os.path.realpath(__file__)) + + +def run_process_with_queue(command, arguments=None): + if os.name == 'nt': + creationflags = subprocess.CREATE_NEW_PROCESS_GROUP + else: + creationflags = 0 + args = [command] + if arguments: + args += [str(a) for a in arguments] + ON_POSIX = 'posix' in sys.builtin_module_names + p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + bufsize=1, close_fds=ON_POSIX, creationflags=creationflags) + print_queue = Queue() + t = threading.Thread(target=enqueue_output, args=(p.stdout, print_queue)) + t.daemon = True # thread dies with the program + t.start() + + return p, print_queue + + +def kill(proc): + ''' Kill process by id, including subprocesses. + + Works for Linux and Windows + ''' + process = psutil.Process(proc.pid) + for child_proc in process.children(recursive=True): + child_proc.kill() + process.kill() + + +def eurun_control(min_connections=2, run_time=5): + ''' Start a EUDAQ run control instance + + Waits for min_connections to send configure command. + Start the run for run_time and then send EOR. + ''' + from PyEUDAQWrapper import PyRunControl + prc = PyRunControl("44000") + while prc.NumConnections < min_connections: + time.sleep(1) + print("Number of active connections: ", prc.NumConnections) + # Load configuration file + prc.Configure(os.path.join(dir_path, "TestConfig.conf")) + # Wait that the connections can receive the config + time.sleep(2) + # Wait until all connections answer with 'OK' + while not prc.AllOk: + time.sleep(0.5) + print("Successfullly configured!") + # Start the run + prc.StartRun() + # Wait for run start + while not prc.AllOk: + time.sleep(0.5) + # Main run loop + time.sleep(run_time) + # Stop run + prc.StopRun() + time.sleep(2) + + +def enqueue_output(out, queue): + ''' https://stackoverflow.com/questions/375427/non-blocking-read-on-a-subprocess-pipe-in-python ''' + for line in iter(out.readline, b''): + queue.put(line) + out.close() + + +def queue_to_string(queue, max_lines=1000): + return_str = '' + for _ in range(max_lines): + try: + line = queue.get_nowait() + except Empty: + break + else: # got line + return_str += line.decode("utf-8") + '\n' + return return_str + + +class TestEudaq(unittest.TestCase): + + @classmethod + def setUpClass(cls): + try: + import PyEUDAQWrapper + eudaq_wrapper_path = os.path.dirname(PyEUDAQWrapper.__file__) + cls.eudaq_bin_folder = os.path.abspath(os.path.join(eudaq_wrapper_path, '..', 'bin')) + cls.no_eudaq_install = False + except ImportError: + cls.no_eudaq_install = True + + @classmethod + def tearDownClass(cls): + os.remove(os.path.join(data_folder, 'tlu_example_data_out.h5')) + os.remove(os.path.join(data_folder, 'tlu_example_data_snd.h5')) + + def test_replay_data(self): + ''' Test the communication with replayed data using the replay data functionality. + + Starts a complete EUDAQ data taking scenario: + Run control + TestDataConverter + pytlu producer + + Success of this test is checked by stdout inspection. + ''' + + if self.no_eudaq_install: + self.skipTest("No working EUDAQ installation found, skip test!") + t_rc = threading.Thread(target=eurun_control, kwargs={"min_connections": 2, + "run_time": 5}) + t_rc.start() + time.sleep(2) + # EUDAQ 1.7-dev --> EUDAQ 1.x-dev: Change of name from TestDataCollector --> DataCollector + # EUDAQ 1.x-dev Another change of name from DataCollector --> euDataCollector + data_coll_process, data_coll_q = run_process_with_queue(command=os.path.join(self.eudaq_bin_folder, 'euDataCollector.exe')) + time.sleep(2) + pytlu_prod_process, pytlu_prod_q = run_process_with_queue( + command='pytlu_eudaq', + arguments=['--replay', + '%s' % os.path.join(data_folder, 'tlu_example_data.h5'), + '--delay', '0.001']) + time.sleep(10) + t_rc.join() + time.sleep(5) + # Read up to 1000 lines from Data Collector output + coll_output = queue_to_string(data_coll_q) + prod_output = queue_to_string(pytlu_prod_q) + + # Subprocesses have to be killed since no terminate signal is available + # from euRun wrapped with python + kill(data_coll_process) + kill(pytlu_prod_process) + + # Check configurig of entities + self.assertTrue('Configuring' in coll_output, msg='Cannot find configuring step in log output!') + self.assertTrue('Configured' in coll_output, msg='Cannot find finished configuring message in log output!') + self.assertTrue('Received Configuration' in prod_output, msg='Cannot find received configuration message in log output!') + + # Check event building from replayed data + self.assertTrue('Complete Event' in coll_output) + + # Check stop run signal received + self.assertTrue('Received EORE Event' in coll_output) + self.assertTrue('Stop Run received' in prod_output) + + def test_send_data(self): + ''' Test the data sending function of the tlu_eudaq scan. + + The send raw data and stored raw data by the EudaqScan.handle_data() function is compared to the + fixture to make sure that all data is handled. + ''' + + # Counter variable for calls to data send function + self.n_calls = 0 + + def SendEvent(data, skipped_triggers=None, event_counter=None): + ''' Fake EUDAQ send function that is called per event. + ''' + raw_data_table_snd.append(np.array([data])) + self.n_calls += 1 + + raw_data_file = os.path.join(data_folder, 'tlu_example_data.h5') + raw_data_file_out = os.path.join(data_folder, 'tlu_example_data_out.h5') + raw_data_file_snd = os.path.join(data_folder, 'tlu_example_data_snd.h5') + + scan = tlu_eudaq.EudaqScan() + + h5_file_snd = tb.open_file(raw_data_file_snd, mode='w') + raw_data_table_snd = h5_file_snd.create_table(h5_file_snd.root, name='raw_data', + description=scan.data_dtype, title='data', + filters=tb.Filters(complib='blosc', complevel=5)) + + scan.set_callback(SendEvent) + + # Create storage structures and variables that are not created since we fake readout + scan.h5_file = tb.open_file(raw_data_file_out, mode='w') + scan.data_table = scan.h5_file.create_table(scan.h5_file.root, name='raw_data', + description=scan.data_dtype, title='data', + filters=tb.Filters(complib='blosc', complevel=5)) + scan.meta_data_table = scan.h5_file.create_table(scan.h5_file.root, name='meta_data', + description=scan.meta_data_dtype, title='meta_data', + filters=tb.Filters(complib='blosc', complevel=5)) + # Fake data taking using raw data file + with tb.open_file(raw_data_file) as in_file: + meta_data = in_file.root.meta_data[:] + raw_data = in_file.root.raw_data + for readout in meta_data: + index_start, index_stop = readout[0], readout[1] + readout_meta_data = [readout[name] for name in readout.dtype.names] + + data_tuple = tuple([raw_data[index_start:index_stop]] + readout_meta_data) + scan.handle_data(data_tuple) + + h5_file_snd.close() + scan.h5_file.close() + + # Check raw data is stored locally when using scan_eudaq + data_equal, error_msg = utils.compare_h5_files(raw_data_file, raw_data_file_out, node_names=['raw_data']) + self.assertTrue(data_equal, msg=error_msg) + + # Check send raw data is complete and correct when using scan_eudaq + data_equal, error_msg = utils.compare_h5_files(raw_data_file, raw_data_file_snd, node_names=['raw_data']) + self.assertTrue(data_equal, msg=error_msg) + + self.assertEqual(scan.event_counter, self.n_calls) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_pytlu_monitor.py b/tests/test_pytlu_monitor.py new file mode 100644 index 0000000..129515e --- /dev/null +++ b/tests/test_pytlu_monitor.py @@ -0,0 +1,166 @@ +''' Script to check the pytlu modules for the online monitor + + Simulation producer, converter and receiver. +''' + +import os +import sys +import unittest +import yaml +import subprocess +import time +import psutil +from PyQt5.QtWidgets import QApplication + +from online_monitor import OnlineMonitor + +import pytlu +pytlu_path = os.path.dirname(pytlu.__file__) +data_folder = os.path.abspath(os.path.join(pytlu_path, '..', 'data')) + + +# Create online monitor yaml config with pytlu monitor entities +def create_config_yaml(): + conf = {} + # Add producer + devices = {} + devices['TLU_Producer'] = {'backend': 'tcp://127.0.0.1:8600', + 'kind': 'pytlu_producer_sim', + 'delay': 0.1, + 'data_file': os.path.join(data_folder, 'tlu_example_data.h5') + } + conf['producer_sim'] = devices + # Add converter + devices = {} + devices['TLU_Converter'] = {'kind': 'pytlu_converter', + 'frontend': 'tcp://127.0.0.1:8600', + 'backend': 'tcp://127.0.0.1:8700' + } + conf['converter'] = devices + # Add receiver + devices = {} + devices['TLU'] = {'kind': 'pytlu_receiver', + 'frontend': 'tcp://127.0.0.1:8700' + } + conf['receiver'] = devices + return yaml.dump(conf, default_flow_style=False) + + +# kill process by id, including subprocesses; works for linux and windows +def kill(proc): + process = psutil.Process(proc.pid) + for child_proc in process.children(recursive=True): + child_proc.kill() + process.kill() + + +def get_python_processes(): # return the number of python processes + n_python = 0 + for proc in psutil.process_iter(): + try: + if 'python' in proc.name(): + n_python += 1 + except psutil.AccessDenied: + pass + return n_python + + +def run_script_in_shell(script, arguments, command=None): + if os.name == 'nt': + creationflags = subprocess.CREATE_NEW_PROCESS_GROUP + else: + creationflags = 0 + return subprocess.Popen( + "%s %s %s" % ('python' if not command else command, script, arguments), + shell=True, creationflags=creationflags) + + +class TestOnlineMonitor(unittest.TestCase): + + @classmethod + def setUpClass(cls): + with open('tmp_cfg.yml', 'w') as outfile: + config_file = create_config_yaml() + outfile.write(config_file) + # Linux CIs run usually headless, thus virtual x server is needed for + # gui testing + if os.getenv('CI', False): + from xvfbwrapper import Xvfb + cls.vdisplay = Xvfb() + cls.vdisplay.start() + # Start the simulation producer to create some fake data + cls.prod_sim_proc = run_script_in_shell('', 'tmp_cfg.yml', + 'start_producer_sim') + # Start converter + cls.conv_manager_proc = run_script_in_shell('', 'tmp_cfg.yml', + command='start_converter') + # Create Gui + time.sleep(2) + cls.app = QApplication(sys.argv) + cls.online_monitor = OnlineMonitor.OnlineMonitorApplication( + 'tmp_cfg.yml') + time.sleep(2) + + @classmethod + def tearDownClass(cls): # Remove created files + time.sleep(1) + kill(cls.prod_sim_proc) + kill(cls.conv_manager_proc) + time.sleep(1) + os.remove('tmp_cfg.yml') + cls.online_monitor.close() + time.sleep(1) + + def test_data_chain(self): + ''' Checks for received data for the 2 receivers + + This effectively checks the full chain: + producer --> converter --> receiver + ''' + + # Qt evsent loop does not run in tests, thus we have to trigger the + # event queue manually + self.app.processEvents() + # Check all receivers present + self.assertEqual(len(self.online_monitor.receivers), 1, + 'Number of receivers wrong') + self.app.processEvents() # Clear event queue + + # Case 1: Activate status widget, data should be received since do not care about active tab + self.online_monitor.tab_widget.setCurrentIndex(0) + self.app.processEvents() + time.sleep(1) + self.app.processEvents() + time.sleep(0.2) + # Data structure to check for no data since receiver widget + # is not active + data_recv_0 = [] + self.app.processEvents() + for receiver in self.online_monitor.receivers: + data_recv_0.append(receiver.trigger_rate_acc_curve.getData()) + + # Case 2: Activate TLU widget, receiver should show data + self.online_monitor.tab_widget.setCurrentIndex(1) + self.app.processEvents() + time.sleep(1) + self.app.processEvents() + time.sleep(0.2) + # Data structure to check for data since receiver widget + # is active + data_recv_1 = [] + for receiver in self.online_monitor.receivers: + data_recv_1.append(receiver.trigger_rate_acc_curve.getData()) + + self.assertTrue(len(data_recv_0[0][0]) != 0) # check for emptyness of data list + self.assertTrue(len(data_recv_1[0][0]) != 0) # check for emptyness of data list + + # Test the UI + def test_ui(self): + # 1 receiver + status widget expected + self.assertEqual(self.online_monitor.tab_widget.count(), 2, + 'Number of tab widgets wrong') + + +if __name__ == '__main__': + suite = unittest.TestLoader().loadTestsFromTestCase(TestOnlineMonitor) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..354e4c3 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,273 @@ +import yaml +import numpy as np +import tables as tb + +from basil.utils.sim.utils import cocotb_compile_and_run, cocotb_compile_clean + + +def nan_equal(first_array, second_array): + ''' Compares two arrays and test for equality. + + Works with array and recarrays. + NaNs are considered equal. + + Parameters + ---------- + first_array : numpy.ndarray + second_array : numpy.ndarray + + Returns + ------- + boolean + ''' + # Check for shape, prevent broadcast + if first_array.shape != second_array.shape: + return False + # Check if both are recarrays + if (first_array.dtype.names is None and second_array.dtype.names is not None) or (first_array.dtype.names is not None and second_array.dtype.names is None): + return False + if first_array.dtype.names is None: # Not a recarray + # Check for same dtypes + if first_array.dtype != second_array.dtype: + return False + # Check for equality + try: + np.testing.assert_equal(first_array, second_array) + except AssertionError: + return False + else: + # Check for same column names and same order + if first_array.dtype.names != second_array.dtype.names: + return False + for column in first_array.dtype.names: + # Check for same dtypes + if first_array[column].dtype != second_array[column].dtype: + return False + # Check for equality + try: + np.testing.assert_equal(first_array[column], second_array[column]) + except AssertionError: + return False + return True + + +def nan_close(first_array, second_array, rtol=1e-5, atol=1e-8, equal_nan=True): + ''' Compares two arrays and test for similarity. + + Works with recarrays. + + Parameters + ---------- + first_array : numpy.ndarray + second_array : numpy.ndarray + rtol : float + atol : float + equal_nan : boolean + If True, NaNs are considered equal. + + Returns + ------- + boolean + ''' + # Check for shape, prevent broadcast + if first_array.shape != second_array.shape: + return False + # Check if both are recarrays + if (first_array.dtype.names is None and second_array.dtype.names is not None) or (first_array.dtype.names is not None and second_array.dtype.names is None): + return False + if first_array.dtype.names is None: # Not a recarray + # Check for same dtypes + if first_array.dtype != second_array.dtype: + return False + return np.allclose(a=first_array, b=second_array, rtol=rtol, atol=atol, equal_nan=equal_nan) + else: + # Check for same column names and same order + if first_array.dtype.names != second_array.dtype.names: + return False + for column in first_array.dtype.names: + # Check for same dtypes + if first_array[column].dtype != second_array[column].dtype: + return False + # Workaround for string data types + if first_array[column].dtype.type is np.string_: + return np.all(first_array[column] == second_array[column]) + # Check for similarity + if not np.allclose(a=first_array[column], b=second_array[column], rtol=rtol, atol=atol, equal_nan=equal_nan): + return False + return True + + +def get_array_differences(first_array, second_array, exact=True, rtol=1e-5, atol=1e-8, equal_nan=True): + '''Takes two numpy.ndarrays and compares them on a column basis. + Different column data types, missing columns and columns with different values are returned in a string. + + Parameters + ---------- + first_array : numpy.ndarray + second_array : numpy.ndarray + + Returns + ------- + string + ''' + def compare_arrays(actual, desired, exact, rtol, atol, equal_nan): + compare_str = '' + if actual.dtype != desired.dtype: + compare_str += ' Type:\n first: %s\n second: %s\n' % (str(actual.dtype), str(desired.dtype)) + if actual.shape != desired.shape: + compare_str += ' Shape:\n first: %s\n second: %s\n' % (str(actual.shape), str(desired.shape)) + try: # Try reshaping, is possible when changed dimension has only one setting + actual = actual.reshape(desired.shape) + except ValueError: + pass + if np.nansum(actual) != np.nansum(desired): + compare_str += ' Sum:\n first: %s\n second: %s\n' % (str(np.nansum(actual)), str(np.nansum(desired))) + if exact: + try: + np.testing.assert_equal(actual=actual, desired=desired) + except AssertionError as e: + compare_str += str(e) + "\n" + else: + try: + np.testing.assert_allclose(actual=actual, desired=desired, rtol=rtol, atol=atol, equal_nan=equal_nan) + except AssertionError as e: + compare_str += str(e) + "\n" + if compare_str: + compare_str = ("Difference (%s):\n" % ("exact" if exact else "close")) + compare_str + else: + compare_str = "No Difference (%s)\n" % ("exact" if exact else "close") + return compare_str + + # Check if both are recarrays + if (first_array.dtype.names is None and second_array.dtype.names is not None) or (first_array.dtype.names is not None and second_array.dtype.names is None): + return "Type mismatch: np.array and np.recarray" + if first_array.dtype.names is None: # Not a recarray + return compare_arrays(actual=first_array, desired=second_array, exact=exact, rtol=rtol, atol=atol, equal_nan=equal_nan) + else: + return_str = '' + first_array_column_names = first_array.dtype.names + second_array_column_names = second_array.dtype.names + additional_first_array_column_names = set(first_array_column_names) - set(second_array_column_names) + additional_second_array_column_names = set(second_array_column_names) - set(first_array_column_names) + if additional_first_array_column_names: + return_str += 'First array has additional columns: %s\n' % ', '.join(additional_first_array_column_names) + if additional_second_array_column_names: + return_str += 'Second array has additional columns: %s\n' % ', '.join(additional_second_array_column_names) + if not additional_first_array_column_names and not additional_second_array_column_names and first_array_column_names != second_array_column_names: + return_str += 'Columns have different order:\nfirst: %s\nsecond: %s\n' % (first_array_column_names, second_array_column_names) + common_columns = set(first_array_column_names) & set(second_array_column_names) + for column_name in common_columns: # loop over all nodes and compare each node, do not abort if one node is wrong + first_column_data = first_array[column_name] + second_column_data = second_array[column_name] + col_compare_str = compare_arrays(actual=first_column_data, desired=second_column_data, exact=exact, rtol=rtol, atol=atol, equal_nan=equal_nan) + return_str += "Column %s:\n%s" % (column_name, col_compare_str) + return return_str + + +def compare_h5_files(first_file, second_file, node_names=None, ignore_nodes=None, detailed_comparison=True, exact=True, rtol=1e-5, atol=1e-8, chunk_size=1000000): + '''Takes two hdf5 files and check for equality of all nodes. + Returns true if the node data is equal and the number of nodes is the number of expected nodes. + It also returns an error string containing the names of the nodes that are not equal. + + Parameters + ---------- + first_file : string + Path to the first file. + second_file : string + Path to the second file. + node_names : list, tuple + Iterable of node names that are required to exist and will be compared. + If None, compare all existing nodes and fail if nodes are not existing. + This is a white list of nodes to check. + ignore_nodes : list, tuple + Iterable of node names that are not required to exist and will not be compared. + If None, no existing nodes is excluded. This is a black list of nodes to check. + detailed_comparison : boolean + Print reason why the comparison failed + exact : boolean + True if the results have to match exactly. E.g. False for fit results. + rtol, atol: number + From numpy.allclose: + rtol : float + The relative tolerance parameter (see Notes). + atol : float + The absolute tolerance parameter (see Notes). + + Returns + ------- + (bool, string) + ''' + + checks_passed = True + error_msg = "" + with tb.open_file(first_file, 'r') as first_h5_file: + with tb.open_file(second_file, 'r') as second_h5_file: + + def walk_nodes(f, n, g="/"): + for item in f.get_node(f.root, g): + if isinstance(item, tb.group.Group): + walk_nodes(f=f, n=n, g=item._v_pathname) + else: + n.append(item._v_pathname) + + fist_file_nodes = [] + walk_nodes(f=first_h5_file, n=fist_file_nodes) # get node names + second_file_nodes = [] + walk_nodes(f=second_h5_file, n=second_file_nodes) # get node names + + if ignore_nodes: + fist_file_nodes = [node for node in fist_file_nodes if node not in ignore_nodes] + second_file_nodes = [node for node in second_file_nodes if node not in ignore_nodes] + + if node_names is None: + additional_first_file_nodes = set(fist_file_nodes) - set(second_file_nodes) + additional_second_file_nodes = set(second_file_nodes) - set(fist_file_nodes) + if additional_first_file_nodes: + checks_passed = False + if detailed_comparison: + error_msg += 'File %s has additional nodes: %s\n' % (first_file, ', '.join(additional_first_file_nodes)) + if additional_second_file_nodes: + checks_passed = False + if detailed_comparison: + error_msg += 'File %s has additional nodes: %s\n' % (second_file, ', '.join(additional_second_file_nodes)) + common_nodes = set(fist_file_nodes) & set(second_file_nodes) + else: + node_names = [(("/" + name) if (name and name[:1] != "/") else name) for name in node_names] + missing_first_file_nodes = set(node_names) - set(fist_file_nodes) + if missing_first_file_nodes: + checks_passed = False + if detailed_comparison: + error_msg += 'File %s is missing nodes: %s\n' % (first_file, ', '.join(missing_first_file_nodes)) + missing_second_file_nodes = set(node_names) - set(second_file_nodes) + if missing_second_file_nodes: + checks_passed = False + if detailed_comparison: + error_msg += 'File %s is missing nodes: %s\n' % (second_file, ', '.join(missing_second_file_nodes)) + common_nodes = (set(fist_file_nodes) & set(second_file_nodes)) & set(node_names) + for node_name in common_nodes: # loop over all nodes and compare each node, do not abort if one node is wrong + nrows = first_h5_file.get_node(first_h5_file.root, node_name).nrows + index_start = 0 + while index_start < nrows: + # reduce memory footprint by taken array dimension into account + read_nrows = max(1, int(chunk_size / np.prod(first_h5_file.get_node(first_h5_file.root, node_name).shape[1:]))) + index_stop = index_start + read_nrows + first_file_data = first_h5_file.get_node(first_h5_file.root, node_name).read(index_start, index_stop) + second_file_data = second_h5_file.get_node(second_h5_file.root, node_name).read(index_start, index_stop) + if exact: + if not nan_equal(first_array=first_file_data, second_array=second_file_data): + checks_passed = False + if detailed_comparison: + error_msg += ('Node %s:\n' % node_name) + get_array_differences(first_array=first_file_data, second_array=second_file_data, exact=True) + break + else: + if not nan_close(first_array=first_file_data, second_array=second_file_data, rtol=rtol, atol=atol, equal_nan=True): + checks_passed = False + if detailed_comparison: + error_msg += ('Node %s:\n' % node_name) + get_array_differences(first_array=first_file_data, second_array=second_file_data, exact=False, rtol=rtol, atol=atol, equal_nan=True) + break + index_start += read_nrows + if checks_passed: + error_msg = 'Comparing files %s and %s: OK\n%s' % (first_file, second_file, error_msg) + else: + error_msg = 'Comparing files %s and %s: FAILED\n%s' % (first_file, second_file, error_msg) + return checks_passed, error_msg