Русский
Русский
English
Статистика
Реклама

Mcu

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

28.04.2021 12:07:29 | Автор: admin
image

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

Определимся что речь будет идти о средах которые могут писать под си. Для начала поговорим о подходах, я выделю 2 основных.

Первый установка ST Visual Develop и выбор в качестве компилятора COSMIC Бывший платный, а ныне бесплатный, но со своими заморочками; регистрация, получение ключа, и прочие танцы с бубном.

Второй же вариант, более простой VS Code + PlatformIO и компилятор SDCC полностью свободный. И опять же не все так просто. Sdcc не умеет исключать не используемые функции. Я решил этот вопрос хоть и успешно, но не без дополнительных действий при написании кода.

Первая среда, для любителей всё делать правильно


Для начала нам нужен ST Visual Develop. Устанавливаем и ставим запуск ярлыка всегда от администратора. В придачу к нему нам дают ST Visual Programmer, полезный инструмент, особенно когда стоит защита от записи и надо разблокировать микроконтроллер, а ведь китайские blue pill всегда приходят заблокированными. Китайцы бояться что мы украдём их круто оптимизированный Blink.

Вот так выглядит STVD
image
Я так понял её создали, когда в моде были 16 битные цвета...

Дальше нужно будет получить компилятор COSMIC и его лицензионный ключ. Заполняем то что просят, получаем дистрибутив и делаем запрос ключа по электронной почте (тут рулетка кому то сразу придёт, кому то придётся подождать).

После пройденного лабиринта из форм и запросов, когда всё уже установлено, начнём
image
image
При первом запуске нужно указать расположение ключа, его лучше поместить в директорию компилятора. После создания нажимаем F7, ошибок быть не должно. Если писать на чистых регистрах, то всё готово, но я такой хардкор не люблю, поэтому продолжим и добавим SPL библиотеку.

Распакуем куда-нибудь и заходим в папку STM8S_StdPeriph_Lib\Project\STM8S_StdPeriph_Template. Тут у нас шаблон проекта stm8s_conf.h это конфигурационный файл библиотеки, через него выбирается контроллер. Зайдём в main тут сразу с первых строк #include "stm8s.h" это ссылка на основную библиотеку, а так же кусок кода отладки который начинается с #ifdef USE_FULL_ASSERT, без отладочного кода будут сыпаться ошибки.

Теперь когда мы прошлись по верхам давайте пробовать запускать библиотеку. Добавляем в проект из шаблона main и конфигурационный файл в. В include files добавляем всё из STM8S_StdPeriph_Lib\Libraries\STM8S_StdPeriph_Driver\inc.
image
Теперь всё должно собраться.

Добавим stm8s_gpio.c и соберём простецкую мигалку. У меня один из светодиодов висит на D3, конфигурация ноги на выход выглядит так:

GPIO_DeInit(GPIOD);GPIO_Init(GPIOD, GPIO_PIN_3, GPIO_MODE_OUT_PP_LOW_SLOW);

Её вписываем в main до бесконечного цикла.

А вот функция смены состояния. GPIO_WriteReverse(GPIOD, GPIO_PIN_3); вписываем её в бесконечный цикл.

Но вот незадача, в SPL нету функций задержки, скопипастим из примера GPIO в библиотеке. Там она выглядит следующим образом.

void Delay(uint16_t nCount){  /* Decrement nCount value */  while (nCount != 0)  {    nCount--;  }}

Впишем её в конец перед #ifdef USE_FULL_ASSERT. Так же впишем её прототип в начало, где под это выделено место в шаблонном main.

/* Private function prototypes -----------------------------------------------*/void Delay (uint16_t nCount);

Ну и наконец впишем функцию со значением в бесконечный цикл после функции смены состояния: Delay(0xFFFF);

Подключаем ST-Link и прошиваем, для этого нажимаем Start Debugging и Run. Светодиод моргает значит всё хорошо.

У кого не получилось вот полный main.c
/**  ******************************************************************************  * @file    Project/main.c   * @author  MCD Application Team  * @version V2.3.0  * @date    16-June-2017  * @brief   Main program body   ******************************************************************************  * @attention  *  * <h2><center> COPYRIGHT 2014 STMicroelectronics</center></h2>  *  * Licensed under MCD-ST Liberty SW License Agreement V2, (the "License");  * You may not use this file except in compliance with the License.  * You may obtain a copy of the License at:  *  *        http://www.st.com/software_license_agreement_liberty_v2  *  * Unless required by applicable law or agreed to in writing, software   * distributed under the License is distributed on an "AS IS" BASIS,   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  * See the License for the specific language governing permissions and  * limitations under the License.  *  ******************************************************************************  */ /* Includes ------------------------------------------------------------------*/#include "stm8s.h"/* Private defines -----------------------------------------------------------*//* Private function prototypes -----------------------------------------------*/void Delay (uint16_t nCount);/* Private functions ---------------------------------------------------------*/void main(void){GPIO_DeInit(GPIOD);GPIO_Init(GPIOD, GPIO_PIN_3, GPIO_MODE_OUT_PP_LOW_SLOW);  /* Infinite loop */  while (1)  {GPIO_WriteReverse(GPIOD, GPIO_PIN_3);    Delay(0xFFFF);  }  }void Delay(uint16_t nCount){  /* Decrement nCount value */  while (nCount != 0)  {    nCount--;  }}#ifdef USE_FULL_ASSERT/**  * @brief  Reports the name of the source file and the source line number  *   where the assert_param error has occurred.  * @param file: pointer to the source file name  * @param line: assert_param error line source number  * @retval : None  */void assert_failed(u8* file, u32 line){   /* User can add his own implementation to report the file name and line number,     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */  /* Infinite loop */  while (1)  {  }}#endif/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/


Теперь посмотрим со стороны на эту среду.

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

Вторая среда, для тех кто не любит заморачиваться.


Второй подход это свободный и обновляемый компилятор SDCC, а так же среда PlatformIO.

Для начала установим VS Code и станем рабами Microsoft, далее найдём расширение PlatformIO.

image

Ждём пока миллион ползунков пройдёт до конца и перезапускаем программу. Открываем расширение (может быть и само откроется). Создаём новый проект и выбираем ближайшую плату с stm8s (микроконтроллер можно будет изменить в конфигурационном файле). В качестве фреймворка выбираем SPL мы же не ардуинщики, нас интересует хардкор.

Не удивляйтесь, проект будет создаваться прилично долго, потому как он на ходу будет подгружать компилятор, библиотеки и тд. И вот перед нами возник девственно голый проект, закрываем его он нам больше не нужен. В меню где мы создавали проект, открываем пример spl-blink, суть в том что blink генерирует неплохой шаблон в котором можно сразу писать. Правим пример под себя и прошиваем, всё моргает.

Разберем поподробнее среду разработки. Во первых весь проект должен лежать в src, иначе среда ведёт себя неадекватно. Во вторых открываем stm8s_conf.h и видим что все библиотеки кроме GPIO закомментированы, если этого не сделать то у мк не хватит памяти что бы поместить весь SPL в микроконтроллер (помните в начале я говорил что он загружает все функции что видит в код?).

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

/* Private define ------------------------------------------------------------*/#define _GPIO_DeInit#define _GPIO_Init#define _GPIO_WriteReverse#define _CLK_DeInit#define _CLK_SYSCLKConfig#define _TIM4_DeInit#define _TIM4_ITConfig#define _TIM4_ClearITPendingBit#define _TIM4_Cmd#define _TIM4_TimeBaseInit#define _TIM4_ClearFlag#define _HAL_GPIO_WritePin#define _GPIO_WriteLow#define _GPIO_WriteHigh//#define STM8S003/* Includes ------------------------------------------------------------------*/#include "stm8s.h"/* Uncomment the line below to enable peripheral header file inclusion */#if defined(STM8S105) || defined(STM8S005) || defined(STM8S103) || defined(STM8S003) ||\    defined(STM8S001) || defined(STM8S903) || defined (STM8AF626x) || defined (STM8AF622x)// #include "stm8s_adc1.h" #endif /* (STM8S105) ||(STM8S103) || (STM8S001) || (STM8S903) || (STM8AF626x) */#if defined(STM8S208) || defined(STM8S207) || defined(STM8S007) || defined (STM8AF52Ax) ||\    defined (STM8AF62Ax)// #include "stm8s_adc2.h"


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

Пройдёмся по преимуществам: работает почти из коробки, полностью бесплатно без попрошайничества, редакции языка обновляются и не придётся учить и переписывать код под си 80-90г. Сам интерфейс настраиваемый и намного приятнее. Для тех кто не любит win есть linux версия.

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

Послевкусие.


Ну и на последок есть ещё среды, варианты и компиляторы, но либо это тот же SDCC вкрученный силой в Eclipse или ещё куда и работающий хуже чем в VS Code, либо это платные варианты IAR, Raisonance. Я лично пользуюсь и тем и тем, но чаще VS Code. Рекомендовать ничего не буду каждому своё, увидимся в комментариях)

Подробнее..

Делаем бесконечную карту памяти для PS1

22.04.2021 20:09:52 | Автор: admin

PS1 (она же PSX, она же PS One) это первое поколение игровых консолей PlayStation от Sony и относится к пятому поколению игровых консолей вообще. Она использует 2х скоростной привод для чтения CD. Такой большой объём данных по меркам актуального для приставки времени позволял игроделам особо не оглядываться на ограничения при создании контента для игр, что делало последних более качественными, по сравнению с играми предыдущего поколения приставок. А ещё, игры теперь могут быть длинными. И если любая игра за редким исключением на консолях предыдущих поколений вполне себе могла быть пройдена за одну игровую сессию, то с играми PS1 всё обстояло иначе. Для сохранения прогресса у PlayStation предусмотрены карты памяти: маленькие сменные модули энергонезависимой памяти.

Если вам интересно, как именно устроена карта памяти PlayStation 1, как она работает и как можно создать свою добро пожаловать под кат.

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


Фотография печатной платы стандартной карты памяти на 15 блоков

Как видно из фото, устройство карты очень простое: контроллер, который обслуживает запросы системы, и, собственно, энергонезависимая память, которая представлена стандартной NOR FLASH. Логически, карта памяти разбита на 15 блоков, которые игры могут использовать. Может показаться, что 15 не логично для двоичной системы, но тут противоречия нет: один блок отдан под файловую систему, там хранятся имена файлов и даже анимированные иконки, прям как потоки у NTFS. Каждый блок имеет размер 8 КиБ, 16 блоков в сумме это 128 КиБ, что и видно по маркировке FLASH памяти на фото выше.

На первых порах этого хватало всем, но потом стали появляться игры, которые использовали более одного блока за раз. Например, некоторые симуляторы, вроде Sega GT, используют 4-5 блоков, а Constructor так вообще всю карту памяти на 15 блоков. Это вынуждало покупать больше карт и ситуация грозила стать как с дискетами или картриджами. Но потом подтянулись пираты и стали выпускать карты на 2, 4 или 8 страниц разом. И переключались страницы либо по хитрой комбинации на джойпаде, либо явной кнопкой на самой карте памяти. Правда, в картах более 2х страниц применялось сжатие, и фактическое число страниц было значительно меньше, а некоторые карты могли тупо заблокироваться. И вывести их из этого состояния было очень трудно, но на что только не шли игроки ради своих сохранений. Вот типичные представители многостраничных карт памяти:


Слева карта памяти на 2 страницы, справа на 8. У правой есть аппаратная кнопка перелистывания страниц и индикатор, отображающий число от 1 до 8, который скрыт за тёмным стеклом

Небольшое лирическое отступление.


Всё началось в 2001м году, когда я купил чудо диск для ПК под названием Все эмуляторы, на котором были эмуляторы PS1 в том числе: это были Bleem! и ранний ePSXe. И мой тогдашний комп даже смог играбельно запускать мои диски от PS1! А чуть позже у меня появился модем и я узнал про DirectPad Pro. Подключение родного джойстика к компьютеру (пусть и через LPT) многого стоит. И работала эта система как на 9х так и на XP! А ещё чуть позже, уже в 2002м я узнал про Memory Card Capture Sakura! Эта программа позволяла работать с настоящими картами памяти, используя всё ту же схему подключения DirectPad Pro. Именно тогда у меня появилась идея сделать бесконечную карту памяти, которая бы позволяла обмениваться информацией с компьютером без необходимости дополнительных устройств. Но на тот момент у меня не было достаточно информации и доступной элементной базы, и идея оставалась лишь идеей, теплясь где-то на задворках сознания.

Прошло почти 9 лет как я осознал, что уже знаю достаточно и имею возможность, чтобы реализовать хоть какой-то вариант бесконечной карты памяти. Однако тут вступил уже другой фактор возраст и всё что с этим связано. Времени на хобби всё меньше, забот всё больше. И вот только сейчас я могу предоставить общественности хоть какой-то результат, полноценный Proof of Concept.

Физический интерфейс.


Итак, карта памяти и джойпады работают через общий интерфейс. Количество сигналов в нём 6, вот их названия и назначения:
  • SEL0 Сигнал выбора первого порта, активный уровень низкий
  • SEL1 Сигнал выбора второго порта, активный уровень низкий;
  • CLK Тактовый сигнал интерфейса, пассивное состояние высокий уровень, по спаду сдвиг, по фронту защёлкивание;
  • CMD Сигнал данных от консоли к периферии;
  • DAT Сигнал данных от периферии к консоли;
  • ACK Аппаратный хэндшейк, активный уровень низкий.

Так же на интерфейсе присутствует два разных напряжения питания: 3.3в и 7.6в. Все сигналы, кроме SEL0 и SEL1 являются общими для всех подключаемых устройств. Именно поэтому нерабочая карта памяти или джойпад во втором слоту влияли на рабочие в первом, хотя после 16ти битных приставок это казалось странным. Я думаю, что многие уже узнали в интерфейсе стандартный SPI всё верно, так и есть. Только добавлен сигнал ACK для подтверждения операции ввода/вывода. Вот назначения сигналов на контактах карты памяти:


Отремонтированная карта памяти с 5ти вольтовым FLASH

Технические характеристики интерфейса такие:
        ___   ___________________________   ____Данные     \ /                           \ /     или        X                             XКоманда ___/ \___________________________/ \____        ___                  ____________                  \                /            \      Такты       \              /              \                  \____________/                \____            |                             |            |           tck               |            |<--------------------------->|+-------+-------+------+-------+|       | мин.  | тип. | макс. |+-------+-------+------+-------+| tck   | 1мкс  | 4мкс |   -   |+-------+-------+------+-------+Тайминг ACK:     ____                                               SEL-     |______________________________________________     ______        __________        ___________        CLK        ||||||||          ||||||||           ||||||||                  |                 |ACK- -----------------------|_|-------------|_|---------                  |   ta1   | |     |  ta2  |                  |<------->| |     |<----->|                            | |  ap                           >|-|<-----+-----+------+-------+--------+|     | мин. |  тип. |  макс. |+-----+------+-------+--------+| ta1 | 0мкс |   -   | 100мкс | Первый байт-подтверждение+-----+------+-------+--------+| ta2 |      | 10мкс |   1мс  | Остальные+-----+------+-------+--------+|  ap | 2мкс |       |        | Длительность ACK+-----+------+-------+--------+

Измеренная частота сигнала CLK является 250кГц, что составляет 4 мкс на период. С физическими параметрами интерфейса разобрались, теперь транспортный уровень. Опытный инженер уже заметил, что джойпад и карта памяти подключены полностью параллельно и могут конфликтовать между собой. Так и есть, для этого присутствует программный арбитраж. После активации сигнала SELn периферия продолжает молчать, но слушает первый присланный байт. Если этот байт равен 0x01, то далее активируется джойпад, а карта памяти продолжает молчать до деактивации сигнала выбора. А если байт был 0x81, то всё наоборот: карта памяти активируется, а джойпад молчит. Естественно, что хост ждёт сигнала ACK на этот байт арбитража и ждёт недолго. Это нужно для того, чтобы успеть опросить остальную периферию, если часть этой периферии отсутствует. Дело в том, что операционная система опрашивает контроллеры и карты памяти строго по сигналу обратного хода луча, или более известного как VBlank. Так принято, что игры в приставках до 5-го поколения завязаны на этот тайминг, который равен частоте кадров. А частота кадров строго стабильна и нормирована: 50Гц для PAL и 60Гц для NTSC. Т.е., период опроса джойстиков и карт памяти равен 20мс для PAL или 16мс для NTSC.

Итак, с арбитражем разобрались, теперь собственно верхний уровень. Какие команды понимает стандартная карта памяти PS1? Да, собственно, их всего 3.
  • R 0x52 или Read. Чтение сектора карты памяти;
  • W 0x57 или Write. Запись сектора карты памяти;
  • S 0x53 или Status. Чтение статуса карты памяти.

Вся карта памяти разбита на сектора. Один сектор 128 байт. Таким образом, в 128КиБ помещается 0x400 или 1024 сектора. При этом стирать сектор перед записью не нужно. Но система гарантированно даёт время на целый следующий кадр при записи. Т.е., читать карту памяти она может каждый кадр, а записывает через один. К слову, всякие Взломщики кодов для ускорения не придерживаются данных таймингов. Разберём каждую команду более детально.

Протокол работы с картой памяти.


Порядок передаваемых данных в каждой команде выглядит вот так:
Чтение:
CMD 0x81 0x52 0x00 0x00 MSB LSB 0x00 0x00 0x00 0x00 0x00 ... 0x00 0x00 0x00DAT ---- FLAG 0x5A 0x5D PRV PRV 0x5C 0x4D  MSB  LSB DATA ... DATA  CHK  ACK

Запись:
CMD 0x81 0x57 0x00 0x00 MSB LSB DATA ... DATA CHK 0x00 0x00 0x00 DAT ---- FLAG 0s5A 0x5D PRV PRV  PRV ...  PRV PRV 0x5C 0x5D  ACK

Статус:
CMD 0x81 0x53 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00DAT ---- FLAG 0x5A 0x5D 0x5C 0x5D 0x04 0x00 0x00 0x80

Легенда:
CMD Данные, которые хост посылает карте.
DAT Данные, которые карта посылает хосту.
FLAG Текущие флаги состояния карты и результат предыдущей команды.
PRV Предыдущие принятые данные, результат упрощения схемы в карте.
MSB Старший байт номера сектора.
LSB Младший байт номера сектора.
DATA Полезные данные.
CHK Контрольная сумма блока.
ACK Флаг подтверждения.

Байт флагов FLAG использует следующие биты:
  • D5 Устанавливается некоторыми картами памяти не от Sony. Назначение неизвестно.
  • D3 Устанавливается при подаче питания и сбрасывается при любой записи. Используется для обнаружения смены карты памяти.
  • D2 Устанавливается при ошибках записи, актуален на следующее обращение после самой операции.

После подачи питания FLAG равен 0x08. И после первой же записи он обнуляется. Операционная система PS1 всегда делает запись в сектор 0x003F для этого, тем самым вызывая износ этого сектора. Но в рамках разметки карты памяти системой какой-либо полезной информации в этом секторе нет. Номер сектора MSB:LSB 10 бит и составляет число от 0x0000 до 0x03FF. Контрольная сумма CHK это обычный XOR всех 128 байт данных + MSB и LSB. Подтверждение ACK может принимать всего 3 значения: G 0x47, E 0x43 и 0xFF. G = Good или ОК. E = Error. Собственно, при чтении из карты ACK всегда равен G, а при записи G = ОК, E = ошибка контрольной суммы а 0xFF означает неправильный номер сектора. Правда, большинство карт просто откидывают неиспользуемые биты в старшем байте номера сектора и поэтому никогда не отвечают 0xFF. Числа 0x0400 и 0x0080 в команде статуса наводят на определённые мысли, что это количество секторов и размер сектора в байтах, но доподлинно это не известно. Ну вот мы и мы подошли к главному:

Реализация своей карты памяти.


Итак, эта вся информация, которая необходима для создания своей карты памяти для PS1. Потенциальные узкие места следующие:
  1. При чтении необходимо время на актуализацию данных. Между номером сектора и фактической передачей данных у нас есть 4 байта, у которых мы можем немного растянуть ACK. К слову, у оригинальной карты памяти на NOR FLASH все ACK идут равномерно, у карт памяти с SPI FLASH после передачи LSB происходит задержка ACK, во время которой контроллер выставляет команду в SPI FLASH и вычитывает первый байт, а остальные он вычитывает по ходу обмена.
  2. При записи после передачи всего пакета и начала самой записи в массив требуется время, но тут система сама даёт необходимую задержку.

Что касается питания, то у джойпадов 3,3в используется для логики а 7,6в для питания моторчиков. У карт памяти обычно используется только одно питание. Если внутри стоит 5в FLASH, то используется 7,6в и стабилизатор. Если стоит 3,3в FLASH, то используется сразу 3,3в.

Первый вариант я собрал на STM32F407VG, который питается от 3,3в, имеет SPI для PSIO, быстрый SDIO и достаточно памяти, чтобы хранить весь образ внутри себя, решая вышеупомянутые проблемы. Фотография готового устройства:


Первая версия моей карты памяти на STM32F407

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


Вторая версия моей карты памяти на STM32F042

Карта у нас ведомая, поэтому стабилизация частоты внешним кварцевым резонатором не нужна, достаточно внутреннего генератора. Аппаратный SPI у этого контроллера один, поэтому я его отдал SD карте, чтобы снизить задержки на транспорт. PSIO тут будет программный.

Программная реализация.


Первое, что надо сделать это работу с SD картой в режиме SPI. Я не буду особо останавливаться на этом, это уже давно разжёвано и растаскано по интернету. Код инита, чтения и записи сектора приведён ниже.
Card_Init()
// Инициализация карты памятиTCardType Card_Init( void ){// Локальные переменныеTCardType Res;uint32_t Cnt,OCR;uint8_t Dat, Resp;// Отключаем картуCARD_OFF; Res = ctNone;// Настраиваем SPI на медленную скорость PCLK/128: 48/128 = 0,375МГцSPI1->CR1 &= ~SPI_CR1_SPE;SPI1->CR1 = SPI_CR1_MSTR | SPI_LOW_SPEED;SPI1->CR1 |= SPI_CR1_SPE;// Топчемся на местеHAL_Delay( 1 );// Посылаем инит 256 байтfor (Cnt = 0;Cnt < 256;Cnt++ ){// Послыаем словоCard_SPI( 0xFF );}// Начинаем инициализацию картыCARD_ON;// Ожидаем готовности картыdo{// Посылаем 0xFFDat = Card_SPI( 0xFF );} while ( Dat != 0xFF );// CMD0: GO_IDLE_STATECard_SendCMD( &CARD_CMD0[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, DISABLE, 128 );// Какой ответ получен?if ( Resp == 0x01 ){// Карта вошла в IDLE_STATE, посылаем CMD8: SEND_IF_CONDCard_SendCMD( &CARD_CMD8[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, ENABLE, 128 );// Если был дан адекватный респонсif ( Resp != 0x01 ){// Это ветка SDv1/MMCdo{// Посылаем ACMD41: APP_SEND_OP_CONDCard_SendCMD( &CARD_ACMD41[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, ENABLE, 128 );} while ( Resp == 0x01 );// Каков был ответ?if ( Resp == 0x00 ){// Обнаружена карта SD v1Res = ctSD1;}else{// Это ветка MMC, нам её некуда втыкатьRes = ctUnknown;}}else{// Это ветка SDv2if ( (OCR & 0x0001FF) == 0x0001AA ){// Это карта SDv2do{// Посылаем ACMD55: APP_CMDCard_SendCMD( &CARD_CMD55[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, DISABLE, 128 );// Если ответ правильныйif ( Resp == 0x01 ){// Посылаем ACMD41: APP_SEND_OP_CONDCard_SendCMD( &CARD_ACMD41[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, ENABLE, 128 );}} while ( Resp == 0x01 );// Каков был ответ?if ( Resp == 0x00 ){// Посылаем CMD58: READ_OCRCard_SendCMD( &CARD_CMD58[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, ENABLE, 128 );// Каков ответ?if ( Resp == 0x00 ){// Анализируем OCRif ( (OCR & 0x40000000) == 0x00000000 ){// Карта обычной ёмкостиRes = ctSD2;}else{// Карта повышенной ёмкостиRes = ctSD3;}}else{// Эта карта неисправнаRes = ctUnknown;}}else{// Эта карта неисправнаRes = ctUnknown;}}else{// Эта карта неисправнаRes = ctUnknown;}}}else{// Карта ответила неправильноif ( Res != 0xFF ) { Res = ctUnknown; }}// Только для карт обычной ёмкостиif ( (Res == ctSD1) || (Res == ctSD2) ){// Устанавливаем размер блока 512 байт// CMD16: SET_BLOCKLENCard_SendCMD( &CARD_CMD16[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( &OCR, DISABLE, 128 );// Каков ответ?if ( Resp != 0x00 ){// Эта карта неисправнаRes = ctUnknown;}}// Выключаем картуwhile ( (SPI1->SR & SPI_SR_BSY) != 0x0000 ) { }CARD_OFF;// Если карта инициализированаif ( (Res != ctNone) && (Res != ctUnknown) ){// Настраиваем SPI на быструю скорость PCLK/2: 48/2 = 24МГцSPI1->CR1 &= ~SPI_CR1_SPE;SPI1->CR1 = SPI_CR1_MSTR;SPI1->CR1 |= SPI_CR1_SPE;}// Выходимreturn Res;}

Card_Read()
// Чтение сектора карты памяти без DMAFunctionalState Card_Read( TCardType CardType, uint8_t *Buf, uint32_t *Loaded, uint32_t Addr ){// Локальные переменныеFunctionalState Res;uint8_t Cmd[ 6 ];uint8_t Dat,Resp;uint32_t Cnt;// ИнитRes = DISABLE;// Посмотрим, у нас в буфере уже загружено?if ( *(Loaded) != Addr ){// Сохраняем новый номер сектора*(Loaded) = Addr;// Корректируем адрес для старых картif ( (CardType == ctSD1) || (CardType == ctSD2) ){// У старых карт адрес вместо LBAAddr *= 0x00000200;}// Работаемwhile ( 1 ){// Если тип карты неправильный - выходимif ( CardType == ctNone ) { break; }if ( CardType == ctUnknown ) { break; }// Готовим команду на чтение сектораCmd[ 0 ] = CARD_CMD17;Cmd[ 1 ] = Addr >> 24;Cmd[ 2 ] = Addr >> 16;Cmd[ 3 ] = Addr >> 8;Cmd[ 4 ] = Addr;Cmd[ 5 ] = 0xFF;// Включаем картуCARD_ON;// Ожидаем готовности картыdo{// Посылаем 0xFFDat = Card_SPI( 0xFF );} while ( Dat != 0xFF );// Посылаем команду чтенияCard_SendCMD( &Cmd[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( (uint32_t *)&Cmd[ 0 ], DISABLE, 128 );// Анализируем ответ на командуif ( Resp != 0x00 ) { break; }// Ожидаем токен данныхCnt = 2048;do{// Считываем данныеDat = Card_SPI( 0xFF );// СчитаемCnt--;} while ( (Dat == 0xFF) && (Cnt > 0) );// Таймаут?if ( Cnt == 0 ) { break; }// Ошибка в токене?if ( Dat != CARD_DATA_TOKEN ) { break; }// Начались данные, загружаемfor (Cnt = 0;Cnt < 512;Cnt++){// Считываем данные*(Buf) = Card_SPI( 0xFF ); Buf++;}// Дочитываем CRCCmd[ 0 ] = Card_SPI( 0xFF );Cmd[ 1 ] = Card_SPI( 0xFF );// Без ошибокRes = ENABLE;// Выходbreak;}}else{// Без ошибокRes = ENABLE;}// Выключаем картуwhile ( (SPI1->SR & SPI_SR_BSY) != 0x0000 ) { }CARD_OFF;// Если была ошибка, обнулим номерif ( Res == DISABLE ) { *(Loaded) = 0xFFFFFFFF; }// Выходreturn Res;}

Card_Write()
// Запись сектора карты памяти без DMAFunctionalState Card_Write( TCardType CardType, uint8_t *Buf, uint32_t *Loaded, uint32_t Addr ){// Локальные переменныеFunctionalState Res;uint8_t Cmd[ 6 ];uint8_t Dat,Resp;uint32_t Cnt;// ИнитRes = DISABLE;// Корректируем адрес для старых картif ( (CardType == ctSD1) || (CardType == ctSD2) ){// У старых карт адрес вместо LBAAddr *= 0x00000200;}// Работаемwhile ( 1 ){// Если тип карты неправильный - выходимif ( CardType == ctNone ) { break; }if ( CardType == ctUnknown ) { break; }// Готовим команду на чтение сектораCmd[ 0 ] = CARD_CMD24;Cmd[ 1 ] = Addr >> 24;Cmd[ 2 ] = Addr >> 16;Cmd[ 3 ] = Addr >> 8;Cmd[ 4 ] = Addr;Cmd[ 5 ] = 0xFF;// Включаем картуCARD_ON;// Ожидаем готовности картыdo{// Посылаем 0xFFDat = Card_SPI( 0xFF );} while ( Dat != 0xFF );// Посылаем команду чтенияCard_SendCMD( &Cmd[ 0 ], CMD_LENGTH ); Resp = Card_WaitResp( (uint32_t *)&Cmd[ 0 ], DISABLE, 128 );// Анализируем ответ на командуif ( Resp != 0x00 ) { break; }// Посылаем токен данныхCard_SPI( CARD_DATA_TOKEN );// Посылаем данные в цикле// Начались данные, загружаемfor (Cnt = 0;Cnt < 512;Cnt++){// Считываем данныеCard_SPI( *(Buf) ); Buf++;}// Досылаем CRCCard_SPI( 0xFF );Card_SPI( 0xFF );// Без ошибокRes = ENABLE;// Выходbreak;}// Выключаем картуwhile ( (SPI1->SR & SPI_SR_BSY) != 0x0000 ) { }CARD_OFF;// Успешно?if ( Res == ENABLE ){// Сохраняем новый номер сектора*(Loaded) = Addr;}else{// Обнуляем*(Loaded) = 0xFFFFFFFF;}// Выходreturn Res;}

Карта инициализируется на скорости 375кГц (PCLK/128), а работает на 24МГц (PCLK/2). При таких скоростях замеры показали, что SDv1 и SDHC отдают сектор в рамках 2,8мс на всю транзакцию полностью. Это следует запомнить, т.к. важно для операции чтения PSIO.

Теперь посмотрим на PSIO. Как было уже сказано выше, он у нас в любом случае программный. Отслеживать надо только два сигнала: SEL и CLK. Первый мы будем отслеживать по обоим фронтам и делать приготовления к обмену данными:
EXTI2_3_IRQHandler()
// Прерывание по перепаду SELvoid EXTI2_3_IRQHandler( void ){// Подтверждаем прерываниеEXTI->PR = 0x00000004;// Анализируем состояние SELif ( MEM_SEL ){// SEL = 1EXTI->IMR &= 0xFFFFFFFE;State.PSIO.Mode = mdSync;// Тушим зелёную лампочкуLED_GREEN_OFF;}else{// SEL = 0EXTI->IMR |= 0x00000001;State.PSIO.Bits = 7;// Тушим лампочкиLED_GREEN_OFF; LED_RED_OFF;}// ОбесточиваемMEM_DAT1; MEM_nACK;}

Сигнал CLK будем ловить только по фронту. Дело в том, что STM32F042 работает всего лишь на 48МГц и его производительность маловата для нашей задачи. И если делать прерывание по обоим фронтам, то во время пересылки байта он практически не вылезает из обработчика прерывания и всё работает прямо на грани возможности, иногда давая сбои. А если реагировать только на фронт, а ту работу, что должна быть сделана по спаду сделать в конце прерывания, то всё отлично успевает меньше, чем за 55% от периода CLK, ведь несколько проверок при этом можно выкинуть. Уверен, что если этот обработчик написать на ассемблере максимально оптимально, то он смог бы работать даже по обоим перепадам. Вот код обработчика:
EXTI0_1_IRQHandler()
// Прерывание по фронту CLKvoid EXTI0_1_IRQHandler( void ){// Подтверждаем прерываниеEXTI->PR = 0x00000001;// Локальные переменныеuint16_t AckTime;// ИнитAckTime = 0;// Считываем данныеState.PSIO.DataIn >>= 1;if ( MEM_CMD ){// Принята 1State.PSIO.DataIn |= 0x80;}else{// Принят 0State.PSIO.DataIn &= 0x7F;}// Считаем битыif ( State.PSIO.Bits > 0 ){// Ещё есть битыState.PSIO.Bits--;}else{// Кончились биты?if ( State.PSIO.Bits == 0 ){// Биты кончилисьState.PSIO.Bits = 7;// Значение по умолчаниюState.PSIO.DataOut = State.PSIO.DataIn;// Анализируем ответswitch ( State.PSIO.Mode ){// Режим синхронизацииcase mdSync : {// Принят первый байт командыif ( State.PSIO.DataIn == 0x81 ){// Команда активации картыState.PSIO.Mode = mdCmd;// Текущий ответState.PSIO.DataOut = State.MemCard.Status;// Посылаем ACKAckTime = AckNormal;}elseif ( State.PSIO.DataIn == 0x01 ){// Команда активации джойстика}// Выходbreak;}// Получаем командуcase mdCmd : {// Меняем режимState.PSIO.Mode = mdParam;// Сохраняем байт в команду и подготовим буферState.MemCard.Cmd = State.PSIO.DataIn;State.MemCard.Bytes = 0;// ОтвечаемState.PSIO.DataOut = 0x5A;// Посылаем ACKAckTime = AckNormal;// Выходbreak;}// Режим получения параметровcase mdParam : {// Почти каждый ответ требует ACKAckTime = AckNormal;// Принимаем параметрыswitch ( State.MemCard.Cmd ){// Команда чтения: Rcase 0x52 : {// Анализируем байтыswitch ( State.MemCard.Bytes ){// Просто все вариантыcase 0 : { State.PSIO.DataOut = 0x5D; break; }case 1 : { break; }case 2 : { State.MemCard.Sector = State.PSIO.DataIn * 0x0100; State.MemCard.Check = State.PSIO.DataIn; break; }case 3 : { State.MemCard.Sector += State.PSIO.DataIn; State.MemCard.Check ^= State.PSIO.DataIn; State.PSIO.DataOut = 0x5C;   State.SDCard.CardOp = coRead; AckTime = AckDelayed; break; }case 4 : { State.PSIO.DataOut = 0x5D; AckTime = AckDelayed; break; }case 5 : { State.PSIO.DataOut = State.MemCard.Sector >> 8; AckTime = AckDelayed; break; }case 6 : { State.PSIO.DataOut = State.MemCard.Sector; AckTime = AckDelayed;   State.PSIO.Mode = mdRdData; State.MemCard.Bytes = 0; break; }default : { State.PSIO.Mode = mdDone; AckTime = 0; break; }}// Сигнализируем чтениемLED_GREEN_ON;// Выходbreak;}// Команда записи: Wcase 0x57 : {// Анализируем байтыswitch ( State.MemCard.Bytes ){// Просто все вариантыcase 0 : { State.PSIO.DataOut = 0x5D; break; }case 1 : { break; }case 2 : { State.MemCard.Sector = State.PSIO.DataIn * 0x0100; State.MemCard.Check = State.PSIO.DataIn; break; }case 3 : { State.MemCard.Sector += State.PSIO.DataIn; State.MemCard.Check ^= State.PSIO.DataIn; // break; }   State.PSIO.Mode = mdWrData; State.MemCard.Bytes = 0; break; }default : { State.PSIO.Mode = mdDone; AckTime = 0; break; }}// Сигнализируем записьюLED_RED_ON;// Выходbreak;}// Команда параметров: Scase 0x53 : {// Выставляем байт согласно номеруswitch ( State.MemCard.Bytes ){// Просто все вариантыcase 0 : { State.PSIO.DataOut = 0x5D; break; }case 1 : { State.PSIO.DataOut = 0x5C; break; }case 2 : { State.PSIO.DataOut = 0x5D; break; }case 3 : { State.PSIO.DataOut = 0x04; break; }case 4 : { State.PSIO.DataOut = 0x00; break; }case 5 : { State.PSIO.DataOut = 0x00; break; }case 6 : { State.PSIO.DataOut = 0x80; break; }default : { State.PSIO.Mode = mdDone; AckTime = 0; break; }}// Выходbreak;}// По умолчаниюdefault : { State.PSIO.Mode = mdDone; break; }}// Считаем номерif ( State.PSIO.Mode == mdParam ) { State.MemCard.Bytes++; }// Выходbreak;}// Режим передачи данных для чтенияcase mdRdData : {// Почти каждый ответ требует ACKAckTime = AckNormal;// Счётчик байтif ( State.MemCard.Bytes < 128 ){// Это передача данныхState.PSIO.DataOut = State.MemCard.Data[ State.MemCard.Bytes ]; State.MemCard.Check ^= State.PSIO.DataOut;}else{// Это хвостик за пределами данныхswitch ( State.MemCard.Bytes ){// Передача контрольной суммыcase 128 : { State.PSIO.DataOut = State.MemCard.Check; break; }// Передача завершающего статусаcase 129 : { State.PSIO.DataOut = 0x47; break; }// Завершение работыdefault : { State.PSIO.Mode = mdDone; AckTime = 0; LED_GREEN_OFF; break; }}}// СчитаемState.MemCard.Bytes++;// Выходbreak;}// Режим приёма данных для записиcase mdWrData : {// Почти каждый ответ требует ACKAckTime = AckNormal;// Счётчик байтif ( State.MemCard.Bytes < 128 ){// Это приём данныхState.MemCard.Data[ State.MemCard.Bytes ] = State.PSIO.DataIn; State.MemCard.Check ^= State.PSIO.DataIn;}else{// Это хвостик за пределамы данныхswitch ( State.MemCard.Bytes ){// Это приём контрольной суммыcase 128 : {// Сравниваем контрольную сумму и выносим решениеif ( State.MemCard.Check == State.PSIO.DataIn ) { State.MemCard.Check = 0x47; } else { State.MemCard.Check = 0x4E; }// Начинаем подтверждать приёмState.PSIO.DataOut = 0x5C;// Выходимbreak;}// Это хвостик данныхcase 129 : { State.PSIO.DataOut = 0x5D; break; }// Это вывод результата командыcase 130 : {// Сначала проверим, что сектор задан верноif ( State.MemCard.Sector < 0x4000 ){// Сектор верен, отдаём результат проверкиState.PSIO.DataOut = State.MemCard.Check;// Какой результат проверки?if ( State.MemCard.Check == 0x47 ){// Заказываем запись сектора в карту памятиState.SDCard.CardOp = coWrite;// После успешной записи обнуляется флагState.MemCard.Status &= ~StateNew;}}else{// Сектор ошибочен, выдаём ошибку сектораState.PSIO.DataOut = 0xFF;}// Выходbreak;}// Завершение работыdefault : { State.PSIO.Mode = mdDone; AckTime = 0; break; }}}// СчитаемState.MemCard.Bytes++;// Выходbreak;}// Заглушка, тупим до конца пакетаcase mdDone : { break; }// По умолчанию - откатываемся в началоdefault : { State.PSIO.Mode = mdSync; break; }}}}// Выставляем свои данныеif ( State.PSIO.Mode != mdSync ){// Выставляем текущий бит выводного байтаif ( State.PSIO.DataOut & 0x01 ){// Выставляем 1MEM_DAT1;}else{// Выставляем 0MEM_DAT0;}// Сдвигаем данныеState.PSIO.DataOut >>= 1;}// Требуется ACK?if ( AckTime > 0 ){// Установим CNTTIM3->CNT = AckTime;// Устанавливаем флагState.PSIO.Ack = DISABLE;// Сбросим событияTIM3->SR = 0x0000;// Включаем таймерTIM3->CR1 |= TIM_CR1_CEN;}}

