查看: 21969|回复: 7

【也是冷知識】如何加快analogRead速度提高采樣率Sampling Rate?

[复制链接]

该用户从未签到

发表于 2015-3-9 01:35 | 显示全部楼层 |阅读模式
本帖最后由 tsaiwn 于 2015-3-9 02:06 编辑

    這部分內容原先是回答某位網友的, 重新整理方便大家查看!

根據官網說明, analogRead( ) 大約要 100us:
   http://arduino.cc/en/Reference/analogRead
也就是說, 一秒最多只能讀取大約一萬次(10K),
更正確的說, 理論上 sampling rate 是 9600 Hz, 接近 10KHz,
但這是因為 Arduino 的 ADC 之  Prescaler 被設為 128;
所以, 假設 Arduino 的時脈(Clock)是 16MHz,
則 ADC clock = 16MHz / 128 = 125KHz;
一次 ADC 轉換要踢它13下, 就是要 13 clock (tick),
於是變成 125KHz/13 = 9600Hz
請注意, 這是因為在 analogRead( ) 內必須等 ADC 取樣完成才會返回,
關於 analogRead( ) 的源代碼與解說可以參考這:
   http://garretlab.web.fc2.com/en/ ... g.c/analogRead.html


    不過 9600Hz 的sampling rate 只是理論值, 因為你的程序本身也要花時間,
調用函數進出需要時間, for Loop 本身也要時間,
把 sample(采樣) 到的值加總到 total 也要時間 !
所以, 如果你用一個 for Loop 全力一直做 analogRead(A0),
那一秒鐘也隻能讀取大約 8925 次, 達不到理論值 9600次 !!
不相信, 用你的 Arduino 測試一下:

[mw_shl_code=c,true]//////
// test A0 sampling rate -- by  tsaiwn@cs.nctu.edu.tw
const int pin = A0;
const int n = 1000;  // sample 採樣 1000 次
void setup() {
  Serial.begin(9600);
  for(int i=0; i< 543; i++) analogRead(pin); // 熱身 :-)
  Serial.println(String("Sample ") + n + " times, pin=" + pin);
  Serial.flush( );
  delay(568);
}
void loop( ) { //
  unsigned long begt, runt, total;
  total = 0;  // clear before sampling
  begt = micros();
  for(int i=0; i< n; i++) {
     total += analogRead(pin);
  }
  runt = micros() - begt;  // elapsed time
  Serial.println(String("Average=") + total/n);
  Serial.print(String("Time per sample: ")+runt/1.0/n +"us");
  Serial.println(String(", Frequency: ")+1000000.0/runt*n +" Hz");
  delay(5566);
}// loop(
[/mw_shl_code]


  你看, 以上程序已經簡短到無法再簡短了,
但是經過實際測試, 結果 sampling rate 只有不到 9KHz,(我測試結果大約 8925 次)
這是因為除了 analogRead( )等待 ADC 轉換的延遲外,
程序中 for Loop 與 tatal += 也都要一些時間 !
有興趣的可以看看 analogRead( ) 這函數的源代碼:

int analogRead(uint8_t pin) {
   extern unsigned char analog_reference;
   uint8_t low, high;
   const unsigned char bitADSC = (1<<ADSC);
   if (pin >= 14) pin -= 14; // allow for channel or pin numbers
   // set the analog reference (high two bits of ADMUX) and select the
   // channel (low 4 bits).  this also sets ADLAR (left-adjust result)
   // to 0 (the default).
   ADMUX = (analog_reference << 6) | (pin & 0x07);
   // start the conversion
   ADCSRA |= (1 << ADSC);  //    sbi(ADCSRA, ADSC);
   // ADSC is cleared when the conversion finishes
   while (bit_is_set(ADCSRA, ADSC));  // while(ADCSRA & bitADSC) ;
   // we have to read ADCL first; doing so locks both ADCL
   // and ADCH until ADCH is read.  reading ADCL second would
   // cause the results of each conversion to be discarded,
   // as ADCL and ADCH would be locked when it completed.
   low  = ADCL;
   high = ADCH;
   // combine the two bytes
   return (high << 8) | low;
}

  看到了吧,  analogRead( ) 裡面有個如下的 while Loop:
     while (bit_is_set(ADCSRA, ADSC));
這句意思是要一直等到 ADC 轉換完成把 ADSC 這 bit 清除為 0;
然後才可以讀取 ADCL 與 ADCH, 合成總共 10 bit, 代表 0 ~ 1023 的取樣值 !

  剛剛說過, 理論值 9.6KHz 是因為 ADC 的 Prescaler設為128;
不過, Prescaler是可以改的!
只要把 ADC 的 Prescaler 改小, 就可以加快 ADC 轉換速度與 analogRead( )速度!

    例如, 把 ADC 的 Prescaler 改為 16,
則理論的 Sample Rate 可達 16MHz / 16 / 13 = 76.8KHz
不過, 經過實測只能達到大約 58KHz
    若 ADC 的 Prescaler 改為 8,
則理論的 Sample Rate 可達 153KHz, 但實測隻有大約93.5KHz
可是一般不建議把 Prescaler 設在 16以下, 否則ADC轉換不太準 !!

以下是測試用 Prescaler 16, 採樣頻率 大約 58KHz;
但在以下程序中,
我已經幫忙寫了數個可以把 ADC 的 Prescaler 設為各種值的函數:

[mw_shl_code=c,true]//////
// speed up sampling rate -- by tsaiwn@cs.nctu.edu.tw
const int pin = A0;
const int n = 1000;  // sample 採樣 1000 次
void setup() {
  Serial.begin(9600);
  //setP64( ); // Prescaler = 64
  //setP32( ); // Prescaler = 32
  //setP8( );   // Prescaler = 8
  setP16( );   // Prescaler = 16
  //setP128( ); // Prescaler = 128 = default
  for(int i=0; i< 543; i++) analogRead(A0); // 熱身 :-)
  Serial.println(String("Sample ") + n + " times, pin=" + pin);
  Serial.flush( );
  delay(568);
}
void loop( ) { //
  long begt, runt, total;
  total = 0;  // clear before sampling
  begt = micros();
  for(int i=0; i< n; i++) {
     total += analogRead(pin);
  }
  runt = micros() - begt;  // elapsed time
  Serial.println(String("Average=") + total/n);
  Serial.print(String("Time per sample: ")+runt/1.0/n +"us");
  Serial.println(String(", Frequency: ")+1000000.0/runt*n +" Hz");
  delay(5566);
}// loop(
void setP16( ) {
  Serial.println("ADC Prescaler = 16");  // 100
  ADCSRA |=  (1 << ADPS2);   // 1
  ADCSRA &=  ~(1 << ADPS1);  // 0
  ADCSRA &=  ~(1 << ADPS0);  // 0
}
void setP8( ) {  // prescaler = 8 ; 不建議設為 16 以下!
  Serial.println("ADC Prescaler = 8");  // 011
  ADCSRA &=  ~(1 << ADPS2);  // 0
  ADCSRA |=  (1 << ADPS1);  // 1
  ADCSRA |=  (1 << ADPS0);  // 1
}
void setP4( ) {  // prescaler = 4 ; 不建議設為 16 以下!
  Serial.println("ADC Prescaler = 4");  // 010
  ADCSRA &=  ~(1 << ADPS2);  // 0
  ADCSRA |=  (1 << ADPS1);  // 1
  ADCSRA &=  ~(1 << ADPS0);  // 0
}
void setP32( ) {
  Serial.println("ADC Prescaler = 32");  // 101
  ADCSRA |=  (1 << ADPS2);   // 1
  ADCSRA &=  ~(1 << ADPS1);  // 0
  ADCSRA |=  (1 << ADPS0);   // 1
}
void setP64( ) {  //  prescaler = 64 ;  // 110
  const unsigned char PS_128 = (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
  const unsigned char PS_64 = (1 << ADPS2) | (1 << ADPS1);
  Serial.println("ADC Prescaler = 64");  // 110
  ADCSRA &= ~PS_128;  // clear that 3 bits
  ADCSRA |=  PS_64;  //  set as 110
}
// Reference  http://www.microsmart.co.za/tech ... vanced-arduino-adc/
void setP128( ) { // 默認就是這樣
  Serial.println("ADC Prescaler = 128");  // 111
  ADCSRA |=  (1 << ADPS2);  // 1
  ADCSRA |=  (1 << ADPS1);  // 1
  ADCSRA |=  (1 << ADPS0);  // 1
} // setP128
[/mw_shl_code]


請注意, 雖然可以把 ADC 的 Prescaler 設為很小,
但是當 Prescaler 小於 16, 用 analogRead( )讀到的值將很不準確!
這有人做過實驗, 如果 Prescaler 在 32 或以上大致還沒問題, 參考:
   http://www.gammon.com.au/forum/?id=12779


補充:
  如果你還要更高的采樣率Sampling Rate, 那必須使用 ADC 轉換完成的中斷處理,
也就是要自己寫 ISR(ADC_vect) 中斷程序, 不要使用 analogRead( ),
自己直接從 ADC 轉換完成後的 ADCL 與 ADCH 讀取答案!

以下是個簡單範例, 只有讀取採樣 10 bits 的左邊 8 bit, 所以答案是 0 到 255:
[mw_shl_code=c,true]// 以下範例是只讀取 10 bits 中的左邊 8 bits
// 所以答案是 0..255, 如有必要, 你可以把該答案 map 到(0, 1023)
volatile unsigned long cnt;
volatile unsigned long total;
const unsigned long every = 50000;
void setup( ) {
  Serial.begin(9600);
  delay(123);
  initADC( );
}
unsigned long n, tot;
void loop( ) {
   if(cnt % every == 0) {
      cli( );
      n = cnt;
      tot = total;
      sei( );
      Serial.print("n="); Serial.print(n);
      Serial.print(", average=");
      Serial.println(tot*1.0/n);
   }
} // loop(

void initADC( ) {
  cli( );
  ADMUX |= (1 << REFS0); //set reference voltage
  ADMUX |= (1 << ADLAR); //left align the ADC value- so we can read highest 8 bits from ADCH register only
  /// 以上是設定 10 bit 的左邊 8 bit 改放在 ADCH
  ADCSRA &= ~(1 << ADPS1);  //  bitClear(ADPS1, ADPS1);   //  101
  ADCSRA |= (1 << ADPS2) | (1 << ADPS0); //set ADC clock with 32 prescaler- 16mHz/32=500kHz
  ADCSRA |= (1 << ADATE); //enabble auto trigger
  ADCSRA |= (1 << ADIE); //enable interrupts when measurement complete
  ADCSRA |= (1 << ADEN); //enable ADC  ;; bitSet(ADCSRA, ADEN);
  cnt = 0;
  ADCSRA |= (1 << ADSC); //start ADC measurements
  sei( );
} // initADC(
volatile int adcHigh;
ISR(ADC_vect) {//when new ADC value ready
  adcHigh = ADCH;  //update the new value from A0 (between 0 and 255)
  ++cnt;
}[/mw_shl_code]


其他參考文件:
   http://www.atmel.com/dyn/resources/prod_documents/DOC2559.PDF
   http://forum.arduino.cc/index.php/topic,6549.0.html
   http://www.microsmart.co.za/tech ... vanced-arduino-adc/
   http://meettechniek.info/embedded/arduino-analog.html
   http://www.instructables.com/id/ ... ling-rate-of-40kHz/
   https://bennthomsen.wordpress.co ... inuous-adc-capture/





该用户从未签到

 楼主| 发表于 2015-3-9 02:02 | 显示全部楼层
再補充一下, 透過 ADC 轉換器,
我們還可以讀取 Arduino 內部的溫度感應器,
然後把溫度傳送到 PC 電腦上!
不過, 因為每個 Arduino 板子實際上會略有差異,
所以以下的程序你必須實際測量後調整裡面 tempBias 的值:

[mw_shl_code=c,true]//從 Serial monitor 印出當前溫度
const int tempBias = 320;  // 要實際測量後調整這
void setup ( ){
  Serial.begin (9600);
  ADCSRA =  bit (ADEN);   // turn ADC on
  ADCSRA |= bit (ADPS0) |  bit (ADPS1) | bit (ADPS2);  // Prescaler of 128
  ADMUX = bit (REFS0) | bit (REFS1)  | 0x08;    // temperature sensor
  delay (20);  // let it stabilize
  bitSet (ADCSRA, ADSC);  // start a conversion  
  while (bit_is_set(ADCSRA, ADSC)) ;; // 等待轉換完成
  int value = ADC;
  Serial.print ("Temperature = ");
  Serial.println (value - tempBias);  
}  // setup(
void loop () {;}
// 參考  http://www.gammon.com.au/forum/?id=12779[/mw_shl_code]

该用户从未签到

发表于 2015-3-9 09:20 | 显示全部楼层
好贴,先收藏了慢慢研究

该用户从未签到

发表于 2016-6-6 12:56 | 显示全部楼层
最近正好有个ADC的问题困扰,楼主有空帮忙看看应该怎么修改比较合适?
[mw_shl_code=cpp,true]void wait(unsigned int time) {
  unsigned long start = micros();
  while (micros() - start <= time) {
  }
}
void high(unsigned int time, int freq, int pinLED) {
  int pause = (1000 / freq / 2) - 4;
  unsigned long start = micros();
  while (micros() - start <= time) {
    digitalWrite(pinLED, HIGH);
    delayMicroseconds(pause);
    digitalWrite(pinLED, LOW);
    delayMicroseconds(pause);
  }
}

class Sony {
  public:
    Sony(int pin);
    void shutterNow();
  private:
    int _pin;
    int _freq;
};
Sony::Sony(int pin)
{
  pinMode(pin, OUTPUT);
  _pin = pin;
  _freq = 40;
}
void Sony::shutterNow()
{
  bool _seq[] = {
    1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1
  };
  for (int j = 0; j < 3; j++) {      //重复发3遍,总共耗时大概35ms
    //    high(2320,_freq,_pin);
    high(2400, _freq, _pin);    //开始信号,引导码,2.4ms
    //    wait(650);
    wait(600);
    for (int i = 0; i < sizeof(_seq); i++) {
      if (_seq == 0) {
        //        high(575,_freq,_pin);
        high(600, _freq, _pin);
        //        wait(650);
        wait(600);
      }
      else {
        //high(1175, _freq, _pin);
    high(1200, _freq, _pin);
        //        wait(650);
        wait(600);
      }
    }
    wait(10000);
  }
}
//End of multiCameraIrControl library

#define SAMPLES 133  

Sony nex5r(10);
bool on = 0;
unsigned long adc;
void setup() {
  Serial.begin(115200);
  bitSet(DDRB, 5);     //LED D13
  ADMUX = 1 << REFS0; //AREF=VCC(5V)。其余位都置0,等于是选择ADC0(A0)
  ADCSRA = 1 << ADEN | 1 << ADSC | 1 << ADATE | 1 << ADIE | 1 << ADPS2 | 1 << ADPS1 | 1 << ADPS0;
  // ADEN: ADC Enable启用ADC
  // ADSC: ADC Start Conversion开始转换
  // ADATE: ADC Auto Trigger Enable启用自动触发(和下面的自由运行模式一起在ADC完成后自动开始一次新的转换)
  // ADIE: ADC Interrupt Enable启用ADC中断
  // ADPS[2:0]: ADC Prescaler Select Bits 128分频
  ADCSRB = 0;  //自由运行模式
  bitSet(DIDR0, ADC0D); //禁止ADC0上的数字输入
}

void loop() {
  if (on) {
//    noInterrupts();//为了防止shutterNow()函数执行过程中被中断打断,在这里关闭全局中断,等shutterNow()函数结束后重新开启,但是程序就出问题了
    bitSet(PORTB, 5);
    Serial.println(adc);
    nex5r.shutterNow();
    bitSet(PINB, 5);
    on = 0;
//    interrupts();//重新开启中断
    delay(200);//临时加的
  }
}

ISR(ADC_vect) {
  static unsigned int i = 0;
  static unsigned long adcAll = 0;
  adcAll += ADC;
  i++;
  ADMUX = 1 << REFS0; //重新选择ADC0(A0),AREF=VCC(5V)

  if (i >= SAMPLES) {
    adc = (adcAll / SAMPLES);
    i = 0;
    adcAll = 0;
    //1024 x 0.6v / 5v = 123
    if (adc > 120) {
      on = 1;
    } else {
      on = 0;
    }
  }
}[/mw_shl_code]

该用户从未签到

发表于 2016-6-6 12:58 | 显示全部楼层
在取消注释81行和87行后,程序似乎一直处于触发状态。

该用户从未签到

发表于 2017-4-15 23:04 | 显示全部楼层
如此好贴不顶真是没天理!
也证明了ARDUINO玩家触及单片机底层知识的真的好少。

该用户从未签到

发表于 2017-6-2 14:30 | 显示全部楼层
好贴。读后感知底层内容之深奥。然,哪里能找到此类教材呢,困惑。

该用户从未签到

发表于 2017-6-19 19:20 | 显示全部楼层
高级帖。希望能解决速度太慢串口漏包的问题。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

热门推荐

esp32s2 在Arduino IDE调试USB CDC
esp32s2 在Arduino IDE调
各位大佬好,最近尝试弄清如何使用esp32s2的usb功能。然后在Github上找的代码,放到自
一直想做个飞行器,但是出了电小问题
一直想做个飞行器,但是出
这是主函数的源码 extern uint8_t PWM_PIN[8]; void initOutput();//初始化函数 void
Arduino mega2560 usb串口01被modbus master库占用,无法下载程序
Arduino mega2560 usb串口
Arduino mega2560 用来测试modbus master通信,因未发现串口可选择,直接下载了串口(
DIY炫彩灯带,竟如此简单,更有硬件开发工具免费领!
DIY炫彩灯带,竟如此简单
什么是涂鸦Arduino SDK? Arduino 是全球最流行的开源硬件平台,涂鸦官方推出的 Ardui
.h .cpp源代码转.ino草图
.h .cpp源代码转.ino草图
在Github上下载了源码,是.h 和.cpp文件,请问一下,如何才可以转成可以在arduinoIDE
Copyright   ©2015-2016  Arduino中文社区  Powered by©Discuz!   
快速回复 返回顶部 返回列表