-
New Case
10/28/2024 at 02:34 • 0 commentsThat driver of all decisions and action - a deadline (supercon) is coming up so ... progress happened! Bought into the hype and bought a bambu printer to iterate the mechanical design. I'll join the many other voices saying they're worth the hype - this is the first printer I've bought that *just works*. Completely redesigned the mechanism for attaching the display, it's now a friction fit flat taper that extends about an inch above the top of the frontend. I hadn't tried them in plastic before, but with a little iteration and some post-processing with a file I'm *very* happy with the result. Clicks together and locks well without any additional mechanical parts or clips or anything. Might add a clip of some kind and work on getting it to fit right off the printer but happy with it for now.
I don't love the smaller switches or where they are, but liked alternatives less. I'll have to incorporate more of the front panel design into the next one. Making the electronics a little smaller should also give me a some leeway there. I do like the look of this a lot better than the previous one though.
I'll do a more complete post and release of everything, and video, after supercon, I'm happy enough with the state of everything to publish at this point.
I do need to make a rev 2 of the board eventually (and a rev 1 for the display section!) to cut down the size and reduce the point-to-point wiring, but sticking with the same one for the moment, again just to get something good enough out the door.
-
More software!
02/22/2024 at 03:46 • 0 commentsI see the light at the end of the tunnel, most of the big pieces are in place now.
So... fonts
Very important for simultaneously having the display readable/legible and information-dense. Easy to make it legible with a huge block font, but then I can't fit more than a few digits. A tiny font could fill the screen, but would b inscrutable. In order to push this tradeoff as far as I can requires good fonts, the axis being legibility vs size (separately, horizontal and vertical).
Two things to know, the fonts for this are stored as bitmaps, and the screen I'm using now is 128x64, AND updates 8 lines at a time - so it's basically got 8 rows of pixels that are updated together. That makes it difficult (and slow, and difficult to use the screen efficiently) to update anything other than a full 8-pixel row, so I haven't been.
Upshot of that is, all the fonts I'm using now are multiples of 8 pixels tall, I've got 8, 16, 24, and 48.
Problem I ran into was, that the display isn't crazy wide, especially for the 48-pixel font, digits were previously about 25-30 pixels wide, so couldn't fit more than a couple digits. Including a unit, milli/micro/kilo/mega and minus sign wouldn't fit more than three digits
So, got to shrink the digits horizontally, but don't want to vertically.
This was my first step into madness.
Fonts and font rendering is a whole rabbit hole I have not dove down before but with this, I've had to start. My toolchain at this point is three programs, one, FontForge that lets me edit the vector fonts - moving parts around, replacing characters, adding parts (like bars on Z's and 0's, converting triangular-style 4's to open football-style ones, etc). Then there's another one, FontBuilder, to convert those to bitmap. Those two are existing programs. Finally I use a java program I wrote to do pixel manipulation and convert the bitmap to a C header file that the display code can understand. That took a while but really wasn't *too* bad. Looking through hundreds of fonts for the perfect blend of legibility, looks, and horizontal size, then tweaking those to look exactly how I want, then tweaking them pixel-by-pixel has taken far more of my finite life (i dunno, thirty or forty hours?) than I ever expected to spend worrying about how the letter M looks (MAN M is a pain at low resolution, you need three lines, which means a minimum of 5 pixels wide with spaces, but then it looks like a sideways E and it's weird. Lot of fonts I liked had weird M's at higher resolutions too. That letter alone has been a huge pain).
Being a multimeter, numbers are the biggest consideration, of course. That actually hasn't been terrible, current version is a lightly modified version of "steelfish", which lets me fit five digits across the display, including a +/-, decimal point, and n/u/m/k/M/G, at 48 pixels high, which is all this meter needs. Smaller fonts are different, hacky combinations of BebasNeue, FreeRoad, and LeagueGothic. Like the look of those more but they're wider, this almost certainly won't be the final version. The 8-pixel high font has been an enormous pain and is almost entirely handmade at this point, based it off an existing pixel font but changed almost everything.
So this is *mostly* nailed down by now, though I'm sure I'll keep tweaking it.
-
Software - Round 394
01/22/2024 at 07:52 • 0 commentsAnother month, another kajillion hours in front of the computer working on the software.
Fixes/changes this time:
-Multicore! The rp2040 has two cores, this now uses both. One is dedicated to sampling and per-sample calculations (calibration, filtering), the other handles low-speed IO, ranging, mode switching, and printing to the screen. Huge pain to keep variables synced between cores, but it works now.
-Stable sampling frequency. Previously this would one-shot ADC samples as needed, now the ADC samples at regular intervals and the pico reads them as they're available. This is really the big one, opens up a ton of new options. Did require going multi-core so the Serial & USB printing doesn't make us wait too long and miss one. With this I can now do...
-AC measurement! Finally got around to that. Needs a high, stable sample rate to get accurate readings. Supports true RMS, over a pretty significant number of readings. Little hack for this I was kind of proud of below as well.
-Also, filtering! IIR filters for 60hz and harmonics are added in and work. I do want some more refinement on switching them in and out and maybe even adjusting strength, Q, etc, but for now I can notch 60Hz, which is the main thing.
Might not sound like a ton, but this required redoing pretty much everything that hadn't already been redone last month. It's more of a lasagna-with-one-layer-of-spaghetti of code now. Still need to clean up the file structure a bit though.
Few specifics:1) This ADC I'm using does not have a very stable oscillator, spec is only "somewhere between 3 and 6.6MHz". This sets the sample frequency, so I'm now using one of the Pico's GPIO (driven of course by a PIO) as the reference clock. First time I've written anything for PIO! Very exciting, very cool peripherals. I did plan ahead for this, though more for synchronizing it with something else if that became necessary/interesting, I hadn't noticed that spec. This is really useful though, because now I can set the clock to anything, on the fly. This lets us change up sampling for high speed, short sampling instant, accuracy, low noise, etc.
2) The way I'm doing RMS conversion is nontrivial. It didn't really seem like there's a good way to do an RMS sliding average that works down to 10's of hz without being dreadfully sluggish, so I went with a full sliding window. Problem is, I've got to sample at ~1KHz or more, and want to integrate for ~1 second so low-frequency waveforms are reasonably accurate, even if I'm not at an exact multiple of the fundamental. Problem now is, storing and re-adding up 1000+ floating point numbers is going to take a decent chunk of space and time on the pico, probably could but there's a better way.
First, we pick a tradeoff of output rate vs storage space vs integration time. The two control parameters are the "chunk" size, and list length (I'll explain those in a second)Memory taken up ~= list length * 2 * reading size (so for a list length of 64, and 32 bit floats, that's 64 * 2 * 4 = 512 bytes). List length must be a power of 2.
Sample rate = ADC sample rate / chunk size - we get the Vrms readings at "sample rate", . If we didn't care about sample rate, we could do huge chunks and save memory, I used a chunk size of 16.
Integration time = chunk size * list length / ADC sample rate - This is the real kicker - since we need integration time to be only the order of a second, and the sample rate is >1KHz, this means chunk size * list length has to be over 1000 as well, so at least one has to be sizeable, which either means slow updates, OR a lot of memory taken up.
On startup we create two arrays of floats, the list, and the sum tree, both empty to start. Also an index.
list will just be a circular buffer, sum tree will be the clever-ish thing I came up with.
The "chunk" I keep referring to is basically just the average of <chunk size> readings in a row (really, the square of the reading minus the DC bias, since it's RMS). So start a variable at 0 and just keep adding to a running total of readings (keep in mind, we're dealing with these one-at-a-time, not processing a list after the fact). When we get to <chunk size> readings on the pile, divide by <chunk size> to get the average. We then write this average to list - so list [ index ] = chunk average.
If the list is short, maybe up to 16 or so, then we can just sum up list and be done. (rather sum, divide by list length, and take 2*sqrt(sum). That wold give the RMS reading. As it stands in the meter, list length is only 64, so this is *probably* fine to do.
For longer lists, or to be extra fancy though, we can use a different method involving that sum tree mentioned earlier. Since we only replaced a single element in the list, really not much has changed (say we replaced element 50 in a list of size 64, the sum of the first 50 elements hasn't changed at all!) Now there's a LOT of little tricks we could play here but a common one is to remember the previous sum, subtract off whatever element we're replacing in the list (before doing so), then add ours! This is great and only takes a single subtract and single add! HOWEVER, this is **floating point**, not theoretical math. Since this does not recompute the total sum at any point, tiny errors will accumulate over time - eventually it will become sort of unstable and diverge from the true reading. Especially since we're mixing large and small numbers. A better way for this is to use a tree! The first layer of the tree has the sum of every pair in the main list, so tree[0] = list[0] + list[1] , tree[1] = list[2] + list[3], and so on. The second layer is sums of pairs of elements of the first layer, so tree[0] + tree[1], and so on. Each layer is half the size of the last, until you get to a single element, which is the total sum!(Note this is useful even by itself, and in other contexts! Summing a huge list of floating point numbers can cause problems! Adding a small number in the list to a large running total can introduce significant error - for example, in a 32-bit float, 100,000,000 + 1 = 100,000,000! If you had a list of 200 million 1's and took their sum by indexing and accumulating, you'd get less than 100,000,000!!! Doing it the tree way, sort of level-by-level, you're adding numbers of roughly the same size every time and will get an accurate result)
Tangent aside, we keep this tree in memory, and every time we add something new, we only recompute the branch above that number, so if we replace list[3], we recompute tree[1], then the next layer, and so on until we get to the sum, This only takes log2(list length) operations! So for a list length of 1024, we can add up all 1024 items (again, assuming we only replaced one) in just 10 operations.
I'm using a list length of 64, so it's only 6 - plenty fast for me.
static int index = 0; index = (index + 1) % listLength; list[ index ] = newReading; uint32_t i = (index & 0xFFFFFE) / 2; //starting branch of the tree tree[ i ] = list[ index & 0xFFFFFE] + list[ index | 1 ]; //either i & i+1 if i is even or i & i-1 if odd i = i & 0xFFFFFE; //easier to not mess with even/odd - i is always even from here out uint32_t next = 0; upperBit = listLength-1; // 0b1000... where the # of 0's is log(listLength)-1 while( i != ( listLength-2 ) ) { //we're at the top of the tree next = ((i>>1) | upperBit); //weird math, but finds the index of one the sum that's one layer up from i tree[ next ] = tree[ i ] + tree[ i+1 ]; i = next & 0xFFFFFE; } } return tree[ next ];
Rather than a complex data structure in code, tree is just another array of the same length as list. If you mess with it a little it's apparent that the total number of nodes in a binary tree is 1 smaller than twice the number of leaves. Since list is our circular buffer, we need half as many leaves, so total size is the same (minus one). The structure of elements within the "tree" list is basically just that it wraps around to the next level, so if there's 8 elements in list, tree[0] = list[0]+list[1] ... tree[3] = list[6] + list[7], then, tree[4] = tree[0] + tree[1] , tree[5] = tree[2] + tree[3], and tree[6] = tree[4] + tree[5] (the final/total sum). Doing it this way means that some really simple bitmath ( i >> 1 | upperBit ) gives us the next node to solve for "up" from i, which makes solving this really fast.
Dang that was long. Anyway that's it for now.
-
Software update
12/21/2023 at 06:47 • 0 commentsWAIT ARDUINO-PICO HAS HAD SERIAL PRINTF THIS WHOLE TIME?!?! WHEN DID THIS HAPPEN?!
I've been using hacks with the preprocessor and dozens of calls like Serial.print("V=");Serial.print(voltageStr);Serial.println(); this whole time!
Dang, wish I'd known that like, four months ago. Anyway.
Real quick update, completely overhauled the software, set it up to handle switching between modes make sense, and cut down the spaghetti code. Well at least a little bit. Also got most of the weird glitches out of doing large fonts with this display, I can now do 24 and 48 point fonts as well, which are at least closer to a usable size. Might choose a different font in the future that's a little taller and narrower, but it works for now. At this point working on making it more real-time so I can get sampling at a consistent frequency to be able to add a notch filter for 60Hz main, some more robust filtering in general, and AC measurement modes.
Here's the current hardware, I do need to cut down the thickness of this at some point, though it is about 1/2" thicker than originally designed - the switches I used for this were cheap ones off aliexpress, for a nicer rev 2 I plan to use some lower profile but more expensive ones. That and the ADC and pico are mounted on female headers, so I need some additional depth for those.
-
IT'S WORKING
11/04/2023 at 08:42 • 2 commentsSpent the friday night party at supercon getting the display to talk to the frontend, and I've got a working meter!
Zero refinement at this point, but you can put a voltage, current, or resistance across it and get a reading!
Next big step is a display upgrade, currently the font is only 1/8th the screen height, want to bring that up to use up most of the display, then refine the operation selection, then add autoranging. Hoping to get that done this weekend, if you're around supercon, feel free to come say hi, I'm the guy that looks like me and won't shut up about his multimeter.
I did some basic calibration a few days ago against a nice 6-1/2 digit bench meter. I made a voltage reference and precisely measured some resistors. I began to feel the pull towards becoming a voltnut, making nice copper clad boxes for transfer standards. It is strong but. resist. For now. I've started dreaming of multiple slopes and integrators. For now I will take the easy way out.
-
IT'S ALIVE (again)
10/27/2023 at 20:55 • 0 commentsThis is like, kind of working!!
I have the first prototype board assembled, and have the paths for voltage, ohms, and current all up and running. Haven't written most of the software, made the display, calibrated anything, or put the thing in a case yet, but the measurement itself looks like it's working!I am excited. Whoo.
Got boards back from fab a little over a week ago, thank you to PCBWay for supplying free boards for the project. Normally wouldn't take free stuff just to advertise for a company, but this is one of a few companies I've been using for years now and happy with their services. I've recommended them for prototypes and quick-turn boards for years, even without any incentive.
Boards looked good, funny enough this is my first time ever making slots, but no issues with that. During assembly, inevitably a couple issues popped up, I'd given a 14-pin SOIC a 16-pin footprint. D'oh. Soldering one side down and a few bodge wires to the other side got that working though. Later on realized that one of the pins on the output shift registers should have been pulled high but was low. Unfortunately these were tiny DHVQFN packages, but some bodge soldering involving squinting that will probably make my eyes stop working six months sooner, later, and we've got a working board.Everything went concerning smooth on power up, using the software from the breadboard just read out from the ADC and controlled the PGA out of the box, and my first attempt at controlling the shift registers, gain stage, and ohms sections all worked on the first try with literally zero debugging ... so surely there's probably some horrific, underlying issue with the whole thing that will keep it from ever working. Either way I'll enjoy the good times before we get to that.
Now to start putting together the case and display, and start on the real software
-
Layout finished, working on mechanical design
09/30/2023 at 07:41 • 0 commentsFinally finished layout, this ended up being a huge pain though and I'm not super happy with it.
Very dense in some places but empty in others.
Mostly the SPI/I2C/Uart busses going to the input module, ADC, PGA, and radios, and shift registers. I'm very not happy with the lack of good separation between the analog and digital, and that I wasn't able to get solid ground planes under all the analog lines. Whole lot of lessons learned but I want to get this out the door already to get *something* together so I can learn all the OTHER ways I screwed it up and can fix them in one go.
Couple big ones:
-Having modules on through-hole 0.1" headers makes things a huge pain. They take up a ton of board space on both layers, so I have to plan on routing around them. In future I'll probably use SMD header pins.
-Pi Pico form factor for the MCU wasn't really the way to do it. Should have had an adapter board and move some of the digital stuff like shift registers to that.
-Should have made the isolated coms a separate module as well.
-Having two separate radios was dumb. Have one, and adapter boards to whatever commodity radios I want.
-Plan placement with wide digital busses in mind. I kind of did but it was insufficient, there were a ton of little pinch points where I had to go all over the place. Also having all the modules spread out meant those busses had to go all the way across and up and down the board instead of being kept short. I originally placed to keep analog lines short, the ohms and volts sections are right next to the low voltage side of the input protection, and the PGA and ADC are right there in the middle as well. I'll probably see more problems from having to route big fast digital busses nearby than if I'd had slightly longer analog lines but good separation.
Any case I'm going to make some of the other little boards I need and get this sent out soon.
Moving on to mechanical design now. I found out solidedge has a free, non-cloud version and is related to NX (which I'm already familiar with from work). Started using that but there's a bunch of weird annoying things about it, like just now I've spent an hour trying to make a simple linear pattern in an assembly. Yeah turns out you can't, you have to make a sketch with a pattern of something, then make a pattern from that sketch. WHY?! NX supports doing this the normal way, why mess with it?
Anyway I'm starting that, should be fairly straightforward, it's pretty much just a box with some holes in it for the connectors and stuff.
-
Changes partway through layout
09/20/2023 at 08:01 • 0 commentsSome inevitable changes have come up during layout.
Biggest changes have all been for size - between the volt and ohm protection, two fuses, input module, that I've got less "vertical' room to work with (since the display is separate), and all the big through-hole modular parts (ADC, Pi Pico, radio modules), space is REALLY tight. I've changed the shift registers from SOIC to a tiny QFN package, and cut a whole bunch of caps and changed the resistors in the voltage frontend to make layout of that section easier.
I've also ditched having the PGA/MUX as a modular component - it just doesn't really seem that useful to change that up. If I've got room in layout I may copy some of the signals to some unused pins on the ADC module - that way if the PGA is limiting in some way in the future it can just be de-populated, and a replacement integrated directly onto the ADC module. I also realized that going to an MCP3464 may not be a terrible idea - these have some analog gain built into them so with that I could potentially skip the separate PGA/MUX chip entirely.
I may have to change to SMD header pins for either the ADC or pico. I like having regular-size pin header holes for as many things as possible since they're so versatile, but if I don't have enough space to route all this there may not be another option.
Either way here's where it's at now:
I don't currently have any regular CAD software so can't make models for the non-standard parts. Namely the fuse holders on the left are just the end clips, the super-wide "DIP chip" in the middle is standing in for the input module, same for the RFM radio in the upper right,. The ADC module is just a light orange box in the middle on the right. I'll probably have to finally give in to one of those free cloud-based ones for this eventually to do the case
-
Schematic and Operation
09/16/2023 at 02:14 • 0 commentsThe schematics are done! I kind of want to do a video going over the design process as this has evolved over what's now several years, there have been a ton of changes to everything.
For now I'll throw some (slightly) better explanations here.Lets get right into it.
Overall block diagram below. Nothing too surprising, we've got our Volts, Amps and Ohms sections (AFE = Analog Front End) separated, a PGA (programmable gain amplifier) and ADC, and microcontroller. Specifics of the interconnects to the MCU aren't super important, mostly just some control outputs to the subsections and digital connections to the PGA and ADC. Only real thing of note is that the front Common jack DOESN'T go straight to the COM global label (which is generally used as our common in the subsections) - it takes a detour through the Amp AFE first. It doesn't affect much but we'll come back to it.
Few things to note in the analog sections are that there are a few different power/signal rails, COM is the star of the show, it's our reference voltage, everything else will be measured relative to that. It's the same voltage as the Common Jack on the front of the meter.If you're familiar with splitting a single power supply for a virtual ground, skip this paragraph. If not I won't go through it in a ton of deail, upshot is that COM is about 1.65V (half of 3.3V) relative to the power supply 0V. It's driven by an opamp (caps are just for decoupling, only R52 and R51 matter):
(this wasn't big enough for it's own section so it's in the MCU section, same as the other supply stuff).
Battery- goes to "D0V" (Digital 0 Volts), Battery+ goes through a 5V regulator to "VSYS". So VSYS is at 5V relative to D0V. The pi pico regulator creates "D3V3" (Digital 3.3V, relative to D0V, used to power digital stuff) from VSYS. "A0V" (Analog 0 Volts ) is connected through a choke to D0V, and U11 shown above creates "A3V3" (Analog 3.3 Volts) relative to that, which is used to power analog parts like opamps. So A0V ~= D0V (but hopefully without some of the noise), and A3V3 is 3.3V relative to A0V, so ~= D3V3 but may be up to a couple hundred mV apart depending on the regulators. COM is halfway between A3V3 and A0V, so is roughly +1.65V relative to A0V.
Upshot: split rails, A0V == D0V = 0V, COM is middle, A3V3==D3V3 = 3.3V COM ~= Common Jack, but only for low currents
We'll start with the Voltage section since it's pretty straightforward. I made a different version of this as well with a constant input impedance (10MΩ), while this one varies between 10MΩ at < +-1.5V and ~1MΩ at higher voltages. I'll go into that version later, it's more complicated and not sure it's going to work, while I've already tested this one.The basic parts are the inputs (Vpin) which goes directly to the front jack, same as Common_Jack, Common, which our reference voltage ("ground"), the protection resistors, R1-R10 (which total to 1MΩ), the attenuation resistors R13 and R14, two switches in U1, "Vout" which goes to the ADC, protection MOVs/GDT, and the frequency compensation caps. All U1 does is switch R13 or R14 (or neither), and their associated caps, to common. If we ignore the caps for now, this causes the node at Vout to be the center tap of a voltage divider, with R1-R10 (1MΩ) on the high side, and either R13 (100KΩ) or R14 (6.8KΩ), in parallel with R12 (9.1MΩ), on the low side.
This gives attenuation values of ~1.11 : 1 for neither resistor (and a 10MΩ input impedance), ~11.1 : 1 for R13, (and a 1MΩ impedance), and ~148:1 for R14. The ADC can take voltage from A0V to A3V3, so about +-1.65V relative to COM. To keep from nonlinearity near the rails, we'll limit to about +-1.5V relative to COM, this sets our three voltage ranges to 1.5*attenuation ratio, or about +- 1.7V, 17V, and 220V.
Note that while technically that last range means AFE should be able to handle it, this is NOT meant to measure anything outside of low voltage DC. I want to get some experience with dealing with front ends designed for this sort of thing and learn what sorts of problems come up, I'm sure there's some horrendous flaw in the design that will get someone killed if they build this and try to measure mains. If you have experience with this sort of thing, do let me know how exactly I messed up :).
The input resistors are split up like that, instead of a single big 1MΩ resistor, for input protection. This way I can gradually step down any overvoltages, with more resilient but less precise components up front, diminishing the fault as it moves to the right until very small parts can handle it without interfering with normal operation outside of a fault. R1 and R2 are larger through-hole resistors which should be able to withstand some higher voltage and abuse, and can be mounted for a very large creepage distance. RT1 is a 230V nominal varistor, which should clamp any overvoltage to about 400V at the node above it. So say we have 10KV at the input, RT1 will conduct with a voltage drop of about 400V, so well have 9600V across 200K, which should pass about 48mA. Most of that will go through RT1 and back out the common jack. For a very short pulse, this *should* be fine, way exceeds the power rating of everything but if it's only a few uS they shouldn't have time to heat up. This is also why we separate COM and Common Jack somewhat, while connected elsewhere (in the amp section) - higher currents in fault conditions should avoid passing through any of the more sensitive parts. So we've now got ~400V across RT1. R3-R6 are 400KΩ together, and RT2 has a glow voltage of about 75V and up to 10mA (won't dive into GDTs right now, they also have an arc condition but that doesn't come up in this example), so we should see a little under 1mA going through it, and 75V at the node at J24. Finally we've got another 400K, and RT3, which should limit us to about 175uA to COM, which RT3 can limit to just ~5V, from there R13 and R14 plus the input protection diodes can protect the inputs to U1. So we've handled 10KV at the input without damaging anything OR dead shorting the input and allowing high current flow (important for avoiding arc flash!), as long as it doesn't last too long.
At least that's the idea/rationale for everything, no idea what will fail in practice (I'm sure there's things I don't know here, so something would go wrong, part of this project is learning what some of those are, and how to do it right!).
Finally all the capacitors are to keep everything working at higher frequencies, since all our nodes are pretty high impedance, a few pF of capacitance to ground will start to form an RC circuit (most notably, the big MOV, RT1 has about 70pF). If we form a capacitive divider with the same ratios as the voltage dividers though, this effect goes away and the circuit can pass higher frequencies, and with the correct attenuation when switched.Whew that was a lot. Moving on to the Amps section. Bonus we finally get to get this whole COM vs Common Jack thing sorted out.
Well *that's* a lot simpler.
We'll start with the ~10 Amp range. Current comes in the front panel jack ("Amps" label) goes straight through a fuse, through R15 which is a 4-terminal shunt resistor, and straight back out the Common Jack. To avoid having voltage drop in the board traces at higher currents throw off the readings, we use a "4-terminal" resistor. Really it's a 2-terminal resistor because they're cheaper, but we're setting up the footprint specially to force some small sense taps right at it. This is where COM comes from - so it IS wired straight through to Common Jack, BUT has these smaller low-current traces and may have some voltage drop relative to it. So, we force it to be connected at only this one point to avoid ground loops and measure only the resistor. This also means fault currents passing to ground out of the Volt AFE can only go straight back to the Common Jack itself, not through other stuff where it might hit a thin trace and cause problems. During normal operation, there will be very very little current flowing from COM in/out of the jack, so they'll be at basically the same voltage.
mA and uA are similar but have larger shunts, R16 and R17, J5 and J6 are wired to a mechanical switch to switch between ranges. Not much else to write home about here, the resistors on the right offer the same kind of input protection to the ADC (uA_Out, mA_Out, and A_Out go straight to the PGA/ADC). Since we're shunting power to ground through the R's and have the fuses already, and there's no actives at all, we don't need much in the way of fancy protection. I may want to add some diodes to limit the maximum burden voltage on the uA range, but have to start worrying about leakage and stuff and it's just another part to add. May be a problem if it's like, put across mains in uA range, but not super worried about that particular situation.
Ohms!
Oh ohms.
This part was the biggest pain in the ... patootie.
Biggest things I wanted out of this that made it hard was
a) Optimizing every part out of it that I could - I wanted this as simple as possible. In the end I got it down to 6 transistors and one opamp
b) Diode test should be at least +3.6V, for white and blue LEDs
c) No separate ohms jack, has to be a single port like most regular multimeters
d) Some level of input protection. Not expecting this to be bulletproof, but should have some chance of surviving high voltage on the port
e) The range of values we have to cover, from single-digit ohms to megaohms - so >6 orders of magnitude of resistance all have to be accounted for
Basic theory of operation is, obviously, based around Ohms law, inject a known current through the resistor we're testing (which I'll shorten to DUT: Device Under Test), and measure the voltage across it. So the Ohms section is just responsible for sending a known current out of the voltage jack, and then we use the regular voltage AFE to measure, well, voltage.
This went through MANY iterations, all around one of two basic ideas. The initial idea was to have a full high-side current source, set by a logarithmic DAC. This was possible but added a lot of complexity and cost, many opamps to do ranging, level shifting, feedback, and measurement. The way I settled on was to just switch one of 4 resistors to the upper rail and measure the voltage drop across is (and thus, current though it). One big change that simplified things a lot was separating the input protection for ohms from the voltage section entirely, originally I had some resistors and a PTC common to both paths, which would be added to the DUT, so any significant resistance or a PTC would make it impossible to measure small values. At some point I realized that if I think about it like a kelvin/4-wire/remote sense terminal at the voltage jack, with completely separate protection paths for volts and the current source, then I can completely ignore the input protection, while it will have some voltage drop from the current source, as long as it doesn't sink any current to ground, it will be the same amount of current going out the jack. The final design for this uses a bank of 4 resistors, from 500 ohms to 1M with a pfet connecting each to a ~9V positive rail (made by a boost regulator), and the other end connected, through the input protection, to the voltage jack. By measuring the voltage drop across that resistor and knowing its resistance, we know the current through it. Knowing that that current is also going through the DUT, and measuring the voltage across the DUT using the regular voltage frontend, we can compute the resistance.
This circuit has three different "flows" (current out, current measurement, and protection), so it's not quite left-right like most schematics. Current out flows top down to Q1, then left out of Vpin, measurement goes from the FET/resistor array to the right through U4 and out at Isense, and protection "flows" from Vpin in through up to Q1, and down to the section at the bottom. Little weird but this kind of made the most sense in my head.
At the top we can see the isolated boost regulator (U6) to make ~9V. Q12 is just to switch it on and off so it doesn't draw power when we're not in ohms/diode mode. This puts about 9.5V relative to COM at the "9V5_Ohms" node. Because it's an isolated regulator, even through we're drawing power from the D3V3-D0V rails, we can have the return path be to COM, so we're not pushing any of this current back into the opamp that generates COM. If we did it could cause noise on the rail or pull it around as current is drawn. We only ever source about 5mA, so the opamp *should* be able to hold it, but still best not to.
D2 drops the 9V5 by a little bit before getting into the shunt resistors, so the positive rail for the opamp (U4) can be a little higher than its inputs will ever be. It is a "rail-to-rail" opamp so hypothetically it'd be fine not to, but this gives us a little breathing room and avoids any weird nonlinearity or loss of performance *right* near the rail. We'll get back to U4 in a second.
From the cathode side of D2, we get to the rail we use for everything else. One of the 4 P-Fets, Q2-Q5, are turned on to enable a specific range, set by the resistor at its drain, R26-R29. R26 for the <1K range, up to R29 for the >~300K range. Q7 through Q11 and the other resistors around there are just to allow the microcontroller to switch the gates of the fets, which have to sit at about the 9V rail to be off.
From there we tap off the voltage at the low side of the range resistor and send it to U4 to measure the voltage across the resistor. We'll get to that in a second what matters for now is that (effectively) no current flows that way, it all goes down to Q1. We will... also get back to Q1 in a second, for not assume it's just all the way on and passes the current down. Since the gate on a fet (effectively) draws no current, all the current flows down to the node at J16. D6 is reverse biased, and RT7 is a MOV that's under it's minimum varistor voltage, so neither draw any current, so it all passes to U5. U5 is a solid state relay, again assume it's on, so all the current passes through to D1, F3, and R25, this stage is all input protection. Ignore the wire heading down, and finally J7 and J8 have a switch between them to connect this circuitry to the voltage jack (Vpin)! Finally.
Finally getting back to U4. So the current is measured by the voltage across whichever shunt resistor (R26-29) is selected. Voltage is measured across both the Pfet and resistor, so from the net with J13 to the one with J15. Since the RdsOn of the fets is really low, it doesn't contribute significantly so we can assume we're pretty much just measuring voltage across the resistor. What we're looking to do is measure this differential voltage without drawing current from J15, and output that relative to COM, somewhere within the range of +-1.5V. Note that while the J13 net is relatively low impedance, the J15 net may have an impedance up to 1 Meg, and since we're counting on all the current from the resistors going through the DUT, we can't draw any current from it anyway. This means a normal differential opamp circuit is out, the resistors used would draw current from the net. What we'll do is convert the differential voltage to a proportional current (being sourced from the high voltage rail), and sink that through a resistor going to COM, converting it back to a voltage, relative to COM.
To accomplish this, the J15 net, goes directly to the + side of the opamp, U4. Basic opamp circuit, long as the feedback is set up correctly, this will drive the - terminal to the same voltage as +, so we can assume it is at the same voltage as J15. Notice then that R30 is connected between opamp - and J13, so it has the same voltage across it as the shunt resistor. Thus, it will have a current through it proportional to that voltage. To keep this current from just being sunk into the opamp, we put another pfet (Q6) with its gate to the opamp output, which means that while the opamp can control the current through R30 by keeping Q6 in its linear region, all that current ends up at the drain of Q6 - hey presto, this forms that voltage controlled current source we said we wanted a second ago! Finally that current goes through R31, and we've got our sense voltage. By setting the ratio of R30 to R31, we can set the ratio of that output to the input voltage as well, in this case 3.3:1 - this means that with the ~1.5V range of the ADC, we can measure a shunt voltage of up to ~5V.
The only real disadvantage to this is that the sum of voltages across R30 and R31 can never exceed the supply voltage - about 9V (plus we need some overhead for the pfet to have room to work). So for example if we have a shunt voltage (J13-J15) of say 7.7V (so sourcing 7.7uA to Vpin if we've selected R29, 1M range), then R30 *should* have 7.7V across it, which means R31 *should* have ~2.3V across it (=7.7 / 3.3, ignore that that's out of the ADC range for now). 7.7V+2.3V = 10V though, and we've only got a 9V supply! So that can't happen, what happens instead is the opamp turns Q6 all the way on, effectively forming a voltage divider of R30 and R31, which gives us about 2.1V across R31 and 6.9V across R30 - so we're no longer measuring current accurately!
The way this can happen is if our DUT is very small relative to the shunt resistor, worst case would be a dead short when we've selected the 1M shunt, R29. If this happens, R25 will only drop a couple mV, and since we're only sourcing ~9uA, D1 will only drop 0.2~0.3V or so so J13 is basically going to be at 0.2 or 0.3V relative to COM, so J13-J15 ~=8.7V, way too much. The way we fix this is having a couple volt drop in that path, if we have *something* (hint: it's Q1) that has a voltage drop of say 4V, then while the net with J16 might be at ~0.3V, J15 will be at 4.3V, so J13-J15 is only 4.7V, perfect! We could do this with something like a voltage reference, that would give very consistent drop, but we can get another benefit by using the fet. Note the numbers used there were pretty arbitrary, you wouldn't measure 0 ohms with that range, you'd use R26, but then R25 and D1 put it back in range, in reality it's values between ranges where we run into problems. This *could* be avoided by having like a dozen different ranges, but this kept a few parts off the BOM.
For low value DUTs, R25 is large enough relative to the shunt to drop enough voltage for this, so we don't need to drop any voltage in Q1, so we pull LowV_EN (which goes to a digital output pin on the microcontroller) low, pulling the gate of Q1 up to the rail, and turning it all the way on.
For higher ranges where R25 doesn't contribute much, LowV_EN is pulled high, turning on Q8, and forming a divider to hold the gate of Q1 at about 4.5V. Q1 is configured as a source follower, if you're familiar with these skip this paragraph, if not read on, uncommon enough use I figure I'll go through it. Basically the source is the output, the drain is connected towards the positive rail (in this case, our shunts). In an (almost) ideal FET, any voltage between source and gate turns it on, so if the source is pulled below the gate by some load (our DUT this case), it will turn on. Since the DUT has some resistance, the fet turning on will push current through it, increasing the voltage across the DUT, an thus raising the voltage of the source. Once the source gets up close to the gate voltage, it will start to turn back off, and sort of "balance" just below the gate voltage. This normally has the effect of buffering the gate voltage - since the fet can pass a ton of current from drain to source, we can source a ton of current from our positive rail to our load/DUT, controlled by the gate voltage but without drawing any current from it. In a real fet it takes some gate voltage to turn on, so it will balance at about that point below the gate. So, upshot is, if we hold the gate voltage at about 4.5V, and assume that Q1 has a Vgs of about 3V at the current we're passing, then it will keep the source at somewhere around 4.5V - 3V = 1.5V.This actually solves two problems - one is getting some voltage drop, doing the math on the possible ranges and resistance values for the DUT (not going to go through that here this has already gone on FAR too long, exercise for the reader :) ) we can find that no matter what our DUT is, at least one of the ranges will both drop enough voltage across Q1 for the shunt sensing to work, and have the measured values be in a reasonable range, at least a couple hundred mV (in theory we could ignore all of this and just use the 1M shunt for everything, but then the voltage across a small DUT would be like, microvolts - difficult to measure accurately). The other problem this solves is the voltage going through the Volt AFE being over 1.5V - if it is we have to go to a higher range, which decreases the input impedance to ~1.1MΩ, which will be in parallel with our DUT. Not a problem for values less than a couple hundred KΩ, but towards the MΩ range, that's going to make it more difficult to get accurate readings. We will calibrate out the effect of the Volt AFE impedance, but that will simultaneously decrease our sensitivity to the DUT, AND force us to a higher range where the reading will be a smaller portion of the ADCs range, both hurting our accuracy. This source follower forces the output to be below about 1.5V, so we can stay in our 1:1 range, with 10MΩ input impedance! It's not super precise but is good enough.
Whew, that was a lot, but we've got all the parts on how this works. Imma put the picture again just so we don't have to keep scrolling up:The couple passives to the right, C28 and C29 are just bypass caps for U4, R43 is there to load D2 so it's got at least ~0.5V drop, without that the load through our shunts will raise and lower it considerably based on the range and DUT. Doesn't really cause any big problems now but might cause some annoying problem down the line. This stabilizes it enough. Could use a ~8.5V linear reg or voltage reference instead of D2 to get even better stability and noise, but I'm not really worried about it. R57 and R58 form a voltage divider, HVsense goes to our micro's ADC, so it's got an idea what the J13 rail is sitting at. Won't be used for the actual ohms calculation but good to know to see if there's some error with it.
Last couple parts aren't super important for operation and are just protection. R25 is used just to limit any current from an overvoltage, it can't be ~1M like the voltage AFE so we can still source about 100mA/100V which isn't great (so plugging into mains would allow 170mA, probably enough to mess something up). F3 is obviously a fuse, we're never pushing more than about 5mA in normal operation so this should be more than enough to never trip unless we do something really bad. Finally D1 will keep any reasonable positive overvoltage from getting in. If we're seeing more than 1KV at the input ... yeah this thing was always going to blow up. RT18 and D6 limit any voltages that do get through, if we're at say the 1M range it won't take much leakage through D1, or big negative transient to pull this node otherwise. D6 will keep the rail from ever going below COM, which is the main concern here (that could potentially blow the gate on Q1). Finally U5 is used for negative overvoltage (which always feels a little weirdly mind bending to think about for some reason, really large, negative voltages...)
The lowermost section will keep the SSR's control LED on most of the time, but if there's a negative voltage at the input it will shut off, protecting the rest of the ohms section. R75 and D5 mirror R25 and D1, limiting the maximum current, and positive voltages. If we get a voltage below COM, C72 (which is a 1.6KV film cap, similar to the Volt AFE ones) will start to charge up. Time constant is only few uS, so D6 should be able to handle it for that time. That will go through R77 to the gate of Q20. During normal operation, R78 will pull the gate high, turning Q20 on and turning the SSR on. When a negative voltage comes through R77, it will pull this gate down towards or even below COM, shutting it off. U26 limits the voltage at the gate to +12 or -7V, so we can't blow up the gate. It's internal protection *should* be enough, but I'll put this in for now. If I'm really shooting to optimize the BOM I can probably cut this. There's a cap and stuff so that this will stay off for some time after a fault, C and R's are sized so it should stay off for a full power line cycle if connected to mains. Finally, if Path_DIS is brought high, it turns on Q21 and shorts out the SSR LED, turning it off. This can be used if we detect a fault and don't want to just rely on the undervoltage circuitry.
At some point I kind of want to make this a transistor based thing to cut cost down, that SSR isn't super cheap, but I already had a couple of them on hand and it simplifies things a bit.
FINALLY on to the next section, the MUX/PGA and ADC
Not a ton going on here, basically the MUX/PGA (U2) is an MCP6S28, which combines an 8 channel multiplexer and SPI-controlled programmable gain amplifier (PGA). For this and the ADC, I've got them mounted on breakout boards so they can be replaced with a different part in future if desired, to improve performance or add features or something. This is also why there's a bunch of additional test points nearby, they'll be placed next to the breakout board footprints in some standardized way (TBD), part of the modularity I was talking about way long ago. Minor note on the PGA, you'll notice J79 means its MISO pin isn't connected - this parts "SPI" isn't quite, it's essentially just a shift register that controls the channel and gain. So, MISO is low-Z even if CS is high, so it will interfere with the ADC. We don't need any data back from it, so it's fine to leave this floating. If I end up replacing this with some other part in future, a 0 ohm resistor or solder bridge on J79 will reconnect it.
ADC is an MCP3461R, a decent, 16 bit, fully differential ADC. One nice feature is it can run up to 1MSPS at 12 bit, which should let this have an "oscilloscope mode". It won't be very good but in a ton of cases I'm measuring something that's oscillating or browning out or something and a meter just isn't fast enough and I don't want to go take this to the bench (or a scope into the field!) - just being able to see a simple waveform on my meter would be a godsend. I know scopemeters exist but they're expensive and designed around being an oscilloscope that's portable, rather than a meter that can kind of act like a scope in a pinch.
One thing that I want to make as standard as possible is having a 1-wire or I2C EEPROM on all the modules to identify them so they can be swapped without changing the MCU firmware. That's well down the road but adding connections for it now.
Last part is labeled the MCU section but has a couple assorted, related things that don't really need a separate sheet:
Top left we find the amp that generates COM from earlier, over to the right of that is the battery input. There's a pfet to allow it to be disabled if we're running on external power or if we want to soft-power off. When powered on, C4 is discharged and will hold Q18's gate low at first, keeping it on. It takes about a half-second to charge, and in that time, the MCU has to turn on and pull Battery_EN high, which will keep it on permanently, or this pfet will shut off, and we'll lose power again. For an auto-power-off that will actually cut all power, we just drop Battery_EN to ground, and it will allow Q18 to shut off, completely cutting off the battery power.
From there a simple 5V linear reg feeds that to VSYS, which notably can take 5V at its output without drawing significant current or backing that through to the input. Is also very low drop out and will pass voltages below 5V to the output, so we *can* run with a battery voltage, and thus VSYS, below 5V. The Pi Pico handles this just fine, and pretty much everything else runs on 3V3 (the only regular part running directly on VSYS, the digital isolator, U19, can tolerate down to 3.3V as well).
M1 right in the middle is a pi pico, not really much to say there. Notably it should be able to be replaced with anything else with a pico form factor, may come in handy if we want to use something else at some point.
Section on the right is the isolated communication part. I wanted this to be able to talk to, and be powered by, anything without having to worry about isolation in that part. U7 passes isolated 5V DC, U8 is just a control signal to turn that off, though I probably won't end up populating this and rely on the other end to disable its output, I wanted the option. U19 is a digital signal isolator, used to pass a full-duplex UART. U18 is a TTL to RS-422 converter, the main way this talks to the outside world is RS-422, so we can use long cables if desired. This whole section will go to a DB9 connector. If we want to say, log to a computer far away, a simple USB-422 bridge will allow a pretty basic cable up to reach 10's or 100's of feet. The last part, U24 and U25 is an option for an LVDS transmitter, this will allow for higher data rates, U19 can handle up to 100Mbit and this can go up to 400. Not quite as robust as RS-422 but might be interesting to have, especially for doing something like high-rate streaming. Don't think the pico's UART can even hit 10Mbit by default but may be a way with PIO or something. U25 is just a 3V3 regulator for it.
In normal operation, the screen will attach to the rest of the meter through this DB9. It's got a separate microcontroller and everything and just talks to this one over UART. Again part of the modularity - if you want a different screen (like a color LCD or OLED or something, or some processing grunt, or whatever, you don't have to redesign/remake the whole thing, just an MCU, screen, battery, and RS-422 transceiver. Plus then we can have a remote display from the whole front end, either with a cable or even wireless:
M1 and M2 are radio modules, footprints for both the common RFM69HCW and NRF24L01 modules. I've used both a ton, love the RFM69HCW for the range and reliability, it's a fairly low-bitrate 915Mhz radio. Back in 2016 I retrofitted an old VHF R/C radio with one of these, and made some receivers with attiny84's, for my quadcopters and planes, been using them ever since. The NRF24L01 is great because it's cheap as chips and you can get them everywhere. Not as good range but can be much faster. I wanted to have some option for wireless coms that ISN'T bluetooth to a phone. It's a short range, unreliable, annoying to set up protocol AND requires me to use my phone to use my multimeter? No thank you. Planning to have a radio in both the front ends here and the screen, so the whole screen module can be detached but still operate normally. For the RFM I've also got a couple other things connected to some GPIO that that chip has, I don't plan to use them with that radio, but should allow for a serial or I2C controlled module in the future instead.
U12, U13, and U16 are some regular ol' shift registers, just didn't have enough IO on the pico to run everything. I realized in writing this I need a couple inputs as well (to read the switches to control everything and enable ohms and mA range), so I still need to add an input register. Oops. Not really much going on with these in any case.
U14 is used as a digital mux, to connect a pin from the MCU to any module to talk 1-wire to it for ID purposes. Q15 and R59 are potentially a replacement for that, to cut down cost. Since 1-wire is pulled up by default, the devices are on the source side, and devices never initiate a transaction, it *should* work with just 1 p-fet, If it were backward the body diode would conduct and ruin everything, but this should be fine. Not fully implementing it in this version (which is why Q15 and R59 are DNP), to save space, but do want to be able to test it. U20 is a 1-wire EEPROM that's on this board, to store some basic hardware info about the board, maybe even calibration info. That way, again, the MCU firmware can be common to more than one design, without having to change if different boards have different parts populated, etc.
Finally U15 is the "input module" (I'll come up with a better name at some point) - the idea is that I can have some headers here with several connections available, and make smaller boards for special functions. Also, again back to that idea of modularity at many levels. Two big things I've wanted in a meter forever is a little built-in electronic load, unfused high current measurement (for things like motors and motor controllers, that have inrush current and loads in the 30-50A range), and XT-60's as input connectors.
At it's most basic, an electronic load can be *so simple*, just a transistor, opamp, and shunt resistor! Won't be able to dissipate much power in this form factor, but could do loads of a couple hundred mW or big pulses really easily. For both the XT-60's and high current range, you can get parts like this: https://www.digikey.com/en/products/detail/allegro-microsystems/ACS711KEXLT-31AB-T/3868195
which are fully isolated, do +- 30A (there's similar parts that do 50 or 100A too!), have almost no burden voltage, and can take peak currents well outside that range. Does everything I want for $1.50. Originally I was planning to build in a part of XT-60's with one of these between them to measure current and a differential input to measure voltage - wouldn't be fully isolated but good enough for little battery powered stuff, but at some point realized there's so few parts in this bit, and so many other little things like this (like an E-load) that might be *super* useful, it'd be cool to be able to swap out just this one part of the meter. So, I'm making this a little ~1"x1"x1" cube with some headers at the bottom that goes to the top surface of the meter, so I can add pretty much anything without having to redo the whole processor, power, adc, communication, isolation, case, and everything, it can be as simple as a little 3D box and a chunk of perf board and some passives. The analog switch below is because I wanted 4 analog inputs, but only had 3 left on the main mux. The DNP fets are to test muxing the UART. I've only got 1 UART left, but want the option of talking to a couple different modules (sorta like the 1-wire thing). Planning to have the standard be to have the module hi-z its TX pin until the mcu asks for something, but might be nice to have the option to have the mcu control it instead. Probably won't use this for anything until the next version, just want to test it for now.
Aaaaaand I think that's everything!
This ended up being way longer of an explanation than I expected, but served as a good rubber ducky of the design, found a couple little things I needed to change/fix during it.
And we're on to layout!
-
Fix for ohms section ranging problem
09/03/2023 at 03:20 • 0 commentsGOT IT!
Figured out how to make ohms work well. Was looking for a way to get the ranges to overlap better, which would be helped by dropping some voltage (even at very low currents, in the uA range) from the source while keeping the output voltage somewhere around 1.5V and decent drop across the high-side shunt.
I asked on the stack (and previous log) for a part with a relatively consistent voltage drop from a couple mA down to a few uA, if anyone's looking for a good solution to that problem, a 2-terminal voltage reference, Paul Stoffregen suggested a REF1112, does exactly that. For some reason I didn't think about a source-follower mosfet though (that doesn't work for the question as asked, but does work for this system as a whole). Finally got around to actually simulating something since I'm away from home and can't test it in person, so I've got a piece of the schematic:
It's not super clean, but all the stuff up top is effectively just some pfets switching the 510, 10K, 100K or 1Meg resistors to the 9V rail. R10 is the resistor being tested, R9 and the diode are input protection, M5 is the source follower (the new part), and Q5/R12-14 are just to enable and set the voltage for that (disabling Q5 lets the output voltage swing up to the full 9V). For high value ohms, this would be enabled to keep the DUT voltage right at about 1.65V, below a few K, this can be disabled as R9 provides enough voltage drop at the higher measurement current. I know this isn't a great explanation, planning to do an actual run down of how all this works once it's working but don't have a ton of time right now.
That said basically the way ohms works is, the voltage measurement path (not shown, impedance ~= 10Meg, which will have to be factored in during measurement but can be ignored for this) is connected to the high side of R10, we then put some amount of current through R9, by knowing that current and the voltage across R10, we have ohms. Since we're effectively kelvin sensing at that node, R9 and D1 (input protection) won't affect the measurement. There will be other stuff as well that makes it more impractical to control accurately. One of R1 through R4 is switched to the 9V rail, we then measure the voltage across it (plus a pfet, but Rdson is in milliohms so can be ignored for now), so we know the current going through it, and thus R9 since there's nowhere else for current to go. This voltage is tapped off at the two wires going out the right at the top of the image, goes to an opamp and pfet which converts the voltage to a current from the 9V rail towards ground:
The ratio of R15 to R16 sets the attenuation (since the shunt might have 6+ volts across it, and we have to reduce that to ~1.5V). We measure across R16, since the current coming down is proportional to the measured voltage, this converts it back to a voltage, but now shifted to be relative to ground (remember shunt voltage is coming from 9V, which is coming from a switcher operating at very low load, so may be noisy or somewhat unstable relative to ground.
Switching between R1, 2, 3, and 4 (510 ohms to 1Meg) we get different ratios of volts per amp (so with a 1Meg shunt, if we're putting in 1uA, we'll see 1V across it, and thus R15, and thus 0.145V across R16). The voltage across R16 is what goes into the ADC as measured current.