STM32 SPI: странное поведение при пустом TXFIFO (предыдущая история байтов?)

Следующий код настраивает и включает SPI2 в качестве подчиненного устройства на моей плате STM32F303RE , записывает байты 0xAA, 0xBB, 0xCC, 0xDD в регистр DR и зацикливается через некоторое время (1) :

/* Enable clocks for GPIOB (SPI2 pins) and SPI2 peripheral. */
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);

/* SPI pin mappings. */
GPIO_PinAFConfig(GPIOB, GPIO_PinSource12, GPIO_AF_5); /* SPI2_NSS */
GPIO_PinAFConfig(GPIOB, GPIO_PinSource13, GPIO_AF_5); /* SPI2_SCK */
GPIO_PinAFConfig(GPIOB, GPIO_PinSource14, GPIO_AF_5); /* SPI2_MISO */
GPIO_PinAFConfig(GPIOB, GPIO_PinSource15, GPIO_AF_5); /* SPI2_MOSI */

GPIO_InitTypeDef gpio_init_struct =
{
    .GPIO_Mode = GPIO_Mode_AF,
    .GPIO_OType = GPIO_OType_PP,
    .GPIO_PuPd = GPIO_PuPd_DOWN,
    .GPIO_Speed = GPIO_Speed_50MHz
};

/* SPI NSS pin configuration. */
gpio_init_struct.GPIO_Pin = GPIO_Pin_12;
GPIO_Init(GPIOB, &gpio_init_struct);
/* SPI SCK pin configuration. */
gpio_init_struct.GPIO_Pin = GPIO_Pin_13;
GPIO_Init(GPIOB, &gpio_init_struct);
/* SPI MISO pin configuration. */
gpio_init_struct.GPIO_Pin = GPIO_Pin_14;
GPIO_Init(GPIOB, &gpio_init_struct);
/* SPI  MOSI pin configuration. */
gpio_init_struct.GPIO_Pin = GPIO_Pin_15;
GPIO_Init(GPIOB, &gpio_init_struct);

SPI_InitTypeDef spi_init_struct =
{
    .SPI_Direction          = SPI_Direction_2Lines_FullDuplex,
    .SPI_Mode               = SPI_Mode_Slave,
    .SPI_DataSize           = SPI_DataSize_8b,
    .SPI_CPOL               = SPI_CPOL_Low,
    .SPI_CPHA               = SPI_CPHA_1Edge,
    .SPI_NSS                = SPI_NSS_Hard,
    .SPI_BaudRatePrescaler  = SPI_BaudRatePrescaler_2,
    .SPI_FirstBit           = SPI_FirstBit_MSB,
    .SPI_CRCPolynomial      = 7
};

SPI_I2S_DeInit(SPI2);
SPI_Init(SPI2, &spi_init_struct);
SPI_CalculateCRC(SPI2, DISABLE);
SPI_TIModeCmd(SPI2, DISABLE);
SPI_NSSPulseModeCmd(SPI2, DISABLE);

SPI_Cmd(SPI2, ENABLE);
SPI_SendData8(SPI2, (uint8_t) 0xAA);
SPI_SendData8(SPI2, (uint8_t) 0xBB);
SPI_SendData8(SPI2, (uint8_t) 0xCC);
SPI_SendData8(SPI2, (uint8_t) 0xDD);

while(1) { }

С мастером, который запрашивает 2 байта на выбор чипа , мастер получает:

0xAA 0xBB
0xCC 0xDD
0xAA 0xAA -----> TXFIFO should be empty here, why not "0x00 0x00"?
0xAA 0xAA
0xAA 0xAA
0xAA 0xAA
0xAA 0xAA
0xAA 0xAA
0xAA 0xAA
......... (0xAA 0xAA infinite times)

Я ожидал, что мастер получит «0x00 0x00» после того, как TXFIFO станет пустым . Почему вместо этого я постоянно получаю « 0xAA 0xAA »? Я не мог найти что-то, что указывало бы на такое поведение в руководстве.

ОБНОВЛЕНИЕ 1

Ожидание завершения транзакций непосредственно перед while(1) и последующая запись нулей в SPI , например:

while(SPI_GetTransmissionFIFOStatus(SPI2) != SPI_TransmissionFIFOStatus_Empty) { }
while(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_BSY) != RESET) { }

#define ZEROS_CNT   (1)
for(int i = 0; i < ZEROS_CNT; i++)
    SPI_SendData8(SPI2, 0);

while(1) { }

отображает следующее основное поведение для различных значений ZEROS_CNT:

ZEROS_CNT = 0 => master receives after TXFIFO is empty: 0xAA infinitely
ZEROS_CNT = 1 => master receives after TXFIFO is empty: 0x00 1 times, followed by 0xBB infinitely
ZEROS_CNT = 2 => master receives after TXFIFO is empty: 0x00 2 times, followed by 0xCC infinitely
ZEROS_CNT = 3 => master receives after TXFIFO is empty: 0x00 3 times, followed by 0xDD infinitely
ZEROS_CNT >= 4 => master receives after TXFIFO is empty: 0x00 infinitely

Похоже, что периферия SPI имеет какую-то историю того, что было записано в TXFIFO, и когда он становится пустым, он отправляет байты из этой истории.

ОБНОВЛЕНИЕ 2

Он ведет себя одинаково независимо от того, сколько байтов мастер запрашивает за один выбор микросхемы. Я пробовал запрашивать 1, 2, 4 и 5 за раз.

Это выглядит довольно странно. Вы пытались отключить SPI после отправки 0xdd?
Привет, Владимир, да, еще одна странность заключается в том, что после отключения он будет отправлять «0x00 0x00», но если он снова включен, он снова начнет отправлять «0xAA 0xAA» ... два обходных пути для этого: либо отключить его пока у меня не появится что-то новое для отправки -ИЛИ- полный сброс периферийного устройства (SPI_I2S_DeInit(SPI2)). Однако я хотел бы знать, почему он ведет себя таким образом (и по какой логике) и можно ли избежать полноценного сброса (который также требует перенастройки периферийного устройства - как бы лишнего).
Не могли бы вы добавить ссылку на соответствующий appnote/datasheet/что-то еще? Я не могу вникнуть в это прямо сейчас, но, возможно, сегодня вечером оно вернется ко мне в голову.
Вы имеете в виду справочное руководство STM32? Для моей платы STM32F303RE это: st.com/content/ccc/resource/technical/document/reference_manual/…
Вы пробовали ждать поступления полученных данных?
@domen См. ОБНОВЛЕНИЕ
Что? Нет. Дождитесь приема данных SPI. Возиться с GPIO при отладке проблемы SPI вызывает проблемы (и хакерские решения, которые могут работать).
У меня есть логический анализатор, вышеизложенное работает корректно. Измените его с ожиданием на SPI RX (кстати, зачем ждать на RX, а не до тех пор, пока TXFIFO не станет пустым, а вместо него не будет установлен BSY?), и вы увидите, что он ведет себя так же. :)
Привет, @domen, извини, я только что выключил свой Raspberry Pi 2 (основной) ранее, и у меня не было времени изменить код. Обновил вопрос. Как я и ожидал, ожидание, когда TXFIFO станет пустым, а BSY сбросится, приводит к такому же поведению.
ХОРОШО. Я считаю, что вы должны ждать пустой TXFIFO перед каждым SPI_SendData8. Хотя, честно говоря, я не уверен, как это будет связано. 0xaa (10101010) похоже, что часы могут быть вызваны на линии данных. Какой длины ваши провода? Что находится в регистрах состояния? Я думаю, также возможно, что он принимает случайный байт, когда в регистре TX нет ничего нового.
часы в два раза быстрее, чем данные, поэтому, если произойдет (очень плохое) соединение, я ожидаю либо 0x00, либо 0xff, в зависимости от полярности канала.
@domen Они вовсе не случайны. Описанный эксперимент должен прояснить это. Ожидание того, что TXFIFO станет пустым после каждого байта, не является вариантом (отказывается от цели FIFO), и в любом случае он все равно ведет себя одинаково.

Ответы (2)

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

  • Как указано в руководстве, TXFIFO имеет размер 32 бита = 4 байта — пусть эти байты будут B1|B2|B3|B4 в указанном порядке.
  • ФАКТ № 1 : Когда мастер запрашивает байт у ведомого, извлекается и возвращается байт B1 , но на самом деле он не удаляется из TXFIFO ---> содержимое TXFIFO поворачивается влево , а не сдвигается влево . Это приводит к тому, что TXFIFO после этого всплывающего окна выглядит как B2|B3|B4|B1 , а не как B2|B3|B4|00 (что, я думаю, имел в виду @ogrenci в своем ответе).
  • ФАКТ №2 : Когда TXFIFO становится пустым, по какой-то причине мастер-запрос получает 1-й байт, который находится в TXFIFO @ в этот момент, вместо 0x00 , как я ожидал.

RXFIFO, скорее всего, ведет себя так же.

Алгоритм для push/pop , написанный на C# , выглядит следующим образом:

public class TXFIFO
{
    public byte[] data;
    byte push_position = 1;
    byte occupied = 0;

    public TXFIFO()
    {
        data = new byte[4];
    }

    public byte Push(byte v)
    {
        // write
        data[push_position - 1] = v;
        // push_position
        if (push_position < 4) push_position++;
        else push_position = 1;
        // occupied
        if (occupied < 4) occupied++;
        return v;
    }

    public byte Pop()
    {
        // read
        if (occupied == 0) return data[0];
        byte v = data[0];
        // rotate left once
        for (int i = 1; i < 4; i++)
            data[i - 1] = data[i];
        data[3] = v;
        //push_position
        if (push_position > 1) push_position--;
        else push_position = 4;
        //occupied
        if (occupied > 0) occupied--;
        return v;
    }

    public byte GetOccupied()
    {
        return occupied;
    }
}

А вот 5 анимаций, которые иллюстрируют изначально описанные сценарии ZEROS_CNT (см. ОБНОВЛЕНИЕ 1 вопроса ). Обратите внимание, что для большей ясности вместо нулей я вставил сюда значения 0x01-0x02-..to..-ZEROS_CNT .

ZEROS_CNT = 0:

введите описание изображения здесь

ZEROS_CNT = 1:

введите описание изображения здесь

ZEROS_CNT = 2:

введите описание изображения здесь

ZEROS_CNT = 3:

введите описание изображения здесь

ZEROS_CNT = 4:

введите описание изображения здесь

ZEROS_CNT = 5:

введите описание изображения здесь

...и так далее...

Как упоминалось ранее в вопросе, обходной путь для отправки 0x00 , когда TXFIFO пуст, будет отключать периферийное устройство SPI , когда TXFIFO пуст, пока новые данные не будут записаны в DR , что я и сделал, после понимания того, что происходит .

На самом деле данные, вероятно, не копируются в буфер, а поддерживаются два индекса для начала и конца буферизованных данных. Когда начало и конец указывают на один и тот же элемент буфера, это может означать одно из двух: либо буфер полностью заполнен, либо полностью пуст. Невозможно определить, какой из них, не поддерживая где-то другой флаг. См. Циклический буфер .
@м.Алин спасибо! Надеюсь, они прояснят ситуацию. JimmyB не уверен, что вы пытаетесь сказать - SPI сохраняет информацию, сообщающую вам, каков текущий уровень занятости FIFO.
@JimmyB о, я понимаю. Да, вы, скорее всего, правы. Было бы слишком неэффективно продолжать смещать элементы влево при выполнении всплывающего окна. Во всяком случае, я думаю, что результаты операций pop/push выше будут отображать те же значения. Я думаю, можно попытаться понять, как эта схема сдвига на блок-диаграмме SPI на самом деле работает на электронном уровне, чтобы действительно выяснить, как эти операции выполняются внутри (используемая структура данных).
Просто хотел дать простое объяснение того, почему определенный элемент остается в буфере. Действительно, если устройство знает, когда буфер пуст, схема SPI может/должна использовать этот бит информации для обеспечения согласованного вывода (0x00) вместо «случайного» мусора.
@JimmyB да, спасибо за вклад и извините, я не понял, что вы пытались предложить в первый раз;)
Глупый вопрос: что ты использовал для анимации?
Привет @pgvoorhees. На самом деле ничего особенного, просто немного C# (мой класс TXFIFO имеет метод Draw, возвращающий экземпляр Bitmap, на котором он рисовал свое состояние (см. Классы Bitmap и Graphics). Я бы создал сценарий (последовательность push/pops) и вызвал Рисовать после каждой операции. Сохранял растровые изображения в файлы изображений и использовал gifcreator.me для создания GIF-файлов. Но, скорее всего, для этого есть инструменты получше, а не изобретать велосипед, у меня просто не было настроения их искать, так как у меня была другая работа.

Значения после 4-го байта не берутся из буфера TX подчиненного устройства. Изучите блок-схему SPI. Первоначально они отправляются мастером в виде данных дампа, когда SCI зацикливается, и они возвращаются из регистра сдвига подчиненного устройства и отображаются как действительные данные. Когда ведущий извлекает данные, ожидается, что это будет переопределено, но в данном случае этого не происходит, потому что подчиненный не помещает данные в буфер TX и не загружает сдвиговый регистр.

Привет, ogrenci, вы имели в виду «отправлено как данные дампа, когда SPI циклически запускается Raspberry PI»?
Привет @КорнелиуЗузу. Я имею в виду Raspberry PI. Я отредактировал ответ.