analog代码分析

42 min read
AG32入门文档

一、analog 的说明及结构

在AG32 芯片中,analog 是个特殊的存在。
在芯片中,包含了adc/dac/cmp 硬核,但硬核却没有和mcu 直接相连。需要经过cpld 的辅助,才能在mcu 中顺利使用adc/dac/cmp 的功能。

结构是: (C 代码)mcu <==> cpld <==> AD/DA 硬核

analog 实现的就是这里的cpld 部分。
这部分其实是个很好的学习样例,它为我们展现了mcu 和cpld 之间是如何搭配工作的。我们可以参考这部分的框架,更好的写出自己的cpld 逻辑。
这部分代码在样例程序examples\analog\ 目录下。

先来看这个examples\analog 工程:

在src 里边,是mcu 这边对adc 模块的调用。里边需关注example_analog.c 和analog_ip.h。
在logic 里边,是cpld 部分的实现。里边需关注analog_ip.v 和ahb2apb.v。

二、MCU 端的处理

整个analog 包含了ADC/DAC/CMP 三部分。三个部分会依次讲到。
先从ADC 开始。

第一部分:驱动部分

在analog_ip.h 文件中,先看对设备地址和寄存器的定义:

从这里的定义可以看出,每个ADC 有6 个寄存器:CTRL、STAT、DATA、RESERVED、CHNL、SEQ。(这6 个寄存器是mcu 和cpld 交互的接口)。
然后,共有3 个ADC(ADC0、ADC1、ADC2),访问地址分别是:0x60000000、0x60001000、和0x60002000。
如果前边看过user_manual.pdf 就会知道,0x60000000~0x80000000 之间是给cpld 使用的地址空间。mcu 要访问cpld,必须要通过以上地址才能触发cpld 的信号。其实不止是这里的AD/DA 模块,所有的cpld 逻辑(包括用户实现的cpld),如果要与mcu 交互,也必须使用这个地址区间。

接下来,按照正常推断:
0x60000000 地址,是ADC0 的CTRL 寄存器;
0x60000004 地址,是ADC0 的STAT 寄存器;
0x60000008 地址,是ADC0 的DATA 寄存器;
0x6000003C 地址,是ADC0 的CHNL 寄存器;
在MCU 端,往0x60000000 地址写一个值,就是写到了“ADC0 的CTRL 寄存器”。
在MCU 端,读0x60000004 地址的值,读到的就是“ADC0 的STAT 寄存器”。

那么,就会带来几个疑问:
1. 为什么ADC0 的CTRL 寄存器地址是0x60000000,可以是0x70000000 吗?
2. MCU 端直接读写0x60000000 这样的地址时,cpld 端会发生什么?
如果看过前边的《AG32 中cpld 的基础》,就已经有答案了。如果没看过,带着疑问继续往下看。后边也会描述到。

继续看这几个寄存器的意义:
CTRL:写寄存器。mcu 对ADC 控制的寄存器(里边包含多个控制项)。
STAT:读寄存器。mcu 读ADC 当前状态的寄存器。
DATA:读寄存器。mcu 读取ADC 转换完结果的寄存器。
CHNL:写寄存器。mcu 设置一轮采样多少个通道数;
SEQ:写寄存器。mcu 设置一轮采样多个通道时的通道序列。

其中:
CTRL 寄存器有5 项:
ADC_CTRL_START:控制ADC 开始启动;
ADC_CTRL_STOP:控制ADC 停止工作;
ADC_CTRL_CONT:控制ADC 连续工作;当该项disable 时,采样一轮就自动停止。否则就会一轮一轮不间断采样,直到mcu 主动来STOP 时。
ADC_CTRL_DMAEN:是否使能DMA;DMA 使能后,采样完一个通道就会触发一次DMA来搬数据(不是一轮完才触发);
ADC_CTRL_SCLK_DIV:设置ADC 的分频系数。是从BUSCLK 基础上分频的。这里ADC 的时钟频率应该小于12M。
STAT 寄存器有2 项值:
ADC_STAT_EN:是否ADC 正在工作;
ADC_STAT_EOC:是否ADC 已经转换完成;
DATA 寄存器的意义:
当ADC 转换完成后,可以从这个寄存器中获取转换后的值。
1. 当配置ADC 只读取一个通道时,等转换完后(ADC_STAT_EOC==1)直接来取就可以。
2. 当配置ADC 一轮读取多个通道时,最好使用DMA,每转换好一个值,用DMA 自动
搬运一次数据(搬运到DMA 配置的buff 中)。
CHNL 寄存器的意义:
配置ADC 一轮读取多少个通道的值。
这里配置的值为:N-1。如,只读取一个通道这里值为0;读取2 个通道这里值为1…
SEQ[]寄存器的意义:
配合CHNL 寄存器使用。填充真正要采集的channal id。
如果只读取1 个通道的值,则填入SEQ[0];
如果读取2 个通道的值,则填入SEQ[0]和SEQ[1];
如果读取3 个通道的值,则填入SEQ[0]和SEQ[1]、SEQ[2]…

