AG32 MCU+CPLD 联合编程(案例描述)

浏览: 作者:Rocky 来源:AGM 时间:2025-06-10 分类:FPGA应用

前言:


AGM是AG32 MCU, 可编程SoC和异构MCU的解决方案提供商, 海振远科技可提供全系列的开发板及SDK资料,方便用户从0开始,快速上手开发。

AGM AG32 MCU和FPGA 目前广泛应用于工业,消费,测试测量和医疗等场景。

AG32内置的FPGA( AGRV2K) 可升级替代EP570, EPM1270, LATTICE XO2-256,XO2-640, XO2-1200 等。

AG32 的管脚可以灵活定义,引脚与STM32。并且内置2KLE FPGA, 非常适合MCU + FPGA/CPLD的应用场景。


目录

一、CPLD控制PIN脚(LED闪烁)

二、MCU通过CPLD控制PIN脚

三、MCU通过寄存器方式操作CPLD

  1. MCU如何访问CPLD

  2. CPLD如何判别及响应

四、MCU通过AHB转APB方式操作CPLD外设

五、CPLD实现一个UartTx“外设”

六、DMA在CPLD中的使用

七、CPLD中如何使用RAM

八、更多样例

  1.ADC如何从0创建的例程

  2.ADC+CPLD控制led灯的例程

  3.ADC+SpiFull的例程


案例列表如下:

1. CPLD控制PIN脚(LED闪烁)

2. MCU通过CPLD控制PIN脚(LED闪烁)

3. MCU通过寄存器方式操作CPLD;

4. MCU通过AHB转APB方式操作CPLD外设;

5. CPLD实现一个简单的UartTx的外设;

6. DMA在cCPLD中的使用(以adc为例);

7. CPLD中如何使用RAM;

8. 其他更多样例(不再详解,自行看代码注释)


这里所有用到的案例例程,如果需要,请联系海振远科技获取。

另外,所有例程请务必参照描述自己重建一份。一则,通过自己手动创建,能更清晰学习整个操作过程;二则,网盘上的例程都是基于当时版本的,在最新版本上未必兼容;三则,需要自己修改的往往是很少的代码(框架和工程都是自动生成,自己只需要填充自己的逻辑),不用费力气去复制整个工程。


以下进入正题。


101


一、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功能),如图:


1_conew1


操作步骤:

就是上节讲到的四步骤,先复习下:

1.在VE文件里配置引脚关系;

2.建立CPLD空工程(使用prepare LOGIC命令)并编写逻辑;

3.Quartus下进行工程转换(和综合);

4.Supra下编译出最终的logic.bin;


1. 在VE文件里配置引脚关系

可以先删除掉其他的引脚定义,只保留时钟配置。然后添加CPLD控制PIN脚的定义如下:


2_conew1


这里添加的 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 两项:


3_conew1


然后,可以在该user_ip的模块外边(即:endmodule 之后)添加led模块代码如下:


4_conew1


此时,user_ip.v文件里其实有两个moudle: user_ip 和 led。

接下来,让led模块在user_ip模块内实例化,让两模块关联起来即可。

即:在 user_ip的module内添加led的实例化 如下:


5_conew1


这里的操作过程如果还是不理解,请联系海振远科技获取样例工程。

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进行了分频的使用;


102


二、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灯交替闪烁。


如需样例工程,请联系海振远科技获取。


回顾:

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部分不再描述。较为简单,也不再举例。


103


从这个案例开始,需要了解AHB总线的基础知识了。如果对AHB总线不是很了解,可以自行百度再学习下。

这里只描述用到的部分:

在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中的接口部分:


6_conew1


其中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信号后,开始数据传输。

大致如下图(无等待类型的图):


7_conew1


比如,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中

-----------------------------------------------

这部分的实例代码,请联系海振远科技获取:

注意:这里展示的,仅仅是基于AHB总线上的数据交互。

在实际应用中,比如要实现一个串口之类的,往往是慢速设备,这些是要挂载到APB上的。慢速设备要经过AHB到APB的bridge,才能最终使用。请继续往下看。


104


四、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的一组信号。使用如下图:


8_conew1


如果实现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中

-----------------------------------------------

这个功能实现后,其实是个简单的“空外设”。可以用它做为实现复杂功能外设的基础。

这部分的实例代码,请联系海振远科技获取:

样例展示到这里,MCU和CPLD的交互上:交互信号、跟AHB交互数据、跟APB交互数据,基本的交互通路已经建立。

接下来,用户根据自己的需求,在CPLD中交互到数据后,编写自己需要的功能即可。


105


五、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工程中的代码和注释。

这部分的实例代码,请联系海振远科技获取。

这个样例完全理解完,就可以尝试读ADC的代码了。

在example/analog工程,展示了ADC模块做为外设,与MCU之间的数据交互(ADC采集后的数据,被MCU读取)。

相当于:ADC硬核+ADC的CPLD逻辑,实现了一个完整的“ADC外设”。

关于ADC/DAC/CMP的代码解析,请联系海振远科技获取。


106


六、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信号。

这部分的实例代码,请联系海振远科技获取。


在这个样例中,展示了两部分代码:

MCU中,配置DMA读取;为了测试,mcu会在另一地址给CPLD写数据;

CPLD中,会对MCU写进来的数据缓存,缓存后触发DMA的信号,让DMA来读取数据。而DMA从CPLD里读取数据后会给CPLD一个clear信号,标志一次DMA交互完成。


107


七、CPLD中如何使用RAM


1. CPLD本身有自带的RAM:

CPLD内部自带RAM,为4个M9K块,每个M9K大小为8192 bits。

(即:4个M9K总空间为4K bytes)

更详细信息,请参考《AGRV2K_Rev2.0.pdf》中的说明:


9_conew1


使用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 中可以看到定义如图:


10_conew1


如果只用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


11_conew1


如果想共用MCU的一段sram,在这段RAM中,比如CPLD只写,MCU只读,也可以。只需要MCU那边强制访问这片区域就行了。


108


八、更多样例


这些样例不再解释,具体请联系海振远科技获取。


样例包括:

1.ADC如何从0创建的例程

2.ADC+CPLD控制LED灯的例程

3.ADC+SpiFull的例程




AG32概述:


AG32系列32位微控制器旨在为MCU用户提供新的自由度和丰富的兼容外设以及兼容的引脚和功能。AG32VF407产品系列提供卓越的品质、稳定性和非凡的价值。

器件特征:

■ 最大 CPU 速度为 248 MHZ

■ 从 Flash 执行零等待

■ SRAM 128KB.FLASH 1MB

■ 支持浮点

■ 1个CAN2.0.5个UART2个I2C

■ 2 个基本定时器,5 个高级定时器

■ 支持 SDIO 、以太网 MAC

■ 支持 USB FS+OTG

■ 看门狗

■ 3个 12 位、最高 3M SPS ADC(17 通道)、2个 DAC

■ 2x 比较器

■ RTC, SPI

■ 内置2KLE FPGA逻辑单元



联系海振远科技

电话:0755-2780 9180;  15323895320;

邮箱: tech@hizyuan.com

Lucy@hizyuan.com