Đo Squarewave 0 - 1 MHz (độ phân giải 0,25Hz) bằng MCU


8

Tôi cần đo tần số của sóng vuông có thể thay đổi trong khoảng từ 0 đến 1 MHz và có độ phân giải 0,25Hz.

Tôi chưa quyết định chọn bộ điều khiển nào nhưng rất có thể nó sẽ là một trong 20pin Attiny.

Thông thường cách tôi đo các tín hiệu tần số thấp hơn bằng cách sử dụng hai bộ định thời một được định cấu hình ở chế độ chụp hẹn giờ để ngắt các cạnh tăng của tín hiệu bên ngoài và bộ hẹn giờ khác được thiết lập để ngắt mỗi giây do đó giá trị của bộ đếm thời gian cũ sau 1 giây sẽ bằng tần số của tín hiệu.

Tuy nhiên, phương pháp này rõ ràng không hoạt động để thu các tín hiệu trong khoảng từ 0 đến 1 MHz với độ phân giải 0,25Hz cho điều này, tôi sẽ cần một bộ đếm 22Bit (micrô AFAIK 8 bit chỉ có bộ đếm 8/16 bit).

Một ý tưởng tôi đã có là phân chia tín hiệu trước khi áp dụng nó vào vi mô nhưng điều này sẽ không thực tế vì tín hiệu sẽ phải chia cho 61 do đó tần số chỉ có thể được cập nhật sau mỗi 61 giây mà tôi muốn nó cứ sau vài giây .

Có phương pháp nào khác cho phép tần số được cập nhật cứ sau 4 giây không?


Cập nhật:

Giải pháp đơn giản nhất là sử dụng ngắt ngoài hoặc chụp hẹn giờ để ngắt trên cạnh tăng của tín hiệu và có isrmột biến số loại tăng long int. Đọc biến cứ sau 4 giây (để cho phép tần số giảm xuống 0,25Hz để đo).


Cập nhật 2:

Như JustJeff đã chỉ ra, MCU 8 bit sẽ không thể theo kịp tín hiệu 1 MHz để các quy tắc không bị gián đoạn trên mọi cạnh tăng và tăng long int...

Tôi đã chọn phương pháp được đề xuất bởi timororr. Khi tôi đi xung quanh để thực hiện nó, tôi sẽ đăng lại và chia sẻ kết quả. Cảm ơn tất cả những gợi ý của bạn.


Báo cáo tiến trình:

Iv'e bắt đầu thử nghiệm một số ý tưởng được trình bày ở đây. Đầu tiên, tôi đã thử mã của Abbeyatcu. Có một vấn đề rõ ràng về TCNT1 không được xóa sau khi tần số được tính toán - không phải là vấn đề lớn ...

Sau đó, tôi nhận thấy khi gỡ lỗi mã cứ khoảng 2 đến 7 lần tần số được tính toán của bộ định thời 1 giây (bộ định thời được cấu hình để đếm các sự kiện bên ngoài) thì số lần tràn sẽ bị rút ngắn hai lần. Tôi đặt điều này xuống độ trễ của Timer 0 ISR và quyết định di chuyển khối lệnh if tạo thành ISR thành chính (xem đoạn trích bên dưới) và chỉ đặt cờ trong ISR. Một số gỡ lỗi cho thấy rằng phép đo đầu tiên sẽ ổn nhưng với mỗi lần đọc tiếp theo, số lần tràn của Timer 1 sẽ kết thúc bằng 2. điều mà tôi không thể giải thích - Tôi đã dự đoán nó sẽ không kết thúc ...

int main()
{
    while(1)
    {
        if(global_task_timer_ms > 0 && (T0_overflow == 1))
        {
            global_task_timer_ms--;
            T0_overflow = 0;
        }

        .....
    }
}

Tiếp theo tôi quyết định tôi sẽ cố gắng thực hiện đề xuất timrorrs. Để tạo khoảng thời gian cần thiết (khoảng 15ms giữa mỗi ngắt timer_isr), tôi sẽ phải xếp tầng hai bộ định thời 8 bit vì bộ định thời 16 bit duy nhất trên Atmega16 đang được sử dụng để thu các cạnh tăng của tín hiệu bên ngoài.

Tôi nghĩ giải pháp này sẽ hoạt động hiệu quả và hiệu quả hơn nhiều vì phần lớn chi phí được chuyển sang bộ hẹn giờ và chỉ còn lại một isr ngắn để cpu xử lý. Tuy nhiên, nó không chính xác như tôi mong đợi, các phép đo dịch chuyển qua lại khoảng 70Hz mà tôi không bận tâm ở tần số cao nhưng chắc chắn không thể chấp nhận được ở tần số thấp hơn. Tôi đã không dành hai thời gian để phân tích vấn đề nhưng tôi đoán cách sắp xếp xếp tầng theo thời gian không chính xác vì tôi đã thực hiện một cách sắp xếp tương tự với đề xuất timrorrs trên bộ điều khiển 8051 chậm hơn có 2 bộ định thời 16 bit và kết quả khá chính xác.

Bây giờ tôi đã quay trở lại đề xuất của Abbeyatcu, nhưng tôi đã chuyển tính toán tần số sang isr Timer 0 (xem đoạn trích bên dưới ), mã này đã tạo ra các phép đo phù hợp và chính xác hợp lý. Với độ chính xác một chút, độ chính xác phải xấp xỉ +/- 10Hz.

ISR(TIMER0_OVF_vect)
{            

    TCNT0 = TIMER0_PRELOAD;         //Reload timer for 1KHz overflow rate

    if(task_timer_ms > 0)
    {
        task_timer_ms--;
    }
    else
    {     
        frequency_hz = 1.0 * TCNT1;
        TCNT1 = 0;
        frequency_hz += global_num_overflows * 65536.0;
        global_num_overflows  = 0;
        frequency_hz /= (TASK_PERIOD_MS / 1000.0);
        task_timer_ms = TASK_PERIOD_MS;
    }                                                 
}       

Nếu có ai có bất kỳ đề xuất nào khác Tôi mở cho họ mặc dù tôi không phải sử dụng phạm vi ... Tôi cũng không còn có ý định nhận độ phân giải 0,25%, có vẻ như không có nhiều điểm với mức độ chính xác mà tôi có vào lúc này .


Có một cách tương đối dễ dàng để làm điều này bằng cách sử dụng ngắt chụp trên PIC và Timer 1 chạy ở tốc độ rất cao. Nếu bạn vẫn quan tâm đến các phương pháp khác hãy cho tôi biết và tôi có thể phác thảo nó trong một câu trả lời.
Kortuk

Tôi chưa bắt đầu công việc này nên tôi vẫn quan tâm.
bắt đầu

Vì một số lý do, nó đã không bao giờ cho tôi biết bạn đã nhận xét về nhận xét của tôi.
Kortuk

@Kortuk: Phần mềm chỉ thông báo cho bạn nếu tôi để lại nhận xét cho một trong những câu trả lời hoặc câu hỏi của bạn. Nó cũng có thể thông báo cho bạn về nhận xét này, vì tôi đặt @Kortuk trước nó. Nhưng đó là thay đổi phần mềm StackOverflow và tôi không biết liệu nó có bị lừa trong cơ sở mã StackExchange hay không.
Robert Harvey

không, nó không cho tôi biết bạn đã trả lời, ngay cả với @kortuk. Đừng lo lắng. Có vẻ như một câu trả lời đã được tìm thấy.
Kortuk

Câu trả lời:


4

Nếu có thể tôi khuyên bạn nên chọn một vi điều khiển hỗ trợ hoạt động của bộ đếm bằng các đầu vào hẹn giờ; thay vì tăng thủ công bộ đếm bên trong ISR (ở tần số cao nhanh chóng kết thúc bão hòa hoạt động của vi điều khiển), bạn cho phép phần cứng xử lý việc đếm. Tại thời điểm này, mã của bạn đơn giản trở thành vấn đề chờ đợi ngắt định kỳ của bạn sau đó tính toán tần số.

Để mở rộng phạm vi và làm cho bộ đếm tần số trở nên khái quát hơn (loại bỏ nhu cầu về nhiều phạm vi với chi phí làm thêm một chút cho MCU), bạn có thể sử dụng kỹ thuật sau.

Chọn tốc độ ngắt định kỳ cho phép đo độ chính xác ở tần số đầu vào cao nhất; điều này sẽ tính đến kích thước bộ đếm của bạn (bạn cần chọn khoảng thời gian hẹn giờ sao cho bộ đếm thời gian sẽ không tràn ở tần số đầu vào tối đa). Trong ví dụ này, tôi sẽ giả sử rằng giá trị bộ đếm đầu vào có thể được đọc từ biến "timer_input_ctr".

Bao gồm một biến để đếm các ngắt định kỳ (nên được khởi tạo thành 0 khi khởi động); trong ví dụ này tôi sẽ gọi biến này là "isr_count". Khoảng thời gian ngắt được chứa trong "isr_ period" không đổi.

