STM32 高速ADC 數據採集(內置,外置SPI,DMA方式)

     大數據本質上是模擬大數據,許多情況下模擬量數據對於數據分析更有價值。在這篇博文中,我們重點來談談Mbed OS 操作系統下的ADC高速數據採樣。

Mbed OS 下的模擬量IO

Mbed OS 的 API 中有模擬量IO:

AnalogIn

AnalogOut

     它們是針對MCU 內部ADC 輸入和DAC 輸出。如果使用過它們的化,就知道它們很慢。根本沒有辦法適應高速數據採集。如果要實現高速ADC 輸入,就需要使用STM32 的HAL 庫自己來設計。如果外擴SPI 的ADC 芯片也是如此,如過低速的話可以使用DigitalOut 和SPI 類來實現。但是要實現高速,就需要HAL 來配合了。

內置ADC 採樣

  ADC 數據採集的方式有兩種,一種是使用內部的ADC,其優點是和CPU集成在一起,MCU 廠商對內置ADC 的支持非常強大。在STM32 系列中,支持下面幾種方式

   查詢方式

   中斷方式

   DMA方式

顯然,Mbed OS 支持的是查詢方式。所以很慢。如果使用中斷方式,那麼每次採樣一個模擬量需要產生一次中斷,執行一大堆中斷處理程序。反而比查詢方式還要慢。要實現高速ADC 輸入的化,唯有采用DMA 方式最合適。

內置ADC 的缺點是它們的精度只有12位。

使用DMA 方式實現ADC 輸入,看上去並不難,其要點是:

1 使用一個定時器定時產生觸發信號,觸發ADC  採集數據

2 當ADC 採樣完成時,觸發DMA 將ADC 傳輸到內存

3 可以設置DMA 位循環方式,啓動DMA 是指定一個緩衝區。這樣DMA 可以連續採集ADC 數據到內存,期間不需要任何程序的干預。(別忘了,DMA 就是指 外設直接內存訪問)

4 DMA 能夠產生一個半完成中斷(HAL_ADC_ConvHalfCpltCallback),和整個完成中斷(HAL_ADC_ConvCpltCallback)。分別是當數據達到緩衝區長度的一半時產生中斷和數據達到緩衝區最高位時產生中斷。

   網絡上有許多人寫了關於STM32 TIM ADC DMA 數據轉換的方式。但是沒有一個是完整的。而且存在各種坑,這也不能怪他們,各自的情況不同,而且作者編寫和轉發的時間也不同。STM32F 包袱也夠多的,早期使用標準庫,現在又使用HAL 庫,又有各種版本cubeMX 工具,所以簡單地拷貝/黏貼很難解決問題。

   在Mbed OS 下,實現底層IO 程序設計是可行的,採用的是HAL 庫。我採用方式是用STM32CubeMX 工具配置好參數之後,然後Copy 到Mbed 中來。

     我寫了一個mbed OS 例子,它採集兩路內置ADC 的數據,並通過UDP 上傳到PC 機上供python 做FFT 個顯示,速度做到十幾M沒有問題。希望對大家有所幫助。程序是調通的,請放心參考。

#include "mbed.h"
#include "stm32f4xx_hal.h"
#include "EthernetInterface.h"
static const char*          mbedIp       = "192.168.31.110";  //IP
static const char*          mbedMask     = "255.255.255.0";  // Mask
static const char*          mbedGateway  = "192.168.31.1";    //Gateway
#define SERVER_PORT   2019
#define SERVER_ADDR "192.168.31.99"
#define UDP_PORT    2018
EthernetInterface eth;
UDPSocket udpsocket;
DigitalOut led(PC_6);
//DigitalOut led1(PC_7);
AnalogOut aout(PA_5);
Thread thread;
uint16_t ADC_DMA_ConvertedValue[512];
bool bufFlg;
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
TIM_HandleTypeDef htim3;
#define DMA_FLAG (1UL << 0)
EventFlags dma_flags;
void Error_Handler(void)
{
 printf("HAL error\n");
}
extern "C" void TIM3_IRQHandler(void)
{ 
 HAL_TIM_IRQHandler(&htim3);  
  }
 extern "C" void DMA2_Stream0_IRQHandler(void)
{
    // led=!led;
    
    HAL_DMA_IRQHandler(&hdma_adc1);
    // dma_flags.set(DMA_FLAG);
} 

