0xAA55 发表于 2019-12-15 00:42:43

【FPGA】FPGA开发学习之路——自制显卡



虽说是自制显卡,其实我自制的这玩意儿暂时只是一个能驱动ILI9341屏幕并显示一些东西的板子。

真正要说到显卡的话,其实还需要具备以下的功能才算实现:
1、接受绘制命令,根据绘制参数进行绘制,并且有自己的指令集用于编码着色器,然后应用流水线运行着色器指令进行并行计算。
2、具备一定的存储空间,可存储材质纹理,图元、图形,以及着色器指令。
3、使用一部分存储空间用于存储帧缓存。

我用的FPGA设备是Cyclone II EP2C5T144C8开发板,有关它的资料请看我的上一篇帖子(点击此处查看帖子)。



这款板子虽然很烂,但很容易买到,也不贵,正好适合入门(然而我还没找到能成功安装到这个板子上的SDRAM模块,所以我买了一个别的、能安装SDRAM模块的FPGA板子)。



因为SDRAM的控制又是另外一回事儿了,这里先考虑使用SPI接口对ILI9341进行控制,使其能够显示东西。

而其实这一步可以参考我之前使用STM32F103对ILI9341的SPI接口的控制过程(点击此处查看帖子)来仿写FPGA的版本。

misc/ili9341_stm32.mp4

所以,我依然还是在使用我的EP2C的板子,对该显示器进行控制。其实开头的那张图就是一个控制的效果——把像素的x和y坐标乘以4后,输出到该像素对应的红色和绿色通道上。

不过有一点,这个显示器我用的是SPI接口的,而SPI接口的情况下它没有VSYNC信号。一个解决的办法是发送0x45命令来获取扫描线的位置,然后避开扫描线进行刷新,就可以避免撕裂效果。

我修改了它显示的内容——计算像素颜色的时候,让输入的坐标x和y随着帧计数的增加而偏移;与此同时蓝色的部分我给它应用一个抖动算法使用的抖动矩阵。这样的话,虽然不太好形容它所显示出来的画面,但看视频就一目了然了。

misc/ili9341_cycloneii.mp4

注意看上面那个视频(如果你的浏览器是IE,你可能看不了这个视频),我在ILI9341的SPI接口的SDI Pin上直接焊了一个上拉电阻。这个在文末有我的解释。



不过,我的EP2C板子使用的时钟是50 MHz的,而这样的话,SPI的波特率最大只能到25 MBaud/s,传输的速度只能到3.125 MB/s,对于18位色(每个像素要传输24个bit)的320x240分辨率的屏幕而言,这样的传输速度无法达到理想的帧速,最快只能是40 Hz。

所以我们需要让时钟变快,从而更快地驱动整个系统,让它能有更高的波特率。这个时候,我们可以通过配置PLL来设置倍频。

我使用MegaWizard Plug-In Manager的ALTPLL的配置向导来配置PLL锁相环。这个东西其实我一开始是在看别人的视频的时候发现的,但一直没找到打开它的入口。后来有个老哥告诉我,要先创建一个傀儡框图,往里面拖拽ALTPLL组件,然后向导就会冒出来了。真麻烦。

所以是在新建文件的地方,创建这个Block Diagram文件,然后随便起个名保存。



然后会看到很多点点。这些点点让我不禁想到了VB6拖控件设计UI的过程。在这里点右键->Insert->Symbol...



从Library里面展开折叠,进megafunctions->IO->altpll,然后点OK。



之后,随便拖拽一下,向导就会冒出来。



这里我把“Which device speed grade will you be using?”选了“Any”,然后“What is the frequency of inclk0 input?”我的板子是50 MHz的,所以选一样的。



然后在下一步里,根据自己的需求勾选这些选项。但其实它们不是很重要,因为你可以在自己的VHDL文件里,使用component块来访问它的所有输入和输出。



这里的“pllena”用于启用或停用PLL,“areset”用于异步重置PLL,“pfdena”是PLL内部的用于维持相位和频率稳定精确的一个组件,可以启用或禁用它。



我们只有一个输入的时钟,这里不启用第二个以及更多的了。

这里设置PLL倍频。我把我的设置为300 MHz。其实这个地方是个十分误导人的地方——你设计的器件很可能无法在单个时钟周期里(1/300000000秒内)完成一个步骤的处理。



ALTPLL能输出不止一个的时钟信号,不过暂时我们似乎不需要(反正需要的时候,自己在自己的components里把它写上就行了)



这里生成netlist用于使用其它EDA仿真器的时候能更好地仿真,不过似乎不需要。



这里勾上pll.cmp,然后点finish完成。



在这里找到pll.cmp,用notepad++打开,你可以看到里面的component声明,可以把它复制进自己的代码里用于调用ALTPLL。







不过,经过我的实际试验,我发现其实300 MHz的PLL并不能成功带动这一整个系统运行起来——看起来是FPGA内部的累加计数器速度跟不上。事实上,经过我查阅多方资料,我发现其实24位以上的计数器要想达到200 MHz以上的累加速度,是需要特殊处理的,而不仅仅是一个signal counter : integer := 0;就可以做得到的。一个典型的做法是使用6个4位加法器迭代,当加法器溢出的时候,下一个加法器累加一次,然后下个加法器溢出的时候,下下个加法器累加一次。因为使用了多个加法器,所以对比单个加法器的AND XOR串,它可以流水线化——通过提前几个周期来让下一个加法器累加,来实现及时的计数。

不过其实,这是我第一次尝试写VHDL的SPI Master。所以频率上不去也是因为一些经验的缺乏。library ieee;
use ieee.std_logic_1164.ALL;
use IEEE.std_logic_unsigned.all;
use ieee.numeric_std.all;

entity spi_master is
generic
(
    g_CLKs_Per_Bit: INTEGER := 2;
       g_CPOL : std_logic := '0';
       g_CPHA : std_logic := '0'
);
port
(
    i_Clk      : instd_logic;
       i_Reset      : instd_logic;
    o_Active   : out std_logic;
    i_SDO_Write: instd_logic;
       i_SDO_Byte   : instd_logic_vector(7 downto 0);
       o_SDO_Full   : out std_logic;
       i_SDI_Read   : instd_logic;
    o_SDI_Byte   : out std_logic_vector(7 downto 0);
       o_SDI_Full   : out std_logic;
       o_Bit_Index: out integer;
       
       o_NSS_Pin : out std_logic;
       o_SCK_Pin : out std_logic;
       o_SDO_Pin : out std_logic;
       i_SDI_Pin : instd_logic
);
end spi_master;

architecture rtl of spi_master is

type t_SM_Main is (s_Idle, s_Active);
signal r_SM_Main : t_SM_Main := s_Idle;

signal r_Clk_Count    : integer range 0 to g_CLKs_Per_Bit - 1:= 0;
constant c_Clk_Middle : integer := g_CLKs_Per_Bit / 2;

signal r_Bit_Index    : integer range 0 to 7;-- 8 Bits Total
signal r_SDO_Buf      : std_logic_vector(7 downto 0) := (others => '0');
signal r_SDI_Buf      : std_logic_vector(7 downto 0) := (others => '0');
signal r_SDI_Ready    : std_logic_vector(7 downto 0) := (others => '0');
signal r_SDO_Full   : std_logic := '0';
signal r_SDI_Full   : std_logic := '0';

signal r_NSS_Pin   : std_logic := '0';
signal r_SDO_Pin   : std_logic := '0';
signal r_SCK_Pin   : std_logic := '0';

begin

o_SDI_Byte <= r_SDI_Ready;
o_SDO_Full <= r_SDO_Full;
o_SDI_Full <= r_SDI_Full;
o_Bit_Index <= r_bit_Index;

o_SDO_Pin <= r_SDO_Pin;
o_SCK_Pin <= r_SCK_Pin xor g_CPOL;
o_NSS_Pin <= r_NSS_Pin;

p_SPI_XCV: process (i_Clk)
begin
    if rising_edge(i_Clk) then
           if i_Reset = '1' then
             r_SM_Main <= s_Idle;
                  r_Clk_Count <= 0;
                  r_Bit_Index <= 7;
                  r_SDI_Full <= '0';
                  r_SDO_Full <= '0';
                  o_Active <= '0';
                  r_NSS_Pin <= '1';
      r_SCK_Pin <= '0';
                  
                else case r_SM_Main is
                  
                  when s_Idle =>
                       r_Clk_Count <= 0;
                       r_Bit_Index <= 7;
          r_SCK_Pin <= '0';
                       r_SDO_Full <= '0';
                       
                       if i_SDI_Read = '1' then
                           r_SDI_Full <= '0';
                       end if;
                       
                  if i_SDO_Write = '1' then
                           r_SDO_Full <= '1';
                      r_NSS_Pin <= '0';
                                o_Active <= '1';
                                r_SDO_Buf <= i_SDO_Byte;
                           r_SM_Main <= s_Active;
                           if g_CPHA = '0' then
                             r_SDO_Pin <= i_SDO_Byte(r_Bit_Index);
                           end if;
                       else
                      r_NSS_Pin <= '1';
                                o_Active <= '0';
                           r_SM_Main <= s_Idle;
                       end if;
                  
      when s_Active =>
                  r_NSS_Pin <= '0';
                       
                       if i_SDI_Read = '1' then
                           r_SDI_Full <= '0';
                       end if;
         
                       if g_CPHA = '0' then
                                if r_Clk_Count < c_Clk_Middle then
                                  r_SDO_Pin <= r_SDO_Buf(r_Bit_Index);
                                  r_SCK_Pin <= '0';
                                  r_Clk_Count <= r_Clk_Count + 1;
                                elsif r_Clk_Count < g_CLKs_Per_Bit - 1 then
                                  r_SCK_Pin <= '1';
                                  r_Clk_Count <= r_Clk_Count + 1;
                                elsif r_Clk_Count = g_CLKs_Per_Bit - 1 then
                                  r_SCK_Pin <= '1';
                                  r_SDI_Buf(r_Bit_Index) <= i_SDI_Pin;
                                  r_Clk_Count <= 0;
                                  if r_Bit_Index > 0 then
                                  r_Bit_Index <= r_Bit_Index - 1;
                                  else
                                  r_Bit_Index <= 7;
                                       r_SDI_Ready <= r_SDI_Buf;
                                       r_SDI_Full <= '1';
                        if i_SDO_Write = '1' then
                                           r_SDO_Full <= '1';
                                           r_SDO_Buf <= i_SDO_Byte;
                                       else
                                           r_SM_Main <= s_Idle;
                            r_SDO_Full <= '0';
                                       end if;
                                  end if;
                                end if;
                               
                       else -- g_CPHA = '1'
                          
                                if r_Clk_Count < c_Clk_Middle then
                                  r_SCK_Pin <= '0';
                                  r_Clk_Count <= r_Clk_Count + 1;
                                elsif r_Clk_Count < g_CLKs_Per_Bit - 1 then
                                  r_SCK_Pin <= '1';
                                  r_SDO_Pin <= r_SDO_Buf(r_Bit_Index);
                                  r_Clk_Count <= r_Clk_Count + 1;
                                elsif r_Clk_Count = g_CLKs_Per_Bit - 1 then
                                  r_SCK_Pin <= '1';
                                  r_SDO_Pin <= r_SDO_Buf(r_Bit_Index);
                                  r_SDI_Buf(r_Bit_Index) <= i_SDI_Pin;
                                  r_Clk_Count <= 0;
                                  if r_Bit_Index > 0 then
                                  r_Bit_Index <= r_Bit_Index - 1;
                                  else
                                  r_Bit_Index <= 7;
                                       r_SDI_Ready <= r_SDI_Buf;
                                       r_SDI_Full <= '1';
                        if i_SDO_Write = '1' then
                                           r_SDO_Full <= '1';
                                           r_SDO_Buf <= i_SDO_Byte;
                                       else
                                           r_SM_Main <= s_Idle;
                            r_SDO_Full <= '0';
                                       end if;
                                  end if;
                                end if;
                               
                       end if; -- g_CPHA
                  
                  when others =>
                       r_SM_Main <= s_Idle;
               
      end case; end if;
    end if;
end process p_SPI_XCV;

end rtl;(知道为什么这个代码的缩进那么丑吗?因为在Quartus II里面,TAB是3个字符长度的,而在这里它是8个字符)

在我实际使用200 MHz的时钟频率来驱动我的SPI Master的时候,其实我看到了显示屏亮了起来,但它迅速就从正常的画面变为了完全的白色。这是因为我用的FPGA板子的所有的pin都是直接连接到FPGA芯片上的。群成员“大能猫”告诉我“你这个可能是GPIO口的IO速率不够,接一个上拉电阻应该就好了。要是我的话,直接来个1000Ω。”

我试了一下,是真的,有电阻它就能正常工作(实际上在找到这么个电阻前,我是用手指碰触SCK Pin,然后我发现只要我保持碰触着它,它就能正常运作)。

misc/pullup_resistor.mp4

大能猫 发表于 2019-12-15 02:38:38

本帖最后由 大能猫 于 2019-12-15 03:12 编辑

硬件日常玄学问题(捂脸
然后,emmm。。。不是IO口速率不够而是IO口驱动能力不够。因为IO口以高频率输出时(比如这里的CLK)相当于一个高频交流电,所以容抗往往会比较大,需要更大的电流才能正确地输出电平(不知道这样讲准不准确
这也是为什么频率越高的东西一般电压越低而且功耗会高
这里上拉电阻主要起到的作用就是补偿电流,因为当IO口电平由低变高时,IO口推挽输出的两个晶体管上管导通(上管与VDD连),下管关闭(下管与GND连),IO口驱动电流不够的原因就是上管导通时电流不够(很大原因是IO口一般是通过一根限流电阻与VDD连接的,这样能保证IO口直接接地也不会烧芯片),导致无法在IO口由高变低前让IO口的电压达到逻辑1的阈值电压(上面说了高频下容抗大,导线和晶体管都是有电容的),这时候如果在导线上接一根到VDD的电阻,就相当于跟IO口内部的电阻并联了,就增加了输入电流。
然后还有一个问题就是上拉电阻不会影响低电平么,因为照理说在IO口由高变低时是导线电容放电过程,而上拉电阻在这过程中间也会补偿放电电流,如果推挽输出的下管跟上管一样接了一根电阻不会使电平跳变前无法下降到逻辑0的阈值电压么
答案是一般不会,因为上管接电阻是防止驱动电流过大烧芯片,而下管一般来说没必要再做限流(感觉有点类似于保险丝接火线不接零线的想法),可能也跟制片的时候地的面积一般会比VDD大很多所以能过的电流 也大(这个原因是猜的),还有个可能的原因是如果下端晶体管电阻过大,而与其连接的IO口阻抗又太小会影响低电平的输出(导致IO口上的分压高于逻辑0的阈值电压)。总之下管一般电阻是比较小的,所以不存在IO口放电能力不够的情况

PS:最后,一般所有的空格和编码问题我都建议用vscode(逃

0xAA55 发表于 2019-12-15 08:15:51

我其实也思考过如果接了个上拉电阻,它是不是就拉不低了。或者,我这里接了的上拉电阻,是不是起到的作用是让波形更“居中”从而被辨识。但,根据大能猫老兄的回答,应该是让眼图更睁开了的样子。或者说,可能因为下拉电阻阻值小,这个上拉电阻并没有让它线性增加输出电平吧。不过我觉得下拉电阻小的原因应该是这个芯片想让自己的脚支持开漏方式的多设备共享某一总线的功能。

忽然想到,“眼图”这个词用日语怎么说呢?“眼睛”我知道是「目」(め、me),“图”则是「図」(ず、zu),连起来是「目図」,读音听起来像“妹子”……

大能猫 发表于 2019-12-15 21:22:06

0xAA55 发表于 2019-12-15 08:15
我其实也思考过如果接了个上拉电阻,它是不是就拉不低了。或者,我这里接了的上拉电阻,是不是起到的作用是 ...

应该是这样
开漏的话也是原因,如果总线上的阻抗不够就会导致电平拉不下来,也是类似我上面说的第三个理由(不过现在的IO口似乎OD模式和推挽输出模式是会接到不同电路上的)

0xAA55 发表于 2019-12-15 21:26:57

大能猫 发表于 2019-12-15 21:22
应该是这样
开漏的话也是原因,如果总线上的阻抗不够就会导致电平拉不下来,也是类似我上面说的第三个理 ...

我的FPGA板子的Pin似乎不能配置开漏还是推拉的模式

大能猫 发表于 2019-12-16 03:15:47

0xAA55 发表于 2019-12-15 21:26
我的FPGA板子的Pin似乎不能配置开漏还是推拉的模式

OD一个比较主要的用途是转换电平标准,比如一个IO口输出5V为高,要接到3.3V为高的IO口上电压太大了,就可以用OD输出,然后上拉一根电阻到3.3V电源。但fpga本身IO就支持大多数的电平标准,这可能是不支持OD模式的主要原因吧

0xAA55 发表于 2019-12-16 04:39:48

大能猫 发表于 2019-12-16 03:15
OD一个比较主要的用途是转换电平标准,比如一个IO口输出5V为高,要接到3.3V为高的IO口上电压太大了,就可 ...

原来如此,我其实虽然知道它们的差异,但我并不知道OD兼容不同电平的情况。

大能猫大佬果然见多识广

大能猫 发表于 2019-12-19 04:37:38

0xAA55 发表于 2019-12-16 04:39
原来如此,我其实虽然知道它们的差异,但我并不知道OD兼容不同电平的情况。

大能猫大佬果然见多识广 ...

过。。。过奖了,就是一些所谓的常识,不知道只是因为没听说过一旦听说过很自然就可以理解为什么,不值一夸(被A5大佬夸有点不知所措)
页: [1]
查看完整版本: 【FPGA】FPGA开发学习之路——自制显卡