Following on the discovery of a really good Windows C compiler to replace my very old DOS version of Quick C (See: Link To Part I). Now I can share an actual example of how the Embedded Breadboarding process can used.
A
lot of times I want to implement some sort of control algorithm for
an embedded project and developing the code on the target may not be
possible for a variety of reasons like: the target PCB is out being
built and I don't have one yet, or because of cost effective reasons:
It just takes longer to debug on the actual target system and lastly:
it is often not possible to generate worst case test data (or Test
Vectors) in real life. In these cases simulation is the best route to
take.
Here
I want to implement an “Analog Meter Like” response for a LCD
display using a Microprocessors built in 10 bit ADC. What I want to
do is,
1) Simulate some actual ADC bits (from a file).
2) Test a prototype Analog Meter Averaging
Routine.
3) Save the results to to a file for analysis.
3) Save the results to to a file for analysis.
This
is where Pelles C [1] comes in. Before I could accomplish this task however, I
needed to write a couple of routines to read the simulated ADC bits
(or the Test Vector) from a text file on my PC and then be able to
feed them to a simulated ADC call and lastly rewrite the output bits
to another text fie so analyzing and graphing the results will be easy.
The
routines in Listings 1-3 below show one way to read the simulated ADC data from a file.
uint16_t ReadFileLength(char *filename)
{
FILE* fh = fopen(filename, "r");
int32_t ch;
uint16_t number_of_lines = 0;
do
{
ch = fgetc(fh);
if(ch == '\n')
number_of_lines++;
} while (ch != EOF);
// The last line doesn't end with a new line!
// but there has to be a line at least before the last line...
if(ch != '\n' && number_of_lines != 0)
number_of_lines++;
fclose(fh);
return(number_of_lines);
}
Listing 1 – First I read in the
file and count the number of lines.
void ReadTextFileToArray(char *file_name, uint16_t lines, uint16_t array[]) //(char *filename, int lines)
{
uint16_t val[lines];
FILE* fh = fopen(file_name, "r");
int current_value;
uint16_t i;
for(i = 0; i < lines; i++ )
{
fscanf(fh, "%d", ¤t_value);
array[i] = (uint16_t)current_value;
}
}
Listing
2 – When I have read the number of lines, I then allocate memory
for an array (see the main() function) and read the test data file again into the uit16 array.
int main(void)
{
uint16_t length;
char *test_vector_file = "C:\\Users\\Public\\Documents\\AdcTestVector.txt";
char *output_vector_file = "C:\\Users\\Public\\Documents\\AveragedTestVector.txt";
length = ReadFileLength(test_vector_file);
uint16_t input_values[length];
uint16_t output_values[length];
ReadTextFileToArray(test_vector_file, length, input_values);
// Call the Simulated Embedded Code
Simulation(input_values, output_values, length);
WriteArrayToTextFile(output_vector_file, output_values, length);
}
Listing 3 – A simple Main Program
that wraps up reading in a text file and putting it in an array of uint16's.
The
text file is just a simple file that contains a list of any number of
simulated ADC bits to act as a Test Vector. The routines in the
listings above count the lines in the file, then allocate memory for
a uint16 array and finally read the text file data into the array
value by value.
At
the end of all this I have a nice array of uint16 bits that can be used
as simulated ADC data to test my Analog Meter Simulated Averaging
code.
Averaging Code in C
The
Analog Meter Averaging code is based on the well known and loved
Temporal Averaging method [2]. Basically this method adds a small
portion of the latest ADC reading to a long running average value, hence
implementing a classic RC Low Pass filter in software. The key is
selecting the proper “small portion” to use. If the small portion
is too large, then the filter doesn't filter very well, if the small
portion is too small then the response is too slow. This is exactly
like changing the “C” value in a RC filter.
The
classic form of the averaging equation used is,
AVG_new
= (Xnew * Alpha) + (AVG_old * (1 - Alpha))
Where:
Alpha can range from 0 to 1.
As
can be seen, the apparent averaging increases (Equivalent RC time
constant gets longer) as Alpha gets smaller. If Alpha is one, then
there is no averaging.
For
this simple example I will use a value of alpha of 1/16,
that makes (1-Alpha) equal to 15/16. This simplifies the actual C
code from division to right shifts for the integer division. So my
averaging function becomes,
AVG
= (X >> 4) + ((AVG * 15) >> 4)
Remember
that right shifting by 4 is like dividing by 16.
Since this example is using a 16 bit uint variable to hold 10 bit simulated ADC data (as it would in the actual application) the multiply by 15 will not cause an overflow of the variable [3].
Listing
4 below puts the entire program together, including reading in the test data, processing it and writing the results back out to a file.
Simulating the Averager
My first test was using
some multipurpose test data that includes a step, for step response testing and some
noise that was generated with Octave[4] to test the filter. The noise
was simulated with random bits ranging from 0 to 1023 or full scale
for a 10 bit ADC. The results of the test are shown in Figure 1.
Figure
1- The results of running my simulated averaging filter. First a
step response, then some very large signal random noise. This gives me a
good feel for how the filter will work in the actual application. More simulations can be run in just a few seconds by writing another test vector to be used as simulated ADC bits and then analyzing the results.
Conclusion
With
just one simulation run I was able to test my averaging algorithm for
step response and filtering quickly and without using any actual
hardware. At the same time I used the actual C code and bit widths that will be used in the final application. Now I can start simulating more realistic test signals that the
application might see to check the system response. After this
simulation phase I will have a very good idea of how the actual
system will perform and there should be no surprises in the actual
application. At the end of this design and test process I can just clip my averaging code and paste it in my actual application knowing it will work as expected (and tested).
Listing
4 (Below) – The entire Pelles C source code for the project.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
uint16_t ReadFileLength(char *filename)
{
FILE* fh = fopen(filename, "r");
int32_t ch;
uint16_t number_of_lines = 0;
do
{
ch = fgetc(fh);
if(ch == '\n')
number_of_lines++;
} while (ch != EOF);
// The last line doesn't end with a new line!
// but there has to be a line at least before the last line...
if(ch != '\n' && number_of_lines != 0)
number_of_lines++;
fclose(fh);
return(number_of_lines);
}
void ReadTextFileToArray(char *file_name, uint16_t lines, uint16_t array[]) //(char *filename, int lines)
{
uint16_t val[lines];
FILE* fh = fopen(file_name, "r");
int current_value;
uint16_t i;
for(i = 0; i < lines; i++ )
{
fscanf(fh, "%d", ¤t_value);
array[i] = (uint16_t)current_value;
}
}
void WriteArrayToTextFile(char *file_name, uint16_t array[], uint16_t lines)
{
uint16_t val[lines];
FILE* fh = fopen(file_name, "w");
uint16_t i;
for(i = 0; i < lines; i++ )
{
fprintf(fh, "%d\n", (int)array[i]);
}
}
// Simulated embedded code
void Simulation(uint16_t test_vector[], uint16_t output_vector[], uint16_t length)
{
uint16_t x, avg;
avg = 0; // Initialize the average
// Loop for all the data
for(int i = 0; i < length ; i++)
{
// This simulates a GetADC() call
x = test_vector[i]; // Apply Averaging
avg = (x >> 4) + ((avg * 15) >> 4);
// Save the result in an array
output_vector[i] = avg;
}
}
int main(void)
{
uint16_t length;
char *test_vector_file = "C:\\Users\\Public\\Documents\\AdcTestVector.txt";
char *output_vector_file = "C:\\Users\\Public\\Documents\\AveragedTestVector.txt";
length = ReadFileLength(test_vector_file);
uint16_t input_values[length];
uint16_t output_values[length];
ReadTextFileToArray(test_vector_file, length, input_values);
// Call the Simulated Embedded Code
Simulation(input_values, output_values, length);
WriteArrayToTextFile(output_vector_file, output_values, length);
}
Caveat Emptor:
This example has been implemented as a LCD meter driver and filter with good results. It is fast, low memory footprint and accurate enough for that application. However a closer look will reveal that this simple implementation looses bits of precision during the X shift operation. This can be seen by inspection that the current X value is shifted down before being added to the result. Those bits that are shifted down are lost. The probable limit for this filter is around 1/32 for Alpha because beyond this too much precision will be lost for even a simple meter driver application.
Floating point in small embedded systems should be reserved for only where nothing else can be done because of it's memory footprint, time consuming calculation times and it has precision problems of it's own (A small embedded system is not a PC with it's unlimited resources). A fixed point solution can be implemented or a more normal box car type of averaging can be implemented if needed to preserve bits of precision as required (Also see Reference 5).
Extra Credit:
Just in case you are wondering how this simple filter preforms with different alpha values, I have plotted the obvious ones that are divisible by 2. These shifts are: 4 (>>2), 8 (>>3) and 16 (>>4) corresponding to 1/4, 1/8th and 1/16th sampling of the current X value. Certainly one of these should work for nearly every simple need.
Simplification:
There
are always a number of ways that any mathematical equation can be
simplified. Sometimes approximations can be used, other times
rearranging terms may be put to good use. The averaging equation here
can be simplified to one right shift (or divide), this reduces the
instructions required to do an average, but somewhat obscures the
original formula. If you are tight for clock cycles you can use this
simplified form of the equation,
avg
= (x >> 4) + ((avg * 15) >> 4); // Two Shifts
is
equivalent to (within the scope of the loss of precision),
avg
= (x + (avg * 15)) >> 4; // Only one shift
Extra Credit:
Just in case you are wondering how this simple filter preforms with different alpha values, I have plotted the obvious ones that are divisible by 2. These shifts are: 4 (>>2), 8 (>>3) and 16 (>>4) corresponding to 1/4, 1/8th and 1/16th sampling of the current X value. Certainly one of these should work for nearly every simple need.
References:
[1] Pelles C homepage: www.pellesc.de
[2] I first ran into Temporal Averaging in an Analog Devices article in the 1980's and first used it on an Apple ][ computer running Apple Basic. This technique also goes by a lot of different names, on WikiPedia the name is: Exponential Smoothing.
[3] Be careful here, I have one embedded compiler that gets confused and does not automatically cast intermediate results into the proper output width by itself. For instance the function,
int16 = int8 * int8;
Will result in an overflow with this particular compiler. Most compilers will cast the int8's into the final result bit width (here an int16) before multiplying, thus avoiding the overflow. This is one case where the simulation will fail to tell you how the actual embedded code will run. Even with simulation, you still have to thoroughly test the final application code running on the target!
[4]
GNU Octave is a matrix / numerical oriented interpreted language much
like Matlab.
[5] Richard Lyons Book: "Understanding Digital Signal Processing", contains a number of clever implementations of this type of averager and several more averager implementations also. Highly recommended, as it is as understandable as any DSP book gets.
By:
Steve Hageman / www.AnalogHome.com
We
design custom electronic Analog, RF and Embedded system for all sorts
of industrial products and clients. We would love to hear from you if
we can assist you on your next project.
No comments:
Post a Comment