-
Log 3 - A working prototype
10/19/2024 at 08:56 • 0 commentsMy initial, cheap and simple, design idea had failed before I even received the boards I had ordered. I had to start over, but the time was running out.
As the first prototype had stopped at the first possible roadblock (I2S streaming) and there were many more unknowns I hadn't bothered checking yet (flash size for sound file and touch-pad sensing, among others) I felt like spending time finding another cheap MCU, with an actual I2S peripheral, would be risky. I instead embraced my inner over engineer and went full overkill.
An RP2040 ought to do it. It actually ought to do it 👏so 👏much that what I had previously failed to do fast enough in C could now probably be done in Python (i.e. by running fast C code written by smarter people than me).
As hinted at in the last log entry, this had always been my plan B. I had routed the touch pads to the SAO connector on the prototype boards and ordered a Xiao RP2040 along with the I2S amplifier breakout board to avoid being delayed too much by the possibility that my bit-banging approach would fail.
Plan A was honestly a long-shot, and mostly done in the hope that a PY32 would be enough. I had to spin a prototype PCB to get my hands on some circular touchpads anyway, and slapping a 25 cent MCU in there wouldn't cost me much additional time or money. You might even have noticed that the prototype didn't have an amplifier on it, as I always planned for a version two.
So with the prototype boards in house I could get to working on a breadboard prototype. This time I would make a functional prototype before ordering another PCB.
Step 1: Touch pad sensing
As revealed above, I hoped to do the code in python. I was now using a mature MCU, and there should be lots of open-source code out there for all the things I needed to do in this project. So, I naturally assumed it would be smooth sailing to get to a prototype.
After a tiny bit of research I opted for using Circuitpython instead of Micropython, as it has better library support and I wasn't going to be doing anything revolutionary here. The Circuitpython filesystem is also way easier to work with than Micropython's (and honestly, this makes a huge difference for any project in my opinion).
For captouch sensing, I didn't even have to install any libraries. The necessary "touchio" library is built-in with the so called "core modules". This library provides both a digital touch/no touch output for each pad, as well as an analog "raw" signal that I assume is a representation of the discharge time. To be honest, the documentation is a bit lacking, so without digging into the source code it's kind of difficult to know exactly what's going on. Luckily, I'm not the first person to use these libraries, and I don't really need to understand what's going on underneath the hood - so looking through some of Todbot's repositories, like the code for his picotouch product series or the code for his Supercon 2023 badge hack, quickly got me going with the syntax.
Converting the raw values to cartesian and polar coordinates was also ridiculously easy, as I was using a 4 touchpad design. Many resources indicate that no more than 3 pads are really necessary for circular pads of this size, but my nervously over-designed layout really paid off in terms of simplifying the code. Here's how I ended up doing the conversion:
y = T1 - T3 # T1 is up, T3 is down x = T2 - T4 # T2 is left, T4 is right angle = np.arctan2(y, x)
In the actual program I've first applied a moving average filter to the raw values, but I've simplified the example for clarity.
How to filter the input values is honestly one of the most complex tasks of this project, and still something I need to spend more time fine tuning at the time of writing. Ideally, I want a ridiculously fast response time, so that I can catch the fastest "wacka-wacka" movements, while not letting through any noise. Letting through noise is horrible because changes in the finger position is what drives the playback speed of the audio, and noisy estimates will cause the audio to stutter whenever a finger is placed stationary on the touch wheel. It will also cause jittery playback speeds when the "DJ" is performing a smooth, constant speed, manual disc revolution. As mentioned, this part of the code is what I will spend the most time improving in the future. For now: I'm happy with a working prototype that actually adjusts the audio playback speed.
Step 2: Audio playback speed
I'll gloss over the details in how I found a drum beat without too much deep bass (that I would have trouble representing with my tiny tweeter speaker) on Freesound, cut it to the shortest possible continuous loop, downsampled and converted it to mono in Audacity, and uploaded it to the MCU and loaded it into RAM. The most interesting part of the audio is how the playback speed is controlled (but I'll cover the loading to RAM part, as that was a bit tricky too).
Now, my idea so far had been to use DMA and modify some buffers that the (pio statemachine-based) I2S-peripheral could shuffle out to the amplifier, but at this point I discover that it's actually a bit tricky to do DMA writes in Circuitpython. This is possible in Micropython, as proven by Supercons 2023 Vectorscope-badge, but I wasn't able to find anything similar in Circutpython. Luckily I didn't really have to, as I was about to discover that a lot of the necessary functionality can be sort of hacked together by utilizing/misusing features of the synthio library.
This library is, as the name implies, intended to be used for digital audio synthesizers, but with more help from the aforementioned todbot, who is clearly a synthio power-user, I was able to find a way to play back a wav file instead of a note - which gave me access to the "pitch bend" effect I needed from the library to control the audio playback speed. The relevant section from his immensely helpful circuitpython-synthio-tricks repository is the one named turn wav files into oscillators. Here he describes how to use a library called adafruit_wave that lets you load any wav file (under a certain size) into RAM.
With my beat camouflaging as a "note" in synthio, I was now able to control the playback speed with the Note.bend property. What I had forgot to think about so far, was that using pitch bending would only allow me to reduce or increase the playback speed. For my effect to work I also need to be able to digitally rewind the record, or in other words to reverse the playback direction of the audio sample.
Now, reversing the playback speed is not currently supported in Circuitpython as far as I could tell. So I guess it's time to dive a bit deeper into how Circuitpython does this in the first place. Opening up the Circuitpython repository is more than a little daunting. Just loading it up in git on my 10 year old laptop takes a loooong time. Compiling it? Even worse. But I quickly noticed that the code for buffering up data to the I2S was fairly simple, and tightly linked with the pitch bend effect, so it shouldn't be hard to make a quick modification that would make it read the sample backwards instead of forwards.
What was a bit worse though, was to figure out how to expose the direction as a property that can be accessed from Python. And with compile times of between 10 and 30 minutes I quickly realized that I would have to accept the first working solution. No time for prettification. The result is honestly really bad, but it works, and since adjusting the playback direction of a Note doesn't really make a lot of sense in the upstream version of Circuitpython, I'll just keep my broken code in a fork that does nothing other than this one specific modification.
Step 3: The rest of the f*cking owl
The rest of the proof-of-concept demo didn't bring up too many interesting challenges, so I'll wrap it up here with a video that shows how this works in practice. Sound up! (Because my SAO could only fit a really tiny speaker, so it's barely audible).
Video hosted on Mastodon:
Post by @simenzhor@mastodon.socialView on MastodonAs mentioned already, this code leaves a lot to be desired, but I'm confident that the right person could make it sound quite cool (even though I still need some convincing that the right person is me).
So even in its current state, this project feels like a good SAO by my standards. With all this on a single PCB it would do something the moment it is plugged in, and there's a lot of room for software hacks/improvements. I guess it's time for a respin!
-
Log 2 - First prototype
10/17/2024 at 19:50 • 0 commentsI pretty quickly landed on the idea that I would be doing a vinyl record that could be touched to control the playback speed of a beat, to create the classic "wacka-wacka" record-scratching sound of the late 20th century.
The problem of course, was that I've never done capacitive touch (either in hardware or software), I've never done software for audio use, I would have to fit everything within the physical constraints of the contest (40x50 mm), and I initially wanted to do it on a cheap Chinese microcontroller that did not have a hardware peripheral for I2S to talk to the amplifier. I could bit bang that though. Or so I thought.
With nothing but naïve optimism and a head full of unanswered questions I did what any sane hardware hacker would do: as little research and maths as possible. I crossed my fingers and started the hardware layout.
Step 1: What is a capacitive touch sensor?
Ok, I had a general understanding of this already, and I'm not going to dive into the details of the physics behind it in this post. But I had a surprisingly hard time figuring out how the seemingly simple sensor could actually be designed and wired up to a microcontroller. What were the requirements for the pin on the MCU?
All the App Notes I could find in the beginning were tens to hundreds of pages long, and written by big companies trying to convince me that I would have to buy their custom ASIC to solve this incredibly difficult issue.
After skimming through a lot of mind numbingly long PDFs I realized I had heard someone talk about the wiring of capacitive touch sensors on a Discord server a while ago. And that's where I found my answer from prolific badge maker Bradán Lane:
Step 2: How does one design circular capacitive touch pads?
Righty-o, schematics out of the way. Now on to the layout... Let's have a quick look at some inspiration, and:
The final design has 4 touch pads with a weird geometry Eeeh... That looks... complicated. Where do you even start designing something like that?
Luckily, this time, there's an app note to the rescue. This excellent app note from TI named Automating Capacitive Touch Sensor PCB Design Using OpenSCAD Scripts was exactly what I was looking for. The app-note provides OpenSCAD scripts for parametric generation of all the most common capacitive touch geometries, along with a thorough explanation of the parameters.
Since KiCAD has a healthy plugin-system I seriously considered porting the script to a python extension, but felt like that was a bit out-of-scope for this project, especially considering the contest deadline. I may have a closer look at that if I ever start another project where I need various sized captouch pads.
My final workflow ended up being as follows:
- Export only the top touchpad geometry from OpenSCAD (Touch 1 in the figure above), as described in the app note.
- Import the DXF into the footprint editor of KiCAD as a graphic (File -> Import -> Graphics... )
- Convert it to a polygon (Right click -> Create from Selection -> Create Polygon from Selection...) using centerlines seems to work best in KiCad 8.
- Merge it with a pad ( I used an SMD pad in my original design, but a THT pad in the screenshot below - not sure which one I prefer to be honest). Click "Add pad", place it somewhere within the polygon, click "Tools -> Repair Graphics..." (for some reason), select "merge overlapping graphics into pads:
- Duplicate it 3 times by "creating an array". "Right click -> Create from Selection -> Create Array...", select the "Circular Array" tab. Verify the circled settings, and remember to check the "rotate items" checkbox
Result:
Nice. Time to slap in a cheap microcontroller and order the boards while moving on to the code. I opted for the Puya PY32F003 as it is dirt cheap ($0.25 in the lowest quantity at LCSC), and I've recently used the even cheaper PY32F002A in another design and had the programming and debugging environment set up and ready to go. The main reason I upgraded to the F003 was to get more unique timer capable pins (TIM), and route one to each of the touch pads.
Full schematics:
Schematics of the first prototype Layout:
Step 3: Bit banging I2S
Using the older F002 based design I had laying around I got to work attempting to bit bang I2S. Fairly quickly I was shuffling out what looked like I2S on my logic analyzer, and I was super happy about that. It was operating at speeds that were good enough for the low quality speakers I would be using here anyway. I had it shuffling 8 bit data at roughly 2k samples per second. Terrible, yes, but I tested the configuration out in Audacity on the beat I was hoping to use, and on my laptop it sounded OK for what I was trying to achieve.
However, when hooking this datastream up a breakout board with an I2S amplifier, everything stayed silent. I could not get the data to be recognized by the amplifier I had selected.
After reading the fine print in the datasheet it turned out that I wasn't meeting the surprisingly strict timing requirements of the amp (ok, the print wasn't actually that fine. Anyway: do you remember all that math and research I did before? Me neither).
My plan until this point had been to shuffle out samples with a varying inter-sample delay to adjust the playback speed, but the amp wouldn't have it. It required an entirely stable data rate, presumably due to some internal PLL or ILO circuit, and a minimum of 8k 16-bit samples per second.
Step 4: Shock/Denial
Step 5: Pain/Anger
Step 6: Bargaining
Step 7: Depression
Step 8: Acceptance
The timing requirements and minimum sample rate of the amp was way beyond what I would be able to do on this MCU. I wouldn't even be able to meet the requirements if shuffling out bits was the only necessary processing task - which it wasn't. I also had to shuffle samples in memory, read captouch pads, calculate finger position and adjust playback speed.
Ok, I had to redesign the board. But I had already spent quite a lot of time and money making it this far, so before lunging into new unknown territory I figured I should first make a working proof of concept with a different MCU.
Luckily, I had forseen this possibility and routed the touch pads out to the 4 "unused" pins on my SAO connector (all the ones that are "not power"), so the boards were not manufactured in vein. I could use them for developing the second prototype with a more capable MCU.
-
Log 1 - Motivation
10/17/2024 at 18:01 • 0 commentsAs the project description emphasizes, this SAO is kind of ridiculous.
I'll be the first to admit that the feature list above grew a bit out of hand. The SAO has ended up closer to a badge than it has to any other SAO I've ever designed (I guess the main thing missing is a SAO port? 🤔).
You could argue, and you'd probably be right, that the only things really needed on this SAO are the four capacitive touch pads, and possibly the i2s amplifier. The badge could do the rest. But in my opinion "the SAO" (as a concept) is at its strongest when you can give it away to someone, they can plug it in, and it will start doing its thing.
I mean: their badge firmware is already busy doing other things, the person receiving the SAO is already busy being at a hacker-con, and they might soon swap your SAO out for another one. Especially if it requires *work* to make it interesting.
For this reason, most of my SAOs (yeah, this is my 7th or 8th since last years SuperCon - depending on how you count) doesn't do much other than blinking an LED or two. Some of them will not even blink the LEDs, just light them. This way I can keep the cost at a minimum and personally assemble them in large enough volumes that I can hand them out like candy.
At SuperCon 2023 I brought around 30 SAOs of a single design, and I quickly felt like that was too little. At Hackaday Europe earlier this year, I increased my stock - but also the number of different designs I brought. At some point during the evening I realized I could look around me and see one of my SAOs in almost every group of people located around me. I thought that was really cool and that's why, when Hackaday announced their 2024 SAO design contest, I felt it was right up my alley. After all, designing SAOs is the main hobby activity I've been doing over the last 12 months.
However, "there's a catch!", as the official contest page phrases it.
Traditionally SAOs have been all about the look, PCB art and blinking LEDs. This year, we want to change that. We’re challenging makers and hackers to build functional SAOs, or as we’ve come to call them, Supercon Add-Ons.
The designs I've been making over the last year have been superhero designs, to which I obviously don't own the copyright - even though I did the actual artwork myself (with comics as reference). In other words: they are not eligible to the contest. Even if they were, they don't respond to this call-to-action.
So I was forced to come up with something completely different if I wanted to participate in the contest. And I did! But I wanted to stay as close to my original philosophy of handing out cheap working SAOs as possible. That being said, I allowed myself to break the rules about cost and assembling a lot of them by myself if it became necessary. After all, the prize of the contest is that someone else will pay for, and assemble, the design - so who cares, right?
I've been curious about capacitive touch for a while, and been on the lookout for a project where I could get more familiar with them, so this contest was the perfect opportunity to dive deep and learn how to design (and use) those.