Данные SPI поступают при каждой второй отправке и в неправильной последовательности.

Я моделирую проект, в котором два STM32 взаимодействуют друг с другом через SPI (драйверы HAL), где SPI Master — это STM32F4, а SPI Slave — это STM32F3. В основном, мастер отправляет 5 байтов данных (1 байт команды, за которым следуют 4 фиктивных байта), а ведомый ответит мастеру 4 байтами данных в зависимости от первого полученного байта команды.

Как должна работать система : нажатие пользовательской кнопки на ведущем устройстве отправит данные SPI (в режиме без прерывания) на ведомое устройство (и ведомое устройство получит данные в режиме прерывания). Затем ведомое устройство отправляет данные обратно ведущему в зависимости от первого полученного байта. На ведомом устройстве есть синий светодиодный индикатор того, были ли получены данные SPI, и зеленый светодиодный индикатор на ведущем устройстве, который показывает, получает ли оно правильную последовательность данных от ведомого устройства. Ниже показано, как синхронизированные данные должны быть отправлены/получены мастером:

Синхронизированный обмен данными SPI

Проблема с ведомым устройством : при первых нажатиях кнопок ведомое устройство успешно получает данные от ведущего. После нескольких циклов наблюдения за светодиодами и установки точек останова в режиме отладки я заметил, что слейв начинает получать данные после каждых 2 нажатий кнопки на мастере. В ведомом устройстве полученные данные верны. Я также должен отметить, что процедура обслуживания прерывания на мастере выполняется при каждом нажатии кнопки, поэтому данные SPI должны отправляться при каждом нажатии кнопки.

Проблема с ведущим устройством : ведущее устройство никогда не получает правильную последовательность данных. Если, например, ведомое устройство отправляет данные: 0x01, 0x02, 0x03, 0x04 в таком порядке, то ведущее устройство получит данные в следующем порядке: 0x02, 0x03, 0x04, 0x01. Кроме того, это также происходит каждые 2 нажатия кнопки.

Вот как выглядит событие:

  1. n нажатие кнопки:
    • Ведомый: данные правильно принимаются ведомым (и отправляются обратно ведущему)
    • Мастер: Получено значение мусора
  2. (n+1) нажатие кнопки:
    • Ведомый: данные не принимаются ведомым (и не отправляются обратно, потому что ведомый отправляет данные на основе полученных данных)
    • Мастер: соответствующие значения получены, но в неправильном порядке
  3. (n+2) нажатие кнопки (то же самое, что произошло при нажатии n-й кнопки):
    • Ведомый: данные правильно принимаются ведомым (и отправляются обратно ведущему)
    • Мастер: Получено значение мусора и так далее.

Вот код конфигурации SPI для Мастера:

static void MX_SPI1_Init (void) {

   /* SPI Configuration for the STM32F4 (Master) */
   /* APB2 Clock set to 8MHz to match APB2 clock of Slave*/

   hspi1.Instance = SPI1;
   hspi1.Init.Mode = SPI_MODE_MASTER;
   hspi1.Init.Direction = SPI_DIRECTION_2LINES;
   hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
   hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
   hspi1.Init.CLKPhase = SPI_PHASE_2EDGE;
   hspi1.Init.NSS = SPI_NSS_SOFT;
   hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2;
   hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
   hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
   hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
   hspi1.Init.CRCPolynomial = 7;

   if (HAL_SPI_Init(&hspi1) != HAL_OK) {
      Error_Handler();
   }
}

Конфигурация SPI для Slave:

static void MX_SPI1_Init (void) {

   /* SPI Configuration for the STM32F3 (Slave) */
   /* APB2 Clock is 8MHz */

   hspi1.Instance = SPI1;
   hspi1.Init.Mode = SPI_MODE_SLAVE;
   hspi1.Init.Direction = SPI_DIRECTION_2LINES;
   hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
   hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
   hspi1.Init.CLKPhase = SPI_PHASE_2EDGE;
   hspi1.Init.NSS = SPI_NSS_SOFT;
   hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
   hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
   hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
   hspi1.Init.CRCPolynomial = 7;
   hspi1.Init.CRCLength = SPI_CRC_LENGTH_DATASIZE;
   hspi1.Init.NSSPMode = SPI_NSS_PULSE_DISABLE;

   if (HAL_SPI_Init(&hspi1) != HAL_OK) {
      Error_Handler();
   }
   
   /* Set interrupt priority and enable interrupts for SPI1 */
   HAL_NVIC_SetPriority(SPI1_IRQn, 1, 0);
   HAL_NVIC_EnableIRQ(SPI1_IRQn);
}

Вот соответствующий код для Мастера (сегмент прерывания кнопки):

