מבוא

בפרויקט זה נממש ב-FPGA דרייבר למנוע צעד (stepper motor) מסוג Unipolar, הכולל שליטה על מהירויות סיבוב קבועות, על כיוון הסיבוב, על מצב צעד/חצי צעד וכן יכלול מצב תאוצה.

הבקר יעבוד לפי הממשק הבא:

FPGA interfaceDescriptionDirectionName
Internal 50 MHz clockSystem clockinclk
KEY0System reset – active lowinresetb
SW1clockwise – 1
counterclockwise – 0
indirection
KEY3Motor speed selector. Each key press should change the motor’s speed by 10 RPM, in the range of 10-60. When a limit is met, speed should change other way around.inspeed_sel
SW21 – continues motion
0 – stop
inon
SW91 – acceleration mode
0 – regular mode
inmode_sel
KEY2Initializes the acceleration/deacceleration sequenceinaccelerate
SW31 – full step
0 – half step
instep_size
HEX0Seven segment display (will display the ones)outsev_seg_o
HEX1Seven segment display (will display the tens)outsev_seg_t
HEX2Seven segment display (will display the hundreds)outsev_seg_h
GPIO pinsOutput pulsesoutpulses_out

רקע תיאורטי

מנוע צעד (stepper motor) הוא סוג של מנוע חשמלי שנועד להסתובב במרווחים קבועים ומדויקים, הנקראים צעדים. המנוע פועל על עקרונות האלקטרומגנטיות ומאפשר שליטה מדויקת במיקום ובתנועה.

מבנה המנוע

מנוע צעד בנוי ממספר סלילים אלקטרומגנטיים המסודרים סביב רוטור מגנטי. כאשר זרם חשמלי מוזן דרך הסלילים בסדר מסוים, נוצר שדה מגנטי המזיז את הרוטור. גיאומטריית המנוע משתנה בין שני סודי מנועים – unipolar או bipolar. במעבדה זו נשתמש במנוע unipolar ולכן מכאן והלאה נתייחס אל סוג זה בלבד.

התמונה לעיל מתארת את גיאומטריית המנוע – זוגות של סלילים נגדיים (באותו צבע) נשלטים על ידי אותם כבלים (מקוצרים). עבור כל זוג סלילים (phase) נוכל לשלוט בקיטוביות על ידי שליטה בהפרש המתחים בין אותם כבלים. בגלל שכיוונם של סלילים נגדיים הפוך, ולפי כלל יד ימין, קיטוב שיגרום למשיכה בסליל יגרום לדחייה בסליל הנגדי לו. יש להתחשב בכך כאשר נתפעל את הסלילים.

צעד מלא

אחת הדרכים לגרום לסיבוב המנוע היא קפיצה בצעדים  שלמים. בברירת המחדל המתח בכל הכניסות יהיה 0[V]. ראשית נמתג את A1 להיות 1 לוגי. קיטוב זה בסליל יגרום לרוטור להימשך אל סליל A+. לאחר מכן נוריד אותו חזרה ל-0 לוגי ובמקומו נעלה את B1. כעת הרוטור יימשך אל B+.  נוריד גם אותו ובמקומו נעלה את A2. לבסוף נוריד אותו ונעלה את B2. נחזור חלילה על מחזור זה ובכך המנוע יסתובב בקפיצות של צעד.

מיתוג הסלילים בסדר שתואר לעיל יגרום לסיבוב נגד כיוון השעון. הפיכת סדר המיתוגים תגרום לסיבוב עם כיוון השעון. וכמובן ששינוי משך הזמן של שלב בודד יגרום לשינוי במהירות המנוע.

חצי צעד

באותו האופן נוכל לשלוט על המנוע ברזולוציה כפולה, אם בין כל שני "שלבים" במחזור של תנועת הצעד השלם, נוסיף צעד ביניים שבו שתי הכניסות דולקות סימולטנית. בכך הרוטור יימשך אל הנקודה שנמצאת בין שני הסלילים – כלומר נשיג התקדמות של חצי צעד.

כיוון שהכפלנו את כמות השלבים, אם משך הזמן של כל שלב יישאר כפי שהיה עבור הצעד השלם, מהירות המנוע תקטן פי 2. יש להתחשב בכך בהמשך כשנרצה לשלוט במהירות המנוע.

מימוש הבקר

