Data Aquisition Box, preferably with ~100kHz@16bit Signal sampling

The place to share and discuss your Ultibo projects.
_Tropo
Posts: 48
Joined: Tue Mar 20, 2018 1:01 pm
Location: Germany

Re: Data Aquisition Box, preferably with ~100kHz@16bit Signal sampling

Postby _Tropo » Wed Jul 11, 2018 3:31 pm

Here's my slightly cleaned up version of my little ADS8320 Unit:

Code: Select all

unit ADS8320;

//{$mode objfpc}
{$mode delphi} {Default to Delphi compatible syntax}
{$H+}
{$inline on}   {Allow use of Inline procedures}

interface

uses
  Classes,
  SysUtils,
  GlobalConst,
  GlobalConfig,

  //RPi3:
  BCM2710,
  Platform{$IFNDEF CONSOLE_EARLY_INIT},PlatformRPi3{$ENDIF},
  BCM2837,

  //RPi2:
  //BCM2709,
  //Platform{$IFNDEF CONSOLE_EARLY_INIT},PlatformRPi2{$ENDIF},
  //BCM2836,

  Devices,
  SPI;

type

  TSPIBytes = array[0..2] of Byte;
  PSPIBytes =  ^TSPIBytes;


function ADC_startDevice(var SampleBytes:PSPIBytes): PSPIDevice;
function ADC_getSample(ADCDevice:PSPIDevice;SampleBytes:PSPIBytes):Word;
function ADS8320SPI0WriteRead(SPI:PSPIDevice;ChipSelect:Word;Source,Dest:Pointer;Size,Flags:LongWord;var Count:LongWord):LongWord;
function BCM2710SPI0ReadWritePoll(SPI:PBCM2710SPI0Device):LongWord;

implementation

function ADC_startDevice(var SampleBytes:PSPIBytes): PSPIDevice;
var
   ADCDevice:PSPIDevice;
begin
  // alloce memory for the buffer for the return bytes of the ADC
  SampleBytes:=AllocMem(SizeOf(TSPIBytes));

  //Locate the SPI device (Adjust for boards other than Pi3)
  ADCDevice := SPIDeviceFindByDescription(BCM2710_SPI0_DESCRIPTION);
  if ADCDevice = nil then
    Exit;

  //Configure SPI Chip Select 0
  if SPIDeviceSetClockRate(ADCDevice ,SPI_CS_0, 2400000) <> ERROR_SUCCESS then
    Exit;

  //Start the SPI device
  if SPIDeviceStart(ADCDevice, SPI_MODE_3WIRE, 2400000, SPI_CLOCK_PHASE_HIGH, SPI_CLOCK_POLARITY_HIGH) <> ERROR_SUCCESS then
    Exit;

  Result:=ADCDevice;
end;

function ADC_getSample(ADCDevice:PSPIDevice;SampleBytes:PSPIBytes):Word;
var
  n_Bytes     : LongWord;
  ADS_WR_Return: LongWord;
  SourceBytes:PSPIBytes;
begin
  n_Bytes:=0;
  SourceBytes:=AllocMem(SizeOf(TSPIBytes));

  //Read 3 bytes from the SPI device
  ADS_WR_Return:=ADS8320SPI0WriteRead(ADCDevice ,SPI_CS_0, SourceBytes, SampleBytes, 3, SPI_TRANSFER_NONE, n_Bytes);
  if ADS_WR_Return <> ERROR_SUCCESS then
   begin
    Result:= ADS_WR_Return;
   end
  else
   begin
    Result:= (((SampleBytes[0] and 3) << 14) or (SampleBytes[1] << 6) or (SampleBytes[2] and (63)));
   end;
end;


function ADS8320SPI0WriteRead(SPI:PSPIDevice;ChipSelect:Word;Source,Dest:Pointer;Size,Flags:LongWord;var Count:LongWord):LongWord;
var
 Control:LongWord;
