Analog computation: solving simultaneous linear equations

Analog computation: solving simultaneous linear equations

Prior to the digital revolution, analog electrical computers were used to solve complicated mathematical equations (missile guiding, aerodynamics, stability, etc.). Many systems that can be modeled as a set of equations (linear, non-linear, differential, ordinary, partial, etc.) can be built as an electrical analog. Put very simply: If you can create a circuit that models y = x + 2, then you can use a voltage source to input a value for x and then use a voltmeter to measure the value of y.

In contrast, a digital computer solves y = x + 2 by using stored data and program memory in tandem with hardware. Very likely x is stored as a binary value somewhere in memory. A machine instruction generates control signals, addressing x in memory and enabling that value to be written to the data bus and stored in a general purpose register. A second machine instruction will generate control signals to use the adder in the arithmetic and logic unit to add the immediate value 2 to the data stored in the general purpose register (hopefully, the value of x, if the software was written correctly). After the addition has been completed, the result will be stored into a general purpose register. A savvy programmer can now take that value and write it to an external display.

Digital computation has overtaken analog computation because memory is abundant and cheap (we can store lots of variables to a high level of precision), noise is easier to reduce/eliminate in digital circuits than analog circuits (we can trust the result of our computation, assuming our algorithms are well-written), processors are very fast (we can get a result quickly), and there are numerous peripheral devices we can use to display the outcome (it’s easy to see the answer without using a voltmeter).

While I love digital computation, I find analog computation equally fascinating. One of my “to-do list projects” has been to build an analog computer that can solve 2nd order, linear, ordinary differential equations. I thought to myself “before I do that, let me do something easy!” So this post is to detail the “something easy” which is a circuit that can solve two simultaneous linear equations. (Spoiler alert: this was surprisingly NOT EASY.) The two simultaneous linear equations that the circuit is solving are equations in the form of

a_0x+a_1y+b_0 = 0 \\ a_2x+ a_3y+b_1=0

Coefficients a0 and a2 are generated by scaling x between -1 and +1. Coefficients a1 and a3 are generated by scaling y between -1 and +1. Coefficients b0 and b1 are generated by scaling between two different voltages (to be explained below). The two equations are executed using a summing circuit (which must by design contain some feedback because x and y are outputs, but are also used with scaling circuitry to generate voltages of a0x, a1y, a2x, and a3y). Fortunately, addition can be performed using a summing amplifier circuit (explained in chapter 4 of my Circuit Analysis textbook).

All of what I mentioned isn’t too terribly difficult to do. However, the output values of x and y can only be measured with a voltmeter. The values of each coefficient can be calculated with a voltmeter and a calculator. So while the proof of concept was nice, I wanted a circuit that could display the values of each coefficient, constant, and output value to an LCD screen, cutting out the need for external components to figure out what the two simultaneous equations are. I think the best way to discuss the design is to show a block diagram, and then discuss each block before talking about the final project.

Block diagram

I teach my digital systems students that analog circuits are harder to design than digital circuits. This project really made that explicitly clear to me. In fact, some of the most difficult aspects of this project are in interfacing the two types of circuits together. But I digress. Let’s take a look at the block diagram, below. Note that the block labeled “voltage regulators” is missing output arrows. This would have really complicated the block diagram, so I left out each of the voltage source connections. Don’t worry, I’ll discuss them below.

Block diagram of the equation solving circuit. A dual power supply acts as an input to voltage regulators. Output arrows of the voltage regulators not shown.

Coefficient generators feed into equation solver. Output of equation solver feeds into coefficient generator and also into inverters. The output of the inverters outputs to the coefficient generators and analog MUX. 

Constant generators output to the equation solver and analog MUX.

Analog MUX outputs into a voltage scaling unit. Output of the voltage scaling unit goes into the MCU. The MCU has control bit outputs to the analog MUX, and outputs to an LCD screen.

Supply and voltage regulation

I wanted this circuit to be able to handle positive and negative values for x and y between -10 and +10. (Any linear multiple of the two equations can be modeled by simply solving a scaled version, so technically this circuit will be able to solve all solvable versions of the two equations assuming we have infinite precision on our inputs and outputs.) Because of the need for positive and negative values, I needed a dual power supply. The supply I use is from a Jameco kit that I soldered together. It sources roughly +/- 17 V (it’s nominally +/- 15 V but mine definitely goes above that range, which is fine). Both the plus and minus voltages are adjustable, but I max both of them out when used with this circuit.

