ربات تکچرخ (مونوویل) تعادلی با آردوینو و پرینتر سهبعدی
رباتهای تعادلی همیشه یکی از جذابترین پروژههای الکترونیک هستن، اما وقتی صحبت از یک ربات تکچرخ (Monowheel) میشه، چالش و هیجان کار چند برابر میشه. حفظ تعادل روی یک چرخ، اون هم در حال حرکت، نیاز به ترکیب دقیق طراحی مکانیکی، الکترونیک و برنامهنویسی پیشرفته داره.
توی این پروژه از تیم آیسیمدار، قراره با استفاده از قطعات پرینت سهبعدی، یک آردوینو و سنسور ژیروسکوپ، یک ربات تکچرخ بسازیم که سعی میکنه تعادل خودش رو حفظ کنه و با کنترل از راه دور مادون قرمز (IR) هدایت بشه. این پروژه یک فرصت عالیه تا با مفاهیم کنترل PID و تنظیم سیستمهای مکانیکی و چرخدندهها در عمل آشنا بشی.
اگر به دنیای رباتیک علاقه داری و میخوای مهارتهای طراحی و برنامهنویسیت رو حسابی به چالش بکشی، قطعات رو آماده کن تا بریم سراغ ساخت این ربات خاص و هیجانانگیز!
قطعات موردنیاز
- پرینتر سهبعدی، فیلامنت و نرمافزار اسلایسر (برای چاپ چرخ، شاسی، چرخدندهها و پایههای موتور - توصیه میشه از فیلامنتهای مقاوم مثل PLA+ استفاده کنی)
- ماژول ژیروسکوپ و شتابسنج (برای فرمان و حفظ تعادل - مدل MPU-9250)
- برد آردوینو و سیمهای جامپر (برای کنترل موتورها بر اساس دیتای سنسور - مدل Arduino Uno R3)
- دو عدد شفت فلزی (طول 50 میلیمتر، قطر 2 میلیمتر برای اتصال چرخدندهها)
- سروو موتور (برای کنترل فرمان و جابجایی وزن تعادلی - مدل SG90 یا نسخههای قویتر)
- موتور DC و درایور موتور (برای به حرکت درآوردن چرخ اصلی - درایور مدل L293D)
- دو عدد پک باتری (یک باتری 9 ولت برای آردوینو و یک باتری متناسب با ولتاژ موتور DC)
- گیرنده و ریموت کنترل مادون قرمز (IR)
- سوکت باتری کتابی (برای اتصال باتری 9 ولت به آردوینو)
- شیلد پروتوتایپ آردوینو (برای راحتی در سیمکشی - اختیاری)
- چسب حرارتی تفنگی (برای نصب قطعات الکترونیکی، موتورها و باتریها)
- واشر یا اسپیسر (برای کاهش اصطکاک چرخدندهها)
- کش پول (برای مرتب کردن دسته سیمها)
- چسب برق (برای عایقکاری و فیکس کردن اتصالات)
طراحی سهبعدی بدنه و تنظیم دقیق چرخدندهها
طراحی این ربات به خاطر فضای محدودی که برای جا دادن تمام قطعات الکترونیکی داره، یکم چالشبرانگیزه. یکی از مهمترین و سختترین بخشها، تنظیم ابعاد چرخدندههاست تا به نرمی و بدون گیر کردن با هم درگیر بشن.
موقع طراحی اولیه در نرمافزارهای مدلسازی، ابزارهای تغییر مقیاس خودکار گاهی اوقات وقتی تعداد دندانهها رو عوض میکنی، ابعاد رو به هم میریزن. ممکنه فایلها رو پرینت بگیری و بعدا متوجه بشی که چرخدندهها روی هم چفت نمیشن. حتی یک میلیمتر خطای محاسباتی باعث میشه مکانیزم حرکتی ربات به مشکل بخوره.
برای اینکه چرخدندهها بینقص کار کنن، باید ابعادشون رو به صورت دستی محاسبه و تنظیم کنی. فرمول کار اینطوریه که ابعاد قبلی رو در (تعداد دندانههای جدید تقسیم بر تعداد دندانههای قدیم) ضرب میکنی. مثلا اگر یک چرخدنده 5 در 5 میلیمتری با 10 دندانه داری و میخوای دندانهها رو به 5 تا کاهش بدی، ابعاد جدیدت باید 2.5 در 2.5 میلیمتر بشه.
با اعمال این فرمول و پرینت مجدد، چرخدندهها کاملا روان و عالی با هم درگیر میشن. یادت باشه اگر مدل سروو یا موتور DC که داری با قطعات این آموزش متفاوته، حتما قبل از پرینت، فایلهای سهبعدی رو متناسب با ابعاد قطعات خودت تغییر بدی تا موقع مونتاژ به مشکل نخوری. ترتیب پرینت قطعات ربات تکچرخ:
- اول چرخ اصلی (Main Wheel)
- دوم شاسی اصلی (Main Frame)
- و در نهایت تمام چرخدندهها
(تنظیمات پرینت رو بر اساس نوع فیلامنت خودت روی مقاومت بالا تنظیم کن)
سیمکشی قطعات و برنامهنویسی کنترلر PID
بریم سراغ مغز متفکر ربات! برای راحتی کار در این شلوغی، پیشنهاد میکنم از یک شیلد روی آردوینو استفاده کنی تا پینهای 5 ولت و GND بیشتری در دسترس داشته باشی. تمام سیمکشیها طبق توضیحات پایین انجام میشه. برای درایور موتور (L293D): پینهای Enable 1 و 2 رو به پین 9 آردوینو وصل کن.
پین Input 1 به پین 7، و پین Input 2 به پین 6 متصل میشه. تغذیه لاجیک (Power 1) رو به 5 ولت آردوینو و تغذیه موتور (Power 2) رو به مثبت باتری موتور وصل کن. خروجیهای 1 و 2 هم مستقیم به موتور DC میرن.
حتما تمام GNDهای مدار (آردوینو، درایور و باتریها) رو به هم متصل کن تا مدار همپتانسیل بشه. اگر دیدی موتور برعکس میچرخه، کافیه جای دو سیم خروجی موتور رو با هم عوض کنی. برای ژیروسکوپ، پین VCC رو به 5 ولت (یا 3.3 ولت بسته به ماژول) و GND رو به GND متصل کن. پینهای SCL و SDA هم به پینهای متناظرشون روی آردوینو وصل میشن.
برای گیرنده IR: پین VCC به 5 ولت، Ground به GND و Signal رو به پین 2 آردوینو متصل کن. حالا بریم سراغ برنامهنویسی.
تعادل یک ربات تکچرخ پروژه کنترلی به شدت پیچیدهایه. برای این کار از الگوریتم کنترل PID (تناسبی، انتگرالگیر، مشتقگیر) استفاده کردیم تا زاویه دقیق سروو موتور رو بر اساس میزان انحراف و سرعت افتادن ربات محاسبه کنیم. سروو موتور با حرکت دادن یک وزنه در جهت مخالف افتادن، سعی میکنه مرکز ثقل رو جابجا کنه.
در کدهای فعلی، سروو موتور واکنش درستی نشون میده، اما ممکنه برای برگشتن کامل به حالت عمودی پیش از افتادن، به تنظیمات دقیقتر ضرایب PID یا استفاده از موتورها و درایور قویتری نیاز داشته باشی. این بخش کاملا جای بهینهسازی و آزمون و خطا داره. کدها بر پایه PID نوشته شدن تا بتونی روی ضرایبش کار کنی.
برای کنترل حرکت:
- با دریافت هر سیگنالی از ریموت IR، موتور اصلی روشن میشه.
- بهتره کد رو طوری تغییر بدی که فقط دکمههای خاصی موتور رو روشن کنن.
- اگر موقع تست ریموت IR نداری، میتونی از Serial Monitor استفاده کنی:
- ارسال عبارت on مساوی است با روشن شدن موتور.
- ارسال عبارت off مساوی است با خاموش شدن موتور.
کدها رو روی برد آپلود کن و بریم برای مونتاژ فیزیکی!
#include <Wire.h>
#include <Servo.h>
#define MPU_ADDR 0x68
#define RAD2DEG 57.29577951308232f
#define GYRO_SCALE 131.0f
const int SERVO_PIN = 5;
const int EN1 = 9;
const int IN1 = 7;
const int IN2 = 6;
const int IR_PIN = 2;
int SERVO_CENTER = 110;
const int SERVO_MIN = 70;
const int SERVO_MAX = 160;
float SERVO_SLEW = 3.0f;
const float CF_ALPHA = 0.995f;
float filterAngle = 0.0f;
float accelZero = 0.0f;
float Kp = 8.0f;
float Ki = 0.6f;
float Kd = 1.2f;
float integral = 0.0f;
float lastError = 0.0f;
float lastDerivative = 0.0f;
const float I_LIMIT = 200.0f;
const float DERIV_FILTER_ALPHA = 0.7f;
float servoPosSmooth = SERVO_CENTER;
int ACC_AXIS = 1;
int GYRO_AXIS = 1;
int SIGN_ACC = 1;
int SIGN_GYRO = 1;
int SIGN_Z = 1;
Servo steer;
unsigned long lastMicros = 0;
float gyroBias[3] = {0.0f,0.0f,0.0f};
volatile bool irPulseFlag = false;
unsigned long lastIrToggleMs = 0;
const unsigned long IR_DEBOUNCE_MS = 500;
int motorPWM = 200;
bool motorState = false;
// -------- Function Prototypes --------
void setMotor(bool on);
void irISR();
void handleSerial();
void printStatus();
void mpureg_write(uint8_t reg, uint8_t val);
void readAccelGyro(int16_t &ax, int16_t &ay, int16_t &az, int16_t &gx, int16_t &gy, int16_t &gz);
float readAccelAngleRaw();
void calibrateGyroBias();
// -------- Setup --------
void setup(){
Serial.begin(115200);
Wire.begin();
pinMode(EN1, OUTPUT);
pinMode(IN1, OUTPUT);
pinMode(IN2, OUTPUT);
analogWrite(EN1, 0);
digitalWrite(IN1, LOW);
digitalWrite(IN2, LOW);
pinMode(IR_PIN, INPUT);
attachInterrupt(digitalPinToInterrupt(IR_PIN), irISR, CHANGE);
steer.attach(SERVO_PIN);
steer.write(SERVO_CENTER);
servoPosSmooth = SERVO_CENTER;
mpureg_write(0x6B, 0x00);
delay(100);
mpureg_write(0x1B, 0x00);
delay(50);
Serial.println("\n=== BALANCE PID START ===");
Serial.println("Hold board upright and still during calibration...");
delay(1500);
calibrateGyroBias();
accelZero = readAccelAngleRaw();
filterAngle = 0.0f;
lastMicros = micros();
Serial.println("Calibration done. Commands: on/off kp/ki/kd center invacc invgyro invz s");
printStatus();
}
// -------- Loop --------
void loop(){
handleSerial();
if(irPulseFlag){
irPulseFlag = false;
if(millis() - lastIrToggleMs > IR_DEBOUNCE_MS){
lastIrToggleMs = millis();
setMotor(!motorState);
}
}
unsigned long now = micros();
float dt = (now - lastMicros) / 1e6f;
if(dt <= 0) dt = 0.001f;
lastMicros = now;
int16_t ax, ay, az, gx, gy, gz;
readAccelGyro(ax, ay, az, gx, gy, gz);
float acc[3] = {(float)ax, (float)ay, (float)az};
float g[3] = {(float)gx, (float)gy, (float)gz};
float accLat = SIGN_ACC * acc[ACC_AXIS];
float accVert = SIGN_Z * acc[2];
float accAngle = atan2(accLat, accVert) * RAD2DEG;
float gyroRate = SIGN_GYRO * ((g[GYRO_AXIS] - gyroBias[GYRO_AXIS]) / GYRO_SCALE);
filterAngle = CF_ALPHA * (filterAngle + gyroRate * dt) + (1.0f - CF_ALPHA) * accAngle;
float setpoint = 0.0f;
float error = setpoint - filterAngle;
integral += error * dt;
if(integral > I_LIMIT) integral = I_LIMIT;
if(integral < -I_LIMIT) integral = -I_LIMIT;
float derivativeRaw = (error - lastError) / dt;
float derivative = DERIV_FILTER_ALPHA * lastDerivative + (1.0f - DERIV_FILTER_ALPHA) * derivativeRaw;
lastDerivative = derivative;
float output = Kp * error + Ki * integral + Kd * derivative;
lastError = error;
float targetServo = SERVO_CENTER - output;
if(targetServo < SERVO_MIN) targetServo = SERVO_MIN;
if(targetServo > SERVO_MAX) targetServo = SERVO_MAX;
if(targetServo > servoPosSmooth + SERVO_SLEW) servoPosSmooth += SERVO_SLEW;
else if(targetServo < servoPosSmooth - SERVO_SLEW) servoPosSmooth -= SERVO_SLEW;
else servoPosSmooth = targetServo;
steer.write((int)servoPosSmooth);
static unsigned long lastPrint = 0;
if(millis() - lastPrint > 200){
Serial.print("angle=");
Serial.print(filterAngle, 2);
Serial.print(" err=");
Serial.print(error, 3);
Serial.print(" out=");
Serial.print(output, 3);
Serial.print(" servo=");
Serial.print((int)servoPosSmooth);
Serial.print(" motor=");
Serial.println(motorState ? "ON" : "OFF");
lastPrint = millis();
}
delay(6);
}
// -------- Motor --------
void setMotor(bool on){
if(on){
analogWrite(EN1, motorPWM);
digitalWrite(IN1, HIGH);
digitalWrite(IN2, LOW);
motorState = true;
Serial.println("MOTOR ON");
} else {
analogWrite(EN1, 0);
digitalWrite(IN1, LOW);
digitalWrite(IN2, LOW);
motorState = false;
Serial.println("MOTOR OFF");
}
}
// -------- IR --------
void irISR(){
irPulseFlag = true;
}
// -------- Serial --------
void handleSerial(){
while(Serial.available()){
String s = Serial.readStringUntil('\n');
s.trim();
if(s.length() == 0) return;
if(s.equalsIgnoreCase("on")) setMotor(true);
else if(s.equalsIgnoreCase("off")) setMotor(false);
else if(s.startsWith("kp ")) { Kp = s.substring(3).toFloat(); Serial.print("Kp="); Serial.println(Kp); }
else if(s.startsWith("ki ")) { Ki = s.substring(3).toFloat(); Serial.print("Ki="); Serial.println(Ki); }
else if(s.startsWith("kd ")) { Kd = s.substring(3).toFloat(); Serial.print("Kd="); Serial.println(Kd); }
else if(s.equalsIgnoreCase("center")) { SERVO_CENTER = (int)servoPosSmooth; Serial.print("Set SERVO_CENTER="); Serial.println(SERVO_CENTER); }
else if(s.equalsIgnoreCase("invacc")) { SIGN_ACC = -SIGN_ACC; Serial.print("SIGN_ACC="); Serial.println(SIGN_ACC); }
else if(s.equalsIgnoreCase("invgyro")) { SIGN_GYRO = -SIGN_GYRO; Serial.print("SIGN_GYRO="); Serial.println(SIGN_GYRO); }
else if(s.equalsIgnoreCase("invz")) { SIGN_Z = -SIGN_Z; Serial.print("SIGN_Z="); Serial.println(SIGN_Z); }
else if(s.equalsIgnoreCase("s")) printStatus();
else { Serial.print("Unknown cmd: "); Serial.println(s); }
}
}
// -------- Status --------
void printStatus(){
Serial.println("--- STATUS ---");
Serial.print("Kp="); Serial.print(Kp);
Serial.print(" Ki="); Serial.print(Ki);
Serial.print(" Kd="); Serial.println(Kd);
Serial.print("ACC_AXIS="); Serial.print(ACC_AXIS);
Serial.print(" SIGN_ACC="); Serial.println(SIGN_ACC);
Serial.print("GYRO_AXIS="); Serial.print(GYRO_AXIS);
Serial.print(" SIGN_GYRO="); Serial.println(SIGN_GYRO);
Serial.print("SIGN_Z="); Serial.println(SIGN_Z);
Serial.print("SERVO_CENTER="); Serial.println(SERVO_CENTER);
Serial.print("IR_PIN="); Serial.println(IR_PIN);
Serial.println("--------------");
}
// -------- MPU helpers --------
void mpureg_write(uint8_t reg, uint8_t val){
Wire.beginTransmission(MPU_ADDR);
Wire.write(reg);
Wire.write(val);
Wire.endTransmission();
}
void readAccelGyro(int16_t &ax, int16_t &ay, int16_t &az, int16_t &gx, int16_t &gy, int16_t &gz){
Wire.beginTransmission(MPU_ADDR);
Wire.write(0x3B);
Wire.endTransmission(false);
Wire.requestFrom(MPU_ADDR, 14, true);
ax = (Wire.read() << 8) | Wire.read();
ay = (Wire.read() << 8) | Wire.read();
az = (Wire.read() << 8) | Wire.read();
Wire.read(); Wire.read();
gx = (Wire.read() << 8) | Wire.read();
gy = (Wire.read() << 8) | Wire.read();
gz = (Wire.read() << 8) | Wire.read();
}
float readAccelAngleRaw(){
int16_t ax, ay, az, gx, gy, gz;
readAccelGyro(ax, ay, az, gx, gy, gz);
float a[3] = {(float)ax, (float)ay, (float)az};
float lat = SIGN_ACC * a[ACC_AXIS];
float vert = SIGN_Z * a[2];
return atan2(lat, vert) * RAD2DEG;
}
void calibrateGyroBias(){
const int N = 400;
long sum[3] = {0,0,0};
Serial.println("Calibrating gyro bias — keep STILL...");
for(int i=0;i<N;i++){
int16_t ax,ay,az,gx,gy,gz;
readAccelGyro(ax,ay,az,gx,gy,gz);
sum[0]+=gx; sum[1]+=gy; sum[2]+=gz;
delay(3);
}
gyroBias[0] = (float)sum[0]/N;
gyroBias[1] = (float)sum[1]/N;
gyroBias[2] = (float)sum[2]/N;
Serial.print("gyroBias X,Y,Z = ");
Serial.print(gyroBias[0],2); Serial.print(" ");
Serial.print(gyroBias[1],2); Serial.print(" ");
Serial.println(gyroBias[2],2);
}
مونتاژ فیزیکی و سوار کردن قطعات روی شاسی
وقتی تمام قطعات پرینت شدن، کار مونتاژ رو شروع میکنیم. قدم اول نصب چرخدندههاست. برای چرخدندهای که قراره با چرخدنده متصل به موتور درگیر بشه، شفت فلزی رو کمی داخل سوراخ بدنه هل بده. حتما یک واشر یا اسپیسر بین شاسی و چرخدنده بذار تا اصطکاک و ساییدگی کم بشه.
حالا چرخدنده رو روی شفت قرار بده، یک واشر دیگه سمت مقابلش بنداز و شفت رو تا آخر رد کن. با یک قطره چسب حرارتی دو سر شفت رو محکم کن تا موقع حرکت بیرون نیاد. همین مراحل رو برای چرخدنده هرزگرد در سمت مخالف شاسی هم تکرار کن.
چرخدنده سوم، همون چرخدنده مخصوص موتوره. کافیه اون رو مستقیم روی شفت خود موتور DC فشار بدی تا محکم بشه. حالا بریم سراغ نصب موتورها.
موتور DC رو داخل محفظه مخصوصش قرار بده و با چسب حرارتی فیکسش کن. اگر موتورت کوچیکتر از محفظه است و لق میزنه، با چند تکه چوب بستنی یا پلاستیک زیرش رو پر کن تا کاملا همسطح و محکم بشه.
برای نصب سروو موتور، اگر از مدل SG90 استفاده میکنی معمولا با کمی فشار به صورت کشویی سر جاش محکم میشه و نیازی به چسب نداره. اما اگر ابعاد رو برای سرووی دیگهای تغییر دادی، با چسب حرارتی سر جاش محکماش کن. قدم سوم، نصب مغز متفکر رباته.
برد آردوینو رو در قسمت زیرین شاسی نصب کن، طوری که پورتهای تغذیه در جهت مخالف موتور DC باشن. برای مرتب کردن سیمهای آردوینو و ماژولها میتونی از کش پول یا بست کمربندی استفاده کنی تا سیمها تو دست و پا نباشن و به چرخدندهها گیر نکنن. اگر از برد دیگهای غیر از Uno استفاده میکنی، هرجا که فضای مناسبی پیدا کردی و دسترسی به پورت تغذیه داشتی نصبش کن.
نصب باتریها، ژیروسکوپ و سنسور IR: نکته به شدت مهم در نصب باتریها، حفظ تعادل وزنی رباته! قبل از چسباندن باتری روی سروو، یک بار ربات رو روشن کن تا بازوی سروو در حالت وسط (Center) قرار بگیره؛ حالا باتری رو دقیقا روی بازوی سروو، رو به بالا چسب بزن تا وزن باتری به عنوان وزنه تعادل عمل کنه.
باتری آردوینو رو هم در سمت مخالف موتور DC نصب کن تا وزن موتور رو خنثی کنه. حواست باشه بین باتری و چرخدندهها فاصله باشه تا اصطکاکی به وجود نیاد.
در نهایت، سنسور IR رو روی باتری و ماژول ژیروسکوپ رو در پهلوی شاسی بچسبان. نکته: وقتی ربات رو راه انداختی، باید یک قطعه کوچیک چوب یا پلاستیک روی لبه شاسی بچسبانی تا بدنه اصلی از داخل چرخ بزرگ بیرون نلغزه. چون درآوردن شاسی برای آپلود مجدد کدها کمی سخته، بهتره این مانع رو تو مراحل آخر کار نصب کنی.
نتیجهگیری و بهینهسازی ربات تعادلی
پروژههای رباتیک تعادلی همیشه جزو پرچالشترین کارهای الکترونیکی هستن و ساخت یک مونوویل از بقیه هم سختتره. اگر رباتت همون اول نتونست تعادلش رو صددرصد حفظ کنه، اصلا به این معنی نیست که جایی رو اشتباه رفتی. رسیدن به پایداری کامل نیاز به زمان، تنظیم دقیق وزنهها و آزمون و خطا داره.
سعی کن با تغییر وزن باتریها، استفاده از درایور قویتر یا دستکاری در کدهای PID، عملکرد این ربات رو بهتر کنی. این یک بستر عالی برای یادگیری عملی کنترلره. امیدوارم از این چالش فنی لذت برده باشی. ابزارها رو بردار و پروژهات رو ارتقا بده!
تو میتونی اولین سازنده باشی!
اگه پروژه رو ساختی به اشتراک بزار و اعتبار هدیه بگیر
این آموزش با استانداردهای اختصاصی آیسیمدار بازنویسی و بهینهسازی شده و تمامی حقوق انتشار آن متعلق به این مجموعه است و در صورت کپی یا بازنشر پیگرد قانونی خواهد داشت.








































گفتگوهای کاربران
هنوز نظری ثبت نشده؛ تو شروع کن!
هنوز نظری ثبت نشده؛ تو شروع کن!