begin
 {}
 {Setup Result}
 Count:=0;
 Result:=ERROR_INVALID_PARAMETER;

 {Check Buffers}
 if (Source = nil) and (Dest = nil) then Exit;


 {Check SPI}
 if SPI = nil then Exit;

 {$IF DEFINED(BCM2710_DEBUG) or DEFINED(SPI_DEBUG)}
 if SPI_LOG_ENABLED then SPILogDebug(SPI,'BCM2710: SPI0 Write Read (ChipSelect=' + SPIChipSelectToString(ChipSelect) + ' Size=' + IntToStr(Size) + ')');
 {$ENDIF}

 {Check Size}
 if Size > BCM2710_SPI0_MAX_SIZE then Exit;

 {Check Chip Select}
 if (ChipSelect <> SPI_CS_NONE) and (ChipSelect > SPI_CS_2) then Exit;

 {Update Statistics}
 Inc(SPI.TransferCount);

 {Write from Source / Read to Dest}
 if Size > 0 then
  begin
   {Setup Data}
   PBCM2710SPI0Device(SPI).Source:=Source;
   PBCM2710SPI0Device(SPI).Dest:=Dest;
   PBCM2710SPI0Device(SPI).Count:=0;
   PBCM2710SPI0Device(SPI).SourceRemain:=Size;
   PBCM2710SPI0Device(SPI).DestRemain:=Size;

   {Memory Barrier}
   DataMemoryBarrier; {Before the First Write}

   {Get Control and Status}
   Control:=PBCM2837SPI0Registers(PBCM2710SPI0Device(SPI).Address).CS and not(BCM2837_SPI0_CS_CS_MASK);

   {Set Mode}
   if (SPI.SPIMode = SPI_MODE_3WIRE) and (Dest <> nil) then
    begin
     Control:=Control or BCM2837_SPI0_CS_REN;
    end
   else
    begin
     Control:=Control and not(BCM2837_SPI0_CS_REN);
    end;

   {Set Chip Select}
   if ChipSelect = SPI_CS_NONE then
    begin
     Control:=Control or (BCM2837_SPI0_CS_CS_MASK); {Select the reserved value}
    end
   else
    begin
     Control:=Control or (ChipSelect and BCM2837_SPI0_CS_CS_MASK);
    end;

   {Check Clock Rate}
   if (ChipSelect = SPI_CS_NONE) or (SPI.ChipSelects[ChipSelect].ClockRate = 0) then
    begin
     {Set Clock Divider}
     PBCM2837SPI0Registers(PBCM2710SPI0Device(SPI).Address).CLK:=(SPI.Divider and BCM2837_SPI0_CLK_CDIV);
    end
   else
    begin
     {Set Clock Divider}
     PBCM2837SPI0Registers(PBCM2710SPI0Device(SPI).Address).CLK:=(SPI.ChipSelects[ChipSelect].Divider and BCM2837_SPI0_CLK_CDIV);
    end;

   {Update Data}
   PBCM2710SPI0Device(SPI).Mode:=BCM2710_SPI0_MODE_IRQ;

   {Note: Cannot fill FIFO when TA bit is not set, interrupt handler will fill on first IRQ}

   {Set Control (Active/Interrupt/Clear)}
   Control:=Control and not(BCM2837_SPI0_CS_INTR or BCM2837_SPI0_CS_INTD);
   Control:=Control or (BCM2837_SPI0_CS_TA or BCM2837_SPI0_CS_CLEAR_RX or BCM2837_SPI0_CS_CLEAR_TX);

   {Set Control and Status}
   PBCM2837SPI0Registers(PBCM2710SPI0Device(SPI).Address).CS:=Control;

   {Memory Barrier, not necessary, since we will be running on a dedicated thread anyway?}
   DataMemoryBarrier; {After the Last Read}

   {Wait for Completion}
   if BCM2710SPI0ReadWritePoll(PBCM2710SPI0Device(SPI)) = ERROR_SUCCESS then

    begin
     {Get Count}
     Count:=PBCM2710SPI0Device(SPI).Count;

     {Check Count}
     if Count < Size then
      begin

       if SPI_LOG_ENABLED then SPILogError(SPI,'BCM2710: Write failure or timeout');

       {Update Statistics}
       Inc(SPI.TransferErrors);
      end;
    end
   else
    begin
     if SPI_LOG_ENABLED then SPILogError(SPI,'BCM2710: Wait failure on write');

     Result:=ERROR_OPERATION_FAILED;
    end;

   {Reset Data}
   PBCM2710SPI0Device(SPI).Source:=nil;
   PBCM2710SPI0Device(SPI).Dest:=nil;
   PBCM2710SPI0Device(SPI).Count:=0;
   PBCM2710SPI0Device(SPI).SourceRemain:=0;
   PBCM2710SPI0Device(SPI).DestRemain:=0;
  end;

 {$IF DEFINED(BCM2710_DEBUG) or DEFINED(SPI_DEBUG)}
 if SPI_LOG_ENABLED then SPILogDebug(SPI,'BCM2710:  Return Count=' + IntToStr(Count));
 {$ENDIF}

 {Return Result}
 if (Size = Count) then Result:=ERROR_SUCCESS;
end;

function BCM2710SPI0ReadWritePoll(SPI:PBCM2710SPI0Device):LongWord;
var
 Control:LongWord;
begin
 {}
 {Check SPI}
 if SPI = nil then Exit;

 {Update Statistics}
 //Inc(SPI.InterruptCount); //no interrupts enabled, shouldn't be necessary

 {Memory Barrier}
 DataMemoryBarrier; {Before the First Write}

 {Read FIFO}
 BCM2710SPI0ReadFIFO(SPI);
 Control:=PBCM2837SPI0Registers(SPI.Address).CS;

 {Write FIFO}
 BCM2710SPI0WriteFIFO(SPI);

 {Get Control and Status}
 Control:=PBCM2837SPI0Registers(SPI.Address).CS;

 {Check Done}
// if ((Control and BCM2837_SPI0_CS_DONE) <> 0) and (SPI.SourceRemain = 0) then
 if (SPI.SourceRemain = 0) then
  begin
   {Read remaining FIFO}
   BCM2710SPI0ReadFIFO(SPI);
   {Reset Control (Active/Interrupt/Deassert/DMA/Clear)}
   Control:=Control and not(BCM2837_SPI0_CS_INTR or BCM2837_SPI0_CS_INTD or BCM2837_SPI0_CS_ADCS or BCM2837_SPI0_CS_DMAEN or BCM2837_SPI0_CS_TA);
   //Control:=Control and not(BCM2837_SPI0_CS_ADCS or BCM2837_SPI0_CS_DMAEN or BCM2837_SPI0_CS_TA);
   Control:=Control or (BCM2837_SPI0_CS_CLEAR_RX or BCM2837_SPI0_CS_CLEAR_TX);

   {Set Control and Status}
   PBCM2837SPI0Registers(SPI.Address).CS:=Control;

   {Set Data Length}
   PBCM2837SPI0Registers(SPI.Address).DLEN:=0;

   {Signal Semaphore}
   //SemaphoreSignal(SPI.SPI.Wait);
  end;

 {Memory Barrier}
 DataMemoryBarrier; {After the Last Read}

 Result:=ERROR_SUCCESS; // This needs to have some actual use and not be ERROR_SUCCESS after every call
