Version 172

    Peter, Jon and Jan are building a programmable electronic load. This document is the common design sheet.

    It's obviously work in progres. And fun.











    MSP432 LaunchPad pin assignments


    J1.1+3V33v3 for the i2c pull-ups
    J1.3P3.2UART1 RX
    J1.4P3.3UART1 TX
    J1.9P6.5i2c SCL
    J1.10P6.4i2c SDA
    J1.2P6.0LCD Power Control
    J1.5P4.13LCD SPI CS
    J1.6P1.5LCD SPI CLK
    J2.2P2.5LCD External COM Inversion (not used)
    J2.6P1.6LCD SPI SIMO
    J3.29 reservedP5.4ADC_INPUT_A1 (not used)
    J3.30 reservedP5.5ADC_INPUT_A0 (not used)
    J4.39 reservedP2.6Board_PWM0 (not used)
    J4.38 reservedP2.4Board_PWM1 (not used)
    need to add SPI1 reserved
    need to add SPI2 reserved
    need to add SPI3 reserved
    need to add SPI4 reserved

    We can ignore the reserved pins. They aren't used. I'd like to avoid that we do use them unless we understand what flex we give up though.

    The PCB should have a white triangular mark next to J1.1 if possible. It's the indicator for the mounting direction (because the connectors are symmetrical, it's easy to mount the wrong way around).


    DAC-ADC BoosterPack








    Output pin assignments

    All pins isolated from LaunchPad if P1 and P2 open.

    P1 and P2 are for BoosterPack testing only, to deliver power to the DAC, ADC and REF ship when no power board is connected.

    In the final design, P1 and P2 need to be open and the power board has to provide GND and V+ for these 3 ICs.

    It should be at least 3V (ISO min. VCC2 requirement) and max 5V5 (max rating for ADC). Let's use 3V3 to 5V.



    P3.1analog outVREFfuture
    P3.2analog outDAC Dfuture
    P3.3analog outDAC Cfuture
    P3.4analog outDAC Bfuture
    P3.5analog outDAC Acurrent control voltage
    P3.6analog inADC Aactually set current readback
    P3.7analog inADC Bsense voltage at device under test or terminals
    P3.8analog inADC Ctemperature
    P3.9analog inADC Dfuture
    P3.10depends on P2GNDshould come from power board GND if we run isolated (P2 open)
    P4.1depends on P2GNDshould come from power board GND if we run isolated (P2 open)
    P4.2depends on P13V3should come from power board if we run isolated (P1 open). 3V3 - 5V
    P4.3digital outi²C SCLfuture
    P4.4digital in/outI²C SDAfuture





    BOM for pcb version 1_2:

    Changes between versions of the ADC/DAC pcb:



      U3 Vsense to VOUT

    U3 Vsense to VOUT

    ADCs labeled reverse on the PCB.

    D=A, C=B, B=C and A=D



    Solder advice: in hindsight I should have used larger IC packages. The board isn't easy to solder at home. But it's doable with proper care, hand stability of a snooker player and eyesight of an eagle.

    It's best to leave capacitor C5 off and solder it separately when the other components (in particular U2) are mounted. Even then it may be good to move it as far as you can from U2's pins. I didn't provide enough clearance in my PCB design.

    The VSSOP footprint of U2 is narrow on the V1_2 board. If you are brave, you can use pliers to bend the pins a bit down. I cut of the pins 5, 7 and 8 with a hobby knife.


    For measurements on this board, check Programmable Electronic Load - ADC and DAC BoosterPack test.


    Load Board

    CAD version:


    eload driver board schematics

    eload driver board, power section


    optional BoosterPack breakout edge connectors to add user interface elements. These aren't isolated from the LaunchPad.


    optional use as a PSU








    RefValueFootprintoptionalloadpsumanf. Nrsourcedescriptionurl
    C347uCapacitors_SMD:C_1210_HandSolderingoptionalxxGRM32ER61C476KE15Lelement14MURATA - SMD Multilayer Ceramic Capacitor, 1210 [3225 Metric], 47 µF, 16 V, ± 10%, X5R, GRM Series
    C1747uCapacitors_SMD:CP_Elec_5x5.3xx16SVPG47M16SVPG47Melement1416SVPG47M16SVPG47MCapacitor 47 F 16 V OS-CON SVPG Series Radial Can SMD 0.025 ohm 5000 hours 105°C
    C1847uCapacitors_SMD:CP_Elec_5x5.3xx16SVPG47M16SVPG47Melement1416SVPG47M16SVPG47MCapacitor 47 F 16 V OS-CON SVPG Series Radial Can SMD 0.025 ohm 5000 hours 105°C
    C1947uCapacitors_SMD:C_1210_HandSolderingoptionalxxGRM32ER61C476KE15Lelement14MURATA - SMD Multilayer Ceramic Capacitor, 1210 [3225 Metric], 47 µF, 16 V, ± 10%, X5R, GRM Series
    C2047uCapacitors_SMD:CP_Elec_5x5.3xx16SVPG47M16SVPG47Melement1416SVPG47M16SVPG47MCapacitor 47 F 16 V OS-CON SVPG Series Radial Can SMD 0.025 ohm 5000 hours 105°C
    C2147uCapacitors_SMD:C_1210_HandSolderingoptionalxxGRM32ER61C476KE15Lelement14MURATA - SMD Multilayer Ceramic Capacitor, 1210 [3225 Metric], 47 µF, 16 V, ± 10%, X5R, GRM Series
    D1MMSZ5233BMMSZ5233B6V Diodes_SMD:D_SOD-123xxMMSZ5233BMMSZ5233Belement14MMSZ5233BMMSZ5233BZener Single Diode MMSZ52 Series 6 V 500 mW SOD-123 5 2 Pins 150°C
    J1Ti_Booster_40_J1Pin_Headers:Pin_Header_Straight_1x10_Pitch2.54mmxxHDFL10Paliexpresspin headers.
    I use ones with extended pins, female side up, e.g. can be combined using multiple Stackable Header 10-pin 2.54mm for Ar duino Shields Pin Lenght 11mm
    One Lot is enough for the whole build
    J2Ti_Booster_40_J2Pin_Headers:Pin_Header_Straight_1x10_Pitch2.54mmoptionalxxHDFL10Paliexpresspin headers.
    I use ones with extended pins, female side up, e.g. can be combined using multiple Stackable Header 10-pin 2.54mm for Ar duino Shields Pin Lenght 11mm
    One Lot is enough for the whole build
    J3Ti_Booster_40_J3Pin_Headers:Pin_Header_Straight_1x10_Pitch2.54mmxxHDFL10Paliexpresspin headers.
    I use ones with extended pins, female side up, e.g. can be combined using multiple Stackable Header 10-pin 2.54mm for Ar duino Shields Pin Lenght 11mm
    One Lot is enough for the whole build
    J4Ti_Booster_40_J4Pin_Headers:Pin_Header_Straight_1x10_Pitch2.54mmoptionalxxHDFL10Paliexpresspin headers.
    I use ones with extended pins, female side up, e.g. can be combined using multiple Stackable Header 10-pin 2.54mm for Ar duino Shields Pin Lenght 11mm
    One Lot is enough for the whole build
    P1CONN_01X02Pin_Headers:Pin_Header_Straight_1x02_Pitch2.54mmxx282834-2282834-2element14282834-2282834-2Wire-To-Board Terminal Block 2.54 mm 2 Ways 30 AWG 16 AWG 1.4 mm Screw
    P2CONN_01X08Pin_Headers:Pin_Header_Straight_1x08_Pitch2.54mmxx282834-8282834-8element14282834-8282834-8Wire-To-Board Terminal Block 2.54 mm 8 Ways 30 AWG 16 AWG 1.4 mm Screw
    P3CONN_01X10Pin_Headers:Pin_Header_Straight_1x10_Pitch2.54mmxxpin headers. I use ones with extended pins, female side up
    P4CONN_01X04Pin_Headers:Pin_Header_Straight_1x04_Pitch2.54mmxxpin headers. I use ones with extended pins, female side up
    Q1BSS138TO_SOT_Packages_SMD:SOT-23xxBSS138-7-Felement14BSS138-7-F - MOSFET Transistor, N Channel, 200 mA, 50 V, 1.4 ohm, 10 V, 1.2 V
    U1LM78L05ACMLM78L05ACMHousings_SOIC:SO-8_5.3x6.2mm_Pitch1.27mmxxLM78L05ACMLM78L05ACMelement14LM78L05ACMLM78L05ACMLinear Voltage Regulator 7805 Fixed Positive 6.7V To 35V In 5V And 0.1A Out SOIC-8
    U2LM2662Housings_SOIC:SO-8_5.3x6.2mm_Pitch1.27mmxxLM2662MLM2662Melement14LM2662MLM2662MDC/DC Adjustable Charge Pump Voltage Converter 1.5V to 5.5V in-5.5V to-1.5V/200 mA out SOIC-8
    U3TLE2144SMD_Packages:SO-16-WxxTLE2144CDWG4TLE2144CDWG4element14TLE2144CDWG4TLE2144CDWG4Operational Amplifier Quad 4 Amplifier 5.9 MHz 45 V/s 2V to 22V WSOIC 16 Pins



    xxLINEAR TECHNOLOGY LT1058 Quad, JFET Input Precision High Speed Op Amp
    rs online
    PCA9557DPCA9557DI/O Expander 8bit 400 kHz I2C SMBus 2.3 V 5.5 V SOIC
    PCA9557DPCA9557D8-channel I/O Expander 400kHz I2C SMBus 16-Pin SOIC


    The schematic explains what is optional and why. If you build the DC load, you dont' have to order components without an x in the load column.

    I haven't looked up sources for jellybean components, only for those that require some thought.

    U4  PCA9557DPCA9557D can be sourced from NXP or TI Both versions are ok




    OFF-BOARD Components


    eload power input stage

    For progress on the design of the power stage, check Programmable Electronic Load - Power Stage.





    • CCS 8 with MSP432 compiler TI v 16.12.0.STS or >
    • TI-RTOS for MSP43X, any SimpleLink version
    • MSP432 LaunchPad (not the black pre-prod because it's out of support)


    The firmware is for the MSP432 microcontroller. But the harware can be driven from any controller, processor, PSoC or computer that can deliver i2c and power between 3V3 and 5V.

    That includes Arduino, Raspberry Pi, BeagleBone, Warp7, IOT20xx, the ST Neo family, ... As long as they match that voltage range and have I2C master, it 'll work.

    The ADC/DAC board takes care that the logic side is galvanicaly isolated from the power side (that is, if you leave the P1 and P2 jumpers open).


    The only requirement is that you have a positive supply between 3V3 and 5V, a ground, and I2C signals (the board has I2C pull-ups that connect to the positive supply that you provide. If you don't want that (maybe your dev board already has 'm), leave R1 and R2 off.

    ADC, DAC and Enable are all I2C driven.


    GITHUB location:

    download latest source files as a zip :

    GIT artifacts don't contain CCS project files, only sources - get a zip with CCS project from the attachments at the end of this document.


    SCPI Interface


    SCPI CommandstatusSCPI standardCommentExample
    *IDN?worksStandard SCPI, implemented by librarycallsignTHEBREADBOARD,ELECTRONICLOAD,0,01.00
    *CLScheckStandard SCPI, implemented by library
    SYST:ERR?worksStandard SCPI, implemented by library
    SYST:ERR:COUN?worksStandard SCPI, implemented by library
    SYST:VERS?worksStandard SCPI, implemented by librarySCPI Standard version1999.0
    *RSTworksStandard SCPI, custom implementation neededreset to a  known status.
    Switch input off and set to constant current mode. Does not alter stored or temporary calibration settings.
    *TST?todoStandard SCPI, custom implementation neededTest instrument. Currently does nothing

    <read voltage>


    worksQuery the voltage on the voltage sense inputs.

    <read current>


    <set mode constant current, constant resistance, what have you, ...>








    wipset the constant regulation mode

    On reset, always constant current

    At this time supports constant current

    FUNC CURRent

    FUNC RESistance

    FUNC VOLTage

    FUNC POWer

    [:SOURce]:FUNCtion?wipget the constant regulation modeAt this time supports constant currentFUNC?

    [:SOURce]:INPut[:STATe] <b>

    worksSet the input state to ON or OFF

    INP ON



    Query the input state

    DEVElop:DAC# <16 bit value>works for DAC1Development low levelSend the passed value to the DAC. Currently only for the first DAC

    DEVE:DAC1 2000

    DEVE:DAC1 #H4000




    works for ADC1Development low levelRetrieve the last buffered ADC value. Currently only for ADC 1 - 3




    [:SOURce]:CURRent[:LEVel][:IMMediate]worksConstant Current ModeCurrently accepts RAW DAC value instead of a voltage

    CURR 5


    Retriev@e the set current level

    [:SOURce]:VOLTage[:LEVel][:IMMediate]todoConstant Voltage ModeWIP. Mode not implemented yetVOLT 2
    CALibratewipCalibration and Configurationdocumented here: Programmable Electronic Load - Calibration Steps


    For ref: this is what typical instrument (DMM) commands look like

    We may want to check the commands of a few existing loads, to see if there's a pattern or standard


        /* DMM */
        {.pattern = "MEASure:VOLTage:DC?", .callback = DMM_MeasureVoltageDcQ,},
        {.pattern = "CONFigure:VOLTage:DC", .callback = DMM_ConfigureVoltageDc,},
        {.pattern = "MEASure:VOLTage:DC:RATio?", .callback = SCPI_StubQ,},
        {.pattern = "MEASure:VOLTage:AC?", .callback = DMM_MeasureVoltageAcQ,},
        {.pattern = "MEASure:CURRent:DC?", .callback = SCPI_StubQ,},
        {.pattern = "MEASure:CURRent:AC?", .callback = SCPI_StubQ,},
        {.pattern = "MEASure:RESistance?", .callback = SCPI_StubQ,},
        {.pattern = "MEASure:FRESistance?", .callback = SCPI_StubQ,},
        {.pattern = "MEASure:FREQuency?", .callback = SCPI_StubQ,},
        {.pattern = "MEASure:PERiod?", .callback = SCPI_StubQ,},


    For ref: SCPI command help

    Where Can I Find Documentation about the SCPI Specification? - National Instruments

    SCPI Commands Common

    Common Commands SCPI


    Suggested commands - this is how BK Precision structures its SCPI (may want to check other loads to see if there is some sort of standard):

    • INPut ON|OFF
    • VOLTage
    • CURRent
    • CURRent:PROTection:STATe ON | OFF
    • CURRent:PROTection:DELay
    • RESistance
    • POWer
    • :FETch or :MEASure VOLTage CURRent POWer (Fetch last sampled, Measure do a sample, I don't think we need to distinguish)
    • SYSTem:LOCal
    • SYSTem:REMote
    • SYSTem:RWLock


    Or the ones from Rigol's DL3000 DC range. They seem to be very similar to the BK ones.



    POSIX threads scheduling and priority overview


    This part of the documentation is changed to SimpleLink and POSIX API.



    RTOS Tasks overview


    TaskPriorityVitalStackArgument0 (schedule)Comments



    1000pulse led as visual clue of RTOS health


    Managed by Semaphore SEM_uart_rx

    react on incoming traffic on UART. Send to SCPI lib.
    threadDisplay10no20481000updates LCD display according to schedule
    threadADC10no10241000samples according to schedule


    Managed by QUEUE_NAME_DAC

    reacts on a mail message to set the DAC output value

    experimental: if a control strategy (e.g.: constant voltage) needs adjustment at regular times, this task will call that function at scheduled time.

    As not needed by constant current strategy (feedback handled by the hardware), this is now scheduled conservative. It's up to later control strategies to adapt the schedule to the own needs...




    reacts on a mail message to enable or disable the input
    threadTemperatureOverProtection10no5121000over-temperature watchdog



    RTOS Semaphore overview


    SEM_uart_rxlibrary: uart_implbinary

    halts the UART read task until char receive interrupt.

    This avoids polling. The UART read functionality takes no processor time unless it receives data.

    wait: threadUART()

    release: UART00_IRQHandler()



    RTOS Event and MailBox overview


    QueueMessage  countCommentFunctions

    halts the DAC read task until an mbDAC message arrives.

    This avoids polling. The DAC functionality takes no processor time unless it receives a message.

    wait: threadDAC()

    send: eloadRawSetDac()

    halt the Input Enable task until an mbInputEnable message arrives.

    wait: threadInputEnable()

    send: eloadInputEnable()




    PID library edit: not needed for a Load - it overshoots


    /pid/pid_lib/ is a port of the Arduino PID Library.

    /pid/pid_imp.c contains the task that manages the PID engine


    Port comments

    The Arduino lib is c++. The port is flattened out to c.

    The impact is that there's one instance of the PID library, which is a downside for our implementation.

    It would be easy if we could have a PID instance for each load strategy (constant current, voltage, resistance, power).

    We can always adapt the library to work with multiple strategies if we add a context handle to all calls. Let's see if we need it.


    Because the constants and function calls are now un-encapsulated, the publicly visible names (api) are pre-pended with PDI_ or pid.


    the Arduino millis() function is replaced with the TI-RTOS Timestamp_get32() function.

        long millis() {
            long t = Timestamp_get32();
    //        long seconds = t >> 15;
            long msecs = (t & 0x7fff) * 1000 /32768;
            return msecs;

    The millis() function isn't part of the public api. It stays local within the ported pid library.


    LCD Display


    check here for a detailed explanation: Programmable Electronic Load - LCD Display


    The firmware supports SHARP LCD display,






    Error Handling



    good example:, in particular

    Complete list: SCPI Errors

    e.g.: this can be used to report an invalid function call (set constant voltage value when in constant current mode, ...):




    Settings conflict - Indicates that a legal program data element was parsed but could not be executed due to the current device state.




    catch in the SCPI interface:

    missing parameter

    parameter out of bounds

    index out of bounds


    - a non-implemented ADC channel:

    -131,"Invalid suffix"


    - a missing DAC value:

    -109,"Missing parameter"


    - more than 16 bits to the DAC:

    DEVE:DAC1 66666
    -224,"Illegal parameter value"



    push SCPI errors from deeper levels, without making code dependent on SCPI:







    UART module


    uses TI-RTOS UART driver.

    Buffered output for transmit

    Interrupt driven for receive, wake up RTOS task (with semaphore) whenever a character arrives on the Rx line.



    void *threadUART(void *arg0)
        char        input;
        UART_Params uartParams;
        int iError;
        /* Call driver init functions */
    //    GPIO_init();
        /* Configure the LED pin */
        /* Create a UART with data processing off. */
        uartParams.writeDataMode = UART_DATA_BINARY;
        uartParams.readDataMode = UART_DATA_BINARY;
        uartParams.readReturnMode = UART_RETURN_FULL;
        uartParams.readEcho = UART_ECHO_OFF;
        uartParams.baudRate = 9600;
        uartParams.readMode = UART_MODE_CALLBACK; // the uart uses a read interrupt
        uartParams.readCallback = &UART00_IRQHandler; // function called when the uart interrupt fires
    #ifdef SCPI_UART_0
        uart = UART_open(Board_UART0, &uartParams);
    #ifdef SCPI_UART_1
        uart = UART_open(Board_UART1, &uartParams);
    #error "define a valid UART"
        if (uart == NULL) {
            /* UART_open() failed */
            while (1);
        iError = sem_init(&SEM_uart_rx, 0, 0);
        /* Configure the LED pin */
        /* Loop forever echoing */
        while (1) {
            UART_read(uart, &input, 1);
            iError = sem_wait(&SEM_uart_rx); // when a character is received via UART, the interrupt handler will release the binary semaphore
            while (iError) {
                // POSIX reported error with semaphore. Can't recover.
            // in my case: I get an interrupt for a single character, no need to loop.
            GPIO_toggle(Board_GPIO_LED1); // LED B - visual clue that we've received a request over USB
            scpi_instrument_input((const char *)&input, 1);
            GPIO_toggle(Board_GPIO_LED1); // LED B - visual clue off


    The interrupt is very simple. It posts a semaphore to flag our UART task that we have a character on the receive line.


    void UART00_IRQHandler(UART_Handle handle, void *buffer, size_t num)


    There are 2 touchpoints with the SCPI interpreter. UART module knows about SCPI lib. SCPI is UART-unaware.

    • initialises the SCPI lib (is that smart? *) with scpi_instrument_init() edit: moved to main().
    • each character received directly sent to SCPI lib with scpi_instrument_input().


        while (1) {
            UART_read(uart, &input, 1);
            iError = sem_wait(&SEM_uart_rx); // when a character is received via UART, the interrupt handler will release the binary semaphore
            while (iError) {
                // POSIX reported error with semaphore. Can't recover.
            // in my case: I get an interrupt for a single character, no need to loop.
            GPIO_toggle(Board_GPIO_LED1); // LED B - visual clue that we've received a request over USB
            scpi_instrument_input((const char *)&input, 1);
            GPIO_toggle(Board_GPIO_LED1); // LED B - visual clue off


    • SCPI lib uses SCPI_Write() to send info to UART (although the SCPI lib isn't aware of that. The linker resolves this dependency. The only thing that the SCPI lib requires is that "there is a function named SCPI_Write()" somewhere).


    size_t SCPI_Write(scpi_t * context, const char * data, size_t len) {
        (void) context;
        GPIO_toggle(Board_GPIO_LED1); // LED B - visual clue that we send a reply over USB
        UART_write(uart, data, len);
        GPIO_toggle(Board_GPIO_LED1); // LED B - visual clue offB
        return len;


    *  I did this because the UART module is the only one that talks to SCPI lib and didn't want to introduce additional dependencies.

    Open for discussion. Maybe in the main() function, where most of the modules are initialised? edit:done



    compile option to choose the UART:




    Undefining SCPI_UART_1 and defining SCPI_UART_0 enables USB

    Undefining SCPI_UART_0 and defining SCPI_UART_1 enables UART1 (TTL 3V3)

    It allows to select the port without code changes.


    #ifdef SCPI_UART_0
        uart = UART_open(Board_UART0, &uartParams);
    #ifdef SCPI_UART_1
        uart = UART_open(Board_UART1, &uartParams);
    #error "define a valid UART"






    ADC samples are taken all the time, based on a TI-RTOS schedule.

    Data is written to a round-robin buffer with 2 buckets.


    volatile ADCValues adcRoundRobin[2];
    volatile uint32_t adcRoundRobinIndex[ADC_ACTIVE_INPUTS] = {0};


    One bucket has stable data and can be read whenever needed. A read pointer points to that stable bucket.

    The other bucket is used to write sample data. Once samples from 4 channels are collected, the read pointer is toggled so that it points to that fresh data.

    I've optimised this. I've added a read pointer for each channel now:


    volatile uint32_t adcRoundRobinIndex[ADC_ACTIVE_INPUTS] = {0};


    Once a channel is collected, the read pointer for that channel is toggled so that it points to the fresh data.



    void *threadADC(void *arg0) {
        uint32_t i;
        a_i2cTransaction.writeBuf = a_txBuffer;
        a_i2cTransaction.readBuf = a_rxBuffer;
        a_i2cTransaction.slaveAddress = ADC_I2C_ADDR;
        // this buffer value never changes. Let's set it at the start.
        // If for some reason this becomes a variable value,
        // move to sampleADC()
        a_txBuffer[2] = ADS1115_CFG_L;
        while (1)
            for (i =0; i< ADC_ACTIVE_INPUTS; i++) {
                // we write value to the inactive robin
                // store value of ADC[i]
                // the ADC needs time between channel selection and sampling
                // we assign 1/ADC_ACTIVE_INPUTS of the task sleep time to
                // each of the ADC_ACTIVE_INPUTS samples
                // this puts more burden on the RTOS switcher - a compromise
                // - but certainly preferable to a loop
                // (except when later on we find out that the wait is only a few cpu cycles)
                adcRoundRobin[adcRoundRobinIndex[i] ? 0 : 1].raw[i] = sampleADC(i, THREAD_USLEEP_ADC / ADC_ACTIVE_INPUTS);
                // after value(s) written, we activate the inactive robin
                adcRoundRobinIndex[i] = adcRoundRobinIndex[i] ? 0 : 1;


    Because there is time needed between switching ADC channels and taking the sample, we give each of the four ADC channels 1/4th of the TI-RTOS schedule that they can spend on that time.


    x = sampleADC(i, (UInt)arg0/4);


    Sampling is done via I²C. First part is switching the ADC channel. Then a sleep to give the ADC time, then fetch:


    uint16_t sampleADC(uint32_t uModule, UInt uSleep) {
        uint16_t uRetval = 0u;
        // ...
         a_txBuffer[1] = array_ADS1115_CFG_H[uModule];
        // ...
        /* Init ADC and Start Sampling */
        if (! I2C_transfer(i2c_implGetHandle(), &a_i2cTransaction)){
            System_printf("Sampling Start Failed \n");
        // there's a pause required between channel selection and data retrieval
        // we consume that part of the task sleep time that's assigned to us by the task.
        // ...
        /* Read ADC */
        if (I2C_transfer(i2c_implGetHandle(), &a_i2cTransaction)) {
            uRetval = ((a_rxBuffer[0] << 8) | a_rxBuffer[1]);
        else {
            System_printf("ADC Read I2C Bus fault\n");
        return uRetval;


    Reading is done via a helper function - exposed as API to the program. This can be used anytime without locking or semaphore, because the time between switching the read pointer and the next possible write in that buffer is way longer (several orders of magnitude) than calling the helper function.


    uint16_t adcImplGetAdc(uint32_t uModule) {
        return adcRoundRobin[adcRoundRobinIndex[uModule]].raw[uModule];




    DAC output is set on command, using RTOS MailBox and messaging.

    The payload for a DAC message contains the DAC channel that needs to be set, and the value.


    typedef struct MsgDAC {
        uint8_t module;
        uint16_t value;
    } MsgDAC;


    The DAC task is started by RTOS. It inialises the DAC settings, then waits until it receives a message.


    void *threadDAC(void *arg0) {
        MsgDAC d_msg;
        d_daci2cTransaction.writeBuf = d_dactxBuffer;
        d_daci2cTransaction.readBuf = d_dacrxBuffer;
        d_daci2cTransaction.slaveAddress = DAC_I2C_ADDR;
        d_daci2cTransaction.writeCount = 3;
        d_daci2cTransaction.readCount = 0;
        mqd_t mq;
        struct mq_attr attr;
        attr.mq_flags = 0;
        attr.mq_maxmsg = 1;
        attr.mq_msgsize = MSGDAC_SIZE;
        attr.mq_curmsgs = 0;
        mq = mq_open(QUEUE_NAME_DAC, O_CREAT | O_RDONLY, 0644, &attr);
        while (1) {
            ssize_t bytes_read;
            bytes_read = mq_receive(mq, (char *)&d_msg, MSGDAC_SIZE, NULL);
            /* wait for mailbox to be posted by writer() */
            if (bytes_read) {
    // ...


    The waiting doesn't take processor time. RTOS takes care that it will only get a slice of clock cycles when there is a message.

    Two things in the RTOS configuration make this happen:

    The mailbox with room for exactly one message, and a receive event.

    Because there is usually no message in the mailbox, the execution of the DAC logic becomes inactive at this line:


            bytes_read = mq_receive(mq, (char *)&d_msg, MSGDAC_SIZE, NULL);


    When we send a DAC message somewhere else in the code (e.g.: when the user requests a new setting of the electronic load), RTOS reactivates the process and hands over the payload message.

    The DAC task sets the output of the requested channel and returns to the point where it waits for a new message.


       while (1) {
            ssize_t bytes_read;
            bytes_read = mq_receive(mq, (char *)&d_msg, MSGDAC_SIZE, NULL);
            /* wait for mailbox to be posted by writer() */
            if (bytes_read) {
                d_dactxBuffer[0] = getAddressFromModule(d_msg.module); // set value direct
                d_dactxBuffer[1] = d_msg.value >> 8; // MSB
                d_dactxBuffer[2] = d_msg.value; // LSB
                if (! I2C_transfer(i2c_implGetHandle(), &d_daci2cTransaction)) {
    //                System_printf("I2C Bus fault\n");
    //                System_flush();


    You select the DAC channel by setting bit 2 and 1 in the control byte (tx_buffer[0]). There's a helper function that gives a correct control record.


    // address 7 - 6: 0, load mode 5 - 4: 01 direct from i2c, 3: reserved 0, 2 - 1: channel select, 0: pwr down 0
    #define DAC857X_CFG_H0 0b00010000
    #define DAC857X_CFG_H1 0b00010010
    #define DAC857X_CFG_H2 0b00010100
    #define DAC857X_CFG_H3 0b00010110



    static const uint8_t array_DAC857X_CFG_H[4] = {DAC857X_CFG_H0, DAC857X_CFG_H1, DAC857X_CFG_H2, DAC857X_CFG_H3};
     * get the hex address for the requested DAC module
    uint8_t getAddressFromModule(uint8_t module) {
        return array_DAC857X_CFG_H[module];


    Bits 7 - 3 and 0 are all fixed.



    Input Enable


    The functionality to activate and deactivate the load is documented here: Programmable Electronic Load - Input Enable Functionality .


    Control Strategies


    The firmware uses strategies to handle the different types of load operation (e.g. constant current, constant, voltage, ...).


    A strategy is a set of functions that you can plug in, and that together run that particular operation type (this is a c version of the Gang of Four's Strategy Design Pattern).

    The goal is to avoid that the firmware is riddled with if or switch statements whenever different behaviour is needed depending on the instrument's mode.

    Each operation strategy will have the same set of functions that run the instrument in that mode.

    The price to pay is not expensive - speed and memory burden is low. It is a little more complex to understand than the c++ version. Once you step trough it with a debugger, things become clear.

    The code also tries to hide that we're using strategies


    Currently starting to implement constant current strategy. The more difficult operation types can be added later, one by one, with not too much impact on the existing code across the firmware.

    . Only the eload API (see below) knows about it. All code goes via that eload API.

    API for the stategies is minimal now:





    eload API


    The eload API offers an abstraction layer for the strategies, so that the rest of the firmware doesn't have to know about it. It simplifies switching operation mode and driving the instrument.


    Example: The eload API offers a simple function eloadGetMode() to check what the instrument's current mode is.


    eload_mode eloadGetMode() {
        return getControlStrategy()->getMode();


    In the background, it uses the strategy mechanism to fetch the mode from the currently active strategy and call the implementation function.


    return getControlStrategy()->getMode();


    The eload API also has the common eload functions that don't need a strategy because they are always valid, regardless of mode.


    Calibration and Configuration Data


    The functionality to store calibration and configuration data in Flash is described in a separate article: Programmable Electronic Load - Calibration Data in Flash.

    The calibration and configuration procedures are documented in Programmable Electronic Load - Calibration Steps


    Changes To Standard MSP_EXP432P401R.c


    The standard file generated by the TI-RTOS application wizard has defaults for peripherals.

    The following has changed: none - I removed this section because with the external ADC/DAC board and the SimpleLink RTOS version, everything is kept standard.


    Remote Software




    Serial communication to the USB UART port.


    SCPI is a non-echoing protocol. To make the characters you type show in the Terminal, set Local echo to Force on.

    If you also set Local line editing to Force on, you have the opportunity to correct characters in the current line before they are sent to the serial port.




    Should return



    Windows GUI


    GUI supports reading the 4 ADCs, setting one of the DACs abd shoot any SCPI command.

    There's a window for SCPI output and a status window that shows SCPI errors.

    Source and binary available from





    The instrument comes with a LabVIEW library: Programmable Electronic Load - Write a LabVIEW Library part 1: Initialise Block




    To resolve:

    power launchpad when device not usb connected,Either PC, or when used stand-alone a USB power brick
    separation of load power rails and control electronic rails (don't damage a connected PC)I2C isolator IC, and I2C only interface with power part.
    MSP432 has no DACExternal DAC, with I2C
    is isolation needed between msp and load?I2C isolator IC, and I2C only interface with power part.
    overload protection?yes, in firmware, based on FET specs - initial a simple shutdown

    MSP432 different adc results A0 and A1 - raised on e2e forum

    Must be my interpretation of the interface or a thing in TI-RTOS driver.

    The raw examples give a nice 729 nd 672 for 0.109V input. That's close enough.

    Button to turn off  remote access?not planned, but can be done using free MSP432 GPIO, no isolation needed
    Block remote commands when sensing front panel activity?not planned, the device doesn't have front panel. Can be implemented if needed
    What is the power-on state?same as *RST: Constant current, 0 amperes, input off

    watch-out: with built-in ref, max ADC value is 2.5V. We need to scale our sense resistor/whatevers for that.

    We can have 3.3V conversion if we have a reasonable external reference. We would have to present this on pin P5.6 - by default muxed to PWM TA2.1

    Not relevant, external ADC with I2C
    PWM signals not available on BoosterPack Connectors. They are routed to Green and Blue led.Changed MSP_EXP432P401R.c to route to P2.6 and P2.4

    PWM frequency (and rc filter) dependent on the number of steps we allow between duty 0 and 100%

    Only checked with setting the parameters in microseconds. I'll also test how granular we get with fractions.

    External DAC, with I2C
    I2C speed sufficient for the control loop?

    A single conversation with a TMP006 for benchmark at 400 kHz: 140 µS end-to-end on oscilloscope - add some time before and after the conversation for setup and data collection.

    Peripheral can go to 1 MHz - not standard supported in RTOS, would only use if speed is an issue and the I2C slaves can have it.

    how to power the DAC and ADC when LaunchPad is isolatedfixed. The driver board delivers that
    ADC1115 address by default same as DAC address 0x48

    In schema/PCB, configure for 0X49 by connecting P1 tp V+


    $6 or more  but this would work



    11.1 Layout Guidelines

    Carefully consider the layout of the PCB in order for the best results to be obtained. Input and output power and ground planes provide a low-impedance path for the input and output power.


    For the output, the positive and negative voltage outputs conduct through wide traces to minimize losses. A good-quality, low-ESR, ceramic capacitor placed as close as practical across the input reduces reflected ripple and ensure a smooth start-up.


    A good-quality, low-ESR, ceramic capacitor placed as close as practical across the rectifier output terminal and output ground gives the best ripple and noise performance.


    The location of the decoupling capacitors in close proximity to their respective pins ensures low losses due to the effects of stray inductance, thus improving the ripple performance. This location is of particular importance to the input decoupling capacitor, because this capacitor supplies the transient current associated with the fast switching waveforms of the power drive circuits.


    If the SYNC pin is being used, the tracking between device SYNC pins must be short to avoid stray capacitance. Never connect a capacitor to the SYNC pin.

    If the SYNC pin is not being used it is advisable to place a guard ring (connected to input ground) around this pin to avoid any noise pick-up. Ensure that no other trace is in close proximity to this trace SYNC trace to

    Maybe a port of the Arduino PID Library? Brett has documented this very deeply. Also has a self-train engine. Needs conversion from c++ to c (done that before for LCD libs, not that hard). Generic.

    Another option is a PID library that Martin Valencia and I selected as project of the month. This one is written for MSP432 (direct register, not driver/RTOS compatible). It's more a school example than a generic library.

    port of the Arduino PID Library

    removed because we don't use PID, can always be ressurected from GIT

    need to set PID SetOutputLimits() to the maximum of the output function (whether PWM or DAC)PID not implemented (was, but removed from source. Can be reintroduced if applicable ...)

    each lib usees its own int formats .

    TI-RTOS: UInt

    scpi: stdint , stdbool and int_fast16_t

    Display: unsigned char and stdint

    PID: double, int, long, unsigned ...

    Whenever possible, let's use <stdint> and <stdbool>. Leave libs unchanged
    Fan Control

    We have 7 pins of the  PCA9557DPCA9557D left Either break these out to a header connector(and have the luxury to control 7 additional isolated input/outpts or add a FET that can control the FAN(12V).

    At the moment, it's not implemented. Up to the user to either have the fan on all the time or to change firmware ...

    ADC/DAC PCB changes:

    - quad DAC

    - I2C isolated broken out

    - guard trace around analog if doable

    -  few GPIOs to software switch things on the power side? Isolation? - I2C extenders on the power board is a better option - keeps things isolated

    WIP. Current board works for proto if VSENSE and VOUT of DAC are bodged with a solder blob


    Low power analog boardproto ready
    High power componentsselected
    Temperature measurement?

    solved in hardware, needs implementation in firmware.


    Voltage divider + protection for voltage sense.

    Add a voltage divider for the voltage sense. Depending on the voltage range we accept, we'll need a (fairly stable) divider that takes care that the DAC input sees a level within its range. Maybe with a zener diode over the sense pins to protect the DAC from overvoltage...


    Not needed - see comment peter: the input resistors keep max current below the ADC max.



    When way too much timeValue

    SCPI over Ethernet/WiFI with CC3100 boosterpack. The MSP432 TI-RTOS and SCPI libs are ready for it.

    It doesn't take too much code changes. We don't even need to store home WiFI credentials on the MSP.

    THe CC3100 can be setup and store a profile by itself.

    I don't see a real need for this. The only advantage it would give us is having two physical layers - that forces us to make all code UART/USB independent.

    We can also stick a *IoT Ready* label on the front panel then .

    (on the other hand, I don't wnt to submit unsafe code, so to cather for my ego, it should at least be an SSH/SSL/WhatHaveYouEncrypted link)

    or allow conditional compilation of MSP432 Ethernet driver and connect it to SCPI


    Add a 4-wire voltage measurement.

    This requires a 3rd ADC channel - and the default TI-RTOS/MSP432 implementation caters for the 2 that we're already using for V and I.

    It should be possible to add additional ones, because MSP432 has more ADCs. Have to check the file MSP_EXP432P401R.h/c and Board.h

    Edit: Done - with the quad ADC we have this option

    scrolling graph display of I/U/P/R ?real
    Temperature measurementreal
    future revision: Connect input voltage to spare ADC (via divider network) to have over power protection independent of sense wiresreal
    future revision: Break out the spare port expander pinsreal




    Related blog

    Programmable Electronic Load - Input Enable Functionality
    Programmable Electronic Load - Power Stage
    Programmable Electronic Load - Temperature Protection
    Programmable Electronic Load - Calibration Data in Flash
    Programmable Electronic Load - Calibration Steps
    Programmable Electronic Load - LCD Display
    peteroakes articles:
    Electronic DC Load - Design and Build to test PSU Project
    youtube: Electronic DC Load Design and Testing
    youtube: Full Build Analogue DC Load
    youtube: Electronic DC Load - Performance Improvements
    Raspberry PI 2, Fun with I2C DACs and ADC's
    jc2048 articles:
    Programmable Electronic Load: Dynamic Behaviour: Part 1 Overview
    Programmable Electronic Load: Dynamic Behaviour: Part 2 The Servo Loop
    Programmable Electronic Load: Dynamic Behaviour: Part 3 Effect of Output Inductance
    Programmable Electronic Load: Dynamic Behaviour: Part 4 Effect of Output Voltage Change
    Programmable Electronic Load: Dynamic Behaviour: Part 5 Stability
    Programmable Load Build
    jancumps articles:
    Programmable Electronic Load - ADC and DAC BoosterPack test
    Programmable Electronic Load - Analyse the Summing Node Zero Point
    Programmable Electronic Load - Measurements Part 1: Control Circuit
    Programmable Electronic Load - Write a LabVIEW Library part 1: Initialise Block
    Programmable Electronic Load - Write a LabVIEW Library part 2: Read Output Block
    Programmable Electronic Load - Write a LabVIEW Library part 3: Close Block
    Programmable Electronic Load - Write a LabVIEW Library part 4: Function Set Block
    Programmable Electronic Load - Write a LabVIEW Library part 5: Input Control Block
    Programmable Electronic Load - Write a LabVIEW Library part 6: Raw DAC Block
    Programmable Electronic Load - Write a LabVIEW Library part 7: Raw ADC Block
    Programmable Electronic Load - Automating a DC Switcher Efficiency Test with LabVIEW
    Programmable Electronic Load - LabVIEW Test Automation: Characterise the Instrument
    Programmable Electronic Load - LabVIEW Test Automation: Characterise the Instrument Pt 2: Oscilloscope Measurements
    TI-RTOS: Switching to the SimpleLink Distribution
    Switch from TI-RTOS to SimpleLink POSIX: Threads and Semaphores
    Switch from TI-RTOS to SimpleLink POSIX: EEPROM API
    Switch from TI-RTOS to SimpleLink POSIX: From MailBox to Message Queue
    Switch from TI-RTOS to SimpleLink POSIX: LCD Display Driver
    Switch from TI-RTOS to SimpleLink POSIX: Sleep when Idle