模式
MCU+CPLD 联合编程(案例描述)
MCU+CPLD 联合编程(概念及流程)
MCU+CPLD 联合编程(案例描述)
在看这里具体案例前,请确认已经了解了上一章节描述的概念。
案例列表如下:
- CPLD控制Pin脚(LED闪烁)
- MCU通过CPLD控制Pin脚(LED闪烁)
- MCU通过寄存器方式操作CPLD;
- MCU通过AHB转APB方式操作CPLD外设;
- CPLD实现一个简单的UartTx的外设;
- DMA在CPLD中的使用(以ADC为例);
- CPLD中如何使用RAM;
- 其他更多样例(不再详解,自行看代码注释)
这里所有用到的案例例程,如果需要,请从给出的链接网盘下获取。
另外,所有例程请务必参照描述自己重建一份。一则,通过自己手动创建,能更清晰学习整个操作过程;二则,网盘上的例程都是基于当时版本的,在最新版本上未必兼容;三则,需要自己修改的往往是很少的代码(框架和工程都是自动生成,自己只需要填充自己的逻辑),不用费力气去复制整个工程。
以下进入正题。
一、CPLD控制Pin脚(LED闪烁)
功能描述:在CPLD里驱动两个LED灯的闪烁。
这是第一个使用到MCU+CPLD联合编程的样例,也是最简单的一个。在这个样例中,MCU部分屏蔽掉对LED灯的控制,然后在CPLD里来驱动两个LED灯的闪烁。
通过这个样例,会了解到:
- 从头到尾新建一个CPLD工程;
- CPLD中如何和外部Pin脚关联;
- 如何通过CLK来驱动LED的闪烁。
准备工作:复制上一份example工程作为试验工程;然后屏蔽掉main()
函数中对TestGpio()
的调用,改为while(1)
;放开platformio.ini
文件中的ip_name
和logic_dir
两个选项(该动作为:开启自定义logic功能),如图:
操作步骤:
就是上节讲到的四步骤,先复习下:
- 在VE文件里配置引脚关系;
- 建立CPLD空工程(使用
prepare LOGIC
命令)并编写逻辑; - Quartus下进行工程转换(和综合);
- Supra下编译出最终的
logic.bin
;
1. 在VE文件里配置引脚关系
可以先删除掉其他的引脚定义,只保留时钟配置。然后添加CPLD控制Pin脚的定义如下:
plaintext
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
两项:
然后,可以在该user_ip
的模块外边(即:endmodule
之后)添加led
模块代码如下:
verilog
module led (
input sysclk,
output LED_D3,
output LED_D2
);
reg [23:0] counter;
always @(posedge sysclk) begin
counter <= counter + 1;
end
assign LED_D3 = counter[23];
assign LED_D2 = ~counter[23];
endmodule
此时,user_ip.v
文件里其实有两个module
: user_ip
和led
。
接下来,让led
模块在user_ip
模块内实例化,让两模块关联起来即可。
即:在user_ip
的module
内添加led
的实例化如下:
verilog
led led_inst (
.sysclk(sysclk),
.LED_D3(LED_D3),
.LED_D2(LED_D2)
);
这里的操作过程如果还是不理解,请自行从网盘上下载样例工程。
链接: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灯的闪烁了。
回顾:
- 对用户逻辑来说,总入口就是
user_ip
的module
; - 这里新建了一个
led
的module
,里边使用了user_ip
的两个led
信号和sysclk
信号; 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灯交替闪烁。
通过这个案例,可以了解到:
- MCU的信号如何传递到CPLD
- CPLD如何反向控制MCU信号
在MCU和CPLD的交互中,两者之间传递信息的方式大致分为四种:
- MCU传递信号给CPLD;(如:MCU的GPIO传递高低信号到CPLD)
- CPLD传递信号给MCU;(如:CPLD对MCU产生中断信号)
- MCU读写寄存器方式操作CPLD数据;
- 不建议,CPLD做为主设备对MCU进行读写。
也就是说,在MCU和CPLD交互中,CPLD更像一个“外设”。
其中,前两种较为简单。后两种要使用AHB总线来操作。这个案例中讨论的就是这里的方式1.(方式2也会顺便讨论)
新建工程的过程,这里不再描述。只描述需要注意的改动点。
1. VE中定义3个信号:
plaintext
LED_TEST1 PIN_31 # LED3
LED_TEST2 PIN_32 # LED2
GPIO4_1 iocvt_chn:OUTPUT
其中前两个是CPLD中信号到引脚,第三个是MCU到CPLD信号。
2. prepare LOGIC
生成CPLD工程后,可以看到user_ip
接口处的定义:
verilog
inout LED_TEST1,
inout LED_TEST2,
input iocvt_chn_out_data,
input iocvt_chn_out_en,
3. 在user_ip.v
中关联下信号:
verilog
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
切换:
c
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
回顾:
- 在VE里定义了2个CPLD信号到Pin脚,1个MCU到CPLD的信号;
- 在CPLD里,直接对MCU过来的一个信号,绑定到2个CPLD信号上,传递给Pin脚;
描述完MCU控制CPLD信号,顺便描述下CPLD控制MCU信号。
这种方式和1相近,只不过是反向。
可以在MCU中定义gpio4_2
为输入并使能中断,则CPLD中设置信号高低时,将触发MCU的GPIO中断。(当然,这里也可以用local int
,但还未整理,需要自己尝试实现)
1. 在VE中定义信号:
plaintext
GPIO4_2 iocvt_chn:INPUT
表示,MCU的GPIO(gpio4_2
)信号将来源于CPLD的iocvt_chn
。
2. prepare LOGIC
工程后,可以看到analog_ip.v
接口中的信号:
verilog
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就要根据这个接口来做对应的处理。
整个访问过程分为两部分:
- MCU如何访问CPLD的“寄存器”;
- 当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
中的接口部分:
verilog
input mem_ahb_htrans,
input mem_ahb_hready,
input mem_ahb_hwrite,
input [31:0] mem_ahb_haddr,
input [2:0] mem_ahb_hsize,
input [2:0] mem_ahb_hburst,
input [31:0] mem_ahb_hwdata,
output mem_ahb_hreadyout,
output [1:0] mem_ahb_hresp,
output [31:0] mem_ahb_hrdata,
其中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
信号后,开始数据传输。
大致如下图(无等待类型的图):
plaintext
MCU -> AHB -> CPLD
比如,MCU要读0x60000004
的寄存器:
MCU端直接C语言这样调用:int cpRdReg = *((int *)0x60000004);
CPLD端,可以根据以上信号做如下处理:
verilog
// 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端,可以根据以上信号做如下处理:
verilog
// 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的一组信号。使用如下图:
plaintext
AHB -> AHB2APB -> 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位
以下展示在APB下如何实现跟MCU的交互,仍以AHB的两个寄存器为例。
1. 首先需要增加AHB转APB的信号关联;
如上图。
Ahb2apb
模块会把AHB信号转换为APB信号。接下来操作APB信号即可。
2. 在转换后的APB信号中,实现写和读的操作。
MCU读操作时:
比如,MCU要读0x60000004
的寄存器:
MCU端直接C语言这样调用:int cpRdReg = *((int *)0x60000004);
CPLD端,可以根据以上信号做如下处理:
verilog
// 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端,可以根据以上信号做如下处理:
verilog
// 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》中的说明:
使用M9K时,直接在Quartus下创建IP就可以了。
2. 使用MCU这边的RAM:
除了CPLD自带的内存,CPLD还可以使用MCU的内存SRAM。(这部分使用不太容易,如果CPLD基础薄弱,不建议使用)
AG32整个芯片系列,内存SRAM大小都是128K。
如果MCU用不了128K,希望分一些给CPLD来用,比如,分出来32K给CPLD。可以按照如下方式设置:
- 限制MCU的使用,比如,让MCU只使用前96K;
限制MCU对RAM的使用,需要修改LD配置(分散加载相关)来实现。
在路径:AgRV_pio\packages\framework-agrv_sdk\misc\devices
下,在文件 AgRV2K_mem.ld
中可以看到定义如图:
plaintext
SRAM_SIZE = 96K
如果只用96K,则修改上边的SRAM_SIZE = 96K
即可。
修改文件并保存后,需要重启VSCODE工程,让设置项使能。
- CPLD中对后32K的使用;
CPLD使用后32K,起始地址是从0x20000000 + 96K
的地址开始。
即:从0x20018000
开始,长度32K,到0x20020000
结束。
CPLD中对于SRAM的寻址方式和MCU相同。
CPLD对SRAM的读写,请参考:
SDK下的examples\custom_ip\logic\ram2ahb.v
和 ahb2ram.v
如果想共用MCU的一段SRAM,在这段RAM中,比如CPLD只写,MCU只读,也可以。只需要MCU那边强制访问这片区域就行了。
八、更多样例
这些样例不再解释,具体请从网盘下获取。
链接:https://pan.baidu.com/s/1wcBnqnray7bu4IURDIoDDQ?pwd=1205 里边的 \cpld-fpga文档\logic样例\
样例包括:
- ADC如何从0创建的例程
- ADC+CPLD控制LED灯的例程
- ADC+SPIFull的例程