jicheng0622

【原创经典】Kinetis 16-bit ADC+DMA+定时器实现AutoScan自动通道扫描采样

1
阅读(6447) 评论(16)

    这次是我第一次尝试在“原创”的后面又加了经典俩字,至于为啥(俺的东北话又情不自禁冒出来了,呵呵),一个是自己一时兴起(今天心情相比于前段时间稍好些),二个是我自认为本篇文章是俺的一篇得意之作而且对广大Kinetis用户也是非常有用的一篇技术博文(对俺来说担当的起“经典”俩字),所以为了避免自己有沽名钓誉之嫌有必要在这里解释一下,呵呵。当然了,不能说俺之前的文章不经典啦(那岂不是在打自己的脸,哈哈),只是相对来说,我觉着这篇文章对一些应用场合更有实际的意义。咳咳,好吧,自己不小心又唠叨了这么多,发现自打写博客之后我都快成为话唠了,用那句经典的广告语来说就是“根本停不下来”啊,呵呵~

    好了,言归正传,我之前不止在一篇博客里面提到过飞思卡尔Kinetis独有的片内16位高精度ADC(其他家大多都是12位的),这个16位分辨率的ADC,虽然其不可能精度达到全16位,但是其最高13/14位的精度也不是其他12位ADC能比的(而且其还支持单端和差分输入,自带PGA和硬件平均功能),所以Kinetis在一些比如温度、重量和电池电压监控等测量方面的应用有其独特的优势。但是让人又爱又恨的是,这个16位ADC不支持通道自动扫描的功能,也就是说每次转换一个通道都需要软件或硬件触发一次(估计上这是一种成本上的妥协和折衷吧),这个问题对上面所提到的温度和重量等大滞环系统没有影响(他们本身对你时间没有太高的要求,一个个慢慢转换就是了),但是在一些对信号的采样率和实时性要求比较高的场合,如果使用查询等待的方式会干耗掉CPU的资源,而使用中断方式的话又会有太多的中断响应。那有没有一种比较优化的方式实现来满足这种对采样率和实时性要求较高的需求呢,咳咳,有点废话了,没有的话我写这篇文章干嘛,呵呵,所以本篇的主要目的就是通过使用ADC+DMA+定时器来灵活实现Kinetis内部ADC自动通道扫描的方法,下面开整,走着:

测试平台:YL_KL26(优龙做的基于KL26的开发板,与FSL官方的FRDM-KL26很相近)

开发环境:IARv7.3

测试代码:基于FRDM-KL26Z_SC\FRDM-KL26Z_SC_Rev_1.0\klxx-sc-baremetal\build\iar\hello_world代码修改

功能描述:以100ksps的采样率对ADC的Channel0(接到YL-KL26板载的变阻器)和Channel26(KL26内部温度传感器)以DMA方式扫描采样,对一个Buffer存满10次采样数据后切换到另一个Buffer继续存,而对之前的Buffer进行处理(数组的偶数元素存的是Channel0的数据,奇数元素存的是Channel26的数据),从而实现双缓存。

(1)首先定义需要用到的变量和宏,如下所示,其中BUFFER_SIZE定义了Buffer的存储深度(为了测试,我先只定义了10个数组,其中5个for Channel0的数据,5个for Channel26的数据),pCounter用来做双缓存的切换计数,ADC_Channel数组为要采样的Channel list(至于本来是2个通道,为什么定义成4个数组,这是由于DMA的Circular Buffer模数计数最小为16个字节构成循环,一个Channel占用4个字节,所以最少定义4个元素,凑足16个字节),而剩下的Result_Buffer和Buffer_Address则为数据缓存和缓存的首地址,用来做DMA目的地址;

#define BUFFER_SIZE     10

#pragma data_alignment = 16 //The ADC_Channel address should be align with the DMA Circular buffer size
uint32 ADC_Channel[4]={0, 26, 0, 26};   //Channel list for Channel0 and Channel 26
unsigned char pCounter=0;               // Counter used for Dual buffer switch
uint8 Result_Buffer1[BUFFER_SIZE]={0};  //Buffer1 
uint8 Result_Buffer2[BUFFER_SIZE]={0};  //Buffer2
uint32 Buffer_Address[2]={(uint32)&Result_Buffer1, (uint32)&Result_Buffer2};    //Buffer list

(2)其次初始化ADC模块(为了方便我定义成8位分辨率),禁止ADC转换完成中断,使能ADC转换完成触发DMA,同时使能单次软件触发模式(即每次写入ADC0_SC1的通道号触发一次ADC转换)。需要注意的是ADC的输入时钟源要小于18MHz;

void init_ADC8(void) 
{ 
  
  // Turn on the ADC0 clock 
  SIM_SCGC6 |= (SIM_SCGC6_ADC0_MASK ); 
            
  ADC0_CFG1 = ADC_CFG1_ADIV(1)          //divide ratio is 2 and the clock rate is (input clock)/2. 
              | ADC_CFG1_MODE(0)        // single-end 8bit mode 
              | ADC_CFG1_ADICLK(0);     //Bus clock         
  
  ADC0_CFG2 = 0; 
  
  ADC0_CV1 =  0x1234u;                  // can be anything 
  ADC0_CV2 = 0x5678u;                   // can be anything 
               
  ADC0_SC2 = 0 | ADC_SC2_DMAEN_MASK;    //ADC conversion complete DMA enable 
  
  ADC0_SC3 = 0;                         // Hardware average disable and once conversion
}

(3)然后初始化TPM定时器模块,该模块配置成自由计数模式,溢出中断触发DMA,其中MOD寄存器即溢出时间决定ADC的采样率,本例程配置成每100kHz的频率溢出一次;

void init_TPM1(void) 
{ 
  SIM_SCGC6 |= (SIM_SCGC6_TPM1_MASK ); 
  SIM_SOPT2 |= SIM_SOPT2_TPMSRC(1); //input clock source is MCGPLL/2=24MHz 
  TPM1_SC = TPM_SC_DMA_MASK | TPM_SC_PS(3);//Divide by 3M=24M/8 
  TPM1_CNT = 0; 
  TPM1_MOD = 30;        //timer interrupt triger with 100ksps 
  
  TPM1_SC |= TPM_SC_CMOD(1);    //start the counter 
}

(4)这部分是DMA的初始化,本例程的核心部分,本例程的实现需要使用两个Channel的DMA,其中一个channel的DMA与TPM定时器配合实现周期性触发ADC转换,即每次TPM定时器溢出触发一次DMA将Channellist的通道号写到ADC_SC1寄存器从而触发一次ADC转换,然后ADC转换完成触发另一个DMA Channel将ADC的结果传到其中一个Buffer,具体配置俺就不多说了,看下面的程序注释就行了,至于TPM和ADC在DMA中的触发源可以在KL16的参考手册中找到,如下图,其中ADC0的触发源为40,TPM1溢出触发源为55;

void DMA_2ChanelADC_Init(void) 
{ 
  SIM_SCGC6 |= SIM_SCGC6_DMAMUX_MASK; 
  SIM_SCGC7 |= SIM_SCGC7_DMA_MASK; 
  
  enable_irq(INT_DMA1-16); 
  
  DMA_SAR0 = (uint32) &ADC_Channel;    //Set source address to ADC_Channel 
  DMA_DSR_BCR0 = DMA_DSR_BCR_BCR(4*BUFFER_SIZE);    //Set BCR to know how many bytes to transfer, 8 bytes for two 32-bit transfer 
  DMA_DCR0 &= ~(DMA_DCR_SSIZE_MASK | DMA_DCR_DSIZE_MASK);    //Clear source size and destination size fields 
  /* Set DMA as follows: 
       Source size is 32-bit size 
       Destination size is 32-bit size 
       Cycle steal mode 
       External requests are enabled 
       source address increments 1 automatically 
*/ 
  DMA_DCR0 |= (DMA_DCR_SSIZE(0) 
             | DMA_DCR_DSIZE(0) 
             | DMA_DCR_CS_MASK  
             | DMA_DCR_ERQ_MASK  
             | DMA_DCR_SINC_MASK 
             | DMA_DCR_SMOD(1)); //16 bytes circular buffer 
  
  DMA_DAR0 = (uint32) &ADC0_SC1A;//0x4003B000;    //Set source address to ADC0_SC1A 
  DMAMUX0_CHCFG0 = DMAMUX_CHCFG_SOURCE(55);    //Select TPM1 overflow as channel source 
  DMAMUX0_CHCFG0 |= DMAMUX_CHCFG_ENBL_MASK;    //Enable the DMA MUX channel 
  
  /*********************************************************************/ 
  DMA_SAR1 = (uint32) &ADC0_RA;//0x4003B010;    //Set source address to ADC0_RA 
  DMA_DSR_BCR1 = DMA_DSR_BCR_BCR(BUFFER_SIZE);    //Set BCR to know how many bytes to transfer 
  DMA_DCR1 &= ~(DMA_DCR_SSIZE_MASK | DMA_DCR_DSIZE_MASK);    //Clear source size and destination size fields 
  /* Set DMA as follows: 
       Source size is 8-bit size 
       Destination size is 8-bit size 
       Cycle steal mode 
       External requests are enabled 
       Destination address increments 1 automatically 
       BCR Bytes trasfer completed interrupt enable 
*/ 
  if ((DMA_DSR_BCR1 & DMA_DSR_BCR_DONE_MASK) == DMA_DSR_BCR_DONE_MASK) 
    DMA_DSR_BCR1 |= DMA_DSR_BCR_DONE_MASK; 
  
  DMA_DCR1 |= (DMA_DCR_SSIZE(1) 
             | DMA_DCR_DSIZE(1) 
             | DMA_DCR_CS_MASK  
             | DMA_DCR_ERQ_MASK  
             | DMA_DCR_DINC_MASK 
             | DMA_DCR_EINT_MASK); 
  
  DMA_DAR1 = (uint32) &Result_Buffer1;    //Set source address to result buffer 
  DMAMUX0_CHCFG1 = DMAMUX_CHCFG_SOURCE(40);    //Select ADC0 COCO flag as channel source 
  DMAMUX0_CHCFG1 |= DMAMUX_CHCFG_ENBL_MASK;    //Enable the DMA MUX channel 
}

image

(5)最后是DMA中断服务函数的配置,这也是一个核心部分,主要涉及到双缓存的切换(可以看到DMA Channel1的目的地址在两个buffer地址之间切换),另外一定记得每进一次DMA完成中断都要重新填一次DMA_DSR_BCR寄存器让其重新开始计数,还有可能有博友比较疑惑的是为啥BCR0的size是BCR1的一倍,这是因为DMA Channel0每次传输的是32位即4个字节的ADC采样通道号(写32位的ADC_SC1寄存器),而DMA Channel1每次传输8位即一个字节的ADC结果(因为我前面把其配置成8位单端模式了),所以这个比例是4:1,如果配置成16位ADC模式的话,这个比例就该改成2:1了,而且前面DMA初始化部分也需要做相应修改。

void DMA1_IRQHandler(void) 
{ 
   
  /* Create pointer & variable for reading DMA_DSR register */   
  if ((DMA_DSR_BCR1 & DMA_DSR_BCR_DONE_MASK) == DMA_DSR_BCR_DONE_MASK) 
  {         
    DMA_DSR_BCR0 |= DMA_DSR_BCR_DONE_MASK;      //Clear Done bit 
    DMA_DSR_BCR1 |= DMA_DSR_BCR_DONE_MASK;      //Clear Done bit 
    
    if(++pCounter>=2) 
    { 
      pCounter=0; 
    } 
    
    DMA_DAR1 = (uint32) Buffer_Address[pCounter];    //Set source address to result buffer 
    
    DMA_DSR_BCR0 = DMA_DSR_BCR_BCR(4*BUFFER_SIZE);      //Reset BCR 
    DMA_DSR_BCR1 = DMA_DSR_BCR_BCR(BUFFER_SIZE);      //Reset BCR 
   
  }  
}

(6)好了,一切准备完毕(当然还有一些具体的细节操作,包括把DMA1中断函数加载到中断向量表等,我就不多做介绍了,可以参考我博文下面附件中的样例工程),编译链接整个工程,然后下载到板子并运行。结果如下图所示,可以看到每进一次中断(即BCR值减到0)切换一次Buffer地址,实现双缓存,效果还是不错的,呵呵。此外,该工程也比较灵活,通过修改BUFFER_SIZE宏来修改采样数据的存储深度,修改TPM1_MOD值来配置ADC的采样率,当然需要注意这个采样率不要超过ADC在当前输入时钟频率下不要超过其最大的转换率,至于原因,你懂的。。。

    呼呼,一下子写了不少,估计需要博友们好好消化一下了。经此一役,发现通过DMA还是可以有很多想象的空间的,只有想不到没有做不到,哈哈。好了,写的有点多,休息一下,准备再战,呵呵,未完待续~

附件为完整IAR下的测试工程:

KL26_DMA_2ChannelADC.zip

  1. @AET-程品豹   

    好久也没看见你了 

    哈哈,是啊,最近被折腾的心醉了!

  2. @天蝎   

    你的忠实博粉啊,期待经常更新!

    话说,公司这么转了几道,对你们有影响吗?

    呵呵,影响挺大,闹心!

  3. 你的忠实博粉啊,期待经常更新!

    话说,公司这么转了几道,对你们有影响吗?

  4. @jicheng0622   

    好久没见天蝎你了啊,呵呵!

    好久也没看见你了 

  5. @天蝎   

    这么神奇!看来多来这还挺有帮助哈

    好久没见天蝎你了啊,呵呵!

  6. @mc4win   

    感谢大神,提供该思路,让我想到了另一种提高我们项目整个采样速度的方案。感谢大神啊。

    很高兴对你有所帮助啊,抛砖引玉了,哈哈。

  7. @天蝎   

    这么神奇!看来多来这还挺有帮助哈

    这里人比较少而精啊。。。

  8. @mc4win   

    感谢大神,提供该思路,让我想到了另一种提高我们项目整个采样速度的方案。感谢大神啊。

    这么神奇!看来多来这还挺有帮助哈

  9. 感谢大神,提供该思路,让我想到了另一种提高我们项目整个采样速度的方案。感谢大神啊。

  10. 帅哥你对飞思卡尔的MCU好熟啊,方便QQ交流沟通下吗?谢谢

  11. 帅哥你对飞思卡尔的MCU好熟啊,方便QQ交流沟通下吗?谢谢

  12. 帅哥你对飞思卡尔的MCU好熟啊,方便QQ交流沟通下吗?谢谢

  13. 用惯了stm32 换成这个好不习惯啊

  14. 用惯了stm32 换成这个好不习惯啊

  15. @匿名用户(180.166.94.122 )
    嗯,这个是L系列的,K系列的跟这个不太一样(主要是DMA的区别,K系列是eDMA,L系列是DMA),我没有写K系列的。