FPGA source code for a PMBus master on Xilinx KC705

This post was written by eli on January 19, 2018
Posted Under: FPGA

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):

Screenshot of Xillybus IP Core parameters

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.

Add a Comment

required, use real name
required, will not be published
optional, your blog address