这里多了解下“一轮”和“连续采样”的概念:
如果只配置一个通道采样,则“一轮”就是这一个通道采样完一次。
如果配置了多个通道采样,则“一轮”是把配置的所有通道全部采样完一次。
如果没有配置“连续采样”,则以上采样完一轮ADC 就自动停止了。
如果配置了“连续采样”,则ADC 启动后,会一轮一轮采样下去,直到主动去STOP。
需要注意的是:每一次采样的值,都是放在DATA 寄存器的。这个寄存器只能存一个值。
所以,如果一轮中会采样多个通道,或者配置了连续采样,一般都需要配置DMA 来自动搬运数据。(不然后边采样到的数据会冲掉前边缓存值)

再结合源码,继续看analog_ip.h 头文件,只看ADC 的全部定义(先不看DAC 和CMP)。
看完后会发现:
1. 每个ADC 有16 个通道;
2. 接下来的ADC 函数封装,都是围绕以上的寄存器设定而进行的;
理解了以上寄存器,其实也就理解了ADC 的驱动函数:

到这里,ADC 的C 驱动部分全部看完。
接下来看example_analog.c 中对ADC 的使用举例。

第二部分:MCU 对ADC 的应用:

在example_analog.c 中,对ADC 操作的样例有两个:
1. TestAdc 函数:这个函数中只设置一个通道进行采样。
流程很简单:设置通道-> 设置工作频率-> 启动转换-> 转换完成后读取数据。
这个函数里,没有启动DMA,也没有启动连续转换。只是单纯的一个通道采集一次。
是对ADC 最基础的应用。
2. TestDacAdc 函数:这个函数中配置了对两个通道进行DMA 读取N 轮的数据。
流程:配置两个通道-> 配置DMA 读取-> 启动DMA -> 等待N 轮读完-> 停止ADC。
函数中的几点说明:
A. 通过以下3 个函数,先配置出来2 个采样通道:

B. 配置DMA 读取时,因为要读取N(N=WAVE_TABLE_SIZE)轮,所以配置长度为:
WAVE_TABLE_SIZE * 2。
这个配置中,一轮是读2 次数据,而WAVE_TABLE_SIZE 轮,则共发生DMA 搬运数
据的次数就是WAVE_TABLE_SIZE * 2 次。
C. 启动ADC 采样后,等待的是DMA 搬运的结束。
注意,这里不是等待ADC 的结束(因为DMA 启动时默认是“连续转换”的,ADC
不会自动结束),这里是等待DMA 结束后再主动去停止ADC。

三、cpld 端analog_ip 描述及对ahb2apb 的应用

在examples\analog\logic 下,存在三个.v 文件:example_board.v、ahb2apb.v 和analog_ip.v。

其中example_board.v 是绑定工程自动生成的框架.v,不用去关注。只需要知道,这里是cpld的最初入口,这里会调用analog_ip.v 里边的“用户逻辑入口”。
其中ahb2apb.v 是cpld 中AHB 转APB 的模块,认为是一个“转换库”即可,不需要去修改。
在芯片中,cpld 跟mcu/ram 一样,是挂载在AHB 总线上的。mcu 和cpld 交互时,是通过AHB总线先到cpld 模块的。然后cpld 要实现的“外设”(如这里的ADC),一般是低速设备,不能直接挂载在AHB 下的。这个时候,需要一个AHB 转APB 的“桥”。这里的ahb2apb.v就是这个“桥”的功能。ahb2apb.v 这个“桥”的实现代码,可以不用太关注,参考样例会使用即可。

analog_ip.v 中才是真正用户需要关心的地方,这里是用户逻辑入口。

其实除了上述的3 个.v,还有系统的alta_sim.v。
Alta_sim.v 中会适配跟芯片相关的一些底层逻辑,对“用户层”提供一些“库函数”。
以上模块在《AG32 下cpld 的使用入门.pdf》中有更详细描述。

在这个功能中,模块analog_ip 是实现ADC/DAC/CMP 的入口。
Mcu 对0x60000000~0x7F000000 的读写操作,都是通过AHB 到达ananlog_ip 的接口的。
比如,mcu 要读0x60000004 的寄存器:
mcu 端直接C 语言这样调用:int cpRdReg = *((int *)0x60000004);
此时,ananlog_ip 接口的mem_ahb_信号组就会被触发。然后在analog_ip.v 里根据信号做出回应。这块具体详情请参考《AG32 下cpld 的基础》。
在analog_ip 中,实例化了ahb2apb“桥”、实例化了3 个ADC 和2 个DAC,以及CMP。
MCU 发过来的读写操作到达analog_ip 后,会先通过“桥”转成apb 信号,然后再发送给挂载在apb 总线上的ADC/DAC/CMP。
ADC/DAC/CMP 在收到信号时,如果确认是给自己的指令,就执行相对应的操作。
相应操作,最后都转化为对“硬核”的控制动作。

从具体代码看analog_ip 的模块组成和调用:
——————————————————————
用户逻辑模块(module analog_ip)//用户模块入口
桥(ahb2apb)
3 个ADC 实例(apb_adc)
2 个DAC 实例(apb_dac)
1 个CMP 实例(apb_cmp)
endmodule

module apb_adc //adc 模块定义
xxxx
endmodule

module apb_dac //dac 模块定义
xxxx
endmodule

module apb_cmp //cmp 模块定义
xxxx
endmodule
——————————————————————
整个结构中看,analog_ip 模块包含了7 个实例:ahb2apb、3 个ADC、2 个DAC、1 个CMP。

接下来对analog_ip 进行具体代码的分析。

module analog_ip 代码注解:

上边这部分是对cpld 做为master 端使用时的信号线赋值。表示这个模块不会有master 的行为(都是MCU 主动来操作cpld 的)。

这一段,依然是对不会操作到的对外信号设置初值。

这里,定义地址线位宽和数据线位宽分别为16 位和32 位。
后续代码中定义的wire 连线数组,都以这个常量为宽度来进行定义。
地址总线为16 位宽,也就是说,cpld 中只识别0 ~ ‘hFFFF 的地址范围。
比如,mcu 端定义的0x60001004 这个地址,在cpld 端目前只使用了0x1004。
也就是说,mcu 端访问0x60001004 和访问0x70001004,对cpld 来说是一样的,都是0x1004。

常量PER_CNT=6,表示这里定义的“外设”个数为6 个:3 个ADC + 2 个DAC + 1 个CMP。
常量PER_BITS=12,表示cpld 的外设的“片内”寻址空间为12 位。即2^12=0x1000(4K)。
也就是说,每个ADC 可用的寄存器数量有1K 个。

接着看6 个“外设”的地址,每个外设间的地址跨度刚好就是4K 大小。
mcu 端访问ADC1(0x60001000),对应的就是这里的ADC1_ADDR(‘h1000)。
注意:ADC1_ADDR 常量并没有在代码中出现,而是在mem_ahb_haddr[ADDR_BITS-1:0]赋值给ahb_haddr,只取16 位时体现出来的。经过ahb2apb“桥”之后,最终这个16 位地址给到apb_paddr。即:mcu 访问0x60001000,到apb_paddr 拿到的就是0x1000。而后续的“片选”、“寄存器偏移”正是从apb_paddr 来开始的。

这里定义的是apb 的信号。
就是mcu 过来的信号,先到ahb,再经过“ahb2apb 桥”转换后的apb 信号。
apb 的各信号意义不再描述,不熟悉的自行百度。
注意这里的地址线(apb_paddr)是16 位宽,读和写的信号线是32 位宽。

这里是设置apb_clock 为bus clock。
而bus_clock,就是在VE 里定义的定义的BUSCLK 字段,如图:

如果VE 里没有定义BUSCLK,则bus_clock 和sys_clock 同频。
(目前样例程序中,并没有定义BUSCLK。如果提高主频SYSCLK 后发现ADC 跑不起来,可以单独定义该BUSCLK,确保分频后的ADC 小于12M)。

这段代码,是ahb2apb 的实例,“桥”。
ahb 的信号经过“桥”的转换后,都到apb_xxx 来给“外设”使用。
而ADC/DAC/CMP,也正是基于apb_xxx 而实现的。
所以说:ADC/DAC/CMP 是挂载在apb 线上的外设,而不是直接挂载在ahb 线上的外设。

这里定义了6 位宽的select(分别对应3 个ADC、2 个DAC、1 个CMP)。
可以认为是“片选”(6 位中只能有1 位是1,也就是当前“选中”要操作的外设)。
代码解读上,apb_paddr[ADDR_BITS-1:PER_BITS] 是取了apb_paddr 的高4 位(即:0x60001000中为1 的那个数值),该值就是前边描述的“片选”。从这个片选能够区分,mcu 过来的信号,到底是要访问6 个外设中的哪个。
这里的等号赋值,是设置select 的6 位中的某一位为1。

常量PER_CNT 就是6(表示3 路ADC、2 路DAC、1 路CMP)。
那么,这里定义了6 路外设,各自的几个信号:
per_psel:该外设是否被选中;
per_penable:该外设是否被使能;
per_pwrite:该外设写信号线(0:读;1:写);
per_paddr:要操作的外设的片内偏移(16 位);
per_pwdata:要写的数据(32 位);
per_prdata:要读的数据(32 位);
注意,这些信号在后续实例化外设时,会被分别连接到6 个外设上。
这些信号线都是同源(来自于apb_xxx 信号组)且并联起来的,然后由select[i]片选来决定哪个外设最终psel 为1.
想象下:apb_xxx 信号组出来后,每个信号线分成了6 支,分别接入到6 个外设。
也就是说,当mcu 的读或写命令过来时,信号会同时到达6 个外设。但6 个外设中只有一个被片选选中。那么被片选选中的这个外设,才真正开始执行动作。

DMA 的两个信号线(CMP 没做DMA 功能)。
dma_req:当数据准备好,并且dma 开启时,cpld 通过这个信号线来通知DMA;
dma_clr:当dma 搬完数据后,DMA 通过这个信号来通知cpld。

这里和上边的dma 两个信号(向外请求req 和向内清除clr)是关联的。
ext_dma_DMACBREQ:这个就是ahb 线上,cpld 对外dma 请求的信号线。当5 个设备(不含cmp)有dma 请求信号时,都会通过ext_dma_DMACBREQ 传递出去给DMA。
注意:如上边注释所标注,ADC2 和DAC1 是共用的。
所以才有表达式:ext_dma_DMACBREQ = dma_req[3:0] | (dma_req[4] << 2) 里边的或。
(注意,数组序列为0:ADC0 1:ADC1 2:ADC2 3:DAC0 4:DAC1 5:CMP)
dma_clr:这个信号组,源于ahb 总线上ext_dma_DMACCLR 过来的信号的重组。
重组表达式,与ext_dma_DMACBREQ 相反,把4 个信号拆分为5 个信号。
注:由于DMA 中ADC2 和DAC1 的共用和CMP 的缺失,信号从AHB 过来的,跟这里的数组存在一个重组的过程。上边的两个表达式就是正向和反向的重组表达式。

上边的过程,就是实例化6 个外设。
想象一下:生成6 个外设模块,并把前边定义好的6 组信号(从apb_xxx 过来的)分别接入到6 个外设中去。

这里pr_select 是定义了一组额外的片选寄存器。
pr_select 仅同步记录select 信号组(相当于select 的缓存);
pr_select 仅用于后边紧跟的apb_prdata 处理。

这里是把当前选中的外设的读信号per_prdata 的值,记录到apb_prdata 寄存器中。(以供ahb2apb 桥,继续传递给ahb_prdata,最终在读周期内传递给mcu)
这里的for 循环中,其实6 次执行中,只有1 次pr_select[i]是有效的(也就是当前片选选中的那个外设),只有这一次会取到per_prdata 有效的值。
所以,这里的apb_prdata 总是会实时缓存“当前使能的外设的读信号的值”。
也就是说,这段代码保证了:mcu 来读6 个外设中的一个的数据时,数据能从外设read 信号线传递到apb 的read 信号线,继而再向上传递。

到这里,analog_ip 模块已经描述完了。
可以看到:
Mcu 写数据时,信号通过AHB 到来后,先通过ahb2apb 转成apb_xxx 信号,然后再分6 组传给6 个外设(片选决定了哪个使能);
Mcu 读数据时,信号通过AHB 到来后,仍是先通过ahb2apb 转成apb_xxx 信号,然后再分6组传给6 个外设(片选决定了哪个使能),只是apb_prdata 会实时获取外设的prdata,再往回传递。

扩展1:
如果不需要3 个ADC、2 个DAC、1 个CMP 里边的全部(比如只需要一个1 个ADC),那么,就把不需要的实例去掉即可。
这里是for 循环生成实例的,也可以不用for 循环直接实例化的。
扩展2:
如果要实现自己的“apb 外设”,只需要跟这里的adc/dac/cmp 一样,生成一个实例,挂载在apb 下边即可。挂载在apb 下边后,跟adc 一样,把必要的信号引入到自己的外设,然后在自己的模块逻辑中影响这些信号即可。
唯一要注意的是,自己新增外设的地址,跟上述的分开即可(自己新增外设也可以建立像上边“片选”的方式)。
网盘上有个样例《6.UartTx 例程》,展示了如何自建一个模块挂载在apb 下,可以参考。

四、CPLD 端对ADC 的处理

接下来,看具体的ADC 模块。

前边提到过,芯片里本身是包含了ADC 硬核的。但mcu 并没有和硬核直接相连,而是通过这里的cpld 来连接的。
那么,硬核ADC 部分的信号接口,从这里的调用看,就是alta_adc 部分:

关于alta_adc 的接口定义,从alta_sim.v 中可以看到:

先看这里的接口很有意义,因为cpld 里绕来绕去,最终要调用的就是这个硬核接口。
也就是说,从mcu 到cpld 里的全部操作,最终都是要转换为对这里接口的操作的。采样的数据也来源于这里。

先看以上接口的几个信号的意义:(这部分可以从Reference Manual 手册上获取)
enb:设置ADC 是否使能(0: enable, 1: disable)
stop:是否开启ADC 采样(0: disable,1: enable 即,关闭adc)
Insel:采样哪个通道;
db:(output)12 位采样到的数据;
eoc:(output)采样完成的信号,一个clk 周期。
使用上,insel 设定好,输入sclk,enb 和stop 都拉低时,进入ADC 采样。在采样结束后,会输出eoc(宽度为一个时钟周期)的低电平,在这个周期的下降沿开始,外部可以读取硬核内转换后的值(这个值会保留到下次采样结束)。

Reference Manual 手册(文件内搜索ADC)上对ADC 时序描述如下:

接下来,继续看ADC module,看模块内如何处理apb_xxx 信号的输入,将信号转换后操作上边的alta_adc。

在具体代码分析时,注意里边的3 个周期:
1. apb 的clk 时钟周期;
2. apb 分频后传给adc 的clk 时钟周期;
3. adc 中采样一次数据的时间(采样一次完整的ADC 数据,需要12+1 个adc_clk 周期)。
其实这3 个周期也是理解以下代码的基础。不理清这3 个层面,代码是无法完全理解的。后边讲解中,这3 个周期会经常出现。

进入module apb_adc 的模块:

相比前边模块,这里ADC 模块的输入输出信号线已经很少了。几个信号线看名字,跟前边也是相仿的,不再描述。

常量SCLK_BIT,定义用多少位记录ADC 的分频数;
常量SEQ_MAX,定义最多支持多少个通道按序列采样;

对应到MCU 端使用的ADC 寄存器列表。mcu 端如下:

