Как круговой DMA-периферия к памяти будет вести себя в конце передачи в STM32? - PullRequest
3 голосов
/ 09 февраля 2020

Я хотел спросить, как поведет себя DMA SPI rx в STM32 в следующей ситуации. У меня есть указанный (например) 96-байтовый массив под названием A, который предназначен для хранения данных, полученных от SPI. Я включаю мой круговой SPI DMA, который работает с каждым байтом, настроенный на 96 байтов. Возможно ли, когда DMA заполнит мой 96-байтовый массив, прервется прерывание Transfer Complete, чтобы быстро скопировать 96-байтовый массив в другой - B, прежде чем циклический DMA начнет запись в A (и уничтожит данные, сохраненные в B) ? Я хочу быстро (каждый раз, когда получу новые данные из A в B) быстро передавать данные с B по USB на P C.

. Я просто думаю о том, как передавать непрерывный поток данных SPI из STM32. через USB на P C, потому что блок из 96 байт данных, передаваемых через USB один раз за определенное время, проще, чем потоковая передача в реальном времени SPI на USB через STM32? Я даже не знаю, возможно ли это

Ответы [ 3 ]

4 голосов
/ 09 февраля 2020

Чтобы это работало, вы должны быть в состоянии гарантировать, что вы можете скопировать все данные до того, как следующий байт SPI будет получен и передан в начало буфера. Будет ли это возможно, будет зависеть от тактовой частоты процессора и скорости SPI, и сможет гарантировать отсутствие прерываний с более высоким приоритетом, которые могут задержать передачу. Чтобы быть в безопасности, ему понадобится исключительно медленная скорость SPI, и в этом случае, вероятно, вообще не потребуется использовать DMA.

В целом это плохая идея и совершенно ненужная. Контроллер DMA имеет прерывание «полупередачи» именно для этой цели. Вы получите прерывание HT, когда первые 48 байтов будут переданы, а DMA продолжит передавать оставшиеся 48 байтов, пока вы копируете нижнюю половину буфер. Когда вы завершите перевод, вы переведете верхнюю половину 1006 *. Это увеличивает время, необходимое для передачи данных, от времени получения одного байта до времени получения 48 байтов.

Если вам действительно нужно 96 байтов на каждую передачу, то вы просто делаете свой буфер 192 байта long (2 x 96).

В псевдокоде:

#define BUFFER_LENGTH 96
char DMA_Buffer[2][BUFFER_LENGTH] ;

void DMA_IRQHandler()
{
    if( DMA_IT_Flag(DMA_HT) == SET )
    {
        memcpy( B, DMA_Buffer[0], BUFFER_LENGTH ) ;
        Clear_IT_Flag(DMA_HT) ;
    }
    else if( DMA_IT_Flag(DMA_TC) == SET )
    {
        memcpy( B, DMA_Buffer[1], BUFFER_LENGTH ) ;
        Clear_IT_Flag(DMA_TC) ;
    }
}

Что касается передачи данных на P C по USB, прежде всего необходимо убедиться, что что ваша скорость передачи USB, по крайней мере, так же быстро или быстрее, чем скорость передачи SPI. Вполне вероятно, что передача USB менее определена c (поскольку она контролируется хостом P C - то есть вы можете выводить данные на USB только тогда, когда хост явно запрашивает это), так что даже если средняя скорость передачи достаточна, возможны задержки, которые требуют дальнейшей буферизации, поэтому вместо простого копирования из буфера DMA A в буфер B USB вам может понадобиться кольцевой буфер или очередь FIFO для подачи на USB , С другой стороны, если у вас уже есть буфер DMA_Buffer[0], DMA_Buffer[1] и B, у вас уже есть FIFO из трех блоков по 96 байт, которых может быть достаточно

2 голосов
/ 09 февраля 2020

Обычно использование циклического прямого доступа к памяти работает только в том случае, если вы запускаете наполовину полный / наполовину пустой, в противном случае у вас недостаточно времени для копирования информации из буфера.

Я бы рекомендовал не копировать данные из буфера во время прерывания. Скорее используйте данные прямо из буфера без дополнительного шага копирования.

Если вы делаете копию в прерывании, вы блокируете другие прерывания с более низким приоритетом во время копирования. На STM32 простая наивная байтовая копия 48 байтов может занять дополнительные 48 * 6 ~ 300 тактов.

Если вы отслеживаете позиции чтения и записи в буферах независимо, вам просто нужно обновить один указатель и отправить отложенный вызов уведомления потребителю буфера.

Если вы хотите более длительный период, тогда не используйте циклический DMA, вместо этого используйте обычный DMA в 48-байтовых блоках и используйте кольцевой байтовый буфер в качестве структуры данных.

Я сделал это для USART со скоростью 460 кбод, которая принимает асинхронно пакеты переменной длины. Если вы убедитесь, что производитель обновляет только указатель записи, а потребитель обновляет только указатель чтения, вы можете избежать скачек данных в большинстве из них. Обратите внимание, что чтение и запись выровненной <= 32-битной переменной в коре m3 / m4 - это atomi c. </p>

Включенный код - это упрощенная версия кольцевого буфера с поддержкой DMA, которую я использовал. Он ограничен размерами буфера 2 ^ n и использует функциональные возможности шаблонов и C ++ 11, поэтому он может не подходить в зависимости от ограничений вашей разработки / платформы.

Чтобы использовать вызов буфера getDmaReadBlock () или getDMAwriteBlock () и получить адрес памяти DMA и длину блока. Когда DMA завершает работу, используйте skipRead () / skipWrite () для увеличения указателей чтения или записи на фактическую сумму, которая была передана.

 /**
   * Creates a circular buffer. There is a read pointer and a write pointer
   * The buffer is full when the write pointer is = read pointer -1
   */
 template<uint16_t SIZE=256>
  class CircularByteBuffer {
    public:
      struct MemBlock {
          uint8_t  *blockStart;
          uint16_t blockLength;
      };

    private:
      uint8_t *_data;
      uint16_t _readIndex;
      uint16_t _writeIndex;

      static constexpr uint16_t _mask = SIZE - 1;

      // is the circular buffer a power of 2
      static_assert((SIZE & (SIZE - 1)) == 0);

    public:
      CircularByteBuffer &operator=(const CircularByteBuffer &) = default;

      CircularByteBuffer(uint8_t (&data)[SIZE]);

      CircularByteBuffer(const CircularByteBuffer &) = default;

      ~CircularByteBuffer() = default;

    private:
      static uint16_t wrapIndex(int32_t index);

    public:
      /*
       * The number of byte available to be read. Writing bytes to the buffer can only increase this amount.
       */
      uint16_t readBytesAvail() const;

      /**
       * Return the number of bytes that can still be written. Reading bytes can only increase this amount.
       */
      uint16_t writeBytesAvail() const;

      /**
       * Read a byte from the buffer and increment the read pointer
       */
      uint8_t readByte();

      /**
       * Write a byte to the buffer and increment the write pointer. Throws away the byte if there is no space left.
       * @param byte
       */
      void writeByte(uint8_t byte);

      /**
       * Provide read only access to the buffer without incrementing the pointer. Whilst memory accesses outside the
       * allocated memeory can be performed. Garbage data can still be read if that byte does not contain valid data
       * @param pos the offset from teh current read pointer
       * @return the byte at the given offset in the buffer.
       */
      uint8_t operator[](uint32_t pos) const;

      /**
       * INcrement the read pointer by a given amount
       */
      void skipRead(uint16_t amount);
      /**
       * Increment the read pointer by a given amount
       */
      void skipWrite(uint16_t amount);


      /**
       * Get the start and lenght of the memeory block used for DMA writes into the queue.
       * @return
       */
      MemBlock getDmaWriteBlock();

      /**
       * Get the start and lenght of the memeory block used for DMA reads from the queue.
       * @return
       */
      MemBlock getDmaReadBlock();

  };

  // CircularByteBuffer
  // ------------------
  template<uint16_t SIZE>
  inline CircularByteBuffer<SIZE>::CircularByteBuffer(uint8_t (&data)[SIZE]):
      _data(data),
      _readIndex(0),
      _writeIndex(0) {
  }

  template<uint16_t SIZE>
  inline uint16_t CircularByteBuffer<SIZE>::wrapIndex(int32_t index){
    return static_cast<uint16_t>(index & _mask);
  }

  template<uint16_t SIZE>
  inline uint16_t CircularByteBuffer<SIZE>::readBytesAvail() const {
    return wrapIndex(_writeIndex - _readIndex);
  }

  template<uint16_t SIZE>
  inline uint16_t CircularByteBuffer<SIZE>::writeBytesAvail() const {
    return wrapIndex(_readIndex - _writeIndex - 1);
  }

  template<uint16_t SIZE>
  inline uint8_t CircularByteBuffer<SIZE>::readByte() {
    if (readBytesAvail()) {
      uint8_t result = _data[_readIndex];
      _readIndex = wrapIndex(_readIndex+1);
      return result;
    } else {
      return 0;
    }
  }

  template<uint16_t SIZE>
  inline void CircularByteBuffer<SIZE>::writeByte(uint8_t byte) {
    if (writeBytesAvail()) {
      _data[_writeIndex] = byte;
      _writeIndex = wrapIndex(_writeIndex+1);
    }
  }

  template<uint16_t SIZE>
  inline uint8_t CircularByteBuffer<SIZE>::operator[](uint32_t pos) const {
    return _data[wrapIndex(_readIndex + pos)];
  }

  template<uint16_t SIZE>
  inline void CircularByteBuffer<SIZE>::skipRead(uint16_t amount) {
    _readIndex = wrapIndex(_readIndex+ amount);
  }

  template<uint16_t SIZE>
  inline void CircularByteBuffer<SIZE>::skipWrite(uint16_t amount) {
    _writeIndex = wrapIndex(_writeIndex+ amount);
  }

  template <uint16_t SIZE>
  inline typename CircularByteBuffer<SIZE>::MemBlock  CircularByteBuffer<SIZE>::getDmaWriteBlock(){
    uint16_t len = static_cast<uint16_t>(SIZE - _writeIndex);
   // full is  (write == (read -1)) so on wrap around we need to ensure that we stop 1 off from the read pointer.
    if( _readIndex == 0){
      len = static_cast<uint16_t>(len - 1);
    }
    if( _readIndex > _writeIndex){
      len = static_cast<uint16_t>(_readIndex - _writeIndex - 1);
    }
    return {&_data[_writeIndex], len};
  }

  template <uint16_t SIZE>
  inline typename CircularByteBuffer<SIZE>::MemBlock  CircularByteBuffer<SIZE>::getDmaReadBlock(){
    if( _readIndex > _writeIndex){
      return {&_data[_readIndex], static_cast<uint16_t>(SIZE- _readIndex)};
    } else {
      return {&_data[_readIndex], static_cast<uint16_t>(_writeIndex - _readIndex)};
    }
  }
`
2 голосов
/ 09 февраля 2020

В одном из моих проектов я столкнулся с подобной проблемой. Задача состояла в том, чтобы передать данные, поступающие с внешнего чипа AD C (соединенного с SPI), на P C по полной скорости USB. Данные были (8 × 16 бит), и меня попросили достичь максимально возможной частоты дискретизации.

В итоге я получил решение с тройным буфером . Существует 4 возможных состояния, в которых может находиться буфер:

  1. ГОТОВ: Буфер заполнен данными, готов к отправке через USB
  2. SENT : Буфер уже отправлен и устарел
  3. IN_USE: DMA (запрошенный SPI) в настоящее время заполняет этот буфер
  4. СЛЕДУЮЩИЙ: This буфер считается пустым и будет использоваться, когда IN_USE заполнен.

Поскольку синхронизация USB-запроса не может быть синхронизирована с процессом SPI, я считаю, что решение с двойным буфером не будет работать , Если у вас нет буфера NEXT , к тому времени, когда вы решите отправить буфер READY , DMA может завершить sh заполнение буфера IN_USE и начните портить буфер READY . Но в тройном буфере буфер READY безопасен для отправки через USB, так как он не будет заполнен, даже если текущий буфер IN_USE заполнен.

Итак состояния буфера с течением времени выглядят так:

Buf0     Buf1      Buf2
====     ====      ====
READY    IN_USE    NEXT
SENT     IN_USE    NEXT
NEXT     READY     IN_USE
NEXT     SENT      IN_USE
IN_USE   NEXT      READY

Конечно, если P C не запускает USB-запросы достаточно быстро, вы все равно можете потерять READY буфер, как только он превращается в СЛЕДУЮЩИЙ (прежде чем стать SENT ). P C отправляет запросы USB IN асинхронно без информации о текущих состояниях буфера. Если нет буфера READY (он находится в состоянии SENT ), STM32 отвечает ZLP (пакет нулевой длины), а P C пытается снова после задержки в 1 мс.

Для реализации на STM32 я использую режим двойной буферизации и изменяю регистры M0AR и M1AR в DMA Transfer Complete ISR для адресации 3 буферов.

Кстати, я использовал (3 x 4000) байтовых буферов и достигли частоты дискретизации 32 кГц в конце. USB настроен как класс поставщика c и использует массовые передачи.

...