Ngắt định kỳ của bạn nên được thực hiện dưới dạng (mã giả C):

void timer_isr()
{
  isr_count++;
  if (timer_input_ctr > 0)
  {
    frequency = timer_input_ctr / (isr_count * isr_period).
    timer_input_ctr = 0;
    isr_count = 0;
  }
}

Rõ ràng ví dụ sơ bộ này dựa trên một số phép toán dấu phẩy động có thể không tương thích với các bộ vi điều khiển cấp thấp, có những kỹ thuật để khắc phục điều này nhưng chúng nằm ngoài phạm vi của câu trả lời này.


1
Thời gian tuyệt vời, điều đó sẽ làm tôi chính xác những gì tôi muốn mà không phải trả thêm IC, điều này luôn tốt, tôi nghĩ rằng tôi đã quá nhanh để loại bỏ khả năng giải quyết vấn đề trong phần mềm. Cảm ơn
bắt đầu

@timrorr, tôi quan tâm đến suy nghĩ của bạn về câu trả lời của tôi dưới đây nếu bạn cảm thấy thích đọc nó
Abbeyatcu

7

Bạn có thể muốn xem xét có hai (hoặc nhiều) phạm vi. Các vấn đề với việc bắt tần số rất thấp có phần khác với các vấn đề với các tần số cao hơn. Như bạn đã lưu ý, ở mức cao trong phạm vi của bạn, bạn có các vấn đề tràn bộ đếm.

Nhưng hãy xem xét ở mức thấp trong phạm vi của bạn, độ chính xác của bạn sẽ bị ảnh hưởng do không có đủ số lượng trong sổ đăng ký. Không chắc chắn nếu bạn thực sự muốn phân biệt giữa 0,25Hz và 0,5Hz, nhưng nếu bạn làm vậy, thì bạn thực sự sẽ phải tính trong bốn giây để làm điều đó.

Ngoài ra, chỉ định độ phân giải 0,25Hz phẳng, được giải thích nghiêm ngặt, có nghĩa là bạn có thể nhận ra 500.000.00Hz từ 500.000.25Hz, độ chính xác khá cao.

Vì những lý do đó, thiết kế cho các phạm vi riêng biệt có thể làm giảm bớt vấn đề kích thước bộ đếm. Lấy số ngẫu nhiên vì lợi ích của ví dụ, ở mức thấp, giả sử 0 đến 100Hz, đếm trong khoảng thời gian 10 giây và bạn có độ phân giải 0,1Hz và bạn chỉ cần truy cập tối đa 1000, thậm chí không phải 10 bit. Sau đó từ 100Hz đến 10kHz, đếm trong khoảng thời gian 1 giây; bạn chỉ nhận được độ phân giải 1Hz, nhưng bộ đếm của bạn chỉ cần chạy tối đa 10.000 vẫn nhỏ hơn 16 bit. Dải tần trên 10kHz đến 1 MHz có thể chỉ trong 0,01 giây và số lượng tối đa vẫn chỉ là 10.000 và mặc dù độ phân giải của bạn sẽ là 100Hz, đây sẽ là độ chính xác hợp lý.


Có, tôi đã đề cập rằng trong bản cập nhật cho câu hỏi của tôi (trước đó) tôi sẽ phải đếm đến 4 giây trong ... và tôi vâng tôi muốn có thể phân biệt giữa 500.000.00Hz và 500.000.25Hz. Tôi đã nghĩ đến việc sử dụng các phạm vi khác nhau, tôi có thể dễ dàng kết nối nó với phần cứng còn lại vì tín hiệu có 6 phạm vi có thể lựa chọn để tôi có thể thiết kế một bộ mã hóa 6 đến 3 đơn giản để chỉ ra phạm vi nào ... nhưng tôi không chắc chắn nếu cần thiết nếu tôi sử dụng bộ đếm phần cứng kết hợp với thời gian cập nhật 4 giây, điều này sẽ giải quyết các vấn đề ở hai đầu của quang phổ
bắt đầu từ

5

Bạn có thể trộn lẫn phần cứng và bộ đếm phần mềm bằng cách đếm số lần tràn của bộ đếm phần cứng trong ISR.

Đếm mọi cạnh của tín hiệu trong ISR sẽ quá chậm đối với tín hiệu 1 MHz. Tôi nghĩ rằng bạn có thể làm đến khoảng 50kHz theo cách đó.


Có thể bạn có thể đúng - nó sẽ quá chậm trong 1 MHz nhưng Id hãy tưởng tượng bộ xử lý RISC 20MIPS có thể làm tốt hơn 50KHz. Dù sao, tôi cũng đang xem xét xung nhịp bộ đếm nhị phân 8 bit với tín hiệu và kết nối bộ mang ra khỏi bộ đếm với chân ngắt ngoài của MCU, sau đó đọc tần số của tín hiệu khi tổng của bit mang bị ngắt cộng với số o / p giá trị của bộ đếm cứ sau n giây, tôi đoán đó là những gì bạn đang nhận được khi bạn nói một sự kết hợp giữa bộ đếm phần cứng và phần mềm.
bắt đầu

Tôi nghĩ rằng OP đã đề cập đến bộ đếm phần cứng tích hợp. Chúng đều có các ngắt tràn có thể được sử dụng để cải thiện phạm vi đếm.
jpc

@starblue, mã tôi đã viết bên dưới những gì bạn có trong đầu với câu trả lời của bạn?
Abbeyatcu

2

Thay vì làm bộ đếm 1 giây, hãy làm bộ đếm 0,1 giây và nhân số đếm với 10?

Nếu đó chỉ là vấn đề lưu trữ số bộ đếm, bạn có thể sử dụng mã bổ sung để theo dõi khi nào bộ đếm sắp tràn và ghi vào vị trí bộ nhớ khác để giữ kiểm đếm không?


2
Tôi nghĩ rằng tôi phải bị đóng băng não .. giải pháp đơn giản nhất tôi nghĩ là chỉ cần tăng một biến kiểu int dài mỗi khi phát hiện ra một cạnh tăng. Đọc giá trị đó mỗi giây một lần và sau đó đặt lại về không.
bắt đầu

2
Trên thực tế, tôi sẽ cần đọc giá trị cứ sau 4 giây để đo xuống 0,25Hz
bắt đầu từ

2

Bạn có thể chỉ sử dụng chức năng chụp và ngắt tràn đầu vào của bộ đếm thời gian 16 bit (cộng với một biến) để thực hiện phép đo không? Đây là cách tôi sẽ làm điều đó với ATTiny24A với AVR-GCC (tất nhiên chưa được kiểm tra và có khả năng có lỗi):

#include <stdint.h>
#include <avr/io.h>
#include <avr/interrupt.h>

#define TIMER1_BITS           16    // 16 bit timer
#define TIMER1_HZ             8.0e6 // 8MHz crystal
#define TIMER1_OVF_PERIOD_SEC (1.0 * (1 << TIMER1_BITS) / TIMER1_HZ)
#define TIMER1_SEC_PER_TICK   (1.0 / TIMER1_HZ)

//global variables for time keeping
double total_period_sec = 0.0;
uint16_t  num_overflows = 0;

void setup_timer1_capture(){
   // set the ICP (input caputure pin) to a floating input
   DDRA  &= ~_BV(7); // it's A7 on the ATTiny24A...
   PORTA &= ~_BV(7);

   TIMSK1 =   _BV(ICIE1)  // enable input pin capture interrupt
            | _BV(TOIE1); // enable overflow interrupt

   TCCR1B =   _BV(ICNC1)  // activate the input noise canceller
            | _BV(ICES1)  // capture on rising edge of ICP
            | _BV(CS10);  // run the timer at full speed

}

ISR(TIM1_CAPT_vect, ISR_NOBLOCK){ //pin capture interrupt
  uint16_t capture_value_ticks = ICR1; // grab the captured value
  // do some floating point math
  total_period_sec =   1.0 * num_overflows * TIMER1_OVF_PERIOD_SEC
                     + 1.0 * capture_value_ticks / TIMER1_SEC_PER_TICK; 

  num_overflows = 0; // clear helper variable to be ready for next time
}

ISR(TIM1_OVF_vect){   //timer overflow interrupt
    num_overflows++;
}

int main(int argc, char *argv[]){
   setup_timer1_capture();

   sei(); // enable interrupts!

   for(;;){ //forever
      // do whatever you want...
      // the most recently calculated period is available in the 
      // total_period_sec variable 
      // (obviously 1.0 / total_period_sec is the frequency in Hz)
   }

   return 0;
} 

... ở bất cứ giá nào, nó biên dịch :)


EDIT Tôi đã xem xét đầu ra tệp lss từ mã của mình và mã được tạo có quá nhiều hướng dẫn để không vượt qua chính nó ở mức 1 MHz với xung nhịp 8 MHz ... ngay cả việc tăng đơn giản một dòng trong TIM1_OVF_vect cũng tạo ra 19 hướng dẫn! Vì vậy, để xử lý các sự kiện 1 MHz, bạn chắc chắn sẽ cần tối ưu hóa, có thể đăng ký phân bổ một số nội dung (có thể là num_overflows và capt_value_ticks), sử dụng trình biên dịch nội tuyến (đánh cắp các nội dung quan trọng từ tệp lss) và di chuyển xử lý ra khỏi các ngắt và vào chính vòng lặp bất cứ nơi nào có thể.


Đo tần số sử dụng chu kỳ hoạt động khá tốt với các dạng sóng chậm (bạn đang dựa vào bộ định thời bên trong nhanh hơn nhiều so với tín hiệu bên ngoài) nhưng nhanh chóng đạt đến giới hạn khi tần số tín hiệu đầu vào tăng. Về cơ bản, như bạn đã tìm thấy, thời gian bên trong ngắt chụp thời gian bắt đầu chiếm ưu thế; không còn thời gian cho bất kỳ phần nào khác của mã để chạy. Mặc dù tôi không quen thuộc với ATTiny, nhưng nhìn nhanh vào biểu dữ liệu cho thấy bộ đếm thời gian / bộ đếm 1 không hỗ trợ đếm sự kiện bên ngoài, vì vậy hãy để phần cứng xử lý việc đếm.
timrorr

@timrorr, wow vâng, đó là cách làm thông minh hơn :) Tôi đã đăng mã AVR-GCC được cập nhật trong một bài riêng. Quan tâm để xem và xem những gì bạn nghĩ?
Abbeyatcu

2

Đăng mã này dưới dạng thay thế cho mỗi đề xuất của @ timrorr cho bài viết trước của tôi. Điều này biên dịch cho ATTiny24A bằng cách sử dụng tiêu chuẩn ngôn ngữ c99, nhưng tôi thực sự chưa thử nghiệm nó theo bất kỳ cách nào ngoài điều đó.

#include <stdint.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/atomic.h>
#define TIMER0_PRELOAD   0x83 // for 8MHz crystal and overflow @ 1kHz
#define TIMER0_PRESCALE 0x03  // divide by 64
#define TASK_PERIOD_MS 4000   // execute task every 4 seconds

//global variables for time keeping
volatile uint16_t  global_num_overflows = 0;
volatile uint16_t  global_task_timer_ms = TASK_PERIOD_MS;

void setup_timers(){
    // set the T1 pin (PA.4) to a floating input (external event)
    DDRA  &= ~_BV(4);
    PORTA &= ~_BV(4);

    // set Timer1 to count external events
    TIMSK1 = _BV(TOIE1);      // enable overflow interrupt
    TCCR1B =   _BV(CS10)      // clock on external positive edge of T1 pin
        | _BV(CS11)
        | _BV(CS12);

    // set Timer0 for task timing (overflow once per ms)
    TCCR0B = TIMER0_PRESCALE;
    TCNT0  = TIMER0_PRELOAD;  // setup appropriate timeout
    TIMSK0 = _BV(TOIE0);      // enable timer0 overflow interrupt
}


ISR(TIM1_OVF_vect){   //timer1 overflow interrupt
    global_num_overflows++;
}

ISR(TIM0_OVF_vect){            //timer0 overflow interrupt @ 1kHz
    TCNT0 = TIMER0_PRELOAD;   // preload timer for 1kHz overflow rate
    if(global_task_timer_ms > 0){
        global_task_timer_ms--;
    }
}

int main(int argc, char *argv[]){
    double frequency_hz = 0;
    uint16_t num_overflows = 0;
    uint16_t num_positive_edges  = 0;
    setup_timers();
    sei(); // enable interrupts!
    for(;;){ //forever
        if(global_task_timer_ms == 0){ // wait for task to be scheduled
            ATOMIC_BLOCK(ATOMIC_FORCEON){
                num_overflows        = global_num_overflows; // copy the volatile variable into a local variable
                global_num_overflows = 0;                    // clear it for next time
                num_positive_edges   = TCNT1;                // copy num positive edge events to local variable
            }

            // calculate the 'average' frequency during this task period
            frequency_hz  = 1.0 * num_positive_edges;  // num edges since last overflow
            frequency_hz += num_overflows * 65536.0;   // edges per overflow of 16 bit timer
            frequency_hz /= (TASK_PERIOD_MS / 1000.0); // over the task interval in seconds

            global_task_timer_ms = TASK_PERIOD_MS;     // reschedule task
        }

        // use frequency_hz for whatever other processing you want to do
    }
    return 0;
}

Đây là một cách sử dụng tốt các khả năng phần cứng của Timer1 và giải phóng hàng tấn chu trình xử lý so với bài viết gốc của tôi.


2
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.