Программирование STM32. Часть 10: SPI + DMA

В этой части мы перейдем к практике работы с DMA на примере интерфейса SPI, а именно рассмотрим передачу и прием данных по SPI в режиме Master с помощью контроллера DMA. Все примеры, как и всегда, для микроконтроллера stm32f103c8. Предыдущая статья здесь, все статьи цикла можно посмотреть тут: http://dimoon.ru/category/obuchalka/stm32f1.

Общие сведения о запросах DMA

Для начала давайте разберемся, к какому каналу DMA подключены запросы от SPI. Открываем Reference manual, в разделе про DMA находим вот такую картинку:

DMA1 request mapping

Из рисунка видно, что каждый канал DMA может обрабатывать запросы от большого числа периферийных модулей. Для примера возьмем канал 3. Он может принимать 5 разных запросов: USART3_RXTIM1_CH2TIM3_CH4TIM3_UP и SPI1_TX. Все эти запросы поступают на входы логического элемента ИЛИ. Как только станет активным один из запросов, на выходе этого элемента появится лог. 1. Далее, этот сигнал поступает на еще один элемент ИЛИ, который может пропускать через себя логический сигнал только в случае установки в единицу специального разрешающего сигнала (Channel 3 EN bit). Тут происходит следующая вещь: запрос DMA может формироваться либо от периферийных устройств, подключенных к этому каналу, либо битом MEM2MEMMEM2MEM используется в том случае, если нам не нужно ждать какого-либо запроса от периферии для передачи данных, например, при копировании одной области памяти в другую. С этим, думаю, все ясно. Есть еще вот такая таблица, в ней все то же самое, только в другом формате:

Summary of DMA1 requests for each channel

Теперь идем в раздел с SPI. В регистре SPI_CR2 есть два интересных бита: TXDMAEN и RXDMAEN:

SPI control register 2 (SPI_CR2)

Если установлен бит TXDMAEN, то при установки флага TXE (буфер передатчика пуст), SPI отправляет в DMA запрос SPIx_TX, а если установлен RXDMAEN, то SPI отправляет запрос SPIx_RX при установке флага RXNE (буфер приемника не пуст). Для SPI1 это будут запросы SPI1_TX и SPI1_RX.

 

Отправка данных по SPI в режиме Master через DMA

Для того, чтобы передать массив данных через SPI с помощью DMA, нужно сделать следующее:

  • Включить тактирование SPI и DMA
  • Настроить нужным образом SPI
  • В регистре SPI_CR2 установить бит TXDMAEN

И в DMA:

  • Записать в регистр адреса периферии DMA_CPARx адрес регистра SPI_DR
  • Записать в регистр адреса памяти DMA_CMARx адрес массива для отправки в SPI
  • Записать в регистр DMA_CNDTRx количество передаваемых элементов
  • Настроить канал DMA
  • Включить канал DMA

Поехали кодить! 😉

Для начала идет инициализация SPI. Вот полный код функции:

Тут все как обычно: инициализация пинов GPIO, к которым подключен SPI, инициализация самого SPI. Добавлено только 2 строчки: включение тактирования DMA1 (RCC->AHBENR |= RCC_AHBENR_DMA1EN) и разрешение генерации запроса DMA (SPI1->CR2 |= 1<<SPI_CR2_TXDMAEN_Pos)

Далее, переходим к функции передачи данных. Назовем ее SPI_Send():

На вход она принимает указатель на передаваемый массив и количество передаваемых байт.

Запрос на передачу данных от SPI1 у нас висит на 3-м канале DMA (см. картинку вверху). Перед началом любых манипуляций с каналом, надо убедиться, что он отключен:

Далее, указываем DMA, что именно мы хотим передать, куда и в каком количестве:

После этого переходим к конфигурированию канала:

Букв много, давайте разбираться. Режим MEM2MEM нам не нужен, устанавливаем в ноль. Приоритет нам так же не особо важен, тоже ноль (низкий). Теперь важно: разрядность памяти и периферии. Так как передаваемый массив у нас по одному байту, то MSIZE=0x00 (8 бит). А регистр SPI у нас 16-и разрядный, поэтому PSIZE=0x01 (16 бит). Далее, после каждой транзакции неплохо было бы увеличивать на единицу указатель на массив в памяти, мы же не хотим 10 раз в SPI отправить элемент, с индексом 0? Ставим MINC=1. А вот инкремент адреса периферийного регистра нам не нужен, PINC=0. Далее, кольцевой режим нам тоже сейчас не нужен, CIRC=0. Ну и напоследок, направление передачи: DIR=1, из памяти в периферию.

Все 🙂 Теперь, чтоб процесс пошел, нам нужно всего лишь включить данный канал DMA:

Ну и полный код функции:

Для примера соорудим вот такой main():

Этот код будет отправлять 10 байт в SPI1 через DMA.

 

Прием данных по SPI в режиме Master через DMA

С приемом данных все немного повеселее. Для начала в функцию инициализации SPI добавим строчку, в которой разрешим DMA-запрос на прием данных:

И функция инициализации будет выглядеть вот так:

DMA-запрос на прием данных от SPI1 заведен на 2-й канал DMA, поэтому переходим к настройке этого канала. Как и для предыдущего случая, сначала отключим канал:

Далее идет настройка, откуда принимаем, куда складываем и в каком количестве:

Ну и настройка канала DMA:

Тут все то же самое, что и при передаче данных, только направление передачи у нас из периферии в память (DIR = 0).

Ну и разрешаем работу канала DMA:

Ну вот, прием запустили. И на этом месте начинается небольшое веселье. Дело в том, что в режиме Master прием данных после включения канала DMA не пойдет. Чтобы разобраться, что тут может быть не так, давайте вспомним, как работает SPI.

SPI представляет собой сдвиговой регистр, и в режиме Master чтобы что-то получить от Slave-устройства, в него нужно что-то передать, так как прием и передача данных в SPI происходит одновременно. Надеюсь, тем, кто работал с SPI хотя бы в AVR-ках, это понятно. В этом и кроется засада. DMA-запрос SPI1_RX будет возникать только тогда, когда в буфере приемника что-то появится. А чтобы в буфере приемника что-то появилось, нужно «толкнуть» SPI записью в буфер передатчика какого-нибудь значения, например, 0xFF. И выход из положения есть! Нужно настроить 3-й канал DMA на передачу данных из памяти в SPI1, чтоб он толкал SPI1, тем самым осуществляя прием данных.

И тут есть маленькая хитрость. Мы можем сэкономить на памяти и не создавать буфер для толкания SPI, по длине равный буферу приема. Достаточно создать одну единственную переменную uint8_t, которую нужное количество раз будем отправлять в SPI. Чтобы такое провернуть, достаточно отключить инкремент адреса памяти при передаче данных из памяти в SPI. Перейдем к коду. Для начала создадим переменную-заполнитель:

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

Далее, инициализация регистров адреса памяти, периферии и количества передаваемых данных:

Вот тут мы и заносим в регистр адреса памяти адрес нашей переменной _filler. Ну и настройка канала DMA:

Обращаю внимание, что инкремент адреса памяти отключен.

И последней строчкой запускаем процесс:

Все! После этого в SPI1 начнет запихиваться значение 0xFF и начнется прием данных.

Для наглядности приведу код функции приема целиком:

Вот и получается, что для получения данных по SPI, который настроен в режиме Master, приходится задействовать 2 канала DMA. Для тех, кто не в курсе или не обратил внимание. переменная _filler объявлена как static, и это не с проста.

Дело в том, что не-static переменные, объявленные внутри функции, выделяются на стеке и живут только во время выполнения данной функции. После выхода из функции, адрес, который был предоставлен переменной, может быть занят уже другой переменной из другой функции. А если переменную внутри функции объявить как static, то она будет расположена в области глобальных переменных, и ее значение будет сохранено после выхода из функции. Единственное ее отличие от глобальной переменной состоит в том, что доступ к ней может быть осуществлен только из функции, в которой она была объявлена.

Небольшой main() для демонстрации:

На этом все, в следующей статье мы научимся с помощью DMA копировать одну область памяти в другую. Отдельное спасибо Cyan-у из телеграмм-чата сообщества easyelectronics.ru за консультацию при изучении взаимодействия SPI с DMA.

Продолжение следует!!! Продолжение

Метки: , , , . Закладка Постоянная ссылка.

12 комментариев: Программирование STM32. Часть 10: SPI + DMA

  1. Олег пишет:

    очень познавательно. Большое спасибо

  2. Павел пишет:

    Вопрос такой: Во время приёма с SPI по DMA, что отправляется по линии MOSI?

    • DiMoon пишет:

      Если SPI в режиме Master, то там ситуация следующая. Если просто настроить прием данных от SPI, то процесс приема данных запущен не будет, так как для того, чтобы что-то принять по SPI Master, нужно что-то передать в SPI. Поэтому делается так: настраиваем прием данных от SPI через один DMA, и настраиваем передачу данных в SPI через другой канал DMA. И передавать можно как раз нужные данные-заполнители (например, 0xFF). Т.е. нам надо 2 массива, один для приема полезных данных, а другой массив-заполнитель.
      Однако, это можно несколько оптимизировать, если по MOSI нам надо передавать все время один и тот же заполнитель во время приема. Для этого, при настройки канала DMA на передачу в SPI, отключаем инкремент адреса памяти, а в регистр количества передаваемых данных заносим сколько нам нужно данных принять. При этом DMA выполюнет указанное количество раз один и тот же заполнитель в MOSI.

  3. Vold пишет:

    Спасибо за хороший пример! 4 дня мучился, пытаясь отправить не массив, а простое число в CMAR3, а из него в SPI->DR. Соединил перемычкой PA6 и PA7, чтобы тут же в регистре данных SPI смотреть что отправил и что прилетело.
    Прилетало невесть что) А теперь все норм)
    Обьясните, почему обязательно нужно использовать массив с указателями, а обычный uint8_t тут неприменим?

    • DiMoon пишет:

      Каждая переменная имеет физический адрес, по которому она размещена в ОЗУ микроконтроллера, и значение, которое содержится по указанному адресу. Регистр CMAR имеет разрядность 32 бита, и в него необходимо занести именно АДРЕС переменной или адрес начала массива, но не само значение. DMA потом сам обратится по указанному адресу в ОЗУ и засунет ЗНАЧЕНИЕ, которое расположено по этому адресу в переменную/регистр_периферии, который расположен по АДРЕСУ, указанному в регистре CPAR.

      CMAR и CPAR с точки зрения программы на Си — обычные переменные, которые расположены где-то в адресном пространстве микроконтроллера. Поэтому необходима вся эта пляска с указателями, как в примере.

      Ну и небольшой пример. Пусть у нас есть переменная uint8_t data = 0x13, которая расположена по адресу в 0x20000000 ОЗУ. И если сделать вот так CMAR = data, то компилятор не скажет и слова, с точки зрения Си тут ни какого криминала нет. Вот только DMA будет обращаться в область памяти 0x00000013, и по этому адресу будет брать какое-то значение, и выпихивать его в SPI. А может данное действие приведет к ошибке обращения к зарезервированной области памяти, точно не могу сказать, это зависит от конкретного случая. Поэтому в CMAR нужно занести именно адрес переменной, в данном случае 0x20000000.

      Советую очень тщательно разобраться с указателями Си, тема это не совсем простая, как может показаться на первый взгляд, но без указателей внутри МК ни как не прожить))

  4. Егор пишет:

    Можно как-нибудь запускать DMA по событию таймера, без прерываний?

  5. Егор пишет:

    Как циклически запускать передачу по SPI через DMA? Можно это сделать по таймеру?
    Только через прерывания?

    • DiMoon пишет:

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

      Но если нужно постоянно передавать один и тот же массив данных без пауз, то тут может помочь кольцевой режим работы DMA (см. бит CIRC в регистре DMA_CCRx). В этом случае, после передачи всего массива в SPI, счетчик переданных данных сбросится в ноль и процесс повторится снова. Это будет выглядеть как сплошной поток байт из SPI. Естественно, значения в передаваемом массиве можно будет изменять по ходу дела.

  6. Алексей пишет:

    Здравствуйте. Интересная статья, а главное полезно. Такой вопрос: Два и более устройств соедены по SPI, Master отправляет какие-то данные, а в ответ должен получить небольшой массив. Как правильно определить конец массива чтобы начать передачу другому устройству?

  7. Николай пишет:

    Спасибо! Все очень доходчиво и код отлично работает. А интересно, может случиться так, что приемник не успеет прочитать байт? По причине, что DMA1_Channel3 запускает передачу байта (_filler) не дожидаясь когда DMA1_Channel2 прочитает буфер передатчика.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *