Skip to content

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

MCU+CPLD 联合编程(概念及流程)
MCU+CPLD 联合编程(案例描述)
在看这里具体案例前,请确认已经了解了上一章节描述的概念

案例列表如下:

  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_namelogic_dir两个选项(该动作为:开启自定义logic功能),如图:

操作步骤

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

  1. 在VE文件里配置引脚关系;
  2. 建立CPLD空工程(使用prepare LOGIC命令)并编写逻辑;
  3. Quartus下进行工程转换(和综合);
  4. 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_D3LED_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_ipled

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

即:在user_ipmodule内添加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灯的闪烁了。

回顾

  1. 对用户逻辑来说,总入口就是user_ipmodule
  2. 这里新建了一个ledmodule,里边使用了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个信号:

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_1toggle切换:

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

回顾

  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中定义信号:

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就要根据这个接口来做对应的处理。

整个访问过程分为两部分:

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

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.vmodule下,就可以完成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。可以按照如下方式设置:

  1. 限制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工程,让设置项使能。

  1. CPLD中对后32K的使用;

CPLD使用后32K,起始地址是从0x20000000 + 96K的地址开始。

即:从0x20018000开始,长度32K,到0x20020000结束。

CPLD中对于SRAM的寻址方式和MCU相同。

CPLD对SRAM的读写,请参考:

SDK下的examples\custom_ip\logic\ram2ahb.vahb2ram.v

如果想共用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的例程