SoC: пишем реализацию framebuffer для контроллера в FPGA

  • Tutorial


Приветствую!

В прошлый раз мы остановились на том, что подняли DMA в FPGA.
Сегодня мы реализуем в FPGA примитивный LCD-контроллер и напишем драйвер фреймбуфера для работы с этим контроллером.

Вы ещё раз убедитесь, что разработка под FPGA и написание драйверов под Linux дело очень простое, но интересное.

Также в конце есть маленький опрос — хочется узнать мнение сообщества. Если не сложно, прошу проголосовать.


Так сложилось, что в HPS на Cyclone V нет встроенного графического контроллера. А жить без дисплея нам никак нельзя — куда же выводить результаты измерений.

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

А мы выбираем реализацию очень простого, но полностью рабочего LCD-контроллера в FPGA, который позволит снять с CPU ненужную нагрузку в виде записи данных в дисплей.

Использовать мы будем LCD на базе чипа ILI9341.

Итак, план на сегодня:
  • Думаем над архитектурой
  • Изучаем наш LCD
  • Пишем Linux драйвер
  • Разрабатываем модуль в FPGA
  • Настраиваем кое-что в U-boot
  • Отлаживаемся


Архитектура


Что такое фреймбуфер в Linux?
В двух словах — это просто область памяти, запись в которую приводит к отображению записанного на дисплее.

Из userspace доступ выполняется через файл устройства /dev/fb[N].
Обычно реализованы стандартные системные вызовы — open(), close(), read(), write(), lseek() и mmap().

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

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

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

После обновления кадра нужно сделать так, чтобы он попал в LCD. Естественно, копирование только изменённой части кадра требует передачи меньшего количества данных. И если бы мы копировали данные при помощи CPU, то нам обязательно стоило бы это учесть.
Но у нас копирование будет выполнять DMA-контроллер в FPGA, поэтому мы не станем беспокоиться по этому поводу и будем перерисовывать весь кадр целиком.

Следующий вопрос в том, когда перерисовывать кадр. Одно из простых решений — выполнять отрисовку синхронно, то есть в конце каждой функции, которая обновляет данные в памяти. Это хорошо работает во всех случаях, кроме использования mmap().
После выполнения отображения не так просто определить, когда userspace'ный процесс изменил содержание памяти. Эту задачу можно решить при помощи deferred_io (а заодно и определить конкретные страницы памяти, которые обновились и которые нужно перерисовывать). Но мы хотим, чтобы наша реализация была как можно более простой и понятной, поэтому мы сделаем по-другому.

Наш контроллер в FPGA будет выполнять отрисовку всего кадра с частотой в n FPS. И делать это он будет асинхронно относительно обновления памяти драйверными функциями. Таким образом всё, что нужно сделать в драйвере — это инициализация LCD и FPGA-контроллера. И даже запись данных в память фреймбуфера нам реализовывать не потребуется, для этого уже есть стандартные функции.

Контроллер в FPGA тоже будет достаточно простым. Его задачи:
  • Читать данные из указанной области по интерфейсу fpga2sdram либо fpga2hps
  • Передавать прочитанные данные в LCD, сформировав нужные транзакции
  • Давать возможность CPU получить прямой доступ к интерфейсу до LCD
  • Выдавать заданный FPS


Описание нашего LCD


Всё, что нам нужно знать про LCD — как его инициализировать и как выглядит транзакция записи.

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

ILI9341 поддерживает несколько интерфейсов. У меня используется параллельный 16-битный интерфейс, называемый 8080 по имени процессора от Intel, в котором он впервые появился. Вот какие сигналы там есть (вначале указано более распространённое название, а в скобках — название из даташита на ILI9341):
  • CS (CSX) — chip-select, активный уровень 0. Сигнал выбора чипа, у меня заведён на землю.
  • RST (RESX) — reset, активный уровень 0. Сигнал сброса, у меня заведён на GPIO HPS.
  • RS (D/CX) — register select. Если сигнал равен 0, то на шине DATA выставлена команда, иначе — данные.
  • WR (WRX) — write strobe. Строб записи.
  • RD (RDX) — read strobe. Строб чтения.
  • DATA (D) — данные или команда, в зависимости от RS.

