46 Commits

Author SHA1 Message Date
bdcaca2c8c revert test point changes 2025-10-10 16:53:38 +08:00
4c9dcad22d ice40: fix flag name 2025-10-08 11:37:22 +08:00
0b6548a199 xc2c: support readback at EEM[2] 2025-09-23 11:35:28 +08:00
a52e82ebf3 implement suservo synchronization 2025-09-22 11:41:03 +08:00
572388d2e8 flake: add ice40 build 2025-08-21 09:42:19 +08:00
6fde32a18c flake.nix: add, builds legacy design 2025-08-21 09:31:20 +08:00
10b47f34af readme: use updated link to kasli-i2c 2025-08-06 10:20:02 +08:00
fc355398c0 revert bash removal 2025-07-30 11:46:52 +08:00
ee0d605d81 remove duplicate platform 2025-07-30 11:45:04 +08:00
6bcaa8f5bc update instructions 2025-07-30 11:43:41 +08:00
54d287388d specify servo as SU-Servo 2025-07-30 10:34:37 +08:00
430eaa8e36 apply some flake8 suggestions on formatting 2025-07-30 10:27:37 +08:00
5c16c9408e name suservo builds as suservo 2025-07-30 10:19:12 +08:00
9846f4e234 use argparse with an optional susservo argument 2025-07-30 10:18:27 +08:00
11369be803 restructure files
Recategorize all files into either xc2c or ice40 directories.
2025-07-30 09:52:26 +08:00
8e7a9b726a update ice40 build instructions 2025-07-09 10:01:39 +08:00
e732c3d555 remove err signal 2025-07-09 10:01:39 +08:00
a6da8916cd update urukul sync docs
... so SYNC_IN would be an input to Urukul, SYNC_OUT would be an output from Urukul.
There are no changes to routing nor logic.
2025-07-09 10:01:39 +08:00
683c389f86 fix io_update_ret resource comment 2025-07-09 10:01:39 +08:00
eedfe2cc5e ice40: update docs 2025-07-09 10:01:39 +08:00
3cd675607f remove timing report 2025-07-09 10:01:39 +08:00
3b396c49cb remove all mention of iostandard and pullup
I/O standard is not settable. It is fixed by VCCIO*.
Pull up should be configured by PULLUP parameter of SB_IO.

You can observe specifying PULLUP in I/O does nothing according to the generated bitstream. It is already set by default.
See this: https://prjicestorm.readthedocs.io/en/latest/_static/bitdocs-8k/tile_33_2.html
2025-07-09 10:01:39 +08:00
074700c02a add used lvds output N pins 2025-07-09 10:01:39 +08:00
2b9b8daf8c logically detach suservo and standalone in gateware
So we get to keep the same pin arrangement in Kasli across versions.
LVDS tristate seems not supported according to project Icestorm's bitstream descriptions.
2025-07-09 10:01:39 +08:00
c538e26b28 remove GB for nu_clk
icetime does not understand its timing characteristics for some nu_clk uses.
2025-07-09 10:01:39 +08:00
0d5a24d8ec package_pin -> i/o 2025-07-09 10:01:39 +08:00
7bda499a1e migrate pins to make servo work
- MOSI is always DIFF out on EEM[0].2
- NU_CLK now share EEM[0] with CS[2]
- CS[2] is replaced by IO_UPDATE_N in servo mode
2025-07-09 10:01:39 +08:00
7039fdd594 fix async dds_reset 2025-07-09 10:01:39 +08:00
9a23c50bba force all CS wire to go through a LUT 2025-07-09 10:01:39 +08:00
22ce5c8d52 revert LVDS IO for LVDS tristate
There are no documentations on how to specify a LVDS tristate.
2025-07-09 10:01:39 +08:00
f47cdf1cf3 ice: forbid signals using global buffer without specified 2025-07-09 10:01:39 +08:00
f90efb785b remove EEM:N pins 2025-07-09 10:01:39 +08:00
b327d6baac qspi_mosi -> nu_mosi 2025-07-09 10:01:39 +08:00
28a9e69c94 fix switch indexing in docs 2025-07-09 10:01:39 +08:00
1b8e3c1c93 add missing import 2025-07-09 10:01:39 +08:00
2218324930 ice40: use differential I/O 2025-07-09 10:01:39 +08:00
d257097cfd update I/O primitives 2025-07-09 10:01:39 +08:00
bce3de2b64 revert proto_rev regression 2025-07-09 10:01:39 +08:00
4190cd490e fix sck1, simplify 2025-07-09 10:01:39 +08:00
1984e61f4f remove unnecessary "sys" clock 2025-07-09 10:01:39 +08:00
0eaff94df0 ice platform: eem p and n were mixed up 2025-07-09 10:01:39 +08:00
e2158d5cf5 update readme with ICE40 instructions 2025-07-09 10:01:39 +08:00
622a424888 disable red error LED 2025-07-09 10:01:39 +08:00
fd1e6a8bc0 fix miso/io_upd_ret double role 2025-07-09 10:01:39 +08:00
99f51510c5 urukul ice40: eems are lvds, not tristate 2025-07-09 10:01:39 +08:00
e16b18441c Port redesign-7 branch to ice40 (without hw testing) 2025-07-09 10:01:39 +08:00
14 changed files with 971 additions and 11 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
build/
__pycache__/

View File

@@ -8,7 +8,21 @@
[NU-Servo](https://github.com/m-labs/nu-servo)
## Building
## Supported Urukul Hardware Revision (HW_REV)
| PLD | HW_REV |
|:--------:|:--------:|
| XC2 CPLD | 1.4, 1.5 |
| iCE40 | 1.6+ |
## Instructions for Xilinx CoolRunner-II CPLD (XC2 CPLD)
Sources are in the `xc2c` directory.
```
cd xc2c
```
### Building
Needs [migen](https://github.com/m-labs/migen) and [Xilinx ISE](https://www.xilinx.com/products/design-tools/ise-design-suite.html). Assumes ISE is installed in ``/opt/Xilinx``.
@@ -16,7 +30,7 @@ Needs [migen](https://github.com/m-labs/migen) and [Xilinx ISE](https://www.xili
make
```
## Flashing
### Flashing
With Digilent [JTAG HS2](https://store.digilentinc.com/jtag-hs2-programming-cable/) cable:
@@ -35,6 +49,43 @@ With Digilent [JTAG HS2](https://store.digilentinc.com/jtag-hs2-programming-cabl
- look for ``Verify: Success``
## Instructions for iCE40 FPGA
Sources are in the `ice40` directory.
```
cd ice40
```
### Building
Needs [migen](https://github.com/m-labs/migen), ``yosys``, ``nextpnr``, ``icestorm``.
```
python urukul.py # Builds non-SU-Servo firmware
```
To build the SU-Servo version of the firmware, add the `--suservo` argument when calling the script.
### Flashing
Use [kasli-i2c](https://git.m-labs.hk/M-Labs/kasli-i2c/src/branch/flash_urukul/).
Otherwise you can also use an FT232H-based board. Connect the wires as follows. Top left is closest to the CPLD, right is facing the edge. Bottom is towards the end of the edge.
```
D4 D1
D2 D0
GND X
X X
D7 D6
```
Use ``iceprog`` (available with ``icestorm`` package):
```
iceprog build/urukul.bin # non-SU-Servo
iceprog build/suservo.bin # SU-Servo
```
# License
GPLv3+

44
flake.lock generated Normal file
View File

@@ -0,0 +1,44 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1755593991,
"narHash": "sha256-BA9MuPjBDx/WnpTJ0EGhStyfE7hug8g85Y3Ju9oTsM4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a58390ab6f1aa810eb8e0f0fc74230e7cc06de03",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"src-migen": "src-migen"
}
},
"src-migen": {
"flake": false,
"locked": {
"lastModified": 1749544952,
"narHash": "sha256-NshlPiORBHWljSUP5bB7YBxe7k8dW0t8UXOsIq2EK8I=",
"owner": "m-labs",
"repo": "migen",
"rev": "6e3a9e150fb006dabc4b55043d3af18dbfecd7e8",
"type": "github"
},
"original": {
"owner": "m-labs",
"repo": "migen",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

107
flake.nix Normal file
View File

@@ -0,0 +1,107 @@
{
description = "CPLD/FPGA gateware on Urukul";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
src-migen = {
url = "github:m-labs/migen";
flake = false;
};
};
outputs =
{
self,
nixpkgs,
src-migen,
}:
let
pkgs = import nixpkgs {
system = "x86_64-linux";
};
migen = pkgs.python3Packages.buildPythonPackage rec {
name = "migen";
src = src-migen;
pyproject = true;
build-system = [ pkgs.python3Packages.setuptools ];
propagatedBuildInputs = [ pkgs.python3Packages.colorama ];
};
ise =
let
isePath = "/opt/Xilinx/14.7/ISE_DS";
makeXilinxEnv =
name:
pkgs.buildFHSEnv {
inherit name;
targetPkgs =
pkgs:
(with pkgs; [
ncurses5
zlib
libuuid
xorg.libSM
xorg.libICE
xorg.libXrender
xorg.libX11
xorg.libXext
xorg.libXtst
xorg.libXi
]);
profile = ''
source ${isePath}/common/.settings64.sh ${isePath}/common
source ${isePath}/ISE/.settings64.sh ${isePath}/ISE
'';
runScript = name;
};
in
pkgs.lib.attrsets.genAttrs [ "xst" "ngdbuild" "cpldfit" "taengine" "hprep6" ] makeXilinxEnv;
in
rec {
packages.x86_64-linux = {
inherit migen;
urukul-xc2c = pkgs.stdenv.mkDerivation {
name = "urukul-xc2c";
src = self;
buildInputs = [ (pkgs.python3.withPackages (ps: [ migen ])) ] ++ (builtins.attrValues ise);
buildPhase = "python xc2c/urukul_impl.py";
installPhase = ''
mkdir -p $out $out/nix-support
cp build/urukul.jed $out
echo file binary-dist $out/urukul.jed >> $out/nix-support/hydra-build-products
'';
};
urukul-ice40 = pkgs.stdenv.mkDerivation {
name = "urukul-ice40";
src = self;
buildInputs = [
(pkgs.python3.withPackages (ps: [ migen ]))
]
++ (with pkgs; [
yosys
nextpnr
icestorm
]);
buildPhase = ''
python ice40/urukul.py
python ice40/urukul.py --suservo
'';
installPhase = ''
mkdir -p $out $out/nix-support
cp build/urukul.bin $out
cp build/suservo.bin $out
echo file binary-dist $out/urukul.bin >> $out/nix-support/hydra-build-products
echo file binary-dist $out/suservo.bin >> $out/nix-support/hydra-build-products
'';
};
};
formatter.x86_64-linux = pkgs.nixfmt-tree;
hydraJobs = packages.x86_64-linux;
};
}

158
ice40/platform.py Normal file
View File

@@ -0,0 +1,158 @@
from migen.build.generic_platform import *
from migen.build.lattice import LatticePlatform
_io = [
("clk25", 0, Pins("K9")),
("tp", 0, Pins("H14")),
("tp", 1, Pins("J16")),
("tp", 2, Pins("J15")),
("tp", 3, Pins("K16")),
("tp", 4, Pins("K15")),
("ifc_mode", 0, Pins("E16 D16 D15 C16")),
# two more user switches marked SW2 on PCB
("sw", 0, Pins("F15 F16")),
("hw_rev", 0, Pins("R3 T2 R2 T1")),
# 10k low: AD9912, 0R high: AD9910
("variant", 0, Pins("T3")),
("err", 0, Pins("L7")),
(
"clk",
0,
Subsignal("div", Pins("P6")),
Subsignal("in_sel", Pins("P5")),
Subsignal("mmcx_osc_sel", Pins("M8")),
Subsignal("osc_en_n", Pins("T5")),
),
(
"dds_common",
0,
Subsignal("master_reset", Pins("C7")),
Subsignal("io_reset", Pins("E14")),
),
(
"dds_sync",
0,
Subsignal("clk0", Pins("B4")), # DDS_SYNC_CLK0
Subsignal("clk_out_en", Pins("N6")), # DDS_SYNC_CLK_OUTEN
Subsignal("sync_sel", Pins("N5")), # DDS_SYNC_CLKSEL
Subsignal("sync_out_en", Pins("N7")), # DDS_SYNC_OUTEN
),
(
"att",
0,
Subsignal("clk", Pins("B3")),
Subsignal("rst_n", Pins("D8")),
Subsignal("le", Pins("D13 C11 D4 A2")),
Subsignal("s_in", Pins("C13 C12 C5 A1")),
Subsignal("s_out", Pins("C14 E11 C4 F9")),
),
(
"dds",
0,
Subsignal("rf_sw", Pins("A7")),
Subsignal("led", Pins("G13 G14")),
Subsignal("smp_err", Pins("F7")),
Subsignal("pll_lock", Pins("E5")),
Subsignal("io_update", Pins("B12")),
Subsignal("profile", Pins("A15 B13 B14")),
Subsignal("osk", Pins("A16")),
Subsignal("drover", Pins("B15")),
Subsignal("drhold", Pins("D14")),
Subsignal("drctl", Pins("B16")),
Subsignal("reset", Pins("B11")),
Subsignal("sck", Pins("B8")),
Subsignal("sdo", Pins("A11")),
Subsignal("sdi", Pins("A10")),
Subsignal("cs_n", Pins("B10")),
),
(
"dds",
1,
Subsignal("rf_sw", Pins("A6")),
Subsignal("led", Pins("F14 F13")),
Subsignal("smp_err", Pins("D11")),
Subsignal("pll_lock", Pins("C10")),
Subsignal("io_update", Pins("L16")),
Subsignal("profile", Pins("J14 K14 J10")),
Subsignal("osk", Pins("H12")),
Subsignal("drover", Pins("G15")),
Subsignal("drhold", Pins("B9")),
Subsignal("drctl", Pins("G16")),
Subsignal("reset", Pins("A9")),
Subsignal("sck", Pins("G11")),
Subsignal("sdo", Pins("N16")),
Subsignal("sdi", Pins("M16")),
Subsignal("cs_n", Pins("M15")),
),
(
"dds",
2,
Subsignal("rf_sw", Pins("C8")),
Subsignal("led", Pins("E13 G12")),
Subsignal("smp_err", Pins("B5")),
Subsignal("pll_lock", Pins("P4")),
Subsignal("io_update", Pins("C6")),
Subsignal("profile", Pins("E9 E6 D7")),
Subsignal("osk", Pins("E10")),
Subsignal("drover", Pins("D9")),
Subsignal("drhold", Pins("D10")),
Subsignal("drctl", Pins("C9")),
Subsignal("reset", Pins("B7")),
Subsignal("sck", Pins("C3")),
Subsignal("sdo", Pins("D5")),
Subsignal("sdi", Pins("A5")),
Subsignal("cs_n", Pins("D6")),
),
(
"dds",
3,
Subsignal("rf_sw", Pins("B6")),
Subsignal("led", Pins("H13 F11")),
Subsignal("smp_err", Pins("P15")),
Subsignal("pll_lock", Pins("P16")),
Subsignal("io_update", Pins("M14")),
Subsignal("profile", Pins("L12 M13 R14")),
Subsignal("osk", Pins("J12")),
Subsignal("drover", Pins("J11")),
Subsignal("drhold", Pins("G10")),
Subsignal("drctl", Pins("H11")),
Subsignal("reset", Pins("J13")),
Subsignal("sck", Pins("K13")),
Subsignal("sdo", Pins("L13")),
Subsignal("sdi", Pins("L14")),
Subsignal("cs_n", Pins("R15")),
),
# All EEM I/O are in LVDS standard. P pin corresponds to port A of LVDS pair.
# N pin/port B must not be requested as per iCE40 docs.
# See Figure 9. Differential I/O Design Example.
# EEM 0:P
("eem_p", 0, Pins("G1")), # SCLK
("eem_p", 1, Pins("M1")), # MOSI
("eem_p", 2, Pins("K3")), # MISO/NU_CLK
("eem_p", 3, Pins("H2")), # CS0
("eem_p", 4, Pins("G2")), # CS1
("eem_p", 5, Pins("E3")), # CS2/NU_CS
("eem_p", 6, Pins("D2")), # IO_UPDATE
("eem_p", 7, Pins("B2")), # DDS_RESET/SYNC_DAT
# EEM 1:P
("eem_p", 8, Pins("H1")), # SYNC_CLK/NU_MOSI0
("eem_p", 9, Pins("L1")), # SYNC_IN/NU_MOSI1
("eem_p", 10, Pins("J1")), # IO_UPDATE_RET/NU_MOSI2
("eem_p", 11, Pins("F2")), # NU_MOSI3
("eem_p", 12, Pins("E2")), # SW0
("eem_p", 13, Pins("D1")), # SW1
("eem_p", 14, Pins("C2")), # SW2
("eem_p", 15, Pins("B1")), # SW3
# Remaining N pins are for LVDS outputs
("eem_n", 2, Pins("K1")), # MISO
("eem_n", 10, Pins("J2")), # IO_UPDATE_RET
]
class Platform(LatticePlatform):
default_clk_name = "clk25"
default_clk_period = 40.0
def __init__(self):
LatticePlatform.__init__(self, "ice40-hx8k-ct256", _io, toolchain="icestorm")

587
ice40/urukul.py Normal file
View File

@@ -0,0 +1,587 @@
from migen import *
from migen.genlib.io import DifferentialInput, DifferentialOutput
from migen.genlib.coding import Encoder
# increment this if the behavior (LEDs, registers, EEM pins) changes
__proto_rev__ = 9
class SR(Module):
"""
Shift register, SPI slave
* CPOL = 0 (clock idle low during ~SEL)
* CPHA = 0 (sample on first edge, shift on second)
* SPI mode 0
* samples SDI on rising clock edges (SCK1 domain)
* shifts out SDO on falling clock edges (SCK0 domain)
* MSB first
* the first output bit (MSB) is undefined
* the first output bit is available from the start of the SEL cycle until
the first falling edge
* the first input bit is sampled on the first rising edge
* on the first rising edge with SEL assered, the parallel data DO
is loaded into the shift register
* following at least one rising clock edge, on the deassertion of SEL,
the shift register is loaded into the parallel data register DI
"""
def __init__(self, width):
self.sdi = Signal()
self.sdo = Signal()
self.sel = Signal()
self.di = Signal(width)
self.do = Signal(width)
# # #
sr = Signal(width)
self.clock_domains.cd_le = ClockDomain("le", reset_less=True)
# clock the latch domain from selection deassertion but only after
# there was a serial clock edge with asserted select (i.e. ignore
# glitches).
self.specials += Instance("SB_DFFES", i_D=0, i_C=ClockSignal("sck1"),
i_E=self.sel, i_S=~self.sel, o_Q=self.cd_le.clk)
self.sync.sck0 += [
If(self.sel,
self.sdo.eq(sr[-1]),
),
]
self.sync.sck1 += [
If(self.sel,
sr[0].eq(self.sdi),
If(self.cd_le.clk,
sr[1:].eq(self.do[:-1])
).Else(
sr[1:].eq(sr[:-1])
)
)
]
self.sync.le += [
self.di.eq(sr)
]
class CFG(Module):
"""Configuration register
The configuration register is updated from the SPI shift register on the
deselection of the CPLD at the end of the SPI transaction.
The initial state is 0 (all bits cleared).
The bits in the configuration register (from LSB to MSB) are:
| Name | Width | Function |
|-----------+-------+-------------------------------------------------|
| RF_SW | 4 | Activates RF switch (per channel) |
| LED | 4 | Activates the red LED (per channel) |
| PROFILE | 4 * 3 | Controls DDS.PROFILE[0:2] (per channel) |
| OSK | 4 | Asserts DDS.OSK (per channel) |
| DRCTL | 4 | Asserts DDS.DRCTL (per channel) |
| DRHOLD | 4 | Asserts DDS.DRHOLD (per channel) |
| IO_UPDATE | 4 | Asserts DDS.IO_UPDATE (per channel), where |
| | | CFG.MASK_NU is high |
| MASK_NU | 4 | Disables DDS from QSPI interface, disables |
| | | IO_UPDATE control through IO_UPDATE EEM signal, |
| | | enables access through CS=3, enables control of |
| | | IO_UPDATE through CFG.IO_UPDATE[0:3] |
| CLK_SEL0 | 1 | Selects CLK source: 0 MMCX/OSC, 1 SMA |
| SYNC_SEL | 1 | Selects SYNC source |
| RST | 1 | Asserts DDS[0:3].RESET, DDS[0:3].MASTER_RESET, |
| | | ATT[0:3].RST |
| IO_RST | 1 | Asserts DDS[0:3].IO_RESET |
| CLK_SEL1 | 1 | Selects CLK source: 0 OSC, 1 MMCX |
| DIV | 2 | Clock divider configuration: 0: default, |
| | | 1: divide-by-one, 2: divider-by-two, |
| | | 3: divide-by-four |
| ATT_EN | 4 | Enable ATT (per channel) |
"""
def __init__(self, platform, n=4):
self.data = Record(
[
("rf_sw", n),
("led", n),
("profile", 3 * n),
("osk", n),
("drctl", n),
("drhold", n),
("io_update", n),
("mask_nu", 4),
("clk_sel0", 1),
("sync_sel", 1),
("rst", 1),
("io_rst", 1),
("clk_sel1", 1),
("div", 2),
("att_en", n),
]
)
dds_common = platform.lookup_request("dds_common")
dds_sync = platform.lookup_request("dds_sync")
att = platform.lookup_request("att")
clk = platform.lookup_request("clk")
self.en_9910 = Signal()
self.comb += [
clk.in_sel.eq(self.data.clk_sel0),
clk.mmcx_osc_sel.eq(self.data.clk_sel1),
clk.osc_en_n.eq(clk.in_sel | clk.mmcx_osc_sel),
dds_sync.sync_sel.eq(self.data.sync_sel),
dds_common.master_reset.eq(self.data.rst),
dds_common.io_reset.eq(self.data.io_rst),
att.rst_n.eq(~self.data.rst),
]
for i in range(n):
sw = Signal()
self.specials += DifferentialInput(platform.request("eem_p", 12 + i), Signal(), sw)
dds = platform.lookup_request("dds", i)
self.comb += [
dds.rf_sw.eq(sw | self.data.rf_sw[i]),
dds.led[0].eq(dds.rf_sw), # green
dds.led[1].eq(self.data.led[i] | (self.en_9910 & (
dds.smp_err | ~dds.pll_lock))), # red
dds.profile.eq(self.data.profile[3 * i : 3 * i + 3]),
dds.osk.eq(self.data.osk[i]),
dds.drhold.eq(self.data.drhold[i]),
dds.drctl.eq(self.data.drctl[i]),
]
class Status(Module):
"""Status register.
The status data is loaded into the SPI shift register on the first
rising SCK edge while the CPLD is selected. The bits from LSB to MSB
are:
| Name | Width | Function |
|-----------+-------+-------------------------------------------|
| RF_SW | 4 | Actual RF switch and green LED activation |
| | | (including that by EEM1.SW[0:3]) |
| SMP_ERR | 4 | DDS.SMP_ERR per channel |
| PLL_LOCK | 4 | DDS.PLL_LOCK per channel |
| IFC_MODE | 4 | IFC_MODE[0:3] |
| PROTO_REV | 7 | Protocol revision (see __proto_rev__) |
| DROVER | 4 | DDS.DROVER per channel |
"""
def __init__(self, platform, n=4):
self.data = Record(
[
("rf_sw", n),
("smp_err", n),
("pll_lock", n),
("ifc_mode", 4),
("proto_rev", 7),
("drover", n),
]
)
self.comb += [
self.data.ifc_mode.eq(platform.lookup_request("ifc_mode")),
self.data.proto_rev.eq(__proto_rev__),
]
for i in range(n):
dds = platform.lookup_request("dds", i)
self.comb += [
self.data.rf_sw[i].eq(dds.rf_sw),
self.data.smp_err[i].eq(dds.smp_err),
self.data.pll_lock[i].eq(dds.pll_lock),
self.data.drover[i].eq(dds.drover),
]
class Urukul(Module):
"""
Urukul IO router and configuration/status
=========================================
The CPLD controls/monitors:
* the four AD9912 or AD9910 DDS (SPI, status, reset, IO update)
* the four digitally controlled RF step attenuators (SPI, reset)
* the four RF switches
* the clock input tree (division and clock selection)
* the synchronization tree (sync source selection, sync clock output,
sync drive)
* the eight LEDs
* the two EEM connectors
* the test pads
* the four configuration switches
Pin Out
-------
Urukul operates from one or two EEM connectors. In standard SPI mode, the
complete Urukul functionality can be accessed using only that interface.
Standard SPI mode only needs the second EEM connector to interface with
high resolution RF switching and synchronization signals. SU-Servo mode
always requires two EEM connectors.
| EEM | LVDS pair | PCB net | Standalone | SU-Servo |
|------+-----------+---------+---------------------+-----------|
| EEM0 | 0 | A0 | SCLK | SCLK |
| EEM0 | 1 | A1 | MOSI | MOSI |
| EEM0 | 2 | A2 | MISO | MISO |
| EEM0 | 3 | A3 | CS0 | CS0 |
| EEM0 | 4 | A4 | CS1 | CS1 |
| EEM0 | 5 | A5 | CS2 | NU_CLK |
| EEM0 | 6 | A6 | IO_UPDATE | IO_UPDATE |
| EEM0 | 7 | A7 | DDS_RESET, SYNC_IN | SYNC_IN |
| EEM1 | 0 | B8 | SYNC_CLK | NU_MOSI0 |
| EEM1 | 1 | B9 | SYNC_OUT | NU_MOSI1 |
| EEM1 | 2 | B10 | IO_UPDATE_RET | NU_MOSI2 |
| EEM1 | 3 | B11 | UNUSED | NU_MOSI3 |
| EEM1 | 4 | B12 | SW0 | SW0 |
| EEM1 | 5 | B13 | SW1 | SW1 |
| EEM1 | 6 | B14 | SW2 | SW2 |
| EEM1 | 7 | B15 | SW3 | SW3 |
IFC_MODE
--------
DIP switches are used to configure the operation of the Urukul CPLD. The
four IFC mode switches (the first bank, SW1) are assigned as:
| IFC_MODE | Name | Function |
|----------+---------+-------------------------------------------------|
| 0 | EN_9910 | On if AD9910 is populated (OR VARIANT) |
| 1 | UNUSED | Unused on Urukul DIOT |
| 2 | EN_EEM1 | On if the SYNC signals on EEM1 should be driven |
| 3 | UNUSED | Unusable on Urukul/v1.0 |
On Urukul/v1.0, IFC_MODE[0] | IFC_MODE[3] drive EN_9910.
On Urukul/v1.1, IFC_MODE[0] | VARIANT (board population) drive EN_9910.
Second bank (SW2) of DIP switches is unused.
See :class:`Urukul`
SPI
---
An SPI interface is provided to access any of the six serial devices (the
configuration/status SPI interface, the attenuator SPI interface, and the
four DDS SPI interfaces). It comprises the SCLK, MOSI, MISO, CS0, CS1, and
CS2 signals. In the SU-Servo mode, both MISO and CS2 (and the functionality
provided by them) are unavailable. I.e. CS >= 4 (individual DDS access)
are only available outside of the SU-Servo mode or through CS = 3 (and
CFG.MASK_NU).
The target chip (or set of chips) is selected by CS0/CS1/CS2 (CS2 being the
MSB). The encoding is as follows:
| CS | chip |
|-----------+--------------------------------------------|
| 0 = 0b000 | None |
| 1 = 0b001 | CFG |
| 2 = 0b010 | ATT |
| 3 = 0b011 | Multiple DDS (those masked by CFG.MASK_NU) |
| 4 = 0b100 | DDS0 |
| 5 = 0b101 | DDS1 |
| 6 = 0b110 | DDS2 |
| 7 = 0b111 | DDS3 |
The SPI interface is CPOL=0, CPHA=0, SPI mode 0, 4-wire, full fuplex. Clock
cycles during CS[0:2] = 0 are ignored (but may still be visible on the DDS
SCK outputs).
See :class:`Urukul` and :class:`SR`
CFG
---
The configuration status register controls the overall operation of Urukul,
allows some configuration options to be changed and the status of some
signals to be monitored.
It is 52 bits wide, MSB first.
See :class:`SR`
CFG write
.........
See :class:`CFG`
CFG read
........
See :class:`Status`
QSPI
----
In the SU-Servo mode, the four DDS are additionally exposed through a
quad-SPI write-only interface defined by the signals NU_CLK, NU_CS, and
NU_MOSI[0:3].
Only those DDS which are **not** masked by CFG.MASK_NU can be accessed
through the QSPI interface. This allows initial setup and configuration of
the DDS individually through the "regular" SPI interface in this mode.
DDS[0:3].CS is driven by NU_CS (for those DDS not masked)
DDS[0:3].SCK is driven by NU_CLK (for those DDS not masked)
DDS[0:3].MOSI is driven by NU_MOSI[0:3] (for those DDS not masked)
DDS[0:3].MISO is unavailable
DDS[0:3].IO_UPDATE is driven by IO_UPDATE (for those DDS not masked)
See :class:`Urukul`
ATT
---
The digital step attenuators are daisy-chained (ATT[n].S_OUT driving the
next ATT[n+1].S_IN) and form a 32 bit SPI compatible shift register. If the
corresponding CFG.ATT_EN bit is set, the data from the attenuator shift
register is transferred to the active attenuation register on the
de-selection of the attenuators after shifting.
Clocking
--------
CFG.CLK_SEL selects the clock source for the clock fanout to the DDS.
Valid clocking options are:
- 0x00: on-board 100MHz oscillator
- 0x01: front-panel SMA
- 0x02: internal MMCX (hardware version >= 1.3 only)
For hardware revisions prior to v1.3, 0x00 selects either the on-board
oscillator or the MMCX, dependent on component population. In these
hardware revisions, the oscillator must be manually powered down to avoid
RF leakage through the clock switch.
If CFG.DIV is 0 the clock division is determined EN_9910. If it is on,
the clock to the DDS (from the XCO, the internal MMCX or the external
SMA) is divided by 4.
If CFG.DIV is 1, 2, or 3 it determines the clock divisor (1, 2, 4).
Synchronization
---------------
IO_UPDATE_RET is provided to determine the round trip time for IO_UPDATE.
DDS_RESET (not EN_9910) and SYNC_IN (EN_9910) share an EEM signal.
DDS_RESET provides a way to deterministically reset all AD9912 DDS SYNC_CLK
divider. (https://ez.analog.com/docs/DOC-14472)
SYNC_IN is an input to the SYNC fanout (input to Urukul, output from the
controlling FPGA upstream) to externally and actively synchronize the
AD9910 SYNC_CLK dividers. The SYNC fanout can be driven using either
SYNC_IN or DDS0.SYNC_OUT (selected by CFG.SYNC_SEL). The SYNC fanout drives
all DDSx.SYNC_IN.
SYNC_CLK and SYNC_OUT are available with EN_9910 & EN_EEM1 to synchronize
external logic to the DDS. A round-trip time measurement using
IO_UPDATE_RET would need to be performed. SYNC_CLK is sourced from DDS0,
while SYNC_OUT is sourced from the SYNC fanout. Both SYNC_CLK and SYNC_OUT
are outputs from Urukul, inputs to the controlling upstream FPGA.
RF switches
-----------
The RF switches are activated with CFG.RF_SW or (logic OR) SW[0:3].
EEM1.SW[0:3] provide a high resolution and high-bandwidth port to RF
switching.
LEDs
----
The green channel LEDs mirror the status of the RF switches. The red LEDs
are activated by ``CFG.LED | (EN_9910 & (DDS[0:3].SMP_ERR |
~DDS[0:3].PLL_LOCK))``. I.e. they are lit by the register or (logic OR) an
synchronization/PLL error on that channel's DDS.
Test points
-----------
The test points expose miscellaneous signals for debugging and are not part
of the protocol revision.
"""
def __init__(self, platform, enable_suservo=False):
clk = platform.request("clk")
dds_sync = platform.request("dds_sync")
dds_common = platform.request("dds_common")
ifc_mode = platform.request("ifc_mode", 0)
variant = platform.request("variant")
att = platform.request("att")
dds = [platform.request("dds", i) for i in range(4)]
ts_clk_div = TSTriple()
self.specials += [
ts_clk_div.get_tristate(clk.div)
]
# Convert LVDS signals from EEM interface
self.eem = eem = [platform.request("eem_p", i) for i in range(12)]
# SPI clock
self.clock_domains.cd_sck0 = ClockDomain("sck0", reset_less=True)
self.clock_domains.cd_sck1 = ClockDomain("sck1", reset_less=True)
en_9910 = Signal() # AD9910 populated (instead of AD9912)
en_eem1 = Signal() # EEM1 connected and sync outputs used
miso_phy = Signal()
io_update_ret = Signal()
mosi = Signal()
nu_clk = Signal()
io_update = Signal()
dds_reset = Signal()
nu_mosi = Signal(4)
self.specials += [
Instance("SB_GB_IO",
p_PIN_TYPE=0b000000, # PIN_TYPE setting for this SB_GB_IO primitive does not actually matter.
p_IO_STANDARD="SB_LVDS_INPUT",
io_PACKAGE_PIN=eem[0],
o_GLOBAL_BUFFER_OUTPUT=self.cd_sck1.clk),
DifferentialInput(eem[1], Signal(), mosi),
DifferentialOutput(miso_phy, eem[2], platform.request("eem_n", 2)),
DifferentialInput(eem[6], Signal(), io_update),
DifferentialInput(eem[7], Signal(), dds_reset),
]
platform.add_period_constraint(eem[0], 8.)
if enable_suservo:
self.specials += [
DifferentialInput(eem[5], Signal(), nu_clk),
*[ DifferentialInput(eem[8 + i], Signal(), nu_mosi[i]) for i in range(4) ]
]
cs = Signal(2)
else:
self.specials += [
Instance("SB_IO",
p_PIN_TYPE=C(0b101000, 6),
p_IO_STANDARD="SB_LVCMOS",
io_PACKAGE_PIN=eem[10],
i_OUTPUT_ENABLE=en_eem1,
i_D_OUT_0=io_update_ret),
Instance("SB_IO",
p_PIN_TYPE=C(0b101000, 6),
p_IO_STANDARD="SB_LVCMOS",
io_PACKAGE_PIN=platform.request("eem_n", 10),
i_OUTPUT_ENABLE=en_eem1,
i_D_OUT_0=~io_update_ret),
]
cs = Signal(3)
for i, csi in enumerate(cs):
self.specials += DifferentialInput(eem[i + 3], Signal(), cs[i])
self.comb += [
en_9910.eq(ifc_mode[0] | variant),
en_eem1.eq(ifc_mode[2]),
io_update_ret.eq(io_update),
self.cd_sck0.clk.eq(~self.cd_sck1.clk),
dds_sync.clk_out_en.eq((not enable_suservo) & en_eem1 & en_9910),
dds_sync.sync_out_en.eq((not enable_suservo) & en_eem1 & en_9910),
]
cfg = CFG(platform)
stat = Status(platform)
sr = SR(52)
assert len(cfg.data) <= len(sr.di)
assert len(stat.data) <= len(sr.do)
self.submodules += cfg, stat, sr
sel = Signal(8)
miso = Signal(8)
self.specials += [
Instance("SB_DFFES", i_D=0, i_C=ClockSignal("sck1"), i_E=sel[2],
i_S=~(sel[2] & cfg.data.att_en[i]), o_Q=att.le[i]) for i in range(4)
]
if enable_suservo:
dds_miso_sel = Signal(max=len(cfg.data.mask_nu))
self.submodules.dds_miso_encoder = Encoder(len(cfg.data.mask_nu))
self.comb += [
self.dds_miso_encoder.i.eq(cfg.data.mask_nu),
dds_miso_sel.eq(Mux(self.dds_miso_encoder.n,
0, self.dds_miso_encoder.o)),
miso[3].eq(Array(miso[4:])[dds_miso_sel]),
]
self.comb += [
cfg.en_9910.eq(en_9910),
Array(sel)[cs].eq(1), # one-hot
miso_phy.eq(Array(miso)[cs]),
att.clk.eq(sel[2] & self.cd_sck1.clk),
Cat(att.s_in, miso[2]).eq(Cat(mosi, att.s_out)),
sr.sel.eq(sel[1]),
sr.sdi.eq(mosi),
miso[1].eq(sr.sdo),
cfg.data.raw_bits().eq(sr.di),
sr.do.eq(stat.data.raw_bits()),
# dividers: z: 1, 0: 2, 1: 4
# 1: div-by-4 for AD9910
# z: div-by-1 for AD9912
ts_clk_div.oe.eq(Array([en_9910, 0, 1, 1])[cfg.data.div]),
ts_clk_div.o.eq(Array([1, 1, 0, 1])[cfg.data.div]),
]
for i, ddsi in enumerate(dds):
sel_spi = Signal()
sel_nu = Signal()
self.comb += [
sel_spi.eq(sel[i + 4] | (sel[3] & cfg.data.mask_nu[i])),
sel_nu.eq(enable_suservo & ~cfg.data.mask_nu[i]),
ddsi.sdi.eq(Mux(sel_nu, nu_mosi[i], mosi)),
# NU_CLK exclusively drive SCLK in SU-Servo mode.
# As per the AD9910 datasheet, CS_N can be tied low.
ddsi.cs_n.eq(~Mux(sel_nu, 1, sel_spi)),
ddsi.sck.eq(Mux(sel_nu, nu_clk, self.cd_sck1.clk)),
miso[i + 4].eq(ddsi.sdo),
ddsi.io_update.eq(Mux(cfg.data.mask_nu[i],
cfg.data.io_update[i], io_update)),
ddsi.reset.eq(cfg.data.rst | dds_reset),
]
tp = [platform.request("tp", i) for i in range(5)]
self.comb += [
tp[0].eq(dds[0].cs_n),
tp[1].eq(dds[0].sck),
tp[2].eq(dds[0].sdi),
tp[3].eq(dds[0].sdo),
tp[4].eq(dds[0].drover),
]
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description="Urukul binary builder for the iCE40 variant")
# iCE40 does not support bi-directional LVDS I/O.
# Hence, Urukuls in the SU-Servo mode use a different binary.
# https://www.latticesemi.com/support/answerdatabase/3/8/8/3881
parser.add_argument("--suservo", action="store_true",
help="enable SU-Servo support")
args = parser.parse_args()
from platform import Platform
platform = Platform()
platform.toolchain.nextpnr_build_template[1:2] = [
"for seed in `seq 1 10`; do",
"echo Seed $seed",
("nextpnr-ice40 {pnr_pkg_opts} --pcf {build_name}.pcf --json {build_name}.json "
"--asc {build_name}.txt --pre-pack {build_name}_pre_pack.py "
"--no-promote-globals --seed $seed && break"),
"done"
]
urukul = Urukul(platform, args.suservo)
platform.build(urukul, build_name="suservo" if args.suservo else "urukul")

View File

@@ -8,7 +8,7 @@ test:
.PHONY: build
build: build/urukul.vm6
build/urukul.vm6: urukul.py urukul_cpld.py
build/urukul.vm6: urukul.py platform.py
python urukul_impl.py
REV:=$(shell git describe --always --abbrev=8 --dirty)

View File

@@ -1,4 +1,5 @@
from migen import *
from migen.genlib.coding import Encoder
# increment this if the behavior (LEDs, registers, EEM pins) changes
@@ -226,10 +227,10 @@ class Urukul(Module):
|------+-----------+---------+-------------------------|
| EEM0 | 0 | A0 | SCLK |
| EEM0 | 1 | A1 | MOSI |
| EEM0 | 2 | A2 | MISO, NU_CLK |
| EEM0 | 2 | A2 | MISO |
| EEM0 | 3 | A3 | CS0 |
| EEM0 | 4 | A4 | CS1 |
| EEM0 | 5 | A5 | CS2, NU_CS |
| EEM0 | 5 | A5 | CS2, NU_CLK |
| EEM0 | 6 | A6 | IO_UPDATE |
| EEM0 | 7 | A7 | DDS_RESET, SYNC_OUT |
| EEM1 | 0 | B8 | SYNC_CLK, NU_MOSI0 |
@@ -428,7 +429,8 @@ class Urukul(Module):
self.clock_domains.cd_sck1 = ClockDomain("sck1", reset_less=True)
platform.add_period_constraint(eem[0]._pin, 8.)
platform.add_period_constraint(eem[2]._pin, 8.)
# platform.add_period_constraint(eem[2]._pin, 8.)
platform.add_period_constraint(eem[5]._pin, 8.)
self.specials += [
Instance("BUFG", i_I=eem[0].i, o_O=self.cd_sck1.clk),
@@ -444,7 +446,7 @@ class Urukul(Module):
en_nu.eq(ifc_mode[1]),
en_eem1.eq(ifc_mode[2]),
[eem[i].oe.eq(0) for i in range(12) if i not in (2, 10)],
eem[2].oe.eq(~en_nu),
eem[2].oe.eq(1),
eem[10].oe.eq(~en_nu & en_eem1),
eem[10].o.eq(eem[6].i),
self.cd_sck0.clk.eq(~self.cd_sck1.clk),
@@ -462,12 +464,16 @@ class Urukul(Module):
sel = Signal(8)
cs = Signal(3)
miso = Signal(8)
self.comb += miso[0].eq(0)
mosi = eem[1].i
self.specials += [Instance("FDPE", p_INIT=1,
i_D=0, i_C=ClockSignal("sck1"), i_CE=sel[2],
i_PRE=~(sel[2] & cfg.data.att_en[i]), o_Q=att.le[i]) for i in range(4)]
dds_miso_sel = Signal(max=len(cfg.data.mask_nu))
self.submodules.dds_miso_encoder = Encoder(len(cfg.data.mask_nu))
self.comb += [
cfg.en_9910.eq(en_9910),
# Important note regarding the chip-select (CS) signals (`eem[3].i`, `eem[4].i`, `eem[5].i`):
@@ -488,7 +494,11 @@ class Urukul(Module):
Array(sel)[cs].eq(1), # one-hot
eem[2].o.eq(Array(miso)[cs]),
miso[3].eq(miso[4]), # for all-DDS take DDS0:MISO
self.dds_miso_encoder.i.eq(cfg.data.mask_nu),
dds_miso_sel.eq(Mux(self.dds_miso_encoder.n,
0, self.dds_miso_encoder.o)),
miso[3].eq(Array(miso[4:])[dds_miso_sel]),
att.clk.eq(sel[2] & self.cd_sck1.clk),
Cat(att.s_in, miso[2]).eq(Cat(mosi, att.s_out)),
@@ -512,8 +522,10 @@ class Urukul(Module):
self.comb += [
sel_spi.eq(sel[i + 4] | (sel[3] & cfg.data.mask_nu[i])),
sel_nu.eq(en_nu & ~cfg.data.mask_nu[i]),
ddsi.cs_n.eq(~Mux(sel_nu, eem[5].i, sel_spi)),
ddsi.sck.eq(Mux(sel_nu, eem[2].i, self.cd_sck1.clk)),
# NU_CLK exclusively drive SCLK in SU-Servo mode.
# As per the AD9910 datasheet, CS_N can be tied low.
ddsi.cs_n.eq(~Mux(sel_nu, 1, sel_spi)),
ddsi.sck.eq(Mux(sel_nu, eem[5].i, self.cd_sck1.clk)),
ddsi.sdi.eq(Mux(sel_nu, eem[i + 8].i, mosi)),
miso[i + 4].eq(ddsi.sdo),
ddsi.io_update.eq(Mux(cfg.data.mask_nu[i],

View File

@@ -1,5 +1,5 @@
def main():
from urukul_cpld import Platform
from platform import Platform
from urukul import Urukul
p = Platform()