Время на прочтение
25 мин
Количество просмотров 180K
Добрый день, уважаемые хабровчане. В этом цикле статей мы с вами пройдем достаточно длинный, но весьма интересный путь по превращению обычного роутера в мини-компьютер с LCD-дисплеем. Для этого мы разработаем сначала USB-видеокарту на базе микроконтроллера STM32F103, потом тестовый драйвер, который позволит нам выводить на него графику, и, наконец – полноценный драйвер фреймбуффера, благодаря которому можно будет запустить настоящие графические приложения, такие как x-сервер. Заодно мы научимся включать наш код в дерево исходников OpenWRT, допиливать его ядро и делать прочие полезные вещи.
Ну а в самом конце мы получим результат, который, я надеюсь, вызовет ностальгическую слезу у многих читателей. Я постараюсь излагать материал таким образом, чтобы в конце каждого этапа мы получали осязаемый результат, не дающий угаснуть энтузиазму. Итак, начнем.
Железо
Традиционно, посмотрим, что из железа нам потребуется. Тех, кто не любит паять, я могу сразу успокоить — все что мы будем делать – чисто фёрмварное и софтварное, так что собственно паять нам не придется. Но зато на понадобится отладочная на контроллере STM32F103VE и QVGA дисплеем, о которой я уже упоминал в своей статье.
Также нам понадобится, собственно, роутер, который я также упоминал в другой статье, но к нему мы вернемся позже. Сейчас же сосредоточимся на разработке самой USB-видеокарты на базе нашей отладочной платы.
У STM32F103 есть в составе два блока, которые нам очень пригодятся. Первый из них – это, разумеется, аппаратный USB-контроллер, который отлично подойдет для организации соединения между нашей видеокартой и хостом. Второй – FSMC – Flexible Static Memory Controller. Он представляет собой контроллер статической памяти, который может быть настроен для использования совместно с микросхемами SRAM, NAND/NOR FLASH и тому подобными устройствами, причем ширина шины и тайминги в нем поддаются настройке. При этом микросхемы мапируются на адресное пространство и, при обращении по соответствующему адресу, FSMC сам генерирует необходимые сигналы на управляющих линиях и шинах адреса и данных, так что для программиста этот процесс является полностью прозрачным.
В случае с дисплеем это нам очень поможет, так как дисплеи подобного рода снабжены интерфейсом, почти полностью совпадающим с интерфейсом NOR-флешки и SRAM: 16-битная шина данных, стробы CS, ~WR, ~RD. С этими сигналами все просто – CS активизирует дисплей, с него начинается цикл обмена данными. ~WR либо ~RD активизируются при обращении на чтение либо запись соответственно.
К сожалению, дисплей не предоставляет нам своей шины адреса и данных для доступа напрямую к видеопамяти, вместо этого нам доступна единая 16-битная шина и дополнительный сигнал RS, Register Select. При активном сигнале RS, значение, выставленное на эту шину, воспринимается как адрес регистра (ячейки RAM) контроллера дисплея, к которому следует обратиться, а последующее чтение либо запись с неактивным RS – операцией с RAM дисплея.
Следует отметить, что RAM в данном случае не является видеопамятью, это память контроллера дисплея, к которой открыт доступ извне посредством описанного выше механизма. Видеопамять же фигурирует в документации как GRAM, Graphics RAM, и ее содержимое доступно через «окошко» в виде одного из регистров. При этом логика контроллера дисплея сама инкрементирует/декрементирует внутренний адрес видеопамяти при последовательном чтении из одного и того же регистра (CTR_WRITE_DATA, CTR_READ_DATA)
FSMC не имеет специализированного сигнала RS, поэтому для этого применяется один трюк: какой-нибудь из доступных сигналов шины адреса FSMC подключается к сигналу RS.
Предположим, что мы подключили сигнал A0 к RS. Тогда при обращении к адресу памяти 0х00000000 на запись (относительно базового адреса, на который мапирован данный банк FSMC) сигнал RS будет неактивен и дисплей воспримет это как установку адреса регистра.
При обращении же к 0х00000001 адресная линия A0 будет активна, и чтение либо запись выполнятся уже для ячейки RAM, то есть, для регистра, адрес которого был задан при обращении по нулевому адресу.
Подробнее про это можно прочитать в апноуте от СТМ, посвященном данному вопросу.
Описание регистров контроллера дисплея доступно его даташите.
Кстати, с даташитами следует быть поосторожнее и внимательно смотреть на его версию, потому что китайские товарищи обожают сначала копировать саму микросхему (не полностью, а как получится), а потом – копировать даташит от этой микросхемы. Поэтому в процессе чтения даташита можно с удивлением обнаружить регистры и функции, которых данный контроллер никогда не поддерживал и которые в следующей версии даташита уже подтерты.
Так, например, ранние версии даташита на данный контроллер сообщают, что дисплей умеет делать аппаратные побитовые операции, включая аппаратную маску для реализации прозрачности, однако, если покопаться глубже, то окажется, что эта строчка попала в даташит на ILI9325 из даташита другого, японского контроллера дисплея, который китайцы благополучно скопировали и обозвали совместимым.
Так как дисплей уже подключен к Mini-STM32, все что нам нужно – узнать, к какому из сигналов выбора чипа он подключен и какая из адресных линий используется в качестве сигнала RS.
Согласно схеме, в качестве сигнала CS используется FSMC_NE1, а в качестве RS – FSMC_A16.
Также дисплей имеет сигнал Reset, выведенный на PE1 и сигнал управления подсветкой, соединенный с PD13.
Заодно посмотрим, какой из сигналов используется для подключения подтяжки USB, о которой поговорим позже – в данной схеме это PC13.
Итак, переходим к коду.
Софт
Работа с LCD
Начнем с разработки небольшой библиотеки для работы с дисплеем. Работать будем в CooCox IDE. Вынесем в заголовочный файл все адреса регистров из даташита:
Объявление регистров LCD
#define CTR_OSC_START 0x0000
#define CTR_DRV_OUTPUT1 0x0001
#define CTR_DRV_WAVE 0x0002
#define CTR_ENTRY_MODE 0x0003
#define CTR_RESIZE 0x0004
#define CTR_DISPLAY1 0x0007
#define CTR_DISPLAY2 0x0008
#define CTR_DISPLAY3 0x0009
#define CTR_DISPLAY4 0x000A
#define CTR_RGB_INTERFACE1 0x000C
#define CTR_FRM_MARKER 0x000D
#define CTR_RGB_INTERFACE2 0x000F
#define CTR_POWER1 0x0010
#define CTR_POWER2 0x0011
#define CTR_POWER3 0x0012
#define CTR_POWER4 0x0013
#define CTR_HORZ_ADDRESS 0x0020
#define CTR_VERT_ADDRESS 0x0021
#define CTR_WRITE_DATA 0x0022
#define CTR_READ_DATA 0x0022
#define CTR_POWER7 0x0029
#define CTR_FRM_COLOR 0x002B
#define CTR_GAMMA1 0x0030
#define CTR_GAMMA2 0x0031
#define CTR_GAMMA3 0x0032
#define CTR_GAMMA4 0x0035
#define CTR_GAMMA5 0x0036
#define CTR_GAMMA6 0x0037
#define CTR_GAMMA7 0x0038
#define CTR_GAMMA8 0x0039
#define CTR_GAMMA9 0x003C
#define CTR_GAMMA10 0x003D
#define CTR_HORZ_START 0x0050
#define CTR_HORZ_END 0x0051
#define CTR_VERT_START 0x0052
#define CTR_VERT_END 0x0053
#define CTR_DRV_OUTPUT2 0x0060
#define CTR_BASE_IMAGE 0x0061
#define CTR_VERT_SCROLL 0x006A
#define CTR_PIMG1_POS 0x0080
#define CTR_PIMG1_START 0x0081
#define CTR_PIMG1_END 0x0082
#define CTR_PIMG2_POS 0x0083
#define CTR_PIMG2_START 0x0084
#define CTR_PIMG2_END 0x0085
#define CTR_PANEL_INTERFACE1 0x0090
#define CTR_PANEL_INTERFACE2 0x0092
#define CTR_PANEL_INTERFACE4 0x0095
#define CTR_OTP_VCMPROGRAM 0x00A1
#define CTR_OTP_VCMSTATUS 0x00A2
#define CTR_OTP_IDKEY 0x00A5
Мы помним, что с точки зрения кода, обращение к FSMC будет простой записью/чтением из памяти, поэтому нам нужно определить по каким именно адресам обращаться. Смотрим в референс мануал на STM32, раздел FSMC, и видим, что для NOR/SRAM выделены адреса, начинающиеся с 0x60000000.
Под банками в широком смысле в мануале подразумеваются большие регионы, выделенные для устройств разного типа, так, банк #1 – это NOR/SRAM, банки #2 и #3 – NAND, банк #4- PC Card.
В свою очередь банк #1 может быть использован для доступа к целым 4 чипам памяти, каждый из которых может независимо от других быть NOR либо SRAM. Так как дисплей подключен как NE1, нас интересует банк, объявленный как FSMC_Bank1_NORSRAM1. Исходя из базового адреса, можно сразу же записать определение
#define LCDRegister (*((volatile uint16_t*) 0x60000000))
Адресом, активизирующим RS будет адрес, в котором активна линия A16, то есть, к примеру 0x60000000 + (2<<16), то есть 0x60020000, поэтому запишем
#define LCDMemory (*((volatile uint16_t*) 0x60020000))
И сразу же определим соответствующие макросы для записи значений в регистры дисплея и в его память:
#define LCD_WRITE_REGISTER(REG, DATA) LCDRegister=REG;LCDMemory=DATA;
#define LCD_BEGIN_RAM_WRITE LCDRegister=CTR_WRITE_DATA;
#define LCD_WRITE_RAM(DATA) LCDMemory=DATA
Заодно определим дефайнами имена пинов и их портов, отвечающих за сброс дисплея и за включение подсветки:
#define BacklightPin GPIO_Pin_13
#define BacklightPort GPIOD
#define ResetPin GPIO_Pin_1
#define ResetPort GPIOE
Теперь напишем код инициализации FSMC и сопутствующей периферии:
Код инициализации FSMC
void LCDInitHardware()
{
SysTick_Config(SystemCoreClock/1000);
GPIO_InitTypeDef GPIO_InitStructure;
FSMC_NORSRAMInitTypeDef FSMC_InitStructure;
FSMC_NORSRAMTimingInitTypeDef FSMC_Timing;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_FSMC, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE | RCC_APB2Periph_AFIO, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_4 | GPIO_Pin_5 |
GPIO_Pin_7 | GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10|
GPIO_Pin_11| GPIO_Pin_14| GPIO_Pin_15; //Interface
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOD, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7 | GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 |
GPIO_Pin_11| GPIO_Pin_12| GPIO_Pin_13| GPIO_Pin_14 |
GPIO_Pin_15; //Interface
GPIO_Init(GPIOE, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = BacklightPin; //Backlight
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(BacklightPort, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = ResetPin; //Reset
GPIO_Init(ResetPort, &GPIO_InitStructure);
GPIO_SetBits(ResetPort,ResetPin);
FSMC_Timing.FSMC_AddressSetupTime = 1;
FSMC_Timing.FSMC_AddressHoldTime = 0;
FSMC_Timing.FSMC_DataSetupTime = 5;
FSMC_Timing.FSMC_BusTurnAroundDuration = 0;
FSMC_Timing.FSMC_CLKDivision = 0;
FSMC_Timing.FSMC_DataLatency = 0;
FSMC_Timing.FSMC_AccessMode = FSMC_AccessMode_B;
FSMC_InitStructure.FSMC_Bank = FSMC_Bank1_NORSRAM1;
FSMC_InitStructure.FSMC_DataAddressMux = FSMC_DataAddressMux_Disable;
FSMC_InitStructure.FSMC_MemoryType = FSMC_MemoryType_SRAM;
FSMC_InitStructure.FSMC_MemoryDataWidth = FSMC_MemoryDataWidth_16b;
FSMC_InitStructure.FSMC_BurstAccessMode = FSMC_BurstAccessMode_Disable;
FSMC_InitStructure.FSMC_WaitSignalPolarity = FSMC_WaitSignalPolarity_Low;
FSMC_InitStructure.FSMC_WrapMode = FSMC_WrapMode_Disable;
FSMC_InitStructure.FSMC_WaitSignalActive = FSMC_WaitSignalActive_BeforeWaitState;
FSMC_InitStructure.FSMC_WriteOperation = FSMC_WriteOperation_Enable;
FSMC_InitStructure.FSMC_WaitSignal = FSMC_WaitSignal_Disable;
FSMC_InitStructure.FSMC_ExtendedMode = FSMC_ExtendedMode_Disable;
FSMC_InitStructure.FSMC_AsynchronousWait = FSMC_AsynchronousWait_Disable;
FSMC_InitStructure.FSMC_WriteBurst = FSMC_WriteBurst_Disable;
FSMC_InitStructure.FSMC_ReadWriteTimingStruct = &FSMC_Timing;
FSMC_InitStructure.FSMC_WriteTimingStruct = &FSMC_Timing;
FSMC_NORSRAMInit(&FSMC_InitStructure);
FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM1, ENABLE);
}
Здесь все довольно просто – мы настраиваем системный таймер на интервал в одну миллисекунду (что будет необходимо для организации задержек в процессе инициализации контроллера дисплея), потом настраиваем все сигналы, которыми владеет FSMC как управляемые “alternative functions”, настраиваем пины Reset и Backlight как Output Push-Pull, после чего переходим к настройкам FSMC.
Настройки таймингов можно попробовать посчитать самому, я же взял рекомендуемые из апноута от STM и они подошли отлично. Выставляем тип памяти как SRAM, ширину шины в 16 бит, отключая все дополнительные фичи, описание которых занимает не одну страницу даташита.
Объявим вспомогательную функцию, позволяющую нам сделать достаточно точную задержку:
Функция задержки на SysTick-таймере
volatile uint32_t Tick = 0x00000000;
void SysTick_Handler()
{
if(Tick>0)
Tick--;
}
void SysTickDelay(uint32_t msDelay)
{
Tick=msDelay;
while(Tick);
}
Далее пишем функцию инициализации непосредственно дисплея:
Инициализация LCD
void LCDInit()
{
LCDHardwareReset();
LCD_WRITE_REGISTER(CTR_OSC_START, 0x0001);
LCD_WRITE_REGISTER(CTR_DRV_OUTPUT1, 0x0100);
LCD_WRITE_REGISTER(CTR_DRV_WAVE, 0x0700);
LCD_WRITE_REGISTER(CTR_ENTRY_MODE, 0x1038);
LCD_WRITE_REGISTER(CTR_RESIZE, 0x0000);
LCD_WRITE_REGISTER(CTR_DISPLAY2, 0x0202);
LCD_WRITE_REGISTER(CTR_DISPLAY3, 0x0000);
LCD_WRITE_REGISTER(CTR_DISPLAY4, 0x0000);
LCD_WRITE_REGISTER(CTR_RGB_INTERFACE1, 0x0001);
LCD_WRITE_REGISTER(CTR_FRM_MARKER, 0x0000);
LCD_WRITE_REGISTER(CTR_RGB_INTERFACE2, 0x0000);
LCD_WRITE_REGISTER(CTR_POWER1, 0x0000);
LCD_WRITE_REGISTER(CTR_POWER2, 0x0007);
LCD_WRITE_REGISTER(CTR_POWER3, 0x0000);
LCD_WRITE_REGISTER(CTR_POWER4, 0x0000);
SysTickDelay(200);
LCD_WRITE_REGISTER(CTR_POWER1, 0x1590);
LCD_WRITE_REGISTER(CTR_POWER2, 0x0227);
SysTickDelay(50);
LCD_WRITE_REGISTER(CTR_POWER3, 0x009C);
SysTickDelay(50);
LCD_WRITE_REGISTER(CTR_POWER4, 0x1900);
LCD_WRITE_REGISTER(CTR_POWER7, 0x1900);
LCD_WRITE_REGISTER(CTR_FRM_COLOR, 0x000E);
SysTickDelay(50);
LCD_WRITE_REGISTER(CTR_HORZ_ADDRESS, 0x0000);
LCD_WRITE_REGISTER(CTR_VERT_ADDRESS, 0x0000);
LCD_WRITE_REGISTER(CTR_HORZ_START, 0x0000);
LCD_WRITE_REGISTER(CTR_HORZ_END, 239);
LCD_WRITE_REGISTER(CTR_VERT_START, 0x0000);
LCD_WRITE_REGISTER(CTR_VERT_END, 319);
LCD_WRITE_REGISTER(CTR_DRV_OUTPUT2, 0x2700);
LCD_WRITE_REGISTER(CTR_BASE_IMAGE, 0x0001);
LCD_WRITE_REGISTER(CTR_VERT_SCROLL, 0x0000);
GPIO_SetBits(BacklightPort,BacklightPin);
}
Последовательность берется из даташита на контроллер дисплея, все что нужно сделать – просто в нужном порядке инициализировать определенные регистры, включив его осциллятор и питание, а также выдержать рекомендованные задержки, давая схематике дисплея время устояться. Также в данном коде задаются режимы работы дисплея, такие, например, как поведение счетчика адреса при записи в GRAM – можно заставить дисплей увеличивать либо уменьшать счетчик, сдвигать указатели вниз либо вверх – то есть задать направление, в котором будут закрашиваться пиксели при их последовательном выводе.
В данном коде функция LCDHardwareReset – небольшой блочок кода, выставляющий активный уровень на линии Reset, выжидающий некоторое время и сбрасывающий его в неактивное состояние:
void LCDHardwareReset()
{
GPIO_ResetBits(ResetPort,ResetPin);
SysTickDelay(50);
GPIO_SetBits(ResetPort,ResetPin);
SysTickDelay(10);
}
Введем еще пару управляющих функций, отвечающих за включение и выключение дисплея на уровне его контроллера:
void LCDOn()
{
LCD_WRITE_REGISTER(CTR_DISPLAY1, 0x0133);
}
void LCDOff()
{
LCD_WRITE_REGISTER(CTR_DISPLAY1, 0x0131);
}
Осталось совсем немного — объявляем очень важную для нас функцию, которая задает прямоугольник, в пределах которого будет выводиться графика. При этом работа с адресом вывода лежит на плечах контроллера дисплея, все что нам нужно – задать границы этой области:
void LCDSetBounds(uint16_t left, uint16_t top, uint16_t right, uint16_t bottom)
{
LCD_WRITE_REGISTER(CTR_VERT_START, left);
LCD_WRITE_REGISTER(CTR_VERT_END, right);
LCD_WRITE_REGISTER(CTR_HORZ_START, top);
LCD_WRITE_REGISTER(CTR_HORZ_END, bottom);
LCD_WRITE_REGISTER(CTR_HORZ_ADDRESS, top);
LCD_WRITE_REGISTER(CTR_VERT_ADDRESS, left);
}
Из названия регистров сразу понятно, что функция задает левую, правую, верхнюю и нижнюю границы, а затем выставляет указатель в позицию, соответствующую верхнему левому пикселю.
Так как эти указатели относятся к вертикальному положению дисплея, а мы его будем использовать в горизонтальном, то нам следует поменять left и top местами, занося значение left в регистры, относящиеся к VERTICAL, а top – к HORIZONTAL.
Ну и наконец функция, которой мы сможем сразу проверить правильность работы дисплея – функция очистки. Представляет собой простую последовательную запись одного и того же цветового значения в GRAM дисплея:
void LCDClear(uint16_t color)
{
LCDSetBounds(0,0,320-1,240-1);
LCD_BEGIN_RAM_WRITE;
uint32_t i;
for(i=0;i<320*240;i++)
LCD_WRITE_RAM(color);
}
Все, что нам нужно сделать, чтобы включить наш дисплей – выполнить
LCDInitHardware();
LCDInit();
LCDOn();
LCDClear(0x0000);
Если все сделано правильно, дисплей включится и закрасится черным. При замене аргумента функции LCDClear и перезапуске программы дисплей должен окрасится в выбранный цвет.
Теперь перейдем к более сложной части – USB.
Работа с USB
Работа с USB – очень обширная и многогранная тема, для которой явно не хватит одной статьи.
Поэтому я рекомендую для начала ознакомиться с очень полезным документом, называющимся USB In A Nutshell (английская версия, русская версия) прежде чем продолжать.
Если коротко подвести итог, то одним из основных понятий в интерфейсе USB является Endpoint, который можно с некоторой натяжкой назвать аналогом сокета.
Также как сокет можно открыть для UDP либо для TCP соединения, эндпоинты тоже разделяются по типам.
- Control Endpoints используются для асинхронной передачи небольших управляющих сообщений, до 64 байт длиной в случае Full Speed USB. Нулевой эндпоинт должен быть в любом USB устройстве и он должен быть именно Control. Через него хост запрашивает начальную информацию и производит инициализацию устройства. Доставка пакета через эндпоинты этого типа гарантирована, в случае ошибки хост автоматически пытается перепослать данные. Control Endpoint – единственный двунаправленный тип эндпоинтов, все остальные работают либо только на получение, либо только на передачу данных.
- Interrupt Endpoints – эндпоинты для которых важен опрос хостом с заданной периодичностью (мы ведь помним, что USB-устройства не могут сами инициировать передачу, они ждут запроса от хоста). Обычно этот тип точек применяется в HID-устройствах типа клавиатуры и мыши. Размер пакета также может составлять до 64 байт в FS USB. Доставка гарантирована.
- Isochronous Endpoint – эндпоинт, использующийся в основном для передачи аудио и видео информации, где не страшна потеря пакета – в этом типе эндпоинта только проверяется целостность данных, переотправка в случае ошибки не производится. Размер пакета до 1024 байт в FS USB.
- Bulk Enpoints – основной тип эндпоинтов, использующийся в устройствах хранения информации.
Предполагает посылку пакетов данных с гарантированной доставкой и размером пакета до 64 байтов для FS USB (для High Speed размер пакета может доходить до 1023 байт). В контроллере USB нашей STM32 есть возможность организовать аппаратную двойную буферизацию для пакетов данного типа, увеличивая пропускную способность.Вся доступная полоса пропускания, оставшаяся после Interrupt, Control и Isochronous делится между Bulk-эндпоинтами – поэтому следует обращать внимание на то, какие еще передачи идут параллельно с Bulk.
Наиболее подходящий для нашего случая тип эндпоинта — Bulk, мы будем слать блоки графической информации, которая будет отображена на дисплее, и в последствие можно будет организовать двойную буферизацию. При этом нам не требуется какая-либо периодичность в посылках и «потоковая» сущность Isochronous эндпоинтов, т.к. графические данные будут идти в несжатом формате в произвольные позиции дисплея и терять пакеты нам совершенно ни к чему.
Чтобы хост понял, что у нас за устройство, что у него есть за эндпоинты и какие у него возможности, хост запрашивает несколько дескрипторов через Контрол Эндпоинт #0.
Описываемая ими структура отлично показана на рисунке из упомянутой выше статьи:
Вернемся к ними немного позже, а сейчас перейдем к рассмотрению того, что нужно добавить в нашу прошивку, чтобы заставить устройство работать с USB. Будем работать последовательно и по пунктам.
- Для начала скачиваем библиотеку от STM для работы с USB с официального сайтаи помещаем папку STM32_USB-FS-Device_Driver из недр скачанного архива (лежит в папке Libraries) в папку с нашим проектом. Добавляем его в проект, выбрав File – Add Linked Folder. Добавляем новую папку в наш проект, обозвав ее как-нибудь типа usb_user, в которой создаем файлы hw_config.h и usb_conf.h – эти файлы требует библиотека от STM.
В hw_config.h сразу же пишем#include "stm32f10x.h"
иначе будет куча эрроров от неразрешенных типов (uint8_t, uint16_t, uint32_t,…)
Не забываем в свойствах проекта указать папку с библиотекой и нашу usb_user как дополнительные пути для поиска инклудов. - Добавим в usb_user новый заголовочный файл, который будет содержать объявления, необходимые для дескрипторов, назовем его, допустим, usb_desc.h, поместив туда следующий код:
#include "usb_lib.h" #define SIZ_DEVICE_DESC 0x12 #define SIZ_CONFIG_DESC 0x19 #define SIZ_STRING_LANGID 0x04 #define SIZ_STRING_VENDOR 0x10 #define SIZ_STRING_PRODUCT 0x10 #define SIZ_STRING_SERIAL 0x12 extern const uint8_t USB_ConfigDescriptor[SIZ_CONFIG_DESC]; extern ONE_DESCRIPTOR Device_Descriptor; extern ONE_DESCRIPTOR Config_Descriptor; extern ONE_DESCRIPTOR String_Descriptor[4];
Здесь дефайны, начинающиеся на «SIZ_» содержат размеры будущих дескрипторов. В процессе проектирования эти размеры определяются после того, как дескрипторы уже написаны, но т.к. я уже спроектировал девайс, можно просто копировать.
extern const uint8_t USB_ConfigDescriptor мы выносим в хедер только потому, что нам потребуется доступ к этой структуре из основного модуля. К остальным дескрипторам не потребуется, по той причине, что в библиотеку мы будем отдавать не сами дескрипторы в виде массивов uint_8, а специальные структуры, которые зовутся ONE_DESCRIPTOR, которые, по такому случаю, объявлены ниже. В них ничего страшного нет, это просто структуры из двух членов, первый из которых – указатель на тот самый дескриптор в виде uint8_t*, второй – шестнадцатибитная длина этого дескриптора.
Теперь перейдем к, собственно, дескрипторам, добавив новый файл usb_desc.c и подключив туда наш заголовок.
Начинаем с описателя самого девайса. Вся необходимая информация о полях есть в статье USB In A Nutshell, отмечу только, что все дескрипторы начинаются с байтовой длины дескриптора (поэтому мы их и вынесли в дефайны), за которой следует байт – тип дескриптора.
Вот так выглядит дескриптор устройства:Дескриптор устройства
const uint8_t USB_DeviceDescriptor[SIZ_DEVICE_DESC] = { 0x12, /* bLength */ 0x01, /* bDescriptorType */ 0x00, 0x02, /* bcdUSB = 2.00 */ 0xFF, /* bDeviceClass: Vendor Specific */ 0x00, /* bDeviceSubClass */ 0x00, /* bDeviceProtocol */ 0x40, /* bMaxPacketSize0 */ 0xAD, 0xDE, /* idVendor*/ 0x0D, 0xF0, /* idProduct*/ 0x00, 0x01, /* bcdDevice = 2.00 */ 1, /* Index of string descriptor describing manufacturer */ 2, /* Index of string descriptor describing product */ 3, /* Index of string descriptor describing the device's serial number */ 0x01 /* bNumConfigurations */ };
Мы создаем «vendor-specific» устройство (не пренадлежащие ни к какому конкретному предопределенному классу вроде HID), с VID= 0xDEAD и PID = 0xF00D, единственной конфигурацией и максимальным размером пакета 64 байта.
Дальше объявляем дескриптор конфигурации, который включает в себя дескрипторы интерфейса и эндпоинтов:Дескриптор конфигурации
const uint8_t USB_ConfigDescriptor[SIZ_CONFIG_DESC] = { /*Configuration Descriptor*/ 0x09, /* bLength: Configuration Descriptor size */ 0x02, /* bDescriptorType: Configuration */ SIZ_CONFIG_DESC, /* wTotalLength:no of returned bytes */ 0x00, 0x01, /* bNumInterfaces: 1 interface */ 0x01, /* bConfigurationValue: Configuration value */ 0x00, /* iConfiguration: Index of string descriptor describing the configuration */ 0xE0, /* bmAttributes: bus powered */ 0x32, /* MaxPower 100 mA */ /*Interface Descriptor*/ 0x09, /* bLength: Interface Descriptor size */ 0x04, /* bDescriptorType: Interface */ 0x00, /* bInterfaceNumber: Number of Interface */ 0x00, /* bAlternateSetting: Alternate setting */ 0x01, /* bNumEndpoints: One endpoints used */ 0xFF, /* bInterfaceClass: Vendor Specific*/ 0x00, /* bInterfaceSubClass*/ 0x00, /* bInterfaceProtocol*/ 0x00, /* iInterface: */ /*Endpoint 1 Descriptor*/ 0x07, /* bLength: Endpoint Descriptor size */ 0x05, /* bDescriptorType: Endpoint */ 0x01, /* bEndpointAddress: (OUT1) */ 0x02, /* bmAttributes: Bulk */ 0x40, /* wMaxPacketSize: */ 0x00, 0x00 /* bInterval: */ };
Тут надо быть внимательным – первым байтом идет размер только, собственно, конфиг-дескриптора, который всегда равен 0х09 байтам. Дальше идет тип этого дескриптора, а вот следом идет двухбайтовая длина всего этого массива, включая конфиг дескриптор, дескрипторы интерфейсов и эндпоинтов. В данном случае она вписалась в один байт, так что второй оставляем нулем.
Дальше пишем что у нас один интерфейс, одна конфигурация (расположенная по индексу 0), что устройство питается от шины и потребляет не более 100 мА.
Дальше в том же массиве идет дескриптор интерфейса, размером в те же 0х09 байт, два индекса, оба нули, по которым хост обращается к конкретно этому интерфейсу, количество эндпоинтов не считая нулевого – у нас будет один, класс устройства, опять «Vendor Specific», никаких сабклассов, никаких протоколов, никаких строковых описателей интерфейса (нули во всех соответствующих байтах).
Наконец, последним идет дескриптор нашего единственного эндпоинта. Эндпоинт 0 в таковом не нуждается, поэтому описываем сразу Bulk Endpoin 1. Байт адреса устанавливает не только номер эндпоинта но и направление передачи своим старшим битом. Задаем тип – Bulk, предельный размер пакета (внимание, под него выделены два байта!) и оставляем последний байт равным нулю, т.к. он не играет роли для Bulk-эндпоинтов.
Дальше объявляем строковые дескрипторы:Строковые дескрипторы
/* USB String Descriptors */ const uint8_t USB_StringLangID[SIZ_STRING_LANGID] = { SIZ_STRING_LANGID, /* bLength */ 0x03, /* String descriptor */ 0x09, 0x04 /* LangID = 0x0409: U.S. English */ }; const uint8_t USB_StringVendor[SIZ_STRING_VENDOR] = { SIZ_STRING_VENDOR, /* Size of Vendor string */ 0x03, /* bDescriptorType*/ /* Manufacturer: "Amon-Ra" */ 'A', 0, 'm', 0, 'o', 0, 'n', 0, '-', 0, 'R', 0, 'a', 0 }; const uint8_t USB_StringProduct[SIZ_STRING_PRODUCT] = { SIZ_STRING_PRODUCT, /* bLength */ 0x03, /* bDescriptorType */ /* Product name: "USB LCD" */ 'U', 0, 'S', 0, 'B', 0, ' ', 0, 'L', 0, 'C', 0, 'D', 0 }; uint8_t USB_StringSerial[SIZ_STRING_SERIAL] = { SIZ_STRING_SERIAL, /* bLength */ 0x03, /* bDescriptorType */ 'U', 0, 'S', 0, 'B', 0, 'L', 0, 'C', 0, 'D', 0, '0', 0, '1', 0 };
Строки записаны юникодом и их можно поменять по желанию.
Наконец заполняем структуры, которые требуются библиотеке:Структуры для библиотеки
ONE_DESCRIPTOR Device_Descriptor = { (uint8_t*)USB_DeviceDescriptor, SIZ_DEVICE_DESC }; ONE_DESCRIPTOR Config_Descriptor = { (uint8_t*)USB_ConfigDescriptor, SIZ_CONFIG_DESC }; ONE_DESCRIPTOR String_Descriptor[4] = { {(uint8_t*)USB_StringLangID, SIZ_STRING_LANGID}, {(uint8_t*)USB_StringVendor, SIZ_STRING_VENDOR}, {(uint8_t*)USB_StringProduct, SIZ_STRING_PRODUCT}, {(uint8_t*)USB_StringSerial, SIZ_STRING_SERIAL} };
- Внесем некоторый описательный код в usb_conf.h:
Код usb_conf.h
#define EP_NUM 0x02 #define BTABLE_ADDRESS (0x00) /* EP0 */ /* rx/tx buffer base address */ #define ENDP0_RXADDR (0x40) #define ENDP0_TXADDR (0x80) /* EP1 */ /* tx buffer base address */ #define ENDP1_RXADDR (0xC0) /* IMR_MSK */ /* mask defining which events has to be handled */ /* by the device application software */ #define IMR_MSK (CNTR_CTRM | CNTR_RESETM)
Почти все из вышеперечисленного не требуется библиотекой и нужно только для улучшения читаемости кода в нашем главном модуле. Исключение — IMR_MSK, маска, показывающая, какие прерывания USB используются. Выставим ее в необходимый минимум – прерывание Correct Transfer и Reset.
Адреса эндпоинтов задаются в адресном пространстве так называемой PMA, Packet Memory Area, с учетом длины пакетов. Так как для обоих эндпоинтов максимальный размер пакета задан в 64 байта, размещаем их с соответствующим шагом, не забывая о таблице этих самых адресов эндпоинтов, которая хранится там же и тоже занимает место. - Теперь нам нужно определить колбэки, которые требует библиотека. Эти колбэки объединяются в структуры DEVICE_PROP и USER_STANDARD_REQUESTS, при этом библиотека будет искать их экземпляры под именами Device_Property и User_Standard_Requests.
Начнем с колбэка, вызываемого в самом-самом начале, при инициализации USB-контроллера, чья функция сводится к сбросу USB-клока, вызова инициализирующих функций библиотеки и активизации подтяжки на линии USB, что заставляет хост увидеть нас на шине.void Device_init()
void Device_init() { DEVICE_INFO *pInfo = &Device_Info; pInfo->Current_Configuration = 0; _SetCNTR(CNTR_FRES); //Reset USB block _SetCNTR(0); //Deassert reset signal _SetISTR(0); //Clear pending interrupts USB_SIL_Init(); GPIO_ResetBits(GPIOC, GPIO_Pin_13); //Enable pull-up }
Следующий колбэк вызывется, когда хост затребует сброс нашего устройства:
void Device_Reset()
void Device_Reset() { //Set device as not configured pInformation->Current_Configuration = 0; pInformation->Current_Interface = 0; //the default Interface /* Current Feature initialization */ pInformation->Current_Feature = USB_ConfigDescriptor[7]; SetBTABLE(BTABLE_ADDRESS); /* Initialize Endpoint 0 */ SetEPType(ENDP0, EP_CONTROL); SetEPTxStatus(ENDP0, EP_TX_STALL); SetEPRxAddr(ENDP0, ENDP0_RXADDR); SetEPTxAddr(ENDP0, ENDP0_TXADDR); Clear_Status_Out(ENDP0); SetEPRxCount(ENDP0, Device_Property.MaxPacketSize); SetEPRxValid(ENDP0); SetEPType(ENDP1, EP_BULK); SetEPRxAddr(ENDP1, ENDP1_RXADDR); SetEPRxCount(ENDP1, 0x40); SetEPRxStatus(ENDP1, EP_RX_VALID); SetEPTxStatus(ENDP1, EP_TX_DIS); /* Set this device to response on default address */ SetDeviceAddress(0); }
Тут мы задаем все определенные заранее адреса и прочие параметры эндпоинтов, после чего выставляем устройству адрес 0, чтобы хост мог обратиться к нему и назначить новый.
Дальше идет пара колбэков, которые мы вообще не будем обрабатывать#define Device_Status_In NOP_Process #define Device_Status_Out NOP_Process
Честно говоря, я не нашел даже места, где они зовутся, видимо, их предполагается звать из нашего кода, если нужно.
Следующая пара колбэков предназначена для случая, когда мы хотим передать какую-нибудь информацию через Control Endpoint 0, не являющуюся частью стандартного протокола:RESULT Device_Data_Setup(uint8_t RequestNo) { return USB_UNSUPPORT; } RESULT Device_NoData_Setup(uint8_t RequestNo) { return USB_UNSUPPORT; }
Так как мы этого не делаем, то, в случае такого обращения, скажем хосту, что мы это не поддерживаем. В будущем сюда можно будет запихать какие-нибудь команды управления подсветкой, электропитанием или разрешением дисплея.
Дальше идет колбэк, который дернется, когда хост захочет переключить интерфейсы. Так как у нас интерфейс только один, говорим USB_UNSUPPORT при обращении по любым индексам кроме нуля.RESULT Device_Get_Interface_Setting(uint8_t Interface, uint8_t AlternateSetting) { if (AlternateSetting > 0) { return USB_UNSUPPORT; } else if (Interface > 0) { return USB_UNSUPPORT; } return USB_SUCCESS; }
Последние три колбэка предназначены для возвращения различных строковых дескрипторов и написаны в рекомендуемой инженерами STM манере, вызовом их библиотечной функции:
Функции для возврата строковых дескрипторов
uint8_t *Device_GetDeviceDescriptor(uint16_t Length) { return Standard_GetDescriptorData(Length, &Device_Descriptor); } uint8_t *Device_GetConfigDescriptor(uint16_t Length) { return Standard_GetDescriptorData(Length, &Config_Descriptor); } uint8_t *Device_GetStringDescriptor(uint16_t Length) { uint8_t wValue0 = pInformation->USBwValue0; if (wValue0 > 4) { return NULL; } else { return Standard_GetDescriptorData(Length, &String_Descriptor[wValue0]); } }
Теперь объединяем все, что получилось в структуру:
DEVICE_PROP Device_Property = { Device_init, Device_Reset, Device_Status_In, Device_Status_Out, Device_Data_Setup, Device_NoData_Setup, Device_Get_Interface_Setting, Device_GetDeviceDescriptor, Device_GetConfigDescriptor, Device_GetStringDescriptor, 0, 0x40 /*MAX PACKET SIZE*/ };
- Теперь опишем колбэки следующей структуры, User_Standard_Requests.
Это будет нетрудно — т.к. мы не являемся HID или еще каким-нибудь устройством, для которого есть строгий стандарт на подобные реквесты, мы можем безбоязненно определить их все как NOP_Process. При этом не стоит пугаться, что не определив User_SetDeviceAddress мы останемся без адреса – этот колбэк носит информационный характер и вызывается библиотекой уже после того, как адрес был установлен.Структура User_Standard_Requests
#define Device_GetConfiguration NOP_Process #define Device_SetConfiguration NOP_Process #define Device_GetInterface NOP_Process #define Device_SetInterface NOP_Process #define Device_GetStatus NOP_Process #define Device_ClearFeature NOP_Process #define Device_SetEndPointFeature NOP_Process #define Device_SetDeviceFeature NOP_Process #define Device_SetDeviceAddress NOP_Process USER_STANDARD_REQUESTS User_Standard_Requests = { Device_GetConfiguration, Device_SetConfiguration, Device_GetInterface, Device_SetInterface, Device_GetStatus, Device_ClearFeature, Device_SetEndPointFeature, Device_SetDeviceFeature, Device_SetDeviceAddress };
- Теперь опишем несколько глобальных переменных, требуемых библиотекой:
__IO uint16_t wIstr; DEVICE Device_Table = { EP_NUM, 1 };
Это глобальная переменная с текущими флагами прерывания, и глобальная структура с количеством эндпоинтов (включая нулевой) и количеством доступных конфигураций.
- Далее объявим пару вспомогательных функций – они не требуются библиотекой, мы просто вызовем их из главной:
void USB_Interrupts_Config(void) и void Set_USBClock()
void USB_Interrupts_Config(void) { NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); NVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); NVIC_InitStructure.NVIC_IRQChannel = USB_HP_CAN1_TX_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); } void Set_USBClock() { /* Select USBCLK source */ RCC_USBCLKConfig(RCC_USBCLKSource_PLLCLK_1Div5); /* Enable the USB clock */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_USB, ENABLE); }
Здесь мы конфигурируем два прерывания.
Low-Priority вызывается всеми усб событиями (успешными передачами, резетом и прочими), поэтому в нем необходимо проверять источник интеррапта.
Высокоприоритетное вызывается только при успешной передаче Isochronous либо Bulk эндпоинта с двойной буферизацией, предназначено для сокращения времени обработки (не приходится проверять источник). Так как у нас пока нет двойной буферизации, его можно не активировать.
Вторая функция устанавливает частоту тактового сигнала USB в 1.5 от системной (48 МГц) и включает тактирование блока USB. - Далее пишем код обработки высокоприоритетного и низкоприоритетного прерывания, который сводится к вызову библиотечного обработчика для высокоприоритетного и проверки источника, после чего вызова библиотечных обработчиков либо наших колбэков для низкоприоритетного.
Высокоприоритетное и низкоприоритетные прерывания
void USB_HP_CAN1_TX_IRQHandler(void) { CTR_HP(); } void USB_LP_CAN1_RX0_IRQHandler(void) { wIstr = _GetISTR(); #if (IMR_MSK & ISTR_CTR) if (wIstr & ISTR_CTR & wInterrupt_Mask) { CTR_LP(); } #endif #if (IMR_MSK & ISTR_RESET) if (wIstr & ISTR_RESET & wInterrupt_Mask) { _SetISTR((uint16_t)CLR_RESET); Device_Property.Reset(); } #endif }
Все, что нам осталось сделать в меине – настроить пин подтяжки USB на выход, вызвать инициализирующие функции для USB и инициализирующие функции для LCD, после чего зависнуть в бесконечном цикле. С этого момента основная работа будет происходить в колбэке, вызываемом при передаче по Bulk Endpoint 1 данных, который мы сейчас и напишем.
- Принцип работы простой – у нас будет два состояния, NOT_ADDRESSED и TRANSFER_IN_PROGRESS. Начинаем мы в первом, и в нем мы воспринимаем первые 8 байт пакета как заголовок, в котором записаны координаты X и Y (два 16-битных числа) а также количество пикселей, которое надо вывести, начиная с этих координат. Получив этот заголовок мы переходим во второе состояние и все пришедшие потом данные (включая конец первого пакета) выводим сразу на наш дисплей, пока не выведем столько пикселей, сколько указано в заголовке. Это, правда, чревато тем, что если вдруг что-то помешает передаче хотя бы одного пакета, то следующий пакет с заголовком будет воспринят как данные и на экран полетит мусор. Однако доставка гарантирована, так что подобная ситуация возможна только в случае проблем на шине. Чтобы ее избежать можно дополнительно ввести несколько байт сигнатуры в заголовок, но в моем коде этого в данный момент нет.
Единственное, что следует отметить при реализации – имеет смысл сделать свою функцию, аналогичную библиотечной PMAToUserBufferCopy, которая копирует байты из памяти буфера эндпоинта в оперативную память контроллера. Так как дисплей у нас теперь тоже видится как часть оперативной памяти, к чему нам дважды гонять туда-сюда байты?
Я взял код PMAToUserBufferCopy за основу, назвал функцию PMAToLCDBufferCopy, и просто поменял в ее коде инкрементирующийся указатель на целевой буфер на постоянный указатель на память дисплея:void PMAToLCDBufferCopy(uint16_t wPMABufAddr, uint16_t offset ,uint16_t wNBytes) { uint32_t n = (wNBytes + 1) >> 1; uint32_t i; uint32_t *pdwVal; pdwVal = (uint32_t *)(wPMABufAddr * 2 + PMAAddr+offset); for (i = n; i != 0; i--) LCD_WRITE_RAM(*pdwVal++); }
Сам колбэк выглядит так:
void EP1_OUT_Callback(void) { uint16_t dataLen = GetEPRxCount(EP1_OUT & 0x7F); uint16_t offset=0; if(GraphicsState==NOT_ADDRESSED) { if(dataLen<=8) { SetEPRxStatus(ENDP1, EP_RX_VALID); return; } PMAToUserBufferCopy(buffer, GetEPRxAddr(EP1_OUT & 0x7F), 8); uint16_t horz = *((uint16_t*)(buffer)); uint16_t vert = *(uint16_t*)(buffer+2); dataTotal = *(uint32_t*)(buffer+4); LCD_WRITE_REGISTER(CTR_HORZ_ADDRESS,vert); //экран повернут LCD_WRITE_REGISTER(CTR_VERT_ADDRESS,horz); offset=16; dataTransfered=0x00; GraphicsState=TRANSFER_IN_PROGRESS; dataLen-=8; } LCD_BEGIN_RAM_WRITE; PMAToLCDBufferCopy(GetEPRxAddr(EP1_OUT & 0x7F), offset, dataLen); dataTransfered+=(dataLen)>>1; if(dataTransfered>=dataTotal) GraphicsState=NOT_ADDRESSED; SetEPRxStatus(ENDP1, EP_RX_VALID); }
Все, осталось записать колбэк в глобальную структуру, которую требует библиотека, установив колбэки от других эндпоинтов (которых у нас нет) в NOP_Process:
Структуры с колбэками для эндпоинтов
void (*pEpInt_IN[7])(void) = { NOP_Process, NOP_Process, NOP_Process, NOP_Process, NOP_Process, NOP_Process, NOP_Process, }; void (*pEpInt_OUT[7])(void) = { EP1_OUT_Callback, NOP_Process, NOP_Process, NOP_Process, NOP_Process, NOP_Process, NOP_Process, };
Проверка
Вот и пришла самая приятная пора – пожинание результатов первого этапа.
Проверку будем осуществлять из-под windows (хотя и из-под линукса проблем не будет), пока используюя юзерспейсную библиотеку LibUSB.
Для этого я воспользовался ее биндингом для C#, LibUSB.Net, скачать который можно отсюда
Подключаем наш девайс к компьютеру – если все хорошо, система должна сообщить, что устройство работает, но не найдены драйверы, и отобразить его под заданным нами именем в диспетчере задач.
В принципе, можно даже не писать код. Просто качаем эту библиотеку, в папке с ней запускаем InfWizard. Выбираем в списке наше устройство и генерируем для него inf-файл, после чего устанавливаем через него драйвер от libusb.
Запускаем идущую в комплекте Test_bulk, выбираем наше устройство, жмем Open, и вводим в строку больше 8 символов. После того, как мы нажмем «Write», они должны прийти в наш колбэк и быть интерпретированы как заголовок и графические данные, после чего отобразиться на дисплее в виде нескольких цветных точек.
Разумеется, это не очень впечатляет, поэтому открываем исходники этого самого Test_Bulk, идем в обработчик кнопки Write и, вместо отправления данных из строки, делаем загрузку из бинарного файла.
var bytesToWrite = File.ReadAllBytes("D:\\myfile.raw");
Тут нам больше ничего не нужно, пойдем готовить файл. Выбираем подходящую картинку размером 320х240, я выбрал вот эту:
Поворачиваем ее набок, потом вспоминаем, что пиксели в BMP хранятся снизу вверх, поэтому отражаем картинку по вертикали, чтобы не заниматься этим в коде. Сохраняем в формате 16-бит RGB565.
Отрезаем первые 0х40 байт (заголовок) от файла в каком-нибудь хекс-редакторе, остальное – сырой битмэп, который можно скармливать нашей видеокарте. Дополняем его заголовком для USB-девайса – адресом вывода (0000, 0000) и длиной данных в пикселях (320х240) — 002C0100
Все, сохраняем его под тем именем, которое указали в Test_bulk, запускаем эту прогу, жмем Write и получаем
На этом у меня все. Статья получилась длинная, но разбивать ее на более мелкие не хотелось, так как тогда потерялась бы целостность этапа. Надеюсь, вы осилили ее до конца и готовы перейти к этапу, который будет изложен в следующей статье – мы наконец возьмемся за роутер и напишем небольшой драйвер, позволяющий работать с нашей видеокартой из-под OpenWRT.
До следующей статьи!
Добрый день, уважаемыее. Вот мы и подошли к самой интересной и важной части моего цикла статей про превращение небольшого роутера в миникомпьютер — сейчас мы с вами будем разрабатывать настоящий драйвер фреймбуфера, который позволит запустить на роутере разные графические приложения. Чтобы энтузиазм не угасал, вот видео одного из таких приложений — думаю, большинство узнают это великолепный старый квест:
На случай, если вы пропустили предыдущие части — вот ссылки:
1 — Миникомпьютер из роутера с OpenWRT: разрабатываем USB-видеокарту
2 — Миникомпьютер из роутера с OpenWRT: пишем USB class-driver под Linux
Итак, приступаем к работе.
Введение
Железо у нас опять не менялось (хотя кое-что в прошивке мы обязательно поменяем в следующей статье), поэтому начнем с обзора того, что нам предстоит сделать.
Несмотря на то, что наш драйвер из прошлой статьи уже умеет выводить графику, использовать его в полной мере (например, вывести консоль или запустить графическое приложение) весьма затруднительно. Дело в том, что подобные приложения требуют вполне определенного, стандартизованного интерфейса, так называемого фреймбуфера. При регистрации фреймбуфера в системе нам необходимо будет заполнить несколько специализированных структур-дескрипторов, на этот раз куда более специфических, чем просто абстрактные «файловые операции», которые мы заполняли в прошлый раз. Операции там тоже будут, но кроме них будут специальные колбэки, такие, как fb_imageblit (вызывается, когда кто-то хочет перенести блок бит с изображением в определенное место экрана), fb_copyarea (похожий перенос, но блок бит берется не из внешнего буфера а из видеопамяти) и т.п. Кроме того, будут структуры с описанием разрешения экрана, битности цвета и того, как в этой «битности» расположены цветовые компоненты.
Первое, что нужно осознать: у нас несколько нестандартная ситуация, если сравнивать с видеокартами PC (хотя для эмбеддед устройств вполне себе обычная) — наше устройство не имеет как таковой видеопамяти, к которой мы могли бы обращаться — точнее, память-то оно имеет, ту самую GRAM, запрятанную в недрах дисплея, но доступ у нас к ней только через «окошко» в 16 бит шириной. Памяти на борту тоже не настолько много, чтобы хранить там весь буфер кадра.
К счастью, в линуксе для этого предусмотрен специальный подход — для нас уже написаны функции с префиксом «sys_«, например, «sys_imageblit«, которые реализуют необходимую фреймбуферу функциональность, работая с областью оперативной памяти системы как с буфером кадра.
То есть, если буфер кадра у нас размещен в видеокарте и у нас есть аппаратная поддержка подобных операций, мы в колбэках просто пинаем нашу железку, отдавая команду «выполнить перенос блока бит» или «скопировать область видеопамяти».
Если же у нас ничего этого нет, мы выделяем в ядре память, размером равную нашему буферу кадра, и в колбэках вызываем эти самые функции с префиксом «sys_«, которые выполняют необходимые операции в RAM.
Таким образом, можно получить полностью работающий фреймбуфер, который вообще не будет взаимодействовать с железом — такой драйвер уже есть и он называется vfb, virtual framebuffer. Его исходный код лежит в drivers/video/vfb.c.
Если к такому драйверу добавить периодическую посылку данных на реальное устройство мы получим уже настоящий драйвер фреймбуфера. Но перед тем, как мы займемся этим, давайте немного понастраиваем нашу систему и потренируемся на виртуальном драйвере, vfb.
Включаем поддержку графики в ядре
С этой частью я провозился довольно долго, в основном, по той причине, что я сначала написал свой драйвер, а потом пытался понять, почему у меня только черный экран — грешил на свои ошибки в коде. Потом догадался поставить вместо него драйвер VFB, вычитав содержимое памяти которого увидел тот же черный экран. Тогда я, наконец, понял, что дело не в драйвере, а в том, что ядро само по себе отказывается выводить на него информацию, после чего проблема решилась довольно быстро. Но обо всем по-порядку.
- Для того, чтобы мы увидели консольный вывод в памяти фреймбуфера (ну и на экране, если он настоящий, а не виртуальный) необходимы два драйвера — это, собственно, драйвер самого фреймбуфера, который создаст устройство /dev/fb[x] и драйвер консоли, работающий поверх него — это драйвер fbcon
- В ядре, соответственно, должна быть включена поддержка фреймбуферов, поддержка виртуальных терминалов (абстракция, объединяющая в себе устройство вывода+устройство ввода, дисплей и клавиатуру), поддержка отображения системной консоли на таких терминалах (да, это тоже можно отключить, тогда системная консоль будет выводиться только на физически существующие символьные устройства вроде ком-портов), сам драйвер fbcon, а также какой нибудь из доступных вбилденных в него шрифтов.
- Тот самый пункт, который я в начале упустил, когда не мог понять, почему ничего не выводится — нужно сообщить ядру, что необходимо выводить содержимое системной консоли на тот /dev/tty[x], который сцапал fbcon!
Дело в том, что драйвер fbcon пытается захватить первый доступный /dev/tty[x], например, tty0. Но ядро ничего туда не выводит, это ни к чему не привязанная абстракция, т.к. на нем не запущено ни приложение, позволяющие логиниться в системе, ни вывод системной консоли.
Для того чтобы решить эту проблему мы должны во-первых сказать ядру, что мы хотим видеть системную консоль на /dev/tty0 (впрочем, это опционально, если вдруг кто-то не хочет видеть процесс загрузки и системный вывод, то этот пункт можно опустить), а во-вторых сообщить иниту, что там нужно запустить софт для логина
Сейчас мы проделаем все три пункта относительно драйвера виртуального фреймбуфера, и, когда получим картинку в памяти, перейдем к написанию своего. fbcon и драйвер фреймбуфера могут быть сбилдены либо оба статически, либо оба в виде подключаемых модулей, либо кто-нибудь один статически, второй динамически — проблем это не вызовет, fbcon сцапает фреймбуфер сразу, как только его увидит.
Правда, при работе с vfb есть одна тонкость — чтобы его активировать, необходимо передать модулю параметр vfb_enable=1, либо запустить ядро с параметром «video=vfb«. С модулем работать будет проще, поэтому ограничимся им. fbcon же вбилдим в ядро.
- Выполняем make kernel_menuconfig и заходим в пункт Device Drivers
- Включаем Graphics Support — Support for frame buffer devices, после чего нам становится доступен список самих драйверов, в котором выбираем Virtual framebuffer.
- Возвращаемся уровнем выше, идем в Character devices и включаем там
Virtual terminal
Enable character translations in console
Support for console on virtual terminal
Support for binding and unbinding console driversБлагодаря последним двум опциям мы сможем вывести на виртуальный терминал системную консоль, а потом, при желании, отбиндить ее от драйвера фреймбуфера, что позволит выгрузить его из памяти.
- Возвращаемся к Graphics Support, идем в ставшее доступным меню Console display driver support и включаем там Framebuffer Console support, после чего активируем пункт Select compiled-in fonts и выбираем там какой-нибудь шрифт — допустим, VGA 8×8 font
- Выходим в основное меню и обращаем внимание на пункт Kernel Hacking — если туда зайти, то можно обнаружить ближе к концу списка пункт, содержащий параметры загрузки ядра. Вообще, их передает ядру бут-лодер, но можно дополнить строку параметров при помощи этого пункта, либо вовсе переопределить ее. Переопределять не будем, а вот дополнить — дополним, т.к. бутлодер передает в нее параметр console=ttyATH0, что означает вывод системной консоли на последовательный порт. К сожалению, прямо тут мы это сделать не можем — данный параметр будет переопределен при применении платформ-специфик патчей, поэтому туда и отправимся. Не трогаем тут ничего, сохраняем конфиг и выходим.
- Идем туда, где, как мы помним, хранятся платформ-специфик файлы и патчи — target/linux/ar71xx/. Заходим в generic и открываем файл config-default. В нем мы видим единственную строку, тот самый параметр, который видели в настройке ядра:
CONFIG_CMDLINE=«rootfstype=squashfs,jffs2 noinitrd»
дописываем в конец console=tty0 и fbcon=font:<имя шрифта>, в качестве имени шрифта задав одно из тех, что выбрали в настройке ядра. Получаем что-то вроде
CONFIG_CMDLINE=«rootfstype=squashfs,jffs2 noinitrd console=tty0 fbcon=font:ProFont6x11»
Последнее, что нам нужно сделать перед пересборкой — зайти в make menuconfig и включить в предоставляемые busybox возможности утилиту fbset, которая позволит задать параметры нашего фреймбуфера. Она находится в меню Base System — Busybox — Linux SyStem Utilities
Теперь можно пересобирать ядро. В build_dir/target-mips_r2_uClibc-0.9.33.2/linux-ar71xx_generic/linux-3.6.9/dri
vers/video/
забираем то, что с расширением .ko
Вопреки ожиданиям, он там будет не один, включение драйвера, использующего функции с префиксом «sys_» активирует сборку нескольких модулей, в которых эти самые функции лежат. Что интересно, в принципе, ничего не мешает вбилдить их статически в ядро, оставив драйвер подключаемым модулем, однако из меню мне этого сделать не удалось, пришлось писать соответствующий патч к Kconfig-файлу. Но это мы сделаем позже, а сейчас просто перешьем роутер новой прошивкой и перекинем на него все модули.
После заходим по SSH на роутер и идем /etc. Открываем файл inittab и видим там что-то вроде этого:
::sysinit:/etc/init.d/rcS S boot
::shutdown:/etc/init.d/rcS K shutdown
ttyATH0::askfirst:/bin/ash --login
В последней строке как раз и сказано, что нужно запустить софт для логина (в данном случае, как и все системные бинарники — часть busybox’а) на ttyATH0, последовательном порту. При этом указано (askfirst) что для активации этой консоли нужно будет сначала нажать enter.
Добавляем еще одну строчку:
tty0::respawn:/bin/ash --login
Посмотрим, правильно ли указаны параметры ядра через
cat /proc/cmdline
и перезагрузим роутер.
Теперь по очереди инсмодим все, кроме драйвера vfb, а в самом конце пишем
insmod vfb.ko vfb_enable=1
После этого мы в dmesg должны увидеть слова наподобие этих: Console: switching to colour frame buffer device 53×21
Размеры консоли будут отличаться в зависимости от выбранного шрифта. Установим фреймбуферу параметры, более похожие на параметры нашего дисплея:
fbset -g 320 240 320 240 16
Это установит видимое и виртуальное разрешение в 320х240 (чаще всего они совпадают, но в принципе, можно задать виртуальное разрешение больше видимого, получив буфер кадра больше выводимого и использовать это для двойной буферизации), а глубину цвета — в 16 бит.
fbcon должен отреагировать на это сменой своего разрешения и сообщением в dmesg, но если этого вдруг не произошло, отключим консоль от фреймбуфера и подключим заново:
echo "0" > /sys/class/vtconsole/vtcon1/bind
echo "1" > /sys/class/vtconsole/vtcon1/bind
Это полезная пара команд, которая нам ни раз пригодится — без этого не выгрузить драйвер фреймбуфера, т.к. он будет занят консолью.
Очень не помешает подключить к роутеру клавиатуру — можно вслепую ввести команды clear и dmesg чтобы быть уверенным, что на виртуальном дисплее что-то есть.
После получаем «скриншот» командой
cat /dev/fb0 > scrn.raw
И скачиваем его на десктоп. Там открываем через GIMP или любой другой софт, который сможет загрузить сырые графические данные в формате RGB565 — задаем размеры изображения 320×240, не забываем о битности, и получаем картинку вроде этой (сообщения об открытии и закрытии /dev/fb0 выдает мой драйвер, т.к. скриншот я снимал не с виртуального фреймбуфера. Виртуальный о таких делах молчит):
Обратили внимание на красивый, «хакерский» зеленый цвет консоли? На самом деле это говорит нам об ошибке, точнее — об одной особенности, с которой нужно считаться. Но об этом мы поговорим позже. Перед тем, как перейти к главному действу — написанию своего драйвера фреймбуфера — давайте сравним доступные консольные шрифты. Для этого я подготовил шесть фото, по три на консоль и на Midnight Commander со шрифтами 4×4, 6×11 и 8×8. На мой взгляд, самый удобный — это 6х11:
4×4
6×11
8×8
4×4
6×11
8×8
Пишем драйвер фреймбуфера
Для начала — о подходе. Очевидным и не очень хорошим решением стало бы периодическое обновление всего экрана — можно было бы завести таймер, который бы кидал все содержимое фреймбуфера по USB уже знакомыми нам из прошлой статьи командами. Однако есть куда более правильное решение, так называемое deferred io.
Суть его проста: нам нужно только указать функцию-колбэк, задать интервал времени и зарегистрировать это самое deferred io для нашего фреймбуфера. При регистрации виртуальная память будет настроена так, что обращение к памяти буфера кадра вызовет исключение, которое поставит наш колбэк в очередь на обработку через заданный нами интервал. При этом операция записи в память не прервется! А когда вызовется колбэк, в него будет передан список страниц, которые были изменены. Не правда ли, очень удобно? Юзерспейс может спокойно писать в видеопамять не задумываясь ни о чем и не прерываясь, при этом периодически будет дергаться наш колбэк со списком страниц памяти, которые были изменены — нам нужно будет кидать на устройство только их, а не весь буфер.
Так как освобождение фреймбуфера — задача не такая простая, как кажется на первый взгляд (USB-устройство может уже отсутствовать, но сам фреймбуфер клиенты еще не отпустят и чистить память в этот момент никак нельзя), мы поступим не очень хорошо — напишем себе жирнейшим шрифтом TODO и клятвенно пообещаем реализовать правильную очистку и отключение устройства чуть-чуть позже, а пока напишем все остальное, чтобы наконец, увидеть плод своих действий. Нормальную очистку вместе с допиливанием прошивки видеокарты (что поднимет FPS минимум в два раза) мы обязательно рассмотрим в следующей статье.
Начнем с простого — так как нам будет приходить список страниц, в которые была произведена запись, проще всего заранее сохранить координаты на экране, соответствующие началу страницы а также указатель на соответствующую ей область памяти. Поэтому создадим структуру с этими полями, не забыв добавить атомарный флаг, показывающий, требуется ли данной странице апдейт. Это необходимо, т.к. внутренние операции фреймбуфера, те, что выполняются через функции с префиксом «sys_» не вызывают наш хендлер deferred io, поэтому нам нужно будет вручную посчитать, к каким страницам было обращение и пометить их как подлежащие апдейту.
Структура страницы памяти
struct videopage
{
int x, y;
unsigned char *mem;
unsigned long length;
atomic_t toUpdate;
};
Здесь все прозрачно, единственное что — храним длину, т.к. последняя страница может быть неполной — нам ни к чему слать дисплею лишние данные.
Объявим несколько дефайнов, связанных с размерами дисплея — количество пикселей в странице, количество страниц во фреймбуфере и т.п.
PAGE_SIZE задефайнена за нас, в исходниках ядра.
Вспомогательные дефайны
#define WIDTH 320
#define HEIGHT 240
#define BYTE_DEPTH 2
#define FB_SIZE WIDTH*HEIGHT*BYTE_DEPTH
#define FP_PAGE_COUNT PAGE_ALIGN(FB_SIZE)/PAGE_SIZE
#define PIXELS_IN_PAGE PAGE_SIZE/BYTE_DEPTH
Объявим требуемые колбэки операций с фреймбуфером и колбэк-хендлер нашего deferred io.
Колбэки
static void display_fillrect(struct fb_info *p, const struct fb_fillrect *rect);
static void display_imageblit(struct fb_info *p, const struct fb_image *image);
static void display_copyarea(struct fb_info *p, const struct fb_copyarea *area);
static ssize_t display_write(struct fb_info *p, const char __user *buf,
size_t count, loff_t *ppos);
static int display_setcolreg(unsigned regno,
unsigned red, unsigned green, unsigned blue,
unsigned transp, struct fb_info *info);
//---------------
static void display_update(struct fb_info *info, struct list_head *pagelist);
Далее идет структура с неизменяемой информацией о дисплее.
Структура fb_fix_screeninfo
static struct fb_fix_screeninfo fixed_info =
{
.id = "STM32LCD",
.type = FB_TYPE_PACKED_PIXELS,
.visual = FB_VISUAL_TRUECOLOR,
.accel = FB_ACCEL_NONE,
.line_length = WIDTH * BYTE_DEPTH,
};
Тоже все просто — задаем строковый ID нашего дисплея, тип пикселей — другие нас не интересуют, у нас самый обычный битмэп, цвет у нас truecolor, во всяком случае близко к нему и уж точно не монохромный и не direct color.
Далее идет более интересная структура с (потенциально) переменной информацией. Но так как возможность смены разрешения мы реализовывать не будем, для нас она будет такой же постоянной как и в прошлой структуре.
Структура fb_var_screeninfo
static struct fb_var_screeninfo var_info =
{
.xres = WIDTH,
.yres = HEIGHT,
.xres_virtual = WIDTH,
.yres_virtual = HEIGHT,
.width = WIDTH,
.height = HEIGHT,
.bits_per_pixel = 16,
.red = {11, 5, 0},
.green = {5, 6, 0},
.blue = {0, 5, 0},
.activate = FB_ACTIVATE_NOW,
.vmode = FB_VMODE_NONINTERLACED,
};
Тут мы видим уже знакомые по fbset видимое и виртуальное разрешения, физические размеры дисплея в миллиметрах (можно задать, в принципе, любыми, я задал такими же, как и его разрешение), битность изображения, и — важные структурки — описатели того, как цветовые компоненты расположены в байтах. В них первым значением идет смещение, вторым длина в битах и третьим — флаг, «значащий бит справа».
Последней объявляем структуру отложенного ввода-вывода, deferred io.
Структура fb_deferred_io
static struct fb_deferred_io display_defio = {
.delay = HZ/6,
.deferred_io = &display_update,
};
Выбор значения периода, поля .delay, задача, большей частью, эмпирическая — если выбрать слишком маленькое значение, обновление будет происходить реже, чем позволяет аппаратура, если выбрать слишком большое — будет забиваться очередь отложенных работ. Наш дисплей в данный момент более чем тихоходный, причем это определяется полностью USB, а не выводом на экран. В реализации из первой статьи полная перерисовка экрана возможно с частотой не выше 3.6 FPS. Но не стоит отчаиваться — во-первых, мы не всегда будем перерисовывать весь экран, а во-вторых, уже в следующей статье, я покажу как выжать максимум из имеющегося у нас железа, так что FPS подскочит до ~8 — с учетом неполной перерисовки мы получим вполне юзабельное устройство, как на видео в начале статьи. Эти 9 FPS, кстати, являются физическим пределом, который мы можем получить на Full Speed USB при передаче сырых видеоданных (без сжатия). Это становится очевидным, если мы вспомним предел скорости передачи FS USB — 12 МБит/с. Наш кадр занимает 320*240*16 = 1 228 800 бит. Таким образом идеальная, сферическая в вакууме частота кадров будет не выше 9.8 FPS. Выбросим отсюда наши заголовки, потери в нашем драйвере, потери в драйвере контроллера хоста, потери в драйвере на STMке — и получим реальный предел в 8-9 ФПС, достигнуть который вполне неплохо. Но это мы сделаем в следующей статье, а сейчас помним, что наша частота около 3.5 FPS, и, по моим замерам, период примерно в два раза больший, то есть 6-7 герц, оказался оптимальным. Такой и задаем, пользуясь заранее задефайненным в исходниках ядра HZ. Кстати, стоит обратить внимание — несмотря на название, этот макрос определяет не частоту в 1 Гц, а соответствующий период (в квантах ядра), поэтому, для получения частоты в 6 Гц его следует не умножать, а делить на шесть.
Наконец заполняем структуру с колбэками операций над фреймбуфером.
Структура fb_ops
static struct fb_ops display_fbops = {
.owner = THIS_MODULE,
.fb_read = fb_sys_read,
.fb_write = display_write,
.fb_fillrect = display_fillrect,
.fb_copyarea = display_copyarea,
.fb_imageblit = display_imageblit,
.fb_setcolreg = display_setcolreg,
};
На чтение сразу запишем колбэк из тех, что «sys_«, нам там делать нечего. Во всех остальных нам нужно будет еще пометить соответствующие страницы на обновление, так что указываем свои.
Структура-дескриптор нашего девайса почти не изменится со времен предыдущей статьи, опишем ее.
Структура-дескриптор usblcd
struct usblcd
{
struct usb_device *udev;
struct usb_interface *interface;
struct device *gdev;
struct fb_info *info;
struct usb_endpoint_descriptor *bulk_out_ep;
unsigned int bulk_out_packet_size;
struct videopage videopages[FP_PAGE_COUNT];
unsigned long pseudo_palette[17];
};
В нее добавился массив наших описателей страниц видеопамяти. Вообще-то, статически размещать большие объемы данных в ядре не рекомендуется, но сама структура у нас вышла небольшая, да и по количеству их всего 38, поэтому, чтобы не морочиться с лишними указателями оставим этот массив статическим, 760 байт или около того ядро как-нибудь осилит.
Последнее поле, pseudo_palette — место под псевдо-палитру, которую требует fbcon. Заполняется она в колбэке .fb_setcolreg, без которого fbcon отказывается работать. Во всех дровах, что я видел, этот колбэк выглядит копипастнутым из файла-примера фреймбуфера из исходников ядра, поэтому мы тоже не будем изобретать велосипед, тем более что, кроме fbcon этим, похоже, никто и не пользуется. С него и начнем.
Колбэк display_setcolreg
#define CNVT_TOHW(val,width) ((((val)<<(width))+0x7FFF-(val))>>16)
static int display_setcolreg(unsigned regno,
unsigned red, unsigned green, unsigned blue,
unsigned transp, struct fb_info *info)
{
int ret = 1;
if (info->var.grayscale)
red = green = blue = (19595 * red + 38470 * green +
7471 * blue) >> 16;
switch (info->fix.visual) {
case FB_VISUAL_TRUECOLOR:
if (regno < 16) {
u32 *pal = info->pseudo_palette;
u32 value;
red = CNVT_TOHW(red, info->var.red.length);
green = CNVT_TOHW(green, info->var.green.length);
blue = CNVT_TOHW(blue, info->var.blue.length);
transp = CNVT_TOHW(transp, info->var.transp.length);
value = (red << info->var.red.offset) |
(green << info->var.green.offset) |
(blue << info->var.blue.offset) |
(transp << info->var.transp.offset);
pal[regno] = value;
ret = 0;
}
break;
case FB_VISUAL_STATIC_PSEUDOCOLOR:
case FB_VISUAL_PSEUDOCOLOR:
break;
}
return ret;
}
Это, как я уже сказал, стандартный код для такого колбэка, включая макрос CNVT_TOHW, который используется для получения значений цветовых компонентов. Его также таскают из драйвера в драйвер — не совсем понятно, почему его в итоге не внесут в основной заголовочный файл fb.h.
Задача данного колбэка — заполнить 16-цветную псевдопалитру, к которой будет обращаться уже упомянутый драйвер консоли.
Теперь объявим небольшую функцию, которой мы будем передавать, по сути, прямоугольник, в области которого было произведено воздействие на видеопамять. Функция вычислит, какие страницы видеопамяти затронуты этим прямоугольником, поставит им флаг «требуется апдейт» и запланирует выполнение того же колбэка, который зовется в случае deferred io. После этого все колбэки операций сведутся к вызову функций «sys_» и написанной нами функции, которую мы обзовем touch, по аналогии с командой в Linux.
Функция display_touch
static void display_touch(struct fb_info *info, int x, int y, int w, int h)
{
int firstPage;
int lastPage;
int i;
struct usblcd *dev=info->par;
firstPage=((y*WIDTH)+x)*BYTE_DEPTH/PAGE_SIZE-1;
lastPage=(((y+h)*WIDTH)+x+w)*BYTE_DEPTH/PAGE_SIZE+1;
if(firstPage<0)
firstPage=0;
if(lastPage>FP_PAGE_COUNT)
lastPage=FP_PAGE_COUNT;
for(i=firstPage;i<lastPage;i++)
atomic_dec(&dev->videopages[i].toUpdate);
schedule_delayed_work(&info->deferred_work, info->fbdefio->delay);
}
Код, я думаю, вполне понятен — просто вычисляем какие страницы мы затронули исходя из разрешения, округляем всегда в большую сторону, а точнее — просто берем с запасом, по одной странице с обоих концов.
Теперь опишем все остальные колбэки — они становятся очень простыми:
Все колбэки операций
static void display_fillrect(struct fb_info *p, const struct fb_fillrect *rect)
{
sys_fillrect(p, rect);
display_touch(p, rect->dx, rect->dy, rect->width, rect->height);
}
static void display_imageblit(struct fb_info *p, const struct fb_image *image)
{
sys_imageblit(p, image);
display_touch(p, image->dx, image->dy, image->width, image->height);
}
static void display_copyarea(struct fb_info *p, const struct fb_copyarea *area)
{
sys_copyarea(p, area);
display_touch(p, area->dx, area->dy, area->width, area->height);
}
static ssize_t display_write(struct fb_info *p, const char __user *buf,
size_t count, loff_t *ppos)
{
int retval;
retval=fb_sys_write(p, buf, count, ppos);
display_touch(p, 0, 0, p->var.xres, p->var.yres);
return retval;
}
Теперь опишем, наконец, наш колбэк от deferred io, который будет слать информацию на дисплей. Он во многом будет совпадать с колбэком .write из прошлой статьи. Будем писать в дисплей по страницам, не забывая приписывать, как и в прошлой статье, к ним требуемый заголовок. Благодаря нашей структуре videopage координаты x, y и длины уже посчитаны, так что все что нужно — просто запихнуть это в буфер и кинуть по USB.
Колбэк отложенного ввода-вывода
unsigned char videobuffer[PAGE_SIZE+8];
static void display_update(struct fb_info *info, struct list_head *pagelist)
{
struct usblcd* dev = info->par;
int retval;
struct page *page;
int i;
int usbSent=0;
list_for_each_entry(page, pagelist, lru)
{
atomic_dec(&dev->videopages[page->index].toUpdate);
}
for (i=0; i<FP_PAGE_COUNT; i++)
{
if(atomic_inc_and_test(&dev->videopages[i].toUpdate))
atomic_dec(&dev->videopages[i].toUpdate);
else
{
*(unsigned short*)(videobuffer)=cpu_to_le16(dev->videopages[i].x);
*(unsigned short*)(videobuffer+2)=cpu_to_le16(dev->videopages[i].y);
*(unsigned long*)(videobuffer+4)=cpu_to_le32(dev->videopages[i].length>>1);
memcpy(videobuffer+8,dev->videopages[i].mem,dev->videopages[i].length);
retval = usb_bulk_msg(dev->udev,
usb_sndbulkpipe(dev->udev, 1),videobuffer,
dev->videopages[i].length+8,
&usbSent, HZ);
if (retval)
printk(KERN_INFO "usblcd: sending error!n");
}
}
}
Так как в этот колбэк мы можем попасть честным путем(через deferrd io), а можем — по собственному почину (запланировав его выполнение в одном из колбэков операций, вызвав display_touch), мы просто пробежимся по всем переданным нам страницам, если таковые имеются, и пометим их как подлежащие апдейту.
Если мы попали сюда не через отложенный ввод-вывод, то список просто будет пустым.
После этого мы просто проходимся по всем страницам атомарно проверяя необходимость апдейта и выполняя этот самый апдейт путем синхронной посылки по USB. В следующей статье, когда будем допиливать драйвер до нормального состояния, мы заменим синхронную посылку более правильным механизмом, именуемым USB Request Block, или URB. Он позволит кинуть USB-хосту запрос на отправку данных и сразу же вернуться к дальнейшей обработке. А о том, что URB долетел (или не долетел) до получателя нам сообщат в прерывании. Это позволит выжать еще чуть-чуть FPS из нашей системы (не превышая, однако, теоретического предела, о котором я говорил выше).
Нам осталось совсем чуть-чуть — раз уж мы решили поступить плохо и не чистить за собой, то осталось только инициализировать все в колбэке Probe.
Колбэк Probe
static int LCDProbe(struct usb_interface *interface, const struct usb_device_id *id)
{
struct usblcd *dev;
struct usb_host_interface *iface_desc;
struct usb_endpoint_descriptor *endpoint;
unsigned char *videomemory;
int retval = -ENODEV;
int i;
dev_info(&interface->dev, "USB STM32-based LCD module connected");
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
if (!dev)
{
dev_err(&interface->dev, "Can not allocate memory for device descriptorn");
retval = -ENOMEM;
goto exit;
}
dev->udev=interface_to_usbdev(interface);
dev->interface = interface;
iface_desc = interface->cur_altsetting;
for (i = 0; i < iface_desc->desc.bNumEndpoints; ++i)
{
endpoint = &iface_desc->endpoint[i].desc;
if(usb_endpoint_is_bulk_out(endpoint))
{
dev->bulk_out_ep=endpoint;
dev->bulk_out_packet_size = le16_to_cpu(endpoint->wMaxPacketSize);
break;
}
}
if(!dev->bulk_out_ep)
{
dev_err(&interface->dev, "Can not find bulk-out endpoint!n");
retval = -EIO;
goto error_dev;
}
dev->gdev = &dev->udev->dev;
dev->info = framebuffer_alloc(0, dev->gdev);
dev->info->par = dev;
dev->info->dev = dev->gdev;
if (!dev->info)
{
dev_err(&interface->dev, "Can not allocate memory for fb_info structuren");
retval = -ENOMEM;
goto error_dev;
}
dev->info->fix = fixed_info;
dev->info->var = var_info;
dev->info->fix.smem_len=FP_PAGE_COUNT*PAGE_SIZE;
dev_info(&interface->dev, "Allocating framebuffer: %d bytes [%lu pages]n",dev->info->fix.smem_len,FP_PAGE_COUNT);
videomemory=vmalloc(dev->info->fix.smem_len);
if (!videomemory)
{
dev_err(&interface->dev, "Can not allocate memory for framebuffern");
retval = -ENOMEM;
goto error_dev;
}
dev->info->fix.smem_start =(unsigned long)(videomemory);
dev->info->fbops = &display_fbops;
dev->info->flags = FBINFO_FLAG_DEFAULT|FBINFO_VIRTFB;
dev->info->screen_base = videomemory;
memset((void *)dev->info->fix.smem_start, 0, dev->info->fix.smem_len);
for(i=0;i<FP_PAGE_COUNT;i++)
{
dev->videopages[i].mem=(void *)(dev->info->fix.smem_start+PAGE_SIZE*i);
dev->videopages[i].length=PAGE_SIZE;
atomic_set(&dev->videopages[i].toUpdate,-1);
dev->videopages[i].y=(((unsigned long)(PAGE_SIZE*i)>>1)/WIDTH);
dev->videopages[i].x=((unsigned long)(PAGE_SIZE*i)>>1)-dev->videopages[i].y*WIDTH;
}
dev->videopages[FP_PAGE_COUNT-1].length=FB_SIZE-(FP_PAGE_COUNT-1)*PAGE_SIZE;
dev->info->pseudo_palette = &dev->pseudo_palette;
dev->info->fbdefio=&display_defio;
fb_deferred_io_init(dev->info);
dev_info(&interface->dev, "info.fix.smem_start=%luninfo.fix.smem_len=%dninfo.screen_size=%lun",dev->info->fix.smem_start,dev->info->fix.smem_len,dev->info->screen_size);
usb_set_intfdata(interface, dev);
retval = register_framebuffer(dev->info);
if (retval < 0) {
dev_err(dev->gdev,"Unable to register_frambuffern");
goto error_buff;
}
return 0;
error_buff:
vfree(videomemory);
error_dev:
kfree(dev);
exit:
return retval;
}
Здесь мы сначала выделяем память под наш дескриптор устройства, потом под структуру описателя фреймбуфера, и только потом — под сам фреймбуфер. Обратите внимание — выделяем через vmalloc. Отличие от kmalloc состоит в том, что vmalloc может перенастроить таблицы страниц виртуальной памяти, и «собрать по кусочками» запрошенный нами буфер. То есть для нас он будет выглядеть единым блоком памяти, но физически он может состоять из страниц даже близко не находящихся друг с другом. kmalloc же возвращает память, которая и физически является единым блоком. Так как мы запрашиваем достаточно большой кусок, хорошей практикой будет воспользоваться vmalloc.
Все, компиляем, инсмодим, и если все сделано правильно лицезреем консоль на дисплее!
Заключение
В этой статье мы сделали, наконец, этот важный шаг — от драйвера кастомного устройства, никак не используемого системой, перешли в объятия фреймворка, который позволил рассказать всем приложениям и драйверам о том, что у нас есть настоящий дисплей. Да, мы поступили немного нехорошо, не очищая за собой ресурсы и никак не хендля отключение устройства, поэтому выдергивать USB и перевтыкать его на работающем девайсе пока не рекомендуется.
Но это мы обязательно исправим в следующей статье. Что нам предстоит сделать?
- Реализовать поддержку двойной буферизации на устройстве, обеспечив повышение FPS раза в два.
- Исправить наш недочет, из-за которого консоль зеленая (если мы запустим что-нибудь кроме консоли, то выглядеть это будет ужасно. Может, кто-нибудь уже догадался в чем дело?)
- Реализовать правильную очистку ресурсов, чтобы можно было спокойно отключать и подключать девайс
- Перейти на использование асинхронного механизма URB’ов вместо синхронной посылки Bulk-мессаг, что снизит потери в драйвере
- Скомпилировать разные веселые приложения, чтобы полностью насладиться нашим мини-компьютером. Так, например, мы посмотрим, что нужно пропатчить в коде движка Gobliins, чтобы они, наконец, запустились.
Ниже представлено еще одно видео со старым квестом (не менее любимым мной, чем Gobliins), несколько фото небольшой графической оболочки, которую можно без проблем запустить на устройстве и старые добрые консольные приложения MC и Elinks (консольный браузер)
Первая Кирандия
Хабр в консольном браузере (шрифт VGA8x8)
Оболочка Gmenu2X
Встроенный в нее файловый браузер
Настройки
На этом у меня пока все. Удачной реализации и до следующей статьи!
Автор: Ariman
Источник
- Техника
- Cancel
Превращение роутер в обычный комп.
Собственно говоря, не одному мне пришла в голову идея превращения роутера в персоналку. Вот товарищ cluster_d дал интересную ссылочку с завершённым проектом:
Персоналка из роутера
Подробное описание тут: http://sven.killig.de/openwrt/slugterm_dl.html . Это USB-монитор. Главное было написать для него соответствующие дрова. Надо отдельно отметить, не смотря на то что роутер почти такой же как и у меня (юсб портов больше), но прошивка совсем другая — это отдельный проект openWRT.
Вот ещё одна штукенция, только уже с OLED дисплеем, подрубленным по UART. Это суть проще.
Зацепленный OLED дисплей
В общем, как говорится было бы желание. Скоро на роутеры будут ставить игруки, и получим персональный комп, не х86 архитектуры. Вообще это я считаю крутяк.
Здесь уже были обзоры мини-ПК от китайского производителя с лаконичным названием XCY, но конкретно этой модели с четырьмя портами LAN не было.
Обзор будет не очень длинным, но с фотками.
Итак, устройство приехало хорошо упакованное. Правда, что-то внутри корпуса бултыхалось сразу после распаковки. Я, решил что рисковать не стоит, сразу раскрутил корпус. Оказалось, что один из ботов крепления платы к корпусу отсутствует на своём месте. Перевернув устройство и потреся его, болт удалось найти. Я проверил затяжку остальных болтов и не зря: все были закручены очень слабо, думаю тот беглец просто выкрутился от тряски при транспортировке. Так себе качество сборки, должен сказать.
В комплекте идёт крепление VESA/стена, короткая инструкция с картинками и болтики. Последние есть для фиксации в креплении, фиксации крепления на мониторе, фиксации SATA-диска в корпусе и пара совсем тонких, похоже для крепления карт.
Само крепление выглядит солидно:
Раз уж пришлось разбирать корпус давайте заглянем внутрь:
Слева вверху SSD марки Kston (нет, я не опечатался) на 64Gb. smartctl его не знает, но определяется он как поддерживающий SATA 3. При этом материнка поддерживает только SATA 2.
Рядом находятся 4 порта LAN. Все гигабитные, это 4 разные карты. lspci говорит что это «Intel Corporation I211 Gigabit Network Connection (rev 03)».
Ниже расположена WiFi-карта марки AzureWave модель AW-NU706H. Один диапазон 2,4ГГц стандарты 802.11BGN, 300 Мбит/с. В Ubuntu определилась из коробки и заработала без проблем.
Закреплена она на забавной металлической подставочке т.к. короче посадочного места.
Про память всё видно на фото. Мне вполне хватает 4Гб, максимум можно поставить 8 (ограничение CPU).
Есть разъёмы для подключения SATA-диска 2,5» (тоже SATA2). Диск я подключил, выглядит это так:
Мне не понравилось, что диск перекрывает вентиляционные отверстия снизу корпуса. Да и провода приходится уминать давольно плотно.
Что касается материнки, то идентифицировать мне её не удалось. Отмечу, что BIOS там кишит настройками, видимо китайцы включили все модули, что умещались во флаш. Дата сборки BIOS — 2019 год. Подробнее можно посмотреть в статье, которую я упомянул в начале, у меня всё то же самое.
CPU — в моей комплектации это Celeron J1800 — находится с другой стороны платы:
Позабавила площадочка для контакта CPU с крышкой. Фактически, верхняя крышка корпуса (алюминиевая) является огромным радиатором для процессора. Нагревается прилично — в простое у меня 41-42 градуса при 22-25 в помещении.
Блок питания внешний и довольно крупный — как у ноута. Отдаёт 12В 5А.
Компьютер пришёл с предустановленной русской(!) Windows 10. Забавно. Однако я покупал его в качестве роутера в загородный дом: мне провели проводной интернет и нужно реализовать фейловер между ним и мобильным интернетом через LTE-роутер Huawei. А ещё поднять на нём OpenVPN шлюз в домашнюю сеть. Сейчас его роль там выполняет RaspberryPi, но в ней один порт LAN.
Также я перенесу на него HomeAssistant и добавлю графану и какой-то бекэнд хранения для них, скорее всего PgSQL. С hass малинка ещё справляется, а вот графана и, тем более, БД для неё будут уже тяжеловаты.
Кроме того, хранилища (включая штатную для hass SQLite) любят часто сбрасывать данные на диск, что для SD-карты в малинке не очень хорошо. Поэтому я и решил поставить в новое устройство классический SATA-диск.
Так что Windows 10 меня не очень заинтересовал и я сразу поставил Ubuntu 20.04 Server. WiFi-адаптер в установщике не обнаружился. Наверняка его можно было поднять через консоль, но мне проще было воткнуть кабель в один из портов. В остальном установка прошла без нюансов.
Миникомпьютер из роутера своими руками
DIY. Уличный Wi-fi роутер своими руками.
Не выкидывай старый роутер
Не выкидывай старый роутер
Домашний роутер-NAS-WiFi точка доступа на МиниПК с 6 LAN
Домашний роутер-NAS-WiFi точка доступа на МиниПК с 6 LAN
Похожие видео:
Собираем миникомпьютер своими руками
Ибп для роутера своими руками
Как собрать миникомпьютер своими руками
Миникомпьютер своими руками
Антенна для роутера своими руками
Добавить комментарий
Ваш e-mail не будет опубликован. Обязательные поля помечены *
Комментарий
Имя *
E-mail *
Сайт
Current ye@r *