Таймер TIM3 буде отвечать за генерацию ACK. Это нужно для того, чтобы во время этой задержки ядро было свободно для работы с SD картой. Обработчик прерывания от таймера вот такой:
TIM3_IRQHandler()
// Прерывание таймера TIM3void TIM3_IRQHandler( void ){// Снимаем флагTIM3->SR = 0x0000;// Анализируем режимif ( State.PSIO.Ack == ENABLE ){// Выключаем сигнал ACKMEM_nACK;}else{// Включаем сигнал ACKMEM_ACK;// Перекидываем режимState.PSIO.Ack = ENABLE;// Новый таймаутTIM3->CNT = 0;// Включаем таймерTIM3->CR1 |= TIM_CR1_CEN;}}


Код достаточно комментирован и я думаю, что в особом разборе не нуждается. Отмечу лишь тот момент, что после получения второго байта номера сектора в команде чтения мы устанавливаем флаг для операции чтения с SD карты для кода, который крутится в вечном цикле функции main(). И сразу после этого 4 следующих ACK выдаются с удлинённым временем. В интерфейсе это выглядит вот так:


Скриншот из программы логического анализатора, выделяются 4 большие задержки в транзакции

В сумме набирается порядка 3,5мс и этого с запасом хватает, чтобы код в основном коде успел считать сектор. Более того, тот код может работать только когда нет прерывания, т.е. как раз в эти большие паузы. Во время записи флаг устанавливается в самом конце и из-за того, что система даёт карте памяти отработать запись, основной код работает без помех со стороны прерываний. А теперь глянем в код основного цикла.
main()
// Основной циклwhile ( 1 ){// Обрабатываем сигнал вытаскивания картыif ( CARD_nCD == 0 ){// Карта вставленаif ( State.SDCard.CardType == ctNone ){// Включаем зелёную лампочкуLED_GREEN_ON; LED_RED_OFF;// Карту только что поменяли, пытаемся обнаружитьState.SDCard.CardType = Card_Init();// Карта обнаружена?if ( State.SDCard.CardType != ctUnknown ){// Анализируем файловую систему картыif ( Card_FSInit( &State.SDCard, &CARD_IMAGE[ 0 ] ) == ENABLE ){// Файлоавая система опознана, разрешаем работуEXTI->IMR |= 0x00000004;// Выключаем лампочкиLED_GREEN_OFF; LED_RED_OFF;}else{// Файловая система не опознанаState.SDCard.CardType = ctUnknown;// Зажигаем обе лампочкиLED_GREEN_ON; LED_RED_ON;}}else{// Зажигаем обе лампочкиLED_GREEN_ON; LED_RED_ON;}}}else{// Карта отсутствуетif ( State.SDCard.CardType != ctNone ){// Только вытащили, отключаем PSIOEXTI->IMR &= 0xFFFFFFFA;// Обнуляем все переменныеState.PSIO.Mode = mdSync; State.PSIO.Bits = 0; State.PSIO.DataIn = 0x00; State.PSIO.DataOut = 0; State.PSIO.Ack = DISABLE;State.MemCard.Status = StateNew;State.SDCard.CardType = ctNone; State.SDCard.CardOp = coIdle; State.SDCard.LoadedLBA = 0xFFFFFFFF;}// Потушим обе лампочкиLED_GREEN_OFF; LED_RED_OFF;}// Если карта естьif ( (State.SDCard.CardType != ctNone) && (State.SDCard.CardType != ctUnknown) ){// Заказана запись?if ( State.SDCard.CardOp == coWrite ){// Вычисляем сектор чтения и смещение в блокеOfs = State.MemCard.Sector & 0x03FF;LBA = (Ofs >> 2) & 0x000000FF;Ofs = (Ofs << 7) & 0x00000180;// Считываем сектор в буферCard_Read( State.SDCard.CardType, &State.SDCard.CardBuf[ 0 ], &State.SDCard.LoadedLBA, State.SDCard.CardList[ LBA ] );// Подменяем наш секторfor (Cnt = 0;Cnt < 128;Cnt++){// Переносим данныеState.SDCard.CardBuf[ Ofs + Cnt ] = State.MemCard.Data[ Cnt ];}// Пишем сетор назадCard_Write( State.SDCard.CardType, &State.SDCard.CardBuf[ 0 ], &State.SDCard.LoadedLBA, State.SDCard.CardList[ LBA ] );// Потушем лампочкуLED_RED_OFF;// Снимаем флагState.SDCard.CardOp = coIdle;}// Заказано чтение?if ( State.SDCard.CardOp == coRead ){// Вычисляем сектор чтения и смещение в блокеOfs = State.MemCard.Sector & 0x03FF;LBA = (Ofs >> 2) & 0x000000FF;Ofs = (Ofs << 7) & 0x00000180;// Считываем сектор в буферCard_Read( State.SDCard.CardType, &State.SDCard.CardBuf[ 0 ], &State.SDCard.LoadedLBA, State.SDCard.CardList[ LBA ] );// Копируем нужный секторfor (Cnt = 0;Cnt < 128;Cnt++){// Переносим данныеState.MemCard.Data[ Cnt ] = State.SDCard.CardBuf[ Ofs + Cnt ];}// Снимаем флагState.SDCard.CardOp = coIdle;}}}