void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc){
   led=1;
    bufFlg=false;
    dma_flags.set(DMA_FLAG);
    }  
 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc){
     led=0;
    bufFlg=true;
     dma_flags.set(DMA_FLAG);
    }     
/** 
  * Enable DMA controller clock
  */
void DMA_Init(void){
    __HAL_RCC_DMA2_CLK_ENABLE();
     hdma_adc1.Instance = DMA2_Stream0;
    hdma_adc1.Init.Channel = DMA_CHANNEL_0;
    hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;
    hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
    hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
    hdma_adc1.Init.Mode = DMA_CIRCULAR;
    hdma_adc1.Init.Priority = DMA_PRIORITY_MEDIUM;
    hdma_adc1.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
    HAL_DMA_Init(&hdma_adc1);
    __HAL_LINKDMA(&hadc1,DMA_Handle,hdma_adc1);
    HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);
     }
  void TIM_Init(void)
 { 
 // TIM_SlaveConfigTypeDef sSlaveConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};
  __HAL_RCC_TIM3_CLK_ENABLE();
  htim3.Instance               = TIM3;
  htim3.Init.Prescaler         = 72-1;
  htim3.Init.CounterMode       = TIM_COUNTERMODE_UP;
  htim3.Init.Period            = 100-1; 
  htim3.Init.ClockDivision     = TIM_CLOCKDIVISION_DIV1;
  
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig);
  HAL_TIM_Base_Init(&htim3);
  //NVIC_EnableIRQ(TIM3_IRQn);
  }
  void ADC_Init(void){
  GPIO_InitTypeDef GPIO_InitStruct;
 __HAL_RCC_GPIOA_CLK_ENABLE();
 __HAL_RCC_ADC1_CLK_ENABLE();           
    /**ADC1 GPIO Configuration    
    PA3     ------> ADC1_IN3
    PA4     ------> ADC1_IN4
    PA5     ------> ADC1_IN5
    PA6     ------> ADC1_IN6 
    */
    GPIO_InitStruct.Pin = GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6;
    GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    hadc1.Instance=ADC1;
    hadc1.Init.DataAlign=ADC_DATAALIGN_RIGHT;             //右對齊
    hadc1.Init.ScanConvMode=ENABLE;                      //不掃描模式
    hadc1.Init.ContinuousConvMode=DISABLE;                //不連續轉換
    hadc1.Init.NbrOfConversion=2;                         //一個規則通道轉換 
    hadc1.Init.DiscontinuousConvMode=DISABLE;             //禁止不連續採樣模式
    hadc1.Init.NbrOfDiscConversion=0;                     //不連續採樣通道數爲0
    hadc1.Init.DMAContinuousRequests = ENABLE;
    hadc1.Init.EOCSelection = ADC_EOC_SEQ_CONV;
    hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING;
    hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T3_TRGO;
    HAL_ADC_Init(&hadc1);   
    ADC_ChannelConfTypeDef ADC1_ChanConf;
    ADC1_ChanConf.Channel=3;                                   //通道3
    ADC1_ChanConf.Rank=1;                                      
    ADC1_ChanConf.SamplingTime = ADC_SAMPLETIME_3CYCLES;           
    HAL_ADC_ConfigChannel(&hadc1,&ADC1_ChanConf);            
    ADC1_ChanConf.Channel=4;                                   //通道4
    ADC1_ChanConf.Rank=2;                                     
    ADC1_ChanConf.SamplingTime = ADC_SAMPLETIME_3CYCLES;           
    HAL_ADC_ConfigChannel(&hadc1,&ADC1_ChanConf);  
}
 void wave(void){
  uint16_t sample = 0;
    while(1) {
        for (int i = 0; i < 8; i++) {
            sample=sample+512;
        aout.write_u16(sample);
        wait_us(2);
         }
      }
} 
int main()
{  int i;
  printf("ADC DMA Test\n"); 
  eth.set_network(mbedIp,mbedMask,mbedGateway);
  eth.connect();
  printf("\nConnected  IP Address : %s\n", eth.get_ip_address());
  
  udpsocket.open(&eth);
  udpsocket.bind(eth.get_ip_address(),UDP_PORT);
  TIM_Init();
  DMA_Init();
  ADC_Init();
  bufFlg=false;
  HAL_ADC_Start_DMA(&hadc1,(uint32_t *)ADC_DMA_ConvertedValue,512); 
  HAL_TIM_Base_Start_IT(&htim3);
  thread.start(wave); 
  while(1) {
    dma_flags.wait_any(DMA_FLAG);
    //    for (i=0;i<8;i++)  
    //    printf("%d  ",ADC_DMA_ConvertedValue[i]);
    //    printf("\n");
    if (bufFlg)
    udpsocket.sendto(SERVER_ADDR,SERVER_PORT, &ADC_DMA_ConvertedValue[256], 512);
    else
    udpsocket.sendto(SERVER_ADDR,SERVER_PORT, &ADC_DMA_ConvertedValue[0], 512);
    dma_flags.clear(DMA_FLAG);
   // wait(1);     
    }
}

外置ADC 方式

   如果嫌棄內置ADC 的精度不夠,那麼可以考慮使用外部ADC 芯片方式。ADI ,TI 這些大公司提供了各自ADC 芯片。本人就使用過AD7689,ads1256,ads127L和ads1274 等芯片和STM32 連接。 這些芯片大多數是以SPI 接口與STM32 相連接。Mbed OS 本身支持SPI 接口,如果使用探詢方式讀取ADC 芯片的話,當然Mbed OS 的SPI 完全可以勝任。如果需要高速ADC 轉換的話,就遇到和內置ADC 同樣的問題了。需要使用DMA 方式。

  深入地研究之後發現,SPI 接口ADC 芯片的DMA 方式也不是省油的燈。而且網絡上的成功例子更加是少的可憐。

  只有靠自己了,我採取的方案如下

ADC 芯片的主時鐘

ADC 芯片需要一個主時鐘,可以外接一個晶振。但是ADC 芯片內部通常有一個分頻,不過也是固定的幾種,也可以通過MCU 可編程輸入。我傾向採用MCU 產生,多少也省了個晶振電路。

 由 TIM4 的通道1 產生一個PWM 的脈衝信號作爲 ADC 的工作時鐘。由TIM2 CH1 (OC_1)輸出。我選取 2MHz。

ADC芯片 的DRDY 信號的俘獲

  當ADC 完成一次採樣後,DRDY 會產生一個低電平脈衝。MCU 需要儘快地將數據通過SPI 讀取。在查詢方式下,通常是採取while 語句實現。

while(DRDY){};

       Data=ADS127L01_ReadData();

不過 在SPI DMA 方式下,如何啓動SPI 的DMA 呢?要知道,STM32 的DMA 是不可以通過GPIO 來觸發啓動的。

  我們採取的方法是使用TIM3 ,將它設置成爲 ETR 計數方式(1 個脈衝數),將DRDY 接入 TIM3_ETR 輸入腳。一旦DRDY 下降沿到來,TIM3計一個脈衝,產生內部的觸發信號TRGO。並且由該信號啓動一個DMA,用它來觸發SPI 發送的DMA傳輸。

SPI 的DMA

SPI 是一種主從式同步串行通信,MCU 設置爲主模式,ADC 芯片爲從模式。當MCU 發送數據時會發送SCLK 時鐘,在發送的同時,也接受了數據。說白了,就是通過發送來接受數據。

   爲了實現SPI 的DMA 傳輸,需要兩個DMA 來實現,一個DMA 由TIM3 啓動,用於發送,另一個DMA 用於SPI 接受。

所以說,實現ADC SPI 芯片 的DMA 傳輸,需要兩個TIM ,兩個DMA和一個SPI

 

硬件接口

TI ads127L01/ads1274 ADC 芯片和STM32F429ZI 接線方式。

ADC_CLK <-TIM4 CH1

DRDY  ->  TIM3_ETR

DOUT  -> SPI1 MISO

DIN <-    SPI MOSI

SCLK <-   SPI CLK

我編寫了TI ads127L01 和ads1274兩種芯片的驅動,源代碼調試通過後在放出來吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章