All posts by Per Magnusson

Hörlursadatper för pejlmottagare

Många rävsaxar/pejlmottagare för radiopejlorientering är konstruerade så att de slås på när man kopplar in hörlurskontakten. Denna hörlurskontakt är åtminstone på mottagare från OK2BWN av typen 5-polig DIN, medan normala konsumentlurar av olika slag vanligtvis har en 3,5-mm TRS- (tip-ring-sleeve, utan mikrofon) eller TRRS-kontakt (tip-ring-ring-sleeve, med mikrofon). För att få de två att passa ihop kan man försöka byta kontakt på hörlurssladden, men det kan vara rätt besvärligt med tanke på de väldigt tunna trådarna den innehåller. En annan lösning är att bygga en liten adapterkabel med en 5-polig DIN-hane i ena änden och en 3,5-mm hona i andra. Det här är också pilligt, inte minst med tanke på att stift behöver byglas, så vissa trådar ska alltså ansluta till mer än en pinne på kontaktdonen.

En bättre metod kan vara att ersätta adapterkabeln med ett litet adapterkretskort. Genom att hålmontera stiftdelen från en vanlig 5-polig DIN-hane för kabelmontage och på samma kort sätta en TRRS-hona för kortmontage kan man göra bygget väldigt mycket enklare, mindre och tillförlitligare än en adapterkabel. Kopplingsschemat visas i Figur 1.

Figur 1. Kopplingsschema för adaptern till mottagare från OK2BWN.

Stift 4 och 5 i DIN-kontakten behöver byglas för att koppla in batteriet till mottagarelektroniken medan stift 2 är jord för hörlurarna och stift 1 och 3 (som är sammankopplade i mottagaren) levererar audiosignalen (mono). De två yttre anslutningarna på 3,5-mm-pluggen (T och R) går till vänster respektive höger hörlur. För hörlurar med mikrofon och alltså TRRS-kontakt brukar den innersta anslutningen, S:et i TRRS, vara mikrofonanslutningen medan den näst innersta, sista R:et, brukar vara jord för både mikrofon och hörlurar. Det förekommer också att jord och mikrofon är omkastade, även om det tycks vara mindre vanligt. För säkerhets skull och för att det nog är allmänt bra att ha så många jordade ledare som möjligt i sladden så har jag kopplat både andra R:et och S till jord. Detta fungerar också med TRS-kontakter som har en längre S-sektion.

Det hela ryms på ett 14 mm x 28,5 mm litet tvålagerskort som jag tillverkade hos JLCPCB. 25 kort kostade inte många tior, men frakten var lite dyrare. Layouten visas i Figur 2.

Figur 2. Layouten för topp- respektive bottensidan.

Kontaktdon som passar på kortet är:

Ref Beskrivning TillverkareArt. nummer Leverantör Lev. artikelnummer
J1Stereo audio jack, TRRSTensility54-00346Digikey839-54-00346DKR-ND
P15-polid DIN, 180 grader, kabelkontakt, hane  Electrokit41002841

Kontaktdonen passar tyvärr från båda sidorna av kortet, så för att det ska bli rätt funktion gäller det att sätta i dem från rätt sida innan lödning. Texterna ”Solder” på mönsterkortet hjälper till att påminna om vilken sida som vartdera kontaktdonet ska lödas på. För att skydda det hela och göra det lätt att sätta i och ta ur adaptern ur mottagaren så ritade jag en liten kåpa för 3D-utskrivft som snäpper fast över kortet och som har några räfflor som underlättar greppet när man drar ur den ur mottagarens kontakt. Se Figur 3, 4 och 5.

Figur 3. Det 3D-utskrivna höljet samt kretskortet med de två kontakterna.

Figur 4. Komplett adapter.

Figur 5. Adaptern inkopplad mellan en mottagare och en hörlur.

Den som vill bygga likadana adaptrar kan ladda ned gerberfilerna för mönsterkortet samt 3D-filen för utskrift:

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, 2025
// MIT licence, http://www.opensource.org/licenses/mit-license.php

#include <cstdint>
#include <cmath>

// Type to represent (positive) rational numbers
typedef struct {
  uint32_t numerator;
  uint32_t denominator;
  uint32_t iterations;   // Just for debugging
} 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;
  uint32_t a = 0, b = 1, c = 1, d = 1, ac, bd, Nint;
  const int maxIter = 100;

  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;
    }
    ii++;
    mediant = ac/(double)bd;
    if(target < mediant) {
      // Discard c/d since 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);
      if(Nint < 1) {
        // Nint should be at least 1, a rounding error may cause N to be just less than that
        Nint = 1;
      }
      // 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 since 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);
      if(Nint < 1) {
        // Nint should be at least 1, a rounding error may cause N to be just less than that
        Nint = 1;
      }
      // 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;
    }
  }

  retval.numerator = ac;
  retval.denominator = bd;
  retval.iterations = ii;
  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;
  uint32_t maxiter;
} rational_test_case_t;

void test_rational_approx()
{
  rational_t result;

  rational_test_case_t test[] = { 
    {0, 3000, 0, 1, 2},
    {1, 3000, 1, 1, 2},
    {0.5, 3000, 1, 2, 2},
    {0.5+1/3001.0, 3000, 751, 1501, 5},
    {1/3001.0, 2500, 1, 2500, 2},
    {1/3001.0, 1500, 0, 1, 2},
    {1/3001.0, 3001, 1, 3001, 2},
    {0.472757439, 1816, 564, 1193, 10},
    {0.472757439, 1817, 859, 1817, 10},
    {0.288, 100000000, 36, 125, 10},
    {0.47195, 1048575, 9439, 20000, 12},
    {0.471951, 1048575, 471951, 1000000, 15},
    {1/128.0, 1048575, 1, 128, 2},
    {17/65536.0, 1048575, 17, 65536, 3},
    {0.618033988749895, 1000000, 514229, 832040, 30}, // Golden ratio - 1, worst case in terms of number of iterations
    {0.141592653589793, 56, 1, 7, 2},
    {0.141592653589793, 57, 8, 57, 2},
    {0.141592653589793, 106, 15, 106, 3},
    {0.141592653589793, 113, 16, 113, 4},
    {0.141592653589793, 32000, 4527, 31972, 5},
    {0.141592653589793, 32989, 4671, 32989, 5},
  };
  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.printf("target = %.8g, maxdenom = %lu, ", test[ii].target, test[ii].maxdenom);
    Serial.printf("approx = %lu/%lu, iter = %lu ", result.numerator, result.denominator, result.iterations);
    if(result.numerator == test[ii].expected_numerator && 
       result.denominator == test[ii].expected_denominator && 
       result.iterations <= test[ii].maxiter) {
      Serial.println(" OK");
    } else {
      if(result.iterations > test[ii].maxiter) {
        Serial.printf("Too many iterations (max %lu) ", test[ii].maxiter);
      }
      Serial.printf("Expected %lu/%lu\n", test[ii].expected_numerator, test[ii].expected_denominator);
    }
  }
}

The printout from the code is:


target = 0, maxdenom = 3000, approx = 0/1, iter = 0  OK
target = 1, maxdenom = 3000, approx = 1/1, iter = 0  OK
target = 0.5, maxdenom = 3000, approx = 1/2, iter = 1  OK
target = 0.500333222, maxdenom = 3000, approx = 751/1501, iter = 4  OK
target = 0.000333222259, maxdenom = 2500, approx = 1/2500, iter = 1  OK
target = 0.000333222259, maxdenom = 1500, approx = 0/1, iter = 1  OK
target = 0.000333222259, maxdenom = 3001, approx = 1/3001, iter = 1  OK
target = 0.472757439, maxdenom = 1816, approx = 564/1193, iter = 7  OK
target = 0.472757439, maxdenom = 1817, approx = 859/1817, iter = 8  OK
target = 0.288, maxdenom = 100000000, approx = 36/125, iter = 5  OK
target = 0.47195, maxdenom = 1048575, approx = 9439/20000, iter = 11  OK
target = 0.471951, maxdenom = 1048575, approx = 471951/1000000, iter = 14  OK
target = 0.0078125, maxdenom = 1048575, approx = 1/128, iter = 1  OK
target = 0.000259399414, maxdenom = 1048575, approx = 17/65536, iter = 2  OK
target = 0.618033989, maxdenom = 1000000, approx = 514229/832040, iter = 28  OK
target = 0.141592654, maxdenom = 56, approx = 1/7, iter = 2  OK
target = 0.141592654, maxdenom = 57, approx = 8/57, iter = 2  OK
target = 0.141592654, maxdenom = 106, approx = 15/106, iter = 2  OK
target = 0.141592654, maxdenom = 113, approx = 16/113, iter = 3  OK
target = 0.141592654, maxdenom = 32000, approx = 4527/31972, iter = 4  OK
target = 0.141592654, maxdenom = 32989, approx = 4671/32989, iter = 4  OK

I also implemented the algorithm in an Excel sheet:

The excel document is available here: