Serial Peripheral Interface - один из самых простых и быстрых интерфейсов, часто применяемый как для подключения периферийных устройств так и для связи микроконтроллеров между собой. Про принципы работы этого интерфейса написано очень много информации, по этому смысла рассказывать о них здесь нет. Вместо этого в статье я покажу пример практического использования SPI для связи контроллера STM32 (на демоплатке) с компьютером посредством переходника USB_TO_SPI, запущенном на базе платформы Versaloon. На стороне демоплаты это будет реализовываться с помощью аппаратного модуля SPI, настроенного в режиме Slave(ведомый). В то же время мы настроем Versaloon внутри себя на интерфейс SPI-Master (ведущий), и с помощью программы vsprog будем рулить им. 

Пример будет делать следующее(придумано от фонаря): сначала мы посылаем демоплате 5 байт, и параллельно принимаем от нее 5 байт 0xAA (напомню, что отправляя один байт по SPI слейву, мы получаем один байт в ответ от него, то есть происходит так называемый полнодуплексный, двухсторонний обмен). Демоплата, приняв эти 5 байт от мастера сохраняет их у себя. Затем мастер отправляет еще 5 байт, и параллельно слейв отдает обратно 5 байт, полученные за первый раз. В дополнение мы также помигаем светодиодами после второго 5и-байтного обмена. Сначала один светодиод помигает столько раз, сколько отправлено в первом байте второго обмена, затем второй светодиод помигает столько раз сколько отправлено во втором байте второго обмена. Наверное формулировка задания весьма сложная, попробую объяснить визуально. Вот так должны выглядеть эти два обмена по 5 Байт каждый:

spiobmen(1).jpg

Почему слейв отдает за первый обмен именно 0xAA? Не знаю, мне так захотелось. То же могу сказать про три FF-ных байта, отправляемых мастером во втором обмене. 

Как известно, физически SPI работает с помощью четырех цифровых линий:

  1. Master Output Slave Input - Выход мастера, вход слейва
  2. Master Input Slave Output - Вход мастера, Выход слейва
  3. Serial ClocK - Последовательный синхросигнал, Выходит от мастера, входит в слейв
  4. Slave Select - Выбор слейва(если слейвов на шине несколько, то нужно выбрать только один что бы линии MISO не конфликтовали)

Последняя линия включает модуль SPI Slave, причем обычно включение это происходит когда на линии логический "0", а выключение, когда на линии логическая "1" (отсюда вход Slave Select слейва, логичнее называть Not Slave Select, то есть когда на линии лог "1", слейв НЕ выбран). 

Но использование последней линии имеет смысл, когда на шине есть несколько слейвов, а в нашем случае только один слейв, по этому его можно включить навсегда подав на вход NSS логический "0". В STM32 это  можно сделать программно (ST-шники называют это Software slave management), а ногу контроллера NSS не трогать, или использовать под другие нужды. В примере так и поступим. 

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

spidemoboardandvsl.jpg

Настройка SPI в режиме SLAVE

Собственно код на основе SPL:

#include "stm32f10x.h"
void tool_delay_ms(uint32_t ms)
{
  volatile uint32_t nCount = (SystemCoreClock / 10000) * ms;  //~ 10 tackts for 1 for iteration
  for (; nCount != 0; nCount--)
    ;
}
int main(void) {
  RCC_PCLK2Config(RCC_HCLK_Div1); /* PCLK2 = HCLK */
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // Enable PORTB Periph clock (PORTB contains leds and SPI pins)
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);  // Enable SPI2 Periph clock
  // Disable JTAG for release LED PIN
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
  GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);
  // Configure PB.4 and PB.5 as Push Pull output at max 10Mhz
  GPIO_InitTypeDef gpio_port;
  gpio_port.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5;
  gpio_port.GPIO_Mode = GPIO_Mode_Out_PP;  //Push Pull
  gpio_port.GPIO_Speed = GPIO_Speed_10MHz;
  GPIO_Init(GPIOB, &gpio_port);
  GPIO_ResetBits(GPIOB, GPIO_Pin_4 | GPIO_Pin_5); // Turn off leds
  // Configure PB.13(SPI2_SCK) and PB.15(SPI2_MOSI)
  gpio_port.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_15;
  gpio_port.GPIO_Mode = GPIO_Mode_IPD;  //Internal pull down
  gpio_port.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOB, &gpio_port);
  // Configure  PB.14(SPI2_MISO)
  gpio_port.GPIO_Pin = GPIO_Pin_14;
  gpio_port.GPIO_Mode = GPIO_Mode_AF_PP;
  gpio_port.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOB, &gpio_port);
  // Configure SPI2 in Slave mode
  SPI_InitTypeDef   SPI_InitStructure;
  SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
  SPI_InitStructure.SPI_Mode = SPI_Mode_Slave;
  SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
  SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;
  SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
  SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
  SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;
  SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
  SPI_InitStructure.SPI_CRCPolynomial = 7;
  SPI_Init(SPI2, &SPI_InitStructure);
  SPI_Cmd(SPI2, ENABLE);
  uint16_t i;
  #define BYTES_COUNT 5
  uint8_t buf[BYTES_COUNT];
  while(1)
  {
    //do first exchange
    i = BYTES_COUNT;
    while (i--)
    {
      SPI2->DR = 0xAA;//Prepare DR for transmit
      while (!(SPI2->SR & SPI_I2S_FLAG_RXNE));  //Wait for data reception
      buf[i] = SPI2->DR; //Read received data
    }
    //do second exchange
    i = BYTES_COUNT;
    while (i--)
    {
      SPI2->DR = buf[i];  //Prepare DR for transmit
      while (!(SPI2->SR & SPI_I2S_FLAG_RXNE));  // Wait for data reception
      buf[i] = SPI2->DR; // Read received data
    }
    //blink count = first byte
    for(i=0; i < buf[BYTES_COUNT - 1]; i++){
      GPIO_WriteBit(GPIOB, GPIO_Pin_4, Bit_SET);
      tool_delay_ms(300);
      GPIO_WriteBit(GPIOB, GPIO_Pin_4, Bit_RESET);
      tool_delay_ms(300);
   }
    //blink count = second byte
    for(i=0; i < buf[BYTES_COUNT - 2]; i++){
      GPIO_WriteBit(GPIOB, GPIO_Pin_5, Bit_SET);
      tool_delay_ms(300);
      GPIO_WriteBit(GPIOB, GPIO_Pin_5, Bit_RESET);
      tool_delay_ms(300);
    }
  }
}
void assert_failed(uint8_t* file, uint32_t line)
{
  while (1){}
}

Настройка модуля SPI2 начинается с настройки пинов. Первыми, настраиваются пины SCK и MOSI в строках 23-26. Ключевым моментом здесь является настройка на вход с внутренней поддтяжной (IPD а не IN_FLOATING) к земле. Подтяжка нужна, чтобы не подключенная линия SCK не ловила наводки, и байты не принимались преждевременно из "ниоткуда". Без подтяжки, пример, скорее всего не будет работать корректно, так как даже правильные байты будут приходить начиная неизвестно с какого бита. 

Настройка самого модуля выполняется в строках 33 - 44. Отдельно стоит отметить строки 37-38. Они определяют полярность и фазу SCK. Полярность - это состояние линии SCK во время отсутствия обмена, либо это логический "0" (низкий, Low), либо лог. "1" (высокий, High). Фаза - это номер фронта сигнала, по которому происходит захват, то есть это либо первый фронт(первый перепад, 1st Edge), либо второй фронт (2nd Edge). Визуально это можно представить так:

Кстати у разных производителей трактовка этих понятий иногда отличается от общепринятой (например в NXP я встречал, что полярность - это состояние линии не в момент простоя, а в момент работы, то есть диаметрально противоположно стандартному обозначению). Да и наверняка найдутся те, которые поспорят что общепринятый стандарт, не тот которым я пользуюсь, а какой-нибудь другой.

В 39ой строке выставляется режим софтварного управления пином NSS, о котором я рассказывал в начале статьи. 

В 40овой строке задается делитель для частоты SCK, но в режиме Slave она не имеет значения вообще, потому что Slave не задает частоту, он может только поддерживать или не поддерживать частоту мастера (В документации заявлено что STM32 слейв должен работать на частоте SCK меньше или равной частоте PCLK/2, а так как мы настроили PCLK в 9ой строке на HCLK - на 24 MHz, то наш слейв должен работать на 12 MHz). Вобщем строку 40 я написал только для того, что бы удовлетворить код валиации в функцие SPL SPI_Init.

В 41ой строке задается режим MSB, при которомы старший бит передаваемого байта отправляется первым (на первом фазовом фронте SCK), а младший бит байта отправляется последним  (на восьмом фазовом фронте SCK).

В строке 47 объявляется 5и-байтный массив, который будет накапливать байты на первом обмене и отдавать на втором.

Сам обмен происходит в 52-57 строках, и как вы заметили, написан без применения SPL. Дело в том что эта секция достаточно критична, поскольку слейв на каждой итерации должен успеть выполнить 55ую строку(встать на ожидание байта) перед тем как мастер отправит очередной байт. Использовать SPL с его многочисленными вызовами функций и валидацией аргументов в данном случае расточительство дополнительных тактов ядра. По этому же принципу вместо цикла for использован считающий "вниз" цикл while. 

Что происходит в строках 67-79, вы должны догадаться сами=).

Прошивать пример можно по SWD. 

Работа с интерфейсом SPI с помощью команд программатора, VerSaloon Scripts (VSS) 

Программатор versaloon имеет определенную базу для написания скриптов. Скрипты позволяют автоматизировать многократные операции прошивки, выполнения всяких тестов, получение и обработка данных. В качестве команд для скриптов выступают специальные внутренние команды, которые позволяют как запускать операции программатора (которые вы обычно вызываете из vsprog), так и инициализировать специальные интерфейсы.

Для просмотра списка команд программатора, нужно выполнить команду:

vsprog -V"vss-help"

Ее вывод достаточно обширный, но для интерфейса SPI нас интересуют вот эти строки:

Info:     interface.spi: spi interface handler
Info:     interface.spi.init: initialize spi, format: spi.init [KHZ MODE FIRSTBIT]
Info:     interface.spi.fini: finalize spi, format: spi.fini
Info:     interface.spi.config: config spi, format: spi.config KHZ MODE FIRSTBIT
Info:     interface.spi.io: spi input and output, format: spi.io DATASIZE DATA...

Также тут можно найти команды по работе с напряжением внешнего питания (возможно вы их уже использовали):

Info:     interface.tvcc.get: get target voltage, format: tvcc.get
Info:     interface.tvcc.set: output power to target, format: tvcc.set VOLTAGE_IN_MV

В общем в этом выводе показаны имена команд, их описания и параметры. Каждую команду можно выполнять по отдельности cпомощью vsprog -V, например:

vsprog -V"tvcc.set 3300"

Либо можно сразу запихать нужные команды в текстовый файл построчно, и таким образом создать скрипт для интерпретатора в vsprog. Для запуска такого скрипта, достаточно передать имя файла программе vsprog. Этот способ удобен при несложных действиях: интерпретатор в программе vsprog весьма тривиальный, он позволяет делать простенькие цыклы, объявлять функции, но вот например как там работать с переменными я не понял, сгенерировать случайный байт тоже не получилось. По этому для серьезных операций лучше использовать другой интерпретатор вроде Bash, Perl, Python, или что вы там используете=). При этом для выполнения команды программатора из интерпретатора, как раз таки удобно вызывать "vsprog -V...".

В нашем случае пример очень простой, по этому скрипт для него я написал в простом текстовом файле SPI.vts (Расширение означает Versaloon Text Script):

q              # тихий режим - не выводить выполняемые команды
tvcc.set 3300    # включить внешнее питание
spi.init            # инициализировать интерфейс
spi.config 1000 0 1     # частота 1000 KHz (1 MHz), фазополярный режим SPI №0, 1=MSB
# Вывод стоки на экран
out "== First exchange == (Send 0x17 0xAB 0xDF 0x00 0x32)"
spi.io 1 0x17     # Отправить 1 байт со значением 0x17
spi.io 1 0xAB
spi.io 1 0xDF 
spi.io 1 0x00
spi.io 1 0x32
delay.delayus 1          # задержка на 1мкс, что бы слейв успел "перейти" к следующему обмену
out "== Second exchange == (Send 0x02 0x03 0xFF 0xFF 0xFF)"
spi.io 1 0x02
spi.io 1 0x03 
spi.io 1 0xFF 
spi.io 1 0xFF
spi.io 1 0xFF
spi.fini        # завершить работу с интерфейсом

При написании команды не обязательно указывать полный префикс, если команда уникальна (команда spi.init уникальна по этому вместо interface.spi.init, я могу написать просто spi.init). Байты в одном обмене отсылаются отдельными командами, хотя можно было бы для одного обмена написать просто:

spi.io 5 0x17 0xAB 0xDF 0x00 0x32

Но в таком случае все 5 байт сначала отправляются в Versaloon а затем он с минимальными задержками между байтами их заталкивает в Slave, анализатор при этом показывает такой результат:

cont.jpg

При этом нужно учитывать, что наш слейв работает на 24 МГц, в то время как versaloon работает намного быстрее (на диаграмме выше, задержек между байтами практически не видно), и соответственно слейв может не успевать принимать байты (хоть в данном случае на диаграмме, на данной частоте в 500 КГц он и успел везде ответить правилино - каждый байт 0хАА). По этому я отправлял их по отдельности, это хорошо увеличивает задержки между байтами (на диаграмме показаны первые два байта первого обмена):

separatebyteslogic.jpg.

Команды вроде достаточно понятны. Если нет - спрашивайте в комментариях. Отдельно расскажу про второй параметр spi.config, поскольку он не очевиден. Этот параметр определяет режим SPI, и может принимать четыре значения: 0, 1, 2, 3. Номер режима более понятен в двоичной системе счисления исходя из значений полярности и фазы:

№ режима CPOL CPHA 
0 (0b00) 0 (LOW) 0 (1Edge)
1 (0b01) 0 (LOW)
1 (2Edge)
2 (0b10) 1 (High) 0 (1Edge)
3 (0b11) 1 (High) 1 (2Edge)
И еще один интересный момент про первый параметр: как написано в vss-help это частота работы интерфейса в килогерцах. В  примере я установил частоту в 1000 КГц, и запустив, померил ее с помощью логического анализатора. Вместо 1000 КГц там было около 500 КГц, то есть в 2 раза меньше. Затем я попробовал указать 5000 КГц, результат был около 4900 КГц(учитывая неточность анализатора, можно считать правильно).

Итак, подключаем интерфейс SPI:

spidemoconnectphoto(1).jpg

Теперь запускаем скрипт, и видим результат:

$ vsprog SPI.vts 
VSProg 1.0 svn:
CopyRight(c) 2008-2010 by SimonQian <[email protected]>
URL: http://www.SimonQian.com/en/Versaloon
mail: [email protected]
SPI.vts>>>q  # тихий режим - не выводить выполняемые команды
Info:   Versaloon(0x01)by Simon(compiled on Sep 20 2013)
Info:   USB_TO_XXX abilities: 0x0000126E:0x010001EF:0xC0000007
Info:   Target runs at 3.095V
Info:   Target runs at 3.096V
== First exchange == (Send 0x17 0xAB 0xDF 0x00 0x32)
Info:   0000 AA
Info:   0000 AA
Info:   0000 AA
Info:   0000 AA
Info:   0000 AA
== Second exchange == (Send 0x02 0x03 0xFF 0xFF 0xFF)
Info:   0000 17
Info:   0000 AB
Info:   0000 DF
Info:   0000 00
Info:   0000 32

Также вы должны увидеть как 2 раза мигнет светодиод на PB4 и 3 раза на PB5. На этом все, советую внимательно посмотреть vss-help, там можно найти много чего весьма полезного, вроде генерация шим, управление GPIO, и т.д.