Программа для контроллера SPI-шлюза (режим SPI-master из терминалки ПК)

Программа, рассмотренная в этой статье, разработана для контроллера SPI-шлюза (шлюз у нас реализован на ATTiny2313). Эта программа позволяет из терминальной программы персонального компьютера общаться в качестве Master-а с различными SPI устройствами. В программе реализованы все 4 режима SPI, размер пакета может быть установлен от 1 до 64 бит, возможен выбор передачи пакета младшим или старшим битом вперёд, а также можно выбрать ручное или автоматическое управление линией SS. Написана программа полностью на ассемблере, в конце статьи выложены исходники (с комментариями) и прошивка.

Для реализации обмена данными по SPI между контроллером и подключаемым устройством были использованы стандартные, написанные нами ранее процедуры.

Протокол обмена с компьютером был придуман на ходу и состоит из сообщений, размером в 1 или несколько байт.

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

Команды используются такие:

  1. 01h — загрузка конфигурации в шлюз. После принятия такой команды шлюз ждёт два байта конфигурации, которые определяют режим SPI, размер пакета, режим управления линией SS, порядок передачи бит (младшим или старшим вперёд) и скорость передачи. В шлюзе конфигурация хранится в двух специально выделенных регистрах: C1 и C2. Здесь нужно пожалуй добавить, что максимальная скорость передачи по SPI получилась где-то в районе 75 КБит/с, что довольно медленно для SPI и позволяет работать с любым устройством. Поскольку в конфигурации по умолчанию как раз заложена эта максимальная скорость, то менять её в общем-то особого смысла нет.
  2. 02h — считывание конфигурации из шлюза. После принятия такой команды шлюз отсылает на компьютер два байта конфигурации (С1, С2).
  3. 03h — загрузить пакет в шлюз. После принятия такой команды шлюз ждёт N байт пакета. N зависит от размера пакета, установленного в конфигурации шлюза и определяется по формуле N=(K-1)/8+1, где К — размер пакета в битах.
  4. 04h — считать пакет из шлюза. После принятия этой команды шлюз отсылает на компьютер N используемых байт сдвигового регистра.
  5. 05h — переключить SS в единицу. Если в конфигурации установлено ручное управление линией SS, то после принятия этой команды шлюз переключает линию SS в единицу.
  6. 06h — переключить SS в ноль. Если в конфигурации установлено ручное управление линией SS, то после принятия этой команды шлюз переключает линию SS в ноль.
  7. 07h — старт обмена. После принятия этой команды шлюз выполняет обмен пакетами с подключенным к нему по SPI устройством, в соответствии с установленной конфигурацией (режим SPI, размер пакета и прочее).

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

Служебные байты могут быть такими:

  1. 01h — предыдущая команда выполнена успешно.
  2. FFh — шлюзом была получена неизвестная команда.
  3. FEh — попытка установить размер пакета равным нулю (то есть получены неправильные байты конфигурации).
  4. FDh — попытка передачи, при высоком уровне на линии SS (такая ошибка возникает, если послать шлюзу команду 07h при ручном управлении линией SS, когда эта линия установлена в 1).

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

Прога написана таким образом, что при загрузке пакета из компьютера в шлюз, сначала надо отправлять младшие байты пакета, а потом старшие (т.е. если пакет имеет вид «2Ah 3Bh 4Eh», то шлюзу надо сначала отправлять 4Eh, потом 3Bh, а потом 2Ah).

Из интересных фишек в реализации отмечу ещё вот какую. В зависимости от режима SPI и порядка передачи бит, чтение бита с шины и сдвиг могут происходить по разному (у нас есть 2 процедуры для чтения и 4 для сдвига) и в разном порядке. Чтобы в процессе обмена не выяснять каждый раз заново, какую именно процедуру нужно выполнять (это бы очень сильно снизило скорость работы) — мы делаем это только один раз, на этапе конфигурирования, а потом записываем в 4 специальных регистра (Proc1_Address_L, Proc1_Address_H, Proc2_Address_L, Proc2_Address_H) адреса процедур, нужных нам в данной конфигурации для чтения и сдвига, причём если сначала выполняется чтение, а потом сдвиг, то мы в первые два регистра пишем адрес процедуры чтения, а во вторые два — адрес процедуры сдвига, если сначала нам нужен сдвиг, потом чтение, то наоборот, — в первые два регистра пишем адрес процедуры сдвига, а во вторые два — адрес процедуры чтения. Всё, теперь непосредственно при обмене нам вообще не нужно задумываться о том, что и как делать, — нужно просто по чётным фронтам вызывать с помощью косвенной адресации процедуру по первому сохранённому адресу, а по нечётным — процедуру по второму сохранённому адресу.

Вот пожалуй и всё описание (если возникнут вопросы — добро пожаловать на форум), а теперь перейдём к алгоритму и программе.

Алгоритм:

Алгоритм работы SPI-шлюза

Итак, в аппаратной части мы имеем:

  1. PB0 — линия SCLK
  2. PB1 — линия MISO
  3. PB2 — линия MOSI
  4. PD5 — линия SS
  5. PD0 — линия Rx
  6. PD1 — линия Tx
Текст программы под катом

.device ATtiny2313
.include "tn2313def.inc"
.list
;-- определяем свои переменные
.def   Low_byte_Address = r0 ; здесь мы храним адрес младшего байта регистра
.def   Hi_byte_Address  = r1 ; здесь мы храним адрес старшего байта регистра
.def   Byte_Number      = r2 ; здесь храним размер сдвигового регистра
.def   C1               = r3 ; первый байт конфигурации
;(старшие 2 бита - режим SPI, младшие 6 бит - размер пакета)
.def   C2               = r4 ; второй байт конфигурации
;(старшие 2 бита - режим управления линией CS (0-man, 1-aut),
; порядок передачи битов (0-L, 1-H), младшие 6 бит - скорость)
.def   SR_Offset        = r5 ; смещение для выравнивания вправо/влево
.def   Razmer_bit       = r6 ; размер регистра в битах
.def   Timer_Set        = r7 ; значение для регистра таймера (скорость)
.def   Proc1_Address_L  = r8 ; младший байт адреса первой процедуры
.def   Proc1_Address_H  = r9 ; старший байт адреса первой процедуры
.def   Proc2_Address_L  = r10; младший байт адреса второй процедуры
.def   Proc2_Address_H  = r11; младший байт адреса второй процедуры
;--------------------------------------
.def   REG_0=r16        ; начало сдвигового регистра (10h)
.def   REG_1=r17
.def   REG_2=r18
.def   REG_3=r19
.def   REG_4=r20
.def   REG_5=r21
.def   REG_6=r22
.def   REG_7=r23        ; конец сдвигового регистра (17h)
;-------------------------------------
.def   Front_Counter=r24  ; счётчик фронтов SCLK
.def   Byte_Counter=r25 ; счётчик байт
;регистры YH=w, YL, ZH=temp1, ZL=temp2
;-------------------------------------
.def   w=r29
.def   temp1=r31
.def   temp2=r30
;-- Используемые порты ---------------
.equ   PORT_SPI=0x18  ; порт B - выходы
.equ   PIN_SPI =0x16  ; порт B - входы
;-- определяем названия выводов ------
.equ     SCLK_Line=0 ; PortB0/PinB0 - clock
.equ     MISO_Line=1 ; PortB1/PinB1 - вход мастера
.equ     MOSI_Line=2 ; PortB2/PinB2 - выход мастера
.equ     CS_Line=5   ; PinD5 - выход Chip Select
; кроме того, мы используем линии Rx (PD0), Tx (PD1)
;-------------------------------------
;-- начало программного кода
        .cseg
        .org 0
        rjmp Init    ; переход на начало программы (вектор сброса)
;-- дальше идут вектора прерываний
;-- если не используем - reti, иначе - переход на начало обработчика
        reti         ; внешнее прерывание INT0
	reti         ; внешнее прерывание INT1
	reti         ; Input capture interrupt 1
	reti         ; Timer/Counter1 Compare Match A
	reti         ; Overflow1 Interrupt
	reti         ; Overflow0 Interrupt
	rjmp RX_INT  ; USART0 RX Complete Interrupt
	reti         ; USART0 Data Register Empty Interrupt
	reti         ; USART0 TX Complete Interrupt
	reti         ; Analog Comparator Interrupt
	reti         ; Pin Change Interrupt
	reti         ; Timer/Counter1 Compare Match B
	rjmp T0_INT  ; Timer/Counter0 Compare Match A
	reti         ; Timer/Counter0 Compare Match B
	reti         ; USI start interrupt
	reti         ; USI overflow interrupt
	reti         ; EEPROM write complete
	reti         ; Watchdog Timer Interrupt
;-- начало программы
Init:   ldi w,RAMEND    ; устанавливаем указатель вершины
        out SPL,w       ; стека на старший байт RAM
        sbi ACSR,ACD    ; выключаем компаратор
	;-- инициализируем порты
        ser w           ;    w=0xFF
        out DDRA,w      ; настраиваем порт A. все линии на выход
	clr w           ;    w=0x00
	out PORTA,w     ; на всех линиях ноль
        ldi w,0b11111101; настраиваем порт B. PB1 - вход
        out DDRB,w
	clr w           ; определяем нач.состояние выходов и подт.на входах
	out PORTB,w     ; (выходы - нули, подтяжек нет)
	ldi w,0b11111100; настраиваем порт D
        out DDRD,w      ; PD0, PD1 - входы
	clr w           ; определяем нач.состояние выходов и подт.на входах
	out PORTD,w     ; (выходы - нули, подтяжек нет)
        ;-- инициализируем UART
        out UBRRH,w     ; UBRRR (для кварца 20МГц и скорости 115200)
	ldi w,10        ; равен 10, т.е. UBRRH=0, UBRRL=10
	out UBRRL,w
	ldi w,0b00001110; поднимаем биты USBS, UCSZ1:0
	out UCSRC,w     ; формат: 1 старт, 8 данные, 2 стоп
	sbi UCSRB,TXEN  ; включить передатчик
	nop
	sbi UCSRB,RXEN  ; включить приёмник
	;-- разрешаем прерывание от приёмника
	sbi UCSRB,RXCIE ; включить прерывания от приёмника
        ;-- Загружаем начальную конфигурацию
	ldi w,0b00001000; Mode0, пакет 8 бит
	mov C1,w
	ldi w,0b00010010; CS-man, младшим вперёд, скорость 18 (около 75 кГц)
	mov C2,w
        rcall Interface_config
	;-- сообщаем компу, что загрузились и ждём команду
        ldi w,0x01
        out UDR,w
	;-- разрешаем глобальные прерывания
        sei
        ;-- ждём у моря погоды ---
Wait_data:
	rjmp Wait_data
 
;---------------------------------------------------------
;--- Обработчик прерывания от таймера ---
T0_INT:
    ;-- генерим фронт (инвертируем clock)
    in  w,PORT_SPI
    ldi temp1,(1<<SCLK_Line)
    eor w,temp1          ; инвертируем clock
    out PORT_SPI,w
    ;-- выполняем действия в зависимости от конфига
    sbrc Front_Counter,0 ; если чётный фронт (считаем обратно), то
    rjmp Proc2          ; выполняем функцию 1, иначе 2
Proc1:
    mov   ZH,Proc1_Address_H
    mov   ZL,Proc1_Address_L
    icall 
    rjmp  Next_T0_INT
Proc2:
    mov   ZH,Proc2_Address_H
    mov   ZL,Proc2_Address_L
    icall
    ;-- уменьшить и проверить счётчик
Next_T0_INT:
    dec  Front_Counter
    breq End_Transfer
    reti
    ;-- Конец передачи
End_Transfer:
    clr w
    out TCCR0B,w      ; выключаем предделитель
    ;-- выравнивание
    sbrc  C2,6        ; если передавали старшим битом вперёд - пропускаем !!! - вот тут ошибка
    rcall Offset_Low
    ;---------------
    in w,UDR          ; читаем порт (чтоб сбросить флаг приёма, если что)
    sbi UCSRB,RXCIE   ; включаем прерывания от приёмника
    sbrc  C2,7        ; если управление линией CS автоматическое, то
    rjmp  Set_CS_H    ; выполняем эту команду
    ldi w,0x01        ; собщаем компу, что всё сделали
    out UDR,w
    reti              ; и выходим
;------------------------------------------
;--- Обработчик прерывания от приёмника ---
RX_INT:
    in   w,UDR           ; читаем байт из приёмника в w
    cpi  w,0x01          ; принятый байт = 1?
    breq Config_Recieve  ; принимаем конфигурацию с компа
    cpi  w,0x02          ; принятый байт = 2?
    breq Config_Transmit ; отправляем конфигурацию на комп
    cpi  w,0x03          ; принятый байт = 3?
    breq Paket_Upload    ; загружаем пакет с компа
    cpi  w,0x04          ; принятый байт = 4?
    breq Paket_Download  ; выгружаем пакет на комп
    cpi  w,0x05          ; принятый байт = 5?
    breq Set_CS_H        ; CS=1
    cpi  w,0x06          ; принятый байт = 6?
    breq Set_CS_L        ; CS=0
    cpi  w,0x07          ; принятый байт = 7?
    breq SPI_Start       ; производим сеанс обмена
    ;-- Сообщаем, что приняли неизвестную команду и выходим
Error_Command:
    ldi  w,0xFF
    out  UDR,w
    reti
;--- Говорим компу, что всё сделали и ждём ещё команд
Send_Ok:
    ldi w,0x01
    out UDR,w
    ret
;--- ОБРАБОТЧИКИ КОМАНД --------------------------
;--------------------------------------------------
;-- принимаем конфигурацию (два байта от компа) ---
Config_Recieve:
Wait_First_Byte:
    sbis UCSRA,RXC  ; ждём первый байт от компа
    rjmp Wait_First_Byte
    in   C1,UDR     ; читаем принятый байт в C1
Wait_Second_Byte:
    sbis UCSRA,RXC  ; ждём второй байт от компа
    rjmp Wait_Second_Byte
    in   C2,UDR     ; читаем принятый байт в C2
    ;-- конфигурим интерфейс
    rcall Interface_Config
    ;----------------
    rcall Send_Ok   ; собщаем компу, что всё сделали
    ;----------------
    reti            ; выходим
;--------------------------------------------------
;-- отсылаем компу конфиг
Config_Transmit:
    out  UDR,C1
Wait_Send_C1:
    sbis UCSRA,UDRE
    rjmp Wait_Send_C1
    out  UDR,C2 
Wait_Send_C2:
    sbis UCSRA,UDRE
    rjmp Wait_Send_C2
    ;----------------
    rcall Send_Ok   ; собщаем компу, что всё сделали
    ;----------------
    reti            ; выходим
;--------------------------------------------------
;-- Загружаем пакет с компа ---
Paket_Upload:
    clr  w
    mov  XL,Low_byte_Address
Wait_Paket_Byte:
    sbis UCSRA,RXC   ; ждём первый байт от компа
    rjmp Wait_Paket_Byte
    in   temp1,UDR   ; читаем принятый байт в temp1
    st   X,temp1     ; сохраняем по адресу, записанному в X
    dec  XL          ; переводим указатель на следующий байт
    subi w,0xF8      ; вычитание F8 равносильно прибавлению восьми
    cp   w,Razmer_bit
    brge End_of_Paket; если w>= кол-ва бит в пакете - приём пакета окончен
    rjmp Wait_Paket_Byte
End_of_Paket:
    ;-- если нужно, то выравниваем
    sbrc C2,6        ; если передаём младшим битом вперёд (6-й бит C2=0),
    rcall Offset_Hi  ; то пропускаем эту команду
    ;-- если нужно - сразу устанавливаем первый бит на выходе
    sbrs C1,6        ; если CPHA=1 - пропускаем команду
    rcall Set_MOSI
    sbrc  C2,7       ; если управление линией CS автоматическое, то
    rjmp  Set_CS_L   ; выполняем эту команду
    ;-------------
    rcall Send_Ok
    ;-------------
    reti
;--------------------------------------------------
;-- Выгружаем пакет на комп ---
Paket_Download:
    mov  w,Byte_Number
    mov  XL,Low_byte_Address
Download_next_byte:
    ld   temp1,X
    out  UDR,temp1
    dec  w
    breq Download_finished
Wait_Send_Byte:
    sbis UCSRA,UDRE
    rjmp Wait_Send_Byte
    rjmp Download_next_byte
Download_finished:
    ;----------------
    rcall Send_Ok   ; собщаем компу, что всё сделали
    ;----------------
    reti            ; выходим
;--------------------------------------------------
;-- Поднимаем CS
Set_CS_H:
    sbi PORTD, CS_Line
    rcall Send_Ok   ; собщаем компу, что всё сделали
    reti
;--------------------------------------------------
;-- Роняем CS и если она в автоматическом режиме - начинаем передачу
Set_CS_L:
    cbi PORTD, CS_Line
    sbrc  C2,7       ; если управление линией CS автоматическое, то
    rjmp  SPI_Start  ; выполняем эту команду
    rcall Send_Ok    ; собщаем компу, что всё сделали
    reti
;--------------------------------------------------
;-- Начинаем передавать содержимое регистра
SPI_Start:
    ;-- если CS=1, то ошибка
    sbic PORTD, CS_Line
    rjmp Error_CS
    ;-----------------------
    cbi UCSRB,RXCIE             ; выключаем прерывания от приёмника
    mov Front_Counter, Razmer_bit ; инициализируем счётчик
    lsl Front_Counter             ; умножаем на 2 (количество фронтов)
    ;-- Сбрасываем таймер, его флаги и включаем прерывание от таймера
    clr w
    out TCNT0,w       ; сбрасываем таймер
    ser w
    out TIFR,w        ; сбрасываем флаги таймера
    ldi w,0b00000010
    out TCCR0B,w      ; включаем предделитель
    ldi w,0b00000001  ; разрешаем прерывание по сравнению
    out TIMSK,w
    reti
Error_CS:
    ldi w,0xFD
    out UDR,w
    reti
;---------------------------------------------------------
;--- ПРОЦЕДУРЫ SPI ---------------------------------------
;-- Конфигурирование интерфейса
Interface_Config:
    ;-- определяем размер регистра
    mov  w,C1
    andi w,0b00111111    ; отрезаем два старших бита
    breq Error_Size      ; если размер=0 - это ошибка
    mov  Razmer_bit,w    ; записываем размер регистра в битах
    ;-- определяем кол-во используемых байт
    dec  w             ; вычитаем 1
    lsr  w             ; 3 раза делим на 2
    lsr  w             ; (получается деление на 8)
    lsr  w
    inc  w             ; и прибавляем 1
    mov  Byte_Number,w ; сохраняем кол-во используемых байт
    ;-- определяем величину сдвига для выравнивания
    mov  w,Razmer_bit
    subi w,64            ; вычитаем 64 (получается N-64)
    mov  SR_Offset,w     ; сохраняем вычисленное значение в регистре
    ;-- записываем адреса младшего и старшего байтов
    ldi  w,0x17             ; адрес REG_7
    mov  Low_byte_Address,w
    ldi  w,0x10             ; адрес REG_0
    mov  Hi_byte_Address,w
    ;-- настраиваем линию SCLK
    sbrc C1,7        ; если CPOL=0 - пропускаем следующую команду
    sbi  PORT_SPI, SCLK_Line  ; устанавливаем линию clock в единицу
    sbrs C1,7        ; если CPOL=1 - пропускаем следующую команду
    cbi  PORT_SPI, SCLK_Line  ; устанавливаем линию clock в ноль
    ;-- устанавливаем скорость и настраиваем таймер
    mov  w,C2
    andi w,0b00111111 ; отрезаем два старших бита
    inc  w
    out  OCR0A,w      ; загружаем значение для сравнения    
    ldi  w,0b00000010 ; таймер отключен от выводов OC0A(B),
    out  TCCR0A,w     ; сброс при совпадении
    ;-- определяем смещение адресов процедур, которые будут использоваться
    ;-- относительно метки Get_Address
    ;-- CPHA ? --
    sbrc  C1,6        ; если CPHA=0 - пропускаем команду
    rjmp  CPHA1
CPHA0:
    ;-- старшим или младшим вперёд
    sbrc  C2,6        ; если С2[6]=0, то младшим вперёд
    rjmp  CPHA0_H
CPHA0_L:              ; CPHA=0, младшим вперёд
    ldi   temp1,11     ; ссылка на Read_MISO_L0H1
    ldi   temp2,25    ; ссылка на Shift_i_Set_MOSI_L0
    rjmp  Next_Int_Config
CPHA0_H:              ; CPHA=0, старшим вперёд
    ldi   temp1,18    ; ссылка на Read_MISO_H0L1
    ldi   temp2,28    ; ссылка на Shift_i_Set_MOSI_H0
    rjmp  Next_Int_Config
CPHA1:
    ;-- старшим или младшим вперёд
    sbrc  C2,6        ; если С2[6]=0, то младшим вперёд
    rjmp  CPHA1_H
CPHA1_L:              ; CPHA=1, младшим вперёд
    ldi   temp1,31    ; ссылка на Shift_i_Set_MOSI_L1
    ldi   temp2,18    ; ссылка на Read_MISO_H0L1
    rjmp  Next_Int_Config
CPHA1_H:              ; CPHA=1, старшим вперёд
    ldi   temp1,34    ; ссылка на Shift_i_Set_MOSI_H1
    ldi   temp2,11    ; ссылка на Read_MISO_L0H1
Next_Int_Config:
    rcall Save_Proc_Address
    ret
Error_Size:
    ldi  w,0xFE      ; сообщаем компу об ошибке
    out  UDR,w
    ret
;-----------------------------------------------
;-- Выравнивание к старшему биту старшего байта
Offset_Hi:
    mov   w, SR_Offset
    tst   w
Next_Offset_Hi:
    breq  End_Offset_Hi
    rcall Shift_Left
    inc   w
    rjmp  Next_Offset_Hi ; команда флаги не меняет, tst не нужно
End_Offset_Hi:
    ret
;-- Выравнивание к младшему биту младшего байта
Offset_Low:
    mov   w, SR_Offset
    tst   w
Next_Offset_Low:
    breq  End_Offset_Low
    rcall Shift_Right
    inc   w
    rjmp  Next_Offset_Low
End_Offset_Low:
    ret
;-----------------------------------------------
;/////////////////////////////////////////////////////////
;-- Если в этом блоке что-то изменить, то адреса сдвинутся
;/////////////////////////////////////////////////////////
;-- Вычисляем адреса команд, которые будем вызывать
Save_Proc_Address:
   rcall Get_Address
Get_Address:
   pop Proc1_Address_H
   pop Proc1_Address_L                  ;адрес предыдущей команды
   mov Proc2_Address_H,Proc1_Address_H
   mov Proc2_Address_L,Proc1_Address_L  ; копируем его
   ;------------------------
   add Proc1_Address_L,temp1
   brcc No_inc_H1
   inc Proc1_Address_H
No_inc_H1:
   ;------------------------
   add Proc2_Address_L,temp2
   brcc No_inc_H2
   inc Proc2_Address_H
No_inc_H2:
   ret
;-- Для передачи младшим битом вперёд при CPHA=0 и старшим битом вперёд
;-- при CPHA=1
Read_MISO_L0H1:                 ; +11
   mov   XL,Low_byte_Address
   ld    temp1, X
   cbr   temp1, 0b00000001
   sbic  PIN_SPI, MISO_Line
   sbr   temp1, 0b00000001
   st    X, temp1
   ret
;-- Для передачи старшим битом вперёд при CPHA=0 и младшим битом вперёд
;-- при CPHA=1
Read_MISO_H0L1:                 ; +18
   mov   XL,Hi_byte_Address
   ld    temp1, X
   cbr   temp1, 0b10000000
   sbic  PIN_SPI, MISO_Line
   sbr   temp1, 0b10000000
   st    X, temp1
   ret
;-----------------------------------------------
;-- Для передачи младшим битом вперёд при CPHA=0
Shift_i_Set_MOSI_L0:           ; +25
   rcall  Shift_Right
   rcall  Set_MOSI
   ret
;-- Для передачи при старшим битом вперёд CPHA=0
Shift_i_Set_MOSI_H0:           ; +28
   rcall  Shift_Left
   rcall  Set_MOSI
   ret
;-- Для передачи младшим битом вперёд при CPHA=1
Shift_i_Set_MOSI_L1:           ; +31
   rcall  Set_MOSI
   rcall  Shift_Right
   ret
;-- Для передачи старшим битом вперёд при CPHA=1
Shift_i_Set_MOSI_H1:           ; +34
   rcall  Set_MOSI
   rcall  Shift_Left
   ret
;//////////////////////////////////////////////////////////
;------------------------------------------------
;-- Установка MOSI
Set_MOSI:
   in     temp1, PORT_SPI
   cbr    temp1, (1 << MOSI_Line)
   sbrc   C2,6    ; если передаём младшим битом вперёд - пропускаем команду
   rjmp   First_HI
First_Low:        ; при передаче младшим битом вперёд
   mov    XL, Low_byte_Address
   ld     temp2, X
   sbrc   temp2, 0
   sbr    temp1, (1 << MOSI_Line)
   rjmp   End_Set_MOSI
First_Hi:        ; при передаче старшим битом вперёд
   mov    XL, Hi_byte_Address
   ld     temp2, X
   sbrc   temp2, 7
   sbr    temp1, (1 << MOSI_Line)
End_Set_MOSI:
   out    PORT_SPI, temp1
   ret
;-----------------------------
;-- сдвиг регистра вправо ----
Shift_Right:
   mov   XL, Hi_byte_Address
   ldi   Byte_Counter, 8
   clc
Next_Shift_Right:
   ld    temp1, X
   ror   temp1
   st    X, temp1
   inc   XL
   dec   Byte_Counter
   brne  Next_Shift_Right
   brcc  Shift_Right_Exit
   mov   XL, Hi_byte_Address
   ld    temp1, X
   sbr   temp1, 0b10000000
   st    X, temp1
Shift_Right_Exit:
   ret
;-- сдвиг регистра влево ----
Shift_Left:
   mov   XL, Low_byte_Address
   ldi   Byte_Counter, 8
   clc
Next_Shift_Left:
   ld    temp1, X
   rol   temp1
   st    X, temp1
   dec   XL
   dec   Byte_Counter
   brne  Next_Shift_Left
   brcc  Shift_Left_Exit
   mov   XL, Low_byte_Address
   ld    temp1, X
   sbr   temp1, 0b00000001
   st    X, temp1
Shift_Left_Exit:
   ret
;--- КОНЕЦ ПРОЦЕДУР SPI -------------------------------

[свернуть]

Для правильной работы шлюза в контроллере должны быть «запрограммированы» следующие фьюзы: SPIEN, SUT0

Скачать готовую прошивку и asm-файл

Приведу небольшой пример работы со шлюзом:

Пусть у нас есть драйвер семисегментных индикаторов max7219. Из даташита мы узнаём, что драйвером этим можно рулить по SPI в режиме Mode0, при этом размер пакета должен быть 16 бит и биты при обмене передаются старшим вперёд.

Итак, заходим в терминалку, выбираем порт, скорость обмена (скорость у нас 115200), подключаемся и делаем следующее:

— отправляем шлюзу 05h // (установить CS=1)
— получаем от шлюза 01h // он сообщает, что установил CS=1
— отправляем шлюзу 01h // говорим шлюзу, что будем слать конфигурацию
— отправляем шлюзу 10h D2h // Mode0, 16 бит, автоматическое управление SS, передача старшим вперёд, скорость «18»
— получаем от шлюза 01h // шлюз сообщает, что загрузил конфигурацию
— отправляем шлюзу 03h // говорим шлюзу, что будем слать пакет (поскольку выбрано автоматическое управление SS, то принятый пакет шлюз сразу же и отправит)
— отправляем шлюзу 01h 0Сh // пакет для max-а: «0Ch 01h» (выход из режима shutdown)
— получаем от шлюза 01h // шлюз сообщает, что всё успешно отправил
— отправляем шлюзу 03h // говорим шлюзу, что будем слать пакет
— отправляем шлюзу 07h 0Bh // пакет для max-а: «0Bh 07h» (scan limit=7, отображаем 7 символов)
— получаем от шлюза 01h // шлюз сообщает, что всё успешно отправил
— отправляем шлюзу 03h // говорим шлюзу, что будем слать пакет
— отправляем шлюзу FFh 09h // пакет для max-а: «09h FFh» (для всех символов режим декодирования B)
— получаем от шлюза 01h // шлюз сообщает, что всё успешно отправил
— отправляем шлюзу 03h // говорим шлюзу, что будем слать пакет
— отправляем шлюзу 02h 0Bh // пакет для max-а: «0Bh 02h» (отобразить на втором индикаторе символ «E»)
— получаем от шлюза 01h // шлюз сообщает, что всё успешно отправил

В результате этих действий на втором семисегментном индикаторе, подключенном к нашему max7219, будет отображаться символ «E».

P.S. Гы-гы-гы, в проге обнаружилась ошибка. После метки End_Transfer, вот в этой строке:

    sbrc  C2,6        ; если передавали старшим битом вперёд - пропускаем !!! - вот тут ошибка

Самое интересное, что комментарий написан правильно, а команда стоит неправильная, должно быть sbrs вместо sbrc. Исправлять и перекомпилировать не стал, оставил возможность вам самим это сделать.

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

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