Top.Mail.Ru

STM32 SPI и DMA. Конфигурация и пример использования.

Всех снова рад приветствовать, сегодня продолжаем шествовать по периферийным модулям микроконтроллеров STM32, очередной герой - интерфейс SPI. Не будем изменять традициям, пробежимся по некоторым теоретическим моментам и перейдем к созданию реального примера.

Приступим! Интерфейс SPI позволяет связать между собой два и более устройств. Большой плюс SPI - быстродействие, так что большой объем данных улетит легко. Но в SPI, в отличие, например от I2C, для подключения N устройств потребуется большее количество линий (N + 3), а не 2, как в I2C. Так что, как и во всем, есть и плюсы и минусы, которые просто необходимо иметь ввиду в каждом конкретном проекте и в конкретной же задаче.

Существует несколько типов подключения к шине, но в общем-то, алгоритм работы при любом подключении практически один и тот же. Ведущий генерирует тактовый сигнал с вывода CLK и синхронно с этим сигналом передает данные по линии MOSI. В то же время подчиненное устройство передает данные в обратном направлении по линии MISO. Получается, что все сыты и довольны 👍 Хотя используется также подключение, при котором подчиненный только кушает байты данных, а сам ничего не шлет. А при подключении нескольких устройств возможно два варианта - независимое и каскадное. При независимом требуется больше линий, но такое подключение используется чаще.

Что же нам предлагает в плане SPI STM32?

  • Возможно использование контроллера, как в качестве ведущего, так и в качестве подчиненного (это и так очевидно).
  • Формат кадра - 8 или 16 бит.
  • Возможность работы в режиме MultiMaster.
  • Наличие огромного количества разных флагов - как для индикации окончания приема и передачи, так и для отлавливания разнообразных ошибок.
  • Соответствующие прерывания.
  • Возможна работа с использованием DMA.
  • Аппаратное управление пином NSS для выбора подчиненного.

В общем, все, что требуется, присутствует. Список прерываний для SPI:

Прерывания SPI

Здесь в оригинальной статье у меня шло описание, относящееся к библиотеке SPL, которая на текущий момент уже не поддерживается. Так что оставлю под спойлером исключительно ностальгических воспоминания ради )

Давайте посмотрим, как можно настроить SPI для работы в нужном режиме. Как и раньше мы будем использовать Standard Peripheral Library. Лезем в библиотеку, находим и открываем файл stm32f10x_spi.h. Прямо в начале файла все, что нам понадобится:

typedef struct
{
	uint16_t SPI_Direction;
	uint16_t SPI_Mode;
	uint16_t SPI_DataSize;
	uint16_t SPI_CPOL;
	uint16_t SPI_CPHA;
	uint16_t SPI_NSS;
	uint16_t SPI_BaudRatePrescaler;
	uint16_t SPI_FirstBit;
	uint16_t SPI_CRCPolynomial;
}SPI_InitTypeDef;

Назначив всем этим полям структуры SPI_InitTypeDef определенные значения, мы настраиваем модуль SPI. Тут вроде бы все понятно, но давайте по традиции разберем для чего нужно каждое отдельное поле.

  • uint16_t SPI_Direction - направление передачи данных, возможные значения:
SPI_Direction_2Lines_FullDuplex
SPI_Direction_2Lines_RxOnly
SPI_Direction_1Line_Rx
SPI_Direction_1Line_Tx
  • uint16_t SPI_Mode - режим работы, подчиненный или ведущий (master или slave).
  • uint16_t SPI_DataSize - DataSize и этим все сказано, размер данных - 8 или 16 бит.
  • uint16_t SPI_CPOL, uint16_t SPI_CPHA - а это настройки тактового сигнала.
  • uint16_t SPI_NSS - тут мы выбираем, как будет управляться сигнал NSS - аппаратно или программно. Соответственно возможные значения поля:
SPI_NSS_Soft
SPI_NSS_Hard
  • uint16_t SPI_BaudRatePrescaler - предделитель частоты.
  • uint16_t SPI_FirstBit - здесь выбираем, с какого бита начнется передача (младшего или старшего).
  • uint16_t SPI_CRCPolynomial - контрольная сумма.

Все возможные значения для всех полей описаны все в том же файле stm32f10x_spi.h чуть ниже определения структуры. В другом файле из SPL - stm32f10x_spi.c - функции для работы с SPI.

А, соответственно, пример будем создавать с использованием уже актуального инструментария. В качестве ознакомления с SPI в STM32 создадим максимально базовый проект - просто закинем некоторое количество байт в SPI через DMA. Другие примеры, где так или иначе используется этот интерфейс, можно найти вот тут:

Я буду использовать отладочную плату STM32F429Discovery в этот раз, но никаких особенностей к процессу это не добавит, для любого другого контроллера суть будет точно такой же.

Создаем проект и первым делом активируем модуль SPI, пусть будет SPI1, по классике:

STM32 SPI настройка.

Режим выбираем "Full-Duplex Master", и поскольку наша задача - создать простой, базовый проект - то остальные настройки можем оставить дефолтными.

CubeMx сразу же "забронировал" 3 вывода контроллера для сигналов SPI:

  • SPI1_SCK
  • SPI1_MOSI
  • SPI1_MISO
Порты ввода-вывода для SPI.

Хотя настройки SPI мы и оставили в покое, тем не менее необходимо вернуться в соответствующий раздел и произвести настройку DMA на вкладке "DMA Settings". Нажав на "Add" добавляем канал DMA, для которого задаем:

  • DMA Request - SPI1_TX, поскольку будем использовать его для передачи данных

Результат выглядит следующим образом:

Использование DMA.

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

DMA и SPI в STM32.

Соответственно, здесь у нас выбрано:

  • режим работы - Normal
  • инкрементирование адреса в периферии отключено, поскольку адрес регистра данных SPI1 всегда один и тот же
  • а для памяти инкрементирование включено
  • размер данных - один байт

Прерывания DMA будут включены автоматически, можно просто для надежности проверить выполнение данного пункта на вкладке "NVIC Settings". На этом с настройкой заканчиваем, генерируем проект и открываем его в выбранной IDE. Вся инициализация, как обычно, была сгенерирована автоматически, можно сразу переходить к сути дела.

Для того, чтобы отследить, когда передача данных завершена, используем соответствующую callback-функцию:

void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)

Переопределяем данную функцию в main.c:

/* USER CODE BEGIN 4 */
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
  if (hspi->Instance == SPI1)
  {
    // Передача завершена
  }
}
/* USER CODE END 4 */

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

Итак, остается только непосредственно осуществить многократно упомянутую отправку данных. Для этого объявим массив тестовых данных (transmitBuffer[]) и зададим его размер (BUFFER_SIZE), передачу производим при помощи:

HAL_SPI_Transmit_DMA()

И итогом будет следующий, весьма тривиальный, код:

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define BUFFER_SIZE                                                     32
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
SPI_HandleTypeDef hspi1;
DMA_HandleTypeDef hdma_spi1_tx;
/* USER CODE BEGIN PV */
uint8_t transmitBuffer[BUFFER_SIZE];
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_SPI1_Init(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */
  /* USER CODE END 1 */
  /* MCU Configuration--------------------------------------------------------*/
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();
  /* USER CODE BEGIN Init */
  /* USER CODE END Init */
  /* Configure the system clock */
  SystemClock_Config();
  /* USER CODE BEGIN SysInit */
  /* USER CODE END SysInit */
  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_SPI1_Init();
  /* USER CODE BEGIN 2 */
  for (uint8_t i = 0; i < BUFFER_SIZE; i++)
  {
    transmitBuffer[i] = i + 1;
  }
  /* USER CODE END 2 */
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
    HAL_SPI_Transmit_DMA(&hspi1, transmitBuffer, BUFFER_SIZE);
    HAL_Delay(1000);
  }
  /* USER CODE END 3 */
}

Отправляем в данном случае циклически, раз в секунду. По окончанию процесса, как уже обсудили, будет вызван callback, в котором можно выставить, к примеру, флаг готовности к следующей передаче. Это уже зависит от задачи, здесь просто исходим из того, что за секунду передача успела осуществиться. А создание базового примера на данном этапе завершаем 👍

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

