Category Archives: Electronics

Fooled by a Blatant Error in the RC0802A LCD Datasheet

I needed a small and inexpensive 3.3 V alphanumeric LCD for a project and found the 2×8 character RC0802A-TIY-ESV sold by tme.eu. I ordered a few but had a very hard time getting anything to display on it. This blog post describes the solution and how I found it.

The LCD has ST7066U as the controller and this is compatible with the industry standard Hitachi HD44780, so ordinary libraries for this chip should work straight away, but I was unable to get any characters to show up. By hooking up the R/W line (which one often grounds to save a pin on the microcontroller, as reading from the LCD is rarely of interest) and writing my own bit-banged routines to gain full control, I was able to confirm that the data I wrote int o DDRAM could be read back. But still nothing showed up on the display. I checked the waveforms using a Saleae logic analyzer which has a built-in decoder for the LCD interface and it confirmed that the commands looked good, including the timing.

I was close to writing to TME to ask them if they had more information, but they did not seem to have any technical contact information, just email addresses for orders, export and general complaints, so that would probably have been a waste of time.

Hooking up an old 2×16 character LCD to the same interface displayed the characters properly, so this further confirmed that the code was fine. I also tried another of the RC0802A-TIY-ESV displays I had bought, but without success.

I noticed however that there was another name/number than RC0802A printed on the PCB, H0802A:

I googled this and found a datasheet on some shady site for an LCD module with this name. That turned out to be a 5 V model, and I started comparing two two datasheets. One thing I quickly found was that pin 3 that was listed as a no-connect in RC0802A datasheet was very much a pin that needed to be connected according to the H0802A datasheet.

LCDs like these normally require an LCD bias voltage to be applied to pin 3 to set the contrast, but the RC0802A-TIY-ESV datasheet was very clear that pin 3 in this case was a no connect:

Not so on the H0802A:

So, in some kind of desperation, I hooked up a 22 k potentiometer between +3.3 V and 0 V with the center pin connected to the supposedly “no connect” pin 3 of the RC0802A. To my relief, it was now possible to find a potentiometer setting that resulted in visible characters!

I measured the voltage on pin 3 with the pot set to give a good contrast and it turned out to be -1.3 V, a strange output voltage of a divider between +3.3 V and 0 V. To allow as low a supply voltage as 3.3 V, the LCD module has an internal charge pump (a version of the classic ‘7660) that creates -3.3 V and there is apparently a 4.7 k pulldown (R5) between -3.3 V and pin 3. Unfortunately, they have forgotten (?) to add the other part of the voltage divider to 0 V to create an LCD bias voltage that results in visible characters.

Anyway, I disconnected the +3.3 V terminal of the potentiometer, resulting in a rheostat between 0V and pin 3 and adjusted the pot to give a good contrast on the LCD. The resistance turned out to be about 1.2 k. After some inspection (and ohm-measurements) on the LCD, I found that it would be be relatively convenient to add a 1.2 k 0603 resistor between a pad of R6 and another of the (not mounted) C11:

With this patch, the LCD works fine, without me having to patch the PCB I had already designed where this LCD plugs in. Placing the patch resistor between pins 1 and 3 on the connector on either the LCD or on the main PCB where it plugs in is of course also an option.

This is what the display looks like with the 1.2 k resistor in place:

Very strange that the Raystar RC0802A-TIY-ESV datasheet explicitly says that pin 3 is a no-connect, whereas it is crucial that it actually gets properly connected to produce visible characters.

Fast Algorithm for Rational Approximation of Floating Point Numbers

When doing frequency synthesis with fractional-N PLLs, one often needs to find a rational approximation of a floating point number with the constraint that the numerator must not be larger than a certain number. The more exact the approximation is, the closer the actual frequency will be to the desired one.

The integer part is obviously easy, but the fractional part requires a more sophisticated algorithm. One such algorithm is based on Farey sequences and the formula for finding the next fraction between two Farey neighbors.