В вечном цикле постоянно анализируется сигнал вставления SD карты. Если её вытащить на ходу, то код отключит PSIO и PS1 потеряет карту. Если же карту вставить обратно (или просто подать питание со вставленной картой), то сначала будет попытка инициализировать карту функцией Card_Init(), которая вернёт тип обнаруженной карты. Это важно, потому что у SDv1 и остальных SDHC/SDXC адресация идёт различными методами. Сам код инициализации никаких секретов не несёт и подсмотрен в куче доступных в интернете примеров про FatFS и подобных проектов.
Следом за инициализацией карты вызывается хитрая функция Card_FSInit(). Это самая главная фишка данного проекта. Дело в том, что STM32F042 скромный по возможностям и потянуть полную поддержку FatFS на необходимой скорости не сможет. Поэтому, я придумал такой метод: файл образа у нас всегда 128КиБ, поэтому, необходимо знать только 256 секторов по 512 байт, в каждом из которых будет ровно 4 сектора нашей карты памяти PS1. Таким образом, мы делаем следующее:

  1. Анализируем сектор LBA=#0 на предмет MBR. Если это действительно MBR, то получаем новый сектор, где находится MBS.
  2. Получив адрес предполагаемого MBS (это может быть #0, если нет MBR или какое-то число, если MBR есть) мы начинаем его анализ на предмет принадлежности одной из FAT: FAT12, FAT16, FAT32 или vFAT.
  3. Если сектор прошёл проверку, то мы забираем из него информацию о структуре и в корневом каталоге ищем элемент с именем файла. В данном случае это MEMCRD00.BIN.
  4. Если такой файл находится, то проверяем его размер он должен быть строго фиксирован 0x20000 байт. Если всё так получаем номер первого кластера.
  5. Если мы дошли до этого пункта, то у нас уже есть вся необходимая информации для построения списка физических LBA секторов, где расположен наш образ. Пробегая по цепочке FAT и используя информацию о структуре из MBS, заполняем таблицу из 256 номеров LBA секторов.

В случае успеха запускается PSIO и PS1 увидит свою карту как обычную. Если на каком-либо этапе произошла ошибка, то работа прерывается, загораются оба светодиода и всё остаётся в таком состоянии до снятия питания или замены SD карты. Вот код этой процедуры:
Card_FSInit()
// Инициализация таблицы секторов по имени файла, поддерживается пока только FAT16FunctionalState Card_FSInit( TSDCard *SDCard, const uint8_t *FName ){// Локальные переменныеFunctionalState Res;uint8_t *Buf;uint8_t Pos;uint16_t ClustSize,Reserv,RootSize,FATSize,Cluster;uint32_t Cnt,LBA,SysOrg,FATOrg,RootOrg,DataOrg;int Compare;// ИнитRes = DISABLE; SysOrg = 0; Cluster = 0xFFFF;// Начинаем с самого сначалаwhile ( 1 ){// Вычитываем сектор 0if ( Card_Read( SDCard->CardType, &SDCard->CardBuf[ 0 ], &SDCard->LoadedLBA, SysOrg ) == DISABLE ) { break; }// Анализируем сектор #0 на MBRif ( *((uint16_t *)&SDCard->CardBuf[ 0x01FE ]) != 0xAA55 ) { break; }// Проверим косвенные признаки MBRif ( ((SDCard->CardBuf[ 0x01BE ] == 0x00) || (SDCard->CardBuf[ 0x01BE ] == 0x80)) && ((SDCard->CardBuf[ 0x01CE ] == 0x00) || (SDCard->CardBuf[ 0x01CE ] == 0x80)) && ((SDCard->CardBuf[ 0x01DE ] == 0x00) || (SDCard->CardBuf[ 0x01DE ] == 0x80)) && ((SDCard->CardBuf[ 0x01EE ] == 0x00) || (SDCard->CardBuf[ 0x01EE ] == 0x80)) ){// Похоже на MBR, анализируем таблицу разделовfor (Cnt = 0;Cnt < 4;Cnt++){// Анализируем признак разделаif ( (SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C2 ] == 0x01) ||// Сигнатура 0x01: FAT12 (SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C2 ] == 0x04) ||// Сигнатура 0x04: FAT16 (SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C2 ] == 0x06) ||// Сигнатура 0x06: Big FAT16 (SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C2 ] == 0x0E) )// Сигнатура 0x0E: vFAT{// Сигнатура подошла, забираем адрес MBS разделаSysOrg = SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C6 ];SysOrg += (SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C7 ] * 0x00000100);SysOrg += (SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C8 ] * 0x00010000);SysOrg += (SDCard->CardBuf[ (Cnt * 0x0010) + 0x01C9 ] * 0x01000000);// Выходимbreak;}}}// Загружаем сектор предполагаемого MBSif ( Card_Read( SDCard->CardType, &SDCard->CardBuf[ 0 ], &SDCard->LoadedLBA, SysOrg ) == DISABLE ) { break; }// Анализируем сектор на MBSif ( *((uint16_t *)&SDCard->CardBuf[ 0x01FE ]) != 0xAA55 ) { break; }if ( SDCard->CardBuf[ 0x000D ] == 0x00 ) { break; }if ( (SDCard->CardBuf[ 0x0010 ] == 0x00) || (SDCard->CardBuf[ 0x0010 ] > 0x02) ) { break; }if ( SDCard->CardBuf[ 0x0015 ] != 0xF8 ) { break; }if ( *((uint32_t *)&SDCard->CardBuf[ 0x001C ]) != SysOrg ) { break; }if ( SDCard->CardBuf[ 0x0026 ] != 0x29 ) { break; }if ( *((uint16_t *)&SDCard->CardBuf[ 0x0036 ]) != 0x4146 ) { break; }if ( *((uint16_t *)&SDCard->CardBuf[ 0x0038 ]) != 0x3154 ) { break; }if ( SDCard->CardBuf[ 0x003A ] != 0x36 ) { break; }// Заполняем локальные переменные, которые нужны для математикиClustSize = SDCard->CardBuf[ 0x000D ];Reserv = *((uint16_t *)&SDCard->CardBuf[ 0x000E ]);RootSize = (SDCard->CardBuf[ 0x0012 ] * 0x0100) + SDCard->CardBuf[ 0x0011 ];FATSize = *((uint16_t *)&SDCard->CardBuf[ 0x0016 ]);// Вычисляем координаты FAT и ROOTFATOrg = SysOrg + Reserv;RootOrg = FATOrg + (FATSize * 2);DataOrg = RootOrg + (RootSize / 16 );// Все данные получены, приступаем к поиску имени файла нужного имиджаfor (LBA = 0;LBA < (RootSize / 16);LBA++){// Загружаем сектор корневой папкиif ( Card_Read( SDCard->CardType, &SDCard->CardBuf[ 0 ], &SDCard->LoadedLBA, RootOrg + LBA ) == ENABLE ){// Перебираем 16 элементов, которые могут находиться в сектореfor (Cnt = 0;Cnt < 16;Cnt++){// Сравниваем имяCompare = memcmp( &SDCard->CardBuf[ Cnt * 32 ], &CARD_IMAGE[ 0 ], 11 );if (  Compare == 0 ){// Файл найден, проверим размерif ( *((uint32_t *)&SDCard->CardBuf[ (Cnt * 32) + 0x001C ]) == 0x00020000 ){// Размер подходит, копируем номер кластераCluster = *((uint16_t *)&SDCard->CardBuf[ (Cnt * 32) + 0x001A ]);// Без ошибокRes = ENABLE;// Выходимbreak;}}}// Если файл найден - выходим экстренноif ( Res == ENABLE ) { break; }}else{// ошибка загрузки - вываливаемсяbreak;}}// Файл найден, данные получены, начинаем построение таблицы доступаif ( Res == ENABLE ){// У нас есть номер кластера, готовимся заполнять табличкуPos = 0;do{// Проверяем номер кластераif ( Cluster < 0x0002 ){// Ошибка, выходимRes = DISABLE; break;}// Вычисляем LBA данных кластераLBA = DataOrg + ((Cluster - 2) * ClustSize);// В цикле по размерку кластера заполняем элементы таблицыfor (Cnt = 0;Cnt < ClustSize;Cnt++){// Вычисляем LBA сектроа внутри кластераSDCard->CardList[ Pos ] = LBA + Cnt;// Следующий элементPos++; if ( Pos == 0 ) { break; }}// Если есть ещё элементы, надо получить новый номер кластера// А для этого надо вычислить номер сектора, где этот кластер находится и загрузить его по цепочкеif ( Pos != 0 ){// Вычисляем сектор нахождения кластераLBA = FATOrg; Reserv = Cluster;while ( Reserv > 256 ) { LBA++; Reserv -= 256; }// Загружаем этот сектор в памятьif ( Card_Read( SDCard->CardType, &SDCard->CardBuf[ 0 ], &SDCard->LoadedLBA, LBA ) == ENABLE ){// Забираем новый номер кластераCluster = *((uint16_t *)&SDCard->CardBuf[ Reserv * 2 ]);}else{// Ошибка загрузкиRes = DISABLE; break;}}} while ( (Cluster != 0xFFFF) && (Pos != 0) );}// Выходbreak;}// Выходreturn Res;}

Скажу честно, так как это всего лишь PoC, то здесь реализован поиск только у FAT16. FAT12, наверное, и не надо поддерживать microSD таких малых объёмов не бывает. А вот FAT32 или vFAT добавить возможно, если это кому-нибудь понадобится в будущем.

Имя образа MEMCRD00.BIN выбрано не случайно. Дело в том, что в будущем я планирую добавить выбор образа через стандартную для многостраничных карт памяти комбинацию кнопок на джойпаде: при зажатом SELECT следует однократное нажатие на L1/R1. И меняя последние 2 символа можно поддержать 100 образов в корневой директории, от MEMCRD00.BIN до MEMCRD99.BIN. Для этого есть задел в обработчике прерывания по SCK в интерфейса PSIO, ветка где анализируется обращение к джойпаду. Сделать сниффер проблем нет, но периферия контроллеров у PS1 богатая и придётся практически всех их поддерживать.

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

P.S. Я бы очень хотел указать здесь список всех источников информации, которые я использовал в создании этого проекта, но увы это очень затруднительно. Многое было подслушано случайно. Кое-что ходило в виде TXT файлов с общей информацией про PS1 более 15 лет назад, для тех, кто хотел написать свой эмулятор. И теперь всё это существует в виде нескольких текстовых файлов на моём жёстком диске. Можно сказать, что источником информации служил весь интернет на протяжении последних 15 лет.
Подробнее..

Разрабатываем web-site для микроконтроллера

09.02.2021 18:11:49 | Автор: admin

С приходом в нашу жизнь различного рода умных розеток, лампочек и других подобных устройств, необходимость наличия веб-сайтов на микроконтроллерах стала неоспоримой. А благодаря проекту lwIP (и его младшему брату uIP) подобным функционалом никого не удивишь. Но поскольку lwIP направлен на минимизацию ресурсов, то с точки зрения дизайна, функционала, а также удобства использования и разработки, подобные сайты сильно отстают от тех, к которым мы привыкли. Даже для встроенных систем, сравнить, например, с сайтом для администрирования на самых дешевых роутерах. В данной статье мы попробуем разработать сайт на Линуксе для какого-нибудь умного устройства и запустить его на микроконтроллере.

Для запуска на микроконтроллере будем использовать Embox. В состав этой RTOS входит HTTP сервер с поддержкой CGI. В качестве HTTP сервера на Linux будем использовать встроенный в python HTTP сервер.
python3 -m http.server -d <site folder>


Статический сайт


Начнем с простого статического сайта, состоящего из одной или нескольких страниц.
Тут все просто, давайте создадим папку и в ней index.html. Этот файл будет скачиваться по умолчанию, если в браузере задан только адрес сайта.
$ ls website/em_big.png  index.html


Сайт еще будет содержать логотип Embox, файл em_big.png, который мы встроим в html.

Запустим http сервер
python3 -m http.server -d website/


Зайдем в браузере на localhost:8000


Теперь добавим наш статический сайт в файловую систему Embox. Это можно сделать скопировав нашу папку в папку rootfs/ темплейта (текущий темплейт в папке conf/rootfs). Или создать модуль указав в нем файлы для rootfs.
$ ls website/em_big.png  index.html  Mybuild


Содержимое Mybuild.
package embox.demomodule website {    @InitFS    source "index.html",        "em_big.png",}

Для простоты мы положим наш сайт прямо в корневую папку (аннотация @InitFs без параметров).

Нам также нужно включить наш сайт в конфигурационном файле mods.conf и туда же добавить сам httd сервер

    include embox.cmd.net.httpd        include embox.demo.website


Кроме того, давайте запустим сервер с нашим сайтом во время старта системы. Для этого добавим строчку в файл conf/system_start.inc

"service httpd /",


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

И имеем такую же картинку как и для локального сайта


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

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

Добавим в наш сайт app.js (сайт на angular) и в нем пару вкладок. Страницы для этих вкладок положим в папку partials, изображения в папку images/, а css файлы в css/.

$ ls website/app.js  css  images  index.html  Mybuild  partials


Запустим наш сайт.


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

Естественно, при этом можно использовать все средства разработки обычных веб-девелоперов. Так, открыв консоль в браузере, мы обнаружили сообщение об ошибке, о том что не хватает favicon.ico:


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

Поиск в интернете сразу выдал, что можно обойтись и без файла, нужно всего лишь добавить строку в head секцию html. Хотя ошибка не мешала, но сделать сайт чуть лучше всегда приятно. И главное, что мы убедились, что обычные средства разработчика вполне применимы при предлагаемом подходе.

Динамический контент


CGI


Перейдем к динамическому контенту. Common Gateway Interface (CGI) интерфейс взаимодействия web-сервера с утилитами командной строки, позволяющий создавать динамический контент. Иными словами, CGI позволяет использовать вывод утилит для генерации динамического контента.

Давайте взглянем на какой-нибудь CGI скрипт
#!/bin/bashecho -ne "HTTP/1.1 200 OK\r\n"echo -ne "Content-Type: application/json\r\n"echo -ne "Connection: Connection: close\r\n"echo -ne "\r\n"tm=`LC_ALL=C date +%c`echo -ne "\"$tm\"\n\n"


Вначале в стандартный output печатается http заголовок, а затем печатаются данные самой страницы. output может быть перенаправлен куда угодно. Можно просто запустить этот скрипт из консоли. Увидим следующее:
./cgi-bin/gettimeHTTP/1.1 200 OKContent-Type: application/jsonConnection: Connection: close"Fri Feb  5 20:58:19 2021"


А если вместо стандартного output это будет socket то браузер получит эти данные.

CGI часто реализуют с помощью скриптов, даже говорят cgi scripts. Но это не обязательно, просто на скриптовых языках подобные вещи делать быстрее и удобнее. Утилита предоставляющая CGI может быть реализована на любом языке. И так как мы ориентируемся на микроконтроллеры, следовательно, стараемся заботиться об экономии ресурсов. Давайте то же самое реализуем на С.
#include <stdio.h>#include <unistd.h>#include <string.h>int main(int argc, char *argv[]) {    char buf[128];    char *pbuf;    struct timeval tv;    time_t time;    printf(        "HTTP/1.1 200 OK\r\n"        "Content-Type: application/json\r\n"        "Connection: Connection: close\r\n"        "\r\n"    );    pbuf = buf;    pbuf += sprintf(pbuf, "\"");    gettimeofday(&tv, NULL);    time = tv.tv_sec;    ctime_r(&time, pbuf);    strcat(pbuf, "\"\n\n");    printf("%s", buf);    return 0;}


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

В наш app.js добавим обработчик для вызова CGI скрипта для одной из нашей вкладки
app.controller("SystemCtrl", ['$scope', '$http', function($scope, $http) {    $scope.time = null;    $scope.update = function() {        $http.get('cgi-bin/gettime').then(function (r) {            $scope.time = r.data;        });    };    $scope.update();}]);


Небольшой нюанс по запуску на Linux с помощью встроенного сервера python. В нашу строку запуска нужно добавить аргумент --cgi для поддержки CGI:
python3 -m http.server --cgi -d .




Автоматическое обновление динамического контента


Теперь давайте разберемся с еще одним очень важным свойством динамического сайта автоматическим обновлением содержимого. Есть несколько механизмов для его реализации:
  • Server Side Includes (SSI)
  • Server-sent Events (SSE)
  • WebSockets
  • И так далее


Server Side Includes (SSI).


Server Side Includes (SSI). Это несложный язык для динамического создания веб-страниц. Обычно файлы использующие SSI имеют формат .shtml.

Сам SSI имеет даже директивы управления, if else и так далее. Но в большинстве примеров для микроконтроллеров, которые мы находили, он используется следующим образом. В .shtml страницу вставляется директива, которая периодически перегружает всю страницу. Это может быть, например
<meta http-equiv="refresh" content="1">

Или
<BODY onLoad="window.setTimeout("location.href='runtime.shtml'",2000)">


И тем или иным образом происходит генерация контента, например, с помощью задания специального обработчика.

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



Обновление страницы (см. вкладку) сильно заметно. И перезагружать всю страницу, выглядит как чрезмерно избыточное действие.

Приведен стандартный пример из FreeRTOS https://www.freertos.org/FreeRTOS-For-STM32-Connectivity-Line-With-WEB-Server-Example.html

Server-sent Events


Server-sent Events (SSE) это механизм, который позволяет установить полудуплексное (одностороннее) соединение между клиентом и сервером. Клиент в этом случае открывает соединение, и сервер использует его для передачи данных клиенту. При этом, в отличие от классических CGI скриптов, цель которых сформировать и отправить ответ клиенту, после чего завершиться, SSE предлагает непрерывный режим. То есть сервер может отправлять сколько угодно данных до тех пор пока либо не завершится самостоятельно, либо клиент не закроет соединение.

Есть несколько небольших отличий от обычных CGI скриптов. Во-первых, http заголовок будет немного другой:
        "Content-Type: text/event-stream\r\n"        "Cache-Control: no-cache\r\n"        "Connection: keep-alive\r\n"


Connection, как видно, не close, а keep-alive, то есть продолжающееся соединение. Чтобы браузер не кешировал данные нужно указать Cache-Control no-cache. Ну и наконец, нужно указать что используется специальный тип данных Content-Type text/event-stream.

Этот тип данных представляет из себя специальный формат для SSE:
: this is a test streamdata: some textdata: another messagedata: with two lines


В нашем случае данные нужно упаковать в следующую строку
data: { time: <real date>}


Наш CGI скрипт будет выглядеть
#!/bin/bashecho -ne "HTTP/1.1 200 OK\r\n"echo -ne "Content-Type: text/event-stream\r\n"echo -ne "Cache-Control: no-cache\r\n"echo -ne "Connection: keep-alive\r\n"echo -ne "\r\n"while true; do    tm=`LC_ALL=C date +%c`    echo -ne "data: {\"time\" : \"$tm\"}\n\n" 2>/dev/null || exit 0    sleep 1done


Вывод если запустить скрипт
$ ./cgi-bin/gettimeHTTP/1.1 200 OKContent-Type: text/event-streamCache-Control: no-cacheConnection: keep-alivedata: {"time" : "Fri Feb  5 21:48:11 2021"}data: {"time" : "Fri Feb  5 21:48:12 2021"}data: {"time" : "Fri Feb  5 21:48:13 2021"}


И так далее раз в секунду

Тоже самое на С
#include <stdio.h>#include <unistd.h>#include <string.h>int main(int argc, char *argv[]) {    char buf[128];    char *pbuf;    struct timeval tv;    time_t time;    printf(        "HTTP/1.1 200 OK\r\n"        "Content-Type: text/event-stream\r\n"        "Cache-Control: no-cache\r\n"        "Connection: keep-alive\r\n"        "\r\n"    );    while (1) {        pbuf = buf;        pbuf += sprintf(pbuf, "data: {\"time\" : \"");        gettimeofday(&tv, NULL);        time = tv.tv_sec;        ctime_r(&time, pbuf);        strcat(pbuf, "\"}\n\n");        if (0 > printf("%s", buf)) {            break;        }        sleep(1);    }    return 0;}


И наконец, нужно еще сообщить angular, что у нас SSE, то есть модифицировать код для нашего контроллера
app.controller("SystemCtrl", ['$scope', '$http', function($scope, $http) {    $scope.time = null;    var eventCallbackTime = function (msg) {        $scope.$apply(function () {            $scope.time = JSON.parse(msg.data).time        });    }    var source_time = new EventSource('/cgi-bin/gettime');    source_time.addEventListener('message', eventCallbackTime);    $scope.$on('$destroy', function () {        source_time.close();    });    $scope.update = function() {    };    $scope.update();}]);


Запускаем сайт, видим следующее:


Заметно, что в отличие от использования SSI, страница не перегружается, и данные плавно и приятно для глаза обновляются.

Демо


Конечно приведенные примеры не реальные поскольку очень простые. Их цель показать разницу между используемыми на микроконтроллерах и в остальных системах подходов.

Мы сделали небольшую демонстрацию с реальными задачами. Управлением светодиодами, получением данных в реальном времени с датчика угловой скорости (гироскопа) и вкладкой с системной информацией.

Разработка сайта велась на хосте. Нужно было только сделать маленькие заглушки для эмуляции светодиодов и данных с датчика. Данные с датчика это просто случайные значения получаемые через стандартный RANDOM
#!/bin/bashecho -ne "HTTP/1.1 200 OK\r\n"echo -ne "Content-Type: text/event-stream\r\n"echo -ne "Cache-Control: no-cache\r\n"echo -ne "Connection: keep-alive\r\n"echo -ne "\r\n"while true; do    x=$((1 + $RANDOM % 15000))    y=$((1 + $RANDOM % 15000))    z=$((1 + $RANDOM % 15000))    echo -ne "data: {\"rate\" : \"x:$x y:$y z:$z\"}\n\n" 2>/dev/null || exit 0    sleep 1done


Состояние светодиодов просто храним в файле.
#!/bin/python3import cgiimport sysprint("HTTP/1.1 200 OK")print("Content-Type: text/plain")print("Connection: close")print()form = cgi.FieldStorage()cmd = form['cmd'].valueif cmd == 'serialize_states':    with open('cgi-bin/leds.txt', 'r') as f:        print('[' + f.read() + ']')elif cmd == 'clr' or cmd == 'set':    led_nr = int(form['led'].value)    with open('cgi-bin/leds.txt', 'r+') as f:        leds = f.read().split(',')        leds[led_nr] = str(1 if cmd == 'set' else 0)        f.seek(0)        f.write(','.join(leds))


То же самое тривиально реализовано и в C варианте. При желании можно посмотреть код в репозитории папка (project/website).

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

Скриншот запущенный на хосте выглядит так


На коротком видео можно увидеть работу на реальном микроконтроллере. Отмечу, что происходит не только общение по http, но и например установка даты с помощью ntp из командной строки в Embox, и конечно обращение с периферией.


Самостоятельно все приведенное в статье можно воспроизвести по инструкции на нашем вики

Заключение


В статье мы показали, что возможно разрабатывать красивые интерактивные сайты и запускать их на микроконтроллерах. Причем делать это легко и быстро используя все средства разработки под хост и затем запускать из на микроконтроллерах. Естественно, разработкой сайта может заниматься профессиональный веб-дизайнер, в то время как embedded разработчик будет реализовывать логику работы устройства. Что очень удобно и сильно экономит время выхода на рынок.

Естественно за это придется расплачиваться. Да SSE потребует немного больше ресурсов чем SSI. Но мы с помощью Embox легко вместились в STM32F4 причем без оптимизации и использовали всего 128 кб ОЗУ. Меньше просто проверять не стали. Так что накладные расходы не такие уж большие. А удобство разработки и качество самого сайта сильно выше. И при этом конечно не стоит забывать, что современные микроконтроллеры заметно подросли и продолжают это делать. Ведь от устройств требуют быть все более умными.
Подробнее..

Добавляем modbus в Embox RTOS и используем на STM32 и не только

17.03.2021 18:23:31 | Автор: admin
image
Нас часто спрашивают, чем Embox отличается от других ОС для микроконтроллеров, например, FreeRTOS? Сравнивать проекты между собой, конечно, правильно. Но параметры, по которым порой предлагают сравнение, лично меня повергают в легкое недоумение. Например, сколько нужно памяти для работы Embox? А какое время переключения между задачами? А в Embox поддерживается modbus? В данной статье на примере вопроса про modbus мы хотим показать, что отличием Embox является другой подход к процессу разработки.

Давайте разработаем устройство, в составе которого будет работать в том числе modbus server. Наше устройство будет простым. Ведь оно предназначено только для демонстрации modbus, Данное устройство будет позволять управлять светодиодами по протоколу Modbus. Для связи с устройством будем использовать ethernet соединение.

Modbus открытый коммуникационный протокол. Широко применяется в промышленности для организации связи между электронными устройствами. Может использоваться для передачи данных через последовательные линии связи RS-485, RS-422, RS-232 и сети TCP/IP (Modbus TCP).

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

Одной из самых популярных реализаций протокола modbus является открытый проект libmodbus. Его и будем использовать. Это позволит сократить время разработки и уменьшить количество ошибок. При этом мы сможем сосредоточиться на реализации бизнес логики, а не на изучении протокола.

Наш проект будем вести в отдельном репозитории. При желании все можно скачать и воспроизвести самостоятельно.

Разработка прототипа на Linux


Начнем с разработки прототипа на хосте. Для того чтобы можно было использовать libmodbus в качестве библиотеки его нужно скачать, сконфигурировать и собрать.
Для этих целей я набросал Makefile

libmodbus-$(LIBMODBUS_VER).tar.gz:    wget http://libmodbus.org/releases/libmodbus-$(LIBMODBUS_VER).tar.gz$(BUILD_BASE)/libmodbus/lib/pkgconfig/libmodbus.pc : libmodbus-$(LIBMODBUS_VER).tar.gz    tar -xf libmodbus-$(LIBMODBUS_VER).tar.gz    cd libmodbus-$(LIBMODBUS_VER); \    ./configure --prefix=$(BUILD_BASE)/libmodbus --enable-static --disable-shared; \    make install; cd ..;


Собственно из параметров конфигурации мы используем только prefix чтобы собрать библиотеку локально. А поскольку мы хотим использовать библиотеку не только на хосте, соберем ее статическую версию.

Теперь нам нужен modbus сервер. В проекте libmodbus есть примеры, давайте на основе какого-нибудь простого сервера сделаем свою реализацию.

    ctx = modbus_new_tcp(ip, port);    header_len = modbus_get_header_length(ctx);    query = malloc(MODBUS_TCP_MAX_ADU_LENGTH);    modbus_set_debug(ctx, TRUE);    mb_mapping = mb_mapping_wrapper_new();    if (mb_mapping == NULL) {        fprintf(stderr, "Failed to allocate the mapping: %s\n",                modbus_strerror(errno));        modbus_free(ctx);        return -1;    }    listen_socket = modbus_tcp_listen(ctx, 1);    for (;;) {        client_socket = modbus_tcp_accept(ctx, &listen_socket);        if (-1 == client_socket) {            break;        }        for (;;) {            int query_len;            query_len = modbus_receive(ctx, query);            if (-1 == query_len) {                /* Connection closed by the client or error */                break;            }            if (query[header_len - 1] != MODBUS_TCP_SLAVE) {                continue;            }            mb_mapping_getstates(mb_mapping);            if (-1 == modbus_reply(ctx, query, query_len, mb_mapping)) {                break;            }            leddrv_updatestates(mb_mapping->tab_bits);        }        close(client_socket);    }    printf("exiting: %s\n", modbus_strerror(errno));    close(listen_socket);    mb_mapping_wrapper_free(mb_mapping);    free(query);    modbus_free(ctx);


Здесь все стандартно. Пара мест, которые представляют интерес, это функции mb_mapping_getstates и leddrv_updatestates. Это как раз функционал, который и реализует наше устройство.

static modbus_mapping_t *mb_mapping_wrapper_new(void) {    modbus_mapping_t *mb_mapping;    mb_mapping = modbus_mapping_new(LEDDRV_LED_N, 0, 0, 0);    return mb_mapping;}static void mb_mapping_wrapper_free(modbus_mapping_t *mb_mapping) {    modbus_mapping_free(mb_mapping);}static void mb_mapping_getstates(modbus_mapping_t *mb_mapping) {    int i;    leddrv_getstates(mb_mapping->tab_bits);    for (i = 0; i < mb_mapping->nb_bits; i++) {        mb_mapping->tab_bits[i] = mb_mapping->tab_bits[i] ? ON : OFF;    }}


Таким образом, нам нужны leddrv_updatestates, которая задает состояние светодиодов, и leddrv_getstates, которая получает состояние светодиодов.
static unsigned char leddrv_leds_state[LEDDRV_LED_N];int leddrv_init(void) {    static int inited = 0;    if (inited) {        return 0;    }    inited = 1;    leddrv_ll_init();    leddrv_load_state(leddrv_leds_state);    leddrv_ll_update(leddrv_leds_state);    return 0;}...int leddrv_getstates(unsigned char leds_state[LEDDRV_LED_N]) {    memcpy(leds_state, leddrv_leds_state, sizeof(leddrv_leds_state));    return 0;}int leddrv_updatestates(unsigned char new_leds_state[LEDDRV_LED_N]) {    memcpy(leddrv_leds_state, new_leds_state, sizeof(leddrv_leds_state));    leddrv_ll_update(leddrv_leds_state);    return 0;}


Так как мы хотим чтобы наше ПО работало и на плате и на хосте, нам понадобятся разные реализации функций установки и получения состояния светодиодов. Давайте для хоста хранить состояние в обычном файле. Это позволит получать состояние светодиодов в других процессах.

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

void leddrv_ll_update(unsigned char leds_state[LEDDRV_LED_N]) {    int i;    int idx;    char buff[LEDDRV_LED_N * 2];        for (i = 0; i < LEDDRV_LED_N; i++) {        char state = !!leds_state[i];        fprintf(stderr, "led(%03d)=%d\n", i, state);        buff[i * 2] = state + '0';        buff[i * 2 + 1] = ',';    }    idx = open(LED_FILE_NAME, O_RDWR);    if (idx < 0) {        return;    }    write(idx, buff, (LEDDRV_LED_N * 2) - 1);    close(idx);}...void leddrv_load_state(unsigned char leds_state[LEDDRV_LED_N]) {    int i;    int idx;    char buff[LEDDRV_LED_N * 2];    idx = open(LED_FILE_NAME, O_RDWR);    if (idx < 0) {        return;    }    read(idx, buff, (LEDDRV_LED_N * 2));    close(idx);        for (i = 0; i < LEDDRV_LED_N; i++) {        leds_state[i] = buff[i * 2] - '0';    }}


Нам нужно указать файл где будет сохранено начальное состояние светодиодов. Формат файла простой. Через запятую перечисляются состояние светодиодов, 1 светодиод включен, а 0 -выключен. В нашем устройстве 80 светодиодов, точнее 40 пар светодиодов. Давайте предположим, что по умолчанию четные светодиоды будут выключены а нечетные включены. Содержимое файла

0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1


Запускаем сервер
./led-serverled(000)=0led(001)=1...led(078)=0led(079)=1


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

ctx = modbus_new_tcp(ip, port);    if (ctx == NULL) {        fprintf(stderr, "Unable to allocate libmodbus context\n");        return -1;    }    modbus_set_debug(ctx, TRUE);    modbus_set_error_recovery(ctx,            MODBUS_ERROR_RECOVERY_LINK |            MODBUS_ERROR_RECOVERY_PROTOCOL);    if (modbus_connect(ctx) == -1) {        fprintf(stderr, "Connection failed: %s\n",                modbus_strerror(errno));        modbus_free(ctx);        return -1;    }    if (1 == modbus_write_bit(ctx, bit_n, bit_value)) {        printf("OK\n");    } else {        printf("FAILED\n");    }    /* Close the connection */    modbus_close(ctx);    modbus_free(ctx);


Запускаем клиент. Установим 78 светодиод, который по умолчанию выключен

./led-client set 78Connecting to 127.0.0.1:1502[00][01][00][00][00][06][FF][05][00][4E][FF][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4E><FF><00>OK


На сервере увидим
...led(076)=0led(077)=1led(078)=1led(079)=1Waiting for an indication...ERROR Connection reset by peer: read


То есть светодиод установлен. Давайте выключим его.
./led-client clr 78Connecting to 127.0.0.1:1502[00][01][00][00][00][06][FF][05][00][4E][00][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4E><00><00>OK


На сервере увидим сообщение об изменении
...led(076)=0led(077)=1led(078)=0led(079)=1Waiting for an indication...ERROR Connection reset by peer: read


Запустим http сервер. О разработке веб-сайтов мы рассказывали в статье. К тому же веб-сайт нам нужен только для более удобной демонстрации работы modbus. Поэтому не буду сильно вдаваться в подробности. Сразу приведу cgi скрипт

#!/bin/bashecho -ne "HTTP/1.1 200 OK\r\n"echo -ne "Content-Type: application/json\r\n"echo -ne "Connection: close\r\n"echo -ne "\r\n"if [ $REQUEST_METHOD = "GET" ]; then    echo "Query: $QUERY_STRING" >&2    case "$QUERY_STRING" in        "c=led_driver&a1=serialize_states")            echo [ $(cat ../emulate/conf/leds.txt) ]            ;;        "c=led_driver&a1=serialize_errors")            echo [ $(printf "0, %.0s" {1..79}) 1 ]            ;;        "c=led_names&a1=serialize")            echo '[ "one", "two", "WWWWWWWWWWWWWWWW", "W W W W W W W W " ]'            ;;    esacelif [ $REQUEST_METHOD = "POST" ]; then    read -n $CONTENT_LENGTH POST_DATA    echo "Posted: $POST_DATA" >&2fi


И напомню что запустить можно с помощью любого http сервера с поддержкой CGI. Мы используем встроенный в python сервер. Запускаем следующей командой
python3 -m http.server --cgi -d .


Откроем наш сайт в браузере


Установим 78 светодиод с помощью клиента
./led-client -a 127.0.0.1 set 78Connecting to 127.0.0.1:1502[00][01][00][00][00][06][FF][05][00][4E][FF][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4E><FF><00>OK


сбросим 79 светодиод
./led-client -a 127.0.0.1 clr 79Connecting to 127.0.0.1:1502[00][01][00][00][00][06][FF][05][00][4F][00][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4F><00><00>OK


На сайте увидим разницу


Собственно все, на Linux наша библиотека прекрасно работает.

Адаптация к Embox и запуск на эмуляторе


Библиотека libmodbus


Теперь нам нужно перенести код в Embox. начнем с самого проекта libmodbus.
Все просто. Нам нужно описание модуля (Mybuild)
package third_party.lib@Build(script="$(EXTERNAL_MAKE)")@BuildArtifactPath(cppflags="-I$(ROOT_DIR)/build/extbld/third_party/lib/libmodbus/install/include/modbus")module libmodbus {    @AddPrefix("^BUILD/extbld/^MOD_PATH/install/lib")    source "libmodbus.a"    @NoRuntime depends embox.compat.posix.util.nanosleep}


Мы с помощью аннотации Build(script="$(EXTERNAL_MAKE)") указываем что используем Makefile для работы с внешними проектами.

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

И говорим что нам нужна библиотека source libmodbus.a

PKG_NAME := libmodbusPKG_VER  := 3.1.6PKG_SOURCES := http://libmodbus.org/releases/$(PKG_NAME)-$(PKG_VER).tar.gzPKG_MD5     := 15c84c1f7fb49502b3efaaa668cfd25ePKG_PATCHES := accept4_disable.patchinclude $(EXTBLD_LIB)libmodbus_cflags = -UHAVE_ACCEPT4$(CONFIGURE) :    export EMBOX_GCC_LINK=full; \    cd $(PKG_SOURCE_DIR) && ( \        CC=$(EMBOX_GCC) ./configure --host=$(AUTOCONF_TARGET_TRIPLET) \        prefix=$(PKG_INSTALL_DIR) \        CFLAGS=$(libmodbus_cflags) \    )    touch $@$(BUILD) :    cd $(PKG_SOURCE_DIR) && ( \        $(MAKE) install MAKEFLAGS='$(EMBOX_IMPORTED_MAKEFLAGS)'; \    )    touch $@


Makefile для сборки тоже простой и очевидный. Единственное, отмечу что используем внутренний компилятор ($(EMBOX_GCC) ) Embox и в качестве платформы (--host) передаем ту, которая задана в Embox ($(AUTOCONF_TARGET_TRIPLET)).

Подключаем проект к Embox


Напомню, что для удобства разработки мы создали отдельный репозиторий. Для того чтобы подключить его к Embox достаточно указать Embox где лежит внешний проект.
Делается это с помощью команды
make ext_conf EXT_PROJECT_PATH=<path to project> 

в корне Embox. Например,
 make ext_conf EXT_PROJECT_PATH=~/git/embox_project_modbus_iocontrol


modbus-server


Исходный код modbus сервера не требует изменений. То есть мы используем тот же код, который разработали на хосте. Нам нужно добавить Mybuild
package iocontrol.modbus.cmd@AutoCmd@Build(script="true")@BuildDepends(third_party.lib.libmodbus)@Cmd(name="modbus_server")module modbus_server {    source "modbus_server.c"    @NoRuntime depends third_party.lib.libmodbus}


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

Нам также понадобятся библиотеки эмуляции. Не буду приводить Mybuild для них, они тривиальны, лишь отмечу, что исходники также используются без изменений.

Нам также нужно собрать нашу систему вместе с modbus сервером
Добавляем наши модули в mods.conf
    include iocontrol.modbus.http_admin    include iocontrol.modbus.cmd.flash_settings    include iocontrol.modbus.cmd.led_names    include third_party.lib.libmodbus    include iocontrol.modbus.cmd.modbus_server    include iocontrol.modbus.cmd.led_driver    include embox.service.cgi_cmd_wrapper(cmds_check=true, allowed_cmds="led_driver led_names flash_settings")    include iocontrol.modbus.lib.libleddrv_ll_stub


А наш файл leds.txt со статусами светодиодов кладем в корневую файловую систему. Но так как нам нужен изменяемый файл, давайте добавим RAM disk и скопируем наш файл на этот диск. Содержимое system_start.inc
"export PWD=/","export HOME=/","netmanager","service telnetd","service httpd http_admin","ntpdate 0.europe.pool.ntp.org","mkdir -v /conf","mount -t ramfs /dev/static_ramdisk /conf","cp leds.txt /conf/leds.txt","led_driver init","service modbus_server","tish",


Этого достаточно запустим Embox на qemu
./scripts/qemu/auto_qemu

modbus и httpd сервера запускаются автоматически при старте. Установим такие же значения с помощью modbus клиента, только указав адрес нашего QEMU (10.0.2.16)
./led-client -a 10.0.2.16 set 78Connecting to 10.0.2.16:1502[00][01][00][00][00][06][FF][05][00][4E][FF][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4E><FF><00>OK


и соответственно
./led-client -a 10.0.2.16 clr 79Connecting to 10.0.2.16:1502[00][01][00][00][00][06][FF][05][00][4F][00][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4F><00><00>


Откроем браузер


Как и ожидалось все тоже самое. Мы можем управлять устройством через modbus протокол уже на Embox.

Запуск на микроконтроллере


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

Но на плате STM32F4-discovery всего 4 светодиода. Было бы удобно задавать количество светодиодов, чтобы не модифицировать исходный код В Embox есть механизм позволяющий параметризировать модули. Нужно в описании модуля (Mybuild) добавить опцию
package iocontrol.modbus.libstatic module libleddrv {    option number leds_quantity = 80...}


И можно будет использовать в коде
#ifdef __EMBOX__#include <framework/mod/options.h>#include <module/iocontrol/modbus/lib/libleddrv.h>#define LEDDRV_LED_N OPTION_MODULE_GET(iocontrol__modbus__lib__libleddrv,NUMBER,leds_quantity)#else#define LEDDRV_LED_N 80#endif


При этом менять этот параметр можно будет указав его в файле mods.conf
    include  iocontrol.modbus.lib.libleddrv(leds_quantity=4)


если параметр не указывается, то используется тот который задан в модуле по умолчанию, то есть 80.

Нам нужно еще управлять реальными линиями вывода. Код следующий
struct leddrv_pin_desc {    int gpio; /**< port */    int pin; /**< pin mask */};static const struct leddrv_pin_desc leds[] = {    #include <leds_config.inc>};void leddrv_ll_init(void) {    int i;    for (i = 0; i < LEDDRV_LED_N; i++) {        gpio_setup_mode(leds[i].gpio, leds[i].pin, GPIO_MODE_OUTPUT);    }}void leddrv_ll_update(unsigned char leds_state[LEDDRV_LED_N]) {    int i;    for (i = 0; i < LEDDRV_LED_N; i++) {        gpio_set(leds[i].gpio, leds[i].pin,                leds_state[i] ? GPIO_PIN_HIGH : GPIO_PIN_LOW);    }}


В файле mods.conf нам нужна конфигурация для нашей платы. К ней добавляем наши модули
    include iocontrol.modbus.http_admin    include iocontrol.modbus.cmd.flash_settings    include iocontrol.modbus.cmd.led_names    include third_party.lib.libmodbus    include iocontrol.modbus.cmd.modbus_server    include iocontrol.modbus.cmd.led_driver    include embox.service.cgi_cmd_wrapper(cmds_check=true, allowed_cmds="led_driver led_names flash_settings")    include iocontrol.modbus.lib.libleddrv(leds_quantity=4)    include iocontrol.modbus.lib.libleddrv_ll_stm32_f4_demo


По сути дела, те же модули как и для ARM QEMU, за исключением конечно драйвера.

Собираем, прошиваем, запускаем. И с помощью того же modbus клиента управляем светодиодами. Нужно только поставить правильный адрес, и не забыть что у нас всего 4 светодиода на плате.

Работу на плате stm32f4-discovery можно увидеть на этом коротком видео


Выводы


На этом простом примере мы постарались показать, в чем же основное отличие Embox от других ОС для микроконтроллеров. В том числе которые имеют POSIX совместимость. Ведь мы по сути дела, взяли готовый модуль, разработали бизнес логику на Linux используя при этом несколько приложений. И запустили все это на нашей целевой платформе. Тем самым существенно упростив и ускорив саму разработку.

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

Запуск QT на STM32. Часть 2. Теперь с псевдо 3d и тачскрином

05.04.2021 20:15:29 | Автор: admin
Мы в проекте Embox некоторое время назад запустили Qt на платформе STM32. Примером было приложение moveblocks анимация с четырьмя синими квадратами, которые перемещаются по экрану. Нам захотелось большего, например, добавить интерактивность, ведь на плате доступен тачскрин. Мы выбрали приложение animatedtiles просто потому, что оно и на компьютере круто смотрится. По нажатию виртуальных кнопок множество иконок плавно перемещаются по экрану, собираясь в различные фигуры. Причем выглядит это вполне как 3d анимация и у нас даже были сомнения, справится ли микроконтроллер с подобной задачей.

Сборка


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

Первый запуск на плате


Размер экрана у STM32F746G-Discovery 480x272, при запуске приложение нарисовалось только в верхнюю часть экрана. Нам естественно захотелось выяснить в чем дело. Конечно можно уйти в отладку прямо на плате, но есть более простое решение. запустить приложение на Линукс с теми же самыми размерами 480x272 с виртуальным фреймбуфером QVFB.

Запускаем на Линукс


Для запуска на Linux нам потребуется три части QVFB, библиотека Qt, и само приложение.

QVFB это обычное приложение, которое предоставит нам виртуальный экран для работы Qt. Собираем его как написано в официальной документации.

Запускаем с нужным размером экрана:
./qvfb -width 480 -height 272 -nocursor


Далее, собираем библиотеку Qt как embedded, т.е. С указанием опции -embedded. Я еще отключил разные модули для ускорения сборки, в итоге конфигурация выглядела вот так:
./configure -opensource -confirm-license -debug \    -embedded -qt-gfx-qvfb -qvfb \    -no-javascript-jit -no-script -no-scripttools \    -no-qt3support -no-webkit -nomake demos -nomake examples


Далее собираем приложение animatedtiles (qmake + make). И запускаем скомпилированное приложение, указав ему наш QVFB:
./examples/animation/animatedtiles/animatedtiles -qws -display QVFb:0


После запуска я увидел, что на Линуксе также рисуется только в часть экрана. Я немного доработал animatedtiles, добавив опцию -fullscreen, при указании которой приложение стартует в полноэкранном режиме.

Запуск на Embox


Модифицированный исходный код приложения будем использовать в Embox. Пересобираем и запускаем. Приложение не запустилось, при этом появились сообщения о нехватке памяти в Qt. Смотрим в конфигурацию Embox и находим что размер кучи установлен 2Мб и его явно не хватает. Опять же, можно попробовать выяснить этот момент с прямо на плате, но давайте сделаем это с удобствами в Линукс.

Для этого запускаем приложение следующим образом:
$ valgrind --tool=massif --massif-out-file=animatedtiles.massif ./examples/animation/animatedtiles/animatedtiles -qws -fullscreen$ ms_print animatedtiles.massif > animatedtiles.out


В файле animatedtiles.out видим максимальное значение заполненности кучи порядка 2.7 Мб. Отлично, теперь можно не гадать, а вернуться в Embox и поставить размер кучи 3Мб.

Animatedtiles запустилось.

Запуск на STM32F769I-Discovery.


Давайте попробуем еще усложнить задачу, и запустим тот же пример на подобном микроконтроллере, но только с большим разрешением экрана STM32F769I-Discovery (800x480). То есть теперь под фреймбуфер потребуется в 1.7 раз больше памяти (напомню, что у STM32F746G экран 480x272), но это компенсируется в два раза большим размером SDRAM (16 Мб против 8Мб доступной памяти SDRAM у STM32F746G).

Для оценки размера кучи, как и выше, сначала запускаем Qvfb и наше приложение на Линуксе:
$ ./qvfb -width 800 -height 480 -nocursor &$ valgrind --tool=massif --massif-out-file=animatedtiles.massif ./examples/animation/animatedtiles/animatedtiles -qws -fullscreen$ ms_print animatedtiles.massif > animatedtiles.out


Смотрим расход памяти в куче около 6 МБ (почти в два раза больше, чем на STM32F746G).

Осталось выставить нужный размер кучи в mods.conf и пересобрать. Приложение запустилось сразу и без проблем, что и продемонстрировано этом коротком видео


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

Данная статья впервые была нами опубликована на английском языке на embedded.com.
Подробнее..

Создание своей оценочной платы для микроконтроллеров

16.04.2021 02:15:22 | Автор: admin

Заказывая много оценочных плат из Китая, я и подумать не мог что всюду будет брак, недоработки и подделка. Всё это ужасно путало в изучении и порой было тяжело понять то ли мой код кривой, то ли контроллер. Примерно в это же время я нашёл микроконтроллеры и другую рассыпуху дешевле чем в Китае при это локально в России. Я сразу загорелся тем, что бы сделать себе платы и забыть о китайских корявых blue pill.

Брак или странная задумка инженеров

Земля на ножках для прошивки висела в воздухе, после доработки всё окей.

Я как раз переезжал, и из-за этого у меня появилось много свободного времени, чем я и воспользовался, когда рисовал платы в EDA. Начать было решено с STM8S003F3U6 потому как очень уж полюбилась мне эта серия микроконтроллеров.

Первые наброски выглядели примерно так:
v1v1

Тут я ещё не знал, что для нормальной прошивки мк нужен pull-up резистор на ногу сброса.

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

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

Детальное фото печатной платы

В итоге у меня было куча плат, с пяток контроллеров и разных вторичных компонентов, выпаянных из разных плат. Всё это вылилось в 3 рабочих прототипа.

В итоге у меня было куча плат, с пяток контроллеров и разных вторичных компонентов, выпаянных из разных плат. Всё это вылилось в 3 рабочих прототипа:

Фото ужасное, но переснять не могу уже раздал их)Фото ужасное, но переснять не могу уже раздал их)

Немного о платах

Остановился на 6 светодиодах. Всего 3 группы: питание, 4 GPIO, 1 GPIO(ШИМ) на обратной стороне. У каждой группы есть маленькая напаиваемая перемычка.

USB только питание, по входу стоит диод, защита usb выхода от обратного напряжения которое может пойти если запитать схему >5в напряжением. Стабилизатор выдерживает пиковые 16в, штатное до 14в.

Контроллер может без проблем работать и от 5в, но расчёт резисторов для светодиодов выполнял для 3.3, да и базово считаю что лучше работать с 3.3, ибо потом по привычке можно что-нибудь спалить.

Продолжаем вакханалию

Это была только половина пути, я ведь хотел много плат и очень не хотел делать их вручную.

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

И так, надо было решать как спаять всё быстро и с минимумом косяков. Трафарета у меня не было и я решил попробовать просто намазать тонким слоем паяльную пасту (благо она была).

Сказано сделано, вот что из этого вышло:

Мелкие капельки припоя что никуда не стекли прилипли прилично, далеко не с первого раза я их смог убрать, так же видно что 2 контроллера немного поплыли и встали криво. Выглядит жуть, но в целом получилось неплохо. Я делал несколько прогонов, первый мк и обвязка, остальное вторым. И по выходу получилось около 70% годных и готовых сразу без танцев плат.

Финальный результат, usb портов к этому времени не подвезли, так что без них.

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

Планы у меня наполеоновские, уже нарисовал и под stm32f0 и под f1 платы, ещё есть идея создать плату на stm32f4(7) с интегрированных ethernet.

Для тех кто дочитал, спойлер след платы!

В следующей статье затрону контроллер и среды, в которых пишу. Ни пуха ни пера и не болейте!

Подробнее..

Запуск сложных C приложений на микроконтроллерах

27.01.2021 20:23:33 | Автор: admin
image Сегодня никого не удивить возможностью разрабатывать на C++ под микроконтроллеры. Проект mbed полностью ориентирован на этот язык. Ряд других RTOS предоставляют возможности разработки на С++. Это удобно, ведь программисту доступны средства объектно-ориентированного программирования. Вместе с тем, многие RTOS накладывают различные ограничения на использование C++. В данной статье мы рассмотрим внутреннюю организацию C++ и выясним причины этих ограничений.

Сразу хочу отметить, что большинство примеров будут рассмотрены на RTOS Embox. Ведь в ней на микроконтроллерах работают такие сложные C++ проекты как Qt и OpenCV. OpenCV требует полной поддержки С++, которой обычно нет на микроконтроллерах.

Базовый синтаксис


Синтаксис языка C++ реализуется компилятором. Но в рантайм необходимо реализовать несколько базовых сущностей. В компиляторе они включаются в библиотеку поддержки языка libsupc++.a. Наиболее базовой является поддержка конструкторов и деструкторов. Существуют два типа объектов: глобальные и выделяемые с помощью операторов new.

Глобальные конструкторы и деструкторы


Давайте взглянем на то как работает любое C++ приложение. Перед тем как попасть в main(), создаются все глобальные C++ объекты, если они присутствуют в коде. Для этого используется специальная секция .init_array. Еще могут быть секции .init, .preinit_array, .ctors. Для современных компиляторов ARM, чаще всего секции используются в следующем порядке .preinit_array, .init и .init_array. С точки зрения LIBC это обычный массив указателей на функции, который нужно пройти от начала и до конца, вызвав соответствующий элемент массива. После этой процедуры управление передается в main().

Код вызова конструкторов для глобальных объектов из Embox:

void cxx_invoke_constructors(void) {    extern const char _ctors_start, _ctors_end;    typedef void (*ctor_func_t)(void);    ctor_func_t *func = (ctor_func_t *) &_ctors_start;    ....    for ( ; func != (ctor_func_t *) &_ctors_end; func++) {        (*func)();    }}

Давайте теперь посмотрим как устроено завершение C++ приложения, а именно, вызов деструкторов глобальных объектов. Существует два способа.

Начну с наиболее используемого в компиляторах через __cxa_atexit() (из C++ ABI). Это аналог POSIX функции atexit, то есть вы можете зарегистрировать специальные обработчики, которые будут вызваны в момент завершения программы. Когда при старте приложения происходит вызов глобальных конструкторов, как описано выше, там же есть и сгенерированный компилятором код, который регистрирует обработчики через вызов __cxa_atexit. Задача LIBC здесь сохранить требуемые обработчики и их аргументы и вызвать их в момент завершения приложения.

Другим способом является сохранение указателей на деструкторы в специальных секциях .fini_array и .fini. В компиляторе GCC это может быть достигнуто с помощью флага -fno-use-cxa-atexit. В этом случае во время завершения приложения деструкторы должны быть вызваны в обратном порядке (от старшего адреса к младшему). Этот способ менее распространен, но может быть полезен в микроконтроллерах. Ведь в этом случае на момент сборки приложения можно узнать сколько обработчиков потребуется.

Код вызова деструкторов для глобальных объектов из Embox:

int __cxa_atexit(void (*f)(void *), void *objptr, void *dso) {    if (atexit_func_count >= TABLE_SIZE) {        printf("__cxa_atexit: static destruction table overflow.\n");        return -1;    }    atexit_funcs[atexit_func_count].destructor_func = f;    atexit_funcs[atexit_func_count].obj_ptr = objptr;    atexit_funcs[atexit_func_count].dso_handle = dso;    atexit_func_count++;    return 0;};void __cxa_finalize(void *f) {    int i = atexit_func_count;    if (!f) {        while (i--) {            if (atexit_funcs[i].destructor_func) {                (*atexit_funcs[i].destructor_func)(atexit_funcs[i].obj_ptr);                atexit_funcs[i].destructor_func = 0;            }        }        atexit_func_count = 0;    } else {        for ( ; i >= 0; --i) {            if (atexit_funcs[i].destructor_func == f) {                (*atexit_funcs[i].destructor_func)(atexit_funcs[i].obj_ptr);                atexit_funcs[i].destructor_func = 0;            }        }    }}void cxx_invoke_destructors(void) {    extern const char _dtors_start, _dtors_end;    typedef void (*dtor_func_t)(void);    dtor_func_t *func = ((dtor_func_t *) &_dtors_end) - 1;    /* There are two possible ways for destructors to be calls:     * 1. Through callbacks registered with __cxa_atexit.     * 2. From .fini_array section.  */    /* Handle callbacks registered with __cxa_atexit first, if any.*/    __cxa_finalize(0);    /* Handle .fini_array, if any. Functions are executed in teh reverse order. */    for ( ; func >= (dtor_func_t *) &_dtors_start; func--) {        (*func)();    }}

Глобальные деструкторы необходимы, чтобы иметь возможность перезапускать C++ приложения. Большинство RTOS для микроконтроллеров предполагает запуск единственного приложения, которое не перезагружается. Старт начинается с пользовательской функции main, единственной в системе. Поэтому в небольших RTOS зачастую глобальные деструкторы пустые, ведь их использование не предполагается.

Код глобальный деструкторов из Zephyr RTOS:

/** * @brief Register destructor for a global object * * @param destructor the global object destructor function * @param objptr global object pointer * @param dso Dynamic Shared Object handle for shared libraries * * Function does nothing at the moment, assuming the global objects * do not need to be deleted * * @return N/A */int __cxa_atexit(void (*destructor)(void *), void *objptr, void *dso){    ARG_UNUSED(destructor);    ARG_UNUSED(objptr);    ARG_UNUSED(dso);    return 0;}

Операторы new/delete


В компиляторе GCC реализация операторов new/delete находится в библиотеке libsupc++, А их декларации в заголовочном файле .

Можно использовать реализации new/delete из libsupc++.a, но они достаточно простые и могут быть реализованы например, через стандартные malloc/free или аналоги.

Код реализации new/delete для простых объектов Embox:

void* operator new(std::size_t size)  throw() {    void *ptr = NULL;    if ((ptr = std::malloc(size)) == 0) {        if (alloc_failure_handler) {            alloc_failure_handler();        }    }    return ptr;}void operator delete(void* ptr) throw() {    std::free(ptr);}

RTTI & exceptions


Если ваше приложение простое, вам может не потребоваться поддержка исключений и динамическая идентификация типов данных (RTTI). В этом случае их можно отключить с помощью флагов компилятора -no-exception -no-rtti.

Но если эта функциональность С++ требуется, ее нужно реализовать. Сделать это куда сложнее чем new/delete.

Хорошая новость заключается в том что эти вещи не зависят от ОС и уже реализованы в кросс-компиляторе в библиотеке libsupc++.a. Соответственно, самый простой способ добавить поддержку это использовать библиотеку libsupc++.a из кросс компилятора. Сами прототипы находятся в заголовочных файлах и .

Для использования исключений из кросс-компилятора есть небольшие требования, которые нужно реализовать при добавлении собственного метода загрузки C++ рантайма. В линкер скрипте должна быть предусмотрена специальная секция .eh_frame. А перед использованием рантайма эта секция должна быть инициализирована с указанием адреса начала секции. В Embox используется следующий код:

void register_eh_frame(void) {    extern const char _eh_frame_begin;    __register_frame((void *)&_eh_frame_begin);}

Для ARM архитектуры используются другие секции с собственной структурой информации .ARM.exidx и .ARM.extab. Формат этих секция определяется в стандарте Exception Handling ABI for the ARM Architecture EHABI. .ARM.exidx это таблица индексов, а .ARM.extab это таблица самих элементов требуемых для обработки исключения. Чтобы использовать эти секции для обработки исключений, необходимо включить их в линкер скрипт:

    .ARM.exidx : {        __exidx_start = .;        KEEP(*(.ARM.exidx*));        __exidx_end = .;    } SECTION_REGION(text)    .ARM.extab : {        KEEP(*(.ARM.extab*));    } SECTION_REGION(text)

Чтобы GCC мог использовать эти секции для обработки исключений, указывается начало и конец секции .ARM.exidx __exidx_start и __exidx_end. Эти символы импортируются в libgcc в файле libgcc/unwind-arm-common.inc:
extern __EIT_entry __exidx_start;extern __EIT_entry __exidx_end;

Более подробно про stack unwind на ARM написано в статье.

Стандартная библиотека языка (libstdc++)


Собственная реализация стандартной библиотеки


В поддержку языка C++ входит не только базовый синтаксис, но и стандартная библиотека языка libstdc++. Ее функциональность, так же как и для синтаксиса, можно разделить на разные уровни. Есть базовые вещи типа работы со строками или C++ обертка setjmp . Они легко реализуются через стандартную библиотеку языка C. А есть более продвинутые вещи, например, Standard Template Library (STL).

Стандартная библиотека из кросс-компилятора


Базовые вещи реализованы в Embox. Если этих вещей достаточно, то можно не подключать внешнюю стандартную библиотеку языка C++. Но если нужна, например, поддержка контейнеров, то самым простым способом является использование библиотеки и заголовочных файлов из кросс-компилятора.

При использовании стандартной библиотеки С++ из кросс-компилятора существует особенность. Взглянем на стандартный arm-none-eabi-gcc:

$ arm-none-eabi-gcc -vUsing built-in specs.COLLECT_GCC=arm-none-eabi-gccCOLLECT_LTO_WRAPPER=/home/alexander/apt/gcc-arm-none-eabi-9-2020-q2-update/bin/../lib/gcc/arm-none-eabi/9.3.1/lto-wrapperTarget: arm-none-eabiConfigured with: ***     --with-gnu-as --with-gnu-ld --with-newlib   ***Thread model: singlegcc version 9.3.1 20200408 (release) (GNU Arm Embedded Toolchain 9-2020-q2-update)

Он собран с поддержкой --with-newlib.Newlib реализация стандартной библиотеки языка C. В Embox используется собственная реализация стандартной библиотеки. Для этого есть причина, минимизация накладных расходов. И следовательно для стандартной библиотеки С можно задать требуемые параметры, как и для других частей системы.

Так как стандартные библиотеки C отличаются, то для поддержки рантайма нужно реализовать слой совместимости. Приведу пример реализации из Embox одной из необходимых но неочевидных вещей для поддержки стандартной библиотеки из кросс-компилятора

struct _reent {    int _errno;           /* local copy of errno */  /* FILE is a big struct and may change over time.  To try to achieve binary     compatibility with future versions, put stdin,stdout,stderr here.     These are pointers into member __sf defined below.  */    FILE *_stdin, *_stdout, *_stderr;};struct _reent global_newlib_reent;void *_impure_ptr = &global_newlib_reent;static int reent_init(void) {    global_newlib_reent._stdin = stdin;    global_newlib_reent._stdout = stdout;    global_newlib_reent._stderr = stderr;    return 0;}

Все части и их реализации необходимые для использования libstdc++ кросс-компилятора можно посмотреть в Embox в папке third-party/lib/toolchain/newlib_compat/

Расширенная поддержка стандартной библиотеки std::thread и std::mutex


Стандартная библиотека C++ в компиляторе может иметь разный уровень поддержки. Давайте еще раз взглянем на вывод:

$ arm-none-eabi-gcc -v***Thread model: singlegcc version 9.3.1 20200408 (release) (GNU Arm Embedded Toolchain 9-2020-q2-update)

Модель потоков Thread model: single. Когда GCC собран с этой опцией, убирается вся поддержка потоков из STL (например std::thread и std::mutex). И, например, со сборкой такого сложного С++ приложение как OpenCV возникнут проблемы. Иначе говоря, для сборки приложений, которые требуют подобную функциональность, недостаточно этой версии библиотеки.

Решением, которые мы применяем в Embox, является сборка собственного компилятора ради стандартной библиотеки с многопоточной моделью. В случае Embox модель потоков используется posix Thread model: posix. В этом случае std::thread и std::mutex реализуются через стандартные pthread_* и pthread_mutex_*. При этом также отпадает необходимость подключать слой совместимости с newlib.

Конфигурация Embox


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

В Embox для оптимизации затрат на поддержку введены несколько абстрактных классов (интерфейсов) различные реализации которых можно задать.

  • embox.lib.libsupcxx определяет какой метод для поддержки базового синтаксиса языка нужно использовать.
  • embox.lib.libstdcxx определяет какую реализацию стандартной библиотеки нужно использовать

Есть три варианта libsupcxx:

  • embox.lib.cxx.libsupcxx_standalone базовая реализация в составе Embox.
  • third_party.lib.libsupcxx_toolchain использовать библиотеку поддержки языка из кросс-компилятора
  • third_party.gcc.tlibsupcxx полная сборка библиотеки из исходников

Минимальный вариант может работать даже без стандартной библиотеки С++. В Embox есть реализация базирующаяся на простейших функциях из стандартной библиотеки языка С. Если этой функциональности не хватает, можно задать три варианта libstdcxx.

  • third_party.STLport.libstlportg стандартная библиотека вслкючающая STL на основе проекта STLport. Не требует пересборки gcc. Но проект давно не поддерживается
  • third_party.lib.libstdcxx_toolchain стандартная библиотека из кросс-компилятора
  • third_party.gcc.libstdcxx полная сборка библиотеки из исходников

Если есть желание у нас на wiki описано как можно собрать и запустить Qt или OpenCV на STM32F7. Весь код естественно свободный.
Подробнее..

Разбираемся в особенностях графической подсистемы микроконтроллеров

08.09.2020 14:23:50 | Автор: admin
Привет!

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


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

Но если вам не нужна вся функциональность Qt? Что если у вас четыре кнопки, один регулятор громкости и пара popup меню? При этом хочется, чтобы выглядело красиво и работало быстро :) Тогда будет целесообразным использовать более легковесные средства, например библиотеку lvgl или аналогичную.

У нас в проекте Embox некоторое время назад был портирован Nuklear проект по созданию очень легковесной библиотека, состоящая из одного хедера и позволяющий легко создавать несложный GUI. Его мы и решили использовать для создания небольшого приложения в котором будет виджет с набором графических элементов и которым можно было бы управлять через touchscreen.

В качестве платформы выбрали STM32F7-Discovery c Cortex-M7 и сенсорным экраном.

Первые оптимизации. Экономия памяти


Итак, графическая библиотека выбрана, платформа тоже. Теперь поймем что по ресурсам. Тут стоит отметить, что основная память SRAM в разы быстрей внешней SDRAM, поэтому если вам позволяют размеры экраны, то конечно лучше положить фреймбуфер в SRAM. Наш экран имеет разрешение 480x272. Если мы хотим цвет в 4 байта на пиксель, то получается порядка 512 Кб. При этом размер внутреннего RAM всего 320 и сразу понятно, что видеопамять будет внешней. Другой вариант уменьшить битность цвета до 16 (т.е. 2 байта), и таким образом сократить расход памяти до 256 Кб, что уже может влезть в основную RAM.

Первое что можно попробовать сэкономить на всем. Сделаем видео буфер на 256 Кб, разместим его в RAM и будем в него же и рисовать. Проблема, с которой сразу же столкнулись это мерцание сцены возникающее если рисовать напрямую в видеопамять. Nuklear перерисовывает всю сцену с нуля, поэтому каждый раз сначала выполняется заливка всего экрана, далее рисуется виджет, потом в него кладется кнопка, в которую помещается текст и так далее. Как следствие, невооруженным взглядом заметно как вся сцена перерисовывается и картинка мигает. То есть, простое помещение во внутреннюю память, не спасает.

Промежуточный буфер. Компиляторные оптимизации. FPU


После того как мы немного повозился с предыдущим способом (размещением во внутренней памяти), в голову сразу стали приходить в воспоминания об X Server и Wayland. Да, действительно, по сути оконные менеджеры и занимаются тем, что обрабатывают запросы от клиентов (как раз наше пользовательское приложение), и далее собирают элементы в итоговую сцену. К примеру, ядро Линукса посылает серверу события от input устройств через драйвер evdev. Сервер, в свою очередь, определяет кому из клиентов адресовать событие. Клиенты, получив событие (например, нажатие на сенсорном экране) выполняют свою внутреннюю логику подсвечивают кнопку, отображают новое меню. Далее (немного по-разному для X и Wayland) либо сам клиент, либо сервер производит отрисовку изменений в буфер. И затем компоновщик (compositor) уже соединяет все кусочки воедино для отрисовки на экран. Достаточно просто и схематичное объяснение вот здесь.

Стало ясно, что нам нужна похожая логика, вот только запихивать X Server в stm32 ради маленького приложения очень уж не хочется. Поэтому попробуем просто рисовать не в видео память, а в обычную память. После отрисовки всей сцены будет копировать буфер в видео память.

Код виджета
        if (nk_begin(&rawfb->ctx, "Demo", nk_rect(50, 50, 200, 200),            NK_WINDOW_BORDER|NK_WINDOW_MOVABLE|            NK_WINDOW_CLOSABLE|NK_WINDOW_MINIMIZABLE|NK_WINDOW_TITLE)) {            enum {EASY, HARD};            static int op = EASY;            static int property = 20;            static float value = 0.6f;            if (mouse->type == INPUT_DEV_TOUCHSCREEN) {                /* Do not show cursor when using touchscreen */                nk_style_hide_cursor(&rawfb->ctx);            }            nk_layout_row_static(&rawfb->ctx, 30, 80, 1);            if (nk_button_label(&rawfb->ctx, "button"))                fprintf(stdout, "button pressed\n");            nk_layout_row_dynamic(&rawfb->ctx, 30, 2);            if (nk_option_label(&rawfb->ctx, "easy", op == EASY)) op = EASY;            if (nk_option_label(&rawfb->ctx, "hard", op == HARD)) op = HARD;            nk_layout_row_dynamic(&rawfb->ctx, 25, 1);            nk_property_int(&rawfb->ctx, "Compression:", 0, &property, 100, 10, 1);            nk_layout_row_begin(&rawfb->ctx, NK_STATIC, 30, 2);            {                nk_layout_row_push(&rawfb->ctx, 50);                nk_label(&rawfb->ctx, "Volume:", NK_TEXT_LEFT);                nk_layout_row_push(&rawfb->ctx, 110);                nk_slider_float(&rawfb->ctx, 0, &value, 1.0f, 0.1f);            }            nk_layout_row_end(&rawfb->ctx);        }        nk_end(&rawfb->ctx);        if (nk_window_is_closed(&rawfb->ctx, "Demo")) break;        /* Draw framebuffer */        nk_rawfb_render(rawfb, nk_rgb(30,30,30), 1);        memcpy(fb_info->screen_base, fb_buf, width * height * bpp);


В этом примере создается окно размером 200 x 200 пикселей, в него отрисовываются графические элементы. Сама итоговая сцена рисуется в буффер fb_buf, который мы выделили SDRAM. А далее в последней строчке просто вызывается memcpy. И все повторяется в бесконечном цикле.

Если просто собрать и запустить этот пример, получим порядка 10-15 FPS. Что конечно не очень хорошо, ведь заметно даже глазом. Причем поскольку в коде рендерера Nuklear много вычислений с плавающей точкой, ее поддержку мы включили изначально, без нее FPS был бы еще ниже. Первая и самая простая (бесплатная) оптимизация конечно флаг компилятора -O2.

Соберем и запустим тот же самый пример получим 20 FPS. Уже лучше, но все равно не достаточно для хорошей работы.

Включение кэшей процессора. Режим Write-Through


Перед тем как перейти к дальнейшим оптимизациям, скажу что мы используем плагин rawfb в составе Nuklear, который как раз и рисует напрямую в память. Соответственно оптимизация работы с памятью выглядит очень перспективно. Первое что приходит на ум это cache.

В старших версиях Cortex-M, таких как Cortex-M7 (наш случай) встроен дополнительный кэш процессора (кэш инструкций и кэш данных). Он включается через регистр CCR блока System Control Block. Но с включением кэша приходят новые проблемы несогласованность данных в кэше и памяти. Есть несколько способов управления кэшем, но в этой статье я не буду на них останавливаться, поэтому перейду к одному из самых простых, на мой взгляд. Чтобы решить проблему несогласованности кэша и памяти можно просто пометить всю доступную нам память как некэшируемую. Это означает, что все записи в эту память будут всегда проходить в память, а не в кэш. Но если мы таким способом пометим всю память, то и от кэша смысла не будет. Есть еще один вариант. Это сквозной режим, при котором все записи в память помеченную как write through попадают одновременно как в кэш, так и в память. Это создает накладные расходы на запись, но с другой стороны, сильно ускоряет чтение, поэтому результат будет зависеть от конкретного приложения.

Для Nuklearа write-through режим оказался очень хорош производительность поднялась с 20 FPS до 45 FPS, что само по себе уже достаточно хорошо и плавно. Эффект конечно интересный, мы даже пробовали отключать write through режим, не обращая внимания на несогласованность данных, но FPS поднимался лишь до 50 FPS, то есть значительного прироста по сравнению с write through не наблюдалось. Отсюда мы сделали вывод, что для нашего приложения требуются много именно операций чтения, а не записи. Вопрос конечно откуда? Возможно, из-за количества преобразований в коде rawfb, которые часто обращаются в память за чтением очередного коэффициента или что-то в этом роде.

Двойная буферизация (пока с промежуточным буфером). Включение DMA


Останавливаться на 45 FPS не хотелось, поэтому решили поэкспериментировать дальше. Следующей идей была двойная буферизация. Идея широко известная, и в общем то нехитрая. Отрисовываем сцену с помощью одного устройства в один буфер, а другое устройство в это время выводит на экран из другого буфера. Если посмотреть на предыдущий код, то хорошо виден цикл, в котором сначала в буфер рисуется сцена, а затем с помощью memcpy содержимое копируется в видео память. Понятно, что memcpy использует CPU, то есть отрисовка и копирование происходят последовательно. Наша идея была в том, что копирование можно делать параллельно с помощью DMA. Другими словами, пока процессор рисует новую сцену, DMA копирует предыдущую сцену в видеопамять.

Memcpy заменяется следующим кодом:

            while (dma_in_progress()) {            }            ret = dma_transfer((uint32_t) fb_info->screen_base,                    (uint32_t) fb_buf[fb_buf_idx], (width * height * bpp) / 4);            if (ret < 0) {                printf("DMA transfer failed\n");            }            fb_buf_idx = (fb_buf_idx + 1) % 2;

Здесь вводится fb_buf_idx индекс буфера. fb_buf_idx = 0 это front buffer, fb_buf_idx = 1 это back buffer. Функция dma_transfer() принимает destination, source и кол-во 32 битных слов. Далее DMA заряжается требуемыми данными, а работа продолжается со следующим буфером.

Попробовав такой механизм производительность выросла примерно до 48 FPS. Чуть лучше чем с memcpy(), но незначительно. Я не хочу сказать, что DMA оказался бесполезен, просто в этом конкретном примере влияние кэша на общую картину показало себя лучше.

После небольшого удивления, что DMA показал себя хуже чем ожидалось, пришла отличная, как нам тогда казалось, мысль использовать несколько DMA каналов. В чем суть? Число данных, которые можно зарядить в DMA за один раз на stm32f7xx составляет 256 Кб. При этом помним, что экран у нас 480x272 и видеопамять порядка 512 Кб, а значит, казалось бы, что можно первую половину данных положить в один канал DMA, а вторую половину во второй. И все вроде бы хорошо Вот только производительность падает с 48 FPS до 25-30 FPS. То есть возвращаемся к той ситуации, когда еще не включили кэш. С чем это может быть связано? На самом деле с тем, что доступ к памяти SDRAM синхронизируется, даже память так и называется Synchronous Dynamic Random Access Memory (SDRAM), поэтому такой вариант лишь добавляет дополнительную синхронизацию, не делая при этом запись в память параллельной, как хочется. Немного поразмыслив, мы поняли, что ничего удивительного тут нет, ведь память то одна, и циклы записи и чтения генерируются к одной микросхеме (по одной шине), а поскольку добавляется еще один источник/приемник, то арбитру, который и разруливает обращения по шине, нужно смешивать циклы команд от разных DMA каналов.

Двойная буферизация. Работа с LTDC


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

У LTDC (LCD-TFT display controller) в составе stm32f74xx есть два аппаратных уровня наложения Layer 1 и Layer 2, где Layer 2 накладывается на Layer 1. Каждый из уровней конфигурируется независимо и может быть включен или отключен отдельно. Мы попробовали включить только Layer 1 и у него переставлять адрес видеопамяти на front buffer или back buffer. То есть один отдаем дисплею, а в другой в это время рисуем. Но получили заметное дрожание картинки при переключении наложений.

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

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

Стало понятно, что дело в чем то другом. И тут вспомнили про выравнивание адреса фреймбуфера в памяти. Так как буфер выделялись из кучи и их адреса были не выровнены выровнены. Выровняли адреса фреймбуферов на 1 Кб получили ожидаемую картинку без дрожания. Потом нашли в документации, что LTDC вычитывает данные пачками по 64 байта, и что не выравненность данных дает значительную потерю в производительности. При этом выровнены должны быть как адрес начала фреймбуфера, так и его ширина. Для проверки мы изменили ширину 480x4 на 470x4, которая не делится на 64 байта, и получил то же самое дрожание картинки.

В итоге, выровняли оба буфера на 64 байта, убедились что ширина выровнена тоже на 64 байта и запустили nuklear дрожание исчезло. Решение, которое сработало, выглядит так. Вместо переключения между уровнями при помощи полного отключения либо Layer 1 либо Layer используем прозрачность. То есть, чтобы отключить уровень, установим его прозрачность в 0, а чтобы включить в 255.

        BSP_LCD_SetTransparency_NoReload(fb_buf_idx, 0xff);        fb_buf_idx = (fb_buf_idx + 1) % 2;        BSP_LCD_SetTransparency(fb_buf_idx, 0x00);

Получили 70-75 FPS! Значительно лучше, чем изначальные 15.

Стоит отметить, что решение работает через управление прозрачностью, а варианты с отключением одного из уровней и вариант с переставлением адреса уровня дают дрожание картинки при FPS больших 40-50, причина нам на данный момент неизвестна. Также забегая вперед скажу, что это решение для данной платы.

Аппаратная заливка сцены через DMA2D


Но и это еще не предел, последней на текущий момент нашей оптимизацией для увеличения FPS, стала аппаратная заливка сцены. До этого мы делали заливку программно:
nk_rawfb_render(rawfb, nk_rgb(30,30,30), 1);

Давайте теперь скажем плагину rawfb, что заливать сцену не нужно, а только рисовать поверх:
nk_rawfb_render(rawfb, nk_rgb(30,30,30), 0);

Сцену будем заливать тем же цветом 0xff303030, только аппаратно через контроллер DMA2D. Одна из основных функций DMA2D это копирование или заливка цветом прямоугольника в оперативной памяти. Основное удобство здесь в том, что это не непрерывный отрезок памяти, а именно прямоугольная область, которая в памяти располагается с разрывами, а значит обычным DMA сходу не обойтись. В Embox мы еще не работали с этим устройством, поэтому давайте просто воспользовались средствами STM32Cube функция BSP_LCD_Clear(uint32_t Color). Она программирует в DMA2D цвет заливки и размеры всего экрана.

Vertical Blanking Period (VBLANK)


Но даже при достигнутых 80 FPS осталась заметная проблема части виджета двигались небольшими разрывами при перемещении по экрану. То есть, виджет будто бы делился на 3 (или больше) части, которые двигались рядом, но с небольшой задержкой. Оказалось что причина в неправильном обновления видеопамяти. А точнее, обновления в неправильные интервалы времени.

У контроллера дисплея есть такое свойство как VBLANK, оно же VBI или Vertical Blanking Period. Оно обозначает временной интервал между соседними видео кадрами. Или чуть точнее, время между последней строкой предыдущего видеокадра и первой строкой следующего. В этом промежутке никакие новые данные не передаются на дисплей, картинка статическая. По этой причине обновлять видеопамять безопасно именно внутри VBLANKа.

На практике, у контроллера LTDC есть прерывание, которое настраивается на срабатывание после обработки очередной строки фреймбуфера (LTDC line interrupt position configuration register (LTDC_LIPCR)). Таким образом, если настроить это прерывание на номер последней строки, то мы как раз и получим начало интервала VBLANK. В этом месте и производим необходимое переключение буферов.

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

В документации можно найти следующую формулу:

          LCD_CLK (MHz) = total_screen_size * refresh_rate,

где total_screen_size = total_width x total_height. LCD_CLK это частота, на которой контроллер дисплея будет загружать пиксели из видеопамяти в экран (к примеру, через Display Serial Interface (DSI)). А вот refresh_rate это уже частота обновления самого экрана, его физическая характеристика. Выходит, зная refresh rate экрана и его размеры, можно сконфигурировать частоту для контроллера дисплея. Проверив по регистрам ту конфигурацию, которую создает STM32Cube, я выяснил что он настраивает контроллер на экран 60 Hz. Таким образом, все сошлось.

Немного об input устройствах в нашем примере


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

У нас все устроено достаточно просто. События от input устройств обрабатываются в основном цикле программы непосредственно перед отрисовкой сцены:

        /* Input */        nk_input_begin(&rawfb->ctx);        {            switch (mouse->type) {            case INPUT_DEV_MOUSE:                handle_mouse(mouse, fb_info, rawfb);                break;            case INPUT_DEV_TOUCHSCREEN:                handle_touchscreen(mouse, fb_info, rawfb);                break;            default:                /* Unreachable */                break;            }        }        nk_input_end(&rawfb->ctx);

Сама же обработка событий от touchscreen происходит в функции handle_touchscreen():

handle_touchscreen
static void handle_touchscreen(struct input_dev *ts, struct fb_info *fb_info,        struct rawfb_context *rawfb) {    struct input_event ev;    int type;    static int x = 0, y = 0;    while (0 <= input_dev_event(ts, &ev)) {        type = ev.type & ~TS_EVENT_NEXT;        switch (type) {        case TS_TOUCH_1:            x = normalize_coord((ev.value >> 16) & 0xffff, 0, fb_info->var.xres);            y = normalize_coord(ev.value & 0xffff, 0, fb_info->var.yres);            nk_input_button(&rawfb->ctx, NK_BUTTON_LEFT, x, y, 1);            nk_input_motion(&rawfb->ctx, x, y);            break;        case TS_TOUCH_1_RELEASED:            nk_input_button(&rawfb->ctx, NK_BUTTON_LEFT, x, y, 0);            break;        default:            break;        }    }}


По сути, здесь происходит конвертация событий input устройств в формат понятный Nuklearу. Собственно наверное и все.

Запускаем на другой плате


Получив вполне приличные результаты, мы решили воспроизвести их на другой плате. У нас была другая похожая плата STM32F769I-DISCO. Там такой же LTDC контроллер, но другой экран с разрешением 800x480. После запуска на ней получили 25 FPS. То есть заметное падение производительности. Это объясняется размером фреймбуфера он почти в 3 раза больше. Но основная проблема оказалась в другом изображение очень сильно искажалось, статической картинки в момент когда виджет должен быть на одном месте не было.

Причина была не ясна, поэтому мы пошли смотреть стандартные примеры из STM32Cube. Там оказалсяз пример с двойной буферизаций для данной платы. В этом примере разработчики в отличие от метода с изменением прозрачности просто переставляют указатель на фреймбуфер по прерыванию VBLANK Этот способ мы уже пробовали ранее для первой платы, но для нее он не сработал. Но применив этот метод для STM32F769I-DISCO, мы получили достаточно плавное изменение картинки с 25 FPS.

Обрадовавшись, мы еще раз проверили данный метод (с переставлением указателей) на первой плате, но он все так же не работал при больших FPS. В итоге, на одной плате работает метод с прозрачностями слоев (60 FPS), а на другой метод с переставлением указателей (25 FPS). Обсудив ситуацию, мы решили отложить унификацию до более глубокой проработки графического стека.

Итоги


Итак, подведем итоги. Показанный пример представляет простой, но в то же время распространенный паттерн GUI для микроконтроллеров несколько кнопок, регулятор громкости, может что-то еще. В примере отсутствует какая либо логика привязанная к событиям, так как упор был сделан именно на графику. По производительности получилось вполне приличное значение FPS.

Накопленные нюансы для оптимизации производительности подводят к выводу, что в современных микроконтроллерах графика усложняется. Теперь нужно, как и на больших платформах, следить за кэшем процессора, что-то размещать во внешней памяти, а что-то в более быстрой, задействовать DMA, использовать DMA2D, следить за VBLANK и так далее. Все это стало похожим на большие платформы, и быть может поэтому я уже несколько раз сослался на X Server и Wayland.

Пожалуй, одной из самых неоптимизированных частей можно считать сам рендеринг, мы перерисовываем всю сцену с нуля, целиком. Я не могу сказать как сделано в других библиотеках для микроконтроллеров, возможно где-то эта стадия встроена в саму библиотеку. Но по итогам работы с Nuklear кажется что в этом месте нужен аналог X Server или Wayland, конечно, более легковесный, что опять таки уводит нас к мысли, что маленькие системы повторяют путь больших.

Наши контакты:

Github: https://github.com/embox/embox
Рассылка: embox-ru[at]googlegroups.com
Телеграмм чат: t.me/embox_chat
Подробнее..

О кэшах в микроконтроллерах ARM

04.11.2020 18:10:50 | Автор: admin
image Привет!

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

Начну с того на чем остановился в предыдущей статье, а именно, на разнице между write-back и write-through режимами, поскольку именно эти два режима чаще всего используются. Если кратко, то:
  • Write-back. Данные по записи подают только в кэш. Реальная запись в память откладывается до тех пор пока кэш не переполнится и не потребуется место для новых данных.
  • Write-through. Запись происходит одновременно и в кэш и в память.

Write-through


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

Конечно кажется будто это должно сильно сказаться на производительности, но сам STM в этом документе говорит, что это не так:
Write-through: triggers a write to the memory as soon as the contents on the cache line are written to. This is safer for the data coherency, but it requires more bus accesses. In practice, the write to the memory is done in the background and has a little effect unless the same cache set is being accessed repeatedly and very quickly. It is always a tradeoff.

То есть, изначально мы предполагали, что раз запись происходит в память, то на операциях записи производительность будет примерно такой же как и совсем без кэша, а основной выигрыш происходит за счет повторных чтений. Однако, STM это опровергает, говорится что данные в в память попадают в фоне, поэтому производительность на записи практически такая же как и в режиме write-back. Это, в частности, может зависеть от внутренних буферов контроллера памяти (FMC).

Минусы режима write-through:
  • При последовательном и быстром доступе в одну и ту же память производительность может снижаться. В режиме write-back последовательные частые доступы к одной памяти будут, наоборот, являться плюсом.
  • Как и в случае с write-back все равно нужно делать cache invalidate после окончания DMA операций.
  • Баг Data corruption in a sequence of Write-Through stores and loads в некоторых версиях Cortex-M7. Нам указал на него один из разработчиков LVGL.

Write-back


Как уже говорилось выше, в этом режиме (в отличие от write-through) данные в общем случае не попадают в память по записи, а попадают только в кэш. Как и у write-through, у этой стратегии имеются два под варианта 1) write allocate, 2) no write allocate. Об этих вариантах мы поговорим дальше.

Write Allocate


Как правило, в кэшах всегда используется read allocate то есть по промаху (cache miss) на чтение данные забираются из памяти и размещаются в кэше. Аналогично, при промахе на запись данные могут подгружаться в кэш (write allocate) или не подгружаться (no write allocate). Обычно на практике используются сочетания write-back write allocate или write-through no write allocate. Далее в тестах мы попробуем чуть более детально проверить в каких ситуациях использовать write allocate, а в каких no write allocate.

MPU


Прежде чем переходить к практической части нам необходимо разобраться как же задавать параметры региона памяти. Для выбора режима кэша (или его отключения) для определенного региона памяти в архитектуре ARMv7-M используется MPU (Memory Protection Unit).

В контроллере MPU поддерживается задание регионов памяти. Конкретно в архитектуре ARMV7-M может быть до 16 регионов. Для этих регионов можно независимо устанавливать: стартовый адрес, размер, права доступа (read/write/execute и т.д.), атрибуты TEX, cacheable, bufferable, shareable, а так же и другие параметры. С помощью такого механизма, в частности, можно добиться любого типа кэширования для определенного региона. Например, мы можем избавиться от необходимости вызывать cache_clean/cache_invalidate просто выделив регион памяти под все операции DMA и пометив эту память как не кэшируемую.

Нужно отметить важный момент при работе с MPU:
The base address, size and attributes of a region are all configurable, with the general rule that all regions are naturally aligned. This can be stated as:
RegionBaseAddress[(N-1):0] = 0, where N is log2(SizeofRegion_in_bytes)


Иными словами, стартовый адрес региона памяти должен быть выровнен на его собственный размер. Если у вас, к примеру, регион 16 Кб, то выравнивать нужно на 16 Кб. Если регион памяти 64 Кб, то выравниваем на 64 Кб. И так далее. Если этого не сделать, то MPU может автоматически обрезать регион под размер соответствующий его стартовому адресу (проверено на практике).

Кстати, в STM32Cube есть несколько ошибок. Например:
  MPU_InitStruct.BaseAddress = 0x20010000;  MPU_InitStruct.Size = MPU_REGION_SIZE_256KB;


Видно, что стартовый адрес выровнен на 64 Кб. А размер региона хотим 256 Кб. В этом случае придется создавать 3 региона: первый 64 Кб, второй 128 Кб, и третий 64 Кб.

Задавать нужно только регионы с отличными от стандартных свойствами. Дело в том, что атрибуты всех памятей при включении кэша процессора описаны в архитектуре ARM. Есть стандартный набор свойств (к примеру, поэтому память SRAM STM32F7 имеет режим write-back write-allocate по умолчанию), Поэтому если вам понадобится не стандартный режим для какой-то из памятей, то нужно будет задать его свойства через MPU. При этом внутри региона можно задать подрегион со своими свойствами, Выделив внутри этого региона еще один с большим приоритетом с требуемыми свойствами.

TCM


Как следует из документации (раздел 2.3 Embedded SRAM), первые 64 Кб SRAM в STM32F7 некэшируемые. В самой архитектуре ARMv7-M по адресу 0x20000000 находится память SRAM. TCM тоже относится к SRAM, но находится на другой шине относительно остальных памятей (SRAM1 и SRAM2), и располагается ближе к процессору. Из-за этого данная память очень быстрая, по сути дела, имеет такую же скорость как и кэш. И из за этого кэширование не нужно, и этот регион не возможно сделать кэшируемым. По сути TCM это еще один такой вот кэш.

Instruction cache


Стоит отметить, что все рассмотренное выше относится к кэшу данных (D-Cache). Но кроме кэша данных в ARMv7-M предусмотрен и кэш инструкций Instruction cache (I-Cache). I-Cache позволяет перенести часть исполняемых (и следующих) инструкций в кэш, что может значительно ускорить работу программы. Особенно, в тех случаях, когда код находится в более медленной памяти чем FLASH, к примеру, QSPI.

Чтобы уменьшить непредсказуемость в тестах с кэшем ниже, мы намеренно отключим I-Cache и будем думать исключительно о данных.

При этом хочу отметить, что включается I-Cache достаточно просто и не требует никаких дополнительных действий со стороны MPU в отличие от D-Cache.

Синтетические тесты


После обсуждения теоретической части, давайте перейдем к тестам, чтобы лучше понять разницу и сферы применимости той или иной модели. Как я и говорил выше, отключаем I-Cache и работаем только с D-Cache. Так же я намеренно компилирую с -O0, чтобы циклы в тестах не оптимизировались. Тестировать будем через внешнюю память SDRAM. С помощью MPU я разметил регион 64 Кб, и будем выставлять этому региону нужные нам атрибуты.

Так как тесты с кэшами очень капризные и находятся под влиянием всего и вся в системе сделаем код линейным и непрерывным. Для этого отключаем прерывания. Так же, замерять время будем не таймерами, а DWT (Data Watchpoint and Trace unit), в котором есть 32 битный счетчик процессорных тактов. На его основе (на просторах интернета) люди делают микросекундные задержки в драйверах. Счетчик довольно быстро переполняется на системной частоте 216 МГц, но до 20 секунд померить можно. Просто будем об этом помнить, и сделаем тесты в этом временном интервале, предварительно обнуляя счетчик тактов перед стартом.

Non-cacheable memory VS. write-back


Итак, начнем с совсем простых тестов.

Просто последовательно пишем в память.
    dst = (uint8_t *) DATA_ADDR;    for (i = 0; i < ITERS * 8; i++) {        for (j = 0; j < DATA_LEN; j++) {            *dst = VALUE;            dst++;        }        dst -= DATA_LEN;    }


Так же последовательно пишем в память, но не по одному байту за раз, а немного развернем циклы.
    for (i = 0; i < ITERS * BLOCKS * 8; i++) {        for (j = 0; j < BLOCK_LEN; j++) {            *dst = VALUE;            *dst = VALUE;            *dst = VALUE;            *dst = VALUE;            dst++;        }        dst -= BLOCK_LEN;    }

Так же последовательно пишем в память, но теперь еще и чтение добавим.
    for (i = 0; i < ITERS * BLOCKS * 8; i++) {        dst = (uint8_t *) DATA_ADDR;        for (j = 0; j < BLOCK_LEN; j++) {            val = VALUE;            *dst = val;            val = *dst;            dst++;        }    }


Если запустить все эти три теста, то они дадут абсолютно одинаковый результат какой бы режим вы не выбрали:
mode: nc, iters=100, data len=65536, addr=0x60100000
Test1 (Sequential write):
0s 728ms
Test2 (Sequential write with 4 writes per one iteration):
7s 43ms
Test3 (Sequential read/write):
1s 216ms


И это резонно, SDRAM не такая уж и медленная, особенно если учесть внутренние буферы FMC, через который она подключена. Тем не менее, я ожидал небольшой вариации в цифрах, но оказалось что ее на этих тестах нет. Ну что же, будем думать дальше.

Давайте попробуем подпортить жизнь SDRAM смешивая чтения и записи. Для этого развернем циклы добавим такую распространенную на практике вещь как инкремент элемента массива:
    for (i = 0; i < ITERS * BLOCKS; i++) {        for (j = 0; j < BLOCK_LEN; j++) {            // 16 lines            arr[i]++;            arr[i]++;***            arr[i]++;        }    }

Результат:
Не кэшируемая память: 4s 743ms
Write-back: : 4s 187ms

Уже лучше с кэшем оказалось на пол секунды быстрей. Давайте попробуем еще усложнить тест добавим доступ по разреженным индексам. К примеру, с одним индексом:
    for (i = 0; i < ITERS * BLOCKS; i++) {        for (j = 0; j < BLOCK_LEN; j++) {            arr[i + 0 ]++;            ***            arr[i + 3 ]++;            arr[i + 4 ]++;            arr[i + 100]++;            arr[i + 6 ]++;            arr[i + 7 ]++;            ***            arr[i + 15]++;        }    }

Результат:
Не кэшируемая память: 11s 371ms
Write-back: : 4s 551ms

Теперь разница с кэшем стала более чем заметна! И в довершение введем второй такой индекс:
    for (i = 0; i < ITERS * BLOCKS; i++) {        for (j = 0; j < BLOCK_LEN; j++) {            arr[i + 0 ]++;            ***            arr[i + 4 ]++;            arr[i + 100]++;            arr[i + 6 ]++;            ***            arr[i + 9 ]++;            arr[i + 200]++;            arr[i + 11]++;            arr[i + 12]++;            ***            arr[i + 15]++;        }    }

Результат:
Не кэшируемая память: 12s 62ms
Write-back: : 4s 551ms

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

Write allocate VS. no write allocate


Теперь давайте разберемся с режимом write allocate. Тут еще сложней увидеть разницу, т.к. если в ситуации между не кэшируемой памятью и write-back становятся хорошо видны уже начиная с 4-го теста, то различия между write allocate и no write allocate до сих пор тестами не вскрылись. Давайте подумаем когда write allocate будет быстрей? Например, когда у вас есть много записей в последовательные ячейки памяти, а чтений из этих ячеек памяти мало. В этом случае в режиме no write allocate будем получать постоянные промахи, и подгружаться по чтению в кэш будут совсем не те элементы. Давайте смоделируем такую ситуацию:
    for (i = 0; i < ITERS * BLOCKS; i++) {        for (j = 0; j < BLOCK_LEN; j++) {            arr[j + 0 ]  = VALUE;            ***            arr[j + 7 ]  = VALUE;            arr[j + 8 ]  = arr[i % 1024 + (j % 256) * 128];            arr[j + 9 ]  = VALUE;            ***            arr[j + 15 ]  = VALUE;        }    }

Здесь в 15 из 16 записей выставляется константа VALUE, в то время как чтение осуществляется из разных (и не связанных с записью) элементов arr[i % 1024 + (j % 256) * 128]. Получается, что при стратегии no write allocate только эти элементы и будут загружаться в кэш. Причина по которой используется такая индексация (i % 1024 + (j % 256) * 128) ухудшение скорости FMC/SDRAM. Так как обращения к памяти по существенно различным (не последовательным) адресам, могут существенно сказываться на скорости работы.

Результат:
Write-back : 4s 720ms
Write-back no write allocate: : 4s 888ms

Наконец-то получили разницу, пусть и не настолько заметную, но уже видимую. То есть наша гипотеза подтвердилась.

И наконец, самый сложный, на мой взгляд, случай. Хотим понять когда no write allocate лучше чем write allocate. Первый лучше если мы часто обращаемся к адресам, с которыми в ближайшее время работать не будем. Такие данные, не нужно заносить в кэш.

В следующем тесте в случае write allocate данные будут заполняться по чтению и по записи. Я сделал массив arr2 на 64 Кб, поэтому кэш будет сбрасываться, чтобы подкачать новые данные. В случае же с no write allocate я сделал массив arr на 4096 байт, и только он попадет в кэш, а значит данные кэша сбрасываться в память не будут. За счет этого и попробуем получить хотя бы небольшой выигрыш.
    arr = (uint8_t *) DATA_ADDR;    arr2 = arr;    for (i = 0; i < ITERS * BLOCKS; i++) {        for (j = 0; j < BLOCK_LEN; j++) {            arr2[i * BLOCK_LEN            ] = arr[j + 0 ];            arr2[i * BLOCK_LEN + j*32 + 1 ] = arr[j + 1 ];            arr2[i * BLOCK_LEN + j*64 + 2 ] = arr[j + 2 ];            arr2[i * BLOCK_LEN + j*128 + 3] = arr[j + 3 ];            arr2[i * BLOCK_LEN + j*32 + 4 ] = arr[j + 4 ];            ***            arr2[i * BLOCK_LEN + j*32 + 15] = arr[j + 15 ];        }    }

Результат:
Write-back : 7s 601ms
Write-back no write allocate: : 7s 599ms

Видно, что write-back write allocate режим чуть-чуть быстрей. Но главное, что быстрей :) Лучшей демонстрации у меня добиться не получилось, но я уверен, что есть практические ситуации, когда разница более ощутима. Читатели могут предложить свои варианты!

Практические примеры


Давайте перейдем от синтетических примеров к реальным.

ping


Один из самых простых это ping. Его легко запустить, а время можно смотреть прямо на хосте. Embox был собран с оптимизацией -O2. Сразу приведу результаты:

Не кэшируемая память : ~0.246 c
Write-back : ~0.140 c


OpenCV


Еще одним примером реальной задачи на которой мы хотели попробовать работу подсистемы cache это OpenCV на STM32F7. В той статье было показано, что запустить вполне реально, но производительность была довольно низкая. Мы используем для демонстрации стандартный пример, который выделяет границы на основе фильтра Canny. Давайте измерим время работы с кешами (и D-cache и I-cache) и без.
   gettimeofday(&tv_start, NULL);    cedge.create(image.size(), image.type());    cvtColor(image, gray, COLOR_BGR2GRAY);    blur(gray, edge, Size(3,3));    Canny(edge, edge, edgeThresh, edgeThresh*3, 3);    cedge = Scalar::all(0);    image.copyTo(cedge, edge);    gettimeofday(&tv_cur, NULL);    timersub(&tv_cur, &tv_start, &tv_cur);

Без кэша:

> edges fruits.png 20
Processing time 0s 926ms
Framebuffer: 800x480 32bpp
Image: 512x269; Threshold=20

С кэшем:

> edges fruits.png 20
Processing time 0s 134ms
Framebuffer: 800x480 32bpp
Image: 512x269; Threshold=20

То есть, 926ms и 134ms ускорение почти в 7 раз.

На самом деле у нас достаточно часто спрашивают про OpenCV на STM32, в частности какая производительность. Получается FPS конечно не высокий, но 5 кадров в секунду, вполне реально получить.

Не кэшируемая или кэшируемая память, но с cache invalidate


В реальных устройствах повсеместно используется DMA, естественно с ним связаны трудности, ведь нужно синхронизировать память даже для режима write-through. Возникает естественное желание просто выделить кусок памяти который будет не кэшируемый и использовать его при работе с DMA. Немного отвлекусь. В Linux это делается функцию через dma_coherent_alloc(). И да, это очень эффективный метод, например, когда идет работа с сетевыми пакетами в ОС, пользовательские данные проходят большой этап обработки прежде чем дойти до драйвера, а в драйвере подготовленные данные со всем шапками копируются в буферы, которые используют не кэшируемую память.

А есть случаи когда в драйвере с DMA более предпочтителен clean/invalidate? Да, есть. К примеру, видеопамять, которая нас и побудила более подробно разобраться с работой cache (). В режиме двойной буферизации у системы есть два буфера, в которые она поочередно рисует, а потом отдает видеоконтроллеру. Если делать такую память не кэшируемой, то случится падение в производительности. Поэтому лучше сделать clean перед тем как отдать буфер в видеоконтроллер.

Заключение


Мы немного разобрались с разными вида кэшей в ARMv7m: write-back, write-through, а также настроек write allocate и no write allocate. Построили синтетические тесты, в которых попытались выяснить когда один режим лучше другого, а также рассмотрели практические примеры с ping и OpenCV. В Embox мы еще только занимаемся данной тематикой, поэтому соответствующая подсистема пока прорабатывается. Хотя достоинства в использовании кэшей определенно заметны.

Все примеры можно посмотреть и воспроизвести собрав Embox из открытого репозитория.

P.S.
Если вам интересна тема системного программирования и OSDev, то уже завтра будет проходить конференция OS Day! В этом году он проходит в онлайне, так что желающие не пропустите! :) Embox выступает завтра в 12.00
Подробнее..

Embox на плате EFM32ZG_STK3200

14.01.2021 22:19:16 | Автор: admin
image
Embox является сильно конфигурируемой RTOS. Основная идея Embox прозрачный запуск Linux программного обеспечения везде, в том числе и на микроконтроллерах. Из достижений стоит привести OpenCV, Qt, PJSIP запущенные на микроконтроллерах STM32F7. Конечно, запуск подразумевает, что в данные проекты не вносились изменения и использовались только опции при конфигурации оригинальных проектов и параметры задаваемые в самой конфигурации Embox. Но возникает естественный вопрос насколько Embox позволяет экономить ресурсы по сравнению с тем же Linux? Ведь последний также достаточно хорошо конфигурируется.

Для ответа на этот вопрос можно подобрать минимально возможную для запуска Embox аппаратную платформу. В качестве такой платформы мы выбрали EMF32ZG_STK3200 от компании SiliconLabs. Данная платформа имеет 32kB ROM и 4kB RAM память. А также процессорное ядро cortex-m0+. Из периферии доступны UART, пользовательские светодиоды, кнопки, а также 128x128 монохромный дисплей. Нашей целью является запуск любого пользовательского приложения, позволяющего убедиться в работоспособности Embox на данной плате.

Для работы с периферией и самой платой нужны драйвера и другой системный код. Данный код можно взять из примеров предоставляемых самим производителем чипа. В нашем случае производитель предлагает использовать SimplifyStudio. Есть также открытый репозиторий на GitHub). Этот код и будем использовать.

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

Пример Makefile для скачивания BSP

PKG_NAME := Gecko_SDKPKG_VER := v5.1.2PKG_ARCHIVE_NAME := $(PKG_NAME)-$(PKG_VER).tar.gzPKG_SOURCES := https://github.com/SiliconLabs/$(PKG_NAME)/archive/v5.1.2.tar.gzPKG_MD5     := 0de78b48a8da80931af1a53d401e74f5include $(EXTBLD_LIB)


Mybuild для сборки BSP
package platform.efm32...@BuildArtifactPath(cppflags="-I$(EXTERNAL_BUILD_DIR)/platform/efm32/bsp_get/Gecko_SDK-5.1.2/hardware/kit/common/bsp/")module bsp_get { }@BuildDepends(bsp_get)@BuildDepends(efm32_conf)static module bsp extends embox.arch.arm.cmsis {    source "platform/emlib/src/em_timer.c",        "platform/emlib/src/em_adc.c",    depends bsp_get    depends efm32_conf}


Mybuild для платы EFM32ZG_STK3200

package platform.efm32.efm32zg_stk3200@BuildArtifactPath(cppflags="-I$(EXTERNAL_BUILD_DIR)/platform/efm32/bsp_get/Gecko_SDK-5.1.2/platform/Device/SiliconLabs/EFM32ZG/Include")@BuildArtifactPath(cppflags="-I$(EXTERNAL_BUILD_DIR)/platform/efm32/bsp_get/Gecko_SDK-5.1.2/hardware/kit/EFM32ZG_STK3200/config")...@BuildArtifactPath(cppflags="-D__CORTEX_SC=0")@BuildArtifactPath(cppflags="-DUART_COUNT=0")@BuildArtifactPath(cppflags="-DEFM32ZG222F32=1")module efm32zg_stk3200_conf extends platform.efm32.efm32_conf {    source "efm32_conf.h"}@BuildDepends(platform.efm32.bsp)@BuildDepends(efm32zg_stk3200_conf)static module bsp extends platform.efm32.efm32_bsp {    @DefineMacro("DOXY_DOC_ONLY=0")    @AddPrefix("^BUILD/extbld/platform/efm32/bsp_get/Gecko_SDK-5.1.2/")    source        "platform/Device/SiliconLabs/EFM32ZG/Source/system_efm32zg.c",        "hardware/kit/common/drivers/displayls013b7dh03.c",...}


После таких достаточно простых действий можно использовать код от производителя. Прежде чем приступить к работе с драйверами необходимо разобраться со средствами разработки и архитектурными частями. В Embox используются обычные средства разработки gcc, gdb, openocd. При запуске openocd нужно указать что мы используем платформу efm32:

sudo openocd -f /usr/share/openocd/scripts/board/efm32.cfg


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

     @Runlevel(0) include embox.arch.generic.arch    include embox.arch.arm.libarch    @Runlevel(0) include embox.arch.arm.armmlib.locore    @Runlevel(0) include embox.arch.system(core_freq=8000000)    @Runlevel(0) include embox.arch.arm.armmlib.exception_entry(irq_stack_size=256)    @Runlevel(0) include embox.kernel.stack(stack_size=1024,alignment=4)    @Runlevel(0) include embox.arch.arm.fpu.fpu_stub


После этого можно попробовать скомпилить Embox и походить с помощью отладчика по шагам, проверив тем самым правильно ли мы задали параметры в линкер скрипте

/* region (origin, length) */ROM (0x00000000, 32K)RAM (0x20000000, 4K)/* section (region[, lma_region]) */text   (ROM)rodata (ROM)data   (RAM, ROM)bss    (RAM)


Первым драйвером реализуемым для поддержки какой-нибудь платы в Embox обычно является UART. На нашей плате есть LEUART. Для драйвера достаточно реализовать несколько функций. При этом мы можем использовать функции из BSP.

static int efm32_uart_putc(struct uart *dev, int ch) {    LEUART_Tx((void *) dev->base_addr, ch);    return 0;}static int efm32_uart_hasrx(struct uart *dev) {...}static int efm32_uart_getc(struct uart *dev) {    return LEUART_Rx((void *) dev->base_addr);}static int efm32_uart_setup(struct uart *dev, const struct uart_params *params) {    LEUART_TypeDef      *leuart = (void *) dev->base_addr;    LEUART_Init_TypeDef init    = LEUART_INIT_DEFAULT;    /* Enable CORE LE clock in order to access LE modules */    CMU_ClockEnable(cmuClock_HFPER, true);  ...    /* Finally enable it */    LEUART_Enable(leuart, leuartEnable);    return 0;}...DIAG_SERIAL_DEF(&efm32_uart0, &uart_defparams);


Для того чтобы функции BSP были доступны нужно просто указать это в описании драйвера, файл Mybuild
package embox.driver.serial@BuildDepends(platform.efm32.efm32_bsp)module efm32_leuart extends embox.driver.diag.diag_api {    option number baud_rate    source "efm32_leuart.c"    @NoRuntime depends platform.efm32.efm32_bsp    depends core    depends diag}


После реализации драйвера UART вам доступны не только вывод, но и консоль где вы можете вызвать свои пользовательские команды. Для этого вам достаточно добавить в конфигурационный файл Embox маленький командный интерпретатор:
    include embox.cmd.help    include embox.cmd.sys.version    include embox.lib.Tokenizer    include embox.init.setup_tty_diag    @Runlevel(2) include embox.cmd.shell    @Runlevel(3) include embox.init.start_script(shell_name="diag_shell")


А также указать, что нужно использовать не полноценный tty доступный через devfs, а заглушку, которая позволяет обращаться к заданному устройству. Устройство задается также в конфигурационном файле mods.conf
    @Runlevel(1) include embox.driver.serial.efm32_leuart    @Runlevel(1) include embox.driver.diag(impl="embox__driver__serial__efm32_leuart")    include embox.driver.serial.core_notty


Еще один очень простой драйвер это GPIO. Для его реализации мы также можем воспользоваться вызовами из BSP. Для этого в описании драйвера укажем что он зависит от BSP
package embox.driver.gpio@BuildDepends(platform.efm32.efm32_bsp)module efm32_gpio extends api {    option number log_level = 0    option number gpio_chip_id = 0    option number gpio_ports_number = 2    source "efm32_gpio.c"    depends embox.driver.gpio.core    @NoRuntime depends platform.efm32.efm32_bsp}


Сама реализация

static int efm32_gpio_setup_mode(unsigned char port, gpio_mask_t pins, int mode) {...}static void efm32_gpio_set(unsigned char port, gpio_mask_t pins, char level) {    if (level) {        GPIO_PortOutSet(port, pins);    } else {        GPIO_PortOutClear(port, pins);    }}static gpio_mask_t efm32_gpio_get(unsigned char port, gpio_mask_t pins) {    return GPIO_PortOutGet(port) & pins;}...static int efm32_gpio_init(void) {#if (_SILICON_LABS_32B_SERIES < 2)  CMU_ClockEnable(cmuClock_HFPER, true);#endif#if (_SILICON_LABS_32B_SERIES < 2) \  || defined(_SILICON_LABS_32B_SERIES_2_CONFIG_2)  CMU_ClockEnable(cmuClock_GPIO, true);#endif    return gpio_register_chip((struct gpio_chip *)&efm32_gpio_chip, EFM32_GPIO_CHIP_ID);}


Этого достаточно чтобы использовать команду pin из Embox. Данная команда позволяет управлять GPIO. И в частности, может использоваться для проверки мигания светодиодом.

Добавляем саму команду в mods.conf
include embox.cmd.hardware.pin


И сделаем так, чтобы она запускалась при старте. Для этого в конфигурационном файле start_sctpt.inc добавим одну из строчек
<source">pin GPIOC 10 blink,

Или

"pin GPIOC 11 blink",


Команды одинаковые, просто номера светодиодов разные.

Попробуем запустить еще и дисплей. Сначала все просто. Ведь мы опять можем использовать вызовы BSP. Для этого нам нужно только добавить их в описание драйвера фреймбуфера

package embox.driver.video@BuildDepends(platform.efm32.efm32_bsp)module efm32_lcd {...    source "efm32_lcd.c"    @NoRuntime depends platform.efm32.efm32_bsp}


Но как только мы делаем любой вызов связанный с дисплеем например DISPLAY_Init у нас секция .bss увеличивается больше чем на 2 kB, при размерах RAM 4 kB, это очень существенно. После изучения данного вопроса, выяснилось, что в самом BSP выделен фреймбуфер размером под дисплей. То есть 128x128x1 бит или 2048 байт.

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

Первым я убрал командный интерпретатор и оставил только вызов уже упомянутой команды pin. Для этого я изменил конфигурационный файл mods.conf следующим образом
    //@Runlevel(2) include embox.cmd.shell    //@Runlevel(3) include embox.init.start_script(shell_name="diag_shell")    @Runlevel(3) include embox.init.system_start_service(cmd_max_len=32, cmd_max_argv=6)


Поскольку я использовал другой модуль для пользовательского старта, я перенес запуск команд в другой конфигурационный файл. Вместо start_script.inc использовал system_start.inc.

Затем, поскольку уже не требовалось использовать индексные дескрипторы в командном интерпретаторе, а также таймеры, я с помощью опций в mods.config, избавился и от них
    include embox.driver.common(device_name_len=1, max_dev_module_count=0)    include embox.compat.libc.stdio.file_pool(file_quantity=0)    include embox.kernel.task.resource.idesc_table(idesc_table_size=3)    include embox.kernel.task.task_no_table    @Runlevel(1) include embox.kernel.timer.sys_timer(timer_quantity=1)...    @Runlevel(1) include embox.kernel.timer.itimer(itimer_quantity=0)


Поскольку я вызывал команды напрямую, а не через командный интерпретатор, я смог уменьшить размер стека
    @Runlevel(0) include embox.arch.arm.armmlib.exception_entry(irq_stack_size=224)    @Runlevel(0) include embox.kernel.stack(stack_size=448,alignment=4)


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

Мне хотелось вывести что нибудь на дисплейю Я подумал, что логотип Embox будет показателен. По хорошему нужно использовать полноценный драйвер фреймбуфера и выводить изображение из файла, ведь все это есть в Embox. Но места совсем не хватало. И для демонстрации я решил вывести логотип прямо в функции инициализации драйвера фреймбуфера. Причем данные конвертировав напрямую в битовый массив. Таким образом мне потребовалось ровно 2048 байт в ROM.

Сам код, как и ранее, использует BSP

extern const uint8_t demo_image_mono_128x128[128][16];static int efm_lcd_init(void) {    DISPLAY_Device_t      displayDevice;    EMSTATUS status;    DISPLAY_PixelMatrix_t pixelMatrixBuffer;    /* Initialize the DISPLAY module. */    status = DISPLAY_Init();    if (DISPLAY_EMSTATUS_OK != status) {        return status;    }    /* Retrieve the properties of the DISPLAY. */    status = DISPLAY_DeviceGet(DISPLAY_DEVICE_NO, &displayDevice);    if (DISPLAY_EMSTATUS_OK != status) {        return status;    }    /* Allocate a framebuffer from the DISPLAY device driver. */    displayDevice.pPixelMatrixAllocate(&displayDevice,            displayDevice.geometry.width,            displayDevice.geometry.height,            &pixelMatrixBuffer);#if START_WITH_LOGO    memcpy(pixelMatrixBuffer, demo_image_mono_128x128,            displayDevice.geometry.width * displayDevice.geometry.height / 8 );    status = displayDevice.pPixelMatrixDraw(&displayDevice,            pixelMatrixBuffer,            0,            displayDevice.geometry.width,            0,            displayDevice.geometry.height);#endif    return 0;}


Собственно все. На коротком видео можно увидеть результат.


Весь код доступен на GitHub. Если есть плата, то же самое можно воспроизвести на ней с помощью инструкции описанной на wiki.

Результат превзошел мои ожидания. Ведь удалось запустить Embox по сути на 2kB RAM. Это означает, что с помощью опций в Embox накладные расходы на использование ОС можно свести к минимуму. При этом в системе присутствует многозадачность. Пусть даже она и кооперативная. Ведь обработчики таймеров вызываются не напрямую в контексте прерывания, а из собственного контекста. Что естественно является плюсом использования ОС. Конечно, данный пример во многом искусственный. Ведь при столь ограниченных ресурсах и функциональность будет ограниченной. Преимущества Embox начинают сказываться на более мощных платформах. Но в то же время это можно считать предельным случаем работы Embox.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru