【冷知识】如何加快analogRead速度提高采样率Sampling Rate?

文章目录


https://www.arduino.cn/thread-12569-1-1.html
这部分内容原先是回答某位网友的, 重新整理方便大家查看!

根据官网说明, 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/arduino/inside/arduino/wiring_analog.c/analogRead.html

不过 9600Hz 的sampling rate 只是理论值, 因为你的程序本身也要花时间, 调用函数进出需要时间, for Loop 本身也要时间, 把 sample(采样) 到的值加总到 total 也要时间 !

所以, 如果你用一个 for Loop 全力一直做 analogRead(A0),
那一秒钟也只能读取大约 8925 次, 达不到理论值 9600次 !!
不相信, 用你的 Arduino 测试一下:

//
// 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(

你看, 以上程序已经简短到无法再简短了,
但是经过实际测试, 结果 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 设为各种值的函数:

//
// 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

请注意, 虽然可以把 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:
kittenblock中小学创客名师推荐的图形化编程软件

// 以下范例是只读取 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;
}

其他参考文件:
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/technical/2014/03/01/advanced-arduino-adc/
http://meettechniek.info/embedded/arduino-analog.html
http://www.instructables.com/id/Arduino-Audio-Input/step6/Sampling-rate-of-40kHz/
https://bennthomsen.wordpress.com/arduino/peripherals/continuous-adc-capture/
再补充一下, 透过 ADC 转换器,
我们还可以读取 Arduino 内部的温度传感器,
然后把温度传送到 PC 计算机上!
不过, 因为每个 Arduino 板子实际上会略有差异,
所以以下的程序你必须实际测量后调整里面 tempBias 的值:

//从 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

  • 5
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当然可以!下面是一个进一步优化的示波器代码,可以在界面中显示幅值、采样率等信息: ```C++ #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #define OLED_RESET 4 Adafruit_SSD1306 display(OLED_RESET); #define NUM_POINTS 128 // 示波器显示的数据点数 #define ADC_PIN A0 // 连接模拟输入的引脚 #define VREF 5.0 // 参考电压 #define RESOLUTION 1023 // 分辨率 void setup() { display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // 初始化OLED显示屏 display.clearDisplay(); display.setTextColor(WHITE); } void loop() { int data[NUM_POINTS]; // 存储示波器数据的数组 // 采集数据 for (int i = 0; i < NUM_POINTS; i++) { data[i] = analogRead(ADC_PIN); delayMicroseconds(100); } // 清空显示屏 display.clearDisplay(); // 绘制示波器图形 for (int i = 0; i < NUM_POINTS - 1; i++) { int x1 = map(i, 0, NUM_POINTS - 1, 0, display.width()); int y1 = map(data[i], 0, RESOLUTION, display.height(), 0); int x2 = map(i + 1, 0, NUM_POINTS - 1, 0, display.width()); int y2 = map(data[i + 1], 0, RESOLUTION, display.height(), 0); display.drawLine(x1, y1, x2, y2, WHITE); } // 显示幅值和采样率信息 display.setTextSize(1); display.setCursor(0, display.height() - 8); display.print("Amplitude: "); display.print((float)data[NUM_POINTS/2] * VREF / RESOLUTION, 2); display.print(" V"); display.setCursor(0, display.height() - 16); display.print("Sampling Rate: "); display.print(1000000.0 / (NUM_POINTS * 100), 1); display.print(" kHz"); // 更新显示 display.display(); } ``` 在上述代码中,我们添加了两行用于显示幅值和采样率信息的代码。幅值的计算通过将模拟输入值映射到参考电压范围内进行实现。采样率的计算通过使用示波器数据点数和延时时间来估算。 请注意,上述代码还需要使用 `Adafruit_GFX` 和 `Adafruit_SSD1306` 库。确保你已经安装了这些库。 这个优化后的示波器代码将在OLED屏幕上显示示波器图形、幅值和采样率信息。你可以根据需要进一步自定义和优化界面。希望这对你有帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值