Транзакция записи предельно простая:
Write transaction



Транзакция чтения не сложнее, но нам она не понадобится, поэтому рассматривать её не будем.

Linux драйвер


Что же есть у нас в драйвере?

Во-первых, функции для чтения/записи регистров FPGA. Подробнее о том, что такое контрольно-статусные регистры, и как их использовать, можно прочитать в статье моего коллеги ishevchuk.
CSR read/write functions
static void fpga_write_reg(int reg, u16 val)
{
        iowrite16(val, fpga_regs + 2*reg);
}

static u16 fpga_read_reg(int reg)
{
        u16 tmp;
        tmp = ioread16(fpga_regs + 2*reg);
        return tmp;
}

static void fpga_set_bit(int reg, int bit)
{
        unsigned long tmp = fpga_read_reg(reg);

        set_bit(bit, &tmp);
        fpga_write_reg(reg, tmp);
}

static void fpga_clear_bit(int reg, int bit)
{
        unsigned long tmp = fpga_read_reg(reg);

        clear_bit(bit, &tmp);
        fpga_write_reg(reg, tmp);
}

Во-вторых, функции для прямой записи в LCD команд и данных. Они будут использоваться для инициализации дисплея.
Функции абсолютно «топорные» — просто делаем транзакцию такой, какой она изображена в даташите (и выше в этой статье).
LCD data/command write functions
static void lcd_write_command(u16 val)
{
        /* Write command code */
        fpga_write_reg(LCD_DATA_CR, val);

        /* WR and RS low, RD high */
        fpga_write_reg(LCD_CTRL_CR, LCD_CTRL_CR_RD);
        ndelay(1);

        /* RS low, WR and RD high */
        fpga_write_reg(LCD_CTRL_CR, LCD_CTRL_CR_RD | LCD_CTRL_CR_WR);
        ndelay(1);

        /* All control signals high */
        fpga_write_reg(LCD_CTRL_CR, LCD_CTRL_CR_RD | LCD_CTRL_CR_WR |
                       LCD_CTRL_CR_RS);
}

static void lcd_write_data(u16 data)
{
        /* Write data */
        fpga_write_reg(LCD_DATA_CR, data);

        /* WR low, RD and RS high */
        fpga_write_reg(LCD_CTRL_CR, LCD_CTRL_CR_RD | LCD_CTRL_CR_RS);
        ndelay(1);

        /* All control signals high */
        fpga_write_reg(LCD_CTRL_CR, LCD_CTRL_CR_RD |
                       LCD_CTRL_CR_RS | LCD_CTRL_CR_WR);
}

Ну и, собственно, наша нехитрая инициализация LCD.
LCD initialization function
static void lcd_init(struct fb_info *info)
{
        // Clear data
        fpga_write_reg(LCD_DATA_CR, 0);

        // All control signals high
        fpga_write_reg(LCD_CTRL_CR, LCD_CTRL_CR_RD | LCD_CTRL_CR_RS | LCD_CTRL_CR_WR);

        mdelay(100);

        lcd_write_command(ILI9341_DISPLAY_ON);

        lcd_write_command(ILI9341_SLEEP_OUT);
        lcd_write_command(ILI9341_INVERTION_OFF);
        lcd_write_command(ILI9341_MEM_ACCESS_CTRL);
        lcd_write_data(MY | MX | MV | BGR);

        lcd_write_command(ILI9341_PIXEL_FORMAT);
        lcd_write_data(0x0055);

        lcd_write_command(ILI9341_COLUMN_ADDR);
        lcd_write_data(0x0000);
        lcd_write_data(0x0000);
        lcd_write_data((DISPLAY_WIDTH-1) >> 8);
        lcd_write_data((DISPLAY_WIDTH-1) & 0xFF);

        lcd_write_command(ILI9341_PAGE_ADDR);
        lcd_write_data(0x0000);
        lcd_write_data(0x0000);
        lcd_write_data((DISPLAY_HEIGHT-1) >> 8);
        lcd_write_data((DISPLAY_HEIGHT-1) & 0xFF);

        lcd_write_command(ILI9341_MEM_WRITE);
}

