DIY實作Blumoduino控制板

蝦咪喜Blumoduino? 它是Arduino UNO+Bluetooth+L293D的整合版,成本台幣700元以內,可以驅動6顆馬達(4DC馬達與2顆伺服馬達)。為什麼取這種怪名子咳,這要稍微解釋一下

坊間有在賣所謂的Motoduino (不含藍芽模組約600元台幣,應該是台灣自己做的而不是義大利原廠出的),它應該是參考Arduino Duemilanove (2007年推出的版本; UNO則為2010年推出的版本)並結合L293D馬達驅動晶片的一塊整合板,可以驅動2DC馬達,適合應用在如遙控車、自走車、機器人控制等… (還蠻熱門的!)


Source: Motoduino Lab


安照這個命名的邏輯,Blu是義大利文的藍色(表示有藍芽功能)Moto是驅動電機(L293D),但Mo-to讀音不乾脆,因此只取前半截讀音,而Duino是義大利的一個知名市鎮(Arduino的由來),如此形成Blu-mo-duino…

好了,這不是重點為什麼要搞一塊這種版子想「自做」飛行器,但都還不會走路怎麼可能飛上天呢因此「自做」飛行器前,還是先DIY一個手機App藍芽遙控車好了上一回我們簡單討論了馬達與其驅動電機板之後,所以這回先兜一個機電控制板。


如圖所示,若將L293D直接與Arduino UNO堆疊起來,所有的I/O埠都被L293D給佔滿了(包括重要的Rx/TxVDD/GND),因此我們必須把這4Pin腳想辦法接出來。



歐吉尚成本有限,不想用焊的(想重複使用),因此花了約10塊錢剪了幾條跳線用的電線、端子跟塑膠端子套,做成4-Pin的端子母接頭給藍芽模組用(HC-06)



小心將Rx/TxVDD/GND4個接腳從Arduino UNO板子接頭拉出來,拔線時注意不要讓過多金屬外露,以免不小心短路。



之後在套上L293D就大功告成了整合Arduino UNOL293D、具有藍芽端子的樂高積木。當然更高竿的可以多花個10塊錢買塊「萬用版」自己裁切,並把他焊成下公上母的三明治夾心擴充板,更像樂高(但原廠要價150元台幣,當你是盤子,我的方法較便宜、輕、又不耗時)



HC-06與手機的藍芽通訊實測結果不錯,我在家裡每個房間關上房門都還Sense得到藍芽信號(~15公尺)




BOM(把虛擬通路也一起放進來Survey, 相同型號)

項目(不含運費)
台灣(台幣)
淘寶(人民幣)
Arduino UNO R3
290~450
12~40
Bluetooth HC-06
210~350
17~30
L293D Board (H-橋式電機驅動器)
180~250
12~21
端子與線材
<10
NA






L293D驅動程式: DC馬達測試

本程式利用鍵盤將使用者指令透過UART傳回Arduino,指令定義為: f(正轉)b(反轉)r(停止)。驅動程式部份是買L293D板子時廠商附贈的範例,經歐吉尚研判(讀過網路上數個不同版本之後)應該都是從Adafruit推出的AF_DCMotor Library抄出來的,該程式完整的定義了將L293D堆疊在Arduino板上之後所有接腳與74HCT595移位暫存器(Shift Register)的控制信號的連接。下圖為廠商提供的電路板接線,但部分接線定義不太正確,經過測試,正確的定義以驅動程式的描述為主。




程式中也貼心的提到,「善待您的馬達,180度反轉向時請讓馬達稍微休眠一下」,其實也應該稍微能減緩因電感造成的瞬間電壓雜訊反過來干擾Arduino板。L293D可以同時驅動4DC馬達,其中函式motor可以選擇欲驅動的馬達編號、轉向與轉速,而最後透過函式motor_output將使用者命令轉換成對74HCT595移位暫存器(Shift Register)的控制信號。

轉向是利用對H-橋式電機信號的高低變換達成(例如1號碼達,改變M1A/M1B信號的電位),而轉速的部份是透過PWM信號來控制,其範圍從最小0到最大255(細節請參閱實作App遠程遙控門禁管制)

// Arduino Pins        // 74HCT595N Pins
#define MOTORLATCH 12  // DIR_LATCH 12
#define MOTORCLK    4  // DIR_CLK   11
#define MOTORENABLE 7  // DIR_EN    13
#define MOTORDATA   8  // DIR_SER   14

#define MOTOR1_PWM 11 // PWM2A (M1)
#define MOTOR2_PWM 3  // PWM2B (M2)
#define MOTOR3_PWM 6  // PWM0A (M4)
#define MOTOR4_PWM 5  // PWM0B (M3)
#define SERVO1_PWM 10 // PWM1B (SER1)
#define SERVO2_PWM 9  // PWM1A (SERVO2)

// 74HCT595N Pins
#define MOTOR1_A 2
#define MOTOR1_B 3
#define MOTOR2_A 1
#define MOTOR2_B 4
#define MOTOR3_A 5
#define MOTOR3_B 7
#define MOTOR4_A 0
#define MOTOR4_B 6

#define FORWARD  1
#define BACKWARD 2
#define BRAKE    3
#define RELEASE  4


void setup() {
    Serial.begin(9600);
}

void loop() {
    char key = 0;
    if ( Serial.available() ) {
       key = Serial.read();
    }
    // 善待您的馬達,180度反轉向時請讓馬達稍微休眠一下
    motor(1, RELEASE, 0);
    delay(1000);
    switch ( key ) {
        case 'f': // 正轉
            motor(1, FORWARD, 50);  
            delay(5000);
            break;
        case 'b': // 反轉
            motor(1, BACKWARD, 50);
            delay(5000);
            break;       
        case 'r':
        default:
            break;
    }
}

void motor(int nMotor, int command, int speed) {
    int motorA, motorB;
    if (nMotor >= 1 && nMotor <= 4) {
        switch (nMotor) {
            case 1:
                motorA = MOTOR1_A;
                motorB = MOTOR1_B;
                break;
            case 2:
                motorA = MOTOR2_A;
                motorB = MOTOR2_B;
                break;
            case 3:
                motorA = MOTOR3_A;
                motorB = MOTOR3_B;
                break;
            case 4:
                motorA = MOTOR4_A;
                motorB = MOTOR4_B;
                break;
            default:
                break;
        }
        switch (command) {
            case FORWARD:
                motor_output (motorA, HIGH, speed);
                motor_output (motorB, LOW, -1); // -1: no PWM set
                break;
            case BACKWARD:
                motor_output (motorA, LOW, speed);
                motor_output (motorB, HIGH, -1); // -1: no PWM set
                break;
            case BRAKE:
                motor_output (motorA, LOW, 255); // 255: fully on.
                motor_output (motorB, LOW, -1); // -1: no PWM set
                break;
            case RELEASE:
                motor_output (motorA, LOW, 0); // 0: output floating.
                motor_output (motorB, LOW, -1); // -1: no PWM set
                break;
            default:
                break;
        }
    }
}

void motor_output (int output, int high_low, int speed) {
    int motorPWM;
    switch (output) {
        case MOTOR1_A:
        case MOTOR1_B:
            motorPWM = MOTOR1_PWM;
            break;
        case MOTOR2_A:
        case MOTOR2_B:
            motorPWM = MOTOR2_PWM;
            break;
        case MOTOR3_A:
        case MOTOR3_B:
            motorPWM = MOTOR3_PWM;
            break;
        case MOTOR4_A:
        case MOTOR4_B:
            motorPWM = MOTOR4_PWM;
            break;
        default:
            speed = -3333;
            break;
    }
    if ( speed != -3333 ) {
        shiftWrite(output, high_low);
        // set PWM only if it is valid
        if (speed >= 0 && speed <= 255) {
            analogWrite(motorPWM, speed);
        }
    }
}

void shiftWrite(int output, int high_low) {
    static int latch_copy;
    static int shift_register_initialized = false;
    // Do the initialization on the fly, at the first time it is used.
    if (!shift_register_initialized) {
        // Set pins for shift register to output
        pinMode(MOTORLATCH, OUTPUT);
        pinMode(MOTORENABLE, OUTPUT);
        pinMode(MOTORDATA, OUTPUT);
        pinMode(MOTORCLK, OUTPUT);
        // Set pins for shift register to default value (low);
        digitalWrite(MOTORDATA, LOW);
        digitalWrite(MOTORLATCH, LOW);
        digitalWrite(MOTORCLK, LOW);
        // Enable the shift register, set Enable pin Low.
        digitalWrite(MOTORENABLE, LOW);
        // start with all outputs (of the shift register) low
        latch_copy = 0;
        shift_register_initialized = true;
    }
    // The defines HIGH and LOW are 1 and 0.
    // So this is valid.
    bitWrite(latch_copy, output, high_low);
    shiftOut(MOTORDATA, MOTORCLK, MSBFIRST, latch_copy);
    delayMicroseconds(5); // For safety, not really needed.
    digitalWrite(MOTORLATCH, HIGH);
    delayMicroseconds(5); // For safety, not really needed.
    digitalWrite(MOTORLATCH, LOW);
}







Blumodunio驅動程式整合藍芽、DC馬達與伺服馬達測試

驅動程式的部份直接利用AF_Motor Library,這樣程式碼可以寫得更精簡(我們要的是「開發新的創意」,基礎的部份就交給廠商去傷腦筋就好了!)AF_Motor函式庫很方便,使用者可以直接控制馬達,而不用管底層信號的連接,且PWM信號還提供多種解析度可以使用。依照使用手冊說明,較高頻可以減少DC馬達運轉時的嗡嗡聲(我用減速馬達確實是如此),但可能減少扭力(我感覺不出來)

各位有沒有發現,我們的程式又更進化了?(同樣的程式這一回程式最大的不同就是,我直接將藍芽控制改成為(萬用的)UART串列控制,若藍芽模組沒接時還可以改由鍵盤控制(透過RS232串列埠),這樣Debug比較方便。

唯一要注意的是,因為L293D直接插在Arduino上,藍芽只能接實體的Tx/Rx信號,無法透過軟體模擬,因此藍芽模組必須在程式碼燒錄完成後(透過USB)才能接上! 此外,Arduino polling速度比9600bps快太多了,在接收藍芽命令(串列訊號)時,我們刻意多等了5msRx有充分的時間做完。

#include <Servo.h>
#include <AFMotor.h> // 利用AF_Motor函式庫

#define MAX_UARTCMDLEN 128
#define SERVO1_PWM 10 // Pin10/SERVO1_PWM, and Pin9/SERVO2_PWM for L293D motor shield

// up to 128 bytes UART command received from the Android system
byte uartCmdBuff[MAX_UARTCMDLEN];
int uartCmdLen = 0; // received BT command length

// The valid values for speed are between 0 and 255.
AF_DCMotor motor(1,MOTOR12_2KHZ); // DC減速馬達, 1KHZ會有鳴叫聲

Servo servo; // 伺服器馬達紅色(VDD), 棕色(GND), 橙色(Signal)

void setup() {
    Serial.begin(9600); // 帶軟體燒錄完成才能接藍芽模組(實體Tx/Rx)
    servo.attach(SERVO1_PWM);
    servo.write(0); // 設置旋轉的角度
    motor.run(RELEASE);
}

void loop() {
    if ( listenSerialCmd()>0 )
        executeSerialCmd();
}

void executeSerialCmd() {
    char cmd[MAX_UARTCMDLEN];
    char* p;
    int value = 0; // angle or speed value
           
    // parse UART command
    sprintf(cmd,"%s",uartCmdBuff);
    if ( (p=strchr(cmd,','))==NULL )
        return;
    *p = '\0';
       
    // get speed or angle
    value = atoi(p+1);
    Serial.print("Dir:"); Serial.print(cmd); 
    Serial.print(", Value:"); Serial.println(value);
   
    // Be friendly to the motor: stop it before reverse.
    // DC馬達的轉速反應與程式的RPM不是線性的
    switch ( cmd[0] ) {
        case 'a':
            servo.write(value); // 設置旋轉的角度
            break;
        case 'f': // DC motor forward
            motor.run(RELEASE);
            motor.setSpeed(value);
            motor.run(FORWARD);
            break;
        case 'b': // DC motor backward
            motor.run(RELEASE);
            motor.setSpeed(value);
            motor.run(BACKWARD);
            break;       
        case 'x':
        default:
            motor.run(RELEASE);
            break;
    }
}

int listenSerialCmd() {
    char tmp; 
    uartCmdLen = 0;
    memset(uartCmdBuff,0,MAX_UARTCMDLEN);
    while( Serial.available()>0 ) {
        if( (tmp=Serial.read())=='O' ){
            uartCmdLen = 0;
        }
        uartCmdBuff[(uartCmdLen++)%MAX_UARTCMDLEN] = tmp;
        delay(5); // wait RX signal
    }
    return uartCmdLen;
}