Прокачиваем usb mass storage device на stm32f103 с помощью freertos и dma

Содержание

Общие сведения о запросах DMA

Для начала давайте разберемся, к какому каналу DMA подключены запросы от SPI. Открываем Reference manual, в разделе про DMA находим вот такую картинку:

Из рисунка видно, что каждый канал DMA может обрабатывать запросы от большого числа периферийных модулей. Для примера возьмем канал 3. Он может принимать 5 разных запросов: USART3_RX, TIM1_CH2, TIM3_CH4, TIM3_UP и SPI1_TX. Все эти запросы поступают на входы логического элемента ИЛИ. Как только станет активным один из запросов, на выходе этого элемента появится лог. 1. Далее, этот сигнал поступает на еще один элемент ИЛИ, который может пропускать через себя логический сигнал только в случае установки в единицу специального разрешающего сигнала (Channel 3 EN bit). Тут происходит следующая вещь: запрос DMA может формироваться либо от периферийных устройств, подключенных к этому каналу, либо битом MEM2MEM. MEM2MEM используется в том случае, если нам не нужно ждать какого-либо запроса от периферии для передачи данных, например, при копировании одной области памяти в другую. С этим, думаю, все ясно. Есть еще вот такая таблица, в ней все то же самое, только в другом формате:

Теперь идем в раздел с SPI. В регистре SPI_CR2 есть два интересных бита: TXDMAEN и RXDMAEN:

Если установлен бит TXDMAEN, то при установки флага TXE (буфер передатчика пуст), SPI отправляет в DMA запрос SPIx_TX, а если установлен RXDMAEN, то SPI отправляет запрос SPIx_RX при установке флага RXNE (буфер приемника не пуст). Для SPI1 это будут запросы SPI1_TX и SPI1_RX.

Команды, поддерживаемые SD-картами

Каждая команда для отправки на SD-карту состоит из шести байтов. Первый байт всегда является кодом команды, следующие четыре байта являются его аргументом. В конце отправляется байт контрольной суммы CRC.

В то время как сумма CRC проверяется в рабочем режиме с помощью интерфейса SDBus, контрольная сумма игнорируется картой при обмене данными через шину SPI. Только при отправке команды CMD0, переключающей режим работы с SDBus на SPI, требуется байт CRC. Его не нужно рассчитывать каким-либо образом, поскольку он является фиксированным значением и равен 0x95.

В табл. 1 указаны несколько команд при работе с шиной SPI с описанием аргументов. В дополнение к стандартным командам CMD карты SD также могут использовать так называемые прикладные команды (ACMD). Для отправки команды необходимо сначала отправить команду CMD55, информирующую SD-карту о том, что следующей командой будет ACMD.

Табл. 1. Команды, поддерживаемые SD-картами в режиме SPIf

команда описание
CMD0 сбрасывает карту, позволяет включить режим шины SPI
CMD12 принудительное завершение передачи множества блоков данных
CMD16 настройка длины блока данных для чтения / записи
CMD17 чтение блока памяти длины, указанной CMD16
CMD24 запись блока памяти с длиной, указанной CMD16
CMD32 адрес первого удаляемого блока передается в аргументе
CMD33 адрес последнего удаляемого блока передается в аргументе
CMD38 удаляет блоки, обозначенные CMD32 и CMD33

А еще быстрее можно?

Операция Время
Передача запроса от драйвера MSC до драйвера карты (с использованием очереди запросов) <100мкс
Отправка карте команды на чтение 70мкс
Ожидание готовности карты 500-1000 мкс
Чтение одного блока с карты 280 мкс
Передача ответа назад в драйвер MSC <100 мкс
Операция Время
Отправка карте команды на запись 70мкс
Ожидание готовности карты 1-5мс
Запись одного блока на карту 0.4-1.2мс
  • Во-первых, SPI это явно не родной интерфейс для SD карт. Даже крутые карты тупят на самых обычных операциях. Тут есть смысл смотреть в сторону SDIO (я уже забрал на почте пакетик с STM32F103RCT6 — там есть поддержка SDIO из коробки)
  • Во-вторых, карта карте рознь. Нужно будет искать ту единственную. Хотя при подключении через SDIO это будет не так критично
  • В-третьих, при наличии достаточного количества памяти можно будет переключиться на чтение блоками бОльшего размера (скажем 4к). Тогда длинная задержка вначале чтения/записи будет нивелироваться большой скоростью передачи. Пока 20кб памяти моего контроллера (STM32F103C8T6) забиты почти под завязку и даже 512 байт для двойной буферизации я выкроил с трудом

