RC Car: Intro

I’m really interested in digital signal processing and control theory, as well as mechatronics, so I thought a cool project I could work on next is designing and building a remote control car!

A remote control car should be fairly straight forward, in theory anyways. It consists of:

  • A microcontroller that acts as the brain
  • An RC receiver, to relay instructions to the microcontroller
  • Motors and motor drivers to move the car
  • Battery to power the system

I want to add a couple more things to the system:

  • IMU to read acceleration and rate of rotation
  • Wireless telemetry

Here’s my preliminary functional block diagram, showing the power tree and what connects to the microcontroller:

Preliminary functional block diagram
Power tree (top), system (bottom)

Let’s go through the diagram, block by block:

  • Battery: it’ll be 2S battery, which means two lithium ion cells in series. Each cell has a nominal voltage of 3.7 V (3 V to 4.2 V, depending on charge), which means the battery will have a nominal voltage of 7.4 V (6 V to 8.4 V). This will be driving motors, so it needs high output current and capacity.
  • Current sensor: I’ll probably use this to estimate how much charge the battery has left, or how long the system has been running for; also how much load is on the motors.
  • Boost: this will generate the 12 V to power the H-bridges, which drive the motors. The switch on this thing will have to be pretty beefy. Let’s say the motors draw 2 amps at 12 V; this means the battery will be outputting about twice that; all of this current will be managed by the buck, and the buck’s switch will have to deal with transient currents even larger than that. 12 V was chosen because it is a commonly used voltage for steppers and brushed motors.
  • Buck: this will generate the 5 V which will power electronics on and off the system. A lot of servo motors use 5 V, so this rail will be used to power servos if they’re needed for this project. It will also be used to power 3.3 V LDO, which will be used to power most, if not all, the electronics on the PCB.
  • LDO: the LDO will generate 3.3 V. Most servos, while powered by 5 V, need or use 3.3 V signals, so this rail is probably necessary. If not, I’ll just use 5 V for everything.
  • Microcontroller: I want to get a powerful microcontroller; no more messing with 8 bit AVRs (as handy as they are). Since I want to do digital signal processing and control algorithms, I’m thinking a 32 bit microcontroller with a floating point unit, filter accelerators, fast processing speed and lots of RAM. I’ll have to see what’s on the market, but it’s probably an STM32 microcontroller.
  • H-bridges: these can drive brushed and stepper motors. The only thing I’m on the fence about is how many H-bridges I’ll use; I only need two for this specific application, but it’d be nice to use the PCB I make for this project for other stuff too. If I want to make something like a CNC, then I’ll need 6 H-bridges to drive three stepper motors. I’ll probably make the schematic and PCB for 6 H-bridges, but only populate them as I need to. Another thing I’ll have to figure out is how much current I need for the motors. For this project, I’m not trying to make the car carry loads or traverse rough terrain, so the requirements are probably easy to meet.
  • IMU: this will let the microcontroller know how much it has moved and rotated. By integrating the acceleration twice, the system can determine its displacement from its starting point. By integrating the gryoscope output, the system can determine its orientation. This will probably only be filtered and then transmitted over RF for telemetry, but it leaves the door open for some cool algorithms, like making a Roomba.
  • USB: used for debug communication (RX, TX over COM port) and programming (using a bootloader)
  • RF TX/RX: I’ve never done RF PCB design, so this will be interesting. The PCB will have an RF antenna, transceiver and amplifier. There’s lots of these for Arduinos, so I’ll use a similar design. This channel will be used for wireless telemetry, allowing the operator to get the state of the system (displacement, voltage rails, current draw, etc.) and maybe send commands.
  • PPM: stands for Pulse Per Minute. This is standard communication protocol used in RC cars and planes. Instead of having 8 different lines to control 8 different motors, with each line carrying a PWM signal, PPM has those 8 PWM signals in series. This allows controlling many motors (or relaying many signals) on a single line. In this application, I’ll use this to control the RC car; I have an RC controller which communicates with an RC receiver, which outputs PPM. This will then communicate with the microcontroller to control the system.
  • LED: used for debugging and quickly indicating the status of the system.
  • ADC: used to measure voltage of the rails on the PCB, and to measure the current draw of the system. Very useful information to transmit over telemetry. I may just use the ADC channels on the microcontroller.
  • GPIO: I’m hoping to connect all the GPIOs on the microcontroller to a connector. This will make debugging a lot easier, since I can easily probe the system, but It’ll also allow expanding upon the PCB by connecting sensors and other devices to it. This will also provide the PWM signals to control servos.

As always, the design will change and evolve as I work on it. But for now, this is the plan; let’s see how it goes.

Hand Crank Generator: Built + Test

Now that the design is complete, let’s build this thing!

I printed all the parts on my 3D printer, and ordered 4 mm rods from Amazon. The rods are really hard to cut, so I had to use a Dremel. It’s good they’re very strong, but it makes manufacturing a pain in the butt. Anyway, here’s the finished product:

Hand crank generator

As you can see, the handle, pillow blocks, gears and motor are all mounted onto the wooden platform. Let’s turn the handle and see what comes out of the generator!

Three phase power!
Three phase power, zoomed in

The oscilloscope shows three phase power! I don’t know exactly how fast I turned the handle, but it was around 1 or 2 revolutions per second. What I do know is that it was a comfortable pace; not too fast or too slow. Sinusoids have a peak to peak voltage of about 10 volts. Definitely usable!

Next, let’s hook up the three phase power to a hex bridge and bulk capacitor:

Perfboard has the hex bridge and bulk capacitor. The headers on the right are connected to pins of capacitor.

When I turn the handle at a comfortable pace, I get about 11 volts. What’s cool is that the voltage across the capacitor depends on the maximum speed I turn the handle; if I turn it slowly, the voltage is pretty small regardless of how long I turn the handle for. Since there’s no load, if I turn the handle quickly for even a small amount of time, the voltage jumps up and remains large. The voltage will eventually drop due to parasitic resistance across the capacitor.

Now, let’s put a load across the pin headers (and across the capacitor) and see what the waveform looks like. I found a 100Ω resistor laying around.

The waveform shows the generator starting up, producing power, then turning off with a 100 Ω load

Let’s break down the waveform. First, the voltage across the resistor jumps up to about 8 volts when I start turning the handle. Then, since the speed I turn the handle isn’t consistent, the voltage jumps around between 7.44 volts and 9.68 volts. When I stop turning the handle, the capacitor discharges through the load.

Let’s say the average voltage across the load is 8.5 volts. Then, using P = V^2/R, the resistor is dissipating almost 3/4 of a watt. Sure enough, when you touch the resistor, it feels warm.

In order to regulate the output voltage better, since the last test showed the power is all over the place, let’s put a buck-boost regulator on the output:

Generator with buck-boost output

Now, when I turn the handle, the output of the buck-boost regulator is unstable for a second, then it snaps to 5 volts. I used my variable load to see how much power the regulator can output, and it was around 2 watts. The power output is throttled by the mechanical component of the system. When I try to output more than that, I can feel the plastic crank and gears struggle under the load. My biggest concern is the plastic that mates the crank to the shaft of the first gear will strip, so I’ll try rebuilding the system with that in mind. But not bad for a proof of concept!

Hand Crank Generator: Design

In my last post, we decided that we needed a gear train to turn the BLDC motor shaft quickly enough to generate useful voltages. Using the datasheet for the motor, I generated a 3D model. Then, I started to design the gears. The final design is shown below:

Hand crank generator, isometric view
Hand crank generator, top view

Turning the crank causes the cyan gear to rotate. The cyan gear will then rotate the magenta gear next to it, and this increases the rotational speed by a factor of two. This is because the magenta gear has half the radius as the cyan gear; in other words, the gear ratio is two to one. Then, the magenta gear spins the yellow gear, which again increases the rotational speed by a factor of two. Then, the yellow gear turns the orange gear, which increases the rotational speed by a factor of three. All together, the shaft of the motor (and the orange gear) will rotate 12 times (2 x 2 x 3) each time the crank is rotated once.

The dark green rods are 4mm thick rods, which are held in place by ball bearings and pillow blocks. The pillow blocks must be carefully placed in the assembly to make sure all the gears mesh properly.

Here are a couple of nuances to note:

  • All gears have a module of 2. This means the gear pitch diameter (the effective diameter of the gear) is 2mm x # of teeth. The cyan gear has 24 teeth, so it has a pitch diameter of 48 mm. The module must be chosen carefully. The smaller the module, the smaller the gear, which means the gear train takes up less space. However, it also means smaller teeth, so the gears won’t be able to bear much torque. Since I’ll also be 3D printing these gears, a smaller module also makes the gears harder to print. A larger module, on the other hand, means the gears are stronger and easier to print, but it also makes each individual gear bigger, making them more expensive (takes more filament to make). Note that all the gears have the same module.
  • The magenta and yellow gears are actually two gears fused together. This allows for the gear train to be compact: the cyan gear spins the small magenta gear with a gear ratio of 2:1. Then, because the small and large magenta gears are physically connected, they effectively have a gear ratio of 1:1. This approach is very common as it allows for compact and high ratio gear trains.
  • The gear thickness is pretty arbitrary. Thicker gear faces make the gears stronger, at the cost of price (filament) and space. The thicker the gears are, the longer rods you’ll need as well. In this case, since this is more of a proof of concept than a work horse, I chose the widths without much thought.
  • When printing the gears, play around with the shell thickness. The shell should be thick enough to make the teeth solid; the body of the gear can have infill. The teeth take the most beating and wear, so you want them to be as strong as possible.
  • The cyan gear is unique, as it has a hub. The cyan gear is attached to a cylinder, which extends past the pillow block, and is connected to the crank. This is system’s weakest link: the 3D printed cylinder is press fit into the 3D printed crank, and this is also where the most torque will occur. Making the overlap of the cyclinder and crank thicker, and reinforcing it with glue would be a good idea.
  • The pillow block near the crank has a different orientation than all the other pillow blocks. This was done to minimize the distance between the pillow block and the crank. As the person turns the crank, they’ll exert a lot of force on the shaft, so the pillow block should be as close as possible to the crank to support the shaft.
  • As shown in the 3D models, there is a lot of play. The yellow and magenta gear, for example, can move along the shaft. I’ll need to put in spacers and washers to fill the voids to avoid that.

Once the gear train is assembled, I’ll hook up the 3 phase power output of the generator (power input for the motor) up to a hex bridge and a bulk capacitor. I ordered the bridge off of Amazon, and had a 25 V, 6800 µF electrolytic capacitor laying around. Next time, I’ll build the system, and then we can give it a test spin!

Hand Crank Generator: Intro

I ran into a coworker who was giving away a bunch of electronics, and when I took a look, I found a BLDC motor! Let’s build a hand crank generator!

Motor or Generator

Normally, when you give a motor power, it spins a shaft. Interestingly, if you turn the shaft of a motor, most motor types actually generate power, just like a generator! The difference between a motor and a generator is what they’re optimized for; they both work on the same principle.

A BLDC motor has two components: the rotor and stator. The stator is a bunch of coils of wires, and pumping electricity through them causes them to generate a magnetic field, just like a solenoid. The rotor is the rotating shaft, and it has a bunch of magnets on it. When the motor is in operation, the coils of wire generate a magnetic field and force the magnets on the rotor to move to align with that field. The energized coil is then turned off, and then the coil next to it is turned on. The rotor will then rotate again to align itself with the new magnetic field. This process repeats over and over again, resulting in the rotor rotating.

If the motor isn’t powered, then turning the shaft (the rotor) will generate power. When the shaft is rotated, the magnets on the rotor move towards and then away from the coils of wires on the stator. This causes EMF according to Lenz’s law, which means the coils of wire will have a voltage difference between its two ends. With enough preparation, you can use that voltage difference to power something!

Why a BLDC?

There are several motor types available which could be used to generate electricity. For this project, I was looking for DC motors, since they’re smaller and easier to work with. The three main types of DC motors are brushed DC motors, steppers, and brushless DC (BLDC) motors. Even before I got the motor from my coworker, I was planning on using a BLDC motor. Why?

Brushed DC motors are great because of their simplicity for this project. You turn the shaft, and you get a rectified sinusoid out on its two terminals. Put a capacitor across it, and you’re done, right? The downside is that they, as their name suggests, have brushes. Inside the motor is a commutator, which wears out over time. To be fair, I probably won’t be using this generator all that often, but it’s still best practice to avoid brushed motors for that reason. Additionally, brushed motors would output single phase sinusoid, but I would prefer two or three phase power.

