Note: the control loop, discussed later, has been updated. See System Testing.
Let’s take a look at the heart of the variable load project: the code to regulate the load.

The load regulator module has four major components:
- LTC2451: This is the current monitor. This ADC is connected to the output of the hall-effect sensor, and communicates with the microcontroller through the I2C / TWI bus.
- MAX5216: This is the DAC the microcontroller uses to affect the amount of current flowing through the load. 0 amps corresponds to a DAC output of 0.5 V, and each 80 mV on top of that allows an additional amp to flow through the load.
- AD8685: This is the ADC used to measure the voltage across the load. The input voltage is put through a voltage divider, which is then read by this ADC. The software accounts for the scaling-down of the input voltage and then scales it back up.
- Timer 1: This is the timer the load regulator uses for timing its actions. For example, the current through the load is measured 60 times per second, or every 17 ms. The load regulator will check its timer to see if 17 ms have passed.

Above, the image shows the LoadRegulator class’ members. The HAL class members are members necessary for the device classes to work. current_monitor, which is an instance of the LTC2451 member, uses HAL_TWI. current_control, which is an instance of MAX5216, uses a GPIO class for its chip select, and SPI class for communication. volt_monitor, an instance of ADS8685, also uses GPIO and SPI. The data members are variables the LoadRegulator classes uses for its operations.
- cal_zero: In theory, at zero current, the hall effect sensor should output 0.5 volts. However, it’s extremely unlikely the output will be exactly 0.5 volts. Therefore, it’s best to figure out what the actual output voltage at zero current is. This voltage is stored here.
- target_current, target_power, target_resistance, target_voltage: the load regulator will need to know what target to aim for. In constant current mode, the amount of current flowing through the load should be target_current. Likewise, in constant power mode, the amount of power the load dissipates should be target_power. Same goes for target_resistance and target_voltage.
- measured_current: every time the current through the load is measured, the value is stored here.
- control_current: this is the knob the module will turn to adjust the current through the load. If measured_current is too small, then control_current is increased. If measured_current is too large, then control_current is decreased.
- measured_voltage: every time the voltage across the load is measured, the value is stored here.
There are two miscellaneous members. First is last_cur_time. This variable stores the last time the current sensor was read. Second is op_mode. This determines what mode the load regulator is in: off, constant current, constant power, constant resistance, or constant voltage.

Above is the constructor. The initialization list is pretty long since this class has so many class members. But thankfully, whatever calls this constructor doesn’t have to worry about providing it with arguments, making this class easy to create. The reason no arguments are necessary is because of the project header files, schematic.h and settings.h. Everything with a SET prefix comes from settings.h, while everything with a SCH prefix comes from schematic.h. Since all the information like timer settings and GPIO pin, port and direction are stored in these header files, the initialization list can use that information and set up all the class members without depending on arguments.
Let’s look inside the constructor. First, the timer that was set up in the initialization list (LR_Timer) has its interrupt enabled. This is necessary for the timer to work, since the timer’s interrupt increments a counter that allows us to measure the passage of time. Then, the voltage monitor is configured. The voltage monitor ADC, ADS8685, has a lot of settings that can be adjusted. The two of interest here are the programmable gain amplifier, which determines the input range, and the reference voltage, which affects the ADC’s resolution. After that, target_power, target_resistance and target_voltage are set to default values. Lastly, some house keeping is done to prevent damaging the load or external power supply on power up.

This is the method that actually regulates the load, and it is called in the main loop. LR_Timer is configured to set a flag every 1 ms, so the code will run at that frequency. Firstly, after clearing the flag, the system sees if it should sample the current monitor. The current monitor ADC has a sample rate of 60 Hz, so there’s no point sampling faster than that, and unnecessary and futile attempts to communicate with the ADC takes up bandwidth on the I2C bus. If the measurement is successful, then measured_current is updated, and error is updated to reflect that the read was successful. Then, the voltage across the load is measured, and measured_voltage is updated. Measuring current and voltage is done in preparation for the next step: the regulation of the load. The load regulator operates at 60 Hz for CC, CP, and CR mode. This is because CC, CP and CR regulation can only run when the current is successfully measured. However, CV only needs a successful voltage reading to run, so it can run at the 1 kHz rate. If the regulator is in the OFF mode, then the current through the load is set to 0.

The code above shows the method used to adjust the control current. Say the user requests a current through the load of 1 A, and the current monitor reports a current reading of 0.5 A. Clearly, the current needs to be increased by 0.5 A. If the user requests 1 W from a 5 V power supply, and the system is only consuming 0.1 A (or 0.5 W), then the current has to be increased by 0.5 W / 5 V = 0.1 A. Likewise, if the resistance is too small or large, then current through the load is adjusted to approach the desired resistance value.
Instead of adding the error to control_current, I use SET_LR_CUR_ERROR_SCALER to adjust how quickly the system approaches the requested value. Currently, it’s set to 1/2. This means that if an error is 0.5 A, then the output is changed by half that, so 0.25 A. This dampens the systems response so that there are no overshoots. I’ll play with this value in the future to see if it’s necessary, but for now 0.5 is a decent place holder.

Software compares target value to measured value, then adjusts accordingly
Above shows the control loop. As mentioned, the target value and measured voltage are used to calculate the required current through the load, and then the software will compare the required current with the measured current. Since measured current is limited to the sample rate of the ADC, which is 60 Hz, this loop can only run at 60 Hz at most. This is a shame since measured_voltage updates at 1 kHz, but that will not help the loop run faster. It will, however, help the load regulator in CV mode.
CC, CP and CR are all very similar since it is possible to calculate how much current is necessary based on the desired target current, power or resistance and the voltage across the load. For example, if the user requests power P, and the voltage across the load is V, then the current through the load should be P/V. If the user requests resistance R, and the voltage across the load is V, then the current through the load should be V/R. However, this is not possible for CV; if the user requests voltage V across the load, then how much current should flow through the load? The answer is unknown, so the code has to guess. Unlike CC, CP and CR which can calculate the necessary output and approach it, CV just measures the voltage across the load and increases the current if the voltage is too high, or decreases it if the voltage is too low. The step size is set by SET_LR_CV_CUR_STEP, which is currently set to 1 mA. Since CV runs 1000 times per second, that means the current through the load can change 1 A per second. Hopefully that’s good enough, but I will add the disclaimer that CV isn’t the system’s strong suit. Also, putting the load regulator on the output of a constant voltage power supply with no current limiting resistor can cause the power supply to become unstable, since the power supply and variable load will be fighting for dominance.

Part of the house keeping in the constructor is calibrating the zero. In the system, ideally the output of the hall effect sensor, and therefore the input to the ADC, is 0.5 V at no current flow. However, this cannot be assumed. Therefore, the ADC input at zero current must be found, and this value is stored in cal_zero. The code above shows how this is done: assuming the load has no current flowing through it, the current monitor is read SET_LR_CAL_AMOUNT times, which is currently 10. Then, cal_zero stores the average of those 10 readings.
I hope you enjoyed reading about the load regulator section of the code. Next, let’s take a look at the thermal regulator.