Monday, January 18, 2016

Breadboarding Embedded Code - Part II


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.

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", &current_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", &current_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; // Initilize the average to mid scale  
      // 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).


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