Despite the lack of public updates, I have been putting some time into this. I don't plan to do much more with the revision 1, it was awesome to get something done, but there were few i's dotted or t's crossed on that design - it was an MVP and a learning experience, more than anything. Rev 2 is in process now, the main changes being a complete redesign of the voltage frontend, functioning resistance protection, and a proper board, with half decent power management, for the display. As might be gleaned from the pictures, I'm also hacking together some equipment to test both the temperature stability and input protection. Before making a board with the final form/fit/finish, I've gotten some test board made - thank you to Pcbway for sponsoring the boards for this project! they've been fantastic. These have a couple different footprints and designs, and switches to start getting some real numbers on things like leakage.
Edit
Oh I also finally bought an oscilloscope with a function gen and made a test script to characterize the frequency response of the frontend. I'll do a more detailed log later, probably after putting out the video. Also I'm like 2/3 of the way through a video going through all the details in the design and circuitry of both rev 1 and some changes coming in rev 2. So, look for that soon.
That 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.
I 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.
Another 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.
staticint 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 outuint32_t next = 0;
upperBit = listLength-1; // 0b1000... where the # of 0's is log(listLength)-1while( 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.
WAIT 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.
Spent 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.
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
Finally 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.
Some 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