├── README ├── test ├── core_tb.py ├── Makefile └── hardware_bringup │ ├── axiom_photonsdi_hw.patch │ └── axiom_photonsdi_hw_test.py ├── photonsdi ├── __init__.py ├── boards │ ├── __init__.py │ └── platforms │ │ ├── __init__.py │ │ └── axiom_photonsdi_hw.py ├── phy │ └── __init__.py ├── util │ ├── __init__.py │ ├── encdec.py │ ├── anc.py │ ├── timing.py │ ├── line_number.py │ ├── lfsr.py │ ├── xyz.py │ ├── crc.py │ └── hamming.py ├── frontend │ └── __init__.py ├── software │ └── __init__.py ├── input │ ├── stream_demuxer.py │ ├── __init__.py │ ├── frame_extractor.py │ ├── data_aligner.py │ └── descrambler.py ├── output │ ├── stream_muxer.py │ ├── frame_generator.py │ ├── __init__.py │ ├── scrambler.py │ └── timing_generator.py └── constants.py ├── doc └── BMBF_gefoerdert_2017_en.jpg ├── setup.py ├── LICENSE ├── .gitignore └── README.md /README: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /test/core_tb.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /photonsdi/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /photonsdi/boards/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /photonsdi/phy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /photonsdi/util/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /photonsdi/frontend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /photonsdi/software/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /photonsdi/boards/platforms/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/BMBF_gefoerdert_2017_en.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixheld/photonSDI/HEAD/doc/BMBF_gefoerdert_2017_en.jpg -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | PYTHON = python3 2 | 3 | CMD = $(PYTHON) 4 | 5 | core_tb: 6 | $(CMD) core_tb.py 7 | 8 | all: core_tb 9 | -------------------------------------------------------------------------------- /photonsdi/input/stream_demuxer.py: -------------------------------------------------------------------------------- 1 | from migen import * 2 | from photonsdi.constants import * 3 | 4 | 5 | class SdiStreamDemuxer(Module): 6 | def __init__(self): 7 | # TODO 8 | return 9 | -------------------------------------------------------------------------------- /photonsdi/output/stream_muxer.py: -------------------------------------------------------------------------------- 1 | from migen import * 2 | from photonsdi.constants import * 3 | 4 | 5 | class SdiStreamMuxer(Module): 6 | def __init__(self): 7 | # TODO 8 | return 9 | -------------------------------------------------------------------------------- /photonsdi/output/frame_generator.py: -------------------------------------------------------------------------------- 1 | from migen import * 2 | from photonsdi.constants import * 3 | 4 | 5 | class SdiFrameGenerator(Module): 6 | def __init__(self): 7 | # TODO 8 | return 9 | -------------------------------------------------------------------------------- /photonsdi/input/__init__.py: -------------------------------------------------------------------------------- 1 | from migen import * 2 | from photonsdi.constants import * 3 | from photonsdi.input.descrambler import * 4 | from photonsdi.input.data_aligner import * 5 | from photonsdi.input.stream_demuxer import * 6 | from photonsdi.input.frame_extractor import * 7 | 8 | 9 | class SdiIn(Module): 10 | def __init__(self): 11 | # TODO 12 | return 13 | -------------------------------------------------------------------------------- /photonsdi/output/__init__.py: -------------------------------------------------------------------------------- 1 | from migen import * 2 | from photonsdi.constants import * 3 | from photonsdi.output.timing_generator import * 4 | from photonsdi.output.frame_generator import * 5 | from photonsdi.output.stream_muxer import * 6 | from photonsdi.output.scrambler import * 7 | 8 | 9 | class SdiOut(Module): 10 | def __init__(self): 11 | # TODO 12 | return 13 | -------------------------------------------------------------------------------- /photonsdi/input/frame_extractor.py: -------------------------------------------------------------------------------- 1 | from migen import * 2 | from photonsdi.constants import * 3 | 4 | 5 | class FrameExtractor(Module): 6 | def __init__(self, elementary_stream_count=2): 7 | assert elementary_stream_count in [2] 8 | datapath_width = elementary_stream_count * SDI_ELEMENTARY_STREAM_DATA_WIDTH 9 | 10 | self.i_data = Signal(datapath_width) 11 | 12 | # TODO 13 | -------------------------------------------------------------------------------- /photonsdi/input/data_aligner.py: -------------------------------------------------------------------------------- 1 | from migen import * 2 | from photonsdi.constants import * 3 | 4 | 5 | class SdiDataAligner(Module): 6 | def __init__(self, elementary_stream_count=2): 7 | assert elementary_stream_count in [2] 8 | datapath_width = elementary_stream_count * SDI_ELEMENTARY_STREAM_DATA_WIDTH 9 | 10 | self.i_data = Signal(datapath_width) 11 | self.o_data = Signal(datapath_width) 12 | 13 | # TODO 14 | -------------------------------------------------------------------------------- /photonsdi/constants.py: -------------------------------------------------------------------------------- 1 | # common constants and defines for the SDI core 2 | 3 | SDI_ELEMENTARY_STREAM_DATA_WIDTH = 10 4 | # the 10 bit SDI data words are transmitted LSB first 5 | 6 | SDI_LINE_LENGTH_WIDTH = 12 # 1080p resolution at maximum for now 7 | SDI_LINE_NUMBER_WIDTH = 11 8 | 9 | SDI_FIRST_PIXEL_NUMBER = 0 # pixel 0 is in the middle of a line(!) 10 | SDI_FIRST_LINE_NUMBER = 1 11 | 12 | SDI_BLANKING_YRGB_10BIT = 0x040 13 | SDI_BLANKING_CrCb_10BIT = 0x200 14 | 15 | SDI_CRC_TAPS = [0, 4, 5, 18] 16 | SDI_CRC_LENGTH = max(SDI_CRC_TAPS) 17 | 18 | SDI_SCRAMBLER_TAPS = [0, 4, 9] 19 | SDI_SCRAMBLER_LENGTH = max(SDI_SCRAMBLER_TAPS) 20 | 21 | SDI_NRZI_TAPS = [0, 1] 22 | SDI_NRZI_LENGTH = max(SDI_NRZI_TAPS) 23 | -------------------------------------------------------------------------------- /photonsdi/util/encdec.py: -------------------------------------------------------------------------------- 1 | from migen import * 2 | from photonsdi.constants import * 3 | 4 | 5 | # when 9 bit values are encoded for the SDI stream, the inverted 9th bit is 6 | # used as 10th bit so that the 10 bit value can't be outside the allowed data 7 | # value range 8 | 9 | 10 | class Sdi9BitEncoder(Module): 11 | def __init__(self): 12 | self.i_data = Signal(9) 13 | self.o_data = Signal(SDI_ELEMENTARY_STREAM_DATA_WIDTH) 14 | 15 | ### 16 | 17 | self.comb += [ 18 | self.o_data.eq(Cat(self.i_data, ~self.i_data[8])) 19 | ] 20 | 21 | 22 | class Sdi9BitDecoder(Module): 23 | def __init__(self): 24 | self.i_data = Signal(SDI_ELEMENTARY_STREAM_DATA_WIDTH) 25 | self.o_data = Signal(9) 26 | self.o_valid = Signal() 27 | 28 | ### 29 | 30 | self.comb += [ 31 | self.o_data.eq(self.i_data[:9]), 32 | If(self.i_data[8] != self.i_data[9], 33 | self.o_valid.eq(1) 34 | ).Else( 35 | self.o_valid.eq(0) 36 | ) 37 | ] 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from setuptools import setup 5 | from setuptools import find_packages 6 | 7 | 8 | if sys.version_info[:3] < (3, 3): 9 | raise SystemExit("You need Python 3.3+") 10 | 11 | 12 | setup( 13 | name="photonSDI", 14 | version="0.1", 15 | description="open source implementation of the serial digital interface (SDI) video standard", 16 | # long_description=open("README").read(), 17 | author="Felix Held", 18 | author_email="photonsdi@felixheld.de", 19 | url="http://www.felixheld.de/projects/photonsdi", 20 | download_url="https://github.com/felixheld/photonsdi", 21 | license="BSD", 22 | platforms=["Any"], 23 | keywords="HDL ASIC FPGA hardware design", 24 | classifiers=[ 25 | "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)", 26 | "Environment :: Console", 27 | "Development Status :: Alpha", 28 | "Intended Audience :: Developers", 29 | "License :: OSI Approved :: BSD License", 30 | "Operating System :: OS Independent", 31 | "Programming Language :: Python", 32 | ], 33 | packages=find_packages(), 34 | include_package_data=True, 35 | ) 36 | -------------------------------------------------------------------------------- /photonsdi/input/descrambler.py: -------------------------------------------------------------------------------- 1 | from migen import * 2 | from photonsdi.constants import * 3 | from photonsdi.util.lfsr import * 4 | 5 | 6 | class SdiDescrambler(Module): 7 | def __init__(self, elementary_stream_count=2): 8 | assert elementary_stream_count in [2] 9 | datapath_width = elementary_stream_count * SDI_ELEMENTARY_STREAM_DATA_WIDTH 10 | 11 | self.i_data = Signal(datapath_width) 12 | self.o_data = Signal(datapath_width) 13 | 14 | ### 15 | 16 | nrzi_output = Signal(datapath_width) 17 | 18 | self.submodules.nrzi = LfsrDescrambler(SDI_NRZI_TAPS, datapath_width) 19 | 20 | self.comb += [ 21 | self.nrzi.i_data.eq(self.i_data) 22 | ] 23 | self.sync += [ 24 | nrzi_output.eq(self.nrzi.o_data), 25 | self.nrzi.i_last_state.eq(self.nrzi.o_state) 26 | ] 27 | 28 | self.submodules.scrambler = LfsrDescrambler(SDI_SCRAMBLER_TAPS, datapath_width) 29 | 30 | self.comb += [ 31 | self.scrambler.i_data.eq(nrzi_output) 32 | ] 33 | self.sync += [ 34 | self.o_data.eq(self.scrambler.o_data), 35 | self.scrambler.i_last_state.eq(self.scrambler.o_state) 36 | ] 37 | -------------------------------------------------------------------------------- /photonsdi/output/scrambler.py: -------------------------------------------------------------------------------- 1 | from migen import * 2 | from photonsdi.constants import * 3 | from photonsdi.util.lfsr import * 4 | 5 | 6 | class SdiScrambler(Module): 7 | def __init__(self, elementary_stream_count=2): 8 | assert elementary_stream_count in [2] 9 | datapath_width = elementary_stream_count * SDI_ELEMENTARY_STREAM_DATA_WIDTH 10 | 11 | self.i_data = Signal(datapath_width) 12 | self.o_data = Signal(datapath_width) 13 | 14 | ### 15 | 16 | scrambler_out = Signal(datapath_width) 17 | 18 | self.submodules.scrambler = LfsrScrambler(SDI_SCRAMBLER_TAPS, datapath_width) 19 | 20 | self.comb += [ 21 | self.scrambler.i_data.eq(self.i_data) 22 | ] 23 | self.sync += [ 24 | scrambler_out.eq(self.scrambler.o_data), 25 | self.scrambler.i_last_state.eq(self.scrambler.o_state) 26 | ] 27 | 28 | self.submodules.nrzi = LfsrScrambler(SDI_NRZI_TAPS, datapath_width) 29 | 30 | self.comb += [ 31 | self.nrzi.i_data.eq(scrambler_out) 32 | ] 33 | self.sync += [ 34 | self.o_data.eq(self.nrzi.o_data), 35 | self.nrzi.i_last_state.eq(self.nrzi.o_state) 36 | ] 37 | -------------------------------------------------------------------------------- /test/hardware_bringup/axiom_photonsdi_hw.patch: -------------------------------------------------------------------------------- 1 | diff --git a/photonsdi/boards/platforms/axiom_photonsdi_hw.py b/photonsdi/boards/platforms/axiom_photonsdi_hw.py 2 | index 97624b5..a728089 100644 3 | --- a/photonsdi/boards/platforms/axiom_photonsdi_hw.py 4 | +++ b/photonsdi/boards/platforms/axiom_photonsdi_hw.py 5 | @@ -28,11 +28,12 @@ _io = [ 6 | ("serial", 0, 7 | Subsignal("tx", Pins("AA19")), 8 | Subsignal("rx", Pins("AB20")), 9 | - IOStandard("LVCMOS33") 10 | + IOStandard("LVCMOS33"), 11 | + Misc("PULLUP=TRUE") 12 | ), 13 | 14 | - ("aux", 0, Pins("AB18"), IOStandard("LVCMOS33")), 15 | - ("aux", 1, Pins("AA18"), IOStandard("LVCMOS33")), 16 | + ("aux", 0, Pins("AB18"), IOStandard("LVCMOS33"), Misc("PULLUP=TRUE")), 17 | + ("aux", 1, Pins("AA18"), IOStandard("LVCMOS33"), Misc("PULLUP=TRUE")), 18 | 19 | # main data connection to the axiom beta 20 | ("axiom_data", 0, 21 | @@ -60,8 +61,8 @@ _io = [ 22 | Subsignal("s4n", Pins("W4")), # swapped 23 | Subsignal("s5p", Pins("AB7")), # swapped 24 | Subsignal("s5n", Pins("AB6")), # swapped 25 | - IOStandard("LVDS_25"), 26 | - Misc("DIFF_TERM=TRUE"), 27 | + IOStandard("LVCMOS25"), 28 | + Misc("PULLUP=TRUE") 29 | ), 30 | 31 | ("sync_in", 0, 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2017 Felix Held 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation and/or 11 | other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 20 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /photonsdi/util/anc.py: -------------------------------------------------------------------------------- 1 | from operator import xor 2 | 3 | from migen import * 4 | from photonsdi.constants import * 5 | from photonsdi.util.encdec import * 6 | 7 | 8 | class Anc8BitEncoder(Module): 9 | def __init__(self): 10 | self.i_data = Signal(8) 11 | self.o_data = Signal(SDI_ELEMENTARY_STREAM_DATA_WIDTH) 12 | 13 | ### 14 | 15 | parity = reduce(xor, [self.i_data[i] for i in range(8)]) 16 | 17 | self.submodules.enc9 = Sdi9BitEncoder() 18 | 19 | self.comb += [ 20 | self.enc9.i_data.eq(Cat(self.i_data, parity)), 21 | self.o_data.eq(self.enc9.o_data) 22 | ] 23 | 24 | 25 | class Anc8BitDecoder(Module): 26 | def __init__(self): 27 | self.i_data = Signal(SDI_ELEMENTARY_STREAM_DATA_WIDTH) 28 | self.o_data = Signal(8) 29 | self.o_valid = Signal() 30 | 31 | ### 32 | 33 | self.submodules.dec9 = Sdi9BitDecoder() 34 | 35 | parity = reduce(xor, [self.dec9.o_data[i] for i in range(8)]) 36 | 37 | self.comb += [ 38 | self.dec9.i_data.eq(self.i_data), 39 | self.o_data.eq(self.dec9.o_data[:8]), 40 | If(self.dec9.o_valid and self.self.dec9.o_data[8] == parity, 41 | self.o_valid.eq(1) 42 | ).Else( 43 | self.o_valid.eq(0) 44 | ) 45 | ] 46 | -------------------------------------------------------------------------------- /photonsdi/util/timing.py: -------------------------------------------------------------------------------- 1 | from photonsdi.constants import * 2 | 3 | 4 | # horizontal timing: this is a bit confusing, but it's the SDI standard 5 | # * a line begins right after the last word of the active video data 6 | # with an EAV packet containing the number of the new line 7 | # * after the blanking area the SAV packet follows which ends right before the 8 | # wrap-around of the pixel counter 9 | # * at the wrap-around of the pixel counter to 0 the active video line starts 10 | 11 | SDI_H_TIMING_PARAMETERS = [ 12 | ("h_total_line_length", SDI_LINE_LENGTH_WIDTH), # 0-indexed 13 | ("h_last_active_pixel", SDI_LINE_LENGTH_WIDTH) 14 | ] 15 | 16 | SDI_V_TIMING_PARAMETERS = [ 17 | ("v_total_line_number", SDI_LINE_NUMBER_WIDTH), # 1-indexed 18 | ("v_begin_active_video", SDI_LINE_NUMBER_WIDTH), 19 | ("v_end_active_video", SDI_LINE_NUMBER_WIDTH) 20 | ] # currently only support for progressive image format, not interlaced or progressive segmented frames 21 | 22 | SDI_TIMING_PARAMETERS = SDI_H_TIMING_PARAMETERS + SDI_V_TIMING_PARAMETERS 23 | 24 | SDI_XYZ_FLAGS = [ 25 | ("h", 1), 26 | ("v", 1), 27 | ("f", 1) 28 | ] 29 | 30 | SDI_TIMING = [ 31 | ("video_line", SDI_LINE_NUMBER_WIDTH), 32 | ("video_pixel_in_line", SDI_LINE_LENGTH_WIDTH), # only valid when h_active is 1 33 | ("begin_sav", 1), 34 | ("begin_eav", 1), 35 | # beware: both h and v active signals have different polarity of h and v signals in xyz word 36 | ("h_active", 1), # beware: 0 during both sav/eav words 37 | ("v_active", 1), 38 | ("field", 1) 39 | ] 40 | -------------------------------------------------------------------------------- /photonsdi/util/line_number.py: -------------------------------------------------------------------------------- 1 | from migen import * 2 | from photonsdi.constants import * 3 | from photonsdi.util.encdec import * 4 | 5 | 6 | class SdiLineNumberEncoder(Module): 7 | def __init__(self): 8 | self.i_line_number = Signal(SDI_LINE_NUMBER_WIDTH) 9 | self.o_data = Signal(2 * SDI_ELEMENTARY_STREAM_DATA_WIDTH) # first data word in LSBs 10 | 11 | ### 12 | 13 | self.submodules.enc9_0 = Sdi9BitEncoder() 14 | self.submodules.enc9_1 = Sdi9BitEncoder() 15 | 16 | self.comb += [ 17 | self.enc9_0.i_data.eq(Cat(Replicate(0, 2), self.i_line_number[:7])), 18 | self.enc9_0.i_data.eq(Cat(Replicate(0, 2), self.i_line_number[7:], Replicate(0, 3))), 19 | self.o_data.eq(Cat(self.enc9_0.o_data, self.enc9_1.o_data)) 20 | ] 21 | 22 | 23 | class SdiLineNumberDecoder(Module): 24 | def __init__(self): 25 | self.i_data = Signal(2 * SDI_ELEMENTARY_STREAM_DATA_WIDTH) # first data word in LSBs 26 | self.o_line_number = Signal(SDI_LINE_NUMBER_WIDTH) 27 | self.o_valid = Signal() 28 | 29 | ### 30 | 31 | self.submodules.dec9_0 = Sdi9BitDecoder() 32 | self.submodules.dec9_1 = Sdi9BitDecoder() 33 | 34 | self.comb += [ 35 | self.dec9_0.i_data.eq(self.i_data[:SDI_ELEMENTARY_STREAM_DATA_WIDTH]), 36 | self.dec9_1.i_data.eq(self.i_data[SDI_ELEMENTARY_STREAM_DATA_WIDTH:]), 37 | self.o_line_number.eq(Cat(self.dec9_0.o_data[2:9], self.dec9_1.o_data[2:6])), 38 | self.o_valid.eq(self.dec9_0.o_valid and self.dec9_1.o_valid) 39 | ] 40 | -------------------------------------------------------------------------------- /photonsdi/util/lfsr.py: -------------------------------------------------------------------------------- 1 | from operator import xor 2 | 3 | from migen import * 4 | 5 | 6 | class LfsrScrambler(Module): 7 | def __init__(self, lfsr_taps, datapath_width): 8 | assert lfsr_taps 9 | lfsr_length = max(lfsr_taps) 10 | 11 | self.i_data = Signal(datapath_width) 12 | self.o_data = Signal(datapath_width) 13 | self.i_last_state = Signal(lfsr_length) 14 | self.o_state = Signal(lfsr_length) 15 | 16 | ### 17 | 18 | feedback_taps = lfsr_taps[:] 19 | feedback_taps.remove(max(feedback_taps)) 20 | 21 | state = [self.i_last_state[i] for i in range(lfsr_length)] 22 | 23 | for i in range(datapath_width): 24 | state.append(reduce(xor, [state[tap] for tap in feedback_taps] + [self.i_data[i]])) 25 | self.comb += [ 26 | self.o_data[i].eq(state.pop(0)) 27 | ] 28 | 29 | self.comb += [ 30 | self.o_state.eq(Cat(*state[:lfsr_length])) 31 | ] 32 | 33 | 34 | class LfsrDescrambler(Module): 35 | def __init__(self, lfsr_taps, datapath_width): 36 | assert lfsr_taps 37 | lfsr_length = max(lfsr_taps) 38 | 39 | self.i_data = Signal(datapath_width) 40 | self.o_data = Signal(datapath_width) 41 | self.i_last_state = Signal(lfsr_length) 42 | self.o_state = Signal(lfsr_length) 43 | 44 | ### 45 | 46 | curval = Cat(self.i_last_state, self.i_data) 47 | 48 | for i in range(datapath_width): 49 | self.comb += [ 50 | self.o_data[i].eq(reduce(xor, [curval[tap + i] for tap in lfsr_taps])) 51 | ] 52 | 53 | self.comb += [ 54 | self.o_state.eq(self.i_data[-lfsr_length:]) 55 | ] 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /photonsdi/util/xyz.py: -------------------------------------------------------------------------------- 1 | from migen import * 2 | from photonsdi.constants import * 3 | from photonsdi.util.timing import * 4 | from photonsdi.util.hamming import * 5 | 6 | 7 | # (7, 3) code: 8 | _generator_matrix = [ 9 | [1, 0, 0, 1, 0, 1, 1], 10 | [0, 1, 0, 1, 1, 0, 1], 11 | [0, 0, 1, 1, 1, 1, 0] 12 | ] 13 | 14 | 15 | class SdiXyzEncoder(Module): 16 | def __init__(self): 17 | self.i_timing_flags = Record(SDI_XYZ_FLAGS) 18 | self.o_data = Signal(SDI_ELEMENTARY_STREAM_DATA_WIDTH) 19 | 20 | ### 21 | 22 | self.submodules.hamming_calc = HammingEncoder(_generator_matrix) 23 | 24 | self.comb += [ 25 | self.hamming_calc.i_data.eq(Cat(self.i_timing_flags.h, 26 | self.i_timing_flags.v, 27 | self.i_timing_flags.f)), 28 | self.o_data.eq(Cat(Replicate(0, 2), 29 | self.hamming_calc.o_data[len(_generator_matrix):], 30 | self.hamming_calc.o_data[:len(_generator_matrix)], 31 | 1)) 32 | ] 33 | 34 | 35 | class SdiXyzDecoder(Module): 36 | def __init__(self): 37 | self.i_data = Signal(SDI_ELEMENTARY_STREAM_DATA_WIDTH) 38 | self.o_timing_flags = Record(SDI_XYZ_FLAGS) 39 | self.o_corrected = Signal() 40 | 41 | ### 42 | 43 | self.submodules.hamming_dec = HammingDecoder(_generator_matrix) 44 | 45 | self.comb += [ 46 | self.hamming_dec.i_data.eq(Cat(self.i_data[6:9], self.i_data[2:6])), 47 | 48 | self.o_timing_flags.h.eq(self.hamming_dec.o_data[0]), 49 | self.o_timing_flags.v.eq(self.hamming_dec.o_data[1]), 50 | self.o_timing_flags.f.eq(self.hamming_dec.o_data[2]), 51 | 52 | self.o_corrected.eq(self.hamming_dec.o_corrected) 53 | ] 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # photonSDI - an open source SDI core 2 | 3 | TODO: Put logo here. 4 | Copyright 2017-2018 / Felix Held 5 | 6 | An open source SDI core 7 | powered by Migen and LiteX 8 | 9 | ![Funded by the German Ministry of Education and Science (BMBF)](doc/BMBF_gefoerdert_2017_en.jpg) 10 | 11 | ## Intro 12 | 13 | photonSDI provides an open source SDI core. 14 | 15 | Since Python is used to describe the HDL, the core is highly and easily 16 | configurable. 17 | 18 | photonSDI is built using LiteX and uses technologies developed in partnership with 19 | M-Labs Ltd: 20 | - Migen enables generating HDL with Python in an efficient way. 21 | - MiSoC provides the basic blocks to build a powerful and small footprint SoC. 22 | 23 | photonSDI can be used as LiteX library or can be integrated with your standard 24 | design flow by generating the verilog rtl that you will use as a standard core. 25 | 26 | Since people already asked: The project is still very much alive. 27 | 28 | ## Targets 29 | 30 | - https://github.com/felixheld/AXIOM-photonSDI-hw 31 | 32 | ## Features 33 | 34 | - None 35 | 36 | ## Possible improvements 37 | 38 | - Lots 39 | 40 | ## Getting started 41 | 42 | 1. Install Python3 and your vendor's software 43 | 44 | 2. Obtain LiteX and install it: 45 | 46 | git clone https://github.com/enjoy-digital/litex --recursive 47 | cd litex 48 | python3 setup.py install 49 | cd .. 50 | 51 | XXX 52 | 53 | #### Simulations 54 | 55 | XXX 56 | 57 | #### Tests 58 | 59 | XXX 60 | 61 | ## License 62 | 63 | photonSDI is released under the very permissive two-clause BSD license. Under 64 | the terms of this license, you are authorized to use photonSDI for closed-source 65 | proprietary designs. 66 | 67 | Even though we do not require you to do so, those things are awesome, so please 68 | do them if possible: 69 | 70 | * tell us that you are using photonSDI 71 | * cite photonSDI in publications related to research it has helped 72 | * send us feedback and suggestions for improvements 73 | * send us bug reports when something goes wrong 74 | * send us the modifications and improvements you have done to photonSDI. 75 | 76 | ## Support and consulting 77 | 78 | * [Mailing list](https://groups.google.com/forum/#!forum/photonsdi/join) 79 | 80 | We love open-source hardware and like sharing our designs with others. 81 | 82 | photonSDI is developed and maintained by Felix Held for the 83 | [TimVideos](https://code.timvideos.us) & [Apertus](https://apertus.org/) 84 | project. 85 | 86 | If you would like to know more about photonSDI or if you are already a happy 87 | user and would like to extend it for your needs, Felix Held can provide standard 88 | commercial support as well as consulting services. 89 | 90 | So feel free to contact us, we'd love to work with you! (and eventually shorten 91 | the list of the possible improvements :) 92 | 93 | ## Contact 94 | 95 | * E-mail: Felix Held 96 | -------------------------------------------------------------------------------- /photonsdi/util/crc.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from operator import xor 3 | 4 | from migen import * 5 | from photonsdi.constants import * 6 | 7 | 8 | class SdiCrcEngine(Module): 9 | """Cyclic Redundancy Check Engine 10 | 11 | Compute next CRC value from last CRC value and data input using 12 | an optimized asynchronous LFSR. 13 | 14 | Parameters 15 | ---------- 16 | data_width : int 17 | Width of the data bus. 18 | taps : list 19 | list of all LFSR taps, including the first and last 20 | this implicitly specifies the number of bits of the internal state 21 | 22 | Attributes 23 | ---------- 24 | data : in 25 | Data input. 26 | last_crc : in 27 | last CRC value. 28 | crc_out : 29 | next CRC value. 30 | """ 31 | def __init__(self, data_width, taps): 32 | state_width = max(taps) 33 | 34 | self.data = Signal(data_width) 35 | self.last_crc = Signal(state_width) 36 | self.crc_out = Signal(state_width) 37 | 38 | ### 39 | 40 | def _optimize_eq(l): 41 | """ 42 | remove an even numbers of XORs with the same bit 43 | replace an odd number of XORs with a single XOR 44 | """ 45 | d = OrderedDict() 46 | for e in l: 47 | if e in d: 48 | d[e] += 1 49 | else: 50 | d[e] = 1 51 | r = [] 52 | for key, value in d.items(): 53 | if value%2 != 0: 54 | r.append(key) 55 | return r 56 | 57 | # compute and optimize the parallel implementation of the CRC's LFSR 58 | # note: SDI counts the CRC state bits in reverse order compared to IEEE 802.3 CRC 59 | # ITU R-REC-BT.2077-0 says: "CRCC0 is the MSB of the error detection code." 60 | curval = [[("state", i)] for i in range(state_width)] 61 | curval.reverse() 62 | for i in range(data_width): 63 | feedback = curval.pop() + [("din", i)] 64 | for j in range(state_width - 1): 65 | if j + 1 in taps: 66 | curval[j] += feedback 67 | curval[j] = _optimize_eq(curval[j]) 68 | curval.insert(0, feedback) 69 | curval.reverse() 70 | 71 | # implement logic 72 | for i in range(state_width): 73 | xor_list = [] 74 | for t, n in curval[i]: 75 | if t == "state": 76 | xor_list += [self.last_crc[n]] 77 | elif t == "din": 78 | xor_list += [self.data[n]] 79 | self.comb += self.crc_out[i].eq(reduce(xor, xor_list)) 80 | 81 | 82 | class SdiChannelCrc(Module): 83 | def __init__(self): 84 | self.i_data = Signal(SDI_ELEMENTARY_STREAM_DATA_WIDTH) 85 | self.i_clear = Signal() # set next crc value to 0 86 | self.i_enable = Signal() # use data at the clock cycle to calculate new crc 87 | self.o_crc = Signal(SDI_CRC_LENGTH) # output latency: 1 clock cycle 88 | 89 | ### 90 | 91 | self.submodules.crc_engine = SdiCrcEngine(SDI_ELEMENTARY_STREAM_DATA_WIDTH, SDI_CRC_TAPS) 92 | 93 | next_last_crc = Signal(SDI_CRC_LENGTH) 94 | 95 | self.sync += [ 96 | If(self.i_clear, 97 | next_last_crc.eq(0) 98 | ).Else( 99 | If(self.i_enable, 100 | next_last_crc.eq(self.crc_engine.crc_out) 101 | ).Else( 102 | next_last_crc.eq(self.crc_engine.last_crc) 103 | ) 104 | ) 105 | ] 106 | 107 | self.comb += [ 108 | self.crc_engine.data.eq(self.i_data), 109 | self.crc_engine.last_crc.eq(next_last_crc), 110 | self.o_crc.eq(next_last_crc) 111 | ] 112 | -------------------------------------------------------------------------------- /photonsdi/output/timing_generator.py: -------------------------------------------------------------------------------- 1 | # parts of this code are inspired from the litevideo output core 2 | 3 | from migen import * 4 | from litex.soc.interconnect import stream 5 | from photonsdi.constants import * 6 | from photonsdi.util.timing import * 7 | 8 | 9 | class SdiTimingGenerator(Module): 10 | def __init__(self, genlock_stream=None): 11 | # TODO: probably some stream handshaking signal generation missing 12 | self.sink = sink = stream.Endpoint(SDI_TIMING_PARAMETERS) # module input 13 | self.source = source = stream.Endpoint(SDI_TIMING) # module output 14 | 15 | ### 16 | 17 | if genlock_stream is None: 18 | # the pipeline steps are counted in reverse, so we can use this numbers for the array ranges 19 | PIPELINE_H_GEN_STEP = 3 20 | PIPELINE_V_GEN_STEP = 2 21 | PIPELINE_TIMING_STEP = 1 22 | PIPELINE_FINAL_STEP = 0 23 | 24 | h_counter = Array(Signal(SDI_LINE_LENGTH_WIDTH) for _ in range(PIPELINE_H_GEN_STEP + 1)) 25 | v_counter = Array(Signal(SDI_LINE_NUMBER_WIDTH) for _ in range(PIPELINE_V_GEN_STEP + 1)) 26 | 27 | h_active = Signal() 28 | v_active = Signal() 29 | field = Signal() 30 | begin_sav = Signal() 31 | begin_eav = Signal() 32 | 33 | h_begin_sav_pos = Signal() 34 | h_begin_eav_pos = Signal() 35 | 36 | # SAV is the last 4 words before the active video that starts at 0 37 | self.comb += h_begin_sav_pos.eq(sink.h_total_line_length - 3) 38 | 39 | # EAV begins right after the end of the active video area 40 | self.comb += h_begin_eav_pos.eq(sink.h_last_active_pixel + 1) 41 | 42 | self.comb += field.eq(0) # only support progressive image format 43 | 44 | self.comb += [ 45 | source.h_active.eq(h_active), 46 | source.v_active.eq(v_active), 47 | source.field.eq(field), 48 | source.video_line.eq(v_counter[PIPELINE_FINAL_STEP]), 49 | source.video_pixel_in_line.eq(h_counter[PIPELINE_FINAL_STEP]), 50 | source.begin_sav.eq(begin_sav), 51 | source.begin_eav.eq(begin_eav) 52 | ] 53 | 54 | self.sync += [ 55 | If(~sink.valid, 56 | h_counter[PIPELINE_H_GEN_STEP].eq(SDI_FIRST_PIXEL_NUMBER), 57 | v_counter[PIPELINE_V_GEN_STEP].eq(SDI_FIRST_LINE_NUMBER), 58 | h_active.eq(0), 59 | v_active.eq(0), 60 | begin_sav.eq(0), 61 | begin_eav.eq(0) 62 | 63 | ).Elif(source.ready, 64 | # horizontal timing generation pipeline stage 65 | If(h_counter[PIPELINE_H_GEN_STEP] == sink.h_total_line_length, 66 | h_counter[PIPELINE_H_GEN_STEP].eq(SDI_FIRST_PIXEL_NUMBER), 67 | ).Else( 68 | h_counter[PIPELINE_H_GEN_STEP].eq(h_counter[PIPELINE_H_GEN_STEP] + 1) 69 | ), 70 | 71 | # vertical timing generation pipeline stage 72 | If(h_counter[PIPELINE_H_GEN_STEP] == h_begin_eav_pos, 73 | If(v_counter[PIPELINE_V_GEN_STEP] == sink.v_total_line_number, 74 | v_counter[PIPELINE_V_GEN_STEP].eq(SDI_FIRST_LINE_NUMBER) 75 | ).Else( 76 | v_counter[PIPELINE_V_GEN_STEP].eq(v_counter[PIPELINE_V_GEN_STEP] + 1) 77 | ) 78 | ), 79 | 80 | # timing generation pipeline stage 81 | If(h_counter[PIPELINE_TIMING_STEP] == SDI_FIRST_PIXEL_NUMBER, 82 | h_active.eq(1) 83 | ).Elif(h_counter[PIPELINE_TIMING_STEP] == sink.h_last_active_pixel, 84 | h_active.eq(0) 85 | ), 86 | 87 | begin_sav.eq(h_counter[PIPELINE_TIMING_STEP] == h_begin_sav_pos), 88 | begin_eav.eq(h_counter[PIPELINE_TIMING_STEP] == h_begin_eav_pos), 89 | 90 | If(v_counter[PIPELINE_TIMING_STEP] == sink.v_begin_active_video, 91 | v_active.eq(1) 92 | ).Elif(v_counter[PIPELINE_TIMING_STEP] == sink.v_end_active_video, 93 | v_active.eq(0) 94 | ) 95 | ) 96 | ] 97 | 98 | for i in range(len(h_counter) - 1): 99 | self.sync += h_counter[i].eq(h_counter[i + 1]) 100 | 101 | for i in range(len(v_counter) - 1): 102 | self.sync += v_counter[i].eq(v_counter[i + 1]) 103 | 104 | else: 105 | self.comb += genlock_stream.connect(source) 106 | -------------------------------------------------------------------------------- /photonsdi/util/hamming.py: -------------------------------------------------------------------------------- 1 | from operator import xor 2 | from migen import * 3 | 4 | 5 | # hamming code, also known as (n, k) code 6 | 7 | class HammingHelpers(object): 8 | @staticmethod 9 | def _calc_symbol_xor_bits(data, generator_matrix, n, k): 10 | symbol_xors = [[] for _ in range(n)] 11 | for i in range(n): 12 | symbol_xors[i] = [data[j] for j in range(k) if generator_matrix[j][i] != 0] 13 | return symbol_xors 14 | 15 | def _calc_symbol(self, data, generator_matrix, n, k): 16 | symbol = [[] for _ in range(n)] 17 | symbol_xors = self._calc_symbol_xor_bits(data, generator_matrix, n, k) 18 | for i in range(n): 19 | symbol[i] = reduce(xor, list(symbol_xors[i])) 20 | return symbol 21 | 22 | @staticmethod 23 | def _calc_parity_check_matrix(generator_matrix, n, k): 24 | # currently only supports non-permutated generator matrix 25 | # create matrix and fill unity matrix part 26 | parity_check_matrix = [[1 if i == (j - k) else 0 for j in range(n)] for i in range(n - k)] 27 | # fill rest of matrix 28 | for i in range(n - k): 29 | for j in range(k): 30 | parity_check_matrix[i][j] = generator_matrix[j][i + k] 31 | return parity_check_matrix 32 | 33 | @staticmethod 34 | def _calc_syndrome_xor_bits(data, parity_check_matrix, n, k): 35 | syndrome_xors = [[] for _ in range(n - k)] 36 | for i in range(n - k): 37 | syndrome_xors[i] = [data[j] for j in range(n) if parity_check_matrix[i][j] != 0] 38 | return syndrome_xors 39 | 40 | def _calc_syndrome(self, data, parity_check_matrix, n, k): 41 | syndrome = [[] for _ in range(n - k)] 42 | syndrome_xors = self._calc_syndrome_xor_bits(data, parity_check_matrix, n, k) 43 | for i in range(n - k): 44 | syndrome[i] = reduce(xor, list(syndrome_xors[i])) 45 | return syndrome 46 | 47 | def _generate_syndrome_mapping_lut(self, generator_matrix, parity_check_matrix, n, k): 48 | # currently only supports non-permutated generator matrix 49 | bitflip_syndrome = [[] for _ in range(k)] # bit position is index, syndrome is data 50 | sample_data = [0 for _ in range(k)] 51 | sample_symbol = self._calc_symbol(sample_data, generator_matrix, n, k) 52 | for i in range(k): 53 | # faulty symbol with exactly one data bit flipped 54 | faulty_symbol = [sample_symbol[j] ^ 1 if j == i else 0 for j in range(n)] 55 | bitflip_syndrome[i] = self._calc_syndrome(faulty_symbol, parity_check_matrix, n, k) 56 | return bitflip_syndrome 57 | 58 | 59 | class HammingEncoder(Module, HammingHelpers): 60 | # assumptions: even parity, non-permutated matrix (left part of generator matrix is unity matrix) 61 | def __init__(self, generator_matrix): 62 | assert(len(generator_matrix) > 0) 63 | n = len(generator_matrix[0]) # total bits (data + parity bits) 64 | k = len(generator_matrix) # data bits 65 | assert(n > k) 66 | 67 | self.i_data = Signal(k) 68 | self.o_data = Signal(n) 69 | 70 | ### 71 | 72 | symbol_bit_xors = self._calc_symbol_xor_bits(self.i_data, generator_matrix, n, k) 73 | for i in range(n): 74 | self.comb += self.o_data[i].eq(reduce(xor, symbol_bit_xors[i])) 75 | 76 | 77 | class HammingDecoder(Module, HammingHelpers): 78 | # assumptions: even parity, non-permutated matrix (left part of generator matrix is unity matrix) 79 | 80 | def __init__(self, generator_matrix): 81 | assert(len(generator_matrix) > 0) 82 | n = len(generator_matrix[0]) # total bits (data + parity bits) 83 | k = len(generator_matrix) # data bits 84 | assert(n > k) 85 | 86 | self.i_data = Signal(n) 87 | self.o_data = Signal(k) 88 | self.o_corrected = Signal() 89 | 90 | ### 91 | 92 | parity_check_matrix = self._calc_parity_check_matrix(generator_matrix, n, k) 93 | 94 | bitflip_syndrome_lut = self._generate_syndrome_mapping_lut(generator_matrix, parity_check_matrix, n, k) 95 | 96 | data_syndrome_xors = self._calc_syndrome_xor_bits(self.i_data, parity_check_matrix, n, k) 97 | data_syndrome = Signal(n - k) 98 | 99 | for i in range(n - k): 100 | self.comb += data_syndrome[i].eq(reduce(xor, list(data_syndrome_xors[i]))) 101 | 102 | for i in range(k): 103 | bitflip_syndrome_i = Signal(n - k) 104 | for j in range(n - k): 105 | self.comb += bitflip_syndrome_i[j].eq(bitflip_syndrome_lut[i][j]) 106 | 107 | self.comb += [ 108 | If(bitflip_syndrome_i == data_syndrome, 109 | self.o_data[i].eq(~self.i_data[i]), 110 | ).Else( 111 | self.o_data[i].eq(self.i_data[i]), 112 | ) 113 | ] 114 | 115 | self.comb += self.o_corrected.eq(data_syndrome != 0) 116 | -------------------------------------------------------------------------------- /test/hardware_bringup/axiom_photonsdi_hw_test.py: -------------------------------------------------------------------------------- 1 | # part of this code is borrowed from https://lab.whitequark.org/notes/2016-10-18/implementing-an-uart-in-verilog-and-migen/ 2 | 3 | # for this test to work on the board, you need to apply axiom_photonsdi_hw.patch 4 | 5 | from migen import * 6 | from photonsdi.boards.platforms import axiom_photonsdi_hw 7 | 8 | 9 | class BaudrateGenerator(Module): 10 | def __init__(self, clkin, baudrate): 11 | self.baud_strobe = Signal() 12 | 13 | def _uart_divisor(sysclk, baudrate): 14 | return int((sysclk + baudrate / 2) // baudrate) 15 | 16 | divisor = _uart_divisor(clkin, baudrate) 17 | baud_cntr = Signal(max = divisor) 18 | self.sync += [ 19 | If(0 == baud_cntr, 20 | baud_cntr.eq(divisor - 1), 21 | self.baud_strobe.eq(1) 22 | ).Else( 23 | baud_cntr.eq(baud_cntr - 1), 24 | self.baud_strobe.eq(0) 25 | ) 26 | ] 27 | 28 | 29 | class UARTTXConst(Module): 30 | def __init__(self, data, i_tx_baud_strobe, tx_start, o_tx): 31 | 32 | tx_number_of_bits = 8 33 | tx_shiftreg = Signal(tx_number_of_bits) 34 | tx_bitnumber = Signal(max=tx_number_of_bits) 35 | 36 | self.submodules.tx_fsm = FSM(reset_state="IDLE") 37 | self.tx_fsm.act("IDLE", 38 | NextValue(o_tx, 1), 39 | If(i_tx_baud_strobe, 40 | If(tx_start, 41 | NextValue(tx_shiftreg, data), 42 | NextState("START") 43 | ) 44 | ) 45 | ) 46 | self.tx_fsm.act("START", 47 | If(i_tx_baud_strobe, 48 | NextState("DATA"), 49 | NextValue(o_tx, 0), 50 | NextValue(tx_bitnumber, 0) 51 | ) 52 | ) 53 | self.tx_fsm.act("DATA", 54 | If(i_tx_baud_strobe, 55 | NextValue(o_tx, tx_shiftreg[0]), 56 | NextValue(tx_shiftreg, Cat(tx_shiftreg[1:8], 0)), # would it be better to replace the 0 with a tx_shiftreg[0]? 57 | NextValue(tx_bitnumber, tx_bitnumber + 1), 58 | If(tx_bitnumber == tx_number_of_bits - 1, 59 | NextState("STOP") 60 | ) 61 | ) 62 | ) 63 | self.tx_fsm.act("STOP", 64 | If(i_tx_baud_strobe, 65 | NextState("IDLE"), 66 | NextValue(o_tx, 1) 67 | ) 68 | ) 69 | 70 | 71 | class MultiUARTTXConst(Module): 72 | def __init__(self, sys_clk_freq, baud_rate, channel_count, offset): 73 | self.tx = Signal(channel_count) 74 | 75 | self.submodules.baudrate_generator = BaudrateGenerator(sys_clk_freq, baud_rate) 76 | 77 | tx_data = Signal(channel_count) 78 | tx_oe = Signal(channel_count) 79 | tx_start = Signal() 80 | 81 | bit_times_between_tx_begins = 16 82 | bit_time_counter = Signal(max=bit_times_between_tx_begins) 83 | 84 | active_output_number = Signal(max=channel_count) 85 | 86 | self.sync += [ 87 | tx_oe.eq(1 << active_output_number), 88 | If(self.baudrate_generator.baud_strobe, 89 | If(0 == bit_time_counter, 90 | bit_time_counter.eq(bit_times_between_tx_begins - 1), 91 | tx_start.eq(1), 92 | If(0 == active_output_number, 93 | active_output_number.eq(channel_count - 1) 94 | ).Else( 95 | active_output_number.eq(active_output_number - 1) 96 | ) 97 | ).Else( 98 | bit_time_counter.eq(bit_time_counter - 1), 99 | tx_start.eq(0) 100 | ) 101 | ) 102 | ] 103 | 104 | for i in range(channel_count): 105 | self.submodules += UARTTXConst(offset + i, self.baudrate_generator.baud_strobe, tx_start, tx_data[i]) 106 | t = TSTriple() 107 | self.comb += [ 108 | t.o.eq(tx_data[i]), 109 | t.oe.eq(tx_oe[i]) 110 | ] 111 | self.specials += t.get_tristate(self.tx[i]) 112 | 113 | 114 | class Iotest(Module): 115 | def __init__(self, platform): 116 | clk_freq = int((1 / (platform.default_clk_period)) * 1000000000) 117 | 118 | serial = platform.request("serial") 119 | aux1 = platform.request("aux") 120 | aux2 = platform.request("aux") 121 | axiom_data = platform.request("axiom_data") 122 | 123 | _list_of_pins = [ 124 | serial.tx, 125 | serial.rx, 126 | aux1, 127 | aux2, 128 | axiom_data.n0p, 129 | axiom_data.n0n, 130 | axiom_data.n1p, 131 | axiom_data.n1n, 132 | axiom_data.n2p, 133 | axiom_data.n2n, 134 | axiom_data.n3p, 135 | axiom_data.n3n, 136 | axiom_data.n4p, 137 | axiom_data.n4n, 138 | axiom_data.n5p, 139 | axiom_data.n5n, 140 | axiom_data.s0p, 141 | axiom_data.s0n, 142 | axiom_data.s1p, 143 | axiom_data.s1n, 144 | axiom_data.s2p, 145 | axiom_data.s2n, 146 | axiom_data.s3p, # swapped 147 | axiom_data.s3n, # swapped 148 | axiom_data.s4p, # swapped 149 | axiom_data.s4n, # swapped 150 | axiom_data.s5p, # swapped 151 | axiom_data.s5n, # swapped 152 | ] 153 | 154 | output_count = len(_list_of_pins) 155 | 156 | self.submodules.test_signal_generator = MultiUARTTXConst(clk_freq, 115200, output_count, 0x20) 157 | 158 | for i in range(output_count): 159 | self.comb += _list_of_pins[i].eq(self.test_signal_generator.tx[i]) 160 | 161 | 162 | def main(): 163 | platform = axiom_photonsdi_hw.Platform() 164 | dut = Iotest(platform) 165 | platform.build(dut) 166 | 167 | 168 | if __name__ == "__main__": 169 | main() 170 | -------------------------------------------------------------------------------- /photonsdi/boards/platforms/axiom_photonsdi_hw.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from litex.build.generic_platform import * 4 | from litex.build.xilinx import XilinxPlatform 5 | 6 | _io = [ 7 | ("clk25", 0, Pins("C18"), IOStandard("LVCMOS18")), 8 | 9 | ("user_led", 0, Pins("E21"), IOStandard("LVCMOS18")), 10 | ("user_led", 1, Pins("E22"), IOStandard("LVCMOS18")), 11 | ("user_led", 2, Pins("G21"), IOStandard("LVCMOS18")), 12 | ("user_led", 3, Pins("G22"), IOStandard("LVCMOS18")), 13 | 14 | ("spiflash_4x", 0, # clock needs to be accessed through STARTUPE2 15 | Subsignal("cs_n", Pins("T19")), 16 | Subsignal("dq", Pins("P22", "R22", "P21", "R21")), 17 | IOStandard("LVCMOS33") 18 | ), 19 | ("spiflash_1x", 0, # clock needs to be accessed through STARTUPE2 20 | Subsignal("cs_n", Pins("T19")), 21 | Subsignal("mosi", Pins("P22")), 22 | Subsignal("miso", Pins("R22")), 23 | Subsignal("wp", Pins("P21")), 24 | Subsignal("hold", Pins("R21")), 25 | IOStandard("LVCMOS33") 26 | ), 27 | 28 | ("serial", 0, 29 | Subsignal("tx", Pins("AA19")), 30 | Subsignal("rx", Pins("AB20")), 31 | IOStandard("LVCMOS33") 32 | ), 33 | 34 | ("aux", 0, Pins("AB18"), IOStandard("LVCMOS33")), 35 | ("aux", 1, Pins("AA18"), IOStandard("LVCMOS33")), 36 | 37 | # main data connection to the axiom beta 38 | ("axiom_data", 0, 39 | Subsignal("n0p", Pins("N18")), 40 | Subsignal("n0n", Pins("N19")), 41 | Subsignal("n1p", Pins("N22")), 42 | Subsignal("n1n", Pins("M22")), 43 | Subsignal("n2p", Pins("N20")), 44 | Subsignal("n2n", Pins("M20")), 45 | Subsignal("n3p", Pins("K18")), 46 | Subsignal("n3n", Pins("K19")), 47 | Subsignal("n4p", Pins("J19")), 48 | Subsignal("n4n", Pins("H19")), 49 | Subsignal("n5p", Pins("J22")), 50 | Subsignal("n5n", Pins("H22")), 51 | Subsignal("s0p", Pins("T1")), 52 | Subsignal("s0n", Pins("U1")), 53 | Subsignal("s1p", Pins("W1")), 54 | Subsignal("s1n", Pins("Y1")), 55 | Subsignal("s2p", Pins("AA1")), 56 | Subsignal("s2n", Pins("AB1")), 57 | Subsignal("s3p", Pins("AB3")), # swapped 58 | Subsignal("s3n", Pins("AB2")), # swapped 59 | Subsignal("s4p", Pins("V4")), # swapped 60 | Subsignal("s4n", Pins("W4")), # swapped 61 | Subsignal("s5p", Pins("AB7")), # swapped 62 | Subsignal("s5n", Pins("AB6")), # swapped 63 | IOStandard("LVDS_25"), 64 | Misc("DIFF_TERM=TRUE"), 65 | ), 66 | 67 | ("sync_in", 0, 68 | Subsignal("hsync", Pins("F3")), 69 | Subsignal("vsync", Pins("G1")), 70 | Subsignal("odd_even", Pins("F1")), 71 | Subsignal("vformat", Pins("G2")), 72 | IOStandard("LVCMOS18") 73 | ), 74 | 75 | # GTP reference clock from si534x 76 | ("sdi_refclk", 0, 77 | Subsignal("p", Pins("F6")), # swapped 78 | Subsignal("n", Pins("E6")), # swapped 79 | ), 80 | # clock feedback path from FPGA to si534x 81 | ("sdi_refclk_fbout", 0, 82 | Subsignal("p", Pins("R1")), 83 | Subsignal("n", Pins("P1")), 84 | IOStandard("LVCMOS18") 85 | ), 86 | ("si534x_spi", 0, 87 | Subsignal("clk", Pins("M1")), 88 | Subsignal("cs_n", Pins("L1")), 89 | Subsignal("mosi", Pins("N2")), 90 | Subsignal("miso", Pins("M2")), 91 | IOStandard("LVCMOS18") 92 | ), 93 | ("si534x_ctl", 0, 94 | Subsignal("rst_n", Pins("M3")), 95 | Subsignal("oe_n", Pins("P4")), 96 | Subsignal("lol_n", Pins("L3")), 97 | Subsignal("irq_n", Pins("K3")), 98 | IOStandard("LVCMOS18") 99 | ), 100 | 101 | ("sdi_out", 0, 102 | Subsignal("p", Pins("B4")), 103 | Subsignal("n", Pins("A4")), 104 | ), 105 | ("sdi_out_spi", 0, 106 | Subsignal("clk", Pins("B1")), 107 | Subsignal("cs_n", Pins("E1")), 108 | Subsignal("mosi", Pins("E2")), 109 | Subsignal("miso", Pins("D1")), 110 | IOStandard("LVCMOS18") 111 | ), 112 | ("sdi_out_gpio", 0, 113 | Subsignal("gpio0", Pins("B2")), 114 | Subsignal("gpio1", Pins("A1")), 115 | IOStandard("LVCMOS18") 116 | ), 117 | 118 | ("sdi_out", 1, 119 | Subsignal("p", Pins("B6")), 120 | Subsignal("n", Pins("A6")), 121 | ), 122 | ("sdi_out_spi", 1, 123 | Subsignal("clk", Pins("C20")), 124 | Subsignal("cs_n", Pins("A18")), 125 | Subsignal("mosi", Pins("B18")), 126 | Subsignal("miso", Pins("A19")), 127 | IOStandard("LVCMOS18") 128 | ), 129 | ("sdi_out_gpio", 1, 130 | Subsignal("gpio0", Pins("D20")), 131 | Subsignal("gpio1", Pins("D21")), 132 | IOStandard("LVCMOS18") 133 | ), 134 | 135 | ("sdi_in", 0, 136 | Subsignal("p", Pins("B8")), 137 | Subsignal("n", Pins("A8")), 138 | ), 139 | ("sdi_in_spi", 0, 140 | Subsignal("clk", Pins("B21")), 141 | Subsignal("cs_n", Pins("A20")), 142 | Subsignal("mosi", Pins("B20")), 143 | Subsignal("miso", Pins("A21")), 144 | IOStandard("LVCMOS18") 145 | ), 146 | ("sdi_in_gpio", 0, 147 | Subsignal("gpio0", Pins("C22")), 148 | Subsignal("gpio1", Pins("D22")), 149 | IOStandard("LVCMOS18") 150 | ), 151 | ] 152 | 153 | 154 | class Platform(XilinxPlatform): 155 | default_clk_name = "clk25" 156 | default_clk_period = 40.0 157 | 158 | def __init__(self): 159 | XilinxPlatform.__init__(self, "xc7a35t-fgg484-2", _io, toolchain="vivado") 160 | self.add_platform_command(""" 161 | set_property CFGBVS VCCO [current_design] 162 | set_property CONFIG_VOLTAGE 3.3 [current_design] 163 | """) 164 | self.toolchain.bitstream_commands = \ 165 | ["set_property BITSTREAM.CONFIG.SPI_BUSWIDTH 4 [current_design]"] 166 | self.toolchain.additional_commands = \ 167 | ["write_cfgmem -force -format bin -interface spix4 -size 16 " 168 | "-loadbit \"up 0x0 top.bit\" -file top.bin"] 169 | --------------------------------------------------------------------------------