7

How I made a Digital Carburettor Synchroniser

 3 years ago
source link: https://treatwell.engineering/how-i-made-a-digital-carburettor-synchroniser-9ece7fa571fb
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

How I made a Digital Carburettor Synchroniser

My first ever motorcycle was a Honda CX500. It was an awesome classic (1982) bike. I bought it from a very nice guy here in Vilnius and it was a great surprise when during one of cocoa.lt meet-ups hosted at our Treatwell office I discovered that he’s also an iOS developer 🤷🏻‍♂️.

Image for post
Image for post
My first bike — Honda CX500

The bike already was in a pretty decent shape. Still, I spent evenings of winter period in a garage refurbishing brake callipers and fixing things like minor oil leaks in cylinder head gasket. It was that winter when I realised that I enjoy restoring bikes even more than to ride them. Soon, after I polished pretty much everything I could there was a time to sell it and look for another project.

By the way, Simonas, if you’re reading this and wonder where is your bike now, I can tell that the beauty had a phenomenal journey. I sold her to a guy named Steven. He came to Lithuania with a plan to buy a classic bike here and ride it around Europe. In two months, he made 7000 miles on it. Visiting Poland, Ukraine, Romania, Bulgaria, Turkey, Greece, Albania, Italy, France and ended up selling it in UK before his flight back to California.

Image for post
Image for post
CX500 somewhere in Romania

In late 1974 Honda introduced (a show in Germany) one of the most iconic motorcycle in biking history. A liquid cooled flat 4-cylinder engine, shaft-drive. To lower the centre of mass as much as possible — a fuel tank was designed under the driver’s seat. Contra-rotating generator was added to smoothen your ride by compensating engine’s torque reaction — just to name a few innovations.

It was my bike of dreams — Gold Wing GL1000.

Honda is still producing Gold Wings today. They evolved to a giant luxury touring bikes with fairings, lots of bags, onboard stereo and so on, but the “naked” models were produced between 1975 and 1977. They’re becoming quite rare and very desirable and I was lucky enough to find and buy one on eBay (many many thanks to my wife for placing the last minute winning bid 👹)

Then, there were long months of waiting till I finally got my GL1000 shipped from Pennsylvania.

Image for post
Image for post
Me super proud at the day of delivery

For a bike that spent almost a month in a salty ship container environment on voyage through Atlantic Ocean and being more that 40 years old the bike came in amazingly good shape.

Still, I bought it in non-running condition, so here the story begins.

Carb rebuild

For most modern combustion engines (cars and bikes) fuel injection systems are used to provide required fuel/air mixture to the cylinders. To fully burn the fuel and leave no excess oxygen in the exhaust — a mixture of about 14.7 : 1 air to fuel ratio is required. Still, most engines tend to provide most power with a slightly richer (more fuel) mixtures of about 12 to 13 : 1.

Maintaining this ratio is not a trivial task at all.

Even an outside temperature has an impact, because air molecules gets more dense in lower temperatures, so more air fills a burning chamber in the engine. Or vice versa — in high altitudes atmosphere is thinner so less air can be sucked into a cylinder by a piston stroke. And so — amount of fuel must also be adjusted.

In advanced fuel injection systems a computer is controlling this ratio by constantly reading electronic sensors of atmospheric and engine temperature, engine load and rotation speed, ignition timing, air flow and amount of unburned oxygen gases in the exhaust pipe. And then computer sprays calculated amount of fuel required for the perfect mixture through a fuel injectors under high pressure.

(My old Mercedes Benz W123 has a very clever (transient) mechanical fuel injection system (K-Jetronic) but explaining that one would require yet another blog post)

In the days when fuel injection was not yet invented a required mixture for the engine was produced by a device called carburettor.

No electronic parts are required for carburettor to do its job. When the air flow is passing a varying diameter tube (aka Venturi tube) an air speed increases at the area of restriction. This increase in air speed causes a pressure drop in that area (this behaviour is explained by Bernoulli’s principle). The difference between atmospheric pressure in a bottom container with a fuel and the lower pressure in the air tube causes the fuel to go up through a tiny fuel passage and mix with the air.

Image for post
Image for post
A stop frame from YouTube video made by HowTechnologyWorks3D