The Farey sequence of order N consists of all completely reduced fractions between 0 and 1. So e.g. the Farey sequence of order 3 consists of {0/1, 1/3, 1/2, 2/3, 1/1}. A sequence of a higher order contains all the terms of all lower orders and then some more. So, with a fractional-N PLL where the denominator is limited to some value D, the possible fractional parts of the N in the PLL is precisely the fractions present in the Farey sequency of order D. And our goal is to find the best one to approximate the fractional part of the desired N, i.e. a number between 0 and 1.

There is a neat and useful formula for finding the next fraction that will appear between two Farey neighbors as the order of the sequence is increased. If a/b < c/d are neighbors in some Farey sequence, the next term to appear between them is the mediant (a+c)/(b+d). So an algorithm to find better and better rational approximations to a number x is to

  1. Start with the Farey sequence of order 1, i.e. {0/1, 1/1}, where a/b = 0/1 and c/d = 1/1 are neighbors.
  2. Calculate the mediant (a+c)/(b+d) = (0+1)/(1+1) = 1/2.
  3. Figure out which of a/b and c/d are further from x than the mediant and replace it with the mediant.
  4. Go to 2 if the denominator is not yet larger than what is allowed.
  5. Use the best result previous to the denominator becoming too large.

This algorithm is described on this archived web page.

This seems fine, until one considers some special cases. Say e.g. the target number is 0.000 001 and the maximum allowed denominator is 2 000 000. The algorithm would narrow down the top end of the interval from 1/1 to 1/2 to 1/3 to 1/4… in each successive iteration. It would thus take a million steps before the perfect approximation of 1/1 000 000 is reached. This is hardly efficient in this case, even though it converges quickly in many (most) other cases.

A way to speed up these degenerate cases is to not just take a single step in each iteration, but instead figure out how many times in a row either a/b or c/d will be discarded in favor of the mediant. It turns out that this is not too hard to do.

If e.g. a/b is to be discarded K times in a row, the resulting number will be (a + K*c)/(b + K*d). So how many times K will a/b be discarded until it is time to narrow down the interval from the other end? Set (a + K*c)/(b + K*d) = x and solve for K, which gives K = (x*b – a)/(c – x*d). K is often not an integer, so then select the biggest integer smaller than K.

If it is instead c/d that is to be discarded, the formula for K is (c – x*d)/(x*b – a).

With this improvement, it takes just one step to get to 1/1 000 000 instead of a million. Quite an improvement.

But there is still a minor numerical issue. When a decimal number can be perfectly expressed as a fraction, a calculation for K might end up very close to, but still less than 1, even though the perfect answer would be 1 exactly. This means that the floor function will return 0 instead of 1 and we might get stuck in an infinite loop where zero is added to the numerator and denominator, so nothing changes. To resolve this, a very tiny positive value should be added before calling the floor() function. There are 53 bits in the mantissa of doubles, so a number slightly larger than one would have an LSB representing 2-52 ≈ 2.2 *10-16. There may be further rounding errors in the calculations, so the tiny value should be well above this level to ensure K does not erroneously end up just below 1. I discovered this problem when trying to approximate 0.288 = 36/125.

I wrote some C-code to implement this improved algorithm. Here it is:

// Farey sequence-based rational approximation of numbers.
// Per Magnusson, 2024
// MIT licence, http://www.opensource.org/licenses/mit-license.php

#include <cstdint>
#include <cmath>

typedef struct {
  uint32_t numerator;
  uint32_t denominator;
} rational_t;


// Find the best rational approximation to a number between 0 and 1.
//
// target - a number between 0 and 1 (inclusive)
// maxdenom - the maximum allowed denominator
//
// The algorithm is based on Farey sequences/fractions. See
// https://web.archive.org/web/20181119092100/https://nrich.maths.org/6596
// a, b, c, d notation from
// https://en.wikipedia.org/wiki/Farey_sequence is used here (not
// from the above reference). I.e. narrow the interval between a/b
// and c/d by splitting it using the mediant (a+c)/(b+d) until we are 
// close enough with either endpoint, or we have a denominator that is
// bigger than what is allowed.
// Start with the interval 0 to 1 (i.e. 0/1 to 1/1).
// A simple implementation of just calculating the mediant (a+c)/(b+d) and
// iterating with the mediant replacing the worst value of a/b and c/d is very
// inefficient in cases where the target is close to a rational number
// with a small denominator, like e.g. when approximating 10^-6.
// The straightforward algorithm would need about 10^6 iterations as it 
// would try all of 1/1, 1/2, 1/3, 1/4, 1/5 etc. To resolve this slow
// convergence, at each step, it is calculated how many times the 
// interval will need to be narrowed from the same side and all those 
// steps are taken at once.
rational_t rational_approximation(double target, uint32_t maxdenom)
{
  rational_t retval;
  double mediant;  // float does not have enough resolution 
                      // to deal with single-digit differences 
                      // between numbers above 10^8.
  double N, Ndenom, Ndenom_min;
  double epsilon = 1e-10; // To handle rounding issues in conjunction with floor
  uint32_t a = 0, b = 1, c = 1, d = 1, ac, bd, Nint;
  const int maxIter = 1000;

  if(target > 1) {
    // Invalid
    retval.numerator = 1;
    retval.denominator = 1;
    return retval;
  }
  if(target < 0) {
    // Invalid
    retval.numerator = 0;
    retval.denominator = 1;
    return retval;
  }
  if(maxdenom < 1) {
    maxdenom = 1;
  }

  mediant = 0;
  Ndenom_min = 1/((double) 10*maxdenom);
  int ii = 0;
  // Farey approximation loop
  while(1) {
    ac = a+c;
    bd = b+d;
    if(bd > maxdenom || ii > maxIter) {
      // The denominator has become too big, or too many iterations.  
    	// Select the best of a/b and c/d.
      if(target - a/(double)b < c/(double)d - target) {
        ac = a;
        bd = b;
      } else {
        ac = c;
        bd = d;
      }
      break;
    }
    mediant = ac/(double)bd;
    if(target < mediant) {
      // Discard c/d as the mediant is closer to the target.
      // How many times in a row should we do that?
      // N = (c - target*d)/(target*b - a), but need to check for division by zero
      Ndenom = target * (double)b - (double)a;
      if(Ndenom < Ndenom_min) {
        // Division by zero, or close to it!
        // This means that a/b is a very good approximation
        // as we would need to update the c/d side a 
        // very large number of times to get closer.
        // Use a/b and exit the loop.
        ac = a;
        bd = b;
        break;
      }
      N = (c - target * (double)d)/Ndenom;
      Nint = floor(N + epsilon);
      // Check if the denominator will become too large
      if(d + Nint*b > maxdenom) {
        // Limit N, as the denominator would otherwise become too large
        N = (maxdenom - d)/(double)b;
        Nint = floor(N);
      }
      // Fast forward to a good c/d.
      c = c + Nint*a;
      d = d + Nint*b;

    } else {

      // Discard a/b as the mediant is closer to the target.
      // How many times in a row should we do that?
      // N = (target*b - a)/(c - target*d), but need to check for division by zero
      Ndenom = (double)c - target * (double)d;
      if(Ndenom < Ndenom_min) {
        // Division by zero, or close to it!
        // This means that c/d is a very good approximation 
        // as we would need to update the a/b side a 
        // very large number of times to get closer.
        // Use c/d and exit the loop.
        ac = c;
        bd = d;
        break;
      }
      N = (target * (double)b - a)/Ndenom;
      Nint = floor(N + epsilon);
      // Check if the denominator will become too large
      if(b + Nint*d > maxdenom) {
        // Limit N, as the denominator would otherwise become too large
        N = (maxdenom - b)/(double)d;
        Nint = floor(N);
      }
      // Fast forward to a good a/b.
      a = a + Nint*c;
      b = b + Nint*d;
    }
    ii++;
  }

  retval.numerator = ac;
  retval.denominator = bd;
  return retval;
}