Коротко про используемые команды.

ILI9341_DISPLAY_ON (0x29) и ILI9341_SLEEP_OUT (0x11), хоть это и неожиданно, включают дисплей и выводят его из спящего режима.

ILI9341_MEM_ACCESS_CTRL (0x36) — это настройка направления сканирования памяти.

ILI9341_PIXEL_FORMAT (0x3a) — формат изображения, у нас это 16 бит на пиксель.

ILI9341_COLUMN_ADDR (0x2a) и ILI9341_PAGE_ADDR (0x2b) задают рабочую область нашего дисплея.

ILI9341_MEM_WRITE (0x2c) — эта команда говорит, что дальше последуют транзакции с данными. При этом текущая позиция выставляется на начальные столбец и строку, которые были заданы, соответственно, при помощи ILI9341_COLUMN_ADDR и ILI9341_PAGE_ADDR. После каждой транзакции столбец будет автоматически инкрементироваться на 1. Когда столбец станет равным конечному, произойдет переход на следующую строку. Когда и столбец и строка станут равны конечным, позиция вернётся в начальную.

Таким образом, после команды ILI9341_MEM_WRITE контроллер в FPGA может просто «по кругу» отсылать данные из памяти в LCD, ни о чём больше не заботясь.

Последнее, что нас интересует в драйвере, это функция probe.
Driver probe function
        struct fb_info *info;
        int ret;

        u32 vmem_size;
        unsigned char *vmem;

        dma_addr_t dma_addr;

        pdev->dev.dma_mask = &platform_dma_mask;
        pdev->dev.coherent_dma_mask = DMA_BIT_MASK(32);

        vmem_size = (etn_fb_var.width * etn_fb_var.height * etn_fb_var.bits_per_pixel) / 8;

        vmem = dmam_alloc_coherent(&pdev->dev, vmem_size, &dma_addr, GFP_KERNEL);
        if (!vmem) {
                dev_err(&pdev->dev, "FB: dma_alloc_coherent error\n");
                return -ENOMEM;
        }

        memset(vmem, 0, vmem_size);

        info = framebuffer_alloc(0, &pdev->dev);
        if (!info)
                return -ENOMEM;

        info->screen_base = vmem;
        info->fbops = &etn_fb_ops;
        info->fix = etn_fb_fix;
        info->fix.smem_start = dma_addr;
        info->fix.smem_len = vmem_size;
        info->var = etn_fb_var;
        info->flags = FBINFO_DEFAULT;
        info->pseudo_palette = &etn_fb_pseudo_palette;

        /* Get FPGA registers address */
        fpga_regs = devm_ioremap(&pdev->dev, FPGA_REGS_BASE, REGSIZE);

        /* Disable refreshing */
        fpga_write_reg(LCD_DMA_CR, 0);

        lcd_init(info);

        set_dma_addr(dma_addr);

        set_fps(fps);

        /* Enable refreshing */
        fpga_set_bit(LCD_DMA_CR, LCD_DMA_CR_REDRAW_EN);

        ret = register_framebuffer(info);
        if (ret < 0) {
                framebuffer_release(info);
                return ret;
        }

        platform_set_drvdata(pdev, info);

        return 0;

Что в ней происходит?
Вначале мы выделяем память в DMA-совместимой зоне при помощи функции dmam_alloc_coherent(). При этом мы получаем два адреса, которые «указывают» на выделенную область. Один будет использоваться в драйвере, а второй мы запишем а FPGA, чтобы DMA-контроллер мог прочитать данные из этой области.

