Top.Mail.Ru

Часть 17. Зародыш драйвера SPI для STM32 на C++.

Что-то мне так сильно повезло, что у меня попёрли дисплеи на SPI, и поэтому пришлось написать библиотеку для работы с этим интерфейсом. Так как очень многие дисплеи могут работать только ведомыми (slave) и могут только принимать данные, написал только часть касающуюся передачи. Постарался сделать все типы передачи данных и часть функций чтения. Хоть передачу и проверял во всех режимах, в 146% работоспособности не уверен.

В процессе работы над драйвером дисплея обнаружил глупейшую ошибку. И, самое странное, STM32F407xx эту ошибку игнорировал и работал правильно, а STM32L052K8x на этой ошибке споткнулся. Сейчас поправил, но перепроверять все режимы заново не стал. Так что, если что-то пойдёт не так, пишите, буду смотреть.

Описывать протокол приёма/передачи особого смыла нет, в интернете очень много статей на этот счёт. Просто в паре режимов, при передаче/приёме по одному проводу, есть некоторые нюансики.

Основные режимы SPI:

  • FullDuplexMaster - полный дуплекс, ведущий (master), может принимать, передавать или одновременно и то и другое
  • FullDuplexSlave - то же самое, только ведомый (slave)
  • HalfDuplexMaster - полудуплекс, ведущий, может передавать/принимать по очереди - по одному проводу
  • HalfDuplexSlave - то же самое, только ведомый
  • ReceiveOnlyMaster - только приём в режиме ведущего
  • ReceiveOnlySlave - только приём в режиме ведомого
  • TransmitOnlyMaster - только передача в режиме ведущего
  • TransmitOnlySlave - только передача в режиме ведомого

Первые два режима используют три линии:

  • MISO - Master Input, Slave Output
  • MOSI - Master Output, Slave Input
  • SCK - Тактовый сигнал (это обозначение по даташиту, иногда обозначают CLK или как-нибудь ещё)

"Хардварный" NSS я никогда не использую. Дёргаю его ручками, потому что у разных дисплеев он должен работать по-разному. У некоторых его нужно "дёрнуть вверх" после команды, у других, чтобы выбрать дисплей, достаточно его "опустить". И с ним невозможно подключать несколько дисплеев.

Так же SPI имеет 4 вида тактирования и много ещё каких плюшек. Но их я делаю по умолчанию, так как устройств с хитрыми режимами очень мало и в руки мне они не попадали, но предусмотреть всё это можно.

SPI на этом МК умеет работать только с 8-ми и 16-ти битными данными.

Инициализация.

Для инициализации нужно несколько шагов:

  1. Подать тактирование на SPI.
  2. Проинициализировать выводы, с которыми будет работать SPI.
  3. Проинициализировать SPI.
  4. Разрешить ему работать.

Чтобы всё это сделать, нужно сначала в конструкторе проинициализировать все необходимые переменные:

SPI::SPI(SPI_TypeDef* Port, bool Alternate)
{
  _SPI_Port     = Port;                                               // Порт, который необходимо инициализировать
  _Alternate    = Alternate;                                          // Альтернативные выводы или нет
  _SpiDiv       = SPI_Div_256;                                        // Коэффициент деления частоты тактового генератора, по умолчанию 256
  _DataSize     = SPI_DataSize_8;                                     // Количество передаваемых бит, по умолчанию 8
  _SpiFaze      = SPI_Faze_0;                                         // Фаза сигнала SCK, по умолчанию 0

  if(Port == SPI1)      _IRQn = SPI1_IRQn;                            // Инициализация прерывания в зависимости от порта, здесь пока не используется
  else if(Port == SPI2) _IRQn = SPI2_IRQn;
  else if(Port == SPI3) _IRQn = SPI3_IRQn;
}

Port - какой порт используем - SPI1, SPI2, SPI3. Alternate - выводы, используемые для связи false - стандартные, true - альтернативные.

Я раньше не говорил про альтернативные выводы. Всё дело в том, что я назначаю их парами, которые расположены рядом. На самом же деле можно назначать их как угодно, проблема только в том, что код разрастётся и в инициализации можно будет заблудиться. Например у МК STM32L052xx на SPI1, MOSI можно назначить на PA7, PA12 и PB5. MISO можно назначить на PA6, PA11 и PB4. SCK на PA5 и PB3. Причём не обязательно парами. Но код и так большой, не будем громоздить. Нужно просто внимательнее распределять выводы и стараться не допускать пересечение устройств на пинах. На крайний случай добавить или переписать код.

Шаг 1. Тактирование. Делается одной строкой в функции init(uint8_t Div, bool DataSize = SPI_DataSize_8), которой передаётся коэффициент деления и количество передаваемых бит. По умолчанию включается Full Duplex Master. Если нужно, можно использовать функцию init(uint8_t Div, bool DataSize, uint8_t TransMode), где дополнительно указывается способ передачи.

RCC->APB2ENR |= RCC_APB2ENR_SPI1EN; // Запускаем тактовый генератор для порта SPI1

Шаг 2. Инициализация выводов для сигналов SPI. Здесь посложнее - с помощью условий выбираются порты для каждого из SPI отдельно. Для SPI1 будет выглядеть таким образом:

_GPIO_Set = 0;
if(_SPI_Port == SPI1) // --------------------- SPI1 -------------------------------
  {
    RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;                                                   
    if(!_Alternate)
    {
      if((_TransMode == SPI_FullDuplexMaster)      || (_TransMode == SPI_FullDuplexSlave)) _GPIO_Set = GPIO_PIN_6 | GPIO_PIN_7;
      else if((_TransMode == SPI_HalfDuplexMaster) || (_TransMode == SPI_ReceiveOnlySlave)  
                                                   || (_TransMode == SPI_TransmitOnlyMaster)) _GPIO_Set = GPIO_PIN_7;
      else if((_TransMode == SPI_HalfDuplexSlave)  || (_TransMode == SPI_ReceiveOnlyMaster) 
                                                   || (_TransMode == SPI_TransmitOnlySlave))  _GPIO_Set = GPIO_PIN_6;
      _GPIO_Clk = GPIO_PIN_5;
      _SetPinAlternate(GPIOA, _GPIO_Clk | _GPIO_Set,   GPIO_AF5_SPI1);      
    }
    else
    {
      if((_TransMode == SPI_FullDuplexMaster)      || (_TransMode == SPI_FullDuplexSlave)) _GPIO_Set = GPIO_PIN_4 | GPIO_PIN_5;
      else if((_TransMode == SPI_HalfDuplexMaster) || (_TransMode == SPI_ReceiveOnlySlave)  
                                                   || (_TransMode == SPI_TransmitOnlyMaster)) _GPIO_Set = GPIO_PIN_5;
      else if((_TransMode == SPI_HalfDuplexSlave)  || (_TransMode == SPI_ReceiveOnlyMaster) 
                                                   || (_TransMode == SPI_TransmitOnlySlave))  _GPIO_Set = GPIO_PIN_4;
      _GPIO_Clk = GPIO_PIN_3;
      _SetPinAlternate(GPIOB, _GPIO_Clk | _GPIO_Set, GPIO_AF5_SPI1);
    }
  }

Как видно из кода, в переменную _GPIO_Set заносятся адреса пинов, которые будут использоваться при инициализации, причём их количество зависит от режима, в котором будет использоваться SPI. И в конце, функцией _SetPinAlternate() производится инициализация необходимых выводов.

Шаг 3. Инициализация SPI. В следующем куске кода в зависимости от режима SPI в переменную CMD_CR1 набиваются адреса битов управления, необходимых для каждого конкретного режима:

if((_TransMode == SPI_FullDuplexMaster)   || (_TransMode == SPI_FullDuplexSlave) ||
    (_TransMode == SPI_TransmitOnlyMaster) || (_TransMode == SPI_TransmitOnlySlave))
 {
   if(_TransMode & MASTER_MODE) CMD_CR1 = SPI_CR1_MSTR | SPI_CR1_SSI; else CMD_CR1 = 0;
 }
 else if((_TransMode == SPI_HalfDuplexMaster) || (_TransMode == SPI_HalfDuplexSlave))
 {
   if(_TransMode & MASTER_MODE) CMD_CR1 = SPI_CR1_MSTR | SPI_CR1_SSI | SPI_CR1_BIDIMODE; else CMD_CR1 = SPI_CR1_BIDIMODE;
 }
 else if((_TransMode == SPI_ReceiveOnlyMaster) || (_TransMode == SPI_ReceiveOnlySlave))
 {
   if(_TransMode & MASTER_MODE) CMD_CR1 = SPI_CR1_MSTR | SPI_CR1_SSI | SPI_CR1_RXONLY; else CMD_CR1 = SPI_CR1_RXONLY;
 }

После того, как команда сформирована, нам остаётся проинициализировать необходимые регистры:

CMD_CR1 |= SPI_CR1_SSM;                                      // Включаем управление CS софтварно. Не люблю хардварное

  // Шаг 3 - Инициализируем SPI
_SPI_Port->CR1  = CMD_CR1;                                   // Инициализируем SPI данными из заранее набитой переменной
_SPI_Port->CR1  |= (_SpiDiv << SPI_CR1_BR_Pos);              // Устанавливаем скорость предачи
_SPI_Port->CR1  |= _SpiFaze;                                 // Фаза тактирования, режим 0 по умолчанию

if(_MSB)      _SPI_Port->CR1  |= SPI_CR1_LSBFIRST;           // Младший бит идёт первым
if(_DataSize) _SPI_Port->CR1  |= SPI_CR1_DFF;                // Устанавливаем количество передаваемых бит
_SPI_Port->CR1 |= SPI_CR1_BIDIOE;                            // Включаем двунаправленную передачу

Шаг 4. Разрешаем работу SPI:

_SPI_Port->CR1  |= SPI_CR1_SPE;                               // Включаем SPI

Далее можно пользоваться.

Передача данных.

Функции передачи у нас есть такие (реализованные и проверенные):

size_t  Transmit(uint16_t Data);                               // Передача байта/слова. Работает во всех режимах
size_t  Transmit(uint8_t *Buff, uint16_t Length);              // Передача 8-ми разрядного буфера. Работает во всех режимах
size_t  Transmit(uint16_t *Buff, uint16_t Length);             // Передача 16-ти разрядного буфера. Работает во всех режимах
size_t  TransmitReceive(uint16_t Data);                        // Реализовано, не проверено. Может работать не во всех режимах
  • Transmit() - передаёт один байт (8 бит) или слово (16 бит). Что будет передаваться, определяется с помощью флага _DataSize.
  • Transmit(uint8_t *Buff, uint16_t Length) - передаёт буфер 8-битных данных.
  • Transmit(uint16_t *Buff, uint16_t Length) - то же, только 16-битных.
  • TransmitReceive(uint16_t Data) - передаёт данные Data с одновременным приёмом. Возвращает или 8-ми, или 16-ти битные данные. Зависит от настройки SPI.

Практически все функции построены почти одинаково, кроме некоторых нюансов. Рассмотрим функцию Transmit():

size_t SPI::Transmit(uint16_t Data)
{
  if((_TransMode == SPI_HalfDuplexMaster) || 
    (_TransMode == SPI_HalfDuplexSlave))                               // Если находимся в полудуплексе, делаем дополнительные телодвижения
  {
    _SPI_Port->CR1 &= ~SPI_CR1_SPE;                                    // Выключаем SPI
    _SPI_Port->CR1 |= SPI_CR1_BIDIOE;                                  // Включаем двунаправленную передачу
  }

  if(!(_SPI_Port->CR1 & SPI_CR1_SPE)) _SPI_Port->CR1 |= SPI_CR1_SPE;   // Если сработала предыдущая ветка, нужно снова включать SPI

  while(!(_SPI_Port->SR & SPI_SR_TXE)){};                              // Ждем, пока не освободится буфер передатчика
  if(_DataSize)                                                        // Проверяем разрядность передатчика
  {
    _SPI_Port->DR = (uint16_t) Data;                                   // Заполняем буфер передатчика 16-ю битами
  }
  else
  {
    *((__IO uint8_t *)&_SPI_Port->DR) = (uint8_t) Data;                // Заполняем буфер передатчика 8-ю битами
  }
  return 0;
}

В этой функции предварительно проверяется режим передачи, если полудуплекс, устанавливается BIDIOE, так как он иногда сбрасывается функцией приёма в некоторых режимах. Так как при проверке этого флага отключается SPI, на всякий пожарный проверяется было ли отключение, если было, включается обратно. Далее проверяется, закончилась ли предыдущая передача, и ожидается завершение в случае, если нет. Когда передача не осуществляется, в регистр DR пишутся наши данные и SPI начинает передачу. Таймаут я не делал. Когда всё настроено правильно, данные всё равно уйдут, независимо от того, принимает их ведомый или нет. Если ведомый не отвечает, функция TransmitReceive(uint16_t Data) вернёт или 0х00, или 0хFF.

Обе функции Transmit(), работающие с буфером, построены так же, как и функция передачи одного байта, просто внутри добавляется цикл, передающий данные из буфера. Функция TransmitReceive() не дописана для всех режимов, работает только с режимом полного дуплекса в режиме ведущего:

size_t SPI::TransmitReceive(uint16_t Data)                             // Передача/приём байта/слова
{
  while(!(_SPI_Port->SR & SPI_SR_TXE)){};                              // Ждем, пока не освободится передатчмк
  _SPI_Port->DR = Data;                                                // Запускаем обмен
  while(!(_SPI_Port->SR & SPI_SR_RXNE)){};                             // Ждем, пока не появится новое значение в буфере приемника
  return _SPI_Port->DR;                                                // Возвращаем значение буфера приемника
}

Для данного режима всё решается просто:

  1. Ждём, когда освободится передатчик.
  2. Записываем туда передаваемые данные.
  3. Ждём поднятия флага приёма.
  4. Возвращаем считанные данные.

Функция работает с 8-ми и 16-ти битными данными. Какие данные будут приняты, зависит от того, на какой режим настроен SPI.

Прием данных.

size_t  Receive(void);                                                 // Приём одного слова/байта, проверено только в FullDuplexMaster
size_t  Receive(uint8_t *Buff, uint16_t Length);                       // Проверено в ведущем полудуплексе
size_t  Receive(uint16_t *Buff, uint16_t Length);                      // Пока не реализовано

Функция Receive(void) принимает 8-ми и 16-ти битные данные. Какие данные будут приняты, зависит от настройки SPI:

size_t SPI::Receive(void)                                              // Приём байта или слова
{
  while(!(_SPI_Port->SR & SPI_SR_TXE)){};                              // Ждем, пока не освободится передатчмк
  _SPI_Port->DR = 0;                                                   // Запускаем обмен
  while(!(_SPI_Port->SR & SPI_SR_RXNE)){};                             // Ждем, пока не появится новое значение в буфере приемника
  return _SPI_Port->DR;                                                // Возвращаем значение буфера приемника
}

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

Может показаться, что это всё звучит довольно глупо, как ведомому сказать, что мы от него хотим. Как он узнает, что и откуда отдать ведущему? На самом деле всё просто - на примере тех же дисплеев. Мы функцией Transmit() посылаем дисплею адрес регистра и говорим, что хотим его считать. Переключаемся на чтение данных, в это время дисплей уже готов отдать данные. Обращаемся к функции Receive() и принимаем данные, которые подготовил нам дисплей, а он, в свою очередь, не обращает внимание на то, что ему в данный момент идёт по входу.

Функция Receive(uint8_t *Buff, uint16_t Length) принимает 8-ми битные данные в буфер указанного размера:

size_t SPI::Receive(uint8_t *Buff, uint16_t Length)                    // Приём байтов в буфер
{
  uint16_t _Lenght = Length;
  uint8_t *RxBuffPtr;
  RxBuffPtr = (uint8_t*) Buff;

  if(_TransMode == SPI_HalfDuplexMaster)                               // Если находимся в полудуплексе, делаем дополнительные телодвижения
  {
    _SPI_Port->CR1 &= ~SPI_CR1_SPE;                                    // Выключаем SPI
    _SPI_Port->CR1 &= ~SPI_CR1_BIDIOE;                                 // Выключаем двунаправленную передачу
  }
  if(!(_SPI_Port->CR1 & SPI_CR1_SPE)) _SPI_Port->CR1 |= SPI_CR1_SPE;   // Если сработала предыдущая ветка, нужно снова включать SPI

  while (_Lenght > 0U)                                                 // Мутим цикл приёма буфера
  {
    while (!(_SPI_Port->SR & SPI_SR_RXNE)){};                          // Ждем, пока не примется байт
    (*RxBuffPtr) = *((__IO uint8_t*) &_SPI_Port->DR);                  // Пишем принятый байт в буфер
    RxBuffPtr += sizeof(uint8_t);                                      // Сдвигаем указатель
    _Lenght--;                                                         // Декрементируем счётчик
  }
  if(_TransMode == SPI_HalfDuplexMaster)
  {
    _SPI_Port->CR1 &= ~SPI_CR1_SPE;                                    // Выключаем SPI
    _SPI_Port->CR1 |= SPI_CR1_BIDIOE;                                  // Включаем двунаправленную передачу
  }
  return 0;
}

Функция написана и проверена только на полудуплексе ведущего, для других режимов не проверялась. Решение для полудуплекса здесь довольно прикольное. В начале производится сброс бита BIDIOE. Это, после включения SPI следующей командой, вызывает появление тактовых импульсов на SCK, что говорит ведомому, что можно отдавать данные. Таким образом, производится приём заданного количества байт данных от ведомого. После приёма данных, бит BIDIOE вновь устанавливается, что вызывает прекращение подачи тактовых импульсов.

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

Все, кто следит за моими статьями, могли заметить, что я практически не пользуюсь прерываниями. Всё это из-за того, что я почти не пишу систем реального времени. В системах с реальным временем я пишу всё немного по-другому. Часть задач, не влияющих на работу в реальном времени, я пишу на библиотеках которые описываю в этих статьях, а критичные по времени куски кода работают сами по себе и пишутся на голом CMSIS, прерывая работу этих библиотек, по таймеру или другим способом.

В работе с дисплеями прерывания применять довольно сложно, там чаще всего нужно дёргать очень много ног, чтобы передать данные или команду. Да и что ни дисплей, то свои уникальные  нюансы.

Например для полного управления дисплеем с параллельным интерфейсом нужны следующие сигналы:

  1. Шина данных
  2. RST
  3. CS
  4. DC
  5. RW
  6. WR

Шина данных может быть любой - 8 бит, 16 бит, SPI, I2C, и так далее.

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

Есть одна ниша, где может использоваться круть, которая есть у МК этого производителя. Это DMA, вот его здесь можно применить, но применить ограниченно, для вывода картинок или спрайтов. Суть такова - практически все дисплеи имеют команду задания окна вывода. Поэтому мы можем задать окно вывода размером с нашу картинку, передав дисплею соответствующие команды, а потом с помощью DMA вывести данные изображения в это окно. Других применений для прерываний или DMA при работе с дисплеями найти довольно трудно.

Мне в руки попало два дисплея на HX8347. Один с параллельным 8-битным интерфейсом, второй на 4-х проводном SPI. На SPI я пока не запустил, а 8-битный вариант я делал на ногодрыге и уже выкладывал про это статью.

Раз мне так повезло иметь разные дисплеи в своем распоряжении, хочу сделать сравнительный тест, кто победит:

  1. Ногодрыг
  2. FSMC
  3. SPI

Как только всё, что задумал сделаю, обязательно выложу результат, а на этом пока всё.

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

Тема на форуме - перейти.

Подписаться
Уведомить о
guest

3 комментариев
Старые
Новые
Межтекстовые Отзывы
Посмотреть все комментарии
Paul
Paul
11 месяцев назад

А зачем нужно отключать SPI?

if(_TransMode == SPI_HalfDuplexMaster) // Если находимся в полудуплексе, делаем дополнительные телодвижения
{
_SPI_Port->CR1 &= ~SPI_CR1_SPE; // Выключаем SPI
_SPI_Port->CR1 &= ~SPI_CR1_BIDIOE; // Выключаем двунаправленную передачу
В даташите нет упоминания, что BIDIOE меняется только при выключенном SPI, а значит это не нужно

3
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x