Saturday, January 14, 2023

Internet of Crosstrainers, part 2

Introduction

As mentioned in the original post here, there were some issues in the described implementation. I had some difficulties to find the time and energy to fix them but now it's here. What's more, I have finally managed to share my first project on github! Please don't ask me why it took so long.

Simple fixes 

The first issue was related to the upload being stuck in case of any WiFi issues. The simple solution is to add a task with a timeout delay. The task should just end the operation regardless of whether the upload was completed or not. In esp-idf it looks pretty much like this.

static void timeout_task(void* arg) {
  vTaskDelay(1000ULL * UPLOAD_TIMEOUT_S / portTICK_PERIOD_MS);
  power_off();
}

The power_off() function should do the same things as what the device would normally do after an upload. This however brings up a new issue, which is loss of data since the device simply shuts down without uploading or storing the data. The answer is to store the data of course. The easiest solution is obviously to have a ring buffer to store the measurements data in the RTC memory of the ESP. This way the upload can be retried next time the device starts up.

The functionality of the device should thus be changed a little. First all the data should go through the ring buffer, since the device might timeout in the middle of upload. This can be implemented in other ways, but that's how I did it. After that the data upload should continue until the ring buffer is empty. Also it's important to check whether the buffer is full before adding any data. This shouldn't be an issue though because the ESP has lots of memory, because the size of data is small and because this is just a backup feature.

Another quite important detail is to add some kind of magic word to the RTC memory, since I'm not really sure how well it retains data and when it is actually cleared. If the magic word doesn't match, the data is lost and the buffer counters (which should also be in the RTC memory) should be cleared.

There is one thing missing here however. The original idea was that the ESP doesn't need to know what time is, only how long the exercise has lasted. The Google Apps Script should create a timestamp based on the upload time. In this case the data is stored without a timestamp and then uploaded later on, so the actual exercise start time is lost. The only solution would be to implement an RTC in the ULP code but I was simply too lazy to do that. The timestamp can be added manually and it doesn't need to be that accurate. So let's leave it at that.

New findings

While cleaning up stuff, I decided to update the PlatformIO ESP platform to the latest version. The platform includes esp-idf framework and other stuff. This update caused much more fun than I anticipated. First of all the upload would randomly end up with a stack overflow. Secondly the mentioned stack overflow would cause a boot loop with a "divide by zero" error. Luckily I realised that I should fix the boot loop first and then deal with the occasional stack overflow. While fixing with these I found another way to get into a completely unrelated boot loop. Well not really a boot loop. The device would operate quite normally without crashing, but still in a way it really shouldn't. However let me explain these in chronological order.

The first boot loop was an interesting one. I made sure that there is no division by zero in my code. For example when averaging the load using the number of revolutions (in case there are zero revolutions for some reason). What I didn't expect was that the division by zero is caused by an example code from the esp-idf page. The function rtc_clk_cal() will simply return 0 after a restart caused by stack overflow. That is a nice thing to know. This is also completely unnecessary code that I just left in for future reference. So that's one possible explanation for device being seemingly stuck as I have observed before.

The stack overflow itself was probably caused by doing the upload directly in the main function without a separate task. At this point I was lazy again and simply made one more array global so that there would be less memory usage in the main. Not the proper way to do it but I tested it multiple times and it seemed to work.

The last boot loop was the most interesting one. I stumbled upon it by accident and had to figure out how to reproduce it. I then figured out that it was caused by simply holding down the IO0 button, which is used for reading the reed switch state. I then spent a few hours trying to figure out why it caused a boot loop. At first I thought that it's because the previous state of the reed pin* is always reset to zero and thus a new edge is detected at each startup of the ULP. I tried to fix that but it didn't solve the problem. I tried to fix it in another way and it did something but not in a way I intended. Which lead to a revelation.

I originally understood that the ULP core would be shut down after starting the main CPU, but apparently I was wrong. The conclusion was that the ULP program continues to function in the background and immediately wakes up the main CPU after it has initiated deep sleep. It shouldn't, but it does. I first checked from the code that it's exactly how it works. I then remembered that I have the diagram based on which this ASM code was written. I checked the diagram and the issue was also very obviously seen there. I have no idea how did I do such a silly mistake but that is now fixed and the code got actually shorter by three lines. Below is the fixed diagram.

Fixed diagram of the ULP functionality

* This idea by itself was a mistake. The state is reset to zero and it first has to go high to be able to detect a falling edge. I realised it while writing this and decided to leave it in.

Final words

This was a rather fun update with lots of revelations. The good thing is that the major issues have now been fixed and that updating the ESP platform to 5.3.0 didn't cause any issues (related to the platform itself that is). The greatest thing however is that I've managed to share the source on github. Hopefully it will be much easier for me to do in the future.

Sunday, September 18, 2022

Internet of Crosstrainers

Introduction

I have to apologise for the title but I just couldn't resist. The idea of this project is actually exactly that, connecting a crosstrainer (physical device) to the internet, or making it "smart" in other words. I use quotes here, because it doesn't really make the device (or anything else) smart. The main point of this project is to make the device a little bit more interesting to use, but the reasoning can be split in two parts.

The first reason is to quantify and record the exercises done on the device. Recording makes it easier to see any progress or lack thereof. Recording in a digital format also makes it possible to generate nice graphs. It could be done by hand, but it's a bit annoying to do it manually.

Another reason is something called gamification. It's an idea that you get some gratification for getting a reward even if it's an imaginary one. My brain clearly doesn't understand that I need physical exercise to such an extent that some virtual points needed to be created. I'm clearly not an expert on this (gamification) topic and I think that this concept is quite stupid but I kinda sorta see what's the point.

At least one result went exactly as expected. Obviously I had to use the crosstrainer while developing, testing and calibrating my device. Of course that's not enough but it's at least something.

Background

Here's the info about the crosstrainer:

  • model is SPORTOP E820
  • bought it used for a very good price
  • probably cost ~300€ when it was new in 2006 (?)
  • has magnetic load and a "computer" (to control the load)

The main problem with this crosstrainer is that it's mostly used as decoration. The good thing is that at least it's not used as a clothes rack, like at my parents place. So I guess I'm already a bit better at that, but there's still some room for improvement.

Research

I did some research for similar projects or commercial products. I did it only after finishing the design, but it doesn't really matter since I didn't find anything useful anyway. I did find some DIY projects. Open Cross Trainer is trying to rebuild a crosstrainer with some extra features, a reasonably intrusive project and doesn't log to cloud, so not exactly what I had in mind. Also I don't need to rebuild mine, it works quite fine for now. Another project is trying to improve the display part of the crosstrainer (including the heart rate). Again not something what I'm after, but there are some things that I could inspect in that project.

Then there's the commercial part. It seems that most of these are designed for gyms like ActiveLinxx and Advagym. They are designed for multiple machines and multiple users trying to improve a whole gym. There seems to be also some for private use (existing in a device you can actually buy) like the iConsole+ but there's no clear explanation anywhere on what any of these actually do. The linked pages seem to be only advertisements.

I also found some funny articles like this one mentioning "Quantified Self" and an app called "Life Fitness Connect" whatever it is. I also found this master thesis that I didn't read, but might read at some point. ActiveLinxx was involved in that thesis and it also mentioned "velocity based training" which sounds silly but apparently is a commonly accepted concept.

Requirements

The most important requirement is to make my device completely autonomous (not the crosstrainer). I originally wanted to add a user selection, but then decided not to, because it would just make the device so much simpler. Also the device could try to guess the user or the selection could simply be done afterwards. Additionally the device shouldn't have any display. The crosstrainer already has a display for the "computer" and there's no (real) need or place for another one.

I wanted to make this device wireless right from the start. The reason is that there's no PC nearby and I'm not installing one just for this reason. Originally I thought about bluetooth, but then I would have to have the PC running and deal with the serial ports, which I really don't like. I then thought about using some nRF radio, but then I would also have to build the receiver. I then remembered the existence of ESP32 which eventually fit this purpose perfectly.

I also wanted to make this device battery powered. The reason for it is that I felt that it could be battery powered and because I simply don't want to make mains powered (USB charger powered) devices unless really necessary. USB chargers are not really intended for constant use (even though I have used them in some projects).

However there lies the problem. The ESP32 is quite power hungry and wouldn't last long using batteries. Even without WiFi enabled, the power usage is noticeable and the device would drain the batteries quite fast. Or at least that would be the case unless the ULP (Ultra Low Power) co-processor is used. I also thought about some ESP32 + AVR combination, but that would be unnecessarily complicated.

I was a bit afraid of using the ULP core, because in the original ESP32 it has to be programmed using assembly. However there are plenty of good examples available and because the assembly part should be really simple. Eventually the ULP part of this project was quite easy. Here are some examples that I've found: esp-idf, wardjm/esp32-ulp-i2c and duff2013/ulptool.

As a bit of a spoiler here, I later realised that this device can be implemented without batteries whatsoever. No, I'm not making a "zero power WiFi device". I simply realised that the crosstrainer itself is battery powered and that my device could be so low-power that it can take the necessary power from the crosstrainer itself. However more about that later on.

One other target was to not break any existing functionality in the device. There was a small hiccup related to this, but eventually it was quite easy. The point is, after all, to just read some values.

Preparation

I started this project by partially disassembling the crosstrainer. I knew already that there's a connector between the "computer" and the rest of the machine (so that it can be disassembled for easier transport) but I didn't know what signals are there. By the way, the connector is a 12-pin JST XH which is used as a wire-to-wire connector. It's not really meant for that but there's heat shrink tubing and something similar to hot melt glue so I would say it's fine. Just unnecessary manual work for the device manufacturer. However at that point I took a multimeter and carefully measured the signals. I also had to use a small mirror to see inside the device to make sense to some of the details. The pinout is listed below after a picture. I made an inline adapter for the connector so that I can measure some values in situ.

Wire to wire connector with an inline adapter

  1. dc jack- (violet)
  2. dc jack switch (cyan)
  3. dc jack+ (pink)
  4. reed- (brown)
  5. reed+ (white)
  6. ?? (white)
  7. ?? (yellow)
  8. pot+ (green)
  9. pot wiper (blue)
  10. pot- (yellow)
  11. motor (black)
  12. motor (red)

I'll briefly explain the list. The motor is a DC motor that adjusts the (spring loaded) load. There's no polarity since it has to turn into both directions. The potentiometer is there to indicate the value of the load. The pot+ voltage is ~1V. The ?? is probably some kind of calibration trimmer in the frame. I actually have a picture of it (see bonus at the bottom), but I don't really have any proof of what it is. The reed is the magnetic switch for detecting rotation of the big wheel. I only guess that it's a reed switch, because I can't see how it could be a hall sensor. But at least there doesn't seem to be any noise (bouncing) so I guess it's all the same. The voltage at the reed+ is ~4V for some reason. The last thing is the DC jack, which allows the crosstrainer to be used with a wall wart (instead of batteries).

This list is really good news. The load can be read without any adaptation and the reed switch can be read with minimal effort. Well there was some effort involved because it took some time to realize what I'm dealing with. I didn't really want to reverse engineer the "computer" PCB, because it's old and messy. I didn't even want to disassemble the "computer" because I was afraid that the LCD assembly would fall apart or the plastic screw holders would crumble to pieces or something. I guess mostly I didn't want to reverse engineer the PCB. :)

Implementation

The final implementation has three parts. The first one is the ULP, which records the exercise data using as little energy as possible. After that there's the main ESP32 code, which simply generates an HTTP request and sends it (completely discarding the response, which is btw a redirect, more about that later on). The URL in question is a Google Scripts URL which has access to my Google Drive and directly updates a single Google Sheet which was created for this exact purpose.

ULP

I started with the ULP implementation, because I thought it would be the most difficult one. There are good examples in the ESP-IDF github. One example shows how to use GPIO and another shows how to use the ADC. I needed both features so I kind of combined parts of these. However I removed the debounce code from the GPIO example and added another layer for the "activity" detection. I actually had to think this part quite thoroughly in advance because this is my first program using assembly and I didn't really know how to "just start prototyping" like I do in C. The final (rough) idea of the ULP program is shown below.

  • wait for a falling edge
  • measure activity
    • count falling edges
    • measure the load
    • count the time
    • wait for a timeout
  • wake up the cpu

I actually had to make a diagram of this program, so I'll also show it here. I usually don't make these, but in this case it really helped.

Diagram of the ULP program


The last part was simply writing the assembly program. The writing was actually pretty uneventful. I made some small mistake with the edge detection and then I made few other small mistakes. However to debug the issues, it was enough to simply stare at the code and compare it with the diagram shown above. Also I don't really know (yet) how else it could be debugged. There aren't really any interesting parts in the code so I will not share it here.

ESP32

The non-ULP part of the ESP32 programming was also very simple. I took the https-request example as the basis for this project and added the ULP part there. The main part is to read the ULP variables, make some adjustments and generate the URL based on the measured values. Some adjustments are necessary for the ADC measurements. The ULP doesn't have multiplication or division and the variables are 16-bit long, so it was easier to implement averaging and such in the main code.

Google Scripts

The Google Scripts part is also done based on an example. The basic idea is to open a Google Sheet, check the last used line number, increment it and add the received data. There are only a few special things. Some data is adjusted, for example the duration is recorded in seconds but displayed in minutes and the conversion is done in the script so that the ESP32 doesn't need to deal with floats. Another thing is that the script will record some metadata that is only used for debugging. This includes the timestamp, added line number, RSSI and the MAC of the last device that made the update. I'm only going to make one device, but I might have another for debugging purposes.

Google Sheets

There's not much to do in this part. The purpose of the script is to make a nice log of the exercises. This data can then be used for making nice graphs. I've decided to plot the energy usage, so that needs to be calculated first. I've concluded that it's simply number of revolutions * load * some constant. The constant was chosen to match the energy values calculated by the "computer" of the crosstrainer (which is a value that my device cannot read). After that the whole columns can be selected as the data for the graph, which means that the graph will be automagically updated every time a new line is added.

The last thing is to make meaningful types of graphs. I've decided to add a bar graph that would represent every exercise, so there is no grouping and the bars have variable gaps between them (depending on when the device was used). I've also added another graph that represents a monthly sum. These two graphs require two new columns (for the x axis formatting), one that only has the date (without time of day) and another that only has a month and a year. I don't know how these graphs will look with a lot of data, but I guess I will find out after some time.

Bonus

At some point I realised that the device could also measure the battery voltage. Obviously the voltage should be reported to the Google Scripts since the device itself doesn't have any display. I then found out that the Google Scripts could also send emails. As a result this might be the first crosstrainer in the world to send notification emails about dying batteries. And die they will, since apparently the crosstrainer itself has ~1.7mA current consumption when idle and it uses C batteries (capacity ~8Ah). Which means that the battery life of the crosstrainer itself, in stand-by mode, is <200 days. However the minimum voltage needs to be calibrated using real batteries, since the internal resistance of the batteries might have more effect than the cell voltage (so cannot really be simulated with a PSU, also I'm not carrying a PSU to the crosstrainer or vice versa).

The voltage could obviously be reported whenever the crosstrainer is used, but then again, it could also monitor the voltage constantly and report the low voltage even if the device is not used. The success rate of that obviously depends on whether the batteries can still support the WiFi functionality at the point where the crosstrainer decides they are empty (it refuses to work at all). For this feature to work, the ESP32 and the Google Script code should detect that we're only reporting the battery voltage and only send a notification email without updating the Google Sheet. The question of which will fail first (the crosstrainer or the ESP32) is still open.

I also realised that the ESP32 has a built in temperature sensor and I thought that it could also be logging the temperature. That's quite a stupid idea, but for some reason the crosstrainer itself also has a temperature display. However I thought that I could add a temperature threshold to use it as a remote fire alarm. It's a nasty topic, but it would be a small feature to ease my mind when not at home. Even if I couldn't necessarily do anything about the fire. However I couldn't make the simplest temperature measurement examples work, so I had to drop this idea at least for now. The ULP code would simply get stuck at the TSENS command on the second run of the ULP code and no one else in this world seems to have had the same problem.

Another dropped feature (at least for now) is the OTA support. The device should fit nicely inside the frame of the crosstrainer, so updating the firmware is not very convenient. For this reason I wanted to add OTA support using the Google Drive, but for some reason it doesn't seem to work. I spent quite a lot of time with it without any success and had to give up so that I can get this project done in any sensible time. After all, the OTA is not really necessary if the device is done "first time right" (which is bullshit, but I had to learn about it so now you need to hear about it too).

PCB

The PCB design is almost as simple as it gets considering the things explained until now. There should be a 12-pin "in-line" connector, an ESP32 and a voltage regulator. I've chosen to use the "stacking" pin header as the single connector on the PCB so that the male and female JST connectors can be both connected to it. The only downside is the lack of polarity, but I guess I will just have to mark it clearly and remember to connect correctly. This is not a very nice solution, but I believe that the female JST PCB connector doesn't exist. As mentioned earlier, I've chosen the original ESP32 (not S2 or C3). And I've chosen the MIC5239 as the regulator because the quiescent current consumption is low enough and because I have it in stock.

The rest of the PCB design is very simple. There should be a (compact) programming connector, there should be an indicator LED, some buttons, test points and proper markings. One more thing is a specific diode circuit for reading the reed switch. The reed output is 4V, which is not nice for the ESP32. The pull-up resistor for the reed switch is 100k, so I couldn't use any zener diode to ground to clamp the voltage to 3V3 (from 4V) because the "computer" could no longer read steps. Instead I wanted to use a zener diode in series, to adjust the voltage to a voltage of ~ 1V .. 3V but I couldn't find one in a small enough package. Instead I'm using two normal diodes in series and in both directions. That way the voltage drop should be approximately 1V in both directions, so the resulting voltage should be also ~ 1V .. 3V. At this point I can say that it didn't really work as expected. But the ESP32 is still functional so I'll leave it at that.

PCB design in EasyEDA

The resulting PCB design is shown below. The PCB is double sided and should be as compact as possible while including the ESP32, the connector and two buttons. The chosen connector was a great idea, but unfortunately it made the device too large to fit into the slot that it was supposed to. I had to desolder the connector, bend a new one to 90 degrees and solder that instead. After that the device could fit into the slot with the wires connected to it.

Assembled PCB

Another thing is to add an external temperature sensor in case I ever decide to implement the temperature measurement feature. An external sensor would also have much better accuracy than the integrated one. Eventually I didn't do this, perhaps some other time.

Case

As explained in some previous post, I don't really have the means for making enclosures, but that's okay in this case. I've realised that the device could be simply inserted into the frame of the crosstrainer where the wire and the inline connector should normally be. In this case there should be some enclosure, but a simple piece of tape should be plenty enough. The case also doesn't need to look good, because no one will have to see it. I would say this solution is "good enough".

Installation

Installation of the device is very simple. First the "computer" needs to be detached. Then the wire-to-wire connector has to be disconnected. Then the ESP32 board should be inserted between the connectors (with the correct polarity of course, unfortunately using simple headers allows connecting the device the wrong way around). Next the ESP32 board should simply be tucked inside the frame and the "computer" should be installed on top of that. Below is a single picture to visualise the installation process. Yes, I specifically wanted to write this long piece of text for something that can be explained with one picture.

Installation of the device

Note that the cable connectors are now at an angle of 90 degrees compared to each other. Also note the markings (arrows) that I figured I could add to both the connector and the device.

Final thoughts

I had a lot of fun and luck with this project. The ESP32 was perfect for this project, the Google Scripts is easy to use and there are plenty of examples for both on the internet. Also the crosstrainer had very nice signals for this purpose. All in all, it was quite an easy project to make and it didn't take much time (apart from all the finishing touches). Which is good compared to the previous project which took more than a year to finalise. Here is the link to the EasyEDA project. I will try to add the codes to gitlab at some point, but it might still take time.

Unfortunately testing this project took a lot of time, because I got sick right after making a functional prototype and then there was an impossible heat wave and there's no proper cooling in the apartment that I live in. However the testing was done and the data was calibrated (apart from the battery voltage when the device refuses to work any further, that would take much more time to test).

It wasn't all that successful though. The first issue was already mentioned connector that I had to replace. The second issue are the side buttons that might get pressed regardless of the used tape, because the black cable is so rigid. The next issue is that even though the device has an activity LED (for easier debugging and development) it cannot really be seen when it's inside a metal frame of the crosstrainer. Related to this issue, the ESP32 might get stuck and drain the batteries without anyone noticing anything. This actually happened once and I still don't know why. Below are some things that still need to be added assuming I have energy to do so:

  • WiFi connection timeout
  • Upload timeout
  • Battery low detection before the upload
I should also find out why the ESP32 seems to get into a boot loop in some cases. It's as if it fails to read that the reset reason is not ULP. I haven't had time to investigate it though so I'll leave it as is for the time being. For now my device is hanging outside the frame so I can see whether it's actually working as it should.

