Sunspot Home

Arduino speaking variometer

These are my backup project notes - please email me if interested

Objective
Build a helmet mounted variometer for paragliding that speaks
the altitude above take-off in feet and the minutes since the flight began

Part 1 - Driving the speaking chip WTv020sd16 from an Arduino Nano

Method
This board seems to present many problems to get it to speak sound files - but it is very cheap!
Files are saved on a micro SD card.

WTV020SD

Links
main debate
more debate
buy one for about £3
SparkFun version
WTV020_manual_V1.3.pdf

Some observations

1) You need >3.5V for the SD card on the module from a source that can give 100mA while it talks without dropping out.
- This is because the SD card cannot run on just 3.3 volts - but the board is designed for 3.3 volts - do not use 5 Volts!!
2) WAV files play OK. I have a set of numbers that are very clear.
3) NB! as delivered 2 of the pins were dry solder jointed to the card!!!!! - I scratched off the paint round the pin and resoldered
4) An old Sandisk 2GB worked for a while off the Arduino 3V - after a few days it stopped
5) New identical Sandisks also failed on the Arduino supply - but all OK now on a dedicated more powerful supply
6) A Kingston 2GB failed on any power supply - people say it is very hit and miss but genuine Sandisk are always OK

The board demands 100 mA at 3.55 volts so I use an LM350 variable supply with 6.5 volts in and 3.55 volts out.
- I have 120 ohms across pins 1 and 2 and 227 ohms from pin 1 to earth (on a 350 ohm variable potentiometer).
On a 'scope it is rock solid 3.55 volts. At a bit below 3.4 volts the board stops talking.

I record WAV files of my voice in Audacity on my Mac and then amplify them to the default maximum without clipping
(to do this select the word you need from your voice recording at - MONO 8000Hz 16bit
- save selection as wav - re-open the saved wav file - click "effect - amplify" and choose the default no clipping maximum setting - re-save)

I see no need to bother with the manufacturers special file system (.ad4 files and the Somo tool)

My sound files on the SD Card (FAT16 format)

Circuit
I only use A1 to drive the data pin and A0 to drive the clock pin
I need to modify Wtv020sd16p.h if I need to use A2 and A3 for other reasons

My first test sketch

/*
Example: Control a WTV020-SD-16P module to play voices from an Arduino board.
Created by Diego J. Arevalo, August 6th, 2012.
Released into the public domain.

Modified by F G Marshall 2013
Objective (soon!)
- use to speak height and elapsed time in a Paragliding flight
- speak the flight duration since boot time every minute
sound file codes -
0021.wav up 0022.wav down 0023.wav minutes 0024.wav hour
0025.wav hours 0026.wav feet
0000.wav to 0009.wav - count oh one two .... nine

*/

#include <Wtv020sd16p.h>

int resetPin = A2; // The pin number of the reset pin (not needed here - kill in .h file?)
int clockPin = A0; // The pin number of the clock pin.
int dataPin = A1; // The pin number of the data pin.
int busyPin = A3; // The pin number of the busy pin (not needed here - kill in .h file?)
int number_to_speak;
char number_to_speak_as_chars[5];
long previousMillis = 0;
long interval = 20000; //time between height reports in msec
long minute = 0;
long previous_minute = 0;

/*
Create an instance of the Wtv020sd16p class.
1st parameter: Reset pin number.
2nd parameter: Clock pin number.
3rd parameter: Data pin number.
4th parameter: Busy pin number.
*/
Wtv020sd16p wtv020sd16p(resetPin,clockPin,dataPin,busyPin);

void setup() {
Serial.begin(115200);
//Initialize the speaking module.
wtv020sd16p.reset();
Serial.println("program starting");
delay(10);

/*
// test some sound files
for (int i = 20; i < 27; i++)
{
Serial.print("play file "); Serial.println(i);

wtv020sd16p.asyncPlayVoice(i);
delay(1000);
wtv020sd16p.stopVoice();
delay(10);
}
*/
}

void loop()
{
int height = 324; //work it out here

unsigned long currentMillis = millis();

if(currentMillis - previousMillis > interval)
{
// save the last time we spoke
previousMillis = currentMillis;

// announce "feet" coming next
wtv020sd16p.asyncPlayVoice(26);
delay(1000);
wtv020sd16p.stopVoice();
//say the height every "interval" milliseconds
say_sound_file_function(height);

minute = millis()/60000; //or use 120000 for speaking the minutes every 2 minutes etc
if (minute > previous_minute)
{

// announce "minutes" coming next
wtv020sd16p.asyncPlayVoice(23);
delay(1000);
wtv020sd16p.stopVoice();
// say how many minutes we have been flying
say_sound_file_function(minute);


Serial.print(" new minute = ");
Serial.println(minute);
}
previous_minute = minute;

Serial.print("currentMillis = "); Serial.println(currentMillis);
}
}

void say_sound_file_function(int number_to_speak)
// speak using the sound files 0000.wav to 0009.wav (0 to 9) (only) to spell out an integer of any length sent here
// e.g. 123 will become characters 1,2,3 in sequence and will load files 0001.wav etc in sequence
// calling "say_sound_file_function(minute)" will bring "int minute" to "int number_to_speak" here
{
Serial.println(number_to_speak);
// convert number_to_speak from int to string
String number_to_speak_as_String(number_to_speak, DEC);
Serial.println(number_to_speak_as_String);
//put "number_to_speak_as_String" into the array of characters "number_to_speak_as_chars"
number_to_speak_as_String.toCharArray(number_to_speak_as_chars,(number_to_speak_as_String.length()+1));//one extra for null character

Serial.print("number_to_speak_as_String.length() = "); Serial.println(number_to_speak_as_String.length());

// feed the characters that make up the string one by one to the speaking chip
for (int i = 0; i < number_to_speak_as_String.length(); i++)
{
Serial.print("number_to_speak_as_String character by character = ");
Serial.println(number_to_speak_as_chars[i]);
int line_to_speak = number_to_speak_as_chars[i] - '0'; //remove any null character
// speak the sound file
wtv020sd16p.asyncPlayVoice(line_to_speak);
delay(1000); // enough to speak any single number
wtv020sd16p.stopVoice();
delay(20);
}

}

20/09/2013 - Work in progress - Lesson 1 quality code!

I can now run a first attempt sketch that speaks the height and says how many minutes the sketch has been running
I will use the buttons OK/UP/DOWN to make a talking menu to adjust the variables
.

NB there is something wrong with the pressure to height calculation - I need to lift the sensor about 3 feet for it to change by 1 foot.

// original code by Rolf R Bakke, Oct 2012

/* (1 meter = 3.2808399 feet)

Wtv020sd16p speaks the height as alt_rounded digit by digit
*/
#include <Wire.h>
// speaking bit +++++++++++++++++++++++++++++++
#include <Wtv020sd16p.h>

int resetPin = A2; // The pin number of the reset pin (not needed here - kill in .cpp file?)
int clockPin = A0; // The pin number of the clock pin.
int dataPin = A1; // The pin number of the data pin.
int busyPin = A3; // The pin number of the busy pin (not needed here - kill in .cpp file?)
int number_to_speak;
char number_to_speak_as_chars[5];
long previousMillis = 0;
long interval = 30000; //time between height reports in msec
long minute = 0;
long previous_minute = 0;
long currentMillis = millis();
int looptime;
/*
Create an instance of the Wtv020sd16p class.
1st parameter: Reset pin number.
2nd parameter: Clock pin number.
3rd parameter: Data pin number.
4th parameter: Busy pin number.
*/
Wtv020sd16p wtv020sd16p(resetPin,clockPin,dataPin,busyPin);
// +++++++++++++++++++++++++++++++++++++++++

// set up the OK/UP/DOWN buttons (D8/D7/D6 wired to 5V, press for 0V))
const int buttonOKpin = 8;
const int buttonUPpin = 7;
const int buttonDOWNpin = 6;
int buttonOKState = 1;
int buttonUPState = 1;
int buttonDOWNState = 1;

// variable for reading the pushbutton status

const float p0 = 101325; // Pressure at sea level (Pa)
float altitude, altitude_in_feet;
float pressure_for_altitude;
int alt_rounded;
long old_millis = 0;
long loops = 0;

const byte led = 13;
unsigned int calibrationData[7];
unsigned long time = 0;

float toneFreq, toneFreqLowpass, pressure, lowpassFast, lowpassSlow, lowpassVerySlow ;

int ddsAcc;

//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> SETUP START
void setup()
{

// initialize the pushbutton pins as an input:
pinMode(buttonOKpin, INPUT);
pinMode(buttonUPpin, INPUT);
pinMode(buttonDOWNpin, INPUT);

// use i2c
Wire.begin();
// use rs232
Serial.begin(9600);
// ++++++++++++++++++++++++++++++++++++++++++++
//Initialize the speaking module.
wtv020sd16p.reset();
Serial.println("program starting");
delay(10);
// ++++++++++++++++++++++++++++++++++++++++++++

setupSensor();

pressure = getPressure();
lowpassFast = lowpassSlow = lowpassVerySlow = pressure;
}

//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> SETUP END

 

//§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§ MAIN LOOP START
void loop()
{

//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: process button presses start
buttonOKState = digitalRead(buttonOKpin);
buttonUPState = digitalRead(buttonUPpin);
buttonDOWNState = digitalRead(buttonDOWNpin);

if (buttonOKState == 0)
{
Serial.println("You pressed the OK button");
delay(2000);
}

if (buttonUPState == 0)
{
Serial.println("You pressed the UP button");
delay(2000);
}

if (buttonDOWNState == 0)
{
Serial.println("You pressed the DOWN button");
delay(2000);
}
//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: process button presses end

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Bakke beep routine start
pressure = getPressure();

// lowpastFast and lowpassSlow are smoothed pressure readings - a "low-pass filter" is applied to the noise in pressure
// if next pressure reading is high by 10 then 1 is added to lowpassFast but only 0.5 is added to lowpassSlow
// if next pressure reading is low by 20 then 2 is subtracted from lowpassFast - - etc

lowpassFast = lowpassFast + (pressure - lowpassFast) * 0.1;
lowpassSlow = lowpassSlow + (pressure - lowpassSlow) * 0.05;
toneFreq = (lowpassSlow - lowpassFast) * 50;
toneFreqLowpass = toneFreqLowpass + (toneFreq - toneFreqLowpass) * 0.1;

// force toneFreq to be equal to toneFreqLowpass provided toneFreqLowpass is between -500 and +500
toneFreq = constrain(toneFreqLowpass, -500, 500);

ddsAcc += toneFreq * 100 + 2000;

if (loops > 50) // re-start the beeping after a few loops when things have settled down after speaking has stoped this main loop
{
if (toneFreq < 0 || ddsAcc > 0)
{
tone(2, toneFreq + 510);
}
else
{
noTone(2);
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Bakke beep routine end

//````````````````````````````````````````````````````````````````````````````````````````````````` altitude / time speaking start
currentMillis = millis();
if(currentMillis - previousMillis > interval)
{
// save the last time we spoke
previousMillis = currentMillis;

// announce "feet" coming next
wtv020sd16p.asyncPlayVoice(26);
delay(1000);
wtv020sd16p.stopVoice();
//say the height every "interval" milliseconds
altitude_function(lowpassSlow); //calculate and print the altitude

minute = millis()/60000; //or use 120000 for speaking the minutes every 2 minutes etc
if (minute > previous_minute)
{
// announce "minutes" coming next
wtv020sd16p.asyncPlayVoice(23);
delay(1000);
wtv020sd16p.stopVoice();
// say how many minutes we have been flying
say_sound_file_function(minute);
Serial.print(" new minute = ");
Serial.println(minute);
}
previous_minute = minute;

Serial.print("currentMillis = "); Serial.println(currentMillis);
}

//``````````````````````````````````````````````````````````````````````````````````````````````````` altitude / time speaking end

// flash the pin13 led with any time left over
ledOff();

looptime = time - millis();
while (millis() < time); //loop frequency timer

time += 20;
ledOn();

loops = loops + 1;
}
//§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§ MAIN LOOP END

//*************************************************************************************************calculate altitude from pressure start
void altitude_function(float pressure_for_altitude)
{
// stop any beeping while I talk
noTone(2);
// calculate the altitude in meters
altitude = (float)44330 * (1 - pow(((float) pressure_for_altitude/p0), 0.190295));
// convert to feet
altitude_in_feet = altitude * (float)3.2808;

Serial.print("Altitude: ");
Serial.print(altitude, 2);
Serial.print(" ");
Serial.print(" feet ");

// round the altitude to the nearest whole number
if (altitude > 0.0)
alt_rounded = floor(altitude + 0.5);
else
alt_rounded = ceil(altitude - 0.5);

Serial.print(" alt_rounded = ");
Serial.print(alt_rounded);
Serial.print(" ");
Serial.println(" feet ");

// now speak the alt_rounded altitude value
say_sound_file_function(alt_rounded);
//*************************************************************************************************calculate altitude from pressure end
}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ use WTV20SD to speak numbers start
void say_sound_file_function(int number_to_speak)
// speak using the sound files 0000.wav to 0009.wav (0 to 9) (only) to spell out an integer of any length sent here
// e.g. 123 will become characters 1,2,3 in sequence and will load files 0001.wav etc in sequence
// calling "say_sound_file_function(minute)" will bring "int minute" to "int number_to_speak" here
{
Serial.print("looptime = "); Serial.println(looptime);
Serial.print("number_to_speak = ");Serial.println(number_to_speak);
// convert number_to_speak from int to string
String number_to_speak_as_String(number_to_speak, DEC);
//put "number_to_speak_as_String" into the array of characters "number_to_speak_as_chars"
number_to_speak_as_String.toCharArray(number_to_speak_as_chars,(number_to_speak_as_String.length()+1));//one extra for null character

// feed the characters that make up the string one by one to the speaking chip
for (int i = 0; i < number_to_speak_as_String.length(); i++)
{
Serial.print("number_to_speak_as_String - character by character = ");
Serial.println(number_to_speak_as_chars[i]);
int line_to_speak = number_to_speak_as_chars[i] - '0'; //remove any null character
// speak the sound file
wtv020sd16p.asyncPlayVoice(line_to_speak);
delay(1000); // enough to speak any single number
wtv020sd16p.stopVoice();
delay(20);
}
loops = 0; // after talking stop the beeps fo a while while things re-settle down - reset here
}
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ use WTV20SD to speak numbers end

//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% original Bakke vario code start
long getPressure()
{
long D1, D2, dT, P;
float TEMP;
int64_t OFF, SENS;

D1 = getData(0x48, 10);
D2 = getData(0x50, 1);

dT = D2 - ((long)calibrationData[5] << 8);
TEMP = (2000 + (((int64_t)dT * (int64_t)calibrationData[6]) >> 23)) / (float)100;
OFF = ((unsigned long)calibrationData[2] << 16) + (((int64_t)calibrationData[4] * dT) >> 7);
SENS = ((unsigned long)calibrationData[1] << 15) + (((int64_t)calibrationData[3] * dT) >> 8);
P = (((D1 * SENS) >> 21) - OFF) >> 15;

//Serial.println(TEMP);
//Serial.println(P);

return P;
}

long getData(byte command, byte del)
{
long result = 0;
twiSendCommand(0x77, command);
delay(del);
twiSendCommand(0x77, 0x00);
Wire.requestFrom(0x77, 3);
if(Wire.available()!=3) Serial.println("Error: raw data not available");
for (int i = 0; i <= 2; i++)
{
result = (result<<8) | Wire.read();
}
return result;
}

void setupSensor()
{
twiSendCommand(0x77, 0x1e);
delay(100);

for (byte i = 1; i <=6; i++)
{
unsigned int low, high;

twiSendCommand(0x77, 0xa0 + i * 2);
Wire.requestFrom(0x77, 2);
if(Wire.available()!=2) Serial.println("Error: calibration data not available");
high = Wire.read();
low = Wire.read();
calibrationData[i] = high<<8 | low;
Serial.print("calibration data #");
Serial.print(i);
Serial.print(" = ");
Serial.println( calibrationData[i] );
}
}

void twiSendCommand(byte address, byte command)
{
Wire.beginTransmission(address);
if (!Wire.write(command)) Serial.println("Error: write()");
if (Wire.endTransmission())
{
Serial.print("Error when sending command: ");
Serial.println(command, HEX);
}
}

void ledOn()
{
digitalWrite(led,1);
}

void ledOff()
{
digitalWrite(led,0);
}
//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% original Bakke vario code end

I believe the 12c level converter is not needed - there is one on the vario board - will test soon .

The push buttons will be used to respond to a spoken menu - there will be a slide switch on the power line and a menu will be offered at re-start.

Comments? email me