void HAL_GPIO_EXTI_Callback (uint16_t GPIO_Pin) {

   /* Clear wake-up power flag */
   __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);

   if (GPIO_Pin == PUSH_BUTTON_PIN) {
      /* Transmit one command byte located in pTxBuff array of length cTxLen */
      HAL_SPI_Transmit(&hspi1, (uint8_t*)pTxBuff, cTxLen, spi_timeout);

      /* Wait for end of SPI transmission */
      while(HAL_SPI_GetState(&hspi1) != HAL_SPI_STATE_READY);

      /* Transmit dummy values located in pTempTxBuff and store retrieved relevant data in pRxBuff */
      HAL_SPI_TransmitReceive(&hspi1, (uint8_t*)pTempTxBuff, (uint8_t*)pRxBuff, cRxLen, spi_timeout);

      /* Flash LED if correct sequence stored in pRxBuff was received - cRxLen = 4 */
      if((pRxBuff[0] == DATA0) && (pRxBuff[1] == DATA1) && (pRxBuff[2] == DATA2) && (pRxBuff[3] == DATA3)) {
         /* Toggle dedicated green LED */
         ToggleLED();
      }

}

И соответствующий код для Slave:

void HAL_SPI_RxCpltCallback (SPI_HandleTypeDef *hspi) {
   /* Toggle blue LED when SPI receives data through interrupt */
   ToggleLED();

   /* Check the data received through SPI interrupt stored in pRxBuff[0] (array is of size 1) */
   if (pRxBuff[0] == COMMAND_DATA) {

      /**
       * pTxBuff := contains appropriate data to be sent to Master device based on this specific command
       * pTempRxBuff := array to store dummy variables, this array is of size 4
       */
      HAL_SPI_TransmitReceive(&hspi1, (uint8_t*)pTxBuff, (uint8_t*)pTempRxBuff, cTxLen, spi_timeout);
   }

   /* Set device in SPI Receive interrupt mode again and store received data in pRxBuff of size 1 */
   HAL_SPI_Receive_IT(&hspi1, (uint8_t*)pRxBuff, cRxCmdLen);
}

Я не понимаю, где здесь ошибка. Если только мастер отправляет данные на ведомое устройство, все работает нормально. Я также сопоставил конфигурацию SPI и тактовую частоту шины. Я также попытался изменить линию направления, SPI_DIRECTION_1LINEкак было предложено в другом месте, но это не сработало. Я также пытался использовать HAL_SPI_Transmit()and HAL_SPI_Receive()отдельно вместо HAL_SPI_TransmitReceive(), но те же проблемы все еще возникают. Кроме того, я хотел бы использовать HAL_SPI_TransmitReceive()оба, чтобы проверить на Мастере и Невольнике, что устройства взаимодействуют должным образом. Любая помощь будет принята с благодарностью.

Спасибо!

Похоже, у вас может быть проблема с синхронизацией либо между испытаниями, либо между ожиданием и реальностью. Рассмотрите возможность использования осциллографа или дешевого логического анализатора USB для работы с sigrok.
Вы не должны делать все эти вызовы SPI внутри HAL_GPIO_EXTI_Callback. Этот обратный вызов вызывается непосредственно из обработчика прерывания EXTI, поэтому вы затем выполняете эти блокирующие вызовы SPI из прерывания EXTI. Скорее просто установите volatileфлаг из EXTI, а затем в своем основном цикле выполните операции SPI, когда он увидит, что флаг установлен.
То же самое и HAL_SPI_RxCpltCallbackв рабе. Вы не должны выполнять блокировку HAL_SPI_TransmitReceiveвнутри обратного вызова (который на самом деле все еще находится внутри прерывания). Либо используйте другой volatileфлаг там, либо сделайте HAL_SPI_TransmitReceive_ITвместо этого и сделайте свое HAL_SPI_RxCpltCallbackсостояние, чтобы он помнил, делал ли он в последний раз HAL_SPI_TransmitReceive_ITдля 4 байтов или HAL_SPI_Receive_ITдля 1 байта.
Также убедитесь, что ваш триггерный переключатель правильно защищен от дребезга, чтобы вы случайно не инициировали последовательности триггеров SPI друг над другом. Это первое, о чем я подумал, прочитав ваш вопрос.
@ChrisStratton Каким будет решение, если возникнут проблемы с синхронизацией? Одна из проблем синхронизации, о которой я мог подумать, заключается в том, что вторая передача ведущего, вероятно, начнется раньше, потому что ведомому устройству потребуется время, чтобы проверить первый байт команды, отправленный ведущим. Должны ли мы также сделать системные часы на обоих устройствах одинаковыми? Ведущий в настоящее время работает на частоте 16 МГц, а ведомый работает на частоте 8 МГц. Я предполагаю, что это должно быть хорошо, потому что транзакция SPI будет тактироваться часами SPI.
@brhans Спасибо за предложение! Я попытаюсь использовать изменчивые флаги и обрабатывать задачи в основном цикле while. Что касается самого кода, правильна ли логика/последовательность для отправки данных с обратной синхронизацией на основе команды, полученной ведущим устройством? Меня другое беспокоит то, что мой подход к этому может быть неправильным.
@MichaelKaras Я добавлю механизм устранения дребезга. Я считаю, что у меня была задержка, но я удалил ее, внося изменения в код.
@brhans Я реализовал второй предложенный вами подход на ведомом устройстве, где вы это делаете, HAL_SPI_TransmitReceive_ITи добавил состояния функции HAL_SPI_RxCpltCallback. Однако, похоже, HAL_SPI_TransmitReceive_ITне вызывается HAL_SPI_RxCpltCallbackпри запросе 4 фиктивных байтов от мастера (хотя прерывание сработало, поскольку оно получило все 4 правильных фиктивных байта от мастера и сохранило их в массиве). Любая причина, по которой это может быть так?
@brhans Я также протестировал HAL_SPI_TransmitReceive_ITwith HAL_SPI_TxRxCpltCallbackи , HAL_SPI_TxCpltCallbackно ни одна из функций не была запущена после HAL_SPI_TransmitReceive_ITуспешной обработки.

Ответы (2)

В SPI MASTER и Slave фактически соединены в цикле с буферами FIFO. Как показано на картинке.
Попробуйте очищать эти буферы данных и сдвиговые регистры перед каждой передачей кадра данных. Как на ведущем, так и на ведомом контроллерах.

Схема связи SPI

(Источник изображения: Electronics Hub — основы последовательного периферийного интерфейса (SPI) )

Embedded Guy - Привет! В соответствии с этим правилом сайта , когда мы включаем в ответ что-то (например, фото, изображение или текст), которое не является нашей собственной оригинальной работой, мы должны правильно указать (процитировать) это. Это изображение пришло откуда-то еще, поэтому, чтобы соответствовать этому правилу, на него должна быть ссылка. Я нашел (как мне кажется) первоисточник и на этот раз добавил эту ссылку для вас. Пожалуйста, вы можете сделать это в будущем? Спасибо! (Я рекомендую вам также прочитать тур и справочный центр , чтобы узнать больше о правилах сайта.)
Embedded Guy - В STM32 есть буферы SPI fifo, которые очень упрощают ошибки выравнивания данных.

В общем, я настоятельно рекомендую не использовать HAL для коммуникационного кода. Для SPI, UART и I2C он действительно работает только в сценарии «счастливого пути» и, как правило, не дает элегантных сбоев (постоянные проблемы с выравниванием данных и т. д.). Кроме того, он очень раздутый и медленный, что может создать проблемы, если вы пытаетесь выполнять действия, требующие большой скорости (например, анализировать кадр SPI во время его активного получения, как в данном случае). Сгенерированный STM32Cube код настраивает для вас функции обработчика прерываний в файле stm32xx_it.c; вы можете закомментировать сгенерированные обработчики HAL и добавить свои собственные. Не забудьте очистить флаги прерывания перед возвратом из функции!

Некоторые мысли о том, что идет не так, и несколько советов по поводу продолжения работы над этим проектом:

  1. Вы неправильно используете HAL. На самом деле невозможно сделать то, что вы хотите сделать правильно, используя HAL вообще. Чтобы ведомое устройство отвечало новыми данными на основе первого байта, полученного от ведущего, вам нужен код для анализа первого байта внутри самого обработчика прерывания SPI, а не обратного вызова. Функции обратного вызова HAL вызываются внутри обработчика прерывания SPI после получения полного кадра (т. е. кадра ожидаемого размера с нарастающим переходом nSS). Чтобы реализовать описанную систему, вам придется написать собственный обработчик прерываний SPI.

  2. Как указал Embedded Guy, периферийные устройства STM32 SPI имеют буфер FIFO, который может быть заполнен мусором из-за программных ошибок (на ведомом иливедущая сторона), ложные фронты тактовых импульсов, вызванные шумом, и т. д. К сожалению, нет явной команды «очистить fifo», которая может быть запущена изнутри самого периферийного устройства. Следующая очевидная идея — использование регистров данных RX/TX для очистки FIFO — также не на 100% эффективна (хотя в большинстве случаев она может быть достаточно хорошей). Например, в ситуации, когда вы получили 5/8 бит слова, чтение регистра данных RX не удалит этот неполный байт из FIFO. Единственный надежный способ очистить буферы SPI FIFO, который я обнаружил, — это использовать периферийное устройство RCC (сброс тактового контроллера) для полного сброса периферийного устройства SPI, а затем быстро повторно инициализировать его. К счастью, это не так страшно, как кажется,

Заключительный совет: может быть нехорошо при проектировании системы пытаться заставить ведомое устройство анализировать первый полученный байт во время активного приема кадра SPI. Это просто потому, что SPI может работать так быстро, что вы, возможно, не закончили синтаксический анализ первого байта и подготовку байтов ответа к тому времени, когда мастер закончил тактирование первого (у вас оченьограниченные временные рамки для этого, только ширина одного часового перехода!). Если ваши часы SPI медленные, а внутри обработчика прерывания работает очень простой и быстрый анализатор, вы, вероятно, сможете обойтись без этой спецификации. Однако может быть лучше отправить байт «команды» в полном кадре SPI, а затем выдать отдельный многобайтовый кадр чтения. В этом случае у ведомого будет гораздо больше времени, чтобы проанализировать основную команду, прежде чем подготовить ответное сообщение.