את הבקר נממש באמצעות FPGA מדגם DE10 של Altera. נצרוב על הבקר את החומרה שנתכנן, נשתמש במתגים ובכפתורים המובנים כקלט עבור המנוע, בתצוגת ה-7 Segment  על מנת להציג את מהירות המנוע ביחידות RPM, ובכן ב-GPIO headers על מנת לשלוט בסלילי המנוע.

מתחי האספקה של ה-FPGA וכן הזרמים המקסימליים בהם הוא מסוגל לעמוד קטנים מדי עבור תפעול סלילי המנוע. לכן נשתמש ברכיב מתווך, שיקבל מתח חיצוני מספק כוח נוסף, ויספק מתח עבור חיווטי הסלילים לפי המתחים שהוא יקבל מה-FPGA. כמו כן, בגלל שנעבוד עם שנאי נפרד עבור המנוע ועבור ה-FPGA, חובה לתאם אדמות בין שני הספקים.

חיווט החומרה

מהמנוע יוצאים ארבעה כבלים: A1 מתחבר אל הדרייבר ליציאת out1, A2 אל יציאת out2, B1 מתחבר אל out3 וכן B2 אל out4.

עבור כניסות הדרייבר: in1 מתחבר אל GPIO1, in2 אל GPIO2, in3 אל GPIO3 וכן in4 אל GPIO4. בנוסף Vdd ו Vss של הדרייבר מתחברים אל ספק כוח של 12V. נוסף על כך, כיוון שה-FPGA מוזן על ידי ספק כוח נפרד, נתאם את אדמות הספקים על ידי חיבור Vss של הדרייבר אל ה-GPIO של GND ב-FPGA.

סכימת עבודה

הגישה שלנו למימוש הבקר היא עבודה באמצעות מכונת מצבים שתנהל את האותות שמפעילים את המנוע. פלט מכונת המצבים יהיה bus ברוחב 4 ביט (coils) שיתחבר מה-GPIO אל כניסות הדרייבר. מכונה זו תמומש כמופע של המודול motorStateMachine. המודול מקבל קונטרולים ברוחב ביט יחיד השולטים על כיוון הסיבוב (dir), האם לפעול בצעד מלא או חצי צעד (is_full_step), קונטרול enable (en) וקונטרול ריסט (resetb). בנוסף המודול מקבל שעון (half_clk) ממנו ישירות נגזרת מהירות הסיבוב.

אותו שעון מגיע מתוך המופע new_clock של המודול frequencyDivider, אשר מקבל בקלט שלו שעון (input_clk), ריסט (resetb) ומספר חיובי ברוחב 25 ביט (half_division_factor). המודול מחלק את תדר השעון שבקלט פעמיים במספר שבקלט, ומוציא אותו כשעון חדש. השעון המקורי שאנו מספקים הוא ה-50MHz הפנימי של ה-FPGA.

למעשה הקלט ב-half_division_factor קובע את מהירות המנוע, ומגיע מתוך מימוש של המודול rpmConverter, אשר מקבל מהירות rpm מ-bus ברוחב 8 ומוציא את half_division_factor המתאים כדי שהשעון 50MHz הפנימי של ה-FPGA יחולק לכדי התדר שמתאים ל-RPM הרצוי. בנוסף, rpmConverter מוציא את ספרות האחדות, העשרות והמאות של הקלט rpm על מנת שיוצגו בצג ה-FPGA באמצעות מופעים של המודול binaryTo7Segment.

קלט ה-rpm עבור rpmConverter מסופק על ידי אחד משני מודולים, הנבחרים באמצעות mux. המודול הראשון הוא velocityConverter אשר מתפעל את מצב המהירות הקבועה. המודול השני הוא accelerator אשר מתפעל את מצב ההאצה. שני מודולים אלו מקבלים את קלטי הכפתורים והמתגים הרלוונטיים אליהם לפי הממשק. ה-mux אשר בורר מי מהם יעביר את rpm הלאה, נשלט על ידי הערוץ is_acc_mode המחובר למתג בהתאם לממשק הרצוי. כל קלטי המתגים עוברים דרך מופעים של המודולdebouncer  אשר מנקה את הרעשים מהסיגנלים שלהם בזמן המיתוג.

פירוט המודולים

motorStateMachine

