Наш магазин на eBay Наш магазин на AliExpress Наш канал в telegram

Программная реализация ведущего шины SPI (подробный разбор и некоторые процедуры на асме для PIC и AVR)

В этой статье рассматривается пример программной реализации на микроконтроллерах PIC и AVR функций ведущего шины SPI для разных режимов (mode0, mode1, mode2, mode3). Чтобы понимать что происходит — для начала, как всегда, читаем теорию (что такое SPI и как он работает). Если с теорией разобрались, тогда можно приступать к практической реализации.

Итак, что должен уметь делать SPI-мастер? Собственно говоря, всего четыре вещи:

  1. читать с шины MISO нужное количество бит;
  2. передавать по шине MOSI нужное количество бит;
  3. формировать на шине SCLK нужное количество импульсов, соблюдая правильную полярность;
  4. управлять шиной SS.

Как вы знаете (вы же с теорией знакомы?) — чтение и установку данных и «Мастер», и «Слэйв» производят по противоположным фронтам сигнала тактирования, причём для приёма и передачи может использоваться один и тот же сдвиговый регистр. Для наглядности давайте нарисуем для всех четырёх режимов диаграммы сигналов, соответствующие передаче по SPI двух бит, а уже по ним распишем действия контроллера:

Как происходит  обмен данными по SPI при различных CPHA

Диаграммы нарисованы для случая, когда передача осуществляется младшим битом вперёд.

Биты, не выделенные жирным, — не имеют для нас значения и могут быть на следующем этапе перезаписаны (это, например, те биты, которые мы уже установили на шину данных, или, наоборот, — уже считали с шины данных).

По диаграммам видно, что в результате обмена данными сдвиговые регистры «Мастера» и «Слэйва» поменялись содержимым (помните, в теоретической части я писал, что у SPI есть такая особенность, — для приёма и передачи можно использовать один и тот же регистр).

Прежде чем переходить к написанию кода, — давайте ещё подумаем вот о чём. Архитектура контроллеров у нас 8-ми битная, а для SPI часто может потребоваться бОльшая размерность сдвигового регистра.

Структура составного сдвигового регистра

Пусть в нашем примере размер сдвигового регистра можно будет выбрать от 1-го до 32-х бит. То есть максимально под сдвиговый регистр мы будем выделять 4 байта (4 восьмибитных регистра, расположенных в памяти так, как на рисунке справа). Для случаев, когда размер регистра превышает 1 байт (то есть равен 2, 3 или 4 байта) мы будем программно имитировать работу с несколькими 8-ми битными регистрами как с одним большим. Используемые для организации сдвигового регистра восьмибитные регистры в дальнейшем будем называть просто байтами, чтобы не путаться.

Что представляет собой чтение с точки зрения «Мастера»? Да ничего особенного, — нужно просто прочитать сигнал на входе (MISO) и записать его в младший или старший бит сдвигового регистра (в зависимости от того — младшим или старшим битом вперёд передаются данные и от значения CPHA). Вот так это выглядит в коде:

Для контроллеров PIC Для контроллеров AVR

1) Для передачи младшим битом вперёд при CPHA=0 и старшим битом вперёд при CPHA=1

Read_MISO:
   bcf   INDF, 0
   btfsc PORT_SPI, MISO_Line
   bsf   INDF, 0
   return

2) Для передачи старшим битом вперёд при CPHA=0 и младшим битом вперёд при CPHA=1

Read_MISO:
   bcf   INDF, 7
   btfsc PORT_SPI, MISO_Line
   bsf   INDF, 7
   return

Для случая 1 в регистр FSR должен быть предварительно загружен адрес младшего используемого байта нашего сдвигового регистра, а для случая 2 — адрес старшего используемого байта этого регистра.

PORT_SPI — адрес порта ввода /вывода, к которому подключена линия MISO,

MISO_Line — номер канала порта.

1) Для передачи младшим битом вперёд при CPHA=0 и старшим битом вперёд при CPHA=1

Read_MISO:
   ld    temp, X
   cbr   temp, 0b00000001
   sbic  PIN_SPI, MISO_Line
   sbr   temp, 0b00000001
   st    X, temp
   ret

2) Для передачи старшим битом вперёд при CPHA=0 и младшим битом вперёд при CPHA=1

Read_MISO:
   ld    temp, X
   cbr   temp, 0b10000000
   sbic  PIN_SPI, MISO_Line
   sbr   temp, 0b10000000
   st    X, temp
   ret

Для случая 1 в регистр X должен быть предварительно загружен адрес младшего используемого байта нашего сдвигового регистра, а для случая 2 — адрес старшего используемого байта этого регистра.

PIN_SPI — адрес регистра, отображающего состояния входов порта ввода/вывода, к которому подключена линия MISO,

MISO_Line — номер канала порта.

Здесь и далее, temp — просто некий вспомогательный регистр.

Кроме того, перед началом передачи, предназначенные для передачи биты, должны быть выровнены по старшему биту старшего используемого нами для организации сдвигового регистра байта при передаче старшим битом вперёд, и по младшему биту младшего используемого байта при передаче младшим битом вперёд. Картинка внизу иллюстрирует, что имеется ввиду на примере 12-ти битного пакета SPI (то есть, когда мы реализуем 12-ти битный сдвиговый регистр; при этом из наших зарезервированных четырёх байт используются только два).

Выравнивание перед началом передачи

Идём дальше. «Сдвиг», как видно по диаграммам, состоит для «Мастера» (впрочем как и для «Слэйва», но мы в этой статье занимаемся только «Мастером») из двух действий: это,
во-первых, собственно сдвиг регистра и, во-вторых, установка очередного бита на шину MOSI.

Начнём мы с отдельной процедуры, реализующей сам сдвиг, поскольку если наш сдвиговый регистр состоит из нескольких байт, то при сдвиге нужно определённым образом переносить биты из одного байта в другой. Кроме того, сдвиг нам нужен не простой, а циклический, то есть при сдвиге вправо младший бит нашего большого регистра должен стать старшим, а при сдвиге влево — старший бит должен стать младшим. Код, реализующий сдвиг будет выглядеть следующим образом:

Для контроллеров PIC Для контроллеров AVR

1) Сдвиг вправо

Shift_Right:
   movf   Hi_byte_Address,0
   movlw  FSR
   movf   Byte_Number,0
   movlw  Byte_Counter
   bcf    STATUS,C
Next_Shift_Right:
   rrf    INDF,1
   incf   FSR
   decfsz Byte_Counter
   goto   Next_Shift_Right
   movf   Hi_byte_Address,0
   movlw  FSR
   btfsc  STATUS,C
   bsf    INDF,7
   return

2) Сдвиг влево

Shift_Left:
   movf   Low_byte_Address,0
   movlw  FSR
   movf   Byte_Number,0
   movlw  Byte_Counter
   bcf    STATUS,C
Next_Shift_Left:
   rlf    INDF,1
   decf   FSR,1
   decfsz Byte_Counter
   goto   Next_Shift_Left
   movf   Low_byte_Address,0
   movlw  FSR
   btfsc  STATUS,C
   bsf    INDF,0
   return

1) Сдвиг вправо

Shift_Right:
   mov   XL, Hi_byte_Address
   mov   Byte_Counter, Byte_Number
   clc
Next_Shift_Right:
   ld    temp, X
   ror   temp
   st    X, temp
   inc   XL
   dec   Byte_Counter
   brne  Next_Shift_Right
   brcc  Shift_Right_Exit
   mov   XL, Hi_byte_Address
   ld    temp, X
   sbr   temp, 0b10000000
   st    X, temp
Shift_Right_Exit:
   ret

2) Сдвиг влево

Shift_Left:
   mov   XL, Low_byte_Address
   mov   Byte_Counter, Byte_Number
   clc
Next_Shift_Left:
   ld    temp, X
   rol   temp
   st    X, temp
   dec   XL
   dec   Byte_Counter
   brne  Next_Shift_Left
   brcc  Shift_Left_Exit
   mov   XL, Low_byte_Address
   ld    temp, X
   sbr   temp, 0b00000001
   st    X, temp
Shift_Left_Exit:
   ret
  1. Low_byte_Address — адрес младшего байта нашего сдвигового регистра,
  2. Hi_byte_Address — адрес его старшего байта,
  3. Byte_Counter — счётчик обработанных байт,
  4. Byte_Number — количество байт, используемых для организации сдвигового регистра.

Ну вот, со сдвигом регистра разобрались, теперь можно приступать и к этапу передачи, именуемому «Сдвиг». Как я ранее уже писал, — этот этап передачи состоит из двух действий: сдвиг регистра и установка очередного бита на шину MOSI. В коде это будет выглядеть так:

Для контроллеров PIC Для контроллеров AVR

1) Для передачи младшим битом вперёд при CPHA=0

Shift_&_Set_MOSI:
   call   Shift_Right
   movf   Low_byte_Address,0
   movwf  FSR
   bcf    PORT_SPI, MOSI_Line
   btfsc  INDF, 0
   bsf    PORT_SPI, MOSI_Line
   return

2) Для передачи старшим битом вперёд при CPHA=0

Shift_&_Set_MOSI:
   call   Shift_Left
   movf   Hi_byte_Address,0
   movwf  FSR
   bcf    PORT_SPI, MOSI_Line
   btfsc  INDF, 7
   bsf    PORT_SPI, MOSI_Line
   return

3) Для передачи младшим битом вперёд при CPHA=1

Shift_&_Set_MOSI:
   movf   Low_byte_Address,0
   movwf  FSR
   bcf    PORT_SPI, MOSI_Line
   btfsc  INDF, 0
   bsf    PORT_SPI, MOSI_Line
   call   Shift_Right
   return

4) Для передачи старшим битом вперёд при CPHA=1

Shift_&_Set_MOSI:
   movf   Hi_byte_Address,0
   movwf  FSR
   bcf    PORT_SPI, MOSI_Line
   btfsc  INDF, 7
   bsf    PORT_SPI, MOSI_Line
   call   Shift_Left
   return

1) Для передачи младшим битом вперёд при CPHA=0

Shift_&_Set_MOSI:
   rcall  Shift_Right
   in     temp1, PORT_SPI
   cbr    temp1, (1 << MOSI_Line)
   mov    XL, Low_byte_Address
   ld     temp2, X
   sbrc   temp2, 0
   sbr    temp1, (1 << MOSI_Line)
   out    PORT_SPI, temp1
   ret

2) Для передачи старшим битом вперёд при CPHA=0

Shift_&_Set_MOSI:
   rcall  Shift_Left
   in     temp1, PORT_SPI
   cbr    temp1, (1 << MOSI_Line)
   mov    XL, Hi_byte_Address
   ld     temp2, X
   sbrc   temp2, 7
   sbr    temp1, (1 << MOSI_Line)
   out    PORT_SPI, temp1
   ret

3) Для передачи младшим битом вперёд при CPHA=1

Shift_&_Set_MOSI:
   in     temp1, PORT_SPI
   cbr    temp1, (1 << MOSI_Line)
   mov    XL, Low_byte_Address
   ld     temp2, X
   sbrc   temp2, 0
   sbr    temp1, (1 << MOSI_Line)
   out    PORT_SPI, temp1
   rcall  Shift_Right
   ret

4) Для передачи старшим битом вперёд при CPHA=1

Shift_&_Set_MOSI:
   in     temp1, PORT_SPI
   cbr    temp1, (1 << MOSI_Line)
   mov    XL, Hi_byte_Address
   ld     temp2, X
   sbrc   temp2, 7
   sbr    temp1, (1 << MOSI_Line)
   out    PORT_SPI, temp1
   rcall  Shift_Left
   ret

Хотелось бы обратить внимание вот на что: поскольку при CPHA=0 по первому фронту на шине тактирования происходит чтение, то первый передаваемый бит в этот момент уже должен быть установлен на шине. Лучше устанавливать его сразу при загрузке передаваемых данных в регистр или это можно делать, например, по сигналу SS, в любом случае — он должен быть установлен ещё до начала тактирования. Для CPHA=1 никакие вспомогательные действия до начала тактирования не нужны.

И ещё одно. После обмена пакетами с помощью приведённых выше процедур изменится граница выравнивания данных. То есть, если посылаемые данные были выровнены к младшему биту младшего байта, то принятые будут выровнены к старшему биту старшего байта, а если посылаемые данные были выровнены к старшему биту старшего байта, то принятые будут выровнены к младшему биту младшего байта.

Далее давайте подумаем как управлять линией SCLK и какие при этом надо решить задачи.

Во-первых, перед началом передачи (а точнее как только сконфигурировали SPI), нужно установить на линии SCLK уровень, соответствующий выбранной полярности (CPOL).

Во-вторых, нужно как-то определять моменты переключения линии SCKL. Очевидно, что частота этих переключений определяет скорость передачи.

Надо сказать, что вообще-то интерфейс SPI не предъявляет каких-либо требований к стабильности частоты импульсов на SCLK, поэтому переключения можно формировать когда угодно, например, как выполнили все действия по установке, чтению и сдвигу данных — так и переключайте себе SCLK. Понижать скорость можно добавлением пустых циклов перед каждым переключением. Это первый метод для управления шиной SCLK.

Второй метод заключается в том, чтобы использовать для отсчёта моментов переключения таймеры. Тут вообще красота, — очень легко можно сделать переключения со стабильной частотой, которая полностью будет определяться параметрами таймера. Алгоритм примерно такой: как только выполнили все подготовительные действия — запускаете таймер, а далее по прерыванию от таймера инвертируете линию SCLK, сбрасываете и перезапускаете таймер.

Чтобы знать когда остановиться (когда передали все биты) — специальным счётчиком нужно отсчитывать количество переданных бит или количество переключений линии SCLK. Естественно лучше считать количество бит, поскольку переключений линии SCLK будет в два раза больше.

Код никакой писать не буду, — тут всё просто (если что — на форум).

Ну и последнее, что нам осталось — управлять линией SS. Естественно, что моменты, когда нужно начинать и заканчивать сеанс — это полностью ваше личное дело, специфичное для каждой конкретной задачи, так что вам и флаг в руки. Скажу только, что обычно с точки зрения мастера есть два варианта управления этой линией:

  1. решение о переключении линии SS принимается независимо ни от чего (ручное управление)
  2. линия SS переключается в низкий уровень автоматически при загрузке новых данных в сдвиговый регистр, а назад в высокий — также автоматически после
    передачи всех бит пакета (автоматическое управление).

И ещё одно. Все приведённые выше коды предполагают, что используются пушпульные выходы. Если выходы предполагается сделать с открытым стоком, то нужно записать в соответствующие защёлки нули, и работать не с регистром порта, а с регистром выбора направления работы порта (тогда сигналы, формируемые контроллером на соответствующих выводах, будут меняться не между нулём и единицей, а между нулём и Z-состоянием).

Вот здесь можно посмотреть пример использования описанных функций.

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