nalog代码分析内容列表:1.analog的说明及结构;2.mcu端的处理;3.cpld端analog_ip描述及对ahb2apb的应用;4.cpld端对ADC的处理;5.cpld端对DAC的处理;6.cpld端对CMP的处理;7.底层逻辑的对应(3路ADC映射到3个硬核)一、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寄存器使用。填充真正要采集的channalid。如果只读取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语言这样调用:intcpRdReg=*((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的模块组成和调用:——————————————————————用户逻辑模块(moduleanalog_ip)//用户模块入口桥(ahb2apb)3个ADC实例(apb_adc)2个DAC实例(apb_dac)1个CMP实例(apb_cmp)endmodulemoduleapb_adc//adc模块定义xxxxendmodulemoduleapb_dac//dac模块定义
xxxxendmodulemoduleapb_cmp//cmp模块定义xxxxendmodule——————————————————————整个结构中看,analog_ip模块包含了7个实例:ahb2apb、3个ADC、2个DAC、1个CMP。接下来对analog_ip进行具体代码的分析。moduleanalog_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为busclock。而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:ADC01:ADC12:ADC23:DAC04:DAC15: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里的全部操作,最终都是要转换为对这里接口的操作的。采样的数据也来源于这里。先看以上接口的几个信号的意义:(这部分可以从ReferenceManual手册上获取)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(宽度为一个时钟周期)的低电平,在这个周期的下降沿开始,外部可以读取硬核内转换后的值(这个值会保留到下次采样结束)。ReferenceManual手册(文件内搜索ADC)上对ADC时序描述如下:
接下来,继续看ADCmodule,看模块内如何处理apb_xxx信号的输入,将信号转换后操作上边的alta_adc。在具体代码分析时,注意里边的3个周期:1.apb的clk时钟周期;2.apb分频后传给adc的clk时钟周期;3.adc中采样一次数据的时间(采样一次完整的ADC数据,需要12+1个adc_clk周期)。其实这3个周期也是理解以下代码的基础。不理清这3个层面,代码是无法完全理解的。后边讲解中,这3个周期会经常出现。进入moduleapb_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这个值的意义,是当前动作是否为读/写通道(adcchannal);第二个表达式里,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信号;这个信号平时为高电平,在采样完后有一个adcclk周期的低电平。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来读取数据后,发送dmaclear信号时,清除该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的接口定义:先看以上接口的几个信号的意义:(这部分可以从ReferenceManual手册上获取)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的讲解。继续看DACmodule,看模块内如何处理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的一组。而从接口定义看,如下:拆开成两组。每组有:intputenb,hyst,mode,input[2:0]imsel,input[1:0]ipsel,outputout,各项意义: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中每一行的最后一个参数{22xx}。这个{22xx},其实是已经预设好的硬核ID(或者叫“硬核位置/坐标”)。比如:{227}对应的是ADC0的ID,在cpld最终编译后这个位置就是硬核ADC0的连接点;{228}对应的是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对应一个引脚,