מכונת המצבים של המנוע מקבלת שעון ממנו נגזרת מהירות המנוע, וכן קונטרולים dir, is_full_step, en, resetb, ומוציאה את הפלט coils ברוחב 4 ביטים כאשר LSB הוא in1 ו MSB הוא in4.

בתוך המודול ממומש מופע נוסף של frequencyDivider המחלק את התדר פי 2. מטרת חלוקה זו היא להתאים בין מהירות המנוע במצב צעד מלא לבין המהירות במצב חצי צעד, שכן קיים הבדל במהירויות כפי שכבר ראינו.

ננתב את השעון המתאים (המקורי או המחולק) לפי מצב הקונטרול is_full_step.

מבנה מכונת המצבים הוא סטנדרטי, כפי שלמדנו בכיתה. עבור כל מצב, המצב הבא נקבע גם לפי הקונטרולים dir ו is_full_step, שיקבעו אם ניכנס לתתי המצבים של חצאי הצעד או שנדלג עליהם, וכן את כיוון המכונה שישפיע על כיוון הסיבוב.

module motorStateMachine (input wire half_clk,
                          input wire dir,
                          input wire is_full_step,
                          input wire en,
                          input wire resetb,
                          output reg [3:0] coils);
    
    parameter ZERO_FREQ = 26'd1;
    parameter ONE       = 26'b1;
    parameter STATE_IDLE = 4'b0000,
    STATE_0001 = 4'b0001,
    STATE_0011 = 4'b0011,
    STATE_0010 = 4'b0010,
    STATE_0110 = 4'b0110,
    STATE_0100 = 4'b0100,
    STATE_1100 = 4'b1100,
    STATE_1000 = 4'b1000,
    STATE_1001 = 4'b1001;
    
    wire full_clk;
    
    wire clk;
    
    assign clk = is_full_step ? full_clk : half_clk;
    
    frequencyDivider half_or_full(half_clk, resetb, ONE, full_clk);
    
    initial begin
        coils <= STATE_IDLE;
    end

בסימולציה לעיל ניתן לראות בבירור כי תחילה הפלט עוקב אחר התבנית של חצי צעד. לאחר כ-100ns הקונטרול dir עולה למעלה ואכן כיוון הסיבוב מתהפך.

סביב 150ns יורד למטה resetb למשך מחזור יחיד ואכן המכונה מתאפסת ומתחילה את המחזור מחדש.

סביב 240ns המכונה עוברת למצב צעד מלא ופלט הסלילים משתנה בהתאם. גם במצב זה dir גורר שינוי תקין בכיוון המחזור.

velocityController

מודול זה בנוי כמונה בינארי ברוחב 8 ביטים אשר מוציא החוצה rpm ומקודם בירידת כפתור, בתנאי ש-resetb למעלה ובתנאי ש is_acc_mode למטה. כלומר המהירות לא תקודם אם מצב הפעולה הנבחר אינו מהירות קבוע,  מצב בו rpm נכנס אל rpmConverter מתוך מודול ההאצה.

הערך ההתחלתי של המונה הוא 10, וכאשר המונה מגיע ל60 הוא עובר למצב החסרה. כלומר ערך המוצא rpm נע בטווח 10-60 הלוך ושוב.

module velocityController (input wire button,
                           input wire resetb,
                           input wire is_acc_mode,
                           output reg [7:0] rpm);
    
    reg inc;
    
    parameter TEN   = 8'd10;
    parameter SIXTY = 8'd60;
    parameter TRUE  = 1'b1;
    parameter FALSE = 1'b0;
    
    initial begin
        rpm <= TEN;
        inc <= TRUE;
    end
    
    always @(negedge button or negedge resetb) begin
        if (~resetb) begin
            rpm <= TEN;
            inc <= TRUE;
        end
        else begin
            if (~is_acc_mode) begin
                
                if (inc) begin
                    rpm = rpm + TEN;
                    if (rpm == SIXTY) begin
                        inc <= FALSE;
                    end
                end
                else begin
                    rpm = rpm - TEN;
                    if (rpm == TEN) begin
                        inc <= TRUE;
                    end
                end
            end
        end
    end
    
endmodule

בסימולציית הגלים לעיל ניתן לראות כי אכן rpm מקודם ב-10 בעת ירידת קלט הכפתור, וכי כאשר is_acc_mode עולה, rpm אדיש לירידת הכפתור.

כאשר resetb יורד, המונה מתאפס, וכאשר המונה מגיע ל-60 או ל-10 המונה עובר מהוספה של 10 להחסרה של 10 ולהפך.

accelerator

מודול זה אחראי לפעולת ההאצה/האטה. לפי ההנחיות, ההאצה צריכה להיות על ידי קידום rpm ב-2 כל 100ms. לכן בתוך המודול ממומש מופע נוסף של frequencyDivide עם קונטרול half_division_factor=2.5M כלומר שעון ה-50MHz יהפוך לשעון 10Hz, כלומר עלייה כל 100ms כנדרש.

הרג'יסטר inc קובע האם אנו מאיצים או מאיטים.

תחילה נאפס את פלט ה-rpm ל-0 ואת inc ל-0.

כל ירידה של resetb תאפס גם היא את inc ואת rpm לתנאים ההתחלתיים שלהם.

ירידה של button כלומר לחיצה על הכפתור המתאים, תגרום להיפוך של inc בתנאי שאנחנו אכן במצב תאוצה (is_acc_mode=1) וגם rpm באחד מהקצוות שמוגדרים לו (198 או 0 בהתאם לדרישות הממשק).

בנוסף, כל עלייה של שעון ה-10Hz תגרום להוספה או החסרה של 2 ל-rpm, בהתאם למצב של inc. זאת כמובן בתנאי שis_acc_mode=1.

module accelerator (input wire input_clk,
                    input wire button,
                    input wire resetb,
                    input wire is_acc_mode,
                    output reg [7:0] rpm);
    // real value
    parameter HALF_10HZ = 25'd2500000;
    // simulation value
    // parameter HALF_10HZ = 25'd1;
    wire clk_10HZ;
    
    frequencyDivider fd(input_clk, resetb, HALF_10HZ, clk_10HZ);
    
    reg inc;
    parameter ZERO           = 8'd0;
    parameter TWO            = 8'd2;
    // real value
    parameter ONE_NINE_EIGHT = 8'd198;
    // simulation value
    // parameter ONE_NINE_EIGHT = 8'd30;
    parameter TRUE           = 1'b1;
    parameter FALSE          = 1'b0;
    
    initial begin
        rpm <= ZERO;
        inc <= FALSE;
    end
    
    always @(negedge button or negedge resetb) begin
        if (~resetb) begin
            inc <= FALSE;
        end
        else begin
            if (is_acc_mode && (rpm == ONE_NINE_EIGHT || rpm == ZERO)) begin
                inc <= ~inc;
            end
        end
    end
    always @(posedge clk_10HZ or negedge resetb) begin
        if (~resetb) begin
            rpm <= ZERO;
        end
        else begin
            if (is_acc_mode) begin
                if (inc == TRUE) begin
                    if (rpm != ONE_NINE_EIGHT) begin
                        rpm <= rpm + TWO;
                    end
                end
                else begin
                    if (rpm != ZERO) begin
                        rpm <= rpm - TWO;
                    end
                end
            end
        end
    end
    
endmodule

בסימולציית הגלים לעיל ניתן לראות את rpm משתנה במרווחים של 2  בכל עליית שעון שנייה (half_division_factor=1 עבור הסימולציה), החל מהרגע שבו button ירד. ניתן לראות כי לחיצות נוספות על הכפתור לא משפיעות כל עוד rpm לא נמצא באחד מהקצוות שלו, 30 או 0 (הקטנו את 198 ל-30 לצרכי קריאות הסימולציה). בנוסף ניתן לראות כי התאוטה מושהה כאשר is_acc_mode יורד לאפס, וממשיכה מאותו מקום כאשר הוא עולה חזרה. יתר על כן, ירידה של resetb מאפסת את תהליך התאוטה.

 debouncer

מודול זה נועד "לנקות" רעשים מסיגנלים שמגיעים מהמתגים המובנים ב-FPGA. המודול מקבל את שעון ה-50MHz הפנימי, ודוגם את פלט המתג במעין shift-register בעל שני ביטים בלבד. פלט ה-debouncer הוא AND על הפלטים של שני הרגיסטרים. כלומר, פולסים מהמתג שקצרים מ-20ns לא יעברו הלאה.

module debouncer (input wire clk,
                  input wire resetb,
                  input wire data_in,
                  output wire data_out);
    
    parameter ZERO = 1'b0;
    
    reg reg1;
    reg reg2;
    
    initial begin
        reg1 <= ZERO;
        reg2 <= ZERO;
    end
    
    always @(posedge clk or negedge resetb) begin
        if (~resetb) begin
            reg1 <= ZERO;
            reg2 <= ZERO;
        end
        else begin
            reg1 <= data_in;
            reg2 <= reg1;
        end
    end
    
    assign data_out = data_in && reg1 && reg2;
endmodule

frequencyDivider

מודול זה הינו מחלק תדר. המודול מקבל אות כניסה בתדר מסוים (הנחת עבודה duty cycle=50%), וכן מספר בינארי ברוחב 25 ביט (כלומר עד 33,554,432). מוצא המודול הוא אות בתדר הקטן מאות הכניסה בפקטור הזהה למספר הבינארי שהוכנס.

בתוך המודול מונה בינארי ברוחב זהה, המקודם בעלייה של אות הכניסה. כאשר ערך המונה קטן ב-1 מהמספר בקלט, מוצא המודול (אשר מאותחל כאחד לוגי) מתהפך והמונה מאופס.

המשמעות היא שזמן המחזור מתארך פי פעמיים המספר שניתן בקלט, כלומר תדר הכניסה מחולק בפקטור של פעמיים מספר הקלט. מכאן הקלט הוא מחצית מפקטור החילוק.

כיוון שהמספר המקסימלי בקלט זה הוא 33,554,432, פקטור החילוק המקסימלי הוא כ-67 מיליון.

module frequencyDivider(input wire input_clk,    // Input signal
                        input wire [24:0] half_division_factor,
                        // the factor in which the input signal
                        // will be divided
                        output reg output_clk);  // Output signal
                                                 // (with divided 
                                                 // frequency)
    
    reg [24:0] counter; // a 25 bit counter
    // Max division factor = 2*(2^25) = ~67M
    
    initial begin
        counter    = 0; // counter starts at zero
        output_clk = 1; // output signal starts at HIGH
    end
    
    always @(posedge input_clk)
    begin
        // increment the counter every positive edge
        counter <= counter + 1;
        
        // reset the counter and toggle the output if division factor 
        // is crossed
        if (counter >= half_division_factor - 1)
        begin
            counter    <= 0;
            output_clk <= ~output_clk;
        end
    end
endmodule

על מנת לבדוק את המודול בסימולציה, נצטרך להכניס פקטור חילוק קטן מאוד ביחס למה שהמודול תומך. פקטורים גבוהים מדי יגרמו לתדר נמוך מדי מכדי לבוא לידי ביטוי בסימולציה, אשר רצה בזמנים קצרים מאוד ביחס לסדר הגודל של 67 מיליון.

סימולציות שונות עבור פקטורי חילוק משתנים הראו את נכוונת המודול.

binaryTo7Segment

כאמור, מודול זה ממיר מספר בעל ארבעה ביטים לכדי הייצוג שלו על גבי צג seven-segment בקונפיגורציית common-anode. עבור מספרים הגדולים מ-9 יוצג מסך ריק.מודול זה הינו אסינכרוני.

הסכימה המתקבלת היא של decoder אשר עבור כל מספר בינארי אפשרי ברוחב 4 ביט מדליק ביט מוצא מסוים באופן בלעדי, וביטים אלו מחוברים אל לוגיקה הקובעת לכל סגמנט בתצוגה – איזה ביטים גורמים להדלקתו.

module binaryTo7Segment(input wire [3:0] digit,
                        output reg [6:0] segment);

    always @* begin     // listens for any change in the                
                        // input bus
        case (digit)
            4'd0: segment    = 7'b1000000; // 0
            4'd1: segment    = 7'b1111001; // 1
            4'd2: segment    = 7'b0100100; // 2
            4'd3: segment    = 7'b0110000; // 3
            4'd4: segment    = 7'b0011001; // 4
            4'd5: segment    = 7'b0010010; // 5
            4'd6: segment    = 7'b0000010; // 6
            4'd7: segment    = 7'b1111000; // 7
            4'd8: segment    = 7'b0000000; // 8
            4'd9: segment    = 7'b0010000; // 9
            default: segment = 7'b1111111; // all segments 
                                           // are off if
                                           // (digit > 9)
        endcase
    end
endmodule