28 комментариев
Старые
Новые
Межтекстовые Отзывы
Посмотреть все комментарии
фиалка молчаливая
9 лет назад

я честно пыталась прочитать и понять, но увы. Комментировать женщинам тут нечего. Тогда просто поздравляю вас с первым днем весны!!!!

Денис
Денис
9 лет назад

Надо будет попробовать, было бы не плохо ещё с КАН протоколом разобраться))

Денис
Денис
9 лет назад

Ждём I2C и примеры где это можно было бы применить )))

Kyle
Kyle
9 лет назад

Спс за пост. Один вопрос: зачем у вас в слейве строчка spi.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; Тут http://tablock.org.ua/post/99/STM32+SPI+Slave+%D0%B8+%D0%B8%D0%BD%D1%82%D0%B5%D1%80%D1%84%D0%B5%D0%B9%D1%81+SPI+%D0%B2+Versaloon+%28USB_TO_SPI%29 написано что она не нужна вообще в режиме слейва, но там автор не юзал СтруктИнит, по этому я так понял оно фейлилось на праверке параметров функции, а у вас СтруктИнит есть, а вы тоже делаете эту настройку? нужна ли она? И еще: на какой максимальной частоте вам удалось запустить передачу?

Тарас
Тарас
8 лет назад

Скажите а возможно ли использовать хардварный SPI при работе с форматом данных 32 бита? Пытаюсь получить данные с AD7794

AlexRoman
AlexRoman
8 лет назад

Никак не могу добиться нормальной работы SPI флеш памяти (mx 25l8005). Данные успешно читаются(точность данных проверена программатором). НО никак не могу заставить ее выставить бит записи и записать что нибудь. Хотя чтение данных и ID (разные команды) она принимает и отвечает. Может кто подскажет. email alexroman5000@gmail.com
P.S. Про ножку WriteProtect в курсе, дело видимо не в ней....

Альфис
Альфис
8 лет назад

Хочу спросить как правильно организовать "GPIO_Mode_*" для вывода NSS в Slave устройствах? В одной статье пишут Альтернативная функция с подтягиванием к питанию "GPIO_Mode_AF_PP", в другой пишут Вход с Pull-up "GPIO_Mode_IPU"!
Вроде и то и это работает, но хотелось узнать поточнее?

Паша
Паша
8 лет назад

Мне бы тоже были интересны примеры с шинами CAN и I2C на этих микроконтроллерах

Chip115
Chip115
7 лет назад

Привет! Мучаюсь тут с SPI. Без отмашки мастера, слейв данные слать не будет. Это понятно. Вот в чем проблема. Мастер посылает байт слейву и ждет от слейва ответа, но если на MISO всегда находится 0x00, то мастер этот 0x00 И прочтет? По факту, слейв передачу не вел и держал линию на земле, но мастер принял это за передачу и выдал прерывание RXNE. Можно ли как-нибудь сделать так, что бы землю на линии MISO мастер не воспринемал как полезные данные?

Виталий
Виталий
7 лет назад

написал небольшую программу для того, чтобы вникнуть в работу SPI, но, где-то что-то не доглядел.

#include "stm32f10x.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_spi.h"

GPIO_InitTypeDef port;
SPI_InitTypeDef spi;
int value;

void Delay(void)
{
unsigned long i;
for (i=0; i<2000000; i++);
}

void initAll()
{

RCC_APB2PeriphClockCmd(RCC_APB2ENR_AFIOEN, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);

GPIO_StructInit(&port);
port.GPIO_Mode = GPIO_Mode_AF_PP;
port.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
port.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &port);

SPI_StructInit(&spi);
spi.SPI_Direction=SPI_Direction_2Lines_FullDuplex;
spi.SPI_Mode=SPI_Mode_Master;
spi.SPI_DataSize=SPI_DataSize_8b;
spi.SPI_CPOL=SPI_CPOL_Low;
spi.SPI_CPHA=SPI_CPHA_2Edge;
spi.SPI_NSS=SPI_NSS_Hard;
spi.SPI_BaudRatePrescaler=SPI_BaudRatePrescaler_4;
spi.SPI_FirstBit=SPI_FirstBit_MSB;
spi.SPI_CRCPolynomial=0;
SPI_Init(SPI1, &spi);
SPI_Cmd(SPI1, ENABLE);
}

int main()
{

initAll();

value=0x93;

while(1)
{
SPI_I2S_SendData(SPI1, value);

Delay();

}
}
Смотрел осциллографом на выводах МК никаких сигналов нет, а если взять пример отсюда: http://electronix.ru/forum/lofiversion/index.php/t98806.html (пост от Jan 24 2012, 04:26), все действительно работает, может у меня есть какая-то грубая ошибка? И еще если симулировать в Keil пример работающий в железе, то почему-то на выводах контроллера при подключении логического анализатора тишина, почему так может быть?

Роман
Роман
7 лет назад

В STM32l-Discovery инициализацию SPI2 провожу так же, как Виталий и, собственно, автор поста. На осцилле не было передачи до тех пор, пока не добавил такой вот отрывок кода, который высмотрел на просторах:

GPIO_PinAFConfig(GPIOB, GPIO_PinSource12, GPIO_AF_SPI2);
GPIO_PinAFConfig(GPIOB, GPIO_PinSource13, GPIO_AF_SPI2);
GPIO_PinAFConfig(GPIOB, GPIO_PinSource15, GPIO_AF_SPI2);

Дмитрий
Дмитрий
6 лет назад

Снова у меня вопрос. Ситуация все та же. STM32f4 — Master, STM32f100 — Slave. Настроил передачу данных Slave. Но удалось заставить принимать правильные данные только воткнув в цикле задержку после отправки данных. В связи с этим вопросы
1) Что за задержка тогда 10000 в функции HAL_SPI_Transmit(&hspi1, (uint8_t*) data_send, BITS, 10000) и чего она дает
2) Что вообще является событием для прерывания:
приход данных по SLK или NSS, или же вообще по MOSI.
3) Какова правильная последовательность действий при приеме и передаче данных между Slave и Master. Должно ли это обязательно ли быть прерывание или нет и что за чем идет. Кто и за кем обращается к сдвиговому регистру
4) Нужно ли Master контролировать скорость отправки данных на конкретной частоте или достаточно сконфигурировать скорость передачи данных
Заранее спасибо за ответ

Дмитрий
Дмитрий
6 лет назад

Всем здравствуйте. Имею такой вопрос. Настроил SPI между двумя stm32. Master - f4, slave - f100. Вопрос следующий. Отсылаю значение одной 16 битовой переменной. При приеме данных слейвом в первый раз младший бит всегда теряется. Отправляю 4 (b100), получаю 2 (b10). При приеме данных мастером все наоборот, появляется лишний бит. Отправил 2 (b10), получаю (b100). При повторном приеме, передаче, приходят правильные данные и больше такое не возникает. В чем причина? Подскажите, заранее спасибо

Вадим
Вадим
4 лет назад

Всем доброго времени суток. У меня есть такой вопрос. как настраивать частоту CLK? Я по протоколу SPI должен опрашивать АЦП MCP3008. Если я не ошибаюсь, максимальная частота с ним это 200кГц. Вот как настроить SPI на какую либо частоту? Заранее спасибо

михаил
михаил
4 лет назад

Будьте так добры, объясните как так у Вас получается отправить байт данных по SPI1, если
spi.SPI_NSS = SPI_NSS_Soft;
но нет строчки где Вы софтово дрыгаете ногой NSS?

михаил
михаил
Ответ на комментарий  Aveal
4 лет назад

Еще один небольшой затык. В функции void initAll() переставил последовательность инициализации: сначала SPI, затем порт, иначе выскакивает лишний SCK, который при неудачном стечении обстоятельств (что произошло у меня) может нарушить работу Slave.
Причем такая ситуация и у др. авторов, которых я смотрел в интернете.

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