Normally, the software architecture would be defined at the start of the project, or before programming begins. However, I didn’t really know how I wanted the code to look back then, so I decided to work on the hardware abstraction layer. Now, most of the low level drivers are complete. Therefore, it seems like the right time to talk about the software at a high level.
Overview

Blocks rely on the blocks below it (eg. LCD uses TWI)
Here’s an overview of the code. The code will consist of 4 modules: the temperature regulator, which keeps the load from getting too hot, the load regulator, which controls the current through the load, the user interface, which allows the user to interface with the system, and the debugger, which is used to get information about the system. I’ll elaborate on each module below, as well as in future posts.
Main is the glue that holds the system together. Firstly, it hosts an instance of each module, and calls methods from the modules. Secondly, it allows modules to share information. Lastly, it has the main timer, which is Timer 3. This is the clock referenced by most modules for time keeping. In this application, Timer 3 will increment a counter every millisecond. The modules that use the timer will use this counter to know how much time has passed. For example, if the temperature regulator runs every 100 ms, then the module will keep checking the counter and do nothing until the counter has increased by at least 100 since the last time the module was run. This way, one timer can be used to coordinate the activities of multiple modules.
Schematic.h contains information dependent on hardware; most of this information comes from the schematic and datasheets for the chips in the schematic. Examples of information contained here are:
- What SPI mode devices on the SPI bus operate in
- Clock speeds for different SPI and I2C devices
- What the reference voltage is for different devices
- Equations to convert counts to voltages for ADCs
- GPIO’s port, pin and direction
- If an LED is active high or active low
Settings.h contains information that doesn’t derive from hardware. Examples of information contained here are:
- Configuration settings for Timer 1 and Timer 3
- How frequently different modules are run
- UART format (eg. 8N1)
- How data is displayed on the LCD (eg. how many decimal points)
Both header files are used by main as well as the modules. This way, when a module is being constructed, main doesn’t have to provide low level information like what port and pin a GPIO should be on; the module can do that by itself by reading the Schematic.h header file. This keeps high level code clean, and the relevant information consolidated into few files.
Temperature Regulator

The temperature regulator is probably the simplest module. Its job is to keep the variable load, the transistor, from getting too hot. This means the temperature regulator must (a) know the transistor’s temperature, and (b) take action to adjust it.
The hardware allows for both of this. Firstly, the transistor will have a thermistor attached to it, and an opamp on the circuit board converts the thermistor’s resistance to a voltage. This voltage is then fed into ATMEGA32U4’s ADC. This allows the temperature regulator to determine the transistor’s temperature by reading from the ADC. Secondly, the variable load is hooked up to a head sink with a fan on it. How hard the fan blows can be controlled by a PWM signal, which is hooked up to the microcontroller. If the temperature regulator sees that the ADC count it too high, which indicates that the transistor is getting too hot, then the PWM’s duty cycle is increased. If the transistor cools off and the ADC count drops down, then the PWM’s duty cycle can be decreased to reduce fan noise.
The temperature regulator doesn’t need to be run very often, since the large heat sink the transistor is mounted on will prevent rapid changes in temperature. For now, the temperature regulator will run every 100 ms, at which point it samples the ADC and determines what the PWM’s duty cycle should be.
Load Regulator

The load regulator is the heart of the project. It is responsible for controlling the current going through the transistor. LTC2451 is the current monitor, and it is used to determine how much current is going through the load. MAX5216 is the DAC that allows the microcontroller to control how conductive the transistor is. ADS8685 is the ADC that tells the microcontroller how much voltage is across the load.
In constant current mode, the load regulator will be told to allow a certain amount of current through the load; say 1 amp. In this scenario, the load regulator will read LTC2451 to see how much current is going through the load. If it’s below 1 amp, then MAX5216 is updated to allow more current. If it’s above 1 amp, then MAX5216 is updated to reduce the current flow. In constant power and constant resistance mode, the load regulator is told to make the load dissipate a certain amount of power, or to have a certain amount of resistance. The load regulator will use this information, and the amount of voltage across the load (information provided by ADS8685) to calculate how much current should flow through the load. Then, like in constant current mode, the load regulator uses LTC2451 and MAX5216 to try to achieve that current.
Constant voltage mode is a unique case. Here, the load regulator doesn’t care how much current flows through the load. It only cares about the voltage across it. Therefore, it will read ADS8685 to see how much voltage is across the load, and then update MAX5216 accordingly. If the voltage is too high, then increase current consumption; if voltage is too low, decrease current consumption.
How frequently the load regulator is run depends on the mode of operation. For constant current, power and resistance mode, the control loop needs to know how much current is flowing through the load, so LTC2451 must be read constantly. Since LTC2451 can only sample at 60 Hz, the control loop only runs 60 times per second, which is around every 17 ms. Meanwhile, in constant voltage mode, LTC2451 isn’t used in the control loop, so the control loop can be run at a higher frequency. I’ll use 1 kHz, or every 1 ms, for now, and see how that goes.
User Interface

The user interface has two jobs. Firstly, it must display information to an LCD screen. This display will show things like the voltage across the load or the amount of current flowing through it, but the display will also be used to navigate menus and settings. Secondly, the user interface will handle input from the user via an encoder. The encoder can turn clockwise or counter-clockwise, and it can also be pressed like a button. These actions will allow the user to change screens, navigate the menu, and change the system’s behavior.
The LCD library, which uses the TWI HAL, will allow the module to write characters to the display. The encoder library, which uses the GPIO HAL, will read user input. The Screens library will tell the user interface module what to do with the input from the encoder, as well as what text should be displayed on the LCD. The user interface is responsible for forwarding the encoder input to the Screens instance, and then outputting information to the LCD, as instructed by the Screens instance.
Updating the LCD display too frequently will make it hard to read, just like how rapidly flipping through the pages of a book makes it illegible. Therefore, I’ll have the display update once every second. However, if I’m navigating through a menu, then waiting a whole second every time I want to move the onscreen cursor would be maddening. Therefore, the display will update every second, or whenever the user provides an input through the encoder.
Debugger

The debugger allows the system to communicate with a computer through a serial, or COM, port. I’m currently using it to interface with the system before I have the user interface module up and running, since that’s my only other way of getting information into or out of the system. I’m not 100% sure yet what the debugger will be used for once the user interface is complete, but I’ll figure that out later. It might not be a bad idea to have the user interface module and the debugger fill the same role; that way, I can control the load via encoder, or through a terminal.
The debugger creates strings that contain information like the amount of voltage across the load or the amount of current flowing through it. It then sends this information out over UART. The UART can also receive information from the serial port, which is stored in a ring buffer. The debugger also has LEDs it can use to quickly indicate when an event has occurred. I currently only use one LED; it is on when the load regulator is running, and off otherwise. I’m using it to see how long it takes for the load regulator to run, and to confirm it is running at the right frequency; however, as more of the code gets written, I’m sure more LEDs will be used up.
The debugger has a lot of strings and performs a lot of string manipulation; this means the debugger is very memory and CPU intensive. This may necessitate removing the debugger if I run out of memory, or it adversely affects system performance. We’ll have to see as more of the code gets fleshed out.
The debugger is run once a second; at this point, it prints a message to the console on the computer, showing useful infomration. The debugger also checks if input was received from the console, and acts accordingly on that input. The input is used to change the load regulator’s mode of operation (constant current, power, etc.) as well as the target value (1 A, 5 Ω, etc.).
Conclusion
I hope you enjoyed the quick overview of the system architecture. Next, we’ll walk through each module and get a deeper understanding of they work.