STM32 с нуля. Интерфейс SPI. Описание и пример использования.

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

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

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

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

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

В общем, все, что требуется, присутствует!

Список прерываний для SPI:

Прерывания SPI

Давайте посмотрим, как можно настроить 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. Будем писать программу и для ведущего и для подчиненного. Что бы такое придумать, чтобы не просто гонять бесполезные данные…

Хм, давайте так: одному контроллеру на вход подается аналоговое напряжение. Он запускает АЦП и в зависимости от полученного значения выдает на шину SPI значение. Значения могут быть такими:

Напряжение       Отсылаемое значение
U = [0..1) В           0x00
U = [1..2) В           0x01
U = [2..3) В           0x02
U = [3..3.3) В         0x03
U >= 3.3 В             0x04

Второй контроллер в зависимости от принятых данных зажигает светодиоды. Если принимает 0х01 – зажигает один диод, если принимает 0х02 – зажигает два, ну дальше вы поняли 🙂

Небольшое лирическое отступление – тут мы будем пользоваться тем, что изучали ранее – вот тут про создание проекта, а здесь про использование АЦП.

Задача поставлена, начинаем реализовывать! Сначала напишем программу для ведущего (SPI Master). Создаем проект, не забыв добавить файлы из SPL для работы с SPI, и пишем следующий код:

/***************************************************************************************/
#include "stm32f10x.h" 
#include "stm32f10x_rcc.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_spi.h"
#include "stm32f10x_adc.h"


/***************************************************************************************/
// Объявляем переменные
GPIO_InitTypeDef port;
SPI_InitTypeDef spi;
ADC_InitTypeDef adc;
uint8_t sendData;
uint16_t counter;
uint16_t data;


/***************************************************************************************/
void initAll()
{ 
	// Тут абсолютно вся инициализация
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);

	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_Soft;
	spi.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;
	spi.SPI_FirstBit = SPI_FirstBit_MSB;
	spi.SPI_CRCPolynomial = 7;
	SPI_Init(SPI1, &spi);

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

	ADC_StructInit(&adc);
	adc.ADC_ContinuousConvMode = ENABLE;
	adc.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
	ADC_Init(ADC1, &adc); 

	port.GPIO_Mode = GPIO_Mode_IPD;
	port.GPIO_Pin = GPIO_Pin_0;
	port.GPIO_Speed = GPIO_Speed_2MHz;
	GPIO_Init(GPIOA, &port);
}


/***************************************************************************************/
int main()
{
	__enable_irq();
	initAll();
	
	// Включаем АЦП
	ADC_Cmd(ADC1, ENABLE);
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
	
	// И конечно же включаем SPI
	SPI_Cmd(SPI1, ENABLE);
	
	while(1)
	{
		counter++;
		// Это просто счетчик, чтобы отсылать на шину данные только когда 
		// счетчик досчитает до 15000, число выбрано абсолютно случайно :)
		// Вообще такую задержку через инкрементирование переменной я решил 
		// сделать, чтобы не перегружать пример, в реальных проектах задержку 
		// лучше организовывать через таймеры

		if(counter == 15000)
		{
			counter = 0;
			data = ADC_GetConversionValue(ADC1);
			
			// Анализируем данные
			if (data == 0xFFF)
			{
					 sendData = 0x04;
			}
			else if (data >= 0xE8B)
			{
						sendData = 0x03;
			}
			else if (data >= 0x9B2)
			{
				sendData = 0x02;
			}
			else if (data >= 0x4D9)
			{
				 sendData = 0x01;
			}
			else
			{
				 sendData = 0x00;
			}

			// Отсылаем, ради этого все и затеивалось
			SPI_I2S_SendData(SPI1, sendData);
		 }
	}
}


/***************************************************************************************/

Master готов, пишем программу для SPI Slave:

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


/***************************************************************************************/
GPIO_InitTypeDef port;
SPI_InitTypeDef spi;
uint8_t data;
uint8_t needUpdate;


/***************************************************************************************/
void initAll()
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

	SPI_StructInit(&spi);
	spi.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
	spi.SPI_Mode = SPI_Mode_Slave;
	spi.SPI_DataSize = SPI_DataSize_8b;
	spi.SPI_CPOL = SPI_CPOL_Low;
	spi.SPI_CPHA = SPI_CPHA_2Edge;
	spi.SPI_NSS = SPI_NSS_Soft;
	spi.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;
	spi.SPI_FirstBit = SPI_FirstBit_MSB;
	spi.SPI_CRCPolynomial = 7;
	SPI_Init(SPI2, &spi);

	GPIO_StructInit(&port);
	port.GPIO_Mode = GPIO_Mode_AF_PP;
	port.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
	port.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &port);

	port.GPIO_Mode = GPIO_Mode_Out_PP;
	port.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
	port.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &port);
}


/***************************************************************************************/
int main()
{
	__enable_irq();
	initAll();
	SPI_Cmd(SPI2, ENABLE);
	NVIC_EnableIRQ(SPI2_IRQn);
	
	// Тут мы разрешаем прерывание по приему
	SPI_I2S_ITConfig(SPI2, SPI_I2S_IT_RXNE, ENABLE);
	
	// Ну вот приняли, теперь просто зажигаем диоды
	while(1)
	{
		if (needUpdate == 1)
		{
			GPIO_ResetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3);

			switch(data)
			{
				case 0x01:
					GPIO_SetBits(GPIOA, GPIO_Pin_0);
					break;

				case 0x02:
					GPIO_SetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1);
					break;

				case 0x03:
					GPIO_SetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2);
					break;

				case 0x04:
					GPIO_SetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3);
					break;

				default:
					break;
			}

			needUpdate = 0;
		}
	}
}


/***************************************************************************************/
void SPI2_IRQHandler()
{
	data = SPI_I2S_ReceiveData(SPI2);
	needUpdate = 1;
}


/***************************************************************************************/

Важное дополнение – инициализация GPIO должна происходить после инициализации SPI, иначе могут возникнуть сбои в работе Slave.

Здесь вся работа с SPI в прерывании. Когда взлетает флаг, сигнализирующий о принятии байта, бежим в прерывание и читаем данные. Приняли данные – надо обновить состояние светодиодов – выставляем переменную needUpdate в единицу. В главном цикле после этого зажигаются нужные светодиоды.

Собственно, вроде бы вот и все, две программки написали, достаточно на сегодня! Конечно, это тестовые программы, просто для знакомства с интерфейсом, поэтому все так “в лоб” сделано 🙂 Наверное, в ближайшее время поковыряем еще и I2C, заодно сравним интерфейс с уже известным нам интерфейсом SPI, так что до скорых встреч!

Поделиться!

Подписаться
Уведомление о
guest
28 Комментарий
старее
новее большинство голосов
Inline Feedbacks
View all comments
фиалка молчаливая
7 лет назад

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

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

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

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

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

Kyle
Kyle
6 лет назад

Спс за пост. Один вопрос: зачем у вас в слейве строчка 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 написано что она не нужна вообще в режиме слейва, но там автор не юзал СтруктИнит, по этому я так понял оно фейлилось на праверке параметров функции, а у вас СтруктИнит есть, а вы тоже делаете эту настройку? нужна ли она? И еще: на какой максимальной частоте вам удалось запустить передачу?

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

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

AlexRoman
AlexRoman
6 лет назад

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

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

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

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

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

Chip115
Chip115
5 лет назад

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

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

написал небольшую программу для того, чтобы вникнуть в работу 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 пример работающий в железе, то почему-то на выводах контроллера при подключении логического анализатора тишина, почему так может быть?

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

В 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);

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

Снова у меня вопрос. Ситуация все та же. 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 контролировать скорость отправки данных на конкретной частоте или достаточно сконфигурировать скорость передачи данных
Заранее спасибо за ответ

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

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

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

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

михаил
михаил
1 год назад

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

михаил
михаил
Reply to  Aveal
1 год назад

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

Присоединяйтесь!

Profile Profile Profile Profile Profile
Vkontakte
Twitter

Язык сайта

Июль 2020
Пн Вт Ср Чт Пт Сб Вс
« Июн    
 12345
6789101112
13141516171819
20212223242526
2728293031  

© 2013-2020 MicroTechnics.ru