To control engine’s speed — a sliding piston is placed to block or open Venturi bore. A slide also has a very precisely manufactured cone shaped needle at the bottom of it. The needle closes or opens passage for the fuel simultaneously with a slider — so the mixture is kept optimal at all positions.

Other static parameters like permeability rate of an air filter or the fuel level at the bottom fuel bowl also can have a huge impact on air/fuel ratio. If you think about it, it makes sense, because if for example a fuel level is low, it takes a little bit more time and energy to get the fuel to the top and the resulting mixture becomes non optimal (lean in such situation).

I own an original workshop manual for GL1000, and the specs says that fuel level at the bowl has to be exactly 21 mm. Yes, 20 and one millimetre.

GL1000 has quite complex carburetion system. Every cylinder (out of 4) has a dedicated carburettor. Each carb has primary, secondary and a circuit for idle operation. Moreover, all carbs are connected to a shared air chamber with a fuel distribution passages inside. As I learned from a fantastic blog — you can’t do a proper GL carb refurbishment if you don’t tear everything apart and carefully clean every single passage.

  • The blog was like a treasure to me since I found it. A ton of unbelievably valuable knowledge is collected there by an expert through the years. I really learned a lot. Like for example, 1975–1977 models have an idle circuit that is way too lean. That causes a bike to choke if accelerated quickly (infamous off-idle glitch). And to fix it — a Pilot Air Jets should be replaced with a smaller ones (to enrich a mixture a bit). Another problem is that Honda discontinued production of those in 1996. So what you do is take the original ones, solder the orifice and then use a micro-drill to make a correct size jet.
  • I also purchased a carb rebuilt kit from this guy — it’s not cheap, but totally worth every penny.

The process was not easy. I found horrible things inside. From nutshells in the air filter (I believe a squirrel had a temporary home there), rust, sand, melted gaskets and nasty gummy substance all over the carbs left by fuel additives they use in USA. But I took my time. I took pictures of my work, double-checked things and tried to really understand what I’m doing. I cleaned things with steel wool, put the parts in an ultrasonic bath and used tons carb cleaner and compressed air to clean every passage I found.

Image for post
Image for post
GL1000 carbs before a rebuild
Image for post
Image for post
Image for post
Image for post
GL1000 carbs taken apart

In about two weeks of my spare time in a garage I managed to bring everything back. And the bike just started right up with no fuel leaks or other issues. That was an awesome moment.

Image for post
Image for post
She’s alive!

The final step to complete a carb rebuild is carb synchronisation.

An accelerator handle on a bike is connected to a carburetion system with a throttle cable. When you turn a handle — a Venturi hole inside carburettor opens, so more air/fuel mixture gets into the engine and your friends can observe a rear tire burnout. When you have more than one carb — you have to make sure that the cable opens every carb evenly. Otherwise, explosions in the cylinders will push pistons with uneven force. And since pistons are all mounted on the same axle (crankshaft) — this causes a stress to the engine and the bike will lag. In extreme cases the engine will not start at all.

Not all motorcycles need this carb sync procedure. If your bike has only one carburettor — then obviously there’s nothing to synchronise (even if the bike has multiple cylinders). Also, many models (like Honda CB750 for example) have inline carb design with a shared axle that goes along all carburettors. So Venturis are already opening on all of them at once.

On GL1000, 4 carbs are connected with adjustable linkages. Carb #3 is the main one (directly connected to the throttle cable) and then you have to adjust screws on linkage to tune the remaining ones.

But how you can tell if the carbs are in sync or not?

Every motorcycle has little screws somewhere on the intake manifolds. You need to unscrew these bolts, place a vacuum gauges there — and in this way you can measure how much air is sucked into each cylinder when the engine is running.

The old school tool for this measuring purpose is as simple as a long hoses partly filled with some thick liquid (usually mercury). When connected, the vacuum from intake manifolds will suck a liquid up, so you can visually see a mark of how much vacuum force there’s in every cylinder (you have to be very careful with engine RPM, or you risk to suck the liquid into the engine 😬)

If you don’t want to mess with mercury — there’s also mercury free options or tools using regular vacuum/pressure gauges on the end of hoses.

Image for post
Image for post
Motion Pro Mercury Carburettor Tuner Syncronizer
Image for post
Image for post
Images from GL1000 workshop manual

