Skip to content

Modify the firmware

Manuel Bl edited this page Feb 24, 2023 · 4 revisions

Since this is an open-source project, it can be modified. One of the use cases is to change what the ZY12PD does at startup, how it reacts to button presses and how it uses the LED. So the USB PD communincation is reused, but the "user interface" is changed.

To do that, copy or clone the entire project and modify main.cpp.

Minimal example

Below is a minimal example. It selects a fixed voltage (9V) once the power source announces its capabilities. When the desired voltage is active, the LED is green. Otherwise, it's red.

This illustrates the minimal setup that needs to be in place:

  • The hardware abstraction layer (HAL) and the power sink need to be initialized.
  • An event callback function must be registered.
  • When the source announces its capabilities (supported voltages and currents), a voltage must be requested almost immediately.
  • The poll() functions of the hardware abstraction layer and the power sink must be call frequently so they can handle the events they are responsible for.

main.cpp

#include "pd_sink.h"

using namespace usb_pd;

// voltage in mV
const int desired_voltage = 9000;

mcu_hal usb_pd::hal;
pd_sink power_sink;


// called when the USB PD controller triggers an event
void sink_callback(callback_event event) {

    if (event == callback_event::source_caps_changed) {
        // source has announced its capabilities; sink must request voltage
        int res = power_sink.request_power(desired_voltage);

        if (res == -1) {
            // desired voltage is not available, request 5V instead
            power_sink.request_power(5000);
        }
    }
}

int main() {
    // initialize the board
    hal.init();
    hal.set_led(color::red);

    // initialize PD power sink
    power_sink.set_event_callback(sink_callback);
    power_sink.init();

    // loop and poll
    while (true) {
        hal.poll();
        power_sink.poll();

        // set LED: green for desired voltage, red otherwise
        hal.set_led(power_sink.active_voltage == desired_voltage
            ? color::green : color::red);
    }
}

Working with programmable power supply

This more advanced example is for power supply with PPS capability: They allow to select any voltage is a given range. The protocol supports a resolution of 10mV.

When the source announces its capabilities, this example will locate the PPS capability. The capability describes the minimum voltage (min_voltage) and the maximum voltage (voltage).

Initially, the lowest voltage will be requested. Every press of the button increases the voltage by 100mV. Once the maximum voltage has been exceed, it starts over at the lowest voltage.

The power supply might be able to deliver down to 3.3V. However, this is not sufficient for the LDO on the ZY12PDN board. It will cut out between 3.5V and 3.9V. So the minimum voltage is at least 4V.

#include "pd_sink.h"
#include <algorithm>

using namespace usb_pd;

// PPS might be able to deliver less than 5V, but
// below 4V, the LDO on the board might cut out.
constexpr int cutout_voltage = 4000;

mcu_hal usb_pd::hal;
pd_sink power_sink;

int voltage = 5000;
const source_capability* pps_cap = nullptr;

// Find the PPS capability
// (take the last one if multiple ones are available)
const source_capability* find_pps_cap() {
    int index = 0;
    for (int i = 0; i < power_sink.num_source_caps; i++) {
        if (power_sink.source_caps[i].supply_type == pd_supply_type::pps)
            index = i;
    }
    return &power_sink.source_caps[index];
}

// called when the USB PD controller triggers an event
void sink_callback(callback_event event) {

    if (event == callback_event::source_caps_changed) {
        // source has announced its capabilities; sink must request voltage
        pps_cap = find_pps_cap();
        voltage = std::max((int)pps_cap->min_voltage, cutout_voltage);
        power_sink.request_power(voltage);
    }
}

int main() {
    // initialize the board
    hal.init();
    hal.set_led(color::red);

    // initialize PD power sink
    power_sink.set_event_callback(sink_callback);
    power_sink.init();

    // loop and poll
    while (true) {
        hal.poll();
        power_sink.poll();

        // If button has been pressed, increase voltage by 100mV
        if (hal.has_button_been_pressed() && pps_cap != nullptr) {
            voltage += 100;
            if (voltage > pps_cap->voltage)
                voltage = std::max((int)pps_cap->min_voltage, cutout_voltage);
            power_sink.request_power(voltage);
        }

        // set LED: green for desired voltage, red otherwise
        hal.set_led(power_sink.active_voltage == voltage ? color::green : color::red);
    }
}

Selecting a specific capability

The next example looks for a capability providing 20 V with at least 2A of current. If it is found, it is requested. If not, it stays at 5V. The led is blue for 20V (at at least 2A) and flashing in red otherwise.

#include "pd_sink.h"

using namespace usb_pd;

mcu_hal usb_pd::hal;
pd_sink power_sink;

int voltage = 5000;
const source_capability* pps_cap = nullptr;

// called when the USB PD controller triggers an event
void sink_callback(callback_event event) {

    if (event == callback_event::source_caps_changed) {
        // source has announced its capabilities; sink must request voltage
        // search capability with 20V and 2A
        for (int i = 0; i < power_sink.num_source_caps; i++) {
            auto cap = &power_sink.source_caps[i];
            if (cap->min_voltage <= 20000 && cap->voltage >= 20000
                    && cap->max_current >= 2000) {
                power_sink.request_power_from_capability(i, 20000, 2000);
                return;
            }
        }

        // no matching voltage found; select 5V
        power_sink.request_power(5000);

    } else {
        // set LED color
        if (power_sink.active_voltage == 20000)
            hal.set_led(color::blue);
        else
            hal.set_led(color::red, 500, 500);
    }
}

int main() {
    // initialize the board
    hal.init();
    hal.set_led(color::red, 500, 500);

    // initialize PD power sink
    power_sink.set_event_callback(sink_callback);
    power_sink.init();

    // loop and poll
    while (true) {
        hal.poll();
        power_sink.poll();
    }
}

Since the ZY12PDN board does not have a switch to turn off the output in case the voltage or current is insufficient, this modified firmware can be combined with the below circuit. It turns off the output below approx. 15V.

Schematic Power Switch

The output of the ZY12PDN board is connected to VBUS and GND. The power consumer is connected to OUT and GND. The circuit uses a high-side switch so the ground can be shared.

For the bipolar transistor (Q1) almost any NPN transistor will work. The P-channel MOSFET (Q2) however will need to be suitable for the high voltages and currents. In particular:

  • VDS ≤ -30V
  • VGS ≤ -20V
  • RDS (on) ≤ 50mΩ
  • ID > 3A (or whatever is needed)

This circuit is not designed to be operated with an input voltage in the range between 12V and 17V (except for short transitions).

Test the circuit thoroughly. I'm not an electrical engineer.

It seems that the circuit was the basis for a discussion on Electrical Engineering on Stack Exchange.