Пара слов о DMA-отображениях. Они бывают двух типов:
  • Потоковые (Streaming)
  • Согласованные (Coherent или Сonsistent)

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

При использовании потоковых отображений доступ возможен строго по очереди. Чаще всего создаются на время одной операции.
Теоретически, могут быть более производительными. Пример — приём/отправка сетевых пакетов.

Вернёмся к функции probe. Дальше мы заполняем fb_info.
Потом мы мапим адресное пространство FPGA, чтобы иметь возможность читать и писать в контрольно-статусные регистры.

После этого мы записываем в FPGA требуемое значение FPS и наш DMA-адрес (не забыв перевести его в номер слова, при необходимости).

Затем включаем отрисовку в FPGA и регистрируем наш фреймбуфер. Всё готово!


Модуль в FPGA


Мы добрались до модуля в FPGA. Тут тоже всё просто.
Напомню, что нам нужно реализовать:
  • Прямой доступ CPU к LCD
  • Чтение данных из памяти фреймбуфера
  • Формирование транзакций записи в сторону LCD
  • Получение нужного FPS

Для обеспечения прямого доступа CPU к LCD у нас, естественно, будут использоваться контрольные регистры.
И обычный мультиплексор — когда управление идёт от CPU, то на интерфейс до LCD коммутируются сигналы с регистров, иначе — сигналы из модуля в FPGA. Выбор происходит в зависимости от состояния конечного автомата, который описан ниже.
Код примитивный:
LCD bus MUX
always_ff @( posedge clk_i )
  if( state == IDLE_S )
    begin
      lcd_bus_if.data <= lcd_ctrl_if.data;
      lcd_bus_if.rd   <= lcd_ctrl_if.rd;
      lcd_bus_if.wr   <= lcd_ctrl_if.wr;
      lcd_bus_if.rs   <= lcd_ctrl_if.rs;
    end
  else      
    // Send data transactions from FPGA.
    begin
      lcd_bus_if.data <= lcd_data_from_fpga;
      lcd_bus_if.rd   <= 1'b1;
      lcd_bus_if.wr   <= lcd_wr_from_fpga;
      lcd_bus_if.rs   <= 1'b1;
    end

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

То есть нам нужно искусственно ограничить скорость чтения. Для этого есть следующие варианты:
  • Читать и записывать в LCD последовательно — прочитали, записали, прочитали, записали и т.д.
  • Рассчитать скорость, с которой нам нужно вычитывать данные, и поддерживать её
  • Использовать FIFO

Первый вариант приведёт к тому, что данные на LCD будут поступать с большими (по меркам FPGA) паузами.
Учитывая прикладную задачу (нам вряд ли нужно получить FPS больше 50), вполне возможно, что нам этого хватит.
Но очень уж это топорно и некрасиво. Поэтому этот вариант отметаем.

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

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

Мы выбираем третий вариант. Для начала нам нужно FIFO:
FIFO instance
 buf_fifo #( 
  .AWIDTH                                 ( FIFO_AWIDTH       ),
  .DWIDTH                                 ( AMM_DATA_W        )
) buf_fifo (
  .clock                                  ( clk_i             ),
  .aclr                                   (                   ),

  .wrreq                                  ( fifo_wr_req       ),
  .data                                   ( fifo_wr_data      ),

  .rdreq                                  ( fifo_rd_req       ),
  .q                                      ( fifo_rd_data      ),

  .almost_full                            (                   ),
  .full                                   (                   ),
  .empty                                  ( fifo_empty        ),
  .usedw                                  ( fifo_usedw        )
);

Для определения момента приостановки чтения мало знать, насколько FIFO уже заполнено. Ведь у нас также есть транзакции чтения, которые сейчас «в процессе». То есть это данные, чтение которых мы уже запросили, но которые к нам ещё не доставлены.
Нам необходимо знать количество таких транзакций в текущий момент. Для этого каждый раз, когда выполняется запрос на чтение, мы будем увеличивать соответствующий счётчик, а при получении подтверждения прочитанных данных — уменьшать.
Pending transactions calculation
// Count of read transactions in progress
logic [FIFO_AWIDTH-1:0]  pending_read_cnt;