Fortunately, I do not have any of those tools so I decided to make a version of my own 🤩.

It should be super simple, right? I need 4 vacuum sensors (Arduino kind of), then quickly connect them on a breadboard with some wires and read the values in Terminal.

Version v1.0

Super exited by the idea I started to search for vacuum sensors.

First of all, I needed to find out what amount of vacuum there’s actually created inside inlet manifolds when the engine is running (around 1000 RPM) and look for sensors in that particular range. Honda Shop Manual was not very helpful here. It says: “Check vacuum gauge readings. All cylinders should read within 50mm Hg (2 in Hg) of each other. If the difference between cylinder readings is greater than 50mm Hg, adjustment is required.” Well, no info about actual vacuum values.

So I started to look for videos on Youtube hoping to find someone actually measuring the vacuum during synchronisation process. I couldn’t find the video of GL1000 syncing, but I guess in general, vacuum on all bikes should be more or less in the same range.

From the nominals on vacuum gauges in the video, I can see, that on running bike the vacuum is around 25 cmHg. This translates to around 32 kilopascal (kPa). Ok, so 40 kPa is OK, a little bit more is even better.

Image for post
Image for post
A stop frame from an excellent The Motorcycle MD video on carb syncing

Secondly, I was worried about sampling rate. My additional goal was to be able to calculate an engine RPM using the sensor data. To know an accurate RPM is crucial for yet another carb tuning procedure — idle mixture adjustment. A little screw on each carb opens a fuel passage for idle circuit. The idle mixture tuning procedure is like this:

  • Get the engine running at around 950 RPM
  • Turn the screw on one carb half a turn (or even less)
  • If RPM increases — get the engine running at 950 RPM again
  • Repeat these steps for all carbs as many times as needed

Adjustments on idle screws on GL1000 are known to be very very subtle. Only professionals with great experience can hear the difference by the ear so, knowing the exact RPM is crucial here.

950 RPM is 15.83 Hz. The theory says that to get any frequency information you have to have at least twice sample rate. 50 samples per second should work fine.

So I ordered and received a bunch of sensors and analog to digital converters (ADC).

  • MPS20N0040D-D (40KPa, 2€ per unit)
  • XGZP101SB1 (100KPa, 1,64€ per unit)
  • I2C ADC1115 16 Bit ADC, 8 to 860 samples/sec, 4 channel (1,93€)
  • ADC1015 ADC 12 Bit, 3300 samples/sec, 4 channel (1,17€)
  • 3.3–5V Digital Barometric Pressure Sensor 0–40KPa (5 pcs pack for 19.37€)
Image for post
Image for post
I didn’t needed all of this stuff, but I ordered some more to have options to experiment.

I didn’t needed all of this stuff, but I ordered some more to have options to experiment.

I was particularly interested in the last option (in the list) here, because the device has an ADC already soldered on the board and the sensor is ready to be read via Serial port.

So I opened a serial Terminal (I’m using CoolTerm) and connected a device to my USB to Serial cable* to see if a device is transmitting any data. And yes, I definitely saw same data!

*A Serial cable like this. But there’s a ton of even cheaper alternatives. I’m using a PL2303HX (~1€) for years and it’s still working fine.

There was not much specs for this Digital Barometric Pressure Sensor in the description when I bought it. But from the image I can see that it’s using a HX710B chip. Let’s google!

OK, so what I found is:

  • the chip is 24-bit (cool, I like precision 🙂)
  • it’s used for weight scales application 🤔
  • produces serial output at 10 or 40 sample rate 👌

Here, I think I should try to briefly explain how these kind of sensors work.

Basically, the sensor consists of 4 a special kind of resistors called strain gauges. A main feature of a strain gauge is that it changes a resistance when it’s deformed.

Resistors are connected in way to form a Wheatstone bridge. If all 4 resistors have an equal resistance the voltage in the output will be zero. Any resistance imbalance in any of resistors will create current flow in the output that can then be detected. Wheatstone bridge allows to make an extremely accurate measurements so even a slight deformation of a strain gauge generates a noticeable effect.

In general a vacuum sensor have two separate plates inside with strain gauges on them. The bottom is always exposed to an atmospheric pressure (theres a miniature hole in the casing of the sensor for the atmospheric air to come in). The top plate is where you connect a hose and so the pressure difference between the plates can be detected.

(A sensor for a weight scales work in a very similar way — that’s why HX710B is described as for use for this application)

Image for post
Image for post

Next, I started to look for any open source project for HX710B. I could’t find anything for it, but found a Python library for HX711 with an example of how to use a lib for scales application.

That definitely worked (even if the chip model I have is not the same) — while the script is running, and me funnily blowing my breath to the attached hose, I observed an output printed in the console!

Next, I had to write a script on Raspberry Pi to read all 4 sensors at once and somehow present results on the screen on my Mac.

By the way, at that time I discovered that it is possible to connect to a Raspberry Pi using a serial port. That way you don’t need a router, there’s not need to setup LAN or WiFi network. Moreover, a RPi can also be powered using the same Serial cable! A little of config tweaking was needed but the procedure is perfectly explained here.

After a setup I could log into RPi using a command:

$ screen /dev/cu.usbserial-143130 115200

Since I already had a working example (in the lib) for reading the sensors, the bigger challenge was the UI part. How do you present a live refreshing data on a terminal?

Fortunately, there’s open source lib for this purpose too. I used blessings and progressive to draw a bar in terminal for each sensor, and present readings as a percentage of that bar. I need to note here that what I seek is the balance between carbs and the exact values of vacuum pressure are irrelevant for the syncing procedure.

The output in console looks like this:

Image for post
Image for post
The output of my Python script
Image for post
Image for post
The very first carb synchroniser prototype using RPi and hx711py lib

If you’re interested, you can find a source code of a script here. Wire connections are self explanatory in code.

Next, I needed to find adaptors that can fit threads on GL1000s intake manifolds so I can connect vacuum hoses to them. I called almost every bike shop here in Vilnius — no one could help me with those. The best suggestion I got from one shop was to buy a whole carb sync set cause they can’t sell adaptors separately. I ended up ordering a set of 4 brass adapters with sealing End Caps from Morgan Carbtune, UK.

GL1000 uses 5mm threads (same for any Honda).

Image for post
Image for post
Brass adapter fitted onto GL1000 intake manifold
Image for post
Image for post
First prototype in action

I was able to achieve a more or less satisfactory syncing results by using this first prototype, the bike was running pretty well, but I detected a few issues. During a carb rebuild I noticed that my carb set had 1 carb taken out from another GL1000 model. Carb #1 was a 771A model (other 3 — 764A). 771A is tkaen from a 1978 year model (emissions controlled), and not only the carb has a slightly modified idle circuit (to meet emissions requirements), it also has a 1 mm smaller Venture bore diameter. And that’s a bad news.

You can read more about how bad GL1000 carb mixing actually is on Randakk’s Blog.

And because of that I couldn’t get all 4 carbs perfectly synced — Carb #1 was always slightly out of sync in respect of all other 3 no matter how hard I tried to tune it.

Also, my python script was laggy (max 10 samples per second) I was not getting accurate readings and the UI was far from user friendly.

I tried to modify a source code for hx711py to make my script more responsive unsuccessfully and finally decided to write a proper C driver (theres nothing faster that C, right?) for the chip to get rid of Raspberry Pi and build a much nicer UI for Mac in Swift!

Version v2.0

To get rid of Raspberry Pi I needed some kind of USB chip that allows me read 4 digital channels. And a found a really nice and cheap one (~ 5€) that proved to work perfectly later — FT232H.

Image for post
Image for post

The chip is a general purpose USB to serial converter speaking most common serial protocols like SPI, I2C, JTAG and so on. But also, it supports bitbang mode with 8 digital GPIO pins to play with.

I’ve done a few small projects on RPi before, where you have to read a bit or two on GPIO pins to respond to things like a button touch, but implementing a full communication protocol from scratch was a new challenge for me.

To interact with the chip — a libFTDI is needed. It’s using libusb as a dependency.

At the time when I was working on this project I had to compile a library by my self. Now I see, that there’s a Homebrew formula for it.

$ brew install libftdi

OK, so I have a Xcode workspace with 2 projects: HX710B driver written in C, and a macOS app that includes this driver, logic and all UI stuff.

Homebrew installs packages under /usr/local

To make sure:

