-
Spring Clip Update
11/26/2023 at 23:11 • 1 commentHey kid… yeah you… wanna see some hot pics of the stamping mold for the clips?
** opens trenchcoat **
So, you’ve probably noticed that sticking things into the Jumperless’s breadboard contacts isn’t quite as easy or smooth as regular breadboards. Here’s why:
I was being to cautious with my original design, I was worried those would catch on the underside of the plastic breadboard or hit the walls and not open enough for larger wires. Turns out, that created the issue of leaving too much of a gap for things to get around the clip or catch on the edge.
So I’m having the spring clip molds updated with a small adjustment:
I’m just extending the arms by 0.25mm to hopefully act as a better guide for the wires as you stick them in.
Now they should cover the entire opening to the breadboard and make it much harder to get a wire around it and possibly damage the clip.
I know this doesn’t help people who already have them, and these probably won’t make it into the next batch being made, so you’ll probably start seeing these Q1 2024. I’ll set up some sort of trade-in program for people who are bothered by it and want the new revision. Or you can just ask me for a set of the new ones when they’re available and I’ll send them to you for free and you can solder in the new ones yourself.
Anyway, just to take full advantage of my Fusion 360 student license’s free cloud rendering credits, here are a bunch of renders of the new clip revision.
The reason that making changes to something like this is such a huge deal, is that they’ve made a big custom fourslide mold for this to do all the stamping and bending (here’s a SmarterEveryDay video about this exact process, just pretend everyone working there is Chinese.) So any adjustments to the part require them to CNC out parts of the mold and add extra metal bits on the mating surfaces. It’s an expensive process, this little edit will probably cost me ~$1,500-2,500 (or if I was doing this in the US, ~$60,000.) But it bothers me that it isn’t as perfect as it could be so I think it’s worth it.
For reference, here’s what I sent to the tooling engineers at Shenzhen Fulimei BBNew DrawingRevisedClipsUpdated.pdf (393.8 KB)
And the new drawing in case they decide to just make an entirely new mold bbSimpleWider Drawing v1.pdf (350.4 KB)
And the STL for good measure bbSimpleWiderOpen v2.stl (342.6 KB)
Let me know how much or little these clips are bothering you. If enough of you really hate it, I could hold up production on this new batch, have them dump the 40,000 clips they’ve made into the nearest wildlife sanctuary, and have them remake them all with this update.
UPDATE:There's also a good news update about all this. So, I ordered 40,000 clips for this current batch of Jumperlesses, and it turns out the spring clip manufacturer made 32,000 of them with positive tolerance, meaning they measured from the inside instead of the outside to account for the thickness of the metal, so they're too wide to fit in the boards.
Elecrow (the people assembling them) told them so and they immediately agreed to remake all 32,000 clips for free. I paid them half price anyway for the remake because they're a pretty small company and I didn't want them to lose money on my project; I want them to stay around because they've been great to work with. But, what that means is, the new clip update will be pushed out way sooner than I had expected.
They had 8,000 extras from the last run, so there will be 100 boards made with the old clips, and the remaining 400 will be made with the updated design. It's funny when someone comes to you with what they think is terrible news and you respond with "Holy shit that's awesome!"
So yeah, there will be 100 boards with the old clips ready before Christmas, and then 400 more sometime in January with the new revision.
I also ordered 10,000 extra clips so when they're ready I can swap out the clips on the boards you currently have if you like.
-
Jumperless Has Exploded
10/10/2023 at 01:32 • 0 commentsSo, apparently YouTube has deemed my Hackaday Prize entry video worthy to show to basically everyone ever.
Well, half of everyone...
C'mon ladies and User-specified people, don't make my analytics look like a drawing of a dick. Jeez, we really need to work on getting more women into engineering, if you're a non-male reading this who isn't into engineering (seems unlikely given we're on Hackaday), it's really super fun, I promise.
Anyway, now the Jumperless is backordered for a while
So now this isn't so much an engineering thing as much as a manufacturing/logistics project. Which is also pretty interesting so I'm going to write a bit about it here. This should also be posted on Manufacturingandlogisticsaday.
I have 50 coming in next week, which when I ordered them seemed like a lot. Now I'm working on getting an order together for 500, and I told them to hurry so I can hopefully get them in before Supercon.
The unknown variable is the spring clip manufacturer, Shenzhen Fulimei Technology Co. LTD. Their lead time is usually around a month, but they've been very helpful and awesome to deal with so they might be able to rush it a bit. Each board uses 80 clips, so for an order of 500, I'm going to need 40k of these things, it's crazy that I've been having to use engineering notation for real life objects lately.
I was originally having these made by JLCPCB, and they always do great work. But they're kind of too big to be bothered with finals assembly stuff. So I turned to Elecrow. The reason I went to them was because we had already swapped boards, they reached out to sell the Jumperless in their store as a partner seller, and they had given me a couple TFT display boards to review. So I could just look at the board they sent and the manufacturing seemed pretty legit, and they could grab a Jumperless to make sure it looks like the ones from JLC.
I was kind of worried about quality, those LEDs on the Jumperless have pads that are soooo close together, so the reflow has to be perfect. Normally you'd just turn the LEDs 90 degrees and they have a lot of clearance, but I wanted the long rectangular lens to line up with the slot in the clips.
I learned how touchy this was when I ordered the first batch from JLC. It was interesting that the main 4 layer PCBs were fine, while the 2 layer wishbone boards had a bunch of shorts. It seems like they build the more complex boards to higher standards, which makes perfect sense. And they had no places to probe for shorts on the wishbones.
Here I am trying to reflow these back to life unsuccessfully
Yes I melted the hell out of the lenses, I shoulda used a hot plate, but they were trash anyway. So I just redesigned this board with the LEDs rotated 90 degrees, which is the correct way.
The point of all this, it's sketchy to switch suppliers, especially when you just YOLO a bunch of DRC violations. Let's hope they're good, but I have a lot of faith in Elecrow. Any board assembly house willing to take sandpaper to a bunch of clear LEDs to diffuse them for me clearly takes some pride in their work.
I know what you may be wondering, will I have a bunch of these available at Supercon? The answer is hell yes.
I have 7k spring clips right now, so worst case, I'll have them ship me a bunch of kits and you can assemble them yourself in The Alley.
The advantage of dealing with me in person is that I'm a total softie who genuinely hates money, so if $300 is a lot to you and you seem cool, it's very easy to get me to give you one for free. Just use this verbal discount code, "Hey, I want one of these but can't really afford it." Boom, now you're a bonafide social-engineering hacker.
To the Hackaday Prize judges or anyone else committed to reading all the text of this project, I'm sorry you had to read through my extremely long-winded ramblings. As a reward for getting through it, here's a picture of my dog (Quark will be at Supercon, so save up some belly scratches for her.)
Okay fine, here's another more recent one.
-
Ṭ̴̯̿̂h̶̫̏̀ę̵̙̒ ̷̩̉C̴̖̞̀͝ọ̵̬̎̔ḓ̵̓e̸̥̞̓̓ part 5 - The Wokwi Bridge App
10/04/2023 at 17:07 • 0 commentsTable of Contents (bolded ones are in this project log)
- General terms - the names I've decided to call things
- What's being stored - how the overall state is stored
- Pathfinding - how we find valid paths for each connection
- Controlling the switches - how we send that data to the CH446Qs
- LEDs - how we choose unique colors for each net
- The Wokwi bridge app - how we scrape the Wokwi page for updates
So, I didn't know Python, but it sounded like the easiest way to scrape a webpage with Beautiful Soup. Coming from C, it really makes you appreciate the hardline greybeards that have strong opinions about what the language should and shouldn't be. Because Python is a mess. But I learned it anyway.What the app actually does
Most of the parsing and stuff is done on the Jumperless itself. Mainly because I wanted it to be relatively app-agnostic, and because I'm way more comfortable with C++. So really all the app is doing is:
- Using BeautifulSoup to scrape the webpage source of the Wokwi project every quarter of a second
- Pulling out the diagram.json section of the page
- Seeing if it's any different from the last diagram.json it pulled
- If so, parses that down into a list of connections
- Sends an 'f' over serial to put the Jumperless in "load preFormatted node list" mode
- Then sends those parsed connections over serial
Here's what the diagram.json looks like
We don't actually care about most of the stuff in that json file, so we just parse out the things that are important. The parsed connections look like this
11-105, 13-105, 13-46, 15-44, 43-100, 100-2, 100-2, 45-25, 38-44, 32-44, 22-24, 20-19, 21-23,
The numbers in the 100 range are special functions, 100 = GND, 103 = 3.3V, 105 = 5V, etc.
There's actually the same parser on the Jumperless itself, which I wrote first and then ported to Python. So you could just paste the whole diagram.json into the serial terminal and the Jumperless will figure it out. The main reason I'm doing parsing on the computer is that the serial buffers can fill up and it makes the updates feel less "snappy".
Other things the app does
When you open the app, it uses pySerial to look at all the things connected to USB. If one of them has Jumperless as the device descriptor, it automatically connects to that one and then moves along to prompting you for the Wokwi project link.
It became annoying to paste the link in every time so now it stores links as a text file and lets you just select from a list if you're opening a project you've used before
The bridge app also has its own menu accessed by typing "menu", which is useful if you want to switch projects without closing and reopening the app.
I guess it's kinda confusing, because the menu on top is coming directly from the Jumperless, while the bridge app menu is coming from the app itself. Hopefully the indenting and spacing hints at that fact.
Most of the code in the bridge app is just fighting with pySerial to keep the connection open and not crash when it gets unplugged. Especially when developing on this, I needed it to not crash when I flash new code onto the Jumperless from VScode/PlatformIO. So yeah, that works and it's very convenient.
How I got it to run in a persistent terminal window is a bit of a hack. When you open the app, it actually just opens a bash script that launches the real app
And here are the entire contents of jumperlesswokibridge
#!/bin/bash # This is the launcher for OSX, this way the app will be opened # when you double click it from the apps folder open -n /Applications/jumperlesswokwibridge.app/Contents/MacOS/jumperlesswokwibridge_cli
jumperlesswokwibridge_cli is where the real app is. The reason I had to do this is typically, when you open an app on a Mac, it just runs a script like this and then immediately closes that window. This is just to get around the closing part.
Packaging
This was kind of a nightmare, at first I was using pyInstaller but it wasn't signing the app it created correctly so macOS would throw a fit. Then I found Platypus and it's amazing. Hella easy to use and it just works.
I don't use windows much but hopefully whatever I did to package that works too. If you have issues, let me know so I can fix them.
If none of these packaged apps work for you, you can just run the Python code in Thonny. There's a requirements.txt file too so you can get all the dependencies (you might have to remove the library called serial (JSON serialization library), because it uses the same name as pySerial (Serial terminal library), friggin' Python) It seems to have problems IDLE for reasons I don't care to find out.
How does Uri from Wokwi feel about me scraping his site every 0.25 seconds?
I think it's rude to scrape small open source projects like that without asking. So I asked and he says it's chill. If Jumperless gets really popular, we'll have to figure something out if I'm putting some serious load on his hosting. But Wokwi is pretty popular and he's assured me it won't be a burden at this scale.
If you want to look at the code for the app yourself, it's here on GitHub
-
"Any sufficiently extra technology is indistinguishable from a Barbie-Oppenheimer crossover meme" - Glittering USB Cables
09/29/2023 at 20:26 • 0 commentsSo Jumperless has a pretty specific color scheme, and it would be a shame to ship them with boring USB cables.
I figured I had to include a cable because USB Mini is pretty uncommon nowadays, even though it's the best USB port ever made, and I refuse to change my mind on that.
Why not USB-C?
I like USB-C, but sending regular USB 2.0 over a USB-C port feels like lying to me. It just comes off as janky if you're not using any of the extra functionality.
And also, I'm scared. If the Raspberry Pi Foundation screwed it up, I'm worried I might too. It's just unnecessary complexity that isn't really needed here.
Why am I wasting time doing this?
Because I came up with the idea while I was drinking, and decided that I should announce it publicly right then so I couldn't back out.
"Any sufficiently extra technology is indistinguishable from a Barbie-Oppenheimer crossover meme"
I'm drunk and therefore decided that every Jumperless will come with a pink glittery USB Mini cable, and now I'm committing to it publicly so I can't change my mind. pic.twitter.com/CrqglTC6Si
— Kevin Santo Cappuccio (@arabidsquid) July 8, 2023Anyway, I'm glad I did. I think it adds to the experience of opening a Jumperless. Every one I box up, I do it with the mentality of "I can't wait to see the look on their face when they open this, they're gonna get so many retail-based endorphins shot right into their friggin' brain." And I hope they throw this box in the closet with all the Apple boxes everyone refuses to throw away.
If you want to do this yourself
I tried a bunch of way more complicated ways to do these in batches like this
and this
But as it turns out, it's much easier to just do these one at a time.
First I mask off the metal ends of the cable with masking tape, then stretch them for a few days so they're less "kinky" with rubber bands.
This is the paint I'm using. It's the kind that bonds to plastic.
Then I just hang them up and spray them, and while they're still wet, I dip them though my glitter pile. If you don't have a huge pile of glitter in your workshop at all times, what are you even doing with your life?
Then I let them dry for like an hour, so the paint isn't fully cured. And go along and squish the glitter into the cable with my fingers really hard so the glitter lays flat and isn't so rough. Then rub all the extra glitter off with my fingers for a while. This gives them a nice smooth finish that kinda feels like a snake, instead of sandpaper.
Here's a long video of that whole process.
Woo, gittery USB cables. Now here's what all the stuff inside a Jumperless box looks like:
-
Doom and Some Other Less-Trivial Demos
09/20/2023 at 20:25 • 0 commentsA lot of these shots will be cut into the Hackaday Prize entry video, but I'm gonna post them here anyway so I don't feel like I've wasted time making these if I decide some of them don't fit.
Doom Over a Jumperless
Doom is the most click-baity demo you can do. And I support it (even though I suck at actually playing Doom)
Someone on Twitter asked if the crosspoint switches would interfere with fast data signals. The answer is no, they won't, they're rated to 50MHz at 3dB rolloff. The physical breadboard will have a much larger effect at those frequencies, so if it works on a regular breadboard, it should work on a Jumperless (wrt frequency, current and voltage have their own limits).
This is the awesome project doom-nano which is stripped down enough to run on an ATMEGA328P at ~15 fps. Note that the enemies don't die when I shoot them, that hasn't been implemented yet.
The nerd-sniper who got me actually was asking about SPI displays, I didn't have a Arduino Nano ESP32 at the time, but I will today (thanks Amazon). So expect a demo of real doom running on a color display soon.
16x2 LCD
And my video light Fuck mask gets its time in the spotlight.
Making this one revealed a bug in the routing code here it wasn't checking the other end of the "bounces" in certain arrangements. So yeah, 2 days of debugging for a 22 second video, that's approaching Kubrick's shooting-to-runtime ratio.
Some 7400-Series Logic Stuff
Here I threw together a random logic circuit with the chips I had laying around, and used a 555 as the clock source. Near the end, I take out the 555 and use the Jumperless's DAC making a square wave as the clock. Which might be useful if you're building just part of something and you don't want to screw around with the clock source.
There's a simple text-based wave generator menu that I'm using to control the frequency and stuff. It looks like this.
On the Wokwi project I had a potentiometer connected to the blue clock line, that's what tells the Jumperless that I want a DAC connected there.
This is the same circuit again:
Magic Flames Released
This is the silly intro shot, same circuit as above (and part of it is redundant).
Obviously, circuits don't blow up like that when you cut one wire. So let's look behind the curtain and see what I didIt's cigarette filter wrapped in NiChrome wire and submersed in a foil tray filled with vape juice hidden behind the breadboard then hooked up to a big 12V lead-acid battery when I wanted it to go off. I'm actually just cutting the 5V power in the video.
This is what it looked like afterwards
Ha.
I also tried just cutting 2 jumpers connected to the leads of the lead-acid battery, but the sparks weren't dramatic enough and too quick to be shot on video.
Another attempt was made by emptying out the powder from a bullet and igniting it, but modern smokeless powder is, well, smokeless. Like really surprisingly so. I even mixed it with sugar and Ammonium Dichromate but it never really looked right. So vape juice it was!
Crosspoint Superzoom
This is just to give people a but of an understanding of what's going on inside a crosspoint switch. Not really a demo, but it was made so people understand that the Jumpeless isn't reading and simulating your signals, just passing them through an analog CMOS switch.
If you want to play with that crosspoint demo in Falstad, here's the link to that circuit.
And the transistor-level analog CMOS switch is here.
Rail Selector Switch
This is a fun little shot of the supply rails being switched in stop motion.
Anyway, I think that's all the video I have for now. But stay tuned for the Hackaday Prize entry video where I cut all this together and talk at you.
-
Getting Started Using Your Jumperless
09/06/2023 at 17:26 • 0 commentsCool, so you have this super sexy object now. How do turn it into an actual prototyping tool? The answer is software.
After you've unboxed (and maybe assembled) your Jumperless, we're going to need a way to talk to it.
Now, the Jumperless is very open and will accept netlists from any program with some basic formatting, if you hate Wokwi and want to use something else, this project log should give you an idea of what format it accepts over serial so you can write your own Bridge to any software you like. But for now, I'm focusing on Wokwi because I think it's better to have solid support for one workflow than shitty support for a bunch.
First, we need to download the App and latest firmware here.
Updating The Firmware
I ship these with the latest firmware installed, so if you just received your Jumperless in the mail, you can skip the firmware update. But I usually push minor fixes to that firmware.uf2 file every few days, so it's probably worth doing anyway.
On your Jumperless, there will be a little button on the opposite side of the USB port. This is the USB Boot button and works exactly the same as a Raspberry Pi Pico.
1. Hold the USB Boot button and plug the Jumperless into your computer.
2. A drive should pop up called RPI-RP2. Drag the firmware.uf2 file into it and it should reset and you're done!
Installing the App
I'm showing this on a fresh install of macOS, but Windows should be roughly the same.
1. On the releases page, download JumperlessWokwiBridgeMacOS.zip (or the .exe for windows) and firmware.uf2
2. It should automatically unzip in your downloads folder. Note that this won't run correctly from anywhere but your Applications folder, so drag it there.
3. Click it to try to run it. Unless you've set your security setting to allow apps from anywhere, it's going to complain.
4. Click Cancel and go to Settings > Privacy and Security > scroll down and click Open Anyway
5. It's going to complain again and just click Open
Hot tip: run
sudo spctl --master-disable
in Terminal to give you another checkbox for Anywhere under Settings > Privacy and Security > Allow Apps From. And you won't have to go through this again for other shoddy software.
6. If everything goes well, you should now be presented with a Terminal window like this
Note that on MacOS, it should just autodetect and connect to the Jumperless if it's plugged in when you start the app, if not, just plug it in and type 'r' to rescan the ports until it finds it.
Okay, now the app is set up, what do we paste into there to connect to our Wokwi project?
If you don't want to use Wokwi, this is a rundown of the alternative ways to control your Jumperless.
Getting Wokwi Set Up
You'll need a stable link to your Wokwi project, and it will only make one when you're signed in and have saved your project to "My Projects". After that the links are saved in a text file in the JumperlessWokwiBridge app and you'll be able to select projects you've used before from a list.
- Go to wokwi.com
- Click Sign Up/Sign In and enter your email (or use Google or Github)
- Click the link they email you to sign in
- Click Go To Your Projects, then New Project > Arduino Nano
- Click Save and give your project a name
(note that the URL has now changed to a unique link) - Click blue “+” (top center) to Add New Part and scroll down to Half Breadboard and click it
It will put it somewhere random, so click somewhere that’s not a hole to drag it. - Open the JumperlessWokwiBridge App and resize the windows so you can see both
(on mac it will autodetect the port if it’s plugged in, on windows you’ll need to select the COM port here first, if the Jumperless wasn’t connected when you opened the app, press ‘r’ the Enter to rescan the ports) - Copy the URL of the Wokwi project you just made
- Paste that URL into the Bridge App window and press Enter
- Name the project and hit Enter (it will save the link so next time you’ll only need to choose it from a list by entering the number)
- Now draw some wires on Wokwi by clicking a hole on the breadboard and dragging the wire to somewhere else
- As soon as you press Save, the changes you made to the Wokwi project should show up on the Jumperless and you should be ready to start prototyping like a future person
Extra Tips for using Jumperless like a Pro
Here are some non-steps but more general tips:
Real Programmers
If you don't want to use Wokwi and are a super l33t hacker, you can also just type (or use some script) the netlists in by hand. Just type 'f' and it will wait for a set of bridges connected with a dash and separated with a comma, whitespace is ignored.
For example,
Will will do this. (Note that it's not appended, it just clears and overwrites the last netlist)
Another exampleThis is a list of what those numbers above 60 define into:
Debug Flags
- If your Bridge app’s terminal is full of a bunch of debugging stuff, you should probably turn the debug flags off until you want to see a particular thing. Sending that much data over serial can occasionally cause the serial drivers to trip over themselves. Here’s how to turn those off:
- From the main Menu (which it defaults to unless you’re in a sub menu,) type ‘d’ then Enter to go to the “toggle debug flags” menu.
- Type ‘0’ then Enter to turn them all off. Then ‘m’ and Enter to return to the main Menu
App Menu
- If you want to do things that pertain to the Jumperless Wokwi Bridge App itself, type “menu” and then Enter (Note: after the part where you enter the Wokwi project link and choose a port, all this output you’re seeing is from the Jumperless’s onboard RP2040, and the Bridge app is just acting like the Serial Monitor from Arduino)
Using the DACs and ADCs
Here's a handy guide for the Logic Analyzer part and what it maps to:
- To use the DACs, you’ll need to add 2 potentiometers to your Wokwi project and connect the Signal (center) pin to where you want it connected on the board. DAC 0 0-5V will be pot1 and DAC 1 ±8V will be pot2.
- Blue “+” to Add New Part > scroll down to Potentiometer (the linear Potentiometer will work too if you prefer) and click it
- If you want the brightness and hue to track the voltage for DAC 1 ±8V, you’ll need to connect ADC3 (±8V scaled) to the same pin as DAC 1. ADC 0-3 are mapped to a Logic Analyzer pins 0-3 in Wokwi, I haven’t come up with a good way display the readings and I’m open to suggestions for how to show them without making the serial output a total mess. But in the code, AnalogRead(26,27,28, or 29) will work as expected, scaled to 0-5V for ADC 0-2 and ±8V for ADC 3. Anyway, here’s how to connect them:
Add New Part > Logic Analyzer (8 channels)
Connect D3 (ADC 3) to the same breadboard row as pot2:Sig (DAC 1) and the brightness will track the output. (Note: the pins are numbered the same as the ADCs, so D0 = ADC0 (pin 26), D1 = ADC1 (pin 27), etc.) - To control the DACs, enter Wavegen from the main Menu by typing ‘w’
- Enter 5 or 8 twice to turn on the 0-5V or the ±8V DAC. The first time you enter it, it won’t turn it on so you can change the settings before you start it to avoid frying things.
- When you type ‘a’ or ‘o’ to change the Amplitude or Offset respectively, it expects numbers in the format ‘3.0’. It will wait for you to enter a decimal and then a number.
I’ll add more tricks as I think of them. If you have any questions about how to use any particular thing, or suggestions for things to work on supporting, let me know.
You can email me at KevinC@ppucc.io or message me through any channel you prefer.
-
Ṭ̴̯̿̂h̶̫̏̀ę̵̙̒ ̷̩̉C̴̖̞̀͝ọ̵̬̎̔ḓ̵̓e̸̥̞̓̓ part 4 - Picking Colors and Putting Them On Your Retinas
09/04/2023 at 18:00 • 1 commentTable of Contents (bolded ones are in this project log)
- General terms - the names I've decided to call things
- What's being stored - how the overall state is stored
- Pathfinding - how we find valid paths for each connection
- Controlling the switches - how we send that data to the CH446Qs
- LEDs - how we choose unique colors for each net
- The Wokwi bridge app - how we scrape the Wokwi page for updates
If you're wondering why you feel a strange sexual attraction to this board, I can give you a hint...
I didn't want to do the LEDs at first because it seemed a bit out-of-scope. But after using the prototype board without them, I found that it involved a lot of counting rows to find where you're connecting things. And that gets pretty tedious.
Around the same time, I had ended my month-long search for anyone who sells just the metal spring clips from a breadboard and resigned to having custom spring clips made by Shenzhen Fulimei Technology Co. LTD.
Hot tip for makers that want a bunch of some thing custom made using a non-so-common process: put your drawings and requirements up on made-in-china.com, you'll get a ton of quotes within days and just pick your favorite.
I'm glad I did, because using this thing is sooo much smoother now, you really only have to look at the board for the first connection and then everything else can just be counted as an offset from there. Also, the new versions of the breadboard shell have every 5th embossed number raised (which is actually super weird on breadboards because they count from 1, so there's 3 holes between 1 and 5, and 4 holes between the rest, enjoy being bothered by that forever) which makes it even easier to locate where you're connecting stuff.
How the colors are chosen
It would be really cool if the colors would correspond to the colors in the Wokwi diagram, but there's a couple of problems with that.
First:
All the wires default to green, and having to select a unique color for each wire is kind of a pain in the current version of Wokwi. I know they'll eventually work on something to change this, but their voting system of which features they should work on next shows it's a fairly low priority.
The other issue is this:
Currently, they only support 15 HTML colors. So having only 15 different possible nets with unique colors (fewer, because black, grey, and brown wouldn't really show up on RGBs)
So we're just gonna pick our own colors.
The special nets have hard-coded colors, they are as follows:
rgbColor specialNetColors[8] = { {00, 00, 00}, {0x00, 0xFF, 0x30}, //GND - Green {0xFF, 0x41, 0x14}, //5V - Red leaning towards orange {0xFF, 0x10, 0x40}, //3.3v - Red leaning towards purple {0xeF, 0x78, 0x7a}, //DAC 0-5V- Pinkish White (changes brightness and hue based on output magnitude) {0xeF, 0x40, 0x7f}, //DAC +-8V- Purplish White (changes brightness based on output magnitude, hue for positive or negative) {0xFF, 0xff, 0xff}, //Current Sense+ - Yellow {0xff, 0xFF, 0xff}}; //Current Sense- - Blue rgbColor railColors[4] = { {0xFF, 0x32, 0x30}, //GND - Green {0x00, 0xFF, 0x30}, //5V - Red {0xFF, 0x32, 0x30}, //GND - Green {0x00, 0xFF, 0x30}}; //5V - Red
Here's what those look like:
Note the bottom rail is 3.3V, top rail is 5V. pot1-sig is DAC 0, pot2-sig is DAC1.
The colors are saved at full brightness and scaled down to the default brightness or the brightness you set in the menu when they're displayed.
For the other nets, here's the general algorithm:
- Get the number of unique nets and divide that by 255 (that's the full color wheel in the NeoPixel Library) to get a color distance
- Check if the hue at (color distance * 1) is the too close to any of the special nets
- If it is, add 1 to the number of unique nets and calculate a new color distance, then try again with an offset (color distance/2)
- If not, set that hue for the net and check the next net at (color distance * 2) etc...
Here's how that plays out:
Correcting for FR4
For the colors to be useful as an indicator, you should be able to recognize a color is the same whether you're directly looking a the LED on the breadboard or it's passing through the yellowish PCB around the Nano Header.
This turned out to be surprisingly complicated, I basically spent 2 days just tweaking the values until it looked right. Why it was so complex is that the yellowish filtering only affects some colors and basically doesn't do anything to others (except making them slightly dimmer).
The shifts are defined as constants in LEDs.h
#define PCBEXTINCTION 30 //extra brightness for to offset the extinction through pcb #define PCBREDSHIFTPINK -18 //extra hue shift to offset the hue shift through pcb #define PCBGREENSHIFTPINK -25 #define PCBBLUESHIFTPINK 35 #define PCBREDSHIFTBLUE -25 //extra hue shift to offset the hue shift through pcb #define PCBGREENSHIFTBLUE -25 #define PCBBLUESHIFTBLUE 42
Here's that whole function if you want to try figuring it out
The shifting is different depending on which side of the spectrum you're on, hence the shifts for the blue and pink separately. I was going to do like a continuous blending between the two, but it turns out it looks better with just a cutoff at a certain hue.
struct rgbColor pcbColorCorrect(rgbColor colorToShift) { uint8_t redShift = 0; uint8_t greenShift = 0; uint8_t blueShift = 0; int testNeg = 0; struct rgbColor colorToShiftRgb = colorToShift; struct hsvColor colorToShiftHsv = RgbToHsv(colorToShiftRgb); // colorToShiftHsv.v += PCBEXTINCTION; if (colorToShiftHsv.h > 100 && colorToShiftHsv.h < 150) { if (PCBREDSHIFTBLUE < 0) { testNeg = colorToShiftRgb.r; testNeg -= abs(PCBREDSHIFTBLUE); if (testNeg < 0) { colorToShiftRgb.r = 0; } else { colorToShiftRgb.r = colorToShiftRgb.r - abs(PCBREDSHIFTBLUE); } } else { colorToShiftRgb.r = colorToShiftRgb.r + abs(PCBREDSHIFTBLUE); if (colorToShiftRgb.r > 254) { colorToShiftRgb.r = 254; } } if (PCBGREENSHIFTBLUE < 0) { testNeg = colorToShiftRgb.g; testNeg -= abs(PCBGREENSHIFTBLUE); if (testNeg < 0) { colorToShiftRgb.g = 0; } else { colorToShiftRgb.g = colorToShiftRgb.g - abs(PCBGREENSHIFTBLUE); } } else { colorToShiftRgb.g = colorToShiftRgb.g + abs(PCBGREENSHIFTBLUE); if (colorToShiftRgb.g > 254) { colorToShiftRgb.g = 254; } } if (PCBBLUESHIFTBLUE < 0) { testNeg = colorToShiftRgb.b; testNeg -= abs(PCBBLUESHIFTBLUE); if (testNeg < 0) { colorToShiftRgb.b = 0; } else { colorToShiftRgb.b = colorToShiftRgb.b - abs(PCBBLUESHIFTBLUE); } } else { colorToShiftRgb.b = colorToShiftRgb.b + abs(PCBBLUESHIFTBLUE); if (colorToShiftRgb.b > 254) { colorToShiftRgb.b = 254; } } } else if (colorToShiftHsv.h >= 150 && colorToShiftHsv.h < 255) { if (PCBREDSHIFTPINK < 0) { testNeg = colorToShiftRgb.r; testNeg -= abs(PCBREDSHIFTPINK); if (testNeg < 0) { colorToShiftRgb.r = 0; } else { colorToShiftRgb.r = colorToShiftRgb.r - abs(PCBREDSHIFTPINK); } } else { colorToShiftRgb.r = colorToShiftRgb.r + abs(PCBREDSHIFTPINK); if (colorToShiftRgb.r > 254) { colorToShiftRgb.r = 254; } } if (PCBGREENSHIFTPINK < 0) { testNeg = colorToShiftRgb.g; testNeg -= abs(PCBGREENSHIFTPINK); if (testNeg < 0) { colorToShiftRgb.g = 0; } else { colorToShiftRgb.g = colorToShiftRgb.g - abs(PCBGREENSHIFTPINK); } } else { colorToShiftRgb.g = colorToShiftRgb.g + abs(PCBGREENSHIFTPINK); if (colorToShiftRgb.g > 254) { colorToShiftRgb.g = 254; } } if (PCBBLUESHIFTPINK < 0) { testNeg = colorToShiftRgb.b; testNeg -= abs(PCBBLUESHIFTPINK); if (testNeg < 0) { colorToShiftRgb.b = 0; } else { colorToShiftRgb.b = colorToShiftRgb.b - abs(PCBBLUESHIFTPINK); } } else { colorToShiftRgb.b = colorToShiftRgb.b + abs(PCBBLUESHIFTPINK); if (colorToShiftRgb.b > 254) { colorToShiftRgb.b = 254; } } } return colorToShiftRgb; }
I do a lot of swapping between RGB and HSV in the code, if I was working with a slower microcontroller, I might have been more strict about it, but on the RP2040 it handles it just fine.
How it sends the data to the LEDs
Jumperless just uses the standard Adafruit_NeoPixel library, one thing to note about it is that it's blocking. So when you're sending data to LEDs, it stops your other code.
I didn't even notice this until I was working on code to have the DACs change hues depending on the measured output. Like this:
Note that DAC 1 (right) is an actual measurement from ADC 3, If you look closely you can see a bit of "wobbliness," that's from taking real measurements of the output. For now DAC 0 (left) is just showing what it should be set to, but it's super easy to change that in the code.
So, to get around the blocking nonsense, I run the LEDs from core 2, just like the CH446Qs. So if anything on core 1 wants to update the LEDs, it just sets showLEDsCore2 = 1 and the second core will see that and do its thing.
void loop1() // core 2 handles the LEDs and the CH446Q8 { if (showLEDsCore2 >= 1) { int rails = showLEDsCore2; //showNets(); if (rails == 1) { lightUpRail(); } delayMicroseconds(5200); leds.show(); delayMicroseconds(9200); showLEDsCore2 = 0; } if (sendAllPathsCore2 == 1) { delayMicroseconds(9200); sendAllPaths(); delayMicroseconds(2200); showNets(); delayMicroseconds(9200); sendAllPathsCore2 = 0; } if (logoFlash == 2) { logoFlashTimer = millis(); logoFlash = 1; } if (logoFlash == 1 && logoFlashTimer != 0 && millis() - logoFlashTimer > 600) { logoFlash = 0; logoFlashTimer = 0; //lightUpRail(); leds.setPixelColor(110, 0x550008); leds.show(); } }
This allows the LEDs to update quickly and not cause weird glitches in the DAC outputs.
That's all I got about the LEDs, if you want me to be clearer about something, let me know and I'll add to this log.
Next part - The Jumperless Wokwi Bridge App or: How I Learned Python and Went From Wishing I Knew Python to Wishing I Didn't
-
Ṭ̴̯̿̂h̶̫̏̀ę̵̙̒ ̷̩̉C̴̖̞̀͝ọ̵̬̎̔ḓ̵̓e̸̥̞̓̓ part 3 - Driving the CH446Qs
08/30/2023 at 03:14 • 0 commentsTable of Contents (bolded ones are in this project log)
- General terms - the names I've decided to call things
- What's being stored - how the overall state is stored
- File Parsing - how we fill in those arrays
- Pathfinding - how we find valid paths for each connection
- Controlling the crosspoint switches - how we send that data to the CH446Qs
- LEDs - how we choose unique colors for each net
- The Wokwi bridge app - how we scrape the Wokwi page for updates
Controlling the Crosspoint SwitchesWhat crosspoint switches crave
Okay, so now we have all our paths filled out with what chips need to have which X and Y inputs connected to make the magic happen.
The CH446Qs are basically clones of the MT8816, except for one important difference, they accept serial addressing. The datasheet is kinda vague about how, but it turns out it's just a kinda weird version of SPI.
Basically, all the chips see the same data signal, and whichever one sees a pulse on the STB when the last bit comes in will connect or disconnect the selected X and Y inputs. The state of the DAT line when the Strobe comes in determines whether it's connecting or disconnecting. That stretched out clock line shows that it doesn't care about real time, which comes in handy.
PIO State Machine
So I have to do something that's kinda like SPI but not quite, this looks like a job for the RP2040 PIO State Machines.
Even knowing assembly, the learning curve for writing a PIO program is steep. The documentation is really hit-or-miss, the examples are uncommented and written like they're playing code golf, insanely terse. Like these people do realize you can name variables after what they do, right? And these are the Official examples in the datasheet. Anyway after a few days of staring at what looks like gibberish, it starts to click.
I copied the SPI.pio example and edited it from there. Let me try to explain some of the things I learned to hopefully make it easier for you to write a PIO program in the future.
I'm just compiling this with the online pioasm compiler and then pasting the compiled code into spi.pio.h
https://wokwi.com/tools/pioasm
Here's where we are:
;this is basically spi but it sets a system IRQ on the last bit to allow the chip select pulse to happen .program spi_ch446_multi_cs .side_set 1 .wrap_target bitloop: out pins, 1 side 0x0 [2] nop side 0x1 [2] jmp x-- bitloop side 0x1 out pins, 1 side 0x1 mov x, y side 0x1 irq 0 side 0x1 wait 0 irq 0 rel side 0x1 jmp !osre bitloop side 0x0 public entry_point: ; Must set X,Y to n-2 before starting! pull ifempty side 0x0 [1] ; Block with CSn high (minimum 2 cycles) nop side 0x0 [1]; CSn front porch .wrap
What wasn't explained well is what the hell a sideset pin is. Basically you do your normal-ish assembly code on the left, and then each operation also affects the sideset pin on the right. It's kind of a hack to allow you to control 2 pins in a single clock cycle. In this case, the sideset pin is attached to the CLK, and pins, 1 is DAT.
So, whats going on is that in the regular code, I'm sending a byte to the sm register with this line
pio_sm_put(pio, sm, chAddress);
(the last bit of chAddress is set to 1 or 0 depending if I want to connect or disconnect)
and that pull ifempty will pull in a byte to the working register and send it out one bit at a time while toggling the clock. When it's out of data to send, it triggers a system interrupt request that can be seen outside of the PIO state machine and I deal with it in an ISR in CH446Q.cpp
At this point, here's where we are in the timing diagram:
Now we need to select the correct CS line to make the right chip make the connection
void isrFromPio(void) { switch (chipSelect) { case CHIP_A: { digitalWriteFast(CS_A, HIGH); break; } case CHIP_B: { digitalWriteFast(CS_B, HIGH); break; } case CHIP_C: { digitalWriteFast(CS_C, HIGH); break; } case CHIP_D: { digitalWriteFast(CS_D, HIGH); break; } case CHIP_E: { digitalWriteFast(CS_E, HIGH); break; } case CHIP_F: { digitalWriteFast(CS_F, HIGH); break; } case CHIP_G: { digitalWriteFast(CS_G, HIGH); break; } case CHIP_H: { digitalWriteFast(CS_H, HIGH); break; } case CHIP_I: { digitalWriteFast(CS_I, HIGH); break; } case CHIP_J: { digitalWriteFast(CS_J, HIGH); break; } case CHIP_K: { digitalWriteFast(CS_K, HIGH); break; } case CHIP_L: { digitalWriteFast(CS_L, HIGH); break; } }
delayMicroseconds(1);
digitalWriteFast(CS_A, LOW); digitalWriteFast(CS_B, LOW); digitalWriteFast(CS_C, LOW); digitalWriteFast(CS_D, LOW); digitalWriteFast(CS_E, LOW); digitalWriteFast(CS_F, LOW); digitalWriteFast(CS_G, LOW); digitalWriteFast(CS_H, LOW); digitalWriteFast(CS_I, LOW); digitalWriteFast(CS_J, LOW); digitalWriteFast(CS_K, LOW); digitalWriteFast(CS_L, LOW); irq_flags = pio0_hw->irq; pio_interrupt_clear(pio, PIO0_IRQ_0); hw_clear_bits(&pio0_hw->irq, irq_flags);//clears the IRQ }
The reason I had to do it in an external interrupt instead of in the PIO code is because there's a limit to how many pins can be attached to a single state machine, 8. And this is just way easier to do.
The C
This all runs on the second core just so it can stay somewhat timing sensitive while not worrying about what's going on elsewhere. How this process is triggered is that when the pathfinding algorithm is finished running in core 0, it sets
volatile int sendAllPathsCore2 = 1; // this signals the core 2 to send all the paths to the CH446Q
Then in loop1, is just constantly checks if that's a 1 and will send the paths and set it back to 0. Just a reminder that the cores on an RP2040 share global variables, because it's very useful.
Here's what the SendAllPaths() functions look like.
void sendAllPaths(void) // should we sort them by chip? for now, no { for (int i = 0; i < numberOfPaths; i++) { sendPath(i, 1); } } void sendPath(int i, int setOrClear) { uint32_t chAddress = 0; int chipToConnect = 0; int chYdata = 0; int chXdata = 0; for (int chip = 0; chip < 4; chip++) { if (path[i].chip[chip] != -1) { chipSelect = path[i].chip[chip]; chipToConnect = path[i].chip[chip]; chYdata = path[i].y[chip]; chXdata = path[i].x[chip]; chYdata = chYdata << 5; chYdata = chYdata & 0b11100000; chXdata = chXdata << 1; chXdata = chXdata & 0b00011110; chAddress = chYdata | chXdata; if (setOrClear == 1) { chAddress = chAddress | 0b00000001; // this last bit determines whether we set or unset the path } chAddress = chAddress << 24; // delayMicroseconds(50); delayMicroseconds(30); pio_sm_put(pio, sm, chAddress); delayMicroseconds(40); //} } } }
The whole process to connect the whole board takes a couple milliseconds at most. But you can shave down these delays if you have some reason to go faster.
To be continued in part 4 - LEDs
-
Ṭ̴̯̿̂h̶̫̏̀ę̵̙̒ ̷̩̉C̴̖̞̀͝ọ̵̬̎̔ḓ̵̓e̸̥̞̓̓ part 2 - Pathfinding
08/25/2023 at 01:37 • 0 commentsTable of Contents (bolded ones are in this project log)
- General terms - the names I've decided to call things
- What's being stored - how the overall state is stored
- File Parsing - how we fill in those arrays
- Pathfinding - how we find valid paths for each connection
- Controlling the crosspoint switches - how we send that data to the CH446Qs
- LEDs - how we choose unique colors for each net
- The Wokwi bridge app - how we scrape the Wokwi page for updates
Pathfinding
This is the really tricky part. I probably wrote all this code about 4 times, trashed it and started over from scratch with only the lessons learned from the last attempt. Earlier versions would add connections one at a time, but you'd end up in weird states because it has no knowledge of what other paths it needs to make room for. So the final version here clears the connections from the last update, takes in all the connections to be made, and finds paths for the whole board every time you add a new wire. All the old connections usually follow the same paths as last time unless they need to be nudged over to make room for some other path, and the actual connection won't be interrupted at all.
Here's the schematic of just the crosspoints, the Nano header, and the breadboard.
If you look closely, you'll see that there generally 2 connections (I'm calling these 2 connections Lanes between each breadboard chip, except for the chip that is across from it on the breadboard. And every breadboard chip has one connection to each of the special function chips. The pins on the Nano header has a connection to 2 special function chips (interleaved to make it easier to connect continuous pairs of pins to the breadboard).
Here's the high level outline of what NetsToChipConnections.cpp is doing
- Sorts all the paths by Net This sort of sets priority, lower net numbers (so all the special function nets) will be picked first and are more likely to have direct connections chosen
- For each path, find the start and end chips
- If there are multiple candidates (Nano header pins will have 2) store both
- If both start and end chips have candidates in common, choose that chip (this would make it a bounce)
- Assign node and path types (BB to BB, NANO to BB, BB to SF, etc...)
- Sort a list of chips from least to most crowded (by how many connections that chip has used)
- Resolve the candidate chips by going down the sorted list of chips and picking the less crowded chip
- Search for a direct path between those 2 chips
- If there isn't one, swap to the other candidate chips and search again
- If one of the nodes is a special function with multiple options swap the nodes with their equivalents and search again
- If there still isn't a direct path, set the altPathNeeded flag and move on At this point, any paths that have a simple direct connection should be done, now we need to deal with the ones that don't
- Resolve alt paths, if the altPathNeeded flag is set
- Search through all the other chips until you find one that has a direct connection to both the start and end chips
- If one chip is connected to the X inputs and the other the Y inputs, set that connection on chip[2] and x[2] y[2]
- If they're both on X or both on Y, set the sameChip flag and the x[3] or y[3] as -2 to denote that that connection is a bounce and it doesn't matter which pin is chosen, as long as it's available
- Search through all the other chips until you find one that has a direct connection to both the start and end chips
- Resolve uncommitted hops, anything set as -2 gets a random unconnected pin assigned to it at the very end so it doesn't take up connection space
There's a lot more subtlety to this but if I go into any more detail you might as well just read the code itself. It will all be in the file NetsToChipConnections.cpp, and if you're running it on a Jumperless or just an RP2040, you can set the Chip Connections and Chip Connections Alt debug flags and it will show you everything it's doing in a somewhat nicely formatted way. There are comments in the code but there are a lot of nested array things that can get pretty confusing, if you need help understanding what's going on in a particular function, let me know and I'd be happy to walk you through it.
Here's the output with the debug flags on (with some redundant bits removed because it's really long)
sorting paths by net number of nets: 20 path[0] net: 1 path[1] net: 8 path[2] net: 9 path[3] net: 10 path[4] net: 11 path[5] net: 12 path[6] net: 13 path[7] net: 14 path[8] net: 15 path[9] net: 16 path[10] net: 17 path[11] net: 18 path[12] net: 19 number unique of nets: 26 pathIndex: 13 numberOfPaths: 13 0 [24,GND,Net 1], 1 [D4,14,Net 8], 2 [10,36,Net 9], 3 [D6,40,Net 10], 4 [D7,39,Net 11], 5 [D8,38,Net 12], 6 [D9,6,Net 13], 7 [D10,7,Net 14], 8 [D11,9,Net 15], 9 [D12,8,Net 16], 10 [D3,25,Net 17], 11 [D2,23,Net 18], 12 [D1,45,Net 19], finding start and end chips path[0] nodes [24-100] finding chips for nodes: 24-GND node: 1 chip: D node: 2 special function candidate chips: I J L Path 0 type: BB to SF Node 1: 24 Node 2: 100 Chip 1: 3 Chip 2: -1 path[1] nodes [74-14] finding chips for nodes: D4-14 node: 1 nano candidate chips: J K node: 2 chip: B Path 1 type: BB to NANO Node 1: 14 Node 2: 74 Chip 1: 1 Chip 2: -1 ... [removed for space] path[11] nodes [72-23] finding chips for nodes: D2-23 node: 1 nano candidate chips: J K node: 2 chip: D Path 11 type: BB to NANO Node 1: 23 Node 2: 72 Chip 1: 3 Chip 2: -1 path[12] nodes [71-45] finding chips for nodes: D1-45 node: 1 nano chip: I node: 2 chip: F Path 12 type: BB to NANO Node 1: 45 Node 2: 71 Chip 1: 5 Chip 2: 8 paths with candidates: 0,1,3,4,5,6,7,8,9,10,11, resolving candidates chips least to most crowded: C: 0 G: 0 H: 0 J: 0 K: 0 L: 0 I: 1 E: 2 A: 3 B: 3 D: 3 F: 3 sf connections: I1 sf connections: J0 sf connections: K0 sf connections: L0 C: 0 G: 0 H: 0 J: 0 K: 0 L: 0 I: 1 E: 2 A: 3 B: 3 D: 3 F: 3 path[0] chip from D to chip J chosen sf connections: I1 sf connections: J1 sf connections: K0 sf connections: L0 C: 0 G: 0 H: 0 K: 0 L: 0 I: 1 J: 1 E: 2 A: 3 B: 3 D: 3 F: 3 path[1] chip from B to chip K chosen ... [removed for space] sf connections: I4 sf connections: J4 sf connections: K3 sf connections: L0 C: 0 G: 0 H: 0 L: 0 E: 2 A: 3 B: 3 D: 3 F: 3 K: 3 I: 4 J: 4 path[11] chip from D to chip K chosen path[0] net: 1 24 to GND chip[0]: D x[0]: 7 y[0]: 2 chip[1]: J x[1]: 15 y[1]: 3 1 1 path[1] net: 8 14 to D4 chip[0]: B x[0]: 11 y[0]: 6 chip[1]: K x[1]: 6 y[1]: 1 8 8 path[2] net: 9 10 to 36 chip[0]: B x[0]: 8 y[0]: 2 chip[1]: E x[1]: 2 y[1]: 5 9 9 path[3] net: 10 40 to D6 chip[0]: F x[0]: 11 y[0]: 2 chip[1]: J x[1]: 6 y[1]: 5 10 10 path[4] net: 11 39 to D7 chip[0]: F x[0]: 10 y[0]: 1 chip[1]: I x[1]: 7 y[1]: 5 11 11 path[5] net: 12 38 to D8 chip[0]: E x[0]: 1 y[0]: 7 chip[1]: K x[1]: 10 y[1]: 4 12 12 path[6] net: 13 6 to D9 chip[0]: A x[0]: 0 y[0]: 5 chip[1]: I x[1]: 9 y[1]: 0 13 13 path[7] net: 14 7 to D10 chip[0]: A x[0]: 1 y[0]: 6 chip[1]: J x[1]: 9 y[1]: 0 14 14 path[8] net: 15 9 to D11 no direct path, setting altPathNeeded flag (BBtoSF) path[9] net: 16 8 to D12 no direct path, setting altPathNeeded flag (BBtoSF) path[10] net: 17 25 to D3 chip[0]: D x[0]: 6 y[0]: 3 chip[1]: I x[1]: 3 y[1]: 3 17 17 path[11] net: 18 23 to D2 chip[0]: D x[0]: 15 y[0]: 1 chip[1]: K x[1]: 4 y[1]: 3 18 18 path[12] net: 19 45 to D1 no direct path, setting altPathNeeded flag (BBtoSF) alt paths BBtoSF path: 8 bb: A sfChip: K xMapBB: 0 yMapSF: 0 xStatus: 13 bb: B sfChip: K xMapBB: -1 yMapSF: 1 xStatus: 65 bb: C sfChip: K xMapBB: 4 yMapSF: 2 xStatus: -1 bb: C sfChip: K xMapBB: 4 yMapSF: 2 xStatus: -1 xMapL0c0: 4 xMapL1c0: 2 xMapL1c1: 5 xMapL0c1: 3 8 chip[0]: B x[0]: 4 y[0]: 1 chip[1]: K x[1]: 13 y[1]: 2 15 -1 Found Path! BBtoSF path: 9 bb: A sfChip: J xMapBB: -1 yMapSF: 0 xStatus: 65 bb: B sfChip: J xMapBB: 2 yMapSF: 1 xStatus: -1 bb: B sfChip: J xMapBB: 2 yMapSF: 1 xStatus: -1 xMapL0c0: 2 xMapL1c0: 0 xMapL1c1: 3 xMapL0c1: 1 9 chip[0]: A x[0]: 2 y[0]: 7 chip[1]: J x[1]: 10 y[1]: 1 16 -1 Found Path! BBtoSF path: 12 bb: A sfChip: I xMapBB: 0 yMapSF: 0 xStatus: 13 bb: B sfChip: I xMapBB: 2 yMapSF: 1 xStatus: 16 bb: C sfChip: I xMapBB: 4 yMapSF: 2 xStatus: -1 bb: C sfChip: I xMapBB: 4 yMapSF: 2 xStatus: 15 xMapL0c0: 4 xMapL1c0: 10 xMapL1c1: 5 xMapL0c1: 11 12 chip[0]: F x[0]: 4 y[0]: 7 chip[1]: I x[1]: 1 y[1]: 2 19 -1 Found Path! resolving uncommitted hops path net node1 type chip0 x0 y0 node2 type chip1 x1 y1 altPath sameChp pathType chipL chip2 x2 y2 x3 y3 0 1 24 0 D 7 2 GND 2 J 15 3 0 0 BB to SF 0 1 8 14 0 B 11 6 D4 1 K 6 1 0 0 BB to NANO 0 2 9 10 0 B 8 2 36 0 E 2 5 0 0 BB to BB 0 3 10 40 0 F 11 2 D6 1 J 6 5 0 0 BB to NANO 0 4 11 39 0 F 10 1 D7 1 I 7 5 0 0 BB to NANO 0 5 12 38 0 E 1 7 D8 1 K 10 4 0 0 BB to NANO 0 6 13 6 0 A 0 5 D9 1 I 9 0 0 0 BB to NANO 0 7 14 7 0 A 1 6 D10 1 J 9 0 0 0 BB to NANO 0 8 15 9 0 B 4 1 D11 1 K 13 2 0 1 BB to NANO 0 C 2 -2 13 -1 9 16 8 0 A 2 7 D12 1 J 10 1 0 1 BB to NANO 0 B 0 -2 3 -1 10 17 25 0 D 6 3 D3 1 I 3 3 0 0 BB to NANO 0 11 18 23 0 D 15 1 D2 1 K 4 3 0 0 BB to NANO 0 12 19 45 0 F 4 7 D1 1 I 1 2 0 1 BB to NANO 0 C 10 -2 4 -1 path net node1 type chip0 x0 y0 node2 type chip1 x1 y1 altPath sameChp pathType chipL chip2 x2 y2 x3 y3 taken connections (-1 is free) chip 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 A 13 14 16 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 13 14 16 B 16 -1 16 -1 15 -1 -1 -1 9 -1 -1 8 -1 -1 -1 -1 -1 15 9 -1 -1 -1 8 -1 C -1 -1 15 -1 19 -1 -1 -1 -1 -1 19 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 D -1 -1 -1 -1 -1 -1 17 1 -1 -1 -1 -1 -1 -1 -1 18 -1 18 1 17 -1 -1 -1 -1 E -1 12 9 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 9 -1 12 F -1 -1 -1 -1 19 -1 -1 -1 -1 -1 11 10 -1 -1 -1 -1 -1 11 10 -1 -1 -1 -1 19 G -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 H -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 chip 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 I -1 19 -1 17 -1 -1 -1 11 15 13 -1 -1 -1 -1 -1 1 13 -1 19 17 -1 11 -1 -1 J -1 -1 18 -1 8 -1 10 -1 12 14 16 -1 -1 -1 -1 1 14 16 -1 1 -1 10 -1 -1 K -1 -1 -1 -1 18 17 8 -1 10 11 12 13 14 15 16 -1 -1 8 15 18 12 -1 -1 -1 L -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 1 -1 -1 -1 -1 -1 -1 -1 -1 8 same chips: @, @, @, @, @, @, C, C, 9 same chips: @, @, @, @, @, @, B, B, 12 same chips: @, @, @, @, @, @, C, C, final paths path net node1 type chip0 x0 y0 node2 type chip1 x1 y1 altPath sameChp pathType chipL chip2 x2 y2 x3 y3 0 1 24 0 D 7 2 GND 2 J 15 3 0 0 BB to SF 0 1 8 14 0 B 11 6 D4 1 K 6 1 0 0 BB to NANO 0 2 9 10 0 B 8 2 36 0 E 2 5 0 0 BB to BB 0 3 10 40 0 F 11 2 D6 1 J 6 5 0 0 BB to NANO 0 4 11 39 0 F 10 1 D7 1 I 7 5 0 0 BB to NANO 0 5 12 38 0 E 1 7 D8 1 K 10 4 0 0 BB to NANO 0 6 13 6 0 A 0 5 D9 1 I 9 0 0 0 BB to NANO 0 7 14 7 0 A 1 6 D10 1 J 9 0 0 0 BB to NANO 0 8 15 9 0 B 4 1 D11 1 K 13 2 0 1 BB to NANO 0 C 2 0 13 0 9 16 8 0 A 2 7 D12 1 J 10 1 0 1 BB to NANO 0 B 0 0 3 0 10 17 25 0 D 6 3 D3 1 I 3 3 0 0 BB to NANO 0 11 18 23 0 D 15 1 D2 1 K 4 3 0 0 BB to NANO 0 12 19 45 0 F 4 7 D1 1 I 1 2 0 1 BB to NANO 0 C 10 1 4 1 path net node1 type chip0 x0 y0 node2 type chip1 x1 y1 altPath sameChp pathType chipL chip2 x2 y2 x3 y3
To be continued with Driving the CH446Qs
-
Ṭ̴̯̿̂h̶̫̏̀ę̵̙̒ ̷̩̉C̴̖̞̀͝ọ̵̬̎̔ḓ̵̓e̸̥̞̓̓
08/18/2023 at 18:11 • 0 commentsI'm going to do a high-level explanation of what the code in the Jumperless is actually doing. There's a lot going on and it's in a bunch of separate files so I think it will be helpful for people who what to understand or improve upon it.
Table of Contents (bolded ones are in this project log)
- General terms - the names I've decided to call things
- What's being stored - how the overall state is stored
- File Parsing - how we fill in those arrays
- Pathfinding - how we find valid paths for each connection
- Controlling the crosspoint switches - how we send that data to the CH446Qs
- LEDs - how we choose unique colors for each net
- The Wokwi bridge app - how we scrape the Wokwi page for updates
This is going to be really long and so I'll be filling out parts as I finish them in no particular order.
General terms
I've made up terms for things here that may or may not be the formal definition, so I should probably let you know what I chose.
Breadboard Chips - This refers to the 8 CH446Q crosspoint switches (labeled A-H) that have their Y pins connected to the breadboard. This excludes Chip L which actually has it's X pins connected to the 4 corners of the board (rows 1, 30, 31 (b1), 60 (b30))
Special Function Chips - This refers to the 4 crosspoints (labeled I-L) that connect to everything else; the Nano header, power supplies, DACs, GPIO from the RP2040, etc...
Nodes - Also used synonymously with Row especially when it's on the breadboard or Nano Header. And end point to a bridge
Bridges - Just 2 nodes that should be connected
Nets - Groups of nodes and bridges that should all be electrically connected to each other
Bounce - Sometimes there won't be an available path directly from one node to another, so in this case it will pick another chip with a free path to the start and end chips and "bounce" through it.
Paths - Similar to a bridge, except that it contains data for how that bridge is connected. So it will have which chips and which X and X pins are needed to make the connection
Defines
You'll see a lot of these, they're all in JumperlessDefinesRP2040.h. But the point of this whole this is so you don't have to dig through the code, so I'll put the abridged version here:
#define CHIP_A 0 ... #define CHIP_L 11 #define t1 1 ... #define t30 30 #define b1 31 ... #define b30 60 #define NANO_D0 70 //these are completely arbitrary ... #define NANO_A7 93 #define GND 100 #define SUPPLY_3V3 103 #define SUPPLY_5V 105 #define DAC0_5V 106 #define DAC1_8V 107 #define CURRENT_SENSE_PLUS 108 #define CURRENT_SENSE_MINUS 109 #define ADC0_5V 110 #define ADC1_5V 111 #define ADC2_5V 112 #define ADC3_8V 113 #define EMPTY_NET 127
What's being stored
There are a few big arrays of structs that store the overall state of everything. Here are the main ones:
chipStatus
This stores the actual hardware layout of each of the 12 crosspoint switches, like what is physically connected where and whether that path is being used. In the code it's and array called ch[12] and it's in MatrixStateRP2040.h
//the struct struct chipStatus{ int chipNumber; char chipChar; int8_t xStatus[16]; //store the bb row or nano conn this is eventually connected to so they can be stacked if conns are redundant int8_t yStatus[8]; //store the row/nano it's connected to const int8_t xMap[16]; const int8_t yMap[8]; }; //the array of those structs struct chipStatus ch[12] = { {0,'A', {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1}, // x status {-1,-1,-1,-1,-1,-1,-1,-1}, //y status {CHIP_I, CHIP_J, CHIP_B, CHIP_B, CHIP_C, CHIP_C, CHIP_D, CHIP_D, CHIP_E, CHIP_K, CHIP_F, CHIP_F, CHIP_G, CHIP_G, CHIP_H, CHIP_H},//X MAP constant {CHIP_L, t2,t3, t4, t5, t6, t7, t8}}, // Y MAP constant ... {10,'K', {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1}, // x status {-1,-1,-1,-1,-1,-1,-1,-1}, //y status {NANO_A0, NANO_A1, NANO_A2, NANO_A3, NANO_D2, NANO_D3, NANO_D4, NANO_D5, NANO_D6, NANO_D7, NANO_D8, NANO_D9, NANO_D10, NANO_D11, NANO_D12, ADC2_5V}, {CHIP_A,CHIP_B,CHIP_C,CHIP_D,CHIP_E,CHIP_F,CHIP_G,CHIP_H}}, {11,'L', {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1}, // x status {-1,-1,-1,-1,-1,-1,-1,-1}, //y status {CURRENT_SENSE_MINUS, CURRENT_SENSE_PLUS, ADC0_5V, ADC1_5V, ADC2_5V, ADC3_8V, DAC1_8V, DAC0_5V, t1, t30, b1, b30, NANO_A4, NANO_A5, SUPPLY_5V, GND}, {CHIP_A,CHIP_B,CHIP_C,CHIP_D,CHIP_E,CHIP_F,CHIP_G,CHIP_H}} };
In general, I use -1 to mean the path is availale to be connected to something. As the pathfinding algorithm runs, it will fill up xStatus and yStatus with the net they're connected to.
Paths
Each path is also stored as an array of structs, this also gets filled out as the pathfinding stuff runs. There are also a couple enums to store the type of path it is, which becomes important for pathfinding because they all are sort of dealt with differently. Note that chip L is kind of a special case because it's kind of the special function chip among special function chips. Most notably, it's Y pins are actually connected to the Y pins on the breadboard chips instead of the X pins like the rest of the special function chips.
enum pathType {BBtoBB, BBtoNANO, NANOtoNANO, BBtoSF, NANOtoSF, BBtoBBL, NANOtoBBL, SFtoSF, SFtoBBL, BBLtoBBL}; enum nodeType {BB, NANO, SF, BBL}; struct pathStruct{ int node1; //these are the rows or nano header pins to connect int node2; int net; int chip[4]; int x[6]; int y[6]; int candidates[3][3]; //[node][candidate] int altPathNeeded; enum pathType pathType; enum nodeType nodeType[3]; bool sameChip; bool Lchip; }; extern struct pathStruct path[MAX_BRIDGES]; //this is the array of paths
Nets
This is where it stores all the info about the nets, this is filled in early on in this whole process during input parsing.
struct netStruct{ uint8_t number; //nets are uint8_t, nodes are int8_t const char *name; // human readable "Net 3" int8_t nodes[MAX_NODES] = {}; //maybe make this smaller and allow nets to just stay connected currently 64x64 is 4 Kb int8_t bridges[MAX_NODES][2]; //either store them here or in one long array that references the net int8_t specialFunction = -1; // store #defined number for that special function -1 for regular net uint8_t intersections[8]; //if this net shares a node with another net, store this here. If it's a regular net, we'll need a function to just merge them into one new net. special functions can intersect though (except Power and Ground), 0x7f is a reserved empty net that nothing and intersect int8_t doNotIntersectNodes[8]; //if the net tries to share a node with a net that contains any #defined nodes here, it won't connect and throw an error (SUPPLY to GND) rgbColor color; //color of the net in hex }; extern struct netStruct net[MAX_NETS]; //The first 8 nets are the Special Function Nets so they're always filled struct netStruct net[MAX_NETS] = { //these are the special function nets that will always be made //netNumber, ,netName ,memberNodes[] ,memberBridges[][2] ,specialFunction ,intsctNet[] ,doNotIntersectNodes[] ,priority { 127 ,"Empty Net" ,{EMPTY_NET} ,{{}} ,EMPTY_NET ,{} ,{EMPTY_NET,EMPTY_NET,EMPTY_NET,EMPTY_NET,EMPTY_NET,EMPTY_NET,EMPTY_NET} , 0}, { 1 ,"GND\t" ,{GND} ,{{}} ,GND ,{} ,{SUPPLY_3V3,SUPPLY_5V,DAC0_5V,DAC1_8V} , 1}, { 2 ,"+5V\t" ,{SUPPLY_5V} ,{{}} ,SUPPLY_5V ,{} ,{GND,SUPPLY_3V3,DAC0_5V,DAC1_8V} , 1}, { 3 ,"+3.3V\t" ,{SUPPLY_3V3} ,{{}} ,SUPPLY_3V3 ,{} ,{GND,SUPPLY_5V,DAC0_5V,DAC1_8V} , 1}, { 4 ,"DAC 0\t" ,{DAC0_5V} ,{{}} ,DAC0_5V ,{} ,{GND,SUPPLY_5V,SUPPLY_3V3,DAC1_8V} , 1}, { 5 ,"DAC 1\t" ,{DAC1_8V} ,{{}} ,DAC1_8V ,{} ,{GND,SUPPLY_5V,SUPPLY_3V3,DAC0_5V} , 1}, { 6 ,"I Sense +" ,{CURRENT_SENSE_PLUS} ,{{}} ,CURRENT_SENSE_PLUS ,{} ,{CURRENT_SENSE_MINUS} , 2}, { 7 ,"I Sense -" ,{CURRENT_SENSE_MINUS} ,{{}} ,CURRENT_SENSE_MINUS ,{} ,{CURRENT_SENSE_PLUS} , 2}, }; char *netNameConstants[MAX_NETS] = {(char*)"Net 0",(char*)"Net 1",(char*)"Net 2" ... (char*)"Net 62"}; //Thanks Copilot
Cool, so those 3 arrays are basically the important ones you'll need to be aware of when I go through the rest of how this all works
File Parsing
I guess "file" is a bit of a misnomer here, the main way of using this thing right now is via the JumperlessWokwiBridge app and that just sends data over serial where it's directly stored into the Nets array. There is support for loading a file using LittleFS but that's not the main way I use this thing anymore because it's slower. But the format is exactly the same.
There are actually 2 indentical file parsers, one in the Wokwi Bridge app and another on the Jumperless, they do the same thing. Really all they do is go through the list formatted with human readable names and replace them with the #defined numbers above. So D0 is replaced with 70 and DAC1_8V is replaced with 107.
Extra formatting is an "opening curly brace" "newline", then "dashes" between nodes and "comma newline" between bridges.
{ 45-GND, 15-SUPPLY_5V, 23-16, 17-46, 42-47, 51-23, 53-52, 48-SUPPLY_3V, 59-46, DAC1_8V-57, 17-2, DAC0_5V-26, ADC3_8V-57, 29-SUPPLY_5V, GND-34, A3-D10, A2-12, A0-10, 35-SUPPLY_3V, 11-D8, A5-20, 9-8, }
Gets parsed into
{ 45-100, 15-105, 23-16, 17-46, 42-47, 51-23, 53-52, 48-103, 59-46, 107-57, 17-2, 106-26, 113-57, 29-105, 100-34, 79-80, 88-12, 86-10, 35-103, 11-78, 91-20, 9-8, }
The Wokwi bridge app does that conversion just to send less data over serial, but even if it didn't, it would just parse it on the Jumperless.
Actually sorting it into nets (real parsing)
Now the real fun begins in NetManager.cpp. At a high level, this is what it does:
- Take in a pair of nodes
- Search the existing nets for either of those nodes
- If it finds one of those nodes in a net, add both nodes to that net
- If it each of those nodes in 2 different nets, check the doNotIntersects, if that's okay, then combine those 2 nets into one net
- Else create a new net with both of those nodes at the first unused net index
After all that runs, you'll end up with a netlist that look like this
Index Name Number Nodes Bridges 0 Empty Net 127 EMPTY_NET {0-0} 1 GND 1 GND,45,34 {45-GND,GND-34} 2 +5V 2 5V,15,29 {15-5V,29-5V} 3 +3.3V 3 3V3,48,35 {48-3V3,35-3V3} 4 DAC 0 4 DAC_0,26 {DAC_0-26} 5 DAC 1 5 DAC_1,57,ADC_3 {DAC_1-57,ADC_3-57} 6 I Sense + 6 I_POS {0-0} 7 I Sense - 7 I_NEG {0-0} Index Name Number Nodes Bridges 8 Net 8 8 23,16,51 {23-16,51-23} 9 Net 9 9 17,46,59,2 {17-46,59-46,17-2} 10 Net 10 10 42,47 {42-47} 11 Net 11 11 53,52 {53-52} 12 Net 12 12 D9,D10 {D9-D10} 13 Net 13 13 A2,12 {A2-12} 14 Net 14 14 A0,10 {A0-10} 15 Net 15 15 11,D8 {11-D8} 16 Net 16 16 A5,20 {A5-20} 17 Net 17 17 9,8 {9-8}
To be continued.... with probably the most interesting part of this whole thing, pathfinding.