
Здравствуйте меня зовут Дмитрий сегодня мы продолжим исследование FPGA плат и напишем полноценный контроллер SD карт (правильней их называть SDHC, потому что карты формата SD давно не продаются). Причем мы создадим полноценный контроллер задействующий все возможности шины SD, а не жалкий SPI аналог.
Железо

Первый вопрос, как нам подключить SD карту к FPGA чипу? Конечно можно проявить чудеса владения паяльником, но я пошел по пути наименьшего сопротивления и просто купил на Али вот такую плату. Кстати светодиод на плате светится только когда вставлена карта. Все линии подтянуты к 3.3В, поэтому если мы ничего не делаем то на всех шинах единица.
Интерфейс

Кроме очевидных питания и земли, присутствует, тактовый сигнал CLK. Шина команд CMD. И четырехбитная шина данных. Кроме контактов самой SD карты присутствуют два контакта SD разъема. CD - chip detect на нем возникает 0 если вставить карту в разъем. WP - write protect на этом контакте будет ноль если активирован переключатель защиты от записи. Я кстати никогда не встречал микро SD карты с подобным переключателем, но контакт есть.
Архитектура модуля

Модуль состоит из 3-х основных модулей это:
M_SD_Card_Control - Это мозг всего модуля именно он определяет когда и что писать или читать. Сам модуль разбит на 4 подмодуля.
M_SD_Card_Init - Модуль инициализации SD карты, в нем зашита последовательность команд, необходимая для начала работ карты.
M_SD_Card_Read - Модуль чтения данных.
M_SD_Card_Write - Модуль записи данных.
M_SD_Card_Check_State - Модуль который проверяет состояние карты (о состояниях ниже). И если она не в том состоянии переключает в правильное.
M_SD_Card_DATA - Этот модуль заведет линиями данных.
M_SD_DATA_GET - Модуль который получает данные.
M_SD_DATA_SEND - Модуль отправляющий данные.
M_CRC16 - Модуль вычисляющий контрольную сумму. Данные защищены CRC16 контрольной суммой
M_SD_Card_CMD - Модуль отвечающий за линию команд.
M_SD_CMD_SEND - Модуль отправляющий команды.
M_SD_CMD_GET - Модуль принимающий отклики от SD карты на эти команды.
M_CRC7 - Модуль для вычисления контрольной суммы CRC - 7 которая используется для защиты команд.
Взглянув на эту структуру, кто-то может подумать что у меня маниакальное желание создавать отдельные модули. Но это не так, потому что в отличии от того-же C++ где мы можем делать функции хоть на 100 страниц и они будут работать. В Верилоге каждый оператор это отдельный элемент который реализуется на чипе, и если их очень много то они начнут создавать помехи друг для друга.
То есть это как электронная плата, вот вы создали конструкцию always считайте что поставили чип на плату. Например если в модуле два конечных автомата, это значит пора модуль поделить. А несколько конечных автоматов получится в любом случае, потому что принимаем данные мы по положительному фронту, а отсылаем по спаду. Вот уже получилось 2 конечных автомата сами собой. Я конечно хотел-бы упростить структуру но это невозможно.
Инициализация
Прежде чем работать с картой её нужно инициализировать. Инициализация должна проходить на частоте 400 килогерц.
Скрытый текст
always @(posedge clk_400k)
begin
if (rst)
begin
State_main_Init = S_IDLE;
Send_CMD_En <= 'd0;
Get_CMD_En <= 'd0;
Init_complite <= 1'b0;
Init_Fail <= 1'b0;
WaitMessageCounter <= 'd0;
end
else
begin
case (State_main_Init)
S_IDLE: if (Init_Enable) State_main_Init <= S_SEND_CMD0;
S_SEND_CMD0:
begin
if (!Send_CMD_Complite)
begin
CMD_ID <= 'd0;
Arg1 <= 'd0;
Arg2 <= 'd0;
Arg3 <= 'd0;
Arg4 <= 'd0;
Send_CMD_En <= 1'b1;
end
else
begin
Send_CMD_En <= 1'b0;
State_main_Init <= S_SEND_CMD8;
end
end
S_SEND_CMD8:
begin
if (!Send_CMD_Complite)
begin
CMD_ID <= 'd8;
Arg1 <= 'd0;
Arg2 <= 'd0;
Arg3 <= 8'b00000001;
Arg4 <= 8'b10101010;
Send_CMD_En <= 1'b1;
end
else
begin
Send_CMD_En <= 1'b0;
State_main_Init <= S_GET_RESP_CMD8;
Get_CMD_En <= 1'b1;
end
end
S_GET_RESP_CMD8:
begin
if(Get_CMD_Complite)
begin
Get_CMD_En <= 1'b0;
WaitMessageCounter <= 'd0;
if (Responce_R1_R3[15: 8] ^ Arg4) State_main_Init <= S_CMD8_RESPONSE_FAIL;
else
begin
State_main_Init <= S_SEND_CMD55;
end
end
else
begin
WaitMessageCounter <= WaitMessageCounter + 1'd1;
if (WaitMessageCounter == 'd255) State_main_Init <= S_FAIL;
end
end
S_SEND_CMD55:
begin
if (!Send_CMD_Complite)
begin
CMD_ID <= 'd55;
Arg1 <= 'd0;
Arg2 <= 'd0;
Arg3 <= 'd0;
Arg4 <= 'd0;
Send_CMD_En <= 1'b1;
end
else
begin
Send_CMD_En <= 1'b0;
State_main_Init <= S_GET_RESP_CMD55;
Get_CMD_En <= 1'b1;
end
end
S_GET_RESP_CMD55:
begin
if(Get_CMD_Complite)
begin
Get_CMD_En <= 1'b0;
WaitMessageCounter <= 'd0;
State_main_Init <= S_SEND_ACMD41;
end
else
begin
WaitMessageCounter <= WaitMessageCounter + 1'd1;
if (WaitMessageCounter == 'd255) State_main_Init <= S_RESP_TIME_OUT;
end
end
S_SEND_ACMD41:
begin
if (!Send_CMD_Complite)
begin
CMD_ID <= 'd41;
Arg1 <= 8'b01000000;
Arg2 <= 8'b00000010;
Arg3 <= 'd0;
Arg4 <= 'd0;
Send_CMD_En <= 1'b1;
end
else
begin
Send_CMD_En <= 1'b0;
State_main_Init <= S_GET_RESP_ACMD41;
Get_CMD_En <= 1'b1;
end
end
S_GET_RESP_ACMD41:
begin
if(Get_CMD_Complite)
begin
Get_CMD_En <= 1'b0;
WaitMessageCounter <= 'd0;
if (Responce_R1_R3[39]) State_main_Init <= S_SEND_CMD2;
else State_main_Init <= S_SEND_CMD55;
end
else
begin
WaitMessageCounter <= WaitMessageCounter + 1'd1;
if (WaitMessageCounter == 'd255) State_main_Init <= S_RESP_TIME_OUT;
end
end
S_SEND_CMD2:
begin
if (!Send_CMD_Complite)
begin
CMD_ID <= 'd2;
Arg1 <= 'd0;
Arg2 <= 'd0;
Arg3 <= 'd0;
Arg4 <= 'd0;
Send_CMD_En <= 1'b1;
end
else
begin
Send_CMD_En <= 1'b0;
State_main_Init <= S_GET_RESP_CMD2;
Get_CMD_En <= 1'b1;
end
end
S_GET_RESP_CMD2:
begin
if(Get_CMD_Complite)
begin
Get_CMD_En <= 1'b0;
WaitMessageCounter <= 'd0;
PNM[0]= Responce_R2[104:97];
PNM[1]= Responce_R2[96:89];
PNM[2]= Responce_R2[88:81];
PNM[3]= Responce_R2[80:73];
PNM[4]= Responce_R2[72:65];
State_main_Init <= S_SEND_CMD3;
end
else
begin
WaitMessageCounter <= WaitMessageCounter + 1'd1;
if (WaitMessageCounter == 'd255) State_main_Init <= S_RESP_TIME_OUT;
end
end
S_SEND_CMD3:
begin
if (!Send_CMD_Complite)
begin
CMD_ID <= 'd3;
Arg1 <= 'd0;
Arg2 <= 'd0;
Arg3 <= 'd0;
Arg4 <= 'd0;
Send_CMD_En <= 1'b1;
end
else
begin
Send_CMD_En <= 1'b0;
State_main_Init <= S_GET_RESP_CMD3;
Get_CMD_En <= 1'b1;
end
end
S_GET_RESP_CMD3:
begin
if(Get_CMD_Complite)
begin
Get_CMD_En <= 1'b0;
WaitMessageCounter <= 'd0;
RCA_Addr <= Responce_R1_R3[39:24];
State_main_Init <= S_SEND_CMD9;
end
else
begin
WaitMessageCounter <= WaitMessageCounter + 1'd1;
if (WaitMessageCounter == 'd255) State_main_Init <= S_RESP_TIME_OUT;
end
end
S_SEND_CMD9:
begin
if (!Send_CMD_Complite)
begin
CMD_ID <= 'd9;
{Arg1, Arg2} <= RCA_Addr;
Arg3 <= 'd0;
Arg4 <= 'd2;
Send_CMD_En <= 1'b1;
end
else
begin
Send_CMD_En <= 1'b0;
State_main_Init <= S_GET_RESP_CMD9;
Get_CMD_En <= 1'b1;
end
end
S_GET_RESP_CMD9:
begin
if(Get_CMD_Complite)
begin
Get_CMD_En <= 1'b0;
WaitMessageCounter <= 'd0;
State_main_Init <= S_SEND_CMD7;
DeviseSize <= Responce_R2[69:48];
end
else
begin
WaitMessageCounter <= WaitMessageCounter + 1'd1;
if (WaitMessageCounter == 'd255) State_main_Init <= S_RESP_TIME_OUT;
end
end
S_SEND_CMD7:
begin
if (!Send_CMD_Complite)
begin
CMD_ID <= 'd7;
{Arg1, Arg2} <= RCA_Addr;
Arg3 <= 'd0;
Arg4 <= 'd0;
Send_CMD_En <= 1'b1;
end
else
begin
Send_CMD_En <= 1'b0;
State_main_Init <= S_GET_RESP_CMD7;
Get_CMD_En <= 1'b1;
end
end
S_GET_RESP_CMD7:
begin
if(Get_CMD_Complite)
begin
Get_CMD_En <= 1'b0;
WaitMessageCounter <= 'd0;
State_main_Init <= S_SEND_CMD55_RCA;
end
else
begin
WaitMessageCounter <= WaitMessageCounter + 1'd1;
if (WaitMessageCounter == 'd255) State_main_Init <= S_RESP_TIME_OUT;
end
end
S_SEND_CMD55_RCA:
begin
if (!Send_CMD_Complite)
begin
CMD_ID <= 'd55;
{Arg1, Arg2} <= RCA_Addr;
Arg3 <= 'd0;
Arg4 <= 'd0;
Send_CMD_En <= 1'b1;
end
else
begin
Send_CMD_En <= 1'b0;
State_main_Init <= S_GET_RESP_CMD55_RCA;
Get_CMD_En <= 1'b1;
end
end
S_GET_RESP_CMD55_RCA:
begin
if(Get_CMD_Complite)
begin
Get_CMD_En <= 1'b0;
WaitMessageCounter <= 'd0;
State_main_Init <= S_SEND_ACMD6;
end
else
begin
WaitMessageCounter <= WaitMessageCounter + 1'd1;
if (WaitMessageCounter == 'd255) State_main_Init <= S_RESP_TIME_OUT;
end
end
S_SEND_ACMD6:
begin
if (!Send_CMD_Complite)
begin
CMD_ID <= 'd6;
Arg1 <= 'd0;
Arg2 <= 'd0;
Arg3 <= 'd0;
Arg4 <= 'd2;
Send_CMD_En <= 1'b1;
end
else
begin
Send_CMD_En <= 1'b0;
State_main_Init <= S_GET_RESP_ACMD6;
Get_CMD_En <= 1'b1;
end
end
S_GET_RESP_ACMD6:
begin
if(Get_CMD_Complite)
begin
Get_CMD_En <= 1'b0;
WaitMessageCounter <= 'd0;
if(Responce_R1_R3[23:8] == 16'b0000100100100000) State_main_Init <= S_INIT_PASSED;
else State_main_Init <= S_ACMD6_RESPONSE_FAIL;
end
else
begin
WaitMessageCounter <= WaitMessageCounter + 1'd1;
if (WaitMessageCounter == 'd255) State_main_Init <= S_RESP_TIME_OUT;
end
end
S_INIT_PASSED:
begin
if (Init_Enable) Init_complite <= 1'b1;
else
begin
State_main_Init <= S_IDLE;
Init_complite <= 1'b0;
$display("SD_CARD_INIT--> SD init complite");
end
end
S_RESP_TIME_OUT:
begin
$display("SD_CARD_INIT--> SD respond time Out");
State_main_Init <= S_FAIL;
end
S_CMD8_RESPONSE_FAIL:
begin
$display("SD_CARD_INIT--> CMD8 Respond fail");
State_main_Init <= S_FAIL;
end
S_ACMD6_RESPONSE_FAIL:
begin
$display("SD_CARD_INIT--> ACMD6 Respond fail");
State_main_Init <= S_FAIL;
end
S_FAIL:
begin
if (Init_Enable) Init_Fail <= 1'b1;
else
begin
State_main_Init <= S_IDLE;
Init_Fail <= 1'b0;
$display("SD_CARD_INIT--> SD init fail");
end
end
endcase
end
end
Сначала посылаем команду CMD0 чтобы перевести карту в состояние сброса idle.
Потом посылаем CMD8 чтобы задать напряжение. Мы будем работать с напряжение 3.3В. Но более новые карты могут работать и с напряжением 1.8В. Кстати если мы так и не получим ответ на эту команду, то это значит что у нас карта первой версии. Но поскольку сейчас карты первой версии можно найти только в музее, то я просто прекращаю инициализацию в этом случае.
Потом надо перевести карту в состояние Ready командой ACMD41, но чтобы карта приняла команду с префиксом A ей нужно предварительно отослать команду CMD55. Кроме того нужно будет ожидать довольно-таки продолжительное время пока карта не перейдет в этот режим, бомбардируя её командами ACMD41 и проверяя бит 39 в отклике.
После этого командой CMD2 переводим карту в режим Ident.
В этом режиме если послать CMD3 то карта вышлет нам свой RCA адрес и перейдет в режим Stby. Теперь чтобы послать команду нашей карте мы должны указывать её RCA, иначе она нам не ответит. Дело в том что несколько SD карт могут работать с одной шиной данных, шина CMD конечно-же у всех должна быть своя ведь каждая карта посылает отклик не обращая внимания на другие, и чтобы можно было выбрать какую-то конкретную карту придумали RCA.
Теперь при помощи команды CMD9 можно узнать содержимое CSD регистра. В нем содержатся технические характеристики карты, но нас более всего интересует её размер. Мы получим 22-битное число. Дописав к которому ещё 10 единиц мы узнаем максимально возможный адрес который можно прочитать или записать.
Теперь командой CMD7 и RCA нашей карты мы её выбираем. В результате она переходит в состояние Tran и готова к работе. Кстати если выполнить команду CMD7 с RCA какой-то "чужой" карты, то наша карта перейдет в состояние Stby. А карта чей RCA мы передали перейдет в состояние Tran.
Осталось только переключить шину данных в 4 - битное состояние командой ACMD6. Все инициализация выполнена.
Передача команд
Команды передаются последовательно по шине CMD. Сначала мы передаем команду а потом ждем оклик от карты. И команда и отклик защищены контрольной суммой CRC-7. Поэтому мне пришлось разобраться как-же её считать вот видео где показано вычисление этой суммы.
Но на самом деле так CRC сумму никто не считает. Для этого используют метод быстрого вычисления CRC суммы по таблице. И я написал небольшую программу.
Скрытый текст
#include <windows.h>
#include <iostream>
#include <stdint.h>
#include <fstream>
#include <filesystem>
#include <bitset>
/* Table for CRC-7 (polynomial x^7 + x^3 + 1)
static const uint8_t CRCTable[256] =
{
0x00, 0x09, 0x12, 0x1b, 0x24, 0x2d, 0x36, 0x3f,
0x48, 0x41, 0x5a, 0x53, 0x6c, 0x65, 0x7e, 0x77,
0x19, 0x10, 0x0b, 0x02, 0x3d, 0x34, 0x2f, 0x26,
0x51, 0x58, 0x43, 0x4a, 0x75, 0x7c, 0x67, 0x6e,
0x32, 0x3b, 0x20, 0x29, 0x16, 0x1f, 0x04, 0x0d,
0x7a, 0x73, 0x68, 0x61, 0x5e, 0x57, 0x4c, 0x45,
0x2b, 0x22, 0x39, 0x30, 0x0f, 0x06, 0x1d, 0x14,
0x63, 0x6a, 0x71, 0x78, 0x47, 0x4e, 0x55, 0x5c,
0x64, 0x6d, 0x76, 0x7f, 0x40, 0x49, 0x52, 0x5b,
0x2c, 0x25, 0x3e, 0x37, 0x08, 0x01, 0x1a, 0x13,
0x7d, 0x74, 0x6f, 0x66, 0x59, 0x50, 0x4b, 0x42,
0x35, 0x3c, 0x27, 0x2e, 0x11, 0x18, 0x03, 0x0a,
0x56, 0x5f, 0x44, 0x4d, 0x72, 0x7b, 0x60, 0x69,
0x1e, 0x17, 0x0c, 0x05, 0x3a, 0x33, 0x28, 0x21,
0x4f, 0x46, 0x5d, 0x54, 0x6b, 0x62, 0x79, 0x70,
0x07, 0x0e, 0x15, 0x1c, 0x23, 0x2a, 0x31, 0x38,
0x41, 0x48, 0x53, 0x5a, 0x65, 0x6c, 0x77, 0x7e,
0x09, 0x00, 0x1b, 0x12, 0x2d, 0x24, 0x3f, 0x36,
0x58, 0x51, 0x4a, 0x43, 0x7c, 0x75, 0x6e, 0x67,
0x10, 0x19, 0x02, 0x0b, 0x34, 0x3d, 0x26, 0x2f,
0x73, 0x7a, 0x61, 0x68, 0x57, 0x5e, 0x45, 0x4c,
0x3b, 0x32, 0x29, 0x20, 0x1f, 0x16, 0x0d, 0x04,
0x6a, 0x63, 0x78, 0x71, 0x4e, 0x47, 0x5c, 0x55,
0x22, 0x2b, 0x30, 0x39, 0x06, 0x0f, 0x14, 0x1d,
0x25, 0x2c, 0x37, 0x3e, 0x01, 0x08, 0x13, 0x1a,
0x6d, 0x64, 0x7f, 0x76, 0x49, 0x40, 0x5b, 0x52,
0x3c, 0x35, 0x2e, 0x27, 0x18, 0x11, 0x0a, 0x03,
0x74, 0x7d, 0x66, 0x6f, 0x50, 0x59, 0x42, 0x4b,
0x17, 0x1e, 0x05, 0x0c, 0x33, 0x3a, 0x21, 0x28,
0x5f, 0x56, 0x4d, 0x44, 0x7b, 0x72, 0x69, 0x60,
0x0e, 0x07, 0x1c, 0x15, 0x2a, 0x23, 0x38, 0x31,
0x46, 0x4f, 0x54, 0x5d, 0x62, 0x6b, 0x70, 0x79
};
*/
uint8_t CRCPoly = 0b10001001; //x^7 + x^3 + 1
void CRC_table_Generate(uint8_t* CRCTable)
{
for (int i = 0; i < 256; ++i)
{
CRCTable[i] = (i & 0x80) ? i ^ CRCPoly : i;
for (int j = 1; j < 8; ++j)
{
CRCTable[i] = CRCTable[i] << 1;
if (CRCTable[i] & 0x80)
{
CRCTable[i] = CRCTable[i] ^ CRCPoly;
}
}
}
}
uint8_t CRC_table( const uint8_t * data, size_t length )
{
uint8_t crc = 0;
uint32_t index;
uint8_t CRCTable[256];
CRC_table_Generate(CRCTable);
if (data)
for (int i = 0; i < length; i++)
{
crc = crc << 1;
index = crc ^ data[i];
crc = CRCTable[index];
}
return crc;
}
int main()
{
uint8_t CRCTable[256];
uint8_t message [5] = {0b00010001,0,0,0b00001001,0};
std::cout<<"CRC = " << std::bitset<7>(CRC_table(message, 5))<< std::endl;
//Create mem_block.txt
std::ofstream fout("mem_block.txt");
CRC_table_Generate(CRCTable);
for (int i = 0; i < 256; i++)
{
fout << "mem[" << i << "] = 8'b" << std::bitset<8>(CRCTable[i]) << ";" << std::endl;
}
return 0;
}Эта программа может вычислить CRC-7 сумму для данных в переменной message. А самое главное она распечатывает в файл mem_block.txt эту самую таблицу, и её уже можно вставить в наш модуль.
Передача данных

Данные передаются пакетами по 512 байт по 4 линиям поэтому обращаться напрямую к данным на карте нельзя, надо скопировать данные в буфер а потом к ним обращается.
Данные защищены контрольной суммой CRC-16. Причем для каждой лини вычисляется своя контрольная сумма. Также как и в случае с CRC-7 используется быстрый метод подсчета по таблицы. Я написал еще одну программу которая может вычислить CRC-16 и распечатать таблицу в файл mem_block.txt.
Скрытый текст
#include <windows.h>
#include <iostream>
#include <stdint.h>
#include <fstream>
#include <filesystem>
#include <bitset>
//x^16 + x^12 + x^5 + 1
/* const unsigned short Crc16Table[256] = {
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF,
0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6,
0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE,
0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485,
0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D,
0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4,
0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC,
0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823,
0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B,
0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12,
0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A,
0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41,
0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49,
0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70,
0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78,
0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F,
0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067,
0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E,
0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256,
0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D,
0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C,
0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634,
0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB,
0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3,
0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A,
0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92,
0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9,
0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1,
0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8,
0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0
}; */
void CRC_table_Generate(uint16_t* CRCTable)
{
const uint16_t LengthCRC = 16;
const uint16_t polynomial = 0x1021;
for (int i = 0; i < 256; i++)
{
CRCTable[i] = i << 8;
for (int j = 0; j < 8; j++)
{
if (CRCTable[i] & 0x8000)
CRCTable[i] = (CRCTable[i] << 1) ^ polynomial;
else
CRCTable[i] = CRCTable[i] << 1;
}
}
}
uint16_t CRC_table( const uint8_t * data, size_t length )
{
uint16_t crc = 0;
uint16_t crc_tmp = 0;
uint32_t index = 0;
unsigned short Crc16Table[256];
CRC_table_Generate(Crc16Table);
for (int i = 0; i < length; i++)
{
crc_tmp = crc << 8;
crc = crc >> 8;
index = crc ^ data[i];
crc = Crc16Table[index] ^ crc_tmp;
}
return crc;
}
int main()
{
uint8_t message [512];
unsigned short CRCTable[256];
CRC_table_Generate(CRCTable);
for (int i = 0; i < 512; i++)
{
message[i] = 0xff;
}
std::cout<<"CRC = "<< std::hex << CRC_table(message, 512) << std::endl;
//mem_block.txt
std::ofstream fout("mem_block.txt");
CRC_table_Generate(CRCTable);
for (int i = 0; i < 256; i++)
{
fout << "mem[" << i << "] = 16'b" << std::bitset<16>(CRCTable[i]) << ";" << std::endl;
}
return 0;
}Кстати после того как я написал эту программу. Я долго не мог понять почему мой алгоритм выдает 0x6995 для массива 512 байт 0xFF хотя должен выдавать 0x7FA1. Оказалось что при вычислении CRC16 обычно используют начальное значение 0xFFFF а для SD карт нужно использовать 0.
Чтение данных.
После инициализации можно переключиться на частоту 25 мегагерц. Причем если чтение на такой частоте проходит без проблем, то вот при записи даже на частоте 1 мегагерц, у меня зависала карта. Поэтому записывал данные на карту я с частотой 400 килогерц, только так удалось избежать зависаний.
Читать данные с карты можно двумя способами. Первый командой CMD17 мы читаем один блок адрес которого мы отправили вместе с командой. Или командой CMD18 в этом случае мы получим и блок данных адрес которого мы отправили и те блоки которые находятся за ним, чтение будет продолжатся пока не мы не отправим команду CMD12. Поэтому у модуля M_SD_Card есть переменная SD_SerialCount которая говорит сколько блоков мы хотим прочитать или записать кроме того на который указывает адрес. На самом деле когда я это испытывал у меня максимум удавалось прочитать 128 блоков (один адресуемый и 127 за ним) за раз, большее количество карта просто отказывалась передавать.
Поскольку мы используем 4 линии данных то разом мы получаем 4 байта в переменной SD_Out_Data то есть 32 бита. Причем первый байт находится, от нуля до седьмого бита, а четвертый от 31 до 25 бита. SD_Out_Data_Addr показывает адрес первого байта.
Запись данных
Данные приходится записывать на частоте 400 килогерц иначе карточка зависает.
Данные также можно записывать двумя способами, это командой CMD24 в этом случае будет записан только один блок 512байт или CMD25 которая будет записывать данные пока не придет команда CMD12 в случае с записью переменная SD_SerialCount работает также как и с чтением.
Данные нужно упаковывать по 4 байта в переменную SD_InPut_Data, а переменная SD_InPut_Data_Addr показывает адрес первого байта для чтения. То есть подготавливаем блок а модуль сам считает из него данные.
Тестирование

Естественно такой огромный модуль нельзя написать без тестов, я использую Icarus-Verilog. Все тесты можно найти в репозитории на GitHub. Я просто оставлю здесь картинку, могу сказать что мне стоило огромных усилий чтобы добиться такова результата.
Демонстрация
Чтобы не быть голословным я создал демонстрацию которая считывает картинку в формате .BMP (картинка обязательно должна иметь качество 24 бита на пиксель) и выводит её на экран.
Вы скажите а как записать картинку на SD карту? Если вы подумали что между делом я ещё и драйвер FAT-32 написал, то нет вы ошиблись. Картинку надо записывать на SD карту при помощи Win32DiskImager.
Для копирования данных с SD карты в SDRAM память используется модуль M_BMP_Copire. Я не буду его здесь подробно описывать, статья и так уже большая, я остановлюсь на одном моменте.
Это то что все строки в BMP имеют выравнивание кратное 4 байтам. Поэтому нельзя надеется что следующая строка начнется там где закончилась предыдущая. Поэтому смещение пикселя я нахожу таким образом:
assign PixelOffset = PixArrayOffset + (PixHeight - 1'b1 - V_count) *
StringLen + H_count * 'd3;
Как видите здесь я использую переменную StringLen которую я рассчитываю таким образом:
StringLen <= (PixWidth * 'd3) + ((PixWidth * 'd3) % 'd4);
Также если вам понравилась эта статья то возможно вам понравятся и другие статьи на эту тему:
Комментарии (22)

edo1h
11.10.2025 21:27сегодня мы продолжим исследование FPGA плат и напишем полноценный контроллер SD карт (правильней их называть SDHC, потому что карты формата SD давно не продаются). Причем мы создадим полноценный контроллер задействующий все возможности шины SD, а не жалкий SPI аналог.
имхо статью надо начинать с краткого описания того чем же жалок spi для sd-карт

HardWrMan
11.10.2025 21:27И забавно слышать о жалкости SPI при:
Данные приходится записывать на частоте 400 килогерц иначе карточка зависает.

kenomimi
11.10.2025 21:27Проблема скорее всего схемотехническая, в китайском шилде. Наблюдаю там трехногий элемент, который скорее всего понижайка 5в -> 3.3в для нужд ардуино. Причем максимально дешевый, на шилды другое не ставят... При записи карта потребляет хорошо так относительно чтения, питание просаживается, карта зависает.

nixtonixto
11.10.2025 21:27Даже самый дешёвый XC6206 выдаёт ток до 300 мА, чего вполне достаточно для MicroSD.

tequier0
11.10.2025 21:27Если я не ошибаюсь, то в статье используется вот этот модуль. Даташит сходу на него не нашел.
Глянул пример использования этого модуля, и там, во-первых, только SPI, во-вторых, частота там около 5 МГц.400 кГц и менее используется обычно только для этапа инициализации карты, дальше скорость апают.

deema35 Автор
11.10.2025 21:27На шилде нет преобразователя. Там только сборки резисторов, и транзистор, который отвечает за зажигание светодиода. Но возможно вы правы по поводу питания. Шилд питается от FPGA карты, возможно она не вывозит, тем более между шилдом и платой тонкие провода, наверно на них падает напряжение.
Потому что как я говорил на высоких частотах запись иногда проходила, а иногда зависала, и добиться стабильного результата удалось только снизив частоту.

deema35 Автор
11.10.2025 21:27Возможно у меня не очень качественная карта. Я думаю что и в режиме SPI она-бы зависала при таких-же частотах.

Goron_Dekar
11.10.2025 21:27А зачем?
Если человек не знает, почему SPI жалок, ему эту статью врадли получится оценить.

deema35 Автор
11.10.2025 21:27имхо статью надо начинать с краткого описания того чем же жалок spi для sd-карт
Ну может слово не подходящее. А так данные передаются по 4 линиям, а SPI только по 1. Данные не защищены CRC, а здесь защищены.

SIISII
11.10.2025 21:27Можно ещё вспомнить, что достаточно продвинутые контроллеры и карты умеют работать на намного больших частотах (кажется, 200 МГц могут поддерживать). Правда, там уже нужны преобразователи уровней, поскольку высокие частоты не на 3,3, а на 1,8.

VT100
11.10.2025 21:27Уже полтора десятка лет есть QSPI и вовсю растёт OctalSPI.
P.S.@deema35, какова схема переходничка? Хотя-бы - номиналы резисторов, на фото - не видно. И длина проводков до платы FPGA? Искать и включать какое-нибудь On Chip Termination - пробовали?

deema35 Автор
11.10.2025 21:27Схема проста все линии подтянуты к 3.3В, и есть транзистор отвечающий за светодиод вот и вся схема. Длины проводов я конечно-же не измерял, как получилось припаять так и получилось я думаю они не длиннее платы а плата 8 сантиметров, так что вряд-ли они работают в режиме длинной линии все таки тут не гигагерцы. Я думаю что проблема в толщине этих проводов, как написали выше когда карта пишет данные она потребляет большие токи. Чтение ведь проходит нормально на всех частотах.

checkpoint
11.10.2025 21:27Было бы интересно посмотреть на диаграммы переходов состояний. Из кода мало чего можно понять.

VelocidadAbsurda
11.10.2025 21:27А зачем в данном случае более громоздкий побайтовый табличный CRC, если весь обмен всё равно идёт последовательно? Прицепить на каждую линию по LFSR с тактированием от CLK и пусть себе считают побитово.

yamifa_1234
11.10.2025 21:27я так понимаю, чтобы не заморачиваться проще все клоки на 400кгц завести?
я про эти:input wire clk_400k, input wire clk_Write, input wire clk_Read,
и второй вопрос, модуль готов для того чтобы скопировать к себе и читать-записывать данные с sd-карты? или нужна дополнительная обвязка с расчетом контрольной суммы и управлением процессом передачи?

deema35 Автор
11.10.2025 21:27Ну если все клоки завести на 400кгц он будет медленней работать. Все контрольные суммы модуль подсчитывает сам. Если сумма не совпадет то вместо SD_Complite будет активирована SD_Fail. Адреса модуль перечисляет сам, так что только буфер надо подготовить.

tequier0
Чтобы конечный автомат был понятнее, предпочтительнее использовать двухблочную Moore FSM с выходными регистрами.
В одном блоке происходит переключение между стейтами согласно управляющим сигналам, во втором - логика стейтов, в т.ч. формирование управляющих сигналов для стейт машины.
Это обеспечивает более контролируемое поведение при синтезе, позволяет избегать латчей, ну и так проще контролировать состояния и наворачивать большое количество логики.
p.s. даже в случае одноблочной fsm best practices считается прописывать default state на случай сбоя/недостаточного покрытия тестами и более прогнозируемого поведения. Машина в таком случае хотя бы имеет шанс восстановиться.