Search code examples
pythonraspberry-pisensorsadc

How to read & accumulate sensor values at high frequency, without constantly writing to disk (RPi 2 b+, MCP3304, Python)


I am attempting to use a Raspberry Pi 2 Model B+ to read analog data on IR intensity from a photodiode via an MCP3304 (5v VDD) at a rate of ~ 10kHz three times in a row (0.1 ms between readings, or a rate of 10 ksps) once every second based on an external stimulus, average those values, then write them to a text file along with the current datetime. The photodiode is simply reading out data to an amplifier, which then feeds into the MCP3304, which, via SPI, feeds data into the RPi. (In essence: RPi receives a digital input, triggering three consecutive samples from the photodiode via an MCP3304 and in-line signal amplifier. Those three samples are stored in memory, averaged, then written to disk along with a datetime stamp to an existing CSV text file.) This is all on Python 2.7.

As it stands, I'm getting a sampling of < 1kHz with the below code ( SensorRead() ). I am very new to Python (and playing with sensors & RPis, for that matter!), and think the way I've setup my class to take three separate, consecutive ADC samples and possibly my setup for writing to disk may be slowing me down. However, I can't quite seem to find a better way. Edit1: I've done a good bit of research on max sampling rates via Python from the RPi GPIO, and it appears to be well above the ADC's restriction at ~ 1MHz or ~ 1000 ksps (e.g. 1,2). Edit2: Perhaps the ADC max sample rate of 100 ksps actually refers to how many bits can be read rather than how many full 12-bit samples can be taken per second?

Yup. This was it. The MCP3304 can do 100ksps, but the Python read rate is closer to 30ksps, which, when split between the 24-bits read by the MCP3304 per iteration, is closer to 1ksps



My two questions:
1) Are there any better ways of getting closer to the full 100 ksps advertised in the MCP3304 spec sheet? This link suggests calling WiringPi every time I want to take a single sample may cause some considerable latency.

and

2) is it possible, with a beginner/moderate level of Python skill for me to do all of this sampling and per-second averaging in the memory, and only write to disk, say, once every minute? Edit: could you please point me in the direction of some related links/resources?

Thanks!

Note 1: the code is "Threaded" because there are some other functions running simultaneously. Note 2: I am also, simultaneously reading a differential channel on the ADC, hence the "differential = True" in the MCP3304 command

'''
FILENAME = "~/output_file_location/and/name.txt"
adc_channel_pd = pin of the ADC from which analog signal is taken
stimulus_in_pin = the the pin that receives the alert to begin sampling
stimulus_LED_alert_pin = pin that goes "high" to illuminate an LED every time the stimulus_in_pin is triggered
Vref = the reference voltage for the ADC (3.3v; VDD = 5V)

'''

# import packages
import wiringpi2 as wiringpi
import time
from gpiozero import MCP3304
import threading
import datetime

# Define important objects
Vref = 3.3
adc_channel_pd = 7
stimulus_in_pin = 32
stimulus_LED_alert_pin = 16

# establish GPIO reading structure
wiringpi.wiringPiSetupPhys()

# set appropriate pin inputs and outputs (0 = input, 1 = output)
wiringpi.pinMode(stimulus_in_pin, 0)
wiringpi.pinMode(stimulus_LED_alert_pin, 1)

# create a class to take 3 PD readings, then average them, immediately upon stimulus
class SensorRead(threading.Thread):
    def __init__(self):
        super(SensorRead, self).__init__()
        self.daemon = True
        self.start()
    def run(self):
        for i in itertools.count():
            if (wiringpi.digitalRead(stimulus_in_pin) == True):
                val_ir_1 = MCP3304(adc_channel_pd, True).value * Vref)
                val_ir_2 = MCP3304(adc_channel_pd, True).value * Vref)
                val_ir_3 = MCP3304(adc_channel_pd, True).value * Vref)
                voltage_ir = round(  (float( (sum([val_ir_1,val_ir_2,val_ir_3])) / 3)) , 9)
                dt_ir = '%s' % datetime.datetime.now()
                f = open(FILENAME, "a")
                f.write("IR Sensor," + dt_ir + "," + str(voltage_ir) + "\n")    
                f.close()
                # print to terminal so I can verify output in real time
                print "IR Sensor:", dt_ir,",",voltage_ir
                # blink ir light on board for visual verification of stimulus in real time
                wiringpi.digitalWrite(stimulus_LED_alert_pin, 1)
                time.sleep(0.5)
                wiringpi.digitalWrite(stimulus_LED_alert_pin, 0)
                # sleep to avoid noise post LED firings
                time.sleep(0.5)

# run class
SensorRead()

Edit: I ended up getting some great results with Cython, as demonstrated in this test-code I wrote to quantify how fast I could read my ADC. I also ended up writing my own function to read from the MCP3304-- which I'll link to once it's all clean-- that I was able to further optimize in Cython.


Solution

  • One point about your question. Three samples in a second is a rate of 3Hz not 100kHz. Sounds to me like what you want is three samples 10us apart.

    1) 10us sample period on MCP3304 using Pi running Linux? Quite possibly not. Do some searching. See for example https://raspberrypi.stackexchange.com/questions/45386/microphone-via-spi where one answer says they achieved 33us (33ksps) using C code and avoiding the SPI driver. Also I suspect you will find Linux process switching and other threads getting in the way and affecting the sample rate particularly if they are also reading the ADC. This may be more likely if you have a dedicated non-Linux processor to read the ADC, programmed in C or assembly language, feeding the three samples to the Pi. Easier if you use a parallel ADC, i.e. not using SPI-like serial comms. See also http://www.hertaville.com/interfacing-an-spi-adc-mcp3008-chip-to-the-raspberry-pi-using-c.html and https://www.raspberrypi.org/forums/viewtopic.php?f=93&t=83069

    2) While the sampling at 10us period using the MCP3304 is difficult-to-impossible on Pi, the averaging and writing is definitely possible.

    I have a solution for your problem, though: if literally all you're going to do with the three samples is to average them, why not add an old-fashioned low-pass analog filter before the input pin and then just take one sample. Hey presto, no need for hardish-realtime ADCing, no need to worry about other processes, or kernel interrupts!