Stationary Bike Digital Read Out
Introduction
I have a pretty bare-bones stationary bike: it does not monitor cadence and the resistance is set by a leadscrew. The combination of these two make following cycling classes pretty difficult as I constantly find myself fumbling with the leadscrew to try to dial in my resistance and guessing on the cadence. A digital read out (DRO) would be great. There is space on the handles of the bike for this and I have an old Air Fuel Ratio gauge that I think would be perfect. Plus I need to justify that 3D printer I just bought.
A few thoughts on the project right off the bat:
The pedals / armature is magnetic, so I should be able to use a hall-effect sensor to identify when the pedal is passing
Since this is to be used on a stationary bike, it creates an opportunity to use the power generated by my legs to power the device. I think I will address this after everything else works
I’ve got an ESP32-WROOM DevKit sitting around, and I just discovered MicroPython was a thing, so this should be a fun learning experience
Reverse engineering an A/F Gauge
As I was waiting for my hall effect sensors to arrive, I figured I would dive right into trying to get this combination LED and 7 segment display gauge to work. This piece was taken out of an AEM Air Fuel Ratio Gauge that I had in an old car. It is a custom board (meaning i cannot just find a schematic) so I had to follow the traces by hand and document them in LTspice.
Upon initial inspection, there were no ICs on the board, just transistors and resistors; indicating it can be controlled by basic logic using the GPIO on the ESP32. After laying out the circuit in LTspice, it appeared pretty straight-forward, but brought challenges controlling it. The LEDS and the 7 segment displays are controlled by providing paths to ground, but the LEDS and 7 segment display segments are tied to each other and a common ground leg. Once the circuit was drawn up, I connected each pin to a GPIO pin on the ESP and started toggling states to make sure the display was working as expected. Since the LEDs and segments of the displays were tied together, I was having trouble figuring out how to display a number on the 7 segments without turning on unwanted LEDs. Well, AEM took advantage of how our vision works here by cycling through the 6 main transistors (three 7 segment displays and three banks of LEDS) while pulling the correct pins to ground. If this process is done fast enough, it looks like everything is on simultaneously. So to avoid turning on unwanted LEDs, while the 7 segment displays are being updated do not provide power to the LED banks.
With the theory of how to control this DRO out of the way, I moved onto the practice. Working in python, I wrote a function to turn on/off the LEDs based on a provided percent and another method to update the 7 segments based on a provided integer. Eventually, the percent will represent the resistance on the bicycle and the integer will represent the cadence (as determined by the hall affect sensor). After deploying the software to the ESP, I gave it run with great success - I am now able to control the LEDs and 7 segment display.
3.15.21 - Software Updates
The hall effect sensors arrived today and I am happy to report that only one sensor was harmed in the making of this post! I also discovered these are digital (the output is 0v or 3.3v) and not analog (output ranges from 0v to 3.3v). This is a little annoying since we cannot set the threshold value, but it should still be able to do the job.
After the sensor was wired and I could read the value on the ESP, it was time to determine how to calculate RPM and connect that the DRO’s 7 segment displays. The function responsible for reading the hall effect and calculating the RPM is shown below. It looks for where the hall effect sensor state goes from True to False (note, that False mean a magnetic field is near the sensor). When this positive-going-zero crossing occurs, the time is recorded in a fix-length buffer. The average difference between the time stamps is calculated, converted to RPMs while accounting for a gear ratio (if I elect to put the sensor on the bike’s flywheel instead), and rounded to the nearest multiple of 5.
The DRO 7 segment displays are updated based on a global variable, so to change the number to the calculated RPM, I just assign the output of the function to the global variable and voila! Now to figure out how to equip the bike with a resistance sensor.
3.18.21 - Cadence
Ooph, the above code was pretty wishful thinking. Initially my plan was to pickup rotational rate directly from the bike cranks, but I believe measuring this at the flywheel is a better solution because it allows for better packaging of the final product. However, making this change requires a re-write of the software. To explain why, we need some math.
The typical cadence on a stationary bike is between 60rpm-120rpm, but the internet says people can do 150rpm, so lets work with that. The gear ratio between the flywheel and the pedals is 4; for every crank of the pedals, the flywheel rotates 4 times. That means while pedaling at 150rpm, the flywheel is rotating at 600 rpm or 10 rotations a second. Represented as a period, that’s 100ms to do a full rotation. Let says the magnet I have takes up 10 degrees of arc. The time the magnet is within range of the hall effect sensor is 100ms * (10 / 360) = 2.77ms. That is not alot of time and it means the code would have to be highly optimized.
Following the recommendations outlined in Maximising MicroPython Speed, mainly writing GPIO registers directly with Viper Code, I was able to get the whole execution loop to execute in 4ms on average. That is pretty good, but not good enough. Then I remembered interrupt service routines (ISR) are a thing.
I switched from reading the hall effect sensor in a loop and calculating RPM from the data to using the hall effect signal as the trigger to an ISR that runs the RPM calculation code. This is MUCH faster because ESP pauses execution of other tasks until the hall effect data is processed - which is fine, because the other functions are not time sensitive.
Integrating Resistance Sensor
As it turns out, getting feedback on the resistance is a bit tricky. The resistance is applied to the flywheel by tilting a group of magnets using a 4 bar linkage. The angular change of the magnets is only about 15 degrees, so I need to design something to amplify the angular sweep then I can relate resistance to angle.
As far as sensors go, I ordered a cheep Throttle Position Sensor (TPS) which is just a rotary potentiometer. Integrating the TPS into the software was pretty trivial: setup a pin as Analog to Digital Conversion, read the value, scale it from 0 - 100, and feed it to the method that updates the LED bank.
The last step before I got into the mechanical design was to replace the 7 segment displays on the board because there were a couple of dead segments.
Well… uhh… it did not go well. I guess now is as good a time as any to learn how to design and manufacture PCBs.
3.20.21 - Board Design
KiCAD to the Rescue
KiCAD is quite a powerful software that makes the barrier to entry for designing PCBs really low. Quite frankly it is pretty amazing that I was able to go from no experience with to a complete board design in about 2 days. You can download the tool from their website: kicad.org. Digikey hosts a library of common parts on GitHub that proved very convenient.
I needed to select through-hole components so I could solder the board myself. The right way to do this would have been to analyze the circuit and select through-hole components that fit the range. Instead, I just found through-hole components that match the specifications of the surface-mount components on the original board. This is a bit risky because I am not positive I found the exact components the original board was using. Fortunately, the components are pretty cheep and I will test the circuit prior to placing the order for the PCB.
3.28.21 - Integration
Instrumentation
While waiting for the circuit components to arrive, I began working on how to outfit the bicycle with the sensors. The biggest challenge was how to integrate the TPS with the lead-screw that controls resistance. Since I did not have access to the CAD of the bike frame, making these fixtures took a few iterations. At one point, I even called upon CAD; Cardboard Assisted Design.
Once the sensors were mounted and wired, I confirmed their functionality by checking the output of the ESP and all was well.
Circuit Testing
Once all the electrical components showed up, I threw them together on a bread-board to ensure the functionality prior to sending the PCB to manufacturing. I am happy to say that this was mostly a challenge of routing wires and updating values in the software to ensure i was turning on the correct segments of the displays. Everything went together nicely with no issues.
The A/F Ratio gauge that I based this off had a photocell that controlled the brightness of the LEDs as a function of ambient lighting conditions. Since the lighting conditions of my garage are not likely to change much, I elected to replace that with a rheostat (AKA a variable resistor, AKA a potentiometer with 2 legs connected). It turns out that actually didn’t do much because this whole thing is powered on the output of the ESP32, so I axed that as well and used the pin for something else.
Another issue that arose was the power draw of the ESP32 + the DRO on boot. Turns out the DRO circuit takes enough power to put the ESP into a boot cycle. So I was able to use that pin to control the flow of power to the board with a BJT and a GPIO pin (similar to how the rest of the circuit works). This allows me to keep the power to the DRO off during boot and then toggle it on once everything start running.
Integration Testing
Finally, after about 2 weeks worth of hard work, it was time to test everything together. The effort LEDs worked as expected right off the bat; however, I still have to deal with that whole nonlinearity of lead-screw setting vs TPS value because those LEDs should be based on the actual effort. The 7 segment display was displaying zeros no matter what speed I pedaled. I tested this all on a bench prior to this point, so I knew the bug was software related. Turned out to be a global vs local variable issues which a quick ‘global’ identifier fixed right up. All-in-all, this part went pretty smooth.
4.9.21 - Working Prototype
Things are starting to come together! When the PCBs came in, i got them populated and tested without issue. I swapped around the GPIO pins to allow for cleaner wiring within the electronics box, but everything worked as expected. Once the board was populated, I was able to package everything into the electronics box, print a few iterations of the PCB housing, and integrate it onto the bike. Works like a charm.
At this point there are still two issues I need to address: hysteresis of the resistance sensor (the TPS) and I still have to account for the fact that the TPS value and resistance are not linearly related. There is a pretty bad hysteresis on the TPS; if I move forward 2 turns on the resistance knob and then backwards 2 turns, the LEDs indicate a different value. I think this is due to slop within the mechanisms and the sensor itself. I believe I can address that by putting in a spring to take up the slop within the mechanism. The other issue has to do with determining how many LEDs to light up. Right now, I know the maximum and minimum ADC value of the TPS, so I scale the readings from 0 to 100% based on those limits and turn on the appropriate number the LEDs. The issue here is that the TPS is converting a linear position into rotary motion through a pin-in-slot mechanism. The linear motion is related to the TPS position by a sine function. That means every 10% increase on the TPS does not necessarily mean a 10% increase in resistance. So I have to figure out how to map TPS value back to linear position, which involves taking a few measurements. There is another interesting issue here: I am assuming that turns of the resistance knob linearly affect the resistance, which probably is not the case. So an easy way to relate TPS value to effort is to measure the rotational velocity of a flywheel while subjected to a constant force and a specified resistance. Since the resistance mechanism is magnet based, as the resistance is varied for a constant force, the rotational velocity (in stead state) should vary. This allows me to correlate TPS position to rotational velocity, which should be linearly related to the resistance setting.
Okay, onto the added functionality that was not originally part of the project, but I am glad I did it: bluetooth. Since the ESP32 has Bluetooth low energy capability, I was able to have it broadcast as a cadence sensor that can be detected by my iPad so I can record the cadence in the Peloton app. It took a few days of me bashing my head against Python and the communication protocol. As it turns out, the communication scheme is fairly simple. Cadence sensors are Generic Attribute Protocol (GATT) devices. They have a simple table-like data structure that is well regulated by Bluetooth SIG (the organization that defines the communication schemes) and operate on a server-client relationship. So all I had to do was have the ESP32 broadcast that it was a Cycling Speed and Cadence (CSC) service and that makes it immediately recognizable by the Peloton app. The way the CSC service is defined, the ESP32 has to send a structured packet that contains information on the time of the last crank event (time of the last full rotation) and cumulative number of rotation that have accrued. After setting up the pairing functionality, and making the necessary information available within the software, I was able to have the ESP pair with my iPad and then broadcast the information needed for the Peloton app to calculate the cadence; which was the same value I calculate locally on the ESP and display on the DRO.
At this point, I am on that part of the pareto efficiency curve where I get minimal value out of each unit of time I invest.
Next Steps:
Re-print some of the DRO housing components to improve fit
Update LEDs to scale based off lead-screw position, not TPS value
If you are curious, the full software & CAD for this project can be found on Github at https://github.com/jkalish14/StationaryBikeDRO