Custom Search

Saturday, December 28, 2013

An arduino anemometer

Why make an anemometer?

There are lots of anemometers on the market, with a lot of different features. What they don't have is a configurable display. By coupling a good wind sensor with an android board and a cheap display, the display can be configured to do things that simply aren't possible with any of the off the shelf systems. See my rc blog for more details about this.

Hardware Interface

The interface to the Inspeed sensor is simple - you put voltage on one of the two lines in the sensor, and it'll close the circuit and give you a pulse back each rotation. That's documented at 2.5mph for one rotation a second. Some of their newer sensors have higher pulse rates - and possibly different interfaces - so consider your application when choosing one.
With that interface, I tied the wind sensor pulse line to an interrupt whose handler just counted pulses:
// The wind sensor interrupt data
volatile static uint16_t sensor_count = 0 ;

// And handler
static void
sensor_tick() {
  sensor_count += 1 ;
}
A system timer was then used to generate a second interrupt that recorded and reset the current count and did some bookkeeping:
volatile static uint8_t current_sensor ;
volatile static uint8_t readings[SPEED_LENGTH] ;
volatile static uint16_t next_speed = 0 ;
volatile static uint8_t wrapped = 0 ;

void
recorder_tick() {

  current_sensor = sensor_count ;   // Warning - could lose a tick here...
  sensor_count = 0 ;
  readings[next_speed++] = current_sensor ;
  if (next_speed >= SPEED_LENGTH) {
    wrapped = 1 ;
    next_speed = 0 ;
  }
}
The main loop of the program just reads the reading array to calculate the various values displayed. Most of it is uninteresting, but will be included in the code segment at the end.

Display

The goal was to display an easy to see indicator for max and average wind speed. The one interesting part of that is how the color is calculated. We want different colors for different wind conditions, as shown by this table:

Average↓ Gust→LowMediumHigh
LowGreenCyanYellow
MediumNPBlueMagenta
HighNPNPRed
If it's not obvious, the NP ones are cases where the gust speed is lower than the average, which is impossible.
In this day and age, when system memory is measured in gigabytes and programs easily top hundreds of megabytes, a straightforward else-if chain with some nested conditions to select color values is the obvious way to do things. If we have to repeat a few conditions, well - they don't take enough memory to matter.
Except we're running on an Arduino mini, where memory space is measured in K, and every byte might count. So I have an excuse to go old school, and figure out how to squeeze the color calculation code into the least amount of space.
Here's the code to set the color on the LCD:
  // Pick a color...
  memset(colors + 1, 0, 3) ;
  colors[color_of(avg) + 1] = 255 ;
  colors[color_of(top) + 1] = 255 ;
  do_command(&lcd, 4, colors) ;
