NO3M

Amateur Radio

Category: Uncategorized (page 2 of 4)

HF Receiver – TFT font speed optimizations

I recently started a new receiver project, based in large part on the work by PA3AKE (https://martein.home.xs4all.nl/pa3ake/hmode/receiver_intro.html).  Initially it will be an all analog design, but perhaps later incorporate DSP in at least the audio domain, perhaps in the IF as well.

Early development for the rig’s display was done with a 3.2 in. TFT, but I decided to use something larger for more screen space.  In addition, it would be more aesthetically pleasing, since the intended chassis / enclosure’s front panel is relatively large (14 in W x 5 in. H).  A “tiny” screen would look a bit out of place on such a large amount of front panel real estate.

A 7 in. 800×480 pixel TFT display using a RA8875 driver (ER-TFTM070-5) was acquired from BuyDisplay.com.  The rig’s microprocessor is a Teensy 3.5 (ARM Cortex M4), with the project’s code developed using the Arduino IDE.

One thing that came up while working out the display layout was effectively rendering the VFO readout.  Early methods resulted in awkward  and jerky display updates.  Keep in mind that the entire program is running out of the main program loop, ie. single threaded.  Multi-threading may provide smoother overall operation, but that is yet to be explored.  For this particular implementation, any VFO frequency display updates are blocking the program execution and the routines need optimized for speed.

What not to do…

  1.  Don’t use bitmap font rendering: rendering text using a bitmap font on graphic displays is sloooow!  This is the case regardless of the display library and font geometry (discussed later…).  Through benchmark testing, it was found that the optimal speed that could be achieved when changing a single digit in the frequency readout using a 50 pixel height font was about 7ms.  Larger fonts are worse.  That may not sound like much, but when shuttling along at a fast rate (2-3 RPM), it consumes a significant amount of time pausing the program flow while the shaft encoder continues to accumulate counts (via hardware interrupts), resulting in less than pleasing readout transitions — more of a hopping effect.
  2.  Don’t update after every frequency increment: when shuttling along at a high rotation rate (2-3 RPM), attempting to update the display readout after every increment exasperates the effects mentioned above.  The program ends up spending most of it’s time dealing with display updates.

 

How to optimize…

1. Choose a “font” with simple geometric properties:

Geometric drawing operations on TFT displays are much faster than font rendering due to built-in accelerated operations in the hardware display driver.  To exploit this speed, a font that has simple geometric properties is required, since we are limited to line drawing, rectangle fills, triangle fills, etc.  Further, horizontal and vertical lines generally render fastest.  All these considerations lead us to basically a block font as illustrated below:

Not particularly attractive, is it?  Since the font will need to be about 44 – 50 pixels high, each line will need to be about 3-4 pixels for the most aesthetic appearance, which can be rendered as rectangle fill operations.  But, for perhaps a slight overhead, we can get a more styled font using line drawing.  Remember the classic 7-segment LCD digits?  This is a natural choice for a VFO readout given our restrictions for optimal rendering speed.  There are still a number of rigs currently on the market not to mention boat loads of previous transceivers that used this type of digit font.  Of course, before TFTs became popular in ham transceivers, this was done with generic or custom LCDs (ie. Elecraft K3).  below is the basic 7-degment font:

Using line draw operations to render the above 7-segment font reduced the a single digit update to 2.5ms!  That’s 36% of the time required using a bitmap font of the same digits.  But, we can improve this even further…

2.  Optimize update routines to only apply differences: transitions between digits, sequential or not,  often involve on average only a few segment changes.  By comparing the previously displayed digit with the digit to be rendered, only the differences between the two need updated on the display, either transitioning from off to on, or visa versa.  Updating in this manner further reduces single digit updates to 1ms!!  That’s only 14% of the original bitmap font rendering time.

Is there anything else left to do??  You bet…

3. Limit display updates: there is no sense in updating the display every time the encoder counter is incremented (ie. frequency changed).  Even with the significant reduction in rendering the digits, a lot of time would still be spent with display updates.  Much like the frames-per-second refresh rate of a television, display updates can be limited to 20-30 times per second and still result in a “smooth” appearance when tuning at high rates (2-3 RPM).

Code example

Below are the snippets of code relevant to rendering the digits and controlling the refresh rate.
These functions will work with the Adafruit or sumotoy RA8875 libraries and have been tested with a 800×480 TFT and 400PPR rotary encoder. Additional features are leading 0 digit suppression (except less than 100 KHz) as well as suppressing the 1 MHz decimal. The 1 Hz digit is also suppressed unless the tuning step size is 1 Hz.

 

#define VFOA_COLOR ConvertRGB(67,128,249)
#define VFOA_TOP 100
#define VFOA_RIGHT 350
#define VFOA_DIGIT_WIDTH 40
#define VFOA_DIGIT_HEIGHT 46
#define VFOA_DECIMAL_WIDTH 15
#define VFOA_SMALL_DIGIT 8
#define VFOA_DECIMAL_HSHIFT1 227
#define VFOA_DECIMAL_HSHIFT2 92
#define VFOA_DECIMAL_VSHIFT 49
#define VFOA_DECIMAL_RADIUS 2
#define VFOA_LABEL_VSHIFT 29
#define VFOA_LABEL_HSHIFT 335

#define VFOB_COLOR ConvertRGB(180,180,180)
#define VFOB_TOP 100
#define VFOB_RIGHT 750
#define VFOB_DIGIT_WIDTH 40
#define VFOB_DIGIT_HEIGHT 46
#define VFOB_DECIMAL_WIDTH 15
#define VFOB_SMALL_DIGIT 8
#define VFOB_DECIMAL_HSHIFT1 227
#define VFOB_DECIMAL_HSHIFT2 92
#define VFOB_DECIMAL_RADIUS 2
#define VFOB_LABEL_VSHIFT 29
#define VFOB_LABEL_HSHIFT 335
#define TFT_BGLIGHT ConvertRGB(10,10,10)

#define NUM_VFO 2
#define VFO_REFRESH 40000

uint32_t vfo_update_us = micros();
uint32_t vfoFreq[NUM_VFO] = { 1800000, 1800000 }; // default
uint32_t vfoFreqLast[NUM_VFO] = { 1800000, 1800000 };
uint16_t vfoStepSize = 10;

word ConvertRGB( byte R, byte G, byte B)
{
  return ( ((R & 0xF8) << 8) | ((G & 0xFC) << 3) | (B >> 3) );
}

void dispVFO(uint32_t vfoOld, uint32_t vfoNew, int vfo, boolean refresh = false) {

  if (vfoNew == vfoOld && !refresh) return; // nothing to do

  byte digits[2];
  static const unsigned char nums[12] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x27, 0x7F, 0x6F, 0x00, 0x40}; // segment map
  byte s = (vfo) ? VFOB_DIGIT_HEIGHT : VFOA_DIGIT_HEIGHT; // digit height
  int i, j;
  uint16_t x, y, col;
  uint32_t vfoNew_tmp = vfoNew;
  uint32_t vfoOld_tmp = vfoOld;
  
  for (i = 0; i < 8; ++i) { // digits

    digits[0] = vfoOld_tmp % 10;
    digits[1] = vfoNew_tmp % 10;
    vfoOld_tmp /= 10;
    vfoNew_tmp /= 10;
    
    if (digits[0] != digits[1] || refresh) { // clear and update only if changed or forced

      for (j = 0; j < 7; ++j) { // segments
        
        if ( refresh || (nums[digits[0]] & (1 << j)) != (nums[digits[1]] & (1 << j))
              || (vfoNew < 10000000 && vfoOld >= 10000000) || (vfoOld < 10000000 && vfoNew >= 10000000)
              || (vfoNew < 1000000 && vfoOld >= 1000000) || (vfoOld < 1000000 && vfoNew >= 1000000)
              || (vfoNew < 100000 && vfoOld >= 100000) || (vfoOld < 100000 && vfoNew >= 100000)
           ) {

          byte s_tmp = s;
          if ( i < 3 ) {
            s_tmp -= (vfo) ? VFOB_SMALL_DIGIT : VFOA_SMALL_DIGIT;
            if (vfo) { // digit position
              x = VFOB_RIGHT - (i * VFOB_DIGIT_WIDTH) - ((i / 3) * VFOB_DECIMAL_WIDTH);
              y = VFOB_TOP + VFOB_SMALL_DIGIT;
            } else {
              x = VFOA_RIGHT - (i * VFOA_DIGIT_WIDTH) - ((i / 3) * VFOA_DECIMAL_WIDTH);
              y = VFOA_TOP + VFOA_SMALL_DIGIT;
            }
          } else {
            if (vfo) { // digit position
              x = VFOB_RIGHT - (i * VFOB_DIGIT_WIDTH) - ((i / 3) * VFOB_DECIMAL_WIDTH);
              y = VFOB_TOP;
            } else {
              x = VFOA_RIGHT - (i * VFOA_DIGIT_WIDTH) - ((i / 3) * VFOA_DECIMAL_WIDTH);
              y = VFOA_TOP;
            }
          }
          if ( nums[digits[1]] & (1 << j) && 
               !(i == 6 && vfoNew < 1000000) && !(i == 7 && vfoNew < 10000000) && !(i == 5 && vfoNew < 100000) && !(i == 0 && settings.vfoStepSize > 1)
             ) {
            col = (vfo) ? VFOB_COLOR : VFOA_COLOR;
          } else {
            col = TFT_BGLIGHT;
          }

          switch (j) {
            case  0:
              //a
              tft.drawFastHLine(x + 1, y    , ((s_tmp * 6) / 10)    , col);
              tft.drawFastHLine(x + 2, y + 1, ((s_tmp * 6) / 10) - 2, col);
              tft.drawFastHLine(x + 3, y + 2, ((s_tmp * 6) / 10) - 4, col);
              tft.drawFastHLine(x + 4, y + 3, ((s_tmp * 6) / 10) - 6, col);
              break;
            
            case  1:
              //b
              tft.drawFastVLine(x + ((s_tmp * 6) / 10) - 2, y + 4, (s_tmp / 2) - 6, col);
              tft.drawFastVLine(x + ((s_tmp * 6) / 10) - 1, y + 3, (s_tmp / 2) - 4, col);
              tft.drawFastVLine(x + ((s_tmp * 6) / 10)    , y + 2, (s_tmp / 2) - 2, col);
              tft.drawFastVLine(x + ((s_tmp * 6) / 10) + 1, y + 1, (s_tmp / 2)    , col);
              break;
            
            case  2:
              //c
              tft.drawFastVLine(x + ((s_tmp * 6) / 10) - 2, y + (s_tmp / 2) + 4, (s_tmp / 2) - 6, col);
              tft.drawFastVLine(x + ((s_tmp * 6) / 10) - 1, y + (s_tmp / 2) + 3, (s_tmp / 2) - 4, col);
              tft.drawFastVLine(x + ((s_tmp * 6) / 10)    , y + (s_tmp / 2) + 2, (s_tmp / 2) - 2, col);
              tft.drawFastVLine(x + ((s_tmp * 6) / 10) + 1, y + (s_tmp / 2) + 1, (s_tmp / 2)    , col);
              break;
            
            case  3:
              //d
              tft.drawFastHLine(x + 4, y + s_tmp - 2, ((s_tmp * 6) / 10) - 6, col);
              tft.drawFastHLine(x + 3, y + s_tmp - 1, ((s_tmp * 6) / 10) - 4, col);
              tft.drawFastHLine(x + 2, y + s_tmp    , ((s_tmp * 6) / 10) - 2, col);
              tft.drawFastHLine(x + 1, y + s_tmp + 1, ((s_tmp * 6) / 10)    , col);
              break;
            
            case  4:
              //e
              tft.drawFastVLine(x    , y + (s_tmp / 2) + 1, (s_tmp / 2)    , col);
              tft.drawFastVLine(x + 1, y + (s_tmp / 2) + 2, (s_tmp / 2) - 2, col);
              tft.drawFastVLine(x + 2, y + (s_tmp / 2) + 3, (s_tmp / 2) - 4, col);
              tft.drawFastVLine(x + 3, y + (s_tmp / 2) + 4, (s_tmp / 2) - 6, col);
              break;
            
            case  5:
              //f
              tft.drawFastVLine(x    , y + 1, (s_tmp / 2)    , col);
              tft.drawFastVLine(x + 1, y + 2, (s_tmp / 2) - 2, col);
              tft.drawFastVLine(x + 2, y + 3, (s_tmp / 2) - 4, col);
              tft.drawFastVLine(x + 3, y + 4, (s_tmp / 2) - 6, col);
              break;
            
            case  6:
              //g
              tft.drawFastHLine(x + 3, y + (s_tmp / 2) - 1, ((s_tmp * 6) / 10) - 4, col);
              tft.drawFastHLine(x + 2, y + (s_tmp / 2)    , ((s_tmp * 6) / 10) - 2, col);
              tft.drawFastHLine(x + 2, y + (s_tmp / 2) + 1, ((s_tmp * 6) / 10) - 2, col);
              tft.drawFastHLine(x + 3, y + (s_tmp / 2) + 2, ((s_tmp * 6) / 10) - 4, col);
              break;
  
            default:
              break;            
          }
        }
      }
    }
  }

  // only update when crossing 1MHz in either direction
  if ((vfoNew < 1000000 && vfoOld >= 1000000) || (refresh && vfoNew < 1000000)) {
    if (vfo) {
      tft.fillCircle(VFOB_RIGHT - VFOB_DECIMAL_HSHIFT1, VFOB_TOP + VFOB_DIGIT_HEIGHT -1, VFOB_DECIMAL_RADIUS, TFT_BGLIGHT);
    } else {
      tft.fillCircle(VFOA_RIGHT - VFOA_DECIMAL_HSHIFT1, VFOA_TOP + VFOA_DIGIT_HEIGHT -1, VFOA_DECIMAL_RADIUS, TFT_BGLIGHT);
    }
  } else if ((vfoOld < 1000000 && vfoNew >= 1000000) || (refresh && vfoNew >= 1000000)) {
    if (vfo) {
      tft.fillCircle(VFOB_RIGHT - VFOB_DECIMAL_HSHIFT1, VFOB_TOP + VFOB_DIGIT_HEIGHT -1, VFOB_DECIMAL_RADIUS, VFOB_COLOR);
    } else {
      tft.fillCircle(VFOA_RIGHT - VFOA_DECIMAL_HSHIFT1, VFOA_TOP + VFOA_DIGIT_HEIGHT -1, VFOA_DECIMAL_RADIUS, VFOA_COLOR);
    }
  }

  return;
}

void setup() {

/* ... */

  dispVFO(0, vfoFreq[0], 0, true);
  dispVFO(0, vfoFreq[1], 1, true);

}

void loop() {

/* ... */

  if (micros() - vfo_update_us > VFO_REFRESH) {
    for (int i=0;  i < NUM_VFO; ++i) {
      dispVFO(vfoFreqLast[i], vfoFreq[i], i);
      vfoFreqLast[i] = vfoFreq[i];
    }
    vfo_update_us = micros();
  }

W8CDX 2018 Field Day

Another good Field Day on the books.  As with previous years, we ran equipment circa 1930s vintage.  The receiver was a HRO-5 and the transmitter was a homebrew wooden rack outfit with a 89-42-802 exciter and PP 814 amplifier giving about 200-250W output.

Different this year was our effort to make some 630M QSOs.  The 80M open-wire fed doublet was put into service as a Marconi vertical with the ladder line shorted at the transmitter end.  A variometer and shunt coil were used for resonance and impedance matching, respectively.  A simple radial system consisting of ten approx. 70 ft long wires were laid out.  They were quite effective, bringing the feedpoint resistance down from about 150 ohms with just a single ground rod to around 30 ohms.  Even then, the overall efficiency was estimated to be near 1%.  The 250W produced by the PP 814s resulted in about 5W EIRP.

Things didn’t look too promising at first, but with patience,  cooperating QSB and QRN, and a great set of ears, Mike / WA3TTS, completed a CW QSO with K3MF on 630M!   It was a great moment!

Later in the evening, another QSO was made with K9KFR via CW with Eric / NO3M on the key.

Thanks to Mike / WA3TTS, Mark (radio enthusiast), Bob / W3BBO,  Neil / W3CUV, and the ARRL group for coming and making it a fun and memorable event!

Dayton 2018 Antenna Forum

A presentation about 630m antennas at the 2018 Dayton Hamvention Antenna Forum on Friday, May 18, at 2:45 PM EDT.  The material covers both transmit and receive antennas.  Hopefully it will inspire more operators to put 630m stations on the air.  Thanks to K3LR for the invitation.

Slide Deck (PDF) – click image below to open/download:

Presentation Video:

 

Older posts Newer posts

© 2019 NO3M

Theme by Anders NorenUp ↑