Надеюсь сообщество простит меня за такой заголовок, просто в последнее время все чаще и чаще сталкиваюсь с программами в которых к микроконтроллерам STM32 подключают различные дисплеи с интерфейсом SPI и очень часто передачу данных при этом делают не правильно.
Как следствие — либо код не работает совсем и тогда в него внедряют различные задержки, или пишут код таким образом что он гарантированно будет работать медленно (по сравнению с возможной скоростью). А кто то, не разобравшись просто копирует чужой «с костылями» код, и потом такие «произведения» ходят по интернету из примера в пример…
Блок SPI описанный в данной статье точно есть у контроллеров семейств: STM32F1, STM32F2, STM32F4. По другим смотрите Reference Manual.

Откуда растут такие проблемы и каким образом они решаются под катом.

Для начала я расскажу как работает интерфейс SPI при передаче данных в режиме MASTER.

В Reference manual на стр. 868 есть наглядная схема устройства интерфейса:

если вы уже пробовали передавать данные при помощи SPI, то эту диаграмму вы уже знаете наизусть, ведь так?

наверняка для многих эта схема вообще уже выглядит исключительно понятной, кстати для тех кто ее еще не настолько изучил — я нашел ее русский вариант (документ источник доступен по клику на изображение):


ну что тут может быть проще?
при отправке данных мы записываем их в "Буфер передатчика", из которых они попадают в сдвиговый регистр и по очереди «выдаются» на линию MOSI (на схеме выделено красным цветом). При передаче генерируются флаги BSY и TXE по которым можно узнать состояние передачи.

Как у AVR ?
НЕТ !!

на схеме нарисовано абсолютно точно: при отправке используются два регистра:
— регистр "Буфера передатчика" (SPI_DR),
регистр сдвига (shift register)

и это два регистра, КАЖДЫЙ из которых может иметь свое значение!!!
Для КАЖДОГО из этих регистров предусмотрен свой флаг — который показывает их заполненность.
  • Для регистра SPI_DR — это флаг TXE
  • Для регистра сдвига — это флаг BSY

Для того чтобы понять как вся эта связка двух регистров и двух флагов работает (в Reference manual я не нашел прямого ответа на этот вопрос, хотя если внимательно читать — то там это описывается в логике работы) — разберемся как происходит передача:
  1. для отправки значения (8-ми либо 16-ти битного) мы записываем его в SPI_DR, одновременно происходит установка флага TXE = 0 — что показывает что SPI_DR содержит значение для отправки
  2. поскольку это первое отправляемое значение (до операции флаг BSY = 0), то значение записывается одновременно и в SPI_DR и в регистр сдвига (Shift_Reg), с которого первый бит (в зависимости от настроек MSB/LSB) выставляется по линии MOSI
  3. В следующем SCK такте, после записи значения в SPI_DR и в сдвиговый регистр (Shift_Reg), устанавливается флаг TXE = 1 — что означает что в регистр SPI_DR можно записать следующее значение для отправки. Обращаю внимание, прежнее значение содержится в Shift_Reg и еще выгружается на линию MOSI! (см. схему ниже! в момент установки флага TXE = 1 происходит отправка лишь второго бита первоначального значения (из 8 или 16 бит значения)
  4. поскольку в SPI_DR больше данные не записываем — то с флагом TXE =1 ничего и не происходит, интерфейс ждет загрузки следующего байта..
  5. данные из Shift_Reg выгружаются по такту SCK на линию MOSI. При передаче последнего бита данных, проверяется есть ли новое значение в SPI_DR для отправки (в этом случае флаг TXE = 0), если нет (это флаг TXE = 1), то устанавливается флаг BSY = 0 и передача прекращается.

Схематично передача одного байта по SPI будет выглядеть так:


Согласитесь это не сложно!!!

Зачем так сделано? — для того чтобы обеспечить непрерывность передаваемых данных!

В тех же AVR, при помощи SPI, невозможно передать несколько байт данных без перерыва, всегда между передаваемыми значениями будет пауза в 1 такт SPI. А вот в STM32 возможна по настоящему непрерывная передача, которая будет выглядеть вот так:

Как видно из схемы — для обеспечения непрерывности передачи достаточно всего лишь ожидать установления флага TXE = 1 и записывать в SPI_DR следующее значение для передачи.

Теперь о подключении дисплеев к STM32 по SPI.

1. 90% приведенных в интернете решений (не правильных решений) предлагают делать отправку данных на дисплей одним из следующих вариантов кода:
  • Первый вариант, проверка флага TXE после загрузки данных в SPI_DR:
    void SPISend(uint16_t data) {
    	SPI_I2S_SendData(SPI1, data);  // отправили данные
    	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); // ждём, пока данные не отправятся
    }
    
  • Второй, на первый взгляд более правильный, вариант, проверка флага BSY после отправки данных в SPI_DR:
    void SPISend(uint16_t data) {
            SPI_I2S_SendData(SPI1, data); //Передаем байт data через SPI1
            while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) == SET) //Передатчик занят?
    }
    


Оба варианта не правильны ! Кстати на просторах интернета еще предлагают третий вариант — проверять флаг RXE после отправки данных в SPI_DR, который обычно используется при получении каждого байта (слова) данных — это оставляю без комментариев…

Посмотрите внимательно на схемы которые я приводил выше! Флаг TXE нужно проверять перед отправкой данных в SPI_DR… Дальше будет работать конвейер самого SPI MOSI уйдет первый байт из Shift_Reg, потом Shift_Reg прогрузится значением из SPI_DR, и опять произойдет отправка в MOSI)
То есть код отправки данных на дисплей должен выглядеть следующим образом (пример для 16-ти битных посылок):
void SPISend(uint16_t data) {
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); // ждём, освобождения SPI_DR
	SPI_I2S_SendData(SPI1, data);  // отправим байт данных в очередь на отправку
}

Только в этом случае можно получить самую большую скорость обмена по SPI!

Почему же самый очевидный способ не используют?

Дело в том что многие дисплеи имеют наряду со стандартными SPI выводами (SCK, MOSI, MISO, CS) и такой вывод как DC (D/C, A0, CMD и так далее)
Вывод DC показывает что же передается в дисплей, обычно при DC=0 дисплей воспринимает переданное как команду, а при DC=1 — как данные.
Соответственно код отправки команды и данных после нее обычно пишут таким образом
// процедура отправки, правильный вариант который все равно не будет работать
void SPISend(uint16_t data) {
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); // ждём, освобождения SPI_DR
	SPI_I2S_SendData(SPI1, data);  // отправим байт данных в очередь на отправку
}

// сама процедура отправки команды и потом данных
void SPISendCommand(uint16_t data, data1, data2) {
        SetCS0; // выбор дисплея для операции

[1]        SetDC0;  // установили режим передачи команд

[2]	SPISend(commandCode);  //  отправим команду

[3]        SetDC1; // установили режим передачи данных
	SPISend(data);  //  отправим данные
	SPISend(data1);  //  отправим данные
	SPISend(data2);  //  отправим данные

[4]        SetCS1; // отмена выбора дисплея
}

Примечание к коду:
SetCSx — выбирает/отменяет выбор дисплея (здесь не привожу)
SetDCx — установка режима передачи команд/данных для дисплея

И этот код не работает !!!

Почему?

В точке [1] мы указываем дисплею что собираемся передавать команды, затем передаем код команды [2], но согласно нашей процедуры отправки и схем работы SPI которые я приводил выше — мы вернемся из подпрограммы отправки байта данных к шагу [3] к моменту отправки всего 2-3 бита команды (!!) — причем чем медленнее интерфейс SPI (ниже частота SCK) — тем меньше бит мы успеем передать!
И в этот момент мы указываем дисплею, что дальше идут данные [3] — у ЛЮБОГО дисплея смена состояния пина DC во время передачи команды/данных вызывает сбой!!!
Потом отправляем три байта данных (хотя команда уже не прошла), и в итоге отменяя выбор дисплея [4] мы окончательно «сносим голову» дисплею!!! ведь у нас согласно схемы работы SPI при отправке нескольких байт — скорее всего при выполнении команды SetCS1 будет передаваться только data1 (он будет в регистре сдвига), а data2 будет еще ждать своей очереди в SPI_DR

Как большинство выходит из этой ситуации? — используют процедуру отправки с проверкой флага BSY после записи в SPI_DR (второй вариант решений который я приводил выше)… или вообще используют искусственные задержки!!! (например, командами delay !)
Вот реальный пример
void LCD_set_XY(unsigned char X, unsigned char Y) {
   unsigned char x;
   x = 6 * X;

   DCOff();
   SPISend(0x80 | x);
   SPISend(0x40 | Y);
   Delay(1); // Задержка, чтобы успела примениться последняя команда
   DCOn(); // Принуждает выполнить последнюю команду
}



Другая крайность, это использование флага BSY везде! код работать будет, но вот о максимальной скорости передачи данных придется забыть, потери составят около 10-20% практически независимо от частоты SCK (!!), так как код будет постоянно ожидать установку BSY=0 и только потом будет готовиться к следующей передаче (готовить следующий байт), и если это приемлемо и правильно при отправке команды (как правило один байт), то при отправке например буфера экрана, например для PCD8544 (Nokia 5110) — будет работать заметно медленнее!!!
// отправка данных\команд на дисплей
void lcd8544_senddata(unsigned char data) {
	SPI_I2S_SendData(SPI2, data);
        while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) == SET) //Передатчик занят?
}

// очистка дисплея
void lcd8544_refresh(void) {

	LCD_DC0;                         // режим передачи команд

	lcd8544_senddata(0x40); // установка курсора в позицию Y=0; X=0
	lcd8544_senddata(0x80);

	LCD_DC1;                         // режим передачи данных

	unsigned char y, x;

	for (y=0;y<6;y++) for (x=0;x<84;x++) lcd8544_senddata(lcd8544_buff[y*84+x]); // отправка буфера
}


хотя, как вы уже наверное догадались — выход лежит на поверхности — при необходимости смены вида передаваемых данных (по линии DC), или отмены выбора дисплея (линией CS) предварительно нужно проверять флаг BSY для того чтобы убедиться что физическая передача данных/команды завершилась. В остальных случаях нужно использовать проверку флага TXE ПЕРЕД загрузкой значения в SPI_DR:
// передача данных на дисплей
void SPI2_SendByte(uint8_t sendData)
{
  while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET);
  SPI_I2S_SendData(SPI2, sendData);
}

// определение области для вывода
void ili9341c_SetWindow(uint16_t ystart, uint16_t xstart, uint16_t yend, uint16_t xend)
{
. . .
  GPIO_ResetBits(GPIOB, GPIO_Pin_12);   // DC=0;

  SPI2_SendByte(LCD_PAGE_ADDR);  

  while (SPI_I2S_GetFlagStatus(SPI2,SPI_FLAG_BSY)!=RESET);  //ждем пока команда уйдет на дисплей (а не перейдет в shift reg)

  GPIO_SetBits(GPIOB, GPIO_Pin_12);     // DC=1;

  SPI2_SendByte(xstart>>8);      // данные 
  SPI2_SendByte(xstart&0xFF);  //      передаются
  SPI2_SendByte(xend>>8);       //                  в непрерывном 
  SPI2_SendByte(xend&0xFF);  //                             режиме без пауз !

  while (SPI_I2S_GetFlagStatus(SPI2,SPI_FLAG_BSY)!=RESET); // ждем пока данные передадутся до конца 

. . .
}


этот код будет работать и максимально быстро и самое главное правильно!!!

некоторые статьи, где реализованы неправильные алгоритмы отправки, либо тема «правильной» отправки так и не раскрыта.
где можно встретить то о чем я здесь рассказывал
не правильные процедуры отправки байта:
от одной крайности
1. geektimes.ru/post/258046
2. blablacode.ru/%D0%BC%D0%B8%D0%BA%D1%80%D0%BE%D0%BA%D0%BE%D0%BD%D1%82%D1%80%D0%BE%D0%BB%D0%BB%D0%B5%D1%80%D1%8B/spi-%D0%B2-stm32-%D1%88%D0%BB%D1%91%D0%BC-%D0%B8%D0%B7-spi1-%D0%B2-spi2.html

до другой habrahabr.ru/post/139384

не уточнен вопрос правильной передачи данных chipspace.ru/stm32-spi

вот здесь начали правильно, а потом опять свалились в BSY easystm32.ru/interfaces/45-spi-interface-part-2

хорошие картинки www.soel.ru/cms/f/?/452409.pdf

барабашку искали тут electronix.ru/forum/index.php?showtopic=107951 в итоге нашли решение, но, по моему, так и не поняли почему именно так :-)

тема почти раскрыта здесь microsin.net/programming/arm/stm32f407-spi.html

есть много других ресурсов, просто устал выбирать с результатов поиска гугла


p.s. Некоторое время назад я сам разбирался с этим интерфейсом, и был, в отличие от многих, удивлен его продуманностью и функциональностью, надеюсь теперь и для Вас интерфейс SPI у STM32 это не черный ящик с непонятно когда используемыми флагами, а четкий, понятный и продуманный автомат для максимально быстрой отправки/получения данных!

если что не так — пишите в комментарии к статье, в личку, или на емайл gorbuкov @ тот_кто_знает_все. ru

Комментарии (44)


  1. DROS
    05.02.2016 22:49
    +1

    Какой знакомый агрегат ILI9341.

    А что касается костыляторства типа ввода задержек через delay(X); — сам еще подумал, когда портировал одну из популярных либ под вышеозначенный дисплей с ардуины на чистый Си под AVR, нафига тут это? Задумался еще. Но выручил товарищ, который на SPI собаку съел без хлеба. А порт писать пришлось из-за отсутствия времени на курение даташита на дисплей… но не про это сейчас.

    Я к чему все это? ИМХО, подобные решения, которые описываются в статье, лезут именно из таких вот либ. Поскольку свое время довольно много перекопал инфы по экрану на контроллере ili9341 — везде натыкался на один и тот же код инициализации, а порой и работы. Зачастую даже с родными комментами на каком-то иероглефическом диалекте. При чем это не зависело не от производителя контроллера, ни от его семейства или разрядности (с небольшими правками кода конечно же). Везде одно и тоже!

    Так что тут нечему удивляться. Проще скопировать и допилить, чем курить маны и даташиты для понимания как правильно. Да и сам порой грешен, что уж тут =(.


    1. VitGo
      05.02.2016 23:02
      +1

      это понятно, у меня у самого примерно такая ситуация была…
      проблемы стали возникать когда я стал менять скорость SPI — вся работа с дисплеями стала рушиться! и вот тогда я понял что работаю с дисплеем не правильно…

      сейчас у меня все модули работы с дисплеями работают как минимум от 1 мгц до максимума… — это как раз самый лучший показатель что все написано верно…


    1. Dima_Sharihin
      06.02.2016 15:07
      +1

      Это еще что. Я видел какой-то феерический код для ili9341, где DMA запускали для передачи 4(!!!) байт.
      Хотя честно признаюсь, писать фреймбуффер на MCU затруднительно, если оперативки на кристалле меньше, чем 150 килобайт(то есть почти всегда). И приходится писать какой-то хитрозакрученный код, который рисует экран по несколько строк. Работает шустро, но писать сложнее(хотя я просто повесил семафор на HAL_SPI_TxCpltCallback)


  1. ValdikSS
    05.02.2016 23:06
    +3

    Держите восклицательный знак, который можно поставить в заголовок — !


    1. VioletGiraffe
      06.02.2016 14:13
      +1

      What Unicode sorcery is this?!


    1. AllexIn
      06.02.2016 17:42
      +1

      Только администрация все равно его уберет.


      1. ValdikSS
        06.02.2016 18:13
        +1

        Почему? У меня есть несколько постов с ним, не убрали.


        1. AllexIn
          06.02.2016 18:15
          +1

          Видимо как часто бывает с модерацией — прокатит/не прокатит.


  1. palmyr
    05.02.2016 23:07
    +2

    Немного позанудствую:
    1) Вы везде пишете STM32, но упоминание конкретной линейки я не нашёл. Это стоит добавить, так как в разных линейках может встречаться (и встречается) разная реализация ip-ядер одних и тех же интерфейсов.
    Всё дальнейшее справедливо для линейки STM32L1**
    2) Вы везде упоминаете про быстродействие. Тогда уже стоит упомянуть, что контроллер SPI может может пережёвывать данные по 16 бит за раз. (смотрите бит SPI_CR1_DFF)
    К тому же вместо бессмысленного ожидания лучше использовать прерывания или DMA.
    PS: недавно как раз подключал дисплей к STM32L151 по SPI. Код писал на прерываниях — всё заработало без проблем с первого раза. По-моему в reference manual всё достаточно хорошо про работу SPI расписано.

    Вот так:


    1. VitGo
      06.02.2016 06:28
      +1

      посмотрел бегло по даташитам
      STM32F1, STM32F2, STM32F4 — имеют одинаковый по функциональности SPI
      в STM32F3 — по все видимости вообще организован FIFO 3x32!!!

      про 16 бит у меня написано, да и не ставилась цель рассказывать про битность… это узкоцелевая статья — про работу флагов интерфейса SPI

      DMA и прерывания вы не сможете удобно и эффективно использовать при передаче «солянки» из данных и команд…

      Что за дисплей подключали?


      1. AlanDrakes
        06.02.2016 11:28
        +1

        DMA и прерывания вы не сможете удобно и эффективно использовать при передаче «солянки» из данных и команд…

        На самом деле — можно. Во всяком случае, в вариантах с заливкой/передачей областей данных. Да, придётся команды передавать минуя DMA, либо посылками по 1-2 байта. Это медленно, согласен. Зато после этого при заполнении области можно заранее подготовить эту область (или указать один нулевой байт) и «выстрелить» DMA.


        1. VitGo
          06.02.2016 11:31
          +1

          я об этом и написал :-)
          одну две команды все равно вручную посылать нужно…
          Опять таки DMA это хорошо, но все таки нужно понимать как происходит работа изнутри…


          1. AlanDrakes
            06.02.2016 11:42
            +1

            Как происходит работа модуля — понятно. В документации черным по-английски это описано.
            Как по мне — так интереснее как раз научиться работать в связке в DMA контроллером. Например, недавно откопал особенность документации DMA в линейке F1, где сказано:

            DMA channel x number of data register (DMA_CNDTRx)

            NDT[15:0]: Number of data to transfer
            Number of data to be transferred (0 up to 65535). This register can only be written when the channel is disabled. Once the channel is enabled, this register is read-only, indicating the remaining bytes to be transmitted. This register decrements after each DMA transfer.

            Так вот, регистр содержит не количество оставшихся БАЙТ, а количество оставшихся транзакций. Я долго пытался понять, почему контроллер случайным образом улетает в HardFault на ровном месте. А затем оказалось, что DMA портил стэк, находящийся за буферами, с которыми производились DMA-операции. Но было весело.


            1. VitGo
              06.02.2016 16:34
              +1

              гм… ну да, DMA оперирует числом транзакций, одновременно в настройках указывается сколько байт (1/2/4) передается за раз…


              1. AlanDrakes
                07.02.2016 06:27

                Логично, да только в описании — байты. =]
                Вот и были весёлые старты.


                1. dsd_corp
                  09.02.2016 11:57

                  Ну вы в ST, надеюсь, отписались? ) Чтоб в следующей версии даташита исправили…


      1. palmyr
        06.02.2016 17:36
        +1

        Что за дисплей подключали?

        LS013B7DH06 — трансфлективный, цветной, низкопотребляющий. Похож на тот, что в новых часах pebble time стоит, только от sharp-а.
        про 16 бит у меня написано, да и не ставилась цель рассказывать про битность

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

        Не вижу никаких проблем. Сначала пишем команду с данными в буфер, потом пересылаем буфер с помощью DMA/прерываний. Собственно, я так и сделал.


        1. VitGo
          06.02.2016 17:44
          +1

          если ваш дисплей нуждается в указании вида данных (команда/данные) по линии DC — то все равно перед отправкой по DMA данных вам нужно будет ждать флага BSY=1 (после отправки команды), и только потом устанавливать DC=1


          1. palmyr
            07.02.2016 16:15

            Линии DC нет — всё управление по SPI. Сначала всегда передаётся команда, потом, если надо, данные.


            1. VitGo
              07.02.2016 17:06

              ООО!!! тогда у вас классный дисплей!!! работать с ним одно удовольствие! :-)
              жаль мелковат…


  1. gleb_l
    06.02.2016 00:15
    +1

    Скажем «Нет!» двойной буферизации в эпоху тотальной ардуинизации :)

    Вообще-то в этом и смысл двойной буферизации — чтобы у МП-системы (в случае SoC это соседи по коммунальному кристаллу) было время на спокойную загрузку следующего байта не за длительность последнего бита, а за целый байт вперед. То есть вменяемому инженеру достаточно было просто прочитать, что в таком-то устройстве ВВ она поддерживается, и у него бы даже рука не поднялась переключать что-то там по ^BUSY — это как на автомобиле по сигналу о нейтральной передаче включить задний ход :)

    Тот же факт, что в инете полно объездов на кривой козе воркэраундов с задержками, означает, что обезъяна с гранатой — явление массовое, а ведь STM32 по насыщенности периферией — что машинный зал EC ЭВМ


    1. khim
      06.02.2016 00:46
      +1

      Обезьна с гранатой, это, к сожалению, не просто массовое явление — это являение повсеместное. Вплоть до того, что сам смотришь на код, написанный тобой же лет так пять назад, когды ты только осваивал какую-нибудь очередную железяку, и тихо сползаешь на пол офигивая от одного вопроса: «как, как это чудо работает? почему? оно не может работать! не должно!»…

      Вопрос в том — как остановить расползание подобных сумасшедших поделий… статьи подобные обсуждаемой — это хорошо, но нужно что-то делать более системное… вопрос только «что»…


      1. VitGo
        06.02.2016 06:31
        +4

        вот и решил на хабре написать…
        все таки ресурс достаточно хорошо индексируется поисковиками, может быть кто то набредет до статьи раньше чем напишет не правильный код…
        тем более что неправильно написанный код еще и нервов портит — потому что вроде работает, но что нить изменишь и уже не работает!..


        1. gleb_l
          06.02.2016 13:42
          +2

          И правильно — хабр, как явление — сам по себе хороший префильтр, если бы еще не форк на geektimes, болезненный для эмбед систем, как разлом печатной платы варварами — то было бы совсем хорошо. А так приходится материал собирать по кускам — тут STM32, там STM32 :) — только среди «желтых страниц» про планы Илона Маска уже (

          А если серьезно — организаторам всего хабраинкубатора хорошо бы задуматься о том, что непродуманный раскол — как непродуманная политика государства — думающий народ погорюет-погорюет — и потихоньку потянется на другие ресурсы — и никакой риторикой их обратно уже не заманишь. Интересно, есть ли статистика просмотров/комментариев/голосования сходных по тематике материалов до раскола и после?


          1. VitGo
            06.02.2016 16:38
            +1

            меня больше напрягает на хабре невозможность писать комментарии спустя какое то время если изначально не участвовал в обсуждении… вот это я точно понять не могу… :-(
            да и невозможность комметирования другими пользователями… получается что туториалы на хабре — это бесполезное занятие, так как спросить смогут далеко не все — сделали бы для них отдельную ветку обсуждения тогда что ли…


  1. Anvol
    07.02.2016 13:42
    +2

    А почему не использовать готовые библиотеки (HAL, а именно — HAL_SPI_Transmit) от производителя? Там, полагаю, такие моменты учтены. Или просто заглянуть внутрь реализации и выдрать оттуда нужный кусок с проверками флагов состояний.


    1. VitGo
      07.02.2016 14:02

      интересный у вас способ программировать :-)
      а почему бы не разобраться как делать правильно? вы уверены что библиотека написана правильно? — я например, после того как сделал минимальную выборку по интернету — уже не уверен!!!
      почитайте хабр — статьи об обнаруженных ошибках в стандартных библиотеках возникают постоянно… разница между мною и вами — в том что я смогу понять как должно быть написано, а вы нет…

      и если поступать так как вы предлагаете то будут получаться системы которые работают и глючат одновременно… :-)


      1. Anvol
        07.02.2016 14:07
        +2

        вы обо мне ничего не знаете, пожалуйста, придержите ваше мнение обо мне при себе. У меня простой вопрос, а о способе программировать я ни слова не писал.

        Библиотеки создаются для того, чтобы многократно переиспользовать их, в том числе и другими программистами. То, что я использую HAL совсем не означает, что ни разу не имплементил передачу вручную. Более того, делал и bit banging и плисы задел немного.

        Меня привлекают общедоступные популярные библиотеки от производителя, которые обновляются регулярно. Они экономят время. При этом, я не отбрасываю теорию и low-level работу, когда это необходимо.


        1. VitGo
          07.02.2016 16:47
          -2

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

          фантастикой не увлекаетесь? у Айзека Азимова был помоему рассказ про профориентацию людей в технократическом обществе будущего… там все люди селекционировались по умениям, потом им давали пленки чтобы освоить то или иное оборудование, потом на основе полученного образования их направляли на те или иные планеты где это оборудование использовалось…

          но были люди которые не подпадали ни под одну из программ..- потому что они обладали интеллектом чтобы создавать эти самые пленки для обучения других…

          Мы с вами просто в разных группах :-) вы любите использовать готовое, я люблю создавать с нуля…

          Меня привлекают общедоступные популярные библиотеки от производителя, которые обновляются регулярно. Они экономят время. При этом, я не отбрасываю теорию и low-level работу, когда это необходимо.

          по тому что вы написали делается абсолютно противоположный вывод…

          за минус спасибо!


          1. Anvol
            07.02.2016 18:37
            +1

            минус — исключительно за переход на личности, который присутствует даже в этом вашем комментарии. Так делать не стоит.


      1. Anvol
        07.02.2016 14:31

        И для справки — SPI_I2S_SendData это функция библиотеки StdPeriph, предшественника HAL. Считается legacy и не рекомендуется к использованию в новых проектах.


        1. VitGo
          07.02.2016 16:48

          я знаю… но я вообще маргинал — люблю ассемблер, а в нем — нет правил!


          1. Anvol
            07.02.2016 18:32

            я знаю… но по материалам данной статьи — вы рассказали как правильно проверять состояние флагов используя язык С + CMSIS + StdPeriph. Ровно так, как это сделано в новых версиях той же библиотеки.


            1. VitGo
              07.02.2016 19:33

              причем тут библиотеки ?

              если вы не будете использовать готовый драйвер дисплея — то вы наступите на эти грабли даже с библиотекой StdPariph и HAL !!

              Я же написал: Весь дьявол в линии DC… вы можете абсолютно правильно послать байт (слово) в SPI_DR, но не проверив перед сменой DC флаг BSY — вы порушите обмен с дисплеем… — вот о чем статья!!!

              и ни одна библиотека Вам это не подскажет сделать!!! — потому что это аппаратные особенности конкретных дисплеев…
              причем выше уже привели ссылку на дисплей у которого нет линии DC и все команды отправляются в перемешку с данными просто по SPI… — там проблемы не будет, и достаточно будет использовать HAL или StdPeriph чтобы все работало быстро…
              но дисплеи на PCD8544, ILI9341, ST7735 — вы не запустите без правильного управления линией DC!!!

              p.s. не считайте опять что я перехожу на личности — вы с какими микроконтроллерами работали и какие дисплеи подключали?


              1. Anvol
                07.02.2016 19:55
                +2

                HAL инкапсулирует логику проверки готовности периферии к приему/передачи данных. Вам нет необходимости проверять никакие флаги. Просто представьте, что ваш цикл проверки флага находится внутри *_SendData.
                работаю с двумя сериями f1 & f4, чаще всего — STM32F103RB и STM32F429ZI. Дисплеи были — st7735, ili9341, ili9806e. Сейчас пробую работу TFT-LCD + FSMC. Это дает бОльшую гибкость по выбору панелей.

                Для асинхронной работы в большинстве случаев использую отсылку/прием через прерывания (функции *_IT) или DMA, где это резонно


                1. VitGo
                  08.02.2016 06:50
                  +1

                  ОО! а с ILI9806E работали по SPI или параллельным?
                  я посмотрел по даташиту SPI должен быть 9-ти битным (первый бит DC)…

                  у меня как то был чернобелый дисплей с 9-ти битным интерфейсом управления — кроме как ногодрыгом с ним работать и не получилось :-(


              1. Anvol
                07.02.2016 20:23

                Вот собственно реализация функции. Столь высокая сложность обсусловлена соблюдением MISRA C 2004 и совместимости с RTOS. Но вы без труда найдете проверку флагов. Функция передачи с использованием прерываний немного проще, но все так же обвешена проверками для соблюдения MISRA.

                HAL_SPI_TransmitReceive
                /**
                  * @brief  Transmit and Receive an amount of data in blocking mode.
                  * @param  hspi: pointer to a SPI_HandleTypeDef structure that contains
                  *               the configuration information for SPI module.
                  * @param  pTxData: pointer to transmission data buffer
                  * @param  pRxData: pointer to reception data buffer
                  * @param  Size: amount of data to be sent and received
                  * @param  Timeout: Timeout duration
                  * @retval HAL status
                  */
                HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout)
                {
                #ifdef USE_SPI_CRC
                  __IO uint16_t tmpreg = 0U;
                #endif
                  uint32_t tickstart = 0U;
                  HAL_StatusTypeDef errorcode = HAL_OK;
                
                  /* Check Direction parameter */
                  assert_param(IS_SPI_DIRECTION_2LINES(hspi->Init.Direction));
                
                  /* Process Locked */
                  __HAL_LOCK(hspi);
                
                  /* Init tickstart for timeout managment*/
                  tickstart = HAL_GetTick();
                
                  if(!((hspi->State == HAL_SPI_STATE_READY) ||     ((hspi->Init.Mode == SPI_MODE_MASTER) && (hspi->Init.Direction == SPI_DIRECTION_2LINES) && (hspi->State == HAL_SPI_STATE_BUSY_RX))))
                  {
                    errorcode = HAL_BUSY;
                    goto error;
                  }
                
                  if((pTxData == NULL) || (pRxData == NULL) || (Size == 0U))
                  {
                    errorcode = HAL_ERROR;
                    goto error;
                  }
                
                  /* Don't overwrite in case of HAL_SPI_STATE_BUSY_RX */
                  if(hspi->State == HAL_SPI_STATE_READY)
                  {
                    hspi->State = HAL_SPI_STATE_BUSY_TX_RX;
                  }
                
                  /* Set the transaction information */
                  hspi->ErrorCode   = HAL_SPI_ERROR_NONE;
                  hspi->pRxBuffPtr  = (uint8_t *)pRxData;
                  hspi->RxXferCount = Size;
                  hspi->RxXferSize  = Size;
                  hspi->pTxBuffPtr  = (uint8_t *)pTxData;
                  hspi->TxXferCount = Size;
                  hspi->TxXferSize  = Size;
                
                  /*Init field not used in handle to zero */
                  hspi->RxISR       = NULL;
                  hspi->TxISR       = NULL;
                
                #ifdef USE_SPI_CRC
                  /* Reset CRC Calculation */
                  if(hspi->Init.CRCCalculation == SPI_CRCCALCULATION_ENABLE)
                  {
                    SPI_RESET_CRC(hspi);
                  }
                #endif
                
                  /* Check if the SPI is already enabled */
                  if((hspi->Instance->CR1 &SPI_CR1_SPE) != SPI_CR1_SPE)
                  {
                    /* Enable SPI peripheral */
                    __HAL_SPI_ENABLE(hspi);
                  }
                
                  /* Transmit and Receive data in 16 Bit mode */
                  if(hspi->Init.DataSize == SPI_DATASIZE_16BIT)
                  {
                    while ((hspi->TxXferCount > 0U) || (hspi->RxXferCount > 0U))
                    {
                      /* Check TXE flag */
                      if((hspi->TxXferCount > 0U) && (__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_TXE)))
                      {
                        hspi->Instance->DR = *((uint16_t *)pTxData);
                        pTxData += sizeof(uint16_t);
                        hspi->TxXferCount--;
                
                #ifdef USE_SPI_CRC
                        /* Enable CRC Transmission */
                        if((hspi->TxXferCount == 0U) && (hspi->Init.CRCCalculation == SPI_CRCCALCULATION_ENABLE))
                        {
                          SET_BIT(hspi->Instance->CR1, SPI_CR1_CRCNEXT);
                        }
                #endif
                      }
                
                      /* Check RXNE flag */
                      if((hspi->RxXferCount > 0U) && (__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_RXNE)))
                      {
                        *((uint16_t *)pRxData) = hspi->Instance->DR;
                        pRxData += sizeof(uint16_t);
                        hspi->RxXferCount--;
                      }
                      if((Timeout != HAL_MAX_DELAY) && ((HAL_GetTick()-tickstart) >=  Timeout))
                      {
                        errorcode = HAL_TIMEOUT;
                        goto error;
                      }
                    }
                  }
                  /* Transmit and Receive data in 8 Bit mode */
                  else
                  {
                    while((hspi->TxXferCount > 0U) || (hspi->RxXferCount > 0U))
                    {
                      /* check TXE flag */
                      if((hspi->TxXferCount > 0U) && (__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_TXE)))
                      {
                        *(__IO uint8_t *)&hspi->Instance->DR = (*pTxData++);
                        hspi->TxXferCount--;
                
                #ifdef USE_SPI_CRC
                        /* Enable CRC Transmission */
                        if((hspi->TxXferCount == 0U) && (hspi->Init.CRCCalculation == SPI_CRCCALCULATION_ENABLE))
                        {
                          SET_BIT(hspi->Instance->CR1, SPI_CR1_CRCNEXT);
                        }
                #endif
                      }
                
                      /* Wait until RXNE flag is reset */
                      if((hspi->RxXferCount > 0U) && (__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_RXNE)))
                      {
                        (*(uint8_t *)pRxData++) = hspi->Instance->DR;
                        hspi->RxXferCount--;
                      }
                      if((Timeout != HAL_MAX_DELAY) && ((HAL_GetTick()-tickstart) >=  Timeout))
                      {
                        errorcode = HAL_TIMEOUT;
                        goto error;
                      }
                    }
                  }
                
                #ifdef USE_SPI_CRC
                  /* Read CRC from DR to close CRC calculation process */
                  if(hspi->Init.CRCCalculation == SPI_CRCCALCULATION_ENABLE)
                  {
                    /* Wait until TXE flag */
                    if(SPI_WaitFlagStateUntilTimeout(hspi, SPI_FLAG_RXNE, SET, Timeout, tickstart) != HAL_OK)
                    {
                      /* Error on the CRC reception */
                      SET_BIT(hspi->ErrorCode, HAL_SPI_ERROR_CRC);
                      errorcode = HAL_TIMEOUT;
                      goto error;
                    }
                    /* Read CRC */
                    tmpreg = hspi->Instance->DR;
                    /* To avoid GCC warning */
                    UNUSED(tmpreg);
                  }
                
                  /* Check if CRC error occurred */
                  if(__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_CRCERR))
                  {
                    SET_BIT(hspi->ErrorCode, HAL_SPI_ERROR_CRC);
                    /* Clear CRC Flag */
                    __HAL_SPI_CLEAR_CRCERRFLAG(hspi);
                
                    errorcode = HAL_ERROR;
                  }
                #endif
                
                  /* Check the end of the transaction */
                  if(SPI_CheckFlag_BSY(hspi, Timeout, tickstart) != HAL_OK)
                  {
                    SET_BIT(hspi->ErrorCode, HAL_SPI_ERROR_FLAG);
                    errorcode = HAL_ERROR;
                  }
                
                error :
                  hspi->State = HAL_SPI_STATE_READY;
                  __HAL_UNLOCK(hspi);
                  return errorcode;
                }
                


                1. kotomyava
                  08.02.2016 11:26

                  Но у вас же тут нет ничего, связанного с внешним сигналом RS(DC), и HAL за вас не сделает с ним ничего, являясь обёрткой для SPI, который, в данном случае, логику работы с дисплеем полностью не реализует — не умеет он по волшебству дёргать внешним пином. Вам, всё равно, в реализации вашего драйвера дисплея надо дождаться где-то конца передачи, и выставить правильно DC, если того требует работа с конкретным дисплеем.
                  А проверять флаги, или получать или состояние spi через HAL, или вообще без обёртки всё это реализовать на ассемблере — какая разница? Это вопрос вкуса. Смысл статьи в том, что надо правильно ловить окончание передачи данных прежде чем изменять состояние пина DC, не более того. И с HAL, в данном конкретном случае, только чуть сложнее выстрелить себе в ногу. Хотя у него есть свои заморочки — почитайте, например в сообществе на easyelectronics, там баги неприятные находили в HAL.


                  1. VitGo
                    08.02.2016 11:32
                    +1

                    ИМХО, вы не правы,
                    если я правильно понял — в конце процедуры есть проверка BSY

                    /* Check the end of the transaction */
                      if(SPI_CheckFlag_BSY(hspi, Timeout, tickstart) != HAL_OK)
                      {
                        SET_BIT(hspi->ErrorCode, HAL_SPI_ERROR_FLAG);
                        errorcode = HAL_ERROR;
                      }
                    

                    Поэтому после вызова этой процедуры можно смело давать команду на смену состояния DC…
                    Сама процедура тоже хороша тем что готова бросать буфер — то есть ее можно очень даже удобно использовать с дисплеями: CS=0, DC=0, кинул команду, DC=1, кинул данные (причем несколько данных), CS=1
                    Очень хорошая процедура!!!
                    Конечно могут быть случаи когда она будет работать медленнее специально оптимизированной — но для общего применения очень даже хорошая процедура…


                    1. Anvol
                      08.02.2016 12:43

                      спасибо. я рад, что смог донести мысль. Именно реализации на HAL позволяют использовать их в многозадачных решениях и позволяют добиться отказоустойчивости за счет таймаутов в блокируемых вызовах.


                    1. kotomyava
                      08.02.2016 15:11
                      +1

                      > Конечно могут быть случаи когда она будет работать медленнее специально оптимизированной

                      Ну так это не полная реализация для рассматриваемого в статье дисплея, о чём я и сказал. Если она используется только для передачи коротких команд, и обёрнута в clear DC — set DC, а для данных используется не она а передача буфера через DMA, например, то всё в принципе, ок. Я нигде не говорил, что она не правильна.

                      Но опять же, при чём тут достоинства HAL? Просто сделано правильно и проверяется, в итоге, HALом нужный флаг, а не через StdPeriph или напрямую. Собственно я об этом и писал — «И с HAL, в данном конкретном случае, только чуть сложнее выстрелить себе в ногу». Ничем реализация блокирующей записи с использованием StdPeriph или с прямой работой с регистрами отличаться не будет — надо будет только чуть больше кода написать, и возможно при этом ошибиться. Зато будет чёткое понимание как оно работает, подходит-ли полностью для конкретного случая, можно сделать не блокирующий вызов, и что-то полезное сделать пока данные отсылаются, и.т.п.

                      Мысль иными словами такая — HAL отнюдь не какая-то «серебряная пуля» или кнопка «сделать хорошо», а лишь один из вариантов абстракции, который можно выбрать, а можно и не выбрать, в зависимости от проекта.


                      1. VitGo
                        08.02.2016 15:15

                        в общем мысль: все равно желательно знать как работает изнутри, тогда все равно что использовать (HAL, StdP, CMSIS) :-)
                        поставлю плюсик :-)


  1. GarryC
    08.02.2016 13:13
    +2

    Вы несколько сумбурно осветили вопрос о порядке проверки готовности передатчика, попробую уточнить. Если у Вас идет передача готовых пакетов и есть какая-либо буферизация, то неважно, когда проверять флаг готовности передачи — в конце (так чаще делают) либо в начале (а вот так делают реже, хотя так правильнее).
    Эта правильность проявит себя, когда Вы тратите значительное (сравнимое со всеменем передачи пакета) время на подготовку очередного пакета и у Вас нет буферизации. Вот тогда действительно получается разница — в первом варианте вы положили пакет в передатчик, потом ждете, пока передача завершится, потом готовите следующий пакет, при этом можно было бы уже передавать, но пока нечего и передатчик простаивает.
    Во втором варианте вы положили пакет в передатчик, и идете готовить следующий, далее, если надо, ждете, когда передача завершится и повторяете процедуру. При этом Вы выигрываете за счет того, что подготовка очередного пакета и передача текущего осуществляются одовременно.
    Но в любом случае называть первый способ неправильный — это неправильно (игра слов, однако), он всего лишь неоптимальный, хотя для меня является критерием профессионализма.

    А вот со второй частью дела обстоят намного хуже, причем не только для SPI, а и для других интерфейсов со значительным временем передачи (как правило, последовательных).
    Что касается различия битов «готовности передачи» и «завершения передачи», то описаны они в документации (как правило) весьма понятно, применение одного вместо другого просто недопустимо (хотя встречается повсеместно) и может приводить (и часто приводит) к появлению труднообнаружимых (причем непостоянных) ошибок. Такая практика не имеет ни малейшего оправдания и объясняется низкой инженерной культурой определенного количества разработчиков.


    1. VitGo
      08.02.2016 14:38

      спасибо! хорошее дополнение…

      кстати, в документации флаги BSY и TXE описаны, но я не нашел места где было бы написано что TXE — это к SPI_DR, а BSY к Shift_reg… как то везде все в общем описано: что мол TXE это флаг буфера, а BSY конца передачи (причем BSY по разному работает в зависимости от режима Master/Slave и это вносит еще большую неразбериху в документацию)

      так же как и на диаграммах (например № 255 в Reference manual) указаны моменты общей генерации флагов, но вот передача данных из SPI_DR и Shift_reg — не указаны… в итоге опять не складывается все в одну картинку, и не понятно какой же флаг в какой момент использовать… поэтому я и нарисовал свои диаграммы где отдельной строкой отобразил SPI_CR и отдельной строкой Shift_Reg — тогда сразу становиться понятной логика срабатывания флагов…