end;


And here a little main script that shows me it's not working as intended:

Code: Select all

program DAQ_development;

//{$mode objfpc}{$H+}
{$mode delphi} {Default to Delphi compatible syntax}
{$H+}          {Default to AnsiString}
{$inline on}   {Allow use of Inline procedures}
uses
  //BCM2709,      {RPi2}
  //BCM2836,      {RPi2}
  //BCM2710,      {RPi3}
  //BCM2837,      {RPi3}
  InitUnit,     {Include InitUnit to allow us to change the startup behaviour}
  RaspberryPi3, {Include RaspberryPi2 to make sure all standard functions are included}
  GlobalConst,
  GlobalConfig,
  GlobalTypes,
  Threads,
  Console,
  SysUtils,
  ThreadUnit,         {Include our thread unit which contains most of the example}
  //MeasureUnit,
  SampleBufferUnit,
  //DataFileHandlingUnit,
  Platform,
  FATFS,
  NTFS,
  FileSystem,  {Include the file system core and interfaces}
  DAQ_Globals,
  // Time
  Ultibo,   {The Ultibo unit provides some APIs for getting and setting timezones}
  DateUtils,
  SPI,
  ADS8320,
  Services; {The services unit includes the NTP client and will automatically include the network}

var
 LeftWindow:TWindowHandle;
 SampleBytes:PSPIBytes;
 ADCDevice:PSPIDevice;
 n_Bytes:LongWord;
 Sample,Sample2,Sample2a:Word;
 
begin
 {Create a console window to show what is happening}
 LeftWindow:=ConsoleWindowCreate(ConsoleDeviceGetDefault,CONSOLE_POSITION_LEFT,True);

 ADCDevice:=ADC_startDevice(SampleBytes);


 // This works:
 n_Bytes:=0;
 if SPIDeviceRead(ADCDevice ,SPI_CS_0, SampleBytes, 3, SPI_TRANSFER_NONE,n_Bytes) <> ERROR_SUCCESS then
    ConsoleWindowWriteLn(LeftWindow,'Failed to read ADC');
 Sample:=(((SampleBytes^[0] and 3) << 14) or (SampleBytes^[1] << 6) or (SampleBytes^[2] and (63)));

 //reset SampleBytes
 SampleBytes^[0]:=0;
 SampleBytes^[1]:=0;
 SampleBytes^[2]:=0;
  // This doesn't:
 Sleep(10);
 Sample2:=ADC_getSample(ADCDevice ,SampleBytes);
 Sample2a:=(((SampleBytes^[0] and 3) << 14) or (SampleBytes^[1] << 6) or (SampleBytes^[2] and (63)));


 ConsoleWindowWriteLn(LeftWindow,'Sample = '+IntToStr(Sample)+'   ,Sample2 = '+IntToStr(Sample2)+',   Sample2a = '+IntToStr(Sample2a));
 ConsoleWindowWriteLn(LeftWindow,'SampleByte_1 = '+  IntToStr(SampleBytes^[0]) +   '  SampleByte_2 = '+  IntToStr(SampleBytes^[1]) +'  SampleByte_3 = '+  IntToStr(SampleBytes^[2]));
 end.
 


...excuse all those unused units, and the slightly messy formating. It's from my developement script i used for lot's of testing...

Thanks so much for your continued support! ou guys are awesome!
User avatar
Ultibo
Site Admin
Posts: 1975
Joined: Sat Dec 19, 2015 3:49 am
Location: Australia

Re: Data Aquisition Box, preferably with ~100kHz@16bit Signal sampling

Postby Ultibo » Thu Jul 12, 2018 12:04 am

_Tropo wrote:Here's my slightly cleaned up version of my little ADS8320 Unit:

I haven't tested this yet but I think all that is missing is a loop to write/read the data.

The SPI device is running at 2.4MHz but the CPU is running at 1.2GHz (or more) so by the time the SPI device gets data delivered into the FIFO for you to read your function has already given up and returned.

I modified the BCM2710SPI0ReadWritePoll() function to include a loop while waiting and it looks like this:

Code: Select all

function BCM2710SPI0ReadWritePoll(SPI:PBCM2710SPI0Device):LongWord;
var
 Control:LongWord;
begin
 {}
 Result:=ERROR_INVALID_PARAMETER;
 
 {Check SPI}
 if SPI = nil then Exit;

 {Loop until Completion}
 while SPI.DestRemain > 0 do
  begin
   {Memory Barrier}
   DataMemoryBarrier; {Before the First Write}
 
   {Read FIFO}
   BCM2710SPI0ReadFIFO(SPI);
 
   {Write FIFO}
   BCM2710SPI0WriteFIFO(SPI);
 
   {Get Control and Status}
   Control:=PBCM2837SPI0Registers(SPI.Address).CS;
 
   {Check Done}
   if ((Control and BCM2837_SPI0_CS_DONE) <> 0) and (SPI.SourceRemain = 0) then
    begin
     {Read remaining FIFO}
     BCM2710SPI0ReadFIFO(SPI);
     
     {Reset Control (Active/Interrupt/Deassert/DMA/Clear)}
     Control:=Control and not(BCM2837_SPI0_CS_INTR or BCM2837_SPI0_CS_INTD or BCM2837_SPI0_CS_ADCS or BCM2837_SPI0_CS_DMAEN or BCM2837_SPI0_CS_TA);
     Control:=Control or (BCM2837_SPI0_CS_CLEAR_RX or BCM2837_SPI0_CS_CLEAR_TX);
 
     {Set Control and Status}
     PBCM2837SPI0Registers(SPI.Address).CS:=Control;
 
     {Set Data Length}
     PBCM2837SPI0Registers(SPI.Address).DLEN:=0;
    end;
 
   {Memory Barrier}
   DataMemoryBarrier; {After the Last Read}
  end;
 
 Result:=ERROR_SUCCESS;
end;

That should be close to working but as I said it hasn't been tested so I might have missed something.

I will still create my own test of using polling to do this just to make sure it works and we might even add a new flag to the SPI functions for SPI_TRANSFER_PIO and make a polling mode available, that won't change anything for you because you still need your own function to do it from a dedicated thread (the SPI functions all use locks).

There are probably a couple of other changes you could make to your code once you get it working but one I did notice that you should change is in ADC_getSample(). There is no need to allocate SourceBytes for the dummy data, just pass nil to the ADS8320SPI0WriteRead() function instead and it will handle it correctly. You also don't want to be allocating memory from a dedicated thread if you have scheduling disabled, again because of locks etc.

Let us know the result of this change.
Ultibo.org | Make something amazing
https://ultibo.org
_Tropo
Posts: 48
Joined: Tue Mar 20, 2018 1:01 pm
Location: Germany

Re: Data Aquisition Box, preferably with ~100kHz@16bit Signal sampling

Postby _Tropo » Thu Jul 12, 2018 11:58 am

Ultibo wrote:I haven't tested this yet but I think all that is missing is a loop to write/read the data.

The SPI device is running at 2.4MHz but the CPU is running at 1.2GHz (or more) so by the time the SPI device gets data delivered into the FIFO for you to read your function has already given up and returned.


Oh, yes of course, you're right! I forgot about the time difference between the SPI filling the RX buffer at 2.4 MHz and the CPU checking it so much faster. So just checking it once obviously cannot work! You're suggested change fixed my issue and my dedicated thread is now doing the sampling of the ADC with a rate 100kSPS and writing it to data files. Thanks so much.
_Tropo
Posts: 48
Joined: Tue Mar 20, 2018 1:01 pm
Location: Germany

Re: Data Aquisition Box, preferably with ~100kHz@16bit Signal sampling

Postby _Tropo » Mon Aug 13, 2018 5:13 pm

Hello there. Another month, another question in the topic...

So my requirements did change a little bit and i now have an additional device i want to take samples from, but at a much lower frequency (~100Hz), namely a BMP280, which samples pressure and temperature at much lower frequencies than the cut off frequency of the pressure sensors (microphones) that are hooked up to my ADC. I have a BMP280 here and try to get it working in parallel to my ADC. The BMP280 supports SPI and I2C interface and since i just got a kind off comfortable with SPI, this would be my interface of choice. I was thinking to let my dedicated thread also handle the sampling of the BMP280. This way i could simply use the polling variant of SPIWriteRead i wrote for the ADC. Since the SPI clock rate of the ADC is at 6Mhz, i have 60 cycles to get all my samples to achieve my desired 100kHz (0.1MHz) sampling rate of the ADC. The transfer of a sample from the ADC takes 22 cycles. The BMP280 will probably take a few more (still have to check), so i would be very tight on time. However, the BMP supports a clock rate of 10Mhz, so if i could use that, i would save myself some time when sampling the BMP280

The question is: did i get it right that the settings i use for SPIDeviceStart() are applied to all devices that are connected to the SPI Bus? Is it possible to change the settings on the fly without stopping and starting the SPI again, wihtin my dedicated thread where all interrupts are disabled? or is it possible to use the auxiliary SPI Bus that the rPi3 is supposed to have on gpio pins 19,20,21,18,17,16 in Ultibo?

But i guess it might be best to not use SPI and/or not let my dedicated thread handle the sampling of the BMP280, right? Any suggestions what approach would make more sense?

Thanks again for bearing with me and all my questions!
User avatar
Ultibo
Site Admin
Posts: 1975
Joined: Sat Dec 19, 2015 3:49 am
Location: Australia

Re: Data Aquisition Box, preferably with ~100kHz@16bit Signal sampling

Postby Ultibo » Tue Aug 14, 2018 12:23 pm

_Tropo wrote:The question is: did i get it right that the settings i use for SPIDeviceStart() are applied to all devices that are connected to the SPI Bus?

Hi,

Not completely, most of the settings apply to all connected devices (like mode, clock phase and polarity) but the clock rate itself can be set per chip select. Since the Pi supports both SPI_CS_0 and SPI_CS_1 you can have two devices connected each with a different clock speed.

_Tropo wrote:Is it possible to change the settings on the fly without stopping and starting the SPI again, wihtin my dedicated thread where all interrupts are disabled?

Yes, the code is all written to switch clock speed for each chip select without stopping and restarting.

I don't know what your final code ended up looking like but I'll refer to the snippet you posted earlier in this thread and you can compare it to what you have now.

If you look at ADC_startDevice from your earlier post you will see that it calls SPIDeviceSetClockRate with the parameter SPI_CS_0, if you add a second call to SPIDeviceSetClockRate immediately after the first one and change the CS value to SPI_CS_1 and the clock rate to the value you want for the new device (eg 10000000) that is all the setup you should need to do.

If you then look at ADS8320SPI0WriteRead (or BCM27XXSPI0WriteRead from the original driver) you should see a line that looks like this:

Code: Select all

 if (ChipSelect = SPI_CS_NONE) or (SPI.ChipSelects[ChipSelect].ClockRate = 0) then

the else part of that statement sets the clock divider (and therefore the clock rate) based on the value set from SPIDeviceSetClockRate above. As long as you haven't removed that code it should all still work as designed.

The secret is that SPI only applies the clock to the bus when a transfer is active, so it can apply a different clock rate for each transfer and only the device whose chip select line is pull low will respond to it.

_Tropo wrote:or is it possible to use the auxiliary SPI Bus that the rPi3 is supposed to have on gpio pins 19,20,21,18,17,16 in Ultibo?

We don't have a driver for that one yet, it will probably be added when we produce a driver for the secondary mini UART which will be necessary once it is possible to make use of the Bluetooth device connected to the main UART.
Ultibo.org | Make something amazing
https://ultibo.org
_Tropo
Posts: 48
Joined: Tue Mar 20, 2018 1:01 pm
Location: Germany

Re: Data Aquisition Box, preferably with ~100kHz@16bit Signal sampling

Postby _Tropo » Thu Aug 16, 2018 6:25 pm

thanks for the hint. I got both devices working on SPI with different clock rates - at least to a certain degree...

But i think that this approach might be to slow to keep my 100kHz sampling rate stable anyway. In order to read the BMP280 i have to write one byte and then read 6, so it's at least 56 cycles @ 10MHz clock rate = 5.6 micro seconds, while the ADC takes 22 cycles @ 6MHz = 3.67 micro seconds. So reading both devices takes at least 9.27 micro seconds when i have 10 micro seconds of time before i need to take the next sample from my ADC. Accessing the devices probably causes some overhead as well, so i think that 100kHz might not be sustainable. Or do you think there should be no extra overhead involved? I'm not doing much else in the loop except for writing some values to my sample buffer.
But the strange thing is, the max difference between 2 samples in the buffer is somewhere between 15 and 18 micro seconds, sometimes up to 22, which i think is much longer than it should take to read the BMP280 on top of my usual operations in the loop. So i guess there is something really wrong.

Here's some of my code:

The routine that runs all alone on the dedicated thread:

Code: Select all

procedure WriteData2Buffer(SampleRate:Integer;ADCDevice:PSPIDevice;SampleBytes:PSPIBytes);
{This is the procedure that runs alone on a dedicated thread without anything else on the core.
It takes a sample at the times correspodning to the sample rate and writes the sample to one of the buffers,
cycling through the buffers}
var
 StartCount           : QWord;
 CurrentCount         : QWord;
 LastCount            : QWord;
 CurrentBuffer        : Word;
 SampleTime           : LongWord;
 startSwitch          : Boolean;
 Sample               : Word;
 dt                   : Word;
 BMPmeasures          : Boolean;
 BMPSampleTime        : Word;

begin

 CLOCK_OFFSET       := Trunc(ClockBase/10);

 SampleTime  := CLOCK_FREQUENCY div SampleRate; // number of clock ticks between taking samples

 STOP_SWITCH   := False;
 startSwitch   := False;
 BMPmeasures   := False;
 BMPSampleTime := 10000;
 // We will start with the 1st Buffer
 CurrentBuffer := 1;
 SampleBuffer[CurrentBuffer]^.State:=3; // set state to writing
 SampleBuffer[CurrentBuffer]^.maxDT:=SampleTime;
 SampleBuffer[CurrentBuffer]^.minDT:=SampleTime;
 while True do
 begin
  if READY_SWITCH then
  begin

   {Check our tick count for elapsed time}
   CurrentCount := ClockGetTotal;

   // waiting until we are at a full second to start measuring
   if not(startSwitch) and (((CurrentCount+CLOCK_OFFSET) mod CLOCK_FREQUENCY ) =0 ) then
   begin
    StartCount  := CurrentCount;
    LastCount   := CurrentCount-SampleTime;

    startSwitch := True;
   end;

   if startSwitch then
   begin
    if CurrentCount >= (LastCount+SampleTime) then
    begin

     Sample:=ADC_getSample(ADCDevice,SampleBytes); //<- first thing we need to do here is to get the sample from the ADC

     // Values from BMP280, not storing them anywhere yet and sampling it far to often (100 times then aimed). It's just to check the timing...
     BMP280_read_register(ADCDevice,BMP280_ADRESS_RAWDATA,BMPReadData);

     dt    :=CurrentCount-LastCount;


     // If there are no samples in the buffer yet, save the current Time as Start time in the buffer:
     if SampleBuffer[CurrentBuffer]^.Count=0 then SampleBuffer[CurrentBuffer]^.StartTime:=(CurrentCount+CLOCK_OFFSET)/US_PER_DAY;

     SampleBuffer[CurrentBuffer]^.maxDT:=Max(SampleBuffer[CurrentBuffer]^.maxDT,dt);
     SampleBuffer[CurrentBuffer]^.minDT:=Min(SampleBuffer[CurrentBuffer]^.minDT,dt);

     //Write current Sample to Buffer
     BufferWrite(SampleBuffer[CurrentBuffer],Sample);



     // Check if Buffer is Full, if TRUE, switch to next buffer
     if SampleBuffer[CurrentBuffer]^.State=4 then
     begin

      inc(CurrentBuffer);
      CurrentBuffer := CurrentBuffer mod N_BUFFER;
      if CurrentBuffer=0 then CurrentBuffer:= N_BUFFER;

      // After switching, check if the "new" buffer is empty. It really should be,
      // since otherwise we sample faster than we can write to disc. If TRUE, change state to writing
      if SampleBuffer[CurrentBuffer]^.State=1 then
      begin
       SampleBuffer[CurrentBuffer]^.State:=3;

       SampleBuffer[CurrentBuffer]^.maxDT:=SampleTime;
       SampleBuffer[CurrentBuffer]^.minDT:=SampleTime;
      end;
     end;

     LastCount:=CurrentCount;


    end;

    if CurrentCount > (StartCount + 1810000000) then
    begin
     STOP_SWITCH:=True;
    end;

    if STOP_SWITCH then Break;

    end; // if startSwitch
  end; // if READY_SWITCH
 end; //while

end; //procedure


And the routine that sample the ADC and the polling version of SPIWriteRead that is called within:

Code: Select all


function ADC_getSample(ADCDevice:PSPIDevice;SampleBytes:PSPIBytes):Word;
var
  n_Bytes     : LongWord;
  ADS_WR_Return: LongWord;
begin
  n_Bytes:=0;

  //Read 3 bytes from the SPI device
  ADS_WR_Return:=ADS8320SPI0WriteRead(ADCDevice ,SPI_CS_0, nil, SampleBytes, 3, SPI_TRANSFER_NONE, n_Bytes);
  if ADS_WR_Return <> ERROR_SUCCESS then
   begin
    Result:= ADS_WR_Return;
   end
  else
   begin
    Result:= (((SampleBytes[0] and 3) << 14) or (SampleBytes[1] << 6) or (SampleBytes[2] and (63)));
   end;
end;


function ADS8320SPI0WriteRead(SPI:PSPIDevice;ChipSelect:Word;Source,Dest:Pointer;Size,Flags:LongWord;var Count:LongWord):LongWord;
var
 Control:LongWord;
begin
 {}
 {Setup Result}
 Count:=0;
 Result:=ERROR_INVALID_PARAMETER;

 {Check Buffers}
 if (Source = nil) and (Dest = nil) then Exit;


 {Check SPI}
 if SPI = nil then Exit;

 {$IF DEFINED(BCM2710_DEBUG) or DEFINED(SPI_DEBUG)}
 if SPI_LOG_ENABLED then SPILogDebug(SPI,'BCM2710: SPI0 Write Read (ChipSelect=' + SPIChipSelectToString(ChipSelect) + ' Size=' + IntToStr(Size) + ')');
 {$ENDIF}

 {Check Size}
 if Size > BCM2710_SPI0_MAX_SIZE then Exit;

 {Check Chip Select}
 if (ChipSelect <> SPI_CS_NONE) and (ChipSelect > SPI_CS_2) then Exit;

 {Update Statistics}
 Inc(SPI.TransferCount);

 {Write from Source / Read to Dest}
 if Size > 0 then
  begin
   {Setup Data}
   PBCM2710SPI0Device(SPI).Source:=Source;
   PBCM2710SPI0Device(SPI).Dest:=Dest;
   PBCM2710SPI0Device(SPI).Count:=0;
   PBCM2710SPI0Device(SPI).SourceRemain:=Size;
   PBCM2710SPI0Device(SPI).DestRemain:=Size;

   {Memory Barrier}
   DataMemoryBarrier; {Before the First Write}

   {Get Control and Status}
   Control:=PBCM2837SPI0Registers(PBCM2710SPI0Device(SPI).Address).CS and not(BCM2837_SPI0_CS_CS_MASK);

   {Set Mode}
   if (SPI.SPIMode = SPI_MODE_3WIRE) and (Dest <> nil) then
    begin
     Control:=Control or BCM2837_SPI0_CS_REN;
    end
   else
    begin
     Control:=Control and not(BCM2837_SPI0_CS_REN);
    end;

   {Set Chip Select}
   if ChipSelect = SPI_CS_NONE then
    begin
     Control:=Control or (BCM2837_SPI0_CS_CS_MASK); {Select the reserved value}
    end
   else
    begin
     Control:=Control or (ChipSelect and BCM2837_SPI0_CS_CS_MASK);
    end;

   {Check Clock Rate}
   if (ChipSelect = SPI_CS_NONE) or (SPI.ChipSelects[ChipSelect].ClockRate = 0) then
    begin
     {Set Clock Divider}
     PBCM2837SPI0Registers(PBCM2710SPI0Device(SPI).Address).CLK:=(SPI.Divider and BCM2837_SPI0_CLK_CDIV);
    end
   else
    begin
     {Set Clock Divider}
     PBCM2837SPI0Registers(PBCM2710SPI0Device(SPI).Address).CLK:=(SPI.ChipSelects[ChipSelect].Divider and BCM2837_SPI0_CLK_CDIV);
    end;

   {Update Data}
   PBCM2710SPI0Device(SPI).Mode:=BCM2710_SPI0_MODE_IRQ;

   {Note: Cannot fill FIFO when TA bit is not set}

   {Set Control (Active/Interrupt/Clear)}
   Control:=Control and not(BCM2837_SPI0_CS_INTR or BCM2837_SPI0_CS_INTD);
   Control:=Control or (BCM2837_SPI0_CS_TA or BCM2837_SPI0_CS_CLEAR_RX or BCM2837_SPI0_CS_CLEAR_TX);

   {Set Control and Status}
   PBCM2837SPI0Registers(PBCM2710SPI0Device(SPI).Address).CS:=Control;

   {Memory Barrier, not necessary, since we will be running on a dedicated thread anyway?}
   DataMemoryBarrier; {After the Last Read}

   {Wait for Completion}
   if BCM2710SPI0ReadWritePoll(PBCM2710SPI0Device(SPI)) = ERROR_SUCCESS then

    begin
     {Get Count}
     Count:=PBCM2710SPI0Device(SPI).Count;

     {Check Count}
     if Count < Size then
      begin

       if SPI_LOG_ENABLED then SPILogError(SPI,'BCM2710: Write failure or timeout');

       {Update Statistics}
       Inc(SPI.TransferErrors);
      end;
    end
   else
    begin
     if SPI_LOG_ENABLED then SPILogError(SPI,'BCM2710: Wait failure on write');

     Result:=ERROR_OPERATION_FAILED;
    end;

   {Reset Data}
   PBCM2710SPI0Device(SPI).Source:=nil;
   PBCM2710SPI0Device(SPI).Dest:=nil;
   PBCM2710SPI0Device(SPI).Count:=0;
   PBCM2710SPI0Device(SPI).SourceRemain:=0;
   PBCM2710SPI0Device(SPI).DestRemain:=0;
  end;

 {$IF DEFINED(BCM2710_DEBUG) or DEFINED(SPI_DEBUG)}
 if SPI_LOG_ENABLED then SPILogDebug(SPI,'BCM2710:  Return Count=' + IntToStr(Count));
 {$ENDIF}

 {Return Result}
 if (Size = Count) then Result:=ERROR_SUCCESS;
end;

function BCM2710SPI0ReadWritePoll(SPI:PBCM2710SPI0Device):LongWord;
var
 Control:LongWord;
begin
 {}
 {Check SPI}
 if SPI = nil then Exit;

 {Loop until Completion}
 while SPI.DestRemain > 0 do
  begin
   {Memory Barrier}
   DataMemoryBarrier; {Before the First Write}

   {Read FIFO}
   BCM2710SPI0ReadFIFO(SPI);
   Control:=PBCM2837SPI0Registers(SPI.Address).CS;

   {Write FIFO}
   BCM2710SPI0WriteFIFO(SPI);

   {Get Control and Status}
   Control:=PBCM2837SPI0Registers(SPI.Address).CS;

   {Check Done}
   if ((Control and BCM2837_SPI0_CS_DONE) <> 0) and (SPI.SourceRemain = 0) then
   //if (SPI.SourceRemain = 0) then
    begin
     {Read remaining FIFO}
     BCM2710SPI0ReadFIFO(SPI);

     {Reset Control (Active/Interrupt/Deassert/DMA/Clear)}
     Control:=Control and not(BCM2837_SPI0_CS_INTR or BCM2837_SPI0_CS_INTD or BCM2837_SPI0_CS_ADCS or BCM2837_SPI0_CS_DMAEN or BCM2837_SPI0_CS_TA);
     Control:=Control or (BCM2837_SPI0_CS_CLEAR_RX or BCM2837_SPI0_CS_CLEAR_TX);

     {Set Control and Status}
     PBCM2837SPI0Registers(SPI.Address).CS:=Control;

     {Set Data Length}
     PBCM2837SPI0Registers(SPI.Address).DLEN:=0;

     {Signal Semaphore}
     //SemaphoreSignal(SPI.SPI.Wait);
    end;

   {Memory Barrier}
   DataMemoryBarrier; {After the Last Read}
  end;
 Result:=ERROR_SUCCESS; // This needs to have some actual use and not be ERROR_SUCCESS after every call
end;



So because of the timing issue and because i only need to sample the BMP280 with 100Hz, i tryed to move it to I2C and handle the sampling outside of my dedicated thread. Without success unfortunately...
Here is a snipped from the BMP Data sheet:

I2C write:
Writing is done by sending the slave address in write mode (RW = ‘0’), resulting in slave address
111011X0 (‘X’ is determined by state of SDO pin. Then the master sends pairs of register
addresses and register data. The transaction is ended by a stop condition.
...


I2C read:
To be able to read registers, first the register address must be sent in write mode (slave address
111011X0). Then either a stop or a repeated start condition must be generated. After this the
slave is addressed in read mode (RW = ‘1’) at address 111011X1, after which the slave sends
out data from auto-incremented register addresses until a NOACKM and stop condition occurs.
This is depicted in Figure 8, where two bytes are read from register 0xF6 and 0xF7.


What i don't get is if i need to manualy write "($F6 shl 1)" for writing or "(($F6 shl 1) or 1)" ($F6 is the adress of the BMP280) to I2C using I2CDeviceWriteRead() on the adress $F6 before i send the register adresses i want to access on the BMP280? Or is that part of the I2C standard and it's done automatically?
what i tried doing is something like:

Code: Select all

I2CDeviceWriteRead(BMPDevice,BMP280_I2C_ADRESS,BMPRegisterAdress,1,BMPReadData,arraySize,n_Bytes);


So i assumed it's handles by the I2CDeviceWriteRead routine. But i'm not receiving any data from the BMP280...

Sorry for bothering you again with so many questions!
_Tropo
Posts: 48
Joined: Tue Mar 20, 2018 1:01 pm
Location: Germany

Re: Data Aquisition Box, preferably with ~100kHz@16bit Signal sampling

Postby _Tropo » Fri Aug 17, 2018 7:47 am

ok some further testing showes that:

Code: Select all

     
      BMPDevice := I2CDeviceFindByDescription(BCM2710_I2C0_DESCRIPTION);
      I2C_Return:=I2CDeviceStart(BMPDevice, 0);
     


Returns with an ERROR_INVALID_PARAMETER. In I2CDeviceStart() it says:

Code: Select all

function I2CDeviceStart(I2C:PI2CDevice;Rate:LongWord):LongWord;
{Start the specified I2C device ready for reading and writing}
{I2C: The I2C device to start}
{Rate: The clock rate to set for the device (0 to use the default rate)}
{Return: ERROR_SUCCESS if completed or another error code on failure}


So i thought i am starting the I2C bus with the default rate by passing 0 as the 2nd argument. Is that the mistake?

EDIT: The breakout adapter thingy i am using labels the I2C pins as "SDA0" and "SCL0". So naturally i thought i need to use "BCM2710_I2C0_DESCRIPTION", which apparently is wrong! I get readings from the device now when using "BCM2710_I2C1_DESCRIPTION"...!!!
User avatar
Ultibo
Site Admin
Posts: 1975
Joined: Sat Dec 19, 2015 3:49 am
Location: Australia

Re: Data Aquisition Box, preferably with ~100kHz@16bit Signal sampling

Postby Ultibo » Fri Aug 17, 2018 12:00 pm

_Tropo wrote:I get readings from the device now when using "BCM2710_I2C1_DESCRIPTION"...!!!

Excellent news, thanks for the update.

The RPi has 3 I2C busses built into the chip, I2C2 is permanently used by the HDMI and is not available to use, I2C0 is only accessible on some devices and is otherwise used for the ID EEPROM functionality. I2C1 is the one used for most other purposes and is the one that is readily accessible on the 40 pin header.

With regard to the I2C protocol, often the data sheets document the entire protocol layer needed to communicate with the device, in the case of the Pi we have an I2C controller available that handles most of the protocol details for us and so we normally only need to write a register address and then read the specified number of bytes.

By my reading of the BMP280 data sheet getting a pressure and temperature reading only requires reading 6 bytes from register 0xF7 (which will read 0xF7 to 0xFC) so your call to I2CDeviceWriteRead is exactly all that is needed.

_Tropo wrote:Sorry for bothering you again with so many questions!

No problem at all, we are happy to help if we can.
Ultibo.org | Make something amazing
https://ultibo.org
_Tropo
Posts: 48
Joined: Tue Mar 20, 2018 1:01 pm
Location: Germany

Re: Data Aquisition Box, preferably with ~100kHz@16bit Signal sampling

Postby _Tropo » Fri Aug 17, 2018 2:59 pm

Thanks for the Input once again! I got the I2C working and can read Samples from it!
_Tropo
Posts: 48
Joined: Tue Mar 20, 2018 1:01 pm
Location: Germany

Re: Data Aquisition Box, preferably with ~100kHz@16bit Signal sampling

Postby _Tropo » Mon Aug 20, 2018 4:46 pm

Alright, as i feared, it also does not work to just let the routine that is not running on my dedicated cpu, which does all the saving from buffers into files, do the sampling of the BMP280. Writing the buffers to disk takes too long, so that i cannot achieve the desired 100Hz for the BMP280. So then i guess i will have to make a 2nd dedicated thread to sample the BMP280. Is that as straight forward as i think it is? Just calling everything in the same order as for the other dedicated thread, but with another CPU_ID (for example CPU_ID_2) and with another Function to pass to BeginThread() as first argument? So i'd just wrap everything in a procedure and call that twice with different arguments, once for each dedicated thread?
On that other dedicated thread, i would leave the interrupts enabled since i would expect that not to have too much impact if i just sample with 100Hz. Would it be possible to have something like a global switch i can set in my first dedicated thread (the one that is running a while true loop anyway, where all interrupts are disabled) and make it work as some kind of semaphore, or some other signal for the BMP-Thread to wake up? I would like to not have the second dedicated thread run under full load as well in order to save some battery. Except it is not expected to make much of a difference. But it should right? Because then i could have the cpu of the second dedicated thread idle most of the time. If that is not possible i could just let the thread sleep for like 95% of the time until the next sample is due before going back to constantly look at the time again, but the other way seems like a better approach.

You think that this is an ok way to do it? Or would some other way be smarter?

Return to “Projects”

Who is online

Users browsing this forum: No registered users and 0 guests