I2C and the Pi Pico - Part 2 (HUSB238)

Introduction

In Part 1, the basic communication mechanism used on the I2C bus was explained. In Part 2, we'll look at using the Raspberry Pi Pico with an I2C device, the Adafruit USB Type C Power Delivery Dummy Breakout

The USB Power Delivery (PD) Protocol allows USB devices and power sources to negotiate over the power supply parameters - voltage and current. There are various version of PD and there are other methods of negotiating power (in particular, current) that have been used in the past for "fast charging". However, for the sake of simplicity, we'll focus on PD 3.0 which, in principle, allows devices to change the default 5V power supply to 9V, 12V, 15V, 18V or 20V at various maximum currents. Power supplies don't have to offer all of these choices (or indeed any other than the default 5V), nor do they have to support the maximum current ratings defined in the specification. However, there is a mechanism by which the device can determine what the power supply is able to deliver and then request the power profile that best suits its needs.

The Adafruit USB Type C Power Delivery Dummy Breakout is a small board that contains a Hynetek HUSB238 chip that understands PD, a USB C socket to connect to a power supply and a power output connector. It can be hardwired (using jumpers on the board) to request a specific output voltage or current from the supply and deliver it to the connector, but also has an I2C interface to allow a microcontroller to program the output requirements. 

Connecting the HUSB238

The HUSB238 receives its power from the power supply to which it is connected - at whatever voltage the power supply is currently providing and so there's no power connection between the device and the Pi Pico, though a ground connection is of course required. The open-drain SCL and SDA lines need to be connected to the relevant GPIO pins and pulled up to the Pico's Vcc rail. For this example, we will use the internal pull-up resistors on the Pi Pico. This should be sufficient, given we will only operate the bus at 100kHz, but in the event of problems, adding discrete resistors may help.

HUSB238 Breakout Board
HUSB238 Breakout Board
 

A PD capable power supply needs to be connected, of course. If a battery powerbank is connected, it may well turn off in the absence of any load on the output so you may need to create a dummy load, however be careful how you do so: it's easy to pick a resistor that's inadequately rated for the current the power supply can deliver and that in turn can lead to burns or smoke.

In the program below, SDA and SCL are connected to GPIO pins 4 and 5 respectively (header pins 6 and 7 on the original Pi Pico), implying the use of I2C hardware controller port 0,

Controlling the HUSB238 via I2C

The bus address of the HUSB238 is fixed at 0x08. Its interface is modeled as a set of 10 separate 8-bit register locations addressed from 0x00 to 0x0a. To write one of the registers, the controller must write two bytes to the HUSB238 bus address: the first is the address of the internal register to be written and the second is the data to be written to that register. Similarly, to read a register the controller must first write the address of the register required and then read the data: in this case, the controller must use the repeated START mechanism to chain together the operations in one bus transaction. Note that the HUSB238 can only read or write one byte of data per bus transaction. 

Details of the registers available via I2C can be found here. In summary they consist of:

  • Two status bytes (PD_STATUS0 and PD_STATUS1) (0x00 and 0x01)
  • Six capability registers indicating available power options (0x02-0x07)
  • An option register to request a specific power option (0x08)
  • A command register (0x09)

PD_STATUS0 is set when the HUSB238 has negotiated a specific power request with the power supply. The most significant 4 bits contain the coded value of the agreed voltage (0x01 = 5V; 0x02 = 9V; 0x03 = 12V; 0x04 = 15V; 0x05 = 18V; 0x06 = 20V). The bottom 4 bits contain a code to indicate the maximum current (from 0.5A to 5A). 

PD_STATUS1 indicates the status of the communication between the HUSB238 and the power supply. Bit 6 (ATTACH) indicates whether the Configuration Channel is connected - this essentially indicates that there is a USB Type C connection at the power supply as well as at the HUSB238 so that PD negotiation is possible. If Bit 6 is set, Bit 7 (CC_DIR) reports the Type C cable orientation. Bits [5:3] (PD_RESPONSE) report the latest response from the power supply to PD protocol requests. The remaining bits refer to 5V power in the absence of PD. 

Each of the capability registers corresponds to one of the possible voltages defined by the PD specification. The most-significant bit is set if the power supply is capable of delivering that voltage, in which case the bottom 4 bits contain the maximum available current encoded in the same way as for PD_STATUS0. These registers are set when the HUSB238 asks the power supply to confirm the voltages and currents it is capable of delivering.

The top 4 bits of the option register (SRC_PDO) are used to request a specific voltage from the power supply. Note that these are encoded differently from the values in PD_STATUS0 (0x01 = 5V; 0x02 = 9V; 0x03 = 12V; 0x08 = 15V; 0x09 = 18V; 0x0a= 20V).

The command register (GO_COMMAND) initiates a communication with the power supply, with the specific command encoded in the lower 5 bits. Writing 0x01 causes a request for the power configuration defined in the SRC_PDO register; writing 0x04 requests refreshing the source capabilities; writing 0x10 causes a reset. 

There's no asynchronous notification - no interrupt lines or similar - so the only way to be aware of the power supply being disconnected (for example) is by periodically polling the status. 

Example Program

The following Pi Pico program reports the contents of each of the HUSB238 registers and repeatedly attempts to select each of the possible output voltages in turn, reporting the register contents again afterwards. 

In lines 8-11 we define the various device details - the I2C hardware port, the GPIO pins and the bus address of the device. Lines 12 and 13 define the register addresses of SRC_PDO and GO_COMMAND.

In lines 15 and 16 we define constants for the total number of registers and the number of possible voltage settings.

At line 18, we define a buffer we will use for send and receiving data from the device.

Line 20 defines an array voltages whose elements are the selectable voltages and line 21 defines an array voltage_codes which contains the equivalent codes used in SRC_PDO to request the voltage whose index corresponds to the value at the same index in voltages.

The function readregs in line 23 attempts to read the present value of every register on the device. Using each register address in turn, it stores the register address in i2c_reg, writes that value using i2c_write_blocking with the nostop parameter set to true (so that the following operation will continue as part of the same transaction) and then calls i2c_read_blocking (this time with nostop set to false to terminate the bus tranaction) to retrieve the register value which it stores in the next location in i2c_buffer.

The function printregs at line 35 prints out the salient contents of the register values stored in i2c_buffer, manipulating the various bit fields where necessary. 

The main program initialises the I2c port at line 94 and configures the GPIO pins (lines 96-99). As previously discussed, using the GPIO pull-up resistors is not generally recommended, but we should be able to get away with it.

The program then read (line 104) and prints (line 106) the initial value of the device registers. The HUSB238 should have by this stage communicated with the power supply to work out its capabilities and, in the absence of any specific request, should be supplying 5V.

At line 108, the program begins a loop, selecting all the possible PD voltages in turn. Firstly, it sets the SRC_PDO register to the appropriate code (lines 110-112) and then writes the command code to change the selected voltage (1) to the GO_COMMAND register. There isn't any defined means (apart from polling) to know if or when the command is complete, so we simply wait 1s (line 118) and then read and print the device registers.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/i2c.h"

// I2C defines
// This example will use I2C0 on GPIO4(SDA) and GPIO5 (SCL) running at 100KHz.

#define I2C_PORT i2c0
#define I2C_SDA 4
#define I2C_SCL 5
#define I2C_DEVICE 0x08
#define REG_SRC_PDO 0x08
#define REG_GO 0x09

#define NUMREGS 10
#define NUMVOLTS 6

static uint8_t i2c_buffer[NUMREGS];

static uint8_t voltages[NUMVOLTS] = {5, 9, 12, 15, 18, 20};
static uint8_t voltage_codes[NUMVOLTS] = {0x10, 0x20, 0x30, 0x80, 0x90, 0xa0};

void readregs()
{
    uint8_t i2c_reg[1];

    for (int r = 0; r < NUMREGS; r++)
        {
            i2c_reg[0] = r;
            i2c_write_blocking(I2C_PORT, I2C_DEVICE, &i2c_reg[0], 1, true); 
            i2c_read_blocking(I2C_PORT, I2C_DEVICE, &i2c_buffer[r], 1, false);
        }
}

void printregs (char * prefix)
{
    const uint8_t vmask = 0x70;
    const uint8_t amask = 0x0f;
    const uint8_t CCmask = 0X80;
    const uint8_t ATmask = 0x40;
    const uint8_t RSPmask = 0x38;
    const uint8_t RSPshift = 3;
    const uint8_t vshift = 4;
    const uint8_t V5mask = 0x04;
    const uint8_t A5mask = 0x03;

    int v = i2c_buffer[0] & vmask;
    int a = i2c_buffer[0] & amask;

    v = v >> vshift;

    
    printf("%s/Register contents: %02x:%02x:%02x (VoltCode=%1x, AmpCode=%1x;) ",  prefix, i2c_buffer[0], i2c_buffer[1], i2c_buffer[8], v, a);
    if (i2c_buffer[1] & CCmask)
        printf("CC1; ");
    else
        printf("CC2; ");

    if (i2c_buffer[1] & ATmask)
        printf("Attached; ");
    else
        printf("Unattached; ");

    int r = i2c_buffer[1] & RSPmask;
    r = r >> RSPshift;

    printf ("Response=%1x; ", r);

    if (i2c_buffer[1] & V5mask)
        printf ("5V; AmpCode=%1x\n", i2c_buffer[1] & A5mask);
    else

        printf ("No 5V\n");

    for (int i = 2 ; i < NUMREGS -2 ; i++)
    {
        printf("  %dV; ", voltages[i-2]);
        if (i2c_buffer[i] & 0x80)
            printf("Detected, AmpCode=%2x. ", i2c_buffer[i] & 0x0f);
        else
            printf("Not detected. ");
    }

    printf ("\n");
}

int main()
{
    stdio_init_all();
    int v = 0;


    // I2C Initialisation. Using it at 100Khz.
    i2c_init(I2C_PORT, 100*1000);
    
    gpio_set_function(I2C_SDA, GPIO_FUNC_I2C);
    gpio_set_function(I2C_SCL, GPIO_FUNC_I2C);
    gpio_pull_up(I2C_SDA);
    gpio_pull_up(I2C_SCL);

    sleep_ms(20000);
    printf ("Starting\n");

    readregs();

    printregs ("@@@@@@@@@@");
    
    while (true) {
        printf("Setting voltage to: %dV (%2x)\n", voltages[v], voltage_codes[v]);
        i2c_buffer[0] = REG_SRC_PDO;
        i2c_buffer[1] = voltage_codes[v];
        i2c_write_blocking(I2C_PORT, I2C_DEVICE, &i2c_buffer[0], 2, false);

        i2c_buffer[0] = REG_GO;
        i2c_buffer[1] = 1;
        i2c_write_blocking(I2C_PORT, I2C_DEVICE, &i2c_buffer[0], 2, false);

        sleep_ms (1000);

        readregs();
        printregs ("----------");

        v++;
        if (v >= NUMVOLTS)
            v = 0;

        sleep_ms(5000);
    }
}

The output produced by the program will of course vary depending on the capabilities of the power supply. The following output comes with the HUSB238 attached to a power banks offering 5V, 9V and 12V. We can see in this case that the availability of 15V, 18V and 20V was not detected. However, we can also see that something is misbehaving: when we set the voltage to 5V, PD_STATUS0 is reported as 0x17 (5V @ 2.25A) but when we set it to 9V, PD_STATUS0 is reported as 0x13 (5V @ 1.25A) - even though a multimeter confirms 9V on the output terminals. When we set the voltage to 12V, PD_STATUS0 is reported as 0x34 (12V @ 1.5A), again as expected. And we get differing values (and response codes in PD_STATUS1) for 15V, 18V and 20V, none of which the powerbank supports.

  Setting voltage to: 5V (10)
----------/Register contents: 17:4f:10 (VoltCode=1, AmpCode=7;) CC2; Attached; Response=1; 5V; AmpCode=3
  5V; Detected, AmpCode= 7.   9V; Detected, AmpCode= 6.   12V; Detected, AmpCode= 4.   15V; Not detected.   18V; Not detected.   20V; Not detected.
Setting voltage to: 9V (20)
----------/Register contents: 13:4f:20 (VoltCode=1, AmpCode=3;) CC2; Attached; Response=1; 5V; AmpCode=3
  5V; Detected, AmpCode= a.   9V; Detected, AmpCode= 6.   12V; Detected, AmpCode= 4.   15V; Not detected.   18V; Not detected.   20V; Not detected.
Setting voltage to: 12V (30)
----------/Register contents: 34:4f:30 (VoltCode=3, AmpCode=4;) CC2; Attached; Response=1; 5V; AmpCode=3
  5V; Detected, AmpCode= 7.   9V; Detected, AmpCode= 6.   12V; Detected, AmpCode= 4.   15V; Not detected.   18V; Not detected.   20V; Not detected.
Setting voltage to: 15V (80)
----------/Register contents: 13:4f:80 (VoltCode=1, AmpCode=3;) CC2; Attached; Response=1; 5V; AmpCode=3
  5V; Detected, AmpCode= a.   9V; Detected, AmpCode= 6.   12V; Detected, AmpCode= 4.   15V; Not detected.   18V; Not detected.   20V; Not detected.
Setting voltage to: 18V (90)
----------/Register contents: 09:01:80 (VoltCode=0, AmpCode=9;) CC2; Unattached; Response=0; No 5V
  5V; Detected, AmpCode= a.   9V; Detected, AmpCode= 6.   12V; Detected, AmpCode= 4.   15V; Not detected.   18V; Not detected.   20V; Not detected.
Setting voltage to: 20V (a0)
----------/Register contents: 13:4f:a0 (VoltCode=1, AmpCode=3;) CC2; Attached; Response=1; 5V; AmpCode=3
  5V; Detected, AmpCode= 7.   9V; Detected, AmpCode= 6.   12V; Detected, AmpCode= 4.   15V; Not detected.   18V; Not detected.   20V; Not detected.

If we try again with a laptop power supply that offers only 5V and 20V we get the output shown below. In this case, PD_STATUS0 contains 0x1a (5V at 3A) for all requested voltages apart from 20V, when it contains 0x64 (20V at 1.5A). The power supply is actually capable of generating a higher current at 20V, so something equally strange is going on here.

Setting voltage to: 5V (10)
----------/Register contents: 1a:cf:10 (VoltCode=1, AmpCode=a;) CC1; Attached; Response=1; 5V; AmpCode=3
  5V; Detected, AmpCode= a.   9V; Not detected.   12V; Not detected.   15V; Not detected.   18V; Not detected.   20V; Detected, AmpCode= 4. 
Setting voltage to: 9V (20)
----------/Register contents: 1a:cf:20 (VoltCode=1, AmpCode=a;) CC1; Attached; Response=1; 5V; AmpCode=3
  5V; Detected, AmpCode= a.   9V; Not detected.   12V; Not detected.   15V; Not detected.   18V; Not detected.   20V; Detected, AmpCode= 4. 
Setting voltage to: 12V (30)
----------/Register contents: 1a:cf:30 (VoltCode=1, AmpCode=a;) CC1; Attached; Response=1; 5V; AmpCode=3
  5V; Detected, AmpCode= a.   9V; Not detected.   12V; Not detected.   15V; Not detected.   18V; Not detected.   20V; Detected, AmpCode= 4. 
Setting voltage to: 15V (80)
----------/Register contents: 1a:cf:80 (VoltCode=1, AmpCode=a;) CC1; Attached; Response=1; 5V; AmpCode=3
  5V; Detected, AmpCode= a.   9V; Not detected.   12V; Not detected.   15V; Not detected.   18V; Not detected.   20V; Detected, AmpCode= 4. 
Setting voltage to: 18V (90)
----------/Register contents: 1a:cf:90 (VoltCode=1, AmpCode=a;) CC1; Attached; Response=1; 5V; AmpCode=3
  5V; Detected, AmpCode= a.   9V; Not detected.   12V; Not detected.   15V; Not detected.   18V; Not detected.   20V; Detected, AmpCode= 4. 
Setting voltage to: 20V (a0)
----------/Register contents: 64:cf:a0 (VoltCode=6, AmpCode=4;) CC1; Attached; Response=1; 5V; AmpCode=3
  5V; Detected, AmpCode= a.   9V; Not detected.   12V; Not detected.   15V; Not detected.   18V; Not detected.   20V; Detected, AmpCode= 4. 

Summary

We've seen how to use the Pi Pico's SDK functions to communicate with a device via I2C and the example should be easily extensible to other types of hardware. Although it is possible to use DMA with the I2C hardware, in many circumstances there'll be little point as the transfers are relatively short. For devices supporting multibyte transfers, there may be a case to be made. 

Equally, we can see that USB C Power Delivery is quite complicated and unpredictable results are likely to occur with real world power supplies (and indeed cables, since they come in many variants). 


Comments