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)endmodulemodule apb_adc //adc 模块定义xxxxendmodulemodule apb_dac //dac 模块定义xxxxendmodulemodule apb_cmp //cmp 模块定义xxxxendmodule——————————————————————整个结构中看,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 没有类比性。 PREVIOUS ← analog中对ADC的剪裁