В этой статье я хочу поделиться своим опытом написания Linux-драйвера для цветного дисплея 320х240 от производителя Newhavendisplays, а именно NHD-5.7-320240WFB-CTXI-T1 для встроенного Linux. Идея написать статью созрела именно потому, что ресурсов для написания драйверов фреймбуфера (FB) не так уж и много, особенно на русском языке.
Модуль писался не для последнего ядра (2.6.30), поэтому признаю, что в интерфейсах ФБ с тех пор многое изменилось.
Но, тем не менее, я надеюсь, что статья будет интересна тем, кто интересуется разработкой уровня ядра Linux. Не исключаю, что реализацию можно было бы сделать проще и элегантнее, поэтому комментарии и замечания приветствуются.
Предыстория
Первоначально задачей было написать драйвер, к которому можно было бы получить доступ с помощью стандартных инструментов, таких как встроенный QT, чтобы в конечном итоге создать простое меню с иконками и текстом для взаимодействия с пользователем.
Платформой стала платка на базе AT91SAM9G45, а точнее www.armdevs.com/IPC-SAM9G45.html Планов транслировать видео не было.
AT91SAM9G45 содержит полнофункциональный встроенный LCD-контроллер с поддержкой DMA и достаточно высокоскоростную шину, с помощью которой можно было бы добиться потенциально приличных скоростей для видео, но увы, он аппаратно не совместим с SSD1963. Поэтому было решено использовать для этой цели обычный интерфейс GPIO как единственную доступную альтернативу.
Интерфейс контроллера SSD1963
Интерфейс контроллера проще всего представить в виде картинки из даташита дисплея:
С точки зрения разработчика драйвера нас интересуют выводы DB0 – DB7. Это 8-битная шина данных, а выводы DC, RD, WR, CS, RES используются для управления процессом передачи данных на SSD1963.
Что касается формата передаваемых данных, то в этом дисплее используется формат 888. Что означает: 8 байт — красный, 8 байт — зелёный, 8 байт — синий.
Довольно часто в дисплеях такого типа можно встретить варианты 555, 565 и т.д., но это не наш случай.
Формат передаваемых данных показан на рисунке.
Прежде чем первый байт данных будет подан на шину, выводы CS и WR должны переключиться с 1 на 0. А после того, как байт данных будет установлен, CS и WR переключатся с 0 на 1, что собственно и передает байт. данные на контроллер SSD1963. Более подробные осциллограммы сигналов можно посмотреть в даташите на контроллер.
www.newhavendisplay.com/app_notes/SSD1963.pdf В исходном коде мы опишем интерфейс с массивами выводов GPIO:
Функция передачи байтов по этому интерфейсу выглядит так:static unsigned int nhd_data_pin_config[] = { AT91_PIN_PE13, AT91_PIN_PE14, AT91_PIN_PE17, AT91_PIN_PE18, AT91_PIN_PE19, AT91_PIN_PE20, AT91_PIN_PE21, AT91_PIN_PE22 }; static unsigned int nhd_gpio_pin_config[] = { AT91_PIN_PE0, // RESET AT91_PIN_PE2, // DC AT91_PIN_PE5, // CLK AT91_PIN_PE6, // RD AT91_PIN_PE1 // WR };
static void nhd_write_data(int command, unsigned short value)
{
int i;
at91_set_gpio_output(AT91_PIN_PE12, 1); //R/D
for (i=0; i<ARRAY_SIZE(nhd_data_pin_config); i++)
at91_set_gpio_output(nhd_data_pin_config[i], (value>>i)&0x01);
if (command)
at91_set_gpio_output(AT91_PIN_PE10, 0); //D/C
else
at91_set_gpio_output(AT91_PIN_PE10, 1); //D/C
at91_set_gpio_output(AT91_PIN_PE11, 0); //WR
at91_set_gpio_output(AT91_PIN_PE26, 0); //CS
at91_set_gpio_output(AT91_PIN_PE26, 1); //CS
at91_set_gpio_output(AT91_PIN_PE11, 1); //WR
}
Как видите, с помощью этой функции можно отправлять на ЖК-контроллер как команды (например, для настройки дисплея), так и данные в виде пикселей.
Модель ядра фреймбуфера Как вы знаете, ядро Linux предоставляет интерфейсы для различных типов драйверов устройств — символьных драйверов, блочных драйверов, драйверов USB и т. д. Драйвер кадрового буфера также является отдельной подсистемой в модели драйверов Linux. Основная структура, которая используется для представления драйвера FB: структура fb_info В Linux/fb.h .
Кстати, этот заголовочный файл будет интересен и любителям юмора в коде ядра Linux, так как содержит интересное определение — #define STUPID_ACCELF_TEXT_SHIT .
Думаю, название говорит само за себя.
Но вернемся к структуре fb_info .
Нас будут интересовать две структуры, которые он содержит — fb_var_screeninfo И fb_fix_screeninfo .
Мы инициализируем их параметрами нашего дисплея.
static struct fb_fix_screeninfo ssd1963_fix __initdata = {
.
id = "SSD1963", .
type = FB_TYPE_PACKED_PIXELS, .
visual = FB_VISUAL_TRUECOLOR, .
accel = FB_ACCEL_NONE, .
line_length = 320 * 4, }; static struct fb_var_screeninfo ssd1963_var __initdata = { .
xres = 320, .
yres = 240, .
xres_virtual = 320, .
yres_virtual = 240, .
width = 320, .
height = 240, .
bits_per_pixel = 32, .
transp = {24, 8, 0}, .
red = {16, 8, 0}, .
green = {8, 8, 0}, .
blue = {0, 8, 0}, .
activate = FB_ACTIVATE_NOW, .
vmode = FB_VMODE_NONINTERLACED,
};
В нашем случае для пикселя будет выделено 4 байта: 8-красный, 8-зеленый, 8-синий, 8-прозрачный.
Позвольте мне объяснить некоторые поля структуры: .
тип – способ размещения в памяти битов, описывающих пиксели.
Упакованные пиксели означают, что байты (в нашем случае 8888 будут располагаться последовательно друг за другом).
.
визуальный – глубина цвета дисплея.
В нашем случае это truecolor — глубина цвета 24 бита.
.
accel - аппаратное ускорение .
transp, красный, зеленый, синий — они просто задали наш формат 8,8,8,8 в виде трёх полей — смещение, длина И msb_right .
Также, чтобы зарегистрировать наш драйвер в ядре, нам необходимо описать ещё две сущности — устройство и драйвер.
Опишем устройство ФБ( структура SSD1963 ), который будет содержать страницы нашей видеопамяти ( структура ss1963_page ): struct ssd1963_page {
unsigned short x;
unsigned short y;
unsigned long *buffer;
unsigned short len;
int must_update;
};
struct ssd1963 {
struct device *dev;
struct fb_info *info;
unsigned int pages_count;
struct ssd1963_page *pages;
};
struct platform_driver ssd1963_driver = {
.
probe = ssd1963_probe, .
remove = ssd1963_remove, .
driver = { .
name = "ssd1963" }
};
Инициализация
Как и в случае с любым другим модулем ядра Linux, мы опишем пару функций инициализации/удаления.
Начнем с инициализации.
Драйверы фреймбуфера обычно регистрируются в системе как Platform_driver : static int __init ssd1963_init(void)
{
int ret = 0;
ret = platform_driver_register(&ssd1963_driver);
if (ret) {
pr_err("%s: unable to platform_driver_register\n", __func__);
}
return ret;
}
module_init(ssd1963_init);
Драйвер платформы, в свою очередь, вызывает функцию проверки для конкретного драйвера, которая выполняет все необходимые операции — выделение памяти, резервирование ресурсов, инициализацию структуры и т. д. Приведем пример функции ssd1963_probe : static int __init ssd1963_probe(struct platform_device *dev)
{
int ret = 0;
struct ssd1963 *item;
struct fb_info *info;
// Allocating memory for ssd1663 device
item = kzalloc(sizeof(struct ssd1963), GFP_KERNEL);
if (!item) {
dev_err(&dev->dev,
"%s: unable to kzalloc for ssd1963\n", __func__);
ret = -ENOMEM;
goto out;
}
item->dev = &dev->dev;
dev_set_drvdata(&dev->dev, item);
// Initializing fb_info struct using kernel framebuffer API
info = framebuffer_alloc(sizeof(struct ssd1963), &dev->dev);
if (!info) {
ret = -ENOMEM;
dev_err(&dev->dev,
"%s: unable to framebuffer_alloc\n", __func__);
goto out_item;
}
item->info = info;
//Here info->par pointer is commonly used to store private data
// In our case, we can use it to store pointer to ssd1963 device
info->par = item;
info->dev = &dev->dev;
info->fbops = &ssd1963_fbops;
info->flags = FBINFO_FLAG_DEFAULT;
info->fix = ssd1963_fix;
info->var = ssd1963_var;
ret = ssd1963_video_alloc(item);
if (ret) {
dev_err(&dev->dev,
"%s: unable to ssd1963_video_alloc\n", __func__);
goto out_info;
}
info->screen_base = (char __iomem *)item->info->fix.smem_start;
ret = ssd1963_pages_alloc(item);
if (ret < 0) {
dev_err(&dev->dev,
"%s: unable to ssd1963_pages_init\n", __func__);
goto out_video;
}
info->fbdefio = &ssd1963_defio;
fb_deferred_io_init(info);
ret = register_framebuffer(info);
if (ret < 0) {
dev_err(&dev->dev,
"%s: unable to register_frambuffer\n", __func__);
goto out_pages;
}
ssd1963_setup(item);
ssd1963_update_all(item);
return ret;
out_pages:
ssd1963_pages_free(item);
out_video:
ssd1963_video_free(item);
out_info:
framebuffer_release(info);
out_item:
kfree(item);
out:
return ret;
}
Несколько комментариев по поводу функции.
Вот последовательно: — Выделяем память для нашего устройства ссд1963 - Выделить память и инициализировать структуру fb_info , сначала значения по умолчанию( фреймбуфер_аллок ), так как нам не нужно менять много параметров, и то с конкретными значениями для нашего драйвера, например fb_var_screeninfo, fb_fix_screeninfo И fb_ops , который мы рассмотрим чуть позже.
— Выделяет память для непрерывного буфера пикселей в виртуальной памяти, которая будет использоваться процессами для записи пользовательского пространства.
— Мы выделяем ssd1963_страница для каждой страницы в виртуальной памяти фреймбуфера.
Каждый ssd1963_страница будет содержать адрес начала страничного буфера относительно всего буфера FB, смещение x, смещение y и длину страничного буфера.
В нашем случае емкость фреймбуфера = длина_линии*высота = 320*4*240 = 307200 байт. Для такой емкости буфера нам понадобится длина_строки*высота/PAGE_SIZE = 307200/4096 = 75 страниц.
Отметим, как они будут располагаться в памяти ФБ.
Понимание такого расположения страниц нам пригодится, когда мы рассмотрим функцию ssd1963_copy чуть позже:
— Зарегистрируйте наш ФБ в системе( регистр_фреймбуфер ) и инициализировать процедуру отложенного обновления данных ( fb_deferred_io_init ), подробнее об этом в разделе «работы с фреймбуфером».
— ssd1963_setup настраивает необходимые GPIO на ЦП AT91SAM9G45 и выполняет первоначальную настройку контроллера ЖК-дисплея.
Алгоритм первоначальной настройки в виде отправки набора загадочных байт в шестнадцатеричном формате был взят из документации SSD1963, поэтому приведу здесь только часть функции: void ssd1963_setup(struct ssd1963 *item)
{
nhd_init_gpio_regs(); //initializations of pins in nhd_data-gpio_pin_config
at91_set_gpio_output(AT91_PIN_PE27, 0); //RESET
udelay(5);
at91_set_gpio_output(AT91_PIN_PE27, 1); //RESET
udelay(100);
nhd_write_data(NHD_COMMAND, 0x01); //Software Reset
.
nhd_write_to_register(0xe0, 0x03); //LOCK PLL
nhd_write_data(NHD_COMMAND, 0xb0); //SET LCD MODE TFT 18Bits
nhd_write_data(NHD_DATA, 0x0c); //SET MODE 24 bits & hsync+Vsync+DEN
…
}
— ssd1963_update_all устанавливает флаг must_update=1 для всех страниц и инициирует механизм обновления отображения в отложенном контексте, вызывая Schedule_delayed_work(&item-> info-> deferred_work, fbdefio-> delay);
Итак, с init разобрались, с функцией удаления все гораздо проще, освобождаем выделенную память и возвращаем структуры FB ядру: static int ssd1963_remove(struct platform_device *device)
{
struct fb_info *info = platform_get_drvdata(device);
struct ssd1963 *item = (struct ssd1963 *)info->par;
if (info) {
unregister_framebuffer(info);
ssd1963_pages_free(item);
ssd1963_video_free(item);
framebuffer_release(info);
kfree(item);
}
return 0;
}
Операции с фреймбуфером
Итак, пришло время взглянуть на структуру fb_ops : static struct fb_ops ssd1963_fbops = {
.
owner = THIS_MODULE, .
fb_read = fb_sys_read, .
fb_write = ssd1963_write, .
fb_fillrect = ssd1963_fillrect, .
fb_copyarea = ssd1963_copyarea, .
fb_imageblit = ssd1963_imageblit, .
fb_setcolreg = ssd1963_setcolreg, .
fb_blank = ssd1963_blank,
};
Я не привожу здесь все методы структуры; любопытный читатель может найти их в исходном коде модуля или в любом другом драйвере в коде ядра в каталоге драйверы/видео .
Как вы уже догадались, структура fb_ops описывает действия, которые может выполнять наш драйвер.
К счастью, разработчики ядра частично облегчили нам работу, предоставив стандартные функции для работы с FB, имеющие суффикс sys_ или fb_sys , Например fb_sys_read .
Нам нужно только добавить функции из fb_ops ( ssd1963_read, ssd1963_write и т. д.) функционал, позволяющий обновлять данные в нашей импровизированной видеопамяти, когда в этом возникает необходимость.
Например, функция ssd1963_fillrect будет выглядеть так: static void ssd1963_fillrect(struct fb_info *p, const struct fb_fillrect *rect)
{
sys_fillrect(p, rect);
ssd1963_touch(p, rect->dx, rect->dy, rect->width, rect->height);
}
Очевидно, системный вызов fb_fillrect обновит видеоданные в определенной прямоугольной области экрана, поэтому нам нужно указать, какие страницы нам нужно обновить, отметив их флажком must_update , а затем вручную вызвать процедуру обновления видеопамяти: static void ssd1963_touch(struct fb_info *info, int x, int y, int w, int h)
{
struct fb_deferred_io *fbdefio = info->fbdefio;
struct ssd1963 *item = (struct ssd1963 *)info->par;
int i, ystart, yend;
if (fbdefio) {
//Touch the pages, so the deferred io will update them.
for (i=0; i<item->pages_count; i++) {
ystart=item->pages[i].
y; yend=item->pages[i].
y+(item->pages[i].
len/info->fix.line_length)+1; if (!((y+h)<ystart || y>yend)) { item->pages[i].
must_update=1;
}
}
//Schedule the deferred IO to kick in after a delay.
schedule_delayed_work(&info->deferred_work, fbdefio->delay);
}
}
Обновление данных в видеопамяти происходит в виде отложенного контекста.
Пользовательское приложение, работающее с графикой, не будет ждать записи каждого кадра в видеопамять, что вполне логично.
Отложенная обработка в fb_info определяется как структура fb_deferred_io : static struct fb_deferred_io ssd1963_defio = {
.
delay = HZ / 20, .
deferred_io = &ssd1963_update,
};
Функция ssd1963_update с прототипом void ssd1963_update(struct fb_info *info, struct list_head *pagelist);
обновляет не все страницы, а только те страницы, которые были изменены в результате перезаписи пользовательского пространства процессом или в результате системного вызова типа fb_fillrect и компании.
Соответственно, функция выглядит так: static void ssd1963_update(struct fb_info *info, struct list_head *pagelist)
{
struct ssd1963 *item = (struct ssd1963 *)info->par;
struct page *page;
int i;
list_for_each_entry(page, pagelist, lru) {
item->pages[page->index].
must_update=1; } //Copy changed pages. for (i=0; i<item->pages_count; i++) { if (item->pages[i].
must_update) { item->pages[i].
must_update=0;
ssd1963_copy(item, i);
}
}
}
На этом этапе вам, вероятно, интересно, что делает функция ssd1963_copy .
Он фактически выполняет всю «грязную» работу по передаче данных со страниц видеопамяти на искусственно созданную 8-битную шину на базе GPIO. Функция ssd1963_copy Здесь необходимо вспомнить рисунок, на котором показано, как наши страницы в памяти соответствуют пикселям дисплея.
Мы видим, например, что в страница[0] информация сохраняется для трех верхних строк дисплея по 320 пикселей каждая и 64 пикселей для 4-й строки.
Таких страниц у нас 75, а картинка с рисунка, и как не трудно заметить, страница[5] будет выглядеть одинаково — 3 строки по 320 и одна по 64. Соответственно, функция, принимающая в качестве параметра индекс страницы, будет содержать переключатель (индекс%5) и в зависимости от смещений для каждой конкретной страницы отправлять данные в выделенное для нее «окно» в памяти дисплея.
Функция довольно длинная, поэтому приведу только часть: static void ssd1963_copy(struct ssd1963 *item, unsigned int index)
{
unsigned short x,y, startx, endx, starty, endy, offset;
unsigned long *buffer;
unsigned int len;
unsigned int count;
x = item->pages[index].
x; y = item->pages[index].
y; buffer = item->pages[index].
buffer; len = item->pages[index].
len; switch (index%5) { case 0: offset = 0; startx = x; starty = y; endx = 319; endy = y+2; len = 960; nhd_set_window(startx, endx, starty, endy); nhd_write_data(NHD_COMMAND, 0x2c); for (count = 0; count < len; count++) { nhd_write_data(NHD_DATA,(unsigned char)((buffer[count+offset])>>16)); //red nhd_write_data(NHD_DATA,(unsigned char)((buffer[count+offset])>>8)); //green nhd_write_data(NHD_DATA,(unsigned char)(buffer[count+offset])); //blue } offset = len; startx = x; starty = y+3; endx = x+63; endy = y+3; len = 64; nhd_set_window(startx, endx, starty, endy); nhd_write_data(NHD_COMMAND, 0x2c); for (count = 0; count < len; count++) { nhd_write_data(NHD_DATA,(unsigned char)((buffer[count+offset])>>16)); //red nhd_write_data(NHD_DATA,(unsigned char)((buffer[count+offset])>>8)); //green nhd_write_data(NHD_DATA,(unsigned char)(buffer[count+offset])); //blue } break; case 1: ….
Вот функция nhd_set_window настраивает с помощью уже известного нам nhd_write_data (NHD_COMMAND, .
); область дисплея, в которую будут записываться данные (пиксели).
nhd_write_data (NHD_COMMAND, 0x2c); — команда контроллеру ЖК-дисплея, по которому теперь будет следовать поток данных.
И напоследок скриншот программы ts_claimate из пакета tslib, работающей на устройстве с дисплеем.
Если кому интересно, могу выслать полный код модуля:
Теги: #embedded linux #драйвер Linux #ядро Linux #C++
-
Бесплатные Статьи Для Вашего Веб-Сайта
19 Oct, 24 -
Мальтус, Томас Роберт
19 Oct, 24 -
Именованные Параметры C++. Не Полезно
19 Oct, 24 -
Китай Отключен От Сети Из-За Землетрясения
19 Oct, 24