Graphic compass

I am working on a small telemetry project with a 10DOF module in an ROV sending a serial data stream back to 'base'. Base is a Uno and an SSD1306 display using Adafruit graphics.

It is easy enough to get a digital compass bearing on the display but I saw this on Youtube and am puzzled how to create the compass rose with the rotating needle without using a huge amount of processing power and memory.

Is there a way to draw a line as a vector (starting point and direction)? (as opposed to calculating the X and y location of the end point with trig.)

Round the bearing to the nearest 10's of degrees divided by 10. Thus you'll have an integer between 0 and 35. That will fit in a uint8_t. Then have an array of structures that you fill with the end point coordinates of your 36 compass points (the origin is the same for all of them, just need the endpoint):

typedef struct {
  uint8_t x;
  uint8_t y;
} point;

const point endPoints[36] = {
  {x0, y0},
  {x1, y1},
  {x2, y2}
  .
  .
  .
  .
  {x35, y35}
};

This is done at compile-time, it takes no real-time processing power. Make it a global (outside of setup() and loop())

You can then use your bearing (0 to 35) to index into the array of structures to get the proper end point:

xEndPoint = endPoints[bearing].x;
yEndPoint = endPoints[bearing].y;

Requires very little real-time processing power and 72 bytes of memory for the array.

You don't even need 36 coordinate pairs (72 values) to get 10° steps. By flipping signs and subtracting from 1 you can generate a full circle from one 45° segment. Using a table of Sine(x) * Radius for x from 0 to 44 you can get 1° resolution with only 45 entries.

The math is all done at compile time so no floating-point will be done at run time. There are a lot of warnings about cramming a 'double' into a 'byte' if you turn up warnings like I do. :slight_smile:

const int ROSE_RADIUS = 20;
const byte SIN[45] =  {
  sin(0 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(1 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(2 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(3 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(4 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(5 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(6 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(7 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(8 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(9 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(10 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(11 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(12 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(13 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(14 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(15 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(16 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(17 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(18 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(19 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(20 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(21 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(22 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(23 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(24 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(25 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(26 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(27 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(28 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(29 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(30 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(31 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(32 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(33 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(34 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(35 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(36 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(37 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(38 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(39 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(40 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(41 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(42 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(43 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5,
  sin(44 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5};

Wow, thanks a lot guys! There are some brilliant people on here! :wink:

Oops. I guess you need 90 data points to calculate every degree. You can put the data into PROGMEM so it doesn't use up valuable SRAM.

To move the needle you should draw over the old position using the background color and then draw the new position in the foreground color. You will have to re-draw the compass labels if part of them can get erased when you erase the old needle. Be sure to re-draw ONLY when the needle has changed position or you will get a lot of annoying flicker. Since you need to save the old position for erasing anyway it is easy to tell if the new position is different.

Here is the new Sine table plus the functions for looking up SineRadius and Cosineradius for any angle from 0 to 359.

#include <avr/pgmspace.h>

// Screen position of the center of the compass rose
const int ROSE_CENTER_X =  200;
const int ROSE_CENTER_Y = 64;

// For values of ROSE_RADIUS below 127 we can use 'char' to save space.
const char ROSE_RADIUS = 20;
// Putting the table of constants into FLASH/PROGMEM to save SRAM

const char SINE_TABLE[90] PROGMEM =  {
  // Casting the result as 'char' avoids warning messages about narrowing
  char(sin(0 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(1 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(2 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(3 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(4 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(5 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(6 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(7 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(8 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(9 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(10 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(11 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(12 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(13 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(14 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(15 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(16 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(17 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(18 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(19 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(20 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(21 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(22 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(23 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(24 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(25 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(26 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(27 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(28 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(29 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(30 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(31 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(32 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(33 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(34 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(35 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(36 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(37 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(38 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(39 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(40 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(41 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(42 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(43 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(44 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(45 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(46 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(47 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(48 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(49 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(50 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(51 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(52 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(53 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(54 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(55 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(56 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(57 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(58 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(59 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(60 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(61 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(62 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(63 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(64 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(65 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(66 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(67 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(68 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(69 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(70 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(71 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(72 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(73 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(74 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(75 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(76 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(77 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(78 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(79 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(80 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(81 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(82 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(83 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(84 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(85 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(86 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(87 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(88 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5),
  char(sin(89 * 2 * PI / 360.0) * ROSE_RADIUS + 0.5)
};


// Draw the compass pointer at an angle in degrees
void drawCompassPointer(int angle) {
  int x_offset, y_offset;  // The resulting offsets from the center point
  
  // Normalize the angle to the range 0 to 359
  while (angle < 0)
    angle += 360;
    
  while (angle > 359)
    angle -= 360;
    
  x_offset = getSine(angle);
  y_offset = getCosine(angle);
  // The actual drawing command will depend on your display library
//  drawLine(ROSE_CENTER_X, ROSE_CENTER_Y, ROSE_CENTER_X + x_offset, ROSE_CENTER_Y + y_offset);
}

int getSine(int angle) {
  // For angles less than 90, use the table directly
  if (angle < 90)
    return pgm_read_byte_near(&SINE_TABLE[angle]);

  if (angle < 180) {
    //  90 to 179: flip direction
    return pgm_read_byte_near(&SINE_TABLE[179 - angle]);
  }

  if (angle < 270) {
    // 180 to 269 flip sign
    return -pgm_read_byte_near(&SINE_TABLE[angle - 180]);
  }
  
  // 270 to 359 flip sign and direction
  return -pgm_read_byte_near(&SINE_TABLE[359 - angle]);
}

// Cosine(angle) == Sine(angle-90)
int getCosine(int angle) {
  // Keep angle-90 between 0 and 359
  if (angle < 90)
    angle += 360;

  return getSine(angle - 90);
}

Nice, I had to sit down and do the trig with pencil and paper. Wanted to convince myself the equations worked because “bearing” angles go in the opposite direction and start at a different point than “math” angles (CW/North verses CCW/East).

gfvalvo:
.... “bearing” angles go in the opposite direction and start at a different point than “math” angles....

That also bugged me when I was in school. Even still (after 55 years) in drafting, I change the coordinate system to zero at the top and angles measured clockwise.