Как стать автором
Обновить

Очередной драйвер SPI флэшек… Но уже с кэшем и «нормальным» api

Уровень сложностиСредний
Время на прочтение14 мин
Количество просмотров6.6K
Всего голосов 24: ↑23 и ↓1+22
Комментарии32

Комментарии 32

Вместо bool write enum будет выглядеть лучше.

Для mutex стандартной терминологией является down/up, выбор take/give неоправдан в случае если код публикуется для масс.
На 144 строке было бы аккуратнее с early-exit и continue.

Где происходит оборачивание навроде spi_cs_activate()/spi_cs_deactivate() более аккуратно выглядит static inline функция, внутри которой всего 3 действия: захват-действие-отпуск, где действие - это вызов еще одной вложенной static inline.

Вместо bool write enum будет выглядеть лучше.

Согласен, будет выглядеть аккуратнее.

Для mutex стандартной терминологией является down/up, выбор take/give неоправдан в случае если код публикуется для масс.

Не готов согласиться, down/up не совсем отражает суть действия и сходу понятны куда меньше.

take/give тоже не идеал, но имхо ближе к сути, и для знакомых с freeRTOS намного яснее отражает суть. lock/unlock - тоже было бы норм, но...

Где происходит оборачивание навроде spi_cs_activate()/spi_cs_deactivate() более аккуратно выглядит static inline функция, внутри которой всего 3 действия: захват-действие-отпуск, где действие - это вызов еще одной вложенной static inline.

Напишите понятнее, что имеется ввиду? Все функции для работы с spi: прижатие чип селекта, приём и передача определены в другом модуле, здесь им делать нечего.

На 144 строке было бы аккуратнее с early-exit и continue.

Подробнее, про какой кусок кода идёт речь?

В случае с mutex это не "понятнее", это глоссарий. Эти термины, которые несмотря на их возможную противоречивость при переводе, наиболее ожидаемы теми кто читает код.

Примерно вот так с inline
static inline mem_status_t mem_init_locked_spi_()
{
	uint8_t jdec[ 4 ] = {MEM_CMD_READ_JEDEC, 0, 0, 0};

	if (spi_tx_rx(jdec, jdec, 4) != SPI_OK)
	{
		return MEM_ERROR;
	}

	for (uint32_t i = 0; i < sizeof(dev_table) / sizeof(mem_dev_t); i++) //-V1008
	{
		if (jdec[ 1 ] == dev_table[ i ].manufact_id &&                   //
			jdec[ 2 ] == dev_table[ i ].memory_id &&                     //
			jdec[ 3 ] == dev_table[ i ].capacity_id)
		{
			dev    = &dev_table[ i ];

			return MEM_OK;
		}
	}

	return MEM_ERROR;
}

static inline mem_status_t mem_init_locked_()
{
	spi_cs_activate();

	mem_status_t status = mem_init_locked_spi_();

	spi_cs_deactivate();

	return status;
}

mem_status_t mem_init()
{
    if (mem_is_init > 0)
    {
        mem_is_init++;
        return MEM_OK;
    }

    spi_cs_deactivate();
    spi_init();
    mem_mux_create();

    // Read JEDEC ID

	mem_mux_take();

	mem_status_t status = mem_init_locked_();

	mem_mux_give();

    if (status == MEM_OK) mem_is_init++;

    return status;
}

Реализация с кучей функций имеет право на существование, но в чем смысл плодить сущности, если и так понятно что происходит?

take/give по нескольким причинам:

  • Нет мнимого чувство что можно использовать счетный, как это может показаться с down/up

  • Так исторически сложилось. Самый тупой аргумент, но имеет место быть

  • Понятность куда важнее, а если имя функции может спровоцировать ошибку, то нафиг оно нужно?

  • Всеобщая практика показывает: кто как хочет, так и ... Делает.

Спор про мьютекс считаю завершенным.

>> Реализация с кучей функций имеет право на существование, но в чем смысл плодить сущности, если и так понятно что происходит?

Завтра вам нужно будет где-то сделать return, и случайно вылетите без анлока. Плюс более строго управляется scope переменных - если нужно что-то использовать/менять в заблокированном контексте, то нужно будет передавать через аргументы и "случайно" оно не вылезет.

>> Спор про мьютекс считаю завершенным.

Хозяин барин конечно, но вот работать на проекте, который как-бы на C или C++, но все ключевые слова там на русском (который потом препроцессится) да переменные все с emoji - не захотел бы, хотя формально все по стандарту да и аргументы можно любые привести под них.

По первой части. Где и зачем может потребоваться сделать дополнительный return в функции инициализации? Странно и не нужно.

В других случаях - согласен. Подобный вариант может быть уместен.

По второй. Что-то вы выдумываете и приписываете мне чужие "заслуги" 😊

Приведите примеры и аргументы правильности использования варианта, предложенного вами.

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

где Вы такой пример взяли? Никто никогда так не делал. Вы же сами написали типовой алгоритм. Читаем сектор в буфер, изменяем в буфере 10 страниц, стираем сектор 1, а не 10 раз, и пишем сектор. Это и есть типовой алгоритм.

Аналогично Ваш пример быстродейcтвия. Именно 2x - это и будет классика и без кеша, так как Вы изначально выделяете буфер размером с сектор.

Впечатление такое, что Вы игнорируете даташит и рекомендации разработчиков микросхем, а сразу строите свой велосипед и радуетесь, что "гениально" придумали два не квадратных колеса.

Посмотрите как реализованы fatfs и stm USB msc, вызывается и пишется блоками по 512 байт. Вот и пример, а выделять отдельно по 4кб для USB и fat глупо, особенно если в чипе всего 20 килобайт оперативы.

Я привел хоть и синтетический пример, но не такой уж и далекий от реального положения вещей

В этом случае глупо выделять кеш.

А еще, если у Вас UART, то в этом случае буфер 256 байт . В этом случае пишем по 256 байт.

В итоге, Ваш универсальный драйвер для гипотетического универсального микроконтроллера. В реальности все делается для конкретного железа.

----------------

Если Вы по USB будете писать во флеш, то у вас не предусмотрена синхронизация. В итоге будете писать по 256 байт и медленно.

При чем здесь uart?

Медленно будет если не использовать кэширование, так как будет по две страницы писаться.

Но если записывать только по смене сектора и по таймауту, то будет быстрее.

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

Вы же сами написали, " а выделять отдельно по 4кб для USB и fat глупо, особенно если в чипе всего 20 килобайт оперативы". А теперь рассказываете про выделение кеш. Что не так?

Кроме того, Вы не учитываете, что для записи надо кроме данных передать команду и адрес. Это 32 такта. Если пишите 1 байт, то время записи составит 40 тактов SPI и только в том случае если используете DMA. Иначе будет еще больше.

Т е в Вашем расчете быстродействия надо считать время записи 1-го байта не 8w, а 40t, а скорость записи страницы как 2080t, где t- такт SPI.

Если Вы из внешнего источника программируете флеш через USB, то скорость USB-2.0 это примерно 7 Мбайт/сек.

Если у Вас SPI работает на 10 МГц и используется DMA, то скорость записи по 1 байту составит 0.25 Mбайт/сек, а скорость записи по страницам составит не более 1.2 Мбайт/сек.

Т е скорость записи во флеш в 6-30 раз меньше, чем скорость поступления данных. В этом случае можно работать налету , что не требует ни кеш, ни большого буфера.

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

 А теперь рассказываете про выделение кеш. Что не так?

Кэш не выделяется, а используется уже готовый буфер, который и так использовался бы при записи данных. И в статье описано, и в комментах вам отвечал.

Время записи, физической записи, в тактах SPI считать крайне неправильная идея. Вы учитываете лишь малую часть этого процесса.