$ brew --prefix/usr/local

In order to access libFTDI functions in my HX710BDriver project I had to add /usr/local/include/libftdi1 to ‘Header Search Paths’ in Build Settings.

I also wanted the driver project to pre-link all the dependencies, so I don’t need to include anything else in my Swift app. To do that, you have to set ‘Perform Single-Object Prelink’ option to YES, and declare paths to libusb and libFTDI static libraries in ‘Prelink libraries’ field.

To expose things in my driver to Swift — a modulemap is needed.

Then, a path to this modulemap file had be added to the ‘Header Search Paths’ in macOS app Project’s Build Settings and thats it — driver’s code can be imported to any Swift file.

The last steps of the setup was to link a driver with macOS app by hitting a plus button in General -> Frameworks, Libraries and Embedded content and not to forget to turn on a USB checkbox in App Capabilities section.

Image for post
Image for post
Image for post
Image for post
Image for post
Image for post
Xcode will just crash if this option is not turned ON
Image for post
Image for post

The sensor itself has 4 pins. VCC and ground — that’s for powering it — and then 2 pins called SCK and OUT. SCK stands for “clock”.

Cool, so where do you start if you want to write a driver for a digital sensor? I never did nothing like this before, but I always wondered how is it possible to get something meaningful using only one wire with 1’s and 0’s at the end of it. You must have some sort of a protocol, a set of instructions of how and when you read a value in the data wire (there can only be 0 or 1 at the time) in order to construct a full number. In my case, a 24 bit number that represents amount of pressure or vacuum.

There was no mention of any protocol in the item description when I ordered these sensors. Like it should be obvious. Well — not for me.

So I started to look for a datasheet of HX710B chip on the internet.

At the time I could only find this one 😬

Image for post
Image for post

I literally spent months to “crack” the reading algorithm for the sensor.

But the real problem wasn’t a language that the datasheet is written in. The paper even contains a simple example in C, and I also found a very similar datasheet for HX711 in English (same thinking path as with Python lib), but I couldn’t get any meaningful data with my driver. Nothing. Something was just not right.

So, I decided to look deep under the hood. And there’s an instrument for that.

I ordered a USB 24MHz 8 Channel Logic Analyser. I got a cheap one (£4.87), professional ones starts at 359€, but it works fine and there’s an open source app for it — PulseView.

Image for post
Image for post

Logic Analyser reads a pin (data pin for example) millions times in a second (24 millions for the one I have) and draws a value (1 or 0) in a sound-editing software kind of track (my device can record 8 tracks) so you can then zoom into a “recording” and see what actually is happening on the digital board you’re examining. Very very cool stuff.

It didn’t take long and I finally got my code working!

Image for post
Image for post
A recording of HX710B reading protocol in PulseView

If you wonder what was wrong in my setup before I hooked up an analyser — I can’t tell you. Well, OK, I can. It turned out to be a wrong wiring on the breadboard 🤦‍♂️🤦‍♂️🤦‍♂️. Always always always double check if your key is at ON position, that your battery is not dead and you have fuel in the tank before taking your bike apart.

But thats totally fine. I’m happy that I had a chance to learn new stuff. Before that I didn’t knew that things like logic analysers even exists.

OK, so lets zoom in a little bit and check whats going on (with bits).

Image for post
Image for post
One cycle of HX710B value extraction in PulseView

What you see here is one full cycle of data reading. A track D0 is representing a state of data (OUT) pin in time. A track D1 — a clock pin. D2 and D3 are not connected to anything here, and so always shows zero.

So a protocol turned out to be like this:

  • set clock pin down (0)
  • start a loop monitoring the data pin
  • wait till data pin goes down (0) — that indicates that the data is ready to be read
  • then fire 24 pulses (ON and OFF) to the clock pin
  • read the data pin every time right after a pulse is fired — that way you’ll get a full number bit by bit
  • pulse a few more times after that (control pulses) to prepare a chip for another cycle. 1 pulse sets 10 Hz sample rate, 3 pulses 40 Hz (I don’t quite understand what’s the difference between 2 and 3)
  • after a first control pulse the data pin will go up (1). So then — just start a next cycle

And thats pretty much it. Thats how any digital data you can imagine can be transmitted over 2 wires. Amazing.

The number in the example cycle is 0000000001101000101011011 in binary. Or 53595 in decimal form. And this is a measurement of one vacuum sensor.

Image for post
Image for post
A way I chose to connect 4 sensors to a FT232H USB device

To collect separate bits into a one integer number (24 bit in my case) — bit operations need to be used.

When you read the status of the pins of USB FTDI chip (in bitbang mode) — you get 1 byte of information representing all the pins at once. Also, the tricky part is that bits are in inverted order.

Lets say you have read a value — 0xD0. In binary form, bit pattern for it is 11010000. And that means that 8th and 7th pins are ON, then the 6th is OFF, the 5th is ON and the first 4 pins are OFF.

To extract a state for the particular pin (let’s say the 6th) — a bit mask can be used.

So I defined an array of bit masks like this:

unsigned char mask_table[] = { 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 };

In binary form this array looks like this:

[00000001, 00000010, 00000100, 00001000, 00010000, 00100000, 01000000, 10000000]

And then, to extract 3rd bit for example, you use a required mask and apply & (AND) logic operation to your data byte. If the result number is not zero — you have 6th pin in ON state and OFF otherwise.

11010000&00100000— — — — —00000000

In 0xD0 byte, a 3rd bit (6th pin) is OFF

Once you can read a bit value for a required pin — a full 24 bit number can be then constructed using bit shifting.

int32_t reading = 0;unsigned char pulse[2] = { 0x0F, 0x00 };for (int i=0; i<24; i++) {   ftdi_write_data(&ftdiContext, pulse, 2);   ftdi_read_pins(&ftdiContext, &pinsStatus);   reading = (reading<<1) | ((pinsStatus & mask_table[7]) != 0x00);}
Image for post
Image for post

The last thing to note about a protocol, is that I needed to handle negative values. Output data in sensor is coded in 2’s complement. That means that the very first bit determines a sign. If it’s 1 — then the reading is a negative number. And in that case I needed to invert all the bits and add +1 to the result of inversion to deconstruct a negative number.

A negative number -53595 looks like this in bit pattern: 11111111111111110010111010100101

To invert all all the bits — you can apply XOR to your number with 0xFF (the 1’s only)

11111111111111110010111010100101^11111111111111111111111111111111——————————————————00000000000000001101000101011010

Thats 53594 in decimal form. So, add +1, and a minus sign. We have deconstructed -53595.

A full HX710B sensor driver written in C can be found here.

Cool, the driver is working and I can read all 4 sensors at once in 40 samples per second, 4x faster than before! But I only could observe results in Debug console. Time to build a UI for my Digital Carb Synchroniser in Swift!

Version v3.0

The very first thing I had to do in my macOS app is to get the data from the driver I just recently wrote in C.

A public interface in the driver’s header is very simple. It contains a definition of a struct with readings from sensors and one function. Function takes two parameters: a flag to stop the reading loop, and a callback to pass data when new readings are available.

So what you then expect to do in Swift is something like this:

Image for post
Image for post

But that code does not compile.

Compile error is:

“A C function pointer cannot be formed from a closure that captures context”
or
“Cannot convert value of type '(Readings) -> ()' to expected argument type '(@convention(c) (Readings) -> Void)?’
Image for post
Image for post

Well, of course there are options to get rid of this error.

What you can do, is for example, firstly change the C function definition in the header, so that it additionally accepts a pointer to your Swift object as a parameter. The function should also return this pointer back along with readings data in a callback.

Then, in Swift, wrap yourself into a void pointer (UnsafeRawPointer). Pass this pointer to a C function along with stopFlag. And then, in callback — unwrap pointer into a real self and you’re ready to call your instance methods. Gosh.

int startReading (int *stop, const void *pointerToCallee, void (*ptr)(const void *callee, struct Readings readings));
Image for post
Image for post

I decided to stay with a way simpler approach as suggested here. A solution is to use a NotificationCenter to broadcast the data I receive in a C closure, catch this notification in another method and then do any things I need with the data.

Once I got the data in Swift — the rest was way easier (I feel a lot more fluent in Swift than in Python or C).

I created 4 GaugeViews with rotating needles to represent amount of vacuum in each carb and went to garage to test the new app.

Image for post
Image for post