Stepper motors don’t use brushes, so no need to worry about the commutator failing. Even better, most stepper motors have two phases, which means spinning the stepper motor would produce two phase power (two sinusoids, 90° apart). Perfect, right? Well, two problems. One, stepper motors aren’t very comfortable to turn. By design, the shaft rotates in steps. That means when you try to turn the shaft, instead of one smooth motion, it’s a stop-and-go motion, which is pretty awkward and uncomfortable. That would also make it hard to turn the shaft quickly. Two, two sinusoids in quadrature is better than just having one phase, but surely there’s something better?

Enter the BLDC motor. It has none of the drawbacks I’ve mentioned previously. It doesn’t have brushes, so you don’t have to worry about wear. Its shaft turns smoothly, so you won’t have the jerky movement problem as steppers did. Best of all, almost all BLDC motors use three phases, which means it will produce three phase power! This means that you’ll have three sinusoid, all 120° apart:

Three phase power
Phase 1 is green. 120° later is phase 2, blue. 120° after that is phase 3, red.
Note that 120° after phase 3 is phase 1 again. The three sinusoids are spaced evenly across 360°.

The only problem with a BLDC is that it generates AC power, as opposed to DC, which is what almost all electronics use. We can fix that pretty easily using a three phase rectifier (which I call hex bridges) and a bulk capacitor:

V1, V2, V3 generate phase 1, phase 2, and phase 3, respectively
D1 and D2 allow P1 to source and sink current, respectively. Similar arrangement is made for other two phases.
C1 filters the output of the hex bridge, keeping the voltage around 1 V, though it’s unregulated. R1 acts as a load.

Specifications

I managed to find the specs for the motor my coworker gave me. The most important spec is the one that tells me how much voltage I’ll get out of the motor for a given rotation rate of the shaft. In other words, how fast do I have to turn the shaft to generate (say) 5 V? The number we’re looking for is the back EMF coefficient:

According to this, I need to rotate the shaft at 1000 RPM to generate 6.64 Vp. Seems impossible, but let’s do the math. The back EMF constant, when put in revolutions per second, is 0.3984 Vp/RPS. Since the hex bridge uses peak to peak voltage, not peak voltage, the back EMF is doubled to 0.7968 Vpp/RPS. In order to generate 5 volts, we’d need to rotate the shaft about 6 times per second. I tried doing that, and that’s pretty hard to do. The fastest I can turn the handle of anything is about… 1 revolution per second. Does that mean this project is impossible?

Gear Train

Fortunately not! Through the use of gears, rotation speed can be increased substantially. When a gear rotates and is connected to another gear who’s diameter is twice as big, the larger gear rotates at half the rotation rate of the smaller one. Reversing this, rotating the large gear makes the small gear rotate at twice the speed!

I’ve decided to use gears to increase the rotation rate by a factor of 12. This means that rotating a crank once will cause the motor shaft to rotate twelve times. Using the back EMF coefficient and gear ratio, turning the crank at 1 revolution per second will generate 9.5616 volts across the bulk capacitor. That’s definitely usable if you put it through a buck-boost regulator!

Only problem is that using two gears to achieve a 12:1 gear ratio is impractical. This would mean the larger gear needs a diameter 12 times that of the smaller one, which means the larger gear takes up 144 (12^2) times more area than the smaller gear. Not very practical. Instead, I’ll make a gear train that achieves the desired gear ratio in several steps: a gear ratio of 2, then 2 again, then 3. This gives an overall ratio of 2 x 2 x 3. Let’s take a look at the gear train next time!

System Testing

Let’s test the system’s current and power capabilities!

Current Control

One thing I noticed very early on in this project was that the system had very poor control over current, which is bad for a variable load. When I set the current to be 2 A, the actual current was about 1.4 A. When set to 4 A, the actual current was about 3 A. Due to this poor performance, I implemented the current control loop discussed earlier. This made the results better, and I moved passed it. Since the assembly is complete, and the code is all done, I decided to revisit this issue and find out why the error was so big without a control loop.

It turns out this is a hardware problem. As discussed before, an opamp compares the current going through the variable load and the output of a DAC, and then adjusts its output (and the load’s conductance) accordingly. Unfortunately, the opamp cannot do this if it is oscillating:

Output of opamp is unstable

Above is the opamp’s output. For a given DAC voltage, the opamp output should rise if not enough current goes through the load, fall if too much current goes through it, and be stable otherwise. But why, then, does the opamp output oscillate when not enough current goes through the load?

Opamp circuit

The large gate capacitance of the transistor causes a phase shift, resulting in oscillation. Since the power transistor is actually a bunch of smaller transistors connected in parallel, the gate capacitance can be quite substantial; 40,000 pF at 25 V, according to the datasheet. This, together with R8, causes the voltage at the gate to be a delayed version of the opamp’s output. Since the opamp is reacting to the current through the load now, but its actions are delayed, the system becomes unstable. The oscillating output of the opamp results in an average current that is incorrect. The control loop shifts the center point of the oscillation, resulting in correct average current, but it’s still oscillating, which can be disastrous for certain applications.

The key to fixing opamp instability is a feedback capacitor; C38 in this case. While R8 and the gate capacitance slows down the opamp’s response, increasing the delay around the loop, C38 provides a low impedance (quick) way for the opamp’s output back to the opamp’s input. In other words, C38 provides a shortcut to counteract the delay. C38 was originally meant to prevent oscillations, but apparently 47 pF is too small for it to be effective. After some simulations, I settled on a new value of 10 nF.

Opamp output, when going from no current to 4 A through load

The waveform above shows the opamp’s response when current through the load goes from 0 A to 4 A. As you can see, the opamp output is stable before and after the transition, and the transition is very smooth. No more oscillations!

After the oscillations were removed, I was curious how accurate the current control was without a control loop. The following test was done:

Current through load at various voltages and current settings
Control loop is disabled for this test

The results are shown above. We can conclude the following:

  1. The error is mostly independent of voltage across the load
  2. The error seems to change by about 10 mA per amp. At 1 amp, error is ~35 mA. At 2 A, error is ~25 mA, etc.
  3. The error is in the range of tens of mA

In a previous post, I used the following image:

Old control loop

