§ Принцип работы SPI

Этот протокол много где может использоваться, в разных модификациях, например, по SPI можно передавать не только по одному биту данных за 1 такт, а по 4 сразу, однако, передачу по 4 бита данных я использовать не буду, только 1 бит.
Протокол предполагает что есть как минимум 3 провода:
  • MOSI - входящие данные из ведущего контроллера (master)
  • MISO - исходящие данные из ведомого контроллера (slave)
  • SCLK - тактовая частота от ведущего контроллера
Принцип действия довольно простой. Ведущий контроллер устанавливает SCLK в 0 и 1, при этом, когда SCLK установлен в 0, ставится сначала старший бит передаваемого байта, после чего меняется на 1, устройство принимает данные и генерирует старший бит ответа, то есть, когда идет передача данных на устройство, само устройство тоже генерирует ответ на каждом SCLK=1.

§ Порты

Любой модуль начинается с объявления портов ввода-вывода.
module sd
(
    // 25 Mhz
    input               clock,

    // Интерфейс с SPI
    output reg          SPI_CS,
    output reg          SPI_SCLK,
    input               SPI_MISO,
    output reg          SPI_MOSI,

    // Интерфейс
    input               sd_signal,  // =1 Прием команды
    input       [ 1:0]  sd_cmd,     // ID команды
    output reg  [ 7:0]  sd_din,     // Исходящие данные в процессор
    input       [ 7:0]  sd_out,     // Входящие данные из процессора
    output reg          sd_busy,    // =1 Устройство занято
    output              sd_timeout  // =1 Вышел таймаут
);

endmodule
Модуль тактируется 25 мгц генератором. На входе в модуль приходят данные от SPI_MISO, исходят SPI_MOSI, SPI_SCLK и SPI_CS. Сигнал CS (Chip Enabled), если установлен в 1, то ведомое устройство считается неактивным и он не должен принимать сигналы SPI_MOSI и SPI_SCLK. При CS=0, устройство (slave) активируется.
Теперь опишу порты той части, которая управляется микроконтроллером хоста.
  • sd_signal — при наличии сигнала =1, модуль начинает выполнение одну из 4-х функции, заданной в sd_cmd
  • sd_din — данные, которые принимаются от ведомого устройства после чтения байта
  • sd_out — данные, которые отсылаются от хост-контроллера к ведомому контроллеру
  • sd_busy — при 1, устройство занято выполнением команды
  • sd_timeout — сигнал 1 говорит о том, что модуль слишком долго не отсылал команды, в данном случае, 0.1 секунды (параметр SPI_TIMEOUT_CNT)
Назначим определения начальных значений портов.
`define SPI_TIMEOUT_CNT     2500000

initial begin

    SPI_CS   = 1'b1;
    SPI_SCLK = 1'b0;
    SPI_MOSI = 1'b0;
    sd_din   = 8'h00;
    sd_busy  = 1'b0;

end
О том, занято ли устройство, делаем вывод по такому сравнению:
assign sd_timeout = (sd_timeout_cnt == `SPI_TIMEOUT_CNT);
Необходим объявить используемые в модуле регистры.
reg  [24:0] sd_timeout_cnt = `SPI_TIMEOUT_CNT;
reg  [2:0]  spi_process = 0;
reg  [3:0]  spi_cycle   = 0;
reg  [7:0]  spi_data_w  = 0;
reg  [7:0]  spi_counter   = 0;
reg  [7:0]  spi_slow_tick = 0;
Объяснение назначения регистров.
  • sd_timeout_cnt — счетчик таймаута, изначально устанавливается в максимальный, чтобы таймаут уже был определен как =1
  • spi_process — номер функции, которая в данный момент исполняется (от 1 до 4), 0 — IDLE, устройство находится в режиме ожидания
  • spi_cycle — подфункция, или текущая строка исполнения конкретной функции
  • spi_data_w — временный сдвиговый регистр, откуда шлются данные на ведомое устройство
  • spi_counter — счетчик от 0 до 7, для приема и отсылки битов
  • spi_slow_tick — счетчик для замедления тактов до 10 кГц

§ Функции включения, выключения, IDLE и 10 кГц

Напишем основной обработчик:
always @(posedge clock) begin

    if (sd_timeout_cnt < `SPI_TIMEOUT_CNT && spi_process == 0)
        sd_timeout_cnt <= sd_timeout_cnt + 1;

end
Этот счетчик увеличивает sd_timeout_cnt на каждом такте на +1, но только тогда, когда функция находится в состоянии IDLE (spi_process=0)
Опишем основное состояние IDLE:
case (spi_process)

    // Ожидание команды
    0: if (sd_signal) begin

        spi_process <= 1 + sd_cmd;
        spi_counter <= 0;
        spi_cycle   <= 0;
        spi_data_w  <= sd_out;
        sd_busy     <= 1;
        sd_timeout_cnt <= 0;

    end

endcase
В состоянии IDLE модуль будет находится до тех пор, пока не придет sd_signal=1. При появлении этого сигнала, это означает, что ранее sd_cmd уже был установлен правильно. Когда это происходит, то все счетчики сбрасываются, в том числе и sd_timeout_cnt, а модулю устанавливается состояние sd_busy=1. В регистр spi_data_w переписывается текущее состояние sd_out, защелкивается и теперь даже если sd_out как-то внешне поменяется, это никак не повлияет на передачу данных.
Рассмотрю две команды, sd_cmd=2 (Chip Enabled=0) и sd_cmd=3 (Chip Enabled=1):
3, 4: begin SPI_CS <= ~spi_process[0]; spi_process <= 0; sd_busy <= 0; end
При sd_cmd=2, в SPI_CS попадет бит 0 (включено), при sd_cmd=3 будет бит 1 (отключено). Модулю установится состояние busy=0 и перейдет в состояние IDLE (spi_process=0).
Команда sd_cmd=0 (10KHZ TICK) уже немного посложнее:
1: begin

    SPI_CS   <= 1;
    SPI_MOSI <= 1;

    // 125*100`000
    if (spi_slow_tick == (125 - 1)) begin

        SPI_SCLK      <= ~SPI_SCLK;
        spi_slow_tick <= 0;
        spi_counter   <= spi_counter + 1;

        // 80 ticks
        if (spi_counter == (2*80 - 1)) begin

            SPI_SCLK    <= 0;
            spi_process <= 0;
            sd_busy     <= 0;

        end

    end
    // Оттикивание таймера
    else begin spi_slow_tick <= spi_slow_tick + 1;  end

end
Алгоритм работы будет такой. Устанавливаются стабильные сигналы 1 на CS и MOSI, и через каждые 125 тактов будет перещелкивать SPI_SCLK из одного состояния в другое. Поскольку скорость тактового генератора равна 2500000 Гц, то, учитывая, что за один герц можно считать два изменения состояния за 1 секунду, то тогда рассчитываем, сколько будет герц выдавать SPI_SCLK по итогу, разделив 2500000 на 125 и еще разделив на 2, что получается 10 кГц.
Для чего это все нужно? Дело в том, что при инициализации sd-карты в режим SPI, необходимо подать 80 тактов на частоте 10 кГц, потому тут есть два счетчика, тот, который снижает такты с 25 мгц до 10 кГц и тот, который отсчитывает 80 таких тактов spi_counter == (2*80 - 1).
Как только это произошло, модуль устанавливает SPI_SCLK в 0 для будущей передачи данных.

§ Прием и передача данных

И осталось разобрать последнюю функцию, которая принимает и передает данные. Эта функция sd_cmd=1.
// Command 1: Read/Write SPI
2: case (spi_cycle)

    // CLK-DN
    0: begin

        SPI_SCLK  <= 0;
        spi_cycle <= 1;
        SPI_MOSI  <= spi_data_w[7];

    end
    // CLK-UP
    1: begin

        SPI_SCLK    <= 1;
        spi_cycle   <= 0;
        sd_din      <= {sd_din[6:0], SPI_MISO};
        spi_data_w  <= {spi_data_w[6:0], 1'b0};
        spi_counter <= spi_counter + 1;

        if (spi_counter == 8) begin

            SPI_SCLK    <= 0;
            SPI_MOSI    <= 0;
            sd_busy     <= 0;
            spi_counter <= 0;
            spi_process <= 0;

        end
    end

endcase
Функция состоит из двух фаз.
  • spi_cycle=0, SPI_SCLK устанавливается в 0, и на SPI_MOSI устанавливает старший бит передаваемого байта от хост-контроллера к ведомому контроллеру
  • spi_cycle=1, SPI_SCLK в 1, при этом из SPI_MISO попадает старший бит данных, которые пришли от устройства. Регистр sd_din сдвигается влево, то есть, сначала в младший его бит попадает старший, и через 8 тактов, старший бит становится на свое место.
Когда было принято 8 бит (spi_counter == 8), то SPI_SCLK устанавливается в 0, процесс переходит в IDLE и на SPI_MOSI тоже ставится 0 в исходное состояние. Занятость модуля убирается, sd_busy=0, и все счетчики очищаются перед новой итерацией.
На этом все. Этот модуль не является конечным модулем в процессе взаимодействия с процессором, поскольку ему нужен еще один контроллер-прослойка для x86, но в целом, его можно использовать с другими процессорами, которые, например, пишут в порты напрямую.

14 мая, 2022
© 2007-2022 Лиственная клавиша шила