第一个表达式里,
这里(ADDR_SEQ0 >> 6) 的常量代入值后为0x40>>6,计算后为1。
apb_paddr[11:6] 是取apb_paddr 的6~11 位。比如mcu 的0x60001008,这里取到0;
而0x60001040~0x6000107F 之间,这里取到1。
从mcu 寄存器定义看,0x60001040 正是adc1->SEQ 数组开始的地址。
所以,is_seq_addr 这个值的意义,是当前动作是否为读/写通道(adc channal);
第二个表达式里,
seq_idx = apb_paddr[5:2],为读取地址后再除以4。
用值带入下,读取地址是0x60001040 时,seq_idx 为0;读取地址是0x60001044 时,
seq_idx 为1;…
所以,seq_idx 这个值的意义,是要读adc->SEQ 数组里的哪个位置。后续用做数组下标。

这里是定义几个寄存器和信号。
请注意这个.v 代码中的寄存器命名方式。adc_开头的,都是跟硬核ADC 相关。
adc_en:adc 硬核是否使能(使能情况下ADC 才正常工作)。
注意:该寄存器是直接连接到硬核接口的。用来控制硬核ADC 是否使能。
当mcu 端调用ADC_Start 时,改变ctrl_adc_start 后,会改变该值为1;
当mcu 端调用ADC_Stop 时,或者ADC 一轮采样完后,会改变该值为0;
adc_eoc:信号线,连接ADC 硬核的eoc 信号;
这个信号平时为高电平,在采样完后有一个adc clk 周期的低电平。
adc_db:信号线,12 位宽,连接ADC 硬核的db 信号;
当采样完数据,adc_eoc 变低后,该信号线上的数据有效,可以往外读取。
apb_eoc:寄存器,用于记录adc_eoc 状态。
它和adc_eoc 信号宽度一样都是1 个adc_clk 周期,但变化比adc_eoc 晚一个节拍。
并且,它和adc_eoc 高低刚好是反的。它正常为低,触发时为高。
这个寄存器值为1,可以表示采样完成(并且数据已经放到apb_db 寄存器)。
apb_db:寄存器,用于记录adc_db 的值;
即:缓存ADC 采样到的值。直到下个数据来时,才冲掉上个缓存值。
每次采样完,在adc_eoc 变化的周期里,就会把adc_db 的值写入到这个寄存器。
eoc_rising:eoc 上升沿,用以标记硬核ADC 一次数据刚采样完。
这个标记的长度也是1 个adc_clk 周期。就是adc_eoc 已经变低但apb_eoc 还没变
高的那个clk 周期,跟adc_eoc 是重合(但高低是反的)。
apb_data_phase:当前ADC 是否被选中并且使能(是否可正常操作该外设)。
当该信号为1 时,该ADC 外设可以处理apb 过来的读写命令。

ctrl_adc_start、ctrl_adc_stop、ctrl_adc_cont、ctrl_adc_dmaen、ctrl_sclk_div:这几个寄存器就是记录mcu 端设置过来的值。
其中,ctrl_adc_start 会自动被置零(在stop 被设置时,或在采样完成时<非连续模式下>)。
ADC_CTRL_REG 定义了CTRL_REG 寄存器的数据格式,对应MCU 端的adc->CTRL。

stat_adc_eoc:表示一轮采样是否结束。
它和apb_eoc 的区别是:
apb_eoc 是实时记录真正硬核采样的状态的,只不过比adc_eoc 晚一个节拍;每采样一个通道就会产生一次信号。
stat_adc_eoc 是给MCU 反馈状态用的寄存器。当一轮数据(可能是一个数据也可能是多个数据,看配置的通道号而定)全部采样完,才会产生一次变化。(搜关键字stat_adc_eoc)看下文中的更多描述。
ADC_STAT_REG 定义了STAT_REG 寄存器的数据格式,对应MCU 端的adc->STAT。

seq_last:是否为一轮采样中的最后一个;
seq_length:记录mcu 设置过来的一轮的采样通道数;
seq_reg:记录mcu 设置过来的一轮采样的通道编号数组,数组长度为16;
seq_cnt:当前这轮已经采样了多少个(类似:运行中的局部变量);
chnl_sel:当前正在采样的是哪个通道;
ADC_CHNL_REG 定义了CHNL_REG 寄存器的数据格式,对应MCU 端的adc->CHNL。

seq_done:是否一轮中的最后一个通道的数据已经采样完;
(因为apb_eoc 表示一次采样结束,seq_last 表示一轮中的最后一次采样)
adc_stop:adc 是否为停止状态(当mcu 发来了stop 命令,或,非连续模式下的一轮采样完)

记录mcu 设置过来的寄存器的值。
当复位时,这几个寄存器清0;
当mcu 设置CTRL 寄存器时,cpld 中对这几个寄存器进行赋值;
当adc 要停止时(不管是以上哪种原因),重新设置start 和stop 寄存器的值,等待mcu 的新指令。

参考上边对apb_eoc 和adc_eoc 意义的描述。
这里就是设置apb_eoc 比adc_eoc 慢一个clk,且高低反转的地方。

围绕apb_db 的处理。
apb_db 里记录了当前的采样值。
在eoc_rising 标记使能时,从adc_db 信号线上读采样值,存储在apb_db 寄存器上。
这里的eoc_rising 是在adc_eoc 变化时(apb_eoc 变化前)就开始变化了。

围绕adc_restart 的处理。
判断式二,当(该外设使能且mcu 来写时且(写的地址是通道数或写地址是通道号))时,adc_restart 设置为1。
判断式三,当adc_restart 被置1 后,下个周期就恢复为0。
也就是说,当mcu 来写通道数/通道号时,adc_resart 寄存器会被置1。然后下个clk 置回0。
即:当该ADC 被重置通道时,adc_restart 有一个apb_clk 的周期被置1。
adc_restart 存在的意义,是后续来自动重置硬核的adc_en 信号。

这里是围绕寄存器stat_adc_eoc 的处理。
stat_adc_eoc 是反馈给mcu,表示是否转换完成的标记。
判断式二,当(该外设使能且mcu 来写时且写地址是状态且写内容是adc_en 赋0)
即:mcu 端通过adc->STAT 写ADC_STAT_EN 位为0 时。
判断式三,当(该外设使能且mcu 来读时且读地址是DATA)
即:mcu 端通过adc->DATA 来读ADC 的值时。
判断式四,当(一轮中的最后一个通道已经采样完)
也就是说,stat_adc_eoc 这个寄存器只有在一轮中的最后一个通道已经采样完时,会被置1,然后一旦有mcu 读ADC 数据的动作,或者MCU 来重置adc_en 的动作,stat_adc_eoc 被自动重置为0。

这里的处理,就是把mcu 设置过来的通道数(adc->CHNL)写入seq_length 寄存器。

当mcu 端设置通道时(adc->SEQ[seq] = channel),触发这里的赋值操作。
寄存器seq_reg 数组存储的是mcu 设置进来的通道号(最多16 个)。

这里是对寄存器seq_last 的处理。
判断式二,apb_eoc 表示当前次采样是否完成。
判断式三,adc_seq_next 的意义是“ADC 采样已经进行一半”,seq_cnt == seq_length 是指当前正在操作的通道就是这一轮的最后一个通道。
所以,seq_last 意义是:当前操作的通道是否为本轮的最后一个通道。
adc_seq_next 是采样中的一个时间点,搜关键字adc_seq_next 看下文的详细描述。

这里是设置寄存器seq_cnt。寄存器seq_cnt 的意义是当前正在采样的通道id。
判断式二,当ADC 停止或者一轮已经处理完时,seq_cnt 被设置为0;
判断式三,当一轮还没有处理完且“可以处理接下来的事情时”,seq_cnt 累加;

这里是定义出来反馈给mcu 各个寄存器的数据,对应mcu 的几个寄存器。
这种表达方式较为便捷。

这里是mcu 来读寄存器数据时,数据反馈的组织表达式。
这里利用了:一次读取只能有一个地址。所以,一串“或”表达式里,只会有一个有值。

寄存器adc_state 的值,表示在当前这次采样中的第几个adc_clk 周期(ADC 采样一次数据需要12 个adc_clk 周期,然后eoc 还要额外一个adc_clk 周期)。
寄存器sclk,这是要输入到ADC 硬核的clk 时钟。输入给硬核的时钟,是在当前apb_clk 基础上分频。分频的参数由mcu 来设置为ctrl_sclk_div。
寄存器sclk_counter,是处理apb 到adc 分频用的。从0 ->累加到分频值(产生一次sclk 反转)->归0 ->累加到分频周期(产生一次sclk 反转)…,不断循环。
信号线sclk_en,则表示是否给ADC 硬核产生clk 波形。
信号线sclk,是直连硬核的clk 信号线。会输出给硬核ADC 的时钟波形。

这段处理的就是apb 到adc 的clk 的分频。
adc_clk 是从apb_clk 上分频的,adc_clk 的宽度是apb_clk 宽度的N 倍(N=分频数,即N=ctrl_sclk_div)
sclk 会连接硬核的clk 时钟线。这里的高低反转,就是为硬核ADC 产生clock 时钟。

信号sclk_rising,如果值为1,表示adc_clk 的上升沿发生了。
注:adc_clk 是从apb_clk 上分频的,adc_clk 的宽度是apb_clk 宽度的N 倍(N=分频数)。
这个信号会在adc_clk 从低翻转到高时发生,宽度为1 个apb_clk 周期。

adc_state 是计数当前正在一次ADC 采样中的第几个adc_clk。
那么(adc_state==4)则表示,在ADC 采样中的第四个adc_clk 时的这个时间点。
所以,adc_seq_next 表示在ADC 采样中的第四个adc_clk 时且adc_clk 处于上升沿的这个时间点。这个宽度为1 个apb_clk 周期。
adc_seq_next 这个本身没有意义,只是表示个时间点,具体要看用到它时的上下文语境。一般它被用于“可以处理接下来的事情了”。

这里是处理adc_state 的。
寄存器adc_state 的值,表示当前正在一次ADC 采样中的第几个adc_clk。(注:一个完整的ADC 的采样周期,是13 个adc_clk)
判断式三中的sclk_rising 是adc_clk 发生上升沿时会被置1 的(宽度为1 个apb_clk)。

这里处理的adc_en,表示adc 硬核是否使能(使能情况下ADC 才正常工作)。
注意:该寄存器是直接传递到硬核接口的。用来控制硬核ADC 是否使能。
处理的逻辑:
当mcu 端调用ADC_Start 时,改变ctrl_adc_start 后,会改变该值为1;
当mcu 端调用ADC_Stop 时,或者ADC 一轮采样完后,会改变该值为0;

这里是处理DMA 的地方。
如果设置了DMA 使能,则每个ADC 采样完后的上升沿,都会触发一次DMA 请求。
等DMA 来读取数据后,发送dma clear 信号时,清除该DMA 请求信号。

cpld 对外的DMA 信号线只有两条:对外请求req 和对内清除clr。处理过程就在这里。

到这里,ADC 模块的代码也全部分析完毕。
回顾一下cpld 里做的任务,包括:存储mcu 设置过来的参数、给硬核产生分频后的clk、硬核采样完后读取结果并存储下来、如果设置多个通道采样则自动切换通道、如果设置DMA则采样完一个通道后发出DMA 请求、如果设置连续采样则一轮一轮持续下去、为MCU 准备数据。
硬核ADC 是不管那么多的,只要满足硬核的两个条件(enable 和clk),硬核就会用设置给它的通道号进行数据采样。那么,连续采样/多通道采样,中间怎么变动通道,怎么判断持续,这些便都是cpld 里实现的了。

便于理解整个交互流程,可以设想一个场景:配置2 个通道采样,并设置DMA 读取,设置
为不连续采样。其工作过程:
1. mcu 端设置采样通道; —-cpld 对应写寄存器
2. mcu 端配置DMA;—-cpld 对应写寄存器
3. mcu 端启动ADC 采样(ADC_START);—-cpld 对应写寄存器
4. cpld 端根据配置的分频数产生adc_clk 给到硬核,硬核开始采样;
5. 当硬核采样到第4 个adc_clk 时,会检测到接下来还有通道要采样,会切换通道号;
6. 第一个通道采样完后,硬核产生adc_eoc 的信号;
7. cpld 根据这个信号,将数据读到apb_db 中,并在下个clk 里产生跟随的apb_eoc 信号;
8. 数据到apb_db 后,触发DMA 信号,通知DMA 来读取数据;
9. 硬核用第5 步切换的通道号继续开始采样;
10. 当硬核采样到第4 个adc_clk 时,cpld 会检测到接下来没有通道要采集了,设置各标记;
11. 第二个通道采样完后,硬核产生adc_eoc 的信号;
12. cpld 根据这个信号,将数据读到apb_db 中,并在下个clk 里产生跟随的apb_eoc 信号;
13. 数据到apb_db 后,触发DMA 信号,通知DMA 来读取数据;
14. 一轮已经全部采样完,adc_en 被设置为disable,硬核停止工作;
15. 设置stat_adc_eoc 为1,并结束cpld 工作。等待下次启动。

五、CPLD 端对DAC 的处理

DAC 的处理和ADC 相似,更简单些。

前边提到过,芯片里本身是包含了ADC/DAC/CMP 硬核的。但mcu 并没有和硬核直接相连,而是通过这里的cpld 来连接的。
那么,硬核ADC 部分的信号接口,从这里的调用看,就是alta_dac 部分:

从alta_sim.v 中看硬核DAC 的接口定义:

先看以上接口的几个信号的意义:(这部分可以从Reference Manual 手册上获取)
enb:设置DAC 是否使能(0: enable, 1: disable)
bufenb:是否使能输出缓冲(运算放大器)(0: enable, 1: disable)
不管是否开启这项,都可以输出。但开启的话,会使输出阻抗很低。
这项是独立使用的,跟其他逻辑无关。
stop:是否开启DAC 转换(0: disable,1: enable 即,关闭adc)
din:10 位输出数据的值;
dout:(output)信号输出到的IO。
使用上,din 数据准备好,不管输出缓冲是否开启,enb 和stop 都拉低时,就进入DAC 转换。
DAC 转换完后,就会处于保持状态。如果有新的数据写进来,则改变完din 后,输出电压自动变化。一直到关闭DAC 转换为止(enb 拉高)。

代码角度看,DAC 比ADC 简单上很多。
MCU 的DAC 代码不再讲解,几个函数也都是调用寄存器的接口。
这里直接进入CPLD 的讲解。

继续看DAC module,看模块内如何处理apb_xxx 信号的输入,以及将信号转换后操作上边的alta_dac。

这里是DAC 模块的接口,跟ADC 模块接口相同。

常量SCLK_BIT,用于记录分频寄存器的位数;

对应MCU 端的寄存器定义的偏移。

apb_data_phase:当前DAC 是否被选中并且使能(是否可正常操作该外设)。
当该信号为1 时,该DAC 外设可以处理apb 过来的读写命令。

记录mcu 端设置过来的几个寄存器值;
注意,这里的ctrl_dac_en 和ctrl_dac_bufen 会连接到硬核DAC 上。

要转换的数据的寄存器,10 位宽度。
这个寄存器连接到硬核DAC 的数据信号线上。

这里就是MCU 端设置(dac->CTRL)时,直接写进来值的地方。
这里的寄存器,记录MCU 过来的信息;

这里记录MCU 端发送数据(dac->DATA)时的数据值。

这里是要返回给MCU 读取寄存器值的定义;

当MCU 读取CTRL 或DATA 时,就是这里处理并返回的。
这里利用了:一次读取只能有一个地址。所以,ctrl_read 和data_read 只会有一个有值。

sclk_counter 是分频计数。
dac_clk 是从apb_clk 上分频出来的,分频数是MCU 端设置下来的ctrl_sclk_div。而这里的sclk_counter 是在每个apb_clk 到来时累加1,加到ctrl_sclk_div 后重置为0.
sclk_pulse 信号,是在每次sclk_counter 累加到ctrl_sclk_div 时,产生的1 个apb_clk 时长的高电平。可以认为sclk_pulse 是一次DAC 数据转换完成的脉冲。

这里是对DMA 的处理。
条件判断三,当dma 使能,并且分频脉冲到来时,产生DMA 信号去通知DMA 搬运下一个数据。

到这里,DAC 的cpld 代码也已经分析完了。
可以看到,DAC 部分的逻辑,都是直来直往的,没有太多的内部控制。
DAC 的分频,也是配合DMA 的需要才设置的分频(一个分频时长,产生一次dma 数据的搬运)。如果不用DMA,是不需要设置分频的。
DAC 开启后,不会自动终止,需要MCU 端来停止。

六、CPLD 端对CMP 的处理

芯片硬核中包含1 个CMP,这个CMP 又分为两路。两路相互独立,可以分开来用。

CPLD 中对CMP 的处理,相比DAC 更加简单。
CPLD 中只是对数据进行一个简单的传递:当MCU 写参数时,cpld 把这些数据存储下,并传递到硬核CMP;当MCU 读设置的参数(和比较结果)时,cpld 返回比较结果。
仅此而已。
所以,cpld 代码本身没有什么,这里只需要理解硬核CMP 的接口部分。
具体使用上,基本可以认为MCU 的接口,就是对应硬核的接口了。

在cpld 里调用硬核的地方:

从这里可以看到,硬核本身包含两组比较器:编号为1 的一组和编号为2 的一组。

而从接口定义看,如下:

拆开成两组。
每组有:
intput enb, hyst, mode,
input[2:0] imsel,
input[1:0] ipsel,
output out,
各项意义:
enb: 同ADC/DAC 的enb。是否开启CMP 功能(0:enable;1:disable)
hyst: 是否开启迟滞性(1:开启;0:不开启)
mode: 设置速度(0:快速;1:慢速)
ipsel:选择哪个正相输入通道;(可选值:x_1 和x_2)
imsel:选择哪个负相输入通道;(7 种选择:3 个IO + 4 个参考电压)
out:输出比较值

使用时,设置正相输入和反相输入,然后enable,就可以获取输出的比较值了。
目前的MCU 端并没有设置mode 和hyst 的接口,如果需要,可以自己在相应位置增加值。
hyst 和mode 在比较器中的作用,请自行百度。

如果需要增加比较器触发的中断,在目前的cpld 处理上,增加out 的检测wire,当满足要求时,触发mcu 端的中断即可。

cpld 代码部分太简单,不再分析。

七、底层逻辑的对应

七、底层逻辑的对应
由上边的描述,已经知道:AG32 中是包含了3 个ADC 硬核和2 个DAC 硬核的。
但从以上的代码中,却看不出来cpld 的3 路ADC 是如何一一对应到3 个ADC 硬核的。
比如说,
如果只开一路ADC1(关闭ADC0 和ADC2),怎么确保是从硬核ADC1 来采集的?
如果只开一路DAC1(关闭DAC0),怎么确保输出的信号能到硬核DAC1 的引脚?
这部分的对应关系,并不是在代码里指定的,而是通过asf 文件配置出来的。
参考样例工程example/analog/logic/路径下的.asf 文件:

ADC 和DAC 的逻辑和硬核对应关系,是在上述文件中指定的。
上述文件中,5 行,分别定义了3 个ADC 和2 个DAC 在硬核逻辑中的对应关系。

上述配置,对照analog_ip.v 中的代码,就会发现gen_per 对应到实例化的对象列表如下:

这里要注意asf 中每一行的最后一个参数{22 xx}。
这个{22 xx},其实是已经预设好的硬核ID(或者叫“硬核位置/坐标”)。
比如:
{22 7}对应的是ADC0 的ID,在cpld 最终编译后这个位置就是硬核ADC0 的连接点;
{22 8}对应的是ADC1 的ID,在cpld 最终编译后这个位置就是硬核ADC1 的连接点;
… …
注:这些位置坐标在内部已经指定好(由supra 编译时指定),不能更改。

另外,该.asf 内的配置,其实也是AG32 的默认配置:
即:如果用户没有这样的配置,那么编译时默认ADC 和DAC 的配置就是这样的顺序。如果用户有自定义的配置,则编译时使用用户的配置。

知道了以上的对应关系,那么想改变顺序,配合cpld 的代码来对应调整asf 即可。

另外,3 路独立的ADC,在各个通道上是连通的。
ADC 采集时,引脚只和通道相关。跟使用哪个ADC 其实关系不大。不管cpld 里边配置了几路ADC,硬核的3 路ADC 都是连通并打开的。
也就是说,就算mcu/cpld 里只打开了ADC0,但asf 里将它配置成ADC 硬核1,cpld 里仍然能读到正常的值(cpld 认为是从adc0 读,但实际从adc1 读)。
对比DAC,DAC 则是一路DAC 对应一个引脚,指定了DACx 就是指定了引脚。在这里其实跟ADC 没有类比性。