Example C sources for controlling a power supply with PMBus

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

Overview

This post is best read after another post of mine, “Controlling the power supplies on a Xilinx KC705 FPGA board with PMBus“. This C code assumes that the FPGA is loaded with the design published in this post.

These are the sources of utilities I wrote to fetch parameters and update the VADJ voltage on a Xilinx KC705 FPGA board. It may be useful as a base for other UDC92xx devices or PMBus-controlled devices in general. Just be sure you know what you’re doing. Which brings me to…

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.

I ran this on a Linux PC host, hence little Endian, to which the KC705 was attached with Xillybus over PCIe. The Xillybus over PCIe part allowed controlling the PMBus with a little piece of logic (given on this post). It’s worth to mention that Xillybus is a major overkill for this tiny data exchange, but given that the PCIe link is already there, it’s a convenient choice.

The pmbus_read() and pmbus_write() in pmbus.c below should be adapted if any other solution for accessing the hardware is chosen. I suppose Linux’ native I2C driver would work, even though I haven’t tried it.

The interface with the PMBus logic in the FPGA through /dev/xillybus_pmbus is in principle as follows:

  • Opening /dev/xillybus_pmbus for write issues a START condition. Closing it while no file handle has /dev/xillybus_pmbus opened for read issues a STOP. But if /dev/xillybus_pmbus is opened for read by some file handle while the write-open filehandle closes, and then this device file is opened for write again, the second open() causes a RESTART condition.
  • Each byte written to the write filehandle causes a byte written on the PMBus. If the first byte of a file indicated a read bus address, all following bytes are turned into 0xff  on the physical wires by the logic (i.e. high-Z), regardless of the sent data, so that the bus slave’s response can be sensed.
  • For each byte that was forced into 0xff due to a read bus address, a byte is available for reception on the read filehandle, if such is open.

The PMBus logic on the FPGA side is something I’ve hacked along for different needs, so it doesn’t cover corner cases well, however it works well for its purpose. See the notes in the this post.

Now to the utilities.

pmbus.c: The common library function

#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>

#include "pmbus.h"

// Note that crc8 can be pre-calculated into an array of 256 bytes
static unsigned char crc8(unsigned char x) {
  int i;

  for (i=0; i<8; i++) {
    char toxor = (x & 0x80) ? 0x07 : 0;

    x = x << 1;
    x = x ^ toxor;
  }

  return x;
}

static unsigned char smbus_pec(unsigned char crc, const unsigned char *p,
			       int count) {
  int i;

  for (i = 0; i < count; i++)
    crc = crc8(crc ^ p[i]);
  return crc;
}

void pmbus_write(int addr, int command, int length, unsigned char *data) {
  int fd, rc;

  unsigned char sendbuf[131];
  int sent, tosend;

  if (length > 128)
    exit(1);

  fd = open("/dev/xillybus_pmbus", O_WRONLY);
  if (fd < 0) {
    perror("Failed to open /dev/xillybus_pmbus write-only");
    exit(1);
  }

  sendbuf[0] = addr & 0xfe;
  sendbuf[1] = command;
  memcpy(sendbuf + 2, data, length);
  sendbuf[length + 2] = smbus_pec(0, sendbuf, length+2);

  for (sent = 0, tosend = length + 3; sent < tosend; sent += rc) {
    rc = write(fd, (void *) (sendbuf + sent), tosend - sent);

    if ((rc < 0) && (errno == EINTR))
      continue;

    if (rc < 0)
      perror("pmbus_write() failed to write");

    if (rc == 0)
      fprintf(stderr, "pmbus_write reached write EOF (?!)\n");

    if (rc <= 0) {
      close(fd);
      exit(1);
    }
  }

  close(fd);
}

void pmbus_read(int addr, int command, int length, unsigned char *data) {
  int fdr, fdw, rc, i;

  unsigned char cmdbuf[2] = { addr & 0xfe, command };
  unsigned char dummybuf[130];
  unsigned char crc;

  const unsigned char *sendbuf[2] = { cmdbuf, dummybuf };
  const int tosend[2] = { 2, length + 2 };
  int sent, recvd;

  if (length > 128)
    exit(1);

  memset(dummybuf, 0, sizeof(dummybuf));
  dummybuf[0] = addr | 1;

  fdr = open("/dev/xillybus_pmbus", O_RDONLY);

  if (fdr < 0) {
    perror("Failed to open /dev/xillybus_pmbus read-only");
    exit(1);
  }

  for (i=0; i<2; i++) {
    fdw = open("/dev/xillybus_pmbus", O_WRONLY);
    if (fdw < 0) {
      perror("Failed to open /dev/xillybus_pmbus write-only");
      exit(1);
    }

    for (sent = 0; sent < tosend[i]; sent += rc) {
      rc = write(fdw, (void *) (sendbuf[i] + sent), tosend[i] - sent);

      if ((rc < 0) && (errno == EINTR))
	continue;

      if (rc < 0)
	perror("pmbus_read() failed to write");

      if (rc == 0)
	fprintf(stderr, "pmbus_read reached write EOF (?!)\n");

      if (rc <= 0) {
	close(fdr);
	close(fdw);
	exit(1);
      }
    }

    // Force a restart on the PMBUS by closing fdw. Had fdr been closed as
    // well, we would get a stop condition instead.

    close(fdw);
  }

  crc = smbus_pec(0, sendbuf[0], tosend[0]);
  crc = smbus_pec(crc, sendbuf[1], 1);

  // Collect the data that was read during the dummy write data cycles
  for (recvd = 0; recvd < length + 1; recvd += rc) {
    rc = read(fdr, (void *) (dummybuf + recvd), length + 1 - recvd);

    if ((rc < 0) && (errno == EINTR))
      continue;

    if (rc < 0)
      perror("pmbus_read() failed to read");

    if (rc == 0)
      fprintf(stderr, "pmbus_read reached EOF (?!)\n");

    if (rc <= 0) {
      close(fdr);
      exit(1);
    }
  }

  close(fdr);

  memcpy(data, dummybuf, length);

  crc = smbus_pec(crc, dummybuf, length);

  if (crc != dummybuf[length]) {
    fprintf(stderr, "Failed CRC check: PEC = 0x%02x, received 0x%02x\n",
	    crc, dummybuf[length]);
    exit(1);
  }
}

void set_page(int addr, unsigned char page) {
  unsigned char read_page;

  pmbus_write(addr, 0x00, 1, &page);

  pmbus_read(addr, 0x00, 1, &read_page);

  if (page != read_page) {
    fprintf(stderr, "Failed to set page to %d (read back %d instead)\n",
	    page, read_page);
    exit(1);
  }
}

void get_phase_info(int addr, unsigned char *data) {
  unsigned char readbytes[5];

  pmbus_read(addr, 0xd2, 5, readbytes);

  if (readbytes[0] != 4) {
    fprintf(stderr, "PHASE_INFO responded with block length %d != 4\n",
	    readbytes[0]);
    exit(1);
  }

  memcpy(data, readbytes + 1, 4);
}

void print_dpwms(unsigned char phase_info) {
  const char *dpwm[8] = { "1A", "1B", "2A", "2B", "3A", "3B", "4A", "4B" };
  int i;

  for (i=0; i<8; i++) {
    if (phase_info & 1)
      printf(" %s", dpwm[i]);
    phase_info >>= 1;
  }
}

void clear_faults(int addr) {
  pmbus_write(addr, 0x03, 0, NULL);
}

double linear2voltage(unsigned short word) {
  return word / 4096.0;
}

double linear2nonvoltage(unsigned short word) {
  double x = word & 0x7ff;
  int exponent = (word >> 11) & 0xf;

  if (word & 0x8000)
    x /= 1 << (16 - exponent);
  else
    x *= 1 << exponent;

  return x;
}

pmbus.h: The header file

void pmbus_write(int addr, int command, int length, unsigned char *data);
void pmbus_read(int addr, int command, int length, unsigned char *data);
void set_page(int addr, unsigned char page);
void get_phase_info(int addr, unsigned char *data);
void print_dpwms(unsigned char phase_info);
void clear_faults(int addr);

double linear2voltage(unsigned short word);
double linear2nonvoltage(unsigned short word);

getvals.c: Dump selected parameters

This command-line utility fetches and displays some selected parameters from the power supply controller.

Note that the PMBus address is hardcoded as 52 in main(). It’s multiplied by two for an 8-bit representation.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "pmbus.h"

enum { PM_VOLTAGE, PM_NONVOLTAGE, PM_STATUS } pm_types ;

struct pm_list {
  unsigned char command;
  int size;
  char *name;
  char *units;
  int type;
};

static struct pm_list pm_common[] = {
  { 0x19, 1, "CAPABILITY", "", PM_STATUS },
  { 0x79, 2, "STATUS_WORD", "", PM_STATUS },
  { 0x7e, 1, "STATUS_CML", "", PM_STATUS },
  { 0x8d, 2, "READ_TEMPERATURE_1", "Celsius", PM_NONVOLTAGE },
  { 0x88, 2, "READ_VIN", "V", PM_NONVOLTAGE },
  { 0x89, 2, "READ_IIN", "A", PM_NONVOLTAGE },
  { 0xd3, 2, "VIN_SCALE_MONITOR", "V/V", PM_NONVOLTAGE },
  { }
};

static struct pm_list pm_paged[] = {
  { 0x01, 1, "OPERATION", "", PM_STATUS },
  { 0x02, 1, "ON_OFF_CONFIG", "", PM_STATUS },
  { 0x20, 1, "VOUT_MODE", "", PM_STATUS },
  { 0x7a, 1, "STATUS_VOUT", "", PM_STATUS },
  { 0x7b, 1, "STATUS_IOUT", "", PM_STATUS },
  { 0x7c, 1, "STATUS_INPUT", "", PM_STATUS },
  { 0x7d, 1, "STATUS_TEMPERATURE", "", PM_STATUS },
  { 0x21, 2, "VOUT_COMMAND", "V", PM_VOLTAGE },
  { 0x5e, 2, "POWER_GOOD_ON", "V", PM_VOLTAGE },
  { 0x5f, 2, "POWER_GOOD_OFF", "V", PM_VOLTAGE },
  { 0x24, 2, "VOUT_MAX", "V", PM_VOLTAGE },
  { 0x23, 2, "VOUT_CAL_OFFSET", "V, signed", PM_VOLTAGE },
  { 0x40, 2, "VOUT_OV_FAULT_LIMIT", "V", PM_VOLTAGE },
  { 0x42, 2, "VOUT_OV_WARN_LIMIT", "V", PM_VOLTAGE },
  { 0x44, 2, "VOUT_UV_FAULT_LIMIT", "V", PM_VOLTAGE },
  { 0x43, 2, "VOUT_UV_WARN_LIMIT", "V", PM_VOLTAGE },
  { 0x46, 2, "IOUT_OC_FAULT_LIMIT", "A", PM_NONVOLTAGE },
  { 0x4a, 2, "IOUT_OC_WARN_LIMIT", "A", PM_NONVOLTAGE },
  { 0x4b, 2, "IOUT_UC_FAULT_LIMIT", "A", PM_NONVOLTAGE },  

  { 0x25, 2, "VOUT_MARGIN_HIGH", "V", PM_VOLTAGE },
  { 0x26, 2, "VOUT_MARGIN_LOW", "V", PM_VOLTAGE },
  { 0x27, 2, "VOUT_TRANSITION_RATE", "V/ms", PM_NONVOLTAGE },
  { 0x29, 2, "VOUT_SCALE_LOOP", "V/V", PM_NONVOLTAGE },
  { 0x2a, 2, "VOUT_SCALE_MONITOR", "V/V", PM_NONVOLTAGE },
  { 0x8b, 2, "READ_VOUT", "V", PM_VOLTAGE },
  { 0x8c, 2, "READ_IOUT", "A", PM_NONVOLTAGE },
 { }
};

static void dumpval(int pmbus_addr, struct pm_list *p) {
  unsigned short val;

  pmbus_read(pmbus_addr, p->command, p->size, (void *) &val);

  switch (p->type) {
  case PM_VOLTAGE:
    printf("  %s = %.4f %s (0x%04x)\n", p->name, linear2voltage(val),
	   p->units, val);
    break;

  case PM_NONVOLTAGE:
    printf("  %s = %.4f %s (0x%04x)\n", p->name, linear2nonvoltage(val),
	   p->units, val);
    break;

  default:
    if (p->size == 1)
      printf("  %s = 0x%02x\n", p->name, val & 0xff);
    else
      printf("  %s = 0x%04x\n", p->name, val);
    break;
  }
}

int main(int argc, char *argv[]) {
  int i;
  struct pm_list *p;

  unsigned char phase_info[4];

  int pmbus_addr = 52 * 2;

  printf("Common parameters:\n");

  for (p = pm_common; p->name; p++)
    dumpval(pmbus_addr, p);

  get_phase_info(pmbus_addr, phase_info);

  for (i=0; i<4; i++) {
    if (phase_info[i] == 0)
      continue;

    printf("\nPage %d: Controlling DPWM:", i);
    print_dpwms(phase_info[i]);
    printf("\n");

    set_page(pmbus_addr, i);

    for (p = pm_paged; p->name; p++)
      dumpval(pmbus_addr, p);
  }

  return 0;
}

updatevoltage.c: Change the voltage of a rail

This is the scary part. It’s written so the voltage can be updated to a lower one without turning off the power rail. Note that the new voltage is hardcoded as new_voltage (1.8V). In main(), the PMBus address is hardcoded as 52, and the page to access as 3. Before changing the page to be accessed, be sure to understand how they relate to power rail. Anyhow, the utility says which power rail it’s going to play with, so pay attention to that.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "pmbus.h"
#include <string.h>

static double new_voltage = 1.8;

enum { PM_VOLTAGE, PM_NONVOLTAGE, PM_STATUS } pm_types ;

struct pm_list {
  unsigned char command;
  int size;
  char *name;
  double diversion;
  char *units;
  int type;
};

static struct pm_list pm_paged[] = {
  { 0x25, 2, "VOUT_MARGIN_HIGH", 5.0, "V", PM_VOLTAGE },
  { 0x26, 2, "VOUT_MARGIN_LOW", -5.0, "V", PM_VOLTAGE },
  { 0x5e, 2, "POWER_GOOD_ON", -5.0, "V", PM_VOLTAGE },
  { 0x5f, 2, "POWER_GOOD_OFF", -8.0, "V", PM_VOLTAGE },
  { 0x43, 2, "VOUT_UV_WARN_LIMIT", -10.0, "V", PM_VOLTAGE },
  { 0x44, 2, "VOUT_UV_FAULT_LIMIT", -15.0, "V", PM_VOLTAGE },
  { 0x21, 2, "VOUT_COMMAND", 0.0, "V", PM_VOLTAGE },
  { 0x40, 2, "VOUT_OV_FAULT_LIMIT", 15.0, "V", PM_VOLTAGE },
  { 0x42, 2, "VOUT_OV_WARN_LIMIT", 10.0, "V", PM_VOLTAGE },
 { }
};

static unsigned short new_val(struct pm_list *p) {
  return (unsigned short) ((4096.0 * new_voltage * (1.0 + p->diversion / 100.0)) + 0.5);
}

static void dumpval(int pmbus_addr, struct pm_list *p) {
  unsigned short val;

  pmbus_read(pmbus_addr, p->command, p->size, (void *) &val);

  switch (p->type) {
  case PM_VOLTAGE:
    printf("  %s = %.4f %s -> %.4f %s (0x%04x -> 0x%04x)\n",
	   p->name, linear2voltage(val),
	   p->units, linear2voltage(new_val(p)), p->units,
	   val, new_val(p));
    break;

  case PM_NONVOLTAGE:
    printf("  %s = %.4f %s\n", p->name, linear2nonvoltage(val),
	   p->units);
    break;

  default:
    if (p->size == 1)
      printf("  %s = 0x%02x\n", p->name, val & 0xff);
    else
      printf("  %s = 0x%04x\n", p->name, val);
    break;
  }
}

int main(int argc, char *argv[]) {
  int dry_run = 0;
  char *wrote = "WRITING";

  struct pm_list *p;

  unsigned char phase_info[4];

  int pmbus_addr = 52 * 2;
  int page = 3;
  char yes[10];

  get_phase_info(pmbus_addr, phase_info);

  if (phase_info[page] == 0) {
    fprintf(stderr, "Requested page %d is not active on device\n", page);
    return 1;
  }
  printf("\nPage %d: Controlling DPWM:", page);
  print_dpwms(phase_info[page]);
  printf("\n");

  set_page(pmbus_addr, page);

  for (p = pm_paged; p->name; p++)
    dumpval(pmbus_addr, p);

  printf("\nUpdate these new values? (Type uppercase YES): ");
  fgets(yes, sizeof(yes), stdin);

  if (strcmp(yes, "YES\n")) {
    printf("\nDidn't get YES. Therefore dry-running.\n");
    dry_run = 1;
    wrote = "Would write";
  }

  for (p = pm_paged; p->name; p++) {
    unsigned short v = new_val(p);
    printf("%s value 0x%04x (%.4f V) to %s (command 0x%02x)\n",
	   wrote, v, linear2voltage(v), p->name, p->command);

    if (!dry_run) {
      unsigned short r;
      pmbus_write(pmbus_addr, p->command, 2, (void *) &v);
      pmbus_read(pmbus_addr, p->command, 2, (void *) &r);

      if (v != r) {
	fprintf(stderr, "Readback failed! Wrote 0x%04x, read back 0x%04x\n",
		v, r);
	exit(1);
      }
    }
  }

  if (dry_run)
    return 0;

  printf("\nCheck the voltage now.\n\nStore current settings to non-volatile memory? (Type uppercase STORE): ");
  fgets(yes, sizeof(yes), stdin);

  if (strcmp(yes, "STORE\n")) {
    printf("OK, did nothing.\n");
  } else {
    printf("Setting page back to 0\n");
    set_page(pmbus_addr, 0);

    printf("SENDING a STORE_DEFAULT_ALL command\n");
    pmbus_write(pmbus_addr, 0x11, 0, NULL);
    printf("Done.\n");
  }

  return 0;
}

The Makefile

If we’re at it:

CC=	gcc
ALL=	getvals updatevoltage
OBJECTS = pmbus.o
HEADERFILES = pmbus.h
LIBFLAGS=
FLAGS=	-Wall -O3 -g

all:	$(ALL)

clean:
	rm -f *.o $(ALL)
	rm -f `find . -name "*~"`

%.o:	%.c $(HEADERFILES)
	$(CC) -c $(FLAGS) -o $@ $<

$(ALL) : %: %.o Makefile $(OBJECTS)
	$(CC) $< $(OBJECTS) -o $@ $(LIBFLAGS)

gcc and GNU Make need to be installed, of course.

Add a Comment

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