📱

Get Our Mobile App

Take your business learning on the go!

Download on the App StoreGet it on Google Play

Fluid Simulation Pendant

mitxela38:02

Transcription

Today we're going to be building this fluid simulation pendant. This is a real-time flip fluid simulation running on embedded hardware within a gold-plated, hand-machined enclosure, and as far as I can tell, nobody's ever done something like this before.

It took a surprising number of clever hacks to make this work, and the result of those accumulated Eureka moments has led to a number of spin-off projects. I mean, really, I've outdone myself on some of them, but those projects, we'll have to wait for a later video.

This whole project is a stepping stone. Back with the volumetric display, I demonstrated a couple of pre-rendered fluid simulations, and naturally, the end goal is to have those volumetric 3D fluid simulations work in real time. So we could have a virtual snow globe filled with glowing voxels that splash about as you shake it, or a simulated flame that really responds to movement and airflow. Those are both excellent ideas. They're also obvious ideas once you've seen the volumetric display, given the number of people who later suggested them. So yes, that is the end goal, but we've got to walk before we can run. So I figured I'd try and make a 2D real-time simulation as a stepping stone towards that goal.

Back in March, I posted my SimSim concept, where I realized we could fake the fluid simulation by using a real fluid, a simulated simulation. This is basically a big mercury tilt switch with a grid of pads, so that as the mercury flows about, it lights up LEDs on the front. What I didn't post at the time is that shortly afterwards, I simulated the SimSim pendant. So this is a simulation, simulation, simulation, SimSim, Sim. I only made this in order to investigate how many or few LEDs we could get away with and still have it feel like a liquid.

One of the great things about writing it in JavaScript is that it's easy to run it on a phone and use the real accelerometer data. And yes, here at MIT, even our internal testing tools have accurate shadows.

This algorithm is known as FLIP, or Fluid Implicit Particle. The other common way of simulating a fluid air combo is SPH, or Smooth Particle Hydrodynamics, but I went with FLIP for a few reasons. For instance, Blender's Mantaflow, which I've used before, is doing FLIP. However, the implementation you see here is not directly ported from anywhere. It's my own implementation based on the work of others.

FLIP goes back to the 1980s. It was originally developed for modeling plasma physics. I started by reading the academic papers, but there's a lot to get your head around. It's a bit later that FLIP started to be used for special effects. A lot of the resources pointed towards this paper from 2005 called "Animating Sand as a Fluid," but by far the most useful resource was the work of Matthias Müller. He is a principal engineer at Nvidia and has authored a number of papers on simulations. But astonishingly, he has a website and YouTube channel called "10 Minute Physics," where he attempts to explain quite advanced physics concepts in 10 minutes. I just absolutely love that he's done this, and I couldn't believe it when I saw he has a video called "How to Write a FLIP Fluid Simulation," explaining FLIP in 10 minutes. It's ambitious, and it certainly took me a lot longer than that to understand it. I had to pause and rewatch the video several times. He's even got MIT-licensed code examples, though I avoided using these where possible. I wanted to understand every step of the process, so I reimplemented the fluid simulation following the outline in the video. I really believe that the best way to make the world a better place is to improve education and access to educational materials, so thank you, Matthias, for putting these videos up, and everyone should check out his channel.

FLIP and its predecessor PIC work by using both an Eulerian grid and a particle system. An Eulerian fluid simulation draws a grid of cells and monitors the flow of fluid into and out of the face of each cell. The net total flow is called the divergence, and if we iteratively adjust the numbers so that the divergence is zero, we will make this fluid incompressible.

To bring the fluid to life, we need these numbers to shift along based on the velocities, and one way of doing this, in the simplest terms, is to look at the velocity field and ask, "What sort of movement would lead to these velocities?" Then, with a whole load of weighted averages, we adjust the velocity at each cell based on the velocity of where it came from. This on its own can produce some cool results like vortex shedding, but it's not enough if we want to have a liquid-air boundary. So we also implement a particle system and use the particles to keep track of what is liquid and what is air.

At each step of the simulation, we advance the particles by their velocity and manage collisions between them. Then we transfer the velocities of those particles to the Eulerian grid, which involves lots of weighted averages, as the particles will be straddling multiple cells. Next, we solve for incompressibility in the grid, and finally, transfer the grid velocities back to the particles. In FLIP, we don't directly set the particle velocities, but adjust their velocities by the changes in the grid, which stops the particles from losing their individual motion.

Each of the steps is remarkably complicated. Even the particle collisions, for it to remotely be viable in real time, they need to be handled via a hash grid. I've encountered hash grid collisions before in the context of video games, so it's interesting to see it used here. Matthias, in fact, has a separate video on how to implement hash grid collisions. The goal is to hash their positions and construct a lookup table so that we can fetch all of the particles in the nine neighboring squares and check for collisions just with them, rather than checking against every other particle. Naturally, the optimum grid size for this is different to the size of our Eulerian grid.

It took me about two weeks to get my simulation into a state I was happy with. I made a few decisions differently to Matthias's code, mostly arbitrary things like how the memory is laid out and some questionable optimizations that I didn't really profile. I made the grid size exactly one, so the inverse spacing is also one, and you can floor the particle position to get the grid location. Although it's JavaScript, I wrote it in my very agnostic way, almost like pseudo-code, so porting it to C took about 10 minutes.

To turn this simulation into a real physical thing we can hold, we need to choose a microchip. We need something quite fast, definitely with an FPU, but also physically small and ideally low power. And just because I already have a dev board for it, I turned to my old faithful, the STM32 L432 KC. This is the same processor I used in The Flash Synth. Over the last few years, a load of new STM32 chips have come out which are even lower power, but I quite like the idea of running with this slightly older chip instead of relying on the very latest parts. We're doing something that could have been done years ago, just nobody thought to do it.

I ported the algorithm to the chip, and we can test it by dumping ASCII art out over the UART. I found that with a diameter of 16, in order to hit my target of 60 FPS, yes, I had to slightly overclock the chip up to 100 MHz from the nominal 80, but that's probably fine.

At this point, the design was set. If we use 0402 LEDs at a pitch of 1.5 mm, the display will be about 25 mm wide, which will sit nicely over a LIR2450 coin cell. I do want this to be rechargeable rather than needing to swap out the battery, so we'll need to add a charging connector and a charge controller. We'll need an accelerometer. I went for quite a fancy one that gives extremely low power in standby mode. I like the idea of not having a switch. You put it down and it goes to sleep. Use it, no power. Pick it up again, and it turns on automatically.

The only thing left is how we're going to drive the display. As always, the matrix takes up most of the PCB, and even a multi-layer board has the problem that the large number of vias make it very hard to position and route things on the back. For a smaller matrix, we could wire it directly to the GPIO, but at this size, not only would we need 32 GPIO pins, but the limited on-resistance of a normal GPIO means we get weird brightness effects depending on how many pixels are on. The proper way to do this is with shift registers, and it's possible to get very small shift registers, including ones with built-in output FETs for the low side. But routing this is still going to be very difficult.

So instead, I looked into Charlieplexing it. For our 20x16 LEDs, we could do that in 16 GPIO pins. I'm no stranger to Charlieplexing. For instance, the Industrial Piercings Charlieplexing. In that case, not to save on GPIO pins, but to minimize the number of tracks. Things tend to get a bit weird when you try and make bigger Charlieplex displays, though. There are weird failure modes, and there's a lot of overhead as you need to illuminate each pixel individually. It's also maybe a lot more mentally taxing to route.

If you cast your mind back to 2017, when I did the Charlie Star project, I spent a long time thinking about Charlieplexing. I managed to route six LEDs onto three pins on a single-layer board, but it was such a mind-bending experience that I ended up writing a poem about it. The experience taught me something important about Charlieplexing: that it just consists of a pair of opposing LEDs between each unique pair of GPIO pins. As a constraint, that's not very constraining. As we had more LEDs, the number of ways of drawing a schematic that meets that constraint expands exponentially, and the human brain is not very good at this. You can have two schematics that have identical netlists but look completely different, and it's almost impossible as a mere human to mentally reshuffle one into the other. The only viable way to produce a larger Charlieplex display is to find some pattern that repeats.

It seems a consensus on wiring up a Charlieplex display is to do it like this. This is actually a KiCad script someone has written to produce Charlieplex schematics, and the main appeal here is that it looks very similar to a conventional matrix. It also makes it really apparent where the N squared minus N comes from, as it's all the unique combinations of pins minus the diagonal where each pin would be combined with itself. The main problem with this is that it's still going to be a real pain to route. It'll be at least as difficult as a conventional matrix plus the wibbly bit down the middle, and I'd really like to mount all of the support components on the back of the same circuit board.

There are certainly lots of other ways of doing it. What I eventually went with was this. I'm not the first person to wire up a Charlieplex matrix in a diagonal crisscross, but I might be the first person to point out the routing benefits of doing so. To start, putting the LEDs end to end has a minor benefit that if they're surface mount, a solder bridge isn't going to affect the behavior. But the real benefit is more subtle than that. In the conventional style, we have rows and columns that necessarily are on separate layers, so at a minimum, we need one via per LED. But in the diagonal arrangement, the nets only cross twice for every four LEDs. In other words, we can route this in half the number of vias as a conventional matrix. That is huge. That makes all the difference when it comes to sticking surface mount parts on the back of the board.

I spent a long time drawing out diagrams on paper before I was happy. I eventually hand-wired a small matrix just to prove that I'm not going crazy. The repeating unit is four LEDs and two crosses. We can duplicate this and make the pattern as big as we like. The pin labels go between the LEDs on the edge here. The signals alternate up and down diagonally and reflect at the edges. That gives us N squared minus N LEDs for N GPIO. Then the final LEDs terminate the signals on the edge here, but they don't match the pattern. Need to squish things around to get this into an octagon. Anyway, we then re-annotate the schematic so the references are sensible.

We can kind of follow the same procedure for the component layout. It's a little trickier, or at least it's easy to do this slightly wrong and have all the net names mismatched. A fairly recent addition to KiCad is the ability to geographically re-annotate components on the PCB, so as long as all the parts are in the right places, you can re-annotate them to match the schematic, then call "Update PCB from Schematic," and the net names of all the associated tracks should update to be correct. I recommend practicing doing the layout with a small number of LEDs first. Potentially, we could have scripted this layout, but I find that if you figure out exactly what to copy and paste and use a custom grid size to position it, you can do these kinds of layouts very quickly, even on four layers.

Routing this is going to be tight. The microchip is a QFN with a big pad underneath, but we're not worried about thermal performance. This chip is so low power that even overclocked, it draws less than 10 milliamps. I think we do need an electrical connection to it, but that can just be a tiny little pad. The rest of the chip can sit over vias and solder mask. The rest of the parts can be squeezed in without a problem. We do actually have a fair amount of freedom to move the display vias about. I went for the absolute smallest and lowest power LDO I could find, claims a quiescent current of 25 nanoamps in a 1 mm square package. There's a spring pin for the battery, and we have to make sure that all of the capacitors are fairly low profile, at least lower profile than the other pins, so they don't short on the battery.

All that's left is to route the display signals to the processor, and this was really quite eye-opening. If it's Charlieplexed, then we absolutely 100% need a lookup table between each pixel and its pin combination. So if a lookup table is mandatory, then there is no overhead to changing the mapping. That means that each of these signals can be routed to any GPIO pin of the microcontroller, whichever ones are the most convenient. This is not how circuit board design usually goes. We'll keep them all on the same GPIO port for simplicity, but what I ended up doing was routing each track to be near the processor, looking at what pin was closest, and then going back to the schematic to make the connection.