To test the algorithm on an Arduino (actually a Pi Pico), I wrote the following code:

#include <arduino.h>

typedef struct {
  double target;
  uint32_t maxdenom;
  uint32_t expected_numerator;
  uint32_t expected_denominator;
} rational_test_case_t;

void test_rational_approx()
{
  rational_t result;

  rational_test_case_t test[] = { 
    {0, 3000, 0, 1},
    {1, 3000, 1, 1},
    {0.5, 3000, 1, 2},
    {0.5+1/3001.0, 3000, 751, 1501},
    {1/3001.0, 2500, 1, 2500},
    {1/3001.0, 1500, 0, 1},
    {1/3001.0, 3001, 1, 3001},
    {0.472757439, 1816, 564, 1193},
    {0.472757439, 1817, 859, 1817},
    {0.288, 100000000, 36, 125},
  };
  uint32_t n_tests = sizeof(test)/sizeof(test[0]);

  for(uint32_t ii = 0; ii < n_tests; ii++) {
    result = rational_approximation(test[ii].target, test[ii].maxdenom);
    Serial.print("target = ");
    Serial.print(test[ii].target, 7);
    Serial.print(", maxdenom = ");
    Serial.print(test[ii].maxdenom);
    Serial.print(", approx = ");
    Serial.print(result.numerator);
    Serial.print("/");
    Serial.print(result.denominator);
    if(result.numerator == test[ii].expected_numerator && result.denominator == test[ii].expected_denominator) {
      Serial.println(" OK");
    } else {
      Serial.print(" Expected: ");
      Serial.print(test[ii].expected_numerator);
      Serial.print("/");
      Serial.println(test[ii].expected_denominator);
    }
  }
}

The printout from the code is:

target = 0.0000000, maxdenom = 3000, approx = 0/1 OK
target = 1.0000000, maxdenom = 3000, approx = 1/1 OK
target = 0.5000000, maxdenom = 3000, approx = 1/2 OK
target = 0.5003332, maxdenom = 3000, approx = 751/1501 OK
target = 0.0003332, maxdenom = 2500, approx = 1/2500 OK
target = 0.0003332, maxdenom = 1500, approx = 0/1 OK
target = 0.0003332, maxdenom = 3001, approx = 1/3001 OK
target = 0.4727574, maxdenom = 1816, approx = 564/1193 OK
target = 0.4727574, maxdenom = 1817, approx = 859/1817 OK
target = 0.2880000, maxdenom = 100000000, approx = 36/125 OK

I also implemented the algorithm in an Excel sheet:

The excel document is available here:

Reverse-Engineering the OK2BWN Compact 3,5F ARDF Receiver

I maintain a set of simple ARDF (amateur radio direction finding, or “fox hunting”) receivers of the type Compact 3,5F from OK2BWN for use by beginners. A problem with some of the receivers is that the gain cannot be set low enough, which means that the audio becomes unbearably loud close to a transmitter. I could not find any schematics online, so I decided to reverse-engineer the receiver so that I could figure out what to change to allow lower gain.

The OK2BWN Compact 3,5F Receiver
The inside of the receiver

The receiver is built on a single-layer board using some through-hole components on the top side and surface mount components on the bottom. The discrete SMD components are large (1206 size), so the resistors have their values printed on them. This all makes the tracing of the circuitry relatively simple. I photographed both sides, flipped the photo of the top side and overlayed them. Then it was “just” a matter of sketching out the circuitry, a process that involved a couple of false starts before it all converged to something that looks like a direct conversion receiver.

Even before starting, I had a good idea of the general architecture as the receiver contained a single SA612 mixer with an analog VCO and an LM386 audio amplifier, but no crystal or ceramic IF filters (so it was not a superheterodyne). Since there was only one mixer it also could not be an image reject receiver, hence receiving both the desired and undesired sideband, which is also evident when tuning the receiver as a CW signal appears for two different tuning settings close to each other. Another thing that was obvious from using the receiver and looking at the parts inside, was that the signal from the E-field antenna was combined with the signal from the H-field antenna by a few turns on the ferrite core when the “antenna selector” switch was pushed to power the E-field amplifier located on a separate board.

Below are the photos of the top and the bottom of the board, as well as a combined photo where the flipped top side components are shown somewhat transparently over the bottom side photo.

The bottom side of the PCB
The top side mirrored to help with reverse engineering
The top side transparently overlayed on the bottom side.

Below is the schematics I came up with. As far as I know, it is correct, but I give no guarantees.

Reverse-engineered schematics

An annotated photo of the bottom of the board is shown below.

Annotated layout

I have not bothered to desolder and measure unmarked components like capacitors, so apart from the electrolytics, I do not know their values. I wrote 100 nF on capacitors I think only serve the function of DC-blocks, supply decoupling or low-pass filtering of the varactor bias, but this is just a guess. They could have some other value, although that is unlikely to be critical. The capacitors in series with the varactors might theoretically have some low value to adjust/reduce the tuning range, so there the 100 nF guess could be more wrong.

There are a few mystery components in the circuit. The four-pin through-hole part (Q1A, Q1B in the schematic) near the antenna is unmarked, except for a white stripe. Based on its place in the circuit and the components around it, I have guessed it is a double JFET in cascode configuration. While there are indeed dual JFETs available today, I could not find one with this package. My guess is that it is an obsolete part. Also, the varactors are unmarked, but the conclusion they are varactors is more solid than the guess regarding the dual JFET.

A JFET cascode amplifier is a good choice as a low-noise-amplifier for a ferrite antenna as it has high input impedance and low noise. Here it has a tuned drain circuit which increases the gain at the cost of some added complexity (and risk of oscillation?). The Q is limited by the 1.5 kohm input impedance of the SA612.

I suppose having a tuned RF amplifier was necessary to get enough gain for weak signals. The other gain elements are the mixer (nominally 14 dB) and the LM386. The RF amplifier is the only adjustable gain element in the receiver, which is perhaps an unusual choice. The input gate is DC-biased to 0 V via the ferrite antenna while the source is connected to a decoupled (C35) resistive network around the gain potentiometer, which allows changing the operating point of the JFET to select a suitably steep part of the ID-VGS curve to get the desired gain.

Adjusting the minimum gain

Now that we have the schematics, what do we need to do to allow more attenuation/less gain? The obvious place to look is near the existing gain control circuit. When potentiometer R4 is at its maximum counter-clockwise (CCW) position, the gate-source of JFET Q1A is maximally back biased to operate at the shallowest ID-VGS point and thus lowest gain. But we need it to be lower still.

VGS(off), i.e. the (negative) voltage from gate to source of a JFET required to turn the transistor fully off is not a well-controlled parameter. It can easily vary by a factor larger than two (sometimes larger than five) between devices with the same part number. As discussed the way gain is adjusted in this receiver is largely by adjusting VGS, so it is no surprise that different receivers will have different minimum gains.

To reduce the minimum gain for a receiver, it is fortunately quite easy to make a modification that results in a more negative VGS when the gain pot is maximally turned CCW. Just reduce R1 that together with the gain pot R4 (and R3 and R6) forms a voltage divider from +5V. A lower R1 value means the source will be biased to a higher voltage when the gain pot is at max CCW, but it will have very little effect when the pot is at the opposite end.

I found that parallelling the 33 kΩ R1 with 100 kΩ to get 24.8 kΩ resulted in a decent minimum gain for most receivers I modified. One receiver that was not quite as bad received 130 kΩ across R1 (26.3 kΩ total).

Below is a photo of a board with a blue 100 kΩ resistor soldered on top of the 33 kΩ R1 that was there originally.

R1 has received 100 kΩ on top.

Further comments and observations

The antenna is wound using two parallel and mirrored windings. This is supposed to eliminate any possibility of direction error. A direction-finding ferrite antenna should have a null exactly when the magnetic field-lines enter perpendicular to the core, but if there is a single winding on the core with some distance between the start and end of winding, there will be one effective turn over a part of the broadside ferrite core, leading to a slight offset in the null. How big a problem this is in practice is a bit unclear, but it generally is considered good practice to use two windings in parallel, wound symmetrically in opposite directions to cancel this parasitic broadside turn.

There are three varactor-tuned circuits in the receiver that need to be simultaneously tuned to the same frequency as set by the tuning knob: The antenna, the low-noise amplifier and the local oscillator. Getting all three circuits to agree on the frequency for all settings of the tuning dial might have been a challenge when designing the receiver. There is a trimmer capacitor across the antenna that can be adjusted to ensure the antenna agrees with the LO for at least one frequency and the two inductors are also trimmable to allow alignment on at least one (mid-band) frequency. And maybe it is not a big problem if the stages are slightly out of tune near the band edges.

The shielding of the receiver is a bit relaxed. There is a copper clad board acting as shields on each side of the circuit board and ferrite antenna. The shield at the front panel is only connected to the electronics via a thin wire and the shield in the bottom of the box does not seem to have any secure connection at all to the rest of the circuitry. This is not good shielding practice, but perhaps it is good enough at 3.5 MHz. The PCB seems to have provisions along the edges for better shield connections, but maybe this turned out to be unnecessary.

There are shield walls above and below the ferrite antenna, solidly soldered to the copper clad board. Having a good electric shield around the ferrite antenna is crucial for getting distinct nulls in the antenna pattern.

The way the front-back ambiguity of the figure-eight pattern of the ferrite is resolved in this kind of receiver is a bit shaky. When pressing the “antenna selector” button, the E-field antenna signal is injected into the ferrite core and picked up by the ordinary receiver chain. If it is in phase with the signal from the H-field antenna, the total signal gets louder, but if there is close to a 180 degree phase difference, the signal gets weaker, at least if the signals have close to the same amplitude. So by comparing the signal strength with one broadside of the ferrite towards the transmitter with the signal strength with the receiver rotated 180 degrees (which gives a 180 degree phase shift/inversion of the H-field signal, but does not affect the E-field signal), one can figure out if the transmitter is ahead or behind. The big problem however is that the signal strength from the E-field antenna is highly dependent on e.g. the height above ground, so it may not have close to the ideal amplitude and thus not be close to cancelling out the H-field signal when they have opposing phase. And particularly in close vicinity of the transmitter (in the near-field), the two parts of the field may have an unexpected phase and amplitude relationship. This may make it hard to distinguish which orientation of the receiver produces the strongest signal. Trying this at different heights, from knee level to above head level, to get different E-field strengths is often necessary. It would be better if the phase could be compared more directly, but this would add significantly to the complexity of the receiver, which is not an option for a low-cost receiver like this.

The LC-oscillator built with the (now obsolete) SA612 mixer is a configuration I have not seen before. It is not one of the topologies suggested in the data sheet.

A somewhat dubious component choice is the 10-V rated electrolytic C26 connected across the 9-V battery. This is a little too little voltage margin for my taste. A cap with a rating of at least 16 V should be used in that position in my opinion.

There is a through-hole 78L05 5-V regulator on the board, powering parts of the circuitry that benefits from a stable (not dependent on battery discharge level) voltage and also circuits that are not 9-V-tolerant.