Here, you can see that the output of the comparator depends almost entirely on the ADC reading. For example, if target_current is 1 A, but the ADC reports 0.5 A, then the output of the comparator will go up. If the ADC continues to report 0.5 A for some reason, then the output of the comparator will continue to increase, potentially reaching a massive number. This arrangement was necessary because when the opamp was oscillating, the output of the DAC didn’t correspond well to the current through the load. Therefore, it was necessary to let the comparator have a wide output range to counteract the oscillation. As stated before, setting target_current to 4 A would result in a current of 3 A. This would cause the comparator output to increase until the ADC reported 4 A.

This has two major disadvantages. Firstly, the comparator output has too much freedom. Let’s say that no power supply is connected to the variable load. If you set target_current to 1 A, then the ADC will always report 0 A since there’s no voltage across the transistor. This will cause the comparator output to increase to it’s maximum value, resulting in the transistor being as conductive as possible. Then, if power is applied, since the transistor is at maximum conductance, the power supply hooked up to the load will probably blow a fuse or trigger the overcurrent protection since the transistor has become a dead short.

Secondly, this control loop can only run as fast as the ADC can be sampled. Since the comparator output can only be updated when the ADC is read, and the ADC can only be sampled at 60 Hz, the control loop can only run 60 times per second at most. This is fine for constant current mode, but if you want to test a system with variable output voltages, and the system is in constant power or constant resistance mode, then 60 Hz is probably too slow.

By fixing the opamp instability, we can come up with a new scheme to address both of these issues:

New control loop

In the new control scheme, desired_current is calculated. Then, offset is added to the calculated value. I’ll address what and how offset is calculated later; for now, say it’s 0. In other words, desired_current is written directly to the DAC. This will affect the conductance of the transistor, and this will result in a change in the current through the load. This current is then measured and then read by the ADC. Now, the software reads the ADC, compares it to desired_current, then adds the difference to offset. The whole loop then starts again.

Let’s take an example. Say you want to set a current through the load as 1 A. According to the table presented earlier, the resulting current is 1.025 A, which is a little over. Now, offset becomes -0.025 A. Then, instead of the DAC being updated for 1 A, it is updated for (1-0.025) = 0.975 A. The current through the load is measured again, and offset is updated once more.

How does this new scheme address the two issues brought up earlier? Let’s take a look.

Firstly, this new scheme allows the DAC to update much more quickly. In the old system, the comparator output determines the value written to the DAC, and as said before, the comparator output only updates at 60 Hz. In the new system, the DAC is updated at 1 kHz since the DAC output depends primarily on desired_current. Sure the value written to the DAC is modified by offset, which does update at 60 Hz, but offset is very small; offset is for fine tuning. In other words, now that the opamp is fixed and the current through the load has a strong dependence on the output of the DAC, we can get close to the desired current through the load rapidly, and then approach the desired current using offset.

Secondly, offset is bounded to ±50 mA. In the old system, if the ADC showed that the current through the load was too small, then the transistor would become more and more conductive, which resulted in the possibility of blowing a fuse or resulting in overcurrent. Now, if the ADC reports that the current through the load is too small, then offset will continue to increase until it hits 50 mA. Then, since it has reached its limit, it will stop increasing. Now, the transistor will only conduct 50 mA more than it’s expected too. For example, if the load is set to conduct 1 A, but there is no voltage across it, then offset will increase to 50 mA. Now, when power is applied across the load, instead of blowing a fuse, the load will only consume about 1.05 A. As the current through the load is measured, offset will eventually decrease, and the current through the load will become 1 A.

Thermal Testing

How much power can this thing handle? Let’s test it out!

Disclaimer: unfortunately, I only have a power supply that can output about 150 W. I’ll can test up to the limit, but then I’ll have to extrapolate if the load can handle more.

Calculated junction temperature vs dissipated power

The above shows the results of my experiment. Thermocouple 1 is the most important temperature, as it was placed very close to the transistor on the heatsink. This is the temperature of the heatsink. Of course, the temperature is overestimated due to its proximity to the load, but I wanted to be conservative. Now, in order to calculate the junction temperature, I used the trusty equation ΔT = P * RTH. Here, ΔT is the difference in junction temperature and heatsink temperature, P is the dissipated power, and RTH is the sum of all thermal resistances between the transistor’s junction and the heatsink. The datasheet gives the thermal resistance from junction to case (of the transistor), and then case to heatsink. I also applied thermal paste between the case and the heatsink, and accounted for it in my calculations, but it is ultimately negligible as it adds almost no thermal resistance. Using the thermal equation, we can calculate the junction temperature as the sum of the temperature reported by thermocouple 1, and the product of power and thermal resistance.

The transistor is rated for a maximum junction temperature of 175 °C. At 150 W, it reaches 76 °C. Quite a ways to go! Fortunately, the data is almost exactly linear. Increasing the power dissipated by 25 W increases the junction temperature by 8 °C. Knowing this, we can extrapolate that at 450 W of dissipated power, the junction temperature will be 173 °C. This sets the maximum power of the load to 450 W, but to be conservative, let’s say that the maximum power is 400 W.

Conclusion

After fixing a problem with the hardware, we were able to improve the system performance by improving system response speed to changing voltages across the transistor. Thermal testing has shown that, conservatively, the maximum power limit for the system is 400 W.

Closing Thoughts

The opamp being unstable is an interesting example of the interaction between hardware, software and system engineering. I find it fascinating that the change in behavior of a single component led me to rewrite the load regulator control loop, which in turn hugely improved system performance. Really goes to show how much impact hardware can have on the software architecture and system performance. The moral of the story is that a system can be hobbled by poor hardware design. Conversely, investing a little more in better hardware can improve software and the system by leaps and bounds, and make your life a lot easier. (Another moral is when you encounter a problem, probably best to figure it out immediately rather than ignoring it and forging on. Though if I did that for this project, I wouldn’t have learned the moral above. Can’t win, right?)

Assembly

Now that the software is complete, let’s put the system together!

PCB holder
PCB holder with PCB

During testing, I used a 3D printed jig to hold the PCB so that I could easily hook things up to it, and not have to worry about it dragging across the surface of my desk. It’s also useful for making the PCB level so I can easily probe the test points. This setup was great for writing the register level drivers, but I couldn’t test the variable load with it easily.

PCB, heatsink and fan on base

I used the new setup shown above when I wanted to test the variable load and fan. I also added the heatsink so I could keep the load cool while testing it. The fixture I used above shows how the fan is mounted to the heatsink, and everything is on a platform for the sake of compactness. This is the setup I used to write the rest of the code.

Once I was happy with the code, I designed and then printed the full enclosure. I had a lot of trouble printing something so big, as I got a lot of warping. I got around this by (1) adding brims, (2) reducing the bed temperature, and (3) putting a lot more effort into bed leveling. The print took 32 hours, and 276 g of plastic.

Everything inside enclosure

The picture above shows the assembly. First, note the threaded inserts; this will be used to mount the cover onto the enclosure. Second, note the thick green wires; these are 10 AWG and should be able to handle about 30 amps. These wires had to be bent before being assembled since they were so stiff, so they were more like metal rods. Third, the orange wire shows the thermistor going from the PCB to the variable load. The thermistor was taped to the heatsink with a tiny dab of thermal compound. Lastly are a bunch of connectors to the PCB, like the banana jacks for the power supply being tested, and the barrel jack for powering the PCB and fan.

Now let’s look at the variable load, heatsink and fan. I drilled and then threaded holes on the heatsink for the variable load, then mounted it with thermal compound. The heatsink was then put inside the enclosure. Note that the walls and screws keep the heatsink from moving inside the enclosure during transport. The fan was then mounted to the heatsink and the right wall of the enclosure.

Finished product!

Above shows the enclosure with a cover! The LCD is mounted to the cover via screws, and the encoder has threads that are used to mount it to the cover. I also added a power switch to turn the system on and off since connecting and disconnecting the barrel jack was inconvenient.

Software Testing

Now that all the modules are done, and strung together in main, let’s do some testing. Specifically, let’s look at the timing.

For this testing, each module turns an LED on (active low) when it starts running code, and off when it finishes. The LED is left alone if the code does not run. By doing this, we can see how long it takes for a module to run, as well as how frequently it runs.

Load regulator, desired current update duration
Load regulator, offset current adjustment duration
Load regulator, desired current update period

The load regulator adjusts the desired current, and then adjusts the offset current. The desired current takes 132 us to run, while the offset takes 196 us to run. The desired current is updated at about 900 Hz. The code ideally updates at 1 kHz, but 900 Hz should be frequent enough for our purposes, since this load is designed for inputs that change slowly. The current offset is adjusted at about 60 Hz, so that’s fine.

Updating the desired current and adjusting the offset both probably suffer from the same slowdown: waiting for the hardware. For example, in order to update the desired current, the ADC must be read, which involves setting up and then waiting for a 16 bit SPI transaction. Likewise, in order to adjust the offset, the current monitor ADC must be read, which again requires the code to wait for the I2C transaction to begin and end. The way to address this issue is to do the reading in two steps: first, set up the read. Then, exit the code and do something else. During this time, the read will complete. Finally, when the load regulator module is run again, it can just act on the received data. This eliminates the need for the code to wait for the data, speeding up execution in exchange for increased complexity.

UI module, updating LCD screen
LCD screen update, zoomed in

The user-interface takes about 85 ms to update a screen, and the screen is updated every second, assuming no user input. It’s so slow for two reasons. Firstly, the entire screen isn’t updated in one go. In order to avoid blocking the load regulator module, which has the highest priority, the user interface module transmits one character to the screen, and then exits. Since the screen is 4×20 characters, the module must be run about 80 times per screen. The second reason the screen update is slow is because the screen is updated through I2C at 100 kHz. In order to save GPIO, I am using an I2C interface, which is notoriously slow; additionally, I can’t go 400 kHz like I can with the current monitor ADC due to limitations of the IC on the LCD module. This means that each character update, which must be done 80 times per screen, is painfully slow.

LCD update, set up
LCD update, single character

Let’s take a closer look at the 85 ms. There’s actually two different durations: the set up and the character update. The set up is where the code determines what the current screen is, what the user input does (if there were any), and what characters should be transmitted to the LCD. All combined, this process takes 2.25 ms to run.

Next is updating the screen, one character at a time. The time it takes to change one character on the screen is 0.81 ms, and this is done 80 times. Ideally, the code would take 80*0.81+2.25 = 67.05 ms to run, but this is assuming no interruptions occur.

Speaking of interruptions, you’ll see that the time between each character update varies; some pauses are longer than others. These pauses are when the user interface module stops updating the screen so that other modules can execute; how long it takes for the code to get back to the user interface determines the length of the pause. Let’s see how the load regulator and user interface modules affect each other:

LCD update and load regulator

CH1 is the LED for the user interface, and CH2 is the LED for the load regulator. As you can see, the two modules impact each other’s performance. When the LCD isn’t being updated, the load regulator is being sampled at a higher rate. Conversely, because the load regulator is being read in-between character updates, the screen isn’t being updated as quickly as it could be. Let’s take a closer look at how the two modules interleave their activity:

Load regulator, away from LCD update

As mentioned previously, when the LCD isn’t being updated, the load regulator runs at about 900 Hz.

Load regulator, during single character update

When the user interface module is updating characters on the LCD, the load regulator is cut down to about 500 Hz, which is a big hit. In the example above, one character is updated to the LCD. Then, the load regulator wasn’t set to run yet, so the code updates the next character. This causes the next load regulator run to be delayed, impacting performance.

Load regulator, during LCD set up

Even worse is when the user interface setup occurs. Since the setup takes longer than two character updates, the load regulator frequency is reduced even further, to around 300 Hz.

It is ultimately a good thing that the two modules interleave their activity; if one module hogged the CPU, then the system performance would suffer much worse than what we’re seeing here. Additionally, since the variable load is designed to work with constant or slowly changing input power, the intermittent reduction in how frequently the load regulator module is run is fine by me. Lastly, as bad as it seems, the user interface is only run once a second, which is very infrequent, or when the user provides input, which means the user is probably browsing the menu, and the variable load’s performance during that time probably won’t be critical, since the user is most likely about to change the variable load’s mode or target value.

If you can’t stand the user interface interfering with the execution of the load regulator module, then I recommend using interrupts to run the load regulator, rather than a timer like I did. I used timers because it makes the coding and debugging simpler. For my purposes the current behavior is acceptable.

Temperature regulator duration

The temperature regulator takes 496 us to run. This is a shockingly long time for such as simple code. All it’s doing is reading an ADC, then updating the duty cycle! As previously mentioned with the load regulator, the slowdown is most likely starting and then waiting for the ADC to complete its conversion, which for the ATMEGA32U4 can take a while, since the ADC isn’t very fast. Like before, the way to address this would be to execute other code while waiting for the ADC conversion to complete, reducing the slowdown significantly. However, since this module only runs twice a second, it has very little impact on the system performance.

Debugger duration

The debugger takes 147 us to run, and runs once a second. It’s a lot faster than I expected. Since the debugger uses the string library, and uses functions like strcat and strcpy, I was worried the debugger would take milliseconds to run. A nice surprise!

Funny story, actually. The current code only sets up the string to transmit, and then lets an interrupt transmit the string one character at a time. That means the software doesn’t have to wait for the transmission to complete, reducing the duration of the debugger module. The original code I wrote for the debugger didn’t do that, and the code actually had to wait for the whole string to finish transmitting before moving on. Since the baudrate is slow (compared to I2C or SPI, for example), the debugger actually took hundreds of milliseconds to complete! Shows you how much time-saving interrupts are capable of.

Overall, I’m very happy with the timing of the system. The most important timing for this project is the load regulator, and though it’s disappointing that it’s not at 1 kHz, and it can drop down to around 300 Hz, I doubt that it’ll matter in the end. The difference between 900 Hz and 1 kHz is negligible, and the dips in timing won’t matter since they happen so briefly and infrequently. Since the code seems finished, let’s move on to the assembly!

Main

Now that all the modules are complete, let’s see how they’re tied together in main.

Start of main

Above is the start of main, which consists mostly of constructors. In order are the main timer, load regulator, temperature regulator, user interface, and debugger constructors. Note that the temperature regulator, user interface and debugger take the main timer as an argument, as the main timer is used by those modules to know when it should run its code. Additionally, the user interface and debugger take in the load regulator and temperature regulator, as the two modules send commands to and receive information from the regulators.

After the constructors, the sleep mode is set up. If none of the modules need to be run, then I don’t want the microcontroller to be running through the loop over and over again looking for something to do. Instead, after one iteration through the while loop, the microcontroller will go to sleep and wake up when any interrupt occurs.

Next is enabling the global interrupts, allowing the timers to run, as well as calibrating the zero current for the load regulator. Note that calibration makes the current through the load zero, which is probably a good thing since you don’t want the system to be sinking power when it’s not fully awake.

Infinite loop inside main

Next is the infinite while loop. Thanks to burying all the complex code inside modules, the loop itself is very simple. First is the running the load regulator. The load regulator checks the current through the load, and the voltage across it, and then takes action based on those two values. Next is the temperature regulator: it checks the load temperature, and then increases or decreases the fan speed as necessary in automatic mode, or sets the fan to a set speed in manual mode. After that is the UI module: it checks for user input, modifies the system if necessary, and updates the LCD screen.

Now comes something slightly different: the fail-safe. In the future, as I’m modifying the modules, it’s possible that I break something and risk damaging the load. For example, maybe I change the temperature-to-duty-cycle equation in the temperature regulator and make the fan ineffective, or the load regulator makes the load dissipate way more power than it should. In either case, it’s good to have a hard-stop built into the system. In this case, if the temperature reported by the temperature regulator exceeds a specific temperature, then the current through the load is immediately set to zero, regardless of the state of all other modules.

Next is the debugger module. The debugger outputs useful information over serial, and then reads and acts on commands it receives. Note that the debugger may not be compiled, depending on the value of DEBUG. If it’s not compiled, then it’s one less thing for the system to worry about.

Lastly is setting the CPU to sleep and waking it up. sleep_enable shouldn’t be interrupted, so cli disables interrupts, and then sei enables it. Then, since sleep has been enabled, sleep_cpu puts the CPU into whatever sleep mode was set by set_sleep_mode. The system will then halt at this command until an interrupt occurs; in this case, it’ll be until the user inputs a command through serial or encoder, or a timer interrupt occurs. Either way, this allows the system to save a little bit of power by preventing the CPU from running when there’s no need.

When an interrupt occurs, the system moves on from sleep_cpu and disables sleep mode. sei is also called again to make sure that interrupts are enabled.

That’s the main! Now that all code has been written, let’s do some testing!

Debugger Module

The debugger module is very straight forward. Let’s take a look!

Debugger.h

Above is the header file for the Debugger module. The debugger has 4 main components: a reference to the load regulator, a reference to the thermal regulator, a reference to the main timer and an instance of the HAL_UART class. The references to the regulators are necessary so that the debugger can retrieve information from the modules, as well as issue commands to them as necessary. The reference to the timer is needed so that the module knows when it should be executing code. Lastly, the HAL_UART class is necessary for the module to read and write information through serial communication.

There’s only one method for this class: run_debugger. It is responsible for outputting information about the system through serial, as well as reading user input.

Start of run_debugger

First, the method checks the time and sees if enough time has passed for the rest of the method to run. If it has, then it grabs the measured voltage and current information from the load regulator module, along with the desired current and current offset. Then, if the load regulator is in constant current mode, then a string is constructed in char_tx_buffer to display the information. In the case of constant current mode, the measured voltage, measured current, desired current and offset are displayed.

String construction for constant power mode

In constant power mode, measured voltage, measured power, target power, desired current and current offset are displayed.

All other modes have information displayed similar to constant current and constant power mode, so I won’t be going through all of them. When the mode is OFF, only the measured voltage is displayed.

Duty cycle and transmission

After the switch statement, regardless of what mode the load regulator is in, the debugger module adds the duty cycle of the fan, which it reads out of the temperature regulator. This completes the string construction, so the module finally transmits the string using the Serial class.

One thing to note is that the string is being transmitted using send_string_int, as opposed to send_string. send_string_int only sets up the transmission, and then the actual transmission is done using an interrupt handler. This allows the debugger module to transmit long strings without significantly impacting the timing of the rest of the system.

Module checking for input

After setting up the transmission, the debugger module checks for input. read_rx_buffer returns the number of characters received; if nothing has been received, then the if statement doesn’t execute, and nothing happens. If a byte (or more) has been received, then the first byte is read. ‘c’, ‘p’, ‘r’, ‘v’ and ‘o’ set the load regulator to constant current, power, resistance or voltage mode, or OFF, respectively. If a number has been received, then the number is interpreted as a floating point, and the target value of the current mode is updated. For example, if “2.5” is the floating point and the load regulator is in constant power mode, then the load regulator will update to have a target power of 2.5 W. If anything else is received, then the input is invalid and ignored.

The debugger module is used for testing and is very useful when an LCD screen is not hooked up to the PCB. It’s pretty simple and limited, but fine for this project.

UI Module

UI Architecture

The User Inteface (UI) module is responsible for three things:

  1. Reading user input
  2. Displaying information to the LCD
  3. Managing a menu interface

Let’s look at the steps above in order.

Reading User Input

The user input will be through an encoder, EN11-HSM1BF20. The encoder can detect when the handle is rotated clockwise or counter-clockwise. The user can also press down on the head, and this will cause the encoder to act like a button.

From EN11-HSM1BF20 datasheet

The encoder has a quadrature output. As the image above shows, when the encoder is left alone, both A and B will be high. When the encoder is rotated clockwise, then B will go low, then A will go low, then B will go high, then A will go high. If the encoder is rotated counter clockwise, then the process is reversed: A will go low, then B will go low, then A will go high, then B will go high.

Interrupt Service Routine (ISR) for reading encoder

The ISR above handles reading the encoder. It is triggered by a pin change interrupt, which will occur when the encoder is rotated or pushed. The highlighted portion shows how the direction is determined: the code will wait until both A and B are low. This “arms” the ISR to determine the direction the next time the ISR is called. When the ISR is called again, it will read A and B and see which one is high. If A is high, then that means the encoder is turning clockwise. If B is high, then the encoder is turning counter-clockwise.

Wait a minute! The datasheet says that if B leads A when the encoder is turned clockwise! In order to keep the ISR as simple as possible, I coded it to assume A leads B when the encoder is turned clockwise. Then, the Encoder class, which reads the ISR’s output, will invert the direction as necessary.

Now let’s take a look at the button. The button by itself is trivial; if the button is pressed, then the signal will go low, when the button is released the signal will go high. Therefore, we can just read if the button is pushed or not. However, I wanted to implement something more advanced. I wanted the system to distinguish between a button press, and a long press. If the button is pushed quickly, then this will act like an OK or ENTER. If the button is pressed for a certain amount of time, then it will act like a BACK or CANCEL. This will greatly improve the user interface.

Pin change ISR (left)
get_pressed method (right)

The ISR is shown again above on the left. The highlighted section shows how the button is read. When a falling edge is detected on the button pin, which means the button has been pressed, the time is saved, a flag is set, and pressed is updated. When a rising edge is detected, which means the button is released, the flag is cleared.

The real magic happens on the right, which is the method used by the Encoder class to read the encoder. Here, if pressed hasn’t been updated, then there’s nothing to do. If pressed has been updated, then we know the user is trying to do a short press or a long press. This is where the flag comes in: if the flag is still set, that means the button is continuing to be pressed, so the code compares the current time to when the button was first pressed. If the two times are sufficiently different, then this is considered a long press, so the method returns LONG_PUSH. If the two times aren’t sufficiently different, then we’ll have to wait a bit to see if the user lets go early enough for it to be a short press, or holds on long enough to be a long press. Since the results are inconclusive, the method returns NO_PUSH for now. If the user lets go early enough to be considered a short press, then the ISR will clear the flag. Then, get_pressed will see that the flag has been cleared, and return PUSH. If sufficient time has passed for the push to be a long push, then the flag clearing is inconsequential since the method will already have determined that the push was a long push, and returned LONG_PUSH already.

Encoder debouncing / filtering

One quick note about the hardware. Above is the original debouncing circuit I designed; R10, R13 and R15 act as pull-ups, while R11, R12 and R14 are current limiting resistors in series with the encoder’s A, B and button pins. C14, C15 and C16 act as filtering capacitors. This circuit didn’t perform very well, so I did a little researched and discovered that this is not the recommended way to debounce encoders. I changed the circuit so that the button, A and B have 10 kΩ pull-ups on them, and then a RC filter (10 kΩ, 0.1 µF) between the signal and the GPIO pin on the microcontroller.

Displaying Info on LCD

To be clear, this section is interfacing with the LCD. How the system determines what text should be displayed will be discussed in the next section.

I really wanted to code everything from scratch, but I ran into a wall here. The problem is that I ordered this part from Amazon, and I couldn’t find much documentation for the product. Pretty much all the comments are “just use the Arduino library.” I did a little digging on my own and found a document that seemed helpful, but I couldn’t really get my code to work using that documentation. It’s possible I wasn’t following the document correctly, but since I wasn’t even sure if the document I found was even the right one for my LCD screen, I decided to swallow my pride and look for a third party library to use instead.

Matthias Hertel copyright notice

I’m using Matthias Hertel’s library. The link to his website is here. Originally, this code was written for the Arduino, so Matthias’ code uses Arduino libraries. Since I’m not using the Arduino IDE or libraries, I had to modify the code. The highlights are:

  • Removed references to Print.h (get rid of header and inheritance)
    • Define print in CPP file
    • write is no longer virtual method
  • Removed references to Arduino.h
    • Replace delayMicroseconds with _delay_us (from AVR library util/delay.h)
  • Removed references to Wire.h
    • In _write2Wire, replace calls to Wire with calls to PCF8574 class (which is based on HAL_TWI)
    • Remove references to Wire methods in set up
  • Remove delay in _sendNibble

Managing a Menu Interface

I’ve never written what I would consider to be a “good” user interface before. Every time I tried, it became spaghetti code. I could never really explain what I did to others, or to my future self, which made maintaining and expanding such messy code a disaster. I wanted to try to fix that with this project, so here goes.

I have two files for managing the menu interface: User_Interace.cpp and Screen.cpp. User_Interface.cpp is responsible for printing text to the LCD screen, and passing user input (from the encoder) to the current screen. Note that User_Interface.cpp is NOT responsible for figuring out what to print; it merely prints what it’s told to print. Likewise, User_Interface.cpp is NOT responsible for figuring out what actions to take based on a certain input; it merely passes the user input along to another part of the code. I designed the system this way because the text to print to the LCD, and what actions to take given user input is context dependent; that is to say, both of those things depend on what screen you’re on. For example, user input might scroll text, or it might change a value, depending on what screen you’re on.

That’s where Screen.cpp comes in. Screen.cpp is responsible for determining what text should be printed, and what to do with user input. Note that Screen.cpp isn’t responsible for actually printing text, and is only responsible for knowing what SHOULD be printed. Screen.cpp takes care of the context dependent part of the user interface; for some screens, rotating the encoder does nothing. But for other screens, rotating the encoder moves a cursor or changes a value. How do we manage that?

In my attempt to make the code maintainable and easy to expand, I’ve decided to use inheritance. Each screen (for example, main menu screen, adjust mode screen, adjust target value screen) will inherit from the base class Screen. Before looking at the code, let’s see how the user interface will look:

UI Screens

The VL screen shows the status of the variable load: voltage, current, power, and resistance. It also shows what mode the load regulator is in (OFF, CC, CP, CR or CV), as well as the target value (in amps, watts, ohms or volts). Lastly, the screen also shows the load’s temperature and fan speed. This is the screen on start-up. The short press on the encoder will enable or disable the load regulator. Long press will take you to the main menu screen, which will allow you to access all other screens. They are:

  • LR Mode screen: lets user select operating mode (OFF, CC, CP, CR, CV). If OFF is selected, then return to main menu. If any other mode is selected, then go to LR Val screen.
  • LR Val screen: allows user to adjust target value of selected mode. For example, in CC mode, allows user to select amount of current to flow through the load. If the current mode is OFF, then user it sent to LR Mode screen. After user has selected target value, return to main menu and update Load Regulator with new mode and / or target value.
  • TR Val screen: allows user to select fan speed, or let it be chosen automatically based on load temperature. After user has made a selection, return to main menu.
  • Info screen: displays information about the system. User does short press to return to main menu screen.

The image of the flow of the screens, as well as the list above shows that what the next screen is depends on the current screen. So Screen.cpp will need to tell User_Interface.cpp what the next screen should be.

Now that we know how the screens are interconnected, let’s look at the code and inheritance:

Screen class in header file

I’m only going to point out a few key points:

  • text is the variable that contains any and all text relevant to the screen. For example, the main menu is composed of 5 lines, which are “Back”, “Set mode”, “Set target”, “Fan control” and “Info”. These 5 strings will be stored in text.
  • update_screen_chars is what User_Interface calls to figure out what should be printed to the LCD screen. For example, since main menu is composed of 5 lines of text, but the LCD only has four lines, update_screen_chars will have to figure out what 4 lines should be printed. This is determined by using a cursor that moves around the screen.
  • update_text and handle_input are virtual functions. This means that these methods can be redefined by whatever class inherits from the Screen class. This is important because how text is updated, and how user input is handled, is screen dependent, so it makes sense that the derived classes override them.

Essentially, the base class provides basic methods used by all screens, as well as methods that User_Interface can call for all screens.

So now, two questions remain. How do you program a screen, and how do you jump between screens? Let’s look at the first question.

Here’s the code for the main menu screen:

Code for Main Menu screen

There’s three parts. First is the constructor; it just sets up some data members, and then loads defined strings into text (“Back”, “Set mode”, etc.). Next is re-defining update_text. For the main menu, the text doesn’t change, so there’s nothing to do here. “Back” is always going to be “Back”. Lastly, we re-define handle_input. Here, user input is passed in (CW, CCW, short push or long push), and a screen is returned. If no input is provided, then nothing happens and the method returns main menu screen. CW and CCW move the cursor around, and long press causes the method to return VL screen. If the user does a short press, then the code determines where the cursor is, and uses that to determine what screen should be returned.

But what about update_text? Let’s look at a second screen: TR Val screen.

update_text for TR Val screen

Above is the snippet of code for updating text. index is increased or decreased by rotating the encoder. The screen should display “AUTO” if index is 0, and then it should display “5%”, “10%”, etc. (up to 100%) based on the value of index. Since this requires the string to be displayed to change, update_text is not an empty function. Here, you can see that text is updated using strcpy and strcat.

Great! Now we know how to create a screen: make a constructor, then re-define the virtual functions. Now how do we jump between screens?

update_screen method in User_Interface

update_screen is the method main calls in User_Interface. This method is where User_Interface prints text to the LCD, passes input to the screen, and jumps between screens. Let’s focus on the last part first.

In the image above, the highlighted section shows how jumping between screens is done. Looks pretty simple, right? handle_input will return what the next screen should be, and this goes into a switch statement. Based on this, cur_screen, a pointer to the Screen class, is updated to point to VL screen, or the main menu screen, etc. One nuance I’d like to point out, however, is that cur_screen is a pointer to the base class, not the derived class (VL_Screen, Info_Screen, etc.). This is another advantage of using inheritance. If you wanted to have a single pointer to point to an integer, float, and character, you would constantly have to type-cast it. However, since all screens derive from the base class, you don’t have to type-cast cur_screen since it is a pointer to the base class. This makes jumping between screens straight forward!

We’re almost done. By now, we have Screen.cpp which handles the flow chart of screens, as well as handling user input. Now all that’s left is actually printing strings to the LCD. Should be pretty straight forward, right? For example, for the main menu, all you have to do is print “Back”, then “Set mode”, etc. and then exit the function, right? Unfortunately not.

Remember that the LCD is hooked up to the microcontroller through I2C / TWI. The current monitor ADC is also on this bus, and it must be accessed at 60 Hz, or every 16.7 ms. Printing a whole screen to the LCD takes way longer than 17 ms, so if we were to bulk print to the LCD, then we would prevent the system from accessing the current monitor ADC, reducing performance. In order to prevent this, every time update_screen is called, only one character is printed to the LCD. Then, when update_screen is called again, the next character is printed. This way, communication to the ADC and LCD are interleaved, preventing one from interrupting or interfering with the other. I used a logic analyzer and confirmed that this was the case:

Markers (A1, A2, B1, etc.) indicate reading ADC

As you can see above, when the LCD screen is not being updated, the I2C bus is solely used to read the ADC every 17 ms. However, when the LCD screen needs to be updated, the microcontroller does both at the same time. The ADC is still read every 17 ms, as indicated by the markers. Besides this, however, when the I2C bus isn’t being used, the microcontroller fills the unused time to communicate with the LCD, updating the screen.

One very last thing. I tried clearing the screen every second or so in order to update the text on the LCD screen, but I noticed that causes the screen to flicker. Since this is very annoying, I decided to just fill up the 20 x 4 character array each time, which means that all transactions will be for 80 characters. This increases how long it takes to write a single screen, but as the image above shows, the length of time to write a screen isn’t a problem since it doesn’t interfere with the ADC.

Thanks for reading through this long post. Hopefully this gives insight into how the user interface works!

Design a site like this with WordPress.com
Get started