Probably the hardest bit is getting started. The number of possibilities of how to wire this up is now unthinkably vast. I'm not claiming to have chosen the best routing, but I did manage to fit it all into my target of four layers. Naturally, the board was given the usual treatment from my track rounding plug-in. Even though none of the traces will be visible, I even milled the internal layers, which definitely won't ever get seen, but it costs nothing, so why the hell not?

Finally, we can send that off to China and get started on the metalwork. The basic design is very similar to what I did for the Amulet, but because it's rechargeable, we won't need to open it very often. So instead of a fine thread, I'm going to try and make a snap-fit. This is basically just a groove and a lip, and if it works, should be very quick to make. The holes at the bottom got the charging connector and a status LED. At the time of filming, I was still without access to a metal workshop. I've been asking around for a while, and eventually, I made friends with Martin, and it's his lathe you're looking at now. It's a really nice lathe with lots of excellent tooling, so huge thanks to Martin for letting me have free reign of his workshop.

[Music]

My first cautious attempt at a snap-fit came together pretty well. Coincidentally, Martin just happened to have a bunch of O-rings in the right size, and with that, the seal should be watertight. And while he peered over my shoulder, he asked a perfectly legitimate question of whether I would be sticking a watch glass on the front. Indeed, why aren't I sticking a watch glass on the front? Well, that was pretty easy to make, so let's just make another one. This time with a recess for a watch glass, and like that, I suddenly found myself building two fluid pendants.

[Music]

Is?

Ordinarily, you have to think really hard about the order of operations before you part it off, but here, there's a whole shelf of soft collets, including one that was already the right size to hold the snap-fit by the rim. It's just unfair, isn't it? You shouldn't be allowed to chuck such an awkward part so easily.

The next step is to mount these parts in the mill, and to do that, I'm turning an arbor out of Delrin, which will go into a square holder. The arbor holds it securely, but we add a little bit of hot glue around the edge just to stop it rotating. I'm first making a slot for the jump ring with a 1 mm endmill, and then drilling holes for the charge connector and LED.

[Applause]

That's a magnetic charging connector that I've lightly pressed into the hole, and sourcing this was probably the hardest part of the whole project. I knew that this connector existed because I've seen them before on products, but finding a part number or manufacturer was impossible. There's a range of similar connectors on AliExpress, but this particular one has a very low profile. I eventually did find the manufacturer, and then their website had a link to their AliExpress shop where it was described with exactly the same terms I'd been searching for. So it's just that AliExpress's search feature is unusably bad.

The jump rings I made at home, just wrap some wire around a drill bit. I have been trying to do things quote-properly, so this is hard solder, borax flux, and a blowtorch. With rings linked together, we file it down so that it is an almost press-fit into the slot, and now I'm going to soft solder that joint. Lead-free, while balancing it upside down with a heatproof mat, definitely is not the proper way to do this. I did the gold plating off-camera. It involves a lot of nasty chemicals and PPE, so I didn't feel up to filming it, but I have shown the process before on the Amulet video.

Only a few days later, and my circuit boards turned up. I know you can get them assembled in China too, but when all you have is a pick-and-place machine, everything looks like a nail. I've recently started reflowing small boards like this on a mini hot plate. It's nice that you can see it progressing, and you can poke it with tweezers if anything decides to tombstone. And yeah, there's a few bridges here which aren't going to be an electrical problem, but I think they are a cosmetic problem, so I touched them up manually.

After components on the back of the board were placed by hand, but before we turn this thing on, I need to talk some more about the software. We need a lookup table for our display, and I can tell you right off the bat that I'm not going to be writing this by hand. I made a big deal out of annotating the references correctly, so as long as everything is named logically, we can export a netlist from KiCad in S-expression format, and then a few lines of Python can snarf this in and squirt out an array. Quickly coming up with one-off scripts like this is one of the most valuable skills you can learn. Of course, it's only worth it if you can write the script faster than it would be to transcribe it by hand.

You might be thinking that if we're already overclocking the chip just to run the simulation, how are we going to cope with the overhead of also running the Charlieplex display? Well, my young Padawans, or Padawan, allow me to expound the virtues of DMA. DMA is hardware that lets you copy data from one place to another. It started off as a simple way of freeing up the processor during a memory copy, but nowadays, it's turned into this complex behemoth that can do all kinds of things. A classic example is generating audio output on an STM32. We want to copy data from memory to the DAC, but we don't want to copy it all at once. We need to clock it at a fixed sample rate, so we control the DMA from a timer. We also don't want it to stop when it gets to the end of the array. If we put it into circular mode, it will start over again from the beginning.

On STM32, we get an interrupt both at the end of the transfer and at the halfway point, so this effectively gives us a double buffer. You generate samples into the first half while it's outputting the second half, and vice versa. But that destination peripheral doesn't have to be a DAC. It can be pretty much any register. If we copy data to the GPIO, then we can use this to output any arbitrarily complex pattern of ones and zeros on the pins of the microcontroller. In the past, I've used this to do an LED matrix, and it's really cool because in circular mode, it will just show a static image, and it will continue to show that until we change the data in the array. In fact, we can forget interrupts and do the whole thing asynchronously. If you want to change what's on the display, you just directly update the data in the array. Once it's set up, there is zero overhead.

What's cooler is that the DMA will still run even when the processor is stopped. So if you breakpoint the chip, the display still works, and there is no risk of burning out a column of frozen LEDs. As amazing as that is, DMA does still suffer from bus contention. So if we try to scale up the matrix to span multiple GPIO ports, we run into problems. We want all of the pins to change at the same time, but no matter how hard you try, you cannot update multiple registers in the same clock cycle.

In this instance, I did route all of the signals to the same GPIO port, but we're not running a straight matrix, we're trying to Charlieplex it, which means we need to control not just the output data, but also the mode of the GPIO. The fact we don't update the registers simultaneously leads to visual glitches on the display, these faint diagonal lines. By far the easiest way to get rid of this is to insert dead time into the array. Turn on a pixel, turn everything off, turn on a pixel, turn everything off. It costs us some RAM and lowers the max brightness, but it's certainly an easy fix.

Other visual glitches are caused by the debug pin, which we can fully disable when in normal use, and the interrupt line from the accelerometer. I had to wire this to Port A because that's the hardware wake-up pin, and the datasheet claimed that the pin is floating when not in use, but it turns out that the accelerometer has a bus keeper on there, which was chasing after the high-speed display signals. I fixed this first by adding a resistor, and eventually a diode on that line.

[Music]

Did you spot it? Yes, there's another bodge. Well, I wouldn't really call it a bodge, just a modification. I always consider a printed circuit board as a starting point, a framework ready to be embellished. At this point in my life, I am very comfortable soldering micro-wires to the pads of an X2S chip, only to change my mind later, undo the mod, and then solder something else. Bodging skills don't have to be perfect, you just need to be good enough to be able to undo any mistakes that might happen. Once you reach that level of stability where every change is reversible, building prototypes becomes a lot more fun.

This is all about the battery and what happens when it goes flat. These coin cells don't have undervoltage protection, so they'll get damaged if we run them into the ground. Now, since all of the parts I've used are so low power, simply detecting in software when the battery is low and shutting down the simulation should be enough. The STM32 has an ADC and a bandgap reference, but if we do that, we want to be absolutely sure that it comes back to life when you connect the charging connector. So I added a capacitor from the 5-volt line to the base of a transistor on the reset pin, and this works. Although there's a minor problem that the charging cable has some kind of polyfuse in it. It has to, because it's very easy to let it stick to a metal surface and short out the connections, and after you un-short it, the voltage slowly climbs back up to 5 volts. So it took a fair bit of fiddling to get this reset circuit somewhat reliable.

This first pendant went together and came apart several times as I changed my mind about things. At first, the display was too bright. In a pub environment, it was like staring into a torch. I thought about adding a menu to control settings, viscosity, and so on. We don't have any buttons, but we could do some kind of gesture control. I later decided that that was a terrible idea, and simplicity is more beautiful. But one thing I did add is a means to turn it off. It turns on automatically when it detects movement, but if you want to put it into your pocket or a bag, or want to travel somewhere, it is useful to turn it off.

I was thinking about gestures to do this, maybe swinging it on the end of a chain would be very easy to detect, but eventually went with this. You put it face down on the desk, and after a while, it will go into deep sleep, which is actually the same as regular sleep, except the threshold to wake it up is now much higher. So this will survive being handled normally, but if you shake it, it wakes up again. Perfect.

On the subject of shaking it, with the first assembly, if it got knocked in a certain way, there was a chance the battery would momentarily disconnect. When that happened, the software would see the voltage dropping, think the battery is flat, and turn off. It would then be latched off until the charger was connected, even though the battery wasn't really flat. I fixed this by making the spring tab stronger and putting little bits of foam around the battery, and that did solve the problem entirely, but it made me worried. What I'm getting at is, when I came to build the second pendant, I made another revision of the PCB. I added a hardware supervisor chip. Technically, this will reduce our battery life very slightly, but it means the undervoltage detection is now in hardware, and there is no chance of a software latch-up.

As for the watch glass, well, as a proper tool to insert those, but if you're careful, you can do it with your thumbs. This is me not being careful. Oops. But with a little coaxing, we got the second pendant together. The watch glass does add a bit of thickness, but in addition to protecting those LEDs, the whole assembly is now completely sealed. So in theory, it's waterproof, though I haven't tested it. Not too bad, but also not good enough.

I spent a long time showing these pendants off to people, including around EMF, and everyone loved them. But I can't help but notice the blemishes, particularly the finish of the metal. I didn't properly prepare the surface before gold plating, so I thought I'd build another one. This time, really make an effort to get the enclosure to look nice, and I also realized that if the watch glass seals the assembly, it technically is removable, but we don't need to open the pendant very often, so do we even need the snap-fit at all?

Here's the third pendant I built. It's machined like a cup shape with everything inserted from the front, and then the glass seals it in. The lack of an opening back means we've regained quite a bit of thinness, but the assembly was very fiddly. I had to mount the magnetic connector on flexible wires so the PCB can fold on top. It's a pretty good result, but you probably won't be surprised to hear that I then built a fourth pendant. The assembly is exactly the same, but this time I lapped the back surface before polishing and plating, trying to get this as close as I could to be a mirror finish. While it looks good, it's still not a mirror finish. It's incredibly hard to get the surface to be completely clean and flat before plating. Even just wiping it with a paper towel is enough to leave scratches, which then get embossed by the plating process. Still, I think the pendant is now approaching a standard where I'm happy with it. I'm just kidding. I built like another five pendants after this one. Sometimes I stop and ask myself, "What the hell am I even doing here?"

It was satisfying how quickly this first pendant came together from first posting the concept through to finished pendant. That's all of the fluid simulation, the software, the PCB design, the metalwork. In total, was just under four weeks, which is really quite pleasing. And even this first one has crossed that threshold that when people look at it, they don't say, "That's a cool DIY project." They say, "That's a cool product." Everyone wants me to sell it, but as always, few people realize how much effort is involved in building it. I'm not going to build hundreds of them, but I do have a small handful of handmade pendants here. So if anyone really wants one, and let's be clear, nobody needs a gold-plated fluid simulation pendant, but if you really want one and you're willing to pay an artisanal handcrafted price for it, then I've stuck a page on my website with a PayPal "Buy It Now" button, very much while stocks last.

That about concludes this video. I'm very glad to finally post this, as it means that now I can make videos about some of the other projects I did in the last year, which chronologically wouldn't have made sense to post before this. As always, there's more information on my website, mill.com. I have a Bandcamp page with some of the background music, and a Patreon page where some wonderful people have continued to donate money to me, even though I went for six months without posting anything. I really have no idea who my audience for this stuff is. Do people want animated diagrams about the use of DMA, or should I just do metalworking montages? If only there was some way of having a dialogue with the viewers. We can but dream. Anyway, I'm going to keep doing whatever I feel like, and I'll see you all next time.