手把手教你如何在HDL中调用并使用MAX10的ADC(单通道)

作者: fanyao-367090
上传时间为: 2015-12-10 08:53 AM
2015-12-10
阅读:

一直对MAX10的ADC和内置FLASH情有独钟,认为是提高系统集成度的利器。因此这次MAX10的开发板实用也集中在这两个领域。

因为altera对于MAX10中ADC的官方使用例程都是使用nios,用C去基于地址总线的操作,但我不熟悉nios,也不想用软核(应该说是厌 恶……),于是尝试基于HDL语言(本例中基于verilog)的直接例化调用方法,用纯正的FPGA方法去使用这个ADC~

过程真是让人百般纠结啊……因为MAX10本身硬件特点的原因,导致PLL的使用非常麻烦,最终采用了一种非常奇特的手段才实现了将PLL连接到ADC的工作…

声明:由于altera官方板的模拟通道需要外部输入,因此这次的例程使用的是艾瑞(arrow)的BeMicro Max10开发板。只是芯片封装变成了484管脚的BGA,芯片型号一样是MAX10M08,因此官方板的做法也是一样的。

下面正式开始!

首先,来看一下艾瑞的这块板子:



在图上左上角,有一个Photo Resistor(光敏电阻),连到了MAX10的模拟输入上,这次我们的主角就是它。
MAX10芯片的左边,还有一排8个LED灯,需要用它们来实现ADC的可视化输出。

基本实验内容如下:首先将光敏电阻接到内部的ADC上,然后启动ADC采集光敏电阻的输出,最后读取该输出并将12位数据的高8位通过8个LED输出。

首先新建一个工程,芯片选型为10M08DAF484C8GES。从芯片编号中可得知是484管脚的,C8速度等级,ES工程样片

然后新生成一个.v文件,取名随意,但是要注意最好模块名和文件名统一,这样便于管理。然后在fire选项卡里将其设置为顶层文件,如下图所示:



第一步,例化PLL,生成时钟:

然后在窗口右边的IP核选项栏上找到PLL的IP核




简单来说,altera将ADC部分的时钟输入有两个,为了说清楚,分别介绍:



第一是内核时钟,也就是真正驱动ADC进行采样的时钟。名称为:adc_pll_clock。
该时钟的频率只能有固定的几个选项。ADC部分内置一个分频器,将输入的时钟进行分频,最终得到1M的采样工作频率。

该时钟在MAX10芯片的硬件上就连接到PLL1和PLL3的c0输出口(芯片制造的时候就已经连上了),注意是“PLL 1 和 3 的C0”,用户无法更改。

但是,蛋疼的是!虽然硬件连上了,还是在例化的时候手动用wire去连接……这不是多此一举吗?哎,不吐槽了……继续……

484管脚的M08,具有两个PLL(PLL1和PLL2),只有PLL1可以为ADC提供时钟,PLL2是怎么都不行的(试过一万遍了,泪……)

最蛋疼的事情来了……由于MAX10的PLL,只能接收来自于自己这一侧bank的时钟输入管脚,无法连接到其他bank的时钟输入管脚。


而艾瑞的开发板,正好时钟输入管脚位于PLL2所在的那一侧。
也就是无论你如何例化PLL,只要在Pin Planner中将时钟输入管脚锁定,那就一定会使用PLL2而不是PLL1,而PLL2是死活都连不上ADC的……(这里省去血泪史一万字)

那么,要怎么办呢?难道ADC就不用了?当然不会了……
经高手点播,得到一个超级大法——“PLL级联”,话说估计也是有人被这个搞疯了……
因为有且只有两个PLL,因此只需要例化两个PLL,那没有连到外部时钟输入的,就一定是PLL1了!(我咋没想到呢……)

好,言归正传,ADC模块输入的第二个时钟是外设时钟:peripheral clock。这个时钟指的是外部与ADC进行数据交互的逻辑时钟,由用户自己定义,无任何幺蛾子。(太好了……)

接着,生成两个PLL,相关的例子“小马哈”的帖子中已经说得很清楚啦,这里不在复述。

那么这两个PLL该怎么接呢?例子如下:


其中,上一个PLL的输入连接的是外部输入管脚,也就是i_clk_50m。然后进过PLL输出一个内部50M时钟clk_50m。
下一个PLL的输入就是这个50M时钟,然后将c0输出设置为10M,输出ADC专用的10M时钟adc_clk_10m。各自的LOCK信号都引出来。
到此PLL例化结束,接下来要例化ADC了。

第二步 例化ADC模块,并连接信号

在Q2窗口的右边,找到ADC模块:


双击,启动Qsys,开始ADC模块的设置,具体设置如下图所示


说明一下,首先因为是HDL直接调用,因此什么总线接口啊这些花花肠子就不用搞了,直接选择最纯粹的ADC控制核,也就是ADC core only
然后,将内置DEBUG去掉。想研究的同学可以自己打开试试,我这次没用,而且资源消耗非常大,需要4K多的LE了……
第二项是时钟选择,注意,这里选择的是ADC内核时钟的频率,也就是adc_pll_clock的频率。这个选项会直接影响ADC内部分频器的设置,因此一定要注意,和最终输入的时钟保持一致。
前面例化PLL的时候就将ADC时钟设置为10M,因此这里设置为10M。
第三项是选择参考源,默认是外置的,这里选择内置,这样可以减少些幺蛾子(我也不知道开发板外接参考源没……)。
第四项就是选择要用的通道。08上一共有0到16再加一个TSD总共18个通道。其中0是专用模拟通道,1到16都是和IO复用的。TSD就是内置的温度传感器。
光敏电阻接到了第四通道,因此这里将第四通道点上。
好了,ADC配置完成,点右下角的generate hdl,生成最终的可调用文件,最后finish就可以。随后弹出来的两个窗口,点OK就行。

这里需要注意的是,无论是PLL的IP核,还是ADC的IP核,都要确认.qip文件已经加入了工程。具体步奏如下:
首先点开Q2上方的这个按钮:

,进入工程文件管理界面。
一定要确认刚刚生成的PLL和ADC的.qip文件已经加入到了工程,例子如下:


可以看到我添加了两个PLL的qip文件和一个ADC的qip文件。其中名字是自己起的。
这样在file选项卡上就能看到例化后的.v文件,便于我们查找信号定义及例化相关IP:


上图中的MAX10_ADC.qip就是加入工程的ADC的IP核。其下的所有.v就是例化的具体文件,其中MAX10_ADC.v是IP核的顶层文件。我们将其双击打开:


上图就是该ADC的所有信号定义。如何在verilog中进行例化在这不啰嗦了,以下针对信号进行逐个说明:

input wire adc_pll_clock_clk, // 这就是ADC的内核时钟, 只能接受PLL1和PLL3的c0输出。
input wire adc_pll_locked_export, // 内核时钟的PLL锁定信号,连接相应的PLL的LOCK信号即可。
input wire clock_clk, // ADC模块的外设时钟,用户自定义,与ADC模块相连的逻辑的时钟
input wire command_valid, // 输入命令有效标志
input wire [4:0] command_channel, // 输入命令:通道号
input wire command_startofpacket, // 用于nios总线交互的信号,在纯core模式下忽略
input wire command_endofpacket, // 用于nios总线交互的信号,在纯core模式下忽略
output wire command_ready, // 输入命令生效反馈信号
input wire reset_sink_reset_n, // 外设复位信号
output wire response_valid, // ADC数据有效信号
output wire [4:0] response_channel, // 指明当前有效的数据属于哪一个通道
output wire [11:0] response_data, // ADC的数据输出,12位
output wire response_startofpacket, // 用于nios总线交互的信号,在纯core模式下忽略
output wire response_endofpacket // 用于nios总线交互的信号,在纯core模式下忽略

信号说明结束,我的例化例子如下:
wire adc_rep_vaild;
wire[4:0] adc_rep_channel;
wire[11:0] adc_rep_data;
MAX10_ADC ADC(
.adc_pll_clock_clk(adc_clk_10m), // adc_pll_clock.clk
.adc_pll_locked_export(adc_clk_lock), // adc_pll_locked.export
.clock_clk(clk_50m), // clock.clk

.command_valid(1'b1), // command.valid
.command_channel(5'd4), // .channel
.command_startofpacket(), // .startofpacket
.command_endofpacket(), // .endofpacket
.command_ready(adc_ready), // .ready

.reset_sink_reset_n(1'b1), // reset_sink.reset_n
.response_valid(adc_rep_vaild), // response.valid
.response_channel(adc_rep_channel), // .channel
.response_data(adc_rep_data), // .data
.response_startofpacket(), // .startofpacket
.response_endofpacket() // .endofpacket
);
说明一下,我将信号做了分组,前三个为时钟,中间为命令,下面为反馈。
首先将ADC内核时钟连到PLL1的C0上,时钟输入为10M,然后将PLL1是lock信号连接到内核LOCK输入上。
然后将我的用户逻辑时钟clk_50m连接到ADC的外设时钟上,我和ADC模块的接口时钟是50M。

然后将命令有效一直置一,这样命令就会一直发送,ADC模块就工作在1M的最大采样率下。
通道一直置为4通道,也就是光敏电阻所在的通道,这样ADC就工作在单通道最大采样率下。

将复位信号置一,模块永不复位。然后将ADC的反馈有效,反馈通道,反馈数据都接出来,供后面逻辑进行数据判别。

用户逻辑如下:
always @(posedge clk_50m) begin
if(rst_n == 1'b0)begin
o_led <= 8'h0;
end
else if ((adc_rep_vaild == 1'b1)&(adc_rep_channel == 5'd4)) begin
o_led <= adc_rep_data[11:4];
end
end

是的,非常简单,当ADC的输出有效,且输出的正是第4通道的数据的时候,将数据的高8位输出到外面的8个LED上。


最后 查看结果
综合,设定管脚,下载。在不同光照条件下,8个led的状态不同,因此的确得到了光敏电阻通过ADC输出的数字量:





总结:
在这个例子中,实现了基于HDL语言的直接例化ADC对于单个通道模拟量的采集,由于484管脚的M08芯片具有两个PLL,因此过程比较坎坷

用144管脚的M08,由于只有一个PLL1,因此不存在为ADC选择PLL的问题,直接使用PLL1的C0就可以了

直接用HDL调用ADC,只用例化core就可以,采样时序和采样率那是HDL语言的拿手功夫了。

希望能对希望使用MAX10片内ADC的同学有所帮助~

接下来将会进行双通道的实验。完成后就可以说完全掌握这个ADC的HDL使用方法了。

ADC的例化说明.jpg (66.09 KB, 下载次数: 6)

全部评论 ()
条评论
写评论

创建讨论帖子

登录 后参与评论
系统提示