Временные характеристики операций
Временные характеристики операций

Время на пересылку 260 байт (1 команда + 3 адрес + 256 данные) при ваших 10 МГц будет 26 мкс, что 4,5% (в лучшем случае) от всех временных затрат.

Взгляните на рисунок и будет понятно, что ни о каких 1.2 Мбайт/сек речи и быть не может.

От чего и выбраны магические константы w, e и r, которые от чипа к чипу меняются.

Если пишите один байт, то у вас будет только время записи байта, а вот если переписываете - время стирания и переписывания всего сектора: tse + tpp * n, где n - количество страниц в секторе (в примере было 8, в жизни 16).

Использование DMA немного помогло бы, тут я согласен, но не кардинально. В довесок мы бы получили отложенный возврат статуса операции и обработки ошибок. Можно использовать неблокирующий способ передачи, но нужно будет дополнительно усложнить логику уровнем выше.

Ваш драйвер не решает эту проблему.

Как раз за счет уменьшения этих операций и решает.

"Время на пересылку 260 байт (1 команда + 3 адрес + 256 данные) при ваших 10 МГц будет 26 мкс, что 4,5% (в лучшем случае) от всех временных затрат. "

Вы ошибаетесь. 10 MГц - это тактовая частота SPI.

SPI - это последовательный интерфейс т. е. это сдвиговый регистр.

260 байт - это 2080 бит.

Для передачи 2080 бит по SPI надо 2080 тактов.

В итоге не 26 мкс, а 208 мкс. т е в 8 раз больше.

Точно-точно, прошу прощения, потерял восьмёрку в расчетах. Однако это не сильно меняет ситуацию. А наоборот показывает, что операция длится ещё дольше и хорошо бы свести лишние записи к минимуму.

Тогда мой довод ранее о том, что писать можно налету работает. Я сам так пишу при программировании SoC.

О каких лишних записях Вы говорите?

На лету как раз не выйдет, потому что скорость записи существенно меньше скорости поступления данных.

При использовании fatfs и USB MSC у вас будет буфер размером в 512 байт, что всего 2 страницы. При типовой реализации будут лишние записи и стирания, если переписывать данные. Однако если чуть поднакопить изменения в буфере и записать все разом операций будет меньше.

Что именно вы пишете, мне кажется мы говорим о разных применениях?

Непонятны Ваши рассуждения.

То Вы говорите про запись по USB, то про fatfs.

если программируем с компа SoC то никакой fatfs не используется.

Но даже в этом случае скорость записи существенно меньше, чем скорость передачи данных.

Если Вы используете fatfs, то у Вас скорее всего управление файловой системой будет существенно медленнее, чем передача данных.

Чтобы обсуждать предметно, напишите для какого конкретно решения в железе Вы написали свой драйвер. Иначе это беседа о гипотетическом решении.

Естественно если вы заливаете прошивку с компа, то запись уже идет большими блоками и уже довольно оптимально. И это совершенно другое. Про программирование с компа речи и не шло, было про оперирование данными в микроконтроллере.

Про USB и fatfs говорю потому, что обе эти возможности должны или могут быть реализованы сразу. И у каждой есть ограничения и особенности.

Можно было бы выделить под USB 4 килобайта, что бы сразу писать, но такой роскоши нет или может не быть.

Повторю, то что сказал ранее. Все зависит от конкретного железа.

Допускаю, что Вы сделали решение для Вашего варианта железа, но это не кросс платформенное решение и не универсальное.

Но в статье Вы нет конкретики, а есть претензия на универсальность.

От железа зависит только скорость пересылки данных, даже не от самого железа, а от его возможностей.

Реализация алгоритма не связана с конкретным ядром. Возьмите хоть arm, хоть avr, хоть risc-v, все едино. И ничего не поменяется.

Основной посыл не писать сразу данные, а поднакопить их.

Посыл спорный. Дайте хотя бы один реальный пример.

Ткните пальцем в код, где есть куски, которые завися от железа.

Реальный пример уже приводил, FatFs и ST USB MSC. Возьмите другую связку, например FatFs + AVR USB Software Library.

При любом раскладе взаимодействие будет сводится к блочному чтению, по умолчанию это будут 2 страницы, если конечно у вас нет большего буфера.

любой код - это команды для управления железом. Т е без железа - это бла бла бла.

Код на СИ будет преобразован в код на ASM и его эффективность зависит от компилятора.

Ваш пример не корректный.

Например , FatFS+AVR (какой чип конкретно? какая максимальная у него скорость SPI конкретно?) какая скорость FatFS для этого чипа поиска свободного сектора конкретно

Какой алгоритм загрузки данных в буфер конкретно?

Когда Вы все это учтете, то это и будет конкретный реальный пример.

Но Вы можете этого не делать, а полагать, что все зависит лишь от того, что Вы убрали макросы и комментарии из приведенных Вами аналогов.

---------------

Например, посмотрите это про железо SPI AVR :

https://www.microchip.com/content/dam/mchp/documents/MCU08/ApplicationNotes/ApplicationNotes/TB3215-Getting-Started-with-SPI-DS90003215.pdf

Код, в частности на си, это не команды для железа, а всего лишь человекочитаемое представление оных.

Эффективность решения задачи зависит не только от того какие вы флаги оптимизации включите, а то, как реализуете алгоритм.

Но Вы можете этого не делать, а полагать, что все зависит лишь от того, что Вы убрали макросы и комментарии из приведенных Вами аналогов.

Всё это называется: "не читал, но осуждаю". Дело не в макросах и комментариях.

Не вижу смысла больше общаться с вами, никаких аргументов вы не приводите.

… необходимо после поднятия чип селекта (CS) подождать некоторое время перед следующей командой, такой как запрос статуса занятости. Пока это реализовано с помощью HAL_Delay.

Что-то не припоминаю такой подлости с чтением статуса. Речь именно о 4-битном Винбонде из заголовков?

Там подлость не в чтении статуса, а выдержке с отпущенным чипселектом после операций записи.

В итоге либо делать магическую задержку, либо проверять статус.

Но проверять статус сразу после записи не удается из-за слишком малого времени выдержки.

Микросхем не винбонд, а ZD25Q64B. Она в принципе аналогична. Хотя не исключаю, что это именно её особенность с длинной выдержкой.

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

А кто вас заставляет стирать уже стёртые страницы? Прочитать страницу, и если она вся 0хFF — то можно сразу вписать новую страницу без очистки всего сектора. Накладные расходы на чтение на порядки меньше, чем на стирание, и ресурс экономится. Тем более что при очистке сектора надо будет читать не одну страницу, а сразу весь сектор.

Никто не заставляет, можно и даже нужно. Равно как и запись данных которые уже там есть, расширь проверку не просто на пустоту, но и на равенство.

Это очень хорошая идея

Страници, сектора… Хочу простые и понятные fread и fwrite. ))

Так это доступно уровнем выше))

Пару ключиков для линкера, реализуем функции _write, _read, _lseek и остальные, и дело в шляпе - пользуемся fprintf и всеми остальными благами)

Или можно не сильно заморачиваясь использовать функции fatfs: f_open, f_write, f_read.

А если я захочу несколько флешек юзать, как это нормально переписать?
Там же куча статик переменных.
Городить сишное ООП?
Другой вопрос, как часто надо несколько флешек юзать...

Можно городить ООП: по сути все эти статичные переменные запихнуть в структуру.

Несколько SPI флэшек еще не было нужно.

Если две, то продублировать не проблема: чипселект и буфер в зависимости от адреса выбрать и чуть-чуть усложнить логику. Три - чуть сложнее, но тоже почти так же

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории