Raspberry PI DAQ system

Thread Starter

nsaspook

Joined Aug 27, 2009
13,306
This is a project I'm working on to provide a Linux kernel module for the GPIO and analog in/out ports on the Gertboard or a addon PIC18 controller with a slave SPI interface to the RPi.

The first objective was to write a Comedi compatible Linux module that would interface with the I/O pins in a standard manner usable with the Comedi data acquisition library called daq_gert. The analog interface code is still being designed.

Comedi home page.
http://www.comedi.org/

A RPi Debian based HOWTO and C source code are here.
Howto: https://github.com/nsaspook/daq_gert/blob/master/RPi_Comedi_HOWTO.txt
Source code: https://github.com/nsaspook/daq_gert.git

The second objective is the PIC18 based ADC module that can provide 12 10bit ADC channels and 8 additional I/O pins using the on-board SPI master in the RPi board for communication. This part of the project is in the very early stages of testing but the current software does demonstrate interrupt driven ADC and SPI interface code.

See RPi_PIC directory in the above source code location.

A blinking LED demo of the software running on the RPi board. http://flic.kr/p/dh4ur3
 

Thread Starter

nsaspook

Joined Aug 27, 2009
13,306
The Raspberry PI daq driver also includes code for my PIC ADC channel SPI expander. The MCP3002 chip is removed and a dip 8 header connected to the off-board controller is inserted. When the daq_gert module is loaded it can detect the expander and configure the linux driver to use it instead of the normal 2 channel input chip. The expander code running on a 28pin 18f25k22 chip can provide 11 extra 10bit channels analog channels.

Some prototype pictures:
http://flic.kr/p/dMFtWS
http://flic.kr/p/dMzVvp

Code: https://github.com/nsaspook/daq_gert/blob/master/RPi_PIC/SlaveO.c
 

Thread Starter

nsaspook

Joined Aug 27, 2009
13,306
While updating my RPi linux driver for the new Raspberry PI 2 I ran into a problem many others have, the board is camera-shy.
It's a great board that's light years faster than the original but just don't use a flash for a photo on the bare board.
A photo using my trusty Olympus with a true Xenon flash of my prototype cube caused a lockup.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
13,306
Spent some spare time getting this to work with the new board.
Completed a (mainly) working version of the RPi 2 kernel module AI/AO/DIO driver with async command control (background channel scanning) for the Linux DAQ library so Comedi DAQ applications like XOSCOPE can be used.

The program is running on the PI but is displayed on a remote Linux PC via ssh X forwarding.

It's pretty limited in two channel analog sample speed due to the slow ADC chip (MCP3202 12 bit resolution 100k samples/second so SPI is limited to 1mhz) on the Gertboard and no DMA for memory transfers but I should be able to tweak the speed up to at least a few thousand samples per second using native the hardware and SPI without DMA.

The 4 core RPi 2 makes real kernel threads possible on the hardware so I'm only using 20% for daq I/O on one while the scope software uses 90%+ on another core.
https://github.com/nsaspook/daq_gert/blob/master/daq_gert.c
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
13,306
I've added the code in my kernel protocol driver to speed up the sample rate to 1024 samples in a chunk at the SPI device kernel level instead of single requests. This has the effect of reducing the scope software user-mode CPU use to less than 50% while speeding the max possible scope sample rate to at least 10k per second. The kernel thread CPU use has increased with the lack of DMA but that's ok with 4 cores.
With full DMA the speed should be at least 20k and usable for lo-fi audio waveforms.

The RPi SPI hardware interface is the current bottleneck but good people are working on it.
https://github.com/msperl/spi-bcm2835/wiki
https://www.kernel.org/doc/Documentation/spi/spi-summary
The Linux SPI system uses a work and messages queueing mechanism that while good is not real-time, especially at the user level.

Code:
From my driver:
* In the async command mode transfers can be handled in HUNK mode by creating a SPI message
* of many conversion sequences into one message, this allows for close to native driver wire-speed
* An optimized DMA driver is in the works
* https://github.com/msperl/spi-bcm2835/wiki
* (the current interrupt driven kernel driver is limited to a 12 to 64 byte FIFO and no DMA) HUNK_LEN data samples
* into the Comedi read buffer with a special mix_mode for sampling both ADC devices in an alt sequence for
* programs like xoscope at full speed. The transfer array is currently static but can easily be made into
* a config size parameter runtime value if needed with kmalloc for the required space

// lets talk to the device in a blast on the wire

#define HUNK_LEN   1024
struct comedi_control {
   u8 *tx_buff;
   u8 *rx_buff;
   struct spi_transfer t[HUNK_LEN];
   struct mutex daqgert_platform_lock;
};

...
static unsigned int daqgert_ai_get_sample(struct comedi_device *dev,
   struct comedi_subdevice *s)
{
...
  struct spi_param_type *spi_data = s->private;
  u8 *tx_buff, *rx_buff;
...
    memset(&pdata->t, 0, sizeof(pdata->t)); // clear the transfer array
     if (devpriv->ai_hunk) { /* for single channel command scans with pre-formatted tx_buffer*/
       if (spi_data->device_type == MCP3002) { // 10 bit adc data
         len = 2;
       } else {
         len = 3;
       }
       tx_buff = pdata->tx_buff;
       rx_buff = pdata->rx_buff;
       for (i = 0; i < HUNK_LEN; i++) { /* be sure we toggle CS between ADC commands */
         pdata->t[i].cs_change = 1;
         pdata->t[i].len = len;
         pdata->t[i].tx_buf = tx_buff;
         pdata->t[i].rx_buf = rx_buff;
         tx_buff += len; /* move the buffer ptr to the next transfer slot in the buffer memory */
         rx_buff += len;
       }
       spi_message_init_with_transfers(&m, &pdata->t[0], HUNK_LEN); // make the proper message with the transfers
...
    spi_sync(spi_data->spi, &m); // exchange SPI data
...
}
 

Thread Starter

nsaspook

Joined Aug 27, 2009
13,306
Still hacking the RPi2 driver to something usable.:)
Things are looking pretty good for the MCP3X02 ADC driver section of the code. I can get a 50usec per sample avg over a 1 second burst period using xoscope with 25usecs for wire-speed and ~25 for CPU overhead without DMA. Without DMA the output data stream is not totally continuous at full speed as I have to stop the SPI transfer to process and copy data but that can be fixed with a double-buffer on the SPI side by using another thread and core. With a driver request for less than full speed sample rates the software inserts calculated delays between samples (or groups of samples during a two channel scan) to adjust the sample to the correct time-splice. I'm thinking about having the option to always run a full speed and then integrate the extra samples (costing more CPU cycles) into one per the requested time-splice. (runs at 20,000 S/sec, requests for 1000 S/sec, driver integrates every 20 samples to 1 for output to the data consumer)
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
13,306
I think you should have to quit the SPI exchange to procedure and duplicate information but that can be set with a double-buffer on the SPI part by using another line and primary.
That's just about what I said before :p but I think I have a better idea of modifying the transmit command buffer on the fly.:) It's seems easy as a concept if it were just a simple polled system. The Comedi command sequences can be up to 256 separate channel, range, counts and timing parameters in one block. The driver communicates back to the user program with what it can do by modifying the sent commands and then the user program can accept that to be run continuously in background by the kernel thread (at Linux Ring zero for something close to real-time priority) or quit. A real DMA driver for SPI is expected in the Raspberry kernel at version 4.1 (The RPi foundation kernel is still at 3.18 and 4.01 is out) so all of this tricker won't be needed much longer to maintain precise sample timing. The speed won't be much faster but the timing will be hard locked to the DMA/CPU clock instead of a stream of instructions in the CPU execution cache.

The driver also has the capability to use a PIC18 or 24 device to offload most of the hardware interface timing issues from the RPi but that currently that only works in sync/polled mode.
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
13,306
This is pretty close to max with what you can get with the current RPi2 4.0.y driver via SPI with a 12bit ADC/DAC system on the Gertboard. (1Mhz ADC & 8Mhz DAC clocks)

Two analog 8bit (from a simple 256 entry table) signals generated on the RPi2 using a separate program (bmc) with the Comedilib programming interface via the two AO MCP4822 DAC channels looped back into the two AI MCP3202 ADC channels with Xoscope (via X on another machine) using Comedi as the data source at 10KS/s with two channels per scan. The driver is working in a unblocked mode here so long term timing is affected but in blocked mode for AI sampling it has the SPI bus for 1000 sample blocks in a Kthread at a time so timing is much better at the expense of other SPI users like AO.



 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
13,306
Another video using asynchronous commands to both generate and display several waveforms concurrently. Linux Kthreads (each to a core on the RPi2) are used by two I/O scan sequence commands to share the SPI link to the analog devices. I'm missing the real DMA driver (still waiting for the 'official PI SPI with DMA kernel driver' to be released) so the timing jitter is nasty at high sample rates when the SPI bus is not locked to the I/O data stream for a 1000 sample periods as seen in these demos.


The ao_waveform program source is from the Debian comedilib demo source directory.
https://packages.debian.org/sid/i386/libcomedi-dev/filelist

board_info from the driver
overall info:
version code: 0x00074c
driver name: daq_gert
board name: Gertboard
number of subdevices: 3
subdevice 0:
type: 5 (digital I/O)
flags: 0x00030000
number of channels: 17
max data value: 1
ranges:
all chans: [0,5]
command:
not supported
subdevice 1:
type: 1 (analog input)
flags: 0x00319000
number of channels: 2
max data value: 4095
ranges:
all chans: [0,3.3]
command:
start: now|int
scan_begin: follow|timer
convert: timer
scan_end: count
stop: none|count
command fast 1chan:
start: now 0
scan_begin: follow 0
convert: timer 50000
scan_end: count 1
stop: count 2
subdevice 2:
type: 2 (analog output)
flags: 0x00125000
number of channels: 2
max data value: 4095
ranges:
all chans: [0,2.048]
command:
start: now|int
scan_begin: timer
convert: now
scan_end: count
stop: none|count
command fast 1chan:
start: now 0
scan_begin: timer 2250
convert: now 0
scan_end: count 1
stop: count 2
ao_waveform -v -c 0 -f /dev/comedi0_subd2 -n0
-v verbose debug
-c 0 first channel of the subdevice
-f /dev/comedi0_subd2 is the AO device file created by the driver when it loads during boot
-n 0 first waveform type

Code:
...[B]First it creates a command to submit to the driver:[/B]
  cmd.subdev = options.subdevice;
  cmd.flags = CMDF_WRITE;
  cmd.start_src = TRIG_INT;
  cmd.start_arg = 0;
  cmd.scan_begin_src = TRIG_TIMER;
  cmd.scan_begin_arg = 1e9 / options.freq;
  cmd.convert_src = TRIG_NOW;
  cmd.convert_arg = 0;
  cmd.scan_end_src = TRIG_COUNT;
  cmd.scan_end_arg = options.n_chan;
  cmd.stop_src = TRIG_NONE;
  cmd.stop_arg = 0;

  cmd.chanlist = chanlist;
  cmd.chanlist_len = options.n_chan;

... [B]sends it to the driver[/B]

  if ((err = comedi_command(dev, &cmd)) < 0) {
  comedi_perror("comedi_command");
  exit(1);

... [B]preloads the buffer some data to size check[/B]

  dds_output(data,BUF_LEN);
  n = BUF_LEN * sizeof(sampl_t);
  m = write(comedi_fileno(dev), (void *)data, n);
  if(m < 0){
  perror("write");
  exit(1);
  }else if(m < n)
  {
  fprintf(stderr, "failed to preload output buffer with %i bytes, is it too small?\n"
  "See the --write-buffer option of comedi_config\n", n);
  exit(1);
  }
  if (options.verbose)
  printf("m=%d\n",m);

...[B] triggers the command to start[/B]

  ret = comedi_internal_trigger(dev, options.subdevice, 0);
  if(ret < 0){
  perror("comedi_internal_trigger\n");
  exit(1);
  }

...[B]writes more data[/B]

  while(1){
  dds_output(data,BUF_LEN);
  n=BUF_LEN*sizeof(sampl_t);
  while(n>0){
  m=write(comedi_fileno(dev),(void *)data+(BUF_LEN*sizeof(sampl_t)-n),n);
  if(m<0){
  perror("write");
  exit(0);
  }
  if (options.verbose)
  printf("m=%d\n",m);
  n-=m;
  }
  total+=BUF_LEN;
  //printf("%d\n",total);
  }

  }
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
13,306
Some interesting points on Linux drivers and the kernel in general.
http://kernelnewbies.org/FAQ/LinkedLists
Many Linux device structures are managed by lists/queues/stacks and the Linux kernel provides several easy to use methods for them. This driver is a very simple example.

In this RPi driver the SPI master hardware (with it's own kernel driver) has two native chip selects so two devices are created for the protocol code to use. CS0 is used for ADC devices and CS1 is used for DAC devices with each device having channel selects in their respective command codes.

When the RPi boots, device tables in the kernel (or user supplied tables) tell it which module(s) to load for each device probed. For this driver I created a table entry that selects my driver when the SPI master kernel module code is loaded.

in arch/arm/mach-bcm2709/bcm2709.c
Code:
#ifdef CONFIG_BCM2708_SPIDEV
static struct spi_board_info bcm2708_spi_devices[] = {
#ifdef CONFIG_SPI_SPIDEV
  {
  .modalias = "spidev",
  .max_speed_hz = 500000,
  .bus_num = 0,
  .chip_select = 0,
  .mode = SPI_MODE_0,
  }, {
  .modalias = "spidev",
  .max_speed_hz = 500000,
  .bus_num = 0,
  .chip_select = 1,
  .mode = SPI_MODE_0,
  }
#endif
#ifdef CONFIG_SPI_COMEDI
  {
  .modalias = "spigert",
  .max_speed_hz = 500000,
  .bus_num = 0,
  .chip_select = 0,
  .mode = SPI_MODE_0,
  }, {
  .modalias = "spigert",
  .max_speed_hz = 500000,
  .bus_num = 0,
  .chip_select = 1,
  .mode = SPI_MODE_0,
  }
#endif
};
#endif
if CONFIG_SPI_COMEDI is set when the kernel is created then my module "spigert" is loaded and probed with two SPI devices with chip_select = 0, chip_select = 1.
...
The 'probe' code in my driver then looks for the correct chip_select and puts the device into a Linux linked list global structure for the program so other functions 'daq_gert' in that program can use it later.
Code:
/*
* Do only two chip selects for the Gertboard
*/
if (spi->chip_select == CSnA) {
/*
* get a copy of the slave device 0 to share with comedi
* we need a device to talk to the ADC
*/
INIT_LIST_HEAD(&pdata->device_entry); /* create entry into the Comedi device list */
pdata->slave.spi = spi;
list_add_tail(&pdata->device_entry, &device_list); /* put entry into the Comedi device list */
}
if (spi->chip_select == CSnB) {
/*
* we need a device to talk to the DAC
*/
INIT_LIST_HEAD(&pdata->device_entry);
pdata->slave.spi = spi;
list_add_tail(&pdata->device_entry, &device_list);
}
This creates a new entry for each correct chip_select seen during the two probes using a link structure in my device structure pdata->device_entry a with: INIT_LIST_HEAD, it then adds that entry to the queue 'device_list' with: list_add_tail
....
After the probes are completed the Comedi DAQ protocol part of the driver is auto-loaded.
Code:
if (gert_autoload)
ret = comedi_auto_config(&slave_spi->spi->master->dev, &daqgert_driver, 0);
...
This part of the driver needs to find SPI devices for it to send and receive analog data so it searches the spigert 'device_list' queue for matching devices from a 'board' structure.
Code:
static const struct daqgert_board daqgert_boards[] = {
{
.name = "Gertboard",
.board_type = 0,
.n_aichan = 2,
.n_aochan = 2,
.ai_ns_min = 50000, /* values plus software overhead */
.ai_ns_min_calc = 35000,
.ai_rate_min = 20000,
.ao_ns_min = 5000,
.ao_ns_min_calc = 4500,
.ao_rate_min = 10000,
.ai_cs = 0,
.ao_cs = 1,
.ai_max_speed_hz = 1000000,
.ao_max_speed_hz = 8000000,
},
....
static int32_t daqgert_auto_attach(struct comedi_device *dev, unsigned long unused_context)
{
const struct daqgert_board *thisboard = &daqgert_boards[gert_type];
struct comedi_subdevice *s;
int32_t ret, i;
int32_t num_ai_chan, num_ao_chan, num_dio_chan = NUM_DIO_CHAN;
struct daqgert_private *devpriv;
struct comedi_spigert *pdata;
struct spi_param_type *slave_spi_adc=NULL, *slave_spi_dac=NULL;
....
/*
* loop the spi device queue for needed devices
*/
if (list_empty(&device_list))
return -ENODEV;

list_for_each_entry(pdata, &device_list, device_entry)
{
if (pdata->slave.spi->chip_select == thisboard->ai_cs) {
slave_spi_adc = &pdata->slave;
pdata->slave.spi->max_speed_hz = thisboard->ai_max_speed_hz;
spi_setup(pdata->slave.spi);
dev_info(dev->class_dev, "setup: spi cd %d: %d Hz: assigned to adc devices\n",
pdata->slave.spi->chip_select, pdata->slave.spi->max_speed_hz);
} else {
slave_spi_dac = &pdata->slave;
pdata->slave.spi->max_speed_hz = thisboard->ao_max_speed_hz;
spi_setup(pdata->slave.spi);
dev_info(dev->class_dev, "setup: spi cd %d: %d Hz: assigned to dac devices\n",
pdata->slave.spi->chip_select, pdata->slave.spi->max_speed_hz);
}
}
/*
* check for possible bad spigert table entry
*/
if (!slave_spi_adc || !slave_spi_dac)
return -ENODEV;
We only have 0 and 1 for chip_selects here so a simple if statement works to see if we have a match to the needed 'thisboard->ai_cs' structure element and then copy the queue spi pointer (&pdata->slave) to the local pointer variable (slave_spi_adc/slave_spi_dac) for actual data transmission use from the 'pdata' structure pointer of the list entry in the program.

It seems a complex way just to setup a SPI device the Linux way but by using non-global pointers to variables, dynamic variables with queues and lists it's possible to write easily reusable C code that can automatically adjust to changes in the hardware with just a few table entries, avoid data duplication and reduce the number of program locks needed for correct operation on multi-processor machines like the new RPI2 4 core board.
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
13,306
The next step is to use the RPi2 multi-core capability.

When kernel threads are created they run in the same memory space but can execute on different 'nodes' with SMP machine hardware. The RPi2 has four cores so we want to make each of our two SPI i/o threads have it's own CPU to reduce context switching latencies and to clock our sampling periods from the CPU the thread is running on instead of the user process CPU.

First we check the number of CPU cores online and update the cores we want to run on from the board data structure.
Code:
... added node information
static const struct daqgert_board daqgert_boards[] = {
   {
     .name = "Gertboard",
     .board_type = 0,
     ....
     .ai_node = 3,
     .ao_node = 2,
   },
.....
  /*
    * setup kthreads on other cores if possible
    */
   cpu_nodes = num_online_cpus();
   dev_info(dev->class_dev, "%d cpu(s) online for threads\n", cpu_nodes);
   if (cpu_nodes >= 4) {
     devpriv->ai_node = thisboard->ai_node;
     devpriv->ao_node = thisboard->ao_node;
   }
With this information we can call a thread setup routine using nodes 2&3 for execution units (the devpriv structure memory is set to zero in kzalloc so the default nodes are 0 if the cpu_node test fails).
Code:
/*
* make two threads for the i/o streams
*/
static int32_t daqgert_create_thread(struct comedi_device *dev, struct daqgert_private *devpriv)
{
   const char hunk_thread_name[] = "daqgerth", thread_name[] = "daqgert";
   const char *name_ptr;

   if (devpriv->hunk)
     name_ptr = hunk_thread_name;
   else
     name_ptr = thread_name;

   devpriv->ai_spi->daqgert_task = kthread_create_on_node(&daqgert_ai_thread_function, (void *) dev,
     cpu_to_node(devpriv->ai_node), "%s_a/%d", name_ptr, devpriv->ai_node);
   if (!IS_ERR(devpriv->ai_spi->daqgert_task)) {
     kthread_bind(devpriv->ai_spi->daqgert_task, devpriv->ai_node);
     wake_up_process(devpriv->ai_spi->daqgert_task);
   } else
     return PTR_ERR(devpriv->ai_spi->daqgert_task);

   devpriv->ao_spi->daqgert_task = kthread_create_on_node(&daqgert_ao_thread_function, (void *) dev,
     cpu_to_node(devpriv->ao_node), "%s_d/%d", name_ptr, devpriv->ao_node);
   if (!IS_ERR(devpriv->ao_spi->daqgert_task)) {
     kthread_bind(devpriv->ao_spi->daqgert_task, devpriv->ao_node);
     wake_up_process(devpriv->ao_spi->daqgert_task);
   } else
     return PTR_ERR(devpriv->ao_spi->daqgert_task);

   return 0;
}
We check if 'hunk' (SPI master controlled sample timing) is possible and then select a process name for the thread with an 'h' if true.
Creating the actual thread on the node is next with the name and the wanted node number with a pointer to the task returned with the process sleeping. The error condition for thread creation is a little special. A possible error condition is encoded as a lower value of the returned pointer than normal so IS_ERR is used to detect this condition and is returned as a 'normal' error code by PTR_ERR. The process is then started with wake_up_process.
....

To get the best timing between samples without direct DMA hardware timing we use the hrtimer subsystem. On the RPi this runs at 64-bit 1Mhz but the Linux hrtimer resolution is 64-bit 1nsec so we are really limited to +-1 usec best case precision. (see attachment)
Because now the code is running on a SMP host with multi-level caches on each core we need to make sure state flags are ATOMIC and are updated on all cores, this is normally handled with smp_mb_* 'memory barrier' functions and atomic machine assembly coded bit operations.
https://www.kernel.org/doc/Documentation/memory-barriers.txt
https://www.kernel.org/doc/Documentation/atomic_ops.txt
Code:
       smp_mb__before_atomic();
       set_bit(SPI_AO_RUN, &devpriv->state_bits);
       smp_mb__after_atomic();

      __set_current_state(TASK_UNINTERRUPTIBLE);
       pdata->kmin = ktime_set(0, pdata->delay_nsecs);
       schedule_hrtimeout_range(&pdata->kmin, 0, HRTIMER_MODE_REL_PINNED);
The hrtimer system uses rbtrees to sort events.
https://www.kernel.org/doc/Documentation/rbtree.txt

A 'top' screen-shot of the threads running on separate cores using the 'ps ax' command with the system generating and displaying a sawtooth wave to check for timing variations.
 

Attachments

Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
13,306
Now that most of the SPI based routines are running it's time to check the actual timing so corrections can be made and maybe some software routines can be optimized for better performance. I used the old Tek 2465 triggered on the input start bit to look at the input, clock and output data lines on the MCP3202 ADC chip. The RPi2 SPI master is programmed to send the data in 3 8bit segments.

Top trace input, middle clock, bottom output data.

1Mhz clock with SPI mode 3

The total time between samples is about ~45us with the software overhead (~26us for data transmission) but the desired design time is 50us for a 20khz max sample rate so a timing delay correction will be added between samples to correct for that. The actual average sample jitter over several seconds is not too bad for a non RT Linux system.
The output bits are from the RPi2 generated analog sine wave signal.
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
13,306
If you use the RPi and need to write your own device driver then you must understand at some level how things are configured at boot. The traditional Linux way is to use kernel module tables and files to control and configure hardware but on most new embedded systems and now for the 'new way' on the SoC RPi you will use 'device-tree' files instead. The original concept is from the PowerPC Open-Firmware line of systems: https://ols.fedoraproject.org/OLS/Reprints-2008/likely2-reprint.pdf

http://events.linuxfoundation.org/sites/events/files/slides/petazzoni-device-tree-dummies.pdf
Video of the above pdf:
The Raspberry Pi's version with some needed changes is here: https://www.raspberrypi.org/documentation/configuration/device-tree.md
The 'overlay' tree for the daq_gert module is simple as it uses the existing platform tree and modifies only a few things.

1. It's compatible with three existing SPI driver platform structures that create the spi0.0 and spi0.1 devices.
2. A existing protocol driver for accessing the SPI devices is disabled so I can use them. spidev
3. I then specify what gpio pins the master spi0 will use to communicate with the devices. spi0_pins
4. The information needed to use the devices code with my driver is given. spigert0/1
(listed in the compatible platform files )
Code:
/*
* Device Tree overlay for the spigert module used by Comedi daq_gert analog devices
*
*/

/dts-v1/;
/plugin/;

/ {
  compatible = "brcm,bcm2835", "brcm,bcm2708", "brcm,bcm2709";
  /* disable spi-dev for spi0.0 & spi0.1 */
  fragment@0 {
   target = <&spi0>;
   __overlay__ {
     status = "okay";

     spidev@0{
       status = "disabled";
     };
     spidev@1{
       status = "disabled";
     };
   };
  };

  fragment@1 {
   target = <&gpio>;
   __overlay__ {
  spi0_pins: spi0_pins {
  brcm,pins = <7 8 9 10 11>;
  brcm,function = <4>; /* alt0 */
  };
   };
  };

  fragment@2 {
   target = <&spi0>;
   __overlay__ {
  #address-cells = <1>;
  #size-cells = <0>;
  pinctrl-names = "default";
  pinctrl-0 = <&spi0_pins>;

  spigert@0 {
  compatible = "spigert";
  reg = <0>;  /* CE0 */
  spi-max-frequency = <500000>;
     status = "okay";
  };

  spigert@1 {
  compatible = "spigert";
  reg = <1>;  /* CE1 */
  spi-max-frequency = <500000>;
     status = "okay";
  };
   };
  };
};
Output from the running system device-tree: dtc -I fs /proc/device-tree
Code:
  spi@7e204000 {
  reg = <0x7e204000 0x1000>;
  dmas = <0x3 0x6 0x3 0x7>;
  interrupts = <0x2 0x16>;
  pinctrl-0 = <0x27>;
  compatible = "brcm,bcm2835-spi";
  cs-gpios = <0x0 0x0>;
  clocks = <0x7>;
  status = "okay";
  #address-cells = <0x1>;
  phandle = <0x13>;
  #size-cells = <0x0>;
  dma-names = "tx", "rx";
  pinctrl-names = "default";
  linux,phandle = <0x13>;

  spidev@0 {
  reg = <0x0>;
  compatible = "spidev";
  spi-max-frequency = <0x7a120>;
  status = "disabled";
  #address-cells = <0x1>;
  #size-cells = <0x0>;
  };

  spidev@1 {
  reg = <0x1>;
  compatible = "spidev";
  spi-max-frequency = <0x7a120>;
  status = "disabled";
  #address-cells = <0x1>;
  #size-cells = <0x0>;
  };

  spigert@0 {
  reg = <0x0>;
  compatible = "spigert";
  spi-max-frequency = <0x7a120>;
  status = "okay";
  };

  spigert@1 {
  reg = <0x1>;
  compatible = "spigert";
  spi-max-frequency = <0x7a120>;
  status = "okay";
  };
  };
Output from the system boot log:
Code:
[35090.568221] daq_gert: module is from the staging directory, the quality is unknown, you have been warned.
[35090.570027] spigert spi0.1: setup: cd 1: bpw 8, mode 0x3
[35090.570260] spigert spi0.0: setup: cd 0: bpw 8, mode 0x3
[35090.571456] comedi comedi0: setup: spi cd 1: 8000000 Hz: assigned to dac devices
[35090.571488] comedi comedi0: setup: spi cd 0: 1000000 Hz: assigned to adc devices
[35090.571821] comedi comedi0: Gertboard WiringPi pins setup
[35090.571845] comedi comedi0: RPi new scheme rev a21041, serial 00000000f3083d7a, new rev 1
[35090.571863] comedi comedi0: driver gpio board rev 3
[35090.571885] comedi comedi0: Gertboard WPi pins set [0..7] to outputs
[35090.571901] comedi comedi0: Gertboard spi slave device detection started
[35090.572242] comedi comedi0: Gertboard adc board pre detect code 0, daqgert_conf option value 1
[35090.572400] comedi comedi0: Gertboard adc chip board detected, 2 channels, range code 0, device code 0, PIC code 0, detect code 0
[35090.572452] comedi comedi0: 4 cpu(s) online for threads
[35090.572987] comedi comedi0: daq_gert attached: gpio iobase 0x3f200000, ioremaps 0xbe5e0000  0xf3003000, io pins 0x0, 1Mhz timer value 0x8:0x2badae9c
[35090.580193] comedi comedi0: driver 'daq_gert' has successfully auto-configured 'Gertboard'.
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
13,306
Timing stability from the new spi-2835 RPi driver, still no DMA but it's much better.

Top to bottom (MCP3202/channel 0 then 1) with delay_usecs working great: data in(trigger), chip select, clock (1mhz), data out



Top to bottom (MCP3202/channel 0 then 1) with delay_usecs: data in, chip select(trigger), clock (1mhz), data out
 

Thread Starter

nsaspook

Joined Aug 27, 2009
13,306
Fixed some bugs in my driver and worked around some quirks in the spi framework for Linux to finally get the correct timing.

The project target is precisely timed 20k S/sec with samples from ADC/DAC channels 0&1 as close as possible then delay for the correct amount of time for two samples before restarting the process in a 1000 sample transfer queue as one message from the spi master to reduce timing jitter. It's off frequency by about 1% over the needed S/sec but well within tolerance for regular Linux without direct SPI DMA timing.

The old 2465 with all the options is still the wonder of the analog scope world.


 

Thread Starter

nsaspook

Joined Aug 27, 2009
13,306
The protocol driver has been updated to use the ADS8330 adc chip as an option. I needed something with a higher resolution and sample rate for a project. The ADS8330 is a 16-bit 1-Mhz dual input ADC that in this driver configuration can sample up to 325Ksps using the SPI interface at ~16MHz. One of the neat things about this chip is the ability to cascade several chips into a SPI daisy-chain with manual triggers (using a PIC controller for the device sequencer glue). I've configured the device for auto-triggered conversions using the internal 21MHz conversion clock in this Linux driver.

http://www.ti.com/lit/ds/symlink/ads8330.pdf

SPI buffered 'hunk' transfer mode 16-bit sampling. 3.08 usecs


Single acquisition read times between 16-bit samples using a Comedi.org compatible C program. 13.60 usecs.

I should be able up the spi clock to 32MHz with a proper board instead of the prototype wire jungle. :D


 
Last edited:
Top