Всех снова рад приветствовать, сегодня продолжаем шествовать по периферийным модулям микроконтроллеров STM32, очередной герой - интерфейс SPI. Не будем изменять традициям, пробежимся по некоторым теоретическим моментам и перейдем к созданию реального примера.
Теоретическая часть.
Интерфейс SPI.
Приступим! Интерфейс SPI позволяет связать между собой два и более устройств. Большой плюс SPI - быстродействие, так что большой объем данных улетит легко. Но в SPI, в отличие, например от I2C, для подключения N устройств потребуется большее количество линий (N + 3), а не 2, как в I2C. Так что, как и во всем, есть и плюсы и минусы, которые просто необходимо иметь ввиду в каждом конкретном проекте и в конкретной же задаче.
Существует несколько типов подключения к шине, но в общем-то, алгоритм работы при любом подключении практически один и тот же. Ведущий генерирует тактовый сигнал с вывода CLK и синхронно с этим сигналом передает данные по линии MOSI. В то же время подчиненное устройство передает данные в обратном направлении по линии MISO. Получается, что все сыты и довольны 👍 Хотя используется также подключение, при котором подчиненный только кушает байты данных, а сам ничего не шлет. А при подключении нескольких устройств возможно два варианта - независимое и каскадное. При независимом требуется больше линий, но такое подключение используется чаще.
Что же нам предлагает в плане SPI STM32?
- Возможно использование контроллера, как в качестве ведущего, так и в качестве подчиненного (это и так очевидно).
- Формат кадра - 8 или 16 бит.
- Возможность работы в режиме MultiMaster.
- Наличие огромного количества разных флагов - как для индикации окончания приема и передачи, так и для отлавливания разнообразных ошибок.
- Соответствующие прерывания.
- Возможна работа с использованием DMA.
- Аппаратное управление пином NSS для выбора подчиненного.
В общем, все, что требуется, присутствует. Список прерываний для SPI:
Практическая часть.
Библиотека SPL.
Здесь в оригинальной статье у меня шло описание, относящееся к библиотеке 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 с STM32CubeMx и HAL.
А, соответственно, пример будем создавать с использованием уже актуального инструментария. В качестве ознакомления с SPI в STM32 создадим максимально базовый проект - просто закинем некоторое количество байт в SPI через DMA. Другие примеры, где так или иначе используется этот интерфейс, можно найти вот тут:
- Подключение дисплея на базе ST7735 к микроконтроллеру STM32.
- Дисплей на базе ST7735 и STM32. Вывод изображения.
- STM32 и Ethernet. Часть 1. Подключение и настройка ENC28J60.
Я буду использовать отладочную плату STM32F429Discovery в этот раз, но никаких особенностей к процессу это не добавит, для любого другого контроллера суть будет точно такой же.
Создаем проект и первым делом активируем модуль SPI, пусть будет SPI1, по классике:
Режим выбираем "Full-Duplex Master", и поскольку наша задача - создать простой, базовый проект - то остальные настройки можем оставить дефолтными.
CubeMx сразу же "забронировал" 3 вывода контроллера для сигналов SPI:
- SPI1_SCK
- SPI1_MOSI
- SPI1_MISO
Хотя настройки SPI мы и оставили в покое, тем не менее необходимо вернуться в соответствующий раздел и произвести настройку DMA на вкладке "DMA Settings". Нажав на "Add" добавляем канал DMA, для которого задаем:
- DMA Request - SPI1_TX, поскольку будем использовать его для передачи данных
Результат выглядит следующим образом:
Кликнув на добавленный канал, получаем доступ к его настройкам:
Соответственно, здесь у нас выбрано:
- режим работы - 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, в котором можно выставить, к примеру, флаг готовности к следующей передаче. Это уже зависит от задачи, здесь просто исходим из того, что за секунду передача успела осуществиться. А создание базового примера на данном этапе завершаем 👍
я честно пыталась прочитать и понять, но увы. Комментировать женщинам тут нечего. Тогда просто поздравляю вас с первым днем весны!!!!
Благодарю) И Вас тоже с первым днем!)
Надо будет попробовать, было бы не плохо ещё с КАН протоколом разобраться))
Я CAN на работе часто использую, правда не с STM32, так что можно в будущем что-нибудь попробовать )
Ждём I2C и примеры где это можно было бы применить )))
Надо бы намутить примеров, но все руки не доходят )
Спс за пост. Один вопрос: зачем у вас в слейве строчка 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 написано что она не нужна вообще в режиме слейва, но там автор не юзал СтруктИнит, по этому я так понял оно фейлилось на праверке параметров функции, а у вас СтруктИнит есть, а вы тоже делаете эту настройку? нужна ли она? И еще: на какой максимальной частоте вам удалось запустить передачу?
Можно убрать, если есть StructInit, а по скорости я даже не замерял - не было особо цели разгонять
Скажите а возможно ли использовать хардварный SPI при работе с форматом данных 32 бита? Пытаюсь получить данные с AD7794
Ну по сути отправка 32 бит это то же самое, что и отправка два раза по 16
Никак не могу добиться нормальной работы SPI флеш памяти (mx 25l8005). Данные успешно читаются(точность данных проверена программатором). НО никак не могу заставить ее выставить бит записи и записать что нибудь. Хотя чтение данных и ID (разные команды) она принимает и отвечает. Может кто подскажет. email alexroman5000@gmail.com
P.S. Про ножку WriteProtect в курсе, дело видимо не в ней....
Хочу спросить как правильно организовать "GPIO_Mode_*" для вывода NSS в Slave устройствах? В одной статье пишут Альтернативная функция с подтягиванием к питанию "GPIO_Mode_AF_PP", в другой пишут Вход с Pull-up "GPIO_Mode_IPU"!
Вроде и то и это работает, но хотелось узнать поточнее?
Ну если в общем, то использование в режиме альтернативной функции является более логичным
Мне бы тоже были интересны примеры с шинами CAN и I2C на этих микроконтроллерах
Привет! Мучаюсь тут с SPI. Без отмашки мастера, слейв данные слать не будет. Это понятно. Вот в чем проблема. Мастер посылает байт слейву и ждет от слейва ответа, но если на MISO всегда находится 0x00, то мастер этот 0x00 И прочтет? По факту, слейв передачу не вел и держал линию на земле, но мастер принял это за передачу и выдал прерывание RXNE. Можно ли как-нибудь сделать так, что бы землю на линии MISO мастер не воспринемал как полезные данные?
Сделай программный фильтр в прерывании
написал небольшую программу для того, чтобы вникнуть в работу 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 пример работающий в железе, то почему-то на выводах контроллера при подключении логического анализатора тишина, почему так может быть?
По коду все вроде нормально. А по поводу симулятора в Keil'е, он не особо хорошо в принципе работает, лучше в железе отлаживать, а на симулятор не обращать внимания.
В 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);
Я вообще с серией STM32L не работал, но если в общем, то эти функции просто не нужны при работе с STM32F10x (как в статье). А если использовать STM32F3 или STM32F4, то там уже эти функции нужно вызывать. Видимо для L1 аналогичная ситуация)
Снова у меня вопрос. Ситуация все та же. 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 контролировать скорость отправки данных на конкретной частоте или достаточно сконфигурировать скорость передачи данных
Заранее спасибо за ответ
Всем здравствуйте. Имею такой вопрос. Настроил SPI между двумя stm32. Master - f4, slave - f100. Вопрос следующий. Отсылаю значение одной 16 битовой переменной. При приеме данных слейвом в первый раз младший бит всегда теряется. Отправляю 4 (b100), получаю 2 (b10). При приеме данных мастером все наоборот, появляется лишний бит. Отправил 2 (b10), получаю (b100). При повторном приеме, передаче, приходят правильные данные и больше такое не возникает. В чем причина? Подскажите, заранее спасибо
Всем доброго времени суток. У меня есть такой вопрос. как настраивать частоту CLK? Я по протоколу SPI должен опрашивать АЦП MCP3008. Если я не ошибаюсь, максимальная частота с ним это 200кГц. Вот как настроить SPI на какую либо частоту? Заранее спасибо
Добрый вечер!
Настраивается через предделитель:
spi.SPI_BaudRatePrescaler
Настраивая величину делителя частоты мы меняем частоту модуля SPI. К примеру, если:
spi.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;
То мы получим частоту SPI = Частота шины, к которой подключен модуль SPI / 4.
Будьте так добры, объясните как так у Вас получается отправить байт данных по SPI1, если
spi.SPI_NSS = SPI_NSS_Soft;
но нет строчки где Вы софтово дрыгаете ногой NSS?
Добрый день!
Поскольку тут только один Slave, то не было смысла особого добавлять проверку Chip Select. Если есть несколько Slave устройств, то каждое из них должно отслеживать свой CS.
Еще один небольшой затык. В функции void initAll() переставил последовательность инициализации: сначала SPI, затем порт, иначе выскакивает лишний SCK, который при неудачном стечении обстоятельств (что произошло у меня) может нарушить работу Slave.
Причем такая ситуация и у др. авторов, которых я смотрел в интернете.
Видимо инициализация GPIO подтягивает вывод к одному уровню, а затем инициализация SPI к противоположному. Спасибо за дополнение! Добавлю в статью информацию об этом.
Всё сделал как здесь, но на STM32F030CCT и в качестве периферии USART.
Не работает.