虽说是自制显卡,其实我自制的这玩意儿暂时只是一个能驱动ILI9341屏幕并显示一些东西的板子。
真正要说到显卡的话,其实还需要具备以下的功能才算实现:
1、接受绘制命令,根据绘制参数进行绘制,并且有自己的指令集用于编码着色器,然后应用流水线运行着色器指令进行并行计算。
2、具备一定的存储空间,可存储材质纹理,图元、图形,以及着色器指令。
3、使用一部分存储空间用于存储帧缓存。
我用的FPGA设备是Cyclone II EP2C5T144C8开发板,有关它的资料请看我的上一篇帖子(点击此处查看帖子)。
这款板子虽然很烂,但很容易买到,也不贵,正好适合入门(然而我还没找到能成功安装到这个板子上的SDRAM模块,所以我买了一个别的、能安装SDRAM模块的FPGA板子)。
因为SDRAM的控制又是另外一回事儿了,这里先考虑使用SPI接口对ILI9341进行控制,使其能够显示东西。
而其实这一步可以参考我之前使用STM32F103对ILI9341的SPI接口的控制过程(点击此处查看帖子)来仿写FPGA的版本。
所以,我依然还是在使用我的EP2C的板子,对该显示器进行控制。其实开头的那张图就是一个控制的效果——把像素的x和y坐标乘以4后,输出到该像素对应的红色和绿色通道上。
不过有一点,这个显示器我用的是SPI接口的,而SPI接口的情况下它没有VSYNC信号。一个解决的办法是发送0x45命令来获取扫描线的位置,然后避开扫描线进行刷新,就可以避免撕裂效果。
我修改了它显示的内容——计算像素颜色的时候,让输入的坐标x和y随着帧计数的增加而偏移;与此同时蓝色的部分我给它应用一个抖动算法使用的抖动矩阵。这样的话,虽然不太好形容它所显示出来的画面,但看视频就一目了然了。
注意看上面那个视频(如果你的浏览器是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 : in std_logic;
i_Reset : in std_logic;
o_Active : out std_logic;
i_SDO_Write : in std_logic;
i_SDO_Byte : in std_logic_vector(7 downto 0);
o_SDO_Full : out std_logic;
i_SDI_Read : in std_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 : in std_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,然后我发现只要我保持碰触着它,它就能正常运作)。
|