The microcontroller requires +5 V and ground. Thankfully the dual supply has a ground terminal. I used the LM340 to create a +5 V signal. (This is a discontinued part. The 7805 is nominally identical but I’ve had some noise issues with that regulator that have made me learn my lesson, again, to always put a bypass cap on voltage regulator outputs. Lesson learned.) After initially scaling the constants b0 and b1 between +/- 10 V and noticing that the outputs x and y would pretty much always saturate, I decided to scale the constants between +/- 5 V, so I also used a 7905 negative voltage regulator to obtain a -5 V signal. (I could probably use an even smaller voltage range for the constants, but refuse to include any more regulators than I already have on this circuit.)

I use the 7812 (+12 V regulator) and 7912 (-12 V regulator) to supply all summing and inverting op-amps. This allows the outputs to remain within the range of +/- 10 V without worrying about saturation. (I did some tests and found saturation occurred at +/- 10.5 V with a +/- 12 V supply.)

The circuit also requires +/- 10 V to use as the VDD and VEE connections on the analog MUX. (The analog MUX only supports a maximum voltage span of 20 V, hence the +/- 10 V limitation.) Unfortunately, 10 V is not a standard voltage signal. While there are +10 V regulators, there are no -10 V regulators. However, there are adjustable voltage regulators. I first used a potentiometer to determine the resistance values I’d need to obtain the desired output voltage, and then used fixed resistors. Thankfully, both the positive (LM317) and negative (LM337) regulators used the same resistor values to get a magnitude of 10 V output. The circuit diagram is shown below. While the pin numbers differ for each regulator, the schematic is the same.

Schematic of the +/- 10 V regulator. The input pin connects to the supply voltage (either +17 V or -17 V). The output pin generates the output voltage (+10 V or -10 V), and connects to a 220 ohm resistor with the other lead connected to the adjust pin. The adjust pin also connects a 1.5 kohm resistor in series with ground. A 100 nF bypass capacitor is connected between the output pin and ground.

Coefficient and constant generators

Each of the coefficients and constants is generated using a potentiometer. (I have an infographic about potentiometers on my GitHub page.) The simpler of these are the constants, which scale between +/- 5 V. This means that one fixed lead connects to +5 V, the other fixed lead connects to -5 V, and the wiper therefore outputs a voltage between those two extremes.

The slightly more difficult of these are the coefficients, which either scale between -x and +x or between -y and +y. The positive values of x and y (one fixed lead of the potentiometer) are connected to the output of each of the inverters (see inverters, below). The negative values of x and y (the other fixed lead of the potentiometer) are connected to the output of each of the summing amplifiers (see equation solver, below). To be specific, each of the wipers outputs a0x, a1y, a2x, or a3y. If we know the values of x and y, the microcontroller can divide those out to calculate each coefficient value (read more about this below).

Circuit diagrams for each of these coefficient and constant generators are shown below.

Schematic of each potentiometer. Left shows a0 and a2 generated by scaling between +/- x. Center shows a1 and a3 generated by scaling between +/- y. Right shows b0 and b1 generated by scaling between +/- 5 V.

Equation solver

This is the heart of the circuit! Each op-amp in this circuit is the NE5532 (link is to datasheet PDF), which may be my new favorite op-amp (move over LM741). The chip is DIP-8 and contains two op-amps, so the footprint is small and the op-amp itself is very capable. It’s also less expensive than the LM741 considering it’s a dual package.

There are two summing amplifiers that each contain four inputs. Each output is the negative value of the solution (-x and -y). These negative values feed back to the coefficient generators. They also act as inputs to inverters (explained below) which create the positive values (x and y). The positive values are in turn connected in a feedback loop back to the coefficient generators and summing inputs. The circuit diagram is shown below. Each resistor is nominally identical. (I used 1M resistors to minimize current.)

Schematic of the summing amplifiers. These are described in the text.

First I’ll explain how this works, and then I’ll give you the side quest about how I messed up and learned some lessons. An op-amp wired with negative-feedback (the “Magic Triangle” as I call it) has two important properties that enable us to solve op-amp circuits. First, both the inverting and non-inverting nodes will have the same voltage level (this is called a virtual node, which sounds fancy). Second, no current enters either the inverting or non-inverting inputs of the op-amp.

Magic Triangle Property #1 means that the voltage at the inverting input is zero in this circuit (because the non-inverting input is connected directly to ground), which makes math nice and simple. Magic Triangle Property #2 means that we can sum all of the currents going through the input resistors, and set them equal to the current flowing through the feedback resistor. Let’s do this for the op-amp on the left, the x op-amp.

\frac{a_0x}{R} + \frac{a_1y}{R} + \frac{b_0}{R} + \frac{x}{R} = \frac{x}{R}

This simplifies to

a_0x + a_1y + b_0 = 0

I will leave you to perform the same feat of op-amp magic for the y op-amp to show to yourself that it generates the second equation. (In this case, it legitimately can “be easily shown,” assuming you’re comfortable with Ohm’s Law and Magic Triangles.) Aren’t op-amps amazing?

I have to admit that I’m mildly embarrassed by the quantity and magnitude of mistakes I made with this aspect of the circuit. At first, I made the big mistake of not including a feedback resistor. As I was wiring it up, I knew down to my very core that it was wrong, but just kept on wiring. I even managed to obtain a bunch of correct output values when I did some prototype tests, so I must have (1) been very lucky that the op-amp wasn’t saturating and (2) made enough mistakes that cancelled each other out, at least in the short term.

After integrating more of the circuitry together, I started drawing up my schematics in a LaTeX document to include in this blog post. I put feedback resistors in my schematic because what kind of monster wouldn’t use feedback resistors in a negative-feedback op-amp circuit, right? But I knew I hadn’t put any in my actual circuit, which made me face the fact that I messed up. I cracked open some textbooks to prove to myself what I already knew, and threw in the feedback resistors.

But the math still wasn’t mathing. At first, I did not include the input resistors labeled x and y in the schematic above. I simply had the coefficients and constant term. This means that the op-amp was actually solving the equation

\frac{a_0x}{R} + \frac{a_1y}{R} + \frac{b_0}{R} = \frac{x}{R}

Which reduces to

(a_0 - 1)x + a_1y + b_0 = 0

I was ready to get all sad about this, maybe give up, burn the evidence, and change my career. Then I realized I could just sum in an additional x by the inclusion of one more resistor. It worked! Career saved!

Inverters

Because we want to access the positive values of each solution (x and y), an inverting amplifier with unity gain (feedback and input resistors are nominally identical, I used 1M resistors) can be connected to each output to generate these voltages. These are connected to the x and y terminals shown in the coefficient potentiometers, and are also connected to the x and y summing inputs, which were described above. The circuit diagram for a “simple” (nothing is ever truly simple, but the inverting amplifier is one of the most basic negative-feedback op-amp circuits that you can build) inverting amplifier is shown below.

A schematic of two inverting amplifiers.

I have to give the inverters some credit for being the only part of this circuit that didn’t cause me any grief. I built them correctly, they had no issues, I didn’t have to consider multiple circuit architectures or chips, and they were easy to troubleshoot if I did accidentally misplace a wire.

Analog multiplexer (MUX)

I could stop writing about this circuit here, and just let you go on and live your life. But as mentioned, I really wanted to interface the analog circuit with a microcontroller (MCU) so that the equation could be displayed on an LCD screen, so you’re going to have a lot more to read. (I’m sorry and/or you’re welcome.) The plus side is that interfacing with an MCU provides us with a much simpler way of seeing the equation and solution so that we don’t have to use a voltmeter to measure voltages or a calculator to calculate coefficient values.

There are eight analog voltages that we’d like to read: four coefficients, two constants, and two solutions. Rather than dealing with eight independent voltages, each of these voltages can be input into an 8-to-1 analog MUX (CD404051B, link is to PDF of datasheet). This is actually a brilliant chip, and I don’t think I would have completed this circuit without it. I can MUX between each of these signals by using control bits output from the MCU, and then deal with the single output of the MUX chip (which I’ll scale and then input into the analog to digital converter of the MCU, discussed below).

The chip itself is relatively straightforward to use. There are eight channels, three control bits, an active-HIGH inhibit (I keep it LOW because I don’t want to inhibit the output), a single output pin, and then supply connections (VDD = +10 V, VSS = GND, VEE = -10 V). It doesn’t matter which input connects where, as long as I know which is which so that I can account for it in software.

Voltage scaling

Originally, I intended to use an external analog to digital converter (ADC) to convert the output of the analog MUX into a binary value, then dump that binary value into a microcontroller (MCU). After doing a lot of research on an ADC that could handle voltages between +/- 10 V, I found the MAX174 (link is to datasheet PDF). It was actually the perfect chip, meeting all of my requirements (DIP architecture, parallel output, digital-compatible outputs, and an intuitive datasheet).

However, it occurred to me that I could simply scale the output of the analog MUX to a voltage between 0 and 5 V, and then use the ADC of the MCU itself. This saves a lot of footprint (not to mention cost, that ADC is not cheap) and makes the MCU software slightly simpler, as there’s no longer any need to initiate a conversion (the MAX174 chip needs a start conversion signal which would have to be output by the MCU, whereas the ADC on the MCU can be configured in a free-running mode so that it’s pretty much constantly sampling and converting).

Because each voltage coming out of the analog MUX has a value between -10 V and +10 V (this is represented as VIN in the equation below), and the MCU requires a voltage between 0 V and +5 V (this is represented as VOUT in the equation below), the following equation needs to be satisfied in the scaling logic

V_{out} = 0.25V_{IN} + 2.5

This equation can be accomplished using a summing amplifier! Let’s use a feedback resistor of RF. VIN will be the output of the analog MUX and will connect with an input resistor of RIN. A voltage source will be used to generate the constant. 10 V (selected due to the easy math) will be the second summed input and connects with an input resistor of RS. I decided to connect two 1M resistors in parallel to create a feedback resistance of 500k. Doing the math, this meant RIN = RS = 2M. Because the summing amplifier is inverting, I created one more inverting amplifier to generate a positive output. Both resistors in the inverting amplifier are 1M. This schematic is shown below.

Schematic of the voltage scaling circuit. This circuit is described in the text.

I noticed that the output voltages were consistently about 5% higher than expected across the board. I included a variable resistor in the input path including the 10 V source (if there is an offset error, then trimming the source, which is an offset term, will fix this). (I have an infographic about using a potentiometer as a variable resistor on my GitHub page.) The value of the resistance that trimmed out the error was, as expected, about 100k, which is 5% of the resistance in that path. Trimming out the error electronically was way easier than trying to account for any constant error terms in software. (An astute reader may note that 5% is the tolerance of gold band resistors, which were used in this circuit. Could this account for the 5% offset? It’s possible.)

Each op-amp is supplied by +/- 12 V to prevent saturation on either output. Even though I know that op-amps are scary to folks who aren’t comfortable with them, this was by far the easiest solution to interfacing each analog signal with a digital MCU. And a single DIP-8 (dual op-amp) is way smaller than a DIP-28 (MAX174).

Microcontroller

While I could probably get away with using a smaller footprint microcontroller (MCU) than the ATmega328P, I decided to use the easiest possible MCU. I wanted to prototype using an Arduino to minimize my effort and to allow me to use the Arduino IDE’s serial monitor while prototyping. Honestly, after all the suffering this circuit put me through, I was happy to do the one super easy thing here and just use MCU I’m most comfortable with that has the easiest prototyping options.

As with the circuit itself, I think it makes sense to structure my discussion of the software by starting with a flowchart and going from there. The flowchart below gives a very high-level overview of the circuit. The code itself is pretty lengthy, so I’ve abstracted out most of the actual code and just distilled it into functional blocks.

Microcontroller software flowchart. This is described in the text.

The program flow of the code itself (labeled Program Flow in the flowchart above) configures peripherals, initializes the LCD screen (I use the hd44780 library which I would link to, but I can’t find the original source anymore), and defines global variables. Then the repeating part of the code occurs. If data has changed since the last pass through the code (based on a flag set in the timer/counter interrupt), each analog to digital converter (ADC) value is converted into a voltage (described below), the coefficients are scaled by x or y, and based on the page number, either the equations are displayed or the solutions are displayed (described below).

Calculation of equation variables

The ADC of the ATmega328P has 10-bit precision, which I utilized in this circuit. I am pretty much opposed to floating-point calculations, so I decided to use integer math and scale everything by 1000. This would enable me to round values and display all quantities with hundredth’s place precision. (As I will discuss in the conclusion, this circuit does not have hundredth’s place accuracy, nor does it truly have hundredth’s place precision.) The ADC will output values between 0 and 1023 (2n-1, where n=10), corresponding to original voltages between -10 V and +10 V. Therefore, to obtain a voltage value for each signal, this equation is used: value = (20000L * ADC) >> 10 - 10000. I determine the sign of each value, then scale the coefficients by x and y (taking precautions while scaling as integer math can lead to disastrous results when dividing if you’re not careful). Finally, each value is rounded to get hundredth’s place precision. Each of these values is stored as an integer-based variable in the code.

Why not use floating-point math? Floating-point math not only requires a lot of overhead as far as program memory goes, it does not lead to any appreciable increase in accuracy. In fact, I’d argue that with an 8-bit MCU you’re probably going to sacrifice some accuracy to use floating-point math. The circuit is not super accurate or precise in the first place (read more about this in the conclusion), so the false sense of security that I get from “highly precise floating-point values” is just a facade. And at the end of the day, floating-point math is not necessary. While I did spend extra time scaling values and rounding them using my own equations and external functions, it uses less program memory than floating-point operations. Finally, floating-point values are obnoxious to convert to strings to display on an LCD screen. So I would have had to convert them to scaled integers anyway.

Display paging and overflow

The LCD screen I use is a 16×2 LCD screen using the Hitachi 44780 driver (hence the name of the library). This is not enough characters to display both equations and both solutions simultaneously. I compensated by splitting all of the data into two screens. The first screen shows the truncated form of the two equations (I leave off the =0 as it just doesn’t fit, sadly). The figure below shows how this looks. Note that each equation is in the format of ±.aax±.bby±c.cc (15 characters). (Why not display ±.aax±.bby=±c.cc, which only requires 16 characters? Keep reading to find out.)

Photograph of the circuit breadboard with a focus on the LCD screen. It reads -.82x+.18y-1.31 on the top line and -.43x-.50y-4.96 on the second line.

The second screen shows the solutions. For the above set of equations, the figure below demonstrates the results as shown on the LCD screen. (I will discuss the accuracy of the results in the conclusion of this post.)

Photograph of the circuit breadboard with a focus on the LCD screen. It reads x = -3.18 on the top line and y = -7.13 on the second line.

To page through each of the two screens, a pushbutton is used. Pushing the button activates the pin-change interrupt on PB5 of the MCU. This interrupt checks that the value of the pushbutton is HIGH (this should only occur on a rising-edge, not a falling-edge, and as I’m using a pin-change interrupt this cannot be configured in the peripheral hardware, so it requires a line of code to do this). It also does a quick check of how long it’s been since the pushbutton was last pushed to determine that enough time has elapsed (software debouncing). Once these two criteria are satisfied, the variable holding the page number is toggled. (It will either be 0 or 1, and the XOR operation is used to toggle it.)

In the situation where x or y saturates in either direction (which can be determined by comparing the ADC values of x and y to 0 (negative saturation) and 2n-1 (positive saturation), where n is the resolution of the ADC), I wanted to display somehow that the results are invalid due to saturation. I want this to be clear on both screens. The figure below shows the equation screen of the LCD when saturation has occurred. Note that the 16th character space contains an asterisk, denoting saturation.

Photograph of the circuit breadboard with a focus on the LCD screen. It reads -1.0x-.73y-2.15* on the top line and +0.9x-.49y-4.69* on the second line.

On the second screen, the variable that has saturated is indicated with a further warning. The photo below shows this (this photo was taken using the set of equations shown in the figure above). Note that y has saturated in the positive direction. The solutions to the two equations are not valid.

Photograph of the circuit breadboard with a focus on the LCD screen. It reads x = -7.64 on the top line and y = +10.00 SAT on the second line.

Overflow status side quest

While I ultimately decided to display a saturation warning directly onto the LCD screen, I had a different idea at first. When this idea was still brand new, I had the idea to use a comparator chip to compare x and y to their limits (+/- 10 V), connect each comparator output to an OR gate, and then light up an overflow status LED. While it may seem stupid to write about this because I didn’t actually use this in my design, it did lead me on an interesting side quest, so I think it’s worth discussing. The comparator chip I hoped to use, the LM339, was a great fit in that the quad package could (theoretically) compare x to +10, -10, y to +10 and -10 simultaneously. It has open-collector outputs, so I can easily interface the outputs with digital logic by using pull-ups to +5 V on the output.

Not so fast! The LM339 can’t handle negative voltages. So I figured I could find a way to take the absolute value of x and y and just use two of the four on-chip comparators. There is a circuit using op-amps that can take the absolute value of a voltage. It works very nicely, and in the surprise of the Internet of the 21st century, a non-snarky, actually informative StackExchange post explains how it works. I built it, verified it, and was ready to go.

The absolute value circuit was pretty cool, but using the ADC saved me a lot of footprint, so it was worth switching up my design.

Timer/counter interrupt

I use timer/counter 1 in CTC mode (read Section 14.4 in the Microcontrollers textbook for more information on this) to generate an interrupt every ~0.5 second. Faster cycling times resulted in wildly inconsistent readouts, so I stuck with 0.5 second. I think this was fast enough to get a relatively responsive output as each variable is sampled in a timeframe of 4 seconds.

In this interrupt, an indexing variable is used to generate the control bit signals (read more below on how this interfaces with the analog MUX). The scaled output from the analog MUX then connects to the analog to digital converter (ADC) of the MCU, which is subsequently read and stored into an array (the location is based on the indexing variable). This is stored as a raw binary value, and is converted into a voltage or coefficient value in the program flow itself (described above).

At the conclusion of the timer/counter interrupt, a flag is set indicating that data has changed, and the indexing variable is incremented. If the indexing variable becomes equal to 8, it will be wrapped back to 0. (This ensures that the variable stays between 0 and 7.)

Control bit switches

As it turns out, the analog MUX requires voltages equal to VDD or GND on each of the control bit inputs. I was expecting to use a standard logic value (5 V) for HIGH, but that is not the case if VDD is greater than 5 V. I figured this out through trial and error, but upon closer inspection of the datasheet, it does appear that input HIGH voltage (VIH) does need to be at least 7 V if using a VDD of 10 V. Thankfully my trial and error approach got me to a solution quickly and without too much heartache.

Unfortunately, the idea was to use the microcontroller (MCU) to directly signal each control bit input. But the voltages are incompatible. Fortunately, I do know how to wire up a switch using a transistor, so this became the solution. The circuit diagram below demonstrates what I wired up (one for each of the three control bits). The left side of the figure is the schematic as built. An NPN transistor is used as an active-LOW electronic switch. The MCU connects to the base of the transistor (labeled B), with a 1k resistor used to limit current flow. The collector (C) connects via a 10k pull-up resistor to VDD (10 V), with a connection to the control bit sent to the analog MUX between the pull-up and the collector. The emitter (E) connects directly to GND. The center of the figure demonstrates what happens when the MCU value is 0. This causes the transistor to act as an open switch. The control bit value connects directly to VDD, outputting a 10 V value. The right side of the figure demonstrates what happens when the MCU value is 1. This causes the transistor to act as a closed switch. The control bit now connects directly to GND, outputting a 0 V value.

A figure showing the active-LOW switching logic between the MCU and control bits. These schematics are described in the text.

Fortunately, active-LOW logic is easy to compensate for in software. Because I am so much stronger at writing C code than I am at wiring up transistor circuits, it was easier for me to spend a few seconds writing software that converts to an active-LOW signal than it was for me to figure out how to wire up an active-HIGH switch. I’ve found that I’d rather spend my time where it’s worth spending.

Project implementation

Here’s a video of the final breadboarded circuit:

This was a really interesting project to build, and is really the first big analog circuit project that I’ve tackled. It’s been sitting on my to-do list for four years. I have plenty of reasons why I put it off for so long, but ultimately it was scary for me to do this project because I am not as good at analog circuitry as I am at digital. I realize that’s a bad reason, and that I won’t get better without more practice, but it’s also the truth.

Remember the whole point is that I want to build a circuit that can solve a second order, linear, ordinary differential equation. Honestly, after building this circuit, I am simultaneously feeling more empowered and more scared about starting that project. I have learned a lot in this experience that I’m looking forward to utilizing in the next one. But I also realize that this still isn’t as easy (for me) as a digital circuit, so I’m not going to be entering into it as naively as I did this project.

I’ll conclude with final thoughts about how this project came together, ordered by category (in no particular order).

Accuracy and precision issues

First, just to clarify the difference between accuracy and precision. Accuracy is how correct an answer is. Precision is how well we can make a measurement or represent a result. Say I want to calculate the average distance between the Earth and the Moon. I do some back-of-the-envelope calculations and come up with 384,000 km. That’s relatively accurate, but not very precise. If I gathered up a lot of crappy data and used my most precise calculator to solve for the distance, say I compute a value of 298,467.129021 km. That’s unbelievably precise (in fact, it’s precise down to the millimeter, which is absurd), but isn’t accurate at all. I wouldn’t want to be on a spaceship using that value to get me to the moon. If I want to be accurate and precise, I need to use good data, correct equations, and a precise calculating tool. (How precise I need to be is a different question altogether. Am I trying to get a spaceship to an exact spot on the moon? Or am I just curious about how far away the moon is?) (By the way: Wikipedia tells me the accepted value of the average distance between the Earth and the Moon is 384,399 km, which I would expect is both accurate and precise.)

Accuracy and precision are fundamentally not the same thing. (I’m trying my best to hold back here, I see how highly precise (but not necessarily accurate) answers give students the false confidence that their answers are correct, which is a dangerous misconception I try to bring up whenever I see it, but this isn’t really the forum for that. So I’ll shut up now.)

As you can see in the video and photographs of the circuit, the solutions as calculated and displayed in this circuit are not as accurate (or precise) as what you can get with a calculator. First, let’s address the accuracy. To a certain extent, accuracy wasn’t the point of building this circuit. As a proof of concept, I think it came together really well. Based on what my calculator solved as the “accepted” values for x and y for each of the scenarios posted above (in the video and in the photographs), the absolute value of the percent error of the solutions displayed on the LCD screen ranged between 0% (one of them was right on the money) and 17%, with an average error around 6%.

There are also some precision issues with the circuit, to the extent that the circuit simply cannot distinguish between any two arbitrarily small voltage values. This is going to be limited by the hardware that’s used in the circuit. I can do my best to accommodate for limited precision, but without spending a lot of money on extremely high-resolution components, a high level of precision is not feasible to achieve.

  • General noise and interference issues (accuracy issue) – Analog circuitry has the disadvantage of being prone to noise. While digital signals have a pretty large voltage range between LOW and HIGH signals (so a small amount of voltage swing in either direction is unlikely to completely switch a bit from zero to one or vice versa), analog signals are continuous. So any amount of noise introduced into or interference caused by the circuit itself is going to affect the voltages, which are the signal values. The breadboard has lots of wires (stray inductance), so I imagine there’s a fair amount of interference at play.
  • Component values aren’t exact (accuracy issue) – I did trim out some error in the scaling circuitry so that corrected some of this error, and I also used 1M resistors purchased from the same batch, but that doesn’t mean that the equations for each circuit are as exact as expected.
  • The MCU will introduce some noise into the ADC (accuracy issue) – I have a low-pass filter on the reference voltage and bypass capacitor between the VCC and GND pins on the MCU, but the CPU will definitely introduce some noise into the ADC results. The ATmega328P datasheet discusses methods to reduce noise, which I have implemented in hardware, but the circuit would not benefit from a sleep mode to reduce noise any more, so I’ve pretty much maxed out on what I can do to mitigate this issue.
  • Integer math errors (accuracy and precision issue) – Using integer math introduces some error as well. If you take an ADC value of 456 and plug it into the equation described above to convert it into a voltage, 20000*456 = 9120000. 9120000 / 1024 = 8906.25, but the MCU will truncate this to 8906 (because it’s integer math, the fractional component gets lost). Then 8906 – 10000 = -1094. Scale that down and I would see -1.09 on the LCD screen. I scaled everything by 1000 (which, again, is slightly bold of me considering the precision of the ADC is limiting the voltage swings we can distinguish in the first place) so that will mitigate the errors of integer math somewhat, but not entirely.
  • The ADC has a finite level of precision (precision issue) – The ADC I’m using is 10 bits, so it only samples the signal into 1024 discrete voltages. If that range represents +/- 10 V, a swing of 20 V, each increment on the ADC represents 0.098 V. That’s tenth’s place precision! (If I had used the MAX174 ADC with 12 bits of precision, each ADC increment would represent 0.024 V, so that would have been an improvement in precision, but not accuracy.)

Footprint

This circuit is rather large. I didn’t do an excellent job of spacing things efficiently on the breadboard, and did wind up using two breadboards to connect everything together. (In my defense, the design changed quite a bit, so while I may have been efficient with space at first, I swapped out or removed components, and things wound up being a little chaotic on the board.) There aren’t really a lot of large chips (the largest is the ATmega328P, which is a 0.3″ DIP-28, not exactly huge), but there are six voltage regulators and six main potentiometers (eight in total, one sets the contrast of the LCD screen, and one is the trimmer, which I might eventually replace with a fixed resistor). It’s just a lot of little things contributing to take up a fair amount of space. Certainly I’ve made larger digital logic circuits (I’m looking at you, multiplier circuit), so I don’t think this is the largest circuit I’ve built, but it’s up there.

As I haven’t designed a PCB yet, I have no exact point of comparison. I’ll try to remember to edit this after (and if) I do design and build a PCB.

Power consumption

Honestly, I’m not entirely sure how to quantify power consumption with a dual supply. But I did the next best thing: I measured current. Current flowing from the positive (+17 V) terminal of the dual power supply fluctuated between 50-70 mA, with the variation occurring due to the updating values on the LCD screen causing more or less pixels to be on. Considering that I purposefully used a lot of 1M resistors (and 1M potentiometers), I suppose I shouldn’t be surprised at how little current there is. Much, if not most, of the current (considering it’s a 20 mA swing on just a few pixels alone, I think I’m pretty justified in making this assumption) appears to be sourcing the LCD screen. I imagine the remainder is sourcing the voltage regulators. But each individual op-amp shouldn’t be sourcing or sinking much current at all, and each passive component shouldn’t be experiencing much current flow either.

Current flowing from the negative (-17 V) terminal of the dual power supply was relatively stable at 40 mA. (Note that this value is accurate but not precise. When measuring the positive supply currents, I decided that precision in my recordings was not something I could really aim for as the values oscillated too quickly for me to get a good read. I made the decision to keep my precision for the negative lead at the same level as for the positive lead, even if I could have recorded a more precise value. If I used a more precise value here, say, 38 mA, that might lead the reader to assume that the previous measurements were just as precise (as in, the values were really dead-on between 50 and 70 mA), which is incorrect.) I imagine that most of the current in this context is sourcing the negative voltage regulators.

Other details

My engineering background makes me immediately want to break everything I see. (Not literally, I swear!) What happens if you have two non-solvable equations, such as two parallel lines? Naturally I tested it out, and x and y just saturate. No explosions, no magic blue smoke, no wormholes opening to another dimension. It’s actually kind of anti-climatic.

I also alluded earlier to the fact that *technically* any linear equations could be solved with this, assuming we had enough precision, as we can simply scale any equation to fit. I think my notes on the accuracy and precision of this circuit point to how that’s not really true in practice. Ultimately, the solution space is limited by the fact that x and y saturate above +10 V and below -10 V. (Expanding that range would expand our solution space, but would also cause the analog MUX to release its magic blue smoke, which would be unfortunate.) So we cannot, in practice, solve any two arbitrary simultaneous equations with this circuit.

Future steps

In the next few weeks, I hope to design and build a PCB for this circuit. I don’t want to demolish what I’ve built, but it can’t live on two breadboards on my project table forever. It’s going to take a while for me to put a PCB together, but I also wanted to finish up this blog post, which is why I’m hitting publish here before it’s really, truly, definitively complete. If I learn any important new lessons designing the PCB (which, to be honest, I hope I don’t, because that would mean I mess something up) I’ll write up a second post.

Finally, as mentioned in the introduction, my next project along these lines will be a circuit that solves a second order, linear, ordinary differential equation. I have some steps to break this down into smaller sub-circuits, and then build up to that. I’ve learned a lot in this circuit that will absolutely help inform my decisions on the next project. However, there will be a lot of differences in the projects. Notably: the ability (I hope) to include initial conditions, and the possibility of recording or displaying a graphical representation of the output (so you can see if it’s underdamped, critically damped, or overdamped). Hopefully it doesn’t take another four years for me to complete that one! Stay tuned (but don’t hold your breath).