在看这里具体案例前,请确认已经了解了上一章节描述的概念。
.
案例列表如下:
1. cpld控制pin脚(LED闪烁)
2. mcu通过cpld控制pin脚(LED闪烁)
3. mcu通过寄存器方式操作cpld;
4. mcu通过ahb转apb方式操作cpld外设;
5. cpld实现一个简单的UartTx的外设;
6. dma在cpld中的使用(以adc为例);
7. cpld中如何使用ram;
8. 其他更多样例(不再详解,自行看代码注释)
.
这里所有用到的案例例程,如果需要,请从给出的链接网盘下获取。
另外,所有例程请务必参照描述自己重建一份。一则,通过自己手动创建,能更清晰学习整个操作过程;二则,网盘上的例程都是基于当时版本的,在最新版本上未必兼容;三则,需要自己修改的往往是很少的代码(框架和工程都是自动生成,自己只需要填充自己的逻辑),不用费力气去复制整个工程。
.
以下进入正题。
一、cpld控制pin脚(LED闪烁)
功能描述:在cpld里驱动两个led灯的闪烁。
这是第一个使用到mcu+cpld联合编程的样例,也是最简单的一个。在这个样例中,mcu部分屏蔽掉对led灯的控制,然后在cpld里来驱动两个led灯的闪烁。
通过这个样例,会了解到:
- 1.从头到尾新建一个cpld工程;
- 2.cpld中如何和外部pin脚关联;
- 3.如何通过clk来驱动led的闪烁。
准备工作:复制上一份example工程作为试验工程;然后屏蔽掉main()函数中对TestGpio()的调用,改为while(1);放开platformio.ini文件中的ip_name和logic_dir两个选项(该动作为:开启自定义logic功能),如图:
![](http://www.agm-support.com.cn/wp-content/uploads/2024/08/image-124-1024x122.png)
操作步骤:
就是上节讲到的四步骤,先复习下:
- 1.在VE文件里配置引脚关系;
- 2.建立cpld空工程(使用prepare LOGIC命令)并编写逻辑;
- 3.Quartus下进行工程转换(和综合);
- 4.Supra下编译出最终的logic.bin;
1. 在VE文件里配置引脚关系
可以先删除掉其他的引脚定义,只保留时钟配置。然后添加cpld控制Pin脚的定义如下:
![](http://www.agm-support.com.cn/wp-content/uploads/2024/08/image-125.png)
这里添加的 LED_D3 PIN_32:OUTPUT #LED2 解释如下:
- LED_D3: 这里是cpld中用到的信号名称
- PIN_32: 正常的管脚名字
- OUTPUT: 对cpld来说是输出的方向。除了OUTPUT,还有INPUT和INOUT。通过这些关键字限制信号的输出方向。
- #: VE中的注释符。
- 整行的意思是:定义一个LED_D3的信号(cpld的信号)绑定到Pin32脚上。当LED_D3高低变化时,Pin32将跟着变化。
2. 建立cpld空工程(使用prepare LOGIC命令)并编写逻辑
点击VSCode左边栏的 prepare LOGIC,则自动生成空的cpld工程。该cpld工程在logic文件夹下。(如果不熟悉,请回看上节描述)
此时,打开quartus工程,可以看到 user_ip 中包含了上边定义的 LED_D3和LED_D2 两项:
![](http://www.agm-support.com.cn/wp-content/uploads/2024/08/image-126.png)
然后,可以在该user_ip的模块外边(即:endmodule 之后)添加led模块代码如下:
![](http://www.agm-support.com.cn/wp-content/uploads/2024/08/image-127.png)
此时,user_ip.v文件里其实有两个moudle: user_ip 和 led。
接下来,让led模块在user_ip模块内实例化,让两模块关联起来即可。
即:在 user_ip的module内添加led的实例化 如下:
![](http://www.agm-support.com.cn/wp-content/uploads/2024/08/image-128.png)
这里的操作过程如果还是不理解,请自行从网盘上下载样例工程。
链接:https://pan.baidu.com/s/1wcBnqnray7bu4IURDIoDDQ?pwd=1205 里边的 \cpld-fpga文档\logic样例\1.led灯闪烁。
3. Quartus下进行工程转换
点击Quartus下的tools->TCL Scripts(如果不熟悉,参考上节介绍),等待转换编译完成。
4. Supra下编译出最终的logic.bin
再打开Supra软件,对该cpld工程进行编译(如果不熟悉,参考上节介绍),等待编译完成。
到这里,全部编译logic的动作完成。
接下来,对该logic进行烧录、对code进行编译并烧录。开发板重新上电后,就可以看到2个led灯的闪烁了。
回顾:
1.对用户逻辑来说,总入口就是user_ip的module;
2.这里新建了一个led的module,里边使用了user_ip的两个led信号和sysclk信号;
3.led内部对sysclk进行了分频的使用;
.
二、mcu通过cpld控制pin脚
功能描述:mcu通过gpio控制cpld的信号,然后再传递到Pin脚。实现“mcu <-> cpld <-> Pin”的控制逻辑。
之前单纯用mcu时,是在VE文件里直接定义gpio到Pin的映射。定义该映射后,mcu来操作gpio的高低,就控制了LED灯的亮灭。
现在是在之前的控制中,增加一个cpld的环节。
具体样例中,用mcu的gpio(gpio4_1)来输入信号到cpld,然后cpld把这个信号关联到2个pin上(开发板的2个led),然后mcu中切换gpio4_1时,两个led灯交替闪烁。
通过这个案例,可以了解到:
- 1.mcu的信号如何传递到cpld
- 2.cpld如何反向控制mcu信号
在mcu和cpld的交互中,两者之间传递信息的方式大致分为四种:
1. mcu传递信号给cpld;(如:mcu的gpio传递高低信号到cpld)
2. cpld传递信号给mcu;(如:cpld对mcu产生中断信号)
3. mcu读写寄存器方式操作cpld数据;
4. 不建议,cpld做为主设备对mcu进行读写。
也就是说,在mcu和cpld交互中,cpld更像一个“外设”。
其中,前两种较为简单。后两种要使用AHB总线来操作。这个案例中讨论的就是这里的方式1.(方式2也会顺便讨论)
新建工程的过程,这里不再描述。只描述需要注意的改动点。
1. VE中定义3个信号:
- LED_TEST1 PIN_31 # LED3
- LED_TEST2 PIN_32 # LED2
- GPIO4_1 iocvt_chn:OUTPUT
- 其中前两个是cpld中信号到引脚,第三个是mcu到cpld信号。
2. prepare LOGIC生成cpld工程后,可以看到user_ip接口处的定义:
- inout LED_TEST1,
- inout LED_TEST2,
- input iocvt_chn_out_data,
- input iocvt_chn_out_en,
3.在user_ip.v中关联下信号:
- assign LED_TEST1 = iocvt_chn_out_data;
- assign LED_TEST2 = !iocvt_chn_out_data;
- 这样,
- 当iocvt_chn_out_data为高时,LED_TEST1为高,即PIN_31为高,led3亮
- 当iocvt_chn_out_data为高时,LED_TEST2为低,即PIN_32为低,led2灭
4.在mcu代码里,初始化gpio4_1并toggle切换:
- SYS_EnableAPBClock(APB_MASK_GPIO4);
- GPIO_SetOutput(GPIO4, GPIO_BIT1);
- while (1) {
- UTIL_IdleUs(200e3);
- GPIO_Toggle(GPIO4, GPIO_BIT1);
- }
然后就可以看到一个gpio4_1控制两个led灯交替闪烁。
这里的样例,也可以从网盘中拿到。
链接:https://pan.baidu.com/s/1wcBnqnray7bu4IURDIoDDQ?pwd=1205 里边的 \cpld-fpga文档\logic样例\3.mcu信号到cpld到pin
回顾:
1.在VE里定义了2个cpld信号到Pin脚,1个mcu到cpld的信号;
2.在cpld里,直接对mcu过来的一个信号,绑定到2个cpld信号上,传递给Pin脚;
.
描述完mcu控制cpld信号,顺便描述下cpld控制mcu信号。
这种方式和1相近,只不过是反向。
可以在mcu中定义gpio4_2为输入并使能中断,则cpld中设置信号高低时,将触发mcu的gpio中断。(当然,这里也可以用local int,但还未整理,需要自己尝试实现)
1. 在VE中定义信号:
- GPIO4_2 iocvt_chn:INPUT
- 表示,mcu的gpio(gpio4_2)信号将来源于cpld的iocvt_chn。
2. 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对应变化。
mcu端gpio部分不再描述。较为简单,也不再举例。
.
三、mcu通过寄存器方式操作cpld
从这个案例开始,需要了解AHB总线的基础知识了。如果对AHB总线不是很了解,可以自行百度再学习下。这里有个比较好的讲解:https://blog.csdn.net/weixin_46022434/article/details/104987905
这里只描述用到的部分:
在AG32芯片内部,可以认为:cpu、ram、cpld、dma,这四个部分是挂在ahb总线下的。其他的外设都是挂在apb总线下的。
挂在ahb下的4部分,cpu、dma、cpld 这3个会成为master端,可能来抢占ahb总线。
在真实使用中,cpld经常被用于slave端,有点类似于“外设”,但却是挂在ahb下的。既然cpld是挂在ahb下的,那么它的接口就是要符合标准ahb访问协议的。
那么,当mcu来访问cpld的数据时,cpld就要根据这个接口来做对应的处理。
整个访问过程分为两部分:
1. mcu如何访问cpld的“寄存器”;
2. 当mcu访问时,cpld如何判别及响应;
分别描述。
1. mcu如何访问cpld
这部分很简单。
AG32在对地址编码中,cpld的地址区间设定为:0x60000000 ~ 0x7FFFFFFF
就像ram的地址区间是 0x20000000 ~ 0x20020000,flash的地址区间是0x80000000 ~ 0x800x0000 一样。
当mcu对大于0x60000000这个区间内的地址访问时,解码器会自动丢给cpld的接口。这时mcu就相当于访问了cpld的“寄存器”。
mcu是全局寻址,对这个空间的访问和对ram(0x20000000起)空间的访问是一样的方式,在C代码中,可以这样写:
读cpld:int cpRdReg = *((int *)0x60000000);
写cpld:*((int *)0x60000004) = cpWtReg;
Mcu端读写cpld,直接通过上述语句就可以了。理解和操作上都比较简单。
2. cpld如何判别及响应
这部分描述起来比较麻烦。总体是:cpld被ahb总线接口触发,并且回应要遵循ahb总线协议。
当上述mcu读写动作发生时,AHB总线会把动作拆解为读写信号,传递到analog_ip.v(新版本是user_ip.v)的接口,用户cpld程序需要响应该信号。
以下,以写动作 *((int *)0x60000004) = cpWtReg 为例,描述cpld端会发生的事情。
回顾下analog_ip.v中的接口部分:
![](http://www.agm-support.com.cn/wp-content/uploads/2024/08/image-129.png)
其中slave_ahb_开头的一组信号,是cpld作为主端时用的,暂时不用理会。
Mem_ahb_开头的一组信号,是cpld作为从端使用的。
当mcu有读写操作时,mem_ahb_这组信号将发生变化。
几个信号的概述(更详细的讲解请自行百度):
- 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信号后,开始数据传输。
大致如下图(无等待类型的图):
![](http://www.agm-support.com.cn/wp-content/uploads/2024/08/image-130.png)
比如,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 [0:0] isNewAction = 0;
reg [31:0] hwdata_reg; //定义32位的hwdata_reg
always @(posedge sys_clock) begin //clk上升沿触发
if (mem_ahb_htrans == 2'b10) //NONSEQ状态,第一次传输
begin
isNewAction <= 1;
end
if (mem_ahb_htrans == 2'b00 && //IDLE状态,真正开始写
isNewAction && //新动作
mem_ahb_hwrite && //写 (0 读,1 写)
mem_ahb_haddr[23:0] == 'h00) //写地址为0x60000000(cpld内部用相对偏移)。
begin
hwdata_reg <= mem_ahb_hwdata; //把收到的数据给到hwdata_reg
isNewAction <= 0;
end
end//这个过程,是把mcu写进来的数据收到hwdata_reg中
-----------------------------------------------
这部分的实例代码,请参考网盘上获取:
链接:https://pan.baidu.com/s/1wcBnqnray7bu4IURDIoDDQ?pwd=1205 里边的 \cpld-fpga文档\logic样例\5.mcu读写cpld寄存器
注意:这里展示的,仅仅是基于AHB总线上的数据交互。
在实际应用中,比如要实现一个串口之类的,往往是慢速设备,这些是要挂载到apb上的。慢速设备要经过ahb到apb的bridge,才能最终使用。请继续往下看。
.
四、mcu通过ahb转apb方式操作cpld外设
上节讲述了mcu和cpld之间交互数据的实现方式。
但数据是在ahb层面的响应,慢速设备不能直接使用。慢速设备需要ahb转为apb后,使用apb的信号来交互。这种情况,转变为mcu和apb 之间的交互。
mcu和apb之间的交互,相比mcu和aph之间的交互,多了一层ahb到apb的转换。 这个转换是借助于“桥”ahb2apb.v模块来实现的(在example/analog下找该.v文件)。
该模块:输入是ahb的一组信号,输出是apb的一组信号。使用如下图:
![](http://www.agm-support.com.cn/wp-content/uploads/2024/08/image-131.png)
![](http://www.agm-support.com.cn/wp-content/uploads/2024/08/image-132.png)
如果实现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位
以下展示在apb下如何实现跟mcu的交互,仍以ahb的两个寄存器为例。
1. 首先需要增加ahb转apb的信号关联;
如上图。
Ahb2apb模块会把ahb信号转换为apb信号。接下来操作apb信号即可。
2. 在转换后的apb信号中,实现写和读的操作。
mcu读操作时:
比如,mcu要读0x60000004的寄存器:
mcu端直接C语言这样调用:int cpRdReg = *((int *)0x60000004);
cpld端,可以根据以上信号做如下处理:
----------------------------------------------
//mcu的读操作响应
//mcu端用C语言:int value = *((int *)0x60000004);
reg [31:0] ardata_reg; //定义32位的hrdata_reg
always @(posedge apb_clock) begin //clk上升沿触发
if (!apb_pwrite && //读 (0 读,1 写)
apb_penable && //是否准备好
apb_paddr[11:0] == ADDR_READ) //读地址为0x60000004(cpld内部用相对偏移)。
begin
ardata_reg <= awdata_reg; //把另一准备好的数据给到hrdata_reg
end
end
assign apb_prdata = ardata_reg; //绑定hrdata_reg到读的数据线上-----------------------------------------------
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] == ADDR_WRITE)//写地址为0x60000000(cpld内部用相对偏移)。
begin
TestLedCtrl <= !TestLedCtrl; //led灯切换,表示有写操作触发
awdata_reg <= apb_pwdata; //把收到的数据给到hwdata_reg
end
end
//这个过程,是把mcu写进来的数据收到hwdata_reg中-----------------------------------------------
这个功能实现后,其实是个简单的“空外设”。可以用它做为实现复杂功能外设的基础。
这部分的实例代码,请参考网盘上获取:
链接:https://pan.baidu.com/s/1wcBnqnray7bu4IURDIoDDQ?pwd=1205 里边的 \cpld-fpga文档\logic样例\5.mcu读写cpld寄存器
.
样例展示到这里,mcu和cpld的交互上:交互信号、跟ahb交互数据、跟apb交互数据,基本的交互通路已经建立。
接下来,用户根据自己的需求,在cpld中交互到数据后,编写自己需要的功能即可。
.
五、cpld实现一个UartTx“外设”
功能描述:用cpld实现一个Uart的tx功能,并且认为是一个“外设”。(注意:只是个简单的tx功能,没有rx,也没有更多的功能)
这个例程是对mcu于apb交互的进一步演练。在功能方面,包括:设置寄存器,读取寄存器。
它的cpld实现逻辑,跟ADC样例思路是相同的,都是先ahb转apb,然后实例化外设,mcu写数据时apb接收后处理,mcu读数据时apb触发后处理。
读的时候,是读的串口的state状态;
写的时候,会把写的数据继续丢给uart模块处理(转化为IO的高低波形输出)
mcu端,在while(1)里边:
查询cpld的写状态,当状态合适时,发数据给cpld,cpld根据时序转换为波形输出到定义的Pin。
更多细节,请参考cpld工程中的代码和注释。
这部分的实例代码,请参考网盘上获取:
链接:https://pan.baidu.com/s/1wcBnqnray7bu4IURDIoDDQ?pwd=1205 里边的 \cpld-fpga文档\logic样例\6.UartTx例程
.
这个样例完全理解完,就可以尝试读ADC的代码了。
在example/analog工程,展示了ADC模块做为外设,与mcu之间的数据交互(ADC采集后的数据,被mcu读取)。
相当于:ADC硬核+ADC的cpld逻辑,实现了一个完整的“ADC外设”。
关于ADC/DAC/CMP的代码解析,请从网盘上获取:
链接:https://pan.baidu.com/s/1wcBnqnray7bu4IURDIoDDQ?pwd=1205 里边的 \cpld-fpga文档\analog代码分析。
.
六、dma在cpld中的使用
cpld中实现DMA的逻辑:
- Mcu为master,cpld为slave,mcu对cpld的交互方式为存取寄存器的方式;
- mcu中配置好DMA(读取cpld中准备好的数据);
- cpld中准备好数据后,触发dma信号,dma自动搬运到mcu指定的ram;
- 搬运一次后,dma给cpld一个clear信号,完成一次dma搬运;
- 等到cpld中再次准备好数据,将再次触发dma信号,重复3和4;
对于cpld来说,mcu来读取数据和dma来读取数据,是一致的,cpld无从区分到底是mcu来读还是dma来读。
dma来读取时,只是每次读完后会多给cpld一个clear信号。
这部分的实例代码,请参考网盘上获取:
链接:https://pan.baidu.com/s/1wcBnqnray7bu4IURDIoDDQ?pwd=1205 里边的 \cpld-fpga文档\logic样例\7.cpld中配合实现mcu的dma读取
在这个样例中,展示了两部分代码:
- mcu中,配置dma读取;为了测试,mcu会在另一地址给cpld写数据;
- cpld中,会对mcu写进来的数据缓存,缓存后触发dma的信号,让dma来读取数据。而dma从cpld里读取数据后会给cpld一个clear信号,标志一次dma交互完成。
更多信息参考工程 源码及注释。
.
七、cpld中如何使用ram
1. cpld本身有自带的ram:
cpld内部自带ram,为4个M9K块,每个M9K大小为8192 bits。
(即:4个M9K总空间为4K bytes)
更详细信息,请参考《AGRV2K_Rev2.0.pdf》中的说明:
![](http://www.agm-support.com.cn/wp-content/uploads/2024/08/image-133.png)
使用M9K时,直接在Quartus下创建IP就可以了。
2. 使用mcu这边的ram:
除了cpld自带的内存,cpld还可以使用mcu的内存sram。(这部分使用不太容易,如果cpld基础薄弱,不建议使用)
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 中可以看到定义如图:
![](http://www.agm-support.com.cn/wp-content/uploads/2024/08/image-134.png)
如果只用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
![](http://www.agm-support.com.cn/wp-content/uploads/2024/08/image-135.png)
.
如果想共用mcu的一段sram,在这段ram中,比如cpld只写,mcu只读,也可以。只需要mcu那边强制访问这片区域就行了。
.
八、更多样例
这些样例不再解释,具体请从网盘下获取。
链接:https://pan.baidu.com/s/1wcBnqnray7bu4IURDIoDDQ?pwd=1205 里边的 \cpld-fpga文档\logic样例\
样例包括:
1.ADC如何从0创建的例程
2.adc+cpld控制led灯的例程
3.adc+spiFull的例程
.