Оглавление
§ Принцип работы SPI
Этот протокол много где может использоваться, в разных модификациях, например, по SPI можно передавать не только по одному биту данных за 1 такт, а по 4 сразу, однако, передачу по 4 бита данных я использовать не буду, только 1 бит.
Протокол предполагает что есть как минимум 3 провода:
MOSI – входящие данные из ведущего контроллера (master)
MISO – исходящие данные из ведомого контроллера (slave)
SCLK – тактовая частота от ведущего контроллера
Принцип действия довольно простой. Ведущий контроллер устанавливает SCLK в 0 и 1, при этом, когда SCLK установлен в 0, ставится сначала старший бит передаваемого байта, после чего меняется на 1, устройство принимает данные и генерирует старший бит ответа, то есть, когда идет передача данных на устройство, само устройство тоже генерирует ответ на каждом SCLK=1.
§ Порты
Любой модуль начинается с объявления портов ввода-вывода.
module sd
(
input clock,
input reset_n,
output reg SPI_CS,
output reg SPI_SCLK,
input SPI_MISO,
output reg SPI_MOSI,
input sd_signal,
input [ 1:0] sd_cmd,
input [ 7:0] sd_out,
output reg [ 7:0] sd_din,
output reg sd_busy,
output sd_timeout
);
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)
О том, занято ли устройство, делаем вывод по такому сравнению:
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)
if (reset_n == 1'b0) begin
spi_process <= 0;
sd_timeout_cnt <= `SPI_TIMEOUT_CNT;
SPI_CS <= 1'b1;
SPI_SCLK <= 1'b0;
SPI_MOSI <= 1'b0;
sd_din <= 8'h00;
sd_busy <= 1'b0;
end else 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;
SPI_SCLK <= 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=1 (10KHZ TICK) уже немного посложнее:
2: begin
SPI_CS <= 1;
SPI_MOSI <= 1;
if (spi_slow_tick == (125 - 1)) begin
SPI_SCLK <= ~SPI_SCLK;
spi_slow_tick <= 0;
spi_counter <= spi_counter + 1;
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.
1: case (spi_cycle)
0: begin
SPI_SCLK <= 0;
spi_cycle <= 1;
SPI_MOSI <= spi_data_w[7];
spi_data_w <= {spi_data_w[6:0], 1'b0};
end
1: begin spi_cycle <= 2; SPI_SCLK <= 1; end
2: begin spi_cycle <= 3; sd_din <= {sd_din[6:0], SPI_MISO}; end
3: begin
SPI_SCLK <= 0;
spi_cycle <= 0;
spi_counter <= spi_counter + 1;
if (spi_counter == 7) begin
SPI_MOSI <= 0;
sd_busy <= 0;
spi_counter <= 0;
spi_process <= 0;
end
end
endcase
Функция состоит из двух 4 фаз.
spi_cycle=0, SPI_SCLK устанавливается в 0, и на SPI_MOSI устанавливает старший бит передаваемого байта от хост-контроллера к ведомому контроллеру, и тут же смещается влево
spi_cycle=1, SPI_SCLK в 1
spi_cycle=2, из SPI_MISO попадает старший бит данных, которые пришли от устройства
spi_cycle=3, регистр sd_din сдвигается влево, то есть, сначала в младший его бит попадает старший, и через 8 тактов, старший бит становится на свое место.
Когда было принято 8 бит (spi_counter == 7), то процесс переходит в IDLE и на SPI_MOSI тоже ставится 0 в исходное состояние. Занятость модуля убирается, sd_busy=0, и все счетчики очищаются перед новой итерацией.
Эффективная скорость модуля будет невысокой. Если он работает на частоте 25 мгц, то для того чтобы отослать 1 бит данных, требуется 4 такта, что дает 6.25 Мгц. Если же разделить на 8 бит (но так конечно, все равно не будет, так что +2 бита добавляем), то теоретически максимальная возможная скорость подобного устройства будет равна 625 килобайт в секунду. То есть это максимально простой модуль из возможных.
На этом все. Этот модуль не является конечным модулем в процессе взаимодействия с процессором, поскольку ему нужен еще один контроллер-прослойка для x86, но в целом, его можно использовать с другими процессорами, которые, например, пишут в порты напрямую.