Close

Simple Analog Input

A project log for Setting up the LinkIt C cross-compiler

Are you a "dyed in the wool" C programmer. Love your Arduino but want something more powerful. The LinkIt Smart MT7688 board may for you!

agpcooperagp.cooper 11/03/2017 at 02:020 Comments

Simple Analog Input

Simple as in real simple!

I had a look at two I2S or SPI analog chips:

and

But I said simple as in real simple!

Here is my first design:

How does that work?

  1. The GPIO shorts the capacitor to ground (i.e. output low).
  2. the GPIO turns into an input and times the rising capacitor voltage until it exceeds 1.65 volts (i.e. 50% of 3.3v).

The main limitation is that the input voltage must be more than 1.65 volts.

The rise time of this network is in the order of 30 us to 150 us.

Extending the range down to zero

My second design:

Dual Slope?

If I connect the 91k resistor to another GPIO pin, I can measure the rise and fall times.

This give a more accurate result.

Lets get serious!

During my Internet search I came across Sigma-Delta ADC:

https://www.parallax.com/sites/default/files/downloads/AN008-SigmaDeltaADC-v1.0.pdf

Here is the design:

The second 1nF capacitor is to minimise supply voltage noise. The nominal voltage range is -0.825 V to +4.125 volts.

The following web-pages have calculators for the resistor values but some thought is required for the capacitor values or the time constant:

http://www.pulsedpower.net/Applets/Electronics/SigmaDeltaADC/SigmaDelta.html

http://www.pulsedpower.net/Applets/Electronics/SigmaDeltaADC/SigmaDeltab.html

Using with OpenWRT linux

To start I would use a timer running at 4 uS intervals (I not sure how fast signal works!).

This suggests for 8 bits precision (not accuracy!) a sample time of 1024us or a sample rate of 977 Hz.

  1. The timer interrupt would test the GPIO (in) and set the GPIO (out) the inverted value.
  2. The clock counter is incremented.
  3. If GPIO (out) was set high then the value counter is incremented.
  4. When the clock counter reaches 255, the value counter is exported (as the read value), and then both counters are reset.

Here is my untested code (waiting for me to make up an ADC board):

#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <sys/time.h>
#include <unistd.h>
#include <stdbool.h>
#include <mraa.h>

int running=true;
void sig_handler(int signo)
{
  if (signo==SIGINT) {
    printf("\nClosing down nicely\n");
    running=false;
  }
}

mraa_result_t result=MRAA_SUCCESS;
mraa_gpio_context gpioIn=NULL;
mraa_gpio_context gpioOut=NULL;

// Sigma-Delta ADC
volatile value=0;
void timer_handler (int signum)
{
  static int count=0;
  static int highCount=0;
  static int state=0;

  state=mraa_gpio_read(gpioIn);
  mraa_gpio_write(gpioOut,1-state);
  count++;
  if (state==1) highCount++;
  if (count>255) {
    count=0;
    value=highCount;
    highCount=0;
  }
}

int main(int argc, char** argv)
{
  // Install ^C trap
  signal(SIGINT, sig_handler);

  // Install timer_handler as the signal handler for SIGVTALRM
  struct sigaction sa;
  struct itimerval timer;
  memset(&sa,0,sizeof(sa));
  sa.sa_handler=&timer_handler;
  sigaction(SIGVTALRM,&sa,NULL);

  // Configure the timer to expire after 4us
  timer.it_value.tv_sec=0;
  timer.it_value.tv_usec=4;
  // and every 4us after that
  timer.it_interval.tv_sec=0;
  timer.it_interval.tv_usec=4;
  // Start the virtual timer
  setitimer(ITIMER_VIRTUAL,&timer,NULL);

  // Initialise "mraa"
  mraa_init();

  const char* board_name=mraa_get_platform_name();
  fprintf(stdout,"Welcome to MRAA\n");
  fprintf(stdout,"Version: %s\n",mraa_get_version());
  fprintf(stdout,"Running on %s (^C to exit)\n",board_name);

  // Set up gpios
  gpioIn=mraa_gpio_init(20);
  gpioOut=mraa_gpio_init(21);
  if ((gpioIn==NULL)||(gpioOut==NULL)) {
    fprintf(stdout,"Could not initialise GPIOs\n");
    return(1);
  } else {
    fprintf(stdout,"GPIO20 is input pin\n");
    fprintf(stdout,"GPIO21 is output pin\n");
  }
  mraa_gpio_dir(gpioIn,MRAA_GPIO_IN);
  mraa_gpio_dir(gpioOut,MRAA_GPIO_OUT);


  // The main loop
  while (running) {
    // Assuming 150k input and 100k feedback resistors
    float voltage=(4.950*value/255-0.825);
    fprintf(stdout,"ADC %5.2f\n",voltage);
    usleep(100000); // 100ms
  }


  // Goodbye (after ^C trap)
  mraa_gpio_close(gpioIn);
  mraa_gpio_close(gpioOut);

  return(0);
}

Here is my board design:


Found a reference to the component selection:

Basically:

Feedback Resistor (Rf) 100000 R
Capacitor (C) 0.000000001000 F
Low Pass Freq (Flp) 1592 Hz
Nyquist Freq (Fn) 3183 Hz
Clock Freq (Fclk) 250000 Hz
Oversampling Ratio (M) 79
SNR 53.4
ENOB 8.6
  1. calculate the low pass corner frequency (Flp=1/2/Pi/Rf/C)
  2. calculate the Nyquist frequency (Fn=2*Flp)
  3. calculate the oversampling ratio (M=Fclk/Fn)
  4. calculate the Signal Noise Ratio (SNR=30*log(M)-4.31)
  5. calculate the Effective Number Of Bits (ENOB=(SNR-1.76)/6.02)  

If the Flp (>Fbw) and the ENOB are okay, then good.

Board Done

I made up the board but the LinkIt does not like machine sockets.

I will have to replace them with something else, in the mean time a rubber band holds LinkIt in place:

It does not work at the moment, no surprise.

I wrote some code to check the link between GPIO20 (input) and GPIO21 (output) and it works as expected (including the RC delay).

I checked the timer interrupt code and it only works at the 10 ms scale.

I think I have to look a polling thread?

Fork Instead

Here is some code that uses fork() to start a child process and mmap() to share variables:

#include <unistd.h>
#include <sys/types.h>
#include <sys/mman.h>

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

int main(void)
{
  pid_t childPID;

  /* Parent creates mapped region prior to calling fork() */
  int *addr;              // Pointer to shared memory region
  addr=mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0);
  *addr=0;                // Initialize integer in mapped region
  bool *test;             // Pointer to shared memory region
  test=mmap(NULL,sizeof(bool),PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0);
  *test=true;             // Initialize bool in mapped region

  childPID=fork();
  if (childPID>=0) {
    // fork was successful
    if (childPID == 0) {
      // Timer
      while (true) {
        usleep(1000);
        (*addr)++;
        if ((*addr)%1000==0) *test=true;
      }
    } else {
      //Parent process
      while (true) {
        if (*test) {
          if ((*addr)%1000==0) {
            printf("Test %d\n",*addr);
          }
          *test=false;
        }
      }
    }
  } else {
    // fork failed
    printf("\n Fork failed, quitting!!!!!!\n");
    return(1);
  }
  munmap(addr,sizeof(int));

  return(0);
}

How fast will it go? I suspect context switching time will be in the order of 10 to 100 us. Too slow to work here!


Perhaps I am looking for a "magic bullet" where there is none? The answer may be to  use a SPI and a ADC chip or board.

Blink Without Delay

I was trying to avoid this approach but it works. I suppose getting it working first and then making it tidy is an option. The following code increments the counter every 4us and after 250,000 iterations prints out the elapsed time. Time slip is allowed:

#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>

int main(void)
{
  double elapsedTime;
  struct timespec timer_start,timer_end;

  bool timeout_flag=false;
  unsigned long timeout_nsecs=4000;

  struct timespec ts_start,ts_end;
  signed long duration_nsecs;
  unsigned long cnt=0;

  // start timer
  clock_gettime(CLOCK_MONOTONIC, &timer_start);
  clock_gettime(CLOCK_MONOTONIC,&ts_start);
  while (true) {

    clock_gettime(CLOCK_MONOTONIC,&ts_end);
    duration_nsecs=ts_end.tv_nsec-ts_start.tv_nsec;
    if (duration_nsecs<0) duration_nsecs+=1000000000;
    if (duration_nsecs>=timeout_nsecs) {
      timeout_flag=true;
      ts_start.tv_nsec=ts_end.tv_nsec;
    }

    if (timeout_flag) {
      timeout_flag=false;
      cnt++;
      if (cnt%250000==0) {
        // stop timer
        clock_gettime(CLOCK_MONOTONIC, &timer_end);
        // compute and print the elapsed time in millisec
        elapsedTime=(timer_end.tv_sec-timer_start.tv_sec)*1000.0;         // sec to ms
        elapsedTime+=(timer_end.tv_nsec-timer_start.tv_nsec)/1000000.0;   // nsec to ms
        printf("Elapsed time %lf ms\n",elapsedTime);
        // restart timer
        clock_gettime(CLOCK_MONOTONIC, &timer_start);
      }
      sched_yield();
    }

  }

  return(0);
}

I am getting an elapsed time of about 1096 ms which means I am missing 1 in 10 ticks due to task switching. The sched_yield() helps a bit.

This version catches up on missed ticks:

#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>

int main(void)
{
  double elapsedTime;
  struct timespec timer_start,timer_end;

  bool timeout_flag=false;
  unsigned long timeout_nsec=10000;

  struct timespec ts_start,ts_end;
  unsigned long duration_nsec;
  unsigned long cnt=0;

  // start timer
  clock_gettime(CLOCK_MONOTONIC, &timer_start);
  clock_gettime(CLOCK_MONOTONIC,&ts_start);
  while (true) {

    clock_gettime(CLOCK_MONOTONIC,&ts_end);
    if (ts_end.tv_sec==ts_start.tv_sec) {
      duration_nsec=ts_end.tv_nsec-ts_start.tv_nsec;
    } else {
      duration_nsec=1000000000+ts_end.tv_nsec-ts_start.tv_nsec;
    }
    if (duration_nsec>=timeout_nsec) {
      timeout_flag=true;
      // ts_start.tv_sec=ts_end.tv_sec;
      // ts_start.tv_nsec=ts_end.tv_nsec;
      ts_start.tv_nsec+=timeout_nsec;
      if (ts_start.tv_nsec>=1000000000) {
        ts_start.tv_nsec-=1000000000;
        ts_start.tv_sec++;
      }
    }

    if (timeout_flag) {
      timeout_flag=false;
      cnt++;
      if (cnt%100000==0) {
        // stop timer
        clock_gettime(CLOCK_MONOTONIC, &timer_end);
        // compute and print the elapsed time in millisec
        elapsedTime=(timer_end.tv_sec-timer_start.tv_sec)*1000.0;         // sec to ms
        elapsedTime+=(timer_end.tv_nsec-timer_start.tv_nsec)/1000000.0;   // nsec to ms
        printf("Elapsed time %lf ms\n",elapsedTime);
        // restart timer
        clock_gettime(CLOCK_MONOTONIC, &timer_start);
      }
      // sched_yield();
    }

  }

  return(0);
}

I tried using usleep() to slow down any missed ticks but usleep() does not work too well at the 1 us scale. There are other ways of achieving this (later). Next is to add MRAA code and perhaps (later) pushing the code into a thread. Anyway, here is the basic code:

#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <sys/time.h>
#include <unistd.h>
#include <stdbool.h>
#include <mraa.h>

bool running=true;
void sig_handler(int signo)
{
  if (signo==SIGINT) {
    printf("\nClosing down nicely\n");
    running=false;
  }
}

mraa_result_t result=MRAA_SUCCESS;
mraa_gpio_context gpioIn=NULL;
mraa_gpio_context gpioOut=NULL;


int main(int argc, char** argv)
{
  // Install ^C trap
  signal(SIGINT, sig_handler);

  // Initialise "mraa"
  mraa_init();

  const char* board_name=mraa_get_platform_name();
  fprintf(stdout,"Welcome to MRAA\n");
  fprintf(stdout,"Version: %s\n",mraa_get_version());
  fprintf(stdout,"Running on %s (^C to exit)\n",board_name);

  // Set up gpios
  gpioIn=mraa_gpio_init(20);
  gpioOut=mraa_gpio_init(21);
  if ((gpioIn==NULL)||(gpioOut==NULL)) {
    fprintf(stdout,"Could not initialise GPIOs\n");
    return(1);
  } else {
    fprintf(stdout,"GPIO20 is input pin\n");
    fprintf(stdout,"GPIO21 is output pin\n");
  }
  mraa_gpio_dir(gpioIn,MRAA_GPIO_IN);
  mraa_gpio_dir(gpioOut,MRAA_GPIO_OUT);


  /* The main loop */
  int value=0;
  unsigned int count=0;
  unsigned int highCount=0;
  unsigned int state=0;

  // Timer
  struct timespec ts_start,ts_end;
  unsigned long duration_nsec;
  unsigned long timeout_nsec=4000;
  bool timeout_flag=false;

  // start timer
  clock_gettime(CLOCK_MONOTONIC,&ts_start);
  while (running) {

    clock_gettime(CLOCK_MONOTONIC,&ts_end);
    if (ts_end.tv_sec==ts_start.tv_sec) {
      duration_nsec=ts_end.tv_nsec-ts_start.tv_nsec;
    } else {
      duration_nsec=1000000000+ts_end.tv_nsec-ts_start.tv_nsec;
    }
    if (duration_nsec>=timeout_nsec) {
      timeout_flag=true;
      // ts_start.tv_sec=ts_end.tv_sec;
      // ts_start.tv_nsec=ts_end.tv_nsec;
      ts_start.tv_nsec+=timeout_nsec;
      if (ts_start.tv_nsec>=1000000000) {
        ts_start.tv_nsec-=1000000000;
        ts_start.tv_sec++;
      }
    }

    if (timeout_flag) {
      timeout_flag=false;
      // Sigma-Delta ADC
      state=mraa_gpio_read(gpioIn);
      if (state==1) highCount++;
      mraa_gpio_write(gpioOut,1-state);
      count++;
      if (count>255) {
        count=0;
        value=(value+highCount)>>1;
        highCount=0;
      }
    }

  }


  // Goodbye (after ^C trap)
  mraa_gpio_close(gpioIn);
  mraa_gpio_close(gpioOut);

  // Print the read value
  printf("Average value %d\n",value);
  printf("Average voltage %6.3f\n",4.95*value/255-0.825);


  return(0);
}

With the analog in grounded I got:

root@mylinkit:~/Code# ./mraa_ADC.run
Welcome to MRAA
Version: v0.8.0
Running on LinkIt Smart 7688 (^C to exit)
GPIO20 is input pin
GPIO21 is output pin
^C
Closing down nicely
Average value 37
Average voltage -0.107

---

With the analog in at 3.3v I got:

root@mylinkit:~/Code# ./mraa_ADC.run
Welcome to MRAA
Version: v0.8.0
Running on LinkIt Smart 7688 (^C to exit)
GPIO20 is input pin
GPIO21 is output pin
^C
Closing down nicely
Average value 122
Average voltage  1.543

---

Which is wrong but at least it is going in the right direction.

---

Okay, the problem was the rubber band fix!

The 3v3 pin was not making contract.

On the oscilloscope the duty cycle was not even so I need to balance the code.

---

With no code edits here is analog in grounded:

root@mylinkit:~/Code# ./mraa_ADC.run
Welcome to MRAA
Version: v0.8.0
Running on LinkIt Smart 7688 (^C to exit)
GPIO20 is input pin
GPIO21 is output pin
^C
Closing down nicely
Average value 36
Average voltage -0.126

---

With the analog in at 3.27 volts:

root@mylinkit:~/Code# ./mraa_ADC.run
Welcome to MRAA
Version: v0.8.0
Running on LinkIt Smart 7688 (^C to exit)
GPIO20 is input pin
GPIO21 is output pin
^C
Closing down nicely
Average value 202
Average voltage  3.096

---

With the analog in at 4.94 volts:

root@mylinkit:~/Code# ./mraa_ADC.run
Welcome to MRAA
Version: v0.8.0
Running on LinkIt Smart 7688 (^C to exit)
GPIO20 is input pin
GPIO21 is output pin
^C
Closing down nicely
Average value 255
Average voltage  4.125

(i.e. maximum value)

---

With the analog in floating:

root@mylinkit:~/Code# ./mraa_ADC.run
Welcome to MRAA
Version: v0.8.0
Running on LinkIt Smart 7688 (^C to exit)
GPIO20 is input pin
GPIO21 is output pin
^C
Closing down nicely
Average value 121
Average voltage  1.524
root@mylinkit:~/Code#

---

So other than some tweaks, it works!

---

Calibrated the ADC with:

Here is a plot of the (raw) data for 0v (value=36) and 3v29 (value=201):

Not the noise (due to task switching).

Here is is smoothed (as per the code above):

---

Reworked the code to read samples twice as fast (1.95 ksps) and minimised context switches with "sched_yield()" here are the (smoothed) results:

This looks to be as good as it gets.

AlanX

Discussions