Saturday, 17 March 2018

Higher Speed ADC Part 4

The Structure Of The Code

Pi Side

The Pi side code is fairly straightforward as far as acquisition goes. We'll start up the system with the signal level on the strobe line "high", which we read through the GPIO interface. Then we wait for it to toggle, and every time it toggles we transfer a chunk of SPI data. And that's it.

We read the strobe through Pi GPIO - we want to use GPIO 4 as an input. So we have a simple process of

  • Claim the GPIO pin
  • Set the direction
  • Read the values
We can claim the input pin using the sys interface (write "4" to /sys/class/gpio/export) and set the direction (writing "input" to "/sys/class/gpio/gpio4/direction"). Then we can read the values by looking in "/sys/class/gpio/gpio4/value". Easy Peasy.

Using the SPI is slightly more involved, but not very. First we have to make sure it's enabled in the Pi configuration tool, raspi-config.

When this is done the SPI device will be visible under "/dev/spidev0.0". We want to open this device, use system IOCTLs to configure it and then read data buffers from it when the toggle tells us to.

Setting up the SPI is simple; we need to configure the mode, speed and data format. The mode tells us how the SPI clock and data relate (i.e. which edge to use when sending and receiving). In this case we just want to use "MODE0"to match the setup of the STM32. SPI speed is the clock rate - we're requesting "16000000" for 16MHz, and "8000000" for 8MHz. Data format just specifies the number of bits we get in each transferred word. It's always 8 for us (a byte at a time) So, cutting out the error check the code would look like this....

  spi_mode = SPI_MODE_0;
  spi_bitsPerWord = 8;
  spi_speed = 16000000; // 16M

   spi_cs_fd = open("/dev/spidev0.0", O_RDWR);
   rvalue = ioctl(spi_cs_fd, SPI_IOC_WR_MODE, &spi_mode);
   rvalue = ioctl(spi_cs_fd, SPI_IOC_RD_MODE, &spi_mode);
   rvalue = ioctl(spi_cs_fd, SPI_IOC_WR_BITS_PER_WORD, &spi_bitsPerWord);
   rvalue = ioctl(spi_cs_fd, SPI_IOC_RD_BITS_PER_WORD, &spi_bitsPerWord);
   rvalue = ioctl(spi_cs_fd, SPI_IOC_WR_MAX_SPEED_HZ, &spi_speed);
   rvalue = ioctl(spi_cs_fd, SPI_IOC_RD_MAX_SPEED_HZ, &spi_speed); 

Next up is the actual transfer. This is done by filling in a structure of type "spi_ioc_transfer" and passing it to the SPI_IOC_MESSAGE IOCTL. Assuming the send is of "length" bytes of "data" on device "spi_device", then:

struct spi_ioc_transfer tr;

    memset(&tr, 0, sizeof(struct spi_ioc_transfer));
    tr.tx_buf = (unsigned long)data;
    tr.rx_buf = (unsigned long)data;
    tr.len = length;
    tr.delay_usecs = 0;
    tr.speed_hz = 16000000;
    tr.bits_per_word = 8;
    retVal = ioctl(spi_device, SPI_IOC_MESSAGE(1), &tr) ;
So, our data transfer loop is to wait for the strobe pin to change value and then issue the transfer request.

STM32 Side

This is really a mash up of three of the reference applications: The ADC/DMA, the SPI/DMA and the UART debug application. The peripheral setup is taken (more or less) directly from these reference applications. We build a default application skeleton (using the STM32 Ac6/openstm32 toolset). Then move in the MspInit and DeInit routines from the reference code into a common stm32f7xx_hal_msp.c file. Other than restructuring the includes and making sure the defines in hal_conf.h are correct this is basically just boilerplate.

The UART debug module is useful to include for debug tracing, but is generally too intrusive to use outside error cases.

The reference examples are in the STM32CubeF7 Firmware release under the directory Projects/STM32F767ZI-Nucleo/Examples. I'm using V1.8.0.

The main loop on the STM side is simply the following sequence in a continual loop:

    Wait For ADC Half Complete
    Setup SPI Transfer 
    Toggle Strobe GPIO 

    Wait For ADC Complete
    Setup SPI Transfer 
    Toggle Strobe GPIO 

The completion of ADC conversion and SPI transfer is signalled using volatiles. In the case of the STM32 code these are denoted with the __IO type, e.g.

__IO uint32_t conversionReady;
__IO uint32_t conversionHalfReady;
These variables are set in the interrupt handler, and checked/cleared in the main code. I'm not a fan of this approach since volatiles are neither atomic nor fixed and really shouldn't be used as cheap semaphore substitutes in this way. However this kind of signalling is fairly common in the HAL, and since this is just a check/set/clear process it should be fine.

So, for example, we have the ADC handlers:

void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* AdcHandle) {
    conversionHalfReady = true;
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* AdcHandle) {
    conversionReady = true;
}
And then main code can do tests like:
  if (conversionHalfReady == true) {
            conversionHalfReady = false;
            return true;
        }

As mentioned, since we have a possible overlap between the tail of some SPI transfers and the scheduling of the next then the SPI code then our SPI send is actually:

  Wait for SPI state to become HAL_SPI_STATE_READY
  Issue SPI DMA Transmit

Test Mode

To make sure that we have a solid link initially a test pattern goes through the interface. This is just a counting sequence which is easy to check when we recover it from the incoming buffer. The ADC conversion is running normally on the STM32 side, with the test data buffers substituted in to the final transfer.

Actual Data

For testing then I've got a reference signal generator pushing in waveforms. Testing with a 30KHz sine wave injected into the ADC input with the sampling set to 28 cycles, then from our earlier calculations then we reckon we should be seeing ~675KSample/S, so a 30K sine input implies about 22.5 samples per cycle. So 45 Samples is two cycles, 100 points should cover about 4.4 cycles, and 450 points twenty cycles.

Here's some outputs:
45 Samples:

Graph from samples 10 -> 110:
Graph from samples 10 -> 460:

"Close Enough"