FPGA source code for a PMBus master on Xilinx KC705
Introduction
This post is best read after another post of mine, “Controlling the power supplies on a Xilinx KC705 FPGA board with PMBus“. C utilities for using the design outlined below can be found in another post of mine.
These are the sources for allowing a computer to monitor and control the power supplies of an Xilinx KC705 FPGA board (for Kintex-7) through the PMBus wires attached to the FPGA. This solution is based upon Xillybus, which is a somewhat ridiculous overkill for this task.
The base project is the demo bundle for KC705, which can be downloaded from Xillybus’ website. Even though it works with Linux as well as Windows, the utility sources in the other post are written for Linux. It’s not really difficult to port them to Windows (pay attention to open the files as binary and change the path to the device files) or use them using Cygwin.
Warning: Issuing the wrong command to a power supply controller can destroy an FPGA board in a split second. I take no responsibility for any damage, even if something I’ve written is misleading or outright wrong. It’s YOUR full responsibility to double-check any of your actions.
The following changes are made on the demo bundle (all detailed below):
- Replace the Xillybus IP core with a custom IP core generated in Xillybus’ IP Core Factory.
- Add pmbus_if.v source to the project
- Change xillydemo.v to utilize the updated Xillybus IP core and instantiate pmbus_if.v + expose the pmbus_* ports
- Add pin placement constraints to the XDC file
Setting up a Xillybus custom IP core
It’s recommended to make yourself acquainted with the Xillybus concept in general. The Getting Started guide for Xilinx available at the Documentation section may come handy for this purpose.
Download the bundle for KC705 from the website, and build the project in Vivado: Execute verilog/xillydemo-vivado.tcl in the demo bundle from within Vivado to set up the project.
Then enter Xillybus’ IP Core Factory, set up, generate and download a custom IP core for Kintex-7 with attributes as shown in the screenshot below (click to enlarge):
The name of the device file must be accurate, as well as the other parameters, but the expected BW doesn’t have to.
Once the custom IP core zip file is downloaded, replace the project’s core/xillybus_core.ngc, verilog/src/xillybus.v and verilog/src/xillybus_core.v with those extracted from the zip file.
pmbus_if.v
pmbus_if.v (listed below) implements the logic that handles the PMBus interface. It’s a bit of a hack that I’ve been tweaking for different quick tasks for years, without ever trying to get it written properly. As such, it’s not covering corner cases well, and might not work with other I2C-bus like interfaces, or even with devices other than the one it’s intended for — TI’s UCD92xx series. In particular it has the following known issues:
- A NAK from the device is ignored if it relates to the last byte of a write transaction.
- Cycle extension by virtue of holding the clock signal by the slave is not supported — the master just goes on, ignoring this.
These issues are quite minor, in particular during proper operation.
The clk_freq parameter is set to 1000 MHz, which doesn’t reflect the actual frequency, 250 MHz. The reason is that even though it should have worked with clk_freq = 250 according to the spec, the device’s firmware appears not be quick enough to handle read requests arriving at higher rates, resulting in all-0xff responses occasionally.
Listing follows:
`timescale 1ns / 10ps module pmbus_if (bus_clk, quiesce, user_w_pmbus_wren, user_w_pmbus_data, user_w_pmbus_full, user_w_pmbus_open, user_r_pmbus_rden, user_r_pmbus_data, user_r_pmbus_empty, user_r_pmbus_eof, user_r_pmbus_open, pmbus_clk, pmbus_data); input bus_clk; input quiesce; input user_w_pmbus_wren; input [7:0] user_w_pmbus_data; input user_w_pmbus_open; input user_r_pmbus_rden; input user_r_pmbus_open; output user_w_pmbus_full; output [7:0] user_r_pmbus_data; output user_r_pmbus_empty; output user_r_pmbus_eof; output pmbus_clk; inout pmbus_data; reg [15:0] div_counter; reg sclk_logic; reg sdata_logic; reg sdata_sample; reg pmbus_en; reg pre_en; reg [3:0] state; reg first; reg dir_write; reg save_direction; reg [7:0] write_byte; reg [7:0] read_byte; reg [2:0] idx; reg write_byte_valid; reg fifo_wr_en; reg open_d; reg stop_pending; reg stop_deferred; reg do_restart; parameter clk_freq = 1000; // In MHz, nearest integer parameter st_idle = 0, st_start = 1, st_fetch = 2, st_bit0 = 3, st_bit1 = 4, st_bit2 = 5, st_ack0 = 6, st_ack1 = 7, st_ack2 = 8, st_stop0 = 9, st_stop1 = 10, st_stop2 = 11, // Represented by "default" st_startstop = 12; assign user_r_pmbus_eof = 0; // Emulated open collector output // Note that sclk and sdata must be pulled up, possibly with // a PULLUP constraint on the IOB (or a 10 kOhm ext. resistor) assign pmbus_clk = sclk_logic ? 1'bz : 1'b0 ; assign pmbus_data = sdata_logic ? 1'bz : 1'b0 ; assign user_w_pmbus_full = write_byte_valid || stop_pending; // pmbus_en should be high every 10 us // This allows a huge multicycle path constraint on pmbus_en // A stop condition is presented on the bus when // * in a write access, the write stream closes, and the read stream // is already closed, or // * in a read access, the write stream closes // * a stop condition was prevented previously by an open read stream, // and the read stream closes (in which case a start-stop is presented). always @(posedge bus_clk) begin pmbus_en <= pre_en; sdata_sample <= pmbus_data; fifo_wr_en <= pmbus_en && (state == st_ack0) && !dir_write; open_d <= user_w_pmbus_open; if (open_d && !user_w_pmbus_open) begin stop_pending <= 1; do_restart <= user_r_pmbus_open; end if (user_w_pmbus_wren) begin write_byte <= user_w_pmbus_data; write_byte_valid <= 1; // Zeroed by state machine end if (div_counter == ((clk_freq * 10) - 1)) begin div_counter <= 0; pre_en <= 1; end else begin div_counter <= div_counter + 1; pre_en <= 0; end // State machine if (pmbus_en) case (state) st_idle: begin sclk_logic <= 1; sdata_logic <= 1; stop_pending <= 0; if (write_byte_valid) state <= st_start; // st_startstop is invoked only if the stream for reading data // was open during the write session, which indicates that the // next cycle will be a read session. This prevented a // stop condition, so a restart can takes place. But then // this stream closed suddenly without this read session, // so a dirty stop condition needs to be inserted. if (stop_deferred && !user_r_pmbus_open) state <= st_startstop; end st_start: begin sdata_logic <= 0; // Start condition first <= 1; dir_write <= 1; stop_deferred <= 0; state <= st_fetch; end st_fetch: begin sclk_logic <= 0; idx <= 7; state <= st_bit0; end st_bit0: begin if (dir_write) sdata_logic <= write_byte[idx]; else sdata_logic <= 1; // Keep clear when reading state <= st_bit1; end st_bit1: begin sclk_logic <= 1; read_byte[idx] <= sdata_sample; state <= st_bit2; end st_bit2: begin sclk_logic <= 0; idx <= idx - 1; if (idx != 0) state <= st_bit0; else state <= st_ack0; end st_ack0: // Don't handle the last ACK cycle until the outcome is known. // This allows a NAK on the last received byte, and also ensures // a stop condition at the end of a write cycle (and not a // restart if the file was reopened by host for the next cycle). if (write_byte_valid || stop_pending) begin if (dir_write) sdata_logic <= 1; // The slave should ack else sdata_logic <= stop_pending; // We ack on read save_direction <= !write_byte[0]; state <= st_ack1; end st_ack1: if (!dir_write || !sdata_sample || stop_pending) begin state <= st_ack2; // Read mode or slave acked. Or Quit. write_byte_valid <= 0; end st_ack2: begin sclk_logic <= 1; if (write_byte_valid) begin if (first) dir_write <= save_direction; first <= 0; state <= st_fetch; end else if (stop_pending) state <= st_stop0; end // The three stop states toggle the clock once, so that // we're sure that the slave has released the bus, leaving // its acknowledge state. Used only in write direction. st_stop0: begin sclk_logic <= 0; state <= st_stop1; end st_stop1: begin if (do_restart && dir_write) begin sdata_logic <= 1; // Avoid stop condition stop_deferred <= 1; end else begin sdata_logic <= 0; end state <= st_stop2; end st_startstop: begin sdata_logic <= 0; stop_deferred <= 0; state <= st_idle; end default: // Normally this is st_stop2 begin sclk_logic <= 1; write_byte_valid <= 0; // st_idle will raise sdata to '1', making a stop condition // unless sdata_logic was driven low in st_stop1 state <= st_idle; end endcase if (quiesce) // Override all above. begin state <= st_idle; stop_pending <= 0; write_byte_valid <= 0; stop_deferred <= 0; end end fifo_8x2048 fifo ( .clk(bus_clk), .srst(!user_r_pmbus_open), .din(read_byte), .wr_en(fifo_wr_en), .rd_en(user_r_pmbus_rden), .dout(user_r_pmbus_data), .full(), .empty(user_r_pmbus_empty)); endmodule
xillydemo.v
The file should be changed to this:
module xillydemo ( input PCIE_PERST_B_LS, input PCIE_REFCLK_N, input PCIE_REFCLK_P, input [7:0] PCIE_RX_N, input [7:0] PCIE_RX_P, output [3:0] GPIO_LED, output pmbus_clk, inout pmbus_data, output [7:0] PCIE_TX_N, output [7:0] PCIE_TX_P ); // Clock and quiesce wire bus_clk; wire quiesce; // Wires related to /dev/xillybus_pmbus wire user_r_pmbus_rden; wire user_r_pmbus_empty; wire [7:0] user_r_pmbus_data; wire user_r_pmbus_eof; wire user_r_pmbus_open; wire user_w_pmbus_wren; wire user_w_pmbus_full; wire [7:0] user_w_pmbus_data; wire user_w_pmbus_open; xillybus xillybus_ins ( // Ports related to /dev/xillybus_pmbus // FPGA to CPU signals: .user_r_pmbus_rden(user_r_pmbus_rden), .user_r_pmbus_empty(user_r_pmbus_empty), .user_r_pmbus_data(user_r_pmbus_data), .user_r_pmbus_eof(user_r_pmbus_eof), .user_r_pmbus_open(user_r_pmbus_open), // CPU to FPGA signals: .user_w_pmbus_wren(user_w_pmbus_wren), .user_w_pmbus_full(user_w_pmbus_full), .user_w_pmbus_data(user_w_pmbus_data), .user_w_pmbus_open(user_w_pmbus_open), // Signals to top level .PCIE_PERST_B_LS(PCIE_PERST_B_LS), .PCIE_REFCLK_N(PCIE_REFCLK_N), .PCIE_REFCLK_P(PCIE_REFCLK_P), .PCIE_RX_N(PCIE_RX_N), .PCIE_RX_P(PCIE_RX_P), .GPIO_LED(GPIO_LED), .PCIE_TX_N(PCIE_TX_N), .PCIE_TX_P(PCIE_TX_P), .bus_clk(bus_clk), .quiesce(quiesce) ); pmbus_if pmbus_if_ins(.bus_clk(bus_clk), .quiesce(quiesce), .user_w_pmbus_wren(user_w_pmbus_wren), .user_w_pmbus_data(user_w_pmbus_data), .user_w_pmbus_full(user_w_pmbus_full), .user_w_pmbus_open(user_w_pmbus_open), .user_r_pmbus_rden(user_r_pmbus_rden), .user_r_pmbus_data(user_r_pmbus_data), .user_r_pmbus_empty(user_r_pmbus_empty), .user_r_pmbus_eof(user_r_pmbus_eof), .user_r_pmbus_open(user_r_pmbus_open), .pmbus_clk(pmbus_clk), .pmbus_data(pmbus_data) ); endmodule
Adding pin placement constraints
The following lines should be appended at the end of vivado-essentials/xillydemo.xdc:
set_property PACKAGE_PIN Y14 [get_ports pmbus_data] set_property IOSTANDARD LVCMOS15 [get_ports pmbus_data] set_property PACKAGE_PIN AG17 [get_ports pmbus_clk] set_property IOSTANDARD LVCMOS15 [get_ports pmbus_clk]
Building the project
With the project set up as outlined above, generate the bitstream as usual.