always_ff @( posedge clk_i )
  case( { read_req_w, amm_if.read_data_val } )
    2'b01:
      pending_read_cnt <= pending_read_cnt - 1'd1;
    
    2'b10:
      pending_read_cnt <= pending_read_cnt + 1'd1;
  endcase

В итоге останавливать чтение мы будем, когда сумма записанных в FIFO слов и транзакций «в процессе» почти сравняется с глубиной нашей очереди. В качестве «почти» выберем 50 свободных слов:
Stop reading
logic stop_reading;

assign stop_reading = ( pending_read_cnt + fifo_usedw ) > ( 2**FIFO_AWIDTH - 'd50 );

Формирование самих транзакций чтения на Avalon MM примитивно. Главное правильно инкрементировать адрес в зависимости от типа интерфейса: fpga2sdram или fpga2hps (более подробное описание интерфейсов и различий см. тут):
Read transactions
// fpga2sdram used word address, so we must added 1 every time, 
// fpga2hps used byte address, so we must added 8 (for 64-bit iface).
logic [31:0] addr_incr;

assign addr_incr = ( USE_WORD_ADDRESS == 1 ) ? 1 : ( AMM_DATA_W >> 3 );

always_ff @( posedge clk_i )
  if( state == IDLE_S )
    amm_if.address <= lcd_ctrl_if.dma_addr;
  else
    if( read_req_w ) 
      amm_if.address <= amm_if.address + addr_incr;

// Always read all bytes in word
assign amm_if.byte_enable = '1;

// We don't use burst now
assign amm_if.burst_count = 1;

assign amm_if.read = ( state == READ_S );

// Remove Quartus warnings
assign amm_if.write_data = '0;
assign amm_if.write      = 0;


Читать данные мы научились, теперь нужно научиться писать их в LCD. Для этого сделаем простенький конечный автомат на два состояния: если в FIFO есть данные, автомат переходит в состояние отправки транзакции. А после окончания записи возвращается обратно в IDLE:
FSM for writing to LCD
enum int unsigned {
  LCD_IDLE_S,
  LCD_WRITE_S
} lcd_state, lcd_next_state;

always_ff @( posedge clk_i )
  lcd_state <= lcd_next_state;

always_comb
  begin
    lcd_next_state = lcd_state;

    case( lcd_state )
      LCD_IDLE_S:
        begin
          if( !fifo_empty ) 
            lcd_next_state = LCD_WRITE_S;
        end

      LCD_WRITE_S:
        begin
          if( lcd_word_cnt == 5'd31 ) 
            lcd_next_state = LCD_IDLE_S;
        end
    endcase
  end

assign fifo_rd_req = ( lcd_state == LCD_IDLE_S ) && ( lcd_next_state == LCD_WRITE_S );


Нужно помнить, что одна транзакция до LCD — это передача 16 бит данных, а каждое слово в FIFO имеет размер 64 бита (зависит от настройки интерфейса fpga2sdram/fpga2hps). Поэтому на каждое прочитанное слово мы будем формировать 4 транзакции.
Формировать их просто — для этого нам достаточно сделать один счётчик и использовать в нём нужные разряды:
Read transactions
// ILI9341 Data transaction from FPGA:
//             __    __    __    __    __    __    __    __    __   
// clk/4 |  __|  |__|  |__|  |__|  |__|  |__|  |__|  |__|  |__|  |
//
// data  | ///<  split[0] |  split[1] |  split[2] |  split[3] >////
//
//             _______________________________________________
// rd    | xxxx                                               xxxx 
//
//                   _____       _____       _____       _____
// wr    | xxxx_____|     |_____|     |_____|     |_____|     xxxx 
//
//             _______________________________________________
// rs    | xxxx                                               xxxx 


logic [3:0][15:0] fifo_rd_data_split;
assign fifo_rd_data_split = fifo_rd_data;

logic [15:0] lcd_data_from_fpga;
logic        lcd_wr_from_fpga;

logic [4:0] lcd_word_cnt;

always_ff @( posedge clk_i )
  if( lcd_state == LCD_IDLE_S )
    lcd_word_cnt <= '0;
  else   
    lcd_word_cnt <= lcd_word_cnt + 1'd1;

assign lcd_data_from_fpga = fifo_rd_data_split[ lcd_word_cnt[4:3] ];
assign lcd_wr_from_fpga = ( lcd_state == LCD_IDLE_S ) ? 1'b1 : lcd_word_cnt[2];


Почти всё. Осталось сделать основной конечный автомат, который будет управлять всем вышеописанным.
Логика его работы простая — если наш модуль LCD контроллера включен, то нужно отрисовать один кадр.
Для реализации заданного FPS есть «состояние-пауза», в котором происходит ожидание нужного количества тактов.
После этого стартует чтение данных из памяти (запись в LCD начнётся автоматически, как только в FIFO появятся данные).
Когда будет прочитан весь кадр, останется только дождаться завершения транзакций к LCD:
Main FSM
logic [31:0] word_cnt;

always_ff @( posedge clk_i )
  if( state == IDLE_S )
    word_cnt <= '0;
  else
    if( read_req_w ) 
      word_cnt <= word_cnt + 1'd1;

logic reading_is_finished;
assign reading_is_finished = ( word_cnt == WORD_IN_FRAME - 1 ) && read_req_w;


logic stop_reading;
assign stop_reading = ( pending_read_cnt + fifo_usedw ) > ( 2**FIFO_AWIDTH - 'd50 );


logic all_is_finished;
assign all_is_finished = ( pending_read_cnt == 0          ) && 
                         ( fifo_usedw       == 0          ) && 
                         ( lcd_state        == LCD_IDLE_S ); 


enum int unsigned {
  IDLE_S,
  FPS_DELAY_S,
  READ_S,
  WAIT_READIND_S,
  WAIT_WRITING_S
} state, next_state;

always_ff @( posedge clk_i )
  state <= next_state;

// FIXME:
//   If lcd_ctrl_if.redraw_en == 1
//   CPU have one takt for read 0 in lcd_ctrl_if.dma_busy
//   Fix: add WAIT_WRITING_S -> FPS_DELAY_S path
always_comb
  begin
    next_state = state;

    case( state )
      IDLE_S:
        begin
          if( lcd_ctrl_if.redraw_stb || lcd_ctrl_if.redraw_en ) 
            next_state = FPS_DELAY_S;
        end   

      FPS_DELAY_S:
        begin
          if( fps_delay_done_w )
            next_state = READ_S;
        end
    
      READ_S:
        begin
          if( reading_is_finished ) 
            next_state = WAIT_WRITING_S;
          else 
            if( stop_reading ) 
              next_state = WAIT_READIND_S;
        end

      WAIT_READIND_S:
        begin
          if( !stop_reading ) 
            next_state = READ_S;
        end
      
      WAIT_WRITING_S:
        begin
          if( all_is_finished ) 
            next_state = IDLE_S;
        end
    endcase
  end


Всё, наш контроллер LCD готов.

Настройка U-boot


В прошлой статье я писал, что включение fpga2sdram интерфейса необходимо выполнять в U-boot. Иначе при транзакции чтения система полностью зависнет. Для этого нужно добавить в окружение следующие строки:
u-boot-env.txt
...
fpgadata=0x10000000
fpgafile=/lib/firmware/fpga/fpga.rbf
fpgaboot=setenv fpga2sdram_handoff 0x3fff; ext2load mmc 0:2 ${fpgadata} ${fpgafile}; fpga load 0 ${fpgadata} ${filesize}
bridge_enable_handoff=mw $fpgaintf ${fpgaintf_handoff}; go $fpga2sdram_apply; mw $fpga2sdram ${fpga2sdram_handoff}; mw $axibridge ${axibridge_handoff}; mw $l3remap ${l3remap_handoff} 
bootcmd=run fpgaboot; run bridge_enable_handoff; run mmcboot
...

Отладка


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

Вот так выглядят транзакции от CPU:


Мы видим запись команд 0x29, 0x11, 0x36 и данных 0xE8. Всё верно.

А так выглядят транзакции от FPGA:


И тут тоже всё так, как мы и планировали.

Ура! У нас получился LCD-контроллер в FPGA.
Спасибо тем, кто дочитал до конца! Удачи!

Полезные ссылки


Исходники на github
Девайс, на котором проводились все работы
Документация по написанию framebuffer драйверов
Документация по ILI9341

Замечание по поводу предыдущей статьи


В прошлой статье я измерял пропускную способность интерфейса fpga2sdram.
К сожалению, мной была допущена ошибка. А именно — клок PLL был задан равным 125 МГц, а не 25 МГц, как есть на самом деле.
Из-за этого коэффициенты умножителя и делителя для PLL были рассчитаны неверно.
В итоге DDR3 работал на 66 МГц вместо положенных 333 МГц.

При правильных коэффициентах и ширине интерфейса в 256 бит пропускная способность составляет около 16-17 Гбит/c, что соответствует теоретической для интерфейса DDR3 с шириной 32 бита и частотой 333 МГц.

Приношу свои извинения!

Маленький опрос


Хочется узнать мнение сообщества. Если не сложно, прошу проголосовать.

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

Куда лучше публиковать статьи, подобные этой — на Хабрахабр или на Geektimes?

НТЦ Метротек
78,00
Разработка и производство Ethernet устройств etc.
Поделиться публикацией

Похожие публикации

Комментарии 10

    +2
    а как же видео с doom'ом?
      +3
      А вот:
      +2
      Вы ещё раз убедитесь, что разработка под FPGA и написание драйверов под Linux дело очень простое, но интересное.

      Сильное заявление. Задачи на стыке схемотехники и софта одни из самых трудных, иначе 3D- сканер был бы уже в каждом смартфоне. С другой стороны, да, схема взаимодействия «через общую память» помогает снизить сложность (относительно синхронного взаимодействия). Спасибо, очень полезная статья.
        +1
        Если система большая и/или комплексная, то в ней, конечно, будет присутствовать сложность.
        Независимо от того, драйвер ли это, модуль в FPGA, многопоточная программа или реализация хитрого алгоритма на OpenCL.

        Я имел в виду, что небольшие драйвера и модули в FPGA (примерно такие, какие я рассматриваю в своих статьях)
        на самом деле намного более простые, чем они кажутся большинству людей.

        И разработка в этом направлении не сложнее, чем в других отраслях.

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

        Спасибо за оценку!

        +2
        Добавьте вариант к голосованию — мне все равно где эти статьи будут, мне нравится!
          +2
          Большое спасибо!

          Просто хочется публиковать статьи там, где они будут более востребованы.
            +1
            Однозначно здесь, для этого Хабр и попилили на части — чтобы здесь статьи похардкорнее пошли. И ваша статья вполне к месту, это ж железяки + Linux, мечта гика.
            0
              +5
              Вообще конечно печально, что теперь железные статьи размазаны на два сайта. Как и их аудитория. Я думал хабр стал только софтверным, а тут такой вот шедевр появился. Теперь не знаю что и делать.
            –3
            Публиковать надо там, где есть хаб FPGA, то есть на Гиктаймс. Вопрос о том, кто это такой одаренный перенес этот хаб туда, где размещают «информацию преимущественно научно-популярного характера и не относящуюся к программированию, разработке и другим тематикам», к делу, считаю, не относится.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое