// SPDX-License-Identifier: Apache-2.0 ; specs/widgets/fits-on.t27 -- how many copies of a design fit on an XC7A200T, and what runs out first ; Source of truth for public/widgets/fits-on/ (gHashTag/trinity, apps/website). The page is written ; by scripts/widget-pages-from-spec.mjs from this file and hands every constant below to tool.js as ; window.T27_WIDGET. tool.js holds no English of its own: every word on screen is a SAY_ constant ; here, every number the tool relies on is a K_ constant or a field of device.json. ; ASCII only (L3), English only (LANG-EN). ; WHAT IT DOES: the reader types the LUT count of a design, and if they like its flip-flop, DSP48E1, ; RAMB36E1 and RAMB18E1 counts. For each resource the page divides the XC7A200T's total by the ; count per copy and rounds down; the smallest quotient is the number of copies that fit, and the ; resource that gives it is named as the binding one. A grid draws one cell per copy over the ; binding resource's capacity, with what is left over after the last copy. Block RAM is counted in ; 18Kb halves: a RAMB36E1 takes two, a RAMB18E1 one, and the device has K_RAMB18E1 of them. ; HOW LUTS ARE DIVIDED: nextpnr-xilinx's SLICE_LUTX total, 269200, counts LUT BELs: 33650 slices ; times 8, a LUT6 BEL and a LUT5 BEL at each of the four positions A-D of a slice. A position holds ; one six-input LUT, or two smaller LUTs that share its inputs. In node0's routed result ; (/tmp/x7board/node0/node_routed.json) nextpnr placed its 4738 LUT cells on 4738 different ; positions: not one position was shared. So the page divides by positions, K_SLICE_LUTX / 2 = ; 134600, by default; the other button divides by the 269200 BELs, which is the ceiling only if ; every LUT found a partner, and is what nextpnr's own percent column divides by. No datasheet LUT ; figure is quoted: none was available to check locally. ; DATA PROVENANCE: device.json is written by scripts/widget-data/fits-on.mjs from the "Device ; utilisation" block of two real nextpnr-xilinx runs for the ALINX AX7203 board (part ; xc7a200tfbg484-2, chipdb xc7a200tfbg484-2.bin): /tmp/x7board/node0/nextpnr.log (top ; trinet_node_v2_ax7203, sha256 fbdeb0d27c53e0e0...) and ; ~/trinity-fpga/build/tnf16_board_ax7203/nextpnr.log (top tnf16_board_ax7203, sha256 ; 60e4ef9b281d70f7...). Both logs print the same total for every BEL type; the script stops if ; they do not, and stops if a K_ number below is not the number it parsed. The logs carry no ; version line; the manifest.json beside each names nextpnr-xilinx 0.9.2-107-g7037c948. The ; example counts are the same blocks' used column and the yosys stat block of yosys.full.log ; beside each log (yosys 0.67+post b8e7da6f40, synth_xilinx -flatten -abc9 -nocarry -nodsp). ; WHAT IT DOES NOT CLAIM: that the copies route, meet timing or work. Every number is at 100% of ; the binding resource, an upper bound; no lower practical utilisation is applied, because none of ; this page's sources measures one. A yosys LUT count is a synthesis count, not the count nextpnr ; places. Pins, clocks, carry chains and routing are not counted. The PAD total is a chipdb BEL ; count, not a count of bonded pins; this page uses it for nothing and claims no pin count for the ; QMTech Wukong (fgg676) or any other package. ; phi^2 + 1/phi^2 = 3 | TRINITY module widget_fits_on; pub const KIND : str = "widget-tool"; pub const ID : str = "fits-on"; pub const TITLE : str = "Fits on a 200T: how many copies of your design an XC7A200T holds"; pub const DESCRIPTION : str = "Type your LUT count (and FF, DSP, BRAM) and see how many copies fit on an XC7A200T, one grid cell per copy, and which resource runs out first. Totals from nextpnr-xilinx."; pub const IMAGE_ALT : str = "Fits on a 200T card: 28 copies of the AX7203 node0 design (4,738 LUTs, 2,378 FFs, 1 RAMB36E1) fit on an XC7A200T, drawn as 28 cells over 134,600 LUT6 positions; LUTs bind first, flip-flops would allow 113. Totals from nextpnr-xilinx."; pub const HOOK : str = "nextpnr said 1% LUTs. That design fits 28 times on a 200T, not 100: type your own LUT count and see which resource runs out first."; pub const CATEGORY : str = "fpga"; pub const DATA_SOURCES : [5]str = ["nextpnr-xilinx 0.9.2-107-g7037c948 (version from manifest.json beside the log), Device utilisation block: /tmp/x7board/node0/nextpnr.log, sha256 fbdeb0d27c53e0e0aea1981f157193a553f36f1ccf580155f0e779c9d9146d7c, ALINX AX7203, xc7a200tfbg484-2", "the same block in ~/trinity-fpga/build/tnf16_board_ax7203/nextpnr.log, sha256 60e4ef9b281d70f733dd135663349a3af0a43d6751ec9a3df1cefd16969a301e, ALINX AX7203, xc7a200tfbg484-2", "yosys 0.67+post b8e7da6f40 stat blocks in the yosys.full.log beside each log: the synthesis LUT and FF counts", "/tmp/x7board/node0/node_routed.json, sha256 e2f04fb0fb09311843cf8f3ed59b6d5f8e2ebf75c5c93c584ea43f33de4b7f61: NEXTPNR_BEL of each SLICE_LUTX cell, counted per slice position", "scripts/widget-data/fits-on.mjs: parses all of the above into public/widgets/fits-on/device.json and checks this spec against it"]; pub const READS_LOCAL_FILES : bool = false; pub const SENDS_NOTHING : bool = true; ; --- Totals, as nextpnr-xilinx printed them in both logs -------------------------------------- pub const K_DATA_FILE : str = "device.json"; pub const K_SLICE_LUTX : u32 = 269200; pub const K_SLICE_FFX : u32 = 269200; pub const K_CARRY4 : u32 = 33650; pub const K_DSP48E1 : u32 = 740; pub const K_RAMB36E1 : u32 = 365; pub const K_RAMB18E1 : u32 = 730; ; PAD BELs in the fbg484 chipdb: a BEL count, not bonded pins; quoted only to say so, never used. pub const K_PAD_FBG484 : u32 = 1062; ; A slice has one CARRY4, four LUT positions and two LUT BELs per position. pub const K_POSITIONS_PER_SLICE : u8 = 4; pub const K_BELS_PER_POSITION : u8 = 2; pub const K_LUT_POSITIONS : u32 = 134600; ; --- What the page divides by ------------------------------------------------------------------ ; Rows: LUT, FF, DSP, BRAM in 18Kb halves. K_CAPACITY[0] is replaced by K_SLICE_LUTX when the ; reader picks LUT BELs (K_DIVIDE_MODES[1]). pub const K_RESOURCES : [4]str = ["LUT", "FF", "DSP", "BRAM18"]; pub const K_CAPACITY : [4]u32 = [134600, 269200, 740, 730]; pub const K_DIVIDE_MODES : [2]str = ["positions", "bels"]; ; Inputs, in the order of the K_EX_* arrays: SLICE_LUTX, SLICE_FFX, DSP48E1, RAMB36E1, RAMB18E1. pub const K_INPUTS : [5]str = ["SLICE_LUTX", "SLICE_FFX", "DSP48E1", "RAMB36E1", "RAMB18E1"]; pub const K_HALVES_PER_INPUT : [2]u8 = [2, 1]; ; The grid draws at most this many cells; above it one cell stands for several copies. pub const K_GRID_MAX_CELLS : u16 = 600; pub const K_INPUT_MAX : u32 = 10000000; ; --- Two real designs, used column of each log (checked by scripts/widget-data/fits-on.mjs) --- pub const K_EXAMPLES : [2]str = ["node0", "tnf16"]; pub const K_EX_NODE0 : [5]u32 = [4738, 2378, 0, 1, 0]; pub const K_EX_NODE0_COPIES : u32 = 28; pub const K_EX_NODE0_COPIES_BELS : u32 = 56; pub const K_EX_NODE0_YOSYS_LUT : u32 = 4732; pub const K_EX_NODE0_LUT6_POSITIONS : u32 = 4738; pub const K_EX_TNF16 : [5]u32 = [838, 541, 0, 0, 0]; pub const K_EX_TNF16_COPIES : u32 = 160; pub const K_EX_TNF16_COPIES_BELS : u32 = 321; pub const K_EX_TNF16_YOSYS_LUT : u32 = 836; ; --- Words on screen --------------------------------------------------------------------------- ; Templates take {0}, {1}, ... in order; numbers are filled from K_ constants or device.json. pub const SAY_LOADING : str = "Reading the device totals..."; pub const SAY_ERR_DATA : str = "The device totals did not load."; pub const SAY_INPUT_TITLE : str = "One copy of your design uses"; pub const SAY_INPUT_NAMES : [5]str = ["LUTs", "Flip-flops", "DSP48E1", "RAMB36E1", "RAMB18E1"]; pub const SAY_INPUT_NOTES : [5]str = ["SLICE_LUTX, or a yosys LUT1-LUT6 count", "SLICE_FFX, or yosys FD* cells", "optional", "optional, 36Kb block RAM", "optional, 18Kb block RAM"]; pub const SAY_PRESET_LABEL : str = "Real designs"; pub const SAY_PRESET_NAMES : [2]str = ["node0", "tnf16"]; pub const SAY_PRESET_LINE : str = "{0} on the {1} ({2}): nextpnr placed {3} SLICE_LUTX; yosys had counted {4} LUT1-LUT6 cells."; pub const SAY_DIVIDE_LABEL : str = "Divide LUTs by"; pub const SAY_DIVIDE_NAMES : [2]str = ["LUT6 positions", "LUT BELs"]; pub const SAY_EMPTY : str = "Type a LUT count, or pick a real design."; pub const SAY_RESULT_MANY : str = "{0} copies fit"; pub const SAY_RESULT_ONE : str = "1 copy fits"; pub const SAY_RESULT_NONE : str = "Does not fit"; pub const SAY_BINDING : str = "{0} run out first: {1} per copy, {2} on the device."; pub const SAY_TOO_BIG : str = "One copy needs {1} {0}; the device has {2}."; pub const SAY_UPPER_BOUND : str = "At 100% of {0}: an upper bound, not a promise that it routes."; pub const SAY_COLS : [4]str = ["resource", "per copy", "on the device", "copies"]; pub const SAY_RES_NAMES : [4]str = ["LUTs", "Flip-flops", "DSP48E1", "Block RAM, 18Kb halves"]; pub const SAY_RES_UNITS : [4]str = ["LUT6 positions", "SLICE_FFX", "DSP48E1", "RAMB18E1 sites"]; pub const SAY_LUT_BELS_UNIT : str = "LUT BELs"; pub const SAY_NOT_USED : str = "not used"; pub const SAY_GRID_LINE : str = "One cell per copy over the device's {0}; the last, hollow cell is what is left: {1}."; pub const SAY_GRID_SCALED : str = "One cell per {0} copies over the device's {1}; the last, hollow cell is what is left: {2}."; pub const SAY_GRID_LABEL : str = "{0} copies drawn over {1} {2}"; pub const SAY_NOTES_TITLE : str = "Read this before you trust the number"; pub const SAY_NOTE_LUT : str = "nextpnr-xilinx counts LUT BELs: its SLICE_LUTX total {0} is {1} slices x {2}, a LUT6 and a LUT5 BEL at each of a slice's four positions. A position holds one six-input LUT, or two smaller LUTs that share its inputs. In node0's routed result nextpnr put its {3} LUT cells on {4} different positions, none shared. So this page divides by positions, {5}; nextpnr's own percent column divides by {0}, and reads half as full for a design like that one. LUT BELs gives the ceiling if every LUT found a partner."; pub const SAY_NOTE_BOUND : str = "A yosys LUT count is a synthesis count, not what nextpnr places: for node0 yosys counted {0} LUT1-LUT6 cells and nextpnr placed {1}. Every result here is at 100% of the binding resource, an upper bound; placement and routing need room, so fewer copies will route. How many fewer, none of this page's sources measures, so no lower figure is shown."; pub const SAY_NOTE_PACKAGE : str = "The package decides which pins are bonded, not what the die holds: LUT, FF, DSP and block RAM totals are the XC7A200T's in any package. Both logs are from the {0} ({1}), not the QMTech Wukong (fgg676). Their PAD total, {2}, is a chipdb BEL count, not a count of bonded pins; this page uses it for nothing and claims no pin count for the Wukong."; pub const SAY_SOURCE_TITLE : str = "Where the totals come from"; pub const SAY_SOURCE_LINE : str = "{0}: sha256 {1}, {2} bytes, top {3}"; pub const SAY_SOURCE_VERSION : str = "The logs carry no version line; the manifest.json beside them names nextpnr-xilinx {0}. Both logs print the same total for all {1} BEL types."; ; --- Tests --------------------------------------------------------------------------------------- test the_totals_are_the_parsed_nextpnr_totals { assert K_SLICE_LUTX == 269200; assert K_SLICE_FFX == 269200; assert K_CARRY4 == 33650; assert K_DSP48E1 == 740; assert K_RAMB36E1 == 365; assert K_RAMB18E1 == 730; assert K_PAD_FBG484 == 1062; } test lut_bels_are_eight_per_slice_and_positions_half_of_them { assert K_SLICE_LUTX == K_CARRY4 * K_POSITIONS_PER_SLICE * K_BELS_PER_POSITION; assert K_LUT_POSITIONS == K_SLICE_LUTX / K_BELS_PER_POSITION; assert K_LUT_POSITIONS == K_CARRY4 * K_POSITIONS_PER_SLICE; assert K_SLICE_FFX == K_CARRY4 * 8; assert K_RAMB18E1 == K_RAMB36E1 * K_HALVES_PER_INPUT[0]; } test the_capacity_row_is_the_totals { assert K_CAPACITY[0] == K_LUT_POSITIONS; assert K_CAPACITY[1] == K_SLICE_FFX; assert K_CAPACITY[2] == K_DSP48E1; assert K_CAPACITY[3] == K_RAMB18E1; assert K_RESOURCES[0] == "LUT"; assert K_DIVIDE_MODES[0] == "positions"; } test node0_fits_28_times_and_luts_bind { assert K_LUT_POSITIONS / K_EX_NODE0[0] == K_EX_NODE0_COPIES; assert K_EX_NODE0_COPIES * K_EX_NODE0[0] <= K_LUT_POSITIONS; assert K_EX_NODE0_COPIES * K_EX_NODE0[0] + K_EX_NODE0[0] > K_LUT_POSITIONS; assert K_SLICE_FFX / K_EX_NODE0[1] > K_EX_NODE0_COPIES; assert K_RAMB18E1 / (K_HALVES_PER_INPUT[0] * K_EX_NODE0[3] + K_EX_NODE0[4]) > K_EX_NODE0_COPIES; assert K_SLICE_LUTX / K_EX_NODE0[0] == K_EX_NODE0_COPIES_BELS; } test node0_used_one_position_per_lut_cell { assert K_EX_NODE0_LUT6_POSITIONS == K_EX_NODE0[0]; assert K_EX_NODE0_YOSYS_LUT < K_EX_NODE0[0]; } test tnf16_fits_160_times_and_luts_bind { assert K_LUT_POSITIONS / K_EX_TNF16[0] == K_EX_TNF16_COPIES; assert K_EX_TNF16_COPIES * K_EX_TNF16[0] + K_EX_TNF16[0] > K_LUT_POSITIONS; assert K_SLICE_FFX / K_EX_TNF16[1] > K_EX_TNF16_COPIES; assert K_SLICE_LUTX / K_EX_TNF16[0] == K_EX_TNF16_COPIES_BELS; assert K_EX_TNF16_YOSYS_LUT < K_EX_TNF16[0]; } test the_page_reads_no_file_and_sends_nothing { assert READS_LOCAL_FILES == false; assert SENDS_NOTHING == true; assert CATEGORY == "fpga"; assert ID == "fits-on"; }