And here's a bonus picture of the potentiometer deep inside the frame of the crosstrainer. It is located between the two large rotating wheels. I had to hold a mirror, flashlight and a phone to take this picture.

Random potentiometer deep inside the frame of the crosstrainer

Monday, June 28, 2021

PS3 Controller Battery Upgrade

Introduction

I happen to own four PS3 controllers. Two of these are bare SIXAXIS controllers and two are DUALSHOCK controllers. One of them by the way is clearly a fake and it looks quite interesting from inside. Unfortunately all of these have some issues.. One is missing a battery, another has a very very bad battery (a recycled one apparently, less surprisingly it's the fake one). One more has a battery that is slightly less bad and one has R2 or L2 stuck all the time. I don't use my Playstation 3 much but I use these to play (local) Co-op games on the PC so it would be nice to have at least two (and preferably four) functional controllers. After some thought I've decided to replace the batteries with new ones, instead of buying new controllers especially since they're not manufactured anymore. Opening the controllers and replacing the battery is relatively simple, so let's get to it.

I'm not the first one to come up with this, but let's make it clear that I want the new battery to fit inside the controllers, so let's leave out projects such as these (one, two, three). I also recently saw this abomination, but who am I to judge. I definitely don't need such battery capacity, even the original capacity of a new PS3 controller would be enough for me since I suspect that the actual capacity of the current batteries is much less than the nominal capacity. However I'm not going to measure the actual capacity even if it would be interesting to see how much they have degraded. I would like to, but I don't have proper means to do that. :(

Starting point

The original LIP1359 Li-ion battery has an approximate size of 5x36x58 mm, 610 mAh capacity and a 2-pin JST connector with 2 mm pitch (PH series). The first issue is finding a proper battery. I've thought of just ordering a spare from eBay but that's just a gamble. You usually get a decent battery, but it could also have one tenth of the specified capacity.. Second option is to buy from some (possibly shady) local shop. The product will probably be the same cheap fake as above but the price will be much higher. That does not spark joy.

Another option is to buy a generic battery (with a protection chip of course, as is the original) from a reliable source and adapt it for the PS3 controller. After opening some controllers I was quite confident that a 6 mm thick battery could fit inside the controller. However I've decided to go with a battery of the same thickness since I found a suitable one. Theoretically a 6 mm thick battery would fit into the controller, but the tolerances for the battery I found state that the maximum thickness of the battery is 6.5 mm and we definitely don't want to squeeze an unprotected (without a plastic case) lithium battery.

The battery I found is a LP503759 and it has a whopping 1350 mAh capacity, which is a lot compared to the original battery. The battery is also almost the same size as the original one: 5x37x59 mm. Battery of this size might even fit into the original fittings, but they will definitely need a little adjustment. The only issue is that it has a wrong type of connector, more specifically SY instead of a PH (both are JST). To make it nice, I've decided to actually buy a crimping tool and make a proper connector without soldering any wires together or directly onto the board (as is done in many other projects). Moreover the wires of both proper batteries that I have, have been trapped between some plastic parts and the insulation is clearly damaged. This is something that should be taken into account when putting the controllers back together.

The capacity change from 610 mAh to 1350 mAh with almost equally sized batteries sounds suspicious. However the original battery has a plastic case and the actual thickness of the original battery is probably significantly smaller. Unfortunately I didn't have an extra battery to disassemble and measure. Additionally the original battery is a Li-ion and the new one is LiPo. I wasn't actually sure if it's okay to replace a Li-ion with a LiPo but these are even sold as actual replacement batteries. Also other people have replaced the original battery with a LiPo and that seems to work. Originally I wanted to install an even bigger battery but I suppose ~2x the original is enough for me. It's not like I play all the time. In retrospect, I kind of doubt that any bigger battery could be fitted safely inside the controller. Perhaps a 5.5 or 6 mm thick one, but the height of 38 mm is pretty much a hard limit.

Upgrade process

The upgrade process is relatively simple. First the disassembly. Take a Philips screwdriver, unscrew 5 screws at the bottom and wiggle the upper part of the controller until you get the two parts apart from each other. I suggest watching some YouTube video about this, because some controllers seem to be much more difficult to open than the others. There is a very nasty piece of plastic at the bottom of the controller, between the analogue sticks that causes this. The next steps are to disconnect the battery connector from the board and recycle the old battery according to local regulations. Be nice to mother nature. Below is a picture of a SIXAXIS controller with the original battery. The battery happens to be missing a corner since I wanted to see if it's protected or not.

PS3 controller with the original battery

The next step is to prepare the new battery. First the wires should be cut to a correct length. After that the wires should be stripped and crimped, preferably one by one so they don't get shorted accidentally. I've bought an IWS-2820M tool from IWISS for this purpose, but I will definitely have other purposes for it later. Here's a great explanation from bigclive about different crimping tools and how to use them. I've also ordered PHR-2 casings and respective pins (SPH-002). More info about the connectors can be found for example here. Obviously the plastic casing could be recycled from the old batteries as well but I didn't bother with that.

The next step is to remove the original fittings, adjust them with some tool to accept a larger battery and put them back. I used a scraper tool mentioned in this video for the adjustment purposes. A dremel would have been much better but I currently don't have one. The plastic part is quite flimsy so the task is actually quite difficult. After the adjustment the new battery can be installed in place of the old one and the controller can be reassembled. Special care should be taken so that no wires get squeezed between plastic parts. To be safe, I used a string to hold the battery in place, while assembling the controller. I didn't tie the string together, but instead held it tight with my hand while assembling the controller. This way I could pull the string out just before snapping the controller back together, when I knew that the battery is already quite snugly in place.

Unfortunately the adjustment was not that easy. At first had to adjust the fittings to accept a wider battery. Then I realised that the bottom part of the case is actually barely large enough to fit the original battery. Since I didn't want to squeeze the new battery in any dimension, I also had to adjust the bottom part of the case. Unfortunately I don't have any pictures of that. Nevertheless, below is a picture of a controller with a new battery ready for assembly. This is a different controller than in the previous picture, but I guess that's okay.

PS3 controller with the new battery

Result

After a quick test, I am very satisfied with the result. The two controllers with new batteries hold the charge without any issues. I use the unofficial windows driver for these controllers and it allows displaying the battery charge with the LEDs on the controllers. Based on these LEDs the new batteries last significantly longer than the old ones. I only replaced two batteries and left two old batteries as is, so it was easy to make a comparison.

Additionally I have the crimping tool and know how to use it. I've concluded that it's actually quite good for crimping JST connectors. However it doesn't feel to be as good for crimping dupont style connectors that I needed in another project. But that's a story for another time.

Final words

I've originally had this idea in October last year and that's when I ordered the crimping tool from AliExpress. It never arrived though, so I got my refund and ordered again from a different seller in January this year. That tool also never arrived, so I got my refund and ordered a new one from Amazon this time. However I was so worried that there's some kind of Bermuda Triangle that is sucking all the IWISS tools inside it, that I ordered another one from Banggood. Eventually the Banggood one arrived first and then the Amazon one, so now I have two of these. That doesn't matter though, because a proper tool would still cost 20x (?) more.

ps. I'm kinda worried about the person who made this video (based on the content). I hope he's still alive. :)

Here's a small bonus. Below is a picture of the fake controller and its battery. The battery was originally insulated with heat srink tube that is usually used for insulating batteries and it was simply glued to the PCB, so there are no fittings of any kind. I've replaced this battery with an original PS3 battery so I had to improvise a bit. It took me so long to write this post that I've actually forgotten how did I solve this. I guess that's good. :)

Clearly fake PS3 controller

Clearly recycled battery of the clearly fake PS3 controller :)

Thursday, May 6, 2021

ESP32-Radio (Part 2)

Introduction

As I've explained in part 1.5, I've had quite some issues in the beginning of the project. However I was able to design adapter boards for the display, buttons and the rotary encoder. They also required multiple revisions, but nevertheless they are now done and they are enough for me to proceed with the project. I've had a lot of issues while trying to make the buttons work well, but I think I've got the hang of it now. The display on the other hand is working nicely from the start and I will do further development in the final part. I've tried to make the adapter boards as generic as possible so that I can actually reuse them in some other projects or prototypes.

Display

Schematic

I've made a minimalistic adapter board for the RX12864H-BIW display from Raystar. I like this specific display since the it's quite cheap, compact and easy to use. This exact display uses the ST7567 driver and requires only two small external caps and basically nothing else. The downside is the FFC connector and a requirement for quite tricky mounting holes (also it's always out of stock at TME). I've also included smd solder blob type switches on the board for all the pull-ups and pull-downs that my project requires (hardwiring is bad) and finally a MOSFET for driving the backlight with a PWM signal. The schematic in all it's simplicity is shown below.

Adapter board for the RX12864H display from Raystar

The PSB, ERD and RWR connections are designed for the serial configuration that I'm going to use. These are specified in the datasheet. For the backlight resistors R2-R5 are connected in parallel, because a single 0603 resistor cannot handle such power that is going to be dissipated there (approx. 100 mA * 1.5 V = 150 mW). Typical power rating for an 0603 resistor is 1/16 which is 0.0625 W, so it's a good idea to split the current to at least three resistors. It would make much more sense to use a larger resistor but I only have a good selection of 0603 resistors. SW1 is also there for bypassing the backlight control in case it's not needed.

The only issue with this board is that the backlight will easily flicker if the input voltage varies even slightly. This happens especially in the device I am building since the current consumption of the ESP32 changes quite wildly. I've seen this issue in some previous project (while using ESP8266), but have forgotten it until now. Adding capacitors doesn't really help but adding a voltage regulator is an adequate filtering solution. The 3V3 is unfortunately not enough for the backlight since the forward voltage is 3.3-3.7V. For this reason I've decided to add an adjustable regulator and set it to 4V in the final design. The voltage drop of the regulator should be small enough for the device to work even with 4.5V input voltage (for very bad cables / power supplies). I was too lazy to make a new revision of the adapter with this regulator included, but perhaps some day I will. A good choice for an adjustable regulator is AP2127K-ADJ which is in a SOT23-5 case, can handle current up to 400 mA and has a small voltage drop. Additionally datasheet of the regulator has exact resistor values calculated for 4 V output. :)

The display is intended to be connected with the SPI interface. In addition to the standard SPI interface (Clock, Data In & Chip Select), the display uses typical D/C-signal (Data/Command, although it's marked A0 which probably means Address) and a reset signal. All this can be seen later from the final design.

PCB

I've made the PCB only slightly larger than the display. Only so that the mounting holes, connector and pin descriptions can fit on the board. This time I also remembered to include as much useful information as possible, for example the name of the driver chip, suggested resistor values, SDA/SCL mapping and C1/C2 voltage ratings (on the bottom, so not seen here). Also the FFC connector and smd solder blob type switches are on the bottom side. The top side of the PCB is shown below.

Adapter board for the RX12864H display from Raystar

The trickiest part was to do the mounting holes for the display module. There are four different details here. The largest hole (shown in grey) in the middle is for the flex cable. The flex happens to be so long that it can be folded under the display, through the hole and into the connector that is FPC1 in the middle of the board (on bottom side). Next there are two rectangles on both sides, these are for plastic clips that are used to attach the display to the PCB. Next are the two large through hole pads at the bottom of the display, these are actually for very narrow strips of double sided PCB that houses the backlight (6 LEDs and 6 series resistors). As a side note the max rated current for the backlight is 96 mA, which means 16 mA per LED, which is reasonably conservative. Lastly there are two small mounting holes in both bottom corners of the display, these are only 0.6 mm in diameter and 78 mm apart. Luckily my measurements were correct and JLCPCB was able to deliver the necessary accuracy that the display fits perfectly (at least two test units).

ps. The reason why a second revision of this board had to be made was that I simply didn't notice the small guide pins at the bottom of the display. Additionally I made the through hole pads too large and I honestly don't know why I did that mistake. I also forgot to add multiple resistors in parallel for the backlight, but that was not a big deal for a prototype.

Code

As already mentioned the display is connected with the SPI interface and pretty much imitates the HD44780 interface. There are a few things no note here however. First of all, this is a graphical display, so there is no hardcoded font data. Secondly the data is arranged vertically on the display unlike it is in many other displays. Lastly the datasheet (or any page on the whole internet) doesn't have any clear example on how to use this exact display. However after looking closely at the datasheet, I found all the necessary details and improvised the rest. I did take some pointers from this source, but I'm highly allergic to that level of code.

After configuring the SPI bus, the software should configure the display. The datasheet tells us that the bias should be 1/9. After that the boost level, regulation ratio and EV (?) should be set. Since the input voltage of the display is 3.3V and the typical voltage for the LCD panel is supposed to be 10V according to the datasheet, I've selected the boost level to be 4X, which would result in max voltage of 13.2V. I then selected the regulation ratio to be 6.0 so that the 10V is in the middle of EV setting (based purely on the graph in the datasheet). Lastly I've set the EV value to be 32 which is in the middle of the configuration range. After some trial and error, I've selected the perfect EV value to be 23, but that will vary from display to display. Additionally I've set the Y axis to be inverted, since the whole panel seems to be upside down and I didn't want to invert all the font data. The configuration process is shown below.

void rx12864h_configure() {
   rx12864h_cmd(RX12864H_BIAS_SEL | 0); // BS=0 -> 1/9
   rx12864h_cmd(RX12864H_START_LINE | 0); // Start line: 0
   rx12864h_cmd(RX12864H_SEG_DIR | 0); // MX=0 -> X normal
   rx12864h_cmd(RX12864H_COM_DIR | (1<<3)); // MY=1 -> Y inverted
   rx12864h_cmd(RX12864H_REG_RAT | 0b110); // Regulation Ratio
   rx12864h_cmd(RX12864H_SET_EV);
   rx12864h_cmd(0x17); // 0..63
   rx12864h_cmd(RX12864H_SET_BOOST);
   rx12864h_cmd(0b00); // 00=4X, 01=5X, 10=6X
   rx12864h_cmd(RX12864H_PWR_CTRL | 0b111); // VB=1 VR=1 VF=1
   rx12864h_cmd(RX12864H_DISPLAY_ON | 1); // Display on
}

After the configuration of the display the picture data can simply be sent to the display one 8-bit column at a time. Unfortunately the counters don't wrap, so they have to be incremented/reset manually. The whole update method is represented below. Since the display is 128 x 64 pixels and has a vertical configuration, there are exactly 128 columns and 8 "pages". The data is sent in bursts of 128 bytes, so one "page" at a time (as a single transmission event).

void rx12864h_update_display(char *picture) {
   for(uint8_t i = 0; i < 8; i++) {
      rx12864h_cmd(RX12864H_PAGE_ADDR | i);
      rx12864h_cmd(RX12864H_COL_ADDR);
      rx12864h_cmd(0x00);
 // Clear column counter

      rx12864h_data_burst(picture, 128);
      picture += 128;
   }
}

Lastly there is the backlight. "Fortunately" configuration of PWM is "very simple" in ESP32. I use lot's of quotes, because I can see how it's made "more simple" for the end user and somehow it's still more complicated than in an AVR. Anyhow, I've configured one pin to output an 8-bit PWM signal with the frequency of 1 kHz and that seems to work just fine to control the backlight. I will not share an example here, because it can easily be replicated from here (also it's just ugly boiler plate code).

Buttons

Schematic

I've made a minimalistic adapter for two buttons and one rotary encoder. Except it's not so minimalistic since the board accepts two different types of buttons and two different types of rotary encoders. The buttons are completely different (D6R90 from C&K and B3FS-4002 from OMRON), but the rotary encoders (specifically EC11J15 and EC11E15 from ALPS) are basically the same except for the mounting (surface mount and through hole). I've wanted to include these two specific button types since they both are very good in quality but differ quite much in the height. If I were ever to design a case for this device, the D6R90 would look quite nice poking from the enclosure by itself (as I've done previously in some other project, albeit with cheaper buttons).

Additionally there are pull-ups and debounce filters on the board. Unfortunately the pull-ups are done incorrectly on this board, since the pull-up resistor is on the wrong side of the series resistor. This means that the resistors work as a voltage divider when the button is pressed, which is okay if the values are chosen correctly. However this means that the rise time of the button signal is significantly longer than the fall time. Additionally the rotary encoder has a minimum working current of 1 mA (as stated in the datasheet), which limits the pull-up resistors to 3.3 k when the used voltage is 3.3 V. Apparently the using the rotary encoder with a smaller current will introduce much more noise as was stated in some source, which I unfortunately no longer have, sorry. The schematic is shown below.

Adaptor boards for D6/B3FS series buttons and EC11 series rotary encoder

As can be seen in the schematic, the two button types and rotary encoders are connected in parallel, since only one of these can be soldered at a time.

PCB

The PCB is equally simple in this case, the different buttons and the different rotary encoders are located on top of each other so that only one type can be soldered at a time. This was quite simple and logical thing to do in my opinion. The board is shown below in all its glory. I didn't even bother adding a ground plane and I probably should, since the boards look very odd without a ground plane.

Adaptor boards for D6/B3FS series buttons and EC11 series rotary encoder

The only issue with this current board itself is that I forgot to relocate the SW5 and SW7 labels. :(

ps. The reason why the second revision of this board had to be made was that I used ready made components in the first revision and the symbol for the B3FS had wrong pinout, which means that the button was constantly shorted. Also the EC11J15 did not fit in it's footrpint. You might ask how an SMD component cannot fit into its footprint that was supposedly made by the manufacturer and I simply do not have an answer for that. I only know that it physically did not fit, since the tap at the bottom of the component did not fit into the hole in the board. Moral of the story, never use ready made components. If you want something done, do it yourself.

Software

The buttons are very simple devices. I mean they should be. However I've spent quite some time trying to make them work properly with the ESP32. I suppose the source of these issues is that I tried to use GPIO interrupts to read the buttons as I've done with many AVR projects previously. It seems that compared to an AVR, the ESP32 is much too sensitive for detecting the falling edge. I suppose I could have used detection of the rising edge but then the schematic would need to be changed and that's just stupid. I've spent many hours trying to make this work using interrupts, but then it took only a few minutes to make it work after I gave up on the interrupts.

Reading the properly debounced buttons without an interrupt is very simple, especially when using FreeRTOS. You basically make an endless loop with a delay statement, that checks the state of the button pins and compares to the previous state. This way a falling edge can be detected, which means that the button has been pressed. Fancier features like the long press support can be added later of course, but edge detection is the basic principle for reading a button press.

The rotary encoder is a bit more difficult. There are two pins in this encoder and they are shorted in specific order when the encoder is turned. The specific order is gray code (not binary) for good reasons. However we don't really need to care about that, we just define the four states that can be created with two pins, and try to detect specific state changes. The principle is basically the same as with the buttons, but here we check the state of multiple pins instead of just one. The state change is then interpreted as one clockwise or counterclockwise step. Below is a picture that shows the four stages of a rotary encoder (taken from the ALPS datasheet).

Gray code of a rotary encoder

Let's assume that the left dotted line is at state 0 and the right one is at state 2. In this case we can, for example, detect state changes from 0 to 1 and from 2 to 3 as one clockwise step. Similarly a change from 3 to 2 and from 1 to 0 would mean one counterclockwise step. And that's basically it. Below are first some example definitions for pins and rotary encoder states. The states are defined similarly to the picture starting from the left dotted line.

#define KNOB_RE_A_PIN 27
#define KNOB_RE_B_PIN 26
#define KNOB_BUTTON_PIN 14

#define RE_MASK ((1ULL<<KNOB_RE_A_PIN)|(1ULL<<KNOB_RE_B_PIN))

#define RE_STATE_0 ((1ULL<<KNOB_RE_A_PIN)|(1ULL<<KNOB_RE_B_PIN))
#define RE_STATE_1 ((1ULL<<KNOB_RE_B_PIN))
#define RE_STATE_2 (0)
#define RE_STATE_3 ((1ULL<<KNOB_RE_A_PIN))

After that we can make a task for reading both the buttons and the rotary encoder. I've also specified defines for each action, since they cannot really match with the pins (at least in case of the rotary encoder). Another remark is that I've used the GPIO_REG_READ macro instead of the usual gpio_get_level simply because there's no need to use the latter one. I suppose the latter one would also have some unnecessary overhead. Lastly the actions are sent to another task with the help of task notifications, which is a great feature by the way. In any case here's the example code.

static void read_buttons_task(void* arg) {
   uint32_t prev_buttons = 0xffff;

   for(;;) {
      vTaskDelay(10 / portTICK_PERIOD_MS); // 10 ms delay
      uint32_t buttons = GPIO_REG_READ(GPIO_IN_REG);

      if ((prev_buttons & RE_MASK) == RE_STATE_0 && 
            (buttons & RE_MASK) == RE_STATE_1)
         xTaskNotify(gpio_task_h, KNOB_CW_BIT, eSetBits);
      if ((prev_buttons & RE_MASK) == RE_STATE_2 && 
            (buttons & RE_MASK) == RE_STATE_3)
         xTaskNotify(gpio_task_h, KNOB_CW_BIT, eSetBits);

      if ((prev_buttons & RE_MASK) == RE_STATE_1 && 
            (buttons & RE_MASK) == RE_STATE_0)
         xTaskNotify(gpio_task_h, KNOB_CCW_BIT, eSetBits);
      if ((prev_buttons & RE_MASK) == RE_STATE_3 && 
            (buttons & RE_MASK) == RE_STATE_2)
         xTaskNotify(gpio_task_h, KNOB_CCW_BIT, eSetBits);

      if (prev_buttons & (1ULL<<KNOB_BUTTON_PIN) && 
            ~buttons & (1ULL<<KNOB_BUTTON_PIN))
         xTaskNotify(gpio_task_h, KNOB_BUTTON_BIT, eSetBits);

   prev_buttons = buttons;
}

SPI Issues

As I've already understood from the start, this project would require something to be able to use multiple SPI devices on the same bus. However I wasn't sure how big of a deal it would be. Originally I might have thought that the SPI library has some thread safety built in, but apparently it doesn't. It actually took me quite a while to understand why the device was crashing when I tried to change the volume. Since the VS1053b chip has a separate control bus, the volume control was clashing with the upload of audio data and thus the device crashed (with a ~50% probability). So I technically had to solve this issue before I even connected the display.

As the warning in the link above states, there are two options for using multiple devices on the same bus. One would be to refactor everything nicely into one task. I didn't want to take this path, since I would like to have the display and audio decoder as separate as possible. The second option is to just use a mutex. This was a much nicer option, even if it might not be as efficient (see background here). (I might actually rethink this later on, especially if I want a high and consistent refresh rate for the display. After all, the common task can be in the main module and only call respective functions from the display & audio decoder modules.)

With this info the solution is simple, just create a mutex object in the main module and pass it to both the display and the audio decoder modules. There is still a possibility that for example the volume change will interrupt the update of the display, but it will take a tiny amount of time and it will not crash the device. Previously I wouldn't have known how to measure or debug these kind of things, but luckily I now have a logic analyzer and was able to capture these events. For your viewing pleasure, below is a screenshot from PulseView with decoders added for audio decoder data and display control & data.

SPI traffic of the audio decoder and the display captured with PulseView

The SPI frequency is set to 1 MHz in this test, because I have a rather cheap 24 MHz logic analyzer and it's not really usable beyond 2 MHz. Also with 1 MHz it was very easy to capture such a case as seen above. The audio data is sent in bursts of 32 bytes as already explained. For this reason the display update can interrupt audio upload, since there is no priority whatsoever. The display data on the other hand is sent one "page" at a time, which is 128 bytes. Additionally the control data for the LCD (as explained in the software part) can be seen in the picture, since the control and data are sent to one SPI device as seen by the ESP32 (unlike for VS1053b).

Let's do some math here just to measure what kind of performance can be expected from the device. Let's assume that the SPI frequency is 1 MHz and we have an 128 kbps audio stream. A single display update would require 128 * 64 bits of data to be transferred (without any overhead included). We also know the overhead for the control, which is 3 bytes per page so 3*8 bytes in total. With this info we can calculate the (theoretical) maximum refresh rate of the display. 

(1 000 000 - 128 * 1024) / (128 * 64 + 3 * 8 * 8) = ~100 Hz

This is some very good performance. Even with 320 kbps stream, the refresh rate would still be ~80 Hz with 1 MHz SPI frequency. We should however consider the unilization of SPI bus, since the ESP32 has also other tasks (even if DMA is used). If we consider 30 Hz refresh rate, the utilization would be (128 * 1024 + 128 * 64 + 3 * 8 * 8) / 1 000 000 = ~14 %. That doesn't really seem much, but we're not taking overhead into account (apart from LCD control). Nor are we taking into account the overhead that comes from the 32 byte bursts for the VS1053b chip. Below is a zoomed out picture of ~1 second timeframe with a 320 kbps stream playing and 3 Hz display refresh rate.

Zoomed out SPI traffic with 320 kbps stream and 3 Hz display refresh rate

From the picture it kind of looks that the utilization of the SPI bus is around 50% and it looks like it would be ~100% with the 30 Hz display refresh rate. This is however not a very good analysis since it might look bad simply because of the fancy graphics. Regardless of that we have the option of increasing SPI frequency to significantly reduce bus utilization. The maximum frequency for LCD is 10 MHz and for the audio decoder also 10 MHz, although there is a catch regarding the configuration that I will explain later. With 10x speed increase the device shouldn't have any issues at all. This math is necessary especially if we want to add a spectrum analyzer to the device. And we definitely want to do that later. :)

Result

With the hardware and software shown here, I was able to control the radio and make it display useful information. The first thing to do was obviously volume control and after that followed the menu to control all settings of the VS1053b chip and also change the radio station. The display looks very cool with its blue backlight but it was also quite useful while displaying the buffer usage and the signal strength in real time. The only issue is that the edges of the display look annoyingly bright without any case, as compared to a standard eBay 128x64 LCD with a metal bezel. At least I can say that the picture quality is significantly better.

Also here are the links for display adapter and button adapter EasyEDA projects. It seems that EasyEDA is using some new OSHW Lab platform for sharing projects and I have to say that it looks unfinished. However the EasyEDA project sharing was not that much better so who cares.

Final words

Apart from a few slowdowns, this project is quite on track. I might even be able to finish it before my personal deadline, which is in September.

It's worth noting that while the SMD version of the rotary encoder is quite nice, it's also 2+ times more expensive than the through hole variant at least at LCSC. I wonder why it's like that.

Sunday, May 2, 2021

ESP32-Radio (Part 1.5)

Introduction

It's been quite a long time since my last post and that is because I felt unreasonably tired lately, which might have been related to my work. In any case I've had a lot of progress with this project even if I was stuck with very basic, vary fundamental and very annoying issues for some time. Most of these issues are related to the basic things discussed in part one, so I'm writing this part "one and a half" before part two. I've also learned quite a few things related to the FreeRTOS that is included in the ESP-IDF. So let's discuss these few little things.

Buffer overflow

As discussed in part 1, the radio server seems to send data at a much faster rate than it's supposed to right after the connection has been established. I suspect this is done so that the client can fill the audio buffer quickly and start playing the audio. Without any check the audio buffer in ESP32 would fill up quickly and overflow while also making annoying noises. I fixed this issue by making the code loop until there is some free space in the buffer. This works "nicely" since the ESP32 has two cores and the other core can run the task that is responsible for sending the audio data to the decoder chip (among others).

At this point I think that is the worst way to solve this issue. The problem is that the code is simply stuck without the possibility to run any other code on that core. The correct way would be to use FreeRTOS Stream Buffers which would allow task switching in case the buffer is full. Using this would however require quite a lot of rewriting, which I didn't (yet) want to do. Eventually I realized that simply using vTaskDelay (in the loop) with a minimum time, would basically do the same thing. The code would check whether the buffer is full and issue this wait statement if it is. This in turn would allow other tasks to run on this same core instead of just running NOPs. Before this change the device definitely had some hiccups from time to time, but this change significantly improved performance and the issues are now gone.

VS1053b MIDI

As discussed in the first part, the VS1053b board from eBay has a design flaw, where the IOs are not pulled down as they are supposed to (as shown in the datasheet). This is related to the fact that this board is possibly the shittiest design that I have ever acquired from eBay. As explained here, "most modules will start in MIDI mode". There is also a link that points here. I originally failed to understand the fact that after making these "few register writes", it is necessary to make a soft reset so that the chip can read the state of the pins at boot.

Interestingly the device worked quite fine with my faulty code for a long while but then it simply stopped working. The funny part here is that when the device boots into MIDI mode, the AUDATA is configured to 44101 (which is okay for most servers and means 44100 Hz sampling rate and stereo mode) and the DREQ pin is high, which means that the device is constantly draining data and not playing anything.. However the latter link has a picture of pins that have to be physically connected (circled in green in the picture below) for the device to boot properly without any software fixes. I should have used the physical fix right from the start, but instead I wasted a lot of time making workarounds to detect the "stuck device" and rebooting it. That was a huge waste of time, cannot recommend. However here is the link to the original explanation right from the manufacturers of the chip.

VS1053b Board Buzzing

As I've explained in the first part, the board that I have, was making a very annoying and rather loud buzzing sound. The sound was coming from the board itself and couldn't be heard in the audio output. I spent quite some time poking around the VS1053b board without any luck. I also tried measuring voltages on the board, again without any luck. I tried to live with but after some time I was too annoyed again. It almost gave me headaches since it was so loud. Then I remembered how these things should be debugged: simply by taking a cap and poking it in different places. After some poking the buzzing suddenly stopped. After two months of ~constant buzzing it simply stopped (I had it connected to a USB port and thus it was on all the time my PC was on, just to see that it doesn't crash or disconnect).

And here's the explanation. The main output cap (C1) of the 1V8 regulator (U3, the core voltage) was located quite far away from the regulator. The regulator in question is AMS1117 and according to some datasheet it requires an insane 22 microfarad output capacitor. This capacitor should obviously be as close to the regulator as possible. Instead it's a few centimetres away and the connecting tracks are routed god knows where (shown in red and purple in the picture below). There is however a 100 nanofarad capacitor (C18) right next to the regulator, which, I suspect, was simply faulty in some way. After I've desoldered both 100n capacitors (3V3 and 1V8) and replaced them with 4.7 microfarad capacitors (C18 and C17 in the picture), the board has not made any sound (except for the audio output of course).


VS1053b evaluation board from eBay

I tried to understand this later on with the help of an oscilloscope without any luck. My final conclusion is that the original capacitor C18 in the picture above was faulty, since the board made no audible sound even without anything soldered in its place.

I've also acquired a second board to have just in case, and that board was even worse. It had the VS1003 chip instead of VS1053 and had a 2.5 V regulator instead of 1.8 V one. This part needs clarification. For the VS1053b the voltage ranges are 2.5 .. 3.6 V for AVDD, 1.7 .. 1.85 V for CVDD and 1.8 .. 3.6 V for IOVDD. So two regulators (3.3 V and 1.8 V) are enough for the VS1053b. For VS1003 however, the respective voltages are 2.6 .. 2.85 V for AVDD, 2.4 .. 2.85 for CVDD and CVDD-0.6 .. 3.6 V for IOVDD. Since the board looks the same as in the picture above, I assume that on this board AVDD is connected to 3.3 V. And the question is why would you do that? Absolute maximum ratings exist for a reason. Picture of the board is shown below.


VS1003 evaluation board from eBay

Perhaps less surprisingly this board didn't work correctly. It usually started okay and then the audio just faded out after some time of playing. I tried to debug it for a while and then simply gave up because the board is clearly not worth it.

Header issues

As I've explained in the first part, the server will send an HTTP header after the connection has been made. The new thing is that this header might vary a bit between different servers. This might be an issue, because I've only tried this with exactly two different servers and found three different issues. Here are the issues that I've encountered until now.

One server doesn't seem to send the sample rate of the audio at all. This is not an issue, since the sample rate can be read from the decoder chip and more specifically the AUDATA register. The value should be adjusted simply by (AUDATA >> 1) * 2 to get the actual value (to lose the stereo bit).

One server sends some garbage instead of the stream name. This is very annoying. However it can be easily solved by not using this value. The name of the radio station can simply be hard-coded with the URL address of the server in the device. I had an idea of fetching some channel list automagically for example from SHOUTcast API, but that's for another time.

One of the servers seems to use "content-type" as the tag while another uses "Content-Type". Solution for this is simply to use tolower function before trying to detect the tag.

Prototyping issues

This prototyping phase has lasted quite long for this project and the reason is that I want to try as much of the final implementation with it as possible. By as much as possible I mean the display and the buttons & rotary encoder. Since the pins of the ESP32 can be assigned quite freely, I can change the schematic based on the board design, to make the routing as simple as possible. The prototype can then be easily reconfigured to test the new connections before ordering the PCBs.

However there are a few issues related to the prototype. The worst issue is that because of the long wires, the device will easily pick up noise. I suppose it's not very dangerous, but suddenly hearing the audio at the maximum volume is certainly not fun. I've noticed this already at my desk and then I definitely noticed this when testing at the location where the end device is supposed to be used. Basically switching a noisy lamp or a power supply on or off will make the device output audio at full volume and also somehow garbled. Luckily the volume goes to the exact maximum, so it's very easy to detect as an error case.

I've concluded that this is not very good even for a prototype. So with my newly knowledge of FreeRTOS, I quickly made task that regularly checks whether the volume is at maximum and lowers it to some sensible level if it is. In retrospect, this fix had one of the best effect / effort ratios of all the things that I've made for this project.. Unfortunately the audio is still garbled so this fix doesn't solve that. I suppose it would require restarting the audio decoder. That would then require logic for finding the beginning of the next audio packet in the stream, or it would simply glitch for a while until the decoder gets the next header and recovers. In any case this should not be an issue with the final design and until then at least I don't need to hurt my ears.

Another issue related to the prototype is that some wires usually end up being near/above the WiFi antenna of the ESP32. This seems to be an issue because it introduces some noticeable noise in the audio output of the device. This is rather annoying because I can clearly hear it whenever there is a silent moment in the music. Interestingly there are no audio cables near the WiFi antenna, which means that the voltage regulation of the audio board is not very good. However it's enough to just insert some item between the antenna and the wires to get rid of this problem. For example a USB memory stick is very much enough. The signal strength will obviously be worse after this, but at least there is no noise in the audio whatsoever. This "fix" is okay for the prototype and should not be an issue with the final design. This is one good reason to follow the esp32 hardware design guidelines. :)

Final Words

I can only say that a lot of lessons were learned here. A simple RTFM can be applied here of course but it seems that FreeRTOS contains quite a few new things for me to learn. Also the issue with MIDI was just a very annoying mistake. The rest are simply adopting to the environment.

Internet of Crosstrainers, part 2

Introduction As mentioned in the original post here , there were some issues in the described implementation. I had some difficulties to fin...