The first byte of the colors array is the set color command for the LCD. The other three are the values to use for red, green and blue - in that order. So for both the average and top wind speed, we're going to set either red, green or blue to on instead of off. Given that, the color of a wind speed just needs a cunning plan1:
// Select a color with a cunning plan
static uint8_t
color_of(uint8_t speed) {
  if (speed >= RED_SPEED) return 0 ;
  if (speed >= BLUE_SPEED) return 2 ;
  return 1 ;
}
So if the two values are the same, we get either either a red (no flying now), blue (maybe the larger craft) or green (why aren't you outside flying?) display. If the gusts are noticeably faster, we'll get cyan, yellow or possibly magenta instead.

Conclusion

This actually works quite well in practice - that green is really obvious, and the others are closely enough related that I can remember which is which without to much trouble.
Here's the complete Sketch:
#include <TimerOne.h>
#include <SoftwareSerial.h>
#include <string.h>

// Values that control display
#define BLUE_SPEED  8   // Minimum speed for blue display
#define RED_SPEED   21  // Minimum speed for red dispaly

/*
 * Values that control recording. Note that DELAY_SECONDS and
 * DELAY_SECONDS * TREND_BUCKETS must both go evenly into RECORD_TIME.
 */
#define RECORD_TIME 900 // Length of the recording
#define DELAY_SECONDS   1   // Sample time length. Max of 8.
#define TREND_BUCKETS   3   // # of buckets to use for trend checking.

// Physical constants
#define SENSOR_INT  1   // Meaning pin #3
#define SERIAL_OUT  2   // Serial line to the display
#define LINE_LENGTH 16  // Length of output line on LCD

// Display (TREND_* maps to first custom char for that trend).
#define TREND_UNKNOWN   0
#define TREND_DOWN  2
#define TREND_EVEN  4
#define TREND_UP    6

// 1 PULSE/SEC = 2.5 MPH
#define PULSES_TO_SPEED(count) (((count) * 5 + DELAY_SECONDS) / (DELAY_SECONDS * 2))

// The wind sensor interrupt data
volatile static uint16_t sensor_count = 0 ;

// And handler
static void
sensor_tick() {
  sensor_count += 1 ;
}

// The recording instrument data
#define SPEED_LENGTH (RECORD_TIME / DELAY_SECONDS)

// Length of trend buckets in the record
#define BUCKET_LENGTH   (SPEED_LENGTH / TREND_BUCKETS)

volatile static uint8_t current_sensor ;
volatile static uint8_t readings[SPEED_LENGTH] ;
volatile static uint16_t next_speed = 0 ;
volatile static uint8_t wrapped = 0 ;

void
recorder_tick() {

  current_sensor = sensor_count ;   // Warning - could lose a tick here...
  sensor_count = 0 ;
  readings[next_speed++] = current_sensor ;
  if (next_speed >= SPEED_LENGTH) {
    wrapped = 1 ;
    next_speed = 0 ;
  }
}

// Select a color with a cunning plan
static uint8_t
color_of(uint8_t speed) {
  if (speed >= RED_SPEED) return 0 ;
  if (speed >= BLUE_SPEED) return 2 ;
  return 1 ;
}

SoftwareSerial lcd = SoftwareSerial(0, SERIAL_OUT) ;

void
do_command(SoftwareSerial *lcd, uint8_t len, const char *data) {
  lcd->write(0xFE) ;
  while (len--)
    lcd->write(*data++) ;
  delay(10) ;
}


void
setup() {
#ifdef MAKE_CHARS
  char chars[][11] = {
    {0xC1, 1, 0, B01001, B00101, B00011, B11111, B11111, B00011, B00101, B01001},
    {0xC1, 1, 1, B10010, B10100, B11000, B11111, B11111, B11000, B10100, B10010},
    {0xC1, 1, 2, 4, 2, 1, 0, 0, 0, 0, 0},
    {0xC1, 1, 3, 0, 0, 0, B10001, B01001, B00101, B00011, B11111},
    {0xC1, 1, 4, 0, 0, 0, B11111, B11111, 0, 0, 0},
    {0xC1, 1, 5, B01000, B00100, B00010, B11111, B11111, B00010, B00100, B01000},
    {0xC1, 1, 6, 0, 0, 0, 0, 0, 1, 2, 4},
    {0xC1, 1, 7, B11111, B00011, B00101, B01001, B10001, 0, 0, 0},
    } ;
#endif

  pinMode(3, INPUT_PULLUP) ;
  attachInterrupt(SENSOR_INT, sensor_tick, RISING) ;
  Timer1.initialize(1000000 * DELAY_SECONDS) ;
  Timer1.attachInterrupt(recorder_tick) ;

  lcd.begin(57600) ;
  delay(10) ;

#ifdef MAKE_CHARS
  do_command(&lcd, 33, "\x40 Meyer Heliport Wind Conditions ") ;
  for (uint8_t i = 0; i < 8; i += 1)
    do_command(&lcd, 11, chars[i]) ;
#endif
  do_command(&lcd, 1, "\x4B") ;     // Turn off underline cursor
  do_command(&lcd, 1, "\x54") ;     // blinking cursor
  do_command(&lcd, 1, "\x52") ;     // and autoscroll
  do_command(&lcd, 2, "\xC0\01") ;  // Get arrows
}

void
loop() {
  int8_t trend ;
  int16_t end, start ;
  uint8_t cur, top, now, avg, bucket_count ;
  uint16_t i, bucket_counter ;
  uint32_t sum, bucket_sum ;
  int32_t buckets[TREND_BUCKETS] ;
  char bhold[LINE_LENGTH + 1], colors[4] = {0xD0} ;

  end = next_speed ;
  cur = current_sensor ;
  start = wrapped ? end : 0 ;
  trend = TREND_UNKNOWN ;
  if (!wrapped && end == 0) {   // Haven't gotten a sample yet.
    avg = top = cur  ;
  } else {          // Do some stats...
    i = start ;
    bucket_sum = sum = bucket_counter = bucket_count = top = 0 ;
    do {
      now = readings[i] ;
      if (now > top)
    top = now ;
      sum += now ;
      bucket_sum += now ;
      bucket_counter += 1 ;
      if (bucket_counter == BUCKET_LENGTH) {
    buckets[bucket_count++] = bucket_sum ;
    bucket_counter = bucket_sum = 0 ;
      }
      i += 1 ;
      if (i >= SPEED_LENGTH)
    i = 0 ;
    } while (i != end) ;
    avg = sum / (wrapped ? SPEED_LENGTH : end) ;

    if (bucket_count == TREND_BUCKETS) {
      for (i = 0; i < TREND_BUCKETS - 1; i += 1) {
    if (abs(buckets[i] - buckets[i+1]) > max(buckets[i], buckets[i+1]) / 8) {
      trend = TREND_EVEN ;
      break ;
    } else if (buckets[i] < buckets[i + 1]) {
      if (trend == TREND_UNKNOWN)
        trend = TREND_UP ;
      else if (trend == TREND_DOWN) {
        trend = TREND_UNKNOWN ;
        break ;
      }
    } else if (buckets[i] > buckets[i + 1]) {
      if (trend == TREND_UNKNOWN)
        trend = TREND_DOWN ;
      else if (trend == TREND_UP) {
        trend = TREND_UNKNOWN ;
        break ;
      } else if (trend == TREND_UNKNOWN) {
        trend = TREND_EVEN ;
      }
    }
      }
    }
  }

  avg = PULSES_TO_SPEED(avg) ;
  top = PULSES_TO_SPEED(top) ;
  cur = PULSES_TO_SPEED(cur) ;

  // Now, display the data
  do_command(&lcd, 1, "\x58") ; // clear screen
  do_command(&lcd, 1, "\x48") ; // go home

  // Pick a color...
  memset(colors + 1, 0, 3) ;
  colors[color_of(avg) + 1] = 255 ;
  colors[color_of(top) + 1] = 255 ;
  do_command(&lcd, 4, colors) ;

  lcd.println("Cur Avg Max Tnd") ;
  snprintf(bhold, LINE_LENGTH + 1, "%3d %3d %3d  ", cur, avg, top) ;
  lcd.print(bhold) ;
  lcd.write(trend) ;
  lcd.write(trend + 1) ;
  delay(1000) ;
}

  1. If you don't think it's a cunning plan, you're probably not a fan of Blackadder