Decently accurate time pulses from a raspberry pi

For stand-alone microprocessors

Decently accurate time pulses from a raspberry pi

Postby Doug Coulter » Sun Oct 13, 2019 5:41 pm

I'm doing a new data aq setup, which will be the usual raspberry pi and some arduinos. Getting good time sync between all the players so the timestamps in the resulting database is always an issue, and with say, 3 machines - on pie, two arduino nano clones - simultaneity is always a challenge.

I kinda sorta solved it at fairly low resolution by having the pi do a high priority interval timer, and drive an io pin directly. In the same thread, it does gettimeofday() to uSecond resolution and updates the file in /tmp/time.txt with the epoch and usec count as the one line in it.

Without doing anything cool/tricky (like making /tmp a tmpfs or setting nice) - this seems to have at most around 200us jitter on the tail side of my output impulse, and no drift at all vs NTP time.
That's fairly amazing for a pre-emptive opsys (linux) in a pi 3b which is what I'm testing it on. I may use a pi-4b in the real deal, dunno, other than this, speed doesn't matte much to just read in arduino data at 10Hz and send it to a database in another machine. In the existing big fusor setup, that pi - 3b is loafing doing that.

I can't yet be sure, but it looks like pretty much all the timing jitter is actually in the system call to get time once the timer thread is hit (syscalls have a habit of suspending to the opsys). Pretty sweet.

At any rate, this seems nice enough to be a cheatsheet kind of share the tip thing, so here goes.
This is just demo/example code. What it shows is how to do a periodic timer in separate thread, in C, and how to directly bit bang a pi IO pin - without the use of ?? content IO libraries or wrappers.
I added my shtick to various examples I combined and tested it. As is, it just does it's thing for 30 seconds. I will add some kind of communication and some other wrapping so this can just run as a daemon and be started and stopped say, with a message to a socket or touching some file. (this is the tricky part, that other stuff is easy, at least here where I have a pool of daemon examples).

Code: Select all
/*
Using timerfd
The timerfd interface is a Linux-specific set of functions that present POSIX timers as file descriptors (hence the fd) rather than signals
thus avoiding all that tedious messing about with signal handlers.
It was first implemented in GNU libc 2.8 and kernel 2.6.25: if you have them I highly recommend this approach.

You create a timer by calling timerfd_create() giving the POSIX clock id CLOCK_REALTIME or CLOCK_MONOTONIC.
For periodic timers such as we are creating it does not matter which you choose.
For absolute timers the expiry time is changed if the system clock is changed and the clock is CLOCK_REALTIME.
In almost all cases, CLOCK_MONOTONIC is the one to use. timerfd_create returns a file descriptor for the timer.

To set the timer running, call timerfd_settime() giving flag = TFD_TIMER_ABSTIME for an absolute timer or 0 for
relative, as we want here, and the period in seconds and nanoseconds. To wait for the timer to expire,
read from its file descriptor. It always returns an unsigned long long (8 byte unsigned integer)
representing the number of timer events since the last read, which should be one if all is going well.
If it is more than one then some events have been missed. In my example below I keep a record in "wakeups_missed".

*/

/*
* Copyright (C) 2009 Chris Simmonds (chris@2net.co.uk)
*
* This is a demonstration of periodic threads using the Linux timerfd
* interface which was introduced in GNU libc 2.8 and kernel 2.6.25.

Munged around to create just one thread, and bang an io pin on a raspi by Doug Coulter
  9/26/2019
  BC2708 is pi3
  BC2711 is pi4, change as required
  note you have to explicitly link pthread in your build
  I started with this for timing:  http://www.2net.co.uk/tutorial/periodic_threads
  and this for pi IO: https://www.raspberrypi.org/forums/viewtopic.php?t=244031
  See the github referenced from that thread to get some sample code
*/

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/timerfd.h>

#include <fcntl.h>
#include <sys/mman.h>

#include <unistd.h>  // for usleep
// Access from ARM Running Linux

#define BCM2708_PERI_BASE        0x3F000000
#define BCM2711_PERI_BASE        0xFE000000
#define GPIO_BASE                (BCM2708_PERI_BASE + 0x200000) /* GPIO controller */
#define PAGE_SIZE (4*1024)
#define BLOCK_SIZE (4*1024)

// GPIO setup macros. Always use INP_GPIO(x) before using OUT_GPIO(x) or SET_GPIO_ALT(x,y)
#define INP_GPIO(g) *(gpio+((g)/10)) &= ~(7<<(((g)%10)*3))
#define OUT_GPIO(g) *(gpio+((g)/10)) |=  (1<<(((g)%10)*3))
#define SET_GPIO_ALT(g,a) *(gpio+(((g)/10))) |= (((a)<=3?(a)+4:(a)==4?3:2)<<(((g)%10)*3))

#define GPIO_SET *(gpio+7)  // sets   bits which are 1 ignores bits which are 0
#define GPIO_CLR *(gpio+10) // clears bits which are 1 ignores bits which are 0

#define GET_GPIO(g) (*(gpio+13)&(1<<g)) // 0 if LOW, (1<<g) if HIGH

#define GPIO_PULL *(gpio+37) // Pull up/pull down
#define GPIO_PULLCLK0 *(gpio+38) // Pull up/pull down clock

////////////////////////////////////////////////////////////////////////////////////////////////////
// I/O access
volatile unsigned *gpio;
int  mem_fd;
void *gpio_map;
unsigned int rep;

////////////////////////////////////////////////////////////////////////////////////////////////////
struct periodic_info {
   int timer_fd;
   unsigned long long wakeups_missed;
};
static int thread_1_count;

////////////////////////////////////////////////////////////////////////////////////////////////////
static int make_periodic(unsigned int period, struct periodic_info *info)
{
   int ret;
   unsigned int ns;
   unsigned int sec;
   int fd;
   struct itimerspec itval;

   /* Create the timer */
   fd = timerfd_create(CLOCK_MONOTONIC, 0);
   info->wakeups_missed = 0;
   info->timer_fd = fd;
   if (fd == -1)
      return fd;

   /* Make the timer periodic */
   sec = period / 1000000;
   ns = (period - (sec * 1000000)) * 1000;
   itval.it_interval.tv_sec = sec;
   itval.it_interval.tv_nsec = ns;
   itval.it_value.tv_sec = sec;
   itval.it_value.tv_nsec = ns;
   ret = timerfd_settime(fd, 0, &itval, NULL);
   return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
static void wait_period(struct periodic_info *info)
{
   unsigned long long missed;
   int ret;

   /* Wait for the next timer event. If we have missed any the
      number is written to "missed" */
   ret = read(info->timer_fd, &missed, sizeof(missed));
   if (ret == -1) {
      perror("read timer");
      return;
   }

   info->wakeups_missed += missed;
}
////////////////////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////////////////////
static void *thread_1(void *arg)
{
   struct periodic_info info;
   struct timeval ts;
   FILE *fp;


   printf("Thread 1 period 100ms\n");
   make_periodic(100000, &info);
   while (1) { // @@@ may change this to look for an exit cue, or do that in main

   GPIO_CLR = 1<<21;  // make it low true
   GPIO_CLR = 1<<21;
// write time of day to a file here, as close as we can to when we set the pin
// this seems to take around 1ms on a pi3b
   gettimeofday(&ts,NULL);
   fp = fopen("/tmp/time.txt","w");
   fprintf(fp,"%010ld.%06ld\n",ts.tv_sec,ts.tv_usec);
   fclose(fp);

   usleep (10); // let pin stay low awhile
   GPIO_SET = 1<<21;  // set high again till hext hit
   GPIO_SET = 1<<21;
      thread_1_count++;
      wait_period(&info);
   }
   return NULL;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void setup_io()
{
   /* open /dev/mem */
   if ((mem_fd = open("/dev/mem", O_RDWR|O_SYNC) ) < 0) {
      printf("can't open /dev/mem \n");
      exit(-1);
   }

   /* mmap GPIO */
   gpio_map = mmap(
      NULL,             //Any adddress in our space will do
      BLOCK_SIZE,       //Map length
      PROT_READ|PROT_WRITE,// Enable reading & writting to mapped memory
      MAP_SHARED,       //Shared with other processes
      mem_fd,           //File to map
      GPIO_BASE         //Offset to GPIO peripheral
   );

   close(mem_fd); //No need to keep mem_fd open after mmap

   if (gpio_map == MAP_FAILED) {
      printf("mmap error %d\n", (int)gpio_map);//errno also set!
      exit(-1);
   }
\
// Always use volatile pointer!
  gpio = (volatile unsigned *)gpio_map;
// Set GPIO pin 21 to output, and high - we'll use falling edge in arduino to trigger
  INP_GPIO(21); // must use INP_GPIO before we can use OUT_GPIO
  OUT_GPIO(21); // above is original author comment, I don't have a clue (DC)
  GPIO_SET = 1<<21;
  GPIO_SET = 1<<21; // I tell you twice
} // setup_io
////////////////////////////////////////////////////////////////////////////////////////////////////
int main(int argc, char *argv[])
{
   pthread_t t_1;
    setup_io();
   printf("Periodic thread using timerfd\n");

   pthread_create(&t_1, NULL, thread_1, NULL);

   sleep(30); // @@@ in real use, do something more intelligent here, handle stop/start stuff
  GPIO_SET = 1<<21;    //leave high
   printf("thread1 %d iterations\n", thread_1_count);
   return 0;
}



Here's the dev directory on the pi I did the work on, compressed - Makefile and whatnot.
You have to be root or sudo to bang /dev/mem which is where the IO lives.
SyncFDIO.7z
Zip of the above + Makefile
(6.27 KiB) Downloaded 209 times
Posting as just me, not as the forum owner. Everything I say is "in my opinion" and YMMV -- which should go for everyone without saying.
User avatar
Doug Coulter
 
Posts: 3515
Joined: Wed Jul 14, 2010 7:05 pm
Location: Floyd county, VA, USA

Return to Embedded software

Who is online

Users browsing this forum: No registered users and 11 guests

cron