What I also wanted to improve in my setup — is to use vacuum hoses made from stronger material. The thing is, that the intake manifolds gets hot pretty quickly, then the hoses attached to them starts to melt and leak the air. I tried to support them with hose clamps at leaking areas — but that was a mess in general. So I switched to a strong 5mm rubber hoses (same type as are used for fuel lines).

And I used wood to make support for the sensors on breadboard — previously I lost connection sometimes due to vibration of the running engine.

Image for post
Image for post
Image for post
Image for post
Image for post
Image for post
Image for post
Image for post
Image for post
Image for post

I connected everything, launched the app to begin reading the sensors, started the engine and faced yet another BIG problem. I got value out of range errors for all sensors.

The soft hoses I used in first prototype were definitely leaking or were too flexible, so pressure range appeared to be in sensors limits. And now, with hard ones — I got minimum available pressure reading all the time.

OK, so those sensors were specified to handle 40kPa. The first thing that came into my mind was to replace sensor units on the HX710B chip, and luckily, I already had ordered 100kPa range sensors for spare parts.

Re-soldering was not easy. Those parts a small. Original sensors were marked as MPS-3117 A2 and I replaced them with ones that have a 1015 1811 2212 code at the bottom. Google finds nothing with this number, so I’m not sure if they have specs (100kPa) as promised in description.

Image for post
Image for post

So I re-soldered one unit and tested it with bike engine running. Unfortunately — same results. The needle in the GaugeView was a little bit less responsive (like, heavier) but I still got out of range values.

But, then I understood what I have to do to fix it!

If you think about it, what HX710B chip really does, is that it just converts voltage that occurs between the strain gauge plates to 24 bit value. If the voltage becomes too high — it simply does not fit the 24 bit number!

To drop the voltage you have to increase resistance. So the theory was that if I solder additional resistor on the chip — it will reduce voltage in the Wheatstone bridge and my problem will be fixed.

Ok, so where to put this additional resistor? Lets look in the datasheet again and try to “decompile” connections on the real board.

Image for post
Image for post

I decided to try to put a 500 Ω resistor on top of the capacitor C2 and look what will happen.

I went to an electronic components shop nearby and got some SMD resistors. I bought them in two sizes because I was not sure which will fit. And boy — those things are tiny.

Image for post
Image for post
Measuring resistance. These things are tiny.
Image for post
Image for post
Additional 0.5 kOhm SMD resistor added on top of C2 capacitor

Soldering the resistors was a nightmare, but I finally did it. And it was worth it. I needed to tweak some things in the code (like max value for GaugeView needle rotation), but finally — it works!

Cool, so here I almost achieved my goal of building my own digital carburettor synchroniser.

My last goal was to calculate engine RPM using the data from the sensors.

Version v4.0

Testing the app on site (I mean, in a garage) isn’t a best way to debug things. To be able to conveniently work on the app at home I logged some output of the sensors while a bike was running. One line in the output contains: time in milliseconds and readings for all 4 carbs.

What I do next, is read an output log file, parse time for every line, and then fire events with data in corresponding time intervals that way simulating a real device.

To support this in app I created a SensorReader protocol (with two functions start and stop) and made my both classes ReaderHX710B and SimulatedReader to conform to it. And there’s a NSButton in the UI to choose between those two implementations.

I have ~40 samples per second, and a required RPM for bike tuning is around 950 RPM. It translates to 15.83 Hz. Is it even possible to extract frequency information from data where you have ~2.5 samples per period? Well, as I mention before, a theory tells that to calculate frequency in a signal you need at least twice sample rate. So I fit the range, but should not expect to measure anything above 1200 RPM (20 Hz).

Lets visualise some data I had captured to see what we have there.

Image for post
Image for post
Sample data (1 second)

I put some data into a spreadsheet and a pattern in the above picture appeared. It is clearly some sinusoidal pattern!

The very first thing that comes into mind when you need to deal with frequencies in signal is Fast Fourier Transformation. Apple’s Accelerate.framework have functions to perform FFT’s, so I created a new Playground to tryout this example. My data in the test var signal is 40 samples. Maximum frequency is: samples / 2 = 20. Then, if we’re following Apple’s example, the values in signal array are split into complex number space, and then when you apply a “forward” conversion to a FFT setup (that is converting from time domain into a spacial (frequency) domain) you have another array of complex numbers as a result. Imaginary parts of complex numbers in this result array represents amplitude for each corresponding frequency.

I put a result array into a table, and this is how it looks like:

Image for post
Image for post
FFT results

Results were not what I expected to achieve. My signal is super simple. It contains only one sine wave. As you can see in the table above — the bar at 7 Hz dominates, but the are a lot more non zero bars across the 20 Hz range. This is a consequence of having a non integer period data. FFT transform assumes, that the data you provide have an integer number of periods. That is, the last sample in your data perfectly closes the last sinusoidal period. If it’s not the case — FFT ends up with a phenomenon known as spectral leakage. You can try to fix this by applying a technique called windowing. What you do, is multiply you input with some kind of windowing function and expect a reduced spectral leakage effect.

But I see a bigger problem in the table above. There’s a big big difference if my engine is running at 7 Hz or at 8 Hz. 420 and 480 RPM respectively. It’s a huge jump in case you’re trying to fine tune an engine — I need fractional precision analysis. I’m pretty sure that there are ways to increase precision when doing spacial analysis. There are ways to calculate frequency of a very large periods, even having an unevenly-sampled data (Lomb-Scarle Periodogram), but I decided not to dig into that and calculate RPM in another — a very simple way.

Let’s look into the data again.

Image for post
Image for post

There’s 1 second of data (exactly 40 samples) and we can see that there’s a pattern that takes about 5 samples for every cycle. 40 / 5 = 8 Hz. Well, that’s almost as accurate as my FFT attempt. And what I can do — is take a bigger window (let say 5 seconds with 200 samples) and calculate my frequency in even better precision. And my the algorithm for RPM measure is only a case of finding a number of peaks in a sensor data over a specified period of time.

To double check that I’m on a right track — I generated 40 samples of SIN() function in the same spreadsheet and tried to change a frequency parameter that will fit my data. It turned out that I have 8.5Hz frequency in my signal (or 510 RPM).

Image for post
Image for post
Yellow charts shows output of SIN() function at steps of 0.025 (1/40) second in 8.5 Hz

A function to detect a peak in my RPMCalculator value is super simple. A value is a peak if it’s greater that a previous one and also greater that a next one.

func calculateRPM(data: Int, timestamp: TimeInterval) {    before = value    value = after    after = data    samples = samples.suffix(windowCount)    if value > before && value > after {        samples.append(Sample(isPeak: true, timestamp: timestamp))    } else {        samples.append(Sample(isPeak: false, timestamp: timestamp))    }    takeRPMMeasure()}

A tachometer on a bike suggested that the engine is running ~1000 RPM while my calculations showed about 600. Well, these old mechanical tachometers are known not to be very accurate. But the difference is quite huge. Maybe I made a mistake somewhere. But again, I don’t need to know the exact RPM of the engine. I’m interested only if RPM changes in any direction when I’m adjusting tuning screws on carbs, so there’s nothing wrong if calculated RPM value I does not reflect RPM in reality.

I upgraded my UI by adding RPMMeterView and I can conclude here that my Digital Carb Synchroniser is finished and usable.

Image for post
Image for post

Version v5.0

Version v5.0 is yet in the future.

I need to build a proper casing for electronics, design a board to solder sensors on it properly, build a tight fit for the hoses.

I also noticed, that when I choose 26 pulses setting in the HX710B sensor driver — it gives me constant values (seems not to respond to vacuum changes). I think that in 26 pulse mode a sensor gives a value for the atmospheric pressure. I need to double check that, but if that’s the case — it’s great news. Because I can then use those atmospheric pressure readings to calibrate sensors at the start of synchronisation session (or even recalibrate them on the fly)!

Also, vacuum in the intake manifolds can reveal a ton of very important things about your engine way beyond simple sync. You can diagnose a leaky or sticky valve, blown head gasket, choked muffler or even ignition timing problems only by using vacuum gauge readings — so there’s plenty of room for improvements.

I’m also looking at things like this small board from DFRobot to get rid of wires and send data via Bluetooth and maybe have UI on an iPhone.

We’ll see, but this project already was an awesome journey for me and I hope it’s worth sharing.

Thank you for your time.

A source code for the project can be found here.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK