STM32 с нуля. Интерфейс SPI.

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

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

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

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

В общем, все на месте )

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

Прерывания SPI
Давайте посмотрим, как можно настроить SPI в STM32 для работы в нужном режиме. Как и раньше мы будем использовать 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 STM32. Тут вроде бы все понятно, но давайте по традиции разберем для чего нужно каждое отдельное поле:

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 значение. Значения могут быть такими:

Напряжение       Отсылаемое значение
0 < U < 1В            0х00
1В < U < 2В          0х01
2В < U < 3В          0х02
3В < U < 3.3В       0х03
3.3В < U               0х04

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

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

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

/****************************spi master.c****************************/
#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);	
 
    port.GPIO_Mode = GPIO_Mode_AF_PP;
    port.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_4 | 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);	
 
    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_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)
    {
        //Это просто счетчик, чтобы отсылать на шину данные только когда счетчик 
	//досчитает до 15000, число взято абсолютно "от балды" ))
        counter++;	
	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;
	}
        if(counter == 15000)
	{			
            //Отсылаем, ради этого все и затеивалось
	    SPI_I2S_SendData(SPI1, sendData);
	}	
    }		
}
 
 
 
/****************************End of file****************************/

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

/****************************spi slave.c****************************/
#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);
 
    port.GPIO_Mode = GPIO_Mode_AF_PP;
    port.GPIO_Pin = GPIO_Pin_12 | 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);
 
    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);
}
 
 
 
/*******************************************************************/
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); 
	    if (data == 0x01)
	    {
	        GPIO_SetBits(GPIOA, GPIO_Pin_0);
	    }
	    if (data == 0x02)
	    {
		GPIO_SetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1);
	    }
	    if (data == 0x03)
	    {
		GPIO_SetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2);
	    }
	    if (data == 0x04)
	    {
		GPIO_SetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3);
	    }
	    needUpdate = 0;
	}
    }
}
 
 
 
/*******************************************************************/
void SPI2_IRQHandler()
{
    data = SPI_I2S_ReceiveData(SPI2);	
    needUpdate = 1;
}
 
 
 
/****************************End of file****************************/

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

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

Понравилась статья? Поделись с друзьями!

STM32 с нуля. Интерфейс SPI.: 22 комментария
  1. Надо будет попробовать, было бы не плохо ещё с КАН протоколом разобраться))

    • Я CAN на работе часто использую, правда не с STM32, так что можно в будущем что-нибудь попробовать )

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

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

  4. Никак не могу добиться нормальной работы 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»!
    Вроде и то и это работает, но хотелось узнать поточнее?

    • Ну если в общем, то использование в режиме альтернативной функции является более логичным

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

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

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

  9. В 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 аналогичная ситуация)

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

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

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

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