Оглавление
- Главная идея нашего USB Mass Storage Bootloader
- Внутренняя поддержка Flash
- HAL_SPI модуль
- Файловая система FAT
- Что у меня получилось
- Немного теории
- Описание SPI
- Общие сведения о запросах DMA
- Резюме
- Описание работы USB
- Прокачиваем драйвер MSC
- USB Mass Storage пример
- Отправка данных по SPI в режиме Master через DMA
Главная идея нашего 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
Внутренняя поддержка Flash
Производитель внедрил в микроконтроллеры STM32 специальный драйвер Flash Program / Erase Controller (FPEC), который можно использовать для изменения содержимого флэш-памяти. В микроконтроллерах STM32 флэш-память организована в виде страниц, размер которых составляет 1 КБ или 2 КБ, в зависимости от общего объема памяти.
Флэш-память находится в адресном пространстве процессора, начиная с адреса 0x08000000. Подробное описание того, как обрабатывать флэш-память в микроконтроллерах STM32, можно найти в документе PM0042 под названием «Программирование флэш-памяти STM32F10xxx», который доступен на веб-сайте STMicroelectronics.
После сброса микроконтроллера FPEC защищает содержимое флэш-памяти от сохранения, поэтому нет возможности случайного повреждения ее содержимого. Чтобы внести какие-либо изменения, вы должны сначала разблокировать память. Это делается путем ввода двух значений в реестр Flash_KEYR:
KEY1 = 0x45670123
а затем
KEY2 = 0xCDEF89AB
С этого момента могут выполняться операции стирания и записи памяти, пока не произойдет ошибка, которая блокирует доступ к памяти до следующей разблокировки. Алгоритм записи во флэш-память через FPEC показан на рис. 2.
Рис. 2. Алгоритм программирования флэш-памяти в микроконтроллерах STM32
Запись в память производится 2 байтами. Сначала установите бит PG в реестре Flash_CR, информируя контроллер о том, что мы будем программировать. После этого действия вы можете изменить содержимое памяти, что требует ввода 2 байтов по адресу, где они должны быть размещены в памяти. Затем дождитесь окончания этой операции и убедитесь, что запись была произведена правильно, для чего необходимо прочитать записанные 2 байта и убедиться в их правильности — это можно сделать путем сравнения. Если ошибок не нет, то вы можете записать следующие данные таким же образом. После сохранения всей страницы вы должны удалить бит PG.
Содержимое флэш-памяти в микроконтроллерах STM32 может быть удалено двумя способами: страница за страницей или все ее содержимое. В случае загрузчика память будет удаляться постранично. Постраничный алгоритм удаления показан на рис. 3.
Рис. 3. Алгоритм стирания флэш-памяти в микроконтроллерах STM32 (постраничный)
Мы начинаем стирать содержимое памяти, устанавливая бит PER, информирующий FPEC в регистре Flash_CR о том, что страница флэш-памяти будет удалена. Затем введите адрес удаляемой страницы в регистр Flash_AR и установите бит STRT в регистр Flash_CR. В этот момент начинается физическое удаление содержимого указанной страницы памяти. На следующем этапе мы ожидаем его завершения, после чего проверяем, была ли страница удалена правильно, читая и проверяя ее содержимое.
HAL_SPI модуль
Для программирования SPI HAL определяет С структуру SPI_HandleTypeDef:
Давайте проанализируем наиболее важные поля данной структуры.
- Instance: указатель на дескриптор SPI, который мы будем использовать. Например, SPI1 — это дескриптор первого SPI интерфейса.
- Init: объект C структуры SPI_InitTypeDef, которая используется для настройки интерфейса, далее мы рассмотрим ее более подробно.
- ptxBuffPtr, pRxBuffPtr: указатели на временные буферы для принимаемых и отправляемых данных SPI. Они используются, когда SPI работает в режиме прерывания и не могут быть изменены из программы пользователя.
- hdmatx, hdmarx: указатели на объекты DMA_HandleTypeDef структуры, используемой, когда SPI работает в режиме с DMA.
Настройка SPI производится с использованием объекта структуры SPI_InitTypeDef:
- Mode: параметр, определяющий в каком режиме, master или slave работает SPI интерфейс. Может принимать два значения SPI_MODE_MASTER и SPI_MODE_SLAVE.
- Direction: параметр, задающий работу SPI интерфейса либо в 4-проводном режиме (2 отдельные линии для входных и выходных данных соответсвенно), либо в 3-проводном режиме (одна I/O линия). Может принимать следующие значения: SPI_DIRECTION_2LINES или полнодуплексный 4-проводной режим; SPI_DIRECTION_2LINES_RXONLY или полудуплексный 4-проводной режим; SPI_DIRECTION_1LINE или полудуплексный 3-проводной режим.
- DataSize: параметр, задающий размер данных, передаваемых по шине SPI. Может принимать два следующих значения: SPI_DATASIZE_8BIT и SPI_DATASIZE_16BIT.
- CLKPolarity: настройка SCK CPOL, принимает значения SPI_POLARITY_LOW (CPOL=0) и SPI_POLARITY_HIGH (CPOL=1).
- CLKPhase: настройка фазы тактового сигнала SCK, принимает также два значения SPI_PHASE_1EDGE (CPHA=0) и SPI_PHASE_2EDGE (CPHA=1).
- NSS: параметр, задающий поведение линии NSS. Принимает значения SPI_NSS_SOFT для программного управления NSS, SPI_NSS_HARD_INPUT и SPI_NSS_HARD_OUTPUT для настройки NSS сигнала в аппаратном режиме.
- BaudRatePrescaler: делитель частоты шины APB, определяет максимальную тактовую частоту линии SCK. Может принимать значения SPI_BAUDRATEPRESCALER_2, SPI_BAUDRATEPRESCALER_4,…,SPI_BAUDRATEPRESCALER_256.
- FirstBit: параметр, определяющий порядок передачи данных по шине SPI: SPI_FIRSTBIT_MSB или SPI_FIRSTBIT_LSB.
- TIMode: включение/выключение режима поддержки протокола TI. Значения: SPI_TIMODE_DISABLE или SPI_TIMODE_ENABLE.
- CRCCalculation и CRCPolynomial: во всех микроконтроллерах STM32 SPI периферия может аппаратно генерировать CRC контрольную сумму. Значение CRC может передаваться последним байтом в режиме передачи или автоматическая проверка на ошибку CRC может быть произведена с последним принятым байтом данных. Значение CRC вычисляется с использованием нечетного программируемого многочлена на каждом бите. Вычисление происходит на определяемом параметрами CPHA и CPOL фронте тактовой частоты каждой выборки. Вычисленное CRC проверяется автоматически в конце блока данных. Когда появляется ошибка между вычисленным значением CRC от принятых данных и CRC, переданной от передающего устройства, устанавливается состояние ошибки. CRC не доступно в режиме работы SPI с DMA в циклическом режиме. Для более подробной информации рекомендуется ознакомиться с референс мануалом на конкретный микроконтроллер.
Как обычно, для конфигурации SPI мы используем функцию:
HAL_StatusTypeDef HAL_SPI_Init (SPI_HandleTypeDef * hspi);
которая принимает в качестве входного параметра указатель на объект структуры SPI_HandleTypeDef, рассмотренной выше.
Файловая система FAT
С точки зрения файловой системы каждый носитель данных (жесткий диск, карта памяти) разделен на сектора и кластеры. Сектор — это наименьшее количество байтов, которое может быть записано или прочитано. Обычно размер сектора составляет 512 байт. Файлы сохраняются в пронумерованных кластерах.
Размер кластера зависит от файловой системы и носителя. Каждый кластер полностью выделен для данного файла. Это означает, что даже если файл намного меньше размера кластера, он все равно занимает столько же, сколько один кластер на диске.
Ключевым элементом файловой системы FAT (File Allocation Table) является, в соответствии с ее именем, таблица размещения файлов. Файловая система FAT представлена четырьмя разновидностями, во встроенных системах обычно используется две, в зависимости от размера носителя и требований к приложениям, это будет FAT16 или FAT32.
Рис. 2. Разделение носителя информации в системе FAT
Носитель данных в файловой системе FAT разделен на пять частей, все они показаны на рис. 2. Первая логическая часть носителя данных, расположенная в первом секторе, представляет собой зарезервированную область, которая содержит всю основную информацию о текущем разделе (носителе).
К этой информации относятся, в частности: тип и размер разделов, размер сектора и количество секторов в кластере. За зарезервированной областью находятся таблицы размещения файлов, которые являются основным источником информации о данных, сохраняемых на носителе. Обычно, помимо основной таблицы размещения, есть и ее копия. Четвертая область — это корневой каталог, который создается автоматически при создании файловой системы. Последний, пятый сектор — это область данных.
Что у меня получилось
Я решил по-максимуму задействовать возможности периферии микроконтроллера, но вместе с тем не впадать в пучину предварительных оптимизаций. Читаемость и поддерживаемость кода для меня гораздо важнее, чем незначительный прирост производительности. Тем более, что с производительностью и так все вышло очень хорошо.
Что реализовано:
- три независимых последовательных порта;
- поддержка аппаратного контроля потока (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 можно использовать как самостоятельно, так и в составе схем обеспечивающих согласование уровней и развязку. Возможность настройки сигнальных линий позволяет упростить разработку таких схем.
Немного теории
Usb in a nutshellпереводUSB Made Simpleспецификации для конкретных классов USB устройств
- Дескриптор устройства (Device Descriptor) — описывает устройство в целом, его название, производитель, серийный номер. Строковые данные описываются отдельными строковыми дескрипторами (String Descriptor)
- Дескриптор конфигурации (Configuration Descriptor) — устройство может иметь одну или несколько конфигураций. Каждая конфигурация определяет скорость общения с устройством, набор интерфейсов и параметры питания. Так, например, ноутбук, который работает от батареи, может попросить устройство (выбрать конфигурацию) использовать более низкую скорость обмена и переключиться на собственный источник питания (вместо ноутбучной батареи). Разумеется это работает только если устройство предоставляет такую конфигурацию.
- Дескриптор интерфейса (Interface descriptor) — описывает интерфейс общения с устройством. Интерфейсов может быть несколько. Например разные функции (MSC, CDC, HID) будут реализовывать свои интерфейсы. Некоторые функции (например CDC или DFU) реализуют сразу несколько интерфейсов для своей работы. В нашем случае композитного устройства нам потребуется реализовать сразу несколько интерфейсов от разных функций и заставить их ужиться друг с другом.
- Дескриптор конечной точки (Endpoint descriptor) — описывает канал связи в рамках конкретного интерфейса, задает размер пакета, описывает параметры прерываний. Используя конечные точки мы будем получать и принимать данные.
- Есть еще куча разных дескрипторов, которые описывают отдельные аспекты конкретных интерфейсов
Описание SPI
Serial Peripheral Interface (SPI) — это последовательный, синхронный, полнодуплексный протокол передачи данных между главным (master) контроллером (обычно микроконтроллером или другими устройством с программируемой функциональностью) и несколькими ведомыми (slave) устройствами. Как мы увидим далее, SPI позволяет передавать данные как в полнодуплексном, так и в полудуплексном режиме. Спецификация SPI стандарт в протоколах передачи данных и была разработана в конце 70-х компанией Motorola и на данный широко используется как протокол передачи данных для многих цифровых микросхем. В отличие от протокола I2C, SPI не задает жестких условий в протоколе передачи данных по шине, давая ведомым (slave) устройствам полную свободу в структуре сообщений обмена данными.
Рисунок 1: Архитектура типовой SPI шины.
Типовая шина SPI содержит 4 сигнала, как показано на рисунке 1, даже, если возможно управлять некоторыми SPI устройствами, используя лишь 3 сигнала (в таком случае мы говорим о 3-проводном SPI).
- SCK: сигнал, используемый для генерации тактовой частоты синхронизации передачи данных по SPI. Генерируется master устройством и это означает, что каждая передача по шине SPI всегда начинается по инициативе master устройства. В отличие от I2C SPI более быстрый интерфейс передачи данных и тактовая частота обычно в районе нескольких мегагерц. В наше время вполне обычно найти SPI устройства со скоростью обмена данными от 100 МГц и выше. Более того, протокол SPI позволяет работать в одной шине на различных скоростях в одно и то же время.
- MOSI: расшифровывается как Master Output Slave Input и используется для передачи данных от главного (master) устройства к ведомому (slave). В отличие от I2C, где для обмена между устройствами используется лишь один провод, в SPI предусмотрено две линии передачии данных.
- MISO: расшифровывается как Master Input Slave Output и используется для передачи данных от ведомого (slave) устройства к главному (master).
- SSn: Slave Select используется для адресации устройств в шине SPI, где „n“ — количество линий адресации. В отличие от I2C, SPI не использует адреса ведомых (slave) устройств, а использует физические линии адресации, которые устанавливаются в низкий логический уровень для выбора устройства. В типовой SPI шине только одно ведомое (slave) устройство может быть активно в одно время путем установки линии SS в низкий уровень. В этом причина того, что в одной и той же шине могут быть устройства с разной скоростью передачи данных.
Имея две раздельные шины данных, MOSI и MISO, SPI по сути является полнодуплексным интерфейсом передачи данных, таким образом ведомое (slave) устройство может отдавать данные главному (master) пока одновременно принимает данные от него же. В одноранговых шинах (когда одно главное (master) и одно ведомое (slave) устройства), сигнал SS может не использоваться, лишь достаточно его подтянуть к «земле» резистором на несколько килоом, линии MISO/MOSI соединяются в одно общую линию Slave In/Slave Out (SISO). В таком случае мы говорим о двухпроводном SPI, хотя по существу он конечно же трехпроводной.
Рисунок 2: Как передаются данные по шине SPI в полнодуплексном режиме
Каждая транзакция в шине начинается с подачи тактирования в линию SCK в соответствии с допустимой тактовой частотой ведомого (slave) устройства. В то же время главное (master) устройство устанавливает LOW уровень на линии SS и передача данных начинается. Обычно обмен данными подразумевает использование двух регистров (в основном 8-битные, хотя некоторые slave устройства поддерживают 16-битный размер слова), одного в главном (master) устройстве и один в ведомом (slave). Данные побитно сдвигаются в сдвиговом регистре, начиная со старшего бита, пока младший бит сдвигается в этот же самый регистр. В это же самое время, данные от ведомого (slave) устройства сдвигаются в младший бит регистра данных. Когда все биты регистра будут сдвинуты в одну и другую сторону обмен данными будет осуществлен. Если необходимо передать более одного слова данных, сдвиговые регистры обнуляются и процесс повторяется. Обмен данными может продолжаться сколько угодно большое количество циклов тактового генератора. Когда обмен завершен master выключает тактовый сигнал и линия SS возвращается в исходное состояние высокого логического уровня.
Рисунок 2 показывает процесс обмена данными в полнодуплексном режиме, рисунок 3 же демонстрирует процесс в полудуплексном режиме.
Рисунок 3: Как передаются данные по шине SPI в полудуплексном режиме
Общие сведения о запросах 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.
Резюме
Как это выглядит для разработчика
- Заливаем обычным программатором в микроконтроллер 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
- 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/
Прокачиваем драйвер 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
- К этому моменту данные посылки уже отправлены
- Готовимся к следующей транзакции
USB Mass Storage пример
STM32_USB-FS-Device_Lib_V4.0.0.о USB и использовании STM32_USB-FS-Device_Libhttps://github.com/avislab/STM32F103/tree/master/Example_USB_Mass_Storagemass_mal.h
Давайте покопаемся в коде и разберемся как выполняется работа с Mass Storage. Оказывается микроконтроллер вообще ничего не знает о FAT, кластерах и так далее. Он работает только как посредник — передает данные с USB во Flash, и с Flash в USB. А куда и что писать, собственно, решает операционная система.
Отдавать 20Кб флэш памяти микроконтроллера для FAT — это многовато. А что вообще можно придумать для реализации Bootloader-а в режиме USB Mass Storage? Фактически нам нужно выполнять только одну операцию — копирование одного файла. Можно даже с фиксированным именем. Можно обманывать операционную систему и программно формировать данные FAT, но фактически не хранить ее. Для одного файла в корне с фиксированным именем это вполне реальная идея для экономии памяти. При копировании файла на наш USB Mass Storage надо будет лишь записать данные во флэш. Но я прикинул сколько придется углубляться в «муляж» FAT12 и сколько выгребать «граблей», что решил пока отложить этот замысел. Решим задачу более простым путем.
Я не нашел в Интернете чтобы так кто-то делал, поэтому говорю что это моя собственная идея. Мне приятно так думать, но у меня есть подозрение что я не первый такой хитрый 🙂
Отправка данных по 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