מבוא
בפרויקט זה נממש ב-FPGA דרייבר למנוע צעד (stepper motor) מסוג Unipolar, הכולל שליטה על מהירויות סיבוב קבועות, על כיוון הסיבוב, על מצב צעד/חצי צעד וכן יכלול מצב תאוצה.
הבקר יעבוד לפי הממשק הבא:
FPGA interface | Description | Direction | Name |
---|---|---|---|
Internal 50 MHz clock | System clock | in | clk |
KEY0 | System reset – active low | in | resetb |
SW1 | clockwise – 1 counterclockwise – 0 | in | direction |
KEY3 | Motor 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. | in | speed_sel |
SW2 | 1 – continues motion 0 – stop | in | on |
SW9 | 1 – acceleration mode 0 – regular mode | in | mode_sel |
KEY2 | Initializes the acceleration/deacceleration sequence | in | accelerate |
SW3 | 1 – full step 0 – half step | in | step_size |
HEX0 | Seven segment display (will display the ones) | out | sev_seg_o |
HEX1 | Seven segment display (will display the tens) | out | sev_seg_t |
HEX2 | Seven segment display (will display the hundreds) | out | sev_seg_h |
GPIO pins | Output pulses | out | pulses_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
כתיבת תגובה