CPLD使用基础
一、时钟配置与使用:
1、外部晶振与内部振荡器的使用:
mcu 和cpld 联合编程时,整颗芯片需要一颗外部晶振。
(芯片有内部振荡器,但误差较大,校准后5%以内误差,参后续介绍)
该晶振是mcu 和cpld 共用的,(没必要为cpld 再单独提供一颗晶振)。
晶振可以是有源的,也可以是无源的。
【注:这里的外部晶振配置,跟单纯使用MCU 是一样的】
如果是无源晶体,频率限制在4M~16M。要接到芯片的OSC_IN/OUT 引脚。然后VE 中直接定义主频多少M 即可。如:
(这是目前开发板上使用的配置)
如果是有源晶体,频率无限制。根据接入点分为两种情况:
1) 如果接入到OSC_IN 引脚:
ve 里定义同上(修改HSECLK 项的值为有源晶振频率)。
同时要在platformio.ini 里增加配置:
-D BOARD_HSE_BYPASS=SYS_HSE_BYPASS_ON
注:上边的-D 代表宏定义的意思,后边跟的内容是宏定义的内容。-D 后可加
空格,可不加空格。等号两边不能加空格。
2) 如果接入到别的IO 引脚(如PIN_2):
VE 配置中,除了配置HSECLK 项外,还需要配置PLL_CLKIN 项,如图:
同时,需要在platformio.ini 里增加配置:-D BOARD_HSE_BYPASS=SYS_HSE_NONE
注:上边的-D 代表宏定义的意思,后边跟的内容是宏定义的内容。-D 后可加
空格,可不加空格。等号两边不能加空格。
如果使用内部振荡器:
校准后精度大约在5%以内,节省成本且对时钟要求不高的话可以使用。
使用方式:
在VE 里增加:”PLL_CLKIN PIN_OSC”,如下图:
(注:不用配置HSECLK 项)
同时在platformio.ini 里增加配置:-D BOARD_HSE_BYPASS=SYS_HSE_NONE
注:上边的-D 代表宏定义的意思,后边跟的内容是宏定义的内容。-D 后
可加空格,可不加空格。等号两边不能加空格。
注:自动校准目前有以下使用限制:
a)逻辑部分要压缩,platformio.ini 中配置board_logic.compress = true
b)校准动作是在烧录时进行的。
烧录时,需要使用swd 方式且通过我们的软件烧录,uart 不支持。
(即:出厂烧录不支持uart 方式)
目前测试过jlink 和dap 校准结果都还不错,但是也出现过一个客户使用其他
烧录器校准结果差很多。
(对一个全新的或是wipe 过后的芯片烧录会看到校准信息)
2、PLL 倍频及分频:
整颗芯片只有一个PLL 倍频模块(mcu 和cpld 共用)。
倍频分频操作是封装在系统内部的(用户无须也不能控制这个时钟树)。
实现原理:
A. 系统会根据所有用到的频率项(mcu 和cpld 要用到的全部频率),计算出他们
的最小公倍数。该数值就是要倍频到的目标值;
B. 以外部时钟作为输入,PLL 倍频到这个目标值,然后再以这个目标值为基准,分
频给mcu 各外设和cpld 来使用。
C. 倍频和分频,无须开发者关注。
开发者只要设置好自己需要的各个时钟频率即可。
开发者可设置的频率分为mcu 部分和cpld 部分。
mcu 部分,只需要关注系统主频。
主频是在VE 里通过SYSCLK 项配置,该主频是mcu 的工作频率。
外设频率则基于这个主频再分频(参考各个外设的驱动部分)。
cpld 部分,cpld 最多可以输入5 路不同频率的时钟。
默认情况下,cpld 工程接口中输入到cpld 的sys_clock,就是跟mcu 同频的SYSCLK
系统时钟(由VE 决定多少M)。
Bus_clock 则是在SYSCLK 基础上进行分频的另一路时钟(其实就是后续的PLLCLK3)
Bus_clock 在VE 中频率定义(必须是SYSCLK 的整数倍分频):
如果ve 里没有定义BUSCLK,则bus_clock 和sys_clock 同频。
bus_clock 是为了防止cpld 部分速度跟不上sysclk 而设定的。
cpld 中除了这两路(其实就是0 路和第3 路),还有3 路可以使用。
3、cpld 可用的时钟(除去SYSCLK 的另外4 路):
Cpld 的时钟除了以上输入的sys_clock,还有4 路可以独立使用。
参考《AGRV2K_逻辑设置.pdf》,如下图:
这里的PLLCLK1、PLLCLK2、PLLCLK3、PLLCLK4 就是可使用的独立时钟。
注意:当mcu 中使用USB 时,PLLCLK1 自动给了USB,不能再使用;当mcu 中使
用了MAC 时,PLLCLK2 自动给了MAC,不能再使用。另外,上述的BUSCLK 对应的
是这里的PLLCLK3。如果用了BUSCLK 的名字,这里的PLLCLK3 就不能再用。
这里整理下5 路时钟:
PLLCLK0:就是SYSCLK (名字使用SYSCLK)
PLLCLK1:开USB 时,这路时钟给USB 用(60M),不开USB 时给用户用;
PLLCLK2:开MAC 时,这路时钟给MAC 用(25/50M),不开MAC 时给用户用;
PLLCLK3:用BUSCLK 时(只能是sysclk 整数分频)不能用PLLCLK3,否则可用;
PLLCLK4:独立给用户使用;
以PLLCLK3 和PLLCLK4 为例,说明怎么使用该时钟。
在VE 里配置如下:
PLLCLK3 40 # 40MHz
PLLCLK4 60 # 60MHz
PLL_CLKOUT3 pll_clk3
PLL_CLKOUT4 pll_clk4
则可定义pllclk3 为40M 输入,pllclk4 为60M 输入。
在生成的cpld 入口处,分别对应信号pll_clk3 和pll_clk4,如图:
输入的时钟,可以跟sys_clock 一样使用。
4、几个时钟的设置限制及计算方式:
上边提到的倍频后PLL 目标值,其数值关系需要满足:
A. PLL=HSE*X/Y,X,Y 皆为整数
B. PLL 小于1200MHZ。
C. 所有的设置频率必须能被这个最终PLL 整除。
举例:mcu 主频100M,系统用了MAC(50M),系统用了USB(60M),cpld 自定义了
PLLCLK3 为80M,cpld 自定义了PLLCLK4 为60M。则,PLL 目标值就是100\50\60\80\60的最小公倍数,为1200M。
如果使用到一些特殊频率,则可以调整其他频率往这个特殊频率的倍数上来凑。
(如果配置后不满足这里的条件,编译时会报错)
5、cpld 可运行的最高频率:
mcu 的运行最高频率是248M。而cpld 中没有标准的最高频率。
最大能跑多少M,取决于cpld 里的设计。
如果是逻辑电路,则不存在时钟的概念。
如果是时序电路,则看设计中门电路的复杂程度。如果跑100M 的时钟,每个上升
沿之间就是10 纳秒,在设计时,要保证10 纳秒内对应的动作能全部执行完。
如果是简单电路,一般是可以跑到200M 以上。
二、mcu 和cpld 的交互:
cpld 工程创建及编译的操作流程,参考文档《AG32 下fpga 和cpld 的使用入门》
在工程中,用户逻辑部分编写是从analog_ip.v 的接口下开始的。
mcu 和cpld 之间的交互,可以分为:
1. mcu 传递信号给cpld;(如mcu 的gpio 传递高低信号到cpld)
2. cpld 传递信号给mcu;(如:对mcu 产生中断信号)
3. mcu 读写数据到cpld;
4. 不建议,cpld 做为主设备对mcu 写。
也就是说,在mcu 和cpld 交互中,cpld 更像一个外设。
其中,前两种较为简单。后两种要使用AHB 总线来操作。
下边针对四种情况分别说明:
1. mcu 传递信号给cpld;
这种使用较简单。步骤如下:
在ve 中定义信号:
GPIO4_1 iocvt_chn:OUTPUT
表示,用mcu 的gpio(gpio4_1)来输入信号到cpld。
然后,prepare LOGIC 工程后,可以看到analog_ip.v 接口中的信号:
input iocvt_chn_out_data,
input iocvt_chn_out_en,
这里的iocvt_chn_out_data,就是对接到mcu 的gpio4_1 的信号。
当控制mcu 的gpio4_1 高低切换时,cpld 中的iocvt_chn_out_data,会对应来变化。
具体样例,可以参考网盘“logic 样例\3.mcu 信号到cpld 到pin”的样例,该样例中,
展示了mcu 控制cpld 继续控制led 的过程。
除了gpio 信号输出到cpld,其他比如pwm 输出信号等,都可以输入到cpld。
2. cpld 传递信号给mcu;
这种方式和1 相近,只不过是反向。
可以在mcu 中定义gpio4_2 为输入并使能中断,则cpld 中设置信号高低时,将触发
mcu 的中断。
在VE 中定义信号:
GPIO4_2 iocvt_chn:INPUT
表示,用mcu 的gpio(gpio4_2)信号来源于cpld 的iocvt_chn。
然后,prepare LOGIC 工程后,可以看到analog_ip.v 接口中的信号:
output iocvt_chn_in_data,
这里的iocvt_chn_in_data,就是对接到mcu 的gpio4_2 的信号。
当cpld 中控制iocvt_chn_in_data 信号高低时,mcu 中的gpio4_2 对应变化。
这里不再举例。
3. mcu 读写数据到cpld;
在地址设计中,cpld 的地址区间是:0x60000000 ~ 0x7FFFFFFF
当mcu 对这个区间内的地址访问时,相当于访问了cpld 的“寄存器”。
mcu 是全局寻址,对这个空间的访问和对ram(0x20000000 起)空间的访问是一样
的方式,在C 代码中,可以这样写:
读cpld:int cpRdReg = *((int *)0x60000000);
写cpld:*((int *)0x60000004) = cpWtReg;
Mcu 端读写cpld 较为简单,直接通过上述语句就可以了。
当mcu 读写动作发生时,cpld 端是如何反应的?
当上述mcu 读写动作发生时,AHB 总线会把动作拆解为读写信号,传递到analog_ip.v的接口,用户cpld 程序需要响应该信号。
以下,以写动作*((int *)0x60000004) = cpWtReg 为例,描述cpld 端会发生的事情。
回顾下analog_ip.v 中的接口部分:
其中slave_ahb_开头的一组信号,是cpld 作为主端时用的,暂时不用理会。
Mem_ahb_开头的一组信号,是cpld 作为从端使用的。
当mcu 有读写操作时,mem_ahb_这组信号将发生变化。
这部分是遵循标准的AHB 总线协议的。如果对AHB 总线印象不深,请自行百度。
可参考的讲解:https://blog.csdn.net/weixin_46022434/article/details/104987905
几个信号的概述(更详细的讲解请自行百度):
Ahb_htrans: 当前传输类型(00: IDLE、01: BUSY、10: NONSEQ、11: SEQ)
Ahb_ready:mcu 读时要mcu 要准备好cpld 才会写
Ahb_hwrite: 要读还是要写(1 为写,0 为读)
Ahb_haddr[32]: 要操作的地址
Ahb_hsize:transfer 的大小,以字节为单位
Ahb_hburst:批量传输
Ahb_hwdata[32]:写的数据,32 位
Ahb_hreadyout:输出信号,mcu 写时cpld 是否准备好
Ahb_hresp:输出信号,响应信号(OK、retry、error、split)
Ahb_hrdata[32]:读的数据,32 位
根据AHB 时序,在一次传输中,cpld(slave 端)会先拿到addr 地址,读或写的标记,然后交互ready 信号后,开始数据传输。
大致如下图(无等待类型的图):
比如,mcu 要读0x60000004 的寄存器:
mcu 端直接C 语言这样调用:int cpRdReg = *((int *)0x60000004);
cpld 端,可以根据以上信号做如下处理:
———————————————-
//mcu 的读操作响应
//mcu 端用C 语言:int value = *((int *)0x60000004);
reg [31:0] hrdata_reg; //定义32 位的hrdata_reg
always @(posedge sys_clock) begin //clk 上升沿触发
if (mem_ahb_htrans == 2’b10 && //NONSEQ 状态,第一次传输
mem_ahb_hready && //master 已ready,可以给数据线写入了
!mem_ahb_hwrite && //读(0 读,1 写)
mem_ahb_haddr[23:0] == ‘h04) //读地址为0x60000004(cpld 用相对偏移)
begin
hrdata_reg <= hwdata_reg; //把另一准备好的数据给到hrdata_reg
end
end
assign mem_ahb_hrdata = hrdata_reg; //绑定hrdata_reg 到读的数据线上
———————————————–
以上代码,加入到analog_ip.v 的module 下,就可以完成cpld 对mcu 读动作的响应。
比如,mcu 要写0x60000000 的寄存器:
mcu 端直接C 语言这样调用:*((int *)0x60000000) = value;
cpld 端,可以根据以上信号做如下处理:
———————————————-
//mcu 的写操作响应
//mcu 端用C 语言:*((int *)0x60000000) = value;
reg [31:0] hwdata_reg; //定义32 位的hwdata_reg
always @(posedge sys_clock) begin //clk 上升沿触发
if (mem_ahb_htrans == 2’b00 && //IDLE 状态
mem_ahb_hreadyout && //cpld 已ready 状态,ahb 上数据可以写过来
mem_ahb_hwrite && //写(0 读,1 写)
mem_ahb_haddr[23:0] == ‘h00) //写地址为0x60000000(cpld 用相对偏移)
begin
hwdata_reg <= mem_ahb_hwdata; //把收到的数据给到hwdata_reg
end
end
//这个过程,是把mcu 写进来的数据收到hwdata_reg 中
———————————————–
这部分的实例代码,请参考网盘上cpld 样例工程《5.mcu 读写cpld 寄存器》。
注意:这里展示的,仅仅是基于AHB 总线上的数据交互。
在实际应用中,比如要实现一个串口之类的,往往是慢速设备,这些是要挂载到apb
上的。慢速设备要经过ahb 到apb 的bridge,才能最终使用。请继续往下看。
三、mcu 通过ahb 转apb 后的数据交互:
上节讲述了mcu 和cpld 之间交互数据的实现方式。
但数据是在ahb 层面的响应,慢速设备不能直接使用。
慢速设备需要ahb 转为apb 后,使用apb 的信号来交互。这种情况,转变为mcu 和apb之间的交互。
mcu 和apb 之间的交互,相比mcu 和aph 之间的交互,多了一层ahb 到apb 的转换。
这个转换是借助于ahb2apb.v 模块来实现的(在example/analog 下找该.v 文件)。
该模块:输入是ahb 的一组信号,输出是apb 的一组信号。使用如下图:
如果实现mcu 和apb 的交互,则需要操作的是转换后的这组apb 信号。
关于apb 总线的使用,更多信息请自行百度。
这里只是简述下apb 信号列表(与ahb 略有不同):
apb_psel:片选
apb_penable:表示传输进入第二周期(准备好了读/写)
apb_pwrite:传输方向(1-写;0-读)
apb_paddr[32]:地址总线,要操作的地址
apb_pwdata[32]:写的数据,32 位
apb_prdata[32]:读的数据,32 位
mcu 写操作时:
比如,mcu 要写0x60000000 的寄存器:
mcu 端直接C 语言这样调用:*((int *)0x60000000) = value;
cpld 端,可以根据以上信号做如下处理:
———————————————-
//mcu 的写操作响应
//mcu 端用C 语言:*((int *)0x60000000) = value;
reg [31:0] awdata_reg; //定义32 位的hwdata_reg
always @(posedge apb_clock) begin //clk 上升沿触发
if (apb_pwrite && //写(0 读,1 写)
apb_penable && //是否准备好
apb_paddr[11:0] == ‘h00) //写地址为0x60000000(cpld 内部用相对偏移)
begin
awdata_reg <= apb_pwdata; //把收到的数据给到hwdata_reg
end
end
//这个过程,是把mcu 写进来的数据收到hwdata_reg 中
———————————————–
这个功能实现后,其实是个简单的“空外设”。可以用它做为实现复杂功能外设的基础。
这部分的实例代码,请参考网盘上cpld 样例工程《5.mcu 读写cpld 寄存器》。
样例展示到这里,mcu 和cpld 的交互上:交互信号、跟ahb 交互数据、跟apb 交互数
据,基本的交互通路已经建立。
接下来,用户根据自己的需求,在cpld 中交互到数据后,编写自己需要的功能即可。
四、ADC 样例与UartTx 样例
这里借助ADC 样例和UartTx 样例,展示下更多的使用方法。
ADC 和UartTx,都是接到apb 的。
功能方面,都包括:设置寄存器,读取寄存器。
ADC 样例:
SDK 安装后的example/analog 工程,展示了ADC 模块做为外设,与mcu 之间的数据交
互(ADC 采集后的数据,被mcu 读取)。
这里的ADC 模块,在cpld 里做的工作,是把串行数据转换成并行数据。
因为AG32 芯片中集成的ADC 硬核是串行数据输出的,而mcu 访问数据都是并行的。
为了实现mcu端访问数据的统一,这里增加了ADC 的cpld 代码,实现该串行转并行的功能。
相当于:ADC 硬核+ADC 的cpld 逻辑,实现了一个完整的“ADC 外设”。
ADC 的用户cpld 代码,都是在analog_ip.v 中实现的。
直接以analog_ip.v 中的实现来说明:
1. 在analog_ip 的接口下边,首先是ahb2apb 的信号转换。
转换后,后续的ADC/DAC/CMP,都是以apb 信号线来操作。
2. 定义出信号线数组,用于连接实例化的6 个对象:3 个ADC/2 个DAC/1 个CMP;
这个里边,wire [PER_CNT-1:0] select 是定义片选,注意它的取位,这里取值后的数
组,分别对应到’h000,’h1000,’h2000,’h3000,’h4000,’h5000。
其他数组,都分别对应6 个实例化以后的连线。
3. 然后用for 循环genarate 实例化ADC/DAC/CMP;
包含3 个ADC,2 个DAC,1 个CMP。
实例化时,连线都对应到上边定义的数组中去了。
4. 接下来的pr_select,是记录当前片选是哪个。
索引值:0 ADC0;1 ADC1;2 ADC2;3 DAC0;4 DAC1;5 CMP
5. 接下来的apb_prdata,
6. 再接下来,就是3 个模块ADC/DAC/CMP 各自的实现了。
具体内容不再解析。
注意几个点:
A. mcu 中设置寄存器时,都是值下来后,在ADC/DAC/CMP 中缓存到reg 中;
如:0x60005004 是CMP 的CHANNAL 寄存器,mcu 对这里寄存器设置值时,
最终cpld 里执行:
B. mcu 中要获取的值,也类似传递。
UartTx 样例:
这个样例比ADC 样例简单些。
这个样例中,实现一个串口功能(只有TX 功能,固定为115200 频率)。
对mcu 来说,它也是cpld 实现的一个“外设”。
它的cpld 实现逻辑,跟上边的ADC 样例思路是相同的,都是先ahb 转apb,然后实例
化外设,mcu 写数据时apb 接收后处理,mcu 读数据时apb 触发后处理。
读的时候,是读的串口的state 状态;
写的时候,会把写的数据继续丢给uart 模块处理(转化为IO 的高低波形输出)
mcu 端,在while(1)里边:
查询cpld 的写状态,当状态合适时,发数据给cpld,cpld 根据时序转换为波形输
出到定义的PIN。
更多细节,请参考cpld 工程中的代码和注释。
该工程位于网盘上“logic 样例/6.UartTx 例程”。
五、dma 在cpld 中的使用;
cpld 中实现DMA 的逻辑:
1. Mcu 为master,cpld 为slave,mcu 对cpld 的交互方式为存取寄存器的方式;
2. mcu 中配置好DMA(读取cpld 中准备好的数据);
3. cpld 中准备好数据后,触发dma 信号,dma 自动搬运到mcu 指定的ram;
4. 搬运一次后,dma 给cpld 一个clear 信号,完成一次dma 搬运;
5. 等到cpld 中再次准备好数据,将再次触发dma 信号,重复3 和4;
对于cpld 来说,mcu 来读取数据和dma 来读取数据,是一致的。
dma 来读取时,只是每次读完后会多给cpld 一个clear 信号。
更多细节,请参考网盘上《7.cpld 中配合实现mcu 的dma 读取》部分的样例。
在这个样例中,展示了两部分代码:
1. mcu 中,配置dma 读取;为了测试,mcu 会在另一地址给cpld 写数据;
2. cpld 中,会对mcu 写进来的数据缓存,缓存后触发dma 的信号,让dma 来读取数
据。而dma 从cpld 里读取数据后会给cpld 一个clear 信号,标志一次dma 交互完成。
六、cpld 中使用ram:
cpld 本身有自带的ram,为4 个M9K 块,每个M9K 大小为8192 bits。
(即:4 个M9K 总空间为4K bytes)
更详细信息,请参考《AGRV2K_Rev2.0.pdf》中的说明。
除了cpld 自带的内存,cpld 还可以使用mcu 的内存sram。
AG32 整个芯片系列,内存sram 大小都是128K。
如果mcu 用不了128K,希望分一些给cpld 来用,比如,分出来32K 给cpld。可以按照如下方式设置:
1. 限制mcu 的使用,让mcu 只使用前96K;
限制mcu 对ram 的使用,需要修改ld 配置(分散加载相关)来实现。
在路径:AgRV_pio\packages\framework-agrv_sdk\misc\devices 下,在文件
AgRV2K_mem.ld 中可以看到定义如图:
如果只用96K,则修改上边的SRAM_SIZE = 96K 即可。
修改文件并保存后,需要重启VSCODE 工程,让设置项使能。
2. cpld 中对后32K 的使用;
cpld 使用后32K,起始地址是从0x20000000 + 96K 的地址开始。
即:从0x20018000 开始,长度32K,到0x20020000 结束。
cpld 中对于sram 的寻址方式和mcu 相同。
cpld 对sram 的读写,请参考:
SDK 下的examples\custom_ip\logic\ram2ahb.v 和ahb2ram.v
七、一些小技巧:
如何定义信号量数组:
如果cpld 里想用信号量数组,并使每个元素对应到不同PIN,可以在ve 里定义如下:
mcu_a[0] PIN_31
mcu_a[1] PIN_32
mcu_a[2] PIN_33
mcu_a[3] PIN_34
…
如图:
那么,prepare LOGIC 后,将在自动生成的cpld 框架.v 接口里,表示如下:
这里就表现为数组的形式了。