Модуль FatFs

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

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

Одним из таких общедоступных инструментов является модуль FatFs, задачей которого является создание моста между физическим уровнем (носителем данных) и приложением, работающим на микроконтроллере. Подробную информацию о FatFs можно найти на сайте автора. Роль модуля FatFs показана на рис. 3.

Рис. 3. Расположение модуля FatFs в программном проекте

Рис. 4. Структура файлов FatFs

Сам модуль FatFs написан на языке C. Файлы, необходимые для правильной работы FatFs, показаны на рис. 4 в виде дерева, скопированного из проекта с использованием файловой системы FAT. Теоретически для правильной работы модуля FatFs требуется наличие часов реального времени (RTC) во встроенной системе. Это требование можно легко обойти, введя фиксированные значения вместо даты и времени.

Bootloader в режиме USB Mass Storage

Bootloader в режиме USB Mass Storagehttp://blog.myelectronics.com.ua/stm32-usb-mass-storage-bootloader/

Поясню. У нас всего 64 Кб, пусть 10Кб займет Bootloader, остается 54Кб. Половину отдадим под Mass Storage, это будет 27Кб. Из этого объема FAT12 откусит 20Кб, останутся 7Кб под файл прошивки. То есть на Mass Storage не влезет файл больше 7Кб. Если оптимизировать распределение памяти и отдать под Mass Storage 38Кб, тогда под программы останется 16 Кб. Это для нас тоже не приемлемо. Поэтому такое решение для нашего микроконтроллера, с небольшим объемом Flash-а, не подходит. Но поблагодарим автора! Именно фрагменты этого проекта я использовал для создания примера Bootloader-а описанного в этой статье.
Дальнейшие поиски привели меня к Bootloader-у описанного в статье http://easyelectronics.ru/proshivka-arm-cortex-m3-na-primere-stm32-i-lpc1300.html Шикарный замысел, но реализация полностью не приемлема. Контроллер действительно видно как MASS STORAGE диск, на него вроде бы можно залить новую прошивку, но корректно это происходит только когда файл копировать программой FAR. И только FAR! Удивительно, но простое копирование файла происходит по-разному если это делать разными файл-менеджерами. Операционная система Ubuntu вообще не смогла смонтировать устройство. Поправить что-то в этом Bootloader-е нельзя, поэтому эта реализация тоже была отвергнута. Пришлось писать свой Bootloader.
Почему именно USB Mass Storage? Скажем, я делаю не коммерческий проект и хочу чтобы для обновления прошивки конечным пользователям не нужно было искать специфический софт и драйверы. Для работы с Mass Storage устройствами операционные системы используют стандартные драйверы, поэтому с драйверами проблем вообще нет. Обновление прошивки может происходить на разных операционных системах поэтому желательно чтобы программы для обновления прошивки вообще не надо было, или использовались стандартные решения. В этом случае простое копирование файла — наиболее красивое решение.
Забегая вперед скажу, что не все так просто. Мне пока не удалось реализовать этот замысел в идеальном виде. Поэтому, желательно чтобы процедура заливки прошивки была более или менее стандартной и, как я сказал, можно было обойтись стандартными командами или стандартным программным обеспечением.
Продолжаем разбираться …

Прокачиваем драйвер MSC

протокол USB предоставляет такой механизм прямо из коробки

Транзакция SCSI: Read(10) LUN: 0x00 (LBA: 0x00000000, Len: 1)Хост отправляет команду на чтение. Со стороны микроконтроллера вызывается функция MSC_BOT_DataOut()
Команда обрабатывается по цепочке функций MSC_BOT_DataOut() -> MSC_BOT_CBW_Decode() -> SCSI_ProcessCmd() -> SCSI_Read10()
Поскольку драйвер находится в состоянии hmsc->bot_state == USBD_BOT_IDLE, то готовится процедура чтения: проверяются параметры команды, запоминается сколько всего блоков нужно прочитать, после чего передается управление функции SCSI_ProcessRead() с просьбой прочитать первый блок
Функция SCSI_ProcessRead() читает данные в синхронном режиме. Именно тут микроконтроллер занят бОльшую часть времени.
Когда данные получены они перекладываются (с помощью функции USBD_LL_Transmit() ) в выходной буфер конечной точки MSC_IN, чтобы хост мог их забрать
Драйвер переходит в состояние hmsc->bot_state = USBD_BOT_DATA_IN

Транзакция SCSI: Data InХост забирает данные из выходного буфера микроконтроллера пакетами по 64 байта (максимальный рекомендованный размер пакета для USB Full Speed устройств). Все это происходит на самом низком уровне в ядре USB, драйвер MSC в этом не участвует
Когда хост забрал все данные возникает событие Data In. Управление передается в функцию MSC_BOT_DataIn()

Акцентирую Ваше внимание, что эта функция вызывается после реальной отправки данных.
Драйвер находится в состоянии hmsc->bot_state == USBD_BOT_DATA_IN, что означает мы все еще в режиме чтения данных.
Если еще не все заказанные блоки прочитаны – стартуем чтение очередного кусочка и ждем завершения, перекладываем в выходной буфер и ждем пока хост заберет данные. Алгоритм повторяется
Если все блоки прочитаны, то драйвер переключается в состояние USBD_BOT_LAST_DATA_IN для отправки финального статуса команды

Транзакция SCSI: ResponseК этому моменту данные посылки уже отправлены
драйвер лишь получает об этом уведомление в переходит в состояние USBD_BOT_IDLE

  • Транзакция SCSI: Read(10) LUN: 0x00 (LBA: 0x00000000, Len: 1)
    • Микроконтроллер получает команду на чтение, проверяет все параметры, запоминает количество блоков, которые нужно прочитать
    • Микроконтроллер стартует чтение первого блока в асинхронном режиме
    • Выходим из прерывания не дожидаясь окончания чтения
  • Когда чтение закончилось вызывается коллбек
    • Прочитанные данные отправляются в выходной буфер
    • Хост их вычитывает без участия драйвера MSC
  • Транзакция SCSI: Data In
    • Вызывается коллбек функция DataIn(), которая сигнализирует о том, что хост забрал данные и можно делать следующее чтение
    • Запускаем чтение следующего блока. Алгоритм повторяется начиная с обратного вызова о завершении чтения
    • Если все блоки прочитаны – отправляем пакет статуса
  • Транзакция SCSI: Response
    • К этому моменту данные посылки уже отправлены
    • Готовимся к следующей транзакции

Что у меня получилось

Я решил по-максимуму задействовать возможности периферии микроконтроллера, но вместе с тем не впадать в пучину предварительных оптимизаций. Читаемость и поддерживаемость кода для меня гораздо важнее, чем незначительный прирост производительности. Тем более, что с производительностью и так все вышло очень хорошо.

Что реализовано:

  • три независимых последовательных порта;
  • поддержка аппаратного контроля потока (RTS/CTS) на двух из трех портов;
  • поддержка управляющих сигналов DSR/DTR/DCD/RI;
  • поддержка 7 и 8-битной длины слова;
  • поддержка контроля четности;
  • 1, 1.5 и 2 стоповых бита;
  • совместимость со стандартными драйверами Linux, macOS и Windows;
  • подписанный INF файл для Windows XP, 7 и 8;
  • поддержка произвольных скоростей (более 2 Мбит/с);
  • сигнал TXA для управления трансиверами RS-485 (DE, /RE);
  • DMA на передачу и прием данных USART;
  • встроенный командный интерпретатор для конфигурации;
  • нет зависимостей от сторонних библиотек кроме CMSIS;
  • проект с открытым исходным кодом, лицензия MIT;

Командный интерпретатор позволяет настраивать следующие параметры:

  • тип выхода: двухтактный, открытый сток;
  • тип подтяжки входных линий: вверх, вниз, плавающая;
  • инверсия для управляющих сигналов: активный высокий/низкий;

Командный интерпретатор активируется на первом CDC порту при подключении пина PB5 к земле, поддерживает часть управляющих последовательностей ANSI (стрелочки, backspace), и принимает вполне дружелюбные на вид команды:

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

Распиновка вышла следующей:

Signal Direction UART1 UART2 UART3
RX IN PA10 PA3 PB11
TX OUT PA9 PA2 PB10
RTS OUT N/A PA1 PB14
CTS IN N/A PA0 PB13
DSR IN PB7 PB4 PB6
DTR OUT PA4 PA5 PA6
DCD IN PB15 PB8 PB9
RI IN PB3 PB12 PA8
TXA OUT PB0 PB1 PA7

Пины, выделенные жирным шрифтом, являются толерантными к 5 В.

Сигнал TXA (TX Active) служит для управления микросхемами трансиверов RS-485 (DE, /RE). TXA активен во время передачи данных и переключается в неактивное состояние не более чем за 0.6 мкс после завершения передачи. Это соответствует спецификациям RS-485 на скоростях до 920 кБод c почти двукратным запасом по времени переключения.

К сожалению, реализовать RTS/CTS на UART1 не вышло из-за того, что соответсвующие пины заняты сигналами USB. Можно было вывести RTS на какой-нибудь другой пин, поскольку RTS управляется программно, в зависимости от степени заполнения кольцевых буферов на прием, но порт c RTS и без CTS мне показался странной штукой и я решил, что так делать не надо.

Проект написан на языке C, и подразумевает использование arm-none-eabi-gcc для сборки. Я использовал специфичный для GCC синтаксис атрибутов и расширения языка С. Совместимость проекта с проприетарными компиляторами меня не интересует, но если кто-то считает это важным, то я готов принять соответствующий пул-реквест.

В результате у меня получилось удобное и мощное устройство которое полностью закрывает все мои потребности в последовательных портах. STM32 Blue Pill можно использовать как самостоятельно, так и в составе схем обеспечивающих согласование уровней и развязку. Возможность настройки сигнальных линий позволяет упростить разработку таких схем.

MSC — запоминающее устройство

SdFat

  • Пользователю предоставляется класс File, через который можно создавать/открывать файлы, читать и писать данные. Клиентский код не парится взаимодействием с носителем информации и тонкостями файловой системы.
  • Класс Volume занимается всей кухней по обслуживанию файловой системы, каталога, кластеров, FAT и такого прочего. Общение с носителем данных делегируется в нижележащие уровни.
  • Драйвер SD карты — этот компонент знает как общаться с картой, какие ей слать команды и какие слушать ответы. Библиотека предоставляет несколько видов драйверов для карт подключенных по SPI и SDIO. Теоретически можно подставить свой драйвер, например, для RAM диска.
  • Вышележащие слои кроссплатформенные, они ничего не знают о том как именно данные будут писаться на карту или читаться с нее. Это позволяет собирать библиотеку под разные платформу (как Ардуино, так и другие). Для конкретной платформы или микроконтроллера можно написать драйвер, который будет реализовывать передачу данных через необходимый интерфейс. По умолчанию библиотека предоставляет несколько драйверов, в т.ч. для ардуиновского SPI, но я заморочился и написал свой драйвер с преферансом и поэтессами передачей через DMA на основе HAL.
  • Наконец, HAL обеспечивает работу с регистрами конкретного микроконтроллера

спецификации вот линка

Софт

Перейдем теперь к программной составляющей. Нам понадобится драйвер для программатора и управляющая софтина, через через которую мы будем загружать прошивку в микроконтроллер. Для этого нам нужно на официальном сайте www.st.com скачать пакет STM32 ST-LINK utility. А что нужно сделать перед скачиванием чего-нибудь с www.st.com? Правильно! Надо у них зарегистрироваться!!!

Кину ссылку на архив в конце статьи.

Итак, мы добыли архив с пакетом STM32 ST-LINK Utility. После установки подключаем программатор к ПК по USB и подключаем питание к отладочной плате. Если драйвера на программатор не установились автоматически, то идем в диспетчер устройств, там находим наш STM32 STLink, выбираем Обновить драйвер->Выполнить поиск на этом компьютере. После этого все должно заработать. Затем, из меню «Пуск» запускаем программу «STM32 ST-LINK Utility«:

Выглядит интерфейс ST-LINK Utility вот так:

Далее, нам необходимо произвести небольшие настройки. В меню выбираем Target->Settings…

Открывается вот такое окошко:

Выставляем настройки, как на скриншоте и нажимаем OK. После этого программатор автоматически подключится к прошиваемому микроконтроллеру. Пробежимся по кнопкам управления:

«Подключиться к микроконтроллеру» — выполняет подключение программатора к МК. При этом работа прошивки останавливается. После этого можно выполнять дальнейшие манипуляции с flash-памятью МК.

«Отключиться от микроконтроллера» — после того, как мы все сделали, нажимаем на эту кнопку и SL-Link отключается от МК, при этом запускается загруженная во flash-память прошивка.

«Очистить чип» — при нажатии на эту кнопку стирается вся flash-память микроконтроллера. Это необходимо делать перед загрузкой другой прошивки в МК.

Для того, чтобы прошить наш .hex или .bin файл в МК нужно сделать следующее. В меню выбираем Target->Programm…

После этого у нас открывается окошко выбора файла прошивки. Выбираем нужный нам файл, после чего появляется вот такое окно:

Здесь нам нужно нажать Start чтобы запустить процесс. После того, как прошивка была загружена в МК, нажимаем на кнопку «Отключиться от микроконтроллера«.

Те, кто раньше работал с микроконтроллерами AVR знают о такой вещи как фьюз-биты. Если в AVR-ках неправильно их выставить, то прошивка может работать некорректно. Для вас хорошая новость: в STM32 фьюз-битов нет! Достаточно просто залить в МК файл с управляющей программой и все будет работать.

Ну что ж, на этом, пожалуй, закончу, всем кто дочитал, спасибо за внимание Продолжение тут

Описание работы USB

  • USB устройства подключаются к Хосту (чаще всего, это — компьютер). Хост — главный, он всем управляет.
  • USB устройство не может быть инициатором передачи данных. То есть, оно сидит и молчит, пока его не спросят. Спросили или прислали данные, — оно ответило или приняло данные и замолчало.
  • USB устройство имеет уникальный идентификатор. Каждое USB устройство может иметь несколько конечных точек, каждая из которых имеет уникальный адрес. Именно через конечные точки передаются данные или команды.
  • USB устройство имеет Дескриптор. Это массив данных, в котором содержится описание устройства. Благодаря дескриптору, операционная система получает информацию о USB устройстве и использует для общения с ним соответствующий драйвер.
  • Хост идентифицирует USB-устройство по ID вендора и ID продукта (Vendor ID — VID и Product ID — PID)

http://microtechnics.ru/osnovy-interfejsa-usb/http://webhamster.ru/mytetrashare/index/mtb0/1410507212bb4zf8gacjhttp://radiokot.ru/circuit/digital/pcmod/63/http://microtechnics.ru/mikrokontroller-stm32-i-usb/http://microtechnics.ru/stm32-peredacha-dannyx-po-usb/

Немного теории

Usb in a nutshellпереводUSB Made Simpleспецификации для конкретных классов USB устройств

  • Дескриптор устройства (Device Descriptor) — описывает устройство в целом, его название, производитель, серийный номер. Строковые данные описываются отдельными строковыми дескрипторами (String Descriptor)
  • Дескриптор конфигурации (Configuration Descriptor) — устройство может иметь одну или несколько конфигураций. Каждая конфигурация определяет скорость общения с устройством, набор интерфейсов и параметры питания. Так, например, ноутбук, который работает от батареи, может попросить устройство (выбрать конфигурацию) использовать более низкую скорость обмена и переключиться на собственный источник питания (вместо ноутбучной батареи). Разумеется это работает только если устройство предоставляет такую конфигурацию.
  • Дескриптор интерфейса (Interface descriptor) — описывает интерфейс общения с устройством. Интерфейсов может быть несколько. Например разные функции (MSC, CDC, HID) будут реализовывать свои интерфейсы. Некоторые функции (например CDC или DFU) реализуют сразу несколько интерфейсов для своей работы. В нашем случае композитного устройства нам потребуется реализовать сразу несколько интерфейсов от разных функций и заставить их ужиться друг с другом.
  • Дескриптор конечной точки (Endpoint descriptor) — описывает канал связи в рамках конкретного интерфейса, задает размер пакета, описывает параметры прерываний. Используя конечные точки мы будем получать и принимать данные.
  • Есть еще куча разных дескрипторов, которые описывают отдельные аспекты конкретных интерфейсов

Файловая система FAT

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

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

Ключевым элементом файловой системы FAT (File Allocation Table) является, в соответствии с ее именем, таблица размещения файлов. Файловая система FAT представлена ​​четырьмя разновидностями, во встроенных системах обычно используется две, в зависимости от размера носителя и требований к приложениям, это будет FAT16 или FAT32.

Рис. 2. Разделение носителя информации в системе FAT

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

К этой информации относятся, в частности: тип и размер разделов, размер сектора и количество секторов в кластере. За зарезервированной областью находятся таблицы размещения файлов, которые являются основным источником информации о данных, сохраняемых на носителе. Обычно, помимо основной таблицы размещения, есть и ее копия. Четвертая область — это корневой каталог, который создается автоматически при создании файловой системы. Последний, пятый сектор — это область данных.

Отправка данных по SPI в режиме Master через DMA

Для того, чтобы передать массив данных через SPI с помощью DMA, нужно сделать следующее:

  • Включить тактирование SPI и DMA
  • Настроить нужным образом SPI
  • В регистре SPI_CR2 установить бит TXDMAEN

И в DMA:

  • Записать в регистр адреса периферии DMA_CPARx адрес регистра SPI_DR
  • Записать в регистр адреса памяти DMA_CMARx адрес массива для отправки в SPI
  • Записать в регистр DMA_CNDTRx количество передаваемых элементов
  • Настроить канал DMA
  • Включить канал DMA

Поехали кодить!

Для начала идет инициализация SPI. Вот полный код функции:

void SPIInit(void)
{
  RCC->APB2ENR |= RCC_APB2ENR_SPI1EN; //Включаем тактирование SPI1
  RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; //включаем тактирование порта GPIOA
  RCC->AHBENR |= RCC_AHBENR_DMA1EN; //Включаем тактирование DMA1
  
  
  //Настройка GPIO
  
  //PA7 - MOSI
  //PA6 - MISO
  //PA5 - SCK
  //Для начала сбрасываем все конфигурационные биты в нули
  GPIOA->CRL &= ~(GPIO_CRL_CNF5_Msk | GPIO_CRL_MODE5_Msk 
                | GPIO_CRL_CNF6_Msk | GPIO_CRL_MODE6_Msk
                | GPIO_CRL_CNF7_Msk | GPIO_CRL_MODE7_Msk);
  
  //Настраиваем
  //SCK: MODE5 = 0x03 (11b); CNF5 = 0x02 (10b)
  GPIOA->CRL |= (0x02<<GPIO_CRL_CNF5_Pos) | (0x03<<GPIO_CRL_MODE5_Pos);
  
  //MISO: MODE6 = 0x00 (00b); CNF6 = 0x01 (01b)
  GPIOA->CRL |= (0x01<<GPIO_CRL_CNF6_Pos) | (0x00<<GPIO_CRL_MODE6_Pos);
  
  //MOSI: MODE7 = 0x03 (11b); CNF7 = 0x02 (10b)
  GPIOA->CRL |= (0x02<<GPIO_CRL_CNF7_Pos) | (0x03<<GPIO_CRL_MODE7_Pos);
  
  
  //Настройка SPI
  SPI1->CR1 = 0<<SPI_CR1_DFF_Pos  //Размер кадра 8 бит
    | 0<<SPI_CR1_LSBFIRST_Pos     //MSB first
    | 1<<SPI_CR1_SSM_Pos          //Программное управление SS
    | 1<<SPI_CR1_SSI_Pos          //SS в высоком состоянии
    | 0x04<<SPI_CR1_BR_Pos        //Скорость передачи: F_PCLK/32
    | 1<<SPI_CR1_MSTR_Pos         //Режим Master (ведущий)
    | 0<<SPI_CR1_CPOL_Pos | 0<<SPI_CR1_CPHA_Pos; //Режим работы SPI: 0
  
  
  SPI1->CR2 |= 1<<SPI_CR2_TXDMAEN_Pos; //Разрешаем запрос к DMA
  SPI1->CR1 |= 1<<SPI_CR1_SPE_Pos; //Включаем SPI
}

GPIOSPISPIDMA1RCC->AHBENR |= RCC_AHBENR_DMA1ENDMASPI1->CR2 |= 1<<SPI_CR2_TXDMAEN_Pos

Далее, переходим к функции передачи данных. Назовем ее SPI_Send():

void SPI_Send(uint8_t *data, uint16_t len)
{
 ...
}

Запрос на передачу данных от SPI1 у нас висит на 3-м канале DMA (см. картинку вверху). Перед началом любых манипуляций с каналом, надо убедиться, что он отключен:

  //отключаем канал DMA после предыдущей передачи данных
  DMA1_Channel3->CCR &= ~(1 << DMA_CCR_EN_Pos);

DMA

  DMA1_Channel3->CPAR = (uint32_t)(&SPI1->DR); //заносим адрес регистра DR в CPAR
  DMA1_Channel3->CMAR = (uint32_t)data; //заносим адрес данных в регистр CMAR
  DMA1_Channel3->CNDTR = len; //количество передаваемых данных
  //Настройка канала DMA
  DMA1_Channel3->CCR = 0 << DMA_CCR_MEM2MEM_Pos //режим MEM2MEM отключен
    | 0x00 << DMA_CCR_PL_Pos //приоритет низкий
    | 0x00 << DMA_CCR_MSIZE_Pos //разрядность данных в памяти 8 бит
    | 0x01 << DMA_CCR_PSIZE_Pos //разрядность регистра данных 16 бит 
    | 1 << DMA_CCR_MINC_Pos //Включить инкремент адреса памяти
    | 0 << DMA_CCR_PINC_Pos //Инкремент адреса периферии отключен
    | 0 << DMA_CCR_CIRC_Pos //кольцевой режим отключен
    | 1 << DMA_CCR_DIR_Pos;  //1 - из памяти в периферию

MEM2MEMMSIZE=0x00SPIPSIZE=0x01SPIMINC=1PINC=0CIRC=0DIR=1

Все Теперь, чтоб процесс пошел, нам нужно всего лишь включить данный канал DMA:

  DMA1_Channel3->CCR |= 1 << DMA_CCR_EN_Pos; //включаем передачу данных
void SPI_Send(uint8_t *data, uint16_t len)
{
  //отключаем канал DMA после предыдущей передачи данных
  DMA1_Channel3->CCR &= ~(1 << DMA_CCR_EN_Pos); 
  
  DMA1_Channel3->CPAR = (uint32_t)(&SPI1->DR); //заносим адрес регистра DR в CPAR
  DMA1_Channel3->CMAR = (uint32_t)data; //заносим адрес данных в регистр CMAR
  DMA1_Channel3->CNDTR = len; //количество передаваемых данных
  
  //Настройка канала DMA
  DMA1_Channel3->CCR = 0 << DMA_CCR_MEM2MEM_Pos //режим MEM2MEM отключен
    | 0x00 << DMA_CCR_PL_Pos //приоритет низкий
    | 0x00 << DMA_CCR_MSIZE_Pos //разрядность данных в памяти 8 бит
    | 0x01 << DMA_CCR_PSIZE_Pos //разрядность регистра данных 16 бит 
    | 1 << DMA_CCR_MINC_Pos //Включить инкремент адреса памяти
    | 0 << DMA_CCR_PINC_Pos //Инкремент адреса периферии отключен
    | 0 << DMA_CCR_CIRC_Pos //кольцевой режим отключен
    | 1 << DMA_CCR_DIR_Pos;  //1 - из памяти в периферию
  
  DMA1_Channel3->CCR |= 1 << DMA_CCR_EN_Pos; //включаем передачу данных
}
uint8_t data;

void main()
{
  for(int i=0; i<sizeof(data); i++)
  {
    data = i+1;
  }
  
  SPIInit();
  SPI_Send(data, sizeof(data));
  
  for(;;)
  {
  }
}

SPI1DMA

Резюме

Как это выглядит для разработчика

  • Заливаем обычным программатором в микроконтроллер Bootloader как обычную программу;
  • Готовим и компилируем проект с учетом адреса начиная с которой будет находиться основная программа (зависит от размера Bootloader-а);
  • Готовый bin-файл заливаем в микроконтроллер как это будет делать конечный пользователь. Смотри ниже …

Как это выглядит для пользователя

  • Получает от разработчика новый bin-файл;
  • Подключает устройство USB-кабелем к компьютеру при этом удерживает нажатой кнопку. Это может быть любая кнопка управления вашим устройством.
    Если в устройстве не используются кнопки, это может быть какой-то переключатель или перемычка, которые дадут сигнал Bootloader-у что надо перейти в режим обновления прошивки. В примере Bootloader-а надо замкнуть PB1 на землю. Bootloader переходит в режим USB Mass Storage, операционная система видит диск размером 54Кб;
  • с помощью программы Win32DiskImager (Windows) или dd (для Ubuntu) пользователь записывает bin-файл на диск;
  • отключает устройство от компьютера и включает. Новая прошивка работает.

Смотри также:

  • 1. STM32. Программирование STM32F103. Тестовая плата. Прошивка через последовательный порт и через ST-Link программатор
  • 2. STM32. Программирование. IDE для STM32
  • 3. STM32. Программирование STM32F103. GPIO
  • 4. STM32. Программирование STM32F103. Тактирование
  • 5. STM32. Программирование STM32F103. USART
  • 6. STM32. Программирование STM32F103. NVIC
  • 7. STM32. Программирование STM32F103. ADC
  • 8. STM32. Программирование STM32F103. DMA
  • 9. STM32. Программирование STM32F103. TIMER
  • 10. STM32. Программирование STM32F103. TIMER. Захват сигнала
  • 11. STM32. Программирование STM32F103. TIMER. Encoder
  • 12. STM32. Программирование STM32F103. TIMER. PWM
  • 13. STM32. Программирование STM32F103. EXTI
  • 14. STM32. Программирование STM32F103. RTC
  • 15. STM32. Программирование STM32F103. BKP
  • 16. STM32. Программирование STM32F103. Flash
  • 17. STM32. Программирование STM32F103. Watchdog
  • 18. STM32. Программирование STM32F103. Remap
  • 19. STM32. Программирование STM32F103. I2C Master
  • 20. STM32. Программирование STM32F103. I2C Slave
  • 21. STM32. Программирование STM32F103. USB
  • 22. STM32. Программирование STM32F103. PWR
  • 23. STM32. Программирование STM32F103. Option bytes
  • 24. STM32. Программирование STM32F103. Bootloader
  • STM32. Скачать примеры
  • System Workbench for STM32 Установка на Ubuntu
  • Keil uVision5 – IDE для STM32
  • IAR Workbench – IDE для STM32
  • Управление бесколлекторным двигателем постоянного тока (BLDC) с помощью STM32
  • Управление PMSM с помощью STM32

Главная идея нашего USB Mass Storage Bootloader

bin0x08002800binbinHEXbin-файлddWin32DiskImagerST-LINK Utilitybin

Команда для заливки на Mass Storage bin-файла под Ubuntu:

Итак, наш USB Mass Storage, который теперь играет роль Bootloader-а, прошивку уже обновляет. Теперь осталось доделать USB Mass Storage пример таким образом, чтобы он мог передавать управление залитой программе. Я это сделал с помощью кнопки. Когда мы подключаем микроконтроллер к компьютеру USB кабелем, и при этом нога PB1 замкнута на землю, контроллер стартует как USB Mass Storage устройство и можно заливать прошивку. Если во время старта PB1 на землю не замкнута, а висит в воздухе, или подключена к +, микроконтроллер передает управление залитой прошивке.
Вот как выглядит код нашего Bootloader-а (доработанного примера USB Mass Storage):

https://github.com/avislab/STM32F103/tree/master/Example_Bootloader

Управление сигналом Slave Select

Как говорилось ранее, ведомое устройство в SPI шине не имеет адреса, по которому его можно было бы идентифицировать, но оно начинает обмен данными и продолжает его пока сигнал Slave Select (SS) находится в низком логическом уровне LOW. STM32 микроконтроллеры предоставляют два режима управления сигналом SS или NSS, как он называется в документации ST. Давайте проанализируем их.

  • Программный режим NSS (NSS software mode): сигнал SS управляется из программы и используется любой свободный GPIO пин.
  • Аппаратный режим NSS (NSS hardware mode): для работы с сигналом SS используется специальный IO пин, который управляется SPI интерфейсом. В данном режиме доступны две конфигурации NSS выхода:— NSS выход включен: данная настройка используется только, когда устройство работает в режиме master. NSS сигнал устанавливается в LOW, когда master начинает передачу и остается в уровне LOW пока SPI отключен. Стоит заметить, что этот режим возможно использовать, когда лишь одно ведомое (slave) устройство в шине и ее SS пин подключен к сигнальной линии NSS.— NSS выход отключен: эта конфигурация допускает использование режима multi-master для устройств, работающих в режиме master. Для ведомых (slave) устройств NSS пин работает, как простой вход, т.е. slave выбран, когда NSS в LOW.

Поддержка SD-карты

Как упоминалось ранее, процессор обменивается данными с SD-картой через интерфейс SPI. На рис. 4 показан рекомендуемый способ подключения разъема SD-карты к микроконтроллеру.

Рис. 4. Электрическая схема подключения SD-карты к микроконтроллеру

Блок-схема приложения загрузчика показана на рисунке 5. Библиотека FAT, используемая в проекте, дает возможность читать файлы с SD-карты и поддерживает длинные имена.

Рис. 5. Блок-схема приложения, выполняющего роль загрузчика

Как показано в примере 1 функция FAT_ConnectEvent присоединена к библиотеке FAT и вызывается когда на SD-карте обнаруживается раздел FAT16/32. В нем открывается корневой каталог, затем проверяется, есть ли на карте каталог \stm32f10x и файл file_name.txt.

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

Пример 1. Функция FAT_ConnectEvent

void FAT_ConnectEvent (FAT_Desc * FAT32D) {
FILE file_desc;
uint32_t result;
uint32_t address_shift = 0;
if (! sfopen (&amp;file_desc, (const char *) FAT32D- & gt; FAT_name_string_descriptor, "r")) {
return;
}
if (! sfopen (&amp;file_desc, "stm32f10x / file_name.txt", "r")) {
return;
}
fread (boot_read_buffer, 1, boot_page_size, &amp;file_desc);
fclose (&amp;file_desc);
if (! sfopen (&amp;file_desc, (const char *) boot_read_buffer, "r")) {
return;
}
FlashUnlock ();
do {
if (FlashErasePage (DEF_APP_ADDRESS + address_shift))
{
break;
}
result = fread (boot_read_buffer, 1, boot_page_size, &amp;file_desc);
if (FlashWritePage (DEF_APP_ADDRESS + address_shift, boot_read_buffer, result))
{
break;
}
address_shift + = boot_page_size;
} while (result == boot_page_size);
FlashLock ();
fclose (&amp;file_desc);
}