# 電路設計與程式
## 概述
Arduino 讀取搖桿移動時,接收子相應的訊號大小,並且用該數值大小作為控制各個馬達的依據。目前為止,我們已經實做出可以用 SPEkTRUM DXe 遙控器直接控制所有馬達運動的電路與程式。在附錄中會附上該程式。
雖然直接使用遙控器附的接收子便可完成最基礎的遙控功能。然而,使用 Arduino 做控制,可以使得機構整體的擴充性更強,在功能發想方面亦有更多元的空間,未來若需做改良(如:增加感測器、使用更多馬達等等、PID 控制等等),也可以使用 Arduino 方便的可程式功能做更快速的開發。
![](https://i.imgur.com/yADHRGE.png)
## 電路架構
基礎的架構如下:
![](https://i.imgur.com/my2F3Bk.png)
以下分別說明各個部分:
## SPEkTRUM DXe 遙控器與接收子
我們面臨的第一個問題是:接收子所接收到的訊號形式為何?關於這點,我們做了一下的觀察:
1. 若使用 Arduino 讀取類比電壓的 `analogRead()` 函數,無論如何轉動搖桿,電壓均為定值。
2. 將接收子的訊號輸出接到伺服馬達,發現在兩者均在工作電壓的狀況下,可以直接控制伺服馬達。
由上述兩項觀察,我們猜測接收子可能是接收 PWM 訊號,搖桿的轉動幅度不同,對應的腳位會出現工作週期的 PWM 。於是將輸出腳位連接至 Arduino,並且使用 `pulseIn()` 函數讀取不同動作時對應的 duty cycle,發現數字會依照搖桿動作變化,可以更加確定該訊號為 PWM 。
我們更進一步,使用以下的程式讀取各個動作相應的 PWM:
```C++
#define REER_NO 13
#define ROTATE_NO 12
#define ELEV_NO 11
#define CLIMB_NO 10
double ctrl_data[4] = {0, 0, 0, 0};
void get_control_data()
{
for(int i = 0; i < 4; i++){
ctrl_data[i] = pulseIn(10 + i, HIGH, 10e6);
}
}
```
結果如下:
不動時:
![](https://i.imgur.com/S4GFjbP.png)
右方搖桿往上:
![](https://i.imgur.com/3zGVKZ3.png)
右方搖桿往下:
![](https://i.imgur.com/XVPNO8u.png)
可以發現 REER 會依照搖桿位置,得到約 1100 ~ 1700 左右的數字。這是我們控制的第一步。
## MG996R 伺服馬達
為輝勝公司生產的伺服馬達。我們採用該馬達的理由為:
1. 內部為金屬齒輪,耐磨耗。
2. 扭力大:在 6V 供電下,最大扭矩可以達 11 kgf·cm^[http://www.electronicoscaldas.com/datasheet/MG996R_Tower-Pro.pdf]。
3. 方便控制:Arduno 預設的 Servo.h 函式庫可以直接控制。
我們另外改裝該馬達,使得該馬達可以旋轉完整的 360 度。
## L298N
![](https://i.imgur.com/PKmq7px.png)
一般來說,直流馬達的轉速,是透過施加在馬達兩端的電壓決定的。然而,這樣一來便產生以下疑問:
1. Arduino 僅有 5V 供電。若馬達需要更強的電壓,該如何解決?
2. 如何用 Arduino 控制馬達兩端的電壓?
3. 若馬達急轉,則容易產生 backlash EMF 傷害電路。需要有改善方法。
幸運地,使用 L298N^[https://cdn.instructables.com/ORIG/FCN/YABW/IHNTEND4/FCNYABWIHNTEND4.pdf] 電機驅動板,便可以解決上述所有問題。
L298N 的使用方法大致分成 3 部分:
1. 供電:分成 2 部分:
* 馬達電壓:驅動馬達的電壓,必須介於 5V ~ 36V 之間。
* 邏輯供電:供給 L298N 自身 IC 。約 5V。
需注意的是若使用 Arduino 控制,則所有供電都必須與 Arduino 共地。
2. 正/反轉控制:一個馬直流馬達有 2 個輸入邏輯訊號的腳位,以及 1 個 enable 腳位:
* 邏輯訊號:將直流馬達輸入「高電位 - 低電位」時,與輸入「低電位 - 高電位」時,會給予馬達相反的電壓,其大小由 enable 送入的 PWM 訊號來決定。
* enable 訊號:若 enable 訊號為 0,則無論邏輯輸入為何,馬達均不會轉動; 若將 enable 輸入 PWM 訊號,則可以依照 PWM 訊號,調整電壓供給的大小。
而一片 L298N 可控制兩個直流馬達,亦即有 4 個邏輯輸入腳位,以及 2 個 enable 腳位。
3. 輸出:由左右兩側的 2P 端子平台輸出。
如此一來,便解決了我們的 1. 與 2. 點。另外,第 3. 點因為 L298N 內建保護電路,因此亦解決了該問題。可以發現該 IC 為一個功能強大的 IC 。
以前進為例,我們使用下程式實作控制前進與後退的功能:
```C++
#define LEFT_EN 9
#define LEFT_1 8
#define LEFT_2 7
#define RIGHT_EN 3
#define RIGHT_1 5
#define RIGHT_2 4
...
int reer(){
if(ctrl_data[GET_CTRL_DATA(REER_NO)] > 1700){
/* move forward */
analogWrite(LEFT_EN, 255);
digitalWrite(LEFT_1, HIGH);
digitalWrite(LEFT_2, LOW);
analogWrite(RIGHT_EN, 255);
digitalWrite(RIGHT_1, HIGH);
digitalWrite(RIGHT_2, LOW);
Serial.println("\tFORWARD\t");
return 1;
} else if(ctrl_data[GET_CTRL_DATA(REER_NO)] < 1250) {
/* move backward */
analogWrite(LEFT_EN, 255);
digitalWrite(LEFT_1, LOW);
digitalWrite(LEFT_2, HIGH);
analogWrite(RIGHT_EN, 255);
digitalWrite(RIGHT_1, LOW);
digitalWrite(RIGHT_2, HIGH);
Serial.println("\tBACKWARD\t");
return -1;
} else {
analogWrite(LEFT_EN, 0);
analogWrite(RIGHT_EN, 0);
return 0;
}
}
```
這個程式目前僅能使直流馬達做單一速度的轉與反轉。然而,只要修改當中 PWM 的數值,即可以達成控制轉速的功能。
## N20 減速直流馬達
是一種具有減速機構的馬達,用來作為爬繩機前後運動與轉彎的驅動。前後運動較為直觀,然而就轉彎部分,我們一開始有以下想法:
1. 設計類似車輛方向盤的轉彎機構,使用伺服馬達控制方向盤。
2. 使用伺服馬達控制萬向輪。
3. 分別控制兩顆直流馬達,以兩顆馬達的正、反轉來控制轉向。
我們認為爬繩機想解決的核心問題並不在前後運動與轉彎,而是在升降機構與爬繩機構,加上我們有 L298N 可以控制個別直流馬達的轉速,因此決定以最簡單的的方式,也就是方法 3. 來解決這個問題。
```C++
int rotate(){
if(ctrl_data[GET_CTRL_DATA(ROTATE_NO)] < 1250){
/* Turn right */
analogWrite(LEFT_EN, 255);
digitalWrite(LEFT_1, HIGH);
digitalWrite(LEFT_2, LOW);
analogWrite(RIGHT_EN, 255);
digitalWrite(RIGHT_1, LOW);
digitalWrite(RIGHT_2, HIGH);
Serial.print("\tRIGHT\t");
return 1;
} else if(ctrl_data[GET_CTRL_DATA(ROTATE_NO)] > 1700) {
/* Turn left */
analogWrite(LEFT_EN, 255);
digitalWrite(LEFT_1, LOW);
digitalWrite(LEFT_2, HIGH);
analogWrite(RIGHT_EN, 255);
digitalWrite(RIGHT_1, HIGH);
digitalWrite(RIGHT_2, LOW);
Serial.print("\tLEFT\t");
return -1;
} else {
analogWrite(LEFT_EN, 0);
analogWrite(RIGHT_EN, 0);
return 0;
}
}
```
程式邏輯與前進後退部分雷同,惟轉彎方向係透過輪子正反轉來達成,即:
1. 右轉:左輪正轉,右輪反轉。
2. 左轉:右輪正轉,左輪反轉。
3. 不轉:將 enable 設為邏輯 0。
## 附錄
### 控制用程式碼
```C++==
#include<Servo.h>
#define LEFT_EN 9
#define LEFT_1 8
#define LEFT_2 7
#define RIGHT_EN 3
#define RIGHT_1 5
#define RIGHT_2 4
#define LIFT_PIN A0
#define CLIMB_PIN A1
#define REER_NO 13
#define ROTATE_NO 12
#define ELEV_NO 11
#define CLIMB_NO 10
#define GET_CTRL_DATA(x) (((x) - (10)))
Servo lift;
Servo climb_t;
double ctrl_data[4] = {0, 0, 0, 0};
void get_control_data()
{
for(int i = 0; i < 4; i++){
ctrl_data[i] = pulseIn(10 + i, HIGH, 10e6);
}
}
void dump_control_data()
{
Serial.print("REER: ");
Serial.print(ctrl_data[3]);
Serial.print(" ROTATE: ");
Serial.print(ctrl_data[2]);
Serial.print(" ELEV: ");
Serial.print(ctrl_data[1]);
Serial.print(" CLIMB: ");
Serial.println(ctrl_data[0]);
}
int reer(){
if(ctrl_data[GET_CTRL_DATA(REER_NO)] > 1700){
/* move forward */
analogWrite(LEFT_EN, 255);
digitalWrite(LEFT_1, HIGH);
digitalWrite(LEFT_2, LOW);
analogWrite(RIGHT_EN, 255);
digitalWrite(RIGHT_1, HIGH);
digitalWrite(RIGHT_2, LOW);
Serial.println("\tFORWARD\t");
return 1;
} else if(ctrl_data[GET_CTRL_DATA(REER_NO)] < 1250) {
/* move backward */
analogWrite(LEFT_EN, 255);
digitalWrite(LEFT_1, LOW);
digitalWrite(LEFT_2, HIGH);
analogWrite(RIGHT_EN, 255);
digitalWrite(RIGHT_1, LOW);
digitalWrite(RIGHT_2, HIGH);
Serial.println("\tBACKWARD\t");
return -1;
} else {
analogWrite(LEFT_EN, 0);
analogWrite(RIGHT_EN, 0);
return 0;
}
}
int rotate(){
if(ctrl_data[GET_CTRL_DATA(ROTATE_NO)] < 1250){
/* Turn right */
analogWrite(LEFT_EN, 255);
digitalWrite(LEFT_1, HIGH);
digitalWrite(LEFT_2, LOW);
analogWrite(RIGHT_EN, 255);
digitalWrite(RIGHT_1, LOW);
digitalWrite(RIGHT_2, HIGH);
Serial.print("\tRIGHT\t");
return 1;
} else if(ctrl_data[GET_CTRL_DATA(ROTATE_NO)] > 1700) {
/* Turn left */
analogWrite(LEFT_EN, 255);
digitalWrite(LEFT_1, LOW);
digitalWrite(LEFT_2, HIGH);
analogWrite(RIGHT_EN, 255);
digitalWrite(RIGHT_1, HIGH);
digitalWrite(RIGHT_2, LOW);
Serial.print("\tLEFT\t");
return -1;
} else {
analogWrite(LEFT_EN, 0);
analogWrite(RIGHT_EN, 0);
return 0;
}
}
int elevate()
{
if (ctrl_data[GET_CTRL_DATA(ELEV_NO)] < 1200) {
/* up */
lift.write(180);
Serial.print("\tUP\t");
return 1;
} else if (ctrl_data[GET_CTRL_DATA(ELEV_NO)] > 1700) {
/* down */
lift.write(0);
Serial.print("\tDOWN\t");
return -1;
} else {
lift.write(90);
return 0;
}
}
int climb()
{
if (ctrl_data[GET_CTRL_DATA(CLIMB_NO)] > 1800) {
/* climb forward */
climb_t.write(180);
Serial.print("\tCLIMB_FORWARD\t");
return 1;
} else if (ctrl_data[GET_CTRL_DATA(CLIMB_NO)] < 1100) {
/* climb backward */
climb_t.write(0);
Serial.println("\tCLIMB_BACKWARD\t");
return -1;
} else {
climb_t.write(90);
return 0;
}
}
void action()
{
if(!reer())
rotate();
elevate();
climb();
}
void setup()
{
pinMode(RIGHT_EN, OUTPUT);
pinMode(LEFT_EN, OUTPUT);
pinMode(RIGHT_1, OUTPUT);
pinMode(RIGHT_2, OUTPUT);
pinMode(LEFT_1, OUTPUT);
pinMode(LEFT_2, OUTPUT);
lift.attach(LIFT_PIN);
climb_t.attach(CLIMB_PIN);
Serial.begin(9600);
}
void loop(){
get_control_data();
dump_control_data();
if(ctrl_data[0] > 1 && ctrl